Screen rendering
As far as this doc is concerned, 'screen rendering' is the process of drawing the map and entities onto the screen. Other elements such as the HUD are not covered here.
Current code
The current code for screen rendering is all contained in the draw_screen() function, and operates as follows:
- plot_getvisiblearea() is called, to calculate (the upper bound of) the visible area of the map.
- Using the plot coordinates calculated by plot_getvisiblearea(), the min/max loclist coordinates are calculated.
- For each loclist within the min/max area, objects_draw(), peds_draw(), and cars_draw() is called on the appropriate sublists. These functions add the visible objects, peds and cars to the vobj list.
- vobjs_sort() is called, to sort the list in order of decreasing distance.
- plot_map() is called, to draw the map, interleaved with the objects
- Finally the onscreen messages and HUD are drawn, by messages_do() and drawhud() respectively.
This is known as the 'plotter MK 2', as it interleaves the vobj rendering with the map block rendering (as opposed to 'plotter MK 1', which rendered the map column-by-column, and then rendered all vobjs directly ontop) It is not a perfect algorithm, but it is hoped that incremental improvements can be made to hide or eliminate any current faults with the vobj plotting.
Map plotting
plot.c contains the map plotting code. The basic operation of the map plotting algorithm is as follows:
- Work out the visible area of the map
This involves expanding a pyramid down from the camera, and working out the lowest point in the area under the pyramids base. If the lowest point is lower than the base of the pyramid, the base of the pyramid is lowered to that point; the sides of the pyramid stay fixed at the field of view of the screen (i.e. 45 degrees up & down; approximately 45 degrees left and right). The 'lowest point under rectangle' function (rectangle_bot()) simply finds the stack inside the rectangle that has the lowest start height, working under the assumption that stacks won't have any unneccesary blocks at their base.
- Plot the map layer by layer
Starting at the bottom and working up, each layer of the map is plotted. This is interleaved with calls to the vobjs_draw() function, which will draw the last vobj in the vobj list and then remove it. This allows vobjs to be occluded by map blocks, albeit in a primitive manner, as it depends heavily upon calculating a 'correct' Z coordinate for the vobj. This is where the majority of the improvements to plotter MK 2 will be made.
For each layer of the map, the blocks are plotted from the outside in, in order to allow for the correct occlusion of block sides.
- Block plotting
This is performed by the plot_block() function (and its children). The basic operation is to calculate the coordinates of the 12 corners of the block in screen space, round them to ints, and plot the visible sides. 12 corners are used instead of 8, in order to account for the sloping top; there are 4 coordinates for the base of the block, 4 for the top, and 4 for the sloping top. Although it may seem that only 8 coordinates are needed, some of the 'extra' coordinates are used by the code to ease certain calculations (such as texture scales)
Tile plotting
There are 3 tile plotters called by the plot_block() function. All plotters are handed a set of coordinates, and a texture prerotated a multiple of 90 degrees to face the correct direction for plotting. Plotting is performed from the top row of the trapezium to the bottom.
It's worth pointing out at this point that all tile textures are 64x64, 8bit, with 256 entry palettes. They have an optional mask (where palette entry 0 is used as the mask colour). During rendering, the sprites are converted to 16bpp, with a proper mask channel, if required. The tile sprites are the same scale as other textures in the game (i.e. one pixel = 1/64th of a block = 1/16th of a metre). By default the sprites have no fullbright palette entries. However a sprite name with '^' as its first character will result in the first N palette entries up to (but not including) the first black entry to be used as fullbright colours.
plot_tile_h()
This is used for 'horizontal' tiles, i.e. ones that are a trapezium with the top and bottom edges parallel. This function is used for the north and south sides of blocks, as well as the flat tops of blocks, or the tops of blocks that slope north-south. The function also deals with cases where corners are missing; i.e. when plotting the north/south face of a block with an east-west slope. This adds extra complexity, for the code must calculate the correct texture deltas to use. The function relies on the assembler function tile_row_plot() to plot each scanline of the tile.
For historical puproses, it's worth mentioning plot_tile_v(). This was the original counterpart to plot_tile_h(), essentially a copy of plot_tile_h() but all logic rotated 90 degrees to allow for plotting of east/west faces of tiles. It also plotted in columns rather than rows. However this was found to be significantly slower than plotting in rows, so the following two functions were written to replace it.
plot_tile_e()
This is the function for plotting the east faces of blocks, or the top of a block that slopes down to the east. As with plot_tile_h() it must detect and deal with cases where the input isn't a trapezium (i.e. the sides of sloping blocks). Since it plots by rows, it must also perform calculations to perform perspective-correct texture mapping, otherwise textures will appear distorted and may swim.
The function is split into two parts: The first part performs some calculations which the scanline plotter (tile_new_col_plot()) uses to calculate texture coordinates, while the second, much longer part drives the scanline plotter itself. Overall the function is shorter than plot_tile_h(), but that is likely because it only deals with half as many tile shapes.
plot_tile_w()
This is the counterpart to plot_tile_e(); i.e. it is used to plot the west faces of blocks, or the top of a block that slopes down to the west. It is similar in structure to plot_tile_e(), and uses the same low-level scanline plotter.
vobjs
vobjs, AKA 'Visible objects', is the way that the visible state of a visible entity is represented. The vobj struct (as defined in vobj.h) contains a pointer to the sprite, the rotation & scale values to plot the sprite with (ready for passing to gnarlplot), the location of the sprite (in worldspace), and the 'corrected Z value' (used for sorting the list for map plotting).
The idea behind the vobj is that it removes the differences between peds, cars and objects, allowing all visible entities to be processed using the same code and in the same way - and in that goal it succeeds.
Tile sprite caching
Similar to the ped and car sprites, tile sprites are also cached from frame-to-frame. The tile sprite cache is fixed in size, currently at 200 entries. When the cache becomes full, the least-recently-used entries are the ones to be recycled. A tile can potentially have a cached sprite for each frame and rotation. However, a tile cannot have multiple entries for different stages of the day/night cycle. The code uses this to its advantage, for if a tile already has a cache entry allocated, but for a different stage of the day/night cycle, then that entry will be reused to build the new sprite, as opposed to getting a fresh tile from the list.
TODO:
MUST-DOCS: UNKNOWN: Try and find solution to objects appearing ontop of bridges if they stand next to a support! GTA 1 didn't have this problem, so maybe they had some extra funky code to fix it? Or didn't snap the Z coordinate to the top if the sprite was too far down inside the wall? (But how would that explain ped rendering? Assuming peds did appear ontop, that is?) Maybe check if the nearest corners of the blocks underneath the vobj come down to the same height as the vobj itself? Or maybe only take into account the heights of nearby blocks which are sloping? (would solve the issue of objects appearing ontop of buildings, at least) But that would break the code which allows peds to be visible when stood up against a wall. Maybe try using the (accurate!) collision bounds of the object, rather than the bounds of the sprite? Or somehow get the accurate bounds of the sprite? (since some cars are slightly smaller than their sprites) This information would likely need to be passed from the game to the vobj code, e.g. size of the base car delta image (since deltas are shrunk to smallest size during loading) Can then use bbox_worldintersect to get highest point. Although, this would still need code to prevent objects appearing ontop of bridges, as would any approach that uses a rectangle larger than the collision bounds.
MUST-DOCS: DELAYED: Document the plotter MK 2 amendments!
MUST-CODE: UNKNOWN: Fix line lengths being off by 1, etc.
CODE: MED: Add support for screen fading? FADE script command that fades to/from black, maybe per-frame flashes of light for lightning, etc. Would be post-render (pre-HUD?) process, unless proper day/night thing implemented.
MUST-CODE: QUICK: Homogonize FOV-related camera calculations. Externalize *_PER_Z values, maybe allow for runtime tweaking to get best viewpoint, etc.
MUST-CODE: MED: Speed up fullscreen map - just copy the previous frames' image, and redraw the edges where it's scrolling. Need to cache the frame in RAM to avoid penalties reading from PCI RAM, and limit to integral scale factors to prevent number accuracy problems. When caching, create a sprite/screen that's large enough to hold all the tiles for the current screen, and render that sprite with an offset to the main screen. Can just allocate one sprite that's the same size as the screen but has a 64 pixel border?
MUST-CODE: MED: Bug in fullscreen map - sometimes certain tiles appear black.
MUST-CODE: MED: Fix rendering of west faces with sloping top edges (with the top point to the north)? Or is it an obscure bug in canvas rotation?
CODE: QUICK: Limit vobj zofs to bounding sphere radius?
???
Profit!