Voxel Space: Comanche's terrain rendering in less than 20 lines of code (2020)
github.com
github.com
Of course, this demo is just a starting point. I could optimize it even further, also in JavaScript or WebAssembly. But that's not the goal of the project. It is more a fan site for the technique.
(on my side I still participate in the demoscene, coding my n-th 3D engine for fun, so you see, once you get hooked... :-) )
This does limit the rotation allowed before the user starts to notice!
That is exactly what is going on here. I just move the horizon line up and down. This works very well as long as the rotation remains small. That is also the reason, why Comanche was a helicopter game and not an airplane game.
I did add my own "skew" parameter that would fake a little bit of horizon roll (skew by shifting up and down the column-renders to the screen along a shallow diagonal ... if you know what I mean). If you kept it small it looked like roll and added a sense of realism to the motion.
:)
Also, you want to render each ray/vertical screen line one at a time, rather than running each single step of the ray for all rays/vertical lines.
Once you are rendering each ray at once, it's easy to do a circular rather than flat projection, which feels a lot more stable when you rotate and admits higher fields of view without distortion.
But then you have a division of each coordinate. And this was more expensive than a memory lookup. So you might end up with a table as well. You have to test and optimize both approaches to be really sure.
I don’t disagree that you need to benchmark approaches to see if they will work, but it’s got to be interesting to consider if it’s possible to avoid allocating a buffer to store the height reached per column, right?
Discussed here <https://github.com/s-macke/VoxelSpace#more-performance>
This may require more performance. Remember, this algorithm was used for an early Nineties game.
The advantage of drawing along a line of same-distance (i.e. constant z) is that per pixel, there are no divisions, only additions and one multiplication (which could perhaps be optimized away?). Drawing vertical screen lines, you get the z-division for each step.
(Then again, perhaps the division could be removed with a look-up-table)
Yep, we used a lookup table with built-in levels of detail. Since rays always sample the terrain at the same Z distances from the camera, the lookup was trivial. Rendering vertical lines had additional benefits back then, including faster use of paged VGA video modes and, as described, doing circular projection to avoid deformations on the sides of the screen.
I could imagine mixing in "normal" polygons with this approach may not be trivial, though. I couldn`t find much information about this circular projection online, some napkin-math tells me that straight lines in world-space don't project into straight lines in screen-space. just drawing a triangle is not trivial.
As mentioned in the README of the linked repo:
Instead of drawing from back to the front we can draw from front to back. The advantage is, the we don't have to draw lines to the bottom of the screen every time because of occlusion. However, to guarantee occlusion we need an additional y-buffer. For every column, the highest y position is stored. Because we are drawing from the front to back, the visible part of the next line can only be larger then the highest line previously drawn.
This was released in 1999/2000 https://youtu.be/kxkSM6_F39g
Sadly voxel hardware acceleration was never really a thing and in the early 2000’s GPUs started leapfrogging a head of CPUs so much that you basically had to switch to polygons to get any decent performance.
† In mass production.
Edit: Small analysis on some screenshots... Mountains on the horizon look very polygonal on F-22. I'm not sure it uses the same technology.
Roll is visible in the game clip. I think it's not a true roll but just each column of pixels raised up or down so that the horizon is tilted appropriately. Given the fast movement and the nature of the scene, the user doesn't notice that vertical features in the scene are not rotated. It's neat.
But I can't get a local copy to run. I am not a web developer. Could anybody help me?
I cloned it from Github. But it doesn't seem to load the height maps from disk. What makes sense because websites are not allowed to access files, right?
You can launch a simple webserver to publish a directory to localhost port 8000 with `python -m SimpleHTTPServer`, and this should work.
I wonder how far you could take this with some additional layers on top of height map and color.
So that is to say, by following the painter's algorithm (far to close), we draw a lot of pixels over and over again. If the closer terrain is rendered first (reverse painter's algorithm), fewer pixels have to be filled for the farther terrain. In some cases, none at all.
The basic premise is this: an element of the color/height map which is farther from the viewer will never occlude an element which is closer, regardless of its relative height, and regardless of the perspective projection. The elements of the map are colored, vertical columns. A column farther from the viewer cannot occlude a closer column.
Suppose our task is to render exactly two arbitrary elements into an empty frame buffer. We can confidently render the closer one first, and then render the farther one, painting the "pillar" from top to bottom, until we are about to hit a pixel which has already been colored. All those early breaks out of the column-painting loops can be expected show a marked speedup.
I'd be surprised if the 1992 game didn't do it that way; they were squeezing every cycle they could get out of 386 and 486 boxes.
>More performance
>There are of course a lot of tricks to achieve higher performance.
>Instead of drawing from back to the front we can draw from front to back. The advantage is, the we don't have to draw lines to the bottom of the screen every time because of occlusion. However, to guarantee occlusion we need an additional y-buffer. For every column, the highest y position is stored. Because we are drawing from the front to back, the visible part of the next line can only be larger then the highest line previously drawn. Level of Detail. Render more details in front but less details far away.
Not necessarily. If there is a uniformly colored background, and the pixels being drawn are of a different color, you can test the existing pixel value to determine whether it's a foreground object, or background. In the rare case that an object pixel value happens to be the same as the background, you can make an alteration in the least signficant bit of the least sensitive color to make it different.
If there is a complex background, chances are you have a full copy of it, and can refer to it to see if any pixel in the image has changed.
And in any case, a Y buffer is just the size of one scan line. E.g. using a 1024x768 screen as an example (3 byte RGB pairs), you need 1024x2 bytes for the Y buffer. That's .08% of the size of the frame buffer. You could use a scan line in the frame buffer for this storage. If you are not actually using the entire screen (because, say, the game shows the inside of a cockpit with instrumentation) there is no sacrifice at all; after being done using that temporary memory, you will draw over it anyway.
Edit: Originally, I had said: “Though in a highly constrained scenario such as this, with the right approach you could get away with a 1-bit depth buffer.” That would be true if you had one depth value per pixel, which is of course so common nowadays that no one even needs to think about it. Except in this case I should have thought about it, because this algorithm requires only one depth (or, alternatively, as TFA and parent comment explain, Y) value per column of pixels.
Years later, when I got a more powerful box, I tried playing it again, but the game speed was proportional to cpu speed I think so it was unplayable...
I should give it another go in dosbox!
Dosbox offers ability to adjust CPU "speed" to help with this today, but it's often hard to get emulation speed right on early DOS games that ran uncapped.
I don't know how they pulled that terrain rendering off with 1994 hardware. The limited rendering distance probably helps.
I'm not sure about that. A quick search shows that the (on the box) requirements were 33mhz 486, which seems to have been fairly reasonable for the time.
Maybe fun trivia: James Schmalz trying to replicate Magic Carpet in pure assembly language and showing it to Tim Sweeney resulted in the embryo of UnrealEd and what later became the Unreal engine.
modeX memory layout has a write-enable bit for every 4th (or was it 8th?)column but the addresses was the same so you often would want to write every 4th pixel if doing something "exact" and then change the IO registers so it'd be every 4th again but offset by one, then repeating for offset 2 and 3 and then be done. Unless you were doing flat-shading where this write-enabling could be useful for writing 16 pixels in one 32bit word write with 4 writes enabled (this could also be useful for doing a half-resolution rendering by having 2 offsets enabled and then doing every other column).
(I really loved writing software-rendering code back in those days, glad to be getting much of that power back with shaders these days)
You have to test and optimize both variants to be really sure.
"Terrain rendering algorithm in less than 20 lines of code"
So, it just means, that a clean implementation of the terrain rendering algorithm takes 20 lines.
With initialization, keyboard handling, map selection, collision detection, fps display and some JavaScript optimizations it needs much more lines of code.
This and F15 were amazing games of the time. I bought the full thrustmaster setup for these.