Wednesday, April 3, 2013

GR Graphics begins, and loading objects

Gr Graphics began with re-shaping and re-adapting what I learned from GR-Collider to create a graphics-suitable Mesh.

I intend to merge the two classes again in the future, but rapid prototyping meant that this will have to be delegated to the future.



Similarities/differences include:

grGraphics::TriangleMesh 

  • Manages storage for several per-vertex attributes that are relevant to Graphics (Position, Normal, Tangent, Bitangent, Texture Coordinate). 
  • Manages some per-object presentation data (4x4 transformation matrix, texture ID, material ID et.c.)
  • All vector datatypes are packed as closely as possible for quicker upload to the GPU and are not intended/optimized to be used outside the GPU (Vector3 datatype is 3 packed floats). 
  • Can perform vertex-attribute interleaving if so desired
  • Can manage GPU buffers if so desired (*)

grCollider::TriangleMesh
  • Manages very few per-vertex attributes (Position mainly)
  • Manages several per-Object attributes (Mass, Velocity, Inertia Tensor et.c.)
  • Position and Rotation are stored independently (rotation typedeffed as Matrix3x3 or Quaternion)
  • All datatypes are optimized for speed and CPU calculations (Vector3 is a 16 bytes aligned SIMD type, optimized for cpu calculations). 
  • Can cache pretransformed lazily - computed Global coordinate positions if so desired.

So, loading objects was almost a cheat - it was already almost ready from GR Collider. With only small modifications, GR Collider's custom xml-format Mesh loader became a wavefront-obj loader for grGraphics::TriangleMesh in about an hour of coding.

I could just as well have used assimp or another importer, but I wanted the hands-on on this one.

This factory was practically the most important step in my simple Asset Pipeline. Options for a flat-vertex list or indexed-vertices are given, with duplicate detection and removal plus layout optimization for the normally expected indexed variety.

(*) This part bears some explanation. I used a scheme where the "shader program" can actually "instruct" the  object to attach and manage a buffer. Because these calls are frequent, I did not want to virtualize them, so I used templates for this part.
Templatizing the TriangleMesh was NOT the way to do that, because one of its key requirements is having many meshes in the same container.
The next best thing was using templated functions to expose the whole buffer-interface, and liberally using Inversion of Control to leave the actual management to the buffers. There IS an interface defined for the VertexBuffers, but it is NOT normally used for common functions - these are statically called and not virtually, and they are only defined (pure) virtual in the IVertexDataBuffer and not in the buffer implementations. In fact, they are declared inline (not force-inline) and, if suitable they can and will be inlined by the compiler, except when called through the IVertexDataBuffer interface.

As this library is C++11, std::move is used liberally to move this interfaces in the relevant functions of the object



//INSIDE TRIANGLEMESH.H 

        //Shaders CAN and WILL reuse VertexDataBuffers requested by other shaders if
        //they have the same signature as an already existing VertexDataBuffer
        //vertexBuffersRefcount allows management of vertexBuffers.
        //I also toyed around with the idea of hashing et.c., but incurring a linear
        //penalty on "attach" is irrelevant, as the types of buffers should be a handful
        //at most, and will only be called on init at most, usually just once

        std::vector<IVertexDataBuffer*> vertexBuffers;
        std::vector<tiny_index_type> vertexBuffersRefcount;


        //The shader is expected to call this function only on the first object in its
        //list, to get the slot ID that it will be using. Subsequent calls will use the
        //other method to directly use the same ID.
        //CAVEAT: the lists must be shared between shaders, so that we can ensure that
        //the buffer lists are common among them, else we are in for a world of hurt.
        template <typename MemoryObjectType> tiny_index_type attachGpuObject(typename MemoryObjectType::_ProgramType& program) {
            dataInvalidated = true;
            for (size_t i = 0; i < vertexBuffers.size(); ++i) { //Find an existing.
                if (vertexBuffersRefcount[i] && vertexBuffers[i]->flags() == MemoryObjectType::getFlags()) {
                    ++vertexBuffersRefcount[i];
                    return i;
                }
            }
            //No existing signature? Try to reuse an empty slot.
            for (size_t i = 0; i < vertexBuffers.size(); ++i) {
                if (!vertexBuffersRefcount[i]) {
                    vertexBuffers[i] = new MemoryObjectType(program);
                    static_cast<MemoryObjectType*>(vertexBuffers[i])->MemoryObjectType::allocate(*this);
                    static_cast<MemoryObjectType*>(vertexBuffers[i])->MemoryObjectType::uploadVertexData(*this);
                    ++vertexBuffersRefcount[i];
                    return i;
                }
            }
            //Still here? That means no free slot existed either, so create one.
            auto pVbo = new MemoryObjectType(program);
            pVbo->allocate(*this);
            pVbo->uploadVertexData(*this);
            vertexBuffersRefcount.push_back(1);
            vertexBuffers.push_back(pVbo);
            return vertexBuffers.size() - 1;
        }

        //This version is called in the second and subsequent calls to attach across
        //objects. It is unchecked, and it is the shader's responsibility not to send
        //garbage calls.
        template <typename MemoryObjectType> void attachGpuObject(typename MemoryObjectType::_ProgramType& program, tiny_index_type location) {

            while (location >= vertexBuffers.size()) {
            vertexBuffers.push_back(0);
                vertexBuffersRefcount.push_back(0);
            }
            if (!vertexBuffersRefcount[location]++) {
            vertexBuffers[location] = new MemoryObjectType(program);
                static_cast<MemoryObjectType*>(vertexBuffers[location])->allocate(*this);
            }
        }

        void detachGpuObject(tiny_index_type location) {
            if (!(--vertexBuffersRefcount[location])) {
                delete vertexBuffers[location];
                vertexBuffers[location] = 0;
            }
        }

        template<typename MemoryObjectType> void beginGpuBufferRender(tiny_index_type location) {
            //This may be optimized to keep the invalidation flag per VBO, incurring a small
            //CPU penalty but avoiding uploads for frequently changed objects
            if (dataInvalidated) {
                for (size_t i = 0; i < vertexBuffers.size(); ++i) {
                    if (vertexBuffersRefcount[i]) {
                        vertexBuffers[i]->uploadVertexData(*this);
                    }
                }
                dataInvalidated = false;
            }
            static_cast<MemoryObjectType*>(vertexBuffers[location])->beginRendering();
        }

        template<typename MemoryObjectType> void endGpuBufferRender(tiny_index_type location) {
            static_cast<MemoryObjectType*>(vertexBuffers[location])->endRendering();
        }
        template<typename MemoryObjectType> MemoryObjectType&  getGpuObject(tiny_index_type location) {
            return *static_cast<MemoryObjectType*>(vertexBuffers[location]);
        }

        bool hasAttachedGpuObject(tiny_index_type location) {
            return (vertexBuffersRefcount.size() > location) && (vertexBuffersRefcount[location] > 0);
        }


No comments:

Post a Comment