Tutorial Series - Introduction to Direct3D with XAML application development for Windows Phone 8 : Transformation Matrices

Transformation matrices

I will dedicate this post to matrices. If you missed this mathematical discipline thinking it was probably useless: bad luck, in the world of 3D rendering this topic is essential! :)

A simple explanation (basically) is that each matrix is ​​defined as an array of numbers (4x4 in our case) which will simplify our life for applying transformations to the cube mesh (rotation, translation and scaling). As I do not claim to give you an algebra class (that my notions are actually quite limited), I suggest you to visit the resources section at the end of this article (stolen on a previous post from my colleague David Rousset ^^):

Before depressing, the good news is that Direct3D manages much of the inherent complexity of the matrices, the important point to understand is that the coordinates of our object (vertices in first) will be transformed in the following way.

  1. In the first place, we will apply transformations on the object (model) to position it appropriately in the scene.
  2. Then, we'll look at this object from a certain point of view. Imagine a camera that fits over our cube. We will apply the appropriate transformations to see the object in the new coordinates system.
  3. And finally the projection. It is nice to be in a 3D world, but our screen is a flat surface where each pixel is represented by 2 values (x and y). Projection will help us to switch from the 3D world to the 2D surface.

These transformations are defined by different matrices: model, view and projection. Our initial data (stored in the Vertex Buffer) will be transformed by these matrices to obtain the final coordinated of our pixel on the screen.

This is largely the role of the Vertex Shader (yet another technical term ^^) to perform this operation. How does it do? It will simply multiply the starting coordinates with each matrix model, view and projection, job completed!

Constant Buffer

Before talking about the Vertex Shader, a small additional speech about the Constant Buffer, an awesome program for you dear reader. :)

The graphics card is our friend, it works quickly because this hardware has everything available to work. Its memory is dedicated and we already allocated some information on this area with our previous buffers (Vertex Buffer and Index Buffer).

The same goes for the matrices, we will store them in a Constant Buffer. It is a memory space that Vertex Shader will be able to consume to perform the projection calculations. With this last piece, our Vertex Shader is finally ready to work on our data!

Ok, now that we have introduced the concepts, some code.

1. Constant buffer declaration

a. In SceneRenderer.cpp, we will start by declaring the structure of our Constant Buffer. This structure simply consists of our three 4x4 matrices: model, view and projection.

 struct ModelViewProjectionConstantBuffer
{
    DirectX::XMFLOAT4X4 model;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
};

b. And by the way, we add two member variables in SceneRenderer class to manipulate these matrices.

 Microsoft::WRL::ComPtr<ID3D11Buffer> m_constantBuffer;
ModelViewProjectionConstantBuffer m_constantBufferData;
  • m_constantBuffer: this is a pointer to our buffer data (as well as m_indexBuffer and m_vertexBuffer), directly accessible by the graphics card (and hence accessible by our Vertex Shader).

  • m_constantBufferData: our matrices will be first stored in this variable (as cubeVertices and cubeIndices). In short, our three transformation matrices are initialized in this variable and we send the result to m_constantBuffer.

2. Constant buffer initialization

Initialization is performed in SceneRenderer::CreateDeviceResources, we proceed in a similar way to Index/Vertex Buffer with a call to ID3D11Device::CreateBuffer. The only difference is that the input data is empty at the moment (but we will fix this soon).

 CD3D11_BUFFER_DESC constantBufferDesc(sizeof(ModelViewProjectionConstantBuffer), D3D11_BIND_CONSTANT_BUFFER);
DX::ThrowIfFailed(
    m_d3dDevice->CreateBuffer(
    &constantBufferDesc,
    nullptr,
    &m_constantBuffer
    )
    );

3. Matrices initialization: model, view et projection

a. model matrix

We begin by addressing the matrix model. In this example, I want to rotate the cube: we're going to apply a rotation matrix (on the Y axis), and update the rotation angle for each new frame.

This job is performed in SceneRenderer::Update method with a call to XMatrixRotationY: it simply takes an angle argument (in radians). This type of method exists for all transformations, by the way matrix calculation become a black box that you can use easily.

 void SceneRenderer::Update(float timeTotal, float timeDelta)
{
    (void) timeDelta; // Unused parameter.
    XMStoreFloat4x4(&m_constantBufferData.model, XMMatrixTranspose(XMMatrixRotationY(timeTotal * XM_PIDIV4)));
}

For the two next topics (view and projection matrices initialization), the code is executed once at application launch in SceneRenderer:CreateWindowSizeDependentResources.  

b. view matrix

The view matrix sets the scene in a new coordinate systems, where object is targeted by a camera. To define a camera, we need three vectors:

-
eye defines the position of the camera

-
at defines the position of the focal point (the direction of the targeted object)

-
up defines the up direction of the camera

 

  
Then XMatrixLookAtRH method allows us to reposition the scene with these three new coordinates.

 XMVECTOR eye = XMVectorSet(0.0f, 0.7f, 1.5f, 0.0f);
XMVECTOR at = XMVectorSet(0.0f, -0.1f, 0.0f, 0.0f);
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

XMStoreFloat4x4(&m_constantBufferData.view, XMMatrixTranspose(XMMatrixLookAtRH(eye, at, up)));

Note: Vectors eye, at and up require only three values ​​(x, y, and z). However, you can notice that we are working with four values ​​(the latter being 0.0f) because our transformation matrices are 4x4

c. projection matrix

This latter matrix will helps us to switch from 3D world to 2D surface. We want to project our scene to the flat screen surface. We will also use a small gift from Direct3D to simplify our life: XMMatrixPerspectiveFovRH. However, this function takes four arguments a little bit more tricky that need some explanation :)

 

-
FovAngleY defines the top-down field-of-view angle in radians. This angle is 70 degrees in our case, we just need to convert this value in radian :)

  • AspectRatio defines the aspect ratio of the view space (we devide x by y)
  • NearZ is the distance to the near clipping plane, all graphics elements before this plane are not displayed.
  • FarZ is the distance to the far clipping plane, all graphics elements after this plane are not displayed.

 

As drawing is probably more meaningful, it looks like this:

fov (field of view) diagram

And voila, we have our third matrix !

 float aspectRatio = m_windowBounds.Width / m_windowBounds.Height;
float fovAngleY = 70.0f * XM_PI / 180.0f;  // convert in radian

if (aspectRatio < 1.0f)
{
    fovAngleY /= aspectRatio;
}

XMStoreFloat4x4(
    &m_constantBufferData.projection,
    XMMatrixTranspose(
        XMMatrixPerspectiveFovRH(
            fovAngleY,        // field-of-view
            aspectRatio,      // aspect ratio
            0.01f,            // near clipping plane
            100.0f            // far clipping plane
            )
        )
    );

Perfect, we have our three matrix! But when am I supposed to copy the information from m_constantBufferData variable to m_constantBuffer buffer!?

A little bit early to discuss about this point, but you can take a look at the method SceneRenderer::Render(), the call to UpdateSubresource performs exactly this job. We will talk soon in details about this point.

 

 m_d3dContext->UpdateSubresource(
    m_constantBuffer.Get(),
    0,
    NULL,
    &m_constantBufferData,
    0,
    0
    );

Resources

 

Download the Code