Czech Welcome Guff Coding 3D - Engine Guestbook Links Downloads About author Mail me Mailform
box_en 3D Engine 3 - Lights and lighting methods 4 (radiosity) box_en

Radiosity

    So, we're gonna chat about radiosity. It sounds kind of mystical, but it's not so much complicated. First a bit of historic data - it started in 1984 on Cornell University on piece of dingy greasy paper with sign Modelling the Interaction of Light Between Diffuse Surfaces on it. Goral has that paper on his conscience, Torrance made it dingy and greasy and Greenberg written what was inside. So, we're dealing with diffuse surfaces - it means radiosity won't help us computing specular surfaces. The idea is simple and in fact it can be used for computing of interaction of any ray-like spreading quantity such as light, maybe heat and with assumption of motionless air and perfetly linear Brown's motion even distribution of deadly smell from decease.
    But how did it work ? The world was subdivided to infinite number of small surfaces - patches, while the original geomethry of the world could be thrown away. Every patch has it's own radiative (to be radiated) and illuminative energy (it's illumination) On the beginning, all patches has zero illuminative energy (aparts from objects such as lights) and zero radiative energy (again - but lights). Maybe i didn't mention radiosity is dealing with area lights. Then so-called radiosity matrix was created. It's square matrix with numbers of patches on it's sides, containing form factors. They describe light transmittance from one patch to another. For example when patches aren't visible, form factor will be zero. In case they're directly one against the other one with very small distance between, their form factor should be one. Then you walked trough that matrix, sending radiative energies from every patch to every other patch. Part of energy will go into receiving patch illuminative energy and part to it's radiative energy so it's gonna be reflected ... If you pay attention, you may notice the matrix is too large - because we're reapeating form factors for x -> y by form factor y <- x. Are we ? No, we're not. Form factors for transimittance from patch a to b isn't the same as the one for transmittance from b to a. Sollution was supposed to be done when radiative energy of every patch was zero. After Torrance ate thousand-th tin of sardines, Goral died from fish smell and Greenberg eventually found he's solving two nested infinite loops. (we already know it was too much even for NASA power computers, not mention Torrance's 486 SX DX 2 XXX !) So Torrance grabbed Goral and got him out and in 1988 came Cohen, Chen, Wallace and Greenberg with even more dingy paper, because Chen could't live without his indical kari and old Cohen was fond of eating big chunks of meat. Greenberg was resisting from Wallace to participate in his project, but he claimed his scotch grandfather is paper-maker and their paper comes from free Scottland. Paper, meat - oily, a bit yellow from kari was called A Progressive Refinement Approach to Fast Radiosity Image Generation. This time, things were a bit easier (in fact things are most time easier when you ged rid of double nested infinite loop): We weren't walking trough any matrix (that would eat up some gigabytes of memory anyway), but we pick a patch with greatest radiative energy and "shoot" it. Iterate until radiative energy of patch with greatest radiative energy has radiative energy near-to zero. (as ing. Gandalovich would say : "that piece of crap is going to be iterating yet when you pass your final exam here when you didn't put here terminating condition !" ... and he'd certainly add "yeah, sine tone ... sharp as razor it is !") So this refinement infinitely times cut computing time, because before it was infinite and now it's finite. So we can begin coding
    Just a few things before we start. Patches should be small enough so their relative distance is many times greater than their size. (allright, it won't ever work for neighbour patches, but we transmit no energy to neighbour patches, right ?) There are plenty methods for subdividing world into patches. We could take each surface and say - split it 8×8 patches ! That would be a bit waste - and as you can see area of patch is what matters here, we should like to keep all patches uniform size (did it remind you lightmap pixels ? right. we'll show even lightmaps can be used for radiosity) Other method is to subdivide object according to it's size so smaller object results in less patches. Finally there are some hybrid subdivision methods which are dividing surface in progress of sollution as they get into shadow edges or like that ...
    A very good thing is, radiosity gives us HDR results (energy of patch can be zero-to-infinite) so we should think about how to display that ... There are a few methods and we'll show two of them. First is simple - just do nothing and see screen is coming white. Allright, we have to carefully adjust initial energies so the result looks fine ... The other method would include some exposition calculation and some nice glow post-effect for parts that are brighter than we can display.
    I might add radiosity is global illumination sollution method. It means everything affect everything. You change a piece of geomethry and half of our world's illumnation is going to change. We can also only compute diffuse - diffuse and diffuse - specular interactions. We can't reflect light from specular surfaces, using radiosity, so if we'd like to, we should connect radiosity for example with photon mapping or some raytracing technique. In games, radiosity isn't used very much, since every object, moving in map would cause whole radiosity sollution has to be recomputed. We could trick it with some "local" radiosity, solving illumination only for some part of world in certain distance from viewer, that could work ... But static radiosity is on the other hand quite popular. You can see it in Max Payne, Far Cry or Half Life 2, because these games have cool radiosity lit wolrd, a bit worsened by sharp stencil (shadow-buffer in HL2) shadows ...

far cry
Far Cry - soft lightmap shadows for static part of world + stencil for moveable stuff




Computing form factors

    Form factors can be figured out from equation :
form-factor equation
    Where i a j are patch indices. Fi-j is form factor for energy transfer from i to j (not conversely !), r is patch centers distance, these strange greek characters are radiation angles (angles between patch normal and ray vector) Ai and Aj are patc areas and dAi and dAj is differential area (ie. difference of areas) This is (a few mathematical wizards already know) first version of radiosity (these two integrals mean that double nested infinite loops) Progressive refinement "sounds" like this:
progressive refinement form-factor equation
    That's similar, but a bit simplified. Mathematically it's completely different equation so they're not equal. Yes, and i'd forgot about Hij, that is visiblity from one patch to the other. Usually you cast more rays so you have some fractional visibility, not just 0 / 1. It depends a bit if you'd like to do realtime radiosity ...


And how does it look ?

    As i told you above - radiosity is for area lights so it results in nice smooth shadows and bright scenes. There's a slight problem with absolutely - reflexive surfaces since they will remain black, but we should know they reflect it's surrounding (use mirror, enviroment map or whatever) Results are this nice:

cornell box
Cornellova box (notice color bleeding !)


3ds max
Tohle je 3ds max 5 (maybe fake, but could be radiosity)


movsd - realtime radiosity 2
Demo Realtime radiosity 2 (areas, backfacing light aren't completely black)


movsd - realtime radiosity 2
RR2 aswell (illumination on the table right below the light is > 255)


Co my s tím ?

    So ... we know mathematical implementation, but there are also hemicubes ! Hemicube is simply half of cube, looks like this:

    hemicube
What is it for ? It's simple - you take your rasterizer, put the camera into the centre of patch (which is also center of cube, this hemicube is part of), set FOV to 90 degrees and render front wall of hemicube, then also it's sides. (you can see camera's FOV set to render front wall) We don't draw colors, but our favorite poly-id and when everything is drawn, every patch will receive energy proportional to number of pixels with it's id in hemicube. How much ? Sum of all transfered energies must be equal to radiation energy of patch. But how to distribute it ? Well, hemicube in fact solve part of radiosity equation. There's differential area (more pixels -> more energy), there's distance (distant patches will occupy less pixels), there's also visibility and finally there's cos() of receiving patch angle (more dip = less pixels). We should also take distributing patch angle into account. It's done by map, assigned to all walls of cubemap and containing weight values of each pixel. What should it exactly contain ? Well, it will be our cosine as sides of hemicube shouldn't transmit so much of energy as it's front. There's also a little error in pixel sizes, because we converted hemisphere (that's how we should meassure receiving patch areas) into hemicube so pixels near center of front wall are in fact bigger than pixels on the sides:

    cube to sphere
You can see projection of two pixels from cubemap onto sphere - and as you can see, they do have different area. So we'll have to write some code for computing hemicube form factors. These will be two maps, one for front wall and one for sides (all sides are equal)     We should also use ambient light as sum of all radiative energies of all patches, so we won't see scene brighten gradually, instead we'll see shadows to rise ...     I was talking about color bleeding. How to handle this ? When distributing energy, some part of energy comes to illumination and some will be radiated. And that radiated energy should change it's color, according to surface color on the patch ...     When we sum it all together, we have to zero all energies (apart from lights) and emit radiative energies of patches with gratest readiative energy. (it will be propably lights and when lights energies are emitted, some patches beneath it will come to act) We should sum all radiative energies to get ambient color. Assuming radiative energies (and also ambient light) will drop hyperbolically (ie. it will drop fast in first few seconds and then it will be infinitely approach zero) so it may take a long time to compute. You can precompute patch colors and then render it as gouraud-shaded patches as in this VRML demo from Helios:

    
helios32
Helios32 (běží interaktivně ve VRML97)

I might add vertex color is computed as average of patch colors, sharing this vertex.


... what about realtime ?

    Of course it's possible to do realtime (we have RR2 demo, right ?) It's done simply - there's not much lightsources, there's not too much patches and using per vertex lighting is also quite cheap. In such a sollution you will set minimal radiation energy quite high so it's done quite fast. On the other hand it's not very accurate. You can also use textures for radiative and illuminative energy, while radiative energy can have smaller resolution than illuminative. (the error won't be much visible if visible at all)


Simple radiosity processor

    So. The first one will be real simpleton. We get 3ds file with subdivided surfaces, object names will be "__light" or some other for non-light patches (we can substitute face = patch) We will also say the patches have approximately same area so the differential areas are always one so can be left out. We won't do hemicube at here, but we'll try simple raytracing. Hemicube will come in next example. I decided to add textures (and then fell to black-and white lights becuse of performance ...) So, we have our first function, performing n radiosity iterations :


struct Face {
    int n_id, n_used;
    int n_material;
    //
    Vertex *vertex[3];
    //
    Plane normal;
    //
    float f_rad_energy;
    float f_illumination;
};
// face has it's radiative and iluminative energy parameters ...

void Radiosity_Loop(World *p_world, int n_loops)
{
    Face *p_emitter;
    Vector v_start;
    int i, j, k, l;
    float f_form;
    int n_faces;

    while(n_loops --) {
        for(i = 0, p_emitter = 0; i < p_world->n_object_num; i ++) {
            for(j = 0; j < p_world->object[i].n_face_num; j ++) {
                if(!p_emitter || p_world->object[i].face[j].f_rad_energy >
                   p_emitter->f_rad_energy)
                    p_emitter = &p_world->object[i].face[j];
            }
        }
        // find face with gratest radiative energy

        v_start.x = (p_emitter->vertex[0]->x +
            p_emitter->vertex[1]->x + p_emitter->vertex[2]->x) / 3;
        v_start.y = (p_emitter->vertex[0]->y +
            p_emitter->vertex[1]->y + p_emitter->vertex[2]->y) / 3;
        v_start.z = (p_emitter->vertex[0]->z +
            p_emitter->vertex[1]->z + p_emitter->vertex[2]->z) / 3;
        // get it's centre

        for(i = 0; i < p_world->n_object_num; i ++) {
            for(j = 0; j < p_world->object[i].n_face_num; j ++) {
                f_form = f_FormFactor(p_world, v_start, p_emitter,
                    &p_world->object[i].face[j]);
                if(f_form > 0) {
                    p_world->object[i].face[j].f_rad_energy +=
                        p_emitter->f_rad_energy * f_form * .2f;
                    p_world->object[i].face[j].f_illumination +=
                        p_emitter->f_rad_energy * f_form * .8f;
                }
                // 90 % comes to illuminative, 10 % to radiative energy of receiving patch
            }
        }

        p_emitter->f_rad_energy = 0;
        // this is real important !!
    }

    for(i = 0; i < p_world->n_object_num; i ++) {
        for(j = 0; j < p_world->object[i].n_vertex_num; j ++){
            n_faces = 0;
            // set face count to 0

            p_world->object[i].vertex[j].i = 0;

            for(k = 0; k < p_world->object[i].n_face_num; k ++) {
                for(l = 0; l < 3; l ++) {
                    if(p_world->object[i].face[k].vertex[l] ==
                       &p_world->object[i].vertex[j]) {
                        n_faces ++;
                        p_world->object[i].vertex[j].i +=
                            p_world->object[i].face[k].f_illumination;
                    }
                }
            }
            // average energy

            p_world->object[i].vertex[j].i /= (float)n_faces;
        }
    }
    // calculate vertex illumination color values
}

It's quite simple. We pick face with greatest radiative energy, distribute that energy, set his raduiative energy to zero. Then recompute vertex colors so we can see process of energy distribution. I should go trough f_FormFactor():


float f_FormFactor(World *p_world, Vector v_start, Face *p_emitter, Face *p_receiver)
{
    Vector v_end, v_ray;
    float f_form;

    v_end.x = (p_receiver->vertex[0]->x + p_receiver->vertex[1]->x +
        p_receiver->vertex[2]->x) / 3;
    v_end.y = (p_receiver->vertex[0]->y + p_receiver->vertex[1]->y +
        p_receiver->vertex[2]->y) / 3;
    v_end.z = (p_receiver->vertex[0]->z + p_receiver->vertex[1]->z +
        p_receiver->vertex[2]->z) / 3;
    // the other face centre

    if(v_end.x * p_emitter->normal.a + v_end.y * p_emitter->normal.b +
       v_end.z * p_emitter->normal.c + p_emitter->normal.d > 0.1)
        return 0.0;
    // check for back-facing faces

    v_ray.x = v_end.x - v_start.x;
    v_ray.y = v_end.y - v_start.y;
    v_ray.z = v_end.z - v_start.z;
    // ray

    f_form = f_Visibility(p_world, v_start, v_end, v_ray);
    // visibility term is the base

    f_form /= Pi * _Len(v_ray);
    // divide it pi * distance

    _Normalize(v_ray);
    return f_form * _PlDot(v_ray, p_emitter->normal) *
        (- _PlDot(v_ray, p_receiver->normal));
    // and finish it by multiplication of dot products of ray and normals
    // (see, we assume differential area to be 1 ...)
}

This is very simple interpretation of the second (progressive) refined radiosity equation. Get center of the second patch, compute ray, visibility, add distance factor and ray / normal angles. The result is quite non-precise form factor, but for now it's enough. Function f_Visibility() return patch vivibility value. That should be value in range <0, 1> (that's why it return float), but we do just single check so the result is value {0, 1}. I should mention the raytracing function is clearly bottleneck, some BSP would do a great deal.

    ... and how to put it together ? Simply load world, initialize patch (face) energies, do some iterations of radiosity and simply draw frame. But how ? We have our HDR values ! This time i'll assume values in 0 - 65535 range so we should fit into 16:16 fixed point value for interpolation illumination accross the surface. We just have to change color modulation a bit:


n_color = (((n_color >> 16) * (i1 >> 16) > 0xff00)? 0xff0000 :
          (((n_color >> 16) * (i1 >> 16)) >> 8) << 16) |
          (((n_color & 0x00ff00) * (i1 >> 16) > 0x00ff0000)? 0x00ff00 :
          (((n_color & 0x00ff00) * (i1 >> 16)) >> 8) & 0x00ff00) |
          (((n_color & 0x0000ff) * (i1 >> 16) > 0x0000ff00)? 0x0000ff :
          (((n_color & 0x0000ff) * (i1 >> 16)) >> 8) & 0x0000ff);

Where n_color is diffuse color of surface and i1 is 16:16 FP illumination. Everytime we move it 16 bits right what we shouldn't, because we're loosing accuracy. We also need to check for color overflows. The last thing we should do is to hope our optimizer will handle repeating expressions and make code run fast.

    So that much for the first example. It wasn't so complicated at all. You'll notice the result is quite slow, but i won't bother optimalizing this - it's just your first date with mrs radiosity. You can sit with her, watching as she converges to the sollution. The very definition of romantic moments :o).     You can switch on/off HDR, but it won't be any faster since switch off mean "clamp all illumination values up to 255" ... it's just about seeing the difference.

textured

hdr

clamped
last one's without HDR

 radiosity example

    ... these textures initially showed up as bad ones, because they're too dark. You can toggle them by pressing "T" key. You can also toggle radiosity iterating by "L" or HDR with "H". Have fun with this one !


... And that's all ?

    Ne, to zdaleka není všechno. Ono ... k tomu HDR - ono se obraz vyrenderuje do float bufferu (pro r, g a b složku je float hodnota), nebo do dvou bufferů, z nichž jeden tvoří spodních 8 bitů a druhý vrchních 8 bitů barevných složek. Buffery se potom zprůměrují a tím se spočítá průměrná světlost. Podle ní se potom upraví kontrast a jas a výsledný obraz se ořízne do normálního 32bpp bufferu.
    Občas se ještě dělá to, že se vezme kopie obrazu, kde zůstanou jen hodnoty RGB přesahují určitou mez. Tenhle buffer se potom prožene nějakým blurem a přičte se k obrazu. Vznikne potom krásný efekt, kdy světlé věci září i do okolí ... Asi si to potom zkusíme s OpenGL ...
    HDR používá po zpatchování třeba taky Far Cry (obrázek), a když už jsem u Far Cry, měl bych připomenout že při počítání průměrné světlosti by se potom měly zachovat také nějaké meze, jinak dopadnete jako FarCry, kde malá žárovička v tmavé místnosti vypadá jako sluníčko :o)

