In my DX11 forward renderer (not a PBR renderer), meshes are responsible only for binding themselves to a shader and calling the drawIndexed function. Each mesh also stores its own transformation matrix. This transformation matrix never changes, as it is defined when the model file is created (i.e: it's an model transform created by blender, not actually a world transform)
class Mesh
{
public:
void draw() const;
private:
XMMATRIX m_modelTransform;
// vertex buffer and index buffer also goes here
}
void Mesh::draw() const
{
UINT stride = sizeof(Vertex);
UINT offset = 0;
// DX11 Context
auto context = WND->getContext();
// Bind vertex and index buffers to pipeline
context->IASetVertexBuffers(0, 1, m_vertexBuffer.GetAddressOf(), &stride, &offset);
context->IASetIndexBuffer(m_indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// Draw
context->DrawIndexed(m_indexCount, 0, 0);
}
Meshes exist under a “MeshView” class:
class MeshView
{
public:
// ...
private:
Material const* m_material;
Mesh const* m_mesh;
}
the material class contains a set of textures: albedo and roughness, as well as a metallic and roughness factor.
Since you can actually have a hierarchy of meshes in glTF, My “Model” class is effectively a list of MeshViews:
class Model
{
public:
//...
private:
vector<MeshView> const m_meshViews
}
The model class draw method takes a shader and world transform matrix, both arguments are required:
void Model::draw(
Shader const& shader,
XMMATRIX const& transform
) const
{
// shader.use() calls VSSetShader and PSSetShader
shader.use();
for (MeshView const& vm : *m_viewModels)
{
vm.draw(shader, transform);
}
}
The meshview draw method binds its material to the shader, multiplies the model's world transform matrix by the mesh's model transform matrix, and then draws the mesh:
void MeshView::draw(
Shader const& shader,
XMMATRIX const& transform
) const
{
// bind albedo and roughness textures to shader
if (m_material)
{
m_material->use(shader);
}
// the model buffer is a cbuffer that exists on every shader at register(b0)
shader.bindModelBuffer(m_mesh->getModelTransform() * transform);
m_mesh->draw();
}
Every shader requires this data:
- A mesh (i.e: a vertex buffer and index buffer)
- An albedo texture (and an optional roughness material) at register(t0)
- A model buffer at register(b0)
Other shaders have other optional cbuffers. The logic of how to bind data to these is handled in “DrawableMixins". Every Object in my game has a pointer to an IDrawableMixin, which is an object that defines a single draw method:
class IDrawableMixin
{
public:
virtual void draw(
IWorldObject const* object,
Shader const* shader = nullptr
) const = 0;
};
Where IWorldObject is the base class for all entities in my game. Each IDrawableMixin defines different behavior, but are still mostly the same. I have a “WorldShader”, which is a shader that performs a simple 3D perspective transform and textures my objects. This has an equivalent “WorldObjectDrawable”
void WorldObjectDrawable::draw(
IWorldObject const* object,
Shader const* shader // this argument allows the world object to override the specific shader used here
) const
{
static Shader const* s_shader = ASSET->getShader("World"); // get the World Shader
if (!shader) // allows the object to override the shader used here
{
shader = s_shader;
}
auto model = object->getModel(); // worldObjects store a model
if (model)
{
model->draw(*shader, object->getTransform());
}
}
This is a very simple shader; it simply calls the model draw methods described above using the World shader.
For a more complicated example, I have the “UI shader”, which allows me to draw flat images on the User interface that also support sprite-based animations:
/* This is called "WeaponDrawable" because I use flat sprites like in
* Doom or Wolfenstein 3D to draw the weapon the player is using
*/
void WeaponDrawable::draw(
IWorldObject const* object,
Shader const* shader
) const
{
static Shader const* s_shader = ASSET->getShader("UI");
/* The quad mesh is a simple 2D box. It requires no material properties
* and has no other meshes in its hierarchy. All it requires is an albedo
* texture to draw on it
*/
static Mesh const* s_quad = ASSET->getQuad();
/* WeaponDrawable requires the IWorldObject that calls it to be a child class
* called IWeapon. Dynamic casts are slower than static casts so maybe don't do this
*/
IWeapon const* weapon = dynamic_cast<IWeapon const*>(object);
if (weapon == nullptr)
{
return;
}
/* This next section is about setting the UV coordinates on the quad so that it renders
* a subsection of the animation sprite sheet */
auto animation = weapon->getAnimation();
SpriteAnimationBuffer buffer;
buffer.m_src = XMFLOAT4(
(animation->getFrameWidth() / (f32)texture->getWidth()) * (f32)animation->getFrame(),
animation->getFrameHeight() / (f32)texture->getHeight() * animation->getRowNumber(),
(animation->getFrameWidth() / (f32)texture->getWidth()),
animation->getFrameHeight() / (f32)texture->getHeight()
);
XMStoreFloat4(&buffer.m_dest, weapon->getPos());
/* This is a fun little animation that makes the gun bob back and forth as you walk
*/
f32 x = cosf(weapon->getTheta() / 150.0f);
f32 y = (sinf(weapon->getTheta() / 75.0f) + 1.0f) /2.0f;
buffer.m_dest.y -= y * weapon->getSpeed() / 4.0f;
buffer.m_dest.x -= x * weapon->getSpeed() / 2.0f;
/* Most Shaders have a "custom cbuffer" which exists at register(b1). This allows
* me to send shader-specific cbuffer configurations */
s_shader->bindCBuffer(&buffer);
s_shader->use();
animation->getTexture()->use();
s_quad->draw();
}
By no means would I say that what I'm doing is typical, or that you should emulate me. I'm just showing an example of how I did it and you can compare with yours. If you think my system is stupid and you hate it, that's fine. If you like some aspects of it, feel free to use those as well. My intention was to design a system where the objects for rendering more closely mirror the hierarchy of objects in glTF (the model format I'm using)
For another example of someone else's renderer, check out the DX11 tutorials on rastertek: https://rastertek.com/tutdx11win10.html . His tutorials don't just show you simple examples of how to draw a triangle or do shadows; he develops a whole framework for a renderer that follows through each step of the tutorial, so it gives you an idea of how you might structure your project.