Skip to main content

Matrix demo

This program illustrates how the 32-bit Floating Point Unit add-on facilitates mathematical operations. The demo uses matrix algebra to render a rotating 3D cube:

How it works

  • CLASS VECTOR is represented using the new FLOAT variable type.
  • The sin(θ)\sin(\theta) and cos(θ)\cos(\theta) functions are calculated from a lookup table, with angles measured as a BYTE where 255 corresponds to 2π2\pi.
  • The MULTIPLY_MATRIX() function uses homogeneous coordinates, dividing by W for a 3D perspective projection.
  • The 2D lines are plotted using Bresenham's line algorithm with INT instead of FLOAT, because screen space has discrete pixels.

2D raster

The image is painted onto a 48 × 48 pixel sprite, writing directly to an IO_SPRITE::PIXELS_ADDRESS memory buffer without relying on the Hybrix framework [ENGINE].

# SET UP THE RASTER BUFFER
VAR SPRITE: IO_SPRITE
IO::SPRITES[0] -> SPRITE
10 -> SPRITE.X
10 -> SPRITE.Y
48 -> SPRITE.WIDTH
48 -> SPRITE.HEIGHT

VAR BUFFER: BYTE[SIZE 2304]
NEW BYTE[SIZE 2304]() -> BUFFER
TO_ADDRESS(BUFFER) -> SPRITE.PIXELS_ADDRESS

A pixel is plotted by writing its color into the array:

COLOR -> BUFFER[X0 + Y0 * 48]

Clearing the buffer would require a LOOP that processes 2,304 pixels, which can be relatively slow. The KERNEL::MEMSET_BYTE() utility uses optimized assembly language, which is significantly faster:

# CLEAR THE RASTER
KERNEL::MEMSET_BYTE(TO_ADDRESS(BUFFER), 2, 2304)

⚠️ Be careful with KERNEL::MEMSET_BYTE()! If used incorrectly, it can corrupt the program's memory.

3D projection

The PROJECTION_MATRIX is a read-only matrix that looks like this:

PROJECTION_MATRIX=[40.00.024.072.00.040.024.072.00.00.01.22222221.44444440.00.01.03.0]\texttt{PROJECTION\_MATRIX} = \begin{bmatrix} 40.0 & 0.0 & -24.0 & 72.0 \\ 0.0 & -40.0 & -24.0 & 72.0 \\ 0.0 & 0.0 & -1.2222222 & 1.4444444 \\ 0.0 & 0.0 & -1.0 & 3.0 \end{bmatrix}

The cube's corners form a box ranging from (1.0,1.0,1.0)(-1.0, -1.0, -1.0) to (1.0,1.0,1.0)(1.0, 1.0, 1.0). PROJECTION_MATRIX implements a 3D perspective projection that maps the cube's space to be centered in the 48 × 48 surface.

If you comment out line 197 like this...

# MAIN::MULTIPLY_MATRIX(VECTOR, MAIN::PROJECTION_MATRIX)

...you can see the perspective projection by itself:

Rotation and scaling

The cube is animated by rotating and scaling it. Here is a textbook matrix for rotating a 3D vector about its Y axis by an angle of θ\theta:

[cos(θ)0sin(θ)00100sin(θ)0cos(θ)00001]\begin{bmatrix} \cos(\theta) & 0 & \sin(\theta) & 0 \\ 0 & 1 & 0 & 0 \\ -\sin(\theta) & 0 & \cos(\theta) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

Our demo also scales the (x,y,z)(x,y,z) vector by a SCALE. Since SIN oscillates from 1.0-1.0 to 1.01.0, the expression below will oscillate from 0.50.5 to 0.90.9 (which keeps the cube from going outside its window):

VAR SCALE: FLOAT
0.7 + SIN * 0.2 -> SCALE

The resulting matrix looks like this:

ROTATION_MATRIX=[cos(θ)scale0sin(θ)scale00scale00sin(θ)scale0cos(θ)scale00001]\texttt{ROTATION\_MATRIX} = \begin{bmatrix} \cos(\theta)\cdot scale & 0 & \sin(\theta)\cdot scale & 0 \\ 0 & scale & 0 & 0 \\ -\sin(\theta)\cdot scale & 0 & \cos(\theta)\cdot scale & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

These matrixes contain a lot of zero's, which are wasted multiplications. Our program might run faster with specialized versions of MULTIPLY_MATRIX() that avoid multiplying by zero. However, an even better optimization is to merge all the operations into one matrix (matrix × matrix multiplication), so that each vector only needs to be multiplied once. When there are lots of vectors, this is significantly faster. The demo was kept simple for readability.

Code listing

(Click to see the code)

⚠️ This program will not work without the 32-bit FPU add-on.

CLASS VECTOR
VAR X: FLOAT
VAR Y: FLOAT
VAR Z: FLOAT

FUNC COPY(OTHER: VECTOR)
OTHER.X -> .X
OTHER.Y -> .Y
OTHER.Z -> .Z
END FUNC
END CLASS

CLASS EDGE
# THESE ARE INDEXES INTO THE POINTS ARRAY
VAR A: INT
VAR B: INT
END CLASS

MODULE MAIN
VAR VERTEXES: VECTOR[]
VAR EDGES: EDGE[]

VAR PROJECTION_MATRIX: FLOAT[SIZE 16]

# LOOKUP TABLE FOR MAIN::SIN() AND MAIN::COS()
VAR SIN256_QUADRANT: FLOAT[SIZE 65]

# BRESENHAM'S LINE ALGORITHM
# WARNING: THERE IS NO CLIPPING! POINTS ASSUMED TO BE IN BOUNDS
FUNC PLOT_LINE(BUFFER: BYTE[SIZE 2304], X0, Y0, X1, Y1, COLOR: BYTE)
VAR DX, SX, DY, SY, ERROR, ERROR2
DX <- MATH::ABS(X1 - X0)
DY <- -MATH::ABS(Y1 - Y0)

SX <- 1
IF X0 >= X1
DO SX <- -SX

SY <- 1
IF Y0 >= Y1
DO SY <- -SY

ERROR <- DX + DY

LOOP
COLOR -> BUFFER[X0 + Y0 * 48]
ERROR2 <- MATH::SHIFT_LEFT(ERROR, 1)
IF ERROR2 >= DY THEN
IF X0 = X1
DO DROP

ERROR <- ERROR + DY
X0 <- X0 + SX
END IF

IF ERROR2 <= DX THEN
IF Y0 = Y1
DO DROP

ERROR <- ERROR + DX
Y0 <- Y0 + SY
END IF
END LOOP
END FUNC

FUNC MULTIPLY_MATRIX(VECTOR: VECTOR, M: FLOAT[SIZE 16])
VAR X_IN: FLOAT, Y_IN: FLOAT, Z_IN: FLOAT
X_IN <- VECTOR.X
Y_IN <- VECTOR.Y
Z_IN <- VECTOR.Z

VAR X: FLOAT, Y: FLOAT, Z: FLOAT
VAR W: FLOAT

# HOMOGENEOUS COORDINATE MATRIX * VECTOR MULTIPLICATION
X <- M[0] * X_IN + M[1] * Y_IN + M[2] * Z_IN + M[3]
Y <- M[4] * X_IN + M[5] * Y_IN + M[6] * Z_IN + M[7]
Z <- M[8] * X_IN + M[9] * Y_IN + M[10] * Z_IN + M[11]
W <- M[12] * X_IN + M[13] * Y_IN + M[14] * Z_IN + M[15]

VECTOR.X <- X / W
VECTOR.Y <- Y / W
VECTOR.Z <- Z / W
END FUNC

FUNC SIN(ANGLE: BYTE): FLOAT
VAR RESULT: FLOAT

