Author: Jeff Hill |
On the Design and Implementation of a 3D Rendering System in C++, Part 1: Vectors and Matrices
This article is the first in a series discussing a number of oft-overlooked topics in computer graphics. The standard textbooks on 3D rendering tend towards the more
theoretical aspects of the subject, and even the most practical references often implement either partial systems, or trivial systems. These articles do not attempt to
detail the mechanics of a rendering system; there are many good articles on the subject by those much more qualified than your humble author. Specifically I will
assume the existance of a low-level rendering toolkit, such as OpenGL, Direct3D, Quickdraw 3D or RAVE. Any system capable of rendering a scene composed of
textured and shaded triangles will do. I use OpenGL on MacOS as my development platform of choice, however the topics discussed should work with little
modification on any rendering system.
Vectors and Matrices
The first question any rendering system needs to address is that of vectors and matrices. 4 element or 3, float, double or fixed point, row-major or column major, the
number of variants is nearly limitless. 3D mathematics libraries rival string classes as the hobby-horse of choice among graphics coders. When I sat down to write my
latest revision of the ever-present vector/matrix library, I made a number of decisions based on the current philosophy in the graphics programming community, and
on my experience with C++ language features. Here is a list of the decisions I made, and why.
Often vector/matrix classes are templates, either on scalar type, vector/matrix size, or both. I feel that this is the wrong choice for two reasons; the template versions
can be non-trivial to write when compared to the non-template versions, and using templates doesn't really accomplish anything, it simply defers the decisions until
later. By choosing a scalar type, some math functions can be significantly optimized using lookup tables (Graphics Gems I covers this, as do a number of docs).
Parameterizing on the scalar type forces either many versions of these optimizations, or the potential that perfectly reasonable code won't compile... neither is a
desirable situation. Parameterizing on the size can be worse. Dot product may not be defined on 2 element vectors, or it may be, depending on your preference. I
don't want to have to remember what decision I made on this 2 months from now when a piece of code refuses to compile. Better to resolve the issue ahead of time,
and force the size to a constant.
The Scalar Type Is A typedef
This allows templates to be avoided, but it also allows for the scalar type to be changed easily.
4 Element Vectors
Yes, the vast majority of vectors only use 3 elements. The fourth element makes the structure
pack nicely, and allows the use of one class for both "regular" vectors and homogenous coordinates.
Vectors Are Interpreted As Columns
The first book on 3D rendering I read was the Watt/Watt book, "Advanced Animation and
Rendering Techniques", in which the row vector standard is used. Since
then I have had to mentally transpose the matrices printed in every other graphics
reference other than docs written by similarly handicapped authors. It seems the
mathematical convention has won out over the hacker convention in the references, so
rather than fight the standard, I will adopt it.
Operator * Is Not Overloaded For Two Vectors
What does operator * mean on a vector anyhow? matrix * vector is obvious,
as is vector * scalar or scalar * vector, but is vector * vector dot or cross
Ask two programmers, I'll lay odds they say different things. Again, resolve
the confusion by avoiding it in the first place.
Operator [ ] Is Index, operator() Is Index And Modify
Const-correctness is a holy grail in C++. If your program is const-correct, then
some of the nastiest sorts of bugs can be avoided completely. To this end, operator
returns a const scalar_t& while operator() returns a scalar_t&. On matrices, it's
scalar_t*'s, so to access an element is either matrix[x][y] or matrix(x)[y]. Yes, you
can cast away const, but you need to work to do it, so unintentional errors can be caught
by the compiler with no run-time overhead.
Almost All Operations Are Inline
Only matrix inverse, adjoint and determinant are out-of-line. The argument that this
makes the code hard to read is moot, as you can defer the definition of inline
functions using the inline keyword, then define all the implementations in a block at
the end of the header. On modern processors, inline can make worlds of
difference. Optimizing compilers can make a small inline function essentially free, when
the non-inline function has function call overhead. Almost all vector/matrix ops
are small enough that code stream bloat due to inline expansion is simply not an issue.
Transformation Matrices Are Defined In Math3D
While transformation and projection matrices are arguably not mathematical entities, but
uses of those entities, the utility of having these functions in this part of the
library outweighs any philosophical objections that may arise from their place in this library.
Place All Math Functions In A Namespace
Namespaces are a gift from the Gods. They allow the freedom to choose short, descriptive names
with a guarantee that there will be no name collision. Even when
working on a small project, namespaces allow the creation of seperate "modules", distinct
from header/implementation file restrictions.
So, with the design considerations out of the way, the implementation
is trivial busywork. I chose a float for the scalar type, as floats can be twice as fast as doubles,
and fixed-point is essentially dead. Modern processors have floating point units that pipeline
computation, so the difference between bit-shifting and floating point
multiplies has all but disappeared. The code included with this article has been completely
tested, and it is in use in a number of projects on MacOS.
On the Design and Implementation of a 3D Rendering System in C++
Jeff Hill may be contacted at email@example.com.
Discuss this article in the forums
Date this article was posted to GameDev.net: 8/3/1999
(Note that this date does not necessarily correspond to the date the article was written)
Comments? Questions? Feedback? Click here!