Rendering

The render code itself is what we have been leading up to in this chapter, and is what we will cover now. One thing of note is that we will generally not really use the ID3D11Device object in this code, but will use ID3D11DeviceContext frequently.

The ID3D11DeviceContext object is mostly intended to construct a list of commands for the rendering device to execute. The context will be used to queue commands that may not be executed straight away. DirectX 11 will try to execute commands “as soon as possible”, but reserves the right to queue multiple commands if deemed more optimal. Pushing commands can be forced using the Flush function, however unless necessary, this is very much not recommended.

In addition to render commands, two notable commands are Map and Unmap. The Map function will allow updating a resource where the device context is at that point in time. This means the data of a resource can be updated after a specific draw call, even if the draw call itself did not start yet. The Map function will return a pointer the new data can be written to. Unmap stops allowing you from writing to the resource, and allows the resource to be used with the new data.

renderer->deviceContext->lpVtbl->ClearRenderTargetView (
    renderer->deviceContext,
    renderer->rtv,
    (float[4]){ 0.0f, 1.0f, 0.0f, 1.0f }
);
renderer->deviceContext->lpVtbl->ClearDepthStencilView (
    renderer->deviceContext,
    renderer->dsv,
    D3D11_CLEAR_DEPTH,
    1.0f,
    0
);

Initially, we would like to make sure the render target and depth render target are cleared. The render target itself does not necessarily need to be cleared as long as you know every pixel is going to be rendered over. Since we do not render a skybox, we will clear the render target in addition to the depth render target.

The depth render target will be cleared to 1. This is the maximum value the depth buffer can have, and since we usually use LESS or LESS_EQUAL as the depth test mode, we want to use the maximum possible value as a clear value.

renderer->deviceContext->lpVtbl->OMSetBlendState (
    renderer->deviceContext,
    renderer->bs,
    (float[4]){1.0f, 1.0f, 1.0f, 1.0f},
    0xFFFFFFFF
);
renderer->deviceContext->lpVtbl->OMSetDepthStencilState (
    renderer->deviceContext,
    renderer->ds,
    0
);
renderer->deviceContext->lpVtbl->RSSetState (
    renderer->deviceContext,
    renderer->rs
);

renderer->deviceContext->lpVtbl->OMSetRenderTargets (
    renderer->deviceContext,
    1,
    (ID3D11RenderTargetView*[1]){
        renderer->rtv
    },
    renderer->dsv
);
renderer->deviceContext->lpVtbl->RSSetViewports (
    renderer->deviceContext,
    1,
    (D3D11_VIEWPORT[1]) {
        [0] = {
            .TopLeftX = 0.0f,
            .TopLeftY = 0.0f,
            .Width    = (float)renderer->windowWidth,
            .Height   = (float)renderer->windowHeight,
            .MinDepth = 0.0f,
            .MaxDepth = 1.0f
        },
    }
);
renderer->deviceContext->lpVtbl->RSSetScissorRects (
    renderer->deviceContext,
    1,
    (D3D11_RECT[1]) {
        [0] = {
            .left   = 0,
            .top    = 0,
            .right  = renderer->windowWidth,
            .bottom = renderer->windowHeight,
        }
    }
);

renderer->deviceContext->lpVtbl->IASetPrimitiveTopology (
    renderer->deviceContext,
    D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST
);
renderer->deviceContext->lpVtbl->IASetInputLayout (
    renderer->deviceContext,
    renderer->il
);

renderer->deviceContext->lpVtbl->VSSetShader (
    renderer->deviceContext,
    renderer->vs,
    NULL,
    0
);
renderer->deviceContext->lpVtbl->VSSetConstantBuffers (
    renderer->deviceContext,
    0,
    1,
    (ID3D11Buffer*[1]){
        renderer->cb
    }
);

renderer->deviceContext->lpVtbl->PSSetShader (
    renderer->deviceContext,
    renderer->ps,
    NULL,
    0
);
renderer->deviceContext->lpVtbl->PSSetSamplers (
    renderer->deviceContext,
    0,
    1,
    (ID3D11SamplerState*[1]){
        renderer->sampler
    }
);

Here we set all of the state we would like to have set.

D3D11_MAPPED_SUBRESOURCE cbSubRes;
renderer->deviceContext->lpVtbl->Map (
    renderer->deviceContext,
    (ID3D11Resource*)renderer->cb,
    0,
    D3D11_MAP_WRITE_DISCARD,
    0,
    &cbSubRes
);
rvm_aos_mat4 mvpMat = { 0 };
rvm_aos_mat4_mul_aos_mat4 ( &mvpMat, projMat, viewMat );
memcpy ( cbSubRes.pData, &mvpMat, sizeof ( rvm_aos_mat4 ) );
renderer->deviceContext->lpVtbl->Unmap (
    renderer->deviceContext,
    (ID3D11Resource*)renderer->cb,
    0
);

The model-view-projection matrix can change every frame, so every frame we update the data in the constant buffer to reflect the new matrix. We call Map to get the pointer to the constant buffer data in cbSubRes. We then memcpy the matrix into the memory of the constant buffer, updating the matrix in the constant buffer.

for ( uint32_t i = 0; i < renderer->meshCount; i++ )
{
    renderer->deviceContext->lpVtbl->IASetVertexBuffers (
        renderer->deviceContext,
        0,
        ATTRIBUTE_COUNT,
        (ID3D11Buffer*[ATTRIBUTE_COUNT]){
            [ATTRIBUTE_POSITION] = renderer->vb,
            [ATTRIBUTE_TEXCOORD] = renderer->vb
        },
        (UINT[ATTRIBUTE_COUNT]){
            [ATTRIBUTE_POSITION] = 3 * sizeof ( float ),
            [ATTRIBUTE_TEXCOORD] = 2 * sizeof ( float )
        },
        (UINT[ATTRIBUTE_COUNT]){
            [ATTRIBUTE_POSITION] = (UINT)renderer->meshes[i].positionAttributeOffset,
            [ATTRIBUTE_TEXCOORD] = (UINT)renderer->meshes[i].texcoordAttributeOffset
        }
    );

    for ( uint32_t j = 0; j < renderer->meshes[i].submeshCount; j++ )
    {
        renderer->deviceContext->lpVtbl->PSSetShaderResources (
            renderer->deviceContext,
            0,
            1,
            &renderer->tex[renderer->meshes[i].submeshes[j].diffuseTexture]
        );

        renderer->deviceContext->lpVtbl->IASetIndexBuffer (
            renderer->deviceContext,
            renderer->ib,
            renderer->meshes[i].submeshes[j].indexType,
            (UINT)renderer->meshes[i].submeshes[j].indexOffset
        );

        renderer->deviceContext->lpVtbl->DrawIndexed (
            renderer->deviceContext,
            (uint32_t)renderer->meshes[i].submeshes[j].indexCount,
            0,
            0
        );
    }
}

Here we loop over every single mesh, and apply the vertex buffers required according to the input layout. The layout created in this case required 2 input elements, and as such reference to 2 buffers, offsets and strides. Since the data is in the same buffer, we pass the same buffer twice, but with different strides and offsets.

Then for every submesh, we would like to bind the applicable texture, set the index buffer with the proper offset, and call the draw function. A submesh is usually signified by a different material applied to the same physical object. Since it is the same object, the vertex data can still be in the same buffer and on the same offset, but with different indices. This saves on a call to set the vertex buffers per submesh.