|
|
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
//=============================================================================//
#include "vrad.h"
#include "leaf_ambient_lighting.h"
#include "bsplib.h"
#include "vraddetailprops.h"
#include "mathlib/anorms.h"
#include "pacifier.h"
#include "coordsize.h"
#include "vstdlib/random.h"
#include "bsptreedata.h"
#include "messbuf.h"
#include "vmpi.h"
#include "vmpi_distribute_work.h"
static TableVector g_BoxDirections[6] = { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 1, 0 }, { 0, -1, 0 }, { 0, 0, 1 }, { 0, 0, -1 }, };
static void ComputeAmbientFromSurface( dface_t *surfID, dworldlight_t* pSkylight, Vector& radcolor ) { if ( !surfID ) return;
texinfo_t *pTexInfo = &texinfo[surfID->texinfo];
// If we hit the sky, use the sky ambient
if ( pTexInfo->flags & SURF_SKY ) { if ( pSkylight ) { // add in sky ambient
VectorCopy( pSkylight->intensity, radcolor ); } } else { Vector reflectivity = dtexdata[pTexInfo->texdata].reflectivity; VectorMultiply( radcolor, reflectivity, radcolor ); } }
// TODO: it's CRAZY how much lighting code we share with the engine. It should all be shared code.
float Engine_WorldLightAngle( const dworldlight_t *wl, const Vector& lnormal, const Vector& snormal, const Vector& delta ) { float dot, dot2;
Assert( wl->type == emit_surface );
dot = DotProduct( snormal, delta ); if (dot < 0) return 0;
dot2 = -DotProduct (delta, lnormal); if (dot2 <= ON_EPSILON/10) return 0; // behind light surface
return dot * dot2; }
// TODO: it's CRAZY how much lighting code we share with the engine. It should all be shared code.
float Engine_WorldLightDistanceFalloff( const dworldlight_t *wl, const Vector& delta ) { Assert( wl->type == emit_surface );
// Cull out stuff that's too far
if (wl->radius != 0) { if ( DotProduct( delta, delta ) > (wl->radius * wl->radius)) return 0.0f; }
return InvRSquared(delta); }
void AddEmitSurfaceLights( const Vector &vStart, Vector lightBoxColor[6] ) { fltx4 fractionVisible;
FourVectors vStart4, wlOrigin4; vStart4.DuplicateVector ( vStart );
for ( int iLight=0; iLight < *pNumworldlights; iLight++ ) { dworldlight_t *wl = &dworldlights[iLight];
// Should this light even go in the ambient cubes?
if ( !( wl->flags & DWL_FLAGS_INAMBIENTCUBE ) ) continue;
Assert( wl->type == emit_surface );
// Can this light see the point?
wlOrigin4.DuplicateVector ( wl->origin ); TestLine ( vStart4, wlOrigin4, &fractionVisible ); if ( !TestSignSIMD ( CmpGtSIMD ( fractionVisible, Four_Zeros ) ) ) continue;
// Add this light's contribution.
Vector vDelta = wl->origin - vStart; float flDistanceScale = Engine_WorldLightDistanceFalloff( wl, vDelta );
Vector vDeltaNorm = vDelta; VectorNormalize( vDeltaNorm ); float flAngleScale = Engine_WorldLightAngle( wl, wl->normal, vDeltaNorm, vDeltaNorm );
float ratio = flDistanceScale * flAngleScale * SubFloat ( fractionVisible, 0 ); if ( ratio == 0 ) continue;
for ( int i=0; i < 6; i++ ) { float t = DotProduct( g_BoxDirections[i], vDeltaNorm ); if ( t > 0 ) { lightBoxColor[i] += wl->intensity * (t * ratio); } } } }
void ComputeAmbientFromSphericalSamples( int iThread, const Vector &vStart, Vector lightBoxColor[6] ) { // Figure out the color that rays hit when shot out from this position.
Vector radcolor[NUMVERTEXNORMALS]; float tanTheta = tan(VERTEXNORMAL_CONE_INNER_ANGLE);
for ( int i = 0; i < NUMVERTEXNORMALS; i++ ) { Vector vEnd = vStart + g_anorms[i] * (COORD_EXTENT * 1.74);
// Now that we've got a ray, see what surface we've hit
Vector lightStyleColors[MAX_LIGHTSTYLES]; lightStyleColors[0].Init(); // We only care about light style 0 here.
CalcRayAmbientLighting( iThread, vStart, vEnd, tanTheta, lightStyleColors ); radcolor[i] = lightStyleColors[0]; }
// accumulate samples into radiant box
for ( int j = 6; --j >= 0; ) { float t = 0;
lightBoxColor[j].Init();
for (int i = 0; i < NUMVERTEXNORMALS; i++) { float c = DotProduct( g_anorms[i], g_BoxDirections[j] ); if (c > 0) { t += c; lightBoxColor[j] += radcolor[i] * c; } } lightBoxColor[j] *= 1/t; }
// Now add direct light from the emit_surface lights. These go in the ambient cube because
// there are a ton of them and they are often so dim that they get filtered out by r_worldlightmin.
AddEmitSurfaceLights( vStart, lightBoxColor ); }
bool IsLeafAmbientSurfaceLight( dworldlight_t *wl ) { static const float g_flWorldLightMinEmitSurface = 0.005f; static const float g_flWorldLightMinEmitSurfaceDistanceRatio = ( InvRSquared( Vector( 0, 0, 512 ) ) );
if ( wl->type != emit_surface ) return false;
if ( wl->style != 0 ) return false;
float intensity = max( wl->intensity[0], wl->intensity[1] ); intensity = max( intensity, wl->intensity[2] ); return (intensity * g_flWorldLightMinEmitSurfaceDistanceRatio) < g_flWorldLightMinEmitSurface; }
class CLeafSampler { public: CLeafSampler( int iThread ) : m_iThread(iThread) {}
// Generate a random point in the leaf's bounding volume
// reject any points that aren't actually in the leaf
// do a couple of tracing heuristics to eliminate points that are inside detail brushes
// or underneath displacement surfaces in the leaf
// return once we have a valid point, use the center if one can't be computed quickly
void GenerateLeafSamplePosition( int leafIndex, const CUtlVector<dplane_t> &leafPlanes, Vector &samplePosition ) { dleaf_t *pLeaf = dleafs + leafIndex;
float dx = pLeaf->maxs[0] - pLeaf->mins[0]; float dy = pLeaf->maxs[1] - pLeaf->mins[1]; float dz = pLeaf->maxs[2] - pLeaf->mins[2]; bool bValid = false; for ( int i = 0; i < 1000 && !bValid; i++ ) { samplePosition.x = pLeaf->mins[0] + m_random.RandomFloat(0, dx); samplePosition.y = pLeaf->mins[1] + m_random.RandomFloat(0, dy); samplePosition.z = pLeaf->mins[2] + m_random.RandomFloat(0, dz); bValid = true;
for ( int j = leafPlanes.Count(); --j >= 0 && bValid; ) { float d = DotProduct(leafPlanes[j].normal, samplePosition) - leafPlanes[j].dist; if ( d < DIST_EPSILON ) { // not inside the leaf, try again
bValid = false; break; } } if ( !bValid ) continue;
for ( int j = 0; j < 6; j++ ) { Vector start = samplePosition; int axis = j%3; start[axis] = (j<3) ? pLeaf->mins[axis] : pLeaf->maxs[axis]; float t; Vector normal; CastRayInLeaf( m_iThread, samplePosition, start, leafIndex, &t, &normal ); if ( t == 0.0f ) { // inside a func_detail, try again.
bValid = false; break; } if ( t != 1.0f ) { Vector delta = start - samplePosition; if ( DotProduct(delta, normal) > 0 ) { // hit backside of displacement, try again.
bValid = false; break; } } } } if ( !bValid ) { // didn't generate a valid sample point, just use the center of the leaf bbox
samplePosition = ( Vector( pLeaf->mins[0], pLeaf->mins[1], pLeaf->mins[2] ) + Vector( pLeaf->maxs[0], pLeaf->maxs[1], pLeaf->maxs[2] ) ) * 0.5f; } }
private: int m_iThread; CUniformRandomStream m_random; };
// gets a list of the planes pointing into a leaf
void GetLeafBoundaryPlanes( CUtlVector<dplane_t> &list, int leafIndex ) { list.RemoveAll(); int nodeIndex = leafparents[leafIndex]; int child = -(leafIndex + 1); while ( nodeIndex >= 0 ) { dnode_t *pNode = dnodes + nodeIndex; dplane_t *pNodePlane = dplanes + pNode->planenum; if ( pNode->children[0] == child ) { // front side
list.AddToTail( *pNodePlane ); } else { // back side
int plane = list.AddToTail(); list[plane].dist = -pNodePlane->dist; list[plane].normal = -pNodePlane->normal; list[plane].type = pNodePlane->type; } child = nodeIndex; nodeIndex = nodeparents[child]; } }
// this stores each sample of the ambient lighting
struct ambientsample_t { Vector pos; Vector cube[6]; };
// add the sample to the list. If we exceed the maximum number of samples, the worst sample will
// be discarded. This has the effect of converging on the best samples when enough are added.
void AddSampleToList( CUtlVector<ambientsample_t> &list, const Vector &samplePosition, Vector *pCube ) { const int MAX_SAMPLES = 16;
int index = list.AddToTail(); list[index].pos = samplePosition; for ( int i = 0; i < 6; i++ ) { list[index].cube[i] = pCube[i]; }
if ( list.Count() <= MAX_SAMPLES ) return;
int nearestNeighborIndex = 0; float nearestNeighborDist = FLT_MAX; float nearestNeighborTotal = 0; for ( int i = 0; i < list.Count(); i++ ) { int closestIndex = 0; float closestDist = FLT_MAX; float totalDC = 0; for ( int j = 0; j < list.Count(); j++ ) { if ( j == i ) continue; float dist = (list[i].pos - list[j].pos).Length(); float maxDC = 0; for ( int k = 0; k < 6; k++ ) { // color delta is computed per-component, per cube side
for (int s = 0; s < 3; s++ ) { float dc = fabs(list[i].cube[k][s] - list[j].cube[k][s]); maxDC = max(maxDC,dc); } totalDC += maxDC; } // need a measurable difference in color or we'll just rely on position
if ( maxDC < 1e-4f ) { maxDC = 0; } else if ( maxDC > 1.0f ) { maxDC = 1.0f; } // selection criteria is 10% distance, 90% color difference
// choose samples that fill the space (large distance from each other)
// and have largest color variation
float distanceFactor = 0.1f + (maxDC * 0.9f); dist *= distanceFactor;
// find the "closest" sample to this one
if ( dist < closestDist ) { closestDist = dist; closestIndex = j; } } // the sample with the "closest" neighbor is rejected
if ( closestDist < nearestNeighborDist || (closestDist == nearestNeighborDist && totalDC < nearestNeighborTotal) ) { nearestNeighborDist = closestDist; nearestNeighborIndex = i; } } list.FastRemove( nearestNeighborIndex ); }
// max number of units in gamma space of per-side delta
int CubeDeltaGammaSpace( Vector *pCube0, Vector *pCube1 ) { int maxDelta = 0; // do this comparison in gamma space to try and get a perceptual basis for the compare
for ( int i = 0; i < 6; i++ ) { for ( int j = 0; j < 3; j++ ) { int val0 = LinearToScreenGamma( pCube0[i][j] ); int val1 = LinearToScreenGamma( pCube1[i][j] ); int delta = abs(val0-val1); if ( delta > maxDelta ) maxDelta = delta; } } return maxDelta; } // reconstruct the ambient lighting for a leaf at the given position in worldspace
// optionally skip one of the entries in the list
void Mod_LeafAmbientColorAtPos( Vector *pOut, const Vector &pos, const CUtlVector<ambientsample_t> &list, int skipIndex ) { for ( int i = 0; i < 6; i++ ) { pOut[i].Init(); } float totalFactor = 0; for ( int i = 0; i < list.Count(); i++ ) { if ( i == skipIndex ) continue; // do an inverse squared distance weighted average of the samples to reconstruct
// the original function
float dist = (list[i].pos - pos).LengthSqr(); float factor = 1.0f / (dist + 1.0f); totalFactor += factor; for ( int j = 0; j < 6; j++ ) { pOut[j] += list[i].cube[j] * factor; } } for ( int i = 0; i < 6; i++ ) { pOut[i] *= (1.0f / totalFactor); } }
// this samples the lighting at each sample and removes any unnecessary samples
void CompressAmbientSampleList( CUtlVector<ambientsample_t> &list ) { Vector testCube[6]; for ( int i = 0; i < list.Count(); i++ ) { if ( list.Count() > 1 ) { Mod_LeafAmbientColorAtPos( testCube, list[i].pos, list, i ); if ( CubeDeltaGammaSpace(testCube, list[i].cube) < 3 ) { list.FastRemove(i); i--; } } } }
// basically this is an intersection routine that returns a distance between the boxes
float AABBDistance( const Vector &mins0, const Vector &maxs0, const Vector &mins1, const Vector &maxs1 ) { Vector delta; for ( int i = 0; i < 3; i++ ) { float greatestMin = max(mins0[i], mins1[i]); float leastMax = min(maxs0[i], maxs1[i]); delta[i] = (greatestMin < leastMax) ? 0 : (leastMax - greatestMin); } return delta.Length(); }
// build a list of leaves from a query
class CLeafList : public ISpatialLeafEnumerator { public: virtual bool EnumerateLeaf( int leaf, int context ) { m_list.AddToTail(leaf); return true; }
CUtlVector<int> m_list; };
// conver short[3] to vector
static void LeafBounds( int leafIndex, Vector &mins, Vector &maxs ) { for ( int i = 0; i < 3; i++ ) { mins[i] = dleafs[leafIndex].mins[i]; maxs[i] = dleafs[leafIndex].maxs[i]; } }
// returns the index of the nearest leaf with ambient samples
int NearestNeighborWithLight(int leafID) { Vector mins, maxs; LeafBounds( leafID, mins, maxs ); Vector size = maxs - mins; CLeafList leafList; ToolBSPTree()->EnumerateLeavesInBox( mins-size, maxs+size, &leafList, 0 ); float bestDist = FLT_MAX; int bestIndex = leafID; for ( int i = 0; i < leafList.m_list.Count(); i++ ) { int testIndex = leafList.m_list[i]; if ( !g_pLeafAmbientIndex->Element(testIndex).ambientSampleCount ) continue;
Vector testMins, testMaxs; LeafBounds( testIndex, testMins, testMaxs ); float dist = AABBDistance( mins, maxs, testMins, testMaxs ); if ( dist < bestDist ) { bestDist = dist; bestIndex = testIndex; } } return bestIndex; }
// maps a float to a byte fraction between min & max
static byte Fixed8Fraction( float t, float tMin, float tMax ) { if ( tMax <= tMin ) return 0;
float frac = RemapValClamped( t, tMin, tMax, 0.0f, 255.0f ); return byte(frac+0.5f); }
CUtlVector< CUtlVector<ambientsample_t> > g_LeafAmbientSamples;
void ComputeAmbientForLeaf( int iThread, int leafID, CUtlVector<ambientsample_t> &list ) { CUtlVector<dplane_t> leafPlanes; CLeafSampler sampler( iThread );
GetLeafBoundaryPlanes( leafPlanes, leafID ); list.RemoveAll(); // this heuristic tries to generate at least one sample per volume (chosen to be similar to the size of a player) in the space
int xSize = (dleafs[leafID].maxs[0] - dleafs[leafID].mins[0]) / 32; int ySize = (dleafs[leafID].maxs[1] - dleafs[leafID].mins[1]) / 32; int zSize = (dleafs[leafID].maxs[2] - dleafs[leafID].mins[2]) / 64; xSize = max(xSize,1); ySize = max(xSize,1); zSize = max(xSize,1); // generate update 128 candidate samples, always at least one sample
int volumeCount = xSize * ySize * zSize; if ( g_bFastAmbient ) { // save compute time, only do one sample
volumeCount = 1; } int sampleCount = clamp( volumeCount, 1, 128 ); if ( dleafs[leafID].contents & CONTENTS_SOLID ) { // don't generate any samples in solid leaves
// NOTE: We copy the nearest non-solid leaf sample pointers into this leaf at the end
return; } Vector cube[6]; for ( int i = 0; i < sampleCount; i++ ) { // compute each candidate sample and add to the list
Vector samplePosition; sampler.GenerateLeafSamplePosition( leafID, leafPlanes, samplePosition ); ComputeAmbientFromSphericalSamples( iThread, samplePosition, cube ); // note this will remove the least valuable sample once the limit is reached
AddSampleToList( list, samplePosition, cube ); }
// remove any samples that can be reconstructed with the remaining data
CompressAmbientSampleList( list ); }
static void ThreadComputeLeafAmbient( int iThread, void *pUserData ) { CUtlVector<ambientsample_t> list; while (1) { int leafID = GetThreadWork (); if (leafID == -1) break; list.RemoveAll(); ComputeAmbientForLeaf(iThread, leafID, list); // copy to the output array
g_LeafAmbientSamples[leafID].SetCount( list.Count() ); for ( int i = 0; i < list.Count(); i++ ) { g_LeafAmbientSamples[leafID].Element(i) = list.Element(i); } } }
void VMPI_ProcessLeafAmbient( int iThread, uint64 iLeaf, MessageBuffer *pBuf ) { CUtlVector<ambientsample_t> list; ComputeAmbientForLeaf(iThread, (int)iLeaf, list);
VMPI_SetCurrentStage( "EncodeLeafAmbientResults" );
// Encode the results.
int nSamples = list.Count(); pBuf->write( &nSamples, sizeof( nSamples ) ); if ( nSamples ) { pBuf->write( list.Base(), list.Count() * sizeof( ambientsample_t ) ); } }
//-----------------------------------------------------------------------------
// Called on the master when a worker finishes processing a static prop.
//-----------------------------------------------------------------------------
void VMPI_ReceiveLeafAmbientResults( uint64 leafID, MessageBuffer *pBuf, int iWorker ) { // Decode the results.
int nSamples; pBuf->read( &nSamples, sizeof( nSamples ) );
g_LeafAmbientSamples[leafID].SetCount( nSamples ); if ( nSamples ) { pBuf->read(g_LeafAmbientSamples[leafID].Base(), nSamples * sizeof(ambientsample_t) ); } }
void ComputePerLeafAmbientLighting() { // Figure out which lights should go in the per-leaf ambient cubes.
int nInAmbientCube = 0; int nSurfaceLights = 0; for ( int i=0; i < *pNumworldlights; i++ ) { dworldlight_t *wl = &dworldlights[i]; if ( IsLeafAmbientSurfaceLight( wl ) ) wl->flags |= DWL_FLAGS_INAMBIENTCUBE; else wl->flags &= ~DWL_FLAGS_INAMBIENTCUBE; if ( wl->type == emit_surface ) ++nSurfaceLights;
if ( wl->flags & DWL_FLAGS_INAMBIENTCUBE ) ++nInAmbientCube; }
Msg( "%d of %d (%d%% of) surface lights went in leaf ambient cubes.\n", nInAmbientCube, nSurfaceLights, nSurfaceLights ? ((nInAmbientCube*100) / nSurfaceLights) : 0 );
g_LeafAmbientSamples.SetCount(numleafs);
if ( g_bUseMPI ) { // Distribute the work among the workers.
VMPI_SetCurrentStage( "ComputeLeafAmbientLighting" ); DistributeWork( numleafs, VMPI_DISTRIBUTEWORK_PACKETID, VMPI_ProcessLeafAmbient, VMPI_ReceiveLeafAmbientResults ); } else { RunThreadsOn(numleafs, true, ThreadComputeLeafAmbient); }
// now write out the data
Msg("Writing leaf ambient..."); g_pLeafAmbientIndex->RemoveAll(); g_pLeafAmbientLighting->RemoveAll(); g_pLeafAmbientIndex->SetCount( numleafs ); g_pLeafAmbientLighting->EnsureCapacity( numleafs*4 ); for ( int leafID = 0; leafID < numleafs; leafID++ ) { const CUtlVector<ambientsample_t> &list = g_LeafAmbientSamples[leafID]; g_pLeafAmbientIndex->Element(leafID).ambientSampleCount = list.Count(); if ( !list.Count() ) { g_pLeafAmbientIndex->Element(leafID).firstAmbientSample = 0; } else { g_pLeafAmbientIndex->Element(leafID).firstAmbientSample = g_pLeafAmbientLighting->Count(); // compute the samples in disk format. Encode the positions in 8-bits using leaf bounds fractions
for ( int i = 0; i < list.Count(); i++ ) { int outIndex = g_pLeafAmbientLighting->AddToTail(); dleafambientlighting_t &light = g_pLeafAmbientLighting->Element(outIndex);
light.x = Fixed8Fraction( list[i].pos.x, dleafs[leafID].mins[0], dleafs[leafID].maxs[0] ); light.y = Fixed8Fraction( list[i].pos.y, dleafs[leafID].mins[1], dleafs[leafID].maxs[1] ); light.z = Fixed8Fraction( list[i].pos.z, dleafs[leafID].mins[2], dleafs[leafID].maxs[2] ); light.pad = 0; for ( int side = 0; side < 6; side++ ) { VectorToColorRGBExp32( list[i].cube[side], light.cube.m_Color[side] ); } } } } for ( int i = 0; i < numleafs; i++ ) { // UNDONE: Do this dynamically in the engine instead. This will allow us to sample across leaf
// boundaries always which should improve the quality of lighting in general
if ( g_pLeafAmbientIndex->Element(i).ambientSampleCount == 0 ) { if ( !(dleafs[i].contents & CONTENTS_SOLID) ) { Msg("Bad leaf ambient for leaf %d\n", i ); }
int refLeaf = NearestNeighborWithLight(i); g_pLeafAmbientIndex->Element(i).ambientSampleCount = 0; g_pLeafAmbientIndex->Element(i).firstAmbientSample = refLeaf; } } Msg("done\n"); }
|