I am just about finished with the D3D9 and D3D10 code, and that covers the first eight articles.
And I am working out how to deliver the Larrabee “Shared Surfaces” and Larrabee “Native” versions.
If you haven’t been keeping up “Shared Surfaces” is a Larrabee coding style where some code is moved over to Larrabee and some remains under D3D control, and the management of rendering buffers and Present remains with D3D. And “Native”, it should be obvious, is all Larrabee all the time. With all of those done, this series will be an excellent teaching tool in terms of compare and contrast.
Given some of the comments I am getting so far, I want to make clear the progression of the article series and the purpose of each sample on the way.
· Intro - introduces article series and its purpose, and use of DXUT
· Base - provides empty shell DXUT app that works in both D3D9 and D3D10
· Solve – provide simple solver and attach it to the self camera
· TextureLoad – embellishments (texture load, adverts, tessellation)
With that in mind, please don’t expect a feature before its time.
I am striving to make the code be uniform across all articles but I am not being 100% strict about it, for instance now the sky camera hot key is “k” instead of “s” so that W-A-S-D camera controls work. So if small corrections are made over time, don’t get upset.
One other adjustment I am making is to only show the new code in the callbacks in the interest of making the articles a bit shorter. I’ll use ellipses (…) to indicate where code has been elided.
I have found that the complexity factor of the articles mean it is taking me a bit longer to work through these, for instance this article is on the order of 30 pages in Word. So expect an article every 2-4 weeks. If I get it done faster than that, great my batting average goes up. As work on the Larrabee titles heats up, my batting average may even go down so fair warning.
Now we need some way to represent the maze, then to generate it, then to generate geometry that represents the maze, and then textures and rendering code. On to the code.
We need to decide how the maze is defined and represented. First, each wall will be defined as a quad or two triangles as per Image 1 below:
The maze will be generated on a regular grid, with width==height, up to a maximum of 16x16, or 256 “cells”. Image 2 below shows a top view of the regular grid:
Initially, each cell will be generated with all four walls, as shown in Image 3:
And then we will “knock down” walls as appropriate to generate the connected maze. The maze floor and maze roof will be generated and rendered separately.
Now that we have that sorted, we will add a new folder to the project, Maze, that will contain our maze-specific code as follows:
we will use bit flags to represent the presence of a wall, and the angles for turning, as follows:
const BYTE MAZE_WALL_NORTH = (1<<0);>
const BYTE MAZE_WALL_EAST = (1<<1);>
const BYTE MAZE_WALL_SOUTH = (1<<2);>
const BYTE MAZE_WALL_WEST = (1<<3);>
const BYTE MAZE_WALL_ALL = MAZE_WALL_NORTH MAZE_WALL_EAST MAZE_WALL_SOUTH MAZE_WALL_WEST;
#define NORTH_ANGLE 0x8000
#define EAST_ANGLE 0xc000
#define SOUTH_ANGLE 0x0000
#define WEST_ANGLE 0x4000
And the maze contents for each cell will be a wall-mask to contain the bit flags and a cell-id to identify which textures to use as follows:
struct MazeContents
{
BYTE wallMask[MAZE_MAX_WIDTH*MAZE_MAX_HEIGHT];
BYTE cellID[MAZE_MAX_WIDTH*MAZE_MAX_HEIGHT];
};
Finally we will define a class to wrap the maze representation, generation, and operations as follows:
class CMaze
{
public:
CMaze();
~CMaze();
HRESULT Init( DWORD width , DWORD height , DWORD Seed );
void Empty();
DWORD GetWidth() const { return m_dwWidth; };
DWORD GetHeight() const { return m_dwHeight; };
DWORD GetSize() const { return m_dwWidth*m_dwHeight; };
BYTE GetCellMaskXY( DWORD x , DWORD y ) const {
assert(m_pMaze->wallMask!=NULL);
return (m_pMaze)->wallMask[x+(y*m_dwHeight)];
};
BYTE GetCellMaskID( DWORD cellID ) const {
assert(m_pMaze->wallMask!=NULL);
return (m_pMaze)->wallMask[cellID];
};
BYTE GetCellID( DWORD x , DWORD y ) const {
assert(m_pMaze->cellID!=NULL);
return (m_pMaze)->cellID[x*m_dwWidth+y];
};
void SetCellID( DWORD x , DWORD y, BYTE id ) const {
assert(m_pMaze->cellID!=NULL);
(m_pMaze)->cellID[x*m_dwWidth+y] = id;
};
BOOL CanGoNorth( DWORD x , DWORD y ) const {
return !(GetCellMaskXY(x,y)&MAZE_WALL_NORTH);
};
BOOL CanGoEast( DWORD x , DWORD y ) const {
return !(GetCellMaskXY(x,y)&MAZE_WALL_EAST);
};
BOOL CanGoSouth( DWORD x , DWORD y ) const {
return !(GetCellMaskXY(x,y)&MAZE_WALL_SOUTH);
};
BOOL CanGoWest( DWORD x , DWORD y ) const {
return !(GetCellMaskXY(x,y)&MAZE_WALL_WEST);
};
DWORD WalkWalls( );
MazeContents* m_pMaze;
protected:
...
private:
CMaze( const CMaze& );
void operator=( const CMaze& );
};
Let’s now add methods to this class to generate the maze representation.
Maze Generator
Each class needs a constructor and a destructor, from cmaze.cpp:
CMaze::CMaze()
{
m_dwWidth = m_dwHeight = m_dwSize = 0;
m_dwSeed = 0;
m_pMaze = NULL;
m_dwMaxView = g_nMazeSize;
}
CMaze::~CMaze()
{
if( m_pMaze != NULL )
DXTRACE_ERR( TEXT("Warning: Destructing CMaze object without calling Empty()\n"),E_FAIL );
Empty();
}
These are both pretty straightforward. The main item to note is the expectation that cleanup is performed before the destructor is invoked.
Then we have a cleanup method called Empty() :
void CMaze::Empty()
{
if( m_pMaze != NULL )
SAFE_DELETE( m_pMaze );
m_dwWidth = m_dwHeight = m_dwSize = 0;
m_dwSeed = 0;
}
whose main responsibility is memory clean-up. The main method of interest, where the heavy lifting is done, is in Init():
HRESULT CMaze::Init( DWORD dwWidth, DWORD dwHeight, DWORD dwSeed )
{
HRESULT hr;
CellNode* pCells = NULL;
CellNode* pCellNode = NULL;
DWORD dwNumWalls;
WallNode* pWalls = NULL;
WallNode* pWallNode = NULL;
WallNode tempWall;
DWORD dwIndex;
DWORD i;
m_Random.Reset( dwSeed );
// Empty out any old data
Empty();
// Store parameters and compute number of cells in the maze
m_dwWidth = dwWidth;
m_dwHeight = dwHeight;
m_dwSeed = dwSeed;
m_dwSize = m_dwWidth * m_dwHeight;
// Must be non-zero
if( m_dwSize == 0 )
{
hr = E_INVALIDARG;
DXTRACE_ERR( TEXT("Maze height and width must be greater than 0"), E_INVALIDARG );
goto LFail;
}
// Validate maze size
if( m_dwWidth>MAZE_MAX_WIDTH m_dwHeight>MAZE_MAX_HEIGHT )
{
hr = E_INVALIDARG;
DXTRACE_ERR( TEXT("Maze height and width must be less than 16"), E_INVALIDARG );
goto LFail;
}
if( (m_dwWidth % 2) != 0 (m_dwHeight % 2) != 0 )
{
hr = E_INVALIDARG;
DXTRACE_ERR( TEXT("Maze height and width must be divisable by 2"), E_INVALIDARG );
goto LFail;
}
// Allocate maze, and initially make all walls solid
m_pMaze = new MazeContents;
if( m_pMaze == NULL )
{
hr = E_OUTOFMEMORY;
DXTRACE_ERR( TEXT("new"), hr );
goto LFail;
}
memset( m_pMaze->wallMask, MAZE_WALL_ALL, m_dwSize );
So far this is all initialization and validation code. Notice we reset the random number generator and called Empty() to prepare to initialize.
// Okay, now we're going to generate the maze.
// We use Kruskal's algorithm
// Allocate and initialize temporary cell list
pCells = new CellNode[m_dwSize];
if( pCells == NULL )
{
hr = E_OUTOFMEMORY;
DXTRACE_ERR( TEXT("new"), hr );
goto LFail;
}
pCellNode = pCells;
for( i = 0; i <>pNext = NULL;
pCellNode->pPartition = pCellNode;
pCellNode++;
}
// Create list of walls
dwNumWalls = ((m_dwWidth-1)*m_dwHeight) +
((m_dwHeight-1)*m_dwWidth);
pWalls = new WallNode[dwNumWalls];
if( pWalls == NULL )
{
hr = E_OUTOFMEMORY;
DXTRACE_ERR( TEXT("new"), hr );
goto LFail;
}
At this point we have a cell list and wall list.
pWallNode = pWalls;
for( i = 1; i <>
{
for( DWORD j = 0; j <>
{
pWallNode->dwX = i;
pWallNode->dwY = j;
pWallNode->dwType = MAZE_WALL_WEST;
}
}
for( i = 0; i <>
{
for( DWORD j = 1; j <>
{
pWallNode->dwX = i;
pWallNode->dwY = j;
pWallNode->dwType = MAZE_WALL_NORTH;
}
}
// Randomly permute the wall list
for( i = dwNumWalls-1; i > 0; i-- )
{
dwIndex = m_Random.Get(i);
tempWall = pWalls[dwIndex];
pWalls[dwIndex] = pWalls[i];
pWalls[i] = tempWall;
}
Now we have a fully initialized and randomized wall list.
// Walk through all the walls
pWallNode = pWalls;
for( i = 0; i <>
{
// Determine the cells either side of the wall
DWORD dwCellA = pWallNode->dwX + (pWallNode->dwY * m_dwWidth);
DWORD dwCellB = dwCellA;
if( pWallNode->dwType == MAZE_WALL_NORTH )
dwCellB -= m_dwWidth;
else
dwCellB--;
// Are they already connected (partitions equal)?
CellNode* pCellA = pCells+dwCellA;
CellNode* pCellB = pCells+dwCellB;
if( pCellA->pPartition != pCellB->pPartition )
{
// Nope, so let's take out that wall.
// First, connect the partition lists
while ( pCellA->pNext )
pCellA = pCellA->pNext;
pCellB = pCellB->pPartition;
pCellA->pNext = pCellB;
while ( pCellB )
{
pCellB->pPartition = pCellA->pPartition;
pCellB = pCellB->pNext;
}
// Now remove the walls in our maze array
if( pWallNode->dwType == MAZE_WALL_NORTH )
{
m_pMaze->wallMask[dwCellA] &= ~MAZE_WALL_NORTH;
m_pMaze->wallMask[dwCellB] &= ~MAZE_WALL_SOUTH;
}
else
{
m_pMaze->wallMask[dwCellA] &= ~MAZE_WALL_WEST;
m_pMaze->wallMask[dwCellB] &= ~MAZE_WALL_EAST;
}
}
}
Here we walk the list of walls, determine the cells on either side, if the cells are not connected, then “knock out” the intervening wall and connect the two sets. We do this for all the walls, making a completely connected maze.
// Free temporary wall and cell lists
delete[] pWalls;
delete[] pCells;
return S_OK;
LFail:
SAFE_DELETE_ARRAY( pCells );
SAFE_DELETE_ARRAY( pWalls );
SAFE_DELETE_ARRAY( m_pMaze );
return hr;
}
And finally we clean up. Next WalkWalls:
DWORD CMaze::WalkWalls( )
{
DWORD nCount = 0;
for ( UINT x = 0; x <>
{
for ( UINT y = 0; y <>
{
BYTE cell = GetCellMaskXY(x,y);
SetCellID(x,y,(BYTE)(x*g_nMazeWidth+y));
if( (cell & MAZE_WALL_NORTH ) ) nCount++;
if( (cell & MAZE_WALL_SOUTH) ) nCount++;
if( (cell & MAZE_WALL_WEST) ) nCount++;
if( (cell & MAZE_WALL_EAST) ) nCount++; }
}
return nCount;
}
WalkWalls gets used to count the walls so we know how many to draw. There are a couple other utility methods defined in cmaze.h in the class definition, go read them if you are interested.
Now that we have a maze internal representation, we need to use it and generate geometry from it. And we will do that for both D3D9 and D3D10. We’ll create a class to wrap the internal representation and the rendering representation, and call it CMazeSim. It will consist of the lifetime methods (constructor, destructor), the internal instance variables, the operations or mini-API that clients use to manipulate the Maze, and generator and rendering methods as shown below.
class CMazeSim
{
public:
//lifetime
CMazeSim();
~CMazeSim();
//internals
CMaze m_Maze;
DWORD m_dwCellWallCount;
//operations
HRESULT Init10(DWORD dwRandomSeed,
ID3D10Device* pd3dDevice);
HRESULT Init9(DWORD dwRandomSeed,
IDirect3DDevice9* pd3dDevice);
HRESULT Reset10(DWORD dwRandomSeed,
ID3D10Device* pd3dDevice);
HRESULT Reset9(DWORD dwRandomSeed,
IDirect3DDevice9* pd3dDevice);
HRESULT Unset9();
HRESULT Update10(float fElapsedTime);
HRESULT Update9(float fElapsedTime);
HRESULT Stop10();
HRESULT Stop9();
//generation
void LoadNonIndexedQuad( MazeVertex* vertices,
const D3DXVECTOR3& vOrigin,
const D3DXVECTOR3& vBasis1,
const D3DXVECTOR3& vBasis2,
const D3DXVECTOR3& vNormal );
HRESULT LoadMazeFloor10(ID3D10Device* pd3dDevice);
HRESULT LoadMazeWalls10(ID3D10Device* pd3dDevice);
HRESULT LoadMazeRoof10(ID3D10Device* pd3dDevice);
HRESULT LoadMazeVertices10(ID3D10Device* pd3dDevice);
HRESULT LoadMaze10(ID3D10Device* pd3dDevice);
HRESULT LoadMazeFloor9(IDirect3DDevice9* pd3dDevice);
HRESULT LoadMazeWalls9(IDirect3DDevice9* pd3dDevice);
HRESULT LoadMazeRoof9(IDirect3DDevice9* pd3dDevice);
HRESULT LoadMazeVertices9(IDirect3DDevice9* pd3dDevice);
HRESULT LoadMaze9(IDirect3DDevice9* pd3dDevice);
//rendering
DWORD m_dwNumCullableThings;
void DrawMazeVisNoneRoof10(ID3D10Device* pd3dDevice,
double fTime, float fElapsedTime);
void DrawMazeVisNoneWalls10(ID3D10Device* pd3dDevice,
double fTime, float fElapsedTime);
void DrawMazeVisNoneFloor10(ID3D10Device* pd3dDevice,
double fTime, float fElapsedTime);
void DrawMaze10(ID3D10Device* pd3dDevice,
D3DXMATRIX mCurrentView,
D3DXMATRIX mWorldViewProj,
double fTime, float fElapsedTime);
void DrawMazeVisNoneRoof9(IDirect3DDevice9* pd3dDevice,
double fTime, float fElapsedTime);
void DrawMazeVisNoneWalls9(IDirect3DDevice9* pd3dDevice,
double fTime, float fElapsedTime);
void DrawMazeVisNoneFloor9(IDirect3DDevice9* pd3dDevice,
double fTime, float fElapsedTime);
void DrawMaze9(IDirect3DDevice9* pd3dDevice,
D3DXMATRIX mCurrentView,
D3DXMATRIX mWorldViewProj,
double fTime, float fElapsedTime);
};
Note the instance variable m_Maze, of type CMaze. That is our internal representation.
Maze Sim-Operations
The constructor and destructor are empty for CMazeSim. Init10 and Init9 are more interesting, below:
HRESULT CMazeSim::Init10(DWORD dwRandomSeed,
ID3D10Device* pd3dDevice)
{
HRESULT hr = S_OK;
//gen the maze
hr = Reset10(dwRandomSeed,pd3dDevice);
//load the maze resources
LoadMaze10(pd3dDevice);
return hr;
}
HRESULT CMazeSim::Init9(DWORD dwRandomSeed,
IDirect3DDevice9* pd3dDevice)
{
HRESULT hr = S_OK;
//gen the maze
hr = Reset9(dwRandomSeed,pd3dDevice);
//reload the maze resources
LoadMaze9(pd3dDevice);
return hr;
}
Here we see the calls to Reset and then LoadMaze, in both D3D10 and D3D9 variants. LoadMaze handles loading the long-lived resources like the effects and textures and only needs to be invoked once.
Reset handles manipulating the CMaze ivar to regenerate the maze by invoking Empty and then Init, and then generating the maze geometry using LoadMazeVertices, in both D3D10 and D3D9 variants. Because the maze can change when it is regenerated, LoadMazeVertices needs to be called every time Reset is called. We also count how many walls are in the maze, so we can render the right amount later. And in the D3D9 variant we have to reset the effect file, see below:
HRESULT CMazeSim::Reset10(DWORD dwRandomSeed,
ID3D10Device* pd3dDevice)
{
HRESULT hr = S_OK;
//clear maze
m_Maze.Empty();
//reinit new maze
hr = m_Maze.Init( g_nMazeWidth,
g_nMazeHeight,
dwRandomSeed);
//count the walls
m_dwCellWallCount = m_Maze.WalkWalls();
//reload the maze geometry
LoadMazeVertices10(pd3dDevice);
return S_OK;
}
HRESULT CMazeSim::Reset9(DWORD dwRandomSeed,
IDirect3DDevice9* pd3dDevice)
{
HRESULT hr = S_OK;
//clear maze
m_Maze.Empty();
//reinit new maze
hr = m_Maze.Init( g_nMazeWidth,
g_nMazeHeight,
dwRandomSeed);
//count the walls
m_dwCellWallCount = m_Maze.WalkWalls();
if (g_pEffectMaze9) V_RETURN(g_pEffectMaze9->OnResetDevice() );
//reload the maze geometry
LoadMazeVertices9(pd3dDevice);
return S_OK;
}
In both D3D10 and D3D9 the Stop method releases resources, as shown below:
HRESULT CMazeSim::Stop10()
{
//d3d maze
SAFE_RELEASE(g_pMazeInputLayout);
if( g_pMazeFloor10 ) g_pMazeFloor10->Release();
if( g_pMazeWalls10 ) g_pMazeWalls10->Release();
if( g_pMazeRoof10 ) g_pMazeRoof10->Release();
if( g_pTextureFloorRV ) g_pTextureFloorRV->Release();
if( g_pTextureWallsRV ) g_pTextureWallsRV->Release();
if( g_pTextureRoofRV ) g_pTextureRoofRV->Release();
SAFE_RELEASE( g_pEffectMaze10 );
// Destroy the maze
m_Maze.Empty();
return S_OK;
}
HRESULT CMazeSim::Stop9()
{
SAFE_RELEASE( g_pMazeInputDecl );
SAFE_RELEASE( g_pMazeFloor9 );
SAFE_RELEASE( g_pMazeWalls9 );
SAFE_RELEASE( g_pMazeRoof9 );
SAFE_RELEASE( g_pTextureFloor );
SAFE_RELEASE( g_pTextureWall );
SAFE_RELEASE( g_pTextureRoof );
if( g_pEffectMaze9 ) g_pEffectMaze9->OnLostDevice();
SAFE_RELEASE( g_pEffectMaze9 );
// Destroy the maze
m_Maze.Empty();
return S_OK;
}
A difference between D3D10 and D3D9 is D3D9 requires additional resource handling during a lost device as shown in Unset9:
HRESULT CMazeSim::Unset9()
{
SAFE_RELEASE( g_pMazeFloor9 );
SAFE_RELEASE( g_pMazeWalls9 );
SAFE_RELEASE( g_pMazeRoof9 );
if( g_pEffectMaze9 ) g_pEffectMaze9->OnLostDevice();
return S_OK;
}
Maze Sim Geometry and Resources – D3D10
Now we need to show how we generate the D3D geometry from the maze internal representation. The “high-level” method is LoadMazeVertices10:
HRESULT CMazeSim::LoadMazeVertices10(ID3D10Device* pd3dDevice)
{
//floor
LoadMazeFloor10(pd3dDevice);
//walls
LoadMazeWalls10(pd3dDevice);
//roof
LoadMazeRoof10(pd3dDevice);
return S_OK;
}
It calls LoadMazeFloor10, LoadMazeWalls10, and LoadMazeRoof10:
HRESULT CMazeSim::LoadMazeFloor10(ID3D10Device* pd3dDevice)
{
HRESULT hr;
D3D10_BUFFER_DESC bd;
MazeVertex vertices[g_nMazeWidth*g_nMazeHeight*2*3];
DWORD nw = 0;
//populate buffer
for( DWORD i = 0; i < j =" 0;" usage =" D3D10_USAGE_DEFAULT;" bytewidth =" sizeof(" bindflags =" D3D10_BIND_VERTEX_BUFFER;" cpuaccessflags =" 0;" miscflags =" 0;" psysmem =" vertices;" hr =" pd3dDevice-">CreateBuffer( &bd,
&InitData,
&g_pMazeFloor10 );
if( FAILED( hr ) )
return hr;
else
return S_OK;
}
HRESULT CMazeSim::LoadMazeWalls10(ID3D10Device* pd3dDevice)
{
HRESULT hr;
D3D10_BUFFER_DESC bd;
MazeVertex vertices[g_nMazeWidth*g_nMazeHeight*4*2*3];
DWORD nw = 0;
//full height
for( DWORD i = 0; i < j =" 0;" cell =" m_Maze.GetCellMaskXY(i,j);" m_dwcellwallcount =" nw;" usage =" D3D10_USAGE_DEFAULT;" usage =" D3D10_USAGE_DEFAULT;" bytewidth =" sizeof(" bindflags =" D3D10_BIND_VERTEX_BUFFER;" cpuaccessflags =" 0;" miscflags =" 0;" psysmem =" vertices;" hr =" pd3dDevice-">CreateBuffer( &bd,
&InitData,
&g_pMazeWalls10 );
if( FAILED( hr ) )
return hr;
else
return S_OK;
}
HRESULT CMazeSim::LoadMazeRoof10(ID3D10Device* pd3dDevice)
{
HRESULT hr;
D3D10_BUFFER_DESC bd;
MazeVertex vertices[g_nMazeWidth*g_nMazeHeight *2*3];
DWORD nw = 0;
//populate buffer
for( DWORD i = 0; i < j =" 0;" usage =" D3D10_USAGE_DEFAULT;" usage =" D3D10_USAGE_DEFAULT;" bytewidth =" sizeof(" bindflags =" D3D10_BIND_VERTEX_BUFFER;" cpuaccessflags =" 0;" miscflags =" 0;" psysmem =" vertices;" hr =" pd3dDevice-">CreateBuffer( &bd,
&InitData,
&g_pMazeRoof10 );
if( FAILED( hr ) )
return hr;
else
return S_OK;
}
In all three of these methods, we loop across the maze extent and use LoadNonIndexedQuad to populate an array of MazeVertices. The vectors passed in to LoadNonIndexedQuad depend on whether it’s a floor, wall, or roof quad we are generating. Then we set up a D3D10 buffer descriptor and pass it to pd3dDevice->CreateBuffer to generate our vertex buffer.
The magic of LoadNonIndexedQuad is to loop in the width and height directions based on the amount of tessellation and generate vertices and texture coordinates. It uses the first vector as an origin, and the next two vectors as basis vectors to control the iteration:
void CMazeSim::LoadNonIndexedQuad( MazeVertex* vertices,
const D3DXVECTOR3& vOrigin,
const D3DXVECTOR3& vBasis1,
const D3DXVECTOR3& vBasis2,
const D3DXVECTOR3& vNormal )
{
// Compute edge steps
float fStep = 1.0f / g_dwTesselation;
D3DXVECTOR3 vStep1 = vBasis1 * fStep;
D3DXVECTOR3 vStep2 = vBasis2 * fStep;
// Fill out grid of vertices
D3DXVECTOR3 rowstart = vOrigin;
float u = 1;
for( DWORD i = 0;i < pos =" rowstart;" v =" 0;" j =" 0;j">pos = pos;
vertices->tex.x = u;
vertices->tex.y = v;
vertices++;
//t1v2
vertices->pos = pos+vStep2;
vertices->tex.x = u;
vertices->tex.y = v+fStep;
vertices++;
//t1v3
vertices->pos = pos+vStep1;
vertices->tex.x = u-fStep;
vertices->tex.y = v;
vertices++;
//t2v1
vertices->pos = pos+vStep2;
vertices->tex.x = u;
vertices->tex.y = v+fStep;
vertices++;
//t2v2
vertices->pos = pos+vStep2+vStep1;
vertices->tex.x = u-fStep;
vertices->tex.y = v+fStep;
vertices++;
//t2v3
vertices->pos = pos+vStep1;
vertices->tex.x = u-fStep;
vertices->tex.y = v;
vertices++;
}
}
}
Look back and note the second basis vector for the floor is up, for the roof down. And that we call LoadNonIndexedQuad four times in LoadMazeWalls10, once for each north, south, east, and west wall that could be in a cell.
Now we need to load the resources. LoadMaze10 is the method that performs this work:
HRESULT CMazeSim::LoadMaze10(ID3D10Device* pd3dDevice)
{
HRESULT hr;
D3D10_PASS_DESC PassDesc;
ID3D10EffectPass* pPass;
ID3D10Blob* l_p_Errors = NULL;
LPVOID l_pError = NULL;
// prepare for effect files
WCHAR str[MAX_PATH];
DWORD dwShaderFlags = D3D10_SHADER_ENABLE_STRICTNESS;
#if defined( DEBUG ) defined( _DEBUG )
// Set the D3D10_SHADER_DEBUG flag to embed debug information
dwShaderFlags = D3D10_SHADER_DEBUG;
#endif
// Read the maze D3DX effect file
V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH,
L"effects\\maze-singletexture10.fx" ) );
V_RETURN( D3DX10CreateEffectFromFile( str,
NULL, NULL, "fx_4_0",
dwShaderFlags, 0, pd3dDevice, NULL,NULL, &g_pEffectMaze10, &l_p_Errors, NULL ));
if( l_p_Errors )
{
l_pError = l_p_Errors->GetBufferPointer();
// then cast to a char* to see it in the locals window
}
//technique
g_pRenderMazeSolid10= g_pEffectMaze10->GetTechniqueByName(
"RenderMazeSolid" );
g_pRenderMazeAlpha10= g_pEffectMaze10->GetTechniqueByName(
"RenderMazeAlpha" );
// Obtain the parameter handles
g_pmMazeWorldViewProj10 = g_pEffectMaze10->GetVariableByName(
"g_mWorldViewProj" )->AsMatrix();
g_pmMazeWorldView10 = g_pEffectMaze10->GetVariableByName(
"g_mWorldView" )->AsMatrix();
g_pDiffuseTex10 = g_pEffectMaze10->GetVariableByName(
"g_txDiffuse" )->AsShaderResource();
//create the layout
pPass = g_pRenderMazeSolid10->GetPassByIndex( 0 );
pPass->GetDesc( &PassDesc );
V( pd3dDevice->CreateInputLayout( MazeVertexLayout, sizeof(MazeVertexLayout)/sizeof(D3D10_INPUT_ELEMENT_DESC),
PassDesc.pIAInputSignature,
PassDesc.IAInputSignatureSize,
&g_pMazeInputLayout ) );
//load the floor texture
hr = D3DX10CreateShaderResourceViewFromFile( pd3dDevice,
L"media\\panel-light-stripe.bmp", NULL, NULL, &g_pTextureFloorRV, NULL );
if( FAILED( hr ) )
return hr;
//load the wall texture
hr = D3DX10CreateShaderResourceViewFromFile( pd3dDevice,
L"media\\panel-light-rad.bmp", NULL, NULL, &g_pTextureWallsRV, NULL );
if( FAILED( hr ) )
return hr;
//load the roof texture
hr = D3DX10CreateShaderResourceViewFromFile( pd3dDevice,
L"media\\panel-light-plain.bmp", NULL, NULL, &g_pTextureRoofRV, NULL );
if( FAILED( hr ) )
return hr;
return S_OK;
}
Here we load the effect file, get the techniques and variables from it, use the description to generate the vertex layout, and then load the floor, wall, and roof texture.
Maze Sim Geometry and Resources – D3D9
The D3D9 code path has a similar master loader for vertices, LoadMazeVertices9:
HRESULT CMazeSim::LoadMazeVertices9(IDirect3DDevice9* pd3dDevice)
{
//floor
LoadMazeFloor9(pd3dDevice);
//walls
LoadMazeWalls9(pd3dDevice);
//roof
LoadMazeRoof9(pd3dDevice);
return S_OK;
}
It calls LoadMazeFloor9, LoadMazeWalls9, and LoadMazeRoof9:
HRESULT CMazeSim::LoadMazeFloor9(IDirect3DDevice9* pd3dDevice)
{
HRESULT hr;
MazeVertex* vertices;
if ( g_pMazeFloor9) SAFE_RELEASE(g_pMazeFloor9);
if( FAILED( pd3dDevice->CreateVertexBuffer( ((g_nMazeWidth * g_nMazeHeight) *2*3) *sizeof(MazeVertex),
D3DUSAGE_DYNAMIC D3DUSAGE_WRITEONLY,
FVF_MAZETEXTURED,
D3DPOOL_DEFAULT,
&g_pMazeFloor9, NULL ) ) )
return E_FAIL;
//lock to load
g_pMazeFloor9->Lock( 0,//offset
0,//entire vb
(VOID**)&vertices,
D3DLOCK_DISCARD );
DWORD nw = 0;
//populate buffer
for( DWORD i = 0; i < j =" 0;">Unlock();
return S_OK;
}
HRESULT CMazeSim::LoadMazeWalls9(IDirect3DDevice9* pd3dDevice)
{
HRESULT hr;
MazeVertex* vertices;
DWORD nw = 0;
if ( g_pMazeWalls9) SAFE_RELEASE(g_pMazeWalls9);
if( FAILED( pd3dDevice->CreateVertexBuffer( ((g_nMazeWidth * g_nMazeHeight)* 4*2*3)*sizeof(MazeVertex),
D3DUSAGE_DYNAMIC D3DUSAGE_WRITEONLY,
FVF_MAZETEXTURED,
D3DPOOL_DEFAULT,
&g_pMazeWalls9, NULL ) ) )
return E_FAIL;
//lock to load
g_pMazeWalls9->Lock( 0,//offset
0,//entire vb
(VOID**)&vertices,
D3DLOCK_DISCARD );
//load full height
for( DWORD i = 0; i < j =" 0;" cell =" m_Maze.GetCellMaskXY(i,j);">Unlock();
m_dwCellWallCount = nw;
//done
return S_OK;
}
HRESULT CMazeSim::LoadMazeRoof9(IDirect3DDevice9* pd3dDevice)
{
HRESULT hr;
MazeVertex* vertices;
if ( g_pMazeRoof9) SAFE_RELEASE(g_pMazeRoof9);
//create roof vb
if( FAILED( pd3dDevice->CreateVertexBuffer( ((g_nMazeWidth* g_nMazeHeight) *2*3)*sizeof(MazeVertex),
D3DUSAGE_DYNAMIC D3DUSAGE_WRITEONLY,
FVF_MAZETEXTURED,
D3DPOOL_DEFAULT,
&g_pMazeRoof9, NULL ) ) )
return E_FAIL;
//lock to load
g_pMazeRoof9->Lock( 0,//offset
0,//entire vb
(VOID**)&vertices,
D3DLOCK_DISCARD );
DWORD nw = 0;
//populate buffer
for( DWORD i = 0; i < j =" 0;">Unlock();
return S_OK;
}
Which are very similar to their D3D10 variants except for the required vertex buffer handling changes. It is a bit more convenient to fill a memory structure and pass it to D3D10 to create a populated vertex buffer instead of the D3D9 way of creating an empty buffer, locking it, populating it, and unlocking it.
Now we need to load the resources with LoadMaze9 is to load the effect file and textures:
HRESULT CMazeSim::LoadMaze9(IDirect3DDevice9* pd3dDevice)
{
HRESULT hr;
LPVOID l_pError = NULL;
LPD3DXBUFFER p_Errors;
// prepare for effect files
WCHAR str[MAX_PATH];
DWORD dwShaderFlags = D3D10_SHADER_ENABLE_STRICTNESS;
#if defined( DEBUG ) defined( _DEBUG )
// Set the D3D10_SHADER_DEBUG flag to embed debug information
dwShaderFlags = D3D10_SHADER_DEBUG;
#endif
// Read the maze D3DX effect file
V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH,
L"effects\\maze-singletexture9.fx" ) );
D3DXCreateEffectFromFile( pd3dDevice, str,
NULL, NULL, dwShaderFlags,
NULL, &g_pEffectMaze9, &p_Errors );
if( p_Errors )
{
l_pError = p_Errors->GetBufferPointer();
// then cast to a char* to see it in the locals window
p_Errors->Release();
}
//technique
g_pRenderMazeSolid9 = g_pEffectMaze9->GetTechniqueByName( "RenderMazeSolid" );
g_pRenderMazeAlpha9 = g_pEffectMaze9->GetTechniqueByName( "RenderMazeAlpha" );
// Obtain the parameter handles
g_hmMazeWorldViewProjHandle9 = g_pEffectMaze9->GetParameterByName( NULL, "g_mWorldViewProj" );
g_pDiffuseTex9 = g_pEffectMaze9->GetParameterByName( NULL, "g_txDiffuse" );
//create the layout
pd3dDevice->CreateVertexDeclaration( decl_TextureModDiffuse_Vert,
&g_pMazeInputDecl );
//load the floor texture
V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH,
L"media\\panel-light-stripe.bmp" ) );
V_RETURN( D3DXCreateTextureFromFileEx( pd3dDevice, str,
D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT,
0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED,
D3DX_DEFAULT, D3DX_DEFAULT, 0,
NULL, NULL, &g_pTextureFloor ) );
//load the wall texture
V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH,
L"media\\panel-light-rad.bmp" ) );
V_RETURN( D3DXCreateTextureFromFileEx( pd3dDevice, str,
D3DX_DEFAULT, D3DX_DEFAULT,D3DX_DEFAULT,
0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED,
D3DX_DEFAULT, D3DX_DEFAULT, 0,
NULL, NULL, &g_pTextureWall ) );
//load the roof texture
V_RETURN( DXUTFindDXSDKMediaFileCch( str, MAX_PATH,
L"media\\panel-light-plain.bmp" ) );
V_RETURN( D3DXCreateTextureFromFileEx( pd3dDevice, str,
D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT,
0, D3DFMT_UNKNOWN, D3DPOOL_MANAGED,
D3DX_DEFAULT, D3DX_DEFAULT, 0,
NULL, NULL, &g_pTextureRoof ) );
return S_OK;
}
Maze Sim Rendering-D3D10
Let’s get ready to render! Here we have a “high-level” method,DrawMaze10:
void CMazeSim::DrawMaze10(ID3D10Device* pd3dDevice,
D3DXMATRIX mView,
D3DXMATRIX mWorldViewProj,
double fTime, float fElapsedTime)
{
// Render the maze
g_pmMazeWorldViewProj10->SetMatrix( ( float* )&mWorldViewProj );
// Set primitive topology
pd3dDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
pd3dDevice->IASetInputLayout(g_pMazeInputLayout);
//opaque pass
g_pRenderMazeSolid10->GetPassByIndex(0)->Apply(0);
//walls
DrawMazeVisNoneWalls10(pd3dDevice, fTime, fElapsedTime);
//floor
DrawMazeVisNoneFloor10(pd3dDevice, fTime, fElapsedTime);
//make roof transparent
if ( !g_bSkyCam )
{
g_pRenderMazeAlpha10->GetPassByIndex(0)->Apply(0);
//roof
DrawMazeVisNoneRoof10(pd3dDevice, fTime, fElapsedTime);
}
}
that wraps rendering the floor, walls, and roof:
void CMazeSim::DrawMazeVisNoneFloor10(ID3D10Device* pd3dDevice,
double fTime, float fElapsedTime)
{
// Set vertex buffer
UINT stride = sizeof( MazeVertex );
UINT offset = 0;
pd3dDevice->IASetVertexBuffers(0,1, &g_pMazeFloor10, &stride, &offset );
//texture
g_pDiffuseTex10->SetResource( g_pTextureFloorRV );
pd3dDevice->PSSetShaderResources(0,1,&g_pTextureFloorRV);
pd3dDevice->Draw( g_nMazeWidth*g_nMazeHeight*2*3, 0);
}
void CMazeSim::DrawMazeVisNoneWalls10(ID3D10Device* pd3dDevice,
double fTime, float fElapsedTime)
{
// Set vertex buffer
UINT stride = sizeof( MazeVertex );
UINT offset = 0;
pd3dDevice->IASetVertexBuffers(0,1, &g_pMazeWalls10, &stride, &offset );
//texture
g_pDiffuseTex10->SetResource( g_pTextureWallsRV );
pd3dDevice->PSSetShaderResources(0,1,&g_pTextureWallsRV);
pd3dDevice->Draw( m_dwCellWallCount*3*2, 0);
}
void CMazeSim::DrawMazeVisNoneRoof10(ID3D10Device* pd3dDevice,
double fTime, float fElapsedTime)
{
// Set vertex buffer
UINT stride = sizeof( MazeVertex );
UINT offset = 0;
pd3dDevice->IASetVertexBuffers( 0, 1, &g_pMazeRoof10, &stride, &offset );
//texture
g_pDiffuseTex10->SetResource( g_pTextureRoofRV );
pd3dDevice->PSSetShaderResources(0,1,&g_pTextureRoofRV);
pd3dDevice->Draw( g_nMazeWidth*g_nMazeHeight*2*3, 0);
}
That is it for the D3D10 rendering, very straightforward. Read the effect file to see the very simple shader and stateblocks used.
Maze Sim Rendering-D3D9
The D3D9 code path also has a high-level method, DrawMaze9:
void CMazeSim::DrawMaze9(IDirect3DDevice9* pd3dDevice,
D3DXMATRIX mView,
D3DXMATRIX mWorldViewProj,
double fTime, float fElapsedTime)
{
HRESULT hr;
UINT cPass;
// Render the maze
pd3dDevice->SetVertexDeclaration( g_pMazeInputDecl );
//opaque pass
g_pEffectMaze9->SetTechnique( g_pRenderMazeSolid9 );
//floor
V( g_pEffectMaze9->SetTexture( g_pDiffuseTex9, g_pTextureFloor ) );
V( g_pEffectMaze9->SetMatrix( g_hmMazeWorldViewProjHandle9,
&mWorldViewProj ) );
V( g_pEffectMaze9->Begin( &cPass, 0 ) );
V( g_pEffectMaze9->BeginPass( 0 ) );
DrawMazeVisNoneFloor9(pd3dDevice, fTime, fElapsedTime);
V( g_pEffectMaze9->EndPass() );
V( g_pEffectMaze9->End() );
//walls
g_pEffectMaze9->SetTechnique( g_pRenderMazeSolid9 );
V( g_pEffectMaze9->SetTexture( g_pDiffuseTex9, g_pTextureWall ) );
V( g_pEffectMaze9->SetMatrix( g_hmMazeWorldViewProjHandle9,
&mWorldViewProj ) );
V( g_pEffectMaze9->Begin( &cPass, 0 ) );
V( g_pEffectMaze9->BeginPass( 0 ) );
DrawMazeVisNoneWalls9(pd3dDevice, fTime, fElapsedTime);
V( g_pEffectMaze9->EndPass() );
V( g_pEffectMaze9->End() );
//make roof transparent if "in the maze" else dont render
if ( !g_bSkyCam )
{
pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );
pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_ONE);
pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);
pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
V( g_pEffectMaze9->SetTexture( g_pDiffuseTex9,
g_pTextureRoof ) );
V( g_pEffectMaze9->SetMatrix( g_hmMazeWorldViewProjHandle9,
&mWorldViewProj ) );
V( g_pEffectMaze9->Begin( &cPass, 0 ) );
V( g_pEffectMaze9->BeginPass( 0 ) );
DrawMazeVisNoneRoof9(pd3dDevice, fTime, fElapsedTime);
V( g_pEffectMaze9->EndPass() );
V( g_pEffectMaze9->End() );
pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );
pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CCW );
}
}
You can see the D3D9 version is a little bit longer, with more state setting via the API where in D3D10 the state is contained in the effect file.
Now we have drawing the floor, walls, and roof:
void CMazeSim::DrawMazeVisNoneFloor9(IDirect3DDevice9* pd3dDevice,
double fTime, float fElapsedTime)
{
HRESULT hr;
V( pd3dDevice->SetStreamSource( 0, g_pMazeFloor9, 0,
sizeof( MazeVertex ) ) );
pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0,
g_nMazeWidth*g_nMazeHeight*2);
}
void CMazeSim::DrawMazeVisNoneWalls9(IDirect3DDevice9* pd3dDevice,
double fTime, float fElapsedTime)
{
HRESULT hr;
V( pd3dDevice->SetStreamSource( 0, g_pMazeWalls9, 0,
sizeof( MazeVertex ) ) );
pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0,
m_dwCellWallCount*2);
}
void CMazeSim::DrawMazeVisNoneRoof9(IDirect3DDevice9* pd3dDevice,
double fTime, float fElapsedTime)
{
HRESULT hr;
V( pd3dDevice->SetStreamSource( 0, g_pMazeRoof9, 0,
sizeof( MazeVertex ) ) );
pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0,
g_nMazeWidth*g_nMazeHeight*2);
}
Read the effect file to see the very simple shader and stateblocks used.
Callbacks and Maze Rendering-D3D10
We are in the final stretch, with the callbacks for D3D10 and D3D9 left.
BOOL g_bRunSim = true;
CMazeSim g_MazeSim;
DWORD g_nMazeSize, g_nMazeWidth, g_nMazeHeight, g_nMazeCells;
DWORD g_dwRandomSeed;
DWORD g_dwTesselation = 1;
In the D3D10 code path, we have some global state and an instance of the CMazeSim class shown above.
HRESULT CALLBACK OnD3D10CreateDevice( ID3D10Device* pd3dDevice, const DXGI_SURFACE_DESC* pBackBufferSurfaceDesc,
void* pUserContext )
{
…
//init internal maze representation
g_nMazeSize =16;
g_nMazeWidth =16;
g_nMazeHeight =16;
g_dwRandomSeed =314159;
hr = g_MazeSim.Init10(g_dwRandomSeed,pd3dDevice);
return S_OK;
}
Above the Init10 method on CMazeSim is called from the OnD3D10CreateDevice callback.
For rendering, the CMazeSim class completely encapsulates this, as follows:
void CALLBACK OnD3D10FrameRender( ID3D10Device* pd3dDevice,
double fTime, float fElapsedTime,
void* pUserContext )
{
…
//scale and pos the maze
D3DXMatrixIdentity(&mWorldViewProj);
D3DXMATRIX mWorldMaze, mWorldRot,mWorldScale;
D3DXMatrixTranslation( &mWorldMaze, 0.0f, 0.1f, 0.0f );
D3DXMatrixMultiply( &mWorldViewProj, &mView, &mProj );
D3DXMatrixMultiply( &mWorldViewProj, &mWorldMaze,
&mWorldViewProj );
// Render the maze
g_MazeSim.DrawMaze10(pd3dDevice, mView, mWorldViewProj,
fTime, fElapsedTime);
…
}
For resource releasing, again CMazeSim and method Stop10 encapsulates the work:
void CALLBACK OnD3D10DestroyDevice( void* pUserContext )
{
…
//destroy the internal maze
g_MazeSim.Stop10();
}
Callbacks and Maze Rendering-D3D9
We have some extern’ed global state and an instance of the CMazeSim class.
extern CMazeSim g_MazeSim;
extern DWORD g_nMazeSize,g_nMazeWidth,g_nMazeHeight,g_nMazeCells;
extern DWORD g_dwRandomSeed;
And the Init9 method on CMazeSim is called from the OnD3D9CreateDevice callback.
HRESULT CALLBACK OnD3D9CreateDevice( IDirect3DDevice9* pd3dDevice,
const D3DSURFACE_DESC* pBackBufferSurfaceDesc,
void* pUserContext )
{
…
//init internal maze representation
g_nMazeSize =16;
g_nMazeWidth =16;
g_nMazeHeight =16;
g_dwRandomSeed =314159;
hr = g_MazeSim.Init9(g_dwRandomSeed,pd3dDevice);
return S_OK;
}
Now we have a slight difference between the D3D10 and the D3D9 implementation, in that for D3D9 we have OnD3D9ResetDevice handling to call CMazeSim::Reset9 as follows:
HRESULT CALLBACK OnD3D9ResetDevice( IDirect3DDevice9* pd3dDevice,
const D3DSURFACE_DESC* pBackBufferSurfaceDesc,
void* pUserContext )
{
…
//maze
hr = g_MazeSim.Reset9(g_dwRandomSeed,pd3dDevice);
return S_OK;
}
for resources in the default video memory pool.
Rendering is very similar, invoking the D3D9 version of DrawMaze:
void CALLBACK OnD3D9FrameRender( IDirect3DDevice9* pd3dDevice,
double fTime, float fElapsedTime,
void* pUserContext )
{ …
//scale and pos the maze
D3DXMatrixIdentity(&mWorldViewProj);
D3DXMATRIX mWorldMaze, mWorldRot,mWorldScale;
D3DXMatrixTranslation( &mWorldMaze, 0.0f, 0.1f, 0.0f );
D3DXMatrixMultiply( &mWorldViewProj, &mView, &mProj );
D3DXMatrixMultiply( &mWorldViewProj, &mWorldMaze,
&mWorldViewProj );
// Render the maze
g_MazeSim.DrawMaze9(pd3dDevice, mView, mWorldViewProj,
fTime, fElapsedTime);
…
}
Now we need to clean up, which again shows a slight difference from the D3D10 code to clean up default pool resources in OnD3D9LostDevice to call CMazeSim::Unset9:
void CALLBACK OnD3D9LostDevice( void* pUserContext )
{
…
//maze
g_MazeSim.Unset9();
}
And then we have similar code for OnD3D9DestroyDevice to call CMazeSim::Stop9:
void CALLBACK OnD3D9DestroyDevice( void* pUserContext )
{
…
//destroy the internal maze
g_MazeSim.Stop9();
}
Conclusion
With that we now have a basic maze renderer, as shown in the two screen captures below:
D3D10
D3D9
Here is a link to the code project. And here is a link to a doc with properly formattted code blocks.
Although we still lack a solver, better camera controls, animated characters, and more interesting rendering. That is to come in the future articles.
Look for the solver article around the end of January.