Render multiple distinct objects from one vertex buffer

I’m new to Vulkan coding, and worked through the Vulkan Example web site. Using that code as a base, I added an instance buffer which holds a vec3. The position data was mapped to the fragment shader and then I used VkCmdDrawIndexed to successfully draw a set of identical objects to the screen. In the fragment shader I had:

layout(location = 0) in vec3 inPosition;
layout(location = 4) in vec3 instancePos;
    
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition + instancePos, 1.0);

and the VkCmdDrawIndexed command:

vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 27, 0, 0, 0);

This works fine, and as expected I had 27 identical objects in a 3 x 3 x 3 cube. I’m now trying to extend things by rendering multiple non-identical objects but I can only ever see the first object on screen. I have followed tips from this post but still only see the first object in the perMeshData array. The title of the post in the Khronos forums is ‘Render multiple meshes with one VBO’:

My drawing command looks like this:

for(auto m : perMeshData){
  vkCmdDrawIndexed(commandBuffers[i], m.numIndices, 1, m.startIndex, m.startVertIndex, m.startInstance);
}

Logging what is passed to vkCmdDrawIndexed when drawing a cube and mesh I can see:

vKCmdDrawIndexed = CB 36 1 0 0 0
vKCmdDrawIndexed = CB 6 1 36 24 1

But only the first object (cube) is rendered. I’ve checked what is in the vertex, index and instance arrays and it looks like legitimate data. Likewise, the logged for vkCmdDrawIndexed looks sane I think. If I swap objects around in the perMeshData array, I always only see the first object. As with the instanced example, I am passing a position vector for each object to the shader in the instance buffer objects. Can anyone suggest why I’m not seeing multiple objects being rendered? I’m happy to paste more bits of code here, just ask.

Summary

This text will be hidden

How do you fill your vertex buffer? If you export from a modelling application you usually won’t need to offset vertices but only indices, as the indices already contain the proper vertex offset themselves. Also you shouldn’t increase firstInstance if you don’t do instancing related rendering.

Thank you thank you!! It was the vertex offset. As you say, the indices already have the vertex offset baked in. firstInstance needs to increase, as each object has the model position set from the instance buffer object.

For anyone reading this thread in future, my draw command now looks like:

vkCmdDrawIndexed(commandBuffers[i], m.numIndices, 1, m.startIndex, 0, m.startInstance);

and the vertex buffer is filled from a .obj file that I set in each MeshData struct using tiny_obj_loader.h. The code is hacked together, and probably sub optimal in many ways, but it works :slightly_smiling_face: . I hope this helps someone else out in future.

Holds the position information:

struct InstanceData {
    glm::vec3 pos;
    glm::vec3 rot;
    float scale;
};

Vertex information:

struct Vertex {
    glm::vec3 pos;
    glm::vec3 color;
    glm::vec2 texCoord;
    glm::vec3 normal;

    bool operator==(const Vertex& other) const {
        return pos == other.pos && color == other.color && texCoord == other.texCoord && normal == other.normal;
    }
};

Information about each mesh:

struct MeshData{
        uint32_t startIndex = 0;
        uint32_t numIndices = 0;
        uint32_t startInstance = 0;
        std::string obj_file = "";
        glm::vec3 pos;
};

Hashing information to compare the vertices - needed by uniqueVertices below.

namespace std {
    template<> struct hash<Vertex> {
        size_t operator()(Vertex const& vertex) const {
            return (((hash<glm::vec3>()(vertex.pos) ^ (hash<glm::vec3>()(vertex.color) << 1)) >> 1) ^ (hash<glm::vec2>()(vertex.texCoord) << 1) ^
                (hash<glm::vec3>()(vertex.normal) << 1));
        }
    };
}

To load the mesh data from the object file:

void loadMeshData(){
        tinyobj::attrib_t attrib;
        std::vector<tinyobj::shape_t> shapes;
        std::vector<tinyobj::material_t> materials;
        std::string warn, err;
        std::unordered_map<Vertex, uint32_t> uniqueVertices = {};
        
    for(auto &m : perMeshData){
            
        m.startIndex = (uint32_t) indices.size();
        m.startInstance = (uint32_t) instances.size();
            
            if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, m.obj_file.c_str())) {
                throw std::runtime_error(warn + err);
            }
            
            for (const auto& shape : shapes) {
            for (const auto& index : shape.mesh.indices) {
                Vertex vertex = {};

                vertex.pos = {
                    attrib.vertices[3 * index.vertex_index + 0],
                    attrib.vertices[3 * index.vertex_index + 1],
                    attrib.vertices[3 * index.vertex_index + 2]
                };

                vertex.texCoord = {
                    attrib.texcoords[2 * index.texcoord_index + 0],
                    1.0f - attrib.texcoords[2 * index.texcoord_index + 1]
                };

                vertex.color = {1.0f, 1.0f, 1.0f};
                
                vertex.normal = {
                    attrib.normals[3 * index.normal_index + 0],
                    attrib.normals[3 * index.normal_index + 1],
                    attrib.normals[3 * index.normal_index + 2]
                };

                if (uniqueVertices.count(vertex) == 0) {
                    uniqueVertices[vertex] = static_cast<uint32_t>(vertices.size());
                    vertices.push_back(vertex);
                }
                
                indices.push_back(uniqueVertices[vertex]);
                m.numIndices ++;
            }
        }
            InstanceData instancedata = {};
            instancedata.pos = m.pos;
                
            instances.push_back(instancedata);
    }
}