So you want to do 3D graphics? First, you have to understand the math. There's no other way to do it. The concepts behind the graphics are not difficult, however. What we're dealing with is basically the transformation of points from this:

If we take a closer look at what we're given to work with -- that is, the 3D space, or the so-called 'worldspace', in which our points are originally represented, from top-down, we see this:

The Y axis is no longer visible since it extends directly upwards from the diagram, away from your screen. Now, let's place a typical point into worldspace:

The point P is at coordinates (x,y,z). Now, the concept of what we want to do is that we want to place the point on the screen so that it will be along the line of sight between the eye and the real point P, in the hopes that we can fool the eye into believing that it IS the point P. To do this, we need some representation of the screen in our 3D world. This is easier than it sounds, because the screen is nothing more than a section of a plane -- perfectly flat. Things become substantially simpler if we restrict the screen to be parallel to the XY plane. Our diagram now looks like this:

Two points are important in setting up the line-of-sight between the eye and the point P, and those are the eye and the point P themselves. We know where the point P is, but we haven't declared where the eye is yet. Having placed the screen like this, somewhat away from the XY plane, we can use the origin as the point that we're projecting from. Representing this in the diagram gives us this:

.. where S is the point whose coordinates we need to know to place the pixel on the screen in the line-of-sight from E to P. Getting the coordinates of this point is easy. We know that the 'z' coordinate is the same as the screen, and we can put the screen wherever we want to. For simplicity, we can make this value 1. Getting the X and Y coordinates is a little bit more complicated, but still not hard. Notice that a system of similar triangles has been set up: the triangle with ES as hypotenuse has the same angles as the triangle with EP as hypotenuse. This means that there is proportionality between the edges of the two triangles. To simplify things, let's add two points to the diagram:

Using these 5 points, and denoting the distance between two points by specifying the two points (eg, EP for the distance from the eye to the point), the following proportionalities can be observed:

JS ES EJ ---- == ---- == ---- KP EP EK

We can construct a similar system as viewed from the right-hand side:

Because of this, the above proportionalities can be expanded to:

Sx Sy Sz ---- == ---- == ---- Px Py Pz

Since we know that Sz -- the Z coordinate of the screen -- is 1, we can re-arrange these proportionalities into the following equations:

Px Sx == ---- Pz

Py Sy == ---- Pz

Thus, the conversion from worldspace to the screen, which is referred to as viewspace, is a straightforward set of divisions. All that remains to be attended to are minor technicalities, such as the dimensions of the screen. It's not as simple as saying that one unit is one pixel, because by simplifying Sz as 1, that would require that the user be 1 pixel away from the screen, which is not the typical scenario. Assuming that we're dealing with SCREEN 13, which is 320x200x256 with roughly 1 mm per side on the pixels, the user is usually about 75 cm away from the monitor, which translates to 750 pixels. With larger screens, the user usually places themselves farther away, and with smaller monitors, the user usually sits closer to the monitor, so any inequities in pixel size with respect to monitor size tend to balance out proportionally and can be ignored. Now, if the user is 750 pixels away from the screen, the point on the screen S has a z value of 1, that means that the unit of z in worldspace is equivalent to 750 pixels. Since the system uses the same units for x and y as it does for z, the screen's boundaries in worldspace must be (-160 / 750, -100 / 750) to (160 / 750, 100 / 750). The x and y coordinates of S are thus scaled down by 750 from the corresponding screen coordinates, and they must be multiplied by 750 to get these values. The distance 75 cm is arbitrary, and it can be modified or calculated from angles to get different fields of vision, or FOVs (like in Quake). Once you have the coordinates, you can do whatever you want with them. Some sample code to demonstrate this follows. As usual, the code is optimized for readability, rather than speed.

CONST SphereRadius# = 1 CONST SphereCentreX# = 0 CONST SphereCentreY# = 0 CONST SphereCentreZ# = 8 TYPE pointType x AS DOUBLE y AS DOUBLE z AS DOUBLE oldScreenX AS INTEGER oldScreenY AS INTEGER END TYPE DECLARE SUB makeSphere (dot() AS pointType) DIM dot(1 TO NumDots%) AS pointType makeSphere dot() SCREEN 13 DO '0.003 radians == 17.2 degrees rotation# = rotation# + .003 IF rotation# > 6.283185 THEN rotation# = rotation# - 6.283185

FOR i% = 1 TO NumDots% thisX# = dot(i%).x thisY# = dot(i%).y thisZ# = dot(i%).z

'rotate point around Y axis at Sphere Center (X,Y,Z) rotatedX# = COS(rotation#) * (thisX# - SphereCentreX#) - SIN(rotation#) * (thisZ# - SphereCentreZ#) + SphereCentreX# rotatedY# = thisY# rotatedZ# = SIN(rotation#) * (thisX# - SphereCentreX#) + COS(rotation#) * (thisZ# - SphereCentreZ#) + SphereCentreZ#

'do a little bit of depth queuing (very simple =D) IF rotatedZ# > SphereCentreZ# THEN colour% = 8 'dark gray ELSEIF rotatedZ# > SphereCentreZ# - SphereRadius# / 2 THEN colour% = 7 'light gray ELSE colour% = 15 'white END IF

'draw pixel PSET (screenX%, screenY%), colour%

'erase old pixel in a fairly flicker-free way IF (dot(i%).oldScreenX <> screenX%) OR (dot(i%).oldScreenY <> screenY%) THEN PSET (dot(i%).oldScreenX, dot(i%).oldScreenY), 0 END IF

'store new pixel's position for next time dot(i%).oldScreenX = screenX% dot(i%).oldScreenY = screenY% NEXT i% LOOP UNTIL INKEY$ <> "" SCREEN 0: WIDTH 80, 25

SUB makeSphere (dot() AS pointType) FOR i% = 1 TO SphereStripes% heading# = 6.283185 * i% / SphereStripes% FOR j% = 1 TO SphereDotsPerStripe% pitch# = 3.141592 * (j% - 1) / (SphereDotsPerStripe% - 1) - 1.570796 thisDotX# = SphereRadius# * SIN(heading#) * COS(pitch#) thisDotY# = SphereRadius# * SIN(pitch#) thisDotZ# = SphereRadius# * COS(heading#) * COS(pitch#) thisDotIndex% = (i% - 1) * SphereDotsPerStripe% + j% dot(thisDotIndex%).x = thisDotX# + SphereCentreX# dot(thisDotIndex%).y = thisDotY# + SphereCentreY# dot(thisDotIndex%).z = thisDotZ# + SphereCentreZ# NEXT j% NEXT i% END SUB