Tutorial *97*

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 - pox@planetquake.com

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)];

Replace that line with the following block of code:


// light lerping - pox@planetquake.com

//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)];

}

NOTE: I'll tend to use curly quotes to isolated additions made to the source so associated variables can be easily added/removed - you can put the variable definitions in the routine's header with the rest of the variables if you want.

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.



PART 2: Adjust the light value using the information gathered in PART 1
This next bit assumes you have completed the animation lerping tutorial by Phoenix from QER skip to the next section if you haven't (though you really should add frame interpolation!)
Still in gl_rmain.c go to the GL_DrawAliasBlendedFrame routine and find these lines;



d[0] = shadedots[verts2->lightnormalindex] - shadedots[verts1->lightnormalindex];

l = shadelight * (shadedots[verts1->lightnormalindex] + (blend * d[0]));

Replace both those lines with the following;


// light lerping - pox@planetquake.com

// 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;

Replace that line with the following;


// light lerping - pox@planetquake.com

// 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;

   }



}

In both cases, we start off by calculating the light level once for each of the quantized angles that the entity's actual angle falls between. Next, we check if the values are equal, in which case no further calculation is necessary, but if they are different (which is almost always the case) we need to decide how to adjust light level. To do this, we need to multiply the previously determined lightlerpoffset by the difference in light levels between the two angles (essentially getting a percentage of the light level difference). We need to check which variable (l1 or l2) is larger to accomplish this. Once we know which value is bigger, we'll also know whether to add or subtract the percentage of light (diff) from l1 to get the interpolated light level (l) NOTE: you can just as easily use l2 to calculate the interpolated light value, I just kept it like this to be consistent.

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
In render.h, find the entity_t struct and add this below the last variable definition;



// light lerping - pox@planetquake.com

float    last_shadelight;

This will store the last shadelight value encountered by each entity.
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;

Add the following right below that line;


// light lerping - pox@planetquake.com

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
This next part is totally optional, but I find it gives models more roundness, definition & specularity - Basically we're going to increase the lighting contrast. Still in gl_rmain.c, in GL_DrawAliasFrame and/or GL_DrawAliasBlendedFrame, right after the big block of code we added 4 code blocks up, insert this;



// light contrast - pox@planetquake.com

l *= l;

Pretty tough eh? Since the actual normal vectors are unknown, we'd have a hard time computing true specular lighting, but this little hack does a nice job, albeit limited. To deal with the increased contrast, you'll probably want to change the 200.0 in the line 3 code blocks above to something closer to 100.0 - Play with the value, to see what suits your tastes (assigning it to the temp1 cvar is a quick way to test it without re-compiling).

That's it! If you find any bugs or have any refinements for this tutorial, please let me know.


 
Sign up
Login:
Passwd:
[Remember Me]