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;
}
}
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.
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?
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\$