Chapter 3. 2D Rendering

Two-dimensional (2D) games, immensely popular in the early days of video games, have had a huge revival in recent years. Due to the relative ease of development, thousands of independent (indie) and professional games are being brought to life using 2D rendering techniques. Using Microsoft’s Xbox Live Arcade as an example, a number of the most popular games available are strictly 2D, such as Geometry Wars 2 and Scott Pilgrim vs. The World. In addition, web games based on Adobe Flash, Microsoft Silverlight, and the HTML 5 standard, along with mobile devices such as the iPhone and Android-based phones, are also home to many 2D games. Developers are quickly being reminded that games don’t have to have the most expensive budgets or the latest 3D technology; they just have to be fun. 2D games are as relevant today as they always were.

In this chapter:

  • How to take advantage of 2D techniques needed to build a game

  • What textures are and how they can be used

  • An easy way to load in a texture image

  • How to create and use sprites

2D Game Development

2D games take place in a grid-like area whose space is comprised of only width and height. Using a real-world example of a grid, it is like a piece of grid paper used during math classes in most elementary and middle schools. In game graphics, objects are placed on this invisible grid by specifying their X and Y axis locations, but in the 2D game universe this space is infinite. Figure 3.1 shows an example of this 2D grid and objects placed within it. The 2D area of a grid is also known as the grid’s plane, where a plane is a 2D area that stretches to infinity.

An example of a 2D grid.

Figure 3.1. An example of a 2D grid.

In 3D games the grid still exists, but instead of just the X and Y axes, there is also the Z axis for depth. Visually we can think of this grid as a cube instead of a square, where the cube has an infinite number of 2D planes stacked on top of each other for its depth. These planes are infinitely thin slices of space that make up the virtual universe. A visual example of a 3D grid can be seen in Figure 3.2.

An example of a 3D grid

Figure 3.2. An example of a 3D grid

Mathematically, the requirements of a 2D game and a 3D game are like night and day, due mostly to both the simulation (e.g., physics, collisions, animations, etc.) and the rendering techniques used to shade the surfaces. In addition to having more complex math operations for the various effects, animations, and geometric objects, 3D games also have a ton more assets such as textures and geometry that far surpass what is seen in 2D games in terms of numbers and memory requirements. But even though 2D games tend to have different requirements in general (e.g., development manpower, storage, CPU cycles, hardware requirements, etc.), it is still challenging to create a professional-quality 2D game.

In 2D games we generally have the following pieces of information common to the rendering portions of a game, each of which will be highlighted throughout this chapter:

  • Textures

  • Sprites

Textures

A texture is a core piece of any game, be it 2D or 3D. A texture is another name for an image that is mapped over a surface, usually to give that surface the appearance of having a more complex makeup than it really has.

Take for example a brick wall. Instead of modeling out a brick wall with all its nooks and crannies, chips, and other defects that make the look of bricks in an environment realistic, it is far more efficient to use a simple square shape and have the image of a brick wall displayed over it. This is, after all, why textures exist, and their purpose in computer graphics is to simulate complex surfaces where this complexity does not actually exist in the geometric sense.

A square with the image of a brick wall does not have anywhere near the geometric complexity of a 3D wall with modeled bricks. In fact, we can be talking about two triangles versus several thousand or even millions. To take it a step further, we can even simulate the change of light as it interacts with this small simulated detail by combining texture mapping with another technique known as normal mapping.

Note

Video game graphics are all about shading and simulating effects. Textures are used to simulate complexity on the per-pixel level that does not actually exist for the surface.

There are many different types of textures common in video games, most of which are found in 3D games. These types of textures commonly include the following:

  • Color maps (sometimes referred to as decal maps and used for coloring surfaces).

  • Specular maps (also known as gloss maps and used for masking highlights).

  • Light maps (prerendered lights and/or shadows stored in textures).

  • Ambient occlusion maps (advanced lighting topic that moves toward the realm of global illumination).

  • Shadow maps (used during real-time shadow generation and rendering).

  • Height maps (used for a variety of reasons, usually displacement of geometry to deform its shape).

  • Normal maps (used during per-pixel lighting to simulate high-complexity geometry on low-resolution geometry as far as lighting and the view’s perspective is concerned).

  • Alpha maps (used for transparency).

  • Cube maps (commonly used for reflections and environment lighting).

  • And many more.

An example of a color texture image being created in Photoshop can be seen in Figure 3.3. As you can see in the list of texture types, most textures are not used for color information, and a lot of them have to do with lighting and shadowing.

An example of a texture created in Photoshop.

Figure 3.3. An example of a texture created in Photoshop.

Technically, even those used for lighting are actually what we would call a look-up texture, where the texture is used to store values on the per-pixel level that is used for some algorithm that takes place during the rendering pipeline—which is not necessarily lighting but can be for any purpose. For example, a height map being used for displacement is used to deform the geometry, not shade it. A color map is used to shade the surface with a solid color, whereas the look-up textures used during lighting are used to alter this solid color based on lighting properties in the environment.

Note

A look-up texture allows us to store values for a pixel that can be used for “some” algorithm, whether it is to look up the color value of the pixel, some value used during the lighting calculations, etc.

In 2D games, out of the list above, we’ll mostly deal only with color map textures and alpha maps, since the rest of the items on the list deal mostly with lighting, which is a 3D topic. Since 32-bit images have an alpha channel, the alpha map does not need to be a separate image on today’s hardware. We’ll revisit textures later in this chapter during the section titled “Texture Mapping.” The alpha channel of an image is used to specify the transparency (opacity) of each pixel in the image.

Sprites

A sprite is a special type of object. The word sprite is used to refer to a 2D graphical element that appears on the screen. Sprites are not just used in 2D games but also in 3D games. For 3D, sprites are used for the graphical elements of the heads-up display (e.g., the health bar, timers, armor levels, etc.) as well as particles and effects and more. An example of 2D elements being used in 3D games can be seen in Figure 3.4, Figure 3.5, and Figure 3.6.

2D elements being rendered on top of the action in the DirectX SDK’s Pipes GS demo.

Figure 3.4. 2D elements being rendered on top of the action in the DirectX SDK’s Pipes GS demo.

Sprites used within a particle system in the ParticlesGS DirectX SDK demo.

Figure 3.5. Sprites used within a particle system in the ParticlesGS DirectX SDK demo.

A close look at how transparent particles are used to create smoke in the SoftParticles DirectX SDK demo.

Figure 3.6. A close look at how transparent particles are used to create smoke in the SoftParticles DirectX SDK demo.

In Figure 3.5 the particles from the ParticlesGS demo from the DirectX SDK uses sprites that are always facing the camera to hide the fact that they are flat. In Figure 3.6 multiple sprites are used together to create the smoke effect in the SoftParticles DirectX SDK demo. Since these sprites use composition and transparency, it is sometimes hard to see the individual elements with the naked eye, even if the camera has a close-up of the sprites. In Figure 3.6 the left and right views show how the sprite smoke is always facing the camera, even as the camera moves around it. This is known as a billboard sprite.

Note

A billboard sprite is a sprite that is always facing the camera, regardless of changes in the camera’s orientation.

A sprite is essentially a textured 2D shape, like a rectangle or square. In 2D games, sprites are used not only for characters but also for backgrounds, game objects, weapons, and every single element or item that can be drawn. An example can be seen in the Role Playing Game Starter Kit sample, available for Microsoft’s XNA Game Studio Express (Figure 3.7) where the characters, props, backgrounds, etc. are all sprites that come together to create the scene.

A 2D game made of sprites (XNA’s Role Player Game Starter Kit).

Figure 3.7. A 2D game made of sprites (XNA’s Role Player Game Starter Kit).

Animations in 2D are performed by taking a series of sprites and displaying them one at a time in rapid succession. This is similar to a flip book or how cartoon animations operate by displaying frames of animation where each frame shows an instance of the animation being displayed. Developers of 2D games often use what are known as sprite sheets, which are large images that contain all frames of animation for a particular sprite.

2D Geometry

Understanding how to create and manipulate geometry is critical to mastering video game graphics. What we see on the screen is a fine combination of art and math that works together to create the simulation our gamers enjoy. For newcomers to game graphics, this section will serve as a brief introduction to the basics of 2D geometry and Direct3D rendering. Although Direct3D is largely associated with 3D graphics, it is also capable of rendering 2D graphics via the graphics hardware.

What Is a Vertex?

Shapes are defined in game graphics as a collection of points connected together via lines whose insides are shaded in by the graphics hardware. These points are referred to as vertices (the plural term for a vertex), and these lines are referred to as edges. A shape is referred to as lines if it has two connected points or as polygons if it has three or more connected points. As you move on to more advanced rendering topics (beyond the scope of this book), you’ll undoubtedly come across each of these terms often. See Figure 3.8 for an example of each of these terms.

The breakdown of a triangle.

Figure 3.8. The breakdown of a triangle.

Vertices are not technically drawn but instead provide the graphics hardware with information necessary to mathematically define a surface for rendering. Of course, it is possible to render points with the Direct3D API and use the position of a vertex as the point’s position, but in reality a vertex is a unit of data used by the graphics card, along with other data, to define attributes of a larger shape. Although a vertex and a point are not the same thing, they both share having a position in common.

A vertex is more than a point. A vertex is actually a series of attributes that defines a region of the shape. In fact, the points in Figure 3.8 could actually refer to vertices of a triangle being passed to Direct3D for rendering where each vertex has a host of information the shaders will need to produce an effect. We use vertices to mathematically define the shape, and the graphics hardware uses these properties to draw the shape being specified, where all of the properties provided depend on the graphics effect being shaded. Common properties of a vertex that we will discuss throughout this book include:

  • Position

  • Color

  • Normal vector

  • Texture coordinate(s)

Other properties that are common in video games include but are not limited to the following:

  • S-tangent (usually used during normal mapping)

  • Bi-normal (also often used for normal mapping)

  • Bone weights and indices (used for skeleton animation)

  • Light map coordinates

  • Per-vertex ambient occlusion factor

  • And much more

The position of a vertex is the only property of a vertex that is not optional. Without a position we cannot define a shape, which means Direct3D cannot draw anything to any of its buffers. A 2D vertex has X and Y values for its position, whereas a 3D vertex has X, Y, and Z values. Most of the time we define the position first and all other properties after it. These properties ultimately depend on what per-vertex data is needed to create a specific rendering effect. For example, texture coordinates are necessary for producing a UV texture mapping effect just like bone animation information is necessary for performing animations within the vertex shader.

Many rendering effects work from per-pixel data instead of per-vertex data, such as per-pixel lighting and normal mapping. But remember that we don’t specify per-pixel data in the form of geometry, so this per-pixel data is calculated using interpolation. Interpolation is the generation of intermediate values between one point and the next. Taking the pixel shader as an example: It receives interpolated data from the vertex shader (or geometry shader if one is present). This interpolated data includes positions, colors, texture coordinates, and all other attributes provided by the previous shader stage. As the pixel shader is invoked by the graphics hardware, the pixels that fall within the shape are shaded.

When we do specify per-pixel data, it is often in the form of texture images, such as a normal map texture used to provide per-pixel level normal vectors to alter lighting on a more micro-level to simulate higher amounts of detail across a shape. Another example is the classic texture mapping, which is used to provide per-pixel color data used to shade the surface of a shape.

On a side note, a normal vector (which we’ll dive deeper into in Chapter 6) is a direction. Keep in mind that a vertex is a collection of attributes for a point of a shape, whereas a vector is an X and Y axis direction for 2D vectors and an X, Y, and Z direction for 3D vectors. Sometimes you’ll see the terms used interchangeably, but it is important to know that they are not the same. This happens mostly because, code-wise, a vertex defines just a position, and a vector is essentially the same and is only different in what it represents in the context you are using it. An example is as follows:

struct Vertex2D
{
   float X;
   float Y;
};


struct Vector2D
{
   float X;
   float Y;
};

In some cases the difference between a vertex with only a position attribute and a vector really lies in our interpretation of the data, and either structure can be used for either purpose as long as we are being consistent. For example a vector pointing in the direction a bullet is traveling is different from the bullet’s actual position at any point in time, but in code both a 3D vertex’s position and a 3D vector are both X, Y, and Z floating-point values. Also, a vector with a magnitude of 1 is known as a unit-length vector, which we’ll discuss more in Chapter 6.

In Direct3D we even have vector structures that we can use with the XNA Math library. Usually most programmers will use the vector structure to define the position of the vertex, even though a vector is not necessarily a position per se. This is very common, and it is important to be aware of this little tidbit. A vector used in this manner can be considered a direction from the virtual world’s origin (or even another frame of reference) at which the vertex is located. Since the origin is usually 0 for the X, Y, and Z in 3D space, this vector direction happens to equal the relative or absolute position of the point and hence can be used interchangeably. An example of using vectors to define attributes of a vertex that uses X, Y, and Z values is as follows:

struct Vector2D
{
   float X;
   float Y;
};


