Skip to content

Vertex System Architecture

Tiago Jose Sousa Magalhães edited this page Dec 22, 2019 · 2 revisions

This section describes the architecture behind the Vertex system in the DX Renderer framework.

GPUs use vertices to draw fragments that are then displayed on the screen, to do this, they need to have in memory a list of vertices and corresponding associated data. As a base vertices only need a position so that the GPU knows where to draw them, however, we can associate more data to each vertex, for example, we can associate a color to each vertex to allow for more complex coloring. In practice, vertexes are usually associated with two other data fields, a normal, which is a 3D vector and texture coordinates, which are a 2D vector.

Vertex data is kept in a GPU buffer called a Vertex Buffer, to add a vertex to the vertex buffer we must first identify the type of data it contains and how it will be visible in shaders.

Bellow, we will explain how to create your vertex with custom data.

In DX Renderer, all vertices must be a subclass of the Vertex class, which is a vertex that only contains position data. To create a new vertex type we need to overload 2 polymorphic methods and 1 static method, these are GetElementSize, GetData and GetInputLayout, respectively.

GetElementSize

This method tells us the size in bytes of the data that makes up the vertex. The way this calculation is done is up to the implementation and doest not matter as long as it is correct.

The base implementation of this method is:

UINT64 Vertex::GetElementSize() const
{
	return sizeof(m_position);
}

If we wanted to add a color component(represented by a 4D vector contained as a DirectX::XMFLOAT4) to our vertices, we could change this method to have the following implementation:

UINT64 Vertex::GetElementSize() const
{
	return sizeof(m_position)+sizeof(m_color);
}

GetData

This method gives us the data that we need to copy into the vertex buffer. The way this data pointer is obtained is up to the implementation as long as it is all laid out linearly in memory. The base implementation merely returns a pointer to the position data that it contains and looks like this:

void* Vertex::GetData()
{
	return &this->m_position;
}

If we want to add a color element to the vertex, the way we overload this method will depend on the data format, if we merely add the member field to the base class then we can use the same implementation as the color data will follow after the position field.

GetInputLayout

This method defines how vertex data is laid out in memory, this is done through the D3D12_INPUT_ELEMENT_DESC structure and has the following base implementation:

D3D12_INPUT_ELEMENT_DESC position_element_description = {};
position_element_description.SemanticName = "POSITION";
position_element_description.SemanticIndex = 0;
position_element_description.Format = DXGI_FORMAT_R32G32B32_FLOAT;
position_element_description.InputSlot = 0;
position_element_description.AlignedByteOffset = 0;
position_element_description.InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
position_element_description.InstanceDataStepRate = 0

std::vector<D3D12_INPUT_ELEMENT_DESC>* input_elements_descriptions = new std::vector<D3D12_INPUT_ELEMENT_DESC>();
input_elements_descriptions->push_back(position_element_description);
D3D12_INPUT_LAYOUT_DESC input_layout = {};
input_layout.NumElements = input_elements_descriptions->size();
input_layout.pInputElementDescs = input_elements_descriptions->data()
return input_layout;

For the most part the field that one needs to change here are SemanticName, Format and AlignedByteOffset. These have the following meaning:

  • SemanticName - Name under which the data element will be avaible inside shaders.
  • Format - Data Format of the element
  • AlignedByteOffset - Bytes from the start of the data buffer until we reach the element being defined

We merely need to overload these methods and add in the extra data in order to create our own vertex.

If we want to add in a color element to the vertex we can use the following implementation for this method:

D3D12_INPUT_ELEMENT_DESC position_element_description = {};
position_element_description.SemanticName = "POSITION";
position_element_description.SemanticIndex = 0;
position_element_description.Format = DXGI_FORMAT_R32G32B32_FLOAT;
position_element_description.InputSlot = 0;
position_element_description.AlignedByteOffset = 0;
position_element_description.InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
position_element_description.InstanceDataStepRate = 0

D3D12_INPUT_ELEMENT_DESC color_element_description = {};
color_element_description.SemanticName = "COLOR";
color_element_description.SemanticIndex = 0;
color_element_description.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
color_element_description.InputSlot = 0;
color_element_description.AlignedByteOffset = 12;
color_element_description.InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
color_element_description.InstanceDataStepRate = 0

std::vector<D3D12_INPUT_ELEMENT_DESC>* input_elements_descriptions = new std::vector<D3D12_INPUT_ELEMENT_DESC>();
input_elements_descriptions->push_back(position_element_description);
input_elements_descriptions->push_back(color_element_description);
D3D12_INPUT_LAYOUT_DESC input_layout = {};
input_layout.NumElements = input_elements_descriptions->size();
input_layout.pInputElementDescs = input_elements_descriptions->data()
return input_layout;

The DX Renderer implementation of a vertex buffer, is a template and only accepts data types that are of the Vertex type or of one of its subclasses, attempting to use another data type will lead to a compile-time error.