4.11.1 Lighting

Crystal Space supports three types of lighting: static,
pseudo-dynamic and dynamic. They are different in the ways you
can change their attributes while the engine is running. Static lights cannot
be changed at all, but produce best performance and least memory consumption.
Pseudo-dynamic lights can change their color and brightness, but not their
position. Dynamic lights can also change their position.

In the interest of accuracy, it is actually possible to change the so-called
“unchangeable” attributes of static and pseudo-dynamic lights, but after
doing so, it is necessary to perform time-consuming recalculation of lightmaps
in order for the changes to be reflected in the rendered world.

Every light has a position in world space coordinates and a current
sector. Every light also has a radius (expressed in squared distance).
This radius indicates where the light levels of the three light tables will
reach zero.

Static Lights

This section gives a very brief explanation of the static lighting system used
in Crystal Space. Warning: Although the basic principle
described here is still valid, much more is going on now. This document needs
to be updated to reflect the current state of the engine.

Static lighting is based on lightmaps. These are like textures except
that every pixel defines the combined brightness of all lights at its
position. Before the rendering loop starts, these lightmaps are precalculated
so there is little performance loss at runtime, where we only need to blend
texture and lightmap of a polygon.

Lightmaps are calculated as follows:

First initialize all lightmaps to black (or the ambient color).

For every light in the world generate a view frustum as seen from the position
of that light. The frustum is defined as a set of 3D polygon or light
patches. Every light patch is clipped against the relevant portals until
nothing remains (light patch becomes empty). Every light patch in this
frustum is in fact a projection of the light on the normal polygons. It is in
that area that we need to update the lightmap for every polygon.

For every polygon that was hit, cast beams from the light to every lightmap
point on the polygon (lightmap points are often defined every 16 by 16 texels
although you can change this value if desired). We know that in principle the
beam of light should reach the polygon since the light patch gives the
boundary of where the light can reach the polygon. But the frustum was
generated ignoring Things. So it is possible that there is a Thing blocking a
specific beam of light. We need to test for that.

If there is a successful hit then calculate the distance between the light and
the point on the polygon and update the intensity there.

To create the three lightmaps for every polygon a 2D bounding box in texture
space is calculated. This defines a rectangle that overlaps with the texture
on the polygon and is correctly aligned with it (so that every 16 by 16 texel
grid has one lightmap position). A consequence of this is that rotated
textures can waste a lot of lightmap space. For example, see the following
polygon:

And assume that the texture is aligned horizontally. Then we would need a
lightmap of the following size:

Note also, that the texture in the texture cache will have that size.

Pseudo-Dynamic Lights

The pseudo-dynamic lighting system is an extension to the static lighting
system described above. Pseudo-dynamic lights can not move but you can change
the intensity or color. When some light is made pseudo-dynamic (a simple
flag) the processing is a bit different. The shadow-computation remains the
same but every polygon that is hit by the pseudo-dynamic light needs to have
separate lightmaps for every pseudo-dynamic light that hits the polygon. So
in the end all the polygons that are only hit by static lights have just one
lightmap (for the three tables) for all static lighting information. Every
polygon that is hit by one or more pseudo-dynamic lights will have one or more
extra lightmap tables for every pseudo-dynamic light and one extra for all
static lights. This information is then added together to result in the final
lightmap table that can then be used by the texture cache routine (as
explained earlier).

The lightmaps for the pseudo-dynamic lights are stored without the strength of
the light. So where the light shines brightest the value in the lightmap will
be 255, and where it shines least bright the value will be 0. When combining
all pseudo-dynamic lightmaps and the static lightmap the strength of every
light will be multiplied with the distance value to result in the real
lightmap value. This also implies that there is only one extra lightmap for
every polygon/pseudo-dynamic light because the shadow information (which is
basically what is represented in pseudo-dynamic lightmaps) is the same for all
three light-tables.

Here are some performance considerations for pseudo-dynamic lighting. There
is a memory penalty for every polygon that is hit by some pseudo-dynamic
lighting because extra lightmaps need to be created. So if you use no
pseudo-dynamic lighting you will suffer no penalty but if you use a very large
pseudo-dynamic light (with a large radius) it will probably hit a large number
of polygons and you will have a considerable increase in memory requirements.

There is also a performance penalty whenever the intensity of a pseudo-dynamic
light changes. This is because all the textures for the polygons that are hit
by the pseudo-dynamic light need to be recalculated (they are removed from the
texture cache).

If the intensity of a pseudo-dynamic light does not change there is no
performance hit at all.

Dynamic Lights

Crystal Space also supports true dynamic lights. These are rendered atop the
normal lightmaps as explained above and support colors and limited shadows.

Shadow Calculation

The algorithm to see if a given beam reaches a specific point on some polygon
works as follows. The beam is described as two vertices (start and
end):

Start at the “sector” of the light.

If the current sector is equal to the sector of the polygon then there is a
hit, since all polygons of a sector are completely visible from anywhere in a
sector.

Otherwise, if the light is not in this sector, the beam could reach this
sector through a portal. Since the beam certainly ends at the polygon (this
is given, the start and end vertices define a beam that will—if not
blocked—reach the polygon) and the beam passed through portals to reach here
then the beam hits the polygon. So perform the following steps:

Determine which polygon of the current sector intersects with the beam.

If the polygon is on the same plane as the destination polygon then there is a
hit. The reason this is true has to do with the fact that the lightmaps are
slightly bigger than what is really needed. Bilinear interpolation is used to
finally light the polygon and it has to be able to correctly interpolate at
the boundaries of the polygon as well.

If the polygon that is hit is a portal then recursively continue with the
following sector. If the recursive calls returns a hit then we have a hit.

If the polygon is not a portal there is no hit so return from the algorithm
(the beam of light does not reach the polygon).

In all cases where a hit occurred according to the previous steps, arrive at
this step to check if there are no Things blocking the beam. For every Thing
in the current sector check if the beam intersects with one of the polygons of
the Thing. If so then there is no hit. Otherwise there is a hit.