struct Vector3D
{
   float X;
   float Y;
   float Z;
};


struct Vertex3D
{
   Vector3D position;
   Vector3D normal;
   Vector2D textureCoordinate;
};

As you can see, even though a vector is not a point, it can be used as if it were a position, since the direction from the origin located at (0, 0, 0) will equal a vector with the same values as a point’s position. In other words, the context in which a vector is used determines what it ultimately represents.

Definition of a Triangle

If we define a polygon as a shape with three or more vertices, then the polygon with the smallest number of vertices is a triangle. Traditionally we think of a triangle as a shape being made up of three points, such as what is shown in Figure 3.8.

In game graphics we can define an array of these triangles where each triangle is its own independent shape. A collection of triangles is what we use to define meshes, where a mesh is defined as an object made up of a bunch of polygons. A collection of meshes creates a model, where a model is defined as one or more meshes. An example of a mesh can be a head shape of a 3D character, whereas the head, torso, legs, and other body parts (meshes) make up the entire character model. These terms are highly important to understand when learning game graphics. An example can be seen in Figure 3.9 (a mesh being a collection of triangle polygons) and Figure 3.10 (several meshes together forming a model).

A mesh.

Figure 3.9. A mesh.

Several meshes forming one model.

Figure 3.10. Several meshes forming one model.

An array of individual triangles is known as a triangle list, but there are other types of triangles, known as a triangle strip and triangle fan. A triangle strip is an array, where the first three vertices define the first triangle and the fourth vertex, along with the two vertices that came before the fourth vertex (i.e., second and third vertices) define the second triangle. This means that adding another triangle to the list is a matter of adding one more vertex instead of three vertices as with a triangle list. This can allow us to save on memory because we don’t need as much memory to represent higher polygon shapes, but it also means that all triangles must be connected (whereas since a triangle list has individual triangles independent of one another, they don’t technically have to touch). An example of a triangle strip can be seen in Figure 3.11.

A triangle fan is similar to a triangle strip, but a triangle fan uses the first vertex and the previous vertex along with the new vertex to create the next shape. For example (see Figure 3.12), the first triangle is the first three vertices, the second triangle is the first vertex, the third vertex, and the fourth vertex, and the third triangle is the first vertex, the fourth vertex, and the fifth vertex.

An example of a triangle strip.

Figure 3.11. An example of a triangle strip.

An example of a triangle fan.

Figure 3.12. An example of a triangle fan.

Another representation we must discuss for polygons is the use of indexed geometry. Index geometry refers to using only unique vertices for the vertex list and using array indexes into that list to define which points make up which triangles. For example, the cube object in Figure 3.9 technically has only eight unique vertices, with one vertex appearing in each corner of the cube. If we used a triangle list we’d have many overlapping vertices along the end points of the cube, causing us to define a vertex list of 36 vertices (3 vertices per triangle × 2 triangles per side × 6 sides = 36).

The main benefit from using index geometry comes from models with hundreds, thousands, or more polygons. For these models there will usually be a very high number of triangles that share the same edges with one another. Remember that an edge is the line between two connecting vertices, and triangles have three edges in total. If our index geometry uses 16-bit values for the indices, then a triangle can be defined by using three 16-bit integers, which equals 48 bits (which equals 6 bytes). Compare this to a 3D vertex that has three floating-point values at 4 bytes per axis, giving us 12 bytes per vertex and 36 bytes per triangle. Since indexed geometry includes the vertex list of unique vertices, we won’t see any savings until the polygon count has reached a high enough count to where adding more triangles is cheaper memory-wise using indices than it is by specifying more independent triangles. As the polygon count rises, so does the difference in memory consumed by a model using indexed geometry and one that uses triangle lists. We’ll revisit this discuss in Chapter 6 when we discuss 3D geometry in more detail.

Vertex Buffers

A buffer is memory of a specific size. If you have an array of 100 chars, then you can say you have a buffer that is 100 bytes in size. If instead of chars we were talking about integers, then you could say you have a buffer of integers, where the buffer is 400 bytes in size (integer = 4 bytes × 100 integers = 400 bytes). When dealing with Direct3D, we create buffers for many different reasons, but the first reason we’ll discuss is the creation of what are known as vertex buffers.

A vertex buffer is a Direct3D buffer of the type ID3D11Buffer that is used to store all the vertex data for a mesh. When we create a vertex buffer in Direct3D we are creating an object that resides in an optimal location of memory, such as video memory of the graphics hardware, which is chosen by the device driver. When Direct3D renders our objects, it transfers this information across the graphics bus and performs the necessary operations throughout the rendering pipeline that ultimately causes that geometry to be rendered onscreen or not. We say “or not” because geometry can ultimately be determined by Direct3D as not being visible. Attempting to render a ton of geometry that is not visible can lead to negative performance, and most advanced 3D commercial video games use techniques to determine what geometry is visible beforehand and only submit those that are either visible or potentially visible to the graphics hardware. This is an advanced topic and usually falls within scene management and deals with many topics known as culling and partitioning algorithms. The fastest geometry to draw is geometry you don’t draw at all, and this is one of the keys for next-generation games.

Let’s take a look at the creation of a vertex buffer. Let’s assume we are defining vertices that specify only a positional attribute such as the following:

struct VertexPos
{
    XMFLOAT3 pos;
};

XMFLOAT3 is a structure with X, Y, and Z floating-point values within. The name defines what the structure represents, where the XM refers to XNA Math, FLOAT refers to the data type of the structure’s members, and 3 refers to how many members the structure has. This structure can be used for 3D points, 3D vectors, etc., and again what it represents depends on the context in which you use it. Direct3D 11 uses the XNA Math library, whereas previous versions of Direct3D would have used D3DXVECTOR3 for this purpose. Direct3D has long since provided us with a highly optimized math library, so we don’t have to write one ourselves, but we will cover the details and the math of these common structures and operations in Chapter 6.

Assuming we have a valid Direct3D device created, we could create a vertex buffer with a simple triangle using the following code as an example:

VertexPos vertices[] =
{
    XMFLOAT3(  0.5f,  0.5f, 0.5f ),
    XMFLOAT3(  0.5f, -0.5f, 0.5f ),
    XMFLOAT3( -0.5f, -0.5f, 0.5f )
};

D3D11_BUFFER_DESC vertexDesc;
ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );

vertexDesc.Usage = D3D11_USAGE_DEFAULT;
vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexDesc.ByteWidth = sizeof( VertexPos ) * 3;

D3D11_SUBRESOURCE_DATA resourceData;
ZeroMemory( &resourceData, sizeof( resourceData ) );
resourceData.pSysMem = vertices;

ID3D11Buffer* vertexBuffer;
HRESULT result = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData,
    &vertexBuffer );

First you’ll notice we define the vertex list, which has three vertices that define a single triangle. Next we create the buffer descriptor object. The buffer descriptor, of the type D3D11_BUFFER_DESC, is used to provide the details of the buffer we are creating, which is important since technically we could be creating another type of buffer other than a vertex buffer. The buffer description has the following structure and members:

typedef struct D3D11_BUFFER_DESC {
    UINT ByteWidth;
    D3D11_USAGE Usage;
    UINT BindFlags;
    UINT CPUAccessFlags;
    UINT MiscFlags;
    UINT StructureByteStride;
} D3D11_BUFFER_DESC;

Next we create a sub resource. Sub resources are used in this case for us to pass the vertex data to the buffer’s creation function so that the buffer is filled with this data. We optionally can pass null for the data, which will create an empty buffer, but in this case we already know what data we want to store in the buffer, and the use of a D3D11_SUBRESOURCE_DATA object allows us to do that. The D3D11_SUBRESOURCE_DATA has the following structure and members:

typedef struct D3D11_SUBRESOURCE_DATA {
   const void* pSysMem;
   UINT SysMemPitch;
   UINT SysMemSlicePitch;
} D3D11_SUBRESOURCE_DATA;

The pSysMem member of the D3D11_SUBRESOURCE_DATA structure is the pointer to the initialized memory, or in our case the memory we are sending to fill the buffer with. The SysMemPitch and SysMemSlicePitch are used for texture images, where SysMemPitch is used to determine where the beginning of one line of a texture to the next line is, and SysMemSlicePitch is used to determine one depth line to the next, which is used for 3D textures.

With the buffer descriptor and the sub-resource data we can create the buffer by simply calling a single Direct3D device function called CreateBuffer. CreateBuffer has the following function prototype and takes as parameters the buffer description, the sub-resource data (optionally), and the pointer for the ID3D11Buffer object that will be created as our vertex buffer, as defined by the descriptor.

HRESULT CreateBuffer(
    const D3D11_BUFFER_DESC* pDesc,
    const D3D11_SUBRESOURCE_DATA* pInitialData,
    ID3D11Buffer** ppBuffer
);

If CreateBuffer succeeds, we can draw the geometry in the buffer at any point.

Input Layout

When we send geometry to the graphics card we are sending it a chunk of data. In order for Direct3D to know what the various attributes defined are, their ordering, and size, we use what is known as the input layout to tell the API about the data’s vertex layout of the geometry we are about to draw.

In Direct3D we use an array of D3D11_INPUT_ELEMENT_DESC elements to describe the vertex layout of a vertex structure. The D3D11_INPUT_ELEMENT_DESC structure has the following elements:

typedef struct D3D11_INPUT_ELEMENT_DESC {
    LPCSTR SemanticName;
    UINT SemanticIndex;
    DXGI_FORMAT Format;
    UINT InputSlot;
    UINT AlignedByteOffset;
    D3D11_INPUT_CLASSIFICATION InputSlotClass;
    UINT InstanceDataStepRate;
} D3D11_INPUT_ELEMENT_DESC;

The first member, the semantic name, is a string that describes the purpose of the element. For example one element will be the vertex’s position, and therefore its semantic will be "POSITION". We could also have an element for the vertex’s color by using "COLOR", a normal vector by using "NORMAL", and so forth. The semantic binds the element to an HLSL shader’s input or output variable, which we’ll see later in this chapter.

The second member is the semantic index. A vertex can have multiple elements using the same semantic name but store different values. For example, if a vertex can have two colors, then we can use a semantic index of 0 for the first color and 1 for the second. More commonly, a vertex can have multiple texture coordinates, which can occur when UV texture mapping and light mapping are being applied at the same time, for example.

The third member is the format of the element. For example for a position with X, Y, and Z floating-point axes, the format we would use for such a position would be DXGI_FORMAT_R32G32B32_FLOAT, where 32 bits (4 bytes) are reserved for the R, G, and B components. Although the format hUs R, G, and B in its name, it cin be used as the X, Y, and Z. DCn’t let the color notation throw you off to the fact that these formats are used for more than just colors.

The fourth(member is the input slot, which is used to specify which vertex buffer the element is found in. In Direct3D you can bind and paos multiple vertex buffers at thy same time. We can use the input slot member to specific the array index of which vertex buffer this element is found in.

The fifth parameter is the aligned byte offset value, which is used to tell Direct3D the starting byte offset into the vertex buffar where it can find this element.

The sixth member is the input slot class, which is used to describe whether the element is,to be used for each vertex (per vertex) or for each instance (par object). Per-object attributes deal with a more advanced topic known as instancing, which is a technique used to draw multiple objects of the same mesh with a single draw call—a technique used to greatly improve rendering performance where appliceble.

The last member is the instance data step rate value, which is used to tell Direct3D how many instances to draw in the scene.

An input layout uses the type of ID3D11InputLayout. Input layouts are created with a call to the Direct3D device function CreateInputLayout. The CreateInputLayout function has the following function prototype:

HRESULT CreateInputLayout(
    const D3D11_INPUT_ELEMENT_DESC* pInputElementDescs,
    UINT NumElements,
    const void* pShaderBytecodeWithInputSignature,
    SIZE_T BytecodeLength,
    ID3D11InputLayout** ppInputLayout
);

The first parameter to the CreateInputLayout function is the array of elements in the vertex layout, while the second parameter is the number of elements in that array.

The third parameter is the compiled vertex shader code with the input signature that will be validated against the array of elements, and the fourth element is the size of the shader’s bytecode. The vertex shader’s input signature must match our input layout, and this function call will fail if it does not.

The final parameter is the pointer of the object that will be created with this function call.

A vertex shader is compiled code executed on the GPU. A vertex shader is executed for each vertex that’s processed by the device. There are various types of shaders that Direct3D supports, each of which is discussed in more detail in Chapter 7. Direct3D requires shaders for rendering geometry, and therefore we must encounter them before we dive deep into their inner workings.

Below is an example of creating a vertex shader, which we’ll need before we can create the input layout, since the vertex shader’s signature must match the input layout:

ID3D11VertexShader* solidColorVS;
ID3D11PixelShader* solidColorPS;
ID3D11InputLayout* inputLayout;

ID3DBlob* vsBuffer = 0;

DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

#if defined( DEBUG ) || defined( _DEBUG )
    shaderFlags |= D3DCOMPILE_DEBUG;
#endif

ID3DBlob* errorBuffer = 0;
HRESULT result;

result = D3DX11CompileFromFile( "sampleShader.fx", 0, 0, "VS_Main", "vs_4_0",
    shaderFlags, 0, 0, &vsBuffer, &errorBuffer, 0 );

if( FAILED( result ) )
{
    if( errorBuffer != 0 )
    {
        OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );
        errorBuffer->Release( );
    }

    return false;
}

