The CMM2 3D Engine
The CMM2 3D Engine
The CMM2 3D Engine
Before looking at the commands in detail I will try and explain the concept and the limitations of the
engine.
The 3D world is an area of space 65532 x 65532 x 32766 units (x, y, z) centred at 0,0,0
In other words an object can be placed from -32766 to 32766 in the x-axis, -32766 to 32766 in the y-
axis and 0 to 32766 in the z-axis
You can define up to 128 3D objects numbered 1 to 128.
Each object is described in terms of how many vertices it has, how many faces it has, which vertices
make up each face and the colours of the edges and infill of each face. Objects can have any number
of vertices and faces limited only by system memory.
The vertices are specified as x,y,z coordinates referenced to the object centre at 0,0,0
In addition for each object you will define the "camera" that is used to view the object. The 3D
engine supports up to 6 camera positions numbered 1 to 6
All cameras look along their Z axis and before you display a 3D object the associated camera must be
initialised by defining the x,y position of the camera and its viewplane. In camera terms the
viewplane can be thought of as the focal length of the camera lens. So the bigger the value of the
viewplane the more the camera "magnifies" the image.
For example, if we position a 3D object 1000 units away from the camera and the viewplane is at
200 then the projected image of the object onto the viewplane will be 20% of its original size. If we
"zoom" the viewplane to a "focal length" of 800 the projected image will now be 80% of its original
size.
When a 3D object is created the data used to create it is stored in CMM2 memory and any MMBasic
arrays used to create the object can be "erased" if required.
All objects are stored in their initial orientation as defined by their initialising data but they can be
rotated in three dimensions using the DRAW3D ROTATE command. This command acts upon the
initial orientation and stores a rotated copy transparently in the object data internally in the
firmware. Rotation takes place around the objects own centre. If you wish to rotate around any
position in the 3D world this should be done as first a rotation of the object and then a move of the
object. It is important to understand that every rotation requested for an object starts from the
initial orientation and is not cumulative. (However, this can be overridden - see the DRAW3D RESET
command)
Rotation is specified using a quaternion but don't worry I've included a very simple MATH command
to convert yaw, pitch and roll angles into the required quaternion (MATH Q_EULER)
Rotation has no effect on a displayed object but merely updates the internal memory definition of
the object.
There are two commands used to place an object in the 3D world - DRAW3D WRITE and DRAW3D
SHOW. The only difference is that, assuming the object was already displayed, the SHOW command
will clear a rectangle on the current write page sufficient to remove the existing object image before
displaying it whereas DRAW3D WRITE just overwrites whatever in on the write page with the 2D
representation of the object.
It is entirely up to the MMBasic programmer to deal with things like overlap of objects in the 3D
world and on the screen but to aid this objects that have been SHOWn can be removed and the
rectangular area of the screen in which they were drawn cleared using the DRAW3D HIDE command.
All objects and camera positions are deleted on any mode change and every time a program is run.
Hopefully the above gives you a basic understanding of how the 3D engine works and its limitations.
The way the camera works may seem to create a specific limitation in terms of multiple views of an
object but we will see in a subsequent post how this can be overcome.
DRAW3D commands
DRAW3D CREATE
DRAW3D CREATE n, nv, nf, camera, vertices(), fc(), faces(), colours() , edge() [,fill()]
DRAW3D CREATE is the command that creates a 3D object and all the information needed for the
object is included in the parameter list. We will use a cube as an example.
n is the object number (1-128)
vertices(): This is a 3 by nv array that holds the x,y,z coordinates of the 3D object. For example the
vertex definition for our cube with side length 2 with Option Base 0 centred on 0,0,0 could be:
DIM FLOAT vertices(2,7) = (-1,1,-1, 1,1,-1, 1,-1,-1, -1,-1,-1, -1,1,1, 1,1,1, 1,-1,1, -1,-1,1)
Note that the negative values represent the vertices closest to the camera.
facecount(): is a count of how many vertices are needed to define each face, so in our example for
the cube which has 6 faces it would be:
DIM INTEGER facecount(5)=(4,4,4,4,4,4)
faces(): This is a very important array and defines the vertices that make up each face of the 3D
object. There is one critical thing in setting up this array. The vertices must be listed in a clockwise
order for each face as though you were looking at that face from in front of it. It doesn't matter
which order the faces are listed as long as they match the correct vertex count in fc() and it doesn't
matter which vertex you start on for each face. In our example this array could be:
DIM INTEGER faces(23)=(0,1,2,3, 1,5,6,2, 0,4,5,1, 5,4,7,6, 2,6,7,3, 0,3,7,4)
colours(): This is an array that holds a simple list of all the colours you want to use to colour the 3D
object. So if we want a different colour for each face and another one for all the edges we could set
this array as follow:
DIM INTEGER colours(6)=(rgb(blue), rgb(green), rgb(yellow), rgb(cyan), rgb(red), rgb(magenta),
rgb(yellow))
edge(): This arrays specifies which of our colours to use for each edge of the 3D object. We will set
them all to the array index in colours() holding the value yellow
DIM INTEGER edge(5)=(6,6,6,6,6,6)
fill(): This array specifies which colour to use for each face of the 3D object. We will set them each to
a different colour by specifying the array index into colours()
DIM INTEGER fill(5)=(0,1,2,3,4,5)
If the optional parameter fill() is omitted then the 3D object will be drawn as a wireframe
Those familiar with 3D graphics will notice that the parameters to DRAW3D CREATE match the way
3D objects are defined in many 3D description files like wrl or ply files.
DRAW3D ROTATE
The n values are the 3D object IDs assigned when the object(s) was(were) created.
From the perspective of the MMBasic user a quaternion is simply a 5 element floating point array
and it is loaded using one of two methods
MATH Q_CREATE defines a rotation around the vector x,y,z by theta degrees (defaults to radians but
supports OPTION ANGLE). If x is zero and y is zero then the rotation is around the z-axis which is
equivalent to rolling the object. If only x is non-zero then the rotation will pitch the object and y non-
zero will yaw the object.
MATH Q_EULER sets q() to perform a rotation as defined by the yaw, pitch and roll angles
With the camera facing the object yaw is looking from the top of the object and rotates clockwise,
pitch rotates the top away from the camera and roll rotates around the z-axis clockwise.
The yaw, pitch and roll angles default to radians but respect the setting of OPTION ANGLE
All objects specified in the ROTATE command are rotated by the same amount. Nothing happens on
the screen but internally the firmware stores the rotated coordinates as well as the original ones.
It is very important to note that the rotate command acts on the original object as defined in the
CREATE command. Rotate commands are not cumulative. This ensures that rounding errors cannot
affect the accuracy.
DRAW RESET
The camera number “n” and the “viewplane” z distance are mandatory, all other parameters are
optional and all default to 0
The camera can be placed anywhere in the plane x, y, 0 but always looks out along the z axis.
The viewplane is perpendicular to the Z axis and is a plane sized 65532 x 65532 in the x and y axis
stretching, like the world from -32766 to 32766 in the x-axis and -32766 to 32766 in the y-axis
However, our VGA display can only show a very small part of the viewplane as limited by the screen
dimensions (MM.HRES x MM.VRES). We could call this the "viewport".
By default the viewport will be set to +/- MM.HRES\2 either side of the camera x position and +/-
MM.VRES either side of the camera y position.
This means If I place a 3D object at 0,0,Z in the 3D world and set my camera at 0,0,0 in the 3D world
then the object will project into the centre of the screen.
Likewise, if I place a 3D object at 400,400,Z in the 3D world and set my camera at 400,400,0 in the
3D world then the object will also project into the centre of the screen.
However, there are occasions when this may not be what we want so there are two extra
parameters to the CAMERA command - PAN-X and PAN-Y.
These move the position of the viewport on the viewplane relative to the camera position.
A practical example makes this clearer:
Suppose we position a number of objects in the 3D world with their lower extermities at x, 0, z. In
other words they are all sitting on the ground.
To look at them we may want the camera somewhere above the ground so we are looking down on
them.
If the viewport is centred on the camera (the default) then all the objects will appear in the bottom
half of the screen.
Now this may be exactly what we want but the firmware allows you to pan the viewport up and
down and/or left and right relative to the camera.
So in our example we could pan the viewport down to better frame the image on the screen.
This does not change the perspective of the image, that is locked in by the relative positions of the
object and the camera.
It merely allows us to frame the image better given our limited screen resolution
DRAW3D SHOW
DRAW3D WRITE
DRAW3D HIDE
DRAW3D CLOSE
DRAW3D DIAGNOSE
So we have 8 vertices and 6 faces. We will use camera number 1 to display the cube and we will
create object number 1
DIM n=1, nv=8, nf=6, camera=1
But first it is worth revisiting one aspect of object creation. Our example cube has a side length of 2
so even at 100% it would only cover a maximum of 4 pixels on the screen - not very useful.
Luckily there is a simple command we can use to scale the vertices before creating the object.
We can make the cube the size we want by simply multiplying all the elements in the vertex array by
the amount we want. So to make the length of each side 200 we can use:
DRAW3D CREATE n, nv, nf, camera, vertices(), facecount(), faces(), colours(), edge(), fill()
We need to set up the camera specified for the object in the create command before we can display
it. To do this we use the command
We are not specifying any optional parameters for the CAMERA command so this says position the
camera at 3D coordinates 0, 0, 0 with a "viewplane" 500 units in front of the camera along the z axis
and orthogonal to it. In our world the camera is always at a z position of zero and objects will always
be positioned with a positive value of z.
As defined above there are two commands for displaying (rendering) a 3D object: DRAW3D WRITE
and DRAW3D SHOW. The only difference is that, assuming the object was already displayed, the
SHOW command will clear a rectangle on the current write page sufficient to remove the existing
object image before displaying it whereas DRAW3D WRITE just overwrites whatever in on the write
page with the 2D representation of the object.
The syntax for both commands is the same so will concentrate on DRAW3D SHOW
It couldn't get any easier. This says that we want to position the centre of the object at coordinates
x, y, z in our virtual 3D world. The camera specified for our object is at a position 0, 0, 0. This
command projects the object 'n' onto the imaginary screen at "viewpoint" from the camera.
We will output to page 1 so we can get a nice tear free display.
Unlike sprites, 3D objects do not store the background image when the object is written or restore it
when the object moves. It is recommended that 3D objects are written onto a blank page and are
blitted or page copied (with transparency) onto the background image. Alternatively, putting 3D
objects onto page 1 in 12-bit mode with the background on page 0 will work very well.
The mechanism of perspective is quite complex but the second example program, attached below,
shows how the movement of the camera (represented by the cursor) changes the image of the
cuboid with the one nearest the camera bigger than the one further away, the bottom of the
octahedron bigger than the top and the side of the octahedron nearest the camera bigger than the
side further away.
One last point. Neither the x, y positions of 3D objects or the camera positions are constrained by
the screen size. In fact they can go from -32766 to + 32766 (0-32766 for Z for the object - the camera
is always at z=0). There are lots of combinations where a 3D object will render off the physical
display page. This is perfectly acceptable and allow valid objects to exist in the "world" without the
constraints of screen space.
Option explicit
option default none
mode 2,16
dim float phi=(1+sqr(5))/2
' data for location of verticies for verticesahedron of edge length 2
data 0,1,3*phi
data 0,1,-3*phi
data 0,-1,3*phi
data 0,-1,-3*phi
data 1,3*phi,0
data 1,-3*phi,0
data -1,3*phi,0
data -1,-3*phi,0
data 3*phi,0,1
data 3*phi,0,-1
data -3*phi,0,1
data -3*phi,0,-1
data 2,(1+2*phi),phi
data 2,(1+2*phi),-phi
data 2,-(1+2*phi),phi
data 2,-(1+2*phi),-phi
data -2,(1+2*phi),phi
data -2,(1+2*phi),-phi
data -2,-(1+2*phi),phi
data -2,-(1+2*phi),-phi
data (1+2*phi),phi,2
data (1+2*phi),phi,-2
data (1+2*phi),-phi,2
data (1+2*phi),-phi,-2
data -(1+2*phi),phi,2
data -(1+2*phi),phi,-2
data -(1+2*phi),-phi,2
data -(1+2*phi),-phi,-2
data phi,2,(1+2*phi)
data phi,2,-(1+2*phi)
data phi,-2,(1+2*phi)
data phi,-2,-(1+2*phi)
data -phi,2,(1+2*phi)
data -phi,2,-(1+2*phi)
data -phi,-2,(1+2*phi)
data -phi,-2,-(1+2*phi)
data 1,(2+phi),2*phi
data 1,(2+phi),-2*phi
data 1,-(2+phi),2*phi
data 1,-(2+phi),-2*phi
data -1,(2+phi),2*phi
data -1,(2+phi),-2*phi
data -1,-(2+phi),2*phi
data -1,-(2+phi),-2*phi
data (2+phi),2*phi,1
data (2+phi),2*phi,-1
data (2+phi),-2*phi,1
data (2+phi),-2*phi,-1
data -(2+phi),2*phi,1
data -(2+phi),2*phi,-1
data -(2+phi),-2*phi,1
data -(2+phi),-2*phi,-1
data 2*phi,1,(2+phi)
data 2*phi,1,-(2+phi)
data 2*phi,-1,(2+phi)
data 2*phi,-1,-(2+phi)
data -2*phi,1,(2+phi)
data -2*phi,1,-(2+phi)
data -2*phi,-1,(2+phi)
data -2*phi,-1,-(2+phi)
' 12 faces with 5 sides
data 0,28,36,40,32
data 33,41,37,29,1
data 34,42,38,30,2
data 3,31,39,43,35
data 4,12,44,45,13
data 15,47,46,14,5
data 17,49,48,16,6
data 7,18,50,51,19
data 8,20,52,54,22
data 23,55,53,21,9
data 26,58,56,24,10
data 25,57,59,27,11
' 20 faces with 6 sides
data 32,56,58,34,2,0
data 0,2,30,54,52,28
data 29,53,55,31,3,1
data 1,3,35,59,57,33
data 13,37,41,17,6,4
data 4,6,16,40,36,12
data 5,7,19,43,39,15
'data 14,38,42,18,7,5
data 22,46,47,23,9,8
data 8,9,21,45,44,20
data 10,11,27,51,50,26
data 24,48,49,25,11,10
data 36,28,52,20,44,12
data 13,45,21,53,29,37
data 14,46,22,54,30,38
data 39,31,55,23,47,15
data 16,48,24,56,32,40
data 41,33,57,25,49,17
data 42,34,58,26,50,18
data 19,51,27,59,35,43
'
dim float q1(4)
dim float yaw=rad(1),pitch=rad(2),roll=rad(0.5)
dim integer i, j, nf=31, nv=60, camera=1
dim float vertices(2,59)
' read in the coordinates of the verticies and scale
for j=0 to 59
for i=0 to 2
read vertices(i,j)
vertices(i,j)=vertices(i,j)*50
next i
next j
'math scale vertices(),50,vertices()
'
dim integer faces(173)
for i=0 to 173
read faces(i)
next i
dim integer fc(30)= (5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6)
dim integer colours(2)=(rgb(red),rgb(white),rgb(black))
dim integer edge(30)=(2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2)
dim integer fill(30)=(0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)
math q_create rad(2),1,0.5,0.25,q1()
draw3d create 1, nv,nf, camera, vertices(), fc(), faces(), colours(), edge(), fill()
draw3d camera 1,800,0,0
'draw3d diagnose 1,mm.hres\2,mm.vres\2,1000
page write 1
draw3d show 1,0,0,1000,1
do
math q_euler yaw,pitch,roll,q1()
inc yaw,rad(1)
inc pitch,rad(2)
inc roll,rad(0.5)
draw3d rotate q1(),1
draw3d show 1,0,0,1000,1
page copy 1 to 0,b
loop