Vectrex: 3d

Bildschirmfoto 2015-12-26 um 12.57.02

The “dream” of all vectrex players and programmers was allways to be able to do a vector game, where all objects are displayed with correct 3d vectors.

Common believe is, that in realtime this is not possible for the pure old brute. And since as of now not one game actually exists it is probably true – or VERY difficult (demos do exist!).

Some time ago I did a set of routines, that look like 3d and are reasonably fast in displaying I used some “tricks” to achieve the fast results.

I never came around doing a full game with these routines, but I am fairly certain, that one could!
Here another image of “demo” I did for the Vectrex “C” package.

Bildschirmfoto 2015-12-26 um 13.07.48

You can grab the whole package: 3D.zip

Here the original readme(s):

3D for assembler.

Only

####################################################
 AS09 Assembler for M6809/H6309 [1.19].
 Copyright 1994-97, Frank A. Vorstenbosch
####################################################

supported, since I make heavy use of the macro
functionality that assembler supplies.

I strongly recomend using that Assembler for vectrex
development anyway, since it IS good.


This Assembler port was done in about 2h.

I don't promise it will work allright.
I only tested it with the CUBEM.

Strangely enough the assembler port needs another vector
to keep the cube in center than the C version.

This MIGHT just be a bug somewhere, but I don't know
if it is in assembly or in C.


If there are errors in the assembler port they will
most probably be in the ###.i files.
The macro definitions for the uniform vectors.

If some vector does not come out as expected, take a look
at these definitions and compare them with their
C counterpart.

They nearly look the same, so if there is a bug you will
probably discover it by a closer look.

It was really a boring job to convert those 14 files, so
there might just be that I made a few mistakes by accidently
falling asleep while doing it :-)

For comments on how to use it, read the "3ddemo.txt".

A small assembler demo is provided, see there how to use it
from assembler.

There are 3 main files for the assembler version.

The file "3D_VAR.I" declares all needed variables, and should
go to the BSS section (including).

The file "3D_MAKRO.I" includes all macros that are there for 3d
stuff. Should be at the beginning of your code section.
But may probably be placed anywhere it fits.

The file "3D_PRG.I" has the two custom vector drawing functions.
Include it somewhere in your code section.
It also holds the cosinus and sinus tables, needed for angle computations.

Thats it for now!

Malban 

And the readme for the Vectrex C-Compiler edition:

VCETREX C - COMPILER

The 3d stuff can only be compiled with optimiziation flag set to
-O0.

It seems all compiler bugs that are still in the Vectrex C compiler
fall back to one main problem.

GCC cannot cope nicely with the m6809 construct of overlapping
registers. (A, B = D)

The original m6809 core I based this gcc version on circumvented that
problem by ALLWAYS using register D, and never bothering about
register A and B (apart from optimizing for 8bit values D -> B).

Most programs compile OK, but some (like the 3d...) do not
compile OK, than we must take measures (like -O0).

