Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
83 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

This chapter covers the basics of pixel shader programming. You are going to learn in the following pages how to code a pixel shader-driven application by using the same lighting reflection models as in the second part of this introduction. There is one big difference: this time we will calculate light reflection on a per-pixel basis, which leads to a bumpy impression of the surface.

Most of the effects that modify the appearance of a surface in a lot of games are calculated today on a per-vertex basis. This means that the lighting/shading calculations are done for each vertex of a triangle, as opposed to each pixel that gets rendered or per-pixel. In some cases per-vertex lighting produces noticeable artifacts. Think of a large triangle with a light source close to the surface. As long as the light is close to one of the vertices of the triangle, you can see the diffuse and specular reflection on the triangle. When the light moves towards the center of the triangle, the rendering gradually loses these lighting effects. In the worst case, the light is directly in the middle of the triangle and there is nearly no effect visible on the triangle, where there should be a triangle with a bright spot in the middle.

That means that a smooth-looking object needs a lot of vertices or a high level of tesselation, otherwise the coarseness of the underlying geometry is visible. That's the reason why the examples in part 2 of this introduction used a highly tesselated Bézier patch class, to make the lighting effect look smooth.

Our first example shows a directional light source with a diffuse reflection model.


Figure 1 - Per-Pixel Diffuse Reflection

Like all the previous and all the upcoming examples, this example is based on the Common Files framework provided with the DirectX 8.1 SDK (Read more in part 2 of this introduction). Therefore <Alt>+<Enter> switches between the windowed and full-screen mode, <F2> gives you a selection of the usable drivers and <Esc> will shutdown the application. Additionally the <B> key toggles diffuse lighting, the <W> and <S> keys allows to zoom in and out, the left mouse button rotates the globe, the right mouse button moves the directional light and the <P> key toggles between ps.1.1 and ps.1.4, if available.

The following examples won't run on graphics hardware that doesn't support pixel shaders. On hardware that does not support ps.1.4 a message "ps.1.4 Not Supported" is displayed.

As in the previous parts of this introduction, we will track the life cycle of the pixel shader in the following pages.

Check for Pixel Shader Support

To be able to fallback on a different pixel shader version or the multitexturing unit, the pixel shader support of the graphics hardware is checked with the following piece of code in ConfirmDevice():

if( pCaps->PixelShaderVersion < D3DPS_VERSION(1,4) )
  m_bPS14Available = FALSE;
if( pCaps->PixelShaderVersion < D3DPS_VERSION(1,1) )
  return E_FAIL;

The value in the D3DCAPS structure, which is filled by GetDeviceCaps(), corresponds to the ps.x.x instruction at the beginning of the pixel shader source. Therefore the macro D3DPS_VERSION(1,4) checks for support of ps.1.4. If ps.1.4 is not available, the BOOL variable m_bPS14Available is set to false and the user is prevented from switching between the pixel shader versions.

Set Texture Operation Flags (with D3DTSS_* flags)

The pixel shader functionality replaces the D3DTSS_COLOROP and D3DTSS_ALPHAOP operations and their associated arguments and modifiers, but the texture addressing, bump environment, texture filtering, texture border color, mip map, texture transform flags (exception ps.1.4: D3DTFF_PROJECTED) are still valid. The texture coordinate index is still valid together with the fixed-function T&L pipeline (Read more in part 3 of this introduction). Using the D3DTEXF_LINEAR flag for the D3DTSS_MINFILTER and D3DTSS_MAGFILTER texture stage states indicates the usage of bilinear filtering of textures (Direct3D terminology: linear filtering). This is done in RestoreDeviceObjects() for the color map:

m_pd3dDevice->SetTextureStageState( 0, D3DTSS_MINFILTER, D3DTEXF_LINEAR);
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR);
Switching on mip map filtering or trinlinear filtering for the color and the normal map with the following statement could be a good idea in production code:
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_MIPFILTER, D3DTEXF_LINEAR);

Set Texture (with SetTexture())

With proper pixel shader support and the texture stage states set, the textures are set with the following calls in Render():

// diffuse lighting

  //no lighting, just base color texture

In case the user switched the diffuse lighting off, only the color texture is set. Otherwise the color texture and the normal or bump map texture are set.

