The earlier answers here explored the calculations that the math box
performs. It's also helpful to understand what the games that use it are
trying to accomplish. Battlezone, the first game that included it, used
the math box for five things:
- 3D "view" transform
- 3D "model" transform and perspective projection
- radar blip position
- 2D distance calculation
- integer division for arctangent
An important formula to know for the first three is rotation about the Y
newX = X * cos(theta) - Z * sin(theta)
newZ = X * sin(theta) + Z * cos(theta)
3D "view" transform
For each object in the world, the game must compute the coordinates of the
center of the object relative to the viewer's current position and facing.
This requires translating the object's X and Z coordinates to put the
viewer at the origin, and then rotating about Y.
The equations for this are:
rel_X = obj_world_X - viewer_X
rel_Z = obj_world_Z - viewer_Z
view_X = rel_X * cos(theta) - rel_Z * sin(theta)
view_Z = rel_X * sin(theta) + rel_Z * cos(theta)
To do this with the math box, we start with:
R0 = cos(theta)
R1 = -sin(theta)
R2 = viewer_Z
R3 = viewer_X
Then, for each object:
R4 = obj_world_Z
R5 = obj_world_X
obj_view_Z = (function $0b)
obj_view_X = (function $12)
3D "model" transform and perspective projection
This one is a bit more complicated. We start with the view coordinates
calculated earlier, and must determine the screen X,Y position of every
vertex in every visible object.
With the object at the origin, we rotate each vertex's coordinates,
translate them to the object's position, and then divide the X and Y
coordinates by Z to get the perspective projection. (Battlezone does some
peculiar things with the math, but I don't want to get too deep here.)
rotated_Z = vertex_X * sin(theta) + vertex_Z * cos(theta)
model_Z = rotated_Z + obj_view_Z
rotated_X = vertex_X * cos(theta) - vertex_Z * sin(theta)
model_X = rotated_X + obj_view_X
screen_X = model_X / model_Z
model_Y = vertex_Y + obj_world_Y
screen_Y = model_Y / model_Z
To do this with the math box, we set registers to the object's position and facing in view space:
R0 = -cos(theta)
R1 = sin(theta)
R2 = obj_view_Z
R3 = obj_view_X
Then, for each vertex:
R4 = vertex_Z
R5 = vertex_X
screen_X = (function $11)
RB = vertex_Y + obj_world_Y
screen_Y = (function $14)
And that's it. The line clipping is performed by the vector graphics
hardware, so the drawing code can just feed (
the vector command list. (Actually that's not entirely it, as some
results are negated and the mesh is defined a certain way to make the math
come out right, but that's essentially it.)
Radar blip position
This needs to do a rotation and then translate the coordinates to where
the radar display sits on the screen. It turns out the model transform does
the necessary calculations if we pass in some zeroes, and does some extra
stuff we can ignore.
The game plugs numbers into the calculation for
screen_X with function
$11, which returns R8/R7. The game then ignores the result, and uses the
intermediate values left in R7 and R8 as the screen coordinates.
2D distance calculation
The game calculates the approximate distance between two objects to see
if they have collided. The formula used is an approximation of an
approximation, but it works well enough.
The code that uses this typically ignores the "busy" flag in favor of
just stalling for 6 cycles. Function $1d or $1e is used depending on
whether the code already has the coordinate deltas handy.
Integer division for arctangent
To determine the direction a tank should turn to shoot the player, the
game needs to compute an arctangent, which it does with integer division
and a lookup table. The math box provides a routine (function $14) that
divides a 32-bit value by a 16-bit value with a certain number of iterations.
Note: the iteration count in R6 is set to 10, not 16, for the benefit of
the screen-coordinate equations. Values are effectively divided by 64.
Battlezone just wants the 8-bit unsigned fractional value here, so it
right-shifts the 16-bit result twice more.
In general the math box API doesn't appear to be intended to perform
general-purpose vector math; it's intended to solve the equations that
Battlezone needed. If you dig into the details, though, it's not a
Consider the rotations performed for the view/model transforms. The
sine/cosine values are provided from a table as signed 16-bit fractions,
with values from 0 to 0x7fff. (Technically it's using 0.9999695 as an approximation for 1.0... close enough.) When calculating rotated coordinates,
the math box code multiplies the numbers and then right-shifts the result
mb_temp = ((int32_t) REG0) * ((int32_t) REG4);
REGc = mb_temp >> 16;
If the code were expecting to multiply two integers or two 8.8 fixed-point
values, it wouldn't be right-shifting 16x. It's clearly intended to
multiply something by a fractional value, such as the sine/cosine values
required for the rotation. However, after multiplying a
1.15 fraction it should right-shift 15x, which means our results are
all divided by two. Battlezone deals with this in two different ways.
For the view transform, the result is doubled by the 6502 code. For the
model transform, the inputs were fixed by doubling the X/Z coordinates
in the shape mesh data. (The Y coordinate isn't part of a rotation and
doesn't get shifted, so doesn't need to be doubled. Which is why, when
you render the meshes, they all look squashed.)
It's possible for the 6502 to continue to do work while the math box is
busy. The view transform code takes advantage of this, doing some work
on the result from function $0b while function $12 is running.
A full disassembly of Battlezone's 6502 code and vector graphics data is
available from 6502disassembly.com.
An article with the information from this posting, but in greater detail, is
I want to thank everyone who provided earlier answers to this question.
The information helped me make sense of what I was seeing while disassembling