This seems to help most of the time. (MOST! Sometimes it doesn't.)

If you use Vectrex C for prototyping be prepared to look at the
*.s code it generates when looking for errors. :-(
It is still buggy :-(

But once you get used to that... it is great for many tedious things :-)


HOW TO USE THE 3D ROUTINES

a. Vectrex C
Well, the original 3d routines are pretty much (C) optimized now.
Actually you can hardly recognize them as C anymore, since most of
it is assembler.

If you use C to use the routines you don't have much to do.
Take following files and put them into your working C directory:

3D H
STA SED

000 H
001 H
010 H
011 H
0N1 H
100 H
101 H
110 H
111 H
11N H
1N1 H
N01 H
N10 H
N11 H

You than have to include the "3d.h" into your source, which in
turn includes all the other *.h files.

The "sta.sed" file is another C-Compiler fix. Look at the demo7
Makefile on how to use it. You have to 'sed' the "*.S" source
produced by gcc. Using sed it a quick-hack-bugfix.
What it does:

It turns lines like "sta #some_place" to "sta some_place".

I couldn't figure out how to tell the inline assembly to remove that
darn '#' without external help :-(

There are two things left for the programmer to do.

'draw', or 'invent' a 3d Vector list. (-> see: 3D VECTOR LIST)

Figure out what kind of vectors are used by the vector list.
Call an initialize function special for that list and draw it on the
screen.
(-> see: 3D INITIALIZATION)
(-> see: GETTING THE 3D VECTOR ON THE SCREEN)


b. Assembler
Not implemented yet :-(
Shouldn't be to hard to convert the 3d 'C' to assembler, since it
is really assembler.
Once that is done (using Macros till you die...) the
rest should work exactly like in 'C'.
(implementation is left to the reader as an exercize!)


3D VECTOR LIST
Here is the structure ('C') used for a 3D Vector list.

struct Line3d
{
 signed char pattern;
 signed char strength;
 signed char *moveTo;
};

 pattern: 255 -> draw full
 2-254 -> draw pattern
 0 -> move only
 1 -> stop

 strength: scale (scale only supported in 'draw_3ds')

 moveTo: a pointer to a pre generated 3d Vector
 (sounds complicated but isn't)

The first two entries are well known from ordinary vector lists,
I won't describe them here.
The last one 'moveTo' needs some explaining. Due to the nature
of the 3d Routines, you can use only a couple of different Vectors
for 3d drawing (13 different ones, sort of).
(-> see: WHAT IS POSSIBLE, WHAT ISNT - I AM CHEATING!)

Following is a list of available vectors (x,y,z).

 0. _0_0_0
 1. _1_0_0 _N_0_0
 2. _1_1_0 _N_N_0
 3. _1_0_1 _N_0_N
 4. _1_1_1 _N_N_N
 5. _0_1_0 _0_N_0
 6. _0_1_1 _0_N_N
 7. _0_0_1 _0_0_N
 8. _N_1_0 _1_N_0
 9. _N_0_1 _1_0_N
10. _0_N_1 _0_1_N
11. _N_1_1 _1_N_N
12. _1_N_1 _N_1_N
13. _1_1_N _N_N_1

(The first shouldn't be neccessary, the second row is the inverted first row)

These vectors are 'moveTo' vectors. From the current point you move to
the direction they specify.

e.g.
 _1_0_0 moves from the current location 1 step in x-direction,
 but doesn't change otherwise
 _1_1_0 moves from the current location 1 step in x-direction,
 and 1 step in y-direction (no z-change)
 this than sort of draws a diagonal line (2d)
and so on.

0 stands for coordinate does not change
1 stands for appropriate coordinate does change positive 1
N stands for appropriate coordinate does change negative 1

You can easily see, that not all directions are available :-(
(-> see: WHAT IS POSSIBLE, WHAT ISNT - I AM CHEATING!)

Only in angle steps of 45 degrees. But I hope that will be
sufficient for the usual vector game.

Here an example for a cube:

struct Line3d cube[] =
{
 {255, CUBE_STRENGTH, _1_0_0}, // first draw a square
 {255, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_N_0},

 {255, CUBE_STRENGTH, _0_0_1}, // go deeper (z)

 {255, CUBE_STRENGTH, _1_0_0}, // draw a second square behing the first
 {255, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_N_0},

 { 0, CUBE_STRENGTH, _1_0_0}, // and now draw the three other 'sides'
 {255, CUBE_STRENGTH, _0_0_N},
 { 0, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _0_0_1},
 { 0, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_0_N},

 { 1, 0, _0_0_N} // other than first parameter don't matter here
};

Note:
Using the above list generates a cube that is rotated by one
of its corners.
To use the cube's center as rotation point you have to
offset the first point accordingly, like:

struct Line3d cubeM[] =
{
 {0, CUBE_STRENGTH/2, _N_1_N},
 {255, CUBE_STRENGTH, _1_0_0},
 {255, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_N_0},

 {255, CUBE_STRENGTH, _0_0_1},

 {255, CUBE_STRENGTH, _1_0_0},
 {255, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_N_0},

 { 0, CUBE_STRENGTH, _1_0_0},
 {255, CUBE_STRENGTH, _0_0_N},

 { 0, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _0_0_1},
 { 0, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_0_N},

 { 1, 0, _0_0_N} // other than first parameter don't matter here
};


3D INITIALIZATION
In order to have some optimized bahaviour of the 3d calculating stuff,
it would be better to only calculated the vectors that:

a. are used
b. haven't been calculated already

If you look at the vector list for the cube you will notice that
there are only 3 different vectors used (and of course the mirrors,
but as you will see they don't count).

The initializing routine takes care of generating the rotated vectors.
It generates vectors YOU specify. And it generates them at the
RAM location the 'moveTo' vectors point to.
Using that sort of indirection in the vector list makes is possible
to calculate the 'moveTo' vectors only once per initialization.
They can even be used by many different 3d figures at the same time
(provided they have the same rotation angles).

All that is left to do is figure out what vectors are used by your
vector list.

Lets go thru that step by step, using the above cube:

These are all vectors:
 {255, CUBE_STRENGTH, _1_0_0},
 {255, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_N_0},

 {255, CUBE_STRENGTH, _0_0_1},

 {255, CUBE_STRENGTH, _1_0_0},
 {255, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_N_0},

 { 0, CUBE_STRENGTH, _1_0_0},
 {255, CUBE_STRENGTH, _0_0_N},
 { 0, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _0_0_1},
 { 0, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_0_N},

Now eliminate all doubles:
 {255, CUBE_STRENGTH, _1_0_0},
 {255, CUBE_STRENGTH, _0_1_0},
 {255, CUBE_STRENGTH, _N_0_0},
 {255, CUBE_STRENGTH, _0_N_0},

 {255, CUBE_STRENGTH, _0_0_1},

 {255, CUBE_STRENGTH, _0_0_N},

Now eliminate all invers ones, and mark them as invers 'A'lso:
 {255, CUBE_STRENGTH, _1_0_0}, 'A'
 {255, CUBE_STRENGTH, _0_1_0}, 'A'
 {255, CUBE_STRENGTH, _0_0_1}, 'A'

Wow!
Only three different once left!
These now go as makros into our initialize routine:

init3dCube()
{
 cosx = cosinus3d[angle_x];
 sinx = sinus3d[angle_x];
 cosy = cosinus3d[angle_y];
 siny = sinus3d[angle_y];
 cosz = cosinus3d[angle_z];
 sinz = sinus3d[angle_z];

 // set to init for cube!
 INIT_1_0_0_A;
 INIT_0_1_0_A;
 INIT_0_0_1_A;
}

Done!

 cosx, sinx
 cosy, siny
 cosz, sinz

MUST be defined befor calling the init makros.
If you use the same 'style' as I, you will have your angle information
in angle_x, angle_y, angle_z. Than the first six lines in the
initialization routine should be like above.

Note:
Following are the available pregenerated makros for use in the
initialization:

only positive only negative both
INIT_0_0_0 INIT_0_0_0_I INIT_0_0_0_A
INIT_1_0_0 INIT_1_0_0_I INIT_1_0_0_A
INIT_1_1_0 INIT_1_1_0_I INIT_1_1_0_A
INIT_1_0_1 INIT_1_0_1_I INIT_1_0_1_A
INIT_1_1_1 INIT_1_1_1_I INIT_1_1_1_A
INIT_0_1_0 INIT_0_1_0_I INIT_0_1_0_A
INIT_0_1_1 INIT_0_1_1_I INIT_0_1_1_A
INIT_0_0_1 INIT_0_0_1_I INIT_0_0_1_A
INIT_N_1_0 INIT_N_1_0_I INIT_N_1_0_A
INIT_N_0_1 INIT_N_0_1_I INIT_N_0_1_A
INIT_0_N_1 INIT_0_N_1_I INIT_0_N_1_A
INIT_N_1_1 INIT_N_1_1_I INIT_N_1_1_A
INIT_1_N_1 INIT_1_N_1_I INIT_1_N_1_A
INIT_1_1_N INIT_1_1_N_I INIT_1_1_N_A

// following are the (redundant) inverse ones
INIT_N_0_0 INIT_N_0_0_I INIT_N_0_0_A
INIT_N_N_0 INIT_N_N_0_I INIT_N_N_0_A
INIT_N_0_N INIT_N_0_N_I INIT_N_0_N_A
INIT_N_N_N INIT_N_N_N_I INIT_N_N_N_A
INIT_0_N_0 INIT_0_N_0_I INIT_0_N_0_A
INIT_0_N_N INIT_0_N_N_I INIT_0_N_N_A
INIT_0_0_N INIT_0_0_N_I INIT_0_0_N_A
INIT_1_N_0 INIT_1_N_0_I INIT_1_N_0_A
INIT_1_0_N INIT_1_0_N_I INIT_1_0_N_A
INIT_0_1_N INIT_0_1_N_I INIT_0_1_N_A
INIT_1_N_N INIT_1_N_N_I INIT_1_N_N_A
INIT_N_1_N INIT_N_1_N_I INIT_N_1_N_A
INIT_N_N_1 INIT_N_N_1_I INIT_N_N_1_A


GETTING THE 3D VECTOR ON THE SCREEN
There are two functions for that. After initialization you can call them.
They will draw to where the 'cursor' currently is.

a. without using strength information
void draw_3d(struct Line3d *figure)

a. with using strength information
void draw_3ds(struct Line3d *figure)

A 3D Vector output routine will than probably look like:

 ...
 init3dCube();
 zero_beam();
 move_to(40, 0);
 draw_3ds(cube);
 ...

WHAT IS POSSIBLE, WHAT ISN'T - I AM CHEATING!
Well as you will already know - only special vectors are
available, - uniform vectors with lengths of 1.
That means only vectors with 45 degrees difference.

Each direction increases in 4 degree steps!


HOW DOES IT WORK
Well you will probably know by now.
The routines generate vectors.

The generated vectors can be used and shared by vector lists.
The direction of the vector is of no importance - so that
the invers direction is really the same vector (apart from the sign).

That way I only generate at MOST 13 different vectors.
Usually I only generate vectors that I'm told to generate.
Therefor most of the time I calculate only 3-6 different vectors
per rotation.

There are two main optimizations used:

- reuse of the generated vectors
- using uniform vectors

REUSE
The vector list 'drawn' by the programmer points directly with its
'directions' to the ram location where the vectors are calculated to.
There is no need to do any copying. If the programmer rotates
5 different figures while using the same angle - we still only calculate
about 4 - 8 different rotation vectors (13 at the very very most).

UNIFORM VECTORS
Using uniform vectors and specialized makros for each of them helps a lot.
Uniform vectors have always a length of one. I use a lookup table
for the sinus - cosinus information used. We get rid of many multiplications
by using uniform vectors, since often:
 1 * sinx = sinx
-1 * sinx = -sinx
 0 * sinx = 0
Can (obviously) be used to do optimization.
I use makros for each possible constellation of vectors, so I also
get rid of 'if' - clauses for examining the possible optimizations.
I KNOW when one component is 1 or -1 or 0.

The resulting vectors have a length of 0 to 64*sinx + 64*cosx.
This is not optimal, since the sum NEVER reaches 128.
(well I couldn't be bothered)

(the multiplacation is done in assembler - not C)

ROOM FOR IMPROVEMENT
Well apart from being C. I think the routines are pretty well optimized.
But that doesn't mean one cannot draw more cycles out of them.

The usual 'do something in between waiting loops' does certainly apply.
Actually this would be a well worth and easy optimization, since the
calculation for the rotation-vectors, can be put into that 'loop', since
it is just that - calculation!!! (not effecting any internal vectrex
hardware registers)

Or some other clever stuff I didn't think of.

...

In the demo each 'figure' is calculated 'all the time'.
One could change the routines that each 3d object has its own
private RAM location. That way if the figure does not rotate...
you don't have to recalculate everything again.

If you re-calculate your figures e.g. only every second frame...
you could do two times the 3d stuff... or so...

...

Note:
Further for most 'applications' it won't even be neccessary to
rotate for all 3 directions.
One could modify the whole setup to only rotate
x,y axis... than one would be able to again save much time
for calculations...





Leave a Reply

Your email address will not be published.