Define Constants (with SetPixelShaderConstant()/def)

We set four constant values into c33 in RestoreDeviceObjects(). These constants are used to bias the values that should be send via the vertex shader color output register to v0 of the pixel shader:

// constant values
D3DXVECTOR4 half(0.5f,0.5f,0.5f,0.5f);
m_pd3dDevice->SetVertexShaderConstant(33, &half, 1);

In FrameMove() the light direction, the concatenated world-, view- and projection matrix and the world matrix is set into c12, c8 and c0:

// light direction
m_pd3dDevice->SetVertexShaderConstant(12, &m_LightDir, 1 ); // light direction

// world * view * proj matrix
D3DXMatrixTranspose(&matTemp,&(m_matWorld * m_matView * m_matProj));
m_pd3dDevice->SetVertexShaderConstant(8, &matTemp, 4);

// world matrix
m_pd3dDevice->SetVertexShaderConstant(0, &matTemp, 4);

This example uses a directional light, that is moveable with the right mouse button. The mouse movement is tracked via the WM_MOUSEMOVE in the Windows message procedure function MsgProc().

Pixel Shader Instructions

Provided in all the upcoming examples is a very simple ps.1.1 pixel shader in diff.psh, that only displays the color map. Additionally a ps.1.1 pixel shader in diffDot3.psh and a ps.1.4 pixel shader in diffDot314.psh, that are specific for the respective example can be found in the accompanying example directory on the DVD:

tex t0 //sample texture
mov r0,t0

tex t0 ; color map
tex t1 ; normal map
dp3 r1, t1_bx2, v0_bx2 ; dot(normal,light)
mul r0,t0, r1 ; modulate against base color

texld r0, t0 ; color map
texld r1, t0 ; normal map
dp3 r2, r1_bx2, v0_bx2 ; dot(normal, light)
mul r0, r0, r2

Contrary to vertex shaders, the instructions in a pixel shader need a specific ordering, which is called the instruction flow. This instruction flow differs between ps.1.1 - ps.1.3 and ps.1.4. ps.1.1 - ps.1.3 allow four types of instructions, that must appear in the order shown below:

  • version instruction: ps.1.1
  • constant instruction: def c0, 1.0, 1.0, 1.0, 1.0
  • texture address instructions: tex*
  • arithmetic instructions: mul, mad, dp3 etc.

Every pixel shader starts with the version instruction. It is used by the assembler to validate the instructions which follow. After the version instruction a constant definition could be placed with def. Such a def instruction is translated into a SetPixelShaderConstant() call, when SetPixelShader() is executed.

The next group of instructions are the texture address instructions. They are used to load data into the tn registers and additionally in ps.1.1 - ps.1.3 to modify texture coordinates. Up to four texture address instructions could be used in a ps.1.1 - ps.1.3 pixel shader. The ps.1.1 pixel shader uses the tex instruction to load a color map and a normal map.

Until ps.1.4, it is not possible to use tex* instructions after an arithmetic instruction. Therefore dp3 and mul must come after the tex* instructions. There could be up to eight arithmetic instructions in a ps.1.1 shader.

The ps.1.4 pixel shader instruction flow is a little bit more complex:

  • version instruction: ps.1.4
  • constant instruction: def c0, 1.0, 1.0, 1.0, 1.0
  • texture address instructions: tex*
  • arithmetic instructions: mul, mad, dp3 etc.
  • phase marker
  • texture address instruction
  • arithmetic instructions

A ps.1.4 pixel shader must start with the version instruction. Then as many def instructions as needed may be placed into the pixel shader. This example doesn't use constants. There can be up to six texture addressing instructions after the constants. The diffuse reflection model shader only uses two texld instructions to load a color map and a normal map.

After the tex* instructions, up to 8 arithmetic instructions can modify color, texture or vector data. This shader only uses two arithmetic instructions: dp3 and mul.

So far a ps.1.4 pixel shader has the same instruction flow like a ps.1.1 - ps.1.3 pixel shader, but the phase marker allows it to double the number of texture addressing and arithmetic instructions. It divides the pixel shader in two phases: phase 1 and phase 2. That means as of ps.1.4 a second pass through the pixel shader hardware can be done. Adding the number of arithmetic and addressing instructions shown in the pixel shader instruction flow above, leads to 28 instructions. If no phase marker is specified, the default phase 2 allows up to 14 addressing and arithmetic instructions. This pixel shader doesn't need more tex* or arithmetic instructions, therefore a phase marker is not used.

