|
Features

Animation With Cg
The
Limited Execution Environment of Cg Programs
When
you write a program in a language designed for modern CPUs using a modern
operating system, you expect that a more-or-less arbitrary program, as
long as it is correct, will compile and execute properly. This is because
CPUs, by design, execute general-purpose programs for which the overall
system has more than sufficient resources.
However,
GPUs are specialized rather than general-purpose, and the feature set
of GPUs is still evolving. Not everything you can write in Cg can be compiled
to execute on a given GPU. Cg includes the concept of hardware "profiles,"
one of which you specify when you compile a Cg program. Each profile corresponds
to a particular combination of GPU architecture and graphics API. Your
program not only must be correct, but it also must limit itself to the
restrictions imposed by the particular profile used to compile your Cg
program. For example, a given fragment profile may limit you to no more
than four texture accesses per fragment.
As
GPUs evolve, additional profiles will be supported by Cg that correspond
to more capable GPU architectures. In the future, profiles will be less
important as GPUs become more full-featured. But for now Cg programmers
will need to limit programs to ensure that they can compile and execute
on existing GPUs. In general, future profiles will be supersets of current
profiles, so that programs written for today's profiles will compile without
change using future profiles.
This
situation may sound limiting, but in practice the Cg programs shown in
this book work on tens of millions of GPUs and produce compelling rendering
effects. Another reason for limiting program size and scope is that the
smaller and more efficient your Cg programs are, the faster they will
run. Real-time graphics is often about balancing increased scene complexity,
animation rates, and improved shading. So it's always good to maximize
rendering efficiency through judicious Cg programming.
Keep
in mind that the restrictions imposed by profiles are really limitations
of current GPUs, not Cg. The Cg language is powerful enough to express
shading techniques that are not yet possible with all GPUs. With time,
GPU functionality will evolve far enough that Cg profiles will be able
to run amazingly complex Cg programs. Cg is a language for both current
and future GPUs.
Vertices,
Fragments, and the Graphics Pipeline
To put Cg into its proper context, you need to understand how GPUs render
images. This section explains how graphics hardware is evolving and then
explores the modern graphics hardware-rendering pipeline.
The
Evolution of Computer Graphics Hardware
Computer
graphics hardware is advancing at incredible rates. Three forces are driving
this rapid pace of innovation, as shown in Figure 1-2. First, the semiconductor
industry has committed itself to doubling the number of transistors (the
basic unit of computer hardware) that fit on a microchip every 18 months.
This constant redoubling of computer power, historically known as Moore's
Law, means cheaper and faster computer hardware, and is the norm for our
age.
The
second force is the vast amount of computation required to simulate the
world around us. Our eyes consume and our brains comprehend images of
our 3D world at an astounding rate and with startling acuity. We are unlikely
ever to reach a point where computer graphics becomes a substitute for
reality. Reality is just too real. Undaunted, computer graphics practitioners
continue to rise to the challenge. Fortunately, generating images is an
embarrassingly parallel problem. What we mean by "embarrassingly
parallel" is that graphics hardware designers can repeatedly split
up the problem of creating realistic images into more chunks of work that
are smaller and easier to tackle. Then hardware engineers can arrange,
in parallel, the ever-greater number of transistors available to execute
all these various chunks of work.
Our
third force is the sustained desire we all have to be stimulated and entertained
visually. This is the force that "connects" the source of our
continued redoubling of computer hardware resources to the task of approximating
visual reality ever more realistically than before.
As
Figure 1-2 illustrates, these insights let us confidently predict that
computer graphics hardware is going to get much faster. These innovations
whet our collective appetite for more interactive and compelling 3D experiences.
Satisfying this demand is what motivated the development of the Cg language.
Animation
Movement
in Time
Animation
is the result of an action that happens over time-for example, an object
that pulsates, a light that fades, or a character that runs. Your application
can create these types of animation using vertex programs written in Cg.
The source of the animation is one or more program parameters that vary
with the passing of time in your application.
To
create animated rendering, your application must keep track of time at
a level above Cg and even above OpenGL or Direct3D. Applications typically
represent time with a global variable that is regularly incremented as
your application's sense of time advances. Applications then update other
variables as a function of time.
You
could compute animation updates on the CPU and pass the animated data
to the GPU. However, a more efficient approach is to perform as much of
the animation computation as possible on the GPU with a vertex program,
rather than require the CPU to do all the number-crunching. Offloading
animation work from the CPU can help balance the CPU and GPU resources
and free up the CPU for more involved computations, such as collision
detection, artificial intelligence, and game play.
A
Pulsating Object
In
this first example, you will learn how to make an object deform periodically
so that it appears to bulge. The goal is to take a time parameter as input
and then modify the vertex positions of the object geometry based on the
time. More specifically, you need to displace the surface position in
the direction of the surface normal, as shown in Figure 6-1.
By
varying the magnitude of the displacement over time, you create a bulging
or pulsing effect. Figure 6-2 shows renderings of this effect as it is
applied to a character. The pulsating animation takes place within a vertex
program.
The
Vertex Program
Example
6-1 shows the complete source code for the C6E1v_bulge
vertex program, which is intended to be used with the C2E2f_passthrough
fragment program from Chapter 2. Only the vertex position and normal are
really needed for the bulging effect. However, lighting makes the effect
look more interesting, so we have included material and light information
as well. A helper function called computeLighting
calculates just the diffuse and specular lighting (the specular material
is assumed to be white for simplicity).
|
|
| float3
computeLighting ( |
float3
lightPosition,
float3 lightColor,
float3 Kd,
float shininess,
float3 P,
float3 N,
float3 eyePosition) |
{
//
Compute the diffuse lighting
float3 L = normalize(lightPosition - P);
float diffuseLight = max(dot(N, L), 0);
float3 diffuseResult = Kd * lightColor * diffuseLight;
// Compute
the specular lighting
float3 V = normalize(eyePosition - P);
float3 H = normalize(L + V);
float3 specularLight = lightColor * pow(max(dot(N,
H), 0),
shininess);
if (diffuseLight <= 0) specularLight = 0;
float3 specularResult = lightColor * specularLight;
return diffuseResult + specularResult;
}
void
C6E1v_bulge(float4
position : POSITION,
float3
normal : NORMAL,
out
float4 oPosition : POSITION,
out float4 color : COLOR,
uniform
float4x4 modelViewProj,
uniform float time,
uniform float frequency,
uniform float scaleFactor,
uniform float3 Kd,
uniform float shininess,
uniform float3 eyePosition,
uniform float3 lightPosition,
uniform float3 lightColor)
{
float displacement = scaleFactor * 0.5 *
sin(position.y
* frequency * time) + 1;
float4 displacementDirection = float4(normal.x,
normal.y,
normal.z, 0);
float4 newPosition = position +
displacement *
displacementDirection;
oPosition = mul(modelViewProj, newPosition);
color.xyz = computeLighting(lightPosition, lightColor,
Kd, shininess,
newPosition.xyz, normal, eyePosition);
color.w = 1;
}
|
 |
 |
 |
