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.
Game Gunship 2000 published by MicroProse in 1991
It was during that year NovaLogic published the game Comanche.
Game Comanche published by NovaLogic in 1992
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 ray casting.
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 the perspective projection (Objects are smaller farther away)
- 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.
- Perform the perspective projection 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, screen_height):
# 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):
height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon
DrawVerticalLine(i, height_on_screen, screen_height, colormap[pleft.x, pleft.y])
p1eft.x += dx
# Call the render function with the camera parameters:
# position, height, horizon line position,
# scaling factor for the height, the largest distance, the screen width and the screen height parameter
Render( Point(0, 0), 50, 120, 120, 300, 800, 600 )
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, screen_height):
# 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):
height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon
DrawVerticalLine(i, height_on_screen, screen_height, colormap[pleft.x, pleft.y])
p1eft.x += dx
p1eft.y += dy
# Call the render function with the camera parameters:
# position, viewing angle, height, horizon line position,
# scaling factor for the height, the largest distance,
# the screen width and the screen height parameter
Render( Point(0, 0), 0, 50, 120, 120, 300, 800, 600 )
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
def Render(p, phi, height, horizon, scale_height, distance, screen_width, screen_height):
# precalculate viewing angle parameters
var sinphi = math.sin(phi);
var cosphi = math.cos(phi);
# initialize visibility array. Y position for each column on screen
hidden = np.zeros(screen_width)
for i in range(0, screen_width):
hidden[i] = screen_height
# Draw from front to the back (low z coordinate to high z coordinate)
dz = 1.
z = 1.
while z < distance
# 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):
height_on_screen = (height - heightmap[pleft.x, pleft.y]) / z * scale_height. + horizon
DrawVerticalLine(i, height_on_screen, hidden[i], colormap[pleft.x, pleft.y])
if height_on_screen < hidden[i]:
hidden[i] = heightonscreen
p1eft.x += dx
p1eft.y += dy
# Go to next line and increase step size when you are far away
z += dz
dz += 0.2
# Call the render function with the camera parameters:
# position, viewing angle, height, horizon line position,
# scaling factor for the height, the largest distance,
# the screen width and the screen height parameter
Render( Point(0, 0), 0, 50, 120, 120, 300, 800, 600 )