13: Moving into the 3rd dimension
A few days after releasing my demo code in lesson 11 of this site, Hikey suggested a future tutorial might detail how to draw a textured cube.  So began this tutorial.

Why all the fuss with a textured cube?
Putting a textured cube on the screen requires knowledge of:
    1) Texture uploading VU->GS
    2) Storing a model of texture co-ordinates and vertex co-ordinates
    3) Transforming those model co-ordinates to world co-ordinates using a camera matrix
    4) Transforming those world co-ordinates to screen co-ordinates using a perspective projection

Basically, we're doing a lot of very general steps that come in very handy for any model, not just your basic cube.

Part 1 has already been covered in 9: Uploading Image data, and you'll also find useful details in 10: Texture Mapping.  This tutorial starts off with a brief resume of 3D graphics techniques, then adds the texture mapping code, then finally adds some interactivity to make a nice-looking basic 3D sample.

Memory mapping
Again, we start building up our demo by getting a solid idea of how we're going to use the available memory... cue Memory Map!

QWord Usage
0 Start of Model Data - STQ of vertex 1
1 XYZ(W) of vertex 1
2 STQ of vertex 2
... ... (Model Data)
71 XYZ(W) of vertex 36
72 GIFtag for drawing the cube
73 Transformed STQ of vertex 1
74 RGBA of vertex 1
75 Transformed XYZ of vertex 1
... ... (Transformed Vertex Data)
180 Transformed XYZ of vertex 36
181-184 Object to World matrix
185-188 World to Camera matrix
189-192 Camera to Screen matrix (Perspective projection)
193-196 Incremental rotation matrix
197 Scaling vector
198-202 Texture uploading tag
203 Texture data tag
204-715 Texture data
716-1021 Unallocated
1022 Texture upload flag
1023 Harness Data


The first thing to note is that there are two separate areas for the model data, pre- and post-transformation.  This is usual to avoid all sorts of errors that can occur from repeated calculations on the same numbers. (Although later on I break my own advice to make one calculation a little easier...).  First of all you define a model with its own centre at the origin, so to make our unit cube we'll use vertices from (-0.5, -0.5, -0.5) to (0.5, 0.5, 0.5).  This ensures that we don't get an unwanted translation effect when we rotate the model.

To get from those object co-ordinates to world co-ordinates, you would usually apply a rotation to the model to get it to face the right way, then translate it to the part of the 3D world you want it to inhabit.  Then, you would transform from world to camera co-ordinates by applying a transformation to simulate the action of a camera, and finally you apply a projection of some sort to drop from 3D camera co-ordinates to 2D screen co-ordinates.  There are many ways of doing this projection, I'm just going to give you a projection matrix to use.  If you want to know more about projections, I may well write another tutorial later, but you could always consult a good graphics programming book, e.g. Computer Graphics: Principle and Practice by Foley et al.

Also to note is that throughout this first demo, I've defined a cube by defining each face in terms of 2 triangles, specifying all 3 vertices every time, making for a total vertex count of 36.  As you may be aware, there are only 8 vertices on a cube, so why the duplication?  The answer is that I'm dealing with triangles, which are easy to work with and fit nicely in with my previous tutorials.  Next tutorial I will work on some optimisations for the same demo, one of which will be to use triangle strips instead of triangles, and this will greatly reduce my memory consumption.  So, this tutorial is about getting it working, next tutorial will be about getting it working *well*.

Generate that data
I've been given a helping hand with the data generation program by using some of the files found in the common/samples directory of the sps2 project, namely geommath.h which includes files to define vectors, matrices and quaternions.  I'll put them up on my project page if I get permission from Sparky et al, but until then, you'll have to download the sps2 project and use them from there.

The files define 5 new types that come in very handy.  Vec3 is a 3-Dimensional vector, Vec4 is it's 4-Dimensional equivalent.  Casting from Vec3 to Vec4 just adds a w field of 1.0f, casting from Vec4 to Vec3 makes (X,Y,Z,W) go to (X/W, Y/W, Z/W).  Two types to define matrices are included, Mat33 for a 3x3 matrix, and (you guessed it) Mat44 for a 4x4 matrix.  Finally there is the Quat type for use with quaternions.  In this tutorial, I'll only be using the Vec and Mat types.