In this simple example, the main difference between the ps.1.1 and the ps.1.4 pixel shader is the usage of the tex instructions in ps.1.1 to load the texture data into t0 and t1 and the usage of the texld instruction in ps.1.4 to load the texture data into r0 and r1. Both instructions are able to load four components of a texture. Valid registers for tex are tn registers only, whereas texld accepts as the destination registers only rn registers and as source registers tn in both phases and rn only in the second phase.

The number of the temporary destination register of texld is the number of the texture stage. The source register always holds the texture coordinates. If the source register is a texture coordinate register, the number of the tn register is the number of the texture coordinate pair. For example texld r0, t4 samples a texture from texture stage 0 with the help of the texture coordinate set 4. In this pixel shader, the texture with the color map is sampled from texture stage 0 with the texture coordinate set 0 and the texture with the normal map is sampled from texture stage 1 with the texture coordinate set 1.

The dp3 instruction calculates the diffuse reflection with a dot product of the light and the normal vector on a per-pixel basis. This instruction replicates the result to all four channels. dp3 does not automatically clamp the result to [0..1]. For example the following line of code needs a _sat modifier:

dp3_sat r0, t1_bx2, r0
The dot product instruction executes in the vector portion of the pixel pipeline, therefore it can be co-issued with an instruction that executes only in the alpha pipeline.

dp3 r0.rgb, t0, r0
+ mov r.a, t0, r0

Because of the parallel nature of these pipelines, the instructions that write color data and instructions that write only alpha data can be paired. This helps reducing the fill-rate. Co-issued instructions are considered a single entity, the result from the first instruction is not available until both instructions are finished and vice versa. Pairing happens in ps.1.1 - ps.1.3 always with the help of a pair of .rgb and .a write masks. In ps.1.4, a pairing of the .r, .g. or .b write masks together with an .a masked destination register is possible. Therefore for example dp3.r is not possible in ps.1.1 - ps.1.3.

The calculation of the dp3 instruction in both pixel shaders is similar to the calculation of the diffuse reflection on a per-vertex basis in RacorX3 in part 2 of this introduction, although the values provided to dp3 for the per-pixel reflection model are generated on different ways.

Per-Pixel Lighting

Per-pixel lighting needs per-pixel data. High resolution information about how the normal vector is stored in a two dimensional array of three-dimensional vectors called a bump map or normal map. Each vector in such a normal map represents the direction in which the normal vector points. A normal map is typically constructed by extracting normal vectors from a height map whose contents represent the height of a flat surface at each pixel (read more in [Dietrich][Lengyel]). This is done by the following code snippet:

