Simple GPU Path Tracing, Part. 4.0 : Mesh Loading

 

In this post, we will be using the great tinygltf library to load GLTF files into our scene.


 

Here's the code for this post.

 Tinygltf is a header library, so we only need to add the .hpp file into our include directory. It also relies on some stb libraries and on json.hpp which we also have to add into our include directories

Let's now create a GLTFLoader.h and .cpp files, and create this function : 

void LoadGLTF(std::string FilePath, std::shared_ptr<scene> Scene);

It takes as input the file path of the gltf, and the scene that it will be added into.

Here's the content of this function : 


void LoadGLTF(std::string FileName, std::shared_ptr<scene> Scene)
{
    tinygltf::Model GLTFModel;
    tinygltf::TinyGLTF ModelLoader;

    std::string Error, Warning;

    std::string Extension = FileName.substr(FileName.find_last_of(".") + 1);
    bool OK = false;
    if(Extension == "gltf")
    {
        OK = ModelLoader.LoadASCIIFromFile(&GLTFModel, &Error, &Warning, FileName);
    }
    else if(Extension == "glb")
    {
        OK = ModelLoader.LoadBinaryFromFile(&GLTFModel, &Error, &Warning, FileName);
    }
    else OK=false;
       
    if(!OK)
    {
        printf("Could not load model %s \n",FileName);
        return;
    }

    std::vector<std::vector<uint32_t>> InstanceMapping;
   
    LoadGeometry(GLTFModel, Scene, InstanceMapping);
    LoadInstances(GLTFModel, Scene, InstanceMapping);
    LoadMaterials(GLTFModel, Scene);
}  


So there are 3 things we need from the gltf file : Geometry data, instances data, and materials.


Geometry

I won't show the entirety of the code, it's all available here, but essentially a gltf file can be composed of multiple gltf meshes. Each mesh can be composed of multiple primitives. A primitive is the same as our shape struct in our scene representation. So we will loop through each mesh, and through each primitive in that mesh, and create shapes and add them to the scene.