As a side note, the included files also define the *, +, -, +=, -= and *= operations, and the == and != comparison operators.  Then there are functions to calculate the cross product of vectors, the inverse and determinant of matrices, and a whole host of other useful functions.  I'll use a few of them here, but for a full list of what's available, check out the *.h files!

OK, onto the actual data generation.  It's tedious, but here's my definition of a cube:

Vec4 verts[NUM_VERTS] = {    Vec3(-0.5f,-0.5f, 0.5f),
                        Vec3( 0.5f,-0.5f, 0.5f),
                        Vec3(-0.5f, 0.5f, 0.5f),
                        Vec3( 0.5f, 0.5f, 0.5f),
                        Vec3(-0.5f, 0.5f, 0.5f),
                        Vec3( 0.5f,-0.5f, 0.5f),

                        Vec3(-0.5f,-0.5f,-0.5f),
                        Vec3(-0.5f, 0.5f,-0.5f),
                        Vec3( 0.5f,-0.5f,-0.5f),
                        Vec3( 0.5f, 0.5f,-0.5f),
                        Vec3( 0.5f,-0.5f,-0.5f),
                        Vec3(-0.5f, 0.5f,-0.5f),

                        Vec3(-0.5f,-0.5f,-0.5f),
                        Vec3(-0.5f,-0.5f, 0.5f),
                        Vec3(-0.5f, 0.5f,-0.5f),
                        Vec3(-0.5f, 0.5f, 0.5f),
                        Vec3(-0.5f, 0.5f,-0.5f),
                        Vec3(-0.5f,-0.5f, 0.5f),

                        Vec3( 0.5f,-0.5f,-0.5f),
                        Vec3( 0.5f, 0.5f,-0.5f),
                        Vec3( 0.5f,-0.5f, 0.5f),
                        Vec3( 0.5f, 0.5f, 0.5f),
                        Vec3( 0.5f,-0.5f, 0.5f),
                        Vec3( 0.5f, 0.5f,-0.5f),

                        Vec3(-0.5f, 0.5f, 0.5f),
                        Vec3( 0.5f, 0.5f, 0.5f),
                        Vec3(-0.5f, 0.5f,-0.5f),
                        Vec3( 0.5f, 0.5f,-0.5f),
                        Vec3(-0.5f, 0.5f,-0.5f),
                        Vec3( 0.5f, 0.5f, 0.5f),

                        Vec3(-0.5f,-0.5f, 0.5f),
                        Vec3(-0.5f,-0.5f,-0.5f),
                        Vec3( 0.5f,-0.5f, 0.5f),
                        Vec3( 0.5f,-0.5f,-0.5f),
                        Vec3( 0.5f,-0.5f, 0.5f),
                        Vec3(-0.5f,-0.5f,-0.5f) };

One little trick I've used in there, because I'm lazy, is to define verts[ ] as an array of Vec4s, but to fill it with an array of Vec3s, which will be automatically cast to Vec4.  This saves me having to put ",1.0f" on every line of the code.  Lazy, I know, but I'll live :)  Another thing to note, which will come in handy in your future graphical career, is that I've defined each triangle's vertices in a clockwise order as you look at the cube.  Clockwise doesn't matter so much, but the fact that they're all defined in the same order is important.  If you know about cross products, you'll appreciate why.

(The cross product of two vectors is itself a vector, perpendicular to the other two.  There are 2 possibilities, one pointing in exactly the opposite direction to the other.  If you're consistent with your vertex labelling, then your cross products will all come out in the same way.  For this demo however, it doesn't matter.)

Then, even more tediously, follow the STQ texture co-ordinates.  I decided, boringly enough, that each face should contain the entire texture, so each face is defined the same, like so:

