Skip to content

SpriteBatch

Chuck Walbourn edited this page Mar 22, 2023 · 46 revisions
DirectXTK

This is a native Direct3D 12 implementation of the SpriteBatch helper from XNA Game Studio 4 (Microsoft.Xna.Framework.Graphics.SpriteBatch), providing the same functionality and API.

Related tutorial: Sprites and textures, Writing custom shaders

Header

#include <SpriteBatch.h>

Initialization

To create the Pipeline State Object (PSO) used internally, the SpriteBatch class needs a state description provided in SpriteBatchPipelineStateDescription. The class also requires a ResourceUploadBatch to handle uploading static resources.

ResourceUploadBatch resourceUpload(device);

resourceUpload.Begin();

RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
    m_deviceResources->GetDepthBufferFormat());

SpriteBatchPipelineStateDescription pd(rtState);

spriteBatch = std::make_unique<SpriteBatch>(device, resourceUpload, pd);

// Upload the resources to the GPU.
auto uploadResourcesFinished = resourceUpload.End(m_deviceResources->GetCommandQueue());

// Wait for the upload thread to terminate
uploadResourcesFinished.wait();

For exception safety, it is recommended you make use of the C++ RAII pattern and use a std::unique_ptr or std::shared_ptr

Because of the way that DirectX 12 works, if you need a different combination of state you need to create a distinct instance of SpriteBatch. Each SpriteBatch instance has one PSO.

Drawing

The viewport needs to be set explicitly with SetViewport before drawing:

spriteBatch->SetViewport(viewPort);

And the resource descriptor heap for the sprite textures need to be set (see DescriptorHeap for more information):

ID3D12DescriptorHeap* heaps[] = { resourceDescriptors->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);

Then use Draw to submit the work to the command-list:

spriteBatch->Begin(commandList);

spriteBatch->Draw(resourceDescriptors->GetGpuHandle(Descriptors::MySpriteTexture),
                  GetTextureSize(tex),
                  XMFLOAT2(x, y));

spriteBatch->End();

The Draw method has many overloads with parameters controlling:

  • Specify screen position as XMFLOAT2, XMVECTOR or RECT (in units of pixels if using default transformation)
  • Optional source rectangle (in units of pixels) for drawing just part of a texture (i.e. a sprite sheet)
  • Tint color (defaults to 1,1,1,1 which means 'no tinting')
  • Rotation (in radians)
  • Origin point (in units of pixels if using the default transformation): position, scaling and rotation are relative to this point. This defaults to (0,0) which is the upper-left of the sprite image. If you want to use the center of the sprite, for example, set the origin to (width / 2), (height / 2).
  • Scale
  • SpriteEffects enum (for horizontal or vertical mirroring)
  • Layer depth (for sorting)

The source rectangle is used to compute the texture coordinates for the sprite on the texture. The position, rotation, origin, and scale are used to compute the sprite triangle's drawing location.

To provide flexibility, setting the proper descriptor heaps to render with via SetDescriptorHeaps is left to the caller. You can create as many heaps as you wish in your application, but remember that you can have only a single texture descriptor heap and a single sampler descriptor heap active at a given time.

Sorting

The second parameter to Begin is a SpriteSortMode enum. For most efficient rendering, use SpriteSortMode_Deferred (which batches up sprites, then submits them all to the GPU during the End call), and manually draw everything in texture order. If it is not possible to draw in texture order, the second most efficient approach is to use SpriteSortMode_Texture, which will automatically sort on your behalf.

When drawing scenes with multiple depth layers, SpriteSortMode_BackToFront or SpriteSortMode_FrontToBack will sort by the layerDepth parameter specified to each Draw call.

SpriteSortMode_Immediate disables all batching, submitting a separate Direct3D draw call for each sprite. This is expensive, but convenient in rare cases when you need to set shader constants differently per sprite.

Multiple SpriteBatch instances are lightweight. It is reasonable to create several, Begin them at the same time with different sort modes, submit sprites to different batches in arbitrary orders as you traverse a scene, then End the batches in whatever order you want these groups of sprites to be drawn.

SpriteBatch and SpriteSortMode
SpriteBatch sorting part 2
Return of the SpriteBatch: sorting part 3
SpriteSortMode.Immediate in XNA Game Studio 4.0