IF ANGLE < 128 THEN
IF ANGLE < 64 THEN
RESULT <- MAIN::SIN256_QUADRANT[ANGLE]
ELSE
RESULT <- MAIN::SIN256_QUADRANT[64 - MATH::BIT_AND(ANGLE, 63)]
END IF
ELSE
IF ANGLE < 192 THEN
RESULT <- -MAIN::SIN256_QUADRANT[MATH::BIT_AND(ANGLE, 63)]
ELSE
RESULT <- -MAIN::SIN256_QUADRANT[64 - MATH::BIT_AND(ANGLE, 63)]
END IF
END IF
RETURN RESULT
END FUNC

FUNC COS(ANGLE: BYTE): FLOAT
VAR RESULT: FLOAT
RESULT <- MAIN::SIN(TO_BYTE(ANGLE + 64)) # PLUS 90 DEGREES
RETURN RESULT
END FUNC

FUNC WAIT_FOR_PAINT()
1 -> IO::IRQ_PENDING # "WRITE 1 TO CLEAR" IRQ_PENDING
3 -> IO::PAINT_MODE # REQUEST PAINT
1 -> IO::IRQ_WAKE_MASK # SLEEP UNTIL THE PAINT FINISHES
KERNEL::SLEEP()
END FUNC

FUNC START()
# ALLOCATE THE POINTS
VAR POINTS: VECTOR[]
NEW VECTOR[](MAIN::VERTEXES.SIZE) -> POINTS
POINTS.RESIZE(MAIN::VERTEXES.SIZE)

VAR I: INT
0 -> I
LOOP
NEW VECTOR() -> POINTS[I]
I + 1 -> I
IF I >= POINTS.SIZE
DO DROP
END LOOP

# SET UP THE RASTER BUFFER
VAR SPRITE: IO_SPRITE
IO::SPRITES[0] -> SPRITE
10 -> SPRITE.X
10 -> SPRITE.Y
48 -> SPRITE.WIDTH
48 -> SPRITE.HEIGHT

VAR BUFFER: BYTE[SIZE 2304]
NEW BYTE[SIZE 2304]() -> BUFFER
TO_ADDRESS(BUFFER) -> SPRITE.PIXELS_ADDRESS

VAR ROTATION_MATRIX: FLOAT[SIZE 16]
NEW FLOAT[SIZE 16]() -> ROTATION_MATRIX

# RENDERING LOOP
VAR ANGLE: BYTE
0 -> ANGLE
LOOP
# CLEAR THE RASTER
KERNEL::MEMSET_BYTE(TO_ADDRESS(BUFFER), 2, 2304)

# COMPUTE THE ROTATION MATRIX
TO_BYTE(ANGLE + 1) -> ANGLE
VAR COS: FLOAT, SIN: FLOAT
SIN <- MAIN::SIN(ANGLE)
COS <- MAIN::COS(ANGLE)

VAR SCALE: FLOAT
0.7 + SIN * 0.2 -> SCALE

ROTATION_MATRIX[0] <- COS * SCALE
ROTATION_MATRIX[1] <- 0.0
ROTATION_MATRIX[2] <- SIN * SCALE
ROTATION_MATRIX[3] <- 0.0

ROTATION_MATRIX[4] <- 0.0
ROTATION_MATRIX[5] <- 1.0 * SCALE
ROTATION_MATRIX[6] <- 0.0
ROTATION_MATRIX[7] <- 0.0

ROTATION_MATRIX[8] <- -SIN * SCALE
ROTATION_MATRIX[9] <- 0.0
ROTATION_MATRIX[10] <- COS * SCALE
ROTATION_MATRIX[11] <- 0.0

ROTATION_MATRIX[12] <- 0.0
ROTATION_MATRIX[13] <- 0.0
ROTATION_MATRIX[14] <- 0.0
ROTATION_MATRIX[15] <- 1.0

# TRANSFORM THE VERTEXES
0 -> I
LOOP
VAR VECTOR: VECTOR
POINTS[I] -> VECTOR