Vec4 texts[NUM_VERTS] = {    Vec3(0.0f,0.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,1.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),

                        Vec3(0.0f,0.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,1.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),

                        Vec3(0.0f,0.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,1.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),

                        Vec3(0.0f,0.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,1.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),

                        Vec3(0.0f,0.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,1.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),

                        Vec3(0.0f,0.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,1.0f,1.0f),
                        Vec3(0.0f,1.0f,1.0f),
                        Vec3(1.0f,0.0f,1.0f) };

Nothing groundbreaking so far, eh?  Now we want to actually write that model out into the data file, followed by the GIFtag that will display it.  For now, seeing as we haven't uploaded a texture yet, let's keep this untextured.  We'll still send the STQ co-ordinates, but the GS will know not to bother looking at them.

gifpacket_init(&gifpacket, packet_data);

for (i=0; i<NUM_VERTS; i++)
{
    gifpacket_addgsvec4(&gifpacket, &(texts[i]));
    gifpacket_addgsvec4(&gifpacket, &(verts[i]));
}

prim=GS_SET_PRIM(GS_PRIM_TRI, GOURAUD(1), TEXTURED(0), FOG(0), ABE(0), ALPHA(0), AA(0), CTXT(0), FIX(0));
gifpacket_addgsdata(&gifpacket, GIF_SET_TAG(NLOOP(NUM_VERTS), EOP(1), PRE(1), prim, GIF_PACKED, NREG(3)));
gifpacket_addgsdata(&gifpacket, 0x512);

for (i=0; i<NUM_VERTS; i++)
{
    colour.ui32[0] = 127 * (verts[i].z<0);
    colour.ui32[1] = 127 * (verts[i].z>0);
    colour.ui32[2] = 0;
    colour.ui32[3] = 0x80;
    gifpacket_addgspacked(&gifpacket, 0); // Leave a gap for transformed STQ co-ords
    gifpacket_addgspacked(&gifpacket, colour.ul128);
    gifpacket_addgspacked(&gifpacket, 0); // Leave a gap for transformed vertex co-ords
}

The colour lines will ensure that one face is red, one is green, and the other faces fade from red to green, which should look nice (ish :)  Notice, as I said before, that the GIFtag contains STQ information (check out NREG and REGS), but the prim field says TEXTURED(0).  That way, once we're sure that our cube is working properly, we can switch texturing on without a problem.

However, we're not quite done yet.  We're going to need to put 4 4x4 matrices in, 3 of them describing various transformations to get object co-ordinates on the screen, and one extra one to define a small rotation around each axis:

    Mat44 objectToWorld;
    LoadIdentity(&objectToWorld);

    Mat44 worldToCamera;
    LoadIdentity(&worldToCamera);
    SetRow(&worldToCamera, 3, Vec4(0,0,-5.0f,1));
    gifpacket_addgsmat44(&gifpacket, &worldToCamera);

    Mat44 cameraToScreen = makePerspectiveMatrix();
    gifpacket_addgsmat44(&gifpacket, &cameraToScreen);

    Mat44 rotate;
    LoadRotation(&rotate, 0.1f, 0.05f, 0.025f);
    gifpacket_addgsmat44(&gifpacket, &rotate);

where the Mat44 type, and the associated LoadIdentity(), SetRow(), and LoadRotation() functions are defined in the files included form the sps2 project.  However, I've added my own functions gifpacket_addgsmat44() and gifpacket_addgsvec4() to the packet.c file from the original basic sample, and the updated files are available from my project page.  Note that I've stripped out those unneeded VIFpacket instructions.

I've also added the following makeWorldToScreen() function to my main code:

