Tutorial Series – Introduction to Direct3D with XAML application development for Windows Phone 8 : Create a basic mesh


The previous episode showed us that we could display a blue background by using lot of code 🙂 but in return you have now opened the door on DirectX. In this second article, I will attempt to explain the main concepts of Direct3D to help you start. These points are, however, not specific to Windows Phone, and you will find at the bottom of this article additional resources to help you.

Direct3D Initialization

Good news, you do not have to manage this part of code, DrawingSurfaceBackgroundGrid control (or DrawingSurface) supports this point to you, it’s a gift! Of course, if you do not use the XAML controls and you want to develop an application 100% native, you must perform this initialization ... do not dream either 🙂

Specifically, what are the supported operations during this initialization??

1. Creating device and device context objects

Communication with the graphics card (or more broadly the device) is done through these two objects. These are the main entry points to use Direct3D.

  • The device object (ID3D11Device) is primarily responsible for creating the graphic resources (textures, vertex buffers, shaders, ...).
  • The device context object (ID3D11DeviceContext) takes care of rendering operations using resources initialized by the device object.      

 

2. Creating swap chain

The rendering is not performed directly on the screen, it is made into a buffer memory (aka. an array of pixels of the screen size) named back buffer. The swap chain is responsible for copying the contents of the back buffer directly on the screen. For information, the swap chain can theoretically use multiple back buffers, but Windows Phone limit its use to a single buffer.

In addition, Direct3D provides access to data located in the back buffer (our array of pixels) via the render target view object (ID3D11RenderTargetView).

Lost? Let us take for example the lines of code from the previous post. The code Direct3DBackground::Draw method initializes our screen with a blue color.

a/ ClearRenderTargetView method is called from the device context object
b/ We target the back buffer via render target view object. It becomes clearer?

HRESULT Direct3DBackground::Draw(_In_ ID3D11Device1* device, _In_ ID3D11DeviceContext1* context, _In_ ID3D11RenderTargetView* renderTargetView)
{
// Clear the render target to default values.
const float midnightBlue [] = { 0.098f, 0.098f, 0.439f, 1.000f };
context->ClearRenderTargetView(
renderTargetView,
midnightBlue
);

RequestAdditionalFrame();

return S_OK;
}

For additional information, I suggest you to read the following article: Creating a Direct3D device and swap chain for Windows Phone 8.

Building our 3D mesh object

Vertices

A 3D object (or mesh) is generally defined by a list of vertices and faces. For example, eight vertices are needed to describe a cube. And since we're in a three-dimensional world, each vertex has three coordinates: x, y and z. If the center of the cube is placed at the origin of the scene, with vertices values defined ​​between -0.5 and 0.5, we obtain the following table:

image

image

To complete our example, we will assign a color to each vertex (to see the faces more easily when rendering). This color is also made up of three values​​: red, green and blue (RGB format). A vertex will be defined by a position and a color associated in the following way:

struct VertexPositionColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 color;
};

For complex objects, we usually load the values ​​from a file (eg from an Autodesk FBX format which will be the subject of a future post to load our shuttle). In the present case, we will manually declare our vertices table with colors defined arbitrarily:

VertexPositionColor cubeVertices [] =
{
{ XMFLOAT3(-0.5f, -0.5f, -0.5f), XMFLOAT3(0.0f, 0.0f, 0.0f) }, // Vertice 0 at -0.5, -0.5, -0.5 // White
{ XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT3(0.0f, 0.0f, 1.0f) }, // Vertice 1 at -0.5, -0.5, 0.5 // Blue
{ XMFLOAT3(-0.5f, 0.5f, -0.5f), XMFLOAT3(0.0f, 1.0f, 0.0f) }, // Vertice 2 at -0.5, 0.5, -0.5 // Green
{ XMFLOAT3(-0.5f, 0.5f, 0.5f), XMFLOAT3(0.0f, 1.0f, 1.0f) }, // Vertice 3 at -0.5, 0.5, 0.5
{ XMFLOAT3( 0.5f, -0.5f, -0.5f), XMFLOAT3(1.0f, 0.0f, 0.0f) }, // Vertice 4 at 0.5, -0.5, -0.5 // Red
{ XMFLOAT3( 0.5f, -0.5f, 0.5f), XMFLOAT3(1.0f, 0.0f, 1.0f) }, // Vertice 5 at 0.5, -0.5, 0.5
{ XMFLOAT3( 0.5f, 0.5f, -0.5f), XMFLOAT3(1.0f, 1.0f, 0.0f) }, // Vertice 6 at 0.5, 0.5, -0.5
{ XMFLOAT3( 0.5f, 0.5f, 0.5f), XMFLOAT3(1.0f, 1.0f, 1.0f) }, // Vertice 7 at 0.5, 0.5, 0.5 // Black
};

Faces (indices declaration)

