Compare commits

...

11 Commits

Author SHA1 Message Date
Jarrod Doyle d4761ca682
Update overlit warning 2025-02-22 20:41:06 +00:00
Jarrod Doyle fbc24b855a
Warn about lights outside of the world 2025-02-22 20:38:38 +00:00
Jarrod Doyle 1c869bb123
Remove debug cell lighting log 2025-02-22 20:32:00 +00:00
Jarrod Doyle 170ec83fb1
Early out on light vis max range 2025-02-22 20:28:28 +00:00
Jarrod Doyle 18abe30b03
Calculate exact light to cell visibility by default
The old UsePvs flag has been renamed to FastPVS. The old default behaviour of not using any visibility culling has been removed, and the default is now an exact shrinking frustum algorithm.
2025-02-22 19:38:38 +00:00
Jarrod Doyle e78e53ff51
Remove door blocking cell portal polys from casting mesh 2025-02-21 17:29:46 +00:00
Jarrod Doyle cab85f3ea1
Remove debug setting prints 2025-02-21 17:24:49 +00:00
Jarrod Doyle bd3178398c
Warn about 0 brightness lights 2025-02-21 17:24:13 +00:00
Jarrod Doyle b0ceb0f845
Close #16: Apply LmParams saturation 2025-02-21 16:53:57 +00:00
Jarrod Doyle f4d5dcbd5a
Apply LmParams attenuation 2025-02-16 11:44:02 +00:00
Jarrod Doyle 4827ffb76b
Extract light configuration validation to method and add check for aggressive inner radius usage 2025-02-02 08:42:22 +00:00
5 changed files with 287 additions and 346 deletions

View File

@ -56,23 +56,26 @@ public class Light
SpotlightDir = Vector3.Normalize(Vector3.Transform(vhotLightDir, scale * rotate)); SpotlightDir = Vector3.Normalize(Vector3.Transform(vhotLightDir, scale * rotate));
} }
public float StrengthAtPoint(Vector3 point, Plane plane, uint lightCutoff) public float StrengthAtPoint(Vector3 point, Plane plane, uint lightCutoff, float attenuation)
{ {
// Calculate light strength at a given point. As far as I can tell // Calculate light strength at a given point. As far as I can tell
// this is exact to Dark (I'm a genius??). It's just an inverse distance // this is exact to Dark (I'm a genius??).
// falloff with diffuse angle, except we have to scale the length.
var dir = Position - point; var dir = Position - point;
var angle = Vector3.Dot(Vector3.Normalize(dir), plane.Normal);
var len = dir.Length(); var len = dir.Length();
var slen = len / 4.0f; dir = Vector3.Normalize(dir);
var strength = (angle + 1.0f) / slen;
// Base strength is a scaled inverse falloff
var strength = 4.0f / MathF.Pow(len, attenuation);
// Diffuse light angle
strength *= 1.0f + MathF.Pow(Vector3.Dot(dir, plane.Normal), attenuation);
// Inner radius starts a linear falloff to 0 at the radius // Inner radius starts a linear falloff to 0 at the radius
if (InnerRadius != 0 && len > InnerRadius) if (InnerRadius != 0 && len > InnerRadius)
{ {
strength *= (Radius - len) / (Radius - InnerRadius); strength *= MathF.Pow((Radius - len) / (Radius - InnerRadius), attenuation);
} }
// Anim lights have a (configurable) minimum light cutoff. This is checked before // Anim lights have a (configurable) minimum light cutoff. This is checked before
// spotlight multipliers are applied so we don't cutoff the spot radius falloff. // spotlight multipliers are applied so we don't cutoff the spot radius falloff.
if (Anim && strength * Brightness < lightCutoff) if (Anim && strength * Brightness < lightCutoff)
@ -102,6 +105,7 @@ public class Light
} }
else else
{ {
// Interestingly DromEd doesn't apply attenuation here
spotlightMultiplier = (spotAngle - outer) / (inner - outer); spotlightMultiplier = (spotAngle - outer) / (inner - outer);
} }

View File

@ -22,12 +22,19 @@ public class LightMapper
{ {
public Vector3[] AmbientLight; public Vector3[] AmbientLight;
public bool Hdr; public bool Hdr;
public float Attenuation;
public float Saturation;
public SoftnessMode MultiSampling; public SoftnessMode MultiSampling;
public float MultiSamplingCenterWeight; public float MultiSamplingCenterWeight;
public bool LightmappedWater; public bool LightmappedWater;
public SunSettings Sunlight; public SunSettings Sunlight;
public uint AnimLightCutoff; public uint AnimLightCutoff;
public bool UsePvs; public bool FastPvs;
public override string ToString()
{
return $"Ambient Levels: {AmbientLight}, Hdr: {Hdr}, Attenuation: {Attenuation}, Saturation: {Saturation}";
}
} }
private ResourcePathManager.CampaignResources _campaign; private ResourcePathManager.CampaignResources _campaign;
@ -86,12 +93,12 @@ public class LightMapper
return; return;
} }
var sunlightSettings = new SunSettings() var sunlightSettings = new SunSettings
{ {
Enabled = rendParams.useSunlight, Enabled = rendParams.useSunlight,
QuadLit = rendParams.sunlightMode is RendParams.SunlightMode.QuadUnshadowed or RendParams.SunlightMode.QuadObjcastShadows, QuadLit = rendParams.sunlightMode is RendParams.SunlightMode.QuadUnshadowed or RendParams.SunlightMode.QuadObjcastShadows,
Direction = Vector3.Normalize(rendParams.sunlightDirection), Direction = Vector3.Normalize(rendParams.sunlightDirection),
Color = Utils.HsbToRgb(rendParams.sunlightHue, rendParams.sunlightSaturation, rendParams.sunlightBrightness), Color = Utils.HsbToRgb(rendParams.sunlightHue, rendParams.sunlightSaturation * lmParams.Saturation, rendParams.sunlightBrightness),
}; };
var ambientLight = rendParams.ambientLightZones.ToList(); var ambientLight = rendParams.ambientLightZones.ToList();
@ -106,15 +113,17 @@ public class LightMapper
{ {
Hdr = worldRep.DataHeader.LightmapFormat == 2, Hdr = worldRep.DataHeader.LightmapFormat == 2,
AmbientLight = [..ambientLight], AmbientLight = [..ambientLight],
Attenuation = lmParams.Attenuation,
Saturation = lmParams.Saturation,
MultiSampling = lmParams.ShadowSoftness, MultiSampling = lmParams.ShadowSoftness,
MultiSamplingCenterWeight = lmParams.CenterWeight, MultiSamplingCenterWeight = lmParams.CenterWeight,
LightmappedWater = lmParams.LightmappedWater, LightmappedWater = lmParams.LightmappedWater,
Sunlight = sunlightSettings, Sunlight = sunlightSettings,
AnimLightCutoff = lmParams.AnimLightCutoff, AnimLightCutoff = lmParams.AnimLightCutoff,
UsePvs = pvs, FastPvs = pvs,
}; };
Timing.TimeStage("Gather Lights", BuildLightList); Timing.TimeStage("Gather Lights", () => BuildLightList(settings));
Timing.TimeStage("Set Light Indices", () => SetCellLightIndices(settings)); Timing.TimeStage("Set Light Indices", () => SetCellLightIndices(settings));
Timing.TimeStage("Trace Scene", () => TraceScene(settings)); Timing.TimeStage("Trace Scene", () => TraceScene(settings));
Timing.TimeStage("Update AnimLight Cell Mapping", SetAnimLightCellMaps); Timing.TimeStage("Update AnimLight Cell Mapping", SetAnimLightCellMaps);
@ -211,7 +220,7 @@ public class LightMapper
return (noObjMesh, fullMesh); return (noObjMesh, fullMesh);
} }
private void BuildLightList() private void BuildLightList(Settings settings)
{ {
_lights.Clear(); _lights.Clear();
@ -231,31 +240,48 @@ public class LightMapper
switch (brush.media) switch (brush.media)
{ {
case BrList.Brush.Media.Light: case BrList.Brush.Media.Light:
ProcessBrushLight(worldRep.LightingTable, brush); ProcessBrushLight(worldRep.LightingTable, brush, settings);
break; break;
case BrList.Brush.Media.Object: case BrList.Brush.Media.Object:
ProcessObjectLight(worldRep.LightingTable, brush); ProcessObjectLight(worldRep.LightingTable, brush, settings);
break; break;
} }
} }
CheckLightConfigurations();
}
private void CheckLightConfigurations()
{
var infinite = 0; var infinite = 0;
foreach (var light in _lights) foreach (var light in _lights)
{ {
if (light.Radius != float.MaxValue)
if (light.Radius == float.MaxValue)
{ {
continue; if (light.ObjId != -1)
{
Log.Warning("Object {Id}: Infinite light radius.", light.ObjId);
}
else
{
Log.Warning("Brush at {Position}: Infinite light radius.", light.Position);
}
infinite++;
} }
if (light.ObjId != -1) // TODO: Extract magic number
if (light.InnerRadius > 0 && light.Radius - light.InnerRadius > 4)
{ {
Log.Warning("Infinite light from object {Id}", light.ObjId); if (light.ObjId != -1)
{
Log.Warning("Object {Id}: High radius to inner-radius differential ({D}). Lightmap may not accurately represent lightgem.", light.ObjId, light.Radius - light.InnerRadius);
}
else
{
Log.Warning("Brush at {Position}: High radius to inner-radius differential ({D}). Lightmap may not accurately represent lightgem.", light.Position, light.Radius - light.InnerRadius);
}
} }
else
{
Log.Warning("Infinite light from brush near {Position}", light.Position);
}
infinite++;
} }
if (infinite > 0) if (infinite > 0)
@ -265,7 +291,7 @@ public class LightMapper
} }
// TODO: Check if this works (brush is a record type) // TODO: Check if this works (brush is a record type)
private void ProcessBrushLight(WorldRep.LightTable lightTable, BrList.Brush brush) private void ProcessBrushLight(WorldRep.LightTable lightTable, BrList.Brush brush, Settings settings)
{ {
// For some reason the light table index on brush lights is 1 indexed // For some reason the light table index on brush lights is 1 indexed
brush.brushInfo = (uint)lightTable.LightCount + 1; brush.brushInfo = (uint)lightTable.LightCount + 1;
@ -278,10 +304,11 @@ public class LightMapper
} }
var brightness = Math.Min(sz.X, 255.0f); var brightness = Math.Min(sz.X, 255.0f);
var saturation = sz.Z * settings.Saturation;
var light = new Light var light = new Light
{ {
Position = brush.position, Position = brush.position,
Color = Utils.HsbToRgb(sz.Y, sz.Z, brightness), Color = Utils.HsbToRgb(sz.Y, saturation, brightness),
Brightness = brightness, Brightness = brightness,
Radius = float.MaxValue, Radius = float.MaxValue,
R2 = float.MaxValue, R2 = float.MaxValue,
@ -294,7 +321,7 @@ public class LightMapper
lightTable.AddLight(light.ToLightData(32.0f)); lightTable.AddLight(light.ToLightData(32.0f));
} }
private void ProcessObjectLight(WorldRep.LightTable lightTable, BrList.Brush brush) private void ProcessObjectLight(WorldRep.LightTable lightTable, BrList.Brush brush, Settings settings)
{ {
// TODO: Handle PropSpotlightAndAmbient // TODO: Handle PropSpotlightAndAmbient
var id = (int)brush.brushInfo; var id = (int)brush.brushInfo;
@ -308,6 +335,8 @@ public class LightMapper
var propJointPos = _hierarchy.GetProperty<PropJointPos>(id, "P$JointPos"); var propJointPos = _hierarchy.GetProperty<PropJointPos>(id, "P$JointPos");
propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 }; propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 };
propLightColor.Saturation *= settings.Saturation;
var joints = propJointPos?.Positions ?? [0, 0, 0, 0, 0, 0]; var joints = propJointPos?.Positions ?? [0, 0, 0, 0, 0, 0];
// Transform data // Transform data
@ -375,6 +404,11 @@ public class LightMapper
lightTable.AddLight(light.ToLightData(32.0f), propAnimLight.Dynamic); lightTable.AddLight(light.ToLightData(32.0f), propAnimLight.Dynamic);
} }
if (propLight != null && propLight.Brightness == 0)
{
Log.Warning("Concrete object {Id} has Light property with 0 brightness. Adjust brightness or remove property.", id);
}
if (propLight != null && propLight.Brightness != 0) if (propLight != null && propLight.Brightness != 0)
{ {
var light = new Light var light = new Light
@ -439,72 +473,91 @@ public class LightMapper
if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep)) if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep))
return; return;
var lightVisibleCells = Timing.TimeStage("Light PVS", () =>
var lightVisibleCells = new List<int[]>();
if (settings.UsePvs)
{ {
lightVisibleCells = Timing.TimeStage("Light PVS", () => var cellCount = worldRep.Cells.Length;
var aabbs = new MathUtils.Aabb[worldRep.Cells.Length];
Parallel.For(0, cellCount, i => aabbs[i] = new MathUtils.Aabb(worldRep.Cells[i].Vertices));
var lightCellMap = new int[_lights.Count];
Parallel.For(0, _lights.Count, i =>
{ {
var cellCount = worldRep.Cells.Length; lightCellMap[i] = -1;
var aabbs = new MathUtils.Aabb[worldRep.Cells.Length]; var light = _lights[i];
Parallel.For(0, cellCount, i => aabbs[i] = new MathUtils.Aabb(worldRep.Cells[i].Vertices)); for (var j = 0; j < cellCount; j++)
var lightCellMap = new int[_lights.Count];
Parallel.For(0, _lights.Count, i =>
{ {
lightCellMap[i] = -1; if (!MathUtils.Intersects(aabbs[j], light.Position))
var light = _lights[i];
for (var j = 0; j < cellCount; j++)
{ {
if (!MathUtils.Intersects(aabbs[j], light.Position))
{
continue;
}
// Half-space contained
var cell = worldRep.Cells[j];
var contained = true;
for (var k = 0; k < cell.PlaneCount; k++)
{
var plane = cell.Planes[k];
if (MathUtils.DistanceFromPlane(plane, light.Position) < -MathUtils.Epsilon)
{
contained = false;
break;
}
}
if (contained)
{
lightCellMap[i] = j;
break;
}
}
});
var pvs = new PotentiallyVisibleSet(worldRep.Cells);
Parallel.ForEach(lightCellMap, i =>
{
if (i != -1) pvs.ComputeVisibility(i);
});
var visibleCellMap = new List<int[]>(_lights.Count);
for (var i = 0; i < _lights.Count; i++)
{
var cellIdx = lightCellMap[i];
if (cellIdx == -1)
{
visibleCellMap.Add([]);
continue; continue;
} }
var visibleSet = pvs.GetVisible(lightCellMap[i]); // Half-space contained
visibleCellMap.Add(visibleSet); var cell = worldRep.Cells[j];
var contained = true;
for (var k = 0; k < cell.PlaneCount; k++)
{
var plane = cell.Planes[k];
if (MathUtils.DistanceFromPlane(plane, light.Position) < -MathUtils.Epsilon)
{
contained = false;
break;
}
}
if (contained)
{
lightCellMap[i] = j;
break;
}
} }
return visibleCellMap; if (lightCellMap[i] == -1)
{
if (light.ObjId != -1)
{
Log.Warning("Object {Id}: Light is inside solid terrain.", light.ObjId);
}
else
{
Log.Warning("Brush at {Position}: Light is inside solid terrain.", light.Position);
}
}
}); });
} Log.Information("Mission has {c} lights", _lights.Count);
var pvs = new PotentiallyVisibleSet(worldRep.Cells);
var visibleCellMap = new HashSet<int>[_lights.Count];
// Exact visibility doesn't use MightSee (yet?) so we only bother computing it if we're doing fast vis
if (settings.FastPvs)
{
Parallel.ForEach(lightCellMap, i =>
{
if (i != -1) pvs.ComputeCellMightSee(i);
});
}
Parallel.For(0, _lights.Count, i =>
{
var cellIdx = lightCellMap[i];
if (cellIdx == -1)
{
visibleCellMap[i] = [];
return;
}
var visibleSet = settings.FastPvs switch {
true => pvs.ComputeVisibilityFast(lightCellMap[i]),
false => pvs.ComputeVisibilityExact(_lights[i].Position, lightCellMap[i], _lights[i].Radius)
};
// Log.Information("Light {i} sees {c} cells", i, visibleSet.Count);
visibleCellMap[i] = visibleSet;
});
return visibleCellMap;
});
// TODO: Move this functionality to the LGS library // TODO: Move this functionality to the LGS library
// We set up light indices in separately from lighting because the actual // We set up light indices in separately from lighting because the actual
@ -548,7 +601,7 @@ public class LightMapper
continue; continue;
} }
if (settings.UsePvs && !lightVisibleCells[j].Contains(i)) if (!lightVisibleCells[j].Contains(i))
{ {
continue; continue;
} }
@ -582,7 +635,14 @@ public class LightMapper
if (overLit > 0) if (overLit > 0)
{ {
Log.Warning("{Count}/{CellCount} cells are overlit. Overlit cells can cause Object/Light Gem lighting issues. Try running with the --pvs flag.", overLit, worldRep.Cells.Length); if (settings.FastPvs)
{
Log.Warning("{Count}/{CellCount} cells are overlit. Overlit cells can cause Object/Light Gem lighting issues. Try running without the --fast-pvs flag.", overLit, worldRep.Cells.Length);
}
else
{
Log.Warning("{Count}/{CellCount} cells are overlit. Overlit cells can cause Object/Light Gem lighting issues.", overLit, worldRep.Cells.Length);
}
} }
Log.Information("Max cell lights found ({Count}/96)", maxLights); Log.Information("Max cell lights found ({Count}/96)", maxLights);
@ -774,7 +834,7 @@ public class LightMapper
if (!TraceOcclusion(_scene, light.Position, point)) if (!TraceOcclusion(_scene, light.Position, point))
{ {
strength += targetWeights[idx] * light.StrengthAtPoint(point, plane, settings.AnimLightCutoff); strength += targetWeights[idx] * light.StrengthAtPoint(point, plane, settings.AnimLightCutoff, settings.Attenuation);
} }
} }

View File

@ -35,38 +35,12 @@ public class MeshBuilder
var polyVertices = new List<Vector3>(); var polyVertices = new List<Vector3>();
foreach (var cell in worldRep.Cells) foreach (var cell in worldRep.Cells)
{ {
var numPolys = cell.PolyCount; // We only care about polys representing solid terrain. We can't use RenderPolyCount because that includes
var numRenderPolys = cell.RenderPolyCount; // water surfaces.
var numPortalPolys = cell.PortalPolyCount; var solidPolys = cell.PolyCount - cell.PortalPolyCount;
var solidPolys = numPolys - numPortalPolys;
var cellIdxOffset = 0; var cellIdxOffset = 0;
for (var polyIdx = 0; polyIdx < numPolys; polyIdx++) for (var polyIdx = 0; polyIdx < solidPolys; polyIdx++)
{ {
// There's 2 types of poly that we need to include in the mesh:
// - Terrain
// - Door vision blockers
//
// Door vision blockers are the interesting one. They're not RenderPolys at all, just flagged Polys.
SurfaceType primType;
if (polyIdx < solidPolys)
{
primType = cell.RenderPolys[polyIdx].TextureId == 249 ? SurfaceType.Sky : SurfaceType.Solid;
}
else if (polyIdx < numRenderPolys)
{
// we no longer want water polys :)
continue;
}
else if ((cell.Flags & 8) != 0)
{
primType = SurfaceType.Solid;
}
else
{
continue;
}
var poly = cell.Polys[polyIdx]; var poly = cell.Polys[polyIdx];
polyVertices.Clear(); polyVertices.Clear();
polyVertices.EnsureCapacity(poly.VertexCount); polyVertices.EnsureCapacity(poly.VertexCount);
@ -75,6 +49,7 @@ public class MeshBuilder
polyVertices.Add(cell.Vertices[cell.Indices[cellIdxOffset + i]]); polyVertices.Add(cell.Vertices[cell.Indices[cellIdxOffset + i]]);
} }
var primType = cell.RenderPolys[polyIdx].TextureId == 249 ? SurfaceType.Sky : SurfaceType.Solid;
AddPolygon(polyVertices, primType); AddPolygon(polyVertices, primType);
cellIdxOffset += poly.VertexCount; cellIdxOffset += poly.VertexCount;
} }

View File

@ -9,7 +9,6 @@ public class PotentiallyVisibleSet
{ {
private readonly struct Node(List<int> edgeIndices) private readonly struct Node(List<int> edgeIndices)
{ {
public readonly HashSet<int> VisibleNodes = [];
public readonly List<int> EdgeIndices = edgeIndices; public readonly List<int> EdgeIndices = edgeIndices;
} }
@ -75,15 +74,9 @@ public class PotentiallyVisibleSet
} }
private readonly Node[] _graph; private readonly Node[] _graph;
private readonly bool[] _computedMap;
private readonly List<Edge> _edges; private readonly List<Edge> _edges;
private const float Epsilon = 0.1f; private const float Epsilon = MathUtils.Epsilon;
// This is yucky and means we're not thread safe
private readonly List<float> _clipDistances = new(32);
private readonly List<Side> _clipSides = new(32);
private readonly int[] _clipCounts = [0, 0, 0];
// TODO: // TODO:
// - This is a conservative algorithm based on Matt's Ramblings Quake PVS video // - This is a conservative algorithm based on Matt's Ramblings Quake PVS video
@ -99,12 +92,10 @@ public class PotentiallyVisibleSet
public PotentiallyVisibleSet(WorldRep.Cell[] cells) public PotentiallyVisibleSet(WorldRep.Cell[] cells)
{ {
_graph = new Node[cells.Length]; _graph = new Node[cells.Length];
_computedMap = new bool[cells.Length];
var portalCount = 0; var portalCount = 0;
for (var i = 0; i < cells.Length; i++) for (var i = 0; i < cells.Length; i++)
{ {
_computedMap[i] = false;
portalCount += cells[i].PortalPolyCount; portalCount += cells[i].PortalPolyCount;
} }
@ -156,18 +147,119 @@ public class PotentiallyVisibleSet
// Parallel.ForEach(_edges, ComputeEdgeMightSee); // Parallel.ForEach(_edges, ComputeEdgeMightSee);
} }
public int[] GetVisible(int cellIdx) public HashSet<int> ComputeVisibilityFast(int cellIdx)
{ {
// TODO: Handle out of range indices if (cellIdx >= _graph.Length)
var node = _graph[cellIdx];
if (_computedMap[cellIdx])
{ {
return [..node.VisibleNodes]; return [];
} }
ComputeVisibility(cellIdx); var visibleCells = new HashSet<int>();
_graph[cellIdx] = node; foreach (var edgeIdx in _graph[cellIdx].EdgeIndices)
return [.._graph[cellIdx].VisibleNodes]; {
var edge = _edges[edgeIdx];
for (var i = 0; i < edge.MightSee.Length; i++)
{
if (edge.MightSee[i])
{
visibleCells.Add(i);
}
}
}
return visibleCells;
}
public HashSet<int> ComputeVisibilityExact(Vector3 pos, int cellIdx, float maxRange)
{
if (cellIdx >= _graph.Length)
{
return [];
}
var visibleCells = new HashSet<int> { cellIdx };
var visited = new Stack<int>();
visited.Push(cellIdx);
foreach (var edgeIdx in _graph[cellIdx].EdgeIndices)
{
var edge = _edges[edgeIdx];
ComputeVisibilityExactRecursive(pos, maxRange, visibleCells, visited, edge.Destination, edge.Poly);
}
return visibleCells;
}
private void ComputeVisibilityExactRecursive(
Vector3 lightPos,
float maxRange,
HashSet<int> visibleCells,
Stack<int> visited,
int currentCellIdx,
Poly passPoly)
{
visited.Push(currentCellIdx);
visibleCells.Add(currentCellIdx);
var clipPlanes = new List<Plane>(passPoly.Vertices.Count);
clipPlanes.Clear();
for (var i = 0; i < passPoly.Vertices.Count; i++)
{
var v0 = passPoly.Vertices[i];
var v1 = passPoly.Vertices[(i + 1) % passPoly.Vertices.Count];
var normal = Vector3.Cross(v0 - lightPos, v1 - lightPos);
if (normal.LengthSquared() < Epsilon)
{
continue;
}
normal = Vector3.Normalize(normal);
var d = -Vector3.Dot(v1, normal);
var plane = new Plane(normal, d);
clipPlanes.Add(plane);
}
foreach (var targetEdgeIdx in _graph[currentCellIdx].EdgeIndices)
{
// This only checks is there is a point on the plane in range.
// Could probably use poly center + radius to get an even better early out.
var targetEdge = _edges[targetEdgeIdx];
if (visited.Contains(targetEdge.Destination) ||
passPoly.IsCoplanar(targetEdge.Poly) ||
Math.Abs(MathUtils.DistanceFromNormalizedPlane(targetEdge.Poly.Plane, lightPos)) > maxRange)
{
continue;
}
var poly = new Poly(targetEdge.Poly);
foreach (var clipPlane in clipPlanes)
{
ClipPolygonByPlane(ref poly, clipPlane);
}
if (poly.Vertices.Count == 0)
{
continue;
}
ComputeVisibilityExactRecursive(lightPos, maxRange, visibleCells, visited, targetEdge.Destination, poly);
}
visited.Pop();
}
public void ComputeCellMightSee(int cellIdx)
{
if (cellIdx >= _graph.Length)
{
return;
}
foreach (var edgeIdx in _graph[cellIdx].EdgeIndices)
{
ComputeEdgeMightSee(_edges[edgeIdx]);
}
} }
private void ComputeEdgeMightSee(Edge source) private void ComputeEdgeMightSee(Edge source)
@ -239,192 +331,6 @@ public class PotentiallyVisibleSet
} }
} }
public void ComputeVisibility(int cellIdx)
{
if (cellIdx >= _graph.Length)
{
return;
}
// A cell can always see itself, so we'll add that now
_graph[cellIdx].VisibleNodes.Add(cellIdx);
foreach (var edgeIdx in _graph[cellIdx].EdgeIndices)
{
var edge = _edges[edgeIdx];
ComputeEdgeMightSee(edge);
for (var i = 0; i < edge.MightSee.Length; i++)
{
if (edge.MightSee[i])
{
_graph[cellIdx].VisibleNodes.Add(i);
}
}
}
_computedMap[cellIdx] = true;
// if (cellIdx >= _portalGraph.Length)
// {
// return [];
// }
// Additionally a cell can always see it's direct neighbours (obviously)
// foreach (var edgeIndex in _portalGraph[cellIdx])
// {
// var edge = _edges[edgeIndex];
// var neighbourIdx = edge.Destination;
// visible.Add(neighbourIdx);
//
// // Neighbours of our direct neighbour are always visible, unless they're coplanar
// foreach (var innerEdgeIndex in _portalGraph[neighbourIdx])
// {
// var innerEdge = _edges[innerEdgeIndex];
// if (innerEdge.Destination == cellIdx || edge.Poly.IsCoplanar(innerEdge.Poly))
// {
// continue;
// }
//
// ExplorePortalRecursive(visible, edge.Poly, new Poly(innerEdge.Poly), neighbourIdx, innerEdge.Destination, 0);
// }
// }
// return visible;
}
// private void ExplorePortalRecursive(
// HashSet<int> visible,
// Poly sourcePoly,
// Poly previousPoly,
// int previousCellIdx,
// int currentCellIdx,
// int depth)
// {
// // TODO: Might need to lose this
// if (depth > 1024)
// {
// return;
// }
//
// visible.Add(currentCellIdx);
//
// // Only one edge out of the cell means we'd be going back on ourselves
// if (_portalGraph[currentCellIdx].Count <= 1)
// {
// return;
// }
//
// // TODO: If all neighbours are already in `visible` skip exploring?
//
// var separators = new List<Plane>();
// GetSeparatingPlanes(separators, sourcePoly, previousPoly, false);
// GetSeparatingPlanes(separators, previousPoly, sourcePoly, true);
//
// // The case for this occuring is... interesting ( idk )
// if (separators.Count == 0)
// {
// return;
// }
//
// // Clip all new polys and recurse
// foreach (var edgeIndex in _portalGraph[currentCellIdx])
// {
// var edge = _edges[edgeIndex];
// if (edge.Destination == previousCellIdx || previousPoly.IsCoplanar(edge.Poly) || sourcePoly.IsCoplanar(edge.Poly))
// {
// continue;
// }
//
// var poly = new Poly(edge.Poly);
// foreach (var separator in separators)
// {
// ClipPolygonByPlane(ref poly, separator);
// }
//
// if (poly.Vertices.Count == 0)
// {
// continue;
// }
//
// ExplorePortalRecursive(visible, sourcePoly, poly, currentCellIdx, edge.Destination, depth + 1);
// }
// }
// TODO: We're getting multiple separating planes that are the same, let's not somehow?
private static void GetSeparatingPlanes(List<Plane> separators, Poly p0, Poly p1, bool flip)
{
for (var i = 0; i < p0.Vertices.Count; i++)
{
// brute force all combinations
// there's probably some analytical way to choose the "correct" v2 but I couldn't find anything online
var v0 = p0.Vertices[i];
var v1 = p0.Vertices[(i + 1) % p0.Vertices.Count];
for (var j = 0; j < p1.Vertices.Count; j++)
{
var v2 = p1.Vertices[j];
var normal = Vector3.Cross(v1 - v0, v2 - v0);
if (normal.LengthSquared() < Epsilon)
{
// colinear (or near colinear) points will produce an invalid plane
continue;
}
normal = Vector3.Normalize(normal);
var d = -Vector3.Dot(v2, normal);
// Depending on how the edges were built, the resulting plane might be facing the wrong way
var distanceToSource = MathUtils.DistanceFromPlane(p0.Plane, v2);
if (distanceToSource > Epsilon)
{
normal = -normal;
d = -d;
}
var plane = new Plane(normal, d);
if (MathUtils.IsCoplanar(plane, flip ? p0.Plane : p1.Plane))
{
continue;
}
// All points should be in front of the plane (except for the point used to create it)
var invalid = false;
var count = 0;
for (var k = 0; k < p1.Vertices.Count; k++)
{
if (k == j)
{
continue;
}
var dist = MathUtils.DistanceFromPlane(plane, p1.Vertices[k]);
if (dist > Epsilon)
{
count++;
}
else if (dist < -Epsilon)
{
invalid = true;
break;
}
}
if (invalid || count == 0)
{
continue;
}
if (flip)
{
plane.Normal = -normal;
plane.D = -d;
}
separators.Add(plane);
}
}
}
private enum Side private enum Side
{ {
Front, Front,
@ -434,7 +340,7 @@ public class PotentiallyVisibleSet
// TODO: is this reference type poly going to fuck me? // TODO: is this reference type poly going to fuck me?
// TODO: Should this and Poly be in MathUtils? // TODO: Should this and Poly be in MathUtils?
private void ClipPolygonByPlane(ref Poly poly, Plane plane) private static void ClipPolygonByPlane(ref Poly poly, Plane plane)
{ {
var vertexCount = poly.Vertices.Count; var vertexCount = poly.Vertices.Count;
if (vertexCount == 0) if (vertexCount == 0)
@ -444,34 +350,30 @@ public class PotentiallyVisibleSet
// Firstly we want to tally up what side of the plane each point of the poly is on // Firstly we want to tally up what side of the plane each point of the poly is on
// This is used both to early out if nothing/everything is clipped, and to aid the clipping // This is used both to early out if nothing/everything is clipped, and to aid the clipping
// var distances = new float[vertexCount]; var distances = new float[vertexCount];
// var sides = new Side[vertexCount]; var sides = new Side[vertexCount];
// var counts = new int[3]; var counts = new[] {0, 0, 0};
_clipDistances.Clear();
_clipSides.Clear();
_clipCounts[0] = 0;
_clipCounts[1] = 0;
_clipCounts[2] = 0;
for (var i = 0; i < vertexCount; i++) for (var i = 0; i < vertexCount; i++)
{ {
var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]); var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]);
_clipDistances.Add(distance); distances[i] = distance;
_clipSides.Add(distance switch { sides[i] = distance switch
{
> Epsilon => Side.Front, > Epsilon => Side.Front,
<-Epsilon => Side.Back, < -Epsilon => Side.Back,
_ => Side.On, _ => Side.On,
}); };
_clipCounts[(int)_clipSides[i]]++; counts[(int)sides[i]]++;
} }
// Everything is within the half-space, so we don't need to clip anything // Everything is within the half-space, so we don't need to clip anything
if (_clipCounts[(int)Side.Back] == 0 && _clipCounts[(int)Side.On] != vertexCount) if (counts[(int)Side.Back] == 0 && counts[(int)Side.On] != vertexCount)
{ {
return; return;
} }
// Everything is outside the half-space, so we clip everything // Everything is outside the half-space, so we clip everything
if (_clipCounts[(int)Side.Front] == 0) if (counts[(int)Side.Front] == 0)
{ {
poly.Vertices.Clear(); poly.Vertices.Clear();
return; return;
@ -483,11 +385,11 @@ public class PotentiallyVisibleSet
var i1 = (i + 1) % vertexCount; var i1 = (i + 1) % vertexCount;
var v0 = poly.Vertices[i]; var v0 = poly.Vertices[i];
var v1 = poly.Vertices[i1]; var v1 = poly.Vertices[i1];
var side = _clipSides[i]; var side = sides[i];
var nextSide = _clipSides[i1]; var nextSide = sides[i1];
// Vertices that are inside/on the half-space don't get clipped // Vertices that are inside/on the half-space don't get clipped
if (_clipSides[i] != Side.Back) if (sides[i] != Side.Back)
{ {
vertices.Add(v0); vertices.Add(v0);
} }
@ -501,7 +403,7 @@ public class PotentiallyVisibleSet
} }
// This is how far along the vector v0 -> v1 the front/back crossover occurs // This is how far along the vector v0 -> v1 the front/back crossover occurs
var frac = _clipDistances[i] / (_clipDistances[i] - _clipDistances[i1]); var frac = distances[i] / (distances[i] - distances[i1]);
var splitVertex = v0 + frac * (v1 - v0); var splitVertex = v0 + frac * (v1 - v0);
vertices.Add(splitVertex); vertices.Add(splitVertex);
} }

View File

@ -40,8 +40,8 @@ public class LightCommand
[CliArgument(Description = "The name of the mission file including extension.")] [CliArgument(Description = "The name of the mission file including extension.")]
public required string MissionName { get; set; } public required string MissionName { get; set; }
[CliOption(Description = "Use a coarse PVS for tighter cell light indices.")] [CliOption(Description = "Use a fast PVS calculation with looser cell light indices.")]
public bool Pvs { get; set; } = false; public bool FastPvs { get; set; } = false;
[CliOption(Description = "Name of output file excluding extension.")] [CliOption(Description = "Name of output file excluding extension.")]
public string OutputName { get; set; } = "kc_lit"; public string OutputName { get; set; } = "kc_lit";
@ -51,7 +51,7 @@ public class LightCommand
Timing.Reset(); Timing.Reset();
var lightMapper = new LightMapper(InstallPath, CampaignName, MissionName); var lightMapper = new LightMapper(InstallPath, CampaignName, MissionName);
lightMapper.Light(Pvs); lightMapper.Light(FastPvs);
lightMapper.Save(OutputName); lightMapper.Save(OutputName);
Timing.LogAll(); Timing.LogAll();