Mat44 makeWorldToScreen()
{
    Mat44 perspective;

    //Change the 0.06 for a different field of view.
    float n = 0.1f;
    float f = 1000.0f;
    float l = -0.06f * (2048.0f / (640.0f * 0.5f));
    float r =  0.06f * (2048.0f / (640.0f * 0.5f));
    float t =  0.06f * (2048.0f / (350.0f * 0.5f));
    float b = -0.06f * (2048.0f / (350.0f * 0.5f));

    LoadIdentity(&perspective);
    SetRow(&perspective,0,Vec4((2*n) / (r-l)    , 0.0f            , 0.0f                , 0.0f));
    SetRow(&perspective,1,Vec4(0.0f            , -(2*n) / (t-b)    , 0.0f                , 0.0f));
    SetRow(&perspective,2,Vec4(((r+l) / (r-l))    , ((t+b) / (t-b))    , ((f+n) / (f-n))        , -1.0f));
    SetRow(&perspective,3,Vec4(0.0f            , 0.0f            , ((2*f*n) / (f-n))        , 0.0f));
    return (perspective);
}

Thanks go out to Kazan and Sparky for the meat of that function.

Here's how it works.  f, n, l, r, t and b define a viewing frustrum, and stand for far, near, left, right, top and bottom respectively.  Any vertices in this frustrum are projected to lie within (-1, -1, -1) and (1, 1, 1) by the perspective matrix (after the divide by w step) and so anything lying outside this box can be clipped.  More on clipping later.  However, we need to scale those co-ordinates into framebuffer and Z-buffer co-ordinates, so we need X and Y to lie between 0 and 4096, and Z to lie between 0 and the largest number 24 bits can hold.  This 'scaling vector' takes care of it:

scales.f32[0] = 2048.0f;
scales.f32[1] = 2048.0f;
scales.f32[2] = (float)((2<<24)-1)/2;
scales.f32[3] = 1.0f;
gifpacket_addgspacked(&gifpacket,scales.ul128);

However, bear in mind that this only scales the X and Y values to lie between -2048 and +2048, something else must move them along to lie between 0 and 4096.  Thankfully, there's a macro that takes care of this for us in the VCL Standard Macro Library (vcl_sml.i) named VertexFPtoGsXYZ2.

Phew, now what do we do with all this stuff?
Now we go about putting this all on the screen.  The general idea is like this - we start off with a number of transformation matrices, one to take us from object co-ordinates to world co-ordinates, one to take us form world to camera co-ordinates, and one to take us from camera to screen co-ordinates.  We multiply each of these together to create an object to screen matrix.  Think of it like a concatenation.  Object->World * World->Camera * Camera->Screen = Object to Screen.  Then, we loop through each vertex of the model, and multiply it by our new Object to Screen matrix, and display the results.

Generally, your camera to screen matrix is not going to change.  That's going to be your perspective matrix from earlier.  A lot of demos have a constant camera matrix, as they don't allow the camera to move.  Our demo is going to let the camera move, but only along the Z-axis, it's not going to be a totally free camera.  What normally changes the most in programs is the object to world matrix.  Remember that our model is defined with the origin at it's own centre, so you somehow have to get it into the correct position in the world, and pointing the right way.  That's what you'll use the object to world matrix for.

However, I'm going to cut corners in a way by rotating the actual model, and storing the transformed co-ordinates back over the original co-ordinates.  Also, I'm going to allow the camera to move, but keep the object in place.  Thus, I don't need an object to world matrix at all, my object co-ordinates *are* my world co-ordinates.  For that reason, my object to world matrix is just an identity matrix... but I've left it in so that you can see what you would change if you want your object to move.

