A
sprite is a 2D image displayed on the screen. Sprites are more common
in 2D games, where the images act solely as the various game objects,
characters, and environments. In 3D we usually have 3D geometry with
texture images applied to them to simulate detail where none exists. In
some 3D games, sprites can be seen as screen interface graphics such as
health bars, shield bars, and so on.
In the early days of 3D
video games, sprites were used more like they are for 2D games, where
sprite characters and objects populate the game world. This can be seen
in early 3D games such as Id Software’s Doom, Duke Nuke ’Em 3D,
and many more. Today sprites are used mostly for particle effects such
as snow, rain, smoke, dust, fire, weapon effects, and so forth.
Types of Sprites
There are two main types
of sprites: sprites that can orient themselves in 3D space and those
that cannot. The sprites that cannot orient in 3D are known as billboard
sprites. In other words, a billboard sprite always faces the camera but
can be positioned anywhere. Characters in the game Doom were billboard sprites; no matter how the player was oriented, the enemies and many world objects would always face the player.
The decision to use
billboard sprites or not depends on the application. For most particle
systems the particles are so small that changing their rotation might
have a negative effect on the scene or, possibly, no effect at all. In
3D you can draw a textured square to act as a sprite. In this article we
will create billboard sprites in Direct3D 10 using the sprite image in Figure 1 since learning that will give us something new to cover rather than drawing yet another textured square.
To
create a sprite you simply create a 2D image. If any part of the image
is to be transparent or semitransparent, then you can create a 32-bit
RGBA image to represent the object. In the image in Figure 1
the black areas are set to transparent in the alpha channel in Adobe
Photoshop, while the orb itself is the only visible part of the texture
image.
Point Sprites
Direct3D 9 and OpenGL
have what are known as point sprites. A point sprite is essentially a
sprite that is generated from a point and, in the case of Direct3D 9 and
OpenGL, is also hardware accelerated. This means the hardware handles
keeping the sprite facing the camera, and all the programmer has to do
is enable the point sprite feature, specify the properties (such as size
etc.), and supply a list of points to the graphics hardware.
Direct3D 10, however,
does not have this point sprite feature. To use hardware-accelerated
point sprites in Direct3D 10, we must use the geometry shader to create
the sprites from a list of points. Since the geometry shader is doing
the work, the effect is technically GPU hardware accelerated since no
CPU processing is used for the creation of the geometry.
Sprites Demo
The Billboard Sprites demo
builds off of the Alpha Mapping demo and makes several modifications to
the original source code.
In the shader effect file, a
new global uniform variable is created to represent the size of the
sprite. Also added to the effect file is a geometry shader. The geometry
shader is set up to accept an input of a single point, and it outputs
six vertices because it creates two triangles (three points each) that
form the square shape. The body of the geometry shader will use the
input point’s position as the center position and will use the global
size to create four points around the center point. It outputs each
triangle using the stream object’s Append() function, and once a triangle has been fully outputted, the RestartStrip() function is called to mark the start of the next triangle. Keep in mind that each triangle is separated by a RestartStrip() call in a geometry shader.
To ensure that the
triangles are always facing the camera, we use the billboard technique.
This is done by taking the right and up vectors (i.e., directions) of
the view matrix. These vectors act as directions, where -right is left,
right is right, up is up, and -up is down. Therefore, to generate the
point that goes in the upper left, we add the center position to the
-right + up vectors (e.g., pos + [-right + up]) to move the center position to the upper left. We can then pass that position to the triangle stream output using Append(). We do this for the upper-left, upper-right, lower-left, and lower-right positions of the square that will act as the sprite.
The view matrix
represents the camera. By taking the first column (right vector) and the
first row (up vector), we can use those directions in 3D space to move
the center point around in a way that gives the impression that the
resulting square is facing the camera. Mathematically, what is happening
is that we determine which directions are considered right of and up
from the camera, and we generate a square in those directions so that no
matter where the camera is facing, we can always generate geometry that
looks directly at the camera. You can see this by holding up a pencil
and moving it to the left and up (upper left) of where you are facing.
Then move it right and up from its original position and so forth. The
view matrix uses the same idea. As long as you move the object with
respect to what the view (your eyes) considers right and up or down and
left, the resulting geometry is always facing you.
The HLSL shader from the Billboard Sprite demo is shown in Listing 1.
Note that the geometry shader also generates texture coordinates so
that the square can be textured properly. Also, since the size is set
every frame, it is added to the constant buffer that is marked for
change on a frame-by-frame basis. Since the geometry coming into the
geometry shader is the transformed data from the vertex shader, we must
use the original transformed W of each point for the pixel shader to get
the correct data. Once the data has been transformed by the vertex
shader, we don’t have to alter it in a way that changes its meaning and
therefore alters our output in ways we don’t expect.
Listing 1. The Billboard Sprite’s HLSL Effect Shader
/* Billboard Sprite Demo's Shader Ultimate Game Programming with DirectX 2nd Edition Created by Allen Sherrod */
uniform Texture2D decal;
SamplerState DecalSampler { Filter = MIN_MAG_MIP_LINEAR; AddressU = Wrap; AddressV = Wrap; };
BlendState AlphaBlending { AlphaToCoverageEnable = FALSE; BlendEnable[0] = TRUE; SrcBlend = SRC_ALPHA; DestBlend = INV_SRC_ALPHA; BlendOp = ADD; SrcBlendAlpha = ZERO; DestBlendAlpha = ZERO; BlendOpAlpha = ADD; RenderTargetWriteMask[0] = 0×0F; };
cbuffer cbChangesEveryFrame { matrix World; matrix View; float size; };
cbuffer cbChangeOnResize { matrix Projection; };
struct VS_INPUT { float4 Pos : POSITION; };
struct GS_INPUT { float4 Pos : SV_POSITION; };
struct PS_INPUT { float4 Pos : SV_POSITION; float2 Tex : TEXCOORD0; };
GS_INPUT VS(VS_INPUT input) { GS_INPUT output = (GS_INPUT)0;
output.Pos = mul(input.Pos, World); output.Pos = mul(output.Pos, View); output.Pos = mul(output.Pos, Projection);
return output; }
[maxvertexcount(6)] void GS(point GS_INPUT input[1], inout TriangleStream<PS_INPUT> triStream) { PS_INPUT output = (PS_INPUT)0;
matrix modelView = World * View;
// Used for generating billboard geometry aligned to the camera.
float3 right = float3(modelView._m00, modelView._m10, modelView._m20);
float3 up = float3(modelView._m01, modelView._m11, modelView._m21);
float3 pos = input[0].Pos.xyz;
// Must use transformed vertex W. Start first triangle.
output.Pos = float4(pos + (-right + up) * size, input[0].Pos.w); output.Tex = float2(0.0, 1.0); triStream.Append(output);
output.Pos = float4(pos + (right + up) * size, input[0].Pos.w); output.Tex = float2(0.0, 0.0); triStream.Append(output);
output.Pos = float4(pos + (right - up) * size, input[0].Pos.w); output.Tex = float2(1.0, 0.0); triStream.Append(output);
// Start next triangle.
triStream.RestartStrip(); output.Pos = float4(pos + (right - up) * size, input[0].Pos.w); output.Tex = float2(1.0, 0.0); triStream.Append(output);
output.Pos = float4(pos + (-right - up) * size, input[0].Pos.w); output.Tex = float2(1.0, 1.0); triStream.Append(output);
output.Pos = float4(pos + (-right + up) * size, input[0].Pos.w); output.Tex = float2(0.0, 1.0); triStream.Append(output);
triStream.RestartStrip(); }
float4 PS(PS_INPUT input) : SV_Target { return decal.Sample(DecalSampler, input.Tex); }
technique10 Billboard { pass P0 { SetBlendState(AlphaBlending, float4(0.0f, 0.0f, 0.0f, 0.0f), 0xFFFFFFFF);
SetVertexShader(CompileShader(vs_4_0, VS())); SetGeometryShader(CompileShader(gs_4_0, GS())); SetPixelShader(CompileShader(ps_4_0, PS())); } }
|
On
the application side, the Billboard Sprite demo sends four points to
Direct3D 10. Each of these points acts as the center of a sprite, and,
when rendered, the geometry shader generates squares around each of
these points. Since the geometry shader also generates the texture
coordinates, the only thing we need to render is a list of positions.
The global section of the demo’s main.cpp source file is shown in Listing 2, where an effect variable for the sprite’s size has been added to the mix. Also in Listing 2, you can see the modified InitializeDemo() function and RenderScene() function. Notice that in the RenderScene() function the flag D3D10_PRIMITIVE_TOPOLOGY_POINTLIST
is used to specify that we are sending points, not triangles, to the
Direct3D 10 API. Even though the geometry shader generates triangles,
technically, we are rendering points with this function call, and
therefore Direct3D 10 should expect points as the input, not triangle
primitives. Listing 7.4
shows the code that was modified to create the Billboard Sprite demo
and was built from the Alpha Mapping demo. To avoid listing too much
redundant code, we have listed the InitializeDemo() function up until the point at which the vertex buffer is created.
Listing 2. The Modified Code of the Billboard Sprite Demo’s Main Source File
/* Billboard Sprites Ultimate Game Programming with DirectX 2nd Edition Created by Allen Sherrod */
#include<d3d10.h> #include<d3dx10.h>
#pragma comment(lib, "d3d10.lib") #pragma comment(lib, "d3dx10.lib")
#define WINDOW_NAME "Billboard Sprites" #define WINDOW_CLASS "UPGCLASS" #define WINDOW_WIDTH 800 #define WINDOW_HEIGHT 600 // Global window handles. HINSTANCE g_hInst = NULL; HWND g_hwnd = NULL;
// Direct3D 10 objects. ID3D10Device *g_d3dDevice = NULL; IDXGISwapChain *g_swapChain = NULL; ID3D10RenderTargetView *g_renderTargetView = NULL;
struct DX10Vertex { D3DXVECTOR3 pos; };
ID3D10InputLayout *g_layout = NULL; ID3D10Buffer *g_pointsVB = NULL; ID3D10ShaderResourceView *g_spriteDecal = NULL;
ID3D10Effect *g_shader = NULL; ID3D10EffectTechnique *g_billboardTech = NULL; ID3D10EffectShaderResourceVariable *g_decalEffectVar = NULL; ID3D10EffectScalarVariable *g_pointSpriteSize = NULL; ID3D10EffectMatrixVariable *g_worldEffectVar = NULL; ID3D10EffectMatrixVariable *g_viewEffectVar = NULL; ID3D10EffectMatrixVariable *g_projEffectVar = NULL;
D3DXMATRIX g_worldMat, g_viewMat, g_projMat;
bool InitializeDemo() { // Load the shader. DWORD shaderFlags = D3D10_SHADER_ENABLE_STRICTNESS;
#if defined( DEBUG ) || defined( _DEBUG ) shaderFlags |= D3D10_SHADER_DEBUG; #endif
HRESULT hr = D3DX10CreateEffectFromFile( "BillboardSpritesShader.fx", NULL, NULL, "fx_4_0", shaderFlags, 0, g_d3dDevice, NULL, NULL, &g_shader, NULL, NULL);
if(FAILED(hr)) return false;
g_billboardTech = g_shader->GetTechniqueByName("Billboard"); g_worldEffectVar = g_shader->GetVariableByName( "World")->AsMatrix();
g_viewEffectVar = g_shader->GetVariableByName( "View")->AsMatrix();
g_projEffectVar = g_shader->GetVariableByName( "Projection")->AsMatrix();
g_decalEffectVar = g_shader->GetVariableByName( "decal")->AsShaderResource();
g_pointSpriteSize = g_shader->GetVariableByName( "size")->AsScalar();
// Load the texture. hr = D3DX10CreateShaderResourceViewFromFile(g_d3dDevice, "sprite.dds", NULL, NULL, &g_spriteDecal, NULL); if(FAILED(hr)) return false;
// Create the geometry.
D3D10_INPUT_ELEMENT_DESC layout[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 }, };
unsigned int numElements = sizeof(layout) / sizeof(layout[0]); D3D10_PASS_DESC passDesc;
g_billboardTech->GetPassByIndex(0)->GetDesc(&passDesc);
hr = g_d3dDevice->CreateInputLayout(layout, numElements, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, &g_layout);
if(FAILED(hr)) return false;
DX10Vertex vertices[] = { { D3DXVECTOR3( 0.5f, 0.5f, 3.0f) }, { D3DXVECTOR3( 0.5f, -0.5f, 3.0f) }, { D3DXVECTOR3(-0.5f, -0.5f, 3.0f) }, { D3DXVECTOR3(-0.5f, -0.5f, 3.0f) }, { D3DXVECTOR3(-0.5f, 0.5f, 3.0f) }, { D3DXVECTOR3( 0.5f, 0.5f, 3.0f) } }; …
return true; }
void RenderScene() { float col[4] = { 0, 0, 0, 1 };
g_d3dDevice->ClearRenderTargetView(g_renderTargetView, col);
g_worldEffectVar->SetMatrix((float*)&g_worldMat); g_decalEffectVar->SetResource(g_spriteDecal); g_pointSpriteSize->SetFloat(0.3f);
unsigned int stride = sizeof(DX10Vertex); unsigned int offset = 0;
g_d3dDevice->IASetInputLayout(g_layout);
g_d3dDevice->IASetVertexBuffers(0, 1, &g_pointsVB, &stride, &offset);
g_d3dDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_POINTLIST);
D3D10_TECHNIQUE_DESC techDesc; g_billboardTech->GetDesc(&techDesc);
for(unsigned int i = 0; i < techDesc.Passes; i++) { g_billboardTech->GetPassByIndex(i)->Apply(0); g_d3dDevice->Draw(6, 0); }
g_swapChain->Present(0, 0); }
|
Figure 2 shows a screenshot of the Billboard Sprite demo.
|