5
\$\begingroup\$

I'm working on a radiosity processor in DirectX 9. I have efficiently rendered hemicubes from the perspective of texels within a lightmap. Now, as I try to integrate the hemicubes (to sum the incident light that can be seen from the POV of a single texel within the lightmap) I run into a problem.

This tutorial says a hemicube is integrated by adding the colors from each pixel of the hemicube (after they have been scaled by the multiplier map) and then dividing the sum of colors by the total pixel count of the hemicube. The author says the sum of all pixels in the multiplier map should add up to 1... If this is true, why divide the sum of incident light by the number of pixels in the map?

How is a multiplier map created?

How should I handle the integration of hemicubes?

Edited to update changes - now normalizes multiplier map

Here is pseudocode for integration of a single hemicube surface:

// set the max hemicube value using pre-calculated values
   FLOAT fHemicubeMaxValue;
   switch( uHemicubeSize )
   {
      case 32: fHemicubeMaxValue = 1.23e-3f; break;
      case 64: fHemicubeMaxValue = 3.11e-4f; break;
      case 128: fHemicubeMaxValue = 7.77e-5f; break;
   }

   // lock the hemicube render target
   BYTE *HemicubePixels = Lock( HemicubeSurface );

   // temp color
   D3DXVECTOR4 vColor( 0, 0, 0, 0 );

   // sum colors
   for( int y = 0; y < hemicubeHeight; ++y )
       for( int x = 0; x < hemicubeWidth; ++x )
           vColor += ToColor( HemicubePixels[ y * hemicubeWidth + x ] * ToColor( MultiplierMap[ y * hemicubeWidth + x ] * fHemicubeMaxValue );

During integration the hemicube pixels are scaled by the normalized multiplier map so that the incident color components will add up to a value between 0 and 1.

The implementation of radiosity renders hemicubes to a 'master' surface that holds many hemicubes. When the master hemicube surface has reached capacity then incident light is calculated by integrating the hemicubes within the master surface, thus reducing calls to IDirect3DDevice9::GetRenderTargetData() and IDirect3DTexture9::LockRect(). A 'current hemicube' index is maintained and this index is stored within each LumelData struct for lookup during the call to CalcIncidentLight().

Here is pseudocode of entire radiosity implementation:

// pass counters
UINT uiPass = -1; ucReflectivePasses = ...;

// iterate passes
while( ++uiPass < ucReflectivePasses )
{    
   // iterate scene mesh objects
   for each mesh in meshCollection:
   {
      // current surface
      UINT uiSurface = -1;

      // declare current hemicube index
      UINT uiCurHemicube = 0;

      // point to mesh radiative surface( render target )
      meshRadiativeSurface = mesh.GetRadiativeSurface();  

      // point to mesh residual surface( render target )
      meshResidualSurface = mesh.GetResidualSurface(); 

      // storage vector to hold LumelData pointers for lumels whose hemicubes have been rendered to the master hemicube surface.
      // A LumelData struct contains the world space pos, world space normal, uv coord, and index of the lumel's hemicube partition within the master hemicube surface
      std::vector< LumelData* > stgLumelData;

      for each surface in mesh:
      {
         // inc surface counter
         ++uiSurface;

         // get surface world space positions( used to calc a lumel's w.s. pos )
         D3DXVECTOR3 pvSurfacePos[ 3 ];
         memcpy( &pvSurfacePos, mesh.vertex_data[ uiSurface ].pos, 3 * sizeof( D3DXVECTOR3 ) );

         // get surface normal
         D3DXVECTOR3 vSurfaceNormal = mesh.vertex_data[ uiSurface ].normal;

         // get surface light map uv coords
         D3DXVECTOR2 pvSurfaceUV[ 3 ];
         memcpy( &pvSurfaceUV, mesh.vertex_data[ uiSurface ].uv, 3 * sizeof( D3DXVECTOR2 ) );

         // get surface material
         Material matSurface = mesh.materials[ mesh.attribute_data[ uiSurface ] ];

         // half pixel
         D3DXVECTOR2 vHalfPixel( 0.5f / uLightMapWidth, 0.5f / uLightMapHeight );

         // determine lumel coverage using the surface's light map uv coordinates
         RasterizeTri( uLightMapWidth, uLightMapHeight, pvSurfaceUV ) 
         {    
            // calculate edge planes( perpendicular to edge vector )
            // allocate and fill an array of three EdgeData structs
            // determine bounds( min x, max x, min y, max y )
            EdgeData *pEdgeData = NULL;
            D3DXVECTOR4 vSurfaceBounds;
            BuildEdgeData( vHalfPixel, pvSurfaceUV, &vSurfaceBounds, &pEdgeData );

            // set cur scanline position to the first
            int nCurYPos = vSurfaceBounds.z;

            // iterate scanlines
            while( nCurYPos <= vSurfaceBounds.w )             
            {
               // iterate scanline pixels
               int nCurXPos = vSurfaceBounds.x;
               while( nCurXPos <= vSurfaceBounds.y )
               {
                  // determine uv coord of the current lumel
                  D3DXVECTOR2 vLumelUV( nCurXPos / uLightMapWidth + vHalfPixel.x, nCurXPos / uLightMapHeight + vHalfPixel.y );

                  // validate the lumel's position inside the surface using the 'point-behind-plane' test
                  bool bValidLumel = true;
                  UINT ui = -1;
                  while( ++ui < 3 )                   
                  {   
                      if( !IsPointBehindPlane( &vLumelUV, &pEdgeData[ ui ]->pEdgePlane ) )
                      { bValidLumel = false; break; }
                  }

                  if lumel lies behind all three edges( bValidLumel == true ):
                  {
                     // fill a LumelData struct with the lumel's:
                     //   world space pos( calculated using the surface positions, surface uvs, and lumel uv ) 
                     //   world space normal
                     //   lightmap uv coord
                     //   index within the master hemicube surface
                     LumelData *pLumelData = NULL;
                     BuildLumelData( pvSurfacePos, vSurfaceNormal, pvSurfaceUV, &vLumelUV, &pLumelData, uiCurHemicube++ );

                     // store the LumelData pointer to vector
                     stgLumelData.push_back( pLumelData );

                     // render hemicube
                     // draw the residual energy of scene objects( including the skybox ) to the master hemicube surface
                     SceneVisibility( &pLumelData )
                     {
                        // draw the scene
                        DrawScene();

                        // draw the skybox on the first pass only
                        if( !uiPass )DrawSkybox();

                        if the master hemicube surface has reached capacity( pLumelData->uiHemicube >= ucMaxHemicubePartitions ):
                           // calc incident light for each lumel in the storage vector by integrating the corresponding hemicube
                           // write the incident light directly to the radiative surface( alpha blending is used to add the incident light to any existing radiative energy for that lumel )
                           // write the residual energy as calculated using this formula: residual energy = incident light * surface reflectivity + surface emission  
                           CalcIncidentLight( hemicubeMasterSurface, meshRadiativeSurface, meshResidualSurface, &matSurface, &stgLumelData );
                           ui = 0;
                           while( ui++ < stgLumelData.size() )
                              delete stgLumelData[ ui ];
                           stgLumelData.clear();
                           uiCurHemicube = 0;
                      }
                  }
                  nCurXPos++;            
               }
               nCurYPos++;
            }
         }
      }
      // calc incident light for any remaining lumels within the storage vector
      CalcIncidentLight( hemicubeMasterSurface, meshRadiativeSurface, meshResidualSurface, &surface.material, &stgLumelData );
      ui = 0;
      while( ui++ < stgLumelData.size() )
         delete stgLumelData[ ui ];
      stgLumelData.clear();
      uiCurHemicube = 0;
   }
}

Radiosity Screenshot

This is a screenshot taken from the outside of a structure with 2 windows...At the moment the structure is shaded with the lightmap only! The skybox (all white textures for debug) surrounds the structure and is used as the only source of light in the scene.

Radiosity Screenshot2

This is a screenshot from the inside of the structure. You can see very little light covers the geometry.

The structure's outside is entirely white, indicating the form factor calculations are correct. The hemicubes used on the outside of the structure were covered in white color from the skybox, then modulated with the multiplier map and scaled by the maximum form factor value 0.0013 for a 96x32 hemicube, the integration of which adds up to approximately 1...

If the walls inside the structure see very bright light (although in limited quantities), should I expect to see such miniscule amounts of light inside the structure?

\$\endgroup\$
  • 1
    \$\begingroup\$ According to small amount of light in your pictures I think it's OK. If you put a 90 deg FOV camera on the left wall, pointing at window then the window could be about 1/10 of entire image. This alone gives you 1 bright pixel per 10 dark ones on your hemicube. Secondly, you get light only for front faces, which is 1/3 of entire hemicube (I know front face has more influence but still...). You're also doing just one light bounce - if you run the same few times, then more light should bleed through your scene. \$\endgroup\$ – kolenda Mar 24 '14 at 11:28
  • \$\begingroup\$ @kolenda the residual light calculation is: energy = i * r + emission. where i = incident light( from integrated hemicubes ), r = reflectivity of the material, and emission is the amount of light emitted by the surface material...i can run 24 passes that would look very close to the same as the 2nd or 3rd pass. I'm going to expand the pseudocode above so as to better describe my implementation of the radiosity algorithm \$\endgroup\$ – P. Avery Mar 25 '14 at 0:28
  • \$\begingroup\$ The point I'm trying to make is that black pixels contribute to the illumination of a surface...shouldn't I be more concerned with what can be seen? What if I re-normalize the form factor values and exclude hemicube pixels below a certain threhold? But then integration of a hemicube with one white pixel and all others black would result in a white color...Radiosity being the transfer of heat between surfaces makes sense of low light in the room...consider black pixels 'cool' areas of the scene that cancel out the intensity of 'hot' areas... \$\endgroup\$ – P. Avery Mar 25 '14 at 18:27
  • \$\begingroup\$ That's what I said - black pixels doesn't contribute directly to the result, but each black pixel means the 'lack' of white pixel at this point. So the more blacks you have - the smaller is the result. Maybe you should try to use floats and make the 'sun' much brighter? \$\endgroup\$ – kolenda Mar 26 '14 at 17:24
  • \$\begingroup\$ how can the sun be brighter than white? If I scale the result then the system becomes unbalanced and likely won't resolve...the residual color becomes greater with each pass... \$\endgroup\$ – P. Avery Mar 26 '14 at 23:05
1
\$\begingroup\$

You need to weight the samples by the cosine of the sample and the forward direction of the hemicube. This is because the amount the texel contributes is related to what its incident angle is from the normal. You can precompute these cosine values offline for each pixel since they are the same for every hemicube, assuming all your hemicubes are oriented the same relative to the surface normal. That's what the "multiplier map" contains. However this only is accurate for a polar projection

In the code you linked they also add another term which accounts for the perspective distortion of storing the texels in a cube which is rendered with 90 degree FOVs. This means that the samples near the seams are less direct facing. This makes sense if you think of the distance to a plane perpendicular to your viewing direction. It's closest in the center. So they combine these two terms together into multiplayer map.

Finally, they normalize the map. Since each hemicube is the same resolution you can bake in the constant 1.0 / ( hemicubeWidth * hemicubeHeight ) term.

This makes the integration code very simple:

// lock the hemicube render target
BYTE *HemicubePixels = Lock( HemicubeSurface );
FLOAT *MultiplierMap = {...};
// temp color
D3DXVECTOR4 vColor( 0, 0, 0, 0 );

// sum colors
for( int y = 0; y < hemicubeHeight; ++y )
    for( int x = 0; x < hemicubeWidth; ++x )
        vColor += ToColor( HemicubePixels[ y * hemicubeWidth + x ]) * MultiplierMap[ y * hemicubeWidth + x];

// vColor is the result
\$\endgroup\$
  • \$\begingroup\$ from the link i provided i got the impression that the values of pixel colors within the map were to be summed and then each pixel value was to be divided by the sum of pixel colors within the multiplier map...from the tutorial: The sum of all pixels in the map should be 1.0. Sum the total value of all pixels in the Multiplier Map. Divide each pixel by this value. Now, the value at the centre of the map will be much less than 1.0. Another question: The image of the multiplier map on the tutorial has white in the center. doesn't that mean that the value at the center is 1? is the image valid? \$\endgroup\$ – P. Avery Mar 22 '14 at 21:21
  • 1
    \$\begingroup\$ Yeah, sorry you are right: 1.0 / ( hemicubeWidth * hemicubeHeight ) assumes the entire map is 1.0. "Now, the value at the centre of the map will be much less than 1.0" Yeah that seems correct. Maybe the images are unscaled or the author has multiplied by 255.0 to fit it better into an 8-bit texture? That tutorial is pretty old. These days I would use a fp16 probably for better precision, or even better just do the multiplication in the pixel shader as you output the hemicube images. You could use VPOS and a known hemicube size. \$\endgroup\$ – Lucas Mar 22 '14 at 22:40
  • \$\begingroup\$ I am now using the suggestions you've made: 64bit color surfaces, and hemicube/multiplier map modulation in the pixel shader...These helped...The problem is that the multiplier map practically makes negligent texels that receive small amounts of light...The form factor for the center pixel of a 96x32 hemicube is 1.23e-3. If the only area of a hemicube that sees light is towards the center...the incident light will be negligible...even though it may be considerable otherwise...These areas of the scene are darker than expected...how is this typically handled? \$\endgroup\$ – P. Avery Mar 23 '14 at 2:56
  • \$\begingroup\$ I've added an image to help explain the problem... \$\endgroup\$ – P. Avery Mar 23 '14 at 2:58
  • \$\begingroup\$ Honestly, I'm not sure. I think mathematically it makes sense, since if you are seeing an entire white cube then the sum of all the pixels should be one. I would say just try scaling/HDR but it seems like you've tried that based on your other comments. \$\endgroup\$ – Lucas Mar 28 '14 at 1:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.