we keep track of an instance mapping that will allow us to find for a given mesh and a given primitive in that mesh, what's the index of the shape in the scene.

    for(int MeshIndex=0; MeshIndex<GLTFModel.meshes.size(); MeshIndex++)
    {
        tinygltf::Mesh gltfMesh = GLTFModel.meshes[MeshIndex];
        std::vector<shape> &Shapes = Scene->Shapes;
        std::vector<std::string> &ShapeNames = Scene->ShapeNames;

        uint32_t MeshBaseIndex = Shapes.size();

        Shapes.resize(MeshBaseIndex + gltfMesh.primitives.size());
        ShapeNames.resize(MeshBaseIndex + gltfMesh.primitives.size());
        InstanceMapping[MeshIndex].resize(gltfMesh.primitives.size());

        for(int j=0; j<gltfMesh.primitives.size(); j++)
        {

then for each primitive, we will be reading all the needed information : Vertex positions, vertex normals, and triangle indices.

This is done by accessing buffer data from the gltf model object.

example for positions : 

            //Positions
            tinygltf::Accessor PositionAccessor = GLTFModel.accessors[PositionIndex];
            tinygltf::BufferView PositionBufferView = GLTFModel.bufferViews[PositionAccessor.bufferView];
            const tinygltf::Buffer &PositionBuffer = GLTFModel.buffers[PositionBufferView.buffer];
            const uint8_t *PositionBufferAddress = PositionBuffer.data.data();
            //3 * float
            int PositionStride = tinygltf::GetComponentSizeInBytes(PositionAccessor.componentType) * tinygltf::GetNumComponentsInType(PositionAccessor.type);
            if(PositionBufferView.byteStride > 0) PositionStride = (int)PositionBufferView.byteStride;

Then we can read those buffers and fill up our shape struct data : 

            Shape.Positions.resize(PositionAccessor.count);
            Shape.Normals.resize(PositionAccessor.count);
            for (size_t k = 0; k < PositionAccessor.count; k++)
            {
                glm::vec3 Position;
                {
                    const uint8_t *Address = PositionBufferAddress + PositionBufferView.byteOffset + PositionAccessor.byteOffset + (k * PositionStride);
                    memcpy(&Position, Address, 12);
                }

                glm::vec3 Normal;
                if(NormalIndex>=0)
                {
                    const uint8_t *Address = NormalBufferAddress + NormalBufferView.byteOffset + NormalAccessor.byteOffset + (k * NormalStride);
                    memcpy(&Normal, Address, 12);
                }

                Shape.Positions[k] = glm::vec3(Position.x, Position.y, Position.z);
                Shape.Normals[k] = glm::vec3(Normal.x, Normal.y, Normal.z);
            }


Then for indices, they can either be uint8, unit16 or uint32, so we have to handle each case, here's the code with only uint8, it's essentially the same for uint16 and uint32 :

            //Fill indices buffer
            tinygltf::Accessor IndicesAccessor = GLTFModel.accessors[IndicesIndex];
            tinygltf::BufferView IndicesBufferView = GLTFModel.bufferViews[IndicesAccessor.bufferView];
            const tinygltf::Buffer &IndicesBuffer = GLTFModel.buffers[IndicesBufferView.buffer];
            const uint8_t *IndicesBufferAddress = IndicesBuffer.data.data();
            int IndicesStride = tinygltf::GetComponentSizeInBytes(IndicesAccessor.componentType) * tinygltf::GetNumComponentsInType(IndicesAccessor.type);
           
            Shape.Triangles.resize(IndicesAccessor.count/3);
            const uint8_t *baseAddress = IndicesBufferAddress + IndicesBufferView.byteOffset + IndicesAccessor.byteOffset;
            if(IndicesStride == 1)
            {
                std::vector<uint8_t> Quarter;
                Quarter.resize(IndicesAccessor.count);
                memcpy(Quarter.data(), baseAddress, (IndicesAccessor.count) * IndicesStride);
                for(size_t i=0, j=0; i<IndicesAccessor.count; i+=3, j++)
                {
                    Shape.Triangles[j].x = Quarter[i+0];
                    Shape.Triangles[j].y = Quarter[i+1];
                    Shape.Triangles[j].z = Quarter[i+2];
                }
            }

 That's all we need to do for shapes.

Instances

Now we will be going through the scene graph of the gltf file, finding all the instances and adding them to the scene.
we will use a recursion based tree traversal with the function TraverseNodes.
Here's how it starts : 

void LoadInstances(tinygltf::Model &GLTFModel, std::shared_ptr<scene> Scene, std::vector<std::vector<uint32_t>> &InstanceMapping)
{
    glm::mat4 Scale = glm::scale(glm::mat4(1), glm::vec3(25));
    glm::mat4 Translate = glm::translate(glm::mat4(1), glm::vec3(0, 0.4, 0));
    glm::mat4 RootTransform =  Translate * Scale;
    // glm::mat4 RootTransform(0.3f);
    const tinygltf::Scene GLTFScene = GLTFModel.scenes[GLTFModel.defaultScene];
    for (size_t i = 0; i < GLTFScene.nodes.size(); i++)
    {
        TraverseNodes(GLTFModel, GLTFScene.nodes[i], RootTransform, Scene, InstanceMapping);
    }
}

We have to define a root transform that will be then passed to all the children.

In TraverseNodes, we first calculate the local transform of the node, and multiply it with its parent transform to get a global transform. Transforms can either be stored as a matrix, or as position/rotation/scale that we then have to combine into a matrix.

    tinygltf::Node GLTFNode = GLTFModel.nodes[nodeIndex];

    std::string NodeName = GLTFNode.name;
    if(NodeName.compare("") == 0)
    {
        NodeName = "Node";
    }

    std::vector<instance> &Instances = Scene->Instances;
    std::vector<std::string> &InstanceNames = Scene->InstanceNames;
    InstanceNames.push_back(NodeName);

    glm::mat4 NodeTransform;
    if(GLTFNode.matrix.size() > 0)
    {
        NodeTransform[0][0] = (float)GLTFNode.matrix[0]; NodeTransform[0][1] = (float)GLTFNode.matrix[1]; NodeTransform[0][2] = (float)GLTFNode.matrix[2]; NodeTransform[0][3] = (float)GLTFNode.matrix[3];
        NodeTransform[1][0] = (float)GLTFNode.matrix[4]; NodeTransform[1][1] = (float)GLTFNode.matrix[5]; NodeTransform[1][2] = (float)GLTFNode.matrix[6]; NodeTransform[1][3] = (float)GLTFNode.matrix[7];
        NodeTransform[2][0] = (float)GLTFNode.matrix[8]; NodeTransform[2][1] = (float)GLTFNode.matrix[9]; NodeTransform[2][2] = (float)GLTFNode.matrix[10]; NodeTransform[2][3] = (float)GLTFNode.matrix[11];
        NodeTransform[3][0] = (float)GLTFNode.matrix[12]; NodeTransform[3][1] = (float)GLTFNode.matrix[13]; NodeTransform[3][2] = (float)GLTFNode.matrix[14]; NodeTransform[3][3] = (float)GLTFNode.matrix[15];
    }
    else
    {
            glm::mat4 translate(1);
            glm::mat4 rotation(1);
            glm::mat4 scale(1);
            if(GLTFNode.translation.size()>0)
            {
                translate[3][0] = (float)GLTFNode.translation[0];
                translate[3][1] = (float)GLTFNode.translation[1];
                translate[3][2] = (float)GLTFNode.translation[2];
            }
            if(GLTFNode.rotation.size() > 0)
            {
                glm::quat Quat((float)GLTFNode.rotation[3], (float)GLTFNode.rotation[0], (float)GLTFNode.rotation[1], (float)GLTFNode.rotation[2]);
                rotation = glm::toMat4(Quat);

            }
            if(GLTFNode.scale.size() > 0)
            {
                scale[0][0] = (float)GLTFNode.scale[0];
                scale[1][1] = (float)GLTFNode.scale[1];
                scale[2][2] = (float)GLTFNode.scale[2];
            }
            NodeTransform = scale * rotation * translate;
    }

    glm::mat4 Transform = ParentTransform * NodeTransform;

Then, if the node is leaf, it means that it contains instances of shapes. That's where will be adding instances to our scene : 

    //Leaf node
    if(GLTFNode.children.size() == 0 && GLTFNode.mesh != -1)
    {
        tinygltf::Mesh GLTFMesh = GLTFModel.meshes[GLTFNode.mesh];
        for(int i=0; i<GLTFMesh.primitives.size(); i++)
        {
            Scene->InstanceNames.push_back(GLTFNode.name);

            Scene->Instances.emplace_back();
            instance &Instance = Scene->Instances.back();
            Instance.ModelMatrix = Transform;
            Instance.Shape = InstanceMapping[GLTFNode.mesh][i];
            Instance.Material = Scene->Materials.size() + GLTFMesh.primitives[i].material;
        }  
    }

Because we haven't added the gltf materials yet, we can use the Scene->Materials.size() as the base index for the Material index.

Then if the node is not a leaf, it contains children nodes that we will traverse as well : 


    for (size_t i = 0; i < GLTFNode.children.size(); i++)
    {
        TraverseNodes(GLTFModel, GLTFNode.children[i], Transform, Scene, InstanceMapping);
    }

And that's it for instances.

Materials

For materials, we will just loop through all the materials in the gltf model, and add them to the scene, very simple : 

void LoadMaterials(tinygltf::Model &GLTFModel, std::shared_ptr<scene> Scene)
{
    std::vector<material> &Materials = Scene->Materials;
    std::vector<std::string> &MaterialNames = Scene->MaterialNames;
   
    uint32_t BaseInx = Materials.size();
    Materials.resize(Materials.size() + GLTFModel.materials.size());
    MaterialNames.resize(MaterialNames.size() + GLTFModel.materials.size());
    for (size_t i = 0; i < GLTFModel.materials.size(); i++)
    {
        uint32_t Inx = BaseInx + i;

        const tinygltf::Material GLTFMaterial = GLTFModel.materials[i];
        const tinygltf::PbrMetallicRoughness PBR = GLTFMaterial.pbrMetallicRoughness;
       
        MaterialNames[Inx] = GLTFMaterial.name;
       
        material &Material = Materials[Inx];
        Material = {};
        Material.MaterialType = MATERIAL_TYPE_PBR;
        Material.Colour = glm::vec3(PBR.baseColorFactor[0], PBR.baseColorFactor[1], PBR.baseColorFactor[2]);
        Material.Roughness = std::max(0.01, PBR.roughnessFactor);
        Material.Metallic = PBR.metallicFactor;
    }
}    

And that's all ! now we can call the function in our CreateCornellBox() function, and att the gltf object into the scene : 
LoadGLTF("resources\\Models\\ToyCar\\ToyCar.gltf", Scene);

And there's the result : 
 
 
That's getting more and more interesting ! Well it's just a gray colour for now, so I think it's a good time to start adding textures into our workflow.

Links

 

Next Post : Simple GPU Path Tracing, Part. 4.1 : Textures

Commentaires

Articles les plus consultés