-
Notifications
You must be signed in to change notification settings - Fork 411
Line drawing and anti aliasing
Getting Started |
---|
Here we learn how to render a grid in 3D, and enable multi-sample anti-aliasing (MSAA).
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 addition to the Direct3D objects we used in Simple rendering, this tutorial makes use of 3D transformations using the SimpleMath library.
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):
std::unique_ptr<DirectX::BasicEffect> m_effect;
std::unique_ptr<DirectX::PrimitiveBatch<
DirectX::VertexPositionColor>> m_batch;
DirectX::SimpleMath::Matrix m_world;
DirectX::SimpleMath::Matrix m_view;
DirectX::SimpleMath::Matrix m_proj;
In Game.cpp, add to the TODO of CreateDeviceDependentResources after where you have created m_graphicsMemory
:
m_batch = std::make_unique<PrimitiveBatch<VertexPositionColor>>(device);
RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
m_deviceResources->GetDepthBufferFormat());
EffectPipelineStateDescription pd(
&VertexPositionColor::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullNone,
rtState,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE);
m_effect = std::make_unique<BasicEffect>(device, EffectFlags::VertexColor, pd);
m_world = Matrix::Identity;
We are specifying an effect using lines, not the default which is triangles. More details on this, see the technical note below.
In Game.cpp, add to the TODO of CreateWindowSizeDependentResources:
auto size = m_deviceResources->GetOutputSize();
m_view = Matrix::CreateLookAt(Vector3(2.f, 2.f, 2.f),
Vector3::Zero, Vector3::UnitY);
m_proj = Matrix::CreatePerspectiveFieldOfView(XM_PI / 4.f,
float(size.right) / float(size.bottom), 0.1f, 10.f);
m_effect->SetView(m_view);
m_effect->SetProjection(m_proj);
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->SetWorld(m_world);
m_effect->Apply(commandList);
m_batch->Begin(commandList);
Vector3 xaxis(2.f, 0.f, 0.f);
Vector3 yaxis(0.f, 0.f, 2.f);
Vector3 origin = Vector3::Zero;
constexpr size_t divisions = 20;
for (size_t i = 0; i <= divisions; ++i)
{
float fPercent = float(i) / float(divisions);
fPercent = (fPercent * 2.0f) - 1.0f;
Vector3 scale = xaxis * fPercent + origin;
VertexPositionColor v1(scale - yaxis, Colors::White);
VertexPositionColor v2(scale + yaxis, Colors::White);
m_batch->DrawLine(v1, v2);
}
for (size_t i = 0; i <= divisions; i++)
{
float fPercent = float(i) / float(divisions);
fPercent = (fPercent * 2.0f) - 1.0f;
Vector3 scale = yaxis * fPercent + origin;
VertexPositionColor v1(scale - xaxis, Colors::White);
VertexPositionColor v2(scale + xaxis, Colors::White);
m_batch->DrawLine(v1, v2);
}
m_batch->End();
Build and run to see a 3D grid.
Technical notes: Because of the design of the Direct3D 12 pipeline state object (PSO), you'll note that you can't mix the drawing of points, lines, and triangles/quads with the same effect. Each effect is created for one of those basic primitives, meaning you need more than one pipeline state object to draw them all. Be sure to
End
the batch before applying a new PSO to ensure all pending draws are submitted with the proper active PSO.
Taking a closer look at the grid in the previous screenshot, you can see the lines are a little thin and jagged in places. To make this more visible, in Game.cpp, add to the TODO of Update:
auto time = static_cast<float>(timer.GetTotalSeconds());
m_world = Matrix::CreateRotationY(cosf(time));
Build and run to see the grid spinning, and notice the slight shimmering of the lines--it will be more obvious if you maximize the window size.
There are two approaches to addressing this problem, known as aliasing.
The first is to enable a special anti-aliasing mode specific to line drawing in Direct3D.
In Game.cpp, modify in the TODO section of CreateDeviceDependentResources:
CD3DX12_RASTERIZER_DESC rastDesc(D3D12_FILL_MODE_SOLID,
D3D12_CULL_MODE_NONE, FALSE,
D3D12_DEFAULT_DEPTH_BIAS, D3D12_DEFAULT_DEPTH_BIAS_CLAMP,
D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS, TRUE, FALSE, TRUE,
0, D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF);
EffectPipelineStateDescription pd(
&VertexPositionColor::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
rastDesc,
rtState,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE);
This creates a raster state that is the same as our standard
CullNone
but withAntialiasedLineEnable
set to TRUE andMultisampleEnable
set to FALSE. Note also that in Direct3D 12, there's noScissorEnable
setting as there is in Direct3D 11.
Build and run to see the shimmering of the lines lessen, although they will appear to be a bit thicker than a single pixel.
A second more general solution is to use Multisample anti-aliasing (MSAA) which uses more video memory and pixel-fill performance to achieve higher quality rendering results. In this case, we will make use of 4x MSAA where the render target and the depth buffer will be 4 times larger. MSAA can be used with all primitives, not just lines.
In the Game.h file, add the following variables to the bottom of the Game class's private declarations:
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> m_rtvDescriptorHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> m_dsvDescriptorHeap;
Microsoft::WRL::ComPtr<ID3D12Resource> m_depthStencil;
Microsoft::WRL::ComPtr<ID3D12Resource> m_offscreenRenderTarget;
At the top of Game.cpp after the using
statements, add:
namespace
{
constexpr UINT MSAA_COUNT = 4;
constexpr UINT MSAA_QUALITY = 0;
constexpr DXGI_FORMAT MSAA_DEPTH_FORMAT = DXGI_FORMAT_D32_FLOAT;
}
In Game.cpp, modify the constructor:
m_deviceResources = std::make_unique<DX::DeviceResources>(
DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_UNKNOWN);
We are going to be creating a MSAA depth/stencil buffer, so here we are telling DeviceResources we don't need it to create a depth buffer.
In Game.cpp, add to the TODO section of CreateDeviceDependentResources:
// Create descriptor heaps for MSAA.
D3D12_DESCRIPTOR_HEAP_DESC rtvDescriptorHeapDesc = {};
rtvDescriptorHeapDesc.NumDescriptors = 1;
rtvDescriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
D3D12_DESCRIPTOR_HEAP_DESC dsvDescriptorHeapDesc = {};
dsvDescriptorHeapDesc.NumDescriptors = 1;
dsvDescriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
DX::ThrowIfFailed(device->CreateDescriptorHeap(
&rtvDescriptorHeapDesc,
IID_PPV_ARGS(m_rtvDescriptorHeap.ReleaseAndGetAddressOf())));
DX::ThrowIfFailed(device->CreateDescriptorHeap(
&dsvDescriptorHeapDesc,
IID_PPV_ARGS(m_dsvDescriptorHeap.ReleaseAndGetAddressOf())));
Click here for Xbox development note
Note that due to the use of the 'monolithic' DirectX 12.X Runtime, you will need to use IID_GRAPHICS_PPV_ARGS
instead of IID_PPV_ARGS
for Xbox One XDK / Microsoft GDKX development. See DirectXHelpers.
In Game.cpp, modify in the TODO section of CreateDeviceDependentResources:
RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
MSAA_DEPTH_FORMAT);
rtState.sampleDesc.Count = MSAA_COUNT;
rtState.sampleDesc.Quality = MSAA_QUALITY;
CD3DX12_RASTERIZER_DESC rastDesc(D3D12_FILL_MODE_SOLID,
D3D12_CULL_MODE_NONE, FALSE,
D3D12_DEFAULT_DEPTH_BIAS, D3D12_DEFAULT_DEPTH_BIAS_CLAMP,
D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS, TRUE, TRUE, FALSE,
0, D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF);
EffectPipelineStateDescription pd(
&VertexPositionColor::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
rastDesc,
rtState,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE);
m_effect = std::make_unique<BasicEffect>(device, EffectFlags::VertexColor, pd);
m_world = Matrix::Identity;
This creates a raster state that is the same as our standard
CullNone
which hasMultisampleEnable
set to TRUE. We also have to make sure our render target state in the effect's PSO matches our intended MSAA target settings.
In Game.cpp, add to the TODO section of CreateWindowSizeDependentResources:
auto device = m_deviceResources->GetD3DDevice();
CD3DX12_HEAP_PROPERTIES heapProperties(D3D12_HEAP_TYPE_DEFAULT);
// Create the MSAA depth/stencil buffer.
auto depthStencilDesc = CD3DX12_RESOURCE_DESC::Tex2D(
MSAA_DEPTH_FORMAT,
static_cast<UINT>(size.right),
static_cast<UINT>(size.bottom),
1, // This depth stencil view has only one texture.
1, // Use a single mipmap level
MSAA_COUNT,
MSAA_QUALITY
);
depthStencilDesc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
D3D12_CLEAR_VALUE depthOptimizedClearValue = {};
depthOptimizedClearValue.Format = MSAA_DEPTH_FORMAT;
depthOptimizedClearValue.DepthStencil.Depth = 1.0f;
depthOptimizedClearValue.DepthStencil.Stencil = 0;
DX::ThrowIfFailed(device->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_DEPTH_WRITE,
&depthOptimizedClearValue,
IID_PPV_ARGS(m_depthStencil.ReleaseAndGetAddressOf())
));
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = {};
dsvDesc.Format = MSAA_DEPTH_FORMAT;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2DMS;
device->CreateDepthStencilView(m_depthStencil.Get(), &dsvDesc,
m_dsvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
auto msaaRTDesc = CD3DX12_RESOURCE_DESC::Tex2D(
m_deviceResources->GetBackBufferFormat(),
static_cast<UINT>(size.right),
static_cast<UINT>(size.bottom),
1, // This render target view has only one texture.
1, // Use a single mipmap level
MSAA_COUNT,
MSAA_QUALITY
);
msaaRTDesc.Flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
D3D12_CLEAR_VALUE msaaOptimizedClearValue = {};
msaaOptimizedClearValue.Format = m_deviceResources->GetBackBufferFormat();
memcpy(msaaOptimizedClearValue.Color, Colors::CornflowerBlue, sizeof(float) * 4);
DX::ThrowIfFailed(device->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&msaaRTDesc,
D3D12_RESOURCE_STATE_RESOLVE_SOURCE,
&msaaOptimizedClearValue,
IID_PPV_ARGS(m_offscreenRenderTarget.ReleaseAndGetAddressOf())
));
device->CreateRenderTargetView(m_offscreenRenderTarget.Get(), nullptr,
m_rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());
In Game.cpp, add to the TODO of OnDeviceLost:
m_rtvDescriptorHeap.Reset();
m_dsvDescriptorHeap.Reset();
m_depthStencil.Reset();
m_offscreenRenderTarget.Reset();
In Game.cpp, modify Clear:
Change:
auto rtvDescriptor = m_deviceResources->GetRenderTargetView();
auto dsvDescriptor = m_deviceResources->GetDepthStencilView();
to:
auto rtvDescriptor = m_rtvDescriptorHeap->GetCPUDescriptorHandleForHeapStart();
auto dsvDescriptor = m_dsvDescriptorHeap->GetCPUDescriptorHandleForHeapStart();
In Game.cpp, modify Render as follows:
// Prepare the command list to render a new frame.
m_deviceResources->Prepare(D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RESOLVE_DEST);
auto commandList = m_deviceResources->GetCommandList();
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_offscreenRenderTarget.Get(),
D3D12_RESOURCE_STATE_RESOLVE_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET);
commandList->ResourceBarrier(1, &barrier);
Clear();
...
m_batch->End();
barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_offscreenRenderTarget.Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_RESOLVE_SOURCE);
commandList->ResourceBarrier(1, &barrier);
commandList->ResolveSubresource(m_deviceResources->GetRenderTarget(),
0, m_offscreenRenderTarget.Get(), 0,
m_deviceResources->GetBackBufferFormat());
// Show the new frame.
m_deviceResources->Present(D3D12_RESOURCE_STATE_RESOLVE_DEST);
m_graphicsMemory->Commit(m_deviceResources->GetCommandQueue());
Build and run to see the shimmering of the lines lessen compared to the first version, and is slightly less thickened than when we used the AA line mode.
Note we are making use of the fact that the DeviceResources class methods for Prepare and Present allow us to override the resource barrier states used for the render scene setup and presentation swapchain.
The ability to create an MSAA DXGI swap chain is only supported for the older "bit-blt" style presentation modes, specifically DXGI_SWAP_EFFECT_DISCARD
or DXGI_SWAP_EFFECT_SEQUENTIAL
. The newer "flip" style presentation modes DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL
or DXGI_SWAP_EFFECT_FLIP_DISCARD
required for Universal Windows Platform (UWP) apps and Direct3D 12 doesn't support creating MSAA swap chains--attempts to create a swap chain with SampleDesc.Count
> 1 will fail. Instead, you create your own MSAA render target and explicitly resolve to the DXGI back-buffer for presentation as shown here.
In most real-world rendering pipelines, you do not just resolve the MSAA surface and present the single-sample result. Instead you resolve to another render target, perform various post-processing steps, and then eventually present that result. Therefore the "old-style" of creating an MSAA swapchain is really only useful for 'toy' examples in any case.
See this blog series for more information.
-
The MSAAHelper utility can be used to simplify the implementation of the MSAA rendering above.
-
PrimitiveBatch is ideally suited for drawing debug displays such as visualizing bounding volumes, collision data, etc. For more on this, see DebugDraw.
Next lesson: 3D shapes
DirectX Tool Kit docs CommonStates, Effects, EffectPipelineStateDescription, PrimitiveBatch, RenderTargetState, VertexTypes
FXAA
SMAA
Spatial anti-alising
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