-
Notifications
You must be signed in to change notification settings - Fork 411
Simple rendering
Getting Started |
---|
Here we learn how to render a 2D triangle and the use of the built-in basic effects.
First create a new project using the instructions from the earlier lessons: Using DeviceResources and Adding the DirectX Tool Kit which we will use for this lesson.
In order to do a draw operation with Direct3D 12, we need to provide the following objects and settings:
- A vertex buffer containing the vertices of the elements to draw.
- A root signature which defines how the CPU and GPU shader programs share data.
- A pipeline state object which defines all state, the vertex input layout, and the compiled shader programs.
- A primitive topology setting that indicates how to interpret the individual vertices (as a point, a line, a triangle, etc.)
For this lesson, the BasicEffect object will provide the root signature and pipeline state object, VertexPositionColor will provide the input layout, and PrimitiveBatch will provide the vertex buffer and primitive topology.
In the Game.h file, add the following variables to the bottom of the Game class's private declarations (right after the m_graphicsMemory
variable you already added as part of setup):
using VertexType = DirectX::VertexPositionColor;
std::unique_ptr<DirectX::BasicEffect> m_effect;
std::unique_ptr<DirectX::PrimitiveBatch<VertexType>> m_batch;
In Game.cpp, add to the TODO of CreateDeviceDependentResources after where you have created m_graphicsMemory
:
m_batch = std::make_unique<PrimitiveBatch<VertexType>>(device);
RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
m_deviceResources->GetDepthBufferFormat());
EffectPipelineStateDescription pd(
&VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullNone,
rtState);
m_effect = std::make_unique<BasicEffect>(device, EffectFlags::VertexColor, pd);
In Game.cpp, add to the TODO of OnDeviceLost where you added m_graphicsMemory.reset()
:
m_effect.reset();
m_batch.reset();
In Game.cpp, add to the TODO of Render:
m_effect->Apply(commandList);
m_batch->Begin(commandList);
VertexPositionColor v1(Vector3(0.f, 0.5f, 0.5f), Colors::Yellow);
VertexPositionColor v2(Vector3(0.5f, -0.5f, 0.5f), Colors::Yellow);
VertexPositionColor v3(Vector3(-0.5f, -0.5f, 0.5f), Colors::Yellow);
m_batch->DrawTriangle(v1, v2, v3);
m_batch->End();
Build and run to see a simple yellow triangle rendered in 2D.
You don't have to use a type alias here like
VertexType
and you can just useDirectX::VertexPositionColor
for the header andVertexPositionColor
in the cpp file directly. I use the alias here to simplify the tutorial a bit later on.
The image above is drawn using coordinates that are independent of the screen resolution and range from -1
to +1
. Resizing the window will result in the same image scaled to the new window. If instead you want to draw using screen pixel coordinates (which match the coordinate system used by SpriteBatch), then:
In Game.cpp, add to the TODO of CreateWindowSizeDependentResources:
auto size = m_deviceResources->GetOutputSize();
Matrix proj = Matrix::CreateScale(2.f / float(size.right),
-2.f / float(size.bottom), 1.f)
* Matrix::CreateTranslation(-1.f, 1.f, 0.f);
m_effect->SetProjection(proj);
The projection matrix can also be created with
Matrix::CreateOrthographicOffCenter(0.f, float(size.right), float(size.bottom), 0.f, 0.f, 1.f);
If you are not familiar with transformation matrices used in computer graphics, you may want to review Using the SimpleMath library now and return to this tutorial. In simple terms, all the code above does is create a matrix to: (1) shift the 0,0 origin to the upper-right corner, (2) flip the y-axis so 0 is the top instead of bottom of the screen, and (3) scale the size in pixels to take up the entire -1 to 1 range (i.e. 2) in each axis.
In Game.cpp, modify the TODO of Render:
m_effect->Apply(commandList);
m_batch->Begin(commandList);
VertexPositionColor v1(Vector3(400.f, 150.f, 0.f), Colors::Yellow);
VertexPositionColor v2(Vector3(600.f, 450.f, 0.f), Colors::Yellow);
VertexPositionColor v3(Vector3(200.f, 450.f, 0.f), Colors::Yellow);
m_batch->DrawTriangle(v1, v2, v3);
m_batch->End();
Build and run to get the same image, but if you resize the window the triangle will not change in the second version if your window size is 800 by 600.
- The BasicEffect family of shader classes uses shader code built in to the
DirectXTK12.lib
as static data so there's no need to compile shaders at runtime or to load data files from disk. - Internally, both SpriteBatch and PrimitiveBatch make use of a dynamic rather than static vertex buffer object.
- In Direct3D 12, all shader and state choices must be made when the pipeline state object is created. If you want to use different states or a different render target format you need to create a new IEffect instance.
- Since we haven't used any textures yet, we don't need a descriptor heap
- The basic game loop already includes the call to set the
SetViewport
state that tells Direct3D how to map the '-1 to +1' coordinates to the pixel size of your render target. The transformation above does the reverse of that that transformation.
The use of CullNone
for our rasterizer state above allows triangles and quads--which in Direct3D are just two triangles--to be drawn with arbitrary winding order. If you modify CreateDeviceDependentResources above as follows:
EffectPipelineStateDescription pd(
&VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullClockwise,
rtState);
Then build & run you run you will see nothing drawn because the triangle winding order was specified in clockwise order. If you changed it again to:
EffectPipelineStateDescription pd(
&VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullCounterClockwise,
rtState);
Then build & run you will see the triangle reappear.
For 'closed' objects, you typically use backface culling to speed up rendering which can quickly reject triangles that are not facing the viewer and avoids the need to run the pixel shader for those pixels.
The culling mode does not affect points or lines.
In the rendering above, we used 'per-vertex' colors, but we used the same in all three corners. You can also use different colors which will blend smoothly between the vertices.
In Game.cpp, modify the TODO of Render:
...
VertexPositionColor v1(Vector3(400.f, 150.f, 0.f), Colors::Red);
VertexPositionColor v2(Vector3(600.f, 450.f, 0.f), Colors::Green);
VertexPositionColor v3(Vector3(200.f, 450.f, 0.f), Colors::Blue);
...
Build and run to see a simple RGB triangle rendered in 2D.
Start by saving rocks.jpg into your new project's directory, and then from the top menu select Project / Add Existing Item.... Select "rocks.jpg" and click "OK".
In the Game.h file, add the following variable to the bottom of the Game class's private declarations:
std::unique_ptr<DirectX::DescriptorHeap> m_resourceDescriptors;
Microsoft::WRL::ComPtr<ID3D12Resource> m_texture;
enum Descriptors
{
Rocks,
Count
};
In Game.cpp, add to the TODO of CreateDeviceDependentResources:
m_resourceDescriptors = std::make_unique<DescriptorHeap>(device,
Descriptors::Count);
ResourceUploadBatch resourceUpload(device);
resourceUpload.Begin();
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload, L"rocks.jpg",
m_texture.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_texture.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::Rocks));
auto uploadResourcesFinished = resourceUpload.End(
m_deviceResources->GetCommandQueue());
uploadResourcesFinished.wait();
In Game.cpp, add to the TODO of OnDeviceLost:
m_texture.Reset();
m_resourceDescriptors.reset();
Build and run to make sure the texture loads fine. Nothing else should be different yet.
Now go back to your Game.h and modify the VertexType
alias we used earlier to use VertexPositionTexture. Also you need to add a CommonStates member variable.
using VertexType = DirectX::VertexPositionTexture;
...
std::unique_ptr<DirectX::CommonStates> m_states;
In Game.cpp, add to the TODO of CreateResources:
m_states = std::make_unique<CommonStates>(device);
Then in Game.cpp modify CreateDeviceDependentResources:
...
m_effect = std::make_unique<BasicEffect>(device, EffectFlags::Texture, pd);
m_effect->SetTexture(m_resourceDescriptors->GetGpuHandle(Descriptors::Rocks), m_states->LinearClamp());
In Game.cpp, modify the TODO of Render:
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap(), m_states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_effect->Apply(commandList);
m_batch->Begin(commandList);
VertexPositionTexture v1(Vector3(400.f, 150.f, 0.f), Vector2(.5f, 0));
VertexPositionTexture v2(Vector3(600.f, 450.f, 0.f), Vector2(1, 1));
VertexPositionTexture v3(Vector3(200.f, 450.f, 0.f), Vector2(0, 1));
m_batch->DrawTriangle(v1, v2, v3);
m_batch->End();
Build and run to see a simple textured triangle rendered in 2D.
- Since we are rendering with textures, we need to set the descriptor heap for the texture we created above.
- BasicEffect uses heap-based sampler descriptors instead of static root-signature samplers, so we must provide the sampler heap as well. This is being provided by CommonStates in this instance. Since we are not just using static class members, we had to create an object to provide the required heap.
Start by saving rocks_normalmap.dds into your project's directory, and then from the top menu select Project / Add Existing Item.... Select "rocks_normalmap.dds" and click "OK".
In the Game.h file, add the following variable to the bottom of the Game class's private declarations:
Microsoft::WRL::ComPtr<ID3D12Resource> m_normalMap;
Also update the enum Descriptors
to add another value:
enum Descriptors
{
Rocks,
NormalMap,
Count
};
In Game.cpp, update the TODO of CreateDeviceDependentResources:
ResourceUploadBatch resourceUpload(device);
resourceUpload.Begin();
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload, L"rocks.jpg",
m_texture.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_texture.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::Rocks));
DX::ThrowIfFailed(
CreateDDSTextureFromFile(device, resourceUpload,
L"rocks_normalmap.dds",
m_normalMap.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_normalMap.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::NormalMap));
auto uploadResourcesFinished = resourceUpload.End(
m_deviceResources->GetCommandQueue());
uploadResourcesFinished.wait();
In Game.cpp, add to the TODO of OnDeviceLost:
m_normalMap.Reset();
Build and run to make sure this second texture loads fine. Nothing else should be different yet.
Now go back to your Game.h and modify the VertexType
alias we used earlier to use VertexPositionNormalTexture.
using VertexType = DirectX::VertexPositionNormalTexture;
Also change the type of effect. Since we are using a flat 2D triangle, the lighting is not going to be very interesting so we are going to add some simple normal mapping to give the texture some definition.
If you are not familiar with lighting (also known as shading) in the computer graphics sense, you should review some of the basic material on the web or in a standard book. Most of the built-in effects use simple "dot-product" style lighting where the 'light value' is scaled by a value of 0 (not lit) to 1 (fully lit) computed from the angle between the vector to the light (i.e. the inverse of the light direction) and the normal vector at the surface.
std::unique_ptr<DirectX::NormalMapEffect> m_effect;
Then in Game.cpp modify CreateDeviceDependentResources:
...
m_effect = std::make_unique<NormalMapEffect>(device, EffectFlags::None, pd);
m_effect->SetTexture(m_resourceDescriptors->GetGpuHandle(
Descriptors::Rocks), m_states->LinearClamp());
m_effect->SetNormalTexture(m_resourceDescriptors->GetGpuHandle(
Descriptors::NormalMap));
m_effect->EnableDefaultLighting();
m_effect->SetLightDiffuseColor(0, Colors::Gray);
Then in Game.cpp add to Update:
auto time = static_cast<float>(m_timer.GetTotalSeconds());
float yaw = time * 0.4f;
float pitch = time * 0.7f;
float roll = time * 1.1f;
auto quat = Quaternion::CreateFromYawPitchRoll(pitch, yaw, roll);
auto light = XMVector3Rotate(g_XMOne, quat);
m_effect->SetLightDirection(0, light);
In Game.cpp, modify the TODO of Render:
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap(), m_states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_effect->Apply(commandList);
m_batch->Begin(commandList);
VertexPositionNormalTexture v1(Vector3(400.f, 150.f, 0.f), -Vector3::UnitZ, Vector2(.5f, 0));
VertexPositionNormalTexture v2(Vector3(600.f, 450.f, 0.f), -Vector3::UnitZ, Vector2(1, 1));
VertexPositionNormalTexture v3(Vector3(200.f, 450.f, 0.f), -Vector3::UnitZ, Vector2(0, 1));
m_batch->DrawTriangle(v1, v2, v3);
m_batch->End();
Build and run, and you'll see the 2D triangle drawn with dynamic lighting effects.
- The tangent-space normal map used here was generated from a height map using texconv's
-nmap
feature.
texconv rocks_NM_height.dds -nmap l -nmapamp 4
In versions of the toolkit before August 2020, the creation of the normal map effect would be:
m_effect = std::make_unique<NormalMapEffect>(device, EffectFlags::None, pd, false);
Thefalse
on theNormalMapEffect
constructor is because we are not providing a specular map texture. With newer versions of the tool kit, use of a specular map is instead opt-in via theEffectFlags::Specular
.
- For the DirectX Tool Kit normal map effect above, we did not need to provide precomputed per-vertex tangents or bi-tangents. See the Further Reading section of the NormalMapEffect page for details.
Next lesson: Line drawing and anti-aliasing
DirectX Tool Kit docs CommonStates, Effects, EffectPipelineStateDescription, PrimitiveBatch, RenderTargetState, VertexTypes
All content and source code for this package are subject to the terms of the MIT License.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
- Universal Windows Platform apps
- Windows desktop apps
- Windows 11
- Windows 10
- Xbox One
- Xbox Series X|S
- x86
- x64
- ARM64
- Visual Studio 2022
- Visual Studio 2019 (16.11)
- clang/LLVM v12 - v18
- MinGW 12.2, 13.2
- CMake 3.20