Post

Assignment 05 - Core Library and MVP Transforms

September 27, 2026

Objective

This week, I made major improvements to the design of my game engine by introducing a DrawCall constant buffer that stores the localToWorld transform for each mesh/effect draw call. Additionally, I created a Core Library with new classes to represent game objects, actors, cameras, and more. Finally, I implemented the View and Projection matrices and updated the vertex shaders to properly render within a 3D space.

Gameplay

Download x64 .ZIP Download x86 .ZIP

Gameplay Footage

Graphics Library

DrawCall Constant Buffer

Since one of the major goals of this week is to properly start rendering meshes in 3D space, the Model-View-Projection transform needs to be applied in the vertex shader. The View and Projection matrices only need to be calculated once per frame and therefore can use the previous Frame constant buffer. However, a new buffer should be introduced that can be updated with every DrawCall, or a DrawCall constant buffer. The buffer stores the Model transform (or local to world), and can be represented by a single 4x4 matrix. The buffer size is 64 bytes for both platforms, the size of eae6320::Math::cMatrix_transformation.

However, this matrix needs to be cached for each entry in our array within the sDataRequiredToRenderAFrame struct discussed in previous weeks. The transform is 64 bytes, which is small enough to store within the cache but substantially increases its size. I decided to replaced the std::pair<cMesh*, cEffect*> typedef with a new sRenderableObject struct which can be seen below.

1
2
3
4
5
6
7
8
9
10
// MeshEffectPair typedef from last week
typedef std::pair<eae6320::Graphics::cMesh*, eae6320::Graphics::cEffect*> MeshEffectPair;

// new struct introduced this week to replace the MeshEffectPair
struct sRenderableObject
{
    eae6320::Math::cMatrix_transformation localToWorld;
    eae6320::Graphics::cMesh* mesh;
    eae6320::Graphics::cEffect* effect;
};

This also requires an update to the interface used to submit data to the Graphics Library.

1
2
3
4
5
// the old interface to submit meshes and effects
void SubmitPair(cMesh* mesh, cEffect* effect);

// the new interface to submit meshes and effects
void SubmitRenderableObject(cMesh* i_mesh, cEffect* i_effect, const Math::cMatrix_transformation& i_localToWorld);

Shader Platform Independence

I attempted to make the shaders that my engine uses a bit more platform-independent in order to reduce code duplication. First, I used a preprocessor directive to define constant buffers in a way similar to a function call. shaders.inc defines these preprocessor directives in a platform-dependent way.

1
2
3
4
5
#if defined( EAE6320_PLATFORM_D3D )
    #define DeclareConstantBuffer( i_name, i_id ) cbuffer i_name : register( b##i_id )
#elif defined( EAE6320_PLATFORM_GL )
    #define DeclareConstantBuffer( i_name, i_id ) layout( std140, binding = i_id ) uniform i_name
#endif

The constant buffer declaration can then be called independently from the platform.

1
2
3
4
DeclareConstantBuffer( g_constantBuffer_drawCall, 2 )
{
    float4x4 g_transform_localToWorld;
};

Code duplication and platform dependence can be further reduced by used the #define preprocessor directive to replace equivalent variable types and function calls. For example, here is the replacement of float4x4 with mat4.

1
2
3
4
#if defined( EAE6320_PLATFORM_GL )
    #define float4x4 mat4
    // continue with similar preprocessor directives for other types
#endif

Core Library

Now that I am able to render objects in a 3D environment, the project has reached a point that a proper Core Library makes sense. I created a new static library that houses the classes cGameObject, cActor, and cCamera. These classes are meant to represent objects within the game.

cGameObject

I chose to start with cGameObject I chose this as the base representation of any object that may be within a game, but not necessarily tied to the simulation. I chose to make this distinction to plan for future features. Game objects that have a position in 3D space and a part of the simulation can be seen in the next section. Below, you’ll see that all game objects have reference counting, hidden constructors, and an update function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class cGameObject
{
public:
    // References
    //-----------
    EAE6320_ASSETS_DECLAREDELETEDREFERENCECOUNTEDFUNCTIONS(cGameObject);
    EAE6320_ASSETS_DECLAREREFERENCECOUNTINGFUNCTIONS();
    EAE6320_ASSETS_DECLAREREFERENCECOUNT();

    // Update
    //-------
    virtual void UpdateBasedOnInput() { }

protected:
    // Initialization / Clean Up
    //--------------------------
    virtual cResult Initialize();
    virtual cResult CleanUp();

    cGameObject() = default;
    virtual ~cGameObject();
};

