Voxel Space | VoxelSpace
VoxelSpace Voxel Space
Web Demo of the Voxel Space Engine History Let us go back to the year 1992. The CPUs were 1000 times slower than today and the acceleration via a GPU was unknown or unaffordable. 3D games were calculated exclusively on the CPU and the rendering engine rendered filled polygons with a single 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 breathtaking for the time being and in my opinion 3 years ahead of its time. You see many more details such as textures on mountains and valleys, and for the first time a neat shading and even shadows. Sure, it’s pixelated, but all games in those years were pixelated. Render algorithm Comanche uses a technique called Voxel Space, which is based on the same ideas like ray casting. Hence the Voxel Space engine is a 2.5D engine, it doesn’t have all the levels of freedom that a regular 3D engine offers. Height map and color map The easiest way to represent a terrain is through a height map and color map. For the game Comanche a 1024 * 1024 one byte height map and a 1024 * 1024 one byte color map is used which you can download on this site. These maps are periodic:
Such maps limit the terrain to “one height per position on the map” - Complex geometries such as buildings or trees are not possible to represent. However, a great advantage of the colormap is, that it already contains the shading and shadows. The Voxel Space engine just takes the color and doesn’t have to compute illumination during the render process. Basic algorithm For a 3D engine the rendering algorithm is amazingly simple. The Voxel Space engine rasters the height and color map and draws vertical lines. The following figure demonstrate this technique.
Clear Screen. To guarantee occlusion start from the back and render to the front. This is called painter algorithm. 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) Raster the line so that it matches the number of columns of the screen. Retrieve 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 projection.
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 # Raster line and draw a 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]) pleft.x += dx
# Call the render function with the camera parameters: # position, height, horizon line position, # scaling factor for the height, the largest distance, # screen width and the screen height parameter Render( Point(0, 0), 50, 120, 120, 300, 800, 600 )
Add rotation 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
# Raster line and draw a 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]) pleft.x += dx pleft.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, # screen width and the screen height parameter Render( Point(0, 0), 0, 50, 120, 120, 300, 800, 600 )
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.
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 ybuffer = np.zeros(screen_width) for i in range(0, screen_width): ybuffer[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
# Raster line and draw a 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, ybuffer[i], colormap[pleft.x, pleft.y]) if height_on_screen < ybuffer[i]: ybuffer[i] = height_on_screen pleft.x += dx pleft.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, # screen width and the screen height parameter Render( Point(0, 0), 0, 50, 120, 120, 300, 800, 600 )
Links Web Project demo page Voxel terrain engine - an introduction Personal website Maps color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
color, height
License The software part of the repository is under the MIT license. Please read the license file for more information. Please keep in mind, that the Voxel Space technology might be still patented in some countries. The color and height maps are reverse engineered from the game Comanche and are therefore excluded from the license.
This site is open source. Improve this page. |
The Voxel Space engine is a 2.5D rendering technique rooted in concepts similar to ray casting, developed in the context of early 1990s computer graphics when 3D rendering was heavily reliant on the central processing unit and limited by the expense of GPU acceleration. The engine operates by representing terrain using height maps and color maps, where the color map inherently contains shading and shadows, simplifying the rendering process by removing the need for complex illumination calculations during rendering. For the work demonstrating these concepts, such as the game Comanche, height and color maps were employed in a 1024 by 1024 resolution, which are periodic and restrict the representation of complex geometries like buildings or trees, focusing instead on representing terrain height.
The fundamental rendering algorithm employs a painter strategy, drawing from the back to the front to ensure correct occlusion. This process involves determining lines on the map that correspond to the observer's optical distance, considering the field of view and perspective projection to account for objects appearing smaller at greater distances. The process then rasterizes these lines by retrieving height and color data from the associated maps, performing perspective projection on the height coordinates, and drawing vertical lines with the calculated color and height. The core mechanism iterates through distances and rasterizes segments based on the map data to generate the visual representation of the terrain.
To facilitate rotation, the line calculation within the algorithm is extended to incorporate viewing angles using trigonometric functions of the phi angle. This modification allows the engine to render views from different perspectives by recalculating the coordinates of the lines based on the camera's orientation relative to the map coordinates.
For enhanced performance, the rendering order can be reversed to draw from the front to the back. This approach reduces the necessity of drawing lines to the bottom of the screen due to occlusion, but requires an additional y-buffer to guarantee occlusion. This buffer stores the highest y-position previously drawn for each screen column. By drawing sequentially from front to back, the engine only draws a segment if the calculated height falls below the stored value in the y-buffer, thus efficiently managing occlusions. Furthermore, this performance optimization naturally lends itself to a Level of Detail strategy, allowing for rendering greater detail for closer objects while rendering less detail for distant ones. |