Landscape rendering in less than 20 lines of code
- Project demo page
Let us go back to the year 1992. Processing power were 1000 times slower and acceleration via a GPU were unknown or unaffordable. 3D games graphics used very simple rendering algorithms and showed mostly polyons with a simple color.
It was during that year Novalogic published the game Comanche.
The graphics were awesome and in my opinion 3-5 years ahead of its time. You see a lot more details, shading and even shadows. Of course the processing power didn't change.
Comanche uses a technique called voxel space similar to raycasting.
To display the landscape a 10241024 one byte height map and a 10241024 color map is used which you can download on this site. These maps are periodic:
The algorithm draws just vertical lines. The following figure demonstrate this technique.
- Clear Screen.
- For visible surface determination start from the back and render to the front
- Determine the line on the map, which corresponds to the same optical distance from the observer. Consider the field of view and persective correction.
- Segment the line so that it matches the number of columns of the screen.
- Load the height and color from the 2D maps corresponding of the segment of the line.
- Do some perspective corrections for the height coordinate.
- Draw a vertical line with the corresponding color with the height retrieved from the perspective correction.
The core algorithm contains in its simplest form only a few lines of code (python syntax):
def Render(p, height, horizon, scale_height, distance, screen_width):
# Draw from back to the front (high z coordinate to low z coordinate)
for z in range(distance, 1, -1):
# Find line on map. This calculation corresponds to a field of view of 90°
pleft = Point(-z + p.x, -z + p.y)
pright = Point( z + p.x, -z + p.y)
# segment the line
dx = (pright.x - pleft.x) / screen_width
# Draw vertical line for each segment
for i in range(0, screen_width):
DrawVerticalLine(i,
(height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon,
colormap[pleft.x, pleft.y])
p1eft.x += dx
# Call the drawing function with the camera parameters:
# position, height, horizon line position,
# scaling factor for the height, the largest distance, and the screen width parameter
Render( Point(0, 0), 50, 120, 120, 300, 700 )
With the algorithm above we can only view to the north. A different angle needs a few more lines of code to rotate the coordinates.
def Render(p, phi, height, horizon, scale_height, distance, screen_width):
# precalculate viewing angle parameters
var sinphi = math.sin(phi);
var cosphi = math.cos(phi);
# Draw from back to the front (high z coordinate to low z coordinate)
for z in range(distance, 1, -1):
# Find line on map. This calculation corresponds to a field of view of 90°
pleft = Point(
(-cosphi*z - sinphi*z) + p.x,
( sinphi*z - cosphi*z) + p.y)
pright = Point(
( cosphi*z - sinphi*z) + p.x,
(-sinphi*z - cosphi*z) + p.y)
# segment the line
dx = (pright.x - pleft.x) / screen_width
dy = (pright.y - pleft.y) / screen_width
# Draw vertical line for each segment
for i in range(0, screen_width):
DrawVerticalLine(i,
(height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon,
colormap[pleft.x, pleft.y])
p1eft.x += dx
p1eft.y += dy
# Call the drawing function with the camera parameters:
# position, viewing angle, height, horizon line position,
# scaling factor for the height, the largest distance, and the screen width parameter
Render( Point(0, 0), 0, 50, 120, 120, 300, 700 )
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 bootom of the screen. However, you need more logic for the visibility calcuation
- Render more details in front but less details far away