Building a 3D Portal Engine - Issue 05 - Coding A Wireframe Cube by (29 January 1999) Return to The Archives
 Introduction Welcome to the fifth episode of Phantom's Guide! This week we'll discuss the real basics of 3D: The famous wireframe cube. If you're an expert that just came here for scientific discussions of the latest 3D topics, you can leave the room now. Check back next week, the fancy stuff will follow.

The purpose of this article is to get you going if you really don't know where to start. I can rant about PTC and windows programming, data structures and so on, but in the end there's nothing like a good example. Well, I've just coded a very small program (54 lines of code, in fact) that just shows a wireframe cube, and that might be just what you need. Here's the code:

 ``` #include "ptc.h" #include "math.h" float angle, x, y, z, rx, ry, rz, scrx, scry; void line (unsigned short* buf, float x1, float y1, float x2, float y2) { double hl=fabs(x2-x1), vl=fabs(y2-y1), length=(hl>vl)?hl:vl; float deltax=(x2-x1)/(float)length, deltay=(y2-y1)/(float)length; for (int i=0; i<(int)length; i++) { unsigned long x=(int)(x1+=deltax), y=(int)(y1+=deltay); if ((x<640)&&(y<480)) *(buf+x+y*640)=65535; } } void render (unsigned short* buf, float xa, float ya, float za) { float mat; // Determine rotation matrix float xdeg=xa*3.1416f/180, ydeg=ya*3.1416f/180, zdeg=za*3.1416f/180; float sx=(float)sin(xdeg), sy=(float)sin(ydeg), sz=(float)sin(zdeg); float cx=(float)cos(xdeg), cy=(float)cos(ydeg), cz=(float)cos(zdeg); mat=cx*cz+sx*sy*sz, mat=-cx*sz+cz*sx*sy, mat=cy*sx; mat=cy*sz, mat=cy*cz, mat=-sy; mat=-cz*sx+cx*sy*sz, mat=sx*sz+cx*cz*sy, mat=cx*cy; for (int i=0; i<8; i++) // Rotate and apply perspective { rx[i]=x[i]*mat+y[i]*mat+z[i]*mat; ry[i]=x[i]*mat+y[i]*mat+z[i]*mat; rz[i]=x[i]*mat+y[i]*mat+z[i]*mat+300; scrx[i]=(rx[i]*500)/rz[i]+320, scry[i]=(ry[i]*500)/rz[i]+240; } for (i=0; i<4; i++) // Actual drawing { line (buf, scrx[i], scry[i], scrx[i+4], scry[i+4]); line (buf, scrx[i], scry[i], scrx[(i+1)%4], scry[(i+1)%4]); line (buf, scrx[i+4], scry[i+4], scrx[((i+1)%4)+4], scry[((i+1)%4)+4]); } } int APIENTRY WinMain(HINSTANCE hInst,HINSTANCE hPrevInst,LPSTR lpCmdLine,int nCmdShow) { for (int i=0; i<8; i++) // Define the cube { x[i]=(float)(50-100*(((i+1)/2)%2)); y[i]=(float)(50-100*((i/2)%2)), z[i]=(float)(50-100*((i/4)%2)); } Console console; // Initialize PTC and start rendering Format format (16, 31<<11, 63<<5, 31); console.open ("3D", 640, 480, format); Surface surface (640, 480, format); while (!console.key ()) { unsigned short* buf=(unsigned short*)surface.lock (); memset (buf, 0, 640*480*2); render (buf, angle, 360-angle, 0); angle+=0.2f; if (angle==360) angle=0; surface.unlock(); surface.copy (console); console.update(); } return 0; } ```

Please take a moment to get this stuff running. Using VC5 or 6, create a new Win32 application with no files in it. Add the PTC lib file to the project (using add files) and the ptc.h header file, and of course the source that I just showed. Next you have to disable the inclusion of two default libraries (under the 'linker input settings'), LIBC and LIBCD. Now the program should compile just fine. You might try to make it slightly more readible by splitting lines that do multiple variable initializations on a single line. :)

