Compare commits

..

11 Commits

8 changed files with 263 additions and 113 deletions

View File

@ -16,7 +16,7 @@
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="TinyEmbree" Version="1.1.0" /> <PackageReference Include="TinyEmbree" Version="1.1.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -34,6 +34,7 @@ public class LightMapper
private DbFile _mission; private DbFile _mission;
private ObjectHierarchy _hierarchy; private ObjectHierarchy _hierarchy;
private Raytracer _scene; private Raytracer _scene;
private Raytracer _sceneNoObj;
private List<Light> _lights; private List<Light> _lights;
private SurfaceType[] _triangleTypeMap; private SurfaceType[] _triangleTypeMap;
@ -56,12 +57,19 @@ public class LightMapper
VerifyRequiredChunksExist(); VerifyRequiredChunksExist();
var mesh = Timing.TimeStage("Build Mesh", BuildMesh); var (noObjMesh, fullMesh) = Timing.TimeStage("Build Meshes", BuildMeshes);
_triangleTypeMap = mesh.TriangleSurfaceMap; _triangleTypeMap = fullMesh.TriangleSurfaceMap;
_sceneNoObj = Timing.TimeStage("Build RT NoObj Scene", () =>
{
var rt = new Raytracer();
rt.AddMesh(new TriangleMesh(noObjMesh.Vertices, noObjMesh.Indices));
rt.CommitScene();
return rt;
});
_scene = Timing.TimeStage("Build RT Scene", () => _scene = Timing.TimeStage("Build RT Scene", () =>
{ {
var rt = new Raytracer(); var rt = new Raytracer();
rt.AddMesh(new TriangleMesh(mesh.Vertices, mesh.Indices)); rt.AddMesh(new TriangleMesh(fullMesh.Vertices, fullMesh.Indices));
rt.CommitScene(); rt.CommitScene();
return rt; return rt;
}); });
@ -180,7 +188,7 @@ public class LightMapper
return new ObjectHierarchy(_mission); return new ObjectHierarchy(_mission);
} }
private Mesh BuildMesh() private (Mesh, Mesh) BuildMeshes()
{ {
var meshBuilder = new MeshBuilder(); var meshBuilder = new MeshBuilder();
@ -189,12 +197,16 @@ public class LightMapper
if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep) || if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep) ||
!_mission.TryGetChunk<BrList>("BRLIST", out var brList)) !_mission.TryGetChunk<BrList>("BRLIST", out var brList))
{ {
return meshBuilder.Build(); return (meshBuilder.Build(), meshBuilder.Build());
} }
meshBuilder.AddWorldRepPolys(worldRep); meshBuilder.AddWorldRepPolys(worldRep);
var noObjMesh = meshBuilder.Build();
meshBuilder.AddObjectPolys(brList, _hierarchy, _campaign); meshBuilder.AddObjectPolys(brList, _hierarchy, _campaign);
return meshBuilder.Build(); var fullMesh = meshBuilder.Build();
return (noObjMesh, fullMesh);
} }
private void BuildLightList() private void BuildLightList()
@ -730,7 +742,7 @@ public class LightMapper
continue; continue;
} }
if (!TraceOcclusion(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);
} }
@ -812,11 +824,23 @@ public class LightMapper
MathUtils.PlanePointMapper planeMapper, MathUtils.PlanePointMapper planeMapper,
Vector2[] v2ds) Vector2[] v2ds)
{ {
polyCenter += planeMapper.Normal * 0.25f;
// All of the traces here are done using the no object scene. We just want to find a point in-world, we don't
// care about if an object is in the way
var tracePoints = new Vector3[offsets.Length]; var tracePoints = new Vector3[offsets.Length];
for (var i = 0; i < offsets.Length; i++) for (var i = 0; i < offsets.Length; i++)
{ {
var offset = offsets[i]; var offset = offsets[i];
var pos = basePosition + offset; var pos = basePosition + offset + planeMapper.Normal * MathUtils.Epsilon;
// If the target lightmap point is in view of the center
// then we can use it as-is. Using it straight fixes seams and such.
if (!TraceOcclusion(_sceneNoObj, polyCenter, pos))
{
tracePoints[i] = pos;
continue;
}
// If we can't see our target point from the center of the poly // If we can't see our target point from the center of the poly
// then we need to clip the point to slightly inside the poly // then we need to clip the point to slightly inside the poly
@ -825,12 +849,24 @@ public class LightMapper
// the polygon but is partially contained in the polygon // the polygon but is partially contained in the polygon
// 2. Darkened spots from linear filtering of points outside the // 2. Darkened spots from linear filtering of points outside the
// polygon which have missed // polygon which have missed
var occluded = TraceOcclusion(polyCenter + planeMapper.Normal * 0.25f, pos);
if (occluded)
{
var p2d = planeMapper.MapTo2d(pos); var p2d = planeMapper.MapTo2d(pos);
p2d = MathUtils.ClipPointToPoly2d(p2d, v2ds); p2d = MathUtils.ClipPointToPoly2d(p2d, v2ds);
pos = planeMapper.MapTo3d(p2d); pos = planeMapper.MapTo3d(p2d);
pos += planeMapper.Normal * MathUtils.Epsilon;
// If the clipping fails, just say screw it and cast :(
if (TraceOcclusion(_sceneNoObj, polyCenter, pos))
{
var hitResult = _sceneNoObj.Trace(new Ray
{
Origin = polyCenter,
Direction = Vector3.Normalize(pos - polyCenter),
});
if (hitResult)
{
pos = hitResult.Position;
}
} }
tracePoints[i] = pos; tracePoints[i] = pos;
@ -839,7 +875,7 @@ public class LightMapper
return tracePoints; return tracePoints;
} }
private bool TraceOcclusion(Vector3 origin, Vector3 target) private static bool TraceOcclusion(Raytracer scene, Vector3 origin, Vector3 target, float epsilon = MathUtils.Epsilon)
{ {
var direction = target - origin; var direction = target - origin;
var ray = new Ray var ray = new Ray
@ -849,7 +885,7 @@ public class LightMapper
}; };
// Epsilon is used here to avoid occlusion when origin lies exactly on a poly // Epsilon is used here to avoid occlusion when origin lies exactly on a poly
return _scene.IsOccluded(new ShadowRay(ray, direction.Length() - MathUtils.Epsilon)); return scene.IsOccluded(new ShadowRay(ray, direction.Length() - epsilon));
} }
// TODO: direction should already be normalised here // TODO: direction should already be normalised here

