I am still struggling to establish a beachhead on DirectX11. The
closest I have been is the "Simple3D" grid example from the DirectXTK
tutorial, so I am revisiting that project. This time I want to use it
as a platform to write a lot of experimental code, so I need to
reformat it to make it friendlier to new code. The first step is to
make a clearer separation between the template code and my
modifications. Rather than adding code directly into the template
functions I am adding callouts to empty functions that will contain
only my code. For example: CreateDevice() calls out to
CreateDevice2(); CreateResources() calls out to CreateResources2();
etc.
Remember that the DirectXTK package needs to be "installed" into
each newly created project.
"This is a native Direct3D 11 implementation of the built-in
BasicEffect from XNA Game Studio 4 which supports texture mapping,
vertex coloring, directional vertex lighting, directional per-pixel
lighting, and fog."
This is a helper for easily and efficiently drawing dynamically
generated geometry using Direct3D 11 such as lines or trianges. It
fills the same role as the legacy Direct3D 9 APIs DrawPrimitiveUP and
DrawIndexedPrimitiveUP. Dynamic submission is a highly effective
pattern for drawing procedural geometry, and convenient for debug
rendering, but is not nearly as efficient as static buffers which is
more suited to traditional meshes where the VBs and IBs do not change
every frame. Excessive dynamic submission is a common source of
performance problems in apps. Therefore, you should prefer to use
Model, GeometricPrimitive, or your own VB/IB over PrimitiveBatch
unless you really need the flexibility to regenerate the topology
every frame.
PrimitiveBatch manages the vertex and index buffers for you, using
DISCARD and NO_OVERWRITE hints to avoid stalling the GPU pipeline. It
automatically merges adjacent draw requests, so if you call DrawLine
100 times in a row, only a single GPU draw call will be generated.
PrimitiveBatch is responsible for setting the vertex buffer, index
buffer, and primitive topology, then issuing the final draw call.
Unlike the higher level SpriteBatch helper, it does not provide
shaders, set the input layout, or set any state objects.
PrimitiveBatch is often used in conjunction with BasicEffect and the
structures from VertexTypes, but it can work with any other shader or
vertex formats of your own.
An input-layout interface holds a definition of how to feed vertex
data that is laid out in memory into the input-assembler stage of the
graphics pipeline.
std::make_unique<>
is a wrapper around T *pT= new T(), creating
a wrapper object that will automatically call the destructor when it
falls out of scope. In this case, it is the equivalent of m_states= new CommonStates(m_d3dDevice.Get());
This code is creating new CommonStates and BasicEffect objects tied
to the Direct3D device. SetVertexColorEnabled(true) enables
independent color information for each vertex and requires setting
the input layout.
GetVertexShaderByteCode()
retrieves the compiled GPU shader code that implements the selected
effect.
ID3D11Device1::CreateInputLayout()
creates an interface that describes the data layout for the
input-assembler stage of the GPU pipeline. In this case, the data layout
is in the form of vertices with position and color.
Finally, the batch processing interface is created, based on
VertexPositionColor primitives.
ID3D11DeviceContext1 m_d3dContext
controls how the device renders the GPU commands.
OMSetBlendState()
tells the GPU how to blend together color and alpha channels, in this
case from one vertex to the next.
OMSetDepthStencilState()
binds a stencil state to the current context. The stencil state is used
for pixel culling, deciding which pixels are visible and which are occluded
by other pixels. (The stencil state is essentially a Z-buffer.) In this
case, it is not being used.
RSSetState()
binds a ID3D11RasterizerState to the context. This determines how
polygon primitives are filled with solid pixels. In this case, the
VertexShader from CreateDevice2() is being used to blend the colors
from the vertices.
m_effect->Apply()
is a DirectXTK function that simplifies using the predefined effect on
the DeviceContext.
IASetInputLayout()
sets the data input layout (VertexPositionColor) for the device context.
Once the device context is fully defined, I can begin submitting
graphic primitives to the GPU.
In this case, I am drawing a simple triangle with 3 vertices -- the
3D equivalent of "Hello, World!"
End() concludes the graphics batch and releases the GPU to do its
thing, writing to the current back buffer.
Even though this is a static image, it is being updated 30 times per
second.
A slightly more interesting and complex variation is to draw a
simple 3D grid. This will start as a flat plane of squares that will
evolve into a hilly terrain. I don't want to draw the grid using lines
since this will not evolve well. I need to become accustomed to using
polygon primitives (which all devolve into triangles in 3D space) so
the grid will be composed of quadrangles (quads) and rendered using
the VertexPositionColor input format.
The first task is to create a data definition for the grid. I am
creating a simple vector that will hold all the vertices in the grid,
and creating a getVertex(x0,y0) interface that will return a reference
to the vertex above the point(x0,y0) in the x/y plane. This lets me
isolate how the vertices are stored from the code that uses them.
NOTE: Direct3D uses a coordinate system where X is to the right,
Y is up, and Z is out of the screen toward the eye. In this example,
I am placing the grid in the X/Y plane with the vertices elevated
along the Z axis. The default view looks directly up along the Z
axis from the origin.
Game.h:
class Game {
private:
int vertexCt;
std::vector<DirectX::SimpleMath::Vector3> m_vertex;
DirectX::SimpleMath::Vector3 &getVertex(int x0, int z0) { return(m_vertex.at(z0*vertexCt+x0)); };
I need to set all the vertices during CreateResources().
CreateResources():
void Game::CreateResources2(void) {
vertexCt= 20;
m_vertex.resize(vertexCt*vertexCt);
for(int y0=0;y0<vertexCt;y0++) {
for(int x0=0;x0<vertexCt;x0++) {
//(x0,y0) is an integer index into the X/Y plane of the grid.
float x1= (float)x0/(float)vertexCt;
float y1= (float)y0/(float)vertexCt;
float z1= 1.0f;
//(x1,y1,z1) is a scaled value from 0.0 to 1.0 across the grid.
float x2= x1*0.5f;
float y2= y1*0.5f;
float z2= z1*0.5f;
//(x2,y2,z2) is the actual 3D position of the vertex.
getVertex(x0,y0)= Vector3(x2,y2,z2);
}
}
}
This creates 400 vertices (20*20) with elevation of 0, resulting
in a flat grid.
I am reusing the shaders and context from the simple triangle, the
only thing that needs to change is Render2().
Not very impressive, but better than nothing. The "grid" is a solid
block because I am still using the opaque vertex shader, which is
filling in the quads with a solid blend of all white vertices.
Changing the colors of the vertices makes the grid more apparent. VertexPositionColor v0(getVertex(x0,y0),Colors::Goldenrod);
VertexPositionColor v1(getVertex(x0+1,y0),Colors::Bisque);
VertexPositionColor v2(getVertex(x0+1,y0+1),Colors::Chartreuse);
VertexPositionColor v3(getVertex(x0,y0+1),Colors::DarkGray);
Looking straight up at the bottom of the grid makes it impossible
to see any elevation. I need to move the viewpoint by creating a position
for the viewpoint. This requires three transformation matrices: World,
View, and Projection.
Game.h:
class Game {
private:
DirectX::SimpleMath::Matrix m_world;
DirectX::SimpleMath::Matrix m_view;
DirectX::SimpleMath::Matrix m_proj;
m_view is set to look from (1.0,1.0,1.0) to the origin (Zero) with
"up" along the Y-axis. The grid ranges from (0,0) to (0.5,0.5) so the
near corner of the grid is halfway between the eye and the origin.
I can make this a bit more dynamic by rotating the world around the
Z axis with the pivot at (0,0). The Z-axis extends perpendicular to
the X/Y axis of the grid and the origin is at the "far" corner.
A flat grid is too boring. Terrain can be simulated by adding some
sines and cosines to the Z values.
Game::CreateResources2():
for(int y0=0;y0<vertexCt;y0++) {
for(int x0=0;x0<vertexCt;x0++) {
//(x0,y0) is an integer index into the X/Y plane of the grid.
float x1= (float)x0/(float)vertexCt;
float y1= (float)y0/(float)vertexCt;
float z1= -cosf(x1*4.0f*XM_PI)*sinf(y1*2.0f*XM_PI);
//(x1,y1,z1) is a scaled value from 0.0 to 1.0 across the grid.
float x2= x1*0.5f;
float y2= y1*0.5f;
float z2= z1*0.1f;
//(x2,y2,z2) is the actual 3D position of the vertex.
getVertex(x0,y0)= Vector3(x2,y2,z2);
}
}
Rendering the grid as a wire-frame can be helpful.
It is time to get interactive by controlling the view directly
using the keyboard and mouse.
First make the changes to the template code to add
keyboard and mouse
support.
I will use the control symantecs from Darwinia, which work really
well. The ASWD keys are used to update the X/Y coordinates, the mouse
XY updates the view pitch and yaw, and the scroll wheel updates the Z
elevation. I implement the view XY first.
The code works, but the coordinates are all screwed up; there is a
fundamental discord between my coordinates and the example code. My
code assumes the "floor" is in the XY plane while the example puts the
floor in the XZ plane. It is easier to change my code than to figure
out how to change the example's math.
Quite a bit changed since the last code dump, so here is the whole
thing.
SimpleGrid.cpp:
void Game::Initialize2(HWND window) {
m_keyboard= std::make_unique<Keyboard>();
m_mouse= std::make_unique<Mouse>();
m_mouse->SetWindow(window);
m_mouse->SetMode(Mouse::MODE_RELATIVE);
m_kbTab= false;
}
void Game::CreateDevice2(void) {
m_states= std::make_unique<CommonStates>(m_d3dDevice.Get());
m_effect= std::make_unique<BasicEffect>(m_d3dDevice.Get());
m_effect->SetVertexColorEnabled(true);
void const *shaderByteCode;
size_t byteCodeLength;
m_effect->GetVertexShaderBytecode(&shaderByteCode,&byteCodeLength);
DX::ThrowIfFailed(m_d3dDevice->CreateInputLayout(
VertexPositionColor::InputElements,
VertexPositionColor::InputElementCount,
shaderByteCode,byteCodeLength,
m_inputLayout.ReleaseAndGetAddressOf()
));
m_batch= std::make_unique<PrimitiveBatch<VertexPositionColor>>(m_d3dContext.Get());
}
void Game::CreateResources2(void) {
// Create the perspective.
m_doWireFrame= false;
m_world= Matrix::Identity;
m_viewPt= Vector3(0.5f,0.1f,0.5f);
m_yaw= XMConvertToRadians(-170.0f);
m_pitch= XMConvertToRadians(-2.0f);
//m_view= Matrix::CreateLookAt(Vector3(0.5f,1.0f,0.5f),Vector3::Zero,Vector3::UnitY);
m_proj= Matrix::CreatePerspectiveFieldOfView(XM_PI/4.0f,float(m_outputWidth)/float(m_outputHeight),0.1f,10.0f);
//m_effect->SetView(m_view);
m_effect->SetProjection(m_proj);
// Create the grid.
vertexCt= 20;
m_vertex.resize(vertexCt*vertexCt);
// Put the grid in the XZ plane with elevation along the Y axis.
for(int z0=0;z0<vertexCt;z0++) {
for(int x0=0;x0<vertexCt;x0++) {
//(x0,y0) is an integer index into the X/Y plane of the grid.
float x1= (float)x0/(float)vertexCt;
float z1= (float)z0/(float)vertexCt;
float y1= -cosf(x1*4.0f*XM_PI)*sinf(z1*2.0f*XM_PI);
//(x1,y1,z1) is a scaled value from 0.0 to 1.0 across the grid.
float x2= x1*0.5f;
float y2= y1*0.1f;
float z2= z1*0.5f;
//(x2,y2,z2) is the actual 3D position of the vertex.
getVertex(x0,z0)= Vector3(x2,y2,z2);
}
}
}
void Game::OnDeviceLost2(void) {
m_states.reset();
m_effect.reset();
m_batch.reset();
m_inputLayout.Reset();
}
void Game::Update2(float totalTime,float elapsedTime) {
auto mouse= m_mouse->GetState();
Vector3 delta= Vector3(float(mouse.x),float(mouse.y),0.0f);
m_yaw-= delta.x*0.0010f;
m_pitch-= delta.y*0.0001f;
// limit pitch to straight up or straight down
// with a little fudge-factor to avoid gimbal lock
float limit = XM_PI/ 2.0f - 0.01f;
m_pitch = std::max(-limit,m_pitch);
m_pitch = std::min(+limit,m_pitch);
// keep longitude in sane range by wrapping
if(m_yaw > XM_PI) {
m_yaw -= XM_PI * 2.0f;
} else if(m_yaw < -XM_PI) {
m_yaw += XM_PI * 2.0f;
}
auto kb= m_keyboard->GetState();
if(kb.Escape)
PostQuitMessage(0);
if(kb.Tab) {
if(!m_kbTab) {
m_doWireFrame= !m_doWireFrame;
m_kbTab= true;
}
} else {
m_kbTab= false;
}
Quaternion moveQ= Quaternion::CreateFromYawPitchRoll(m_yaw,m_pitch,0.0f);
Vector3 move= Vector3::Zero;
if(kb.Left || kb.A)
move.x= +1.0f;
if(kb.Right || kb.D)
move.x= -1.0f;
if(kb.Up || kb.W)
move.z= +1.0f;
if(kb.Down || kb.S)
move.z= -1.0f;
if(kb.PageUp)
move.y= +1.0f;
if(kb.PageDown)
move.y= -1.0f;
move= Vector3::Transform(move,moveQ)*0.010f;
m_viewPt+= move;
}
void Game::Render2(void) {
float r= 0.1f;
//Debug(DBG_RENDER,"viewPt(%f,%f,%f) (%f,%f)",m_viewPt.x,m_viewPt.y,m_viewPt.z,XMConvertToDegrees(m_yaw),XMConvertToDegrees(m_pitch));
XMVECTOR lookAt= m_viewPt + Vector3(r*sinf(m_yaw),sinf(m_pitch),r*cosf(m_yaw));
XMMATRIX view= XMMatrixLookAtRH(m_viewPt,lookAt,Vector3::Up);
m_effect->SetView(view);
m_effect->SetProjection(m_proj);
//m_effect->SetWorld(m_world);
m_d3dContext->OMSetBlendState(m_states->Opaque(),nullptr,0xFFFFFFFF);
m_d3dContext->OMSetDepthStencilState(m_states->DepthNone(),0);
m_d3dContext->RSSetState(m_states->CullNone());
m_effect->Apply(m_d3dContext.Get());
m_d3dContext->IASetInputLayout(m_inputLayout.Get());
m_batch->Begin();
VertexPositionColor v0,v1,v2,v3;
for(int x0=0;x0+1<vertexCt;x0++) {
for(int z0=0;z0+1<vertexCt;z0++) {
v0= VertexPositionColor(getVertex(x0,z0),Colors::Goldenrod);
v1= VertexPositionColor(getVertex(x0+1,z0),Colors::Bisque);
v2= VertexPositionColor(getVertex(x0+1,z0+1),Colors::Chartreuse);
v3= VertexPositionColor(getVertex(x0,z0+1),Colors::DarkGray);
if(m_doWireFrame) {
m_batch->DrawLine(v0,v1);
m_batch->DrawLine(v1,v2);
} else {
m_batch->DrawQuad(v0,v1,v2,v3);
}
}
}
m_batch->End();
}
//EOF: SIMPLEGRID.CPP
This version lets me walk around the grid. Keys A and D "strafe"
left and right, W and S move forward and back, PageUp and PageDown
elevate up and down. The mouse changes the yaw (left/right) and pitch
(up/down) angles. The TAB key switches between wireframe and fill
modes.
Culling
I have the basics working: creating the model, creating the
Direct3D device, rendering the view, user input, and moving the
camera. There are glitches in the polygon fill mode where the wrong
polygons are being occluded. I need to figure out the stencil and
culling modes.
The fix was to set
OMSetDepthStencilState(m_states->DepthDefault(),0) and
RSSetSet(m_states->CullCounterClockwise()). Note that the culling
assumes the polygons always form a concave solid, that it is
impossible to see the "backside" of a polygon. This is not strictly
true for my simple terrain model, so it is possible to see the
"underside" of the hills from the edges.