Alpha blending

Alpha blending defaults to using premultiplied alpha. To make use of 'straight' alpha textures, provide a blend state when creating the SpriteBatch:

SpriteBatchPipelineStateDescription pd(
    rtState,
    &CommonStates::NonPremultiplied);

spriteBatch = std::make_unique<SpriteBatch>(device, resourceUpload, pd);

SpriteFont is designed to work with premultiplied alpha, so if you need to draw both text and straight-alpha content you should have at least two instances of SpriteBatch. Be sure to End one before starting another one using the same command-list or your sort order will be incorrect. Alternatively, you can use MakeSpriteFont to generate 'straight-alpha' .spritefont files using the /NoPremultiply switch.

Texture sampler

If you do not provide a samplerDescriptor to SpriteBatchPipelineStateDescription, the default is to use a static sampler set to linear filtering with clamp u/v addressing. If you do provide a samplerDescriptor, be sure to set the heap to the command-list when drawing. CommonStates provides heap sampler descriptors which can be used for this purpose:

auto sampler = states->AnisotropicWrap();

SpriteBatchPipelineStateDescription pd(
     rtState, nullptr, nullptr, nullptr, &sampler);

spriteBatch = std::make_unique<SpriteBatch>(device, resourceUpload, pd);

When drawing with this sprite batch:

ID3D12DescriptorHeap* heaps[] = { resourceDescriptors->Heap(), states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);

spriteBatch->Begin(commandList);

spriteBatch->Draw(resourceDescriptors->GetGpuHandle(Descriptors::MySpriteTexture),
                  GetTextureSize(tex),
                  XMFLOAT2(x, y));

spriteBatch->End();

If created with a heap sampler, you can reassign a new sampler with an alternative version of Begin:

ID3D12DescriptorHeap* heaps[] = { resourceDescriptors->Heap(), states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);

spriteBatch->Begin(commandList, states->PointClamp());

spriteBatch->Draw(resourceDescriptors->GetGpuHandle(Descriptors::MySpriteTexture),
                  GetTextureSize(tex),
                  XMFLOAT2(x, y));

spriteBatch->End();

The new sampler is retained for all future draws until another Begin is called to reassign it again.

Pixel centers

In Direct3D 12, the 'pixel center' is at the '.5, .5' coordinate rather than Direct3D 9's integer coordinates being considered the pixel center. If doing 'nearest-point' filtering (such as CommonStates::PointClamp or CommonStates::PointWrap), then the sampled pixel is going to be the one that touces the pixel center based on the sprite location. This means that the position has to move from, say, "1.5" to "1.6" to get the pixel sample to change.

If you want to achieve something more like Direct3D 9's integer coordinates are pixel centers, then you should use DirectMath (or SimpleMath) to adjust your positions provided to SpriteBatch accordingly.

XMVECTOR pos = XMLoadFloat2(&m_position);

static const XMVECTORF32 s_half = { { { 0.5f, 0.5f, 0.f, 0.f } } };

pos = XMVectorAdd(XMVectorTruncate(pos), s_half);

m_spriteBatch->( ..., pos, ...)

Custom render states

By default SpriteBatch uses premultiplied alpha blending, no depth buffer, counter clockwise culling, and linear filtering with clamp texture addressing. When creating the SpriteBatch, you can control the PSO settings via SpriteBatchPipelineStateDescription

SpriteBatchPipelineStateDescription(
    const RenderTargetState& renderTarget,
    const D3D12_BLEND_DESC* blend,
    const D3D12_DEPTH_STENCIL_DESC* depthStencil,
    const D3D12_RASTERIZER_DESC* rasterizer,
    const D3D12_GPU_DESCRIPTOR_HANDLE* samplerDescriptor)

Pass nullptr for any parameters that should use their default value.

This means that if you want to draw sprites with more than one state configuration, you need more than one instance of SpriteBatch and must Begin/End each batch independently.

Custom shaders

You can provide your own shaders when creating the SpriteBatch by overridding defaults for SpriteBatchPipelineStateDescription.customRootSignature, customVertexShader, and customPixelShader. Your custom root signature should have the following entries at root parameter index 0 & 1 which are bound by SpriteBatch.

For the 'default' sampler (i.e. use a static sampler):