View File

@ -11,7 +11,7 @@ public enum SurfaceType
{ {
Solid, Solid,
Sky, Sky,
Water, Object,
Air, Air,
} }
@ -144,7 +144,7 @@ public class MeshBuilder
polyVertices.Add(vertex); polyVertices.Add(vertex);
} }
AddPolygon(polyVertices, SurfaceType.Solid); AddPolygon(polyVertices, SurfaceType.Object);
} }
} }
} }
@ -155,12 +155,13 @@ public class MeshBuilder
var indexOffset = _vertices.Count; var indexOffset = _vertices.Count;
// Polygons are n-sided, but fortunately they're convex so we can just do a fan triangulation // Polygons are n-sided, but fortunately they're convex so we can just do a fan triangulation
// Embree triangle winding order is reverse of LGS winding order, so we go (0, i+1, i) instead of (0, i+1, i)
_vertices.AddRange(vertices); _vertices.AddRange(vertices);
for (var i = 1; i < vertexCount - 1; i++) for (var i = 1; i < vertexCount - 1; i++)
{ {
_indices.Add(indexOffset); _indices.Add(indexOffset);
_indices.Add(indexOffset + i);
_indices.Add(indexOffset + i + 1); _indices.Add(indexOffset + i + 1);
_indices.Add(indexOffset + i);
_primSurfaceMap.Add(surfaceType); _primSurfaceMap.Add(surfaceType);
_triangleCount++; _triangleCount++;
} }

View File

