diff --git a/KeepersCompound.Lightmapper/LightMapper.cs b/KeepersCompound.Lightmapper/LightMapper.cs index bdc7a5e..7cc4e22 100644 --- a/KeepersCompound.Lightmapper/LightMapper.cs +++ b/KeepersCompound.Lightmapper/LightMapper.cs @@ -29,7 +29,7 @@ public class LightMapper public bool LightmappedWater; public SunSettings Sunlight; public uint AnimLightCutoff; - public bool UsePvs; + public bool FastPvs; public override string ToString() { @@ -120,7 +120,7 @@ public class LightMapper LightmappedWater = lmParams.LightmappedWater, Sunlight = sunlightSettings, AnimLightCutoff = lmParams.AnimLightCutoff, - UsePvs = pvs, + FastPvs = pvs, }; Timing.TimeStage("Gather Lights", () => BuildLightList(settings)); @@ -473,72 +473,79 @@ public class LightMapper if (!_mission.TryGetChunk("WREXT", out var worldRep)) return; - - var lightVisibleCells = new List(); - if (settings.UsePvs) + var lightVisibleCells = Timing.TimeStage("Light PVS", () => { - 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; - 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 => + lightCellMap[i] = -1; + var light = _lights[i]; + for (var j = 0; j < cellCount; j++) { - lightCellMap[i] = -1; - var light = _lights[i]; - for (var j = 0; j < cellCount; j++) + if (!MathUtils.Intersects(aabbs[j], light.Position)) { - 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(_lights.Count); - for (var i = 0; i < _lights.Count; i++) - { - var cellIdx = lightCellMap[i]; - if (cellIdx == -1) - { - visibleCellMap.Add([]); continue; } - var visibleSet = pvs.GetVisible(lightCellMap[i]); - visibleCellMap.Add(visibleSet); + // 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; + } + } + }); + Log.Information("Mission has {c} lights", _lights.Count); + + var pvs = new PotentiallyVisibleSet(worldRep.Cells); + var visibleCellMap = new HashSet[_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; } - return visibleCellMap; + var visibleSet = settings.FastPvs switch { + true => pvs.ComputeVisibilityFast(lightCellMap[i]), + false => pvs.ComputeVisibilityExact(_lights[i].Position, lightCellMap[i]) + }; + + // Log.Information("Light {i} sees {c} cells", i, visibleSet.Length); + visibleCellMap[i] = visibleSet; }); - } + + return visibleCellMap; + }); + // TODO: Move this functionality to the LGS library // We set up light indices in separately from lighting because the actual @@ -582,7 +589,7 @@ public class LightMapper continue; } - if (settings.UsePvs && !lightVisibleCells[j].Contains(i)) + if (!lightVisibleCells[j].Contains(i)) { continue; } diff --git a/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs b/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs index a0108ca..755c6d8 100644 --- a/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs +++ b/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs @@ -9,7 +9,6 @@ public class PotentiallyVisibleSet { private readonly struct Node(List edgeIndices) { - public readonly HashSet VisibleNodes = []; public readonly List EdgeIndices = edgeIndices; } @@ -75,15 +74,9 @@ public class PotentiallyVisibleSet } private readonly Node[] _graph; - private readonly bool[] _computedMap; private readonly List _edges; - private const float Epsilon = 0.1f; - - // This is yucky and means we're not thread safe - private readonly List _clipDistances = new(32); - private readonly List _clipSides = new(32); - private readonly int[] _clipCounts = [0, 0, 0]; + private const float Epsilon = MathUtils.Epsilon; // TODO: // - 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) { _graph = new Node[cells.Length]; - _computedMap = new bool[cells.Length]; var portalCount = 0; for (var i = 0; i < cells.Length; i++) { - _computedMap[i] = false; portalCount += cells[i].PortalPolyCount; } @@ -156,18 +147,115 @@ public class PotentiallyVisibleSet // Parallel.ForEach(_edges, ComputeEdgeMightSee); } - public int[] GetVisible(int cellIdx) + public HashSet ComputeVisibilityFast(int cellIdx) { - // TODO: Handle out of range indices - var node = _graph[cellIdx]; - if (_computedMap[cellIdx]) + if (cellIdx >= _graph.Length) { - return [..node.VisibleNodes]; + return []; } - ComputeVisibility(cellIdx); - _graph[cellIdx] = node; - return [.._graph[cellIdx].VisibleNodes]; + var visibleCells = new HashSet(); + foreach (var edgeIdx in _graph[cellIdx].EdgeIndices) + { + var edge = _edges[edgeIdx]; + for (var i = 0; i < edge.MightSee.Length; i++) + { + if (edge.MightSee[i]) + { + visibleCells.Add(i); + } + } + } + + return visibleCells; + } + + // TODO: Max distance :) + public HashSet ComputeVisibilityExact(Vector3 pos, int cellIdx) + { + if (cellIdx >= _graph.Length) + { + return []; + } + + var visibleCells = new HashSet { cellIdx }; + var visited = new Stack(); + visited.Push(cellIdx); + + foreach (var edgeIdx in _graph[cellIdx].EdgeIndices) + { + var edge = _edges[edgeIdx]; + ComputeVisibilityExactRecursive(pos, visibleCells, visited, edge.Destination, edge.Poly); + } + + return visibleCells; + } + + private void ComputeVisibilityExactRecursive( + Vector3 lightPos, + HashSet visibleCells, + Stack visited, + int currentCellIdx, + Poly passPoly) + { + visited.Push(currentCellIdx); + visibleCells.Add(currentCellIdx); + + var clipPlanes = new List(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) + { + var targetEdge = _edges[targetEdgeIdx]; + if (visited.Contains(targetEdge.Destination) || passPoly.IsCoplanar(targetEdge.Poly)) + { + continue; + } + + var poly = new Poly(targetEdge.Poly); + foreach (var clipPlane in clipPlanes) + { + ClipPolygonByPlane(ref poly, clipPlane); + } + + if (poly.Vertices.Count == 0) + { + continue; + } + + ComputeVisibilityExactRecursive(lightPos, 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) @@ -239,192 +327,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 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(); - // 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 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 { Front, @@ -434,7 +336,7 @@ public class PotentiallyVisibleSet // TODO: is this reference type poly going to fuck me? // 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; if (vertexCount == 0) @@ -444,34 +346,30 @@ public class PotentiallyVisibleSet // 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 - // var distances = new float[vertexCount]; - // var sides = new Side[vertexCount]; - // var counts = new int[3]; - _clipDistances.Clear(); - _clipSides.Clear(); - _clipCounts[0] = 0; - _clipCounts[1] = 0; - _clipCounts[2] = 0; + var distances = new float[vertexCount]; + var sides = new Side[vertexCount]; + var counts = new[] {0, 0, 0}; for (var i = 0; i < vertexCount; i++) { var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]); - _clipDistances.Add(distance); - _clipSides.Add(distance switch { + distances[i] = distance; + sides[i] = distance switch + { > Epsilon => Side.Front, - <-Epsilon => Side.Back, + < -Epsilon => Side.Back, _ => Side.On, - }); - _clipCounts[(int)_clipSides[i]]++; + }; + counts[(int)sides[i]]++; } - + // 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; } // 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(); return; @@ -483,11 +381,11 @@ public class PotentiallyVisibleSet var i1 = (i + 1) % vertexCount; var v0 = poly.Vertices[i]; var v1 = poly.Vertices[i1]; - var side = _clipSides[i]; - var nextSide = _clipSides[i1]; + var side = sides[i]; + var nextSide = sides[i1]; // 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); } @@ -501,7 +399,7 @@ public class PotentiallyVisibleSet } // 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); vertices.Add(splitVertex); } diff --git a/KeepersCompound.Lightmapper/Program.cs b/KeepersCompound.Lightmapper/Program.cs index 1583d64..103e951 100644 --- a/KeepersCompound.Lightmapper/Program.cs +++ b/KeepersCompound.Lightmapper/Program.cs @@ -40,8 +40,8 @@ public class LightCommand [CliArgument(Description = "The name of the mission file including extension.")] public required string MissionName { get; set; } - [CliOption(Description = "Use a coarse PVS for tighter cell light indices.")] - public bool Pvs { get; set; } = false; + [CliOption(Description = "Use a fast PVS calculation with looser cell light indices.")] + public bool FastPvs { get; set; } = false; [CliOption(Description = "Name of output file excluding extension.")] public string OutputName { get; set; } = "kc_lit"; @@ -51,7 +51,7 @@ public class LightCommand Timing.Reset(); var lightMapper = new LightMapper(InstallPath, CampaignName, MissionName); - lightMapper.Light(Pvs); + lightMapper.Light(FastPvs); lightMapper.Save(OutputName); Timing.LogAll();