cActor

cActor inherits from cGameObject and is meant to represent objects that exist in 3D space and run in the simulation. Therefore, you will see UpdateSimulationBasedOnTime() and m_rigidBodyState. I also chose to include an m_isVisible variable, but this arguably acts more as an m_isActive variable and may be moved into the base class in future weeks.

1
2
3
4
5
6
7
8
9
10
class cActor : public cGameObject
{
public:
    virtual void UpdateSimulationBasedOnTime(const float i_elapsedSecondCount_sinceLastUpdate) override;

public:
    bool m_isVisible = true;
    Physics::sRigidBodyState m_rigidBodyState;
    Math::cMatrix_transformation GetWorldTransform(const float i_elapsedSecondCount = 0.0f) const;
};

The function GetWorldTransform is what is used to submit the localToWorld transform to the Graphics library. Note the variable i_elapsedSecondCount that is passed into the function. While it may seem odd to pass in time into the GetWorldTransform function, it is used in order to extrapolate movement between draw calls.

cCamera

Now that I have a cActor class, I can create a cCamera class that inherits from it. This class will be used to hold important camera variables and will utilize the m_rigidBodyState accessible through the base class. This can be used in GetViewTransform and GetProjectionTransform to finish off the View-Projection transforms of the Model-View-Projection transformation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class cCamera : public cActor
{
public:
    static cResult CreateCamera(
        cCamera*& o_camera,
        /* other initializing variables */
    );

private:
    float m_verticalFieldOfView_inRadians;
    float m_aspectRatio;
    float m_z_nearPlane;
    float m_z_farPlane;

public:
    Math::cMatrix_transformation GetViewTransform(const float i_elapsedSecondCount = 0.0f) const;
    Math::cMatrix_transformation GetProjectionTransform() const;

    // ...
};

In the main application, I use the keys ‘W’ and ‘S’ to control the camera’s acceleration relative to the player’s forward vector, ‘Q’ and ‘E’ to control the camera’s acceleration relative to the player’s right vector, and ‘A’ and ‘D’ to control the player’s angular velocity.

1
2
3
4
void SubmitCameraTransforms(
    const Math::cMatrix_transformation& i_transform_worldToCamera,
    const Math::cMatrix_transformation& i_transform_cameraToProjected
);

The View and Projection transforms are submitted to the Graphics library through the SubmitCameraTransforms function. The two inputs can be calculated using cCamera::GetViewTransform and cCamera::GetProjectionTransform. Note the input parameter i_elapsedSecondCount in cCamera::GetViewTransform. This allows me to extrapolate the camera’s position and prevent jerky movement using the function below.

1
Math::cMatrix_transformation PredictFutureTransform( const float i_secondCountToExtrapolate ) const;

MyGame

aSquare

We can now create game objects specific to my game. For testing purposes, I created a class called aSquare that inherits from cActor. This new class contains a pointer to a mesh and to an effect, but I chose to make them private member variables in order to control reference tracking. You’ll see the GetMesh function to get a copy of the pointer, and the SetMesh function that properly decrements existing pointers and increments the incoming pointer as it is copied into the class. This easily allows me to swap out the mesh. You’ll see in the gameplay footage that pressing Tab will swap the square mesh with a triangle mesh.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class aSquare : public Core::cActor
{
public:
    static cResult CreateSquareInstance(
        aSquare*& o_square,
        const Math::sVector& i_position = Math::sVector(),
        const Math::cQuaternion& i_orientation = Math::cQuaternion()
    );

    // Mesh and Effect
    //----------------
public:
    Graphics::cMesh* GetMesh() const;
    void SetMesh(Graphics::cMesh* i_mesh);

    Graphics::cEffect* GetEffect() const;
    void SetEffect(Graphics::cEffect* i_effect);

private:
    Graphics::cMesh* m_mesh;
    Graphics::cEffect* m_effect;

    // ...
};

The main application also allows for other controls. Specifically, the Up and Down Arrow keys moves the actor along the y-axis smoothly, and the Left and Right Arrow keys move the actor along the x-axis smoothly. Smooth movement is done using the same prediction functions mentioned earlier in this devlog.