My favorites | English | Sign in

Programmable Graphics Pipeline

O3D uses a programmable graphics pipeline model instead of a fixed-function pipeline. This programmable pipeline makes use of a shader language, based on HLSL and Cg, that enables you to program the GPU directly through the use of vertex shaders and pixel shaders. Before this era of programmable GPUs, the graphics programmer was limited to a fixed-function pipeline. Algorithms for calculating transformations, lighting, texture coordinates, and other environmental effects were pre-programmed into software such as early OpenGL or Direct 3D, which controlled the graphics hardware. In systems based on a fixed-function pipeline, global states are set up for lights, materials, and textures, and then the shape information is passed in to this pipeline. In contrast, with a programmable graphics pipeline, the developer has complete control over the algorithms used in the vertex shader and the pixel shader. In addition, rasterizing and frame-buffer operations can be configured using the O3D API.

The following sections provide an overview of how to program the O3D graphics pipeline, with brief contrasting descriptions of how the same techniques would be accomplished in a fixed-function system.

Contents

  1. Components of the O3D Pipeline
  2. Defining the Shader Algorithms
  3. O3D Shading Language
  4. What the Vertex Shader Does
  5. Rasterization
  6. What the Pixel Shader Does
  7. Frame Buffer Operations
  8. What's Next?

Components of the O3D Pipeline

The O3D programmable pipeline offers complete flexibility with respect to the algorithms used for vertex shading and pixel shading. The following diagram shows the major components of this pipeline:

The vertex shader is composed of algorithms that you write, "borrow," or modify. These algorithms calculate the values of the per-vertex attributes as well as the position of each vertex in homogeneous clip space.

The rasterizer has a set of configurable states that can be set in the O3D State object. The rasterizer is used for interpolation of the vertex attribute values as well as for calculations related to other operations, such as viewport clipping and backface culling.

The pixel shader takes input from the rasterizer and outputs one color for each pixel of the primitive. You write, "borrow," or modify the algorithms that compute these pixel colors.

Frame buffer operations are a set of configurable states set in the O3D State object. These operations are used for depth testing, stenciling, and blending among others.

Defining the Shader Algorithms

The vertex shader and pixel shader each have user-defined input and output. Vertex streams form the input to the vertex shader. The vertex shader uses two types of input values (per-vertex attributes and uniform parameters) to produce a modified position and an arbitrary number of attributes for each vertex in the primitive. These computations are based on custom algorithms you define in your shaders.

O3D Shading Language

O3D uses a variation of the HLSL and Cg shading languages. Vertex and pixel shaders are specified by supplying a string containing the source code. Because it is just a string, you can store a shader in a variable, which can be contained in a hidden <textarea> element within the HTML document, downloaded from a URL, or stored anywhere else your web page would keep a string. See O3D Shading Language for more information.

Back to top

What the Vertex Shader Does

The vertex shader processes each vertex in the input vertex streams and outputs two kinds of information for each vertex:

  • a vertex position in homogeneous clip space
  • an arbitrary number of interpolants (that is, attribute values to be interpolated)

Input: Per-Vertex Attributes

Each vertex can contain an arbitrary number of numerical attributes. Examples of these per-vertex attributes are:

  • Position
  • Texture coordinate
  • Color
  • Normal
  • Weight
  • Magnetic attraction
  • Index in an array
  • and anything else you can think of!

These numerical attributes are used as input to the vertex shader. They can be of type float1, float2, float3, or float4, which are groups of 1 to 4 floating point values, respectively.

Input: Uniform Parameters

In addition to the per-vertex attributes, a number of parameters that apply to all vertices in the primitive are used as input to the vertex shader. These parameters can be matrices, vectors, or floats. For example:

  • Matrices
    • Projection matrix
    • View matrix
    • Model matrix
    • Texture matrix
    • Color matrix
    • Bone matrix
    • etc.
  • Vectors
    • Light position
    • Light color
    • Motion vector
  • Floats
    • Time
    • Mass

Output

The output of the vertex shader is of two types:

  • a vertex position in homogeneous clip space
  • an arbitrary number of attributes for each vertex
-w < x < w, -w < y < w and 0 < z < w

This means that

 -1 < x/w < 1,  -1 < y/w < 1, 0 < z/w < 1

Fixed-Function Pipeline Implementations

The following sections provide examples of how a fixed-function system such as OpenGL or Direct3D would implement the operations performed by the vertex shader, which include:

  • Transformations
  • Lighting calculations
  • Texture coordinate generation
  • Fog

Calculating Transformations in a Fixed-Function System

Here is how you would calculate the transformations for a vertex from local space to homogeneous clip space in a fixed function system. You could incorporate this calculation into your vertex shader, or you could modify it to suit your needs.

