The Viper
engine had several very difficult resource management issues to overcome.
Each level in the game was composed of hundreds of thousands of polygons,
which couldn’t possibly be loaded at once; but we wanted the game’s
load time to be very brief. To accomplish this, we needed the game to
page in and out of memory without affecting the frame rate, and we needed
data loaded from disk to be ready to use with little or no additional
processing.
To achieve
these goals, we used the editor to load all the data into the game’s
native data structures and then wrote those structures directly to Viper’s
data file. These structures then loaded on demand at run time. Because
we knew that this was a design requirement ahead of time, all the data
structures in the game were designed to do this fairly easily. The only
tricky part was when pointers were involved. We had to convert these
pointers to offsets before they were saved to disk. At load time, Viper
converted these offsets back to true pointers, and the data structures
were ready to use.
The
large number of polygons in SpecOp's levels required
a cache system that loaded only the data for the player's immediate
area.
Once we
had a system that could efficiently load and use data, we had to design
a cache system to load only those resources required for areas immediately
around the player’s position. We did this by dividing the world up into
hexagonal pieces. At any time, three of these would be loaded in memory.
The size of each hexagon was determined by how far the player could
see in the environment (you don’t want the player to be able to see
the edge of the world). Each hexagon had to be further across than the
viewing distance, yet small enough to efficiently load from the disk.
The geometry for the hexagon was stored to disk, along with the resources
(trees, pickups, enemy positions, and so on) associated with it. We
completed the system by creating an asynchronous thread, which could
load and unload the hexagons without halting the CPU. After a few optimizations
(such as adjusting the hexagon size), we were able to load and unload
these hexagons with only a one to two FPS impact.
Although
the original design called for a similar system to handle the textures,
we didn’t have time to implement this feature. Instead, we simply loaded
all the textures for a level at the start of the game, which worked
out fairly well. With the additional RAM now available on 3D accelerators,
it might even be a better solution anyway.
Graphics
and Animation
The way
in which Viper handles graphics and animation is one of our most beloved
— and feared — components of the engine. More programmer time was spent
on the graphics component than any other part of the engine. Viper had
to deliver up to 10,000 polygons in a single frame (for reference, that’s
an entire Quake level), and still run at real-time frame rates.
It also had to be able to run without a Z-buffer on the PlayStation,
which required a sort routine for the polygons. Finally, it had to support
software rendering as well as 3D hardware acceleration on the PC. Since
this is such a huge piece of technology, I will only touch on the most
prominent issues.
BSPs.
By far the biggest bane of the 3D programmer is polygon sorting. Most
methods that are reasonably fast also have problems. We chose to use
3D BSP trees because they’re efficient to process, which helps both
the graphics and physics engine run faster. One big problem with the
BSPs, however, is that each one can take a long time to create. We mitigated
this problem by using the hexagon system discussed previously. Since
our world was already divided into non-overlapping pieces, each piece
could have the BSP created individually. Since BSP creation is an exponentially
complex problem, and our levels had hundreds of thousands of polygons,
dividing the world into small pieces saved us from what would have been
a feasibly insoluble problem. Furthermore, if and when we had to modify
the world, we only had to recreate the BSP trees for the hexagons that
had changed. Better yet, we could divide the labor of this task, so
that we’d need each machine in the office to create just a couple of
the BSP trees after hours. Thus, the artists could stay late and go
through several iterations of BSPs in one night.
Zombie
used Z-buffering to handle moving objects for
the software renderer.
Another
BSP-related problem was how to handle moving objects. Early on in the
project, I was dismayed to hear that the guys at id Software had given
up on solving this problem and had resorted to Z-buffering the dynamic
objects in their Quake scenes. After several painful weeks of
work, we finally had an acceptable solution, which exhibited sorting
errors along the lines of Tomb Raider. Our solution was prohibitively
slow, however, and when we reached our PC alpha stage and discovered
that the PlayStation version was only running at five to ten FPS, we
halted the development of that version. At that point, we switched to
Z-buffering for the software renderer as well, since sorting errors
are generally unacceptable on today’s PC games.
A BSP
solution is a mixed blessing. While they efficiently process algorithms,
BSP trees don’t handle dynamic objects well and they don’t like to be
modified at run time. Worse yet, the time required to create them can
really slow up the game designers and artists. Because I won’t be targeting
a platform without Z-buffering again, I’m considering switching the
Viper engine to a different data structure.
Lighting.
Viper doesn’t use light maps. When I debated the use of light maps versus
an RGB vertex lighting scheme, the vertex approach seemed to be better
supported by the hardware accelerators. I didn’t like the time penalty
of creating the light map surfaces, or the fact that the bus would be
flooded with texture data. Viper’s RGB lighting scheme supports an infinite
number of colored light sources, combined at the vertex level and cleverly
optimized to take almost no time penalty. The downside to this cheap
lighting scheme is that it doesn’t always have smooth edges along polygon
borders. I’ll probably dump this method in favor of something better
in the near future, since processor speed is becoming so impressive.
3D
hardware cards. I took a gamble and based all of Viper’s development
on the original 3Dfx Voodoo chipset. Two years ago, this chip had no
marketshare, and cards based on it were more expensive than their competitors.
However, 3Dfx had an excellent developer support group, and its board
was fast and easy to use. Most importantly, it supported the basic polygon
type that SpecOps would be based upon: Z-buffered, RGB-lit, textured,
perspective-correct triangles. When I got the API and saw that you could
start working with the board without writing any Windows code (through
Glide), I knew I’d made the right choice. I wrote to Glide directly
because it’s easy and it’s considerably faster than OpenGL or Direct3D.
By the time you read this, Viper will be supporting other boards through
a minimal subset of OpenGL.
Importing
geometry. Viper can import geometry from Alias, Softimage, Lightscape,
and 3D Studio MAX. Instead of supporting one package really well, we
only had time to support all of them minimally. Still, there is something
to be said for letting the artists work in the programs with which they’re
most comfortable. Also, at the time we were designing the engine, MAX
and Softimage didn’t support color vertex data, leaving us with few
options. I think that we might soon work Viper into a single CAD package
and rely on file format converters to move data around.