HDR far cry
with HDR

HDR far cry
without HDR



The end ..? You gess !

    Well, i'm not going to stop torturing you now. There's one more example with lightmaps, using hemicube and doing HDR-like post production. (in the end i didn't write exposure control - i'm too lazy), But the result is worth it anyway. First have a look at how to calculate such a hemicube:


static const int n_hemicube_size = 256;

static unsigned __int32 p_s_hemicube_buffer[n_hemicube_size * n_hemicube_size];
static unsigned __int32 p_s_hemicube_buffer_side[4][n_hemicube_size *
                                                    n_hemicube_size / 2];
static float p_hemicube_formfactor[n_hemicube_size * n_hemicube_size];
static float p_hemicube_formfactor_side[n_hemicube_size * n_hemicube_size / 2];

static float f_hemicube_max = 1;

void Calc_HemicubeFormFactors()
{
    float f_half_pixel_width;
    float f_pixel_area;
    float dx, dy;
    int x, y;
    float f;

    f_half_pixel_width = (1.0f / n_hemicube_size);
    //
    f_pixel_area = (2.0f / n_hemicube_size);
    f_pixel_area *= f_pixel_area;
    // 2 is cubemap size

    for(x = 0; x < n_hemicube_size; x ++) {
        for (y = 0; y < n_hemicube_size; y ++) {
            dx = ((x - n_hemicube_size / 2) / (n_hemicube_size / 2.0f)) +
                f_half_pixel_width;
            dy = ((y - n_hemicube_size / 2) / (n_hemicube_size / 2.0f)) +
                f_half_pixel_width;

            f = (dx * dx + dy * dy + 1);
            f *= f * Pi;

            p_hemicube_formfactor[x + y * n_hemicube_size] = f_pixel_area / f;
        }
    }
    // top

    for(x = 0; x < n_hemicube_size; x ++) {
        for(y = 0; y < n_hemicube_size / 2; y ++) {
            dx = (x - n_hemicube_size / 2) / (n_hemicube_size / 2.0f) +
                f_half_pixel_width;
            dy = (n_hemicube_size / 2 - 1 - y) / (n_hemicube_size / 2.0f) +
                f_half_pixel_width;

            f = (dx * dx + dy * dy + 1);
            f *= f * Pi;

            p_hemicube_formfactor_side[x + y * n_hemicube_size] =
                (f_pixel_area * (dy + f_half_pixel_width)) / f;
        }
    }
    // side

    for(x = 0, f_hemicube_max = 0; x < n_hemicube_size; x ++) {
        for(y = 0; y < n_hemicube_size / 2; y ++) {
            if(f_hemicube_max < p_hemicube_formfactor_side[x + y * n_hemicube_size])
                f_hemicube_max = p_hemicube_formfactor_side[x + y * n_hemicube_size];
        }
    }
    //
    for(x = 0; x < n_hemicube_size; x ++) {
        for(y = 0; y < n_hemicube_size; y ++) {
            if(f_hemicube_max < p_hemicube_formfactor[x + y * n_hemicube_size])
                f_hemicube_max = p_hemicube_formfactor[x + y * n_hemicube_size];
        }
    }
    // find greatest form-factor value ...

#define __SUM_TEST
#ifdef __SUM_TEST
        float f_sum;

        for(y = 0, f_sum = 0; y < n_hemicube_size; y ++) {
            for(x = 0; x < n_hemicube_size / 2; x ++)
                f_sum += p_hemicube_formfactor_side[y + x * n_hemicube_size];
        }
        // side

        f_sum *= 4;
        // we have 4 sides

        for(x = 0; x < n_hemicube_size; x ++) {
            for(y = 0; y < n_hemicube_size; y ++)
                f_sum += p_hemicube_formfactor[x + y * n_hemicube_size];
        }
        // top

        // tady musi byt f_sum == 1, mě to vyjde 1.00435f
#endif
}

    Now a bit explaining ... First we have some global variables. First constant is hemicube resolution. Then there are some arrays. p_s_hemicube_buffer and p_s_hemicube_buffer_side are buffers, we're going to render what hemicube "see". (_side is four times, because we're going to display hemicube contents onto screen so we want to remember it whole. THen we have p_hemicube_formfactor and p_hemicube_formfactor_side - form-factors of hemicube pixels. (this time single _side is enough since remaining three would contain the same data) We should now take a look at Calc_HemicubeFormFactors():
    On the beginning we calculate f_half_pixel_width - half of hemicube pixel size (width) Hemicube is 1 unit deep and 2 units wide. (like four unit cubes, aligned arround normal vector) Then calculate f_pixel_area - pixel area. In loop we calculate distance from center for each pixel. Then convert it to worldspace distance and calculate form factor:

      f_pixel_area / (Pi * (dx2 + dy2 + 1)2)

