From 9c371b0d6fd3170a51b0fc88f5f12d4f8560e8d9 Mon Sep 17 00:00:00 2001 From: Jarrod Doyle Date: Sun, 5 Jan 2025 14:49:02 +0000 Subject: [PATCH] PVS now works it's just slow as fuck --- KeepersCompound.Lightmapper/LightMapper.cs | 141 +++++++-------- .../PotentiallyVisibleSet.cs | 170 ++++++++++++------ KeepersCompound.Lightmapper/Utils.cs | 10 +- 3 files changed, 197 insertions(+), 124 deletions(-) diff --git a/KeepersCompound.Lightmapper/LightMapper.cs b/KeepersCompound.Lightmapper/LightMapper.cs index 881a068..1e10923 100644 --- a/KeepersCompound.Lightmapper/LightMapper.cs +++ b/KeepersCompound.Lightmapper/LightMapper.cs @@ -371,71 +371,63 @@ public class LightMapper if (!_mission.TryGetChunk("WREXT", out var worldRep)) return; - - var 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 => - { - lightCellMap[i] = -1; - 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 lightVisibleCells = new List(_lights.Count); - var pvs = new PotentiallyVisibleSet(worldRep.Cells); - for (var i = 0; i < _lights.Count; i++) - { - var cellIdx = lightCellMap[i]; - if (cellIdx == -1) - { - lightVisibleCells.Add([]); - continue; - } - var visibleSet = pvs.GetVisible(lightCellMap[i]); - lightVisibleCells.Add(visibleSet); - } - - return lightVisibleCells; - - // TODO: This isn't actually thread safe :) - // Parallel.For(0, worldRep.Cells.Length, i => pvs.GetVisible(i)); - // for (var i = 0; i < worldRep.Cells.Length; i++) - // { - // pvs.GetVisible(i); - // // var visible = pvs.GetVisible(i); - // // Console.WriteLine($"Cell {i}: Count({visible.Length}), Visible[{string.Join(" ", visible)}]"); - // } - }); + // var 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 => + // { + // lightCellMap[i] = -1; + // 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 lightVisibleCells = new List(_lights.Count); + // var pvs = new PotentiallyVisibleSet(worldRep.Cells); + // for (var i = 0; i < _lights.Count; i++) + // { + // var cellIdx = lightCellMap[i]; + // if (cellIdx == -1) + // { + // lightVisibleCells.Add([]); + // continue; + // } + // var visibleSet = pvs.GetVisible(lightCellMap[i]); + // lightVisibleCells.Add(visibleSet); + // } + // + // Console.WriteLine($"17: [{string.Join(", ", pvs.GetVisible(17))}]"); + // + // return lightVisibleCells; + // }); // TODO: Move this functionality to the LGS library // We set up light indices in separately from lighting because the actual @@ -479,10 +471,10 @@ public class LightMapper continue; } - if (!lightVisibleCells[j].Contains(i)) - { - continue; - } + // if (!lightVisibleCells[j].Contains(i)) + // { + // continue; + // } cell.LightIndexCount++; cell.LightIndices.Add((ushort)light.LightTableIndex); @@ -491,7 +483,7 @@ public class LightMapper if (cell.LightIndexCount > 97) { - Console.WriteLine($"WARNING: Too many lights in cell at ({cell.SphereCenter}): {cell.LightIndexCount - 1} / 96"); + // Console.WriteLine($"WARNING: Too many lights in cell at ({cell.SphereCenter}): {cell.LightIndexCount - 1} / 96"); } }); @@ -641,12 +633,15 @@ public class LightMapper } } - foreach (var lightIdx in cell.LightIndices) + // foreach (var lightIdx in cell.LightIndices) + for (var i = 0; i < cell.LightIndexCount; i++) { - if (lightIdx == 0) + var lightIdx = cell.LightIndices[i]; + if (i == 0 || lightIdx == 0) { continue; } + var light = _lights[lightIdx - 1]; // Check if plane normal is facing towards the light @@ -661,7 +656,7 @@ public class LightMapper // If there aren't *any* points on the plane that are in range of the light // then none of the lightmap points will be so we can discard. // The more compact a map is the less effective this is - var planeDist = MathUtils.DistanceFromPlane(plane, light.Position); + var planeDist = Math.Abs(MathUtils.DistanceFromPlane(plane, light.Position)); if (planeDist > light.Radius) { continue; diff --git a/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs b/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs index 8d13dc9..201276c 100644 --- a/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs +++ b/KeepersCompound.Lightmapper/PotentiallyVisibleSet.cs @@ -5,26 +5,48 @@ namespace KeepersCompound.Lightmapper; public class PotentiallyVisibleSet { - private class Edge + private struct Edge { public int Destination; public Poly Poly; + + public override string ToString() + { + return $" _edges; private readonly Dictionary> _visibilitySet; + private const float Epsilon = 0.1f; + // TODO: // - This is a conservative algorithm based on Matt's Ramblings Quake PVS video // - Build portal graph (or just use WR directly) @@ -134,17 +158,14 @@ public class PotentiallyVisibleSet continue; } - // Now we get to the recursive section - ComputeClippedVisibility(visible, edge.Poly, innerEdge.Poly, neighbourIdx, innerEdge.Destination, 0); + ExplorePortalRecursive(visible, edge.Poly, new Poly(innerEdge.Poly), neighbourIdx, innerEdge.Destination, 0); } } return visible; } - // TODO: Name this better - // TODO: This *should* be poly's not edges - private void ComputeClippedVisibility( + private void ExplorePortalRecursive( HashSet visible, Poly sourcePoly, Poly previousPoly, @@ -152,40 +173,59 @@ public class PotentiallyVisibleSet int currentCellIdx, int depth) { - if (depth > 2048) + // TODO: Might need to lose this + if (depth > 1024) { return; } visible.Add(currentCellIdx); - - // Generate separating planes + + // 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(); - separators.AddRange(GetSeparatingPlanes(sourcePoly, previousPoly, false)); - separators.AddRange(GetSeparatingPlanes(previousPoly, sourcePoly, true)); + 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)) + if (edge.Destination == previousCellIdx || previousPoly.IsCoplanar(edge.Poly) || sourcePoly.IsCoplanar(edge.Poly)) { continue; } + + var poly = new Poly(edge.Poly); + foreach (var separator in separators) + { + poly = ClipPolygonByPlane(poly, separator); + } - var poly = separators.Aggregate(edge.Poly, ClipPolygonByPlane); if (poly.Vertices.Length == 0) { continue; } - ComputeClippedVisibility(visible, sourcePoly, poly, currentCellIdx, edge.Destination, depth + 1); + ExplorePortalRecursive(visible, sourcePoly, poly, currentCellIdx, edge.Destination, depth + 1); } } - private static List GetSeparatingPlanes(Poly p0, Poly p1, bool flip) + // 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) { - var separators = new List(); for (var i = 0; i < p0.Vertices.Length; i++) { // brute force all combinations @@ -196,43 +236,67 @@ public class PotentiallyVisibleSet { var v2 = p1.Vertices[j]; - var normal = Vector3.Normalize(Vector3.Cross(v1 - v0, v2 - v0)); - var d = Vector3.Dot(v2, normal); - var plane = new Plane(normal, d); - - // Depending on how the edges were built, the resulting plane might be facing the wrong way - if (MathUtils.DistanceFromPlane(p0.Plane, v2) < MathUtils.Epsilon) + var normal = Vector3.Cross(v1 - v0, v2 - v0); + if (normal.LengthSquared() < Epsilon) { - plane.Normal = -plane.Normal; - plane.D = -plane.D; + // colinear (or near colinear) points will produce an invalid plane + continue; } - // All points should be behind/on the plane + 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.Length; k++) { - if (k == j || MathUtils.DistanceFromPlane(plane, p1.Vertices[k]) > MathUtils.Epsilon) + if (k == j) + { + continue; + } + + var dist = MathUtils.DistanceFromPlane(plane, p1.Vertices[k]); + if (dist > Epsilon) { count++; } + else if (dist < -Epsilon) + { + invalid = true; + break; + } } - if (count != p1.Vertices.Length) + if (invalid || count == 0) { continue; } if (flip) { - plane.Normal = -plane.Normal; - plane.D = -plane.D; + plane.Normal = -normal; + plane.D = -d; } separators.Add(plane); } } - - return separators; } private enum Side @@ -247,6 +311,10 @@ public class PotentiallyVisibleSet private static Poly ClipPolygonByPlane(Poly poly, Plane plane) { var vertexCount = poly.Vertices.Length; + if (vertexCount == 0) + { + return poly; + } // 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 @@ -258,23 +326,24 @@ public class PotentiallyVisibleSet var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]); distances[i] = distance; sides[i] = distance switch { - > MathUtils.Epsilon => Side.Back, - <-MathUtils.Epsilon => Side.Front, + > Epsilon => Side.Front, + <-Epsilon => Side.Back, _ => Side.On, }; counts[(int)sides[i]]++; } // Everything is within the half-space, so we don't need to clip anything - if (counts[(int)Side.Back] == 0) + if (counts[(int)Side.Back] == 0 && counts[(int)Side.On] != vertexCount) { - return new Poly(poly.Vertices, poly.Plane); + return poly; } // Everything is outside the half-space, so we clip everything - if (counts[(int)Side.Back] == vertexCount) + if (counts[(int)Side.Front] == 0) { - return new Poly([], poly.Plane); + poly.Vertices = []; + return poly; } var vertices = new List(); @@ -295,7 +364,7 @@ public class PotentiallyVisibleSet // We only need to do any clipping if we've swapped from front-to-back or vice versa // If either the current or next side is On then that's where we would have clipped to // anyway so we also don't need to do anything - if (side == Side.On || nextSide == Side.On || side != nextSide) + if (side == Side.On || nextSide == Side.On || side == nextSide) { continue; } @@ -305,7 +374,8 @@ public class PotentiallyVisibleSet var splitVertex = v0 + frac * (v1 - v0); vertices.Add(splitVertex); } - - return new Poly([..vertices], poly.Plane); + + poly.Vertices = [..vertices]; + return poly; } } \ No newline at end of file diff --git a/KeepersCompound.Lightmapper/Utils.cs b/KeepersCompound.Lightmapper/Utils.cs index ee31667..c3b8e29 100644 --- a/KeepersCompound.Lightmapper/Utils.cs +++ b/KeepersCompound.Lightmapper/Utils.cs @@ -90,7 +90,15 @@ public static class MathUtils public static float DistanceFromPlane(Plane plane, Vector3 point) { - return Math.Abs(Vector3.Dot(plane.Normal, point) + plane.D) / plane.Normal.Length(); + return (Vector3.Dot(plane.Normal, point) + plane.D) / plane.Normal.Length(); + } + + public static bool IsCoplanar(Plane p0, Plane p1) + { + var m = p0.D / p1.D; + var n0 = p0.Normal; + var n1 = p1.Normal * m; + return Math.Abs(n0.X - n1.X) < Epsilon && Math.Abs(n0.Y - n1.Y) < Epsilon && Math.Abs(n0.Z - n1.Z) < Epsilon; } public record PlanePointMapper