|
P.T.Cruiser
  by Ben Reichardt and Alex Pekker
You are in the middle of the Pacific Ocean. You need to pick up all the golden
stars before the enemy boats close in and kill you. Don't be afraid to blow them
to smithereens first!
  Email: breic@stanford.edu, apekker@stanford.edu
|
Controls
The movement controls are
w
a s
z
To shoot, press 'q'.
Features
  Required functionality
- 3D viewing and objects - 3D viewpoint over a rolling ocean. Dynamic camera system
which takes the shortest path to getting behind the player, snapping into position more
quickly when the player moves faster.
- User input - Key input moves and turns boat using realistic physics model (for example,
resistance depends on the angle the boat makes with incoming waves) and collision detection
- Lighting and smooth shading - Boat and wall models are lit and smoothly shaded
- Texture mapping - Horizon and rockets are textured. Water is multitextured with dynamically
generated textures to simulate wave dynamics. The texture matrix is used to deform textures
separately from underlying geometry. Animated, transparent, billboarded textures are used for
sprites, a radar screen, and a score board.
Advanced features
- On-screen control panel - Radar display and score board implemented using orthographic
projection, and drawing bitmaps. The radar display shows enemy boats as large red dots,
uncollected goals as smaller golden dots. It uses several GL_LINES to simulate the turning
green lines on real radar displays
- View frustrum culling - Water is culled "statically"; there are two display lists for
each quadrant of cartesian space, and which display lists are called depends on which direction
the camera faces. Enemy boats are culled dynamically; before drawing a boat we test its bounding
volume against the view frustrum (only in the horizontal direction since boats can't go flying
into the air :)
- Level of detail control - We have multiple levels of meshing detail for the
water. Water further from the player is drawn with fewer triangles than water closer to the
player. Nevertheless, the different levels of detail are cleanly meshed together so no seams
are visible. Also, there are multiple levels of detail for enemy boats, supported by storing
display lists for several versions of the same model. Which display list is used depends on
the boat's distance from the camera
- Procedural and physically-based modeling - A texture-mapped wake is drawn behind the
player. This wake is generated by the laws of wave physics. The player's boat perturbs the
water underneath it, and these perturbations propagate outward, with slow damping. For
computational reasons we can only accurately simulate wave dynamics is a very small region
close to the player, which moves along with the player
- Collision detection - Simple collision detection based on bounding volumes is implemented.
Rockets collide with boats or obstacles, boats collide with each other and with obstacles, and
the player's boat can "collide" with the goals (in order to pick them up). To avoid an
O(n^2) test of collisions of everything against everything else, we sort the enemy boats and
only test for collisions between boats for those enemies nearest the player (it is extremely
rare that enemies get close enough to each other to collide unless they are close to the player)
- Simulated dynamics - As mentioned above, the boats are moved according to Newtonian
physics, with water resistance depending on the angle between the boat's bow and the incoming
wave. The wake texture is generated by wave physics, by applying a time-dependent specifically
designed filter kernel to several grids of sample points. We also have a particle engine which
is used to draw the spray of water that is thrown into the air by boats' engines. This simulates
gravity. The boats' heights and the goals' heights are sampled from the wave height, in order
to give the appearance that boats bob and roll over the waves
- Advanced rendering effects - We use billboarding for all the sprites. We maintain sorted
orderings of semi-transparent sprites to prevent visual artifacts (with semitransparent textures,
you can't just use the depth buffer). We have done some pretty complicated tricks with the
texture matrix in order to be able to move textures independently from the geometry of the
underlying water. For example, the surface texture of the water moves at a slightly different
speed than the water waves. Also the wake texture behind the player is always centered at the
player (and multitextured on top of the surface texture)
- Parametric curved surfaces - The waves are generated for the first time using 2D Bezier
patches. For speed reasons, though, after that we save a number of sample points and simulate
the motion of the waves by translating the underlying geometry. Because of our meshing
technology, we can use the sample points at any desired resolution
- AI - Enemy boats have a fairly simple AI; when the player gets close enough, they turn
on their engines (visible to the player, because the particle engine begins throwing up a spray
behind the enemy boat) and turn to navigate toward the player, shooting rockets when appropriate.
When they get too close to the player, their behavior is randomized; they can either stay there
shooting or they can swoop around the player to attack him from another side
Of all these features, the most difficult to implement were the level of detail control
for the water (clean meshing between areas of different detail levels is complicated) and
the texture-mapped wake behind the player (difficult to derive an appropriate wave motion
physics function, to tweak all the parameters to get reasonable output, and finally, to
place the texture on top of the surface texture in the right position).
Implementation notes
Everything was programmed in C, on and for MacOS X. The exact same code compiles and runs
for Linux. More information can be found in the source code. While we were unable to fully
comment all the source code, we have tried to at the very least put comments in the header
files to explain what the various packages do.
To compile PTCruiser on UNIX platforms, type "make -f Makefile.platform", where platform is
either sun4 or linux. An executable "game.platform" will be created, which can then be run.
Texture and model sources
The water texture used to be a standard MacOS desktop background pattern. The horizon
texture is just a random image of the sky, which I modified in order to have it loop 360
degrees. I also drew the radar background texture. The numbers are taken from Quake3Arena,
as are the rocket textures and the rocket model. The boat model is taken from
poserworld.com/models2.htm. We used 3D Exploration (www.xdsoft.com/explorer) as a free tool
for exporting .obj files as OpenGL code. We used (custom modified) texture-loading code
from David Blythe's SIGGRAPH '96 Advanced OpenGL course notes.
Sources of inspiration
John F. Kennedy, and his experience on PT Boats during World War II. Patrick O'Brian, for
his seafaring tales. The Macintosh game Spectre for the basic gameplay (get all the goals
while avoiding obstacles and killing enemies).
The score is on the left, the radar on the right. Note that the sinking enemy ship's engine
has turned off. Pretty water, no? (The visual artifacts in the upper left are due to the screen grab program.)
Notice the turbulent wake behind the player's boat. These screen shots don't really do justice
to the motion of the water and the textures, and the dynamic generation of the textures. These rockets
are taken from Quake3Arena.
Another nice screen shot. We worked hard to eliminate visual artifacts with as little work as possible.
For example, we do not use the depth buffer when we draw the water because it is always drawn from back
to front. These screen shots haven't shown it, but we also sort the semi-transparent flames so they are
drawn from back to front (the depth buffer can't be used then).
Note on the time-dependent filter kernel for simulating wave motion: The filter
function in ripples.c is, in pseudocode, approximately   "dest(x, y) =
(source(x - 1, y) + source(x + 1, y) + source(x, y - 1) + source(x, y + 1)) / 4
- dest(x, y)"  . (The actual kernel is slightly more complicated.)
Here, dest is the height array we are copying into (the next frame, which
initially contains the data from the previous frame), and source is is height
array for the current frame. The neutral height is at 0. Thus, the next height
is the average of currently neighboring heights, minus the offset of the
previous height. A heuristic argument for this function is: we average because
waves should smoothly interpolate values; we subtract the previous height
because its offset from zero indicates the amount that the height field should
be relaxed (that is, the velocity in the vertical direction). Since there is a
time delay, we get wave motion, rather than just smooth averaging.