Now that we have our vertices, it remains to define the faces of the cube to complete its definition: it has six faces, each consisting of four vertices (a square). However, if you reason more broadly, some 3D objects cannot be defined with rectangular faces. For example, a square base pyramid is built with one face of 4 vertices (the square basement) + four faces of 3 vertices (the triangular sides).

Algorithms to fill the faces would be particularly complex if the number of vertices is variable. Direct3D (and more generally the 3D world) therefore uses triangles as primitive to define faces, each square can be represented by two triangles.

If we take the example of our cube, the first side is therefore composed of two triangles:

  • Triangle 1: {0, 2, 1}
  • Triangle 2: {1, 2, 3}

 

image

And extrapolating, we end up with 12 triangles for a complete definition of faces. We will catalog / index these triangles in a table named indices.

unsigned short cubeIndices[] = 
{
0,2,1, // -x
1,2,3,

4,5,6, // +x
5,7,6,

0,1,5, // -y
0,5,4,

2,6,7, // +y
2,7,3,

0,4,6, // -z
0,6,2,

1,3,7, // +z
1,7,5,
};

Vertex buffer and Index buffer

We just obtained a definition of our cube stored in two tables (cubeVertices and cubeIndices) but DirectX need a little extra hand to use and interpret the data above.

First, the two lists [vertices and faces (indices)] must be stored in buffers provided for this purpose, named without much originality Vertex Buffer and Index Buffer: this is the purpose of ID3D11Device::CreateBuffer method.

Simply, ID3D11Device::CreateBuffer will take the following input arguments …

  • Data via D3D11_SUBRESOURCE_DATA structure that points to our cubeVertices or cubeIndices tables.
  • A description via D3D11_BUFFER_DESC to indicate the use of these data: vertex buffer, index buffer, ...

 

… and we will return an object that implements the D3D11Buffer interface (our Direct3D buffer). So we get the following code to instantiate our Vertex Buffer:

D3D11_SUBRESOURCE_DATA vertexBufferData = { 0 };
vertexBufferData.pSysMem = cubeVertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;

CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(cubeVertices), D3D11_BIND_VERTEX_BUFFER);
DX::ThrowIfFailed(
m_d3dDevice->CreateBuffer(
&vertexBufferDesc,
&vertexBufferData,
&m_vertexBuffer
)
);

   And code almost equivalent to instantiate the Index Buffer:

D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
indexBufferData.pSysMem = cubeIndices;
indexBufferData.SysMemPitch = 0;
indexBufferData.SysMemSlicePitch = 0;

CD3D11_BUFFER_DESC indexBufferDesc(sizeof(cubeIndices), D3D11_BIND_INDEX_BUFFER);
DX::ThrowIfFailed(
m_d3dDevice->CreateBuffer(
&indexBufferDesc,
&indexBufferData,
&m_indexBuffer
)
);

 

There is one major difference between these two buffers DirectX. They both contain data, but in the state, only the Index buffer is understood by Direct3D. Why?

Our Vertex buffer contains the position of the vertices, but also the colors. And in the future posts, you will add other information associated with each vertex (including texture coordinates, normals …). So, at the moment, DirectX has no idea of the exact meaning of this data that you provided. It is not able to digest without an added boost. This small service is offered by the input layout (see next section).

This is not the case of the Index buffer whose only job is to index the vertices to provide a list of triangles. Direct3D natively knows the meaning of these data

Input layout

The vertex buffer currently contains a series of floating point numbers. Direct3D will process this data into a pipeline whose main stage is called vertex shader (which will be topic of the next post).

In the current state, the graphics card does not know the structure of the data. Direct3D requires that we clear the way by specifying the format of each element: by the way, the vertex shader can interpret this information. The input layout is exactly designed for that purpose! In our case, we obtain the following definition:

const D3D11_INPUT_ELEMENT_DESC vertexDesc [] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

DX::ThrowIfFailed(
m_d3dDevice->CreateInputLayout(
vertexDesc,
ARRAYSIZE(vertexDesc),
fileData->Data,
fileData->Length,
&m_inputLayout
)
);

  • POSITION: Declares the position of our vertices with 3 floats struct (32bits): x, y and z.
  • COLOR: Declares the color of our vertices with 3 floats struct (32bit): r, g and b.

 

You may find that I used the DXGI_FORMAT_R32G32B32_FLOAT format in both cases, which seems to be specific to RGB color usage. This is not a mistake, as it is a 3 floats struct in both cases, why not use the same format?

In more pictorial way, our array of floats is now interpreted as follows by Direct3D:

inputlayout
 
Well, I'll let you digest all this ^^.
 
You can already download the code here (which looks similar as default template in the state, but that will soon change) and check out the portion of code SceneRenderer::CreateDeviceResources() that resumes (except loading shaders) the entire code for this article.
 
