Merging Process: CGame Lighting Control

Insight into the Q3/RTCW/ET/EF/JK2/JA/etc engine merging process for Spearmint.

Not feeling quite ready to take on Spearmint issue 135, I decided to add support for Elite Force’s RF_FULLBRIGHT (used by holodeck door at level start). I quickly decided to generalize cgame’s control of ambient light.

For various effects, developers of id Tech 3 games wanted to explicitly increase or set the brightness for a model.

Ambient light

In Quake 3, there is a RF_MINLIGHT bit flag that does nothing, it is set for some of the model draws though. Instead renderer always add “tr.identityLight * 32” to ambient light. (tr.identityLight is used to scale values to be the same regardless of overbright bits aka screen gamma.)

The check for RF_MINLIGHT is enabled in RTCW/ET (making them incompatible with Q3) and set RF_MINLIGHT on more types of effects.

RTCW/ET added RF_HILIGHT that does nothing (probably did at one point in time). Instead “float hilightIntensity” in refEntity_t is used (range 0.0 to 1.0, it scales “tr.identityLight * 128 * hilightIntensity”). hilightIntensity = 0.25 fully replaces the need for RF_MINLIGHT, I guess they didn’t feel like remove it though.

In RTCW/ET when in “snooper view” (sniper view?) client models (entityNum < MAX_CLIENTS) use ambient light “245”. The code for this is ugly, but only 2 lines. They added entityNum to refEntity_t which is pure evil (???), the renderer isn’t suppose to need to know what game entity numbers are used for models. The 245 is NOT scaled using tr.identityLight, probably a bug? I think the brightness will change based on overbright cvar settings while everything else looks the same (well, it’s suppose to look the same?).

Elite Force added RF_FULLBRIGHT to force ambient light to “tr.identityLight * 0x7F”.

JK2/JA SP added RF_MORELIGHT to add “tr.identityLight * 96”, otherwise adds “tr.identityLight * 32” like Q3.

JK2/JA MP added code for RF_MINLIGHT that set ambientLight RGB to different values based on shaderRGBA, but still always adds “tr.identityLight * 32” like Q3. See here. It’s was validating to find this. I had already decided refEntity_t should have separate values for ambient red, green, and blue because someone was bound to want it.

Other id Tech 3 games (besides Q3/RTCW/ET/EF/JK2/JA) may have other methods, but essentially we want to tell renderer to add to or set ambient light value.

Basically if we simplify all of this we can add “byte ambientLight[3]” to refEntity_t, add a new bit flag, and do the following.

// calculate ent->ambientLight, ent->directedLight,
// and ent->lightDir using level's light grid

if ( ent->e.renderfx & RF_CONST_AMBIENT )
    VectorClear( ent->ambientLight );

ent->ambientLight[0]+=tr.identityLight * ent->e.ambientLight[0];
ent->ambientLight[1]+=tr.identityLight * ent->e.ambientLight[1];
ent->ambientLight[2]+=tr.identityLight * ent->e.ambientLight[2];

Simple, right? Just welding code on top of the existing API is a bad idea. EF at least makes some sense. I think Quake 1/2 have RF_FULLBRIGHT.

Directed light

The final model light is ambient light plus directed light times the dot product of lightdir and vertex normal or something like that.

Elite Force’s RF_FULLBRIGHT disables directed light. Whereas RTCW/ET snooper view mode has a comment that says “allow a little room for flicker from directed light”. So we cannot assume these is a connection between setting ambient light and what to do with directed light.

So will need an additional RF_* bit flag for disabling directed light for Elite Force.

if ( ent->e.renderfx & RE_NO_DIRECTED_LIGHT ) {
    VectorClear( directedLight );
    VectorClear( lightDir );

What else could we want?

The only other way I think people could want to change about ambient light is to decrease by a set value. It would be possible in RTCW/ET by using negative value for hilightIntesity. So instead of storing ambientLight in a byte, use float instead.

We could use a short, but id Tech 3 pretty much never uses shorts. It doesn’t need the full int range and hilightIntesity is a float, so it’s probably best to use floats.

Well, could want to multiply or divide ambient light maybe. I’m not going to handle that though…


Add “float ambientLight[3];” to refEntity_t. Then in the rendering code if RF_CONST_AMBIENT is set, use values from refEntity_t::ambientLight; else add values from refEntity_t::ambientLight. (Of course we’ll multiply the values times tr.identityLight so it doesn’t get screwed up when changing overbright bits cvars.)

In the VM code…

// Q3
ent.ambientLight[0] = 32;
ent.ambientLight[1] = 32;
ent.ambientLight[2] = 32;
if ( snooperView && entityNum < MAX_CLIENTS ) {
 ent.renderfx |= RF_CONST_AMBIENT;
 ent.ambientLight[0] = 245;
 ent.ambientLight[1] = 245;
 ent.ambientLight[2] = 245;
} else if ( hilightIntesity ) {
 ent.renderfx |= RF_CONST_AMBIENT;
 ent.ambientLight[0] = 128 * hilightIntesity;
 ent.ambientLight[1] = 128 * hilightIntesity;
 ent.ambientLight[2] = 128 * hilightIntesity;
} else if ( minLight ) {
 ent.ambientLight[0] = 32;
 ent.ambientLight[1] = 32;
 ent.ambientLight[2] = 32;
} else {
 ent.ambientLight[0] = 0;
 ent.ambientLight[1] = 0;
 ent.ambientLight[2] = 0;
// Elite Force
if ( fullBright ) {
 ent.ambientLight[0] = 0x7F;
 ent.ambientLight[1] = 0x7F;
 ent.ambientLight[2] = 0x7F;
} else {
 ent.ambientLight[0] = 32;
 ent.ambientLight[1] = 32;
 ent.ambientLight[2] = 32;
// JK2 / JA SP
if ( moreLight ) {
 ent.ambientLight[0] = 96;
 ent.ambientLight[1] = 96;
 ent.ambientLight[2] = 96;
} else {
 ent.ambientLight[0] = 32;
 ent.ambientLight[1] = 32;
 ent.ambientLight[2] = 32;
// JK2 / JA MP
ent.ambientLight[0] = 32;
ent.ambientLight[1] = 32;
ent.ambientLight[2] = 32;
if ( minLight ) {
 if ( ent.shaderRGBA[0] == 255 &&
      ent.shaderRGBA[1] == 255 &&
      ent.shaderRGBA[2] == 0 ) {
  ent.ambientLight[0] += 255;
  ent.ambientLight[1] += 255;
  ent.ambientLight[2] += 0;
 } else {
  ent.ambientLight[0] += 16;
  ent.ambientLight[1] += 96;
  ent.ambientLight[2] += 150;

Boom. Now there is a sane way to set/increase/decrease ambient light level (with separate RGB) and disable directed light.

Applying the changes to cgame probably won’t exactly be straightforward. Putting it all into one function that we call for every single refEntity would be a pain. It would either need a list of booleans or a new list of bit flags to pass to function. Instead should probably add special case code where it’s actually used, which will be a lot more flexible long term.

Adding support for RF_FULLBRIGHT that had 11 lines of code turned into 228 lines changed.