So I’m pretty close to being done with the lighting for DaggerXL. There is still some tweaking to be done but it is very minor at this point. Mostly I have to position the lights for different types of flats a bit differently and maybe tweak the animation and radius a bit more. The rendering for the OpenGL shader mode uses the refactored code from the old DaggerXL project, though tweaked for improved performance and to match the Daggerfall a bit better. However for this post I’ll be talking about the lighting for the software renderer.
Lighting in DaggerXL is rendered per-pixel, or close to it, for all lights even in the software renderer. The reason for this is simple, that was how it was handled in the original DOS game. Despite the numerous flaws with the X-Engine one of it’s coolest features was the real-time per-pixel lighting. Before Quake introduced lightmaps, lighting in software renderers was very simplistic at best. Games like Daggerfall were far too vast – especially in those days – for lightmaps to be practical. Per-vertex lighting, which was used primarily even with early GPU support, was also not very useful for Daggerfall due to the coarse nature of the geometry.
Obviously the number of lights per surface is very important to performance, so there are a couple of things done to keep this manageable:
When loading a dungeon block (or equivalent for other environments), any lights that are at nearly the same height are combined into a single light if their light volumes overlap enough. The position of the new “composite” light is simply the average of all the individual light positions. So, for example, if you have 3 torches right next to each other on one light is generated which would be on the center torch – or in the middle of the torches if they are arrayed in a triangular shape. This allows objects that generate lights, such as torches and candles, to be placed almost at-will without increasing the light count per-surface too much.
Here are a few examples:
3 torches in a line, one light is generated in the center. (8 bit software renderer, 1024×768)
Numerous candles in the rafters, this room actually looks much cleaner and runs much better with the light merging. All the candle lights are merged into a single light. (8 bit software renderer, 1024×768)
Aggressive Per-Polygon Culling and Light per-surface Limits
As suggested, the engine goes through a fair amount of work to cull lights aggressively to limit the number affecting a given surface. First lights that do not move are assigned to meshes during the load process. By doing this at load time the CPU doesn’t have to spend time doing this culling as the player moves around. When a mesh is rendered, this light list is sent to the low-level graphics system. As each polygon is being rendered only the lights from this list that may affect that polygon are used for the per-pixel lighting. The light sphere must overlap the polygon bounding sphere, the light must be above the polygon’s plane (otherwise it is behind the polygon and thus won’t have any visual affect anyway) and the true light to polygon distance must be within range to be affected by the light. From this final list of lights the 4 most likely to have the greatest impact are chosen to light the polygon. By limiting the maximum number of lights per-polygon, the worst case performance is known in advance.
Per-pixel Lighting in Software
It would probably be more accurate to call this per-span lighting, though it is very close to per-pixel. Anyway the relative world space position (i.e. world space – polygon vertex 0, in order to preserve precision with fixed point) is assigned as vertex attributes and is interpolated across the edges and per-span. Basically the rasterizer splits polygons into scanlines and each scanline is then split into Affine Spans, with proper perspective interpolation for the span edges. For resolutions 640×480 and smaller the Affine spans are 8 pixels in size or smaller (such as on edges). All the interpolaters are perspective correct at the span start and end position and are linearly interpolated in-between.
Anyway the relative world space position is perspective correct at the span ends and that is where the lighting is computed. A list of up to 4 lights is processed and then the result is modified by the fog, which is based on the Z distance – the same Z value used for z-buffering. For each light the linear attenuation is computed as well as the dotproduct of the polygon normal – which is a single value for Daggerfall since only polygon normals are used – and the normalized light direction. These values are then linearly interpolated across the spans. In order to make this fast, a table lookup is used. Since we use 16.16 fixed point, we know how much precision is needed and can thus use fast approximations that have sufficient precision.
320×200, 8 bit, software renderer
1024×768, 8 bit, software renderer