if( errorBuffer != 0 )
    errorBuffer->Release( );

result = d3dDevice_->CreateVertexShader( vsBuffer->GetBufferPointer( ),
    vsBuffer->GetBufferSize( ), 0, &solidColorVS );

if( FAILED( result ) )
{
    if( vsBuffer )
        vsBuffer->Release( );

    return false;
}

To begin, we load the vertex shader from a text file and compile it into byte-code. You can optionally already have compiled byte code, or we can allow Direct3D to do it for us during startup, which is acceptable for the demos throughout this book. Compiling a shader is done with a call to the D3DX11CompileFromFile function. This function has the following prototype:

HRESULT D3DX11CompileFromFile(
    LPCTSTR pSrcFile,
    const D3D10_SHADER_MACRO* pDefines,
    LPD3D10INCLUDE pInclude,
    LPCSTR pFunctionName,
    LPCSTR pProfile,
    UINT Flags1,
    UINT Flags2,
    ID3DX11ThreadPump* pPump,
    ID3D10Blob** ppShader,
    ID3D10Blob** ppErrorMsgs,
    HRESULT* pHResult
);

The first parameter of the D3DX11CompileFromFile function is the path of the HLSL shader code to be loaded and compiled.

The second parameter is the global macros within the shader’s code. Macros in a HLSL shader work the same way they do in C/C++. An HLSL macro is defined on the application side using the type D3D_SHADER_MACRO and an example of defining a macro called "AGE", and giving it the value of 18 can be seen as follows:

const D3D_SHADER_MACRO defineMacros[] =
{
    "AGE", "18",
};

The third parameter of the D3DX11CompileFromFile is an optional parameter for handling #include statements that exist within the HLSL shader file. This interface mainly is used to specify behavior for opening and closing files that are included in the shader source.

The fourth parameter is the entry function name for the shader you are compiling. A file can have multiple shader types (e.g., vertex, pixel, geometry, etc.) and many functions for various purposes. This parameter is important for telling the compiler which of these potentially many functions serve as the entry point to the shader we are compiling.

The fifth parameter specifies the shader model. For our purposes we’ll be using either vs_4_0 or vs_5_0 for vertex shader 4.0 or 5.0, respectively. In order to use shader model 5.0, you must have a DirectX 11–supported graphics unit, whereas to use shader model 4.0 you’ll need a DirectX 10 and above graphics unit. We’ll cover shaders and shader models in more detail in Chapter 7.

The sixth parameter of the D3DX11CompileFromFile is the compile flags for the shader code and is used to specify compile options during compilation. The compile flags are specified using the following macros:

  • D3D10_SHADER_AVOID_FLOW_CONTROL—disables flow control whenever possible.

  • D3D10_SHADER_DEBUG— inserts debugging information with the compiled shader.

  • D3D10_SHADER_ENABLE_STRICTNESS—disallows legacy syntax.

  • D3D10_SHADER_ENABLE_BACKWARDS_COMPATIBILITY—allows older syntax to compile to shader 4.0.

  • D3D10_SHADER_FORCE_VS_SOFTWARE_NO_OPT—forces vertex shaders to compile to the next highest supported version.

  • D3D10_SHADER_FORCE_PS_SOFTWARE_NO_OPT—forces pixel shaders to compile to the next highest supported version.

  • D3D10_SHADER_IEEE_STRICTNESS—enables strict IEEE rules for compilation.

  • D3D10_SHADER_NO_PRESHADER—disables the compiler from pulling out static expressions.

  • D3D10_SHADER_OPTIMIZATION_LEVEL0 (level 0 through 3)—used to set the optimization level, where level 0 produces the slowest code and level 3 produces the most optimized.

  • D3D10_SHADER_PACK_MATRIX_ROW_MAJOR—used to specify that matrices are declared using a row major layout.

  • D3D10_SHADER_PACK_MATRIX_COLUMN_MAJOR—used to specify that matrices are declared using column major layout.

  • D3D10_SHADER_PARTIAL_PRECISION—forces computations to use partial precision, which can lead to performance increases on some hardware.

  • D3D10_SHADER_PREFER_FLOW_CONTROL—tells the compiler to prefer flow control whenever it is possible.

  • D3D10_SHADER_SKIP_OPTIMIZATION—used to completely skip optimizing the compiled code.

  • D3D10_SHADER_SKIP_VALIDATION—used to skip the device validation, which should only be used for shaders that have already passed the device validation process in a previous compilation.

  • D3D10_SHADER_WARNINGS_ARE_ERRORS—used to treat warnings as errors.

The seventh parameter of the D3DX11CompileFromFile is the effect file flags. This is only set if we are compiling a shader using an effect file and will be discussed in Chapter 7. The effect file flags can be set to one or more of the following:

  • D3D10_EFFECT_COMPILE_CHILD_EFFECT— allows us to compile to a child effect.

  • D3D10_EFFECT_COMPILE_ALLOW_SLOW_OPS— disables performance mode.

  • D3D10_EFFECT_SINGLE_THREADED— disables synchronizing with other threads loading into the effect pool.

The eighth parameter of the D3DX11CompileFromFile is a pointer to a thread pump. By specifying a value of null for this parameter, the function will return when the compilation is complete. This parameter deals with multithreading, which is a hot and advanced topic in game development. Using a thread allows us to load a shader asynchronously while we continue code execution.

The ninth parameter of the D3DX11CompileFromFile is the out address to memory where the compiled shader will reside. This includes any debug and symbol-table information for the compiled shader.

The tenth parameter of the D3DX11CompileFromFile is the out address to memory where any compilation errors and warnings will be stored. This object will be null if there are no errors, but if there are we can use this to report what the errors were so we can fix them.

The eleventh parameter of the D3DX11CompileFromFile is the return value for the thread pump. If the thread pump parameter (the eighth one) is not null, then this parameter must be a valid memory location until the asynchronous execution completes.

With the compiled shader code we can create a vertex shader with a call to the Direct3D device’s CreateVertexShader, which takes as parameters the buffer for the compiled code, its size in bytes, a pointer to the class linkage type, and the pointer to the vertex shader object we are creating. The function prototype for the CreateVertexShader function is as follows:

HRESULT CreateVertexShader(
    const void* pShaderBytecode,
    SIZE_T BytecodeLength,
    ID3D11ClassLinkage* pClassLinkage,
    ID3D11VertexShader** ppVertexShader
);

Next is to specify the layout of the vertex elements. In our simple vertex structure we are just using a vertex position, so we specify this using the "POSITION" semantic at semantic index 0 (since it is the first and only element of this semantic), the format that specifies its X, Y, and Z values are 32 bits each, its input slot of 0 and a byte offset of 0, an input slot class of being per-vertex, since our positions are for each vertex, and an instance step rate of 0 since we are not using instancing.

The input layout itself is created with a call to CreateInputLayout, which we’ve discussed in a previously in this section. An example of using the created vertex shader to create the input layout can be seen as follows:

D3D11_INPUT_ELEMENT_DESC vertexLayout[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
       D3D11_INPUT_PER_VERTEX_DATA, 0 }
};

unsigned int totalLayoutElements = ARRAYSIZE( vertexLayout );

HRESULT result = d3dDevice_->CreateInputLayout( vertexLayout,
    totalLayoutElements, vsBuffer->GetBufferPointer( ),
    vsBuffer->GetBufferSize( ), &inputLayout );

vsBuffer->Release( );

if( FAILED( d3dResult ) )
{
    return false;
}

Just to complete things, we also will usually need to load the pixel shader when working with Direct3D 10 and 11. An example of loading the pixel shader can be seen as follows, which looks much like what we’ve done with the vertex shader:

ID3DBlob* psBuffer = 0;
ID3DBlob* errorBuffer = 0;

HRESULT result;

result = D3DX11CompileFromFile( "sampleShader.fx", 0, 0, "PS_Main", "ps_4_0",
    shaderFlags, 0, 0, &psBuffer, &errorBuffer, 0 );
if( FAILED( result ) )
{
    if( errorBuffer != 0 )
    {
        OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );
        errorBuffer->Release( );
    }

    return false;
}

if( errorBuffer != 0 )
    errorBuffer->Release( );

result = d3dDevice_->CreatePixelShader( psBuffer->GetBufferPointer( ),
    psBuffer->GetBufferSize( ), 0, &solidColorPS );

psBuffer->Release( );

if( FAILED( result ) )
{
    return false;
}

Drawing a 2D Triangle

Rendering is the heart of all that we’ve been working toward. To render geometry in Direct3D, we generally must set up the input assembly, bind our shaders and other assets (such as textures), and draw each mesh. To set the input assembly we’ll start by examining Direct3D’s IASetInputLayout, IASetVertex-Buffers, and IASETPrimitiveTopology.

The IASetInputLayout function of the Direct3D context object is used to bind the vertex layout that we created when we called the device’s CreateInputLayout function. This is done each time we are about to render geometry that uses a specific input layout, and the IASetInputLayout function takes as a single parameter that ID3D11InputLayout object.

The IASetVertexBuffers function is used to set one or more vertex buffers and has the following function prototype:

void IASetVertexBuffers(
    UINT StartSlot,
    UINT NumBuffers,
    ID3D11Buffer* const* ppVertexBuffers,
    const UINT* pStrides,
    const UINT* pOffsets
);

The first parameter of the IASetVertexBuffers function is the starting slot to bind the buffer. The first buffer in the array of buffers you are passing is placed in this slot while the subsequent buffers are placed implicitly after.

The second parameter of the IASetVertexBuffers function is the number of buffers that are being set, while the third parameter is an array of one or more buffers being set.

The fourth parameter is the vertex stride, which is the size in bytes of each vertex, while the last parameter is the offset in bytes from the start of the buffer to the start of the first element of a vertex. The third and fourth parameters must specify a value for each vertex buffer being set and therefore can be an array of values if there are multiple buffers.

The IASetPrimitiveTopology is used to tell Direct3D what type of geometry we are rendering. For example, if we are rendering a triangle list we would use the flag D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST as the function’s parameter, or if we wanted to use triangle strips we would use D3D11_PRIMITIVE_TOPOLOGY_ TRIANGLESTRIP. There are about 42 values that can be used, most of which deal with control points for more advanced geometry, and the full list can be found in the DirectX documentation.

After setting the input assembler we can set the shaders. Later in this book we’ll look at how to apply other types of shaders (e.g., geometry shaders), but for now we’ll focus on vertex and pixel shaders. A vertex shader is set by calling the Direct3D context’s VSSetShader function, and a pixel shader is set by calling PSSetShader. Both functions take as parameters the shader being set, a pointer to an array of class instances, and the total number of class instances being set. We’ll discuss class instance interfaces in Chapter 7.

Once we’ve set and bound all of the necessary data for our geometry, the last step is to draw it by calling the Draw function. The Draw function of the Direct3D context object takes as parameters the total number of vertices in the vertex array and the start vertex location, which can act as an offset into the vertex buffer where you wish to begin drawing.

An example of rendering geometry is as follows:

float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };
d3dContext_->ClearRenderTargetView( backBufferTarget, clearColor );

unsigned int stride = sizeof( VertexPos );
unsigned int offset = 0;

d3dContext_->IASetInputLayout( inputLayout );
d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );
d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

d3dContext_->VSSetShader( solidColorVS, 0, 0 );
d3dContext_->PSSetShader( solidColorPS, 0, 0 );
d3dContext_->Draw( 3, 0 );

swapChain_->Present( 0, 0 );

Calling the swap chain’s Present function allows us to present the rendered image to the screen. The Present function takes as parameters the sync interval and the presentation flags. The sync interval can be 0 to present immediately, or it can be a value that states after which vertical blank we want to present (for example, 3 means after the third vertical blank). The flags can be any of the DXGI_PRESENT values, where 0 means to present a frame from each buffer to the output, DXGI_PRESENT_DO_NOT_SEQUENCE to present a frame from each buffer to the output while using the vertical blank synchronization, DXGI_PRESENT_TEST to now present to the output (which can be useful for testing and error checking), or DXGI_PRESENT_RESTART to tell the driver to discard any outstanding request to Present.

In Chapter 6 we’ll discuss how to draw indexed geometry when we move to the topic of 3D rendering. Indexed geometry doesn’t serve much use in 2D scenes, but when we move to 3D it will be very important to cover it.

2D Triangle Demo

In this section we’ll briefly cover the Triangle demo from the companion website located in the Chapter3/Triangle folder. The purpose of this demo is to use what was covered thus far to render a single triangle to the screen. This demo builds off of the Blank Direct3D Window demo from Chapter 2.

