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,21 +56,24 @@ public class Light
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
// this is exact to Dark (I'm a genius??). It's just an inverse distance
// falloff with diffuse angle, except we have to scale the length.
// this is exact to Dark (I'm a genius??).
var dir = Position - point;
var angle = Vector3.Dot(Vector3.Normalize(dir), plane.Normal);
var len = dir.Length();
var slen = len / 4.0f;
var strength = (angle + 1.0f) / slen;
dir = Vector3.Normalize(dir);
// 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
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
@ -102,6 +105,7 @@ public class Light
}
else
{
// Interestingly DromEd doesn't apply attenuation here
spotlightMultiplier = (spotAngle - outer) / (inner - outer);
}

View File

@ -22,12 +22,19 @@ public class LightMapper
{
public Vector3[] AmbientLight;
public bool Hdr;
public float Attenuation;
public float Saturation;
public SoftnessMode MultiSampling;
public float MultiSamplingCenterWeight;
public bool LightmappedWater;
public SunSettings Sunlight;
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;
@ -86,12 +93,12 @@ public class LightMapper
return;
}
var sunlightSettings = new SunSettings()
var sunlightSettings = new SunSettings
{
Enabled = rendParams.useSunlight,
QuadLit = rendParams.sunlightMode is RendParams.SunlightMode.QuadUnshadowed or RendParams.SunlightMode.QuadObjcastShadows,
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();
@ -106,15 +113,17 @@ public class LightMapper
{
Hdr = worldRep.DataHeader.LightmapFormat == 2,
AmbientLight = [..ambientLight],
Attenuation = lmParams.Attenuation,
Saturation = lmParams.Saturation,
MultiSampling = lmParams.ShadowSoftness,
MultiSamplingCenterWeight = lmParams.CenterWeight,
LightmappedWater = lmParams.LightmappedWater,
Sunlight = sunlightSettings,
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("Trace Scene", () => TraceScene(settings));
Timing.TimeStage("Update AnimLight Cell Mapping", SetAnimLightCellMaps);
@ -211,7 +220,7 @@ public class LightMapper
return (noObjMesh, fullMesh);
}
private void BuildLightList()
private void BuildLightList(Settings settings)
{
_lights.Clear();
@ -231,33 +240,50 @@ public class LightMapper
switch (brush.media)
{
case BrList.Brush.Media.Light:
ProcessBrushLight(worldRep.LightingTable, brush);
ProcessBrushLight(worldRep.LightingTable, brush, settings);
break;
case BrList.Brush.Media.Object:
ProcessObjectLight(worldRep.LightingTable, brush);
ProcessObjectLight(worldRep.LightingTable, brush, settings);
break;
}
}
CheckLightConfigurations();
}
private void CheckLightConfigurations()
{
var infinite = 0;
foreach (var light in _lights)
{
if (light.Radius != float.MaxValue)
{
continue;
}
if (light.Radius == float.MaxValue)
{
if (light.ObjId != -1)
{
Log.Warning("Infinite light from object {Id}", light.ObjId);
Log.Warning("Object {Id}: Infinite light radius.", light.ObjId);
}
else
{
Log.Warning("Infinite light from brush near {Position}", light.Position);
Log.Warning("Brush at {Position}: Infinite light radius.", light.Position);
}
infinite++;
}
// TODO: Extract magic number
if (light.InnerRadius > 0 && light.Radius - light.InnerRadius > 4)
{
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);
}
}
}
if (infinite > 0)
{
Log.Warning("Mission contains {Count} infinite lights", infinite);
@ -265,7 +291,7 @@ public class LightMapper
}
// 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
brush.brushInfo = (uint)lightTable.LightCount + 1;
@ -278,10 +304,11 @@ public class LightMapper
}
var brightness = Math.Min(sz.X, 255.0f);
var saturation = sz.Z * settings.Saturation;
var light = new Light
{
Position = brush.position,
Color = Utils.HsbToRgb(sz.Y, sz.Z, brightness),
Color = Utils.HsbToRgb(sz.Y, saturation, brightness),
Brightness = brightness,
Radius = float.MaxValue,
R2 = float.MaxValue,
@ -294,7 +321,7 @@ public class LightMapper
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
var id = (int)brush.brushInfo;
@ -308,6 +335,8 @@ public class LightMapper
var propJointPos = _hierarchy.GetProperty<PropJointPos>(id, "P$JointPos");
propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 };
propLightColor.Saturation *= settings.Saturation;
var joints = propJointPos?.Positions ?? [0, 0, 0, 0, 0, 0];
// Transform data
@ -375,6 +404,11 @@ public class LightMapper
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)
{
var light = new Light
@ -439,11 +473,7 @@ public class LightMapper
if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep))
return;
var lightVisibleCells = new List<int[]>();
if (settings.UsePvs)
{
lightVisibleCells = Timing.TimeStage("Light PVS", () =>
var lightVisibleCells = Timing.TimeStage("Light PVS", () =>
{
var cellCount = worldRep.Cells.Length;
var aabbs = new MathUtils.Aabb[worldRep.Cells.Length];
@ -480,31 +510,54 @@ public class LightMapper
break;
}
}
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.ComputeVisibility(i);
if (i != -1) pvs.ComputeCellMightSee(i);
});
}
var visibleCellMap = new List<int[]>(_lights.Count);
for (var i = 0; i < _lights.Count; i++)
Parallel.For(0, _lights.Count, i =>
{
var cellIdx = lightCellMap[i];
if (cellIdx == -1)
{
visibleCellMap.Add([]);
continue;
visibleCellMap[i] = [];
return;
}
var visibleSet = pvs.GetVisible(lightCellMap[i]);
visibleCellMap.Add(visibleSet);
}
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
// We set up light indices in separately from lighting because the actual
@ -548,7 +601,7 @@ public class LightMapper
continue;
}
if (settings.UsePvs && !lightVisibleCells[j].Contains(i))
if (!lightVisibleCells[j].Contains(i))
{
continue;
}
@ -582,7 +635,14 @@ public class LightMapper
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);
@ -774,7 +834,7 @@ public class LightMapper
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>();
foreach (var cell in worldRep.Cells)
{
var numPolys = cell.PolyCount;
var numRenderPolys = cell.RenderPolyCount;
var numPortalPolys = cell.PortalPolyCount;
var solidPolys = numPolys - numPortalPolys;
// We only care about polys representing solid terrain. We can't use RenderPolyCount because that includes
// water surfaces.
var solidPolys = cell.PolyCount - cell.PortalPolyCount;
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];
polyVertices.Clear();
polyVertices.EnsureCapacity(poly.VertexCount);
@ -75,6 +49,7 @@ public class MeshBuilder
polyVertices.Add(cell.Vertices[cell.Indices[cellIdxOffset + i]]);
}
var primType = cell.RenderPolys[polyIdx].TextureId == 249 ? SurfaceType.Sky : SurfaceType.Solid;
AddPolygon(polyVertices, primType);
cellIdxOffset += poly.VertexCount;
}

View File

@ -9,7 +9,6 @@ public class PotentiallyVisibleSet
{
private readonly struct Node(List<int> edgeIndices)
{
public readonly HashSet<int> VisibleNodes = [];
public readonly List<int> EdgeIndices = edgeIndices;
}
@ -75,15 +74,9 @@ public class PotentiallyVisibleSet
}
private readonly Node[] _graph;
private readonly bool[] _computedMap;
private readonly List<Edge> _edges;
private const float Epsilon = 0.1f;
// 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];
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,119 @@ public class PotentiallyVisibleSet
// Parallel.ForEach(_edges, ComputeEdgeMightSee);
}
public int[] GetVisible(int cellIdx)
public HashSet<int> 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<int>();
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;
}
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)
@ -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
{
Front,
@ -434,7 +340,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 +350,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,
_ => 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 +385,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 +403,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);
}

View File

@ -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();