-
Notifications
You must be signed in to change notification settings - Fork 409
Sprites and textures
Getting Started |
---|
In this lesson, we will cover the basics of creating a texture from a bitmap file, and then rendering it using a 2D sprite with various drawing options.
First create a new project using the instructions from the previous lessons: Using DeviceResources and Adding the DirectX Tool Kit which we will use for this lesson.
A sprite is a bitmap rendered at some location on the screen. For Direct3D, this requires making use of:
-
A committed resource which is the texture containing the bitmap image pixel data for rendering.
-
The shader resource view descriptor created in a resource descriptor heap which describes the properties of the texture resource.
-
A sampler which describes how the GPU should handle various aspects of reading the texture data for rendering (such as image filtering and/or tiling of the bitmap).
This can be a static sampler in the root signature or a heap sampler referenced in a sampler descriptor heap.
-
Additional Direct3D objects are also required for drawing (vertex buffer, index buffer, pipeline state object, and root signature), but in this tutorial these are handled by SpriteBatch.
-
Some additional Direct3D objects are also required for uploading the texture data (upload heap committed resource, fence, command allocator, and command list) but in this tutorial these are handled by ResourceUploadBatch.
-
Finally, some 'in-flight' memory related to constant buffer views and dynamic vertex buffers are also required, and these are managed by GraphicsMemory.
Start by saving cat.png into your new project's directory, and then from the top menu select Project / Add Existing Item.... Select "cat.png" and click "OK".
In the Game.h file, add the following variable 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::DescriptorHeap> m_resourceDescriptors;
Microsoft::WRL::ComPtr<ID3D12Resource> m_texture;
enum Descriptors
{
Cat,
Count
};
In Game.cpp, add to the TODO of CreateDeviceDependentResources after where you have created m_graphicsMemory
:
m_resourceDescriptors = std::make_unique<DescriptorHeap>(device,
Descriptors::Count);
ResourceUploadBatch resourceUpload(device);
resourceUpload.Begin();
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload, L"cat.png",
m_texture.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_texture.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::Cat));
auto uploadResourcesFinished = resourceUpload.End(
m_deviceResources->GetCommandQueue());
uploadResourcesFinished.wait();
In Game.cpp, add to the TODO of OnDeviceLost where you added m_graphicsMemory.reset()
:
m_texture.Reset();
m_resourceDescriptors.reset();
Build and run the application which will still not be displaying anything but the cornflower blue window, but will have a texture loaded.
Click here for troubleshooting advice
If you get a runtime exception, then you may have the "cat.png" in the wrong folder, have modified the "Working Directory" in the "Debugging" configuration settings, or otherwise changed the expected paths at runtime of the application. You should set a break-point on CreateWICTextureFromFile
and step into the code to find the exact problem.
In DirectX 12 the application is responsible for managing video memory, including uploading resources from 'shared memory' to 'dedicated video memory'. The ResourceUploadBatch class is a helper for managing this process. When the End method is called, it creates a C++11 "future" which allows the upload process to happen potentially on another thread. You must wait on the C++11 future at the end of CreateDeviceDependentResources
to ensure it completes the upload before continuing. For more, see Wikipedia and cppreference.com.
In the Game.h file, add the following variables to the bottom of the Game class's private declarations:
std::unique_ptr<DirectX::SpriteBatch> m_spriteBatch;
DirectX::SimpleMath::Vector2 m_screenPos;
DirectX::SimpleMath::Vector2 m_origin;
In Game.cpp, modify TODO of CreateDeviceDependentResources to include after resourceUpload.Begin
and before the resourceUpload.End
:
RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
m_deviceResources->GetDepthBufferFormat());
SpriteBatchPipelineStateDescription pd(rtState);
m_spriteBatch = std::make_unique<SpriteBatch>(device, resourceUpload, pd);
XMUINT2 catSize = GetTextureSize(m_texture.Get());
m_origin.x = float(catSize.x / 2);
m_origin.y = float(catSize.y / 2);
In Game.cpp, add to the TODO of CreateWindowSizeDependentResources:
auto viewport = m_deviceResources->GetScreenViewport();
m_spriteBatch->SetViewport(viewport);
auto size = m_deviceResources->GetOutputSize();
m_screenPos.x = float(size.right) / 2.f;
m_screenPos.y = float(size.bottom) / 2.f;
If using the UWP template, you should also add
m_spriteBatch->SetRotation(m_deviceResources->GetRotation());
to handle display orientation changes.
In Game.cpp, add to the TODO of OnDeviceLost:
m_spriteBatch.reset();
In Game.cpp, add to the TODO of Render:
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_spriteBatch->Begin(commandList);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Cat),
GetTextureSize(m_texture.Get()),
m_screenPos, nullptr, Colors::White, 0.f, m_origin);
m_spriteBatch->End();
Build and run, and you should get the following screen:
You'll note that setting the descriptor heap is left to the caller. This allows the application to use their own heaps instead of the
DescriptorHeap
class. You can freely mix and match heaps in the application, but remember that you can have only a single texture descriptor heap and a single sampler descriptor heap active at any given time. To change active heaps, you shouldEnd
the current batch, callSetDescriptorHeaps
, and then callBegin
to start a new one.
One thing you should notice is that the edges of the cat look strange with a bit of white outline. The problem here is that the cat.png
file's alpha channel is straight alpha (i.e. the pixels are of the form (R,G,B,A)
). The default behavior of SpriteBatch, however, is to assume you are using premultiplied alpha (i.e. the pixels are of the form (R*A, G*A, B*A, A)
). There are many reasons why using premultiplied alpha is superior, but for now we can fix this mismatch by changing our use of SpriteBatch to use straight alpha blending instead by supplying our own ID3D11BlendState
object. We'll make use of the CommonStates class to provide one of the built-in blend state objects.
In Game.cpp, modify the creation of SpriteBatchPipelineStateDescription
in CreateDeviceDependentResources:
SpriteBatchPipelineStateDescription pd(rtState,
&CommonStates::NonPremultiplied);
Build and run again, and you'll get a nice clean cat:
Rather than use a PNG
and the Windows Imaging Component (WIC) to load the texture, a more efficient thing for us to do is to make use of a DDS
file instead. A DDS
file is a container for all kinds of Direct3D resources including 1D and 2D textures, cubemaps, volume maps, arrays of 1D or 2D textures or cubemaps each optionally with mipmaps. It can contain a wide-array of pixel formats and hardware-supported 'block-compression' schemes to save on video memory usage at runtime.
Visual Studio has a built-in system for converting images to DDS as part of the build process, which you can read about here.
For this tutorial, we will instead make of use of the DirectXTex texconv command-line tool.
- Download the Texconv.exe from the DirectXTex site save the EXE into your project's folder.
- Open a Command Prompt and then change to your project's folder.
Then run the following command-line:
texconv cat.png -pmalpha -m 1 -f BC3_UNORM
Then from the top menu in Visual Studio select Project / Add Existing Item.... Select cat.dds and click "OK".
Now will return to Game.cpp in the CreateDeviceDependentResources and change our use of CreateWICTextureFromFile
to CreateDDSTextureFromFile
:
DX::ThrowIfFailed(
CreateDDSTextureFromFile(device, resourceUpload, L"cat.dds",
m_texture.ReleaseAndGetAddressOf()));
Note that since we used the option -pmalpha
, we should also make sure we change back using premultipled alpha because our "cat.dds" has premultiplied alpha in it.
In Game.cpp, modify SpriteBatchPipelineStateDescription
in CreateDeviceDependentResources:
SpriteBatchPipelineStateDescription pd(rtState);
Build and run we are rendering our 'clean' cat with premultiplied alpha:
- The switch
-pmalpha
causes the texconv command-line tool to convert the image to premultiplied alpha before saving the.dds
file. This assumes the source image is in straight-alpha. - The switch
-m 1
disables the generation of mipmaps for the image. By default, the tool generates a full set of mipmaps when converting to a.dds
, but since our source image is not a power of two in width & height, it also generates a warning message about use with feature level 9.x devices. For standard sprites, we typically do not make use of mipmaps. For DirectX 12, you can also use-fl 11.0
to remove the warning. - The switch
-f BC3_UNORM
selects theDXGI_FORMAT_BC3_UNORM
format for the resulting.dds
file. In combination with the-pmalpha
switch, this results in the "DXT4" block-compression format being used.
Now that we have our cat rendering, we can start to animate it. Here's a simple rotation where we are using the cosf function to give us a time-varying value from -1 to 1.
In Game.cpp, modify the TODO of Render:
float time = float(m_timer.GetTotalSeconds());
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_spriteBatch->Begin(commandList);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Cat),
GetTextureSize(m_texture.Get()),
m_screenPos, nullptr, Colors::White, cosf(time) * 4.f, m_origin);
m_spriteBatch->End();
Build and run to see the cat spinning.
We can scale a sprite's size as well. Again, we are using cosf to give us a time-varying value between -1 and 1.
In Game.cpp, modify the TODO of Render:
float time = float(m_timer.GetTotalSeconds());
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_spriteBatch->Begin(commandList);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Cat),
GetTextureSize(m_texture.Get()),
m_screenPos, nullptr, Colors::White, 0.f, m_origin, cosf(time) + 2.f);
m_spriteBatch->End();
Build and run to see the cat growing and shrinking.
We can modify the color of the sprite with a tint as well:
In Game.cpp, modify the TODO of Render:
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_spriteBatch->Begin(commandList);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Cat),
GetTextureSize(m_texture.Get()),
m_screenPos, nullptr, Colors::Green, 0.f, m_origin);
m_spriteBatch->End();
Build and run to see a green-tinged cat.
With the optional source-rectangle parameter, we can tile a sprite.
In the Game.h file, add the following variables to the bottom of the Game class's private declarations:
RECT m_tileRect;
std::unique_ptr<DirectX::CommonStates> m_states;
In the Game.cpp file, add to TODO section of CreateDeviceDependentResources:
m_states = std::make_unique<CommonStates>(device);
modify the creation of SpriteBatchPipelineStateDescription
:
auto sampler = m_states->LinearWrap();
SpriteBatchPipelineStateDescription pd(
rtState, nullptr, nullptr, nullptr, &sampler);
and then change
m_origin.x = float(catSize.x / 2);
m_origin.y = float(catSize.y / 2);
to
m_origin.x = float(catSize.x * 2);
m_origin.y = float(catSize.y * 2);
m_tileRect.left = catSize.x * 2;
m_tileRect.right = catSize.x * 6;
m_tileRect.top = catSize.y * 2;
m_tileRect.bottom = catSize.y * 6;
In Game.cpp, add to the TODO of OnDeviceLost:
m_states.reset();
In the Game.cpp file, modify in the TODO section of Render:
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap(), m_states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_spriteBatch->Begin(commandList);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Cat),
GetTextureSize(m_texture.Get()),
m_screenPos, &m_tileRect, Colors::White, 0.f, m_origin);
m_spriteBatch->End();
Build and run to see the sprite as an array of 4x4 cats.
By default SpriteBatch
uses a static sampler that is set to 'linear clamp'. In order to do the tiling, we had to override that setting by providing a descriptor-heap sampler instead. The CommonStates object provides a descriptor heap with pre-defined sampler descriptors that can be used for this purpose.
CommonStates also provides static methods for getting the equivalent 'static sampler' description when setting up custom root signatures such as
StaticLinearWrap()
. Using these methods doesn't require making theCommonStates
descriptor heap 'active'.
Using the optional destination rectangle instead of a 2D position, we can stretch a sprite.
In the Game.h file, add the following variable to the bottom of the Game class's private declarations:
RECT m_stretchRect;
In the Game.cpp file, add to the TODO section of CreateWindowSizeDependentResources:
m_stretchRect.left = size.right / 4;
m_stretchRect.top = size.bottom / 4;
m_stretchRect.right = m_stretchRect.left + size.right / 2;
m_stretchRect.bottom = m_stretchRect.top + size.bottom / 2;
In the Game.cpp file, modify in the TODO section of Render:
m_spriteBatch->Begin(commandList);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Cat),
GetTextureSize(m_texture.Get()),
m_stretchRect, nullptr, Colors::White);
m_spriteBatch->End();
Build and run to see the sprite blown up
Our last exercise for this lesson is rendering a sprite as a full background image. Start by saving sunset.jpg to your project directory, and then from the top menu select Project / Add Existing Item.... Select "sunset.jpg" and click "OK".
In the Game.h file, add the following variables to the bottom of the Game class's private declarations:
RECT m_fullscreenRect;
Microsoft::WRL::ComPtr<ID3D12Resource> m_background;
and change
enum Descriptors
{
Cat,
Count
};
to
enum Descriptors
{
Cat,
Background,
Count
};
In Game.cpp, add to the TODO of CreateDeviceDependentResources to include after resourceUpload.Begin
and before the resourceUpload.End
:
DX::ThrowIfFailed(
CreateWICTextureFromFile(device, resourceUpload, L"sunset.jpg",
m_background.ReleaseAndGetAddressOf()));
CreateShaderResourceView(device, m_background.Get(),
m_resourceDescriptors->GetCpuHandle(Descriptors::Background));
In Game.cpp, add to the TODO of CreateWindowSizeDependentResources:
m_fullscreenRect = m_deviceResources->GetOutputSize();
and then modify the m_origin
initialization back to:
m_origin.x = float(catSize.x / 2);
m_origin.y = float(catSize.y / 2);
In Game.cpp, add to the TODO of OnDeviceLost:
m_background.Reset();
In Game.cpp, modify the TODO section of Render to be:
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap(), m_states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
m_spriteBatch->Begin(commandList);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Background),
GetTextureSize(m_background.Get()),
m_fullscreenRect);
m_spriteBatch->Draw(m_resourceDescriptors->GetGpuHandle(Descriptors::Cat),
GetTextureSize(m_texture.Get()),
m_screenPos, nullptr, Colors::White, 0.f, m_origin);
m_spriteBatch->End();
Build and run to see our cat drawing over a sunset background.
If we were only drawing the background (i.e. a single 'sprite' that fills an entire viewport), then using BasicPostProcess's Copy operation is faster than SpriteBatch
.
-
SpriteBatch can be used to draw "filled color rectangles" extremely easily. Just use a 1x1 white texture, and draw it as a sprite with optional tinting. This allows you to easily batch-up solid rectangles with other sprite drawing as well.
-
The various techniques demonstrated in the DirectX 11 tutorials all apply to DirectX 12
SpriteBatch
as well such as animated sprites, scrolling backgrounds, and sprite sheets. -
SpriteBatch can perform sorting of the sprites in a number of different modes. We've used the default sortMode of
SpriteSortMode_Deferred
forBegin
. If drawing with lots of different textures, you should trySpriteSortMode_Texture
. If you are making use of the layerDepth parameter to sort your sprites correctly, useSpriteSortMode_BackToFront
orSpriteSortMode_FrontToBack
. For all these modes, the actual drawing does not happen until you callEnd
--or you exceed the internal vertex buffer size. If you useSpriteSortMode_Immediate
, then the sprite is drawn as soon as you callDraw
. -
SpriteBatch can 'flip' the texture image at runtime using
SpriteEffects_FlipHorizontally
,SpriteEffects_FlipVertically
, orSpriteEffects_FlipBoth
. The default effects parameter forDraw
isSpriteEffects_None
. -
When drawing lots of different sprites, having to load hundreds of individual texture files is inefficient and potentially wastes video memory. One solution is to pack the sprite images into a single (or a few) textures and then render parts of them using sourceRectangle for
Draw
. -
The SpriteBatch class has a 'rotation mode' setting which can be used to handle device orientation changes for Universal Windows Platform (UWP) apps, or just for special effects. You call SetRotation with one of the
DXGI_MODE_ROTATION
settings which controls the final view transformation to flip the output an extra 90 degrees, 180 degrees, or 270 degrees. It defaults to no extra rotation withDXGI_MODE_ROTATION_IDENTITY
. -
The transformationMatrix parameter to
Begin
lets you provide a custom transformation for the sprite renderering. This is combined with the viewport-based final view transform, but if you useSetRotation( DXGI_MODE_ROTATION_UNSPECIFIED )
this will be disabled. The default value for this parameter is the identity matrix. -
You can use custom texture sampler states for special effects utilizing texture addressing. You can also achieve a 'pixel art' look by using
CommonStates::PointClamp
. -
Since all DirectX 12 hardware is at least Hardware Feature Level 11.0, you can count on BC4, BC5, BC6, and BC7 compression format support, as well as texture and cubemap arrays, for your texture resources.
Next lesson: Drawing text
DirectX Tool Kit docs CommonStates, DescriptorHeap, DDSTextureLoader, RenderTargetState, ResourceUploadBatch, SpriteBatch, WICTextureLoader
Direc3D 11 Textures and Block Compression
Premultiplied alpha
Premultiplied alpha and image composition
Premultiplied alpha in XNA Game Studio 4.0
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 [email protected] 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