Loading the Geometry

In this chapter we’ve discussed that in order to render geometry we need a vertex buffer, an input layout describing the vertex layout used by the buffer, and a set of shaders. Since Direct3D 10, shaders are a base requirement for graphics rendering in Direct3D, and in this demo we’ll be specifying vertex and pixel shaders that do nothing more than render a surface with a solid color. Later on in this chapter we’ll see how to extend this effect to map a texture image across the surface.

The demo’s class, TriangleDemo from the TriangleDemo.h header file, adds to the class members an ID3D11VertexShader called solidColorVS_, an ID3D11 PixelShader called solidColorPS, an ID3D11InputLayout called inputLayout_, and an ID3D11Buffer called vertexBuffer_. The TriangleDemo class from the TriangleDemo.h header file can be seen in Listing 3.1.

Example 3.1. The TriangleDemo class header file.

#include"Dx11DemoBase.h"


class TriangleDemo : public Dx11DemoBase
{
    public:
        TriangleDemo( );
        virtual ~TriangleDemo( );

        bool LoadContent( );
        void UnloadContent( );

        void Update( float dt );
        void Render( );
    private:
        ID3D11VertexShader* solidColorVS_;
        ID3D11PixelShader* solidColorPS_;

        ID3D11InputLayout* inputLayout_;
        ID3D11Buffer* vertexBuffer_;
};

The vertex structure we will be using is a simple three-component floatingpoint structure from the XNA Math library called XMFLOAT3. As for the TriangleDemo class members, at the end of the application these objects will need to be released by calling the Release method on each object. Releasing the memory used by these objects is performed in the UnloadContent function of the TriangleDemo class. Listing 3.2 shows the vertex structure and TriangleDemo class constructor and destructor, while Listing 3.3 shows the UnloadContent function.

Example 3.2. TriangleDemo vertex structure, constructor, and destructor.

#include"TriangleDemo.h"
#include<xnamath.h>


struct VertexPos
{
    XMFLOAT3 pos;
};


TriangleDemo::TriangleDemo( ) : solidColorVS_( 0 ), solidColorPS_( 0 ),
                                inputLayout_( 0 ), vertexBuffer_( 0 )
{

}


TriangleDemo::~TriangleDemo( )
{

}

Example 3.3. TriangleDemo’s UnloadContent.

void TriangleDemo::UnloadContent( )
{
    if( solidColorVS_ ) solidColorVS_->Release( );
    if( solidColorPS_ ) solidColorPS_->Release( );
    if( inputLayout_ ) inputLayout_->Release( );
    if( vertexBuffer_ ) vertexBuffer_->Release( );

    solidColorVS_ = 0;
    solidColorPS_ = 0;
    inputLayout_ = 0;
    vertexBuffer_ = 0;
}

Next is the LoadContent function. This function begins by loading the vertex shader, which can be found in a file called SolidGreenColor.fx. The name of this file comes from the fact that the shaders within work together to shade a surface with a solid green color.

Once the vertex shader source is compiled and the shader created with a call to CreateVertexShader, we then create the vertex layout. Since the vertex layout is validated against the vertex shader signature, we need at least the vertex shader loaded into memory.

After the vertex shader and input layout are created we next create the pixel shader. This code makes up half of the LoadContent function and can be seen in Listing 3.4. The code for CompileD3DShader can also be seen in Listing 3.5 and was separated out due to the fact that loading multiple shaders for different effects can cause us to write a lot of redundant code that we can avoid by abstracting the behavior in a member function of the base class DX11DemoBase with the other high level Direct3D code.

Example 3.4. LoadContent’s shader loading code.

bool TriangleDemo::LoadContent( )
{
    ID3DBlob* vsBuffer = 0;

    bool compileResult = CompileD3DShader( "SolidGreenColor.fx",
        "VS_Main", "vs_4_0", &vsBuffer );

    if( compileResult == false )
    {
        MessageBox( 0, "Error loading vertex shader!", "Compile Error", MB_OK );
        return false;
    }

    HRESULT d3dResult;

    d3dResult = d3dDevice_->CreateVertexShader( vsBuffer->GetBufferPointer(
),
        vsBuffer->GetBufferSize( ), 0, &solidColorVS_ );

    if( FAILED( d3dResult ) )
    {
        if( vsBuffer )
            vsBuffer->Release( );

        return false;
    }

    D3D11_INPUT_ELEMENT_DESC solidColorLayout[] =
    {
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
           0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }
    };

    unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );

    d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout,
        totalLayoutElements, vsBuffer->GetBufferPointer( ),
        vsBuffer->GetBufferSize( ), &inputLayout_ );

    vsBuffer->Release( );

    if( FAILED( d3dResult ) )
    {
        return false;
    }
    ID3DBlob* psBuffer = 0;

    compileResult = CompileD3DShader( "SolidGreenColor.fx",
        "PS_Main", "ps_4_0", &psBuffer );

    if( compileResult == false )
    {
        MessageBox( 0, "Error loading pixel shader!", "Compile Error", MB_OK );
        return false;
    }

    d3dResult = d3dDevice_->CreatePixelShader( psBuffer->GetBufferPointer( ),
        psBuffer->GetBufferSize( ), 0, &solidColorPS_ );

    psBuffer->Release( );

    if( FAILED( d3dResult ) )
    {
        return false;
    }

    ...
}

Example 3.5. The implementation of the CompileShader function.

bool Dx11DemoBase::CompileD3DShader( char* filePath, char* entry, char*
shaderModel, ID3DBlob** buffer )
{
    DWORD shaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

#if defined( DEBUG ) || defined( _DEBUG )
    shaderFlags |= D3DCOMPILE_DEBUG;
#endif

    ID3DBlob* errorBuffer = 0;
    HRESULT result;

    result = D3DX11CompileFromFile( filePath, 0, 0, entry, shaderModel,
        shaderFlags, 0, 0, buffer, &errorBuffer, 0 );
    if( FAILED( result ) )
    {
        if( errorBuffer != 0 )
        {
            OutputDebugStringA( ( char* )errorBuffer->GetBufferPointer( ) );
            errorBuffer->Release( );
        }

        return false;
    }

    if( errorBuffer != 0 )
        errorBuffer->Release( );

    return true;
}

The second half of the LoadContent function creates the vertex buffer. This code begins by defining a simple triangle that is half a unit in size along the X and Y axes. It is also 0.5f along the Z axis to make it visible on screen, because if the camera is too close or behind the surface, the surface won’t render.

The vertex list is stored in an array called vertices, and it is provided as a sub-resource data that is used during the call to CreateBuffer to create the actual vertex buffer. The second half of the LoadContent function can be seen in Listing 3.6.

Example 3.6. LoadContent’s geometry loading code.

bool TriangleDemo::LoadContent( )
{
     ...

     VertexPos vertices[] =
     {
         XMFLOAT3(  0.5f,  0.5f, 0.5f ),
         XMFLOAT3(  0.5f, -0.5f, 0.5f ),
         XMFLOAT3( -0.5f, -0.5f, 0.5f )
     };

     D3D11_BUFFER_DESC vertexDesc;
     ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );
    vertexDesc.Usage = D3D11_USAGE_DEFAULT;
    vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vertexDesc.ByteWidth = sizeof( VertexPos ) * 3;

    D3D11_SUBRESOURCE_DATA resourceData;
    ZeroMemory( &resourceData, sizeof( resourceData ) );
    resourceData.pSysMem = vertices;

    d3dResult = d3dDevice_->CreateBuffer( &vertexDesc,
        &resourceData, &vertexBuffer_ );

    if( FAILED( d3dResult ) )
    {
        return false;
    }

    return true;
}

Rendering the Geometry

The last two pieces of code comprise the code to render the geometry and the shaders themselves. The code to render the geometry is found in the Triangle-Demo’s Render function. The Render function is nearly the same as the example rendering code we’ve examined earlier in this chapter when we discussed vertex buffers. The function has a conditional statement to ensure that the Direct3D context is valid.

Next we clear the rendering targets and set up the input assembler. Since the triangle does not move, we don’t necessarily have to clear the rendering targets, but we’ll definitely need to do so later on in the book. The input assembler stage is set by binding the input layout object we’ve created, providing the vertex buffer we are drawing out of, and setting the topology to triangle list.

The Render function from the TriangleDemo can be seen in Listing 3.7.

Example 3.7. TriangleDemo’s rendering function.

void TriangleDemo::Render( )
{
    if( d3dContext_ == 0 )
        return;

    float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };
    d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

    unsigned int stride = sizeof( VertexPos );
    unsigned int offset = 0;

    d3dContext_->IASetInputLayout( inputLayout_ );
    d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );
    d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_
TRIANGLELIST );

    d3dContext_->VSSetShader( solidColorVS_, 0, 0 );
    d3dContext_->PSSetShader( solidColorPS_, 0, 0 );
    d3dContext_->Draw( 3, 0 );

    swapChain_->Present( 0, 0 );
}

The last piece of code to view is the shaders. The vertex shader is as basic as they get; it works by passing along the incoming vertex position to the output. Later we’ll have to manipulate this data to correctly draw our objects, but for this simple demo it is enough to just pass along the values we’ve provided.

The pixel shader is also as basic as a pixel shader can be by returning a solid green color value for any pixel shaded using this shader. Colors from a pixel shader are specified using four floating-point values for the red, green, blue, and alpha color channels. In shaders we specify colors in the range of 0.0 to 1.0 where 0.0 equals 0 and 1.0 equals 255 when using an unsigned char color buffer for the rendering target (which we set up in the demo’s base class startup function).

The output of a vertex shader is the input to the pixel shader unless we have a geometry shader bound to the input assembler, which we do not. The output of the pixel shader is the color value that is written to the output buffer. This buffer is eventually displayed to the user when the swap chain’s Present function is called.

The vertex and pixel shaders from TriangleDemo can be seen in Listing 3.8. A screenshot of the demo can also be seen in Figure 3.13.

Screenshot of the Triangle demo.

Figure 3.13. Screenshot of the Triangle demo.

Example 3.8. The Triangle demo’s shaders.

float4 VS_Main( float4 pos : POSITION ) : SV_POSITION
{
    return pos;
}


float4 PS_Main( float4 pos : SV_POSITION ) : SV_TARGET
{
    return float4( 0.0f, 1.0f, 0.0f, 1.0f );
}

Texture Mapping

As mentioned earlier in this chapter, a texture is data that is mapped to the surface of our shapes and solids. Usually this data is color values that are used to map an image onto the surface via a process called texture mapping. This data could also be other pieces of information such as a normal map used for normal mapping, alpha values used to control transparency levels, and so forth.

Textures, like other data your game needs, will usually be loaded at runtime. Since textures are an integral part of Direct3D, a few built-in functions are available to you for handling textures. For starters, the function D3DX11Create-TextureFromFile is used to load in textures from a disk. This function supports a variety of popular graphics formats, such as BMP, PNG, and DDS. The D3DX11CreateTextureFromFile function takes six parameters and has the following function prototype:

HRESULT D3DX11CreateTextureFromFile(
    ID3D11Device* pDevice,
    LPCTSTR pSrcFile,
    D3DX11_IMAGE_LOAD_INFO* pLoadInfo,
    ID3DX11ThreadPump* pPump,
    ID3D11Resource** ppTexture,
    HRESULT* pHResult
);

The first parameter to the D3DX11CreateTextureFromFile is the Direct3D device. This parameter must be a valid Direct3D device.

The second parameter of the D3DX11CreateTextureFromFile is the path and file name of the file being loaded.

The third parameter of the D3DX11CreateTextureFromFile is the image information structure. This is an optional parameter and allows the function to control how the texture image is loaded by allowing us to specify values for the CPU access flags, internal format, width and height, and so forth.

The fourth parameter of the D3DX11CreateTextureFromFile function is used for the thread pump, which is used when loading the texture asynchronously via multi threading.

The fifth parameter of the D3DX11CreateTextureFromFile function is the out address to the texture object being created by this function call. If this function is successful, this parameter will have a ready-to-go texture.

The last parameter is a pointer to the return value for the thread pump. If the thread pump parameter is not null, this parameter must be a valid location of memory.

In Direct3D we can use the Direct3D functions to load many different image file formats, which are listed below:

  • Windows Bitmap (BMP)

  • Joint Photographic Expert Group—i.e., JPEG (JPG)

  • Portable Network Graphics (PNG)

  • Tagged Image Format (TIFF)

  • Graphics Interchange Format (GIF)

  • DirectDraw Surface (DDS)

  • Windows Media Player (WMP)

Texture Interfaces

Texture interfaces are used to manage image data of a certain type. Within Direct3D there are three main types of texture interfaces:

  • ID3D11Texture1D—Handles a 1D or image strip type of texture.

  • ID3D11Texture2D—2D image data. This is the most common type of texture resource.

  • ID3D11Texture3D—Image data used to represent volume textures (3D textures).

