-
Notifications
You must be signed in to change notification settings - Fork 409
GeometricPrimitive
DirectXTK |
---|
This is a helper for drawing simple geometric shapes including texture coordinates and surface normals.
- Box/Cube
- Cone
- Cylinder
- Dodecahedron
- Icosahedron
- Octahedron
- Sphere (uv or geodesic)
- Teapot (a.k.a. the Utah teapot)
- Tetrahedron
- Torus
Related tutorial: 3D shapes
#include <GeometricPrimitive.h>
The GeometryPrimitive class must be created from a factory method..
std::unique_ptr<GeometricPrimitive> shape;
shape = GeometricPrimitive::CreateTeapot();
For exception safety, the factory functions return a std::unique_ptr
.
-
CreateBox( const XMFLOAT3& size): Creates a box with a non-uniform size.
-
CreateCone( float diameter = 1, float height = 1, size_t tessellation = 32): Creates a cone of a given height, diameter, and tessellation factor.
-
CreateCube( float size = 1): Creates a cube (also known as a hexahedron) of the given size.
-
CreateCylinder( float height = 1, float diameter = 1, size_t tessellation = 32): Creates a cylinder of given height, diameter, tessellation factor.
-
CreateDodecahedron( float size = 1): Creates a dodecahedron of a given size.
-
CreateGeoSphere( float diameter = 1, size_t tessellation = 3): Creates a geodesic sphere with the given diameter and tessellation factor.
-
CreateIcosahedron( float size = 1): Creates a icosahedron of a given size.
-
CreateOctahedron( float size = 1): Creates a octahedron of a given size.
-
CreateSphere( float diameter = 1, size_t tessellation = 16): Creates a uv-sphere of given diameter with the given tessellation factor.
-
CreateTeapot( float size = 1, size_t tessellation = 8): Creates the Utah Teapot of a given size and tessellation factor.
-
CreateTetrahedron( float size = 1): Creates a tetrahedron of given size.
-
CreateTorus( float diameter = 1, float thickness = 0.333f, size_t tessellation = 32): Creates a torus of given diameter, thickness, and tessellation factor.
-
GeometricPrimitive::VertexType is an alias for VertexPositionNormalTexture
-
GeometricPrimitive::VertexCollection is an alias for
std::vector<VertexType>
. -
GeometricPrimitive::IndexCollection is an alias for
std::vector<uint16_t>
.
The factory functions place the vertex and index buffer data needed to render into an upload heap managed by GraphicsMemory which can be used to render directly. This is equivalent to having the VBs/IBs in D3D11_USAGE_DYNAMIC
memory. For better rendering performance, you can also use this upload heap memory to initialize VB/IB resources that are equivalent to D3D11_USAGE_DEFAULT
memory using ResourceUploadBatch with the LoadStaticBuffers method:
ResourceUploadBatch resourceUpload(device);
resourceUpload.Begin();
model->LoadStaticBuffers(device, resourceUpload);
// Can be combined into a single batch with other uploads of textures or
// resources from other primitives
auto uploadResourcesFinished = resourceUpload.End(m_deviceResources->GetCommandQueue());
uploadResourcesFinished.wait();
When using Direct command queues with the ResourceUploadBatch, the static VB/IB resources are in the proper state for drawing after calling LoadStaticBuffers
.
For Copy and Compute command queues, the static VB/IB resources will be left in other states supported by those command list types. To render they need to be in the proper state. With Windows PC, common state promotion will typically fix this up. For Xbox One where this feature is optional or for other usage scenarios, the Transition method allows you to insert resource barriers for transitioning the resource state of the static VB/IB resources after they have been uploaded.
// If using a copy queue for the upload, both resources are in the
// D3D12_RESOURCE_STATE_COPY_DEST state
shape->Transition(commandList,
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_INDEX_BUFFER);
// If using a compute queue for the upload, the VB is in the correct state, but
// the IB is set to D3D12_RESOURCE_STATE_COPY_DEST
shape->Transition(commandList,
D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_INDEX_BUFFER);
Before you can draw, you need an effect instance or provide your own root signature, Pipeline State Object (PSO), etc.
RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
m_deviceResources->GetDepthBufferFormat());
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullCounterClockwise,
rtState);
effect= std::make_unique<BasicEffect>(device,
EffectFlags::PerPixelLighting
| EffectFlags::Texture, psd);
effect->EnableDefaultLighting();
effect->SetTexture(resourceDescriptors->GetGpuHandle(Descriptors::MyTexture),
states->LinearWrap());
The draw transformations are set on the effect before drawing:
effect->SetProjection(projection);
effect->SetView(view);
effect->SetWorld(world);
Then draw the shape using the effect:
ID3D12DescriptorHeap* heaps[] = { resourceDescriptors->Heap(), states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
effect->Apply(commandList);
shape->Draw(commandList);
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.
To draw the geometry as a wireframe, use an effect created with this state description:
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::Wireframe,
rtState);
For alpha blending, you typically use CommonStates::DepthRead
rather than the normal CommonStates::DepthDefault
. To indicate the use of ‘premultiplied’ alpha blending modes, use CommonStates::AlphaBlend
.
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::AlphaBlend,
CommonStates::DepthRead,
CommonStates::CullCounterClockwise,
rtState);
If using 'straight' alpha for textures, use CommonStates::NonPremultiplied
.
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::NonPremultiplied,
CommonStates::DepthRead,
CommonStates::CullCounterClockwise,
rtState);
If you want to render using your own custom shaders, just set the appropriate state before drawing the geometric primitive:
ID3D12DescriptorHeap* heaps[] = { … };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
commandList->SetGraphicsRootSignature(m_rootSignature.Get());
commandList->SetPipelineState(m_pipelineState.Get());
shape->Draw(commandList);
Geometry is created in a specific winding order, typically using the standard counter-clockwise winding common in graphics (i.e. CommonStates::CullCounterClockwise
). The choice of viewing handedness (right-handed vs. left-handed coordinates) is largely a matter of preference and convenience, but it impacts how the geometry is built.
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullCounterClockwise,
rtState);
These geometric primitives (based on the XNA Game Studio conventions) use right-handed coordinates. They can be used with left-handed coordinates by setting the rhcoords parameter on the factory methods to 'false' to reverse the winding ordering (the parameter defaults to 'true').
For a left-handed view system:
shape = GeometricPrimitive::CreateTeapot( 1.f, 8, false ) );
Alternatively, you can instead reverse the winding order for the culling although this can potentially have the ‘flipped in U’ texture problem.
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullClockwise,
rtState);
Using the wrong value for rhcoords for your viewing setup will result in the objects looking 'inside out'._
Lastly you can choose to render with back-face culling disabled, but this generally results in less performance.
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullNone,
rtState);
The rendering setup assumes you are using a standard z-buffer. If have set up your pipeline for reverse zbuffer rendering, be sure to use the appropriate depth/stencil state:
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::Opaque,
CommonStates::DepthReverseZ,
CommonStates::CullCounterClockwise,
rtState);
EffectPipelineStateDescription psd(
&GeometricPrimitive::VertexType::InputLayout,
CommonStates::AlphaBlend,
CommonStates::DepthReadReverseZ,
CommonStates::CullCounterClockwise,
rtState);
These geometric primitives are intended for view from the 'outside' for efficient back-face culling. However, both spheres and boxes are commonly used to form 'skyboxes' for backgrounds. To support this, you set the rhcoords parameter backwards for your view coordinates, and then set invertn to true.
For a right-handed view system:
sky = GeometricPrimitive::CreateBox( XMFLOAT3(10,10,10), false, true);
sky = GeometricPrimitive::CreateSphere( 100.f, false, true);
For a left-handed view system:
sky = GeometricPrimitive::CreateBox( XMFLOAT3(10,10,10), true, true);
sky = GeometricPrimitive::CreateSphere( 100.f, true, true);
There are equivalent static methods for each of the factory methods that return the vertex and index buffer data as std::vector
. These values can be modified, and then used to create a customized geometric primitive or drawn through some other mechanism.
GeometricPrimitive::VertexCollection vertices;
GeometricPrimitive::IndexCollection indices;
GeometricPrimitive::CreateBox( vertices, indices,
XMFLOAT3(1.f/2.f, 2.f/2.f, 3.f/2.f) );
// Tile the texture in a 5x5 grid
for( auto it : vertices )
{
it->textureCoordinate.x *= 5.f;
it->textureCoordinate.y *= 5.f;
}
customBox = GeometricPrimitive::CreateCustom( vertices, indices ) );
You can also use this 'two-stage' creation of the geometric primitive to compute a bounding volume from
vertices
, although for many geometric primitives (i.e. sphere, box, etc.) you can directly create the bounding volume from the same parameters
If you create a NormalMapEffect, PBREffect, or DebugEffect effect that supports GPU instancing, you can use the following input layout in combination with the DrawInstanced method:
static const D3D12_INPUT_ELEMENT_DESC s_InputElements[] =
{
// GeometricPrimitive::VertexType
{ "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
// XMFLOAT3X4
{ "InstMatrix", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
{ "InstMatrix", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
{ "InstMatrix", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
};
You have to explicitly bind the per-instance data in a second Vertex Buffer before drawing:
XMFLOAT3X4 s_instanceTransforms[c_instanceCount] = { ... };
const size_t instBytes = c_instanceCount * sizeof(XMFLOAT3X4);
GraphicsResource inst = m_graphicsMemory->Allocate(instBytes);
memcpy(inst.Memory(), s_instanceTransforms, instBytes);
D3D12_VERTEX_BUFFER_VIEW vbInst = {};
vbInst.BufferLocation = inst.GpuAddress();
vbInst.SizeInBytes = static_cast<UINT>(instBytes);
vbInst.StrideInBytes = sizeof(XMFLOAT3X4);
commandList->IASetVertexBuffers(1, 1, &vbInst);
...
effect->Apply(commandList);
shape->DrawInstanced(commandList, c_instanceCount);
If you want to create a vertex format other than VertexPositionNormalTexture
, you can use the GeometricPrimitive
custom geometry methods to generate the shape data, but you'll need implement the creation of the VB/IB and rendering in your own code (i.e. the GeometricPrimitive::CreateCustom
method only supports VertexPositionNormalTexture
).
Here's an example that repurposes the generated normal information into a per-vertex color.
D3D12_VERTEX_BUFFER_VIEW vertexBufferView;
D3D12_INDEX_BUFFER_VIEW indexBufferView;
UINT indexCount;
Microsoft::WRL::ComPtr<ID3D12Resource> vertexBuffer;
Microsoft::WRL::ComPtr<ID3D12Resource> indexBuffer;
// Create shape data
GeometricPrimitive::VertexCollection vertices;
GeometricPrimitive::IndexCollection indices;
GeometricPrimitive::CreateSphere(vertices, indices);
GeometricPrimitive::VertexCollection newVerts;
newVerts.reserve(vertices.size());
for (auto it : vertices)
{
VertexPositionColor v;
v.position = it.position;
v.color = XMFLOAT4(it.normal.x, it.normal.y, it.normal.z, 1.f);
newVerts.emplace_back(v);
}
// Place data on upload heap
size_t vsize = newVerts.size() * sizeof(VertexPositionColor);
SharedGraphicsResource vb = GraphicsMemory::Get().Allocate(vsize);
memcpy(vb.Memory(), newVerts.data(), vsize);
size_t isize = indices.size() * sizeof(uint16_t);
SharedGraphicsResource ib = GraphicsMemory::Get().Allocate(isize);
memcpy(ib.Memory(), indices.data(), isize);
// You can render directly from the 'upload' heap or as shown here create static IB/VB resources
ResourceUploadBatch resourceUpload(device);
resourceUpload.Begin();
CD3DX12_HEAP_PROPERTIES heapProperties(D3D12_HEAP_TYPE_DEFAULT);
auto vdesc = CD3DX12_RESOURCE_DESC::Buffer(vsize);
auto idesc = CD3DX12_RESOURCE_DESC::Buffer(isize);
DX::ThrowIfFailed(device->CreateCommittedResource(
&heapProperties, D3D12_HEAP_FLAG_NONE, &vdesc, D3D12_RESOURCE_STATE_COPY_DEST,
nullptr, IID_PPV_ARGS(vertexBuffer.ReleaseAndGetAddressOf())));
DX::ThrowIfFailed(device->CreateCommittedResource(
&heapProperties, D3D12_HEAP_FLAG_NONE, &idesc, D3D12_RESOURCE_STATE_COPY_DEST,
nullptr, IID_PPV_ARGS(indexBuffer.ReleaseAndGetAddressOf())));
resourceUpload.Upload(vertexBuffer.Get(), vb);
resourceUpload.Upload(indexBuffer.Get(), ib);
resourceUpload.Transition(vertexBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER);
resourceUpload.Transition(indexBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_INDEX_BUFFER);
auto uploadResourcesFinished = resourceUpload.End(m_deviceResources->GetCommandQueue());
uploadResourcesFinished.wait();
// Create matching effect for new vertex layout
EffectPipelineStateDescription psd(
&VertexPositionColor::InputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullNone,
rtState);
effect = std::make_unique<BasicEffect>(device, EffectFlags::VertexColor, psd);
// Set up buffer views
m_vertexBufferView = { vertexBuffer->GetGPUVirtualAddress(), UINT(vsize), sizeof(VertexPositionColor) };
m_indexBufferView = { indexBuffer->GetGPUVirtualAddress(), UINT(isize), DXGI_FORMAT_R16_UINT };
indexCount = UINT(indices.size());
// Render using our effect
effect->Apply(commandList);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->IASetIndexBuffer(&indexBufferView);
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);
The GeometricPrimitive is tied to a device, but not a command-list. This means that creation/loading is ‘free threaded’. Drawing can be done on any command-list, but keep in mind command-lists are not ‘free threaded’.
Work Submission in Direct3D 12
When Draw
is called, it sets the Primitive Topology, Vertex Buffer in slot 0, and Index Buffer to use.
The GeometricPrimitive class assumes you've already set the Render Target view, Depth Stencil view, Viewport, ScissorRects, Descriptor Heaps (for textures and samplers), Root Signature, and Pipeline State Object (PSO) to the command-list provided to Draw
.
The GeometricPrimitive class allocates a vertex buffer and an index buffer using the GraphicsMemory class. To support DirectX 12 multiple GPU scenarios, the creation functions take an optional parameter used to allocate from a specific device.
std::unique_ptr<GeometricPrimitive> shape0, shape1;
shape0 = GeometricPrimitive::CreateTeapot(1, 8, true, device1);
shape1 = GeometricPrimitive::CreateTeapot(1, 8, true, device1);
In the DirectX 11 version of DirectX Tool Kit, the geometric primitive automatically creates a BasicEffect to simplify usage. With DirectX 12, you must always provide the effect yourself.
Tetrahedron, Cube/Hexahedron, Octahedron, Dodecahedron, and Icosahedron comprise the five Platonic solids. The surface normals for these shapes are constructed as "face-normals" for faceted shading.
The Utah Teapot (also known as the Newell Teapot) is sometimes jokingly referred to as the "Sixth Platonic solid" due to its prevalence in rendering sample images. It was created in 1975 by Martin Newell at the University of Utah. It's become the "Hello, world" of 3D models hence why it's included as a basic geometric primitive shape in DirectX Tool Kit.
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