Introduction
This article introduces DirectX10 (and DX11) with Geometry shader to novice and intermediate level graphics programmers. The attached code is to be referred while going through this article.
Stream output is also demonstrated here to read back values from the GPU.
Background
Geometry shader is the latest addition to the DX10 APIs, it gives programmers an ability to spawn new and destroy existing geometry using the graphics hardware, and this allows fancier effects such as explosions, realistic growth, decay, (add new effects here :-) ) etc. without reloading the mesh.
Using the Code
The attached solution is created using VS2010-Beta2 and uses library files from DirectX-SDK (Aug/2009).
The first block explains creation of DirectX device, the objective here is to instantiate the device object to access methods to perform operations on the GPU.
DirectX device like any COM device must be created by using class factory. Fortunately, DX-SDK allows the programmer to do this via global APIs, this allows DX programming with minimum COM exposure except for the fact that every COM object is required to be released to avoid memory leaks.
D3D10CreateDeviceAndSwapChain( NULL, D3D10_DRIVER_TYPE_HARDWARE ,
NULL,0, D3D10_SDK_VERSION, &sd, &pSwapChain, &pd3dDevice );
D3D10CreateDeviceAndSwapChain
creates the device and the swap chain. Through the swap chain we get the back buffer (back buffering is used for flicker free animation). We then use the device to create a render target and use the back buffer to write on (please refer to the code).
The next block shows us how to create a vertex layout.
We first specify the layout for the vertices, this can done by using structure (refer to code). The vertex layout is required to tell the DX driver about the vertex format it can expect from the user. In the following example, we have specified that the first 3 floats are x-y-z coordinates and the next 2 will be texel coordinates.
D3D10_INPUT_ELEMENT_DESC
D3D10_INPUT_ELEMENT_DESC layout[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 },
};
UINT numElements = sizeof(layout)/sizeof(layout[0]);
D3D10_PASS_DESC PassDesc;
pTechnique->GetPassByIndex( 0 )->GetDesc( &PassDesc );
ID3D10InputLayout *pVertexLayout=0;
pd3dDevice->CreateInputLayout(layout, numElements, PassDesc.pIAInputSignature,
PassDesc.IAInputSignatureSize, &pVertexLayout );
We then have to set the vertex buffer layout using:
pd3dDevice->IASetInputLayout( pVertexLayout );
This vertex layout will be set as active layout, you can always set active layout multiple times during rendering depending on the vertex layout required for different objects, (most DX10 based engines expect only one type of layout):
struct SimpleVertex
{
D3DXVECTOR3 Pos; D3DXVECTOR2 Tex; };
SimpleVertex vertices[] = { D3DXVECTOR3( 0.5f, 0.5f, 0.0f ),
D3DXVECTOR2(1.0,1.0), D3DXVECTOR3( -0.5f, -0.5f, 0.0f ),
D3DXVECTOR2(0.0,0.0), D3DXVECTOR3( -0.5f, 0.5f, 0.0f ),D3DXVECTOR2(0.0,1.0), };
Notice that only three vertices are provided. This is important so as to provide coordinates of a triangle.
We then create a memory buffer for GPU read access and set it to use the coordinates specified in the simple vertex structure (refer to the code). This will help us to pump the vertex data to the GPU, note that this data must be aligned as per the active vertex layout discussed earlier:
...
InitData.pSysMem = vertices;
...
pd3dDevice->CreateBuffer( &bd, &InitData, &pVertexBuffer );
We then load the FX file (the shader file:- effects.txt) which holds the program for vertex shader, geometry shader, pixel shader, the object created is an effects pointer, this pointer can be used for setting up rendering similar to DX9-D3D.
Note that unlike DX9, the vertex shader here is required as fixed point transformations have been removed and all transformations have to be done by using the vertex shader and by passing parameters to variables set up in the vertex shader.
The Pixel shader is required to control rendering (and texturing as seen in the attached sample code), we can alter rendering by setting up variables for different effects (I might write an article about this later).
The geometry shader is not really required, we need it here … well it’s the reason why I am writing this article. Please refer to code to view how the effects pointer has been used to get the ID3D10EffectTechnique
pointer which is used within the render loop.
D3DX10CreateEffectFromFile( L"effects.txt", NULL, NULL, "fx_4_0",
D3D10_SHADER_ENABLE_STRICTNESS, 0, pd3dDevice, NULL,NULL, &pEffect, &pErrors, NULL );
Notice the geometry shader in the effects.txt file.
The Geometry shader is created using:
GeometryShader gStream=ConstructGSWithSO( CompileShader( gs_4_0, GS() ),"SV_POSITION.xyz" );
SetGeometryShader( gStream);
In the attached solution, the geometry shader is used to spawn an additional triangle using command: TriStream.Append
. After executing this shader in the effects pass, the output on screen will result in a square (two triangles). Now comes the part to read back values from the graphics card.
This is done in three parts:
- The first involves creating a temporary buffer with CPU access, and the other to dump vertex data into.
sbdesc.CPUAccessFlags =D3D10_CPU_ACCESS_READ ;
- The next step is to set streaming output (SO) using
SOSetTargets(1,&pBuff,&offset)
. Note that in the effects file (effects.txt), the GS shader has been created using SO.
- The last step is to copy the data from the dump to the temporary buffer for reading purposes (or feedback: used to simulate growth effects). This is done by calling
pd3dDevice->CopyResource(pStaging,pBuff);
D3DXVECTOR3 *ptr = 0;
Points of Interest
The Geometry shader with stream output can also be modified to provide GPGPU since we are now able to read back values from the GPU with ease.