0
votes

I can't figure out how DirectX11 understands which values in the vertex buffer are Vertices, which ones are Normals, and which ones are Texcoords

For example:

The following code works, but it draws the model as all white. But the normals and vertices are drawn correctly

std::vector<float> vertex_buffer;
for (int i = 0, j = 0; i < num_vertices; i+=3, j+=2)
{
    vertex_buffer.push_back(attrib.vertices[i + 0]);
    vertex_buffer.push_back(attrib.vertices[i + 1]);
    vertex_buffer.push_back(attrib.vertices[i + 2]);
    vertex_buffer.push_back(attrib.normals[i + 0]);
    vertex_buffer.push_back(attrib.normals[i + 1]);
    vertex_buffer.push_back(attrib.normals[i + 2]);
    vertex_buffer.push_back(0.0F);
    vertex_buffer.push_back(0.0F);
    vertex_buffer.push_back(0.0F);
}

std::vector<UINT> index_buffer;
for (int i = 0, j = 0; i < num_indices; i+=3, j+=2)
{
    index_buffer.push_back(shapes[0].mesh.indices[i + 0].vertex_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 1].vertex_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 2].vertex_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 0].normal_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 1].normal_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 2].normal_index);
    index_buffer.push_back(0);
    index_buffer.push_back(0);
    index_buffer.push_back(0);
}

For example, the code above produces this: enter image description here

But if I start changing anything in the index buffer in the last 3 values of the 9 values, the model draws the vertices incorrectly.

Here I modify it to use texcoords (I use TinyObjLoader for importing obj files and I have no idea why it has 3 texcoords per vertex, instead of 2)

std::vector<float> vertex_buffer;
for (int i = 0, j = 0; i < num_vertices; i += 3, j += 2)
{
    vertex_buffer.push_back(attrib.vertices[i + 0]);
    vertex_buffer.push_back(attrib.vertices[i + 1]);
    vertex_buffer.push_back(attrib.vertices[i + 2]);
    vertex_buffer.push_back(attrib.normals[i + 0]);
    vertex_buffer.push_back(attrib.normals[i + 1]);
    vertex_buffer.push_back(attrib.normals[i + 2]);
    vertex_buffer.push_back(attrib.texcoords[i + 0]);
    vertex_buffer.push_back(attrib.texcoords[i + 1]);
    vertex_buffer.push_back(attrib.texcoords[i + 2]);
}

std::vector<UINT> index_buffer;
for (int i = 0, j = 0; i < num_indices; i += 3, j += 2)
{
    index_buffer.push_back(shapes[0].mesh.indices[i + 0].vertex_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 1].vertex_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 2].vertex_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 0].normal_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 1].normal_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 2].normal_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 0].texcoord_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 1].texcoord_index);
    index_buffer.push_back(shapes[0].mesh.indices[i + 2].texcoord_index);
}

I get this result:

enter image description here

Clearly, here its not just the textures that are affected, the vertex order is messed up. But Im only changing the fields in the buffer which are supposed to be for texcoords. Why does it affect vertices. Why do vertex and normal coordinates/values work, but texcoords do not.

How does DirectX know which indeces in IndexBuffer refer to Vertices, which ones refer to Normals, and which ones refer to Texcoords

Also, for this model I had to use a Vertex Stride of 36, when I moved the entries from 9 to 8 and changed vetex stride to 32, it was even worse.

Does DirectX automatically assign the first 3 values within stride to vertex, next3 to normal, and next 2 to texcoordinates? Is this how it works?

Thanks,

1

1 Answers

4
votes

The Direct3D Input Assembler is not nearly as flexible as you are assuming. It takes a single index from an index buffer and uses that value to look up the same vertex from 1 or more bound vertex buffers. The entire vertex is then sent to a single invocation of a vertex shader.

The input layout tells you everything you need to know. For example, here is a very simple input layout:

