Drawing

With all the objects from the previous chapters bound to the pipeline we are almost ready to draw an object. When drawing you must ensure a pipeline state of GraphicsPipelineState type is bound, instead of ComputePipelineState.

Next you need to specify what type of primitives you wish to draw by calling ct::RenderAPI::setDrawOperation(), which accepts any of the types defined in DrawOperationType. This determines how are contents of the index buffer interpreted (or vertex buffer if index isn't available). The available draw types are:

  • DOT_POINT_LIST - Each vertex represents a point.
  • DOT_POINT_LIST - Each sequential pair of vertices represent a line.
  • DOT_LINE_STRIP - Each vertex (except the first) forms a line with the previous vertex.
  • DOT_TRIANGLE_LIST - Each sequential 3-tuple of vertices represent a triangle.
  • DOT_TRIANGLE_STRIP - Each vertex (except the first two) form a triangle with the previous two vertices.
  • DOT_TRIANGLE_FAN - Each vertex (except the first two) forms a triangle with the first vertex and previous vertex.
// We're drawing a triangle list
RenderAPI& rapi = RenderAPI::instance();
rapi.setDrawOperation(DOT_TRIANGLE_LIST);

Indexed drawing

Finally you can now draw the object by calling ct::RenderAPI::drawIndexed(). It requires the following parameters:

  • startIndex - Offset into the bound index buffer to start drawing from. In most cases this will be zero.
  • indexCount - Number of indices to draw. Specify total number of indices in the index buffer to draw them all (most common case).
  • vertexOffset - Offset to append to each index in the index buffer. Allows you to draw different set of vertices using the same index buffer. In most cases this will be zero.
  • vertexCount - Number of vertices to draw. Since the actual number of primitives drawn is determined by the index count, this value is internally used just for tracking purposes and wont affect your rendering. In most cases you can specify the number of vertices in the vertex buffer(s).
SPtr<IndexBuffer> ib = ...;
SPtr<VertexBuffer> vb = ...;
UINT32 numIndices = ib->getProperties()->getNumIndices();
UINT32 numVertices = vb->getProperties()->getNumVertices();
RenderAPI& rapi = RenderAPI::instance();
rapi.drawIndexed(0, numIndices, 0, numVertices);

Non-indexed drawing

If drawing without an index buffer you can call ct::RenderAPI::draw() instead. It requires only the vertexOffset and vertexCount parameters, with same meaning as above (except vertexCount in this case does affect the rendering).

SPtr<VertexBuffer> vb = ...;
UINT32 numVertices = vb->getProperties()->getNumVertices();
RenderAPI& rapi = RenderAPI::instance();
rapi.draw(0, numVertices);

Instanced drawing

Both ct::RenderAPI::draw() and ct::RenderAPI::drawIndexed() support drawing multiple instances of the same object using the instanceCount parameter. This can be used as an alternative for issuing multiple draw calls, as they may have a significant CPU overhead. Using instanced drawing you can draw the same geometry multiple times with almost no additional CPU overhead.

// Draw 5 instances of the currently bound geometry
rapi.drawIndexed(0, numIndices, 0, numVertices, 5);

In order for instanced drawing to actually be useful, in most cases we need a way to differentiate the instances. At the very least we usually want to draw the instances at different positions in the world.

This is done by creating a separate VertexBuffer that contains per-instance properties (like position). This buffer is created same as a normal vertex buffer, except it doesn't contain per-vertex data, and instead contains per-instance data.

In order to use such a buffer we need to let the pipeline know by creating an appropriate VertexDeclaration. We need to define a VertexDataDesc that contains per-instance data. This is done by specifying the instanceStepRate parameter when calling VertexDataDesc::addVertElem().

SPtr<VertexDataDesc> vertexDesc = VertexDataDesc::create();
// Per vertex data (same as for non-instanced drawing)
vertexDesc->addVertElem(VET_FLOAT3, VES_POSITION);
vertexDesc->addVertElem(VET_FLOAT3, VES_NORMAL);
vertexDesc->addVertElem(VET_FLOAT2, VES_TEXCOORD);
// Per instance data
vertexDesc->addVertElem(
VET_FLOAT3, // Each entry in the instance vertex buffer is a 3D float
VES_POSITION, // We map it to the position semantic as vertex shader input
1, // We use the semantic index 1, as 0th index is taken by per-vertex VES_POSITION semantic
1, // We use the second bound vertex buffer for instance data
1 // Instance step rate of 1 means the new element will be fetched from the vertex buffer for each drawn instance
);
// Create a vertex declaration
SPtr<VertexDeclaration> vertexDecl = VertexDeclaration::create(vertexDesc);
RenderAPI& rapi = RenderAPI::instance();
// Bind vertex declaration
rapi.setVertexDeclaration(vertexDecl);
// Bind per-vertex and per-instance vertex buffers
rapi.setVertexBuffers(0, { perVertexVB, perInstanceVB });
// Draw ...

Drawing helper

As a way of making drawing easier you can also use ct::RendererUtility::draw() helper method, accessible globally through ct::gRendererUtility(). This method accepts a ct::Mesh as input and will automatically:

  • Bind vertex & index buffer
  • Bind vertex declaration
  • Set draw operation type
  • Execute a draw call
SPtr<Mesh> mesh = ...;