i
i
i
i
i
i
i
i
6 1. Introduction
above in handling exceptions such as division by zero. In these cases an exception
is logged, but in many cases the programmer can ignore that. Specically, for any
positive real number a, the following rules involving division by innite values
hold:
IEEE floating-point has two
representations for zero,
one that is treated as pos-
itive and one that is treated
as negative. The distinction
between 0 and
+
0 only
occasionally matters, but it
is worth keeping in mind
for those occasions when it
does.
+a/(+)=+0
a/(+)=0
+a/(−∞)=0
a/(−∞)=+0
Other operations involving innite values behave the way one would expect.
Again for positive a, the behavior is:
+ =+
∞−∞= NaN
∞×∞=
/ = NaN
/a =
/0=
0/0=NaN
The rules in a Boolean expression involving innite values are as expected:
1. All nite valid numbers are less than +.
2. All nite valid numbers are greater than −∞.
3. −∞ is less than +.
The rules involving expressions that have NaN values are simple:
1. Any arithmetic expression that includes NaN results in NaN.
2. Any Boolean expression involving NaN is false.
Perhaps the most useful aspect of IEEE oating-point is how divide-by-zero is
handled; for any positive real number a, the following rules involving division by
zero values hold:
Some care must be taken
if negative zero (– 0) might
arise.
+a/ +0 = +
a/ +0 = −∞
i
i
i
i
i
i
i
i
1.6. Efficiency 7
There are many numeric computations that become much simpler if the pro-
grammer takes advantage of the IEEE rules. For example, consider the expres-
sion:
a =
1
1
b
+
1
c
.
Such expressions arise with resistors and lenses. If divide-by-zero resulted in a
program crash (as was true in many systems before IEEE oating-point), then
two if statements would be required to check for small or zero values of b or c.
Instead, with IEEE oating-point, if b or c is zero, we will get a zero value for a as
desired. Another common technique to avoid special checks is to take advantage
of the Boolean properties of NaN. Consider the following code segment:
a = f(x)
if (a>0) then
do something
Here, the function f may return “ugly” values such as or NaN, but the if con-
dition is still well-dened: it is false for a = NaN or a = −∞ and true for
a =+. With care in deciding which values are returned, often the if can make
the right choice, with no special checks needed. This makes programs smaller,
more robust, and more efcient.
1.6 Efficiency
There are no magic rules for making code more efcient. Efciency is achieved
through careful tradeoffs, and these tradeoffs are different for different architec-
tures. However, for the foreseeable future, a good heuristic is that programmers
should pay more attention to memory access patterns than to operation counts.
This is the opposite of the best heuristic of two decades ago. This switch has oc-
curred because the speed of memory has not kept pace with the speed of proces-
sors. Since that trend continues, the importance of limited and coherent memory
access for optimization should only increase.
A reasonable approach to making code fast is to proceed in the following
order, taking only those steps which are needed:
1. Write the code in the most straightforward way possible. Compute inter-
mediate results as needed on the y rather than storing them.
2. Compile in optimized mode.
3. Use whatever proling tools exist to nd critical bottlenecks.
i
i
i
i
i
i
i
i
8 1. Introduction
4. Examine data structures to look for ways to improve locality. If possible,
make data unit sizes match the cache/page size on the target architecture.
5. If proling reveals bottlenecks in numeric computations, examine the as-
sembly code generated by the compiler for missed efciencies. Rewrite
source code to solve any problems you nd.
The most important of these steps is the rst one. Most “optimizations” make the
code harder to read without speeding things up. In addition, time spent upfront
optimizing code is usually better spent correcting bugs or adding features. Also,
beware of suggestions from old texts; some classic tricks such as using integers
instead of reals may no longer yield speed because modern CPUs can usually
perform oating-point operations just as fast as they perform integer operations.
In all situations, proling is needed to be sure of the merit of any optimization for
a specic machine and compiler.
1.7 Designing and Coding Graphics Programs
Certain common strategies are often useful in graphics programming. In this
section we provide some advice that you may nd helpful as you implement the
methods you learn about in this book.
1.7.1 Class Design
A key part of any graphics program is to have good classes or routines for geomet-
ric entities such as vectors and matrices, as well as graphics entities such as RGB
colors and images. These routines should be made as clean and efcient as pos-
I believe strongly in the
KISS (“keep it simple,
stupid”) principle, and in
that light the argument for
two classes is not com-
pelling enough to justify the
added complexity. —P.S.
sible. A universal design question is whether locations and displacements should
be separate classes because they have different operations, e.g., a location mul-
tiplied by one-half makes no geometric sense while one-half of a displacement
does (Goldman, 1985; DeRose, 1989). There is little agreement on this question,
which can spur hours of heated debate among graphics practitioners, but for the
sake of example let’s assume we will not make the distinction.
I like keeping points and
vectors separate because
it makes code more read-
able and can let the com-
piler catch some bugs.
—S.M.
This implies that some basic classes to be written include:
vector2. A 2D vector class that stores an x-andy-component. It should
store these components in a length-2 array so that an indexing operator can
be well supported. You should also include operations for vector addition,
vector subtraction, dot product, cross product, scalar multiplication, and
scalar division.
i
i
i
i
i
i
i
i
1.7. Designing and Coding Graphics Programs 9
vector3. A 3D vector class analogous to vector2.
hvector. A homogeneous vector with four components (see Chapter 7).
rgb. An RGB color that stores three components. You should also include
operations for RGB addition, RGB subtraction, RGB multiplication, scalar
multiplication, and scalar division.
transform.A4 × 4 matrix for transformations. You should include a
matrix multiply and member functions to apply to locations, directions, and
surface normal vectors. As shown in Chapter 6, these are all different.
image. A 2D array of RGB pixels with an output operation.
You might also consider a
special class for unit-length
vectors, although I have
found them more pain than
they are worth. —P.S.
In addition, you might or might not want to add classes for intervals, orthonormal
bases, and coordinate frames.
1.7.2 Float vs. Double
I suggest using doubles for
geometric computation and
floats for color computation.
For data that occupies a lot
of memory, such as trian-
gle meshes, I suggest stor-
ing float data, but convert-
ing to double when data is
accessed through member
functions. —P.S.
Modern architecture suggests that keeping memory use down and maintaining
coherent memory access are the keys to efciency. This suggests using single-
precision data. However, avoiding numerical problems suggests using double-
precision arithmetic. The tradeoffs depend on the program, but it is nice to have a
default in your class denitions.
1.7.3 Debugging Graphics Programs
I advocate doing all com-
putations with floats until
you find evidence that dou-
ble precision is needed in a
particular part of the code.
—S.M.
If you ask around, you may nd that as programmers become more experienced,
they use traditional debuggers less and less. One reason for this is that using such
debuggers is more awkward for complex programs than for simple programs.
Another reason is that the most difcult errors are conceptual ones where the
wrong thing is being implemented, and it is easy to waste large amounts of time
stepping through variable values without detecting such cases. We have found
several debugging strategies to be particularly useful in graphics.
The Scientific Method
In graphics programs there is an alternative to traditional debugging that is often
very useful. The downside to it is that it is very similar to what computer pro-
grammers are taught not to do early in their careers, so you may feel “naughty”
if you do it: we create an image and observe what is wrong with it. Then, we
i
i
i
i
i
i
i
i
10 1. Introduction
develop a hypothesis about what is causing the problem and test it. For example,
in a ray-tracing program we might have many somewhat random looking dark
pixels. This is the classic “shadow acne” problem that most people run into when
they write a ray tracer. Traditional debugging is not helpful here; instead, we must
realize that the shadow rays are hitting the surface being shaded. We might notice
that the color of the dark spots is the ambient color, so the direct lighting is what
is missing. Direct lighting can be turned off in shadow, so you might hypothesize
that these points are incorrectly being tagged as in shadow when they are not. To
test this hypothesis, we could turn off the shadowing check and recompile. This
would indicate that these are false shadow tests, and we could continue our de-
tective work. The key reason that this method can sometimes be good practice is
that we never had to spot a false value or really determine our conceptual error.
Instead, we just narrowed in on our conceptual error experimentally. Typically
only a few trials are needed to track things down, and this type of debugging is
enjoyable.
Images as Coded Debugging Output
In many cases, the easiest channel by which to get debugging information out of a
graphics program is the output image itself. If you want to know the value of some
variable for part of a computation that runs for every pixel, you can just modify
your program temporarily to copy that value directly to the output image and skip
the rest of the calculations that would normally be done. For instance, if you
suspect a problem with surface normals is causing a problem with shading, you
can copy the normal vectors directly to the image (x goes to red, y goes to green,
z goes to blue), resulting in a color-coded illustration of the vectors actually being
used in your computation. Or, if you suspect a particular value is sometimes out
of its valid range, make your program write bright red pixels where that happens.
Other common tricks include drawing the back sides of surfaces with an obvious
color (when they are not supposed to be visible), coloring the image by the ID
numbers of the objects, or coloring pixels by the amount of work they took to
compute.
Using a Debugger
There are still cases, particularly when the scientic method seems to have led
to a contradiction, when there’s no substitute for observing exactly what is going
on. The trouble is that graphics programs often involve many, many executions
of the same code (once per pixel, for instance, or once per triangle), making it
completely impractical to step through in the debugger from the start. And the
most difcult bugs usually only occur for complicated inputs.
..................Content has been hidden....................

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