SceneRenderer::CreateDeviceResources
void SceneRenderer::CreateDeviceResources()
{
auto loadVSTask = DX::ReadDataAsync("SimpleVertexShader.cso");
auto loadPSTask = DX::ReadDataAsync("SimplePixelShader.cso");

auto createVSTask = loadVSTask.then([this](Platform::Array<byte>^ fileData) {
DX::ThrowIfFailed(
m_d3dDevice->CreateVertexShader(
fileData->Data,
fileData->Length,
nullptr,
&m_vertexShader
)
);

const D3D11_INPUT_ELEMENT_DESC vertexDesc [] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

DX::ThrowIfFailed(
m_d3dDevice->CreateInputLayout(
vertexDesc,
ARRAYSIZE(vertexDesc),
fileData->Data,
fileData->Length,
&m_inputLayout
)
);
});

auto createPSTask = loadPSTask.then([this](Platform::Array<byte>^ fileData) {
DX::ThrowIfFailed(
m_d3dDevice->CreatePixelShader(
fileData->Data,
fileData->Length,
nullptr,
&m_pixelShader
)
);

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

auto createCubeTask = (createPSTask && createVSTask).then([this]() {
VertexPositionColor cubeVertices [] =
{
{ XMFLOAT3(-0.5f, -0.5f, -0.5f), XMFLOAT3(0.0f, 0.0f, 0.0f) }, // Vertice 0 at -0.5, -0.5, -0.5 // White
{ XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT3(0.0f, 0.0f, 1.0f) }, // Vertice 1 at -0.5, -0.5, 0.5 // Blue
{ XMFLOAT3(-0.5f, 0.5f, -0.5f), XMFLOAT3(0.0f, 1.0f, 0.0f) }, // Vertice 2 at -0.5, 0.5, -0.5 // Green
{ XMFLOAT3(-0.5f, 0.5f, 0.5f), XMFLOAT3(0.0f, 1.0f, 1.0f) }, // Vertice 3 at -0.5, 0.5, 0.5
{ XMFLOAT3( 0.5f, -0.5f, -0.5f), XMFLOAT3(1.0f, 0.0f, 0.0f) }, // Vertice 4 at 0.5, -0.5, -0.5 // Red
{ XMFLOAT3( 0.5f, -0.5f, 0.5f), XMFLOAT3(1.0f, 0.0f, 1.0f) }, // Vertice 5 at 0.5, -0.5, 0.5
{ XMFLOAT3( 0.5f, 0.5f, -0.5f), XMFLOAT3(1.0f, 1.0f, 0.0f) }, // Vertice 6 at 0.5, 0.5, -0.5
{ XMFLOAT3( 0.5f, 0.5f, 0.5f), XMFLOAT3(1.0f, 1.0f, 1.0f) }, // Vertice 7 at 0.5, 0.5, 0.5 // Black
};

D3D11_SUBRESOURCE_DATA vertexBufferData = { 0 };
vertexBufferData.pSysMem = cubeVertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;
CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(cubeVertices), D3D11_BIND_VERTEX_BUFFER);
DX::ThrowIfFailed(
m_d3dDevice->CreateBuffer(
&vertexBufferDesc,
&vertexBufferData,
&m_vertexBuffer
)
);

unsigned short cubeIndices [] =
{
0, 2, 1, // -x
1, 2, 3,

4, 5, 6, // +x
5, 7, 6,

0, 1, 5, // -y
0, 5, 4,

2, 6, 7, // +y
2, 7, 3,

0, 4, 6, // -z
0, 6, 2,

1, 3, 7, // +z
1, 7, 5,
};

m_indexCount = ARRAYSIZE(cubeIndices);

D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
indexBufferData.pSysMem = cubeIndices;
indexBufferData.SysMemPitch = 0;
indexBufferData.SysMemSlicePitch = 0;
CD3D11_BUFFER_DESC indexBufferDesc(sizeof(cubeIndices), D3D11_BIND_INDEX_BUFFER);
DX::ThrowIfFailed(
m_d3dDevice->CreateBuffer(
&indexBufferDesc,
&indexBufferData,
&m_indexBuffer
)
);
});

createCubeTask.then([this]() {
m_loadingComplete = true;
});
}

Next episode soon on shaders and matrices .... keep posted! 🙂

Resources

 

Download the Code

Comments (2)

  1. KD says:

    Can you attach your code for this tutorial. The current code points to first tutorial's code.

  2. How to open the device's keyboard on such xaml and direct3d app? says:

    Hi, your tutorial has been very helpful to me. Thanks very much.

    However, when I want to open the device's keyboard using the code:

    CoreWindow::GetForCurrentThread()->IsKeyboardInputEnabled = true;

    in the component code. The app just exit with error code -1.

    I guess it's because the thread of keyboard is different from the thread of the component.

    Do you know how to solve this?

Skip to main content