Writing a 3D Engine
In this tutorial I'm hoping to give you some ideas about serious 3d engines,
like their general structure, things to do, techniques, etc. By no means
should you consider anything I say from now on as law cast in stone. First
of all: you can write a 3d engine in as many ways as you want it considering
anything from the application it's meant for, to your own taste. Obviously I
wrote my own engine following my own criteria and the knowledge I had available.
Second: I can't even hope to cover everything there is to know about 3d coding.
And third: I don't even know a fraction of it :-). But anyways, I'll try to
make this doc a useful resource from which you can at least get ideas that
you can use in your own work.
It's pretty hard to structure an all-encompassing doc I'm trying to write,
so my plan is to present my main engine structure (and alternatives, of course)
in a main file and have other docs branching off as details. The thing is,
though, that you won't see many of these details till I finish my whole
engine. But you can always ask
if you need anything specific. I'll see how things go...
Stuff to be Covered
Ok, now I'll outline the basic characteristics I planned for my engine
(that is to say when am I gonna consider it "finished", cause I'm pretty
sure I'll be adding new stuff as I learn it). This will give you an idea of
what I'm gonna be talking about, and what you shouldn't expect anytime soon...
This is going to be a demo style engine, which should show any kind of objects
in any kind of world without restrictions - the world's supposed to be totally
dynamic. It also means that objects are generally composed of many small
polygons. This means that BSPs and portals, for example, are out (don't worry
if you're a beginner and aren't familiar with these terms, I'll discuss them
later).
A little bit about object structure: The objects can be represented in
hierarchies (and behave with appropriately controlled motion). They can be
made of any type of polygon and mixtures of different types. By polygon type
I mean its shading and texturing characteristics. Vertices are
shared by polygons - no free polys by default. Hidden surface removal is done
via backface culling and z-sorting. Clipping and culling of objects is done
against the 3d view volume (or frustrum, frustum, whatever) to reduce
unnecessary work.
Another note: the docs will present alternatives to my
own methods (hopefully), but where optimization is concerned I can obviously
only go as far as what I presently know and have implemented. But this time it
won't be like in my previous docs, that I omit optimizations from the docs
and code, cause I expect you to be familiar with the basics and be able
to grasp what the optimized code's supposed to do, without much explanation. Another thing: if you're an
expert in the field (and you're for some strange reason reading this :-), I'm totally
open to suggestions about optimizations, different methods, whatever.
Note: my source code and example 'pseudo' code is in C++, so you should be
familiar with its basic concepts. Ok, time to jump right in!
This is the basic diagram of the processes going on in a 3d engine. It's all
very simple, but I'm gonna use it to illustrate the different "chapters" of
the doc. So why not go in sequence?...
Getting the data
You obviously need some data to display. All this data represents the world.
The 3d world has to be created and saved sometime, so that's it's reusable
at any time. There are any number of file formats you can use, but I didn't
get into that yet, so I'll leave it for later.
Object structure is another important thing that has to be discussed. I'm sure
you know the basic structures in 3d, like the vertex, polygon, etc. (If not, you
shouldn't be reading this doc just yet, take a look at the
previous tutorials). Maybe a little coding tip is in
order here: when storing arrays (of anything other than basic types like int), it is
almost necessary to use an array of pointers for speed and structural
considerations. For example it's necessary to store vertices of polygons as
pointers to the object's vertices, so that they are shared. Otherwise you'd have
to transform each point many times. Plus, it's easier and faster to access
pointers in general. Just an example. BTW, in practice it's
more useful to make that array of pointers actually a pointer to a pointer.
This way it can be dynamically allocated.
This is how i have my structures set up. This you really shouldn't take
seriously, it'll just be useful for you to understand my further code
examples. It's ugly and unorganized, maybe a lot of useless stuff is still
left in there, but I'll get it together sometime...
Some of the elements' function might be unclear to you for now,
but they'll be used later. So here it is:
typedef struct Vtx2d // 2d, projected vertex
{
int x, y; // coords
unsigned char c; // color, necessary for some shading routines
} Vtx2d;
typedef struct Texel // texture coordinates
{
int u, v;
} Texel;
class Vector
{
Fixed x, y, z; // whatever type you want
// all the functions, like arithmetic, transformations, etc. defined as methods
...
};
class Vertex
{
public:
Vector world, obj; // world and object coordinates
Vector world_norm, obj_norm; // world and object normals
Vtx2d scr; // projected, screen coordinates
Texel tex; // texture coordinates
int clipped, out; // visibility status flags
int clipped_by[NUM_CLIP_PLANES];
};
class Poly
{
protected:
Vertex **v; // the vertex array - array of pointers, to be dynamically allocated
int num_vertices;
Vector world_norm, obj_norm; // normals necessary for flat shading
Fixed z; // depth value used for sorting
int p, t, tx, e, f, g, ph, b; // type flags - more than one can be enabled at the same time
// some of the essential functions
public:
... IS_A(...); // returns type information
virtual void draw(); // drawing function that's overloaded for different types
...
};
class TexPoly
{
...
TexPoly(...):Poly() {} // the Poly base class gets inherited by all types
};
...
class Object
{
protected:
Vertex *vertex; int num_vertices; // array of vertices - the vertices are stored in the object structure, where they're indexed from by the polys
Poly **poly ; int num_polys; // array of pointers to polys (this is necessary to be able to store different types of polys in the same array)
Object **child; int num_children; // the array of children as Object pointers
Bitmap **texture; // the array of textures
Bound bbox, own_bbox;
Matrix WrldMatrix;
public:
// essential functions
void transform(const Matrix &M);
void draw(...); // an object also draws itself, just like a Poly
void make_box(...); // this creates a box object (maybe more appropriate as friend...)
};
As you can see above, the object should
contain pointers to all its data: polygons, textures, child objects, etc.,
but I store the vertices as an integral part of the object. In fact it's
the same with polys, but it's necessary to access them through pointers, so the
polymorphism and virtual function system can be exploited from a simple
base class Poly pointer. As you know from basic c++ thinking, a base class
pointer can in fact point to any derived class, so this way I can have
different types of polys 'stored' in the same array.
Ok, enough about code structure. On to physical object structure...
An object is made of vertices and polygons, having links to textures,
child objects and other things. Child objects are just
like normal objects, but they are defined as part of, or subordinate to a parent
object. We'll see later what all this entails, but the main thing is that you
can group objects together like this in logical hierarchies. Take the classic hand
example: The lower arm is subordinate to the upper arm; to the lower arm the wrist and
hand are subordinate, from which tired, numb fingerlike structures are hangin' off...
Anyways, this whole thing is supposed to behave as a unit, so it's logical to
define it with the appropriate, hierarchical structure.
Getting the Transformations
Well this is a simple, and in another way very complicated topic. All I'm concerned
about right here is the fact that I have to make my objects move in certain
ways. Any movement can be defined by a rotation and a translation vector.
How you get these is a whole other story that is totally application specific.
It may be controlled by computer algorythms or human generated input. This all
has to do with simulation which I'm not gonna get into. Every object has to be
transformed in order to get a position in the world. This doesn't necessarily have to
be controlled - objects can contain their own transformation information that
is initialized and stays constant until it gets changed or incremented as
needed.
Absolute, Constant Position
For example walls in a world have their predefined position and don't
(normally) change. Since transformation information is contained in the
object structure, you could say at init time: object[n].location=(12, 0, 1),
object[n].rotation=(0, 90, 0) (this is all symbolic code, of course).
This data will stay constant throughout the running of the program.
Relative Motion
A vehicle on the other hand can move around in which case
a specific movement is added to its location and orientation information.
This is incremental transformation, and it happens relative to present location
and orientation, whatever those may be. For example if the vehicle was at
(2, 0, 1) and facing straight along the z axis, and now it moves forward 1 unit
and turns 5º, the new location will be (2, 0, 1)+(0, 0, 1)=(2, 0, 2) and
it will be rotated around the y axis by 0+5=5º (further transformation with
this example will get more complicated, but I'm not gonna get into that just
yet).
Absolute Motion
Objects can also be controlled by
absolute transformation, in which case the object gets placed into the world
by specific location and orientation data. This happens relative to the world's
center. An example: [frame 1] object[n].rotate(0, 0, 0),
object[n].translate(2, 0, 1), [frame 2] object[n].rotate(0, 5, 0),
object[n].translate(2, 0, 2). This absolute information is very likely to
be generated by the computer, which might well use an incremental method
for coming up with it (there could be exceptions, of course, like mathematical
functions, parametric equations, random data, stuff like that). The difference
from the relative, incremental method, is that the computer has total
control over the object, it doesn't rely on its previous state.
Definition of Space and Transformation
But to understand the basic notion of transformations, I think a little more
phylosophic and rigorous definition of space is necessary. As you know from
your previous 3d coding, objects are defined by default in their own space,
appropriately called object space. Here the object is centered on the origin,
and all its vertices are defined relative to that point. When you translate
that object into its final position in the world, that can be thought of as
transferring it to world space. But you probably just thought of this
as just moving the object around in one space, as we are familiar with in the
real world. But when it comes to the mathematical representation of tranformations
(and many of them!) this becomes inadequate.
First let's define what space itself is: a coordinate system defined by 3
axes. These axes are defined by vectors. In our real world we think of these
as mutually orthogonal to one another and proportional. This doesn't
necessarily have to be true... Let me put it this way: vectors are totally
arbitrary and dependent on the space they're defined in. So a space defined
by orthogonal vectors in a nonorthogonal space might become nonorthogonal in
an orthogonal space. [figure to be drawn someday...] In fact the axes don't
even have to be vectors, they can be defined by, say, functions. These can
produce strange warped spaces, which I'm not even gonna get into. Suffice it
to say that the relationship between spaces (thus the transformations) are
defined by the relationships of their axes. [figures illustrating the basic
transformations and their effects on relationships of coordinate axes].
A perfect way to represent a space (relative to another one, of course) is
a 4x4 matrix. [matrix doc to follow, check out some
others
in the meantime].
Geometry Processing
What's understood under geometry processing is everything a vertex of an
object goes through until it finally gets projected on the screen. That
includes almost everything done in the engine,
other than the rasterization of the final polygons.
Applying Transformations
An understanding of the notion of spaces and their relationships is important
in correctly visualizing things like the order of transformations and their
effects, for example.
You have to understand which transformations happen in which spaces. I'll
illustrate this with an example of the space transformations my vertices
go through to get onto the screen. I'll be concerned with simple rotations
and translations for now.
Transforming Into the World
The way I have things set up, I'm supposed to be able to control an object's
orientation around its own axes and its exact position in space. To achieve
the first transformation: We want the rotation to happen around the object's
real axes; We know that this is the way the object is defined in object space,
so we perform the rotations directly on the object coordinates. We don't put
the resulting points back into the object data, but into separate world data.
vertex.world = vertex.obj.rotated_by(rotation_vector);
Now that we have the object rotated, we can put it into the world
using a translation (translating the world data this time).
vertex.world += translation_vector;
What happens here
is that the object's own frame of reference gets displaced in world space,
along world axes. This way its origin will be located
at the coordinates given by the translation vector, but all its vertices will still
have the same relation to this point as they did to the origin in object space.
(you might have to read that a few times :-)
Now I'd like to point out why the order is important: if
you were to translate the object first, you'd move it off is space, its origin
will be off center. The translation will happen along the object's original
axes which coincide with the world axes. Rotations, as every transformation,
operates on the present
space. In this case that space is the world, whose origin doesn't coincide with
the object's origin, as it did in object space, so the rotations won't happen
around the object's intended axes, but around the world's axes. This of course
gives a generally undesired effect - the object will seemingly move sideways
(around the world's origin) as it rotates, so while its orientation will be right,
its position will be also affected. You can check this with matrix multiplication.
If you multiply a rotation matrix by a translation, its top 3x3 part, the
axis/rotation definition, will not be affected. On the other hand, if you
multiply a translation matrix by a rotation, the translation vector part of
the matrix will actually get rotated. All this comes from the fact that
matrix multiplication is not commutative. Thing to remember: rotate first,
translate after.
Incremental Matrix Transformation
Another thing to be noted, is the difference between transforming by absolute
vectors, and concatenating incremental matrices. As I said before, every
transformation bumps the object into another space. From this follows, that
more consecutive transformations will never happen around the same axes.
Say you already rotated and transformed the object, then want to move
it around incrementally by say a key press. You create a matrix from the key
input that is supposed to rotate the object by an adittional degree and move
it forward by a unit. You multiply the object's transformation matrix (i'll
call it the WorldMatrix, because it puts the object into the world) by this
new little matrix.
This is a little pseudo-code of getting the incremental transformation matrix:
while(!stop)
{
Vector rotate, translate; //define it in here, so it gets automatically cleared every frame
if(key_up_pressed) translate.z=1;
if(key_down_pressed) translate.z=-1;
if(key_right_pressed) rotate.y=1;
...
Matrix rotation_matrix = create_rotation_matrix(rotation);
Matrix translation_matrix = create_translation_matrix(translation);
object.WorldMatrix *= rotation_matrix; //matrix concatenation
object.WorldMatrix *= translation_matrix;
object.draw();
}
You might (or not) be surprised, that the object starts
rotating around the world's center. The translation seems right, because the
object moves into the screen a notch. Well, both of these may be desired
effects in your engine, but I don't think so, especially for the first one. The
wrong rotation happens, of course, because the object was in world space already,
due to that previous transformation mentioned above. The other thing is, that if your object is a type
of vehicle, it should move along its own axes. When a car is pointed 1 degree
away, it doesn't keep going in a straight line (usually :-), as this object
kept moving straight into positive z, no deviation in x. All this is due to
the fact that the previous WorldMatrix in itself
already defines space, so any further operation on it will happen relative to
that space.
The way to avoid this is not to
keep location and orientation in the WorldMatrix itself, but to keep it in
two separate vectors. You increment these vectors and create a new WorldMatrix
from them each time and you'll be fine.
Code for simple understanding:
Vector rotate, translate; //define it out here, so it's static
while(!stop)
{
if(key_up_pressed) translate.z++;
if(key_down_pressed) translate.z--;
if(key_right_pressed) rotate.y++;
...
Matrix rotation_matrix = create_rotation_matrix(rotation); //create full transformation matrices, since "rotate" and "translate" are absolute
Matrix translation_matrix = create_translation_matrix(translation);
object.WorldMatrix = Matrix(); //initialize to identity
object.WorldMatrix *= rotation_matrix;
//an optimization would be to replace the above two lines with:
//object.WorldMatrix = rotation_matrix;
object.WorldMatrix *= translation_matrix;
object.draw();
}
The other fact, that
the object doesn't rotate around its own original axes can't even be fixed even
if you keep the translation in a separate vector and the rotation in a 3x3
matrix and increment that. This time the rotation won't be displaced, but
it will still rotate around its axes as they are in its current state, not
its original object world axes. This might be desired, though, in some
applications, that's mainly why I presented it. The other reason is that it
is often misused. Hopefully you understand the implications of incremental
transformations now...
Camera Transformations
Ok, so I have my object placed and moving in the world the way I want it.
Now all there's left is for me to do is look at it and be able to move around.
I call this camera movement.
Since you wanna be able to view the world from any angle and position, but
in your real view space you're really always located at the origin and
looking at positive z, you have to transform the whole world to get it facing
in front of you the way you want it. Well I don't even have to say it: we're
using spaces, rotation and translation matrices.
The interesting thing, though, is
that the order of transformations is reversed. If you think about it: you have to be placed in
an absolute location in the world, then orient yourself in the view direction.
This orientation has to happen around you, so you have to bring the origin of camera
space to your position in the world. You do this, by a simple translation of
the world. But it's the negative of your translation vector, because
when you move forward, the world has to move back in order to be placed under
you where you want it. Ok, now that your position is at the origin, you can
safely do the rotation, and the world will rotate around you (you might have
also experienced this in the event of hightened brain activity due to the effects
of narcotic substances ;). Anyways, this also has to be a negative rotation:
if you rotate right, the world has to rotate to the left in order for your
intended viewed point to be straight in front of you.
The minor difficulty comes when you try to do further transformations.
Consider this: you are at (0, 0, 1) and rotated 5 degrees around the y axis.
If you do another forward motion, you will end up
at (0, 0, 2) rotated by 5º. This is obviously not desired - you should move
5º to the right also, not just go straight. The way I solve this is to rotate
my translation increment vector by the current rotation. So from the initial
position (0, 0, 1), I will go forward almost one and a little to the side.
void get_camera_matrix(Vector rotate, Vector move)
{
direction+=rotate; // increment rotation
move.rotate(direction); // rotate translation increment, so you're moving in the direction you're looking
location+=move; // increment translation
CameraMatrix=Matrix(); // clear camera matrix to identity
CameraMatrix.translate(-location); // translate world _in the opposite direction_ of where you're moving
CameraMatrix.rotate(-direction); // rotate || ||
}
This works ok for me, but
you could use a much more logical solution. It involves incrementing the camera
matrix by additional incremental transformation matrices. Now you see why I
didn't use it, even though at the present I feel it's nicer. The reason this method works,
is that the incremental method suits the translation-rotation order.
As i said before, a subsequent rotation rotates the translation part of the
matrix, which is exactly what i've achieved explicitly by rotating my translation
increment. So what you should do is get the translation and rotation vectors
from the keyboard as before, create the matrices from them, and multiply them in
the order translation-rotation. Now concatenate this onto the camera's matrix.
Don't clear the camera matrix, ever, if you use this method. (BTW, I changed
my mind and I use this too now. It's just too simple to resist :-) But the above example is still zuseful for
understanding what's really going on).
void get_camera_matrix(Vector rotate, Vector move)
{
CameraMatrix.translate(-move);
CameraMatrix.rotate(-rotate);
// BTW, incrementing a matrix made of low precision numbers such as fixed point can screw up
// the integrity of the matrix, so you might have to orthonormalize it from time to time
}
Ok, so now you have two matrices: WorldMatrix and CameraMatrix that together take your
object straight into view space.
What I do, is get every object's transformation as normal, then transform
them by CameraMatrix. This concatenates onto the WorldMatrix, so now a transformation by
WorldMatrix takes a vertex right into view space. If you wanna keep WorldMatrix clean
in your object for incremental transformations, you'll have to create a whole
new matrix in the object, say ViewMatrix that is the product of WorldMatrix
and CameraMatrix. Same thing, just don't do the transformations separately
on each vertex - the whole beauty of concatenation.
Pseudo code of my object drawing function (C++), which in fact does the
transformations too, among everything else :-) :
main()
{
...
for(int o=0; o < num_objects; o++) // main object handling loop - "get transformation" stage
{
object[o].transform(WhateverMotion); // transform the object to world space
object[o].transform(CameraMatrix); // transform it to camera space
object[o].draw(); // render it
}
}
...
Object::draw()
{
...
for(int v=0; v < num_vertices; v++)
vertex[v].wrld = vertex[v].obj * WorldMatrix; // the object coordinates always stay constant
...
WorldMatrix=Matrix() // clear, because we're using absolute transformations
}
Overview of Object to Camera Space Transformations
I'll repeat the order of all the transformations again:
WorldMatrix CameraMatrix
OBJECT SPACE --------> WORLD SPACE --------> CAMERA/VIEW SPACE
Rot*Trl Trl*Rot
Hierarchical Transformations
(Note: I haven't implemented this yet, but I got a pretty good idea of
how It's done. I'm pretty sure most of what I say below is true)
As I said way above, it makes sense to define objects in hierarchies. One
reason is aesthetics in the code and application, but the main reason is
so that you can transform objects as units. For example you have a body,
with limbs as child objects; when you transform the body, it would be nice
to have the limbs go along implicitly. This can be done pretty simply by
transforming the children by the parent's transformation matrix.
When the children are defined as part of a parent object, they are
considered to be in the parent's object space. This means they are
transformed relative to the parent's origin - WorldMatrix actually transforms
to "parent object space".
Now when the parent is
transformed, its transformation matrix gets passed down to all its children,
where it multiplies to the child's existing transformation matrix. This
will now put the child in world space. If the child itself has children,
this newly created matrix gets passed down to them too. This can go for
as many levels as needed.
In my engine this is pretty easy to code as I have an Object
class with a draw() function. As I loop through all the child objects,
I transform them by the current object's matrix, then send a message to
draw themselves in turn. This creates a kind of recursive effect. In this
recursion the place of the camera transformation is very important. It should
only happen once to the parent object, as you can see in the above code, not
inside the object drawing code (the way I have it now, as I just realized :-).
This is a bit inconvenient because you have to do a monotonous transformation
on every object in the main loop, so what
you can do is check if the current object is a base object. If yes then
concatenate the camera matrix on its world matrix inside the draw() routine.
This is what i'm gonna do, and there are other ways... A little pseudo code:
Object::draw()
{
// draw the object itself
...
// draw its children
for(int c=0; c < num_children; c++)
{
child[c]->transform(WorldMatrix); // child->WorldMatrix *= this->WorldMatrix
// this of course messes up the child's world matrix, so you can't
// use it incrementally anymore; there are ways around this
child[c]->draw();
}
}
What all this means is that each child only moves
by the amount specified by its WorldMatrix relative to the object above
it in the hierarchy. This makes relative motion nicely controllable.
Child objects should be defined in their object space as centered on their
pivot point, with all its axes situated in logical directions. For example
the shoulder should be at (0,0,0) of the upper arm's object space, with
the x axis sticking out of the side, so a rotation around it would rotate
the arm in the vertical zy plane. If you wanna shrug, you just translate
it up on the y axis. Now that the object's orientation in object space is
clear, we have to attach it to the parent. This is done simply by translating
the whole thing to the coordinates of the joint in the parent's space. From
now on no matter where the parent goes, the child will have the exact same
relation to it.
child.WorldMatrix parent.child_pos
CHILD SPACE --------> RELATIVE POSITION ---------> PARENT SPACE
Rot and/or Trl Trl
You can see, that the child's WorldMatrix can be used as usual to move it
relative to its own origin. What's needed is the other translation matix to
put it where it's supposed to be in its parent's space. I think the parent
should have the responsibility for its children's positions, so there will
be position vector associated with each child. Here's the peudocode.
class Object
{
...
Object **child;
Vector *child_pos; // to be dynamically allocated
...
};
Object::draw()
{
...
for(int c=0; c < num_children; c++)
{
child[c]->translate(child_pos[c]); // get the child into this parent object's space
child[c]->transform(WorldMatrix); // child goes wherever the parent does
child[c]->draw();
}
...
}
I haven't tried this yet. It sounds good, but I'll update this if I implement
it differently.
Visibility Determination
It's very important at rendering time to know if what you're about to draw
is going to be at all visible - if not, you won't draw it in the first
place :-), or take special measures if it's partially visible. These are
the two types of measures: culling and clipping. If something's not visible,
it will be discarded, but if it's partially visible, only parts of it will
be discarded. This can be done at many levels: object, poly, vertex. Sometimes
it's necessary for image quality, and often for speed improvement. I emphasize
the speed improvement part and try to use it as much as possible. A lot of
transformation can be avoided, more than 75%.
View Space Clipping/Culling
An obvious criteria an entity has to meet
is to be in the field of view of the viewer. This
field of view is represented by the view volume (frustrum/frustum/
whatever you wanna call it) in 3d space. It is defined
by the projected planes from the screen's sides. Any more detail is more
appropriate for the frustrum clipping tutorial [coming sometime, but till
then here's
a good one writtem by someone else]. Suffice it to say that a point can be
tested for being outside the viewer's field of view, including behind him.
Let's see, the objects behind the view plane of the viewer eliminate
half the workload, with 180° of view left. I you consider a 90° fov or
maybe 60°, that number's already under 25%
A good optimization for this is the use of bounding volumes [a separate doc
to come on this too]. These are either
cubes, spheres, or any other simplified object that accurately encompasses
all of the object's vertices. This means that if any part of this bounding
volume is out of the frustrum, it's fair to check if the object itself if
out. If it's totally out, you can automatically ignore the whole object, and if
the hierarchy is set up right, even its children. If it's totally in, you can
be sre the whole object will have to be drawn, so no firther tests are necessary
But if the object is only partly out, all its vertices are checked too, to
know exactly which ones stand where. If a vertex is out, I flag it and
don't even transform it (more on how this is done later). This is not always
correct, because that vertex might still
be needed by a poly that's partially visible. For this case I have a fallback
restore() function that gets all the data that's needed by a partially visible
poly. If vertices weren't shared, all this could be done on a per-polygon
basis in a more direct way, but I'm not about to abandon my object structure.
(If you got a better idea plese tell me). Here's my pseudo code:
#define IN 1
#define PART 0
#define OUT -1
// 'clip' is the clipping status of the parent object
// 'own_clip' is the status of the bounding volume encompassing only the current object's vertices
void Object::draw(int clip=2) // the default, saying clipping status unknown - only happens for base objects
{
if(clip==2) {update_bound(); clip=PART;} // get bounding volume
int own_clip=IN;
if(clip==PART) // partial clipping of the parent object means uncertain status for the current object, so 'clip' has to be reevaluated
{
clip=MyFrustum.clip(bbox); // clip now means the status of the bounding volume encompassing the object together with all its child objects
own_clip=clip; // bbox is this all-encompassing volume
}
if(clip!=OUT) // if the whole object is not out
{
if(clip==PART) // if the whole object with children is partially in,
own_clip=MyFrustum.clip(own_bbox); // there's still a chance for the main object (encompassed by own_bbox) to be totally in, eliminating unnecessary checks
if(own_clip!=OUT) // as long as the main object is not out, try drawing it
{
for(int v=0; v < num_vertices; v++) // go through all the vertices
{
if(own_clip==PART) // if main object is partially out, check which parts of it are where
{
if(MyFrustum.in(vertex[v])) // checks if vertex is in the frustrum
{
vertex[v].wrld=vertex[v].obj * WrldMatrix; // do transform and project
vertex[v].project();
vertex[v].out=0; // flag as in
}
else
{
vertex[v].out=1; // flag as out
}
}
else // if it's totally in, do everything automatically
{
vertex[v].wrld=vertex[v].obj * WrldMatrix;
vertex[v].project();
vertex[v].out=0;
}
}
// if the object is not out, it's worth trying to draw its polys
for(int p=0; p < num_polys; p++)
{
int p_in = poly[p] - > restore(WrldMatrix); // if restore says it makes sense
if(p_in)
poly[p].draw(); // then draw the poly
}
}
// if the 'all-encompassing-volume' isn't out, it's worth trying to draw the children
for(int c=0; c < num_children; c++)
{
child[c] - > draw(clip); // notice how I pass the current object's clipping status on to the children
}
}
}
int Poly::restore(Matrix &M) // accepts the WorldMatrix to restore the transformations
{
for(int i=0, ins=0; i < num_vertices; i++)
if( !v[i] - > out )
ins++; // count the ins and outs
if(!ins) return 0; // if it's all out don't even bother
if(ins < num_vertices) // if some vertices are in, the rest have to be restored to have enough information to draw any poly at all
{
for(int i=0; i < num_vertices; i++)
if(v[i]->out)
{
v[i]->wrld=v[i]->obj * M;
v[i]->project();
v[i]->out=0;
}
}
if(v[0]->clipped || v[1]->clipped || v[2]->clipped) // if any of the vertices are out
{
MyFrustum.clip(this); // the poly has to be clipped
return 0; // the clipping routine manages the poly itself, so 0 is returned to not draw the old unclipped poly
}
return 1;
}
Hmm, that's pretty complicated, it may take a while till you get why I do
some things. If you don't it means I'm doing something stupid :-) It works,
but I'm not saying it's perfect. I already see things to optimize.
I welcome any suggestions about it, because this is one part of my
engine that's truly my own creation.
The other thing I mentioned is clipping. You surely know that this is about
cutting a polygon off at the side of the screen. It's easy to detect if a poly
will be clipped by the status of its vertices' clipping flags. If they're all
in or out it's easy. The actual clipping is done when one or more, but not all
of the vertices are out. The actual polygon clipping can also be done using
the frustrum planes, so it's described in the frustrum clipping tutorial.
Of course clipping can also be
done against the physical screen using any number of methods, but I don't
consider that part of geometry processing.
Backface Culling
The other hidden surface removal technique you surely know is backface culling.
Just as a refresher, polygons that are facing away from the viewer are
not drawn. This reduces the number of faces to be processed by almost another
half, so in the end you're not left with more than 15% of the work to do -
that is poly filling and even tranformations. As you know, if a normal is
facing away from the viewer, that vertex most likely won't be seen. This
of course is determined by the dot product of the normal and view vector. That 'most
likely' is a bit concerning, but I prepare for the eventuality in my Poly::restore()
function that I mentioned before. It does the good'ol clockwise test, just
to make sure the poly is front facing no matter what the normals say.
The reason vertex normals might be wrongly indicative of orientation is because
they can be tilted for shading variation. Moreover they are usually not even
parallel on the same poly, not to mention not being perpendicular to it. The
reason for this is that vertex normals are shared by polys that are usually
not parallel, so there's no way a normal could be paralled to all of them
at the same time.
So I feel the double insurance is necessary in my case at least. Of course this
can also be solved with the polys having their own normals. If the vertices
are not shared, the whole poly can be discarded this way without worry. You
wouldn't even need a backface test for every vertex. But I'm still not
abandoning vertex sharing ;)
Ok, now to actually doing the backface test. Many people wrongly asume that
if a normal is pointing more than 90° away from the view vector perpendicular
to the view plane ((0, 0, 1) in view space), it's facing away. An even simpler
test for this is if the normal in view space has a z coordinate larger than 0 -
pointing into the screen. But all this is incorrect under perspective
correction. Think about this: a plane perpendicular to your view plane, say
x=0 - it stands edge on, its normal is (-1, 0, 0), the angle 90°. If that
plane moves to x=1, you will actually be able to see its side, even though
it's still exactly perpendicular to your view plane. In fact it could tilt
quite a few degrees clockwise before it turned edge on and away.
So the thing to do is to get the vector that points from the view point (position
of the viewer)
straight to the polygon. This could be any point on the poly, or if you're
doing the check on a per-vertex basis, it's even more straight forward. Now
this is the vector you would check against the normal.
for(int v=0; v < num_vertices; v++)
{
viewvector = vertex[v].world - viewpoint;
if ( vertex[v].world_normal · viewvector < 0 )
vertex[v].out=1;
}
Pretty simple, eh? I just wanted to get one considertation straight.
Here are the modifications necessary to the restore function to deal with
culled vertices:
int Poly::restore(Matrix &M)
{
for(int i=0, ins=0; i < num_vertices; i++)
...
if(!ins) return 0;
if(ins==num_vertices && cull()) return 0; // even if the normals say everything's fine, this checks if the vertices are really clockwise; this check can only be done when all the vertex data is available
if(ins < num_vertices)
{
...
if(cull()) return 0; // after reconstructing the vertex data, we can check for clockwiseness
}
...
}
Inverse Transformations
First of all, you gotta know that I'm obsessed with this stuff :-) I went
through a lot of pain to get it working, but now I use it wherever I can.
Ok, what it does is enable you to do things like visibility determination
and shading without having to transform the object first - you can work with
object coordinates.
The way to do this is to get the things you're comparing the object to (like
the view point, light vector, frustrum, anything) into its own space. Since
the object's WorldMatrix takes it into world space (or if multiplied with
CameraMatrix, into view space), it means its inverse will put things from
world or view space into object space. And view or world space is where you'll
have most of these things.
Let's start with an easy example, the view point. This is always the origin
of view space - (0,0,0). Since WorldMatrix, after being multiplied by
CameraMatrix, will transform an object into view space, the inverse will
put s point from view space into object space. The relation of this new point to the
object's origin will be the same as if the object was in view space. So you
cound say that object_space( viewpoint - (0,0,0) ) = view_space( (0,0,0) - transformed_origin ).
This implies that viewpoint in object space will be -transformed_origin
which equals -translation. So it's clear that the inverse transformation
will (have to) create the effect of reverse motion. Same thing will happen
with rotations. That was kinda obvious from the name, eh?
Ok, you can find out how to create the inverse of a matrix from my matrix
doc [that will come someday, don't worry ;) If you need it urgently, just
ask me]. Once you have the inverse matrix, you don't transform the objects,
but whatever you wanted to compare them to. This creates obvious speed
improvements, because you usually have more vertices than view points,
for example :-). The other thing that's explained in the frustrum doc is
how to inverse transform the frustrum too, so that you only compare object
coordinates to it, only transfroming them when you know they're in.
Here's the drawing function with the inverse transformations this time:
void Object::draw(int clip=2)
{
Matrix InverseWorldMatrix=WorldMatrix.inverse();
Vector InverseLightVector=MyLight*InverseWorldMatrix; // since the light is in world space, the world matrix' inverse has to be used
// or
Vector InverseLightVector=MyLight.rotate(InverseWorldMatrix) // if it's a directional light source, only the rotation should be applied; same goes to any directional vector
transform(CameraMatrix);
Matrix InverseWrldMatrix=WrldMatrix.inverse();
MyFrustum.transform(InverseWrldMatrix); // the frustrum is defined in view space, so we have to use the whole view space transformation's inverse to get it into object space
Vector InverseViewPoint=Vector(0, 0, 0)*InverseWorldMatrix; // same with the view point
...
if(MyFrustum.in(vertex[v])) // this checks object coordinates against the above inverse transformed frustrum
{
if((vertex[v].obj_norm*(InverseViewPoint-vertex[0].obj))>0) // the object normal and point is used with the inverse transfromed view point
{
vertex[v].world=vertex[v].obj * WorldMatrix;
vertex[v].project();
vertex[v].out=0;
}
else
{
vertex[v].out=1;
}
}
...
}
Polygon Sorting
Depth sorting is described in my earlier tutorial, so I'll only present
optimizations here. Also, before you get into this, you should know that
there are a lot of other methods for drawing polygons in right depth order,
some of the even eliminating overdraw. Some of these are BSP trees, portal
rendering, s- and z-buffering. You can check out
some docs
about 'em.
Right now I just use simple z-sorting. I create a buffer of polys to output,
get their average z values, and sort them using any conventional sorting
algorythm. The key in my opinion to sorting speed is what you're actually
sorting. Do not, by any means, go sorting, switching full polygon structures.
What I do is store pointers to the polys in an array, as they are provided.
The size of this
array is determined at setup time by each object's constructor adding its number
of polys to the size of the list, ensuring everything will fit. It's not quite
dynamic enough, but it's not hard to initialize after all. The other choice
would be linked lists which are very dynamic, but could be sorting slower, depending
on the number of links, because you have to reassign more pointers.
With the array of pointers, I just switch two pointers like I did with integers
in my beginner's computer course. In the end I just go through the array
with a for loop and draw all the polys in order.
There's a good doc by Voltaire/OTM
on fast sorting algos useful for this kind of application, like many types
of radix sort. It's definately worth checking out if you wanna use anything
other than C's qsort or even better, a good ol' bubble sort like me :-)
Further References
Check out my collection of docs for all kinds of categories
This page was last updated on 12/06/97
Back to Steel's Programming Resources