# COPY THE ROM COORDINATE INTO RAM
VECTOR.COPY(MAIN::VERTEXES[I])
# ROTATE IT IN 3D
MAIN::MULTIPLY_MATRIX(VECTOR, ROTATION_MATRIX)
# PROJECT ONTO SCREEN
MAIN::MULTIPLY_MATRIX(VECTOR, MAIN::PROJECTION_MATRIX)

I + 1 -> I
IF I >= POINTS.SIZE
DO DROP
END LOOP

# NOW DRAW THE LINES BETWEEN THE SCREEN POINTS
0 -> I
LOOP
VAR EDGE: EDGE
MAIN::EDGES[I] -> EDGE
VAR A: VECTOR, B: VECTOR
POINTS[EDGE.A] -> A
POINTS[EDGE.B] -> B

MAIN::PLOT_LINE(BUFFER,
| TO_INT(A.X), TO_INT(A.Y),
| TO_INT(B.X), TO_INT(B.Y),
| TO_BYTE(I+8))

I + 1 -> I
IF I >= MAIN::EDGES.SIZE
DO DROP
END LOOP

MAIN::WAIT_FOR_PAINT()
END LOOP
END FUNC
END MODULE

# THE VERTEXES OF THE CUBE
DATA MAIN::VERTEXES
[
# FRONT FACE (Z < 0)
{ X: -1.0, Y: -1.0, Z: -1.0 },
{ X: 1.0, Y: -1.0, Z: -1.0 },
{ X: 1.0, Y: 1.0, Z: -1.0 },
{ X: -1.0, Y: 1.0, Z: -1.0 },

# BACK FACE (Z > 0)
{ X: -1.0, Y: -1.0, Z: 1.0 },
{ X: 1.0, Y: -1.0, Z: 1.0 },
{ X: 1.0, Y: 1.0, Z: 1.0 },
{ X: -1.0, Y: 1.0, Z: 1.0 },
]
END DATA

# THE EDGES OF THE CUBE
DATA MAIN::EDGES
[
# FRONT FACE
{ A: 0, B: 1 },
{ A: 1, B: 2 },
{ A: 2, B: 3 },
{ A: 3, B: 0 },

# REAR FACE
{ A: 4, B: 5 },
{ A: 5, B: 6 },
{ A: 6, B: 7 },
{ A: 7, B: 4 },

# Z LINES
{ A: 0, B: 4 },
{ A: 1, B: 5 },
{ A: 2, B: 6 },
{ A: 3, B: 7 },
]
END DATA

# PERSPECTIVE-PROJECT THE -1.0 ... 1.0 CUBE SPACE ONTO A 48X48 SPRITE
DATA MAIN::PROJECTION_MATRIX
[
40.0, 0.0, -24.0, 72.0,
0.0, -40.0, -24.0, 72.0,
0.0, 0.0, -1.2222222, 1.4444444,
0.0, 0.0, -1.0, 3.0
]
END DATA

DATA MAIN::SIN256_QUADRANT
[
0.0000000, 0.0245412, 0.0490677, 0.0735646,
0.0980171, 0.1224110, 0.1467305, 0.1709619,
0.1950903, 0.2191012, 0.2429802, 0.2667128,
0.2902847, 0.3136817, 0.3368899, 0.3598950,
0.3826834, 0.4052413, 0.4275551, 0.4496113,
0.4713967, 0.4928982, 0.5141027, 0.5349976,
0.5555702, 0.5758082, 0.5956993, 0.6152316,
0.6343933, 0.6531728, 0.6715589, 0.6895405,
0.7071068, 0.7242471, 0.7409511, 0.7572088,
0.7730105, 0.7883464, 0.8032075, 0.8175848,
0.8314696, 0.8448536, 0.8577286, 0.8700869,
0.8819213, 0.8932243, 0.9039893, 0.9142098,
0.9238795, 0.9329928, 0.9415441, 0.9495282,
0.9569403, 0.9637761, 0.9700313, 0.9757021,
0.9807853, 0.9852776, 0.9891765, 0.9924795,
0.9951847, 0.9972905, 0.9987955, 0.9996988,
1.0000000
]
END DATA