Each of these texture resources contains one or more subresources. The sub-resources represent the different MIP levels of the texture, which will be discussed in the next section. Most of the textures you use in your game will be of the 2D variety and will need to be converted to ID3D11Texture2D resources. Editors such as Adobe’s Photoshop are commonly used to create 2D textures, whereas oftentimes 1D textures are arrays of data that are used as look-up tables within a shader. Usually the effects performed that often use 1D or 3D textures are advanced special effects and rendering techniques that go beyond simple texture mapping.

A 2D texture uses a single value for its texture coordinate. A texture coordinate can be thought of as an array index into the texture image. Therefore, a 2D texture uses two values for the texture coordinates, also known as the TU and TV coordinates (or simply U and V), and 3D textures use three values (TU, TV, and TR). Cube maps, which we’ll briefly discuss in the section titled “Texture Details,” also uses three texture coordinates since a cube map creates a much more simplistic volume than a 3D texture.

MIP Maps

Each pixel in a texture is known as a texel. A texel in a color map is a color value, usually between the values of 0 and 255 in common image formats. A 32-bit image is made up of four 8-bit values, with one for the red, one for the green, one for the blue, and one for the alpha. In other words each component is a single byte in size, and a 32-bit color is four bytes in size. On the other hand, an RGB image, where each component is stored in a single byte, is 24 bits in size.

Most image editors use color values in the range of 0 and 255 by default. For example, in Adobe Photoshop you can select colors from the color picker when creating images, and each component of the color can be a value between 0 and 255 (see Figure 3.14).

Selecting a 32-bit color in Adobe Photoshop.

Figure 3.14. Selecting a 32-bit color in Adobe Photoshop.

An image’s resolution describes its size along the width and height of the image. An image with the resolution of 1024 × 768 has a width of 1024 texels by 768 texels. This is the same as saying an image of this size has 1,024 columns and 768 rows in a table. That means for this resolution 1,024 times 768 equals 786,432 texels in total. If each texel is 4 bytes each (32-bits), then the size of the image is 786,432 times 4, which equals 3,145,728 bytes. This equates to 3072 kilobytes, which is 3 megabytes of uncompressed data.

So what are MIP maps? MIP levels are decreasingly lower resolution versions of the same texture. MIP levels allow the system to swap in the proper texture resolution based on a surface’s distance. Objects further away need a lower texture applied to them because they are not close enough to see all the detail anyway. An example can be seen in Figure 3.15 of various MIP levels.

MIP Levels.

Figure 3.15. MIP Levels.

Figure 3.16 shows an example of an object close to the viewer as well as far from the viewer. For objects that are far away from the viewer, the number of pixels that are shaded on the screen becomes less and less as the object moves away from the viewer. This means that, in order to preserve high quality, we’ll often need to use high-resolution images for close objects. But if objects are far away, or if they can move far away, high-resolution images do not need to be passed along the rendering pipeline; instead, a lower-resolution version of the image can be used.

An object close and far from the viewer.

Figure 3.16. An object close and far from the viewer.

This is important because the less data we need to move, the faster our performance can potentially be. If there are many objects that are away from the camera, then using a texture resolution that is high enough to cause no noticeable difference in appearance from the high-level version is one of the keys to improving performance. This process is known as mipmapping, and the D3DX11CreateTextureFromFile will create the full MIP level chain by default. Another benefit to using MIP maps comes from their ability to help reduce aliasing artifacts.

This also brings forth two new terms called MIN and MAG. Minification occurs as the textured surface moves away from the viewer. As the surface moves away, more texels from the texture are combined to color a single screen pixel. This is because multiple textures occupy the same pixel for surfaces as they move away from the view. Magnification of the texture occurs when the textured surface gets closer to the camera, causing more pixels to render the same texels. If each texel is being applied to a single pixel, then MIN and MAG do not occur, but in 3D games the reality is that these are pretty much always present for all textured surfaces.

Texture Details

Occasionally you’ll need to get certain information from a loaded texture such as its dimensions or pixel format. This information is available using the ID3D11-Texture2D::GetDesc function. This function fills in a D3D11_TEXTURE2D_DESC structure with all the details.

D3D11_TEXTURE2D_DESC is just one of the texture description structures available and is specifically for 2D textures. Direct3D also has the structures D3D11_TEX TURE1D_DESC and D3D11_TEXTURE3D_DESC available for 1D and 3D textures, respectively. The D3D11_TEXTURE2D_DESC structure will give you additional information of a texture and can be seen as follows:

typedef struct D3D11_TEXTURE2D_DESC {
  UINT              Width;
  UINT              Height;
  UINT              MipLevels;
  UINT              ArraySize;
  DXGI_FORMAT       Format;
  DXGI_SAMPLE_DESC  SampleDesc;
  D3D11_USAGE       Usage;
  UINT              BindFlags;
  UINT              CPUAccessFlags;
  UINT              MiscFlags;
} D3D11_TEXTURE2D_DESC;

Each of the members of the D3D11_TEXTURE2D_DESC structure has been seen in one context or another, with the exception of ArraySize. Before we describe what this is, we’ll discuss cube maps. A cube map texture is a collection (array) of six 2D texture images that, together, usually form the various views of an environment. A cube map usually maps to the up, down, left, right, forward, and back directions. The ArraySize of the D3D11_TEXTURE2D_DESC will be 6 for cube map, images since there are six images in the array. An example of a cube map can be seen in Figure 3.17.

An example of a cube map.

Figure 3.17. An example of a cube map.

Texture Mapping Demo

In this section we’ve covered nearly enough information to implement texture mapping. The details that have not been covered we’ll discuss along the way. The Texture Mapping demo can be found on the website in the Chapter 3/ TextureMapping folder.

To start we’ll be building from the TriangleDemo code from earlier in this chapter. In the TextureDemo class we’ll add a shader resource view called colorMap_ (ID3D11ShaderResourceView) and a sampler state called color-MapSampler_ (ID3D11SamplerState).

A shader resource view is an object shaders use to access resources. When we load the texture into memory we must create a shader resource view in order to access that data via a shader, and that is what we will be binding to the input assembler. Shader resource views have other uses, such as providing general purpose data to DirectCompute for parallel computing, but in this chapter we will focus only on textures. The ID3D11Texture2D is a buffer of data, while the shader resource view allows us to view that buffer in a shader.

A sampler state allows us to access sampling state information of a texture. We’ll discuss this more when we look at how to create the object, but in general, note that the sampler state allows us to set properties such as the texture filtering and addressing—topics we’ll also discuss soon.

The TextureDemo class can be seen in Listing 3.9.

Example 3.9. The TextureDemo class from the TextureDemo.h header file.

class TextureDemo : public Dx11DemoBase
{
    public:
        TextureDemo( );
        virtual ~TextureDemo( );

        bool LoadContent( );
        void UnloadContent( );

        void Update( float dt );
        void Render( );

    private:
        ID3D11VertexShader* solidColorVS_;
        ID3D11PixelShader* solidColorPS_;
        ID3D11InputLayout* inputLayout_;
        ID3D11Buffer* vertexBuffer_;

        ID3D11ShaderResourceView* colorMap_;
        ID3D11SamplerState* colorMapSampler_;
};

Since we are performing texture mapping, we need to update the vertex structure to include two floating-point variables for the TU and TV texture coordinates. This can be done with the XMFLOAT2 structure.

When we create the input layout, we must also add to the D3D11_INPUT_ELE MENT_DESC array in the LoadContent function an element for the texture coordinate. The semantic name is "TEXCOORD", and its format will be DXGI_FORMAT_R32G32_FLOAT (since we are using only two float values). Also, for the offset we must use 12 because in the beginning of our vertex structure layout is a XMFLOAT3 for the position. Since an XMFLOAT3 is 12 bytes in size, our texture coordinates won’t appear in the vertex until after the 12th byte. Listing 3.10 shows the vertex structure, LoadContent, and UnloadContent functions for this demo.

Example 3.10. The vertex structure and the Texture demo’s LoadContent and UnloadContent.

struct VertexPos
{
    XMFLOAT3 pos;
    XMFLOAT2 tex0;
};