Example 6-1. The C6E1v_bulge
Vertex Program
|
Displacement
Calculation - Creating a Time-Based Function
The
idea here is to calculate a quantity called displacement that moves
the vertex position up or down in the direction of the surface normal.
To animate the program's effect, displacement has to change over
time. You can choose any function you like for this. For example, you
could pick something like this:
float
displacement = time;
Of
course, this behavior doesn't make a lot of sense, because displacement
would always increase, causing the object to get larger and larger endlessly
over time. Instead, we want a pulsating effect in which the object oscillates
between bulging larger and returning to its normal shape. The sine function
provides such a smoothly oscillating behavior.
A
useful property of the sine function is that its result is always between
-1 and 1. In some cases, such as in this example, you don't want any negative
numbers, so you can scale and bias the results into a more convenient
range, such as from 0 to 1:
float
displacement = 0.5 * (sin(time) + 1);
Did
you know that the sin function is just as efficient as addition or multiplication
in the CineFX architecture? In fact, the cos function, which calculates
the cosine function, is equally fast. Take advantage of these features
to add visual complexity to your programs without slowing down their execution.
Adding
Controls to the Program
To
allow finer control of your program, you can add a uniform parameter that
controls the frequency of the sine wave. Folding this uniform parameter,
frequency, into the displacement equation gives:
float
displacement = 0.5 * (sin(frequency * time) + 1);
You
may also want to control the amplitude of the bulging, so it's useful
to have a uniform parameter for that as well. Throwing that factor into
the mix, here's what we get:
float
displacement = scaleFactor * 0.5 *
(sin(frequency * time) + 1);
As
it is now, this equation produces the same amount of protrusion all over
the model. You might use it to show a character catching his breath after
a long chase. To do this, you would apply the program to the character's
chest. Alternatively, you could provide additional uniform parameters
to indicate how rapidly the character is breathing, so that over time,
the breathing could return to normal. These animation effects are inexpensive
to implement in a game, and they help to immerse players in the game's
universe.
Varying
the Magnitude of Bulging
But
what if you want the magnitude of bulging to vary at different locations
on the model? To do this, you have to add a dependency on a per-vertex
varying parameter. One idea might be to pass in scaleFactor
as a varying parameter, rather than as a uniform parameter. Here we show
you an even easier way to add some variation to the pulsing, based on
the vertex position:
float
displacement = scaleFactor * 0.5 *
sin(position.y * frequency * time) + 1;
This
code uses the y coordinate of the position to vary the bulging,
but you could use a combination of coordinates, if you prefer. It all
depends on the type of effect you are after.
Updating
the Vertex Position
In
our example, the displacement scales the object-space surface normal.
Then, by adding the result to the object-space vertex position, you get
a displaced object-space vertex position:
float4
displacementDirection = float4(normal.x, normal.y,
normal.z, 0);
float4
newPosition = position +
displacement * displacementDirection
Precompute Uniform Parameters When Possible
The
preceding example demonstrates an important point. Take another look at
this line of code from Example 6-1:
float
displacement = scaleFactor * 0.5 * sin(position.y * frequency *
time) + 1;
If
you were to use this equation for the displacement, all the terms would
be the same for each vertex, because they all depend only on uniform parameters.
This means that you would be computing this displacement on the GPU for
each vertex, when in fact you could simply calculate the displacement
on the CPU just once for the entire mesh and pass the displacement as
a uniform parameter. However, when the vertex position is part of the
displacement equation, the sine function must be evaluated for each vertex.
And as you might expect, if the value of the displacement varies for every
vertex like this, such a per-vertex computation can be performed far more
efficiently on the GPU than on the CPU.
If
a computed value is a constant value for an entire object, optimize your
program by precomputing that value on a per-object basis with the CPU.
Then pass the precomputed value to your Cg program as a uniform parameter.
This approach is more efficient than recomputing the value for every fragment
or vertex processed.
______________________________________________________
|