@ -5,10 +5,18 @@ namespace KeepersCompound.Lightmapper;
public class PotentiallyVisibleSet public class PotentiallyVisibleSet
{ {
private struct Edge private struct Node(List<int> edgeIndices)
{ {
public int Destination; public bool VisibilityComputed = false;
public Poly Poly; public HashSet<int> VisibleNodes = [];
public readonly List<int> EdgeIndices = edgeIndices;
}
private readonly struct Edge(int destination, Poly poly)
{
public readonly HashSet<int> MightSee = [];
public readonly int Destination = destination;
public readonly Poly Poly = poly;
public override string ToString() public override string ToString()
{ {
@ -44,9 +52,8 @@ public class PotentiallyVisibleSet
} }
} }
private readonly List<int>[] _portalGraph; private readonly Node[] _graph;
private readonly List<Edge> _edges; private readonly List<Edge> _edges;
private readonly Dictionary<int, HashSet<int>> _visibilitySet;
private const float Epsilon = 0.1f; private const float Epsilon = 0.1f;
@ -68,19 +75,19 @@ public class PotentiallyVisibleSet
public PotentiallyVisibleSet(WorldRep.Cell[] cells) public PotentiallyVisibleSet(WorldRep.Cell[] cells)
{ {
_graph = new Node[cells.Length];
_edges = []; _edges = [];
_visibilitySet = new Dictionary<int, HashSet<int>>();
_portalGraph = new List<int>[cells.Length];
for (var i = 0; i < cells.Length; i++) for (var i = 0; i < cells.Length; i++)
{ {
_portalGraph[i] = [];
var cell = cells[i]; var cell = cells[i];
var edgeIndices = new List<int>(cell.PortalPolyCount);
// If a cell is "blocks vision" flagged, we can never see out of it // If a cell is "blocks vision" flagged, we can never see out of it
// We can see into it though, so we still want the edges coming in // We can see into it though, so we still want the edges coming in
if ((cell.Flags & 8) != 0) if ((cell.Flags & 8) != 0)
{ {
_graph[i] = new Node(edgeIndices);
continue; continue;
} }
@ -96,8 +103,6 @@ public class PotentiallyVisibleSet
continue; continue;
} }
var other = poly.Destination;
// Checking if there's already an edge is super slow. It's much faster to just add a new edge, even with // Checking if there's already an edge is super slow. It's much faster to just add a new edge, even with
// the duplicated poly // the duplicated poly
var vs = new List<Vector3>(poly.VertexCount); var vs = new List<Vector3>(poly.VertexCount);
@ -106,33 +111,96 @@ public class PotentiallyVisibleSet
vs.Add(cell.Vertices[cell.Indices[indicesOffset + vIdx]]); vs.Add(cell.Vertices[cell.Indices[indicesOffset + vIdx]]);
} }
var edge = new Edge var edge = new Edge(poly.Destination, new Poly(vs, cell.Planes[poly.PlaneId]));
{ edgeIndices.Add(_edges.Count);
Destination = other,
Poly = new Poly(vs, cell.Planes[poly.PlaneId]),
};
_edges.Add(edge); _edges.Add(edge);
_portalGraph[i].Add(_edges.Count - 1);
indicesOffset += poly.VertexCount; indicesOffset += poly.VertexCount;
} }
_graph[i] = new Node(edgeIndices);
} }
Parallel.ForEach(_edges, ComputeEdgeMightSee);
} }
public int[] GetVisible(int cellIdx) public int[] GetVisible(int cellIdx)
{ {
if (_visibilitySet.TryGetValue(cellIdx, out var value)) // TODO: Handle out of range indices
var node = _graph[cellIdx];
if (node.VisibilityComputed)
{ {
return [..value]; return [..node.VisibleNodes];
} }
var visibleCells = ComputeVisibility(cellIdx); var visibleCells = ComputeVisibility(cellIdx);
_visibilitySet.Add(cellIdx, visibleCells); node.VisibilityComputed = true;
node.VisibleNodes = visibleCells;
_graph[cellIdx] = node;
return [..visibleCells]; return [..visibleCells];
} }
private void ComputeEdgeMightSee(Edge source)
{
var sourcePlane = source.Poly.Plane;
var unexploredCells = new Stack<int>();
unexploredCells.Push(source.Destination);
while (unexploredCells.Count > 0)
{
var cellIdx = unexploredCells.Pop();
if (!source.MightSee.Add(cellIdx))
{
continue; // target is already explored
}
// Target must be partly behind source, source must be partly in front of target, and source and target cannot face each other
foreach (var targetEdgeIdx in _graph[cellIdx].EdgeIndices)
{
var target = _edges[targetEdgeIdx];
var targetPlane = target.Poly.Plane;
var validTarget = false;
foreach (var v in target.Poly.Vertices)
{
if (MathUtils.DistanceFromPlane(sourcePlane, v) < -MathUtils.Epsilon)
{
validTarget = true;
break;
}
}
if (!validTarget)
{
continue;
}
validTarget = false;
foreach (var v in source.Poly.Vertices)
{
if (MathUtils.DistanceFromPlane(targetPlane, v) > MathUtils.Epsilon)
{
validTarget = true;
break;
}
}
if (!validTarget)
{
continue;
}
if (Vector3.Dot(sourcePlane.Normal, targetPlane.Normal) > MathUtils.Epsilon - 1)
{
unexploredCells.Push(target.Destination);
}
}
}
}
private HashSet<int> ComputeVisibility(int cellIdx) private HashSet<int> ComputeVisibility(int cellIdx)
{ {
if (cellIdx >= _portalGraph.Length) if (cellIdx >= _graph.Length)
{ {
return []; return [];
} }
@ -141,86 +209,102 @@ public class PotentiallyVisibleSet
var visible = new HashSet<int>(); var visible = new HashSet<int>();
visible.Add(cellIdx); visible.Add(cellIdx);
// Additionally a cell can always see it's direct neighbours (obviously) foreach (var edgeIdx in _graph[cellIdx].EdgeIndices)
foreach (var edgeIndex in _portalGraph[cellIdx])
{ {
var edge = _edges[edgeIndex]; var edge = _edges[edgeIdx];
var neighbourIdx = edge.Destination; foreach (var mightSee in edge.MightSee)
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]; visible.Add(mightSee);
if (innerEdge.Destination == cellIdx || edge.Poly.IsCoplanar(innerEdge.Poly))
{
continue;
}
ExplorePortalRecursive(visible, edge.Poly, new Poly(innerEdge.Poly), neighbourIdx, innerEdge.Destination, 0);
} }
} }
return visible; return visible;
// 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( // private void ExplorePortalRecursive(
HashSet<int> visible, // HashSet<int> visible,
Poly sourcePoly, // Poly sourcePoly,
Poly previousPoly, // Poly previousPoly,
int previousCellIdx, // int previousCellIdx,
int currentCellIdx, // int currentCellIdx,
int depth) // int depth)
{ // {
// TODO: Might need to lose this // // TODO: Might need to lose this
if (depth > 1024) // if (depth > 1024)
{ // {
return; // return;
} // }
//
visible.Add(currentCellIdx); // visible.Add(currentCellIdx);
//
// Only one edge out of the cell means we'd be going back on ourselves // // Only one edge out of the cell means we'd be going back on ourselves
if (_portalGraph[currentCellIdx].Count <= 1) // if (_portalGraph[currentCellIdx].Count <= 1)
{ // {
return; // return;
} // }
//
// TODO: If all neighbours are already in `visible` skip exploring? // // TODO: If all neighbours are already in `visible` skip exploring?
//
var separators = new List<Plane>(); // var separators = new List<Plane>();
GetSeparatingPlanes(separators, sourcePoly, previousPoly, false); // GetSeparatingPlanes(separators, sourcePoly, previousPoly, false);
GetSeparatingPlanes(separators, previousPoly, sourcePoly, true); // GetSeparatingPlanes(separators, previousPoly, sourcePoly, true);
//
// The case for this occuring is... interesting ( idk ) // // The case for this occuring is... interesting ( idk )
if (separators.Count == 0) // if (separators.Count == 0)
{ // {
return; // 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) || sourcePoly.IsCoplanar(edge.Poly)) // if (edge.Destination == previousCellIdx || previousPoly.IsCoplanar(edge.Poly) || sourcePoly.IsCoplanar(edge.Poly))
{ // {
continue; // continue;
} // }
//
var poly = new Poly(edge.Poly); // var poly = new Poly(edge.Poly);
foreach (var separator in separators) // foreach (var separator in separators)
{ // {
ClipPolygonByPlane(ref poly, separator); // ClipPolygonByPlane(ref poly, separator);
} // }
//
if (poly.Vertices.Count == 0) // if (poly.Vertices.Count == 0)
{ // {
continue; // continue;
} // }
//
ExplorePortalRecursive(visible, sourcePoly, poly, currentCellIdx, edge.Destination, depth + 1); // ExplorePortalRecursive(visible, sourcePoly, poly, currentCellIdx, edge.Destination, depth + 1);
} // }
} // }
// TODO: We're getting multiple separating planes that are the same, let's not somehow? // 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) private static void GetSeparatingPlanes(List<Plane> separators, Poly p0, Poly p1, bool flip)