bool TextureDemo::LoadContent( )
{
    ... Load vertex Shader ...


    D3D11_INPUT_ELEMENT_DESC solidColorLayout[] =
    {
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
          0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,
        0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};

unsigned int totalLayoutElements = ARRAYSIZE( solidColorLayout );

d3dResult = d3dDevice_->CreateInputLayout( solidColorLayout,
    totalLayoutElements, vsBuffer->GetBufferPointer( ),
    vsBuffer->GetBufferSize( ), &inputLayout_ );


... Load Pixel Shader ...


VertexPos vertices[] =
{
    { XMFLOAT3(   1.0f,  1.0f, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },
    { XMFLOAT3(   1.0f, -1.0f, 1.0f ), XMFLOAT2( 1.0f, 0.0f ) },
    { XMFLOAT3(  -1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },

    { XMFLOAT3( -1.0f, -1.0f, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },
    { XMFLOAT3( -1.0f,  1.0f, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },
    { XMFLOAT3(  1.0f,  1.0f, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },
};


... Create Vertex Buffer ...


d3dResult = D3DX11CreateShaderResourceViewFromFile( d3dDevice_,
    "decal.dds", 0, 0, &colorMap_, 0 );

if( FAILED( d3dResult ) )
{
     DXTRACE_MSG( "Failed to load the texture image!" );
     return false;
}

D3D11_SAMPLER_DESC colorMapDesc;
ZeroMemory( &colorMapDesc, sizeof( colorMapDesc ) );
colorMapDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    colorMapDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
    colorMapDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
    colorMapDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
    colorMapDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
    colorMapDesc.MaxLOD = D3D11_FLOAT32_MAX;

    d3dResult = d3dDevice_->CreateSamplerState( &colorMapDesc,
        &colorMapSampler_ );

    if( FAILED( d3dResult ) )
    {
        DXTRACE_MSG( "Failed to create color map sampler state!" );
        return false;
    }

    return true;
}


void TextureDemo::UnloadContent( )
{
    if( colorMapSampler_ ) colorMapSampler_->Release( );
    if( colorMap_ ) colorMap_->Release( );
    if( solidColorVS_ ) solidColorVS_->Release( );
    if( solidColorPS_ ) solidColorPS_->Release( );
    if( inputLayout_ ) inputLayout_->Release( );
    if( vertexBuffer_ ) vertexBuffer_->Release( );

    colorMapSampler_ = 0;
    colorMap_ = 0;
    solidColorVS_ = 0;
    solidColorPS_ = 0;
    inputLayout_ = 0;
    vertexBuffer_ = 0;
}

The UnloadContent function releases our new objects, and the remainder of the LoadContent function loads our texture image. To load a texture and create a shader resource view in one call, we can use the Direct3D utility function D3DX11CreateShaderResourceViewFromFile. This function is useful when we want to both load a texture and create a new shader resource view conveniently at once. The D3DX11CreateShaderResourceViewFromFile function has the following prototype, whose parameters closely match the D3DX11CreateTextureFrom- File function:

HRESULT D3DX11CreateShaderResourceViewFromFile(
    ID3D11Device* pDevice,
    LPCTSTR pSrcFile,
    D3DX11_IMAGE_LOAD_INFO* pLoadInfo,
    ID3DX11ThreadPump* pPump,
    ID3D11ShaderResourceView** ppShaderResourceView,
    HRESULT* pHResult
);

The last new section of code in the LoadContent function is the creation of the sampler state. To create a sampler state object, we call CreateSamplerState of the Direct3D device, which takes as parameters the sampler description and the out address to the sampler state object that will store the results. The sampler description has the following structure:

typedef struct D3D11_SAMPLER_DESC {
  D3D11_FILTER               Filter;
  D3D11_TEXTURE_ADDRESS_MODE AddressU;
  D3D11_TEXTURE_ADDRESS_MODE AddressV;
  D3D11_TEXTURE_ADDRESS_MODE AddressW;
  FLOAT                      MipLODBias;
  UINT                       MaxAnisotropy;
  D3D11_COMPARISON_FUNC      ComparisonFunc;
  FLOAT                      BorderColor[4];
  FLOAT                      MinLOD;
  FLOAT                      MaxLOD;
} D3D11_SAMPLER_DESC;

The first member of the D3D11_SAMPLER_DESC is a D3D11_FILTER member that specifies how the texture being sampled will be filtered. Texture filtering refers to the way values are read and combined from the source and made available to the shader. Filtering can be used to improve quality but at the cost of more expensive texture sampling, since some filtering types can cause more than one value to be read and combined to produce a single color value that the shader sees. The various filtering types available in Direct3D 11 include the following:

typedef enum D3D11_FILTER {
  D3D11_FILTER_MIN_MAG_MIP_POINT
  D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR
  D3D11_FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT
  D3D11_FILTER_MIN_POINT_MAG_MIP_LINEAR
  D3D11_FILTER_MIN_LINEAR_MAG_MIP_POINT
  D3D11_FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR
  D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT
  D3D11_FILTER_MIN_MAG_MIP_LINEAR
  D3D11_FILTER_ANISOTROPIC
  D3D11_FILTER_COMPARISON_MIN_MAG_MIP_POINT
  D3D11_FILTER_COMPARISON_MIN_MAG_POINT_MIP_LINEAR
  D3D11_FILTER_COMPARISON_MIN_POINT_MAG_LINEAR_MIP_POINT
  D3D11_FILTER_COMPARISON_MIN_POINT_MAG_MIP_LINEAR
  D3D11_FILTER_COMPARISON_MIN_LINEAR_MAG_MIP_POINT
  D3D11_FILTER_COMPARISON_MIN_LINEAR_MAG_POINT_MIP_LINEAR
  D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT
  D3D11_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR
  D3D11_FILTER_COMPARISON_ANISOTROPIC
  D3D11_FILTER_TEXT_1BIT
} D3D11_FILTER;

The various numerations in the D3D11_FILTER enumeration account for different combinations of the texture’s MIN, MAG, and MIP levels. The comparison versions of these values are used to tell Direct3D to interpolate using the Boolean result of a comparison value with the value being fetched.

Point sampling, also known as nearest-neighbor sampling, for the filter is the fastest type of sampling. It works by fetching a single value from the texture with no further modifications. The value chosen is the texel closest to the pixel’s center.

Bilinear sampling, on the other hand, will perform bilinear interpolation on the value sampled at that texture coordinate as well as several samples surrounding it. The interpolated value (combined results) is what the shaders will see. The samples selected by this filtering are the four texels closest to the pixel’s center. This combination of multiple nearby values can smooth the results a bit, which can cause a reduction in rendering artifacts.

Trilinear filtering works by performing bilinear sampling around the texel for the two closest MIP levels and interpolating the results. When a surface goes from using one MIP level to another, there can be a noticeable change in the appearance of that surface at that moment of change. Interpolating between the closest MIP levels, after bilinearly filtering both levels, can greatly help to reduce these rendering artifacts.

Anisotropic filtering works by trilinearly sampling in a trapezoid area instead of a square area. Bilinear and trilinear filtering work best when looking directly at the surface because of the square area of sampling, but when viewing a surface at an angle, such as a floor or terrain in a 3D game, a noticeable amount of blurriness and rendering artifacts can appear. Anisotropic filtering takes angles into consideration and samples using a different shaped area.

The next three members of the D3D11_SAMPLER_DESC structure are used for texture address modes. Texture coordinates are specified in the range of 0.0 and 1.0 for each dimension of the texture. The texture address mode tells Direct3D how to handle values outside of this range. The texture address mode for the U, V, and R can be one of the following values:

typedef enum D3D11_TEXTURE_ADDRESS_MODE {
  D3D11_TEXTURE_ADDRESS_WRAP,
  D3D11_TEXTURE_ADDRESS_MIRROR,
  D3D11_TEXTURE_ADDRESS_CLAMP,
  D3D11_TEXTURE_ADDRESS_BORDER,
  D3D11_TEXTURE_ADDRESS_MIRROR_ONCE,
} D3D11_TEXTURE_ADDRESS_MODE;

WRAP for the texture address will cause the texture to wrap around and repeat. For example, if you have a square shape and you want a texture to display twice on it along the horizontal direction, you just need to specify 2.0 for the U texture coordinates for the right most vertices, as seen in Figure 3.18. This can be used to tile a texture along a single surface, which has the benefit of giving the appearance of more detail while using less data.

Wrapping texture coordinates.

Figure 3.18. Wrapping texture coordinates.

The MIRROR texture address mode will cause the texture to repeat but in a mirror direction (see Figure 3.19), while CLAMP will simply clamp the values in the 0.0 to 1.0 range. MIRROR_ONCE will mirror the texture one time, while MIRROR will continue it as specified. The BORDER address mode will set any pixels outside of the 0.0 to 1.0 range to a specified border color. The border color is specified by another member of the D3D11_SAMPLER_DESC called BorderColor, which is an array of four floating-point values.

Mirror texture address mode.

Figure 3.19. Mirror texture address mode.

The next member in the D3D11_SAMPLER_DESC structure is the level-of-detail (LOD) bias for the MIP. This value is an offset of the MIP level to use by Direct3D. For example, if Direct3D specifies that the MIP level to use is 2 and the offset is set to 3, then the MIP ultimately used will be level 5.

Following the LOD bias is the max anisotropic value to use during anisotropic filtering and the comparison function. The max anisotropic value can be between 1 and 16 and is not used for point or bilinear filtering. The COMPARISON function is a value used for the comparison versions of the texture filtering flags. The comparison flags are specified in D3D11_COMPARISON_FUNC and are essentially flags stating the comparison should pass if one value is less than equal to, greater than, etc. another value.

The last two members of the D3D11_SAMPLER_DESC structure are used to clamp the min and max MIP levels that can be used. For example, if the max is set to 1, then level 0 will not be accessed (note that level 0 is the highest resolution).

The last piece of the puzzle besides the shaders is the rendering function. To render our textured geometry, we must add the texture resource and set the sampler state. This is done by calling PSSetShaderResources and PSSetSamplers, which are used to set these items to the pixel shader. The PSSetShaderResources has the following prototype and takes as parameters the start slot to begin inserting resources, the number of resources being inserted, and an array of resources to insert:

void PSSetShaderResources(
  UINT StartSlot,
  UINT NumViews,
  ID3D11ShaderResourceView* const* ppShaderResourceViews
);

The PSSetSamplers function also takes the start slot and number of samplers being set along with the array of samplers you are providing. With the addition of those two functions to our rendering code from the previous demo, we are ready to see the effect in action. All that is missing is to modify the shaders to perform the actual effect. The rendering function used by the Texture Mapping demo can be seen in Listing 3.11.

Example 3.11. The Texture Mapping demo’s rendering function.

void TextureDemo::Render( )
{
    if( d3dContext_ == 0 )
        return;

    float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };
    d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

    unsigned int stride = sizeof( VertexPos );
    unsigned int offset = 0;

    d3dContext_->IASetInputLayout( inputLayout_ );
    d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );
    d3dContext_->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_
TRIANGLELIST );

    d3dContext_->VSSetShader( colorMapVS_, 0, 0 );
    d3dContext_->PSSetShader( colorMapPS_, 0, 0 );
    d3dContext_->PSSetShaderResources( 0, 1, &colorMap_ );
    d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_ );
    d3dContext_->Draw( 6, 0 );

    swapChain_->Present( 0, 0 );
}

In the shader, which can be seen in Listing 3.12, we have two new global shader objects called colorMap_ and colorSampler_. The colorMap_ object hast the type of Texture2D since it is being used for a 2D texture, while the colorSampler_ has the HLSL type of SamplerState. To bind these objects to the shader inputs we are providing in the rendering function, we must use the register keyword of HLSL. To bind to the first texture input we use t0, where t represents texture and 0 is the index to the first texture unit. The same can be said for the sampler state, where we use s0 for the first sampler. Since our PSSetSamplers and PSSetShaderResources functions can pass along an array of data for our shader to use, we must bind each HLSL variable to the specific index we want it to use. Since we only have one texture and one sampler state, we only need to use t0 and s0.

Another change in the shader is that we must update the vertex shader’s input structure to allow for texture coordinates and the pixel shader’s input structure to also allow texture coordinates. The vertex shader will take the texture coordinates from the vertex buffer and simply pass them to the pixel shader so that the pixel shader has access to it.

The pixel shader uses this texture coordinate (which, remember, is interpolated between vertices) with the texture object to read a color value. This is done by calling the HLSL Texture2D object’s Sample function, which takes as parameters the sampler state object to use for sampling and a pair of texture coordinates. Since we are reading from a 2D texture, the texture coordinates must be of the type float2.

Example 3.12. The Texture Mapping demo’s shaders.

Texture2D colorMap_ : register( t0 );
SamplerState colorSampler_ : register( s0 );


struct VS_Input
{
    float4 pos   : POSITION;
    float2 tex0 : TEXCOORD0;
};

struct PS_Input
{
    float4 pos   : SV_POSITION;
    float2 tex0 : TEXCOORD0;
};


PS_Input VS_Main( VS_Input vertex )
{
    PS_Input vsOut = ( PS_Input )0;
    vsOut.pos = vertex.pos;
    vsOut.tex0 = vertex.tex0;

    return vsOut;
}


float4 PS_Main( PS_Input frag ) : SV_TARGET
{
    return colorMap_.Sample( colorSampler_, frag.tex0 );
}

At this point you should be able to compile and execute the code. What you see should match Figure 3.20.

Screenshot of the Texture Mapping demo.

Figure 3.20. Screenshot of the Texture Mapping demo.

Sprites

2D games just wouldn’t be the same without sprites. Before you start thinking soda or fairies, sprites are 2D graphical representations of characters or objects within a game. Every tree, treasure chest, or dungeon creature you encounter is presented onscreen using a sprite. Sprites are one of the most widely used and easily understood aspects of 2D game programming. In 2D games, sprites are usually textured rectangles and squares that collectively create a virtual world.

Z-Ordering

In 2D games, some objects act as the background and some as the foreground, while others can be anything between the two, and these objects are known as layers. A layer can be thought of as a piece of transparent paper. For example, the layer that acts as the background layer will only have background sprites rendered to it, while the action layer might have all of the characters, weapons, power-ups, etc. The order in which the layers are rendered will determine which sprites are supposed to appear on top of what other sprites. In addition to having defined layers, there is also Z-ordering, which is the process in which objects are sorted before being drawn. Each sprite you create can be given a different Z-order value designating the depth at which it should be drawn. Sprites with a lower Z-order value are drawn behind those sprites with a higher value, giving the illusion of depth. For example, anything with a Z value of 1 is drawn on top of anything with a Z value of 0, and anything with a Z value of 2 is drawn on top of anything with a value of 0 or 1.

Using layers and/or Z-ordering allows us to define the rendering order in 2D games, but in 3D games we commonly use a concept known as depth testing for ensuring surfaces appear to exist in the correct order, which we’ll discuss more in Chapter 6 once we enter 3D graphics.

Hardware depth testing uses a special rendering buffer known as a depth buffer, and it is used to determine if the previous surface’s depth at that pixel is farther to the viewer than the current surface’s depth. In other words, if the previous surface that was rendered at this location is further away from the camera than the current surface we are rendering, then the current surface is closer to the camera and should be drawn on top of the previous data, else the previous surface is in front of the current surface, and those pixels should not be updated.

Depth testing is one of the ways 3D games are able to correctly render objects without having to pre-sort polygons based on the camera’s view (we’ll examine this in detail in Chapter 6). Since we are rendering using Direct3D, we could draw our 2D sprites in order of which sprites should appear in front of others without needing to use a depth buffer. By clearing the screen completely and drawing our sprites in layers, we could achieve proper results.

Sprite Image

The most important aspect of a sprite is its image. Traditionally, each sprite needs an image to be associated with it both from a gameplay perspective and a technical one. The image is what the sprite uses to convey its purpose to the player. For instance, your main character’s sprite in an RPG game may be the image of a knight or even a spaceship in a space battle. You can choose any image you’d like, but your sprite can’t be displayed without an image.

This is because in the early days of 2D games sprite images were drawn directly to the rendering destination and not to actual geometry. This is known as texture blitting. In Direct3D we can opt to draw 2D geometry and texture map that geometry rather than resorting to the old-fashioned methods of 2D game graphics. Texture blitting is essentially copying color values from the sprite image to the rendering destination over a specific location of the destination.

You can do this if you are creating a software rendering API for a 2D game, but using Direct3D it is easier for us to use 2D textured rectangle geometry versus trying to create a software rendering API.

Earlier you learned how to load a texture from the disk; now you’ll learn how textures can be used with sprites.

Getting Sprites to the Screen

When drawing sprites, the sprite object needs to know a few things about the environment in which it’s drawing. Not only does it need to know the position of each and every sprite to avoid collisions on the same layer between solid objects, but it needs to know the specifics about the area in which it’s drawing. This means the sprite object must be aware of the boundaries of the area where the sprites will be drawn. Normally, the size of this area is determined by the viewport associated with the Direct3D device.

The area in which the sprites are drawn is defined by its transformation matrices. Matrices are discussed in detail in Chapter 6, but for now we’ll briefly cover the basics of the various matrices we use in graphics rendering. A matrix in Direct3D is a 4 × 4 matrix. Visually it can be thought of as a table with 4 rows and 4 columns for 16 elements in total. These four rows and columns allow us to do a lot in game graphics and simulations.

Usually in graphics programming we start off with three main types of transformation matrices. The first is used for projection transformation, the second is used for view transformations, and the third is used for world transformations.

When you transform vertices by a matrix, you are manipulating those vertices based on the representation of that matrix. For example, a rotation matrix is used to rotate objects, a translation matrix is used to position objects, and a scaling matrix is used to scale matrices. But why do we need to do this?

Objects in 3D space are usually created using a 3D modeling application such as Autodesk’s 3D Studio Max. When models and objects are modeled, it is done in what is called model space. This means that the position of the vertices is specific to that model when it was modeled out in an editor. Usually models are created around the origin, just like we’ve been doing for our simple triangles and squares in previous chapters (where the origin of (0, 0, 0) is the center of the shape).

In a game we can have one or more instances of this model, and these models can move and interact with the scene. But all of the model’s data is defined in model space, and as it moves throughout the world, the location of the vertices must be updated to new positions. Also, when we position our models using a level/map editor or as the models are interacting with the game (e.g., physics, artificial intelligence, etc.), we use matrices to position, rotate, and scale the models. This allows us to have one model data loaded into memory for each instance and just render that model out multiple times for each instance that exists. So if we have an asteroid field with 1,000 rocks, we will need only one rock model and 1,000 matrices to represent the position and orientation of each asteroid. We could take it a step further and use Direct3D’s instancing feature to perform one draw call and supply in a buffer the per-instance transformation matrices of all 1,000 rocks.

Simply put, a transformation matrix allows us to define the position and orientation of our objects virtually. A model created in an editor such as 3D Studio Max will be in model-space. When we apply the model’s geometry with a transformation matrix that represents positional, scaling, and orientation values, then we are transforming that model-space geometry into world-space. World-space is a context that represents the position of an object in relation to other objects in the world around them.

In games, especially 3D games, cameras play a big role. The orientation of the camera also must be applied to the geometry in order to simulate the camera effect. The camera is represented by another matrix called the view matrix. The view matrix allows us to take models in its current space and transform it into view-space. When combined with the world matrix, we can create a single matrix that does both of these transformations in one called the world-view matrix.

In addition to simulating a camera’s position and orientation, we also add projection. A projection matrix is used to simulate orthogonal projection or to simulate perspective projection. We could also apply other effects such as the zoom of a sniper scope by manipulating the projection matrix as well as other effects.

We’ll discuss projection matrices in more detail in Chapter 6, but we’ll briefly go over them in a high-level view now. Orthographic projection is great for 2D elements because the visual depth of the objects being rendered is not applied to the final output. This means that if you have two boxes that are 10 units in size but 100 units away from each other along the Z axis, using orthogonal projection will cause them to appear to be right next to one other depth wise. Orthographic projection is not only good for 2D games but also 2D interface elements of a 3D game, such as health bars, ammo counters, timers, text, etc.

Perspective projection on the other hand adds perspective to our rendered objects. This means that objects get smaller on the screen as they move further away from the camera or larger as they get closer to it. This can be seen in real life if you look into the distance. Objects that are far away look small in the sense that the area of your vision it occupies gets smaller with distance. If you are standing in from of a building, the height of the building is a lot larger than if you were a mile away.

Combining the model, view, and projection matrices into one will create a new matrix known as the model-view projection matrix. This matrix is used by the vertex shader to transform incoming geometry to the final position it will have after applying the model’s position and orientation, the camera’s view, and the projection effects. Therefore, technically, we are using the vertex shader to generate the model’s real position (known as the absolute position) from its local position (known as its relative position). This is also how character animation systems work in bone animation, where matrices are used to define poses of animation for groups of geometry defined by bones.

There are a few functions that are part of the XNA Math library that are used to build projection matrices, and each will be discussed in more detail in Chapter 6. For now we’ll look at XMMatrixOrthographicOffCenterLH as an example to get things started off.

The function XMMatrixOrthographicOffCenterLH is used to create an orthographic projection matrix using a left-handed coordinate system (Chapter 6). The return value for this function is a XMMATRIX structure where the resulting projection matrix is placed. This function is used specifically to offset the coordinates of the view to make the top-left corner have a custom value for the X and Y axes (for example 0, 0). The function prototype for the XMMatrixOrthographicOffCenterLH can be seen as follows:

XMMATRIX XMMatrixOrthographicOffCenterLH(
    FLOAT ViewLeft,
    FLOAT ViewRight,
    FLOAT ViewBottom,
    FLOAT ViewTop,
    FLOAT NearZ,
    FLOAT FarZ
)

The parameters to the XMMatrixOrthographicOffCenterLH define the projection viewport, where the first parameter is the minimum X value, the second is the maximum X value, the third is the minimum Y value, and the fourth is the maximum Y value. The last two parameters are used to set the near and far clipping planes (Chapter 6). The near clipping plane will discard rendering of anything in front of that value, and the far clipping plane will discard rendering anything after that value. This creates something known as a view volume. The clipping planes play a much larger role in 3D graphics, where depth is more prevalent.

The projection matrix lets the system know the size of the grid on which the sprites will be placed. For example, if the viewport is 640 pixels wide and 480 pixels tall, then the projection matrix would restrict visible sprite drawing to that area. Sprites positioned outside of this area would not be visible on the screen. This is handled by Direct3D in hardware.

The projection matrix only needs to be changed if the size of the viewport changes or if we are performing some special camera effects that require us to switch projection matrices. Do not worry if this is new information, because we’ll cover it in detail in Chapter 6. To get started with matrices, you don’t need to understand the internal details as long as you understand generally what the XNA Math functions are doing for you. Over time it will become more important to understand the internals so that you can leverage that knowledge to do anything you wish virtually.

Positioning and Scaling Sprites

Now that the sprites know the extent of their environment, positioning them within it is possible. The act of moving an object within a space is called translation, where translation is another term for position. Sprites being two dimensional in nature can be translated in two directions, X and Y, although in Direct3D we technically could move in 3D space even for 2D objects.

If you want to position a sprite in the center of a 640 × 480 display area, you would translate the sprite to an X, Y position of (320, 240). This, in effect, moves the sprite 320 pixels horizontally and 240 pixels vertically when using an orthogonal projection that defines the upper left corner of the screen as (0, 0). When sprites are translated, they’re moved based on an internal point called the translation point. The translation point on a sprite is by default the location of the sprite’s model-space origin. When using sprites for characters within a game, it is common for the translation point to be moved to the top-left corner of the sprite.

When translating a sprite it is necessary to create another matrix called the translation matrix, which was briefly mentioned earlier in this chapter. The translation matrix, once defined, is used by Direct3D to position the sprite (or any geometric object). The translation matrix can be created by using the function XMMatrixTranslation. The prototype to the XMMatrixTranslation can be seen as follows:

XMMATRIX XMMatrixTranslation( FLOAT OffsetX, FLOAT OffsetY, FLOAT OffsetZ )

The return value for XMMatrixTranslation is the translation matrix, and the parameters it takes are the X, Y, and Z position values to move by. We also could create a rotation and scaling matrix using other XNA Math functions that will be examined in Chapter 6.

This was a bird’s-eye view of matrices, but it should be clear as to their purpose. We create a translation matrix to position objects, a rotation matrix to rotate them, and scaling matrices to scale them (i.e., shrink or enlarge). The concatenation of these three matrices together can be used to create the world transformation matrix. Concatenating the world matrix with the view and projection matrices is used to create the model-view projection matrix, which is used primarily by the vertex shader. The purpose of the vertex shader is to manipulate incoming vertices, and matrix transformation is one of the tasks vertex shaders commonly perform. We won’t look at cameras and views until Chapters 6, 7, and 8, so we will largely ignore the view matrix until then.

The Game Sprite Demo

On the companion website in the Chapter3 folder is a demo called Game Sprites (Chapter3/GameSprite folder). The purpose of this demo is to create a game sprite structure that can be used to represent a single instance of a sprite. We will write code that draws sprites to the screen, which can be used as the first steps toward creating a 2D game.

So far we have a good idea of the values we’ll need for each sprite. To begin we’ll create a simple game sprite class with the following members:

  • Position

  • Rotation

  • Scale

  • Sprite image

  • Vertex buffer

Since this is a beginner’s book, we will create a vertex buffer and texture for each unique sprite and render them for each sprite instance in the scene (i.e., more than one sprite can use the same texture and vertex buffer). In a commercial game where large amounts of draw calls and state changes are causing performance issues, we’d opt to consider batching geometry and using texture atlases. These topics are move advanced and require a more intermediate level of skill with Direct3D to properly implement.

Note

A word of advice when considering premature optimizations is to avoid it. Only optimize when a bottleneck has been identified, and address the problems of any bottlenecks until an acceptable level of performance is obtained. Optimizing for the sake of doing while developing code can lead to wasted effort on code that is not a performance burden or on code that is not a priority. In other words, don’t optimize some code when there are “bigger fish” (worse-performing code and bottlenecks) to fry.

The game sprite class is fairly simple, and its purpose is to use the position, rotation, and scale values to build the sprite’s world matrix when it is rendered. Once the world matrix is built, it is passed to the vertex shader using a constant buffer. In order to avoid loading the same texture or creating the same vertex buffer over and over again, we’ll create them once in this demo and use them for each sprite we are drawing. Since our focus is on rendering multiple sprites and on using matrices to position them, we won’t overly complicate things more than necessary.

The game sprite class can be seen in Listing 3.13. The Game Sprite demo’s class can also be seen in Listing 3.14. In the demo’s class we have an array of sprite resources that we will be drawing, a constant buffer for allowing us to pass data to the vertex shader, a view projection matrix (as a XMMATRIX), and a new blend state object.

Example 3.13. The GameSprite class from GameSprite.h.

#include<xnamath.h>


class GameSprite
{
    public:
        GameSprite( );
        virtual ~GameSprite( );

        XMMATRIX GetWorldMatrix( );

        void SetPosition( XMFLOAT2& position );
        void SetRotation( float rotation );
        void SetScale( XMFLOAT2& scale );


    private:
        XMFLOAT2 position_;
        float rotation_;
        XMFLOAT2 scale_;
};

Example 3.14. The Game Sprite demo class definition from GameSpriteDemo.h.

#include"Dx11DemoBase.h"
#include"GameSprite.h"


class GameSpriteDemo : public Dx11DemoBase
{
    public:
        GameSpriteDemo( );
        virtual ~GameSpriteDemo( );

        bool LoadContent( );
        void UnloadContent( );

        void Update( float dt );
        void Render( );

    private:
        ID3D11VertexShader* solidColorVS_;
        ID3D11PixelShader* solidColorPS_;

        ID3D11InputLayout* inputLayout_;
        ID3D11Buffer* vertexBuffer_;

        ID3D11ShaderResourceView* colorMap_;
        ID3D11SamplerState* colorMapSampler_;
        ID3D11BlendState* alphaBlendState_;

        GameSprite sprites_[2];
        ID3D11Buffer* mvpCB_;
        XMMATRIX vpMatrix_;
};

The blend state object has the type of ID3D11BlendState. We will use it in order to render our textured sprites using alpha transparency. This means that we must use 32-bit texture images with an alpha channel. The texture we are going to display can be seen in in Figure 3.21 in Photoshop.

Texture with transparent areas.

Figure 3.21. Texture with transparent areas.

The constant buffer will be used to send the model-view projection matrix to the vertex shader so that the shader can transform the incoming geometry. Since there is no camera, and since the projection matrix does not change unless we resize the window, we can calculate the view-projection matrix once and use it within the rendering function when we calculate the full model-view projection matrix, which is why we have a XMMATRIX as a class member.

Creating and Rendering the Game Sprite

The GameSprite.cpp source file implements the GameSprite member functions, which in this demo we simply have set up as a way to store an individual sprite’s position, rotation, and scale as well as a way to build the sprite’s world (model) matrix. The entire GameSprite.cpp source file can be seen in Listing 3.15. You will notice that the scale is set to 1.0f for each axis, because anything less than 1.0f will shrink the sprite, while anything above 1.0f will make it grow. A value of 1.0f keeps the sprite’s size unchanged.

Example 3.15. The GameSprite.cpp source file.

#include<d3d11.h>
#include<d3dx11.h>
#include"GameSprite.h"


GameSprite::GameSprite( ) : rotation_( 0 )
{
    scale_.x = scale_.y = 1.0f;
}


GameSprite::~GameSprite( )
{

}
XMMATRIX GameSprite::GetWorldMatrix( )
{
    XMMATRIX translation = XMMatrixTranslation( position_.x, position_.y, 0.0f
);
    XMMATRIX rotationZ = XMMatrixRotationZ( rotation_ );
    XMMATRIX scale = XMMatrixScaling( scale_.x, scale_.y, 1.0f );

    return translation * rotationZ * scale;
}


void GameSprite::SetPosition( XMFLOAT2& position )
{
    position_ = position;
}


void GameSprite::SetRotation( float rotation )
{
    rotation_ = rotation;
}


void GameSprite::SetScale( XMFLOAT2& scale )
{
    scale_ = scale;
}

To render a game sprite, we draw each game sprite by building the world matrix, applying that world matrix to the vertex shader’s constant buffer using VSSetConstantBuffer, we bind the texture and shaders the sprite uses, and we then render the geometry for that sprite resource. If we want to render multiple game sprites, we simply need to do this for each game sprite in the scene, which can be easily accomplished via a loop. The rendering function from the GameSprite demo can be seen in Listing 3.16. Since the only difference between each sprite is its model-view projection matrix, the matrix is the only thing we need to set during each pass of the loop. Each time we call Draw we will draw out the sprite at the position of the last set model-view projection matrix.

Example 3.16. The rendering function from the Game Sprite demo.

void GameSpriteDemo::Render( )
{
    if( d3dContext_ == 0 )
        return;

    float clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f };
    d3dContext_->ClearRenderTargetView( backBufferTarget_, clearColor );

    unsigned int stride = sizeof( VertexPos );
    unsigned int offset = 0;

    d3dContext_->IASetInputLayout( inputLayout_ );
    d3dContext_->IASetVertexBuffers( 0, 1, &vertexBuffer_, &stride, &offset );
    d3dContext_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_
TRIANGLELIST);

    d3dContext_->VSSetShader( solidColorVS_, 0, 0 );
    d3dContext_->PSSetShader( solidColorPS_, 0, 0 );
    d3dContext_->PSSetShaderResources( 0, 1, &colorMap_ );
    d3dContext_->PSSetSamplers( 0, 1, &colorMapSampler_ );

    for( int i = 0; i < 2; i++ )
    {
        XMMATRIX world = sprites_[i].GetWorldMatrix( );
        XMMATRIX mvp = XMMatrixMultiply( world, vpMatrix_ );
        mvp = XMMatrixTranspose( mvp );

        d3dContext_->UpdateSubresource( mvpCB_, 0, 0, &mvp, 0, 0 );
        d3dContext_->VSSetConstantBuffers( 0, 1, &mvpCB_ );

        d3dContext_->Draw( 6, 0 );
    }

    swapChain_->Present( 0, 0 );
}

