The Quake engine contains many optimizations that reduce memory and processing requirements to allow it to run acceptably on the hardware available when it was first written. One such optimization is how lightnormal vectors are stored in the mdl format. Instead of storing 3 floats per vertex, and calculating the dotproduct of that vector every frame, the lightnormal is stored as a byte value that corresponds to a lookup table of precalculated dotproducts. For this lookup table to be efficient, an entity's angle is quantized to one of 16 angles. This means the lighting on a model only changes every 22.5 degrees (360/16). Fire up Quake and stand in place while rotating, keeping a close eye on the veiwmodel. You should notice the lighting changes 16 times during a full rotation (the rocket launcher works well to illustrate this). In this tutorial, we'll interpolate (or lerp) the light values in-between those quantized angles to achieve smooth light level transitions. We'll then go on to smooth out some flickering caused by coarse world lighting. PART 1: Get the quantized angles that the true angle falls between, and figure out where it falls In gl_rmain.c, find this line in the 'Alias Models' section: float *shadedots = r_avertexnormal_dots[0]; We need to initialize a couple variables right after that line: // light lerping - [email protected] float *shadedots2 = r_avertexnormal_dots[0]; float lightlerpoffset; Now, in the R_DrawAliasModel routine, find the following line: shadedots = r_avertexnormal_dots[((int)(e->angles[1] * (SHADEDOT_QUANT / 360.0))) & (SHADEDOT_QUANT - 1)]; // light lerping - [email protected] //shadedots = r_avertexnormal_dots[((int)(e->angles[1] * (SHADEDOT_QUANT / 360.0))) & (SHADEDOT_QUANT - 1)]; { float ang_ceil, ang_floor; // add pitch angle so lighting changes when looking up/down (mainly for viewmodel) lightlerpoffset = (e->angles[1]+e->angles[0]) * (SHADEDOT_QUANT / 360.0); ang_ceil = ceil(lightlerpoffset); ang_floor = floor(lightlerpoffset); lightlerpoffset = ang_ceil - lightlerpoffset; shadedots = r_avertexnormal_dots[(int)ang_ceil & (SHADEDOT_QUANT - 1)]; shadedots2 = r_avertexnormal_dots[(int)ang_floor & (SHADEDOT_QUANT - 1)]; } I'll try to explain what we're doing here: The first line calculates the first part of the original equation but stores the result as a float instead of casting it to int. (NOTE: lightlerpoffset is just a convenient variable to temporarily hold this value, it's real value is calculated a few lines down). One change from the original equation is that the pitch angle (e->angles[0]) is also added in. This makes the lighting change on the viewmodel when looking up / down and is more realistic IMHO. Monsters don't usually change in pitch so this has no affect on them, but player models in deathmatch (including some bots), and projectiles (gibs and the like), will also benefit from this. We then round the resulting number up and then down, these will give us the values of the two quantized angles below and above the true angle. The next line sets lightlerpoffset to a value between 0 and 1 that we'll later use to determine how much light needs to be added or subtracted to get the interpolated value.
d[0] = shadedots[verts2->lightnormalindex] - shadedots[verts1->lightnormalindex]; l = shadelight * (shadedots[verts1->lightnormalindex] + (blend * d[0])); // light lerping - [email protected] // d[0] = shadedots[verts2->lightnormalindex] - shadedots[verts1->lightnormalindex]; // l = shadelight * (shadedots[verts1->lightnormalindex] + (blend * d[0])); { float d2, l1, l2, diff; d[0] = shadedots[verts2->lightnormalindex] - shadedots[verts1->lightnormalindex]; d2 = shadedots2[verts2->lightnormalindex] - shadedots2[verts1->lightnormalindex]; l1 = shadelight * (shadedots[verts1->lightnormalindex] + (blend * d[0])); l2 = shadelight * (shadedots2[verts1->lightnormalindex] + (blend * d2)); if (l1 != l2) { if (l1 > l2) { diff = l1 - l2; diff *= lightlerpoffset; l = l1 - diff; } else { diff = l2 - l1; diff *= lightlerpoffset; l = l1 + diff; } } else { l = l1; } } If you haven't applied animation interpolation, or still support the original routine, find the following line in the GL_DrawAliasFrame routine; l = shadedots[verts->lightnormalindex] * shadelight; // light lerping - [email protected] // l = shadedots[verts->lightnormalindex] * shadelight; { float l1, l2, diff; l1 = shadedots[verts->lightnormalindex] * shadelight; l2 = shadedots2[verts->lightnormalindex] * shadelight; if (l1 != l2) { if (l1 > l2) { diff = l1 - l2; diff *= lightlerpoffset; l = l1 - diff; } else { diff = l2 - l1; diff *= lightlerpoffset; l = l1 + diff; } } else { l = l1; } } That's it for interpolating between the quantized angles
(compile and test it out if you like), now we'll smooth out the effects
of world lighting on alias models by averaging the overall light level
between frames.
PART 3: Averaging the world lighting effect to reduce flickering
// light lerping - [email protected] float last_shadelight; Open up gl_rmain.c once again and back in the R_DrawAliasModel routine, find the following line (should be just below the additions made earlier) shadelight = shadelight / 200.0; // light lerping - [email protected] shadelight = (shadelight + currententity->last_shadelight)/2; currententity->last_shadelight = shadelight; This is really simple, we just get the average shadelight value from the last frame to the current frame. This allows for subtle transitions in lighting to be smoothed out to greatly reduce flickering when moving, but still allows harsh transitions to occur (like when near a flickering light). The only problem with this is that last_shadelight is zero for the first frame, but I can live with that if you can ;-)
PART 4 (Optional): Increasing the light contrast
// light contrast - [email protected] l *= l; That's it! If you find any bugs or have any refinements for this tutorial, please let me know. |