We do the same for hemicube side, we only have to calculate it's distance from center. The result is nice hemicube we can convert to bitmap using p_Hemicube_Test() function. My result look like this:

hemicube

(that's a bit modified, the output from program is just front wall and one side below)

    Now when we have hemicube, we should find out how to render it. We know patch center (lightmap texel center) and it's normal. It's enough to set up camera, looking away from patch along it's normal and also four "side" cameras as well. Let's see:


void Render_Hemicube(Vector v_pos, Vector v_normal, Mesh *p_svet, Vector v_radiative)
{
    Vector v_up, v_right;
    Matrix m_camera;
    int i, j;

    Matrix_Init(&m_camera);

    if(fabs(v_normal.x) < .01 &&
       fabs(v_normal.y) - 1 < .01 && fabs(v_normal.z) < .01) {
        v_up.x = 0; 
        v_up.y = 0; 
        v_up.z = 1;
    } else {
        v_up.x = 0; 
        v_up.y = 1; 
        v_up.z = 0;
    }
    // find "up" vector that's not colinear with direction vector

    _Normalize(v_normal);
    _Cross(v_up, v_normal, v_right);
    _Cross(v_right, v_normal, v_up);
    _Normalize(v_right);
    _Normalize(v_up);

    Matrix_Init(&m_camera);
    //
    m_camera[0][0] = v_right.x;
    m_camera[0][1] = v_right.y;
    m_camera[0][2] = v_right.z;
    m_camera[1][0] = v_up.x;
    m_camera[1][1] = v_up.y;
    m_camera[1][2] = v_up.z;
    m_camera[2][0] = v_normal.x;
    m_camera[2][1] = v_normal.y;
    m_camera[2][2] = v_normal.z;
    m_camera[3][0] = v_pos.x;
    m_camera[3][1] = v_pos.y;
    m_camera[3][2] = v_pos.z;
    // set camera looking prependicular from patch center

    Set_FormFactors(p_hemicube_formfactor, v_radiative);

    SubWindow(n_hemicube_size, n_hemicube_size);
    Clipper(n_hemicube_size, n_hemicube_size);
    Init_Screen();
    for(i = 0; i < p_svet->n_object_num; i ++) {
        DrawObject(&p_svet->p_object[i].m_object, &m_camera,
            &p_svet->p_object[i], p_svet->p_material, p_svet->p_lightmap);
    }
    Release_Screen_LinearBuff(p_s_hemicube_buffer);
    // render view to front buffer ...

    Matrix_Rotate_X(&m_camera, -Pi / 2);
    // rotate camera so direction vector is colinear with patch plane ...

    Set_FormFactors(p_hemicube_formfactor_side, v_radiative);

    SubWindow(n_hemicube_size, n_hemicube_size);
    Clipper(n_hemicube_size, n_hemicube_size / 2);
    for(i = 0; i < 4; i ++) {
        Init_Screen();
        for(j = 0; j < p_svet->n_object_num; j ++) {
            DrawObject(&p_svet->p_object[j].m_object, &m_camera,
                &p_svet->p_object[j], p_svet->p_material, p_svet->p_lightmap);
        }
        Release_Screen_LinearBuff(p_s_hemicube_buffer_side[i]);
        // render view to side buffer ...

        Matrix_Rotate_Y(&m_camera, -Pi / 2);
        // rotate camera towards next side ...
    }
}

    So. Function gets v_pos (position of patch center), v_normal (it's normal), p_svet (world geomethry) and v_radiative (radiative color; it's classic RGB, but stored as floats and not clamped to <0, 1>) On the beginning we set up camera as we already did it. We pay attention so our "up" vector is never colinear with direction vector. Then call Set_FormFactors() to set corresponding table with form-factors and radiative color. Then make two more calls, namely SubWindow() that set sub-window resolution, must be always less than image resolution and Clipper() that set valid sub-window area (it's difference, because SubWindow(), change resolution and perspective correction constant so camera direction intersect image in center, but when calling Clipper(), it simply clip image. It's useful when drawing sides of hemicube, we need camera direction to intersect bottom edge of image) Then draw world as we always did and call clone of Release_Screen() It draw front side of hemicube. Then rotate camera 90 degrees arround x axis, so view direction lies in patch plane. Set image clipping to half height so we get only part of image that is above patch plane (that's exactly what we need) and draw it again. Then rotate camera again 90 degrees, this time arround y axis, making camera to look at another side of cubemap. We shouldn't forget to set another form-factor table for sides. How to check if it works ? We could simply copy rectangles on screen:

hemicube contents

    We can also do it a bit complicated - project hemicube onto hemisphere. It's fairly simple transformation. Imagine hemicube with it's surface covered with points. Each point hold value of vector point position - sphere center (sphere, not hemisphere). Now imagine someone fat stepped onto that hemisphere and ironed it. Now we have our hemisphere on the pavement (plane) so our points are on it's surface, but they still hold original vector values. That's what we're going to try to achieve. We can place flat hemisphere somewhere to that raster and for each pixel compute distance to hemisphere center position in raster and divide them by hemisphere radius in pixels. This yields x and y vector coordinates. We divided it by radius so it should be unit vector. It mean when x, y are greater - z is going to be small, vector points near edge of hemisphere. When x and y are zero, we're in center of hemisphere and z will be 1. We see we can use this equation:

      x2 + y2 + z2 = 1

(there's no square root, because we all know sqrt(1) = 1) From it we can derive z is:

      z2 = 1 - x2 - y2
      z = sqrt(1 - x2 - y2)

Now we can take this vector, find out it's greatest component (and it's sign) so we know hemicube side it points to. Then we just have to transform that vector to raster coordinates and draw that pixel. We already know that transformation, it's simply perspective correction, while perspective correction constant is 1. We shouldn't forget to multiply it by raster size and add half raster size to translate point [0, 0] to center. My code is here:


void Copy_HemicubeToScreen2(unsigned __int32 *p_buffer, int n_buffer_w)
{
    Vector v;
    int w, h;
    int x, y;
    float f;

    //Modulate_HemicubeBuffers();

    w = (int)(2 * n_hemicube_size);
    h = w;

    for(y = 0; y < h; y ++) {
        for(x = 0; x < w; x ++) {
            v.x = -(w - x * 2) / (float)w;
            v.y = -(h - y * 2) / (float)h;
            if((f = v.x * v.x + v.y * v.y) > 1)
                continue;
            v.z = (float)sqrt(1 - f);
            // construct vector, pointing from cubemap center ...

            if(v.z > fabs(v.x) && v.z > fabs(v.y)) {
                p_buffer[x + y * n_buffer_w] =
                    p_s_hemicube_buffer[
                        (int)((v.x / v.z * .5f + .5f) *
                        n_hemicube_size) +
                        (int)((v.y / v.z * .5f + .5f) *
                        n_hemicube_size) * n_hemicube_size];
                // greatest is z - front side
            } else if(fabs(v.x) > fabs(v.y)) {
                if(v.x < 0) {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[1]
                        [(int)((-v.y / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((v.z / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 1
                } else {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[3]
                        [(int)((-v.y / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((-v.z / v.x * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 3
                }
                // greatest is y - buffer 1 or 3
            } else {
                if(v.y < 0) {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[2]
                        [(int)((v.x / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((v.z / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 2
                } else {
                    p_buffer[x + y * n_buffer_w] =
                        p_s_hemicube_buffer_side[0]
                        [(int)((v.x / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) +
                         (int)((-v.z / v.y * .5f + .5f) *
                         (n_hemicube_size - 1)) * n_hemicube_size];
                    // 0
                }
                // greatest is y - buffer 0 or 2
            }
        }
    }
}

If you delete comment mark ahead of Modulate_HemicubeBuffers();, you'll see hemicube image modulated by form-factors. I've left it this way because of speed purposes. The result is nicely looking fish-eye like transform: (it's viewed from another patch than before)

hemicube contents

    Now you're propably interested in function for energy "shooting". Classic model is to draw patch-id buffer and then go trough it's pixels and add corresponding energies to each patch, certain pixel points to. But we have lightmaps and it would be a bit difficult to index lightmap texels, although not impossible. Say we have maximal resolution of lightmap 512×512 so index to lightmap take up 2 × 9 = 18 bits (2 times for x and y texel coordinate; 512 = 29) We have 14 (32 - 18) bits left for lightmap page index, allowing us to have 16k of lightmaps. (16k = 16.1024 = 16384)
    We're going to take some advantage of s-buffer, namely zero overdraw. We'll modify lightmap format so we have 6 float values for each texel (2 RGB triplets for illuminative and radiative energy) We'll illuminate lightmap texels same time we render image for cubemap, because we need lightmap (illuminative) values there anyway - and radiative are just next three floats in row ! We need do some modifications in order to support that format. We need (among others) modify lightmap allocation function. That's simple one:


TLightmapTex *p_Lightmap(int n_width, int n_height)
{
    TLightmapTex *p_lightmap;

    if(!(p_lightmap = (TLightmapTex*)malloc(sizeof(TLightmapTex))))
        return 0;
    // alloc lightmap structure data

    p_lightmap->n_width = n_width;
    p_lightmap->n_height = n_height;
    // set lightmap dimensions

    if(!(p_lightmap->p_frontline = (int*)malloc(n_width * sizeof(int))))
        return 0;
    memset(p_lightmap->p_frontline, 0, n_width * sizeof(int));
    // alloc and clear frontline

    if(!(p_lightmap->p_bytes = (float*)malloc(n_width *
       n_height * sizeof(float) * 6)))
        return 0;
    memset(p_lightmap->p_bytes, 0, n_width * n_height * sizeof(float) * 6);
    // alloc buffer for light values and clear it

    return p_lightmap;
}

That's the same function as last time in lightmap-mapping utility. The only change is it alloc 6 floats instead of single __int32 per pixel. Next change is in lightmap content creation function:


void Raytrace_Tile(Group *p_group, Mapping_Info *p_mapping_info, World *p_world,
                   int n_x, int n_y, int n_h, int n_w, int n_p_w,
                   float *p_buffer)
{
    float f_radiative[3];
    Vector v_texel_pos;
    Vector v_pos_v;
    int b_contain;
    //float r, g, b;
    //Vector v_ray;
    int x, y, i;
    //float a;
    //int j;

    for(y = 0; y < n_h; y ++) {
        v_pos_v = p_mapping_info->v_org;
        v_pos_v.x += p_mapping_info->v_v.x * (float)y /
            (float)n_h * p_mapping_info->f_height;
        v_pos_v.y += p_mapping_info->v_v.y * (float)y /
            (float)n_h * p_mapping_info->f_height;
        v_pos_v.z += p_mapping_info->v_v.z * (float)y /
            (float)n_h * p_mapping_info->f_height;
        //
        for(x = 0; x < n_w; x ++) {
            v_texel_pos.x = p_mapping_info->v_u.x * (float)x /
                (float)n_w * p_mapping_info->f_width + v_pos_v.x;
            v_texel_pos.y = p_mapping_info->v_u.y * (float)x /
                (float)n_w * p_mapping_info->f_width + v_pos_v.y;
            v_texel_pos.z = p_mapping_info->v_u.z * (float)x /
                (float)n_w * p_mapping_info->f_width + v_pos_v.z;
            // calculate lightmapy texel worldspace position

            switch(p_mapping_info->n_plane) {
            case Plane_xy:
                v_texel_pos.z = -(v_texel_pos.x * p_group->p_face[0]->normal.a +
                                  v_texel_pos.y * p_group->p_face[0]->normal.b +
                                  p_group->p_face[0]->normal.d) /
                                  p_group->p_face[0]->normal.c;
                break;
            case Plane_xz:
                v_texel_pos.y = -(v_texel_pos.x * p_group->p_face[0]->normal.a +
                                  v_texel_pos.z * p_group->p_face[0]->normal.c +
                                  p_group->p_face[0]->normal.d) /
                                  p_group->p_face[0]->normal.b;
                break;
            case Plane_yz:
                v_texel_pos.x = -(v_texel_pos.y * p_group->p_face[0]->normal.b +
                                  v_texel_pos.z * p_group->p_face[0]->normal.c +
                                  p_group->p_face[0]->normal.d) /
                                  p_group->p_face[0]->normal.a;
                break;
            }
            // project texel position from
            // imaginative rectangle to plane faces are in ...

            f_radiative[0] = 0;
            f_radiative[1] = 0;
            f_radiative[2] = 0;

            b_contain = false;
            for(i = 0; i < p_group->n_face_used; i ++) {
                if(b_Contain_Point(v_texel_pos, p_group->p_face[i])) {
                    if(p_group->p_face[i]->f_radiative_r > 0.01 ||
                       p_group->p_face[i]->f_radiative_g > 0.01 ||
                       p_group->p_face[i]->f_radiative_b > 0.01) {
                        f_radiative[0] = p_group->p_face[i]->f_radiative_r;
                        f_radiative[1] = p_group->p_face[i]->f_radiative_g;
                        f_radiative[2] = p_group->p_face[i]->f_radiative_b;
                    }
                    b_contain = true;
                    break;
                }
            }
            // check if texel is contained by some of faces

            p_buffer[ ((x + n_x) + (y + n_y) * n_p_w) * 6     ] = f_radiative[0];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 1] = f_radiative[1];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 2] = f_radiative[2];
            // HDR illumination color

            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 3] = f_radiative[0];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 4] = f_radiative[1];
            p_buffer[(((x + n_x) + (y + n_y) * n_p_w) * 6) + 5] = f_radiative[2];
            // HDR radiative color

            // convert color to RGB and store it
            // into certain lightmap array elements ...
        }
    }
}

It's almost nothing new at here, we just check if certain texel lie on some of faces and if that face has positive radiative energy, we assign it to that texel. We should count on texel area at here ! We assign some energy to illuminative color aswell so lights are bright, instead of black.
    I should show you how to render span with such a lightmap format. It's slightly modified function from the previous tutorial example:


static void _fastcall Interpolate_Segment_Bilerp(int u1, int du, int v1, int dv,
    int lu1, int ldu, int lv1, int ldv,
    unsigned __int32 *video, unsigned __int32 *v2, int x, int y)
{
    register int frac, tex, col;

    y = (y & 3) << 2;
    do {
        tex = ((u1 >> 16) & A_1) + ((v1 >> B_1) & A_2);
        // texel address

        frac = ((u1 >> 10) & 0x3f) + ((v1 >> 4) & 0xfc0);
        // fractional texel address (6 bits of V + 6 bits of U)

        col = _palette[(_texture[tex] << 6) + multab[0][frac]] +
              _palette[(_texture[(tex + 1) & A_3] << 6) + multab[1][frac]] +
              _palette[(_texture[(tex + W_0) & A_3] << 6) + multab[2][frac]] +
              _palette[(_texture[(tex + W_1) & A_3] << 6) + multab[3][frac]];
        // bilinear interpolation

        tex = (((lu1 + dither_u[frac = (y | (x & 3))]) >> 16) & LA_1) +
              (((lv1 + dither_v[frac]) >> LB_1) & LA_2);
        tex = (tex << 1) + (tex << 2);

        int b = (int)(((col & 0x0000ff) * _lightmap[tex + 2]) / 256);
        b = (b > 0x0000ff)? 0x0000ff : b & 0x0000ff;
        col >>= 8;
        int r = (col & 0x00ff00);
        r = ((r * _lightmap[tex]) > 0xff0000)? 0xff0000 :
            (int)(r * _lightmap[tex]) & 0xff0000;
        int g = (col & 0x0000ff);
        g = ((g * _lightmap[tex + 1]) > 0x00ff00)? 0x00ff00 :
            (int)(g * _lightmap[tex + 1]) & 0x00ff00;

        *video ++ = r | g | b;
        // lit ...

        x ++;
        // next pixel

        u1 += du;
        v1 += dv;
        lu1 += ldu;
        lv1 += ldv;
        // coordinate interpolation
    } while(video < v2);
}

We determine diffuse color, texel index in lightmap, multiply it by six (we have six floats per pixel as i explained you above; multiplying by six can be performed a bit faster by adding bit shift left by 2 and 1) Then add 0, 1, 2 to index and we have illumination color values. Then modulate texture color and write to video-buffer. Texture is bilinear filtered, lightmaps are dithered.
    I'll show you one more function, used for "shooting" energy along with drawing hemicube contents:


static float *p_cur_form_factors;
// pointer to corresponding form-factors of hemicube

static float f_radiative_r;
static float f_radiative_g;
static float f_radiative_b;
// radiative energiy of "shooting" patch


static void _fastcall Interpolate_Segment_Radiate(int u1, int du, int v1, int dv,
    int lu1, int ldu, int lv1, int ldv,
    unsigned __int32 *video, unsigned __int32 *v2, int x, int y)
{
    register int frac, tex, col;

    do {
        tex = ((u1 >> 16) & A_1) + ((v1 >> B_1) & A_2);
        // texel address

        frac = ((u1 >> 10) & 0x3f) + ((v1 >> 4) & 0xfc0);
        // fractional texel address (6 bits of V + 6 bits of U)

        col = _palette[(_texture[tex] << 6) + multab[0][frac]] +
              _palette[(_texture[(tex + 1) & A_3] << 6) + multab[1][frac]] +
              _palette[(_texture[(tex + W_0) & A_3] << 6) + multab[2][frac]] +
              _palette[(_texture[(tex + W_1) & A_3] << 6) + multab[3][frac]];
        // bilinear interpolation

        tex = (((lu1 + dither_u[frac = (((y & 3) << 2) | (x & 3))]) >> 16) & LA_1) +
              (((lv1 + dither_v[frac]) >> LB_1) & LA_2);
        tex = (tex << 1) + (tex << 2);
        // texel index in lightmap

        float f_form_factor = p_cur_form_factors[x + y * n_Width];

        _lightmap[tex + 3] += f_radiative_r * f_form_factor * __reflectance__ *
            ((float)((col >> 16) & 0xff) / 256 + (__bleeding__ - 1)) / __bleeding__;
        _lightmap[tex + 4] += f_radiative_g * f_form_factor * __reflectance__ *
            ((float)((col >> 8) & 0xff) / 256 + (__bleeding__ - 1)) / __bleeding__;
        _lightmap[tex + 5] += f_radiative_b * f_form_factor * __reflectance__ *
            ((float)(col & 0xff) / 256 + (__bleeding__ - 1)) / __bleeding__;
        //
        _lightmap[tex    ] += f_radiative_r * f_form_factor * (1 - __reflectance__);
        _lightmap[tex + 1] += f_radiative_g * f_form_factor * (1 - __reflectance__);
        _lightmap[tex + 2] += f_radiative_b * f_form_factor * (1 - __reflectance__);
        // radiate (float should never overflow)

        int b = (int)(((col & 0x0000ff) * _lightmap[tex + 2]) / 256);
        b = (b > 0x0000ff)? 0x0000ff : b & 0x0000ff;
        col >>= 8;
        int r = (col & 0x00ff00);
        r = ((r * _lightmap[tex]) > 0xff0000)? 0xff0000 :
            (int)(r * _lightmap[tex]) & 0xff0000;
        int g = (col & 0x0000ff);
        g = ((g * _lightmap[tex + 1]) > 0x00ff00)? 0x00ff00 :
            (int)(g * _lightmap[tex + 1]) & 0x00ff00;

        *video ++ = r | g | b;
        // lit ...

        x ++;
        // next pixel

        u1 += du;
        v1 += dv;
        lu1 += ldu;
        lv1 += ldv;
        // coordinate interpolation
    } while(video < v2);
}

Here we get diffuse color again, calculate index of texel in lightmap, read corresponding form-factor (... see how usefull x and y used for dithering is !) and add corresponding radiative and illuminative energy. When computing radiative energy, we perform some color mixing (radiative and pixel diffuse color) in order to get color bleeding. Then return lit pixel color as we'd normally do so we can render hemicube contents to screen when user like to.     Now you should understand how does the example work. I'll put here a brief list of steps it goes trough:
  • create window, init direct-x, etc ...
  • call Calc_HemicubeFormFactors(); - calculate hemicube form-faktors
  • load our world and place lightmaps on it Create_Lightmaps("sshock_room_1obj.3ds", &t_svet)
    (That's where some faces get radiative energies. it's color is color of nearest light. We should count with texel area !)
  • load textures


  • drawing loop
    • find texel with greatest radiative energy (+ his worldspace normal and position)
    • render hemicube (along with shooting radiative energy)
    • refresh one-pixel frames arround lightmaps
    • render screen
    • render screen with lower brightness, blur it and add to previous one
    • display the result
    Ahh, i remember. You could find function for greatest radiative energy patch searching a bit confusing. I'll explain it a bit:


float f_Find_GreatestRadiativeEnergy(Mesh *p_mesh, Vector *v_texel_pos,
    Vector *v_texel_normal, int *n, Vector *v_rad_color)
{
    float f_max_energy, f_energy;
    int n_max_j, n_max_page;
    Group *p_group;
    int x, y;
    int i, j;

    while(1) {
        f_max_energy = -1;
        n_max_j = -1;
        n_max_page = -1;
        for(i = 0; i < p_mesh->n_lightmap_num; i ++) {
            for(j = 0; j < p_mesh->p_lightmap[i].n_width *
               p_mesh->p_lightmap[i].n_height; j ++) {
                if((f_energy = f_Energy(
                   p_mesh->p_lightmap[i].p_buffer[(j * 6) + 3],
                   p_mesh->p_lightmap[i].p_buffer[(j * 6) + 4],
                   p_mesh->p_lightmap[i].p_buffer[(j * 6) + 5])) > f_max_energy) {
                    f_max_energy = f_energy;
                    n_max_j = j;
                    n_max_page = i;
                }
            }
        }
        // find maximal radiative energy in one of lightmaps ...

        if(n_max_j == -1)
            return -1;

        for(i = 0; i < p_mesh->p_g_data->n_group_num; i ++) {
            if(p_mesh->p_g_data->p_group[i].t_rect.n_page != n_max_page)
                continue;

            x = n_max_j % p_mesh->p_lightmap[n_max_page].n_width;
            y = n_max_j / p_mesh->p_lightmap[n_max_page].n_width;
            // texel lightmap-uv-space coordinates ...

            p_group = &p_mesh->p_g_data->p_group[i];
            //
            x -= p_group->t_rect.x;
            y -= p_group->t_rect.y;
            //
            if(x >= 0 && x <= p_group->t_rect.w &&
               y >= 0 && y <= p_group->t_rect.h) {
                // we've found group texel belongs to ...

                v_texel_pos->x = p_mesh->p_m_info[i].v_org.x +

                    p_mesh->p_m_info[i].v_v.x * (float)(y + .5f) /
                    (float)p_group->t_rect.h *
                    p_mesh->p_m_info[i].f_height +

                    p_mesh->p_m_info[i].v_u.x * (float)(x + .5f) /
                    (float)p_group->t_rect.w *
                    p_mesh->p_m_info[i].f_width;
                //
                v_texel_pos->y = p_mesh->p_m_info[i].v_org.y +

                    p_mesh->p_m_info[i].v_v.y * (float)(y + .5f) /
                    (float)p_group->t_rect.h *
                    p_mesh->p_m_info[i].f_height +

                    p_mesh->p_m_info[i].v_u.y * (float)(x + .5f) /
                    (float)p_group->t_rect.w *
                    p_mesh->p_m_info[i].f_width;
                //
                v_texel_pos->z = p_mesh->p_m_info[i].v_org.z +

                    p_mesh->p_m_info[i].v_v.z * (float)(y + .5f) /
                    (float)p_group->t_rect.h *
                    p_mesh->p_m_info[i].f_height +

                    p_mesh->p_m_info[i].v_u.z * (float)(x + .5f) /
                    (float)p_group->t_rect.w *
                    p_mesh->p_m_info[i].f_width;
                //
                switch(p_mesh->p_m_info[i].n_plane) {
                case Plane_xy:
                    v_texel_pos->z =
                        -(v_texel_pos->x * p_group->p_face[0]->normal.a +
                        v_texel_pos->y * p_group->p_face[0]->normal.b +
                        p_group->p_face[0]->normal.d) /
                        p_group->p_face[0]->normal.c;
                    break;
                case Plane_xz:
                    v_texel_pos->y =
                        -(v_texel_pos->x * p_group->p_face[0]->normal.a +
                        v_texel_pos->z * p_group->p_face[0]->normal.c +
                        p_group->p_face[0]->normal.d) /
                        p_group->p_face[0]->normal.b;
                    break;
                case Plane_yz:
                    v_texel_pos->x =
                        -(v_texel_pos->y * p_group->p_face[0]->normal.b +
                        v_texel_pos->z * p_group->p_face[0]->normal.c +
                        p_group->p_face[0]->normal.d) /
                        p_group->p_face[0]->normal.a;
                    break;
                }
                // calculate texel worldspace position ...

                *n = p_group->p_face[0]->n_id;
                // face id

                v_texel_normal->x = p_group->p_face[0]->normal.a;
                v_texel_normal->y = p_group->p_face[0]->normal.b;
                v_texel_normal->z = p_group->p_face[0]->normal.c;
                // texelu normal

                *v_rad_color = v_Energy(
                    p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 3],
                    p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 4],
                    p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 5]);
                // copy radiative energy ...

                p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 3] = 0;
                p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 4] = 0;
                p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 5] = 0;
                // clear radiative energy ...

                return f_max_energy;
            }
        }

        p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 3] = 0;
        p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 4] = 0;
        p_mesh->p_lightmap[n_max_page].p_buffer[(n_max_j * 6) + 5] = 0;
        // because of rasterizer errors we have pixel with radiative energy
        // that belongs to no face ... we'll clear it and try again ...
    }

    return -1;
}

On the beginning we simply find texel with greatest radiative energy in one of lightmap pages, then sequentially go trough all mapping groups and check if texel belongs to one of them. Then we have normal (all faces in group does lie in same plane, remember ?) Then find face containing this texel, return it's id, calculate worldspace position, return radiative color and clear it in lightmap. Anyway, our rasterizer has some errors and sometimes it happens texel that doesn't belong to none of faces render to hemicube - and get some radiative energy. In that case, we simply clear it and try again (that's why all the code is in the loop) When all radiative energies are zero, function return -1.
    Example code immediately begins shooting energy. You can toggle it by pressing 'I', the 'P' key display radiating patch and one of faces in it's plane, 'F' disable hemicube display, 'G' enables the simple one and 'H' enables spherical one. Pressing 'R' you can toggle radiative energies display instead of illuminative and pressing 'Q' you enable post-production. Lightmaps eventually seems a bit handicapped because texels on borders of objects are for example half-visible and they are darker than they should be. You can solve this by pressing 'B' what causes all lightmaps to blur. You should do it only once when you want screenshot your results, otherwise all shadows will blur away.
    What about results ? I've drawn a room in System Shock - mood and after some half hour (we have high resolution lightmaps - thus lots of patches) it lits perfectly. I've noticed a few errors in lightmap mapping, but i'm not going to solve them now. View some screenshots:

illumination

illumination

illumination

... and eventually one more shot, showing color bleeding in action: (i've switched two textures for white and red one - result is reddish glimmer you can see on white texture as well as on the box)

color bleeding

    So, you can download our tiny

 radiosity example 2

... and play arround with it ! We'll do some finishing touches next time and then: first OpenGL tutorial !

    -tHE SWINe-



Valid HTML 4.01!
Valid HTML 4.01