5
NuGet.Config Normal file
View File

@ -0,0 +1,5 @@
<configuration>
<packageSources>
<add key="KeepersCompound.Lightmapper Local" value="./Packages" />
</packageSources>
</configuration>

View File

@ -0,0 +1,5 @@
{
"version": 2,
"contentHash": "Uhc3bpe44l6oPT7XPAWg8MBJRZc4iZP8nZpaAM3+35tvwLxjspcuBIMzFatr124Zfm71HQxDf+K1o5Tic/FQYw==",
"source": null
}

View File

@ -0,0 +1 @@
Uhc3bpe44l6oPT7XPAWg8MBJRZc4iZP8nZpaAM3+35tvwLxjspcuBIMzFatr124Zfm71HQxDf+K1o5Tic/FQYw==

18
Packages/tinyembree/1.1.0.1/tinyembree.nuspec vendored Executable file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>TinyEmbree</id>
<version>1.1.0.1</version>
<title>TinyEmbree</title>
<authors>Pascal Grittmann</authors>
<license type="file">LICENSE</license>
<licenseUrl>https://aka.ms/deprecateLicenseUrl</licenseUrl>
<description>A very simple C# wrapper around the Embree ray tracing kernels.</description>
<copyright>(c) Pascal Grittmann</copyright>
<tags>ray tracing Embree</tags>
<repository type="git" url="https://github.com/pgrit/TinyEmbree" commit="66e46513777d3046b684f4d6fe796e9f3558b6b0" />
<dependencies>
<group targetFramework="net9.0" />
</dependencies>
</metadata>
</package>