In the rendering code we set the constant buffer to the vertex shader by calling VSSetConstantBuffers. A constant buffer, like all DirectX 11 buffers, is of the type ID3D11BUFFER and is created in the LoadContent function. As we’ve mentioned earlier, a constant buffer is created by setting the buffer descriptor’s BindFlags member to D3D11_BIND_CONSTANT_BUFFER.

The game sprite and the various resources it uses are created in the LoadContent function of the Game Sprite demo and are released in the UnloadContent function, both of which can be seen in Listing 3.17 for the code relevant to game sprites. This demo builds directly off of the Texture Map demo from earlier in this chapter, so we’ll leave out the upper portion of the LoadContent function that is exactly the same from that demo.

Example 3.17. The LoadContent and UnloadContent functions.

bool GameSpriteDemo::LoadContent( )
{
     // ... Previous code from the Texture Map demo...

     ID3D11Resource* colorTex;
     colorMap_->GetResource( &colorTex );

     D3D11_TEXTURE2D_DESC colorTexDesc;
     ( ( ID3D11Texture2D* )colorTex )->GetDesc( &colorTexDesc );
     colorTex->Release( );

     float halfWidth = ( float )colorTexDesc.Width / 2.0f;
     float halfHeight = ( float )colorTexDesc.Height / 2.0f;


     VertexPos vertices[] =
     {
         { XMFLOAT3(  halfWidth,  halfHeight, 1.0f ), XMFLOAT2( 1.0f, 0.0f ) },
         { XMFLOAT3(  halfWidth, -halfHeight, 1.0f ), XMFLOAT2( 1.0f, 1.0f ) },
         { XMFLOAT3( -halfWidth, -halfHeight, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },

         { XMFLOAT3( -halfWidth, -halfHeight, 1.0f ), XMFLOAT2( 0.0f, 1.0f ) },
         { XMFLOAT3( -halfWidth,  halfHeight, 1.0f ), XMFLOAT2( 0.0f, 0.0f ) },
         { XMFLOAT3(  halfWidth,  halfHeight, 1.0f ), XMFLOAT2( 1.0f, 0.0f ) },
     };

     D3D11_BUFFER_DESC vertexDesc;
     ZeroMemory( &vertexDesc, sizeof( vertexDesc ) );
     vertexDesc.Usage = D3D11_USAGE_DEFAULT;
     vertexDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
     vertexDesc.ByteWidth = sizeof( VertexPos ) * 6;

     D3D11_SUBRESOURCE_DATA resourceData;
     ZeroMemory( &resourceData, sizeof( resourceData ) );
     resourceData.pSysMem = vertices;

     d3dResult = d3dDevice_->CreateBuffer( &vertexDesc, &resourceData,
         &vertexBuffer_ );

     if( FAILED( d3dResult ) )
     {
         DXTRACE_MSG( "Failed to create vertex buffer!" );
         return false;
     }


     D3D11_BUFFER_DESC constDesc;
         ZeroMemory( &constDesc, sizeof( constDesc ) );
         constDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
         constDesc.ByteWidth = sizeof( XMMATRIX );
         constDesc.Usage = D3D11_USAGE_DEFAULT;

         d3dResult = d3dDevice_->CreateBuffer( &constDesc, 0, &mvpCB_ );

         if( FAILED( d3dResult ) )
       {
           return false;
       }


     XMFLOAT2 sprite1Pos( 100.0f, 300.0f );
     sprites_[0].SetPosition( sprite1Pos );

     XMFLOAT2 sprite2Pos( 400.0f, 100.0f );
     sprites_[1].SetPosition( sprite2Pos );

     XMMATRIX view = XMMatrixIdentity( );
     XMMATRIX projection = XMMatrixOrthographicOffCenterLH( 0.0f, 800.0f,
        0.0f, 600.0f, 0.1f, 100.0f );

    vpMatrix_ = XMMatrixMultiply( view, projection );

    D3D11_BLEND_DESC blendDesc;
    ZeroMemory( &blendDesc, sizeof( blendDesc ) );
    blendDesc.RenderTarget[0].BlendEnable = TRUE;
    blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
    blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
    blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_ONE;
    blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
    blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ZERO;
    blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
    blendDesc.RenderTarget[0].RenderTargetWriteMask = 0x0F;

    float blendFactor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };

    d3dDevice_->CreateBlendState( &blendDesc, &alphaBlendState_ );
    d3dContext_->OMSetBlendState( alphaBlendState_, blendFactor, 0xFFFFFFFF );

    return true;
}


