Using four colours to create super-smooth animated graphics
Aviator has beautifully smooth graphics - there is not a flicker in sight. If you compare this with Elite, then the difference is really obvious; Elite's ships flicker very noticeably, even in the later version for the BBC Master, when an improved algorithm helped reduce the flicker. But it didn't eradicate it entirely, so in this particular battle between these two Acornsoft heavyweights, Aviator wins hands-down.
The reason is that Aviator uses colour cycling to make the animation smooth. Although Aviator is a black-and-white game, it actually uses mode 5, which is a four-colour screen mode, and it uses those extra colours to switch between animation frames smoothly. The downside is that the canopy view in Aviator has a lower resolution than the space view in Elite, but that smoothness is a pretty good trade-off.
Let's have a look at how the main drawing routine in DrawCanopyView implements colour cycling.
How colour cycling works
------------------------
In the flicker-free approach, colour 0 is always mapped to black and is used for the background, and colour 3 is always mapped to white and is used for the dashboard and the canopy edges. We then use colours 1 and 2 inside the canopy, with one of these mapped to black (so it's invisible against the black background), and the other mapped to white (so it's visible).
To achieve smooth, flicker-free animation, we update the invisible screen, erasing and re-drawing lines so that it contains an up-to-date view out of the canopy. When we've finished this relatively slow drawing process, we then re-map colours 1 and 2, which flips the two screens in the blink of an eye. This flipping process is very simple and very quick - we simply map the visible colour to black, so the old scene disappears from view, and then we map the invisible colour to white, so the new, updated scene suddenly becomes visible. We then repeat this process to animate the 3D view without flicker.
So why can't we see the screen updating? Consider the situation where colour 1 is visible (i.e. mapped to white) and colour 2 is hidden (i.e. mapped to black). Pixels that look black on screen can have the following values:
- %00, which is always mapped to black
- %10, which is mapped to black as the hidden colour 2
and pixels that are white on screen can have the following values:
- %11, which is always mapped to white
- %01, which is mapped to white as the visible colour 1
If we update the hidden screen by only changing bit 1, then that won't change the colour shown on screen, as it's bit 0 that controls the visible colour. You can see this in the above: if bit 0 is clear then we're in the first list, so the pixel is black, while if bit 0 is set, we are in the second list, which is white. So as long as we leave bit 0 along when updating the screen, the screen won't change, even though we are updating it. We can do this by OR'ing our new values into bit 1, leaving bit 0 alone.
It's worth noting that the bit we need to change to update the hidden screen is the set bit in the colour number we want to update. So, if colour 2 is hidden, then 2 = %10, so we need to update bit 1 to update the hidden screen; if colour 1 is hidden, then 1 = %01, so we need to update bit 0 to update the hidden screen.
The colour cycling process
--------------------------
The DrawCanopyView implements this approach with the following steps:
- Modify the drawing routines so that they draw in the currently hidden colour (by calling ModifyDrawRoutine, which modifies the code of the main line-drawing routine at DrawCanopyLine)
- Draw the updated lines in the hidden colour (by calling DrawHalfHorizon, DrawClippedLine and DrawGunSights)
- Re-map colours 1 and 2 with the SetColourToBlack and SetColourToWhite routines, to change what's shown on screen
- Erase all the lines from the now-hidden screen (by calling EraseCanopyLines)
- Flip the colour variables, ready for the next iteration (by calling FlipColours)
There are two main variables that manage this process: colourLogic and colourCycle.
The colourLogic variable controls the drawing routines, and has three values:
- %00000000 = erase lines from the screen
- %01000000 = draw lines in colour 2
- %10000000 = draw lines in colour 1
The colourCycle variable keeps track of which colour screen is visible and which is invisible, and flips between these two values:
- %00001111 = show colour 1, hide colour 2
- %11110000 = show colour 2, hide colour 1
When drawing on-screen (as opposed to erasing), these two flip between the following pairs of values:
- colourLogic = %10000000 = draw lines in colour 1
colourCycle = %00001111 = show colour 1, hide colour 2 - colourLogic = %01000000 = draw lines in colour 2
colourCycle = %11110000 = show colour 2, hide colour 1
On the face of it, this looks wrong, but that's because both variables get flipped just after the screen colours are flipped, but colourCycle actually contains the state that we will apply after we do all the drawing, so you can think of the above list like this:
- colourLogic = %10000000 = draw lines in colour 1
... erase and redraw lines in colour 1 ...
colourCycle = %00001111 = show colour 1, hide colour 2 - colourLogic = %01000000 = draw lines in colour 2
... erase and redraw lines in colour 2 ...
colourCycle = %11110000 = show colour 2, hide colour 1
In other words, we flip both variables, then do all the drawing determined by colourLogic, and only when we are done do we apply the colour cycle in colourCycle.
Self-modifying code
-------------------
To save space, the main line-drawing routine at DrawCanopyLine is modified in-place so it draws the correct colour, and either erases or draws as necessary. The code modifications are performed by the ModifyDrawRoutine routine, which physically pokes new values and instructions into the DrawCanopyLine routine to change its operation. To add to the complexity, the ModifyDrawRoutine routine also modifies code within DrawCanopyLine that then modifies even more code within a different part of DrawCanopyLine; in other words, the modification code sometimes modifies the modification code.
To speed things up, the on-screen lines are stored in line buffers, so they can be quickly erased from the hidden screen by the EraseCanopyLines routine. This routine sets colourLogic to %00000000, then calls ModifyDrawRoutine to modify the DrawCanopyLine routine so it erases the currently hidden colour, and then it erases all of the lines stored in the line buffer. This clears the hidden screen in the most efficient way possible, so it's ready to take the new set of lines.
The result of all this self-modifying code and colour switching is a buttery smooth view out of the canopy. If you compare it to Elite's much more flickery space ships, there's no contest; Aviator wins hands down. (Though Elite's hidden-line removal takes things to another level, so credit where it's due...)