DescriptorTable ( SRV(t0), visibility = SHADER_VISIBILITY_PIXEL ),
CBV(b0),
StaticSampler(s0, /*...*/, visibility = SHADER_VISIBILITY_PIXEL )

For the heap-based sampler, use:

DescriptorTable ( SRV(t0), visibility = SHADER_VISIBILITY_PIXEL ),
CBV(b0),
DescriptorTable ( Sampler(s0), visibility = SHADER_VISIBILITY_PIXEL )

The format of the constant buffer at b0 is as follows:

cbuffer Parameters : register(b0)
{
    row_major float4x4 MatrixTransform;
};

To write a custom sprite batch shader in HLSL, make sure it makes the following signature:

void SpriteVertexShader(inout float4 color    : COLOR0,
                        inout float2 texCoord : TEXCOORD0,
                        inout float4 position : SV_Position)
{
    // TODO
}


float4 SpritePixelShader(float4 color    : COLOR0,
                         float2 texCoord : TEXCOORD0) : SV_Target0
{
    // TODO
}

Custom transform

SpriteBatch::Begin also has a transformMatrix parameter, which can be used for global transforms such as scaling or translation of an entire scene. It otherwise defaults to matrix identity.

XMMATRIX matrix = ...;
spriteBatch->Begin(commandList, SpriteSortMode_Deferred, matrix);

The full transformation depends on the orientation setting and/or the viewport setting for the device. The viewport is obtained from the SpriteBatch::SetViewport method, and then used to construct a view matrix based on the setting provided by theSpriteBatch::SetRotation method.

// DXGI_MODE_ROTATION_ROTATE90
finalMatrix =
    matrix * XMMATRIX(0, -yScale, 0, 0, -xScale, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1);

// DXGI_MODE_ROTATION_ROTATE270
finalMatrix =
    matrix * XMMATRIX(0, yScale, 0, 0, xScale, 0, 0, 0, 0, 0, 1, 0, -1, -1, 0, 1);

//DXGI_MODE_ROTATION_ROTATE180
finalMatrix =
    matrix * XMMATRIX(-xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, 1, 0, 1, -1, 0, 1);

// DXGI_MODE_ROTATION_IDENTITY (the default)
finalMatrix =
    matrix * XMMATRIX(xScale, 0, 0, 0, 0, -yScale, 0, 0, 0, 0, 1, 0, -1, 1, 0, 1);

// DXGI_MODE_ROTATION_UNSPECIFIED
finalMatrix = matrix;

Orientation

For phones, laptops, and tablets the orientation of the display can be changed by the user. For Windows Store apps, DirectX applications are encouraged to handle the rotation internally rather than relying on DXGI's auto-rotation handling.

Using the DirectX starting template for Universal Windows Platform (UWP) apps, you will want to add to your CreateWindowSizeDependentResources method:

spriteBatch->SetRotation( m_deviceResources->ComputeDisplayRotation() );

In Common\DeviceResources.h with the standard DirectX UWP template, you need to make ComputeDisplayRotation a public function instead of being private.

When using the directx-vs-templates DeviceResources for UWP, use:

spriteBatch->SetRotation( m_deviceResources->GetRotation() );

Threading model

Creation is fully asynchronous, so you can instantiate multiple SpriteBatch instances at the same time on different threads. Each SpriteBatch instance only supports drawing from one thread at a time, but you can simultaneously submit sprites on multiple threads if you create a separate SpriteBatch instance per command-list.

Work Submission in Direct3D 12

State management

SpriteBatch set it's own root signature and Pipeline State Object (PSO).

The SpriteBatch class assumes you've already set the Render Target view, Depth Stencil view, Viewport, ScissorRects, and Descriptor Heaps (for textures and/or custom samplers), to the command-list provided to Begin.

It is critical that you explicitly call SetViewport to provide the viewport as it cannot be 'read' from the Direct3D 12 command list.

Further reading

SpriteBatch and BasicEffect for C++ Direct3D 11

Premultiplied alpha
Premultiplied alpha and image composition
Premultiplied alpha in XNA Game Studio 4.0

SpriteBatch and custom blend modes

Is SpriteBatch Turing complete?

For Use

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

For Development

  • Visual Studio 2022
  • Visual Studio 2019 (16.11)
  • clang/LLVM v12 - v16
  • 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