It is used to produce glowing surfaces , so it's ideal for creating outdoor scenes.
The main idea about this technique is the fact that for each point on
screen you trace the lightrays from the eye trough the screenplane just
untils it hits the ground. On uses a 2-dimennsional heightmap were he vamlues
at coordinates (x,y) represent the height of the groundlevel.
In practice using a simple optimalisation you trace a lighbeam for
each column on screen. You start with a point that's at the x,y coordinates
of the player and has a height z. you make incremental steps trough the
map changing x,y and z until you notice you hit the ground. Once the ground
has been hit you can determine the colour by directly relating it through
the height of the point hit or by loking up its colors in a colormap.
If you use the height for coloring the pixel you can make the points
with Z-coordinate zero to be blue and then start a gradient going from
yellow (the beach) over green (grass) to brown/grey (rocks of the mointans)
into white (the eterna snow on top of thos montains).
Using a seperate colormap you can create a much better effect. Now
you can have different colored arrays on the same height (like making a
mountainlake) and its much easier to have precalculated shadows from the
heigher arreas on your map. Note that in the screenshot they use the colormap
as a topview radar to indicate enemy positions, here you can clearly see
the precalculated shadowvalues.
An other problem with this type of approach is the space consuming size
of the map. Since the code has to be fast the overhead in decoding the
map datastructurebefore a map position can be checked must be kept as low
as possible. Therefore the most simple thing to do is to keep the datastructure
a simple twodimensional array in memory. For further optimisation the x
and y coordinates should be byte alligned. This gives us the oportunity
to say something like
LD L,X-value
LD A,Y-value
OR %10000000 ; to align the map on #8000
LD H,A
LD A,(HL) ; A contains the height of the map at coordinates (x,y)
If we use a 16 kB mapperpage for this one mapper page can contain a
map of 256x64 pixels. You see that this method needs a lot of memory. Having
a map of 256x256 pixels means once has to sacrifice 4 mapperpages only
to the map alone. One could use bit 6 and 7 of the Y-value to chose the
mapperpage in this example
full sprite
definition
definitions
for simple clipping from right.
The main idea to portal rendering is that we change from a from describing
on entire gamemap in one giant 2D/3D space into several little spaces,
in effect these are our rooms. Every room is its own universum and some
sides of the room are gatways to the other universums. A portal into the
other room.
![]() |
![]() |
The normal maps used in games are drawn in one plane | The same map now made of rooms were some walls are portals (green) who are connected to other rooms. The connections are indicated in purple. |
For simplicities sake let's set the following criteria : Walls are always vertical. No curved walls, no walls leaning inward or outward. As an extra criteria, although this isn't really necesary, we can say that in such a room the floor and ceiling are perfectly horizontal, this way we can use just two numbers to tell the height of floor and ceiling. If we take these criteria into account we get the same constraints as used in DOOM. We can describe our room in a 2D map where each room has two extra atributes, a floor and a ceilling height.
Let's look at the simplest room we can draw. Instead of heaving to sort all the walls according to depth so that the farst wall can be drawn first, it would be much faster if we could draw all the walls in random order, disregarding their screendepth. This would eliminate the need for CPU expensive sorting. The rooms which fullfill this criteria are convex rooms, simply meaning that if we draw all the lines of the walls, no two lines will cross in the room, but all the intersections will be outside of the room. In such case it doesn't matter which wall we draw first, because no matter where our point-of-view is whitin the room, never will two walls overlap each other. Hence, we dont need to sort our walls anymore before rendering.
Having a game with only one room wont make a great First Person Shooter. However, let's look at a normal everyday room. Besides walls we are verry likely to see something else, and I dont mean the objects in the room. An everyday room also contains doors and Windows(tm) which enables to look into the adjendant rooms. They give us a portal into the other rooms.
Advantages
On MSX this method has an incredible amount of advantages. These make
it possible to do stunning things on our humble, beloved system.
![]() |
![]() |
Empty background | After drawing our rooms |
Here is our enemy that walks in the other room.
If we combine this sprite with a window then we can have some of these effects.
![]() |
This is the best result, this is automatically correct if after drawing the sprites in the other room, we return to the first room and draw the heightdifference between the ceilling of the two rooms. If our sprite would be bigger then maybe his feet could be sticking out from beneeth the wall. If we would really texuremap the floor and ceiling afterwards this wouldn't be a problem either. Clipping in this case could be used to draw just a part of the sprite so that drawing of the sprite would be just that litle faster. |
![]() |
Since our MSX isn't that fast and we didn't plane on much drastic height changes (like the fake window creation), we could as well drop the routine who would fill in the height difference. If we dont use any Y-clipping, this means that we would draw the entire sprite. This definitely destroys our wanted window effect, if we would have made a staircase then the effect wouldn't be that great. Besides building such a big sprite will take up a lot of time on our 3.5 MHz MSX's. |
![]() |
The window/portal is ,if not look straight upon, a parallellogram. If we take the greatest inclosed rectangle we could clip our sprite drawing up on this region. This would give us the smallest look upon the sprite so our humble CPU/VDP combination will be able to draw this verry fast. If in extremis we use a verry long window this effect could have some strange effects, small enemys could be clipped completely out of view. |
![]() |
If we would chose to take rectangle witch would surround our parallellogram than drawing will be a little slower than the previous solution, but still much faster than with no clipping wat-so-ever and we still have eliminated the need for heightfilling. Out sprite still sticks out of theire respective real-world-windows, but who needs perfection, hm ;-) |
There are two options we can use to store the height of every room.
Bare in mind that no matter which method you use you are able to construct
a level with multiple niveaus, you can have two rooms above each other
!!
If you would draw the map in one single big plane you could even have
cases were two rooms would overlap in absolute coordinates. Since you draw
only the room you are in and the ones visible trough the portals, you wouldn't
even notice during gameplay.
If you use method two you could even recreate the always climbing stair
that Escher produced one, since every room would be relative heigher to
its predecesor.
Creating an elevator floor is now a simple effect. You will have a subroutine that alters the floorheight of a given room. For example let's say that way have a hallway divied in 3 rooms.Room A has a low floor, room C has a heigher floor and B will act as elevator. Our subroutine will alter the floorheight of room B, if the floor is almost the same as floor A then the portal A->B will be passable, otherwise you will set the flag to nonpassable. You shouldn't e able to pass from room A to room B if the elevator is already to high compared to room A. Same goes for the protal B->C. Notice that the portals C->B and B->A will always be passable. You can always fall down upon a lower section.
Using the same idea, you could make a small room were you will raise the ceiling height, as soon as the ceiling is high enough you can walk trough this door. Ofcourse you could create doors who slide down into the floor or combine both into something like those fancy airlocks you find in most SF televisionseries.
The simplest way would be ofcourse to use a counter and limit the number
of portals we will recursively trace. Verry simple to implement, but way
to
slow to be usefull. We would benefit from to chached rotations ofcourse
but our slow drawing routines would still redraw all the walls, and there
is no sure way to cache the screen x/y projection so this extra math will
be repeated over and over and over again. If we were stupid and all used
cray-computers then this could be used without having a bad consionnes
about it, but we rather want to be proud of our code, and our target computer
, the MSX, is way to slow. Therefore we ban this bad solution from our
minds and continue.
A more usefull way could be to pass a reference to the original room that we started from when we did the recusive call, if a portal of the room would bring us back to our recurse-routine-calling-room, we could simply skip it. This is almost as simple to implement as the previous solution (which we have already forgotten!!) and saves us tremendious on wasted CPU/VDP-cycles. However it means that a lot of the suggested extra's I mentioned earlier on, can't be implemented anymore. For example the trap-the-player-in-a-room-trick can't be used. You would alwaays draw the full steel door, even when we were supposed to be able to still look into the room. Also the transport-with-preview should become inpossible, or teleport must be aligned with a portal that goes back into the current room, so in efect we just have a simple door from room A to room B. It wont be posibble anymore to have multiple teleporters ending up in the same room.
The best solution is to draw the walls only if we look at the walls
from the right angle, if we look at the "back" of the wall we don't draw
it. People who already have some background on this kind of rendering pricinples
will start to yell: 'normal vectors, he is talking about normal vectors'.
Indeed I am. In the more complex, more mathematical approach we would take
the normal vector of the plane describing the wall and a vector representing
our direction of view. We would calculate the vectorproduct of these to
2 vectors which would yielld the cosine of the angle between the vectors.
If this number is negatiev we would be loking at the backside of the plane
if it is possitive we would be able to see the plane. The number could
be used to darken the wall, because it is a messurement of the reflected
light. This is excatly how the shading is calculated in Calculus and SandStone.
O Stn higher end machines you could simply take the coordinates of the
points and calculate the normal vector on this plane. On our beloved MSX
this approach is again to slow, so some of you will now opt for storing
the normal vector also in the data structure describing the rooms. Still,
you would have to calculate the vector product, which is slow !ut the yes/no
question of visibility
Let's cloud ourself in the illusion that we are smart and optimize
further uppon the normalvector idea.
The main idea here is that we use the the angle between our vectors.
The great thing about the vector product is that it also tells us what
the light-reflection is given the fact that our lightvector and viewdirectionvector
are the same. However since we aren't that interested in lightreflectionness
of the surface but only about the yes/no question of visionability, this
means that in effect we are only interested in the question. :
Is the angle between the normalvector and our viewdirection so that
the wall is visible ??
So why bother with complex math if we can handle the question with
some simple angles ? Let us just store the angle of the normal vector of
the walls in our datastructure. Or even better, let us store the
angle of the vector opposite to the normal vector. This means that if viewangle
and this aangle are the same that we are looking directly at the wall.
So a wall will be visible if :
abs ( angle_of_viewdirection - angle_wall ) < 90 degrees.
this is exactly the same question as would be answered by the vectorproduct
question.
So far we overlooked a simple side-effect of our screen projection.
If the angle between the two vectors is 90 degrees whe would see a
line representing the plane, if the angle is greater then we would say
that the plane isn't visible. However we overlooked the fact that
our projection-to-screen will place points farther away closer to the center
of the screen. So walls having an angle-difference from more than 90 degrees
will still be visible.So We need to adjust our equations. In the vectorproduct
question we would need to say that the result must be greater than some
(small) negatieve number and in our solution we would say :
abs ( angle_of_viewdirection - angle_wall ) < (90 + safety marge
) degrees.
The mathematically correct way would be to test after screen projection
or integrate our screen projection formula into calculating the normal
vectors. the safety margins approach isn't completely correct, a point
further away will be projected differently then a closer point while the
mathematical normalvector would be the same. However this approach will
probably be the best method to use on an MSX.
Also note that using this approach doesn't conflict with our rotatedpoints
caching, since for a given directionangle always the same points should
be calculated as visible.
type PLAYERDATA {
byte X; // coordinates relatieve the axis of current room
*room currentroom;
byte viewingdirection;
byte currentheight;
byte wapeonholding
byte ammoleft
....
}
type WALLSIDE {
byte X;
byte Y;
byte ROTATEDX;
byte ROTATEDY;
byte NORMALANGLE;
byte ISVISIBLE
}
type WALLS{
byte type; //0=portal otherwise is the number of texturemap for
the wall
byte iswalktrough;
.. extra info like recoloring of the wall
..animation step if portal is a door sliding open before it's set
to iswalktrough
*ROOM nextroom; //if portal this points to the next room
byte xtrans; // this are the offsets to translate the points of
byte ytrans; // nextroom into coordinates of this room
}
type ROOM {
byte nr_wallpoints;
WALLSIDE POINTS[];
WALLS ROOMSIDES[];
byte rotation_angle;
byte floorheight;
byte ceilingheight;
byte nr_enemy;
ENEMYTYPE ENEMY[];
}
void Main(void){
while (not game over){
moveplayer();
moveenemy();
drawroom( player.currentroom,
player.viewingdirection,
player.X,
player.Y,
player.height,
0,0,255,212,
);
check collision between bullets and player;
check collision between bullets and enemy;
check collision between enemy and player;
}
}
void drawroom (*ROOM room,
byte viewdirection,
byte xtrans,
byte ytrans,
byte ztrans,
byte clipx1,clipy1,clipx2,clipy2){
if (room.rotation_angle != viewdirection) {
room.rotation_angle = viewdirection;
calculate rotation of all points for which NORMALANGLE tells us they are
visible, and update the ISVISIBLE;
}
// the screens view goes from -64 to + 64 , nul
is the center of the screen
// this is don so that the x correction dependeing
on the screen depth of
// the object can be easily calculated by a simple
multiplication. This factor is
// precalced and stored in a lookuptable, the screen
depth is the index
screenx1=2*( (room.POINTS[0].ROTATEDX-xtrans)
*lookuptable(room.POINTS[0].ROTATEDY-ytrans)
)+128;
//since the first point is always (0,0) you better
optimize this ;-)
prevvisible=false;
for (byte i = 1 ; i<room.nr_wallpoints ; i++ ){
next if !ISVISIBLE;
screenx2=2*( (room.POINTS[i].ROTATEDX-xtrans)
*lookuptable(room.POINTS[i].ROTATEDY-ytrans)
)+128;
if (prevvisible){
if (
(screenx1<clipx1) && (screenx2<clipx1) )
|| (screenx1>clipx2) && (screenx2>clipx2) )
) {
// walls are completely left or right from visible array so skip next drawing
part
}else{
clip if needed to part in visible range
if (room.ROOMSIDES[i-1].type==0){
drawroom (room.ROOMSIDES[i-1].nextroom,
room.ROOMSIDES[i-1].xtrans + xtrans,
room.ROOMSIDES[i-1].ytrans + ytrans,
ztrans,
screenx1,clipy1,screenx2,clipy2);
draw effect, like bars or colorfilter, if needed
} else {
draw wall of type room.ROOMSIDES[i-1].type
}
draw sprites
}
}
screenx1=screenx2;
prevvisible=true;
}