Rendering Real-Time 3D Before GPUs

bhouston1 pts0 comments

Rendering Real-Time 3D Before GPUs<br>Between July 1996 and March 1997, while I was in high school, I built a software renderer that was capable of per pixel phong lightining, texture mapping with bump mapping. I taught myself most of the techniques from BBS tutorials, early internet writeups, demo-scene source releases, and Michael Abrash's writing.

I recently went back through the original codebase. This post is a write-up of how I solved the problem of getting high-quality real-time 3D results on hardware with no GPU, a slow FPU, and a 256-color display.

My development machine was my dad's 486 DX2 50. There was no GPU. There was no fixed-function 3D pipeline. The floating-point unit existed, but it was too slow for the frame rates I wanted. VGA Mode 13h gave me 320x200 and 256 colors. If I wanted convincing real-time 3D, I had to work around all of those constraints in software.

I wrote the engine in C++ with Borland C++ 4.0. Wherever the inner loops needed to be fast, I dropped into hand-written x86 assembly through Borland C++'s integrated TASM capabilities.

The full source is here GitHub as 3DMaskDemo1997.

VGA Mode 13h and a Software Double Buffer#

I used VGA Mode 13h , the straightforward 320x200 256-color mode that you could enable with a single BIOS interrupt.

The mode switch was as simple as:

// Switch the VGA card into 320x200 with 256 colors.<br>asm mov ax, 0x13<br>asm int 0x10

Mode 13h did not give me a second hardware page to flip between, so I built my own double buffering in software. Each frame rendered into an off-screen buffer in conventional memory. Once the frame was done, I copied only the dirty rectangle into video memory using a hand-written x86 block copy.

That gave me the flicker-free animation I wanted without moving to Mode X and its unchained page-flipping setup. I built a software substitute for hardware page flipping rather than hardware page flipping itself.

This is the dirty-rectangle copy routine:

void copyrect ( word source, word dest, recttype rect )<br>int copywidth, skipwidth, copyheight, startoffset;

// Round the rectangle to dword boundaries so the copy loop<br>// can use movsd efficiently.<br>copywidth = ((rect.r + 3) >> 2) - (rect.l >> 2);<br>skipwidth = 320 - (copywidth copyheight = rect.b - rect.t + 1;<br>startoffset = rect.t * 320 + ((rect.l >> 2)<br>// Copy only the dirty rectangle from the virtual screen to video memory.<br>asm push ds<br>asm mov ds, [source]<br>asm mov es, [dest]<br>asm mov si, [startoffset]<br>asm mov di, si<br>asm mov dx, [skipwidth]<br>asm mov bx, [copyheight]<br>lineloop:<br>asm mov cx, [copywidth]<br>asm rep movsd<br>asm add di, dx<br>asm add si, dx<br>asm dec bx<br>asm jnz lineloop<br>asm pop ds

Fixed-Point Math and Rotation#

The 486 could do floating-point math, but not quickly enough for the frame rates I wanted. I used fixed-point arithmetic instead.

Instead of using the entire range of a 32-bit integer for whole numbers, I reserved some of those bits for the fractional part. Then I could use ordinary integer add, subtract, and multiply instructions, as long as I kept track of where the implied decimal point lived. That gave me something that behaved enough like floating point for graphics work, but much faster on the hardware I had.

Different parts of the renderer used different fixed-point scales:

8 fractional bits for the rotation math

6 fractional bits for rasterizer screen-space coordinates

14 fractional bits for some texture and lighting coordinate intermediates before scaling them back down

I tuned the precision per subsystem instead of using one format across the whole engine.

I computed four trig values once per frame using the C math library, converted them into fixed point, and reused those cached values for every vertex in the object.

The optimization was doing the trig once per frame instead of once per vertex.

Here is the actual rotation setup from transformvertices():

// Compute the trig values once per frame and store them in<br>// 8-bit fixed-point form.<br>xcos = (int) ( cos ( (float)xrotation / MAX_ANGLE * M_PI * 2 ) * 256 );<br>xsin = (int) ( sin ( (float)xrotation / MAX_ANGLE * M_PI * 2 ) * 256 );<br>ycos = (int) ( cos ( (float)yrotation / MAX_ANGLE * M_PI * 2 ) * 256 );<br>ysin = (int) ( sin ( (float)yrotation / MAX_ANGLE * M_PI * 2 ) * 256 );

for ( loop = 0; loop // Load the object-space vertex.<br>x = objectvertex[loop].x;<br>y = objectvertex[loop].y;<br>z = objectvertex[loop].z;

// Rotate around X and shift away the fixed-point scale.<br>ytemp = y;<br>ztemp = z;<br>y = ( ytemp * xcos - ztemp * xsin ) >> 8;<br>z = ( ytemp * xsin + ztemp * xcos ) >> 8;

// Rotate around Y the same way.<br>xtemp = x;<br>ztemp = z;<br>x = ( xtemp * ycos - ztemp * ysin ) >> 8;<br>z = ( xtemp * ysin + ztemp * ycos ) >> 8;

Plastic Rendering via Phong Light Map#

Plastic mode was the simplest of the three renderers. It used a single material color and a procedurally generated Phong lighting map. That fit directly in the palette, so it did not need the texture quantization step that the textured mode needed later.

I computed...

point mode fixed frame rect loop

Related Articles