While browser-based games may continue to rely solely on the CPU for calculating
shape and color, standalone games, whether professionally produced or the work
of amateurs using tools such as Microsoft's XNA Game Studio Express, absolutely
require the understanding and development of shaders. Showcase games may have
more than 1,000 shaders, but even the simplest arcade-style game requires a
handful if it is to have any sparkle.
With the advent of Microsoft's free XNA Game Studio Express and the widespread
adoption of high-powered GPUs, such as those in AMD's ATI product line, game
development has become approachable by any enthusiast. Shader programming is
required for even simple games, but AMD's RenderMonkey environment makes these
tasks easy. This article will discuss shader basics and the development of some
minimal pixel shaders for lighting, and a follow-up will go into vertex shaders.
As GPUs have increased in capability, shader writing has evolved from an assembly-language
task to a higher-level one. With the increase in the number of shaders used
in a given project, a language such as the C-like High-Level Shader Language
(HLSL) for DirectX or its OpenGL counterpart GLSL becomes almost mandatory in
order to accommodate workflow and schedules.
The ideal tool for developing shaders is AMD's ATI RenderMonkey (the latest
version, 1.62, is available for free download at http://ati.amd.com/developer/rendermonkey/index.html).
As you can see in Figure
1, RenderMonkey is a complete development environment for shaders. The Workspace
panel along the left-hand side of the screen allows you to interactively manipulate
the shader parameters that will ultimately be passed in from the game, effectively
decoupling the development of shaders from game source (at least initially).
In this case, for a pixel shader that Phong-lights a textured surface, we need
such things as the position vectors for the light and the eye, the View and
ViewProjection matrices, and the texture and model to use.
The Shader Editor is the main workspace and shows just how simple HLSL can
be, with intrinsic functions for common linear algebra functions. In addition
to functions such as normalize()
and dot(), notice the use of
operator overloading to make vector and matrix multiplication simple (these
operations are actually available at the assembly-language level, which goes
to show how different GPU capabilities are!). Aside from the texture-mapping
assignment in tex2D(), this function
is essentially straight out of a textbook on Phong shading. Notice also that
the ASM-level instruction count of the shader is shown in the status bar of
the Shader Editor - this is very handy, since instruction counts can be very
low in GPUs that are only a few years old.
While RenderMonkey is a great development environment, programming a 3D game
has always been difficult. The release of Microsoft's XNA Framework has shattered
that barrier, allowing enthusiasts to easily create games that are limited only
by their imagination and their hardware. Further, the combination of RenderMonkey
and XNA Game Studio Express provides a complete "free beer" tool chain,
opening the door to the development not just of games but of graphically intense
"demos" and visualizations of such things as physics, fluid dynamics,
fractals, and AI.
Listing 1 shows a Microsoft
.FX file, including the world's simplest vertex and pixel shaders: Every pixel
is set to be the same color of red (Line 54). The vertex shader requires a single
parameter passed in from the application: matViewProjection. Listing
2 shows the world's simplest XNA demo, which shows a cube sitting in space,
rendered with the "red" pixel shader. The InitializeCamera() function
(lines 39-54) is boiler-plate: the view Matrix is set as the cameraPos when
it is pointed at the origin (Vector3.Zero) and oriented normally (Vector3.Up).
The projection matrix combines the field of view, aspect ratio, and near and
far clip planes. These matrices are multiplied to create the viewProjection
matrix, which defines the "view frustrum" we will be rendering.
The LoadGraphicsContent() function
(lines 70-88) loads the cube model and then the "red" Effect. An Effect
contains a collection set of Passes, each of which contains vertex and pixel
shaders. In this case, we have a single pass that contains our simple "red"
shader. Line 77 shows how we set the shader variable matViewProjection
to the value of the game variable viewProjection:
effect.Parameters["matViewProjection"].SetValue(viewProjection);
The effect is then Cloned to all the parts of the model. When the game runs,
the Draw() function (lines 128-161)
steps over all the meshes in the model, the Effects in the mesh, the Passes
in the Effect, and the ModelMeshParts in the Mesh and, well, draws them. The
output is essentially a cube-shaped red stencil.
The power of shaders begins to be apparent in Listing
3, a pixel shader that implements unlit texture-mapping. As you can see,
a new parameter base_Tex is required.
Listing 4 shows the only
code changes needed in the source code: the loading of a Texture and the passing
in of that to the effect. The output changes dramatically - it's not "Gears
of War" but it does show how RenderMonkey can decouple the development
of shaders from the development of game code and how shaders are the core of
modern rendering.
Finally, to bring us full circle, Listing
5 shows the (again, trivial) code changes required to support the textured
Phong shader shown in Figure
1. Of course, in a real application, you don't use instance variables of
the Game object to store and associate matrices, models, effects, textures,
and parameters, but rather you use structs and objects to encapsulate them and
update them over time. The ease with which RenderMonkey-created HLSL shaders
and variables can be loaded and manipulated in source code, though, is the key
to a high productivity workflow. You can download the source for these demos
at http://developer.amd.com/assets/152-demos.zip.
There are many ubiquitous pixel shaders: Phong and Blinn-Phong lighting, bump
mapping, reflections, and so forth. There are also many less-used algorithms
that may or may not be implementable as high-framerate shaders. You can find
these algorithms in graphics texts, popular sources such as Game
Developer Magazine and the Gamasutra
Website, and discussion forums. However, you'll generally find that source-code
recipes need some amount of tweaking to fit your games' matrices and naming
styles. RenderMonkey is the ideal environment for working through these kinds
of tweaks, since changes can be made and tested so rapidly.
In my next article, I'll discuss the use of RenderMonkey to develop vertex
shaders to really give your application that pro-game glow.
Larry O'Brien founded Game Developer magazine, worked in the game industry
developing middleware for MMORPGs and as a performance consultant, and has taught
game programming at the Computer Game Developer's Conference and Berkeley University
Extension. He now lives in Hawai'i and blogs at http://www.knowing.net/