OUT.POSITION = projectionTransform * viewTransform * modelTransform * IN.POSITION

Calculating Lighting in a Fixed-Function System

Here is how you would calculate lighting for a vertex in a fixed-function system. You could incorporate this calculation into your vertex shader, or you could modify it to suit your needs.

OUT.COLOR = material.emissive
            + light.ambient * material.ambient
            + light.diffuse * material.diffuse * dot(normal, lightvector)
            + light.specular * material.specular * 
               dot(normal, halfvector) material.shininess

Calculating Texture Coordinates in a Fixed-Function System

Here is how you would calculate the texture coordinates for a vertex in a fixed function system. You could incorporate this calculation into your vertex shader, or you could modify it to suit your needs.

OUT.textureCoord = textureMatrix * in.textureCoord

Back to top

Rasterization

The output of the vertex shader has a one-to-one correspondence between each vertex, its position in homogeneous clip space, and the attribute values that are to be interpolated. The rasterizer prepares this data for the pixel shader by interpolating between the values assigned to each vertex for each attribute and producing a corresponding attribute value for each pixel. The input to the pixel shader is an attribute value for each pixel in the primitive.

For example, here is a triangle with three vertex attributes that is used as input to the rasterizer:

The rasterizer then creates position, normal, and color attribute values for each of the pixels in this triangle, as shown in the diagram below. The rasterizer computes these per-pixel values by interpolating between the values that have been specified explicitly for the vertices in the triangle. These per-pixel values are then used as input to the pixel shader.

The O3D rasterizer is similar to the rasterizer in fixed-function systems. O3D has a State object that allows you to set certain rendering states that affect how the rasterizer does its job. For example, you can set a custom viewport as well as specify parameters for backface culling, polygon mode, polygon offset, and more.

What the Pixel Shader Does

The job of the pixel shader is to perform all of its calculations on the per-pixel interpolants supplied as input and, at the end, to output a single float4 color value for each pixel in the primitive.

A simple shader could return a constant color or the color of a uniform param. For example:

float4 pixelShaderFunction(PixelShaderInput input): COLOR {
 return float4(1,0,0,1);
}

or

uniform float4 myColor;

float4 pixelShaderFunction(PixelShaderInput input): COLOR {
 return myColor;
}

A shader might do some lighting calculations. For example:

float4 pixelShaderFunction(PixelShaderInput input) : COLOR {
  float3 surfaceToLight = normalize(lightWorldPos - input.worldPosition);
  float3 normal = normalize(input.normal);
  float3 surfaceToView = normalize(viewInverse[3].xyz - input.worldPosition);
  float3 halfVector = normalize(surfaceToLight + surfaceToView);
  float4 litResult = lit(dot(normal, surfaceToLight),
                         dot(normal, halfVector), shininess);
 return float4((diffuse * litResult.y)).rgb, diffuse.a);
}

A shader can get colors or values from a texture. In your pixel shader code, use the tex2D() function to return a Float4 color from a 2D texture. For example:

sampler mySampler;

float4 pixelShaderFunction(PixelShaderInput input): COLOR {
 return tex2D(mySampler, input.texCoords);
}

For cube-mapped textures, use the texCUBE() function.

One advantage of the pixel shader is that it allows you to perform per-pixel computations for any attribute value. This capability is particularly effective, for example, in calculating the specular lighting component. You will need to balance performance considerations against quality and evaluate the tradeoffs of using the pixel shader for fine-grained computation, or the vertex shader for more efficient (and somewhat less explicit) specification of attributes.

Another advantage of the pixel shader is that it has access to textures, whereas the vertex shader does not. Your pixel shader, for example, could have two texture samplers set as uniform parameters. One texture could be a bitmap of colors, while a second texture could be a map of per-pixel normals. (Think of a hooked rug, where the yarns in the rug are the color values, and the directions the threads are pointing in are the normal values.) If you had a texture consisting of per-pixel normals, the formula for computing the diffuse color using per-pixel normals would be as follows:

OUT.COLOR = material.diffuse * light.diffuse * dot(tex2D(sampler,textureCoord), lightvector)

The variables in the shader code correspond to parameters that have the same names and matching types in the O3D JavaScript transform graph. Typically, these parameters are set on the material. However, they could also be set on a transform, draw element, primitive, or effect. When the render graph is traversed, the values for these parameters in the O3D transform graph are fed into the vertex and pixel shaders, which perform their calculations using these input values to produce the final color for each pixel on the screen.

Frame Buffer Operations

Frame buffer operations are configurable through the State object in O3D. These operations include alpha testing, depth testing, blending, and stenciling. This is the final step in the programmable graphics pipeline. After the frame buffer operations are performed, the output is displayed on the screen.

What's Next?

Learn about the basic structure of a O3D program in the chapter on Program Structure.

Back to top