All in a Spin: 3D Basic

Andrew Boura presents pointers, general ideas and routines to help you to write programs involving 3D graphics in BASIC

Introduction

This article doesn't claim to be a 3D graphics gospel, but the routines work and should allow you to create some quite impressive graphics with surprising ease. An understanding of the maths involved is not particularly necessary. The Simple 3D program listed at the end of this article will create a multi-coloured solid cube, spinning in perspective 3D, and a much more comprehensive 3D animation program with many extra facilities is also supplied on the magazine disc.

The underlying principle involved in the routines is the use of matrix multiplication. Such facilities are built into BBC BASIC, so the calculations for the rotation of objects can be kept very simple and fast. You may be surprised to know that in order to rotate an object by a certain pre-determined amount, only one line is required:

`   object_points() = object_points() . rotation_matrix()`

Defining a 3D object

An object comprises two parts: the points and the faces. The points are stored in a two-dimensional floating point array which has the following layout:

```   points(point, 0) = x
points(point, 1) = y
points(point, 2) = z
```

The faces are stored in a two-dimensional integer array containing point numbers laid out as follows:

```   faces%(face, 0) = first point
faces%(face, 1) = second point
faces%(face, 2) = third point
faces%(face, 3) = fourth point
faces%(face, 4) = colour
```

Once you have initialised your points in 3D space (see figure 1) you must initialise some faces.

Figure 1

In order to be able to determine whether a face is pointing towards or away from you, you need to define the faces consistently in either clockwise or anticlockwise order. The convention for working with 3D is anticlockwise, so we will use that. For an example of how to define a face, see figure 2. If a face is initialised in the wrong order, it will be visible from the side opposite to the one you want.

Figure 2

The reason for making the faces single-sided is obvious when you consider what would happen if all the faces on a cube were double-sided. Because the program isn't clever enough to work out which side is on top (it only knows which way they face), at certain points you would see sides which should be hidden. Also, you would be wasting a lot of time as you would in most cases effectively double the number of faces which need to be handled (to see this effect, simply turn off the hidden surface removal in the full program supplied on the disc). The net result is that, for the sake of speed and simplicity, it is far easier to make objects single-sided and, if a double-sided face is required, define it in both clockwise and anticlockwise directions.

Plotting the object

Once the points and faces have been initialised, how do you plot the object? In the simplest case, you would work your way through the faces and plot them ignoring their depth as shown below:

```   FOR face = 0 TO number_of_faces
GCOL faces%(face, 4)
MOVE     point(faces%(face,1),0), point(faces%(face,1),1)
MOVE     point(faces%(face,0),0), point(faces%(face,0),1)
PLOT 85, point(faces%(face,2),0), point(faces%(face,2),1)
PLOT 85, point(faces%(face,3),0), point(faces%(face,3),1)
NEXT
```

However, this won't deal with perspective or hidden surfaces.

Perspective

In order to calculate perspective you need to use a scaling factor. This is done by the procedure PROCpoint(point, x, y). All you need to do is to feed in the raw values for the x and y coordinates; the modifications due to perspective will be applied and the values to be used when plotting or removing hidden surfaces will be returned.

The basis of perspective is that the further away an object is, the smaller it appears. Many of you will know that in art and drawing, a vanishing point is used; exactly the same idea is used in 3D graphics. Try to picture a three-dimensional cube. In order to plot this onto the two-dimensional screen we need to get rid of the third dimension (z into the screen) by taking the depth into account and modifying the x and y values appropriately (see figure 3).

Figure 3

Take a look at DEF PROCpoint in the main listing. In the first line, S represents the scale, where:

• scale = constant/(z+zoom)

In order to make a point appear closer, we can simply alter the value of z (the depth). By adding zoom to the value of z, we effectively alter how far away it is. The scale is then inversely proportional to the depth, so as the depth gets bigger, the scale gets smaller.

Hidden surface removal

This is more complex. First of all you need the x and y coordinates of the first three points defining the surface (meaning that triangles could be used as well as quadrilaterals in your own 3D graphics code). You carry out the following calculation (where p is short for point):

• (p1_x - p2_x) * (p3_y - p2_y) - (p1_y - p2_y) * (p3_x - p2_x)

The best way to understand how this works is to think of a clock face.

Figure 4

When you look at it from the front, the numbers go from 1 to 12 in a clockwise direction. Now imagine turning the clock around so that it is facing away from you. If you could see the numbers, you would find they were now anti-clockwise (see figure 4). If all you are given is the order of the numbers on a clock face, you can still tell whether it is facing towards or away from you. If the calculation above evaluates to a negative number, then the quadrilateral is facing towards you. For the mathematically inclined, the above function calculates the scalar product of the first two sides of the quadrilateral. The sign of the result depends upon whether the vectors are in clockwise or anticlockwise order as viewed from the screen.

Rotation

We now have an object defined and we know how to display it, but how do we rotate it? The procedure PROCchangeRotation(x, y, z) sets up a rotation matrix; when the points array is multiplied by this matrix (using the formula at the start of the article), the object will rotate by the angle specified (in radians) about each axis. If you aren't comfortable using radians, you can use the RAD operator to convert from degrees.

Matrix multiplication explained

For those who want a cure for insomnia, or are mathematically inclined, the explanation of matrix multiplication is as follows. Two matrices, A and B, can be multiplied together to form their product ab when the number of columns of A is equal to the number of rows of B.

The matrix multiplication operator in BBC Basic is the full stop, so to multiply the two matrices a() and b() you would use the command a().b(). There are some complications, however, because a().b() is not, as you might expect, equal to b().a(), so it is essential that the correct order is used.

The easiest way to describe the process is by example. If A and B are (3 x 2) and (2 x 2) matrices, respectively, given by:

```   A = (a11, a12)    B = (b11, b12)
(a21, a22)        (b21, b22)
(a31, a32)
```

then the product C is a (3 x 2) matrix defined as:

```   C = (a11 x b11 + a12 x b21, a11 x b12 + a12 x b22)
(a21 x b11 + a22 x b21, a21 x b12 + a22 x b22)
(a31 x b11 + a32 x b21, a31 x b12 + a32 x b22)
```

Matrices can be used to manipulate arrays or lists of points in either two- or three-dimensional space. Simple 3D actually works by combining three matrices, one for each axis of rotation, into one matrix called rotate().

Final word

To get a better idea of how all these ideas work together, it would be well worth your while to experiment with the Simple3D BASIC program that's included in the Software.Basic3D directory on this issue, as it uses all the processes described above (in order to achieve smooth animation, Simple3D uses screen banking to switch instantly between two pictures). Also included is a more complex desktop application that can display different 3D models along with a number of sample models using the same techniques discussed in this article.

Once you have understood how it works, you will be ready to make your first leaps/staggers/trips into the world of 3D graphics; good luck!

Andrew Boura