if(FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, m_cColorMap,
  return E_FAIL;

  return E_FAIL;

if(FAILED(D3DXCreateTexture(m_pd3dDevice, desc.Width, desc.Height, 0, 0,
                            D3DFMT_A8R8G8B8,D3DPOOL_MANAGED, &m_pNormalMap)))
  return E_FAIL;


D3DXCreateTextureFromFile() reads in the height map file earthbump.bmp from the media directory and provides a handle to this texture. A new and empty texture is built with D3DXCreateTexture() with the same width and height as the height map. D3DXComputeNormalMap() converts the height field to a normal map and stores this map in the texture map created with D3DXCreateTexture().

The CreateFileBasedNormalMap() function of the DotProduct3 example from the DirectX 8.1 SDK shows how to convert a height map into normal map with source code.

The most important function is D3DXComputeNormalMap(), which was introduced in the DirectX 8.1 SDK. It maps the (x,y,z) components of each normal to the (r,g,b) channels of the output texture. Therefore the height map is provided in the second parameter and the normal map is retrieved via the first parameter. This example doesn't use a paletized texture, so no palette has to be provided in the third parameter. The flags in the fourth entry field allow the user to mirror or invert the normal map or to store an occlusion term in its alpha channel. The last parameter is the amplitude parameter, that multiplies the height map information. This example scales the height map data by 10.

With the help of D3DXComputeNormalMap() we create a map with normals on a per-pixel basis, but there is still one problem left: the normals in the normal map were defined in one space based on how the textures were applied to the geometry. This is called texture space. In contrast the light is defined in world space. The L dot N product between vectors in two different spaces will lead to unintentionally results. There are two solutions for this problem.

  • Generate the normal maps to always be defined relative to world space or
  • Move the light into texture space

The second solution is the most common. On a very abstract level, it can be divided into two steps:

  • A texture space coordinate system is established at each vertex
  • The direction to light vector L is calculated at each vertex and transformed into the texture space

This will be shown in the following two sections.

Establish a Texture Space Coordinate System at each Vertex

The texture coordinates at each vertex form a coordinate system with a U (tangent), V (binormal = u x v) and a W (normal) axis.

Figure 2 - Texture Space

V and U are also called tangent vectors. These three vectors form a rotation/shear matrix, that transforms or maps from world to texture space.

This example uses a sphere on which the textures are mapped on. At different vertices of this sphere, these vectors will point in an entirely different direction. Therefore you have to calculate these vectors for every vertex.

To retrieve the U and V vectors, the partial derivatives of U and V relative to X, Y and Z in world space are calculated (read more in [Dietrich][Lengyel]). This is done by the following piece of code in LoadXFile() function in RacorX.cpp for the whole mesh:

// compute the normals
hr = D3DXComputeNormals(pMeshSysMem,NULL);
  return E_FAIL;

// compute texture
hr = D3DXComputeTangent(pMeshSysMem,0,pMeshSysMem2,1,
  return E_FAIL;

D3DXComputeNormals() computes normals for each vertex in a mesh. It uses in its first parameter a pointer to the mesh. The second parameter can be used to specify the three neighbors for each face in the created progressive mesh, which is not used here.

D3DXComputeTangent() (new in DirectX 8.1) is used to compute the tangent U vector based on the texture coordinate gradients of the first texture coordinate set in the input mesh.

HRESULT D3DXComputeTangent(
  DWORD TexStage,
  DWORD TexStageUVec,
  DWORD TexStageVVec,
  DWORD Wrap,
  DWORD* pAdjacency

The first parameter must be a pointer to an ID3DXMESH interface, representing the input mesh and the third parameter will return a mesh with the one or two vectors added as a texture coodinate set. The texture coordinate set is specified in the TexStageUVec and TexStageVVec parameters. The flag D3DX_COMP_TANGENT_NONE used in one of these parameters prevents the generation of a vector. TexStage chooses the texture coordinate set in the input mesh, that will be used for the calculation of the tangent vectors.

Wrap wraps the vectors in the U and V direction, if this value is set to 1 like in this example. Otherwise wrapping doesn't happen. With the last parameter one can get a pointer to an array of three DWORDs per face that specify the three neighbors for each face in the created mesh.

Both functions store the vectors in the mesh. Therefore the mesh will "transport" the vectors to the vertex shader via the vertex buffer as a texture coordinate set, consisting of the three vector values.

The normal from D3DXComputeNormals() and the tangent from D3DXComputeTangent() form the two axis necessary to build the per-vertex texture space coordinate system to transform L.

Transforming L into Texture Space

After building up the U and W vectors, L is transformed into texture space in the following lines in the vertex shader:

; Input Registers
; v0 - Position
; v3 - Normal
; v7 - Texture
; v8 - Tangent


m3x3 r5, v8, c0 ; generate tangent U
m3x3 r7, v3, c0 ; generate normal W

; Cross product
; generate binormal V
mul r0, r5.zxyw, -r7.yzxw;
mad r6, r5.yzxw, -r7.zxyw,-r0;

;transform the light vector with U, V, W
dp3 r8.x, r5, -c12
dp3 r8.y, r6, -c12
dp3 r8.z, r7, -c12

// light -> oD0
mad oD0.xyz, r8.xyz, c33, c33 ; multiply by a half to bias, then add half

The tangent U produced by D3DXComputeTangent() is delivered to the vertex shader via v8 and the normal W is provided in v3. To save bandwidth, the binormal is calculated via the cross product of the tangent and the normal in the vertex shader. The transform happens with the following formula:

L.x' = U dot -L
L.y' = V dot -L
L.z' = W dot -L

At the end of this code snippet, L is clipped by the output register oD0 to the range [0..1]. That means any negative values are set to 0 and the positive values remain unchanged. To prevent the cut off of negative values from the range of [-1..1] of v8 to [0..1], the values have to be shifted into the [0..1] range. This is done by multiplying by 0.5 and adding 0.5 in the last instruction of the vertex shader. This is not necessary for texture coordinates, because they are usually in the range [0..1].

To get the values back into the [-1..1] range, the _bx2 source register modifiers subtract by 0.5 and multiplies by 2 in the pixel shader.

tex t0 ; color map
tex t1 ; normal map
dp3 r1, t1_bx2, v0_bx2 ; dot(normal,light)
mul r0,t0, r1 ; modulate against base color

texld r0, t0 ; color map
texld r1, t0 ; normal map
dp3 r2, r1_bx2, v0_bx2 ; dot(normal, light)
mul r0, r0, r2

Pixel shader instructions can be modified by an instruction modifier, a destination register modifier, a source register modifier or a selector (swizzle modifier).

mov_IM dest_DM, src_SM || src_SEL

The instruction modifiers (IM) _x2, _x8, _d2, _d8 and _sat multiply or divide the result or in case of the _sat modifier saturate it to [0..1].

The destination register modifiers (DM) .rgb, .a or in case of ps.1.4 additionally .r, .g and .b controls which channel in a register is updated. So they only alter the value of the channel they are applied to.

The source register modifiers (SM) _bias (-0.5), 1- (invert), - (negate), _x2 (scale) and _bx2 (signed scaling) adjust the range of register data. Alternatively a selector or swizzle modifier .r, .g, .b and .a replicates a single channel of a source register to all channels (ps.1.1 - ps.1.3 only .b and .a; ps.1.4 .r, .g, .b, .a; read more in part 3 of this introduction). Both pixel shaders use the _bx2 as a source register modifier, that doesn't change the value in the source register. For example the value of v0 in the mov instruction has not changed after the execution of the following instruction.

mul r1, v0_bx2

The values that are delivered to mul for the multiplication are modified by _bx2 but not the content of the registers itself.

Both pixel shaders shown above produce a valid Lambertian reflection term with the dot product between L, which is now in texture space and a sample from the normal map, as shown in part 2 (read more in [Lengyel]).

The last two lines of the vertex shader store the texture coordinate values of the color texture in two texture coordinate output register:

mov oT0.xy, v7.xy
mov oT1.xy, v7.xy

Sending the same texture coordinates via two texture output registers is redundant, therefore using only one of the output registers would be an improvement. The first idea that came into mind is to set D3DTSS_TEXCOORDINDEX as a texture stage state to use the texture coordinates of the first texture additionally for the second texture. Unfortunately this flag is only valid for usage with the fixed-function T&L pipeline, but not for the usage with vertex shaders. With ps.1.1 there is no way to use the texture coordinates of the first texture for the second texture without sending them via oT1 to the pixel shader a second time. The texture coordinates must be send via one of the texture coordinate registers with the same number as the texture stage.

In a ps.1.4 pixel shader the programmer is able to choose the texture coordinates that should be used to sample a texture in the texld instruction as shown in the ps.1.4 pixel shader:

texld r1, t0 ; normal map

This way the remaining texture coordinate output registers in the vertex shader can be used for other data, for example vector data.

Assemble Pixel Shader

Similar to the examples used in the second part of this Introduction, the pixel shaders are assembled with NVASM or in case of the ps.1.4 pixel shaders with Microsoft's psa.exe, because NVASM doesn't support ps.1.4. The integration of NVASM into the Visual C/C++ IDE is done in the same way as for vertex shaders (Read more in part 2 of this introduction).

Create Pixel Shader

The pixel shader binary files are opened, read and the pixel shader is created by CreatePSFromCompiledFile() in InitDeviceObjects():

CreatePSFromCompiledFile (m_pd3dDevice, "shaders/diffdot3.pso",
CreatePSFromCompiledFile (m_pd3dDevice, "shaders/diff.pso",
CreatePSFromCompiledFile (m_pd3dDevice, "diffdot314.pso",

The CreatePSFromCompileFile() function is located at the end of RacorX.cpp:

// Name: CreatePSFromBinFile
// Desc: loads a binary *.pso file 
// and creates a pixel shader
HRESULT CMyD3DApplication::CreatePSFromCompiledFile (IDirect3DDevice8* m_pd3dDevice,
                                                     TCHAR* strPSPath,
                                                     DWORD* dwPS)
  char szBuffer[128]; // debug output
  DWORD* pdwPS; // pointer to address space of the calling process
  HANDLE hFile, hMap; // handle file and handle mapped file
  TCHAR tchTempVSPath[512]; // temporary file path
  HRESULT hr; // error

  if( FAILED( hr = DXUtil_FindMediaFile( tchTempVSPath, strPSPath ) ) )

  hFile = CreateFile(tchTempVSPath, GENERIC_READ,0,0,OPEN_EXISTING,

    if(GetFileSize(hFile,0) > 0)
      hMap = CreateFileMapping(hFile,0,PAGE_READONLY,0,0,0);
      return E_FAIL;
    return E_FAIL;

  // maps a view of a file into the address space of the calling process
  pdwPS = (DWORD *)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

  // Create the pixel shader
  hr = m_pd3dDevice->CreatePixelShader(pdwPS, dwPS);
  if ( FAILED(hr) )
    OutputDebugString( "Failed to create Pixel Shader, errors:\n" );
    OutputDebugString( szBuffer );
    OutputDebugString( "\n" );
    return hr;


  return S_OK;

This function is nearly identical to the CreateVSFromCompiledFile() function shown in part 2 of this introduction. Please consult the section "Create VertexShader" in the RacorX2 example section of part 2 for more information. The main difference is the usage of CreatePixelShader() instead of CreateVertexShader().

CreatePixelShader() is used to create and validate a pixel shader. It takes a pointer to the pixel shader byte-code in its first parameter and returns a handle in the second parameter.

CreatePixelShader() fails on hardware that does not support ps.1.4 pixel shaders. You can track that in the debug window after pressing <F5>. Therefore the following examples indicate this with a warning message that says:"ps.1.4 Not Supported". To be able to see a ps.1.4 shader running on the Reference Rasterizer (REF) the function CreatePixelShader() has to be called once again after switching to the REF. This functionality is not supported by the examples used throughout this introduction.

OutputDebugString() shows the complete error message in the output debug window of the Visual C/C++ IDE and D3DXGetErrorStringA() interprets all Direct3D and Direct3DX HRESULTS and returns an error message in szBuffer.

Set Pixel Shader

Depending on the choice of the user, three different pixel shaders might be set with SetPixelShader() in Render():

//diffuse lighting.

  if (m_bPixelShader)
  //no lighting, just base color texture

If diffuse lighting is switched on, the user may select with the <P> key between the ps.1.1 and ps.1.4 pixel shader, if supported by hardware. If diffuse lighting is switched off, the simple ps.1.1 pixel shader, that only uses the color texture is set.

Free Pixel Shader Resources

All pixel shader resources are freed in DeleteDeviceObjects():


Non-Shader Specific Code

I used source code published by SGI as open source some years ago to emulate a virtual trackball. The file trackball.h holds an easy to use interface to the SGI code in SGITrackball.cpp.


RacorX6 shows, how the pixel shader is driven by the vertex shader. All the data that is needed by the pixel shader is calculated and provided by the vertex shader. Calculating the U and W vectors, which is necessary to build up a texture space coordinate system, is done one time while the *.x file is loaded with the D3DXComputeNormals() and D3DXComputeTangent() functions. The V vector as the binormal is calculated for every vertex in the vertex shader, because that saves bandwidth. These three vectors that form a texture space coordinate system are used to transform the light vector L to texture space. L is send through the color output register oD0 to the pixel shader. The usage of a texture space coordinate system is the basis of per-pixel lighting and will be used throughout the upcoming example.



  Printable version
  Discuss this article

The Series
  Fundamentals of Vertex Shaders
  Programming Vertex Shaders
  Fundamentals of Pixel Shaders
  Programming Pixel Shaders
  Diffuse & Specular Lighting with Pixel Shaders