{ "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL",      0, DXGI_FORMAT_R32G32B32_FLOAT,    0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD",    0, DXGI_FORMAT_R32G32_FLOAT,       0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },

This corresponds to a Vertex structure like:

struct Vertex
{
    XMFLOAT3 position;
    XMFLOAT3 normal;
    XMFLOAT2 textureCoordinate;
};

In this case, you'd bind a single Vertex Buffer to the system, and of course a single Index Buffer. The stride of the VB would be sizeof(Vertex) or 32-bytes, which is consider an optimal size for most hardware.

It would use something like this pseudo-code:

// StartIndexLocation, BaseVertexLocation, IndexCount are DrawIndexed parameters
// stride and offset are IASetVertexBuffers parameters
for(I = 0; I < IndexCount; I++)
{
    uint16_t/uint32_t index = IndexBuffer[I + StartIndexLocation];

    Vertex v = VertexBuffer_Bytes[((index + BaseVertexLocation) * stride) + offset];

    VertexShader(v);
}

You can also create a multi-stream input layout which takes more than one VB. Here is an example of a 3 stream input layout:

{ "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL",      0, DXGI_FORMAT_R32G32B32_FLOAT,    1, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD",    0, DXGI_FORMAT_R32G32_FLOAT,       2, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },

Here this would correspond to three Vertex structures:

struct Vertex1
{
    XMFLOAT3 position;
};

struct Vertex2
{
    XMFLOAT3 normal;
};

struct Vertex3
{
    XMFLOAT2 textureCoordinate;
};

You'd use strides of 12, 12, and 8 for three bound Vertex Buffers. There is still only a single Index Buffer so all the data for a specific vertex must be in the same index for all three VBs.

It would use something like this pseudo-code:

for(I = 0; I < IndexCount; I++)
{
    uint16_t/uint32_t index = IndexBuffer[I + StartIndexLocation];

    Vertex1 v1 = VertexBuffer0_Bytes[((index + BaseVertexLocation) * stride0) + offset0];
    Vertex2 v2 = VertexBuffer1_Bytes[((index + BaseVertexLocation) * stride1) + offset1];
    Vertex3 v3 = VertexBuffer2_Bytes[((index + BaseVertexLocation) * stride2) + offset2];

    VertexShader(v1, v2, v3);
}

While geometry file formats like WaveFront OBJ and internal data structures for CAD/3D art programs often use multiple indices per vertex for a more compact memory structure, you can't directly render such data using Direct3D or OpenGL. You have to convert it to the interleaved form by duplication of data.

std::vector<XMFLOAT3> positions;
std::vector<XMFLOAT3> normals;
std::vector<XMFLOAT2> texcoords;
// Load these three from the file

std::vector<Vertex> vertexBuffer;
std::vector<uint32_t> indexBuffer;

for each face in WaveFront OBJ:
    for each vertex in the face:
        Vertex v;
        v.position = positions[vertexIndex];
        v.normal = normal[normalIndex];
        v.textureCoordinate = texcoords[textureIndex];

        uint32_t index = AddVertex(vertexIndex, &vertex, vertexCache);
        indexBuffer.push_back(index);

// Helper function to try to minimize vertex duplication        
typedef std::unordered_multimap<UINT, UINT> VertexCache;

uint32_t AddVertex(UINT hash, const Vertex* pVertex, VertexCache& cache)
{
    auto f = cache.equal_range(hash);

    for (auto it = f.first; it != f.second; ++it)
    {
        auto& tv = vertexBuffer[it->second];

        if (0 == memcmp(pVertex, &tv, sizeof(Vertex)))
        {
            return it->second;
        }
    }

    uint32_t index = static_cast<uint32_t>(vertices.size());
    vertexBuffer.emplace_back(*pVertex);

    VertexCache::value_type entry(hash, index);
    cache.insert(entry);
    return index;
}

See WaveFrontReader.h. While my reader implementation isn't perfect, it does handle a number of issues your code is ignoring like negative index values, converting n-gons to triangles, etc.