Software Rasterization School, Part 5: Lighting

From DmWiki

Originally contributed by:

Mihail Ivanchev aka Mihail121 (mihail1212@yahoo.co.uk)
Hans Törnqvist aka coelurus (http://coelurus.thorntwig.se/)

Table of contents

Opening words

Hello, dear readers, and welcome (right about time...) to the 5th tutorial of our software rasterization series! Of course, as we always do, we will begin with an apology because of the long delay between the current and the last tutorial. We've probably disappointed many of you but we are asking for a bit of understanding - life is difficult as you know and we simply can't sit around all day long writing tutorials. Of course, we could sit all night long, but living without sleep is a strategy we are not yet ready to master! Anyway, let's get to the point - the tutorial is finally here, so don't fear!

In this tutorial we're going to explain lighting. You will soon understand why lighting is so important in rendering and why we will waste a great amount of web-space to explain it in detail! You will learn what real-life lighting is, how to transform it to the quite limited computer-world, all the maths behind it and, of course, a lot of implementation tips and ideas for further research.

Just a tip: The earlier parts of the tutorial will cover what lighting is down to the nitty gritty details and can feel very heavy at first. The final result will be based on the first section but vastly simplified computationally. It still is a good idea to read and understand lighting theory in order to advance from this tutorial, otherwise you might get stuck thinking "I know how to compute the lighting for a polygon using the simple three lighting types, but how am I supposed to develop a photorealistic pre-processor with interreflections, subsurface scattering etc from those?". The final implementation in this tutorial is not lighting in the general sense, we only conclude with a simplified lighting-model suitable for realtime software rendering.

With that piece of advice in your mind... let's dive into it!

Real world lighting

In this section of the tutorial, you will learn what lighting really is and why you actually see things! We will give a short and simplified description of light from the viewpoint of physics. However we will not dig too deep into it, because we want to concentrate on the tutorial which is about software rendering, not physics theory! Note that this is not directly related to our implemented lighting model, but we recommend you read this section.

We begin our explanation by saying that light travels through space in the form of electro-magnetic (EM) waves (http://en.wikipedia.org/wiki/Electromagnetic_radiation). The theory behind the EM-waves is far beyond the scope of this tutorial but basic theory is normally taught in high-school so anybody older than 13 years should have some feeling about EM-waves. Even if you don't have this feeling yet or you've misplaced it, don't worry! All you need to know is that light travels as waves which spread throughout space.

Waves (http://en.wikipedia.org/wiki/Wave) can be described by a set of attributes; frequency, period, amplitude and length which are all shown in the diagam below:


Figure 1: Properties of a wave
Figure 1: Properties of a wave


The frequency of a wave gives the number of 'cycles' per second or the number of peaks/troughs (same difference) the wave reaches per second. The period of a wave equals the time for one full wave cycle. Using the frequency (equal to the inverse period) and the length of the wave, we can calculate the speed of the wave using this formula:


speed = length * frequency = length / period


The above holds true for any kind of waves. So the light-waves (the EM-waves) are described using exactly the same set of properties.

Now it's time for us to reveal that EM-waves can be chopped up into different types of waves. Light-waves or, as they can also be called, visible waves are a small subset of all EM-waves. Other subsets include microwaves, infrared waves, gamma waves and ultraviolet waves and more. The light-waves are the only subset of the EM-waves that the human eye can register and "see". The frequencies (lengths) of the different subsets lie in different ranges. If we order all subsets after their frequency or length, we get the so called "spectrum of the electromagnetic waves". It is shown in the diagram below:


Figure 2: Spectrum of EM-waves
Figure 2: Spectrum of EM-waves


The light-waves are the smallest subset in the spectrum. The length of the visible waves is in the range 400-750nm. Each color we know (red, green, blue, you name it) is, in fact, a light-wave with a given frequency/length. Red for example has a length of about 700 nm and purple 400 nm. There are also the so called composite colors that are not presented in the spectrum but appear, when light-waves with different frequencies interfere. Composite colors are for example brown and white. Black is not a color at all in the sense of light but an absence of light-rays reflecting from a surface. The last thing we will say about EM-waves is that they move through vacuum space with constant speed: 3 * 108 m/s.

Seeing is believing

Enough theory (yeah, we hate it too!) - let's explain why lighting is needed so much in computer rendering. Well, actually, an explanation like that would be a complete waste of time, storage space and typing power, because we are pretty sure that everybody realizes the important role of lighting in our world. But, for those of you who are not quite sure, quickly compare the following two screenies:


(a) unlit sphere
Enlarge
(a) unlit sphere
(b) lit sphere
Enlarge
(b) lit sphere


As you can see, we need light to actually "see" the world around us. Without light, we won't be able to see all those cool blond chicks... (Mihail!) Err, I mean things around us (you will understand how later in the article)! It's not like that in computer rendering, we can still render our objects with solid colors so that we see them but the lack of interaction of light will make the scenes look unrealistic! And now you have it - lighting is a key element for the realism of 3D rendered scenes! Observe the world around you! Take a perfectly red apple for example. You'll notice it's not equally red but colored in different shades of red. That's because the apple "is lit" and depending on the amount light that reaches different parts of the apple, you'll see it in different shades of its "primary color".

Now that we know why lighting is so important for our future rendering frenzy, let's discuss probably the most important question of them all! Namely, what does light have to do with seeing objects or how does light help us to see the world around us.

To answer those questions, we introduce light sources. Light sources are objects that emit light rays (EM-waves) with frequencies that are part of the visible subset of the EM-spectrum. Those light waves start traveling in space with the speed of light (3 * 108 m/s). If an object gets in their way, the light rays often split into two parts, the first part passes through the outer shell of the object and continues through the object. The second part reflects from the object. After billions, trillions or perhaps a few more reflections from random objects in space, the light-rays finally reach our eyes and gives us visual information about the surrounding world. So that's how we actually "see". If we enter a room without any light sources, it will appear completely dark, we won't see a thing in it. We hope that it's all clear by now how important the light rays are. Below we present a few diagrams that show the above described process:


Figure 3: Traveling lightrays
Figure 3: Traveling lightrays


Light, or rather "the lack of light", is the cause of shadows. Parts of objects (or perhaps whole objects) that don't get a lot of light appear dark or "shadowed" to us. We will discuss shadows in great detail in a future tutorial since they are also very important for simulating realism in our scenes (and obviously, they're tricky to implement with the lighting model we'll present in this tutorial).

Real world to computer

With all this theory, we need to think of a clever way to construct a good lighting system for our software renderer. This won't be an easy task since simulating the real-world lighting model in a real-time renderer would be an impossible task. There are other methods, such as ray-tracing that closely resembles the lighting theory explained so far and produces near real scenes, but with today's computers, ray-tracing are only on the verge on becoming realtime. Although a full ray-tracer is out of the question at the moment, ray-tracing algorithms used as scene pre-processing can be merged with real-time lighting. For now, we won't mess with this kind of hybrid solutions but if you're interested, check out the neat ray-tracing tutorials here at DevMaster.

Let's get back to the spinning point. For our lighting system, we will use some of the real-world concepts (the most vital) to the max and greatly simplify others to guarantee decent rendering performance.

Lightsources

The first concept we will use is the concept of light sources. They don't have to emit white light only (all frequencies in the visible-spectrum should work, it's not our fault if your screen can't display all colors you want :) ). We will explain a few specialized types of common light sources that we will use in our lighting system.

Point lightsources

The most important are the point light sources, because all other types of light sources are easier understood once this has been taken care of. The point light sources are sources that emit light equally in all possible directions. Since the real shape of the object does not much matter in this case, they are normally represented by a simple point. The following diagram illustrates a point light source:


Figure 4: Notice the lightdensity falloff
Figure 4: Notice the lightdensity falloff


The distance between the light source and the object to be lit is important. It's evident that when light rays travel equally in all directions from a specific source point, the density of light decreases the further away from the source we measure. Particles in the air, such as fog and dust, also mess with the light which can scatter away from the primary paths. The effect that the intensity of light disperses over distance is called distance attenuation.

Directional lightsources

Next, we present directional lights. Directional lights emit light-waves coming from a point light source located at a distance close to infinity. All rays from a directional light source are practically parallel which means that they travel in only one direction. Here we ignore distance attenuation of light rays since if the light rays are strong enough to travel through infinity, they should be able to travel through, say, the dusty air in a student room. The best example of a directional light is the light that comes from the sun. Here is an illustration showing directional light:


Figure 5: Lightrays emitted from an infinite plane
Figure 5: Lightrays emitted from an infinite plane


Spot lightsources

Finally, we'll explain spotlights! A spotlight source is in fact a point light (good thing we've covered that one already, right?) which emits light within a given angle against a directional vector, or within a cone. Try to imagine a point light surrounded by a small, spherical shell that does not let light through. If somebody makes a hole in this spherical shell so that a small part of the light-rays can continue their way to the outside world, we will have a spotlight! If a spotlight points towards a floor, we will see a bright 'spot' (woah! the power insight!). With spotlights, we should also take note of distance attenuation since these light sources are not at an infinite distant location and the light rays most probably attenuate.

Spotlights often have a 'fuzzy edge', meaning the above mentioned spot on the floor is not razor sharp at the edge between lit and unlit. For very small light sources, this could be explained with "diffraction", which won't be explained here. For regular sized spotlights in real life, it can be explained with 'area lighting'.

It might be a tinsy bit confusing right now, so here's a picture illustrating all that:


Figure 6: Spotlight with fuzzy edge
Figure 6: Spotlight with fuzzy edge


The spotlight cone is actually split in two sub-cones. An outer-cone and an inner-cone. Light that travels within the inner-cone is equally bright. Light, traveling outside the inner- and inside the outer-cone is less bright. The light gets dimmer the closer the light rays are to the edge of the outer cone.

Don't worry if you don't understand all that mumbo jumbo right now, it will start to make sense with time and hair-pulling. Let's move on, shall we?

Types of light

Although there is only one type of light in the real world, we will use three types in our software rasterizer to simplify the calculations and still achieve a great deal of realism. Each lighting type represents a common visual surface attribute. These lighting types are: ambient, diffuse and specular. Here are few words for each of them:

Ambient light

Ambient light is light that, after millions, bIlLiOns or even GAZILLIONS of reflections, is equally distributed in space so the objects will be evenly lit from all sides. Note that amblient light is also present in shadows due to the large amount of reflections! The surface attribute here is just that objects reflect light.

Diffuse light

Diffuse light is a direction-dependent type of light that, on collision with an object, is equally distributed in all directions. The amount of reflected light-rays that reach our eyes is proprotional to the cosine of the angle between the direction vector of the light-rays and the normal vector of a surface (more on that later in the tutorial). Diffuse lights represent diffuse surfaces, surfaces that are matt and a little rough on a very small scale.

Specular light

Specular light is also directional and it is used to simulate the bright shiny spots (specular highlights) on objects when looking at them from specific angles. These shiny spots are pretty much the reflection of the light source itself on the object.

All light sources can emit ambient, diffuse and specular light. Also note that the above lighting types are statistical methods, so use your common sense more than a physical simulation of light to picture this lighting model :) .

Colors

So, what we will discuss now, is the mechanism of the interaction between light waves and lit objects. As already said, as light-rays hit some object in space, part of the rays get reflected and the rest gets absorbed by the object. We can see only the reflected rays (unless we'd have translucent materials, but we don't!). The following is crucial to understanding the lighting process so make sure you fully understand it:

The color of the reflected light-rays is, in fact, the color of the object.

Think about it - reflected light-rays carry color information about the object they've reflected from, since parts of the rays get absorbed by the object! Let's give a quick example, which will make everything clear (that's what we hope anyway...). Say we have a very red apple. When we direct bright white light against the apple we see the apple red. Why is that? Well, very simple; as the light-rays hit the apple, all colors (the respective light-rays really) except red will be absorbed and only red light will be reflected. Once reflected, the red light will travel to our eyes and we will see the apple red. Here's an illustration:


Figure 7: This apple makes me lose my appetite
Figure 7: This apple makes me lose my appetite


This means, that if we put a green light beside a totally red apple, the apple will appear black! Yummy...

Materials

There's one more thing to know before we get to the programming and mathematics parts of the tutorial. We need to figure out how to "instruct" the objects in our rasterizer to reflect given part of the light-rays and to absorb the other. For this goal, we introduce the so called materials. Each polygon, that will be sent to the lighting procedure, we assign a specific material. The material then shows what percentage of the light the polygon reflects in different wavelengths and what of the three lighting types are involved (are materials diffuse and shiny?). We will split the lighting information into red, green and blue components as we did in the previous tutorial, because as mentioned there, all colors can be built using the correct combination of these three colors. When calculating the final color values for a polygon, we take into account the light coming from all light sources (or the most important ones) in the scene and its material. The materials also specify "emissive light" for the polygons. The emissive light is light that the polygon itself emits (for example your monitor). In case the polygon is emissively lit you can think of it as a visible light source, which is NOT emitting light to the surrounding objects!

So, ladies and gentleman - we are ready to jump in the real action: programming and mathematics. Make sure you understand what've been said 'til now, because you'll need it!

Almost there...

Before getting into the coding details, let's clear some implementation issues. As you will soon see, light processing requires great amounts of processor time so if we want useable framerates, our lighting implementation will have to differ a little from the lighting model described above. First, instead of having ambient properties for each light, we will have a global ambient light level for our scene. We assume that it is formed as a result of the interaction (addition) of the ambient lighting components of all lights in the scene. Second, instead of specifing both an inner- and an outer-cone for each spot light, we will only specify one cone and therefore have bright spots on walls sharp enough to cut through potatoes:


Figure 8: Simple spotlight
Figure 8: Simple spotlight


The visual quality won't be that impressive using just one cone to simulate spot lights, but it still looks pretty good as we'll not do the lighting for every fragment (or pixel). Implementing dual-cone spotlights can be a good coding exercise too.

Light distance attenuation (relation between the intensity of the light reaching a given surface and the distance it travels to that surface) will also not be implemented, but you can try experimenting with it - it's straightforward to code. The general formula for how light attenuates is as follows:


I = \frac{I_0}{d^2}


where I0 is the lightsource intensity, d is the distance between the lightsource and the point to be lit in space and I is the final intensity.

Code

Programming the whole thing is a little bit easier than explaining it, because now we've already gone through the theory (doh) and we got half of the code for a Gouraud shader! But from now on we don't assign colors to vertices, since, as we already said, nothing in the real world has a color. "Color" is just a light-wave with a given frequency. No geometry that we will send through the lighting process will initially have colors assigned. Instead we will assign each polygon a material and for each vertex, using the material attributes for this polygon and the active lights in the scene, we will calculate the final color for this vertex. The process that each polygon will pass through is called the "lighting pipeline". After all vertices of a polygon have been processed by that pipeline, we rasterize the polygon using the Gouraud shading algorithm creating the illusion of calculating correct lighting values for each pixel of the polygon. Why an illusion? Because even with the newest computers that would be an impossible task in software - the lighting process, as you will see, is damn slow. An algorithm to calculate correct lighting values per-pixel (we do it per-vertex) exists however and it's called "Phong shading". We won't deal with it here but we encourage you to check it out - there's a lot of info on the net.

And now we want to show some code structures to give a better understanding of everything what we've talked about before continuing with the final part of our tutorial:

- this is how a light structure might look like:

struct light
{
    int     type;           /* point, directional, spot */
    vec3    position;       /* light's position */
    vec3    direction;      /* light's direction (spot, directional lights only) */
    col3    col_diffuse;    /* diffuse intensity of emitted light rays */
    col3    col_specular;   /* specular intensity of emitted light rays */
    float   exp;            /* specular exponent */
    float   radius;         /* spot cone radius */
    ...
};

- this is how we might define a material:

struct material
{
    col3    col_emissive;   /* emittion intensity */
    col3    ref_ambient;    /* ambient reflection factor */
    col3    ref_diffuse;    /* diffuse reflection factor */
    col3    ref_specular;   /* specular reflection factor */
    ...
};

- and then the modified polygon with the material index inside:

struct polygon
{
    int v1;         /* vertex #0 */
    int v2;         /* vertex #1 */
    int v3;         /* vertex #2 */
    int material;   /* material index */
    ...
};


As you can see, nothing complex! So, we got our goal down to calculating a color for each vertex given a material and surrounding lights. So how do we do it? Here comes the tricky math part so let's split this up in small sections!

First, for the lighting equations we will need something known as a "normal vector" for each vertex. Mathematically, a "normal vector" is a vector perpendicular to a given surface. Small problem - the vertex is not a surface by itself but just a point in space! Nevertheless we can still find a suitable normal in two ways: having a normal for every vertex in a face that point just like the face normal or average the normal vectors of all polygons that share one vertex! The former is good for objects with sharp edges, the latter for objects that must appear smoother. Take a look at the following diagram for clearance:


(a) vertex normal = face normal
(a) vertex normal = face normal
(b) vertex normal = av. face normal
(b) vertex normal = av. face normal


We will use averaged normals. What do we need the vertex normal for, will be expained right away but remember: ANY rotational transformation we apply to a vertex must also be applied to the vertex normal!!!!

To start the actual calculation, let's define the final vertex color we will use for rasterizing and set it to 0.0 (0 - completely dark; 1 - very bright). This final color is calculated by our lighting system based on the light transfer in the scene.

float final_red = 0.0;
float final_green = 0.0;
float final_blue = 0.0;

Now we proceed with calculating the final color. The first step is to add the emissive color (see the explanation of emission above!) of the polygon to these values:

final_red = materials[ polygon.material_index ].col_emissive.red;
final_green = materials[ polygon.material_index ].col_emissive.green;
final_blue = materials[ polygon.material_index ].col_emissive.blue;

Then, since every vertex is affected by the global ambient level in the scene, we add the ambient light values, multiplied by the ambient reflection factors of the assigned material, to the final colors:

final_red += global_ambient.red * materials[polygon.material_index].ref_ambient.red
final_green += global_ambient.green * materials[polygon.material_index].ref_ambient.green
final_blue += global_ambient.blue * materials[polygon.material_index].ref_ambient.blue

And finally we enter a loop, to see how each light in the scene contributes to the final vertex color.

FOR each light source in the scene DO:

    if no light rays from the light can reach the vertex
        skip this light

    calculate diffuse reflection coefficient (drc)
    add diffuse component of light multiplied by drc and the material diffuse attribute, to the final vertex color

    calculate specular reflection coefficient (src)
    add specular component of light, multiplied by src and the material specular attribute, to the final vertex color

After this, the vertex lighting is done and we can proceed with the next one or rasterize the the polygon if all vertices have been processed!

Let's analyze the above loop. The first task is to check whether emitted light rays can reach the vertex in question. For directional and point lights it's very simple - we calculate the dot product of the vertex normal and the light direction and look at the sign. If it's zero or negative, the light is pointing at the 'behind of the vertex' (meaning the light would come through the object that the vertex belongs to) and the process continues with the next light. For spot lights, we must also check so that light-rays outside the cone do not affect the vertex. We simply add a comparison of the cosine of the spot falloff angle. 'light direction' is the vector pointing from the vertex to the light source (light\_pos - vertex\_pos).

See the illistrations below to get a better idea of the visibility checks for the different types of light:


Figure 9: Visibility checks
Figure 9: Visibility checks


After a successful visibility check, we first add the diffuse light contribution to the final vertex color according the algorithm above. As already stated, diffuse light is equally reflected in all possible directions so the amount of light reflected towards our eyes will depend on the angle between the light direction and the vertex normal. The smaller the angle between the light direction and the vertex normal, the "brighter" the vertex will be. If the angle is large, it means the light rays come in at a very shallow angle against the surface and thus will cover a much larger surface area and the amount of reflected light will dimnish. When we know the amount of reflected light, the diffuse coefficient, we simply scale it by both the material diffuse attribute and by the diffuse component of the light and add it to final vertex color! Since directional lights already has a directional vector specified, getting the light direction of incoming light should be trivial even to a mongoose. We're now interested in calculating the light direction for point and spot light sources and this, as said above, is done by substracting the vertex position from the position of light source, since the rays start at a point as opposed to directional lights (which start at a point at infinity).


To simulate specular reflection however, we would also need to take the camera's direction into the picture! But first, some basic words how the specular reflections occur. We "see" them when light rays are reflected immediately from shiny objects. Diffuse lighting scatter light-rays in all directions due to small-scale roughness, but specular highlights simulate flatter surfaces that scatter light much less. If we want a perfect approximation of the light source's reflection, we would have to borow some really powerful CPUs but fortunately there's an algorithm that makes our life a lot easier!!! Let's take the look at following scheme:


Figure 10: Specular lighting
Figure 10: Specular lighting


Light comes from the direction L and is reflected against the vertex normal N into the direction R. We also have the viewing direction V. Now we gotta think logically: if the reflected rays (R) are entering directly in our eyes (V) we will see the fully bright reflection of the light source. With other words, if R dot V is 1, our eyes hurt because of all the light they get. The bigger the angle between V and R gets, the less light rays will be reflected directly into our eyes and the dimmer we will see the reflection of the light source. The problem here is that calculating the reflection vector R is terribly slow!!! It requires quite a lot of processing power and is therefore unsuitable for our needs. Fortunately, a smart guy called Jim Blinn came up with an approximation which looks very good! The trick is that instead of calculating R, we calculate a 'halfway vector' H and we compute the angle between N and H. Calculating H is pretty trivial, you simply add L and V! You should also normalize the result of course. Now look at the scheme below carefully (use paper and pencil too...) and you'll soon be convinced that it actually really works!!! :) )


Figure 11: Blinn specular model
Figure 11: Blinn specular model


Again, it's only an approximation (just as the specular model that we started with) but as long as it looks fine, we should be happy! Let's summarize how to calculate the specular light contribution to the final vertex color. You calculate the halfway vector H and normalize it. Then you find the dot product of H and N. To control the angle through which specular highlights occur, we raise the dot product to a given power, which we call "specular exponent". It basically gives how sharp the specular reflection will be. Higher numbers result in very shiny spots but computation comes at the price of CPU cycles! The final step is to scale the specular component of the light by the raised dot product, multiply the result by the specular reflection coefficient of the material and to add it to the final vertex color!

And that's all folks!!! Now you should know how to do (very) basic lighting. Of course that's not even \frac{1}{1^{10}} of the things you will have to learn about computer lighting if you're planning a career in the gaming industry. Lighting is probably the biggest and most important part of computer graphics so we encourage you to make sure you understand everything we've talked about in this tutorial before diving into the endless world of "per-pixel" lighting, radiance transfer, radiosity, etc. Without a basic knowledge, you are lost!!! Of course, we will talk about lighting quite often in the future tutorials. We also plan a future tutorial on the topic lightmapping, where you will learn to apply precomputed light textures (lightmaps) to the objects, instead of doing slow per-vertex calculations.

In the next tutorial we will most likely finally discuss clipping and perhaps z-buffering, because if I (Mihail) see and code Painter's algorithm just one more time, I'LL BLOW MY HEAD OFF!!!

Don't forget to check out the source and demo and to give feedback!!! As always, anything will work: simple comments, compliments, criticism, rage, love or antrax letters...

'til the next tutorial, cya!




Author's note:

We apologise that no source is currently available - it's beeing written in the moment but due the lack of time the process is moving slowly.

Some explanations of diffraction (http://diffractive-optics.org/)



DevMaster navigation