FreeBASIC 3D Game Programming via Software Rendering Part 3

By David Gutierrez a.k.a. Prime Productions
Founder of UGH Soft. (Useless Game Horizons)

Chapter 3: Rendering in Wireframe

Introduction:

This is the third installment of a 3D software rendering series I am writing for the Back 2 BASIC e-zine. You will need the FreeBASIC compiler to compile the source code I will be presenting here. This series is from the ground up, so it will start with the very basics. How far I will go depends on time and feedback. This series is meant to fill the gap for 3D tutorials for FreeBASIC. I do not claim to be the best 3D programmer, so some of the techniques I use may seem strange, so I encourage you to keep exploring, and to read other tutorials.

Wireframe Unravled:

Okay, now wireframe is basically taking the 3D points we have learned about and connecting them together with lines. The way we go about this is that we make an array containing the coordinates of each point, and then we take another array containing the number of the point to connect. At least this is one way to do it, my brother has another method. ;)

To make this clearer:

So the way we implement this is by first storing the coordinates of the points of the cube into an array, and storing the points. After that, simply loop through the points and connect the dots. Now that you got that, go ahead and implement it. :P Hey, wait! I want to see some code! You can't do this to me! Alright. alright. I'll SHOW you how to do it:

Let's start with some new routines, namely, a CalcX, and CalcY. This will take the place of our previous PSET3D routine. You can then make many different routines which simply make calls to this function.

DECLARE FUNCTION CalcX(X AS INTEGER, Z AS INTEGER) AS INTEGER
DECLARE FUNCTION CalcY(Y AS INTEGER, Z AS INTEGER) AS INTEGER
CONST XCENTER = 320
CONST YCENTER = 240
CONST ZCENTER = 256
CONST FOV = 256
FUNCTION CalcX(X AS INTEGER, Z AS INTEGER) AS INTEGER
RETURN(FOV * X / (Z + ZCENTER) + XCENTER) 
END FUNCTION
FUNCTION CalcY(Y AS INTEGER, Z AS INTEGER) AS INTEGER
RETURN(FOV * Y / (Z + ZCENTER) + YCENTER)
END FUNCTION
SCREENRES 640, 480, 16
DIM XCoord AS INTEGER
DIM YCoord AS INTEGER
DIM ZCoord AS INTEGER
DO
SCREENLOCK
CLS
FOR XCoord = -50 TO 50 STEP 10
FOR ZCoord = -50 TO 50 STEP 10
FOR YCoord = -50 TO 50 STEP 10
PSET (CalcX(XCoord, ZCoord), CalcY(YCoord, ZCoord)), RGB(255, 255, 255) 'We are now using PSET, but we are using our new CalcX, and CalcY vars.
NEXT
NEXT
NEXT
SCREENUNLOCK
LOOP UNTIL MULTIKEY(1)

 

What does this have to do with wireframe, you say? It is the same cube example, only without rotation! Patience, patience. I am coming to wireframe. Make sure you understand the changes to the code before proceeding. You do? Good. On to wireframe. What we are going to do next is store the coordinates for an 8 point cube in an array:

DIM CubeXPoints(8) AS INTEGER 'We have eight X coords for our 8 points.
DIM CubeYPoints(8) AS INTEGER 'We have eight Y coords for our 8 points.
DIM CubeZPoints(8) AS INTEGER 'We have eight Z coords for our 8 points.

 

Now that we have done that:

CubeXPoints(1) = -50 'First X point is -50
CubeXPoints(2) = -50
CubeXPoints(3) = 50
CubeXPoints(4) = 50
CubeXPoints(5) = -50
CubeXPoints(6) = -50
CubeXPoints(7) = 50
CubeXPoints(8) = 50

 

For Y:

CubeYPoints(1) = -50
CubeYPoints(2) = 50
CubeYPoints(3) = 50
CubeYPoints(4) = -50

 

Okay, I hope you can see that this is ridiculous. To much work. Fortunately, there is a much better way. We could use a type. Like so:

TYPE Point3D
X AS INTEGER
Y AS INTEGER
Z AS INTEGER
END TYPE
DIM CubePoints(8) AS Point3D

 

Cool isn't it? Now we can use each element like this:

CubePoints(1).X = -50

 

Now we don't have so many arrays! Let's also use a FOR...NEXT loop to make it easier:

DIM AS INTEGER X, Y, Z, PntNum
FOR X = -50 TO 50 STEP 100
FOR Z = -50 TO 50 STEP 100
FOR Y = -50 TO 50 STEP 100
PntNum = PntNum + 1
CubePoints(PntNum).X =  X
CubePoints(PntNum).Y = Y 
CubePoints(PntNum).Z = Z 
NEXT
NEXT
NEXT

 

This gives us the following code:

DECLARE FUNCTION CalcX(X AS INTEGER, Z AS INTEGER) AS INTEGER
DECLARE FUNCTION CalcY(Y AS INTEGER, Z AS INTEGER) AS INTEGER
DECLARE SUB LoadCube (PntArray() AS ANY)
CONST XCENTER = 320
CONST YCENTER = 240
CONST ZCENTER = 256
CONST FOV = 256
TYPE Point3D
X AS INTEGER
Y AS INTEGER
Z AS INTEGER
END TYPE
DIM SHARED CubePoints(8) AS Point3D
FUNCTION CalcX(X AS INTEGER, Z AS INTEGER) AS INTEGER
RETURN(FOV * X / (Z + ZCENTER) + XCENTER) 
END FUNCTION
FUNCTION CalcY(Y AS INTEGER, Z AS INTEGER) AS INTEGER
RETURN(FOV * Y / (Z + ZCENTER) + YCENTER)
END FUNCTION
SUB LoadCube (PntArray() AS Point3D)
DIM AS INTEGER X, Y, Z, PntNum
FOR X = -50 TO 50 STEP 100
FOR Z = -50 TO 50 STEP 100
FOR Y = -50 TO 50 STEP 100
PntNum = PntNum + 1
PntArray(PntNum).X =  X
PntArray(PntNum).Y = Y 
PntArray(PntNum).Z = Z 
NEXT
NEXT
NEXT
END SUB
LoadCube(CubePoints())
SCREENRES 640, 480, 16
DIM XCoord AS INTEGER
DIM YCoord AS INTEGER
DIM ZCoord AS INTEGER
DO
SCREENLOCK
CLS
'And to render:
DIM PntNum AS INTEGER
FOR PntNum = 1 TO 8 'Loop for all points
PSET (CalcX(CubePoints(PntNum).X, CubePoints(PntNum).Z), CalcY(CubePoints(PntNum).Y, CubePoints(PntNum).Z)), RGB(255, 255, 255)
NEXT
SCREENUNLOCK
LOOP UNTIL MULTIKEY(1)

 

Okay, we now have the first steps toward wireframe! The next part operates on the same principles, it is only for the point connections data now. What we are going to do is take the data of three points for a triangle. When we do wireframe, we build everything out of triangles. So if a cube has 6 faces, it will have 12 triangles, each quad split in half. The following is the code:

DECLARE FUNCTION CalcX(X AS INTEGER, Z AS INTEGER) AS INTEGER
DECLARE FUNCTION CalcY(Y AS INTEGER, Z AS INTEGER) AS INTEGER
DECLARE SUB LoadCube (PntArray() AS ANY, TriArray() AS ANY) 'We need to add another argument to the LoadCube SUB
CONST XCENTER = 320
CONST YCENTER = 240
CONST ZCENTER = 256
CONST FOV = 256
TYPE Point3D
X AS INTEGER
Y AS INTEGER
Z AS INTEGER
END TYPE
TYPE Tri2D 'And we need another type for the tris. 
P1 AS INTEGER
P2 AS INTEGER
P3 AS INTEGER
END TYPE
DIM SHARED CubePoints(8) AS Point3D
DIM SHARED CubeTris(12) AS Tri2D
FUNCTION CalcX(X AS INTEGER, Z AS INTEGER) AS INTEGER
RETURN(FOV * X / (Z + ZCENTER) + XCENTER) 
END FUNCTION
FUNCTION CalcY(Y AS INTEGER, Z AS INTEGER) AS INTEGER
RETURN(FOV * Y / (Z + ZCENTER) + YCENTER)
END FUNCTION
SUB LoadCube (PntArray() AS Point3D, TriArray() AS Tri2D)
DIM AS INTEGER X, Y, Z
'We'll manually set up the coordinate data so you better understand what is going on.
'Next issue, we'll load from an external file.
PntArray(1).X = -50
PntArray(1).Y = 50
PntArray(1).Z = 50
PntArray(2).X = 50
PntArray(2).Y = 50
PntArray(2).Z = 50
PntArray(3).X = 50
PntArray(3).Y = 50
PntArray(3).Z = -50
PntArray(4).X = -50
PntArray(4).Y = 50
PntArray(4).Z = -50
PntArray(5).X = -50
PntArray(5).Y = -50
PntArray(5).Z = 50
PntArray(6).X = 50
PntArray(6).Y = -50
PntArray(6).Z = 50
PntArray(7).X = 50
PntArray(7).Y = -50
PntArray(7).Z = -50
PntArray(8).X = -50
PntArray(8).Y = -50
PntArray(8).Z = -50
'We'll manually set up the connection data. Next issue, we'll load from an external file.
TriArray(1).P1 = 6
TriArray(1).P2 = 5
TriArray(1).P3 = 1
TriArray(2).P1 = 6
TriArray(2).P2 = 1
TriArray(2).P3 = 2
TriArray(3).P1 = 7
TriArray(3).P2 = 3
TriArray(3).P3 = 4
TriArray(4).P1 = 4
TriArray(4).P2 = 8
TriArray(4).P3 = 7
TriArray(5).P1 = 7
TriArray(5).P2 = 6
TriArray(5).P3 = 2
TriArray(6).P1 = 7
TriArray(6).P2 = 2
TriArray(6).P3 = 3
TriArray(7).P1 = 8
TriArray(7).P2 = 1
TriArray(7).P3 = 5
TriArray(8).P1 = 8
TriArray(8).P2 = 4
TriArray(8).P3 = 1
TriArray(9).P1 = 7
TriArray(9).P2 = 8
TriArray(9).P3 = 5
TriArray(10).P1 = 7
TriArray(10).P2 = 5
TriArray(10).P3 = 6
TriArray(11).P1 = 1
TriArray(11).P2 = 4
TriArray(11).P3 = 3
TriArray(12).P1 = 2
TriArray(12).P2 = 1
TriArray(12).P3 = 3
END SUB
LoadCube(CubePoints(), CubeTris())
SCREENRES 640, 480, 16
DIM XCoord AS INTEGER
DIM YCoord AS INTEGER
DIM ZCoord AS INTEGER
DO
SCREENLOCK
CLS
'And to render:
DIM TriNum AS INTEGER
DIM AS INTEGER PX1, PX2, PX3, PY1, PY2, PY3, PZ1, PZ2, PZ3
FOR TriNum = 1 TO 12 'Loop for all tris
PX1 = CubePoints(CubeTris(TriNum).P1).X 'The first X coord of our triangle
PX2 = CubePoints(CubeTris(TriNum).P2).X 'The second X coord of our triangle
PX3 = CubePoints(CubeTris(TriNum).P3).X 'etc.
PY1 = CubePoints(CubeTris(TriNum).P1).Y
PY2 = CubePoints(CubeTris(TriNum).P2).Y
PY3 = CubePoints(CubeTris(TriNum).P3).Y
PZ1 = CubePoints(CubeTris(TriNum).P1).Z
PZ2 = CubePoints(CubeTris(TriNum).P2).Z
PZ3 = CubePoints(CubeTris(TriNum).P3).Z
LINE (CalcX(PX1, PZ1), CalcY(PY1, PZ1))-(CalcX(PX2, PZ2), CalcY(PY2, PZ2)), RGB(255, 255, 255) 'Lines for wireframe instead of points. We
LINE (CalcX(PX2, PZ2), CalcY(PY2, PZ2))-(CalcX(PX3, PZ3), CalcY(PY3, PZ3)), RGB(255, 255, 255) 'connect point 1 to 2, 2 to 3, and 3 back to 1.
LINE (CalcX(PX3, PZ3), CalcY(PY3, PZ3))-(CalcX(PX1, PZ1), CalcY(PY1, PZ1)), RGB(255, 255, 255)
NEXT
SCREENUNLOCK
LOOP UNTIL MULTIKEY(1)

 

That was a lot of work. But we're all done. A complete wireframe cube! Now I know this isn't the cleanest way to load a cube, but that will be fixed next time around. As a final challenge, and to see if you were paying attention last issue, try to make a rotating wireframe pyramid!

Conclusion:

I hope you've enjoyed learning about wireframe. Next chapter, Building a reusable 3D engine. Until then, Happy Coding!

Email me at: david.primeproductions.gutierrez AT gmail DOT com with questions or comments.