|
One thing that I've always felt was missing from Quake, was the ambience that
coloured lighting provides. This effect was in Quake2 and I feel that it went
a long way to adding to the atmosphere of the game. This tutorial goes part
way to creating that effect with the GLQuake engine. It will only change the
colour of the dynamic lights and does not affect the static lights yet.
Aside: The GL_FLASHBLEND cvar controls how dynamic lights are rendered in GLQuake.
GL_FLASHBLEND 1 - The default. This creates a sphere around the light
that does not interact with the environment and doesn't
look very good in my opinion.
GL_FLASHBLEND 0 - This renders the lights like in normal Quake. The dynamic
light lights up the surfaces around it. Much better if
you ask me.
Part 1: Controlling Coloured Lighting
Part 1 is a discussion on how to control the coloured lights, whilst
Parts 2 & 3 start to implement the changes. If you just want to see GLQuake
with blue explosions skip Part 1.
Part 1: Controlling Coloured Lighting Before doing the actual coding, we must know how to implement the change of colour within the engine. Here are 2 possibilities -
1. From the engine itself, setting a constant colour for each type of
dynamic light.
e.g. in cl_tent.c in CL_ParseTEnt:
after dl->decay = 300; under case TE_EXPLOSION:
you could add something like
dl->colour[0] = 0.8; dl->colour[1] = 0.2; dl->colour[2] = 0;
This way every TE_EXPLOSION in the game would be red.
2. From the QuakeC code, setting the colour of the next dynamic light,
before spawning it. Here is a possible way of doing this -
Write the colour of the explosion like you write the coordinates
just after you create the explosion
e.g. in shalrath.qc in void() ShalMissileTouch:
after :
WriteCoord (MSG_BROADCAST, self.origin_x);
WriteCoord (MSG_BROADCAST, self.origin_y);
WriteCoord (MSG_BROADCAST, self.origin_z);
add :
// Write the colour of the explosion
WriteCoord (MSG_BROADCAST, 0.2);
WriteCoord (MSG_BROADCAST, 0.2);
WriteCoord (MSG_BROADCAST, 0.8);
Then in cl_tent.c in CL_ParseTEnt:
after dl->decay = 300; under case TE_EXPLOSION:
you would add
dl->colour[0] = MSG_ReadCoord ();
dl->colour[1] = MSG_ReadCoord ();
dl->colour[2] = MSG_ReadCoord ();
Both of these ways have their disadvantages, setting the colour from the engine, stops people from defining their own custom colour explosions without the source code for GLQuake and a way of compiling it. Whilst the way I have described of changing it from QuakeC will break old progs.dat and demo files because the engine will be expecting a colour of the explosion that the older files just will not provide. Anyone with a better idea of how to control these things should contact me at epca@powerup.com.au For the purposes of this tutorial, I will use the first method for simplicity. It is up to you how you implement it in your engine, although a standard, like for the fog would be good so that all engines will be compatible. Now on to the coding. Part 2: GL_FLASHBLEND 1 The first thing that needs to be done is to add a way for each dynamic light to track what colour it currently is. In OpenGL, colours are defined by an array of floats. So we need to add that array to the declaration of dlight_t in client.h After :
#ifdef QUAKE2
qboolean dark; // subtracts light instead of adding
#endif
// CDL - epca@powerup.com.au
// Add a colour to the dynamic lights
float colour[3];
// CDL
Note how I have commented where I have begun and completed making changes to the code, so that if I make a mistake, I can easily remove all code relating to the mistake and start again. Now that we have a way to track the colour of the lights, we must do something about changing the colour. So in cl_tent.c in CL_ParseTEnt After :
case TE_EXPLOSION: // rocket explosion
pos[0] = MSG_ReadCoord ();
pos[1] = MSG_ReadCoord ();
pos[2] = MSG_ReadCoord ();
R_ParticleExplosion (pos);
dl = CL_AllocDlight (0);
VectorCopy (pos, dl->origin);
dl->radius = 350;
dl->die = cl.time + 0.5;
dl->decay = 300;
// CDL - epca@powerup.com.au
// Make the dynamic light purple
dl->colour[0] = 0.2; dl->colour[1] = 0.2; dl->colour[2] = 0.8;
// CDL
Now that we have a coloured light, we must render it in the right colour. This is simple, when you realise that dynamic lights are rendered from R_RenderDLight in gl_rlight.c. So in R_RenderDLight in gl_rlight.c Change : glColor3f (0.2,0.1,0.0);
// CDL - epca@powerup.com.au
if (light->colour[0] || light->colour[1] || light->colour[2])
glColor3f (light->colour[0], light->colour[1], light->colour[2]);
else
glColor3f (0.2,0.1,0.0);
// CDL
Compile and run it. You will now see a purple light surround all of your explosions. Notice however, that if you change GL_FLASHBLEND to 0, that you lose the coloured dynamic light that you just created. To fix this, move on to the next part in the tutorial. Part 3: GL_FLASHBLEND 0 In this section of the tutorial, we will be modifying lightmaps. Lightmaps tell the engine how much light is in a specified area, and dynamic lights modify these lightmaps to interact with the environment. There is a problem with the Quake implementation of lightmaps, in that they are only luminense lightmaps, and only tell the engine how light or dark an area is, and not in what colours. Luckily for us, the people at ID gave us the option to use RGBA (coloured) lightmaps, by add -lm_4 to the command line. However, I don't think they tested this mode, because if you run GLQuake with this parameter, the level is just fullbright. To fix this, go into gl_rsurf.c in After :
case GL_RGBA:
stride -= (smax<<2);
bl = blocklights;
for (i=0 ; i
// epca@powerup.com.au
// Fix bug in quake where this mode is just fullbright
dest[0] = dest[1] = dest[2] = 255-t;
// End of fix
This will return the lightmap back to its intended form whilst using RGBA. Back to coloured lighting. In gl_rsurf.c in the declarations at the top After : unsigned blocklights[18*18];
// CDL
unsigned blocklightcolours[3][18*18];
// CDL
Now we have a way to store the changed colours, we must do something about changing them. Dynamic lights are added to the lightmap in the function R_AddDynamicLights in gl_rsurf.c. Compare the following to that function and once you see what is going on, replace it.
void R_AddDynamicLights (msurface_t *surf)
{
int lnum;
int sd, td;
float dist, rad, minlight;
vec3_t impact, local;
int s, t;
int i;
int smax, tmax;
mtexinfo_t *tex;
// CDL - epca@powerup.com.au
dlight_t *dl;
// CDL
smax = (surf->extents[0]>>4)+1;
tmax = (surf->extents[1]>>4)+1;
tex = surf->texinfo;
for (lnum=0 ; lnum<MAX_DLIGHTS ; lnum++)
{
if ( !(surf->dlightbits & (1<<lnum) ) )
continue; // not lit by this light
rad = cl_dlights[lnum].radius;
dist = DotProduct (cl_dlights[lnum].origin, surf->plane->normal) - surf->plane->dist;
rad -= fabs(dist);
minlight = cl_dlights[lnum].minlight;
if (rad < minlight)
continue;
minlight = rad - minlight;
for (i=0 ; i<3 ; i++) {
impact[i] = cl_dlights[lnum].origin[i] - surf->plane->normal[i]*dist;
}
local[0] = DotProduct (impact, tex->vecs[0]) + tex->vecs[0][3];
local[1] = DotProduct (impact, tex->vecs[1]) + tex->vecs[1][3];
local[0] -= surf->texturemins[0];
local[1] -= surf->texturemins[1];
for (t = 0 ; t<tmax ; t++) {
td = local[1] - t*16;
if (td < 0)
td = -td;
for (s=0 ; s<smax ; s++) {
sd = local[0] - s*16;
if (sd < 0)
sd = -sd;
if (sd > td)
dist = sd + (td>>1);
else
dist = td + (sd>>1);
if (dist < minlight) {
blocklights[t*smax + s] += (rad - dist)*256;
// CDL - epca@powerup.com.au
dl = &cl_dlights[lnum];
if (!dl->colour[0] && !dl->colour[1] && !dl->colour[2]) {
blocklightcolours[0][t*smax + s] += (rad - dist)*(0.2*256);
blocklightcolours[1][t*smax + s] += (rad - dist)*(0.1*256);
blocklightcolours[2][t*smax + s] += (rad - dist)*(0.0*256);
} else {
blocklightcolours[0][t*smax + s] += (rad - dist)*(dl->colour[0]*256);
blocklightcolours[1][t*smax + s] += (rad - dist)*(dl->colour[1]*256);
blocklightcolours[2][t*smax + s] += (rad - dist)*(dl->colour[2]*256);
}
// CDL
}
}
}
}
}
If you look at the sections that I have changed, you will see that I am simply adding the value of the colour of the light onto the colours that will be used later. The reason that the colours are multiplied by 256 is that I have stored each colour value between 0 and 1, and the GLQuake engine adds the colours between 0 and 256. If you compile and run at this stage, you will notice that the coloured lighting still isn't working, that is because all we have done is calculate the values to be used and not used them. To use them, we must modify R_BuildLightMaps in gl_rsurf.c. Compare that version to the version below and then replace it.
void R_BuildLightMap (msurface_t *surf, byte *dest, int stride)
{
int smax, tmax;
int t, r, s, q;
int i, j, size;
byte *lightmap;
unsigned scale;
int maps;
int lightadj[4];
unsigned *bl;
// CDL - epca@powerup.com.au
unsigned *blcr, *blcg, *blcb;
// CDL
surf->cached_dlight = (surf->dlightframe == r_framecount);
smax = (surf->extents[0]>>4)+1;
tmax = (surf->extents[1]>>4)+1;
size = smax*tmax;
lightmap = surf->samples;
// set to full bright if no light data
if (r_fullbright.value || !cl.worldmodel->lightdata)
{
for (i=0 ; i<size ; i++)
{
// CDL - epca@powerup.com.au
blocklightcolours[0][i] =
blocklightcolours[1][i] =
blocklightcolours[2][i] =
// CDL
blocklights[i] = 255*256;
}
goto store;
}
// clear to no light
for (i=0 ; i<size ; i++)
{
// CDL - epca@powerup.com.au
blocklightcolours[0][i] =
blocklightcolours[1][i] =
blocklightcolours[2][i] =
// CDL
blocklights[i] = 0;
}
// add all the lightmaps
if (lightmap)
for (maps = 0 ; maps < MAXLIGHTMAPS && surf->styles[maps] != 255 ; maps++)
{
scale = d_lightstylevalue[surf->styles[maps]];
surf->cached_light[maps] = scale; // 8.8 fraction
for (i=0 ; i<size ; i++)
{
// CDL - epca@powerup.com.au
blocklightcolours[0][i] += lightmap[i] * scale;
blocklightcolours[1][i] += lightmap[i] * scale;
blocklightcolours[2][i] += lightmap[i] * scale;
// CDL
blocklights[i] += lightmap[i] * scale;
}
lightmap += size; // skip to next lightmap
}
// add all the dynamic lights
if (surf->dlightframe == r_framecount)
R_AddDynamicLights (surf);
// bound, invert, and shift
store:
switch (gl_lightmap_format)
{
case GL_RGBA:
stride -= (smax<<2);
bl = blocklights;
// CDL - epca@powerup.com.au
blcr = blocklightcolours[0];
blcg = blocklightcolours[1];
blcb = blocklightcolours[2];
// CDL
for (i=0 ; i<tmax ; i++, dest += stride)
{
for (j=0 ; j<smax ; j++)
{
// CDL - epca@powerup.com.au
q = *blcr++; q >>= 7;
if (q > 255) q = 255;
r = *blcg++; r >>= 7;
if (r > 255) r = 255;
s = *blcb++; s >>= 7;
if (s > 255) s = 255;
dest[0] = 255-q;
dest[1] = 255-r;
dest[2] = 255-s;
// CDL
t = *bl++;
t >>= 7;
if (t > 255)
t = 255;
// epca@powerup.com.au
// Fix bug in quake where this mode is just fullbright
//dest[0] = dest[1] = dest[2] = 255-t;
// End of fix
dest[3] = 255-t;
dest += 4;
}
}
break;
case GL_ALPHA:
case GL_LUMINANCE:
case GL_INTENSITY:
bl = blocklights;
for (i=0 ; i<tmax ; i++, dest += stride)
{
for (j=0 ; j<smax ; j++)
{
t = *bl++;
t >>= 7;
if (t > 255)
t = 255;
dest[j] = 255-t;
}
}
break;
default:
Sys_Error ("Bad lightmap format");
}
}
Compile and run, and you will be looking at blue explosions! Conclusion I hope that this tutorial has helped you understand a bit more about the GLQuake Engine, and I hope to see all sorts of coloured lighting in the future. Any questions, comments, corrections? I would love to hear them. Contact me at epca@powerup.com.au |