(Aside [Note 1]: I did rewrite my code to update the mObjectToWorld matrix each frame, but found that successive matrix multiplications quite quickly skewed the cube whilst it was spinning.  The workaround for this would make my code much more complicated, so to keep things simple I've broken my own rule of not modifying the model during the course of the demo.  That way, you can see how things work and take them from there.)

Here comes some code...

    VectorLoad fScales, kScales, vi00

    MatrixLoad mObjectToWorld, kObjectToWorld, vi00
    MatrixLoad mWorldToCamera, kWorldToCamera, vi00    
    MatrixLoad mCameraToScreen, kCameraToScreen, vi00
    MatrixLoad mRotate, kRotate, vi00
    
    ;Ordinarily you would alter the mObjectToWorld matrix here (See note 1 above)

    ;Compute Object to Screen matrix (Object to World * World to Camera * Camera to Screen)
    MatrixMultiply mObjectToScreen, mWorldToCamera, mCameraToScreen
    MatrixMultiply mObjectToScreen, mObjectToWorld, mObjectToScreen
    
    ;Loop through transforming each model vertex, storing transformed verts into GIFtag.
    iaddiu iVertPointer, vi00, kModelStart
    iaddiu iVertEnd, vi00, kModelEnd + 1
    iaddiu iStoreTo, vi00, kCubeStart
    
TRANSFORM_LOOP:
    VertexLoad vST, 0, iVertPointer
    VertexLoad vVert, 1, iVertPointer
    
    MatrixMultiplyVertex vVert, mRotate, vVert
    VertexSave vVert, 1, iVertPointer ;Overwrite original model values to allow rotation to increment
    ;(See note 1 above)
    
    MatrixMultiplyVertex vVert, mObjectToScreen, vVert

    div q, vf00[w], vVert[w]
    mul.xyz acc, fScales, vVert[w]
    madd.xyz vVert, vVert, fScales
    mul.xyz vVert, vVert, q
    mul.xyz vST, vST, q
    
    VertexFptoGsXYZ2 vVert, vVert
    VertexSave vST, 0, iStoreTo
    VertexSave vVert, 2, iStoreTo
    iaddiu iVertPointer, iVertPointer, 2
    iaddiu iStoreTo, iStoreTo, 3
    ibne iVertPointer, iVertEnd, TRANSFORM_LOOP:

The Vector*, Vertex*, and Matrix* macros all come from vcl_sml.i.  Their use is pretty self explanatory, once you realise that vVert is really just a floating point register, and mRotate is the first in a consecutive series of 4 floating point registers.

The important things to realise here are that I've kept matrix multiplication out of the loop, and put only two vertex * matrix calculations in the loop, to save myself many repeated calculations.  As I said before, I really should have only the one calculation in the loop, but I cut corners to make things simple.  I deliberately haven't used the VertexPerspCorr macro available in vcl_sml.i because I needed to introduce the fScales vector, as I explained earlier.  Also, I've transformed the STQ vertices, even though I'm not yet using them, so again I can easily switch on texturing in the future.

I think we're ready to fly now!  Putting this code together, you should see a nice spinning cube in the centre of the screen.  For practice, why don't you add a section which checks to see if R1 or R2 are pressed, and if so, moves the camera closer or further away from the cube.  If you do this, you'll notice one of the many flaws in the above code... there is no clipping!

Clipping is the term used for not drawing things that can't be seen.  Back-face culling is a time saving technique which ensures that faces of a model that face away from you aren't drawn, which should save you some processing time.  With the code as it stands, you can place objects behind the camera, and you'll still see them!  As I said right at the start of this tutorial, I'm also wasteful in using triangles instead of triangle strips.  However, all of these extra pieces of code are beyond the scope of this tutorial, we'll see them next time.

I've written a sample code file which does the coloured spinning cube example above, then extends it to allow texturing (for those interested, my lava.h file was created through clever use of the hexdump program on a Windows .BMP file (remember that .BMP stores colours in 24-bit BGR order, bear that in mind when you read the colour code in that file)  Type man hexdump to find out more about the hexdump program, especially the region on supplying format strings in a text file

Thanks to Sparky for graciously allowing me to post parts of his sample code on my site.  Incase you haven't checked it out yet, please visit the SPS2 project and check it out, it makes working with the PS2 through Linux a lot nicer, and the sample code is much nicer than the libps2dev sample code.  Of course, finish your VU demo for the competiton first...

News: It seems that Sauce is starting up an SPS2 tutorial series - I feel pretty certain that this will make excellent reading, knowing Sauce's level of knowledge, so if you've enjoyed these tutorials then please check out the SPS2 project and get reading Sauce's tutorials.

Prev - 12: Plasmaroids Next - Comments and suggestions