Skip to content

Line drawing and anti aliasing

Chuck Walbourn edited this page Apr 26, 2022 · 15 revisions
Getting Started

Here we learn how to render a grid in 3D, and enable multi-sample anti-aliasing (MSAA).

Setup

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.

Drawing a grid

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.

Screenshot of the 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.

Anti-aliasing

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.

Anti-aliased lines

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 with AntialiasedLineEnable set to TRUE and MultisampleEnable set to FALSE. Note also that in Direct3D 12, there's no ScissorEnable 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.

Screenshot of the AA grid

Multisampling

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())));

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 has MultisampleEnable 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.

Screenshot of the MSAA grid

Technical Note

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.

More to explore

  • See the SimpleMSAA12 Win32 | UWP | GDK sample.

  • 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

Further reading

DirectX Tool Kit docs CommonStates, Effects, EffectPipelineStateDescription, PrimitiveBatch, RenderTargetState, VertexTypes
FXAA
SMAA
Spatial anti-alising

For Use

  • Universal Windows Platform apps
  • Windows desktop apps
  • Windows 11
  • Windows 10
  • Xbox One
  • Xbox Series X|S

Architecture

  • x86
  • x64
  • ARM64

For Development

  • Visual Studio 2022
  • Visual Studio 2019 (16.11)
  • clang/LLVM v12 - v18
  • MinGW 12.2, 13.2
  • CMake 3.20

Related Projects

DirectX Tool Kit for DirectX 11

DirectXMesh

DirectXTex

DirectXMath

Tools

Test Suite

Model Viewer

Content Exporter

DxCapsViewer

See also

DirectX Landing Page

Clone this wiki locally