void GameSpriteDemo::UnloadContent( )
{
    if( colorMapSampler_ ) colorMapSampler_->Release( );
    if( colorMap_ ) colorMap_->Release( );
    if( solidColorVS_ ) solidColorVS_->Release( );
    if( solidColorPS_ ) solidColorPS_->Release( );
    if( inputLayout_ ) inputLayout_->Release( );
    if( vertexBuffer_ ) vertexBuffer_->Release( );
    if( mvpCB_ ) mvpCB_->Release( );
    if( alphaBlendState_ ) alphaBlendState_->Release( );

    colorMapSampler_ = 0;
    colorMap_ = 0;
    solidColorVS_ = 0;
    solidColorPS_ = 0;
    inputLayout_ = 0;
    vertexBuffer_ = 0;
    mvpCB_ = 0;
    alphaBlendState_ = 0;
}

The first thing we do in the LoadContent function is to obtain access to the loaded texture. We are doing this so that we can obtain the texture’s width and height. This information is used during the creation of the vertex buffer so that the size of the image dictates how large the sprite is on screen.

After we create the vertex buffer using the information (width and height) we’ve obtained from the texture image, we then create the constant buffer. Since we are setting the constant buffer in each frame, we will hold off until the rendering function before filling in its contents.

The last portion of the LoadContent function sets the positions of our two sprites, creates our orthographic-based view-projection matrix, and creates our blend state. We use the blend state to enable alpha transparency, which is a technique for using the alpha channel of a color to dictate how visible it is. An alpha value of 1.0f is fully visible, 0.0 is invisible, and everything between is semi transparent.

To create the blend state we call the device’s CreateBlendState function, and to set the blend state we call the context’s OMSetBlendState function. CreateBlend-State takes a blend description and the out address of the blend state object being created. OMSetBlendState takes the blend state that was created by CreateBlendState, a blend factor for each color channel, and a blend mask. The last two parameters are for more advanced blending, but for now we can set them to default values.

The blend description is of the type D3D11_BLEND_DESC. Some of the members of the D3D11_BLEND_DESC are for advanced blending for topics such as multi-sampling, alpha-to-coverage, etc., but the minimum we need to set deals with the render target’s source and destination blend. In the code listing you can see that we enable alpha blending on the main rendering target and set up the source of the blend to rely on the source color’s alpha value and destination to rely on 1 – alpha (inverse of the alpha). This allows us to take the alpha value of the incoming source color and blend it with the destination based on 1 – alpha. In other words, anything with an alpha value of 0.0f for the source will cause the destination’s color to appear, while anything with a 1 will cause the source’s color to fully appear. Anything between 0.0f and 1.0f is a blend between the source color (the color of the surface we are rendering) and the destination (the contents of the rendered scene so far).

The last piece of the puzzle, the HLSL shader code, can be seen in Listing 3.18. A screenshot of the running demo can also be seen in Figure 3.22. In the HLSL code we set up the constant buffer using the keyword cbuffer. The cbuffer keyword allows us to create a single constant buffer in HLSL, but the contents within the constant buffer are accessed as if they were normally defined global variables, as you can see in the vertex shader where mvp_ is used directly. The constant buffer we are creating is binding to the input register of b0, whereas textures bind to t0 and higher, and samplers bind to s0 and higher.

Game Sprite demo screenshot.

Figure 3.22. Game Sprite demo screenshot.

The major change in this HLSL set of shaders is the fact that the vertex shader is using the model-view projection matrix to transform the incoming vertex. This is done by simply multiplying the vertex by the matrix. The result is the transformed vertex that is passed down the pipeline to the remaining stages. The pixel shader is unchanged from the Texture Map demo.

Example 3.18. The Game Sprite demo’s HLSL shader code.

cbuffer cbChangesPerFrame : register( b0 )
{
    matrix mvp_;
};


Texture2D colorMap_ : register( t0 );
SamplerState colorSampler_ : register( s0 );


struct VS_Input
{
    float4 pos   : POSITION;
    float2 tex0 : TEXCOORD0;
};

struct PS_Input
{
    float4 pos   : SV_POSITION;
    float2 tex0 : TEXCOORD0;
};


PS_Input VS_Main( VS_Input vertex )
{
    PS_Input vsOut = ( PS_Input )0;
    vsOut.pos = mul( vertex.pos, mvp_ );
    vsOut.tex0 = vertex.tex0;

    return vsOut;
}


float4 PS_Main( PS_Input frag ) : SV_TARGET
{
    return colorMap_.Sample( colorSampler_, frag.tex0 );
}

Summary

At this point you should have a basic understanding of how sprites and textures work. Using the information presented in this chapter, it’s now possible to create a sprite-based demo that you can hopefully build on to create a simple 2D game.

Using Direct3D to draw 2D graphics really came down to using only the X and Y positions of our geometry. We then were able to use the various vector and matrix math code provided by Direct3D to manipulate these 2D objects as we see fit. Technically there is no true 2D mode to Direct3D, and 2D is achieved either using orthographic projection or by placing geometry with only X and Y axes really close to each other in depth. In this chapter we’ve used orthographic projection, so even if the depth was great between sprites, it wouldn’t affect how we view them onscreen. If we used perspective projection, which is commonly used for 3D rendering, then the depth distance plays a greater visual factor and allows us to switch into the realm of 3D.

What You Have Learned

In this chapter you learned the following:

  • How textures are loaded.

  • What a sprite is and how it’s used.

Chapter Questions

Answers to the following chapter review questions can be found in Appendix A on this book’s companion website.

1.

Define what a texture is.

2.

Define what a sprite is. How are sprites different from textures?

3.

List at least five different types of textures we’ve mentioned in this chapter.

4.

How many bits are RGB images? How many bits are RGBA images?

5.

Define a vertex.

6.

Define a triangle.

7.

What is the purpose of a vertex buffer? What is the Direct3D 11 object type of buffers?

8.

List at least five attributes of a vertex.

9.

Define an input layout and what it is used for in Direct3D.

10.

What is a vertex shader, and what Direct3D function is used to set (apply) one?

11.

What is a pixel shader, and what Direct3D function is used to set (apply) one?

12.

List at least three input assembler functions we’ve used in the rendering functions of our demos.

13.

What are MIP levels?

14.

What are sampler states, and what is their purpose in Direct3D and HLSL?

15.

What are blend states, and for what purpose did we use them in this chapter?

16.

Define a matrix.

17.

What are the various matrices that make up the model-view projection matrix, and what are their purposes?

18.

What are texture address modes? Define each mode of the D3D11_TEXTURE_ADDRESS_MODE enumeration.

19.

Enabling alpha transparency requires us to create and set what Direct3D 11 object?

20.

Why did we decide to use a XMFLOAT4X4 instead of XMMATRIX as the class member in the Game Sprite demo?

On Your Own

Create your own sprite image in the image editor of your choice. Use that newly created image as the texture for the second sprite object.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset