PVS now works it's just slow as fuck

This commit is contained in:
Jarrod Doyle 2025-01-05 14:49:02 +00:00
parent 317b17ab02
commit 9c371b0d6f
Signed by: Jayrude
GPG Key ID: 38B57B16E7C0ADF7
3 changed files with 197 additions and 124 deletions

View File

@ -371,71 +371,63 @@ 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 = Timing.TimeStage("Light PVS", () => // {
{ // var cellCount = worldRep.Cells.Length;
var cellCount = worldRep.Cells.Length; // var aabbs = new MathUtils.Aabb[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));
Parallel.For(0, cellCount, i => aabbs[i] = new MathUtils.Aabb(worldRep.Cells[i].Vertices)); //
// var lightCellMap = new int[_lights.Count];
var lightCellMap = new int[_lights.Count]; // Parallel.For(0, _lights.Count, i =>
Parallel.For(0, _lights.Count, i => // {
{ // lightCellMap[i] = -1;
lightCellMap[i] = -1; // var light = _lights[i];
var light = _lights[i]; // for (var j = 0; j < cellCount; j++)
for (var j = 0; j < cellCount; j++) // {
{ // if (!MathUtils.Intersects(aabbs[j], light.Position))
if (!MathUtils.Intersects(aabbs[j], light.Position)) // {
{ // continue;
continue; // }
} //
// // Half-space contained
// Half-space contained // var cell = worldRep.Cells[j];
var cell = worldRep.Cells[j]; // var contained = true;
var contained = true; // for (var k = 0; k < cell.PlaneCount; k++)
for (var k = 0; k < cell.PlaneCount; k++) // {
{ // var plane = cell.Planes[k];
var plane = cell.Planes[k]; // if (MathUtils.DistanceFromPlane(plane, light.Position) < -MathUtils.Epsilon)
if (MathUtils.DistanceFromPlane(plane, light.Position) < -MathUtils.Epsilon) // {
{ // contained = false;
contained = false; // break;
break; // }
} // }
} //
// if (contained)
if (contained) // {
{ // lightCellMap[i] = j;
lightCellMap[i] = j; // break;
break; // }
} // }
} // });
}); //
// var lightVisibleCells = new List<int[]>(_lights.Count);
var lightVisibleCells = new List<int[]>(_lights.Count); // var pvs = new PotentiallyVisibleSet(worldRep.Cells);
var pvs = new PotentiallyVisibleSet(worldRep.Cells); // for (var i = 0; i < _lights.Count; i++)
for (var i = 0; i < _lights.Count; i++) // {
{ // var cellIdx = lightCellMap[i];
var cellIdx = lightCellMap[i]; // if (cellIdx == -1)
if (cellIdx == -1) // {
{ // lightVisibleCells.Add([]);
lightVisibleCells.Add([]); // continue;
continue; // }
} // var visibleSet = pvs.GetVisible(lightCellMap[i]);
var visibleSet = pvs.GetVisible(lightCellMap[i]); // lightVisibleCells.Add(visibleSet);
lightVisibleCells.Add(visibleSet); // }
} //
// Console.WriteLine($"17: [{string.Join(", ", pvs.GetVisible(17))}]");
return lightVisibleCells; //
// 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)}]");
// }
});
// 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
@ -479,10 +471,10 @@ public class LightMapper
continue; continue;
} }
if (!lightVisibleCells[j].Contains(i)) // if (!lightVisibleCells[j].Contains(i))
{ // {
continue; // continue;
} // }
cell.LightIndexCount++; cell.LightIndexCount++;
cell.LightIndices.Add((ushort)light.LightTableIndex); cell.LightIndices.Add((ushort)light.LightTableIndex);
@ -491,7 +483,7 @@ public class LightMapper
if (cell.LightIndexCount > 97) 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; continue;
} }
var light = _lights[lightIdx - 1]; var light = _lights[lightIdx - 1];
// Check if plane normal is facing towards the light // 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 // 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. // then none of the lightmap points will be so we can discard.
// The more compact a map is the less effective this is // 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) if (planeDist > light.Radius)
{ {
continue; continue;

View File

@ -5,26 +5,48 @@ namespace KeepersCompound.Lightmapper;
public class PotentiallyVisibleSet public class PotentiallyVisibleSet
{ {
private class Edge private struct Edge
{ {
public int Destination; public int Destination;
public Poly Poly; public Poly Poly;
public override string ToString()
{
return $"<Destination: {Destination}, Poly: {Poly}";
}
} }
private class Poly(Vector3[] vertices, Plane plane) private struct Poly
{ {
public Vector3[] Vertices = vertices; public Vector3[] Vertices;
public Plane Plane = plane; public readonly Plane Plane;
public Poly(Vector3[] vertices, Plane plane)
{
Vertices = vertices;
Plane = plane;
}
public Poly(Poly other)
{
// TODO: Can this be reverted?
var vs = new Vector3[other.Vertices.Length];
for (var i = 0; i < vs.Length; i++)
{
vs[i] = other.Vertices[i];
}
Vertices = vs;
Plane = new Plane(other.Plane.Normal, other.Plane.D);
}
public bool IsCoplanar(Poly other) public bool IsCoplanar(Poly other)
{ {
// TODO: should this be in mathutils? return MathUtils.IsCoplanar(Plane, other.Plane);
const float e = MathUtils.Epsilon; }
var m = Plane.D / other.Plane.D;
public override string ToString()
var n0 = Plane.Normal; {
var n1 = other.Plane.Normal * m; return $"<Plane: {Plane}, Vertices: [{string.Join(", ", Vertices)}]";
return Math.Abs(n0.X - n1.X) < e && Math.Abs(n0.Y - n1.Y) < e && Math.Abs(n0.Z - n1.Z) < e;
} }
} }
@ -32,6 +54,8 @@ public class PotentiallyVisibleSet
private readonly List<Edge> _edges; private readonly List<Edge> _edges;
private readonly Dictionary<int, HashSet<int>> _visibilitySet; private readonly Dictionary<int, HashSet<int>> _visibilitySet;
private const float Epsilon = 0.1f;
// 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
// - Build portal graph (or just use WR directly) // - Build portal graph (or just use WR directly)
@ -134,17 +158,14 @@ public class PotentiallyVisibleSet
continue; continue;
} }
// Now we get to the recursive section ExplorePortalRecursive(visible, edge.Poly, new Poly(innerEdge.Poly), neighbourIdx, innerEdge.Destination, 0);
ComputeClippedVisibility(visible, edge.Poly, innerEdge.Poly, neighbourIdx, innerEdge.Destination, 0);
} }
} }
return visible; return visible;
} }
// TODO: Name this better private void ExplorePortalRecursive(
// TODO: This *should* be poly's not edges
private void ComputeClippedVisibility(
HashSet<int> visible, HashSet<int> visible,
Poly sourcePoly, Poly sourcePoly,
Poly previousPoly, Poly previousPoly,
@ -152,40 +173,59 @@ public class PotentiallyVisibleSet
int currentCellIdx, int currentCellIdx,
int depth) int depth)
{ {
if (depth > 2048) // TODO: Might need to lose this
if (depth > 1024)
{ {
return; return;
} }
visible.Add(currentCellIdx); 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<Plane>(); var separators = new List<Plane>();
separators.AddRange(GetSeparatingPlanes(sourcePoly, previousPoly, false)); GetSeparatingPlanes(separators, sourcePoly, previousPoly, false);
separators.AddRange(GetSeparatingPlanes(previousPoly, sourcePoly, true)); GetSeparatingPlanes(separators, previousPoly, sourcePoly, true);
// The case for this occuring is... interesting ( idk )
if (separators.Count == 0)
{
return;
}
// Clip all new polys and recurse // Clip all new polys and recurse
foreach (var edgeIndex in _portalGraph[currentCellIdx]) foreach (var edgeIndex in _portalGraph[currentCellIdx])
{ {
var edge = _edges[edgeIndex]; 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; 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) if (poly.Vertices.Length == 0)
{ {
continue; continue;
} }
ComputeClippedVisibility(visible, sourcePoly, poly, currentCellIdx, edge.Destination, depth + 1); ExplorePortalRecursive(visible, sourcePoly, poly, currentCellIdx, edge.Destination, depth + 1);
} }
} }
private static List<Plane> 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<Plane> separators, Poly p0, Poly p1, bool flip)
{ {
var separators = new List<Plane>();
for (var i = 0; i < p0.Vertices.Length; i++) for (var i = 0; i < p0.Vertices.Length; i++)
{ {
// brute force all combinations // brute force all combinations
@ -196,43 +236,67 @@ public class PotentiallyVisibleSet
{ {
var v2 = p1.Vertices[j]; var v2 = p1.Vertices[j];
var normal = Vector3.Normalize(Vector3.Cross(v1 - v0, v2 - v0)); var normal = Vector3.Cross(v1 - v0, v2 - v0);
var d = Vector3.Dot(v2, normal); if (normal.LengthSquared() < Epsilon)
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)
{ {
plane.Normal = -plane.Normal; // colinear (or near colinear) points will produce an invalid plane
plane.D = -plane.D; 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; var count = 0;
for (var k = 0; k < p1.Vertices.Length; k++) 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++; count++;
} }
else if (dist < -Epsilon)
{
invalid = true;
break;
}
} }
if (count != p1.Vertices.Length) if (invalid || count == 0)
{ {
continue; continue;
} }
if (flip) if (flip)
{ {
plane.Normal = -plane.Normal; plane.Normal = -normal;
plane.D = -plane.D; plane.D = -d;
} }
separators.Add(plane); separators.Add(plane);
} }
} }
return separators;
} }
private enum Side private enum Side
@ -247,6 +311,10 @@ public class PotentiallyVisibleSet
private static Poly ClipPolygonByPlane(Poly poly, Plane plane) private static Poly ClipPolygonByPlane(Poly poly, Plane plane)
{ {
var vertexCount = poly.Vertices.Length; 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 // 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
@ -258,23 +326,24 @@ public class PotentiallyVisibleSet
var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]); var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]);
distances[i] = distance; distances[i] = distance;
sides[i] = distance switch { sides[i] = distance switch {
> MathUtils.Epsilon => Side.Back, > Epsilon => Side.Front,
<-MathUtils.Epsilon => Side.Front, <-Epsilon => Side.Back,
_ => Side.On, _ => Side.On,
}; };
counts[(int)sides[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 (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 // 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<Vector3>(); var vertices = new List<Vector3>();
@ -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 // 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 // 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 // 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; continue;
} }
@ -305,7 +374,8 @@ public class PotentiallyVisibleSet
var splitVertex = v0 + frac * (v1 - v0); var splitVertex = v0 + frac * (v1 - v0);
vertices.Add(splitVertex); vertices.Add(splitVertex);
} }
return new Poly([..vertices], poly.Plane); poly.Vertices = [..vertices];
return poly;
} }
} }

View File

@ -90,7 +90,15 @@ public static class MathUtils
public static float DistanceFromPlane(Plane plane, Vector3 point) 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 public record PlanePointMapper