So what does this program do? Let's start with the program entry, which is in this case the WinMain function. The first thing that happens here is the definition of a cube. A cube has eight vertices, which could be initialized as follows:

 ``` 1. x: -50 y: -50 z: -50 2. x: 50 y: -50 z: -50 3. x: 50 y: 50 z: -50 4. x: -50 y: 50 z: -50 5. x: -50 y: -50 z: 50 6. x: 50 y: -50 z: 50 7. x: 50 y: 50 z: 50 8. x: -50 y: 50 z: 50 ```

The weird construction with the modulo operators does just that (only shorter:). If you take a moment to visualize this data, you'll see that these are 8 vertices centered around (0, 0, 0), wich is handy, because rotations are easy around the origin of 3D space.

After that, we need to set up PTC. I used a 16 bit display in this case, at a resolution of 640x480 pixels. This video mode should work on most computers.

In the main loop the function 'render' is called, with a pointer to the PTC buffer and the rotation around the three axi as input. Note that the rotations are passed in degrees.

The 'render' function is slightly more interesting. Let's see what it needs to do: Ultimately it should draw lines between the rotated vertices, and those lines should be somewhere near the center of the screen. Rotations are done using a matrix. If you forgot how that worked, browse back to the article that discusses them. As you know, a rotation around three axi can be performed by calculating the matrix for the rotation around each axis, and them concatenating them. In this case, I have done the concatenation for you: The matrix 'mat' is filled with sins and cosines at once. I encourage you to alter the code so that the final matrix is calculated by concatenating the separate matrices, so that you can also rotate around the axi in a different order.

The rotated vertices are still centered at the origin. Since perspective calculations do a nice divide by the z coordinate, we need to move the object away from the camera. This is done by adding 300 to the rotated z. Note that you could also add something to x and y: This is how you rotate an object around something else than the origin. In this case, the object is effectively rotated around (0, 0, 300).

Finally, the perspective is calculated. Note that the object is also centered on screen, by adding 320 to the screen-x coordinate, and 240 to the screen-y coordinate.

Now the lines can be drawn to the screen. The line function that I included is very short, and that's the only thing that is good about it. If you need fast code, ditch this function, and include your own assembler bresenham code. Some comments about this code:
• It first determines how many pixels need to be drawn. If the line has a higher vertical than horizontal range, it draws abs(y2-y1) pixels, otherwise abs(x2-x1). This prevents gaps.

• When drawing pixels, each subsequent screen location can be calculated by adding something to the first x-coordinate (x1) and to the first y-coordinate (y2). This 'something' is actually the x or y range, divided by the total number of pixels to be drawn. When you think about it, it's logical that you reach (x2,y2) after adding 'n' times a bit to x and y, where 'n' is the calculated number of pixels. Also note that either 'delta-x' or 'delta-y' is exactly 1.
• If you want to play a bit around with this code, then here are some suggestions:

* Editor's Note: Feel free to submit any modified versions of this program that you make, and I'll probably post it here with your name.
• Alter the line drawing code so that it accepts other colors. Currently the color is always 65535, wich is pure white in a 16 bit color mode. This color consists of red, green and blue: Red is 5 bits, green 6, and blue 5. The final color is calculated with the following formula: red*2048+green*32+blue. Note that red should be an integer between 0 and 31, and so is blue. Green is an integer between 0 and 63.

• Play a little with the position of the object. It can also be partially off-screen, the line drawing code doesn't crash.

• Try to create something else than a cube. Using this code you could design your own name in glorious 3D. :)

• Extend the data structures so that the connections between vertices are no longer hard coded. You could for example define edges, wich should hold a start and an end vertex. Then the rendering code should draw all the edges, wich should make the code much nicer if you have a lot of edges. You could also introduce 'polygons', wich hold more than two vertices.

• Add a cube, and use the stuff from the matrices document to make the second cube spin around the first. To get this right, construct the second cube around (100,0,0), so that the rotation causes it to 'swing' around the first cube.

• Note that the code I just presented is really ugly. It isn't object oriented, doesn't use good structures (not even a polygon structure), and does really nasty matrix math without concatenations. I realize that this sucks, but I think some of you will appreciate a 'single-A4' working example.

This should get you going for the moment. Please check back next week, I'll discuss 'hidden surface removal' then. That will get rid of the ugly wires on the back of the cube... And some more, too.