OTTD is a 2.5D game, having an adequate orthogonal XYZ space (Z is scaled by some factor, but it’s ok). OTTD draws everything with sprites. Each sprite represents some object, or a part of the object. So, we can say, OTTD scene consists of an objects in XYZ space represented by the sprites. And what if we replace the sprites, by a full scale 3D objects, and render everything with OpenGL 3D engine? We’ve got a 3D version of the OTTD that would be nice.
Sprite is an image coming from the GRF file. Each sprite can be identified by the GRFID of the GRF file, and it’s id inside this file. So, we can build a map, from the drawn sprite, to some 3D geometry. Now we just need to replace a 2.5D viewport rendering procedure with our 3D rendering procedure to render 3D objects instead of 2D sprites. Actual things are slightly more complex, but everything fits this idea. We are leaving all of the game logic, GUI and a hi level tile drawings untouched, intercepting only the output in terms of sprites mapped to the 3D resources. This is a general constraint that probably fits to the project goal. Having this in mind, I've started my experiments.
The intermediate result:
This is a test graphic set extracted from the default resources, to check that the new 3D engine is working properly. It’s able to render a huge amount of different objects with different textures, computing pixel lighting and casting 3D shadows, but still supporting palette recoloring and the global color palette animation. I’ve made some overrides, like a foundations drawing, because it was not possible to use original sprite mapping for the full 3D scene. Fixed trees Z positions on slopes, made vehicles positions change smoother, extracting fractional part of the speed, and correcting it’s trajectories on slopes and corners. But everything still from the bottom, on a renderer level. Original game logics was not changed, and still driving everything.
As you can see, nearly everything is working. The only missing thing, is an effects, like a broken down smoke. I am still thinking about it’s implementation (particle system or an animated sprites)… but, all of the other things, can be added with new 3D graphical resources, and we get a complete working and a nice looking game. Of course some things still needs refinement, but they are all already working correctly.
Compatibility and performance:
This is a case where the OpenGL 2D texture arrays and instancing is absolutely required to have a good performance. With it, it’s possible to make a really fast renderer. That’s why the 3D engine was designed to use OpenGL 3.3+ spec compatible hardware (and running best on 4.3). I think this should be ok, OpenGL 3.3 == 10 years old graphic cards. I am using all of benefits from the modern interface. With my computer, drawing everything each frame in 3D with AA and shadows, is even faster in some cases, than the default 2.5D renderer, with it’s dirty blocks and updates on demand. It’s possible to make a compatibility patches for the lower OpenGL versions (with a lot of coding), but the performance of this hardware is too low in any way.
Internal architecture:
OpenGL subsystem, consists of the two things. The Blitter, with a standard interface, and a new 3D viewport renderer.
Blitter is implemented with OpenGL driver (windows driver was split to the base part, and a GDI + OpenGL implementations). Driver itself, does nothing, except the initialization of an OpenGL context, and can be ported very easy for the other platforms. Blitter is implemented as a 2D renderer engine of cached drawing commands. It have a recolor palettes cache, sprites cache as a huge 2D array texture atlas, and a pixel buffer for the damn SetPixel function (still used in the small map). It is a replacement for the software blitters, and designed to reproduce exactly the same 2D output, but with hardware acceleration for drawing, filtering, scrolling, blitting, filling, palette animation, etc. with services required for the 3D engine. It works 5-10 times faster than the standard SSE blitter on my hardware.
Its completed and can be used standalone, without a 3D viewport renderer. But it’s not supporting screenshot functions (lazy to add it). I’ve made some very tiny changes to the blitter interface, to make it much more hardware friendly. Recolor pointer replaced with a palette id, to allow hardware caching and operations (text recolor data stored now in the PaletteID itself). Checker rects drawing was moved from gfx, directly inside the blitter. Added a Flush/Finish functions, to separate mouse and network chat drawing from everything else, removing bad need to save screen blocks under a cursor and a chat window.
3D viewport renderer, works as a call interceptor. It declares the global _draw3d flag, and some important drawing functions, are redirected to the 3d drawing subroutines. Not a lot, just few. Switching off this flag, simply reverts us to the default 2.5D renderer on fly.
First of all, the 3D viewports overrides a dirty block system, with dirty rect system, to allow the GUI still be redrawn infrequently, with a full update of each pixel in each frame at the viewports. It uses stencil masks, built from the screen scape dirty rects binary tree (created with GUI invalidation calls and overlapped regions information), to select the screen areas for the viewport redraw, and manages top GUI redraw calls. This logic is concentrated in the gfx3d source file.
Second thing, is a tiny modification of the OTTD sprite cache, to store GRFID with other information about the sprites. This allow to identify each sprite uniquely across all of the existing sprites in the base set, and an external NewGRF files.
Actual 3D viewport renderer, has a config loader, for linking sprites to resources, a resource manager, to load and cache that resources, and a landscape data structure, to store its state and drawing caches. It intercepts viewport invalidation calls, marking tiles in the internal structure as dirty, instead of marking dirty the screen blocks. In the next frame, it calls a normal tile drawing functions, like the 2D viewport doing this, for each dirty tile, intercepting draw requests from this functions, and resolving sprite links and resources, making actual drawing lists for each tile. It caches nearly everything at this step, leaving for a frame update only an instansing buffer batching for the visible area, and a few OpenGL draw calls.
Resources:
The system has two types of resources. Images and models.
Image, is a PNG image, storing RGB color data, and a palette index in the alpha channel. Index 0 used for opacity (0 = completely transparent). Index 255, used as a selector for RGB color stored in the image. Other indexes, are used for fetching recolor palettes or a global animation palettes directly, if a recolor palette is not specified for the drawing. So, it’s possible to use a pixel color animation and a recolor mechanics for the 3D rendering, exactly like in 2.5D. Alpha blending is not supported, but it’s actually not required, with a transparent flag, recolor mapping and multisampling + ALPHA_TO_COVERAGE function. Images are stored in a 2D array atlas with blocks of 32x32 pixels, so it’s strongly recommended to use a power of two image dimensions, larger or equal to 32.
Model, is a human readable wavefront .obj file in its simplest implementation. Vertex positions, texture coords, normals, faces. That’s all. A lot of existing tools to work with.
Images and models are linked to the sprites by the config files. I am used existing OTTD ini file infrastructure. Config manager scans NewGRF directory for this ini files recursively, and trying to load all of this, at each startup. Files are consists of object definitions, and a sprites linkage. I’ve made a spec for this files (see below), but briefly: Object is a model + image + transform. Sprite linkage is a GRFID + SpriteID\InternalID + optional PaletteID + optional image for the landscape tile with texture coords + optional links to the objects.
Each landscape tile consists of the two triangles, with only one image on each. Yes, this is a limitation, OTTD can draw the landscape overlays for tracks, etc, but this can be implemented by the 3D objects, as a textured planes in a worst case. Generally, only one image can be applied to each tile, but for the shore halftiles, it is separated internally, assigning different images for both triangles, with respect to drawn sprites. Each tile has an additional layer for the selection sprites, that handled separately without lighting and shadows. Selection sprites are linked exactly the same as the other things. Moreover, they can be replaced with 3D objects (like for the terraforming dot). Each tile have a list of the drawn objects, created at the tile validation step. Vehicle sprites, are linked in the same way, as a tile objects.
One of the things that we should change while going from 2.5D to 3D, is a Z scale value. Proper orthogonal projection of the original view, result a tile height of 3.26598620 units, with 16 units of the tile side. Getting future, we can define a default vehicle size (width and height) as 2.82842712 units (8 px). And everything was ok, except catenary, added to the original game. So, instead of 3.26598620 units for the height scale in 3D, to get 1:1 projection with the original 2.5D render, 4.0 units scale was chosen. I want to stay as close as possible to the original look, so 4.0 is not too high, enough for catenary, and easy to use in the model editors. It's exactly half of the original Z scale, that is 8 units height, and makes things simpler.
An example of the config to link the bridge slopes drawing:
Code: Select all
[BRIDGE_SLOPE_SE]
model = base/bridge_slope.obj
image = base/bridge.png
trans = 8 8 0
rotate = 0 0 1 180
trans = -8 -8 0
[SPR_BRIDGE_SLOPE_SE]
grfid = BASE
sprite = 2440:2:8, 4:2:4329, 4368, 4328
object = BRIDGE_SLOPE_SE
SPR_BRIDGE_SLOPE_SE is a sprite linkage for all bridges SE slopes at once (for the test). grfid is an ID of the GRF file, from where the sprite can came on. sprite, is a list of the sprite ids for this entry. Actual sprite ids are used, because this is a base set (for the new grf sets, file internal sprite ids should be used with ident keyword, if it is not a replacement for the base set). So the definition is: link this entry to 2 sprites, from sprite 2440 with step 8, etc. and draw object BRIDGE_SLOPE_SE in 3D for this tile, if the OTTD wants to draw any of this sprites.
Another example of linking the landscape tile image:
Code: Select all
[SPR_ROAD_SLOPE_X]
grfid = 0x42415345
sprite = 1343:2:2
land_image = base/SPR_ROAD_X.png
land_coord = [ 0.5 0.2421875 ] [ 0.0 0.4921875 ] [ 0.5 0.7421875 ] [ 1.0 0.4921875 ]
Where to get GRFID, FileID, SpriteID and PaletteID for the config files? Tile info tool was modified (actually just current cursor image checked), to show you this info. Selecting any tile with tile info tool, will show you a full drawing stack for this tile in a format GRFID:FileID SpriteID:PaletteID with an optional T postfix, for the transparent sprites. It also shows drawing info for the visible vehicles.
Config file spec:
Sources: Windows binaries and a test set: Should be launched with -v win32ogl