Files
HL2Overcharged/utils/vrad/leaf_ambient_lighting.cpp
2025-05-21 21:20:08 +03:00

709 lines
19 KiB
C++

//========= 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");
}