using System.Numerics; using KeepersCompound.LGS; using KeepersCompound.LGS.Database; using KeepersCompound.LGS.Database.Chunks; using TinyEmbree; namespace KeepersCompound.Lightmapper; public class LightMapper { // The objcast element of sunlight is ignored, we just care if it's quadlit private struct SunSettings { public bool Enabled; public bool QuadLit; public Vector3 Direction; public Vector3 Color; } private struct Settings { public Vector3[] AmbientLight; public bool Hdr; public SoftnessMode MultiSampling; public float MultiSamplingCenterWeight; public bool LightmappedWater; public SunSettings Sunlight; public uint AnimLightCutoff; } private ResourcePathManager.CampaignResources _campaign; private string _misPath; private DbFile _mission; private ObjectHierarchy _hierarchy; private Raytracer _scene; private List _lights; private SurfaceType[] _triangleTypeMap; public LightMapper( string installPath, string campaignName, string missionName) { var pathManager = SetupPathManager(installPath); _campaign = pathManager.GetCampaign(campaignName); _misPath = _campaign.GetResourcePath(ResourceType.Mission, missionName); _mission = Timing.TimeStage("Parse DB", () => new DbFile(_misPath)); _hierarchy = Timing.TimeStage("Build Hierarchy", BuildHierarchy); _lights = []; var mesh = Timing.TimeStage("Build Mesh", BuildMesh); _triangleTypeMap = mesh.TriangleSurfaceMap; _scene = Timing.TimeStage("Build RT Scene", () => { var rt = new Raytracer(); rt.AddMesh(new TriangleMesh(mesh.Vertices, mesh.Indices)); rt.CommitScene(); return rt; }); } public void Light() { // TODO: Throw? if (!_mission.TryGetChunk("RENDPARAMS", out var rendParams) || !_mission.TryGetChunk("LM_PARAM", out var lmParams) || !_mission.TryGetChunk("WREXT", out var worldRep)) { return; } 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), }; var ambientLight = rendParams.ambientLightZones.ToList(); ambientLight.Insert(0, rendParams.ambientLight); for (var i = 0; i < ambientLight.Count; i++) { ambientLight[i] *= 255; } // TODO: lmParams LightmappedWater doesn't mean the game will actually *use* the lightmapped water hmm var settings = new Settings { Hdr = worldRep.DataHeader.LightmapFormat == 2, AmbientLight = [..ambientLight], MultiSampling = lmParams.ShadowSoftness, MultiSamplingCenterWeight = lmParams.CenterWeight, LightmappedWater = lmParams.LightmappedWater, Sunlight = sunlightSettings, AnimLightCutoff = lmParams.AnimLightCutoff, }; Timing.TimeStage("Gather Lights", BuildLightList); Timing.TimeStage("Set Light Indices", () => SetCellLightIndices(settings)); Timing.TimeStage("Trace Scene", () => TraceScene(settings)); Timing.TimeStage("Update AnimLight Cell Mapping", SetAnimLightCellMaps); // We always do object casting, so it's nice to let dromed know that :) lmParams.ShadowType = LmParams.LightingMode.Objcast; if (rendParams is { useSunlight: true, sunlightMode: RendParams.SunlightMode.SingleUnshadowed }) { rendParams.sunlightMode = RendParams.SunlightMode.SingleObjcastShadows; } else if (rendParams is { useSunlight: true, sunlightMode: RendParams.SunlightMode.QuadUnshadowed }) { rendParams.sunlightMode = RendParams.SunlightMode.QuadObjcastShadows; } } public void Save(string missionName) { var ext = Path.GetExtension(_misPath); var dir = Path.GetDirectoryName(_misPath); var savePath = Path.Join(dir, missionName + ext); Timing.TimeStage("Save DB", () => _mission.Save(savePath)); } private static ResourcePathManager SetupPathManager(string installPath) { var tmpDir = Directory.CreateTempSubdirectory("KCLightmapper"); var resPathManager = new ResourcePathManager(tmpDir.FullName); resPathManager.Init(installPath); return resPathManager; } private ObjectHierarchy BuildHierarchy() { if (!_mission.TryGetChunk("GAM_FILE", out var gamFile)) { return new ObjectHierarchy(_mission); } var dir = Path.GetDirectoryName(_misPath); var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; var name = gamFile.fileName; var paths = Directory.GetFiles(dir!, name, options); if (paths.Length > 0) { return new ObjectHierarchy(_mission, new DbFile(paths[0])); } return new ObjectHierarchy(_mission); } private Mesh BuildMesh() { var meshBuilder = new MeshBuilder(); // TODO: Should this throw? // TODO: Only do object polys if objcast lighting? if (!_mission.TryGetChunk("WREXT", out var worldRep) || !_mission.TryGetChunk("BRLIST", out var brList)) { return meshBuilder.Build(); } meshBuilder.AddWorldRepPolys(worldRep); meshBuilder.AddObjectPolys(brList, _hierarchy, _campaign); return meshBuilder.Build(); } private void BuildLightList() { _lights.Clear(); // Get the chunks we need if (!_mission.TryGetChunk("WREXT", out var worldRep) || !_mission.TryGetChunk("BRLIST", out var brList)) { return; } worldRep.LightingTable.Reset(); // TODO: Calculate the actual effective radius of infinite lights // potentially do the same for all lights and lower their radius if necessary? foreach (var brush in brList.Brushes) { switch (brush.media) { case BrList.Brush.Media.Light: ProcessBrushLight(worldRep.LightingTable, brush); break; case BrList.Brush.Media.Object: ProcessObjectLight(worldRep.LightingTable, brush); break; } } var infinite = _lights.Count(light => light.Radius == float.MaxValue); if (infinite > 0) { Console.WriteLine($"WARNING: Infinite radius lights found: {infinite}/{_lights.Count}"); } } // TODO: Check if this works (brush is a record type) private void ProcessBrushLight(WorldRep.LightTable lightTable, BrList.Brush brush) { // For some reason the light table index on brush lights is 1 indexed brush.brushInfo = (uint)lightTable.LightCount + 1; var sz = brush.size; // Ignore 0 brightness lights if (sz.X == 0) { return; } var brightness = Math.Min(sz.X, 255.0f); var light = new Light { Position = brush.position, Color = Utils.HsbToRgb(sz.Y, sz.Z, brightness), Brightness = brightness, Radius = float.MaxValue, R2 = float.MaxValue, LightTableIndex = lightTable.LightCount, SpotlightInnerAngle = -1f, }; _lights.Add(light); lightTable.AddLight(light.ToLightData(32.0f)); } private void ProcessObjectLight(WorldRep.LightTable lightTable, BrList.Brush brush) { // TODO: Handle PropSpotlightAndAmbient var id = (int)brush.brushInfo; var propScale = _hierarchy.GetProperty(id, "P$Scale"); var propAnimLight = _hierarchy.GetProperty(id, "P$AnimLight", false); var propLight = _hierarchy.GetProperty(id, "P$Light", false); var propLightColor = _hierarchy.GetProperty(id, "P$LightColo"); var propSpotlight = _hierarchy.GetProperty(id, "P$Spotlight"); var propSpotAmb = _hierarchy.GetProperty(id, "P$SpotAmb"); var propModelName = _hierarchy.GetProperty(id, "P$ModelName"); var propJointPos = _hierarchy.GetProperty(id, "P$JointPos"); propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 }; var joints = propJointPos?.Positions ?? [0, 0, 0, 0, 0, 0]; // Transform data var translate = Matrix4x4.CreateTranslation(brush.position); var rotate = Matrix4x4.Identity; rotate *= Matrix4x4.CreateRotationX(float.DegreesToRadians(brush.angle.X)); rotate *= Matrix4x4.CreateRotationY(float.DegreesToRadians(brush.angle.Y)); rotate *= Matrix4x4.CreateRotationZ(float.DegreesToRadians(brush.angle.Z)); var scale = Matrix4x4.CreateScale(propScale?.value ?? Vector3.One); var vhotLightPos = Vector3.Zero; var vhotLightDir = -Vector3.UnitZ; if (propModelName != null) { var resName = $"{propModelName.value.ToLower()}.bin"; var modelPath = _campaign.GetResourcePath(ResourceType.Object, resName); if (modelPath != null) { var model = new ModelFile(modelPath); model.ApplyJoints(joints); if (model.TryGetVhot(ModelFile.VhotId.LightPosition, out var vhot)) { vhotLightPos = vhot.Position - model.Header.Center; } if (model.TryGetVhot(ModelFile.VhotId.LightDirection, out vhot)) { vhotLightDir = (vhot.Position - model.Header.Center) - vhotLightPos; } } } if (propAnimLight != null) { var lightIndex = lightTable.LightCount; propAnimLight.LightTableLightIndex = (ushort)lightIndex; var light = new Light { Position = propAnimLight.Offset, Color = Utils.HsbToRgb(propLightColor.Hue, propLightColor.Saturation, propAnimLight.MaxBrightness), Brightness = propAnimLight.MaxBrightness, InnerRadius = propAnimLight.InnerRadius, Radius = propAnimLight.Radius, R2 = propAnimLight.Radius * propAnimLight.Radius, QuadLit = propAnimLight.QuadLit, ObjId = id, LightTableIndex = propAnimLight.LightTableLightIndex, Anim = true, SpotlightInnerAngle = -1f, }; if (propSpotlight != null) { light.Spotlight = true; light.SpotlightInnerAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.InnerAngle)); light.SpotlightOuterAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.OuterAngle)); } light.FixRadius(); light.ApplyTransforms(vhotLightPos, vhotLightDir, translate, rotate, scale); _lights.Add(light); lightTable.AddLight(light.ToLightData(32.0f)); } if (propLight != null) { var light = new Light { Position = propLight.Offset, Color = Utils.HsbToRgb(propLightColor.Hue, propLightColor.Saturation, propLight.Brightness), Brightness = propLight.Brightness, InnerRadius = propLight.InnerRadius, Radius = propLight.Radius, R2 = propLight.Radius * propLight.Radius, QuadLit = propLight.QuadLit, ObjId = id, LightTableIndex = lightTable.LightCount, SpotlightInnerAngle = -1f, }; if (propSpotAmb != null) { var spot = new Light { Position = light.Position, Color = Utils.HsbToRgb(propLightColor.Hue, propLightColor.Saturation, propSpotAmb.SpotBrightness), Brightness = propSpotAmb.SpotBrightness, InnerRadius = light.InnerRadius, Radius = light.Radius, R2 = light.R2, QuadLit = light.QuadLit, Spotlight = true, SpotlightInnerAngle = (float)Math.Cos(float.DegreesToRadians(propSpotAmb.InnerAngle)), SpotlightOuterAngle = (float)Math.Cos(float.DegreesToRadians(propSpotAmb.OuterAngle)), ObjId = light.ObjId, LightTableIndex = light.LightTableIndex, }; light.LightTableIndex++; // Because we're inserting the spotlight part first spot.FixRadius(); spot.ApplyTransforms(vhotLightPos, vhotLightDir, translate, rotate, scale); _lights.Add(spot); lightTable.AddLight(spot.ToLightData(32.0f)); } else if (propSpotlight != null) { light.Spotlight = true; light.SpotlightInnerAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.InnerAngle)); light.SpotlightOuterAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.OuterAngle)); } light.FixRadius(); light.ApplyTransforms(vhotLightPos, vhotLightDir, translate, rotate, scale); _lights.Add(light); lightTable.AddLight(light.ToLightData(32.0f)); } } private void SetCellLightIndices(Settings settings) { // TODO: Doors aren't blocking lights. Need to do some cell traversal to remove light indices :( 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)}]"); // } }); // TODO: Move this functionality to the LGS library // We set up light indices in separately from lighting because the actual // lighting phase takes a lot of shortcuts that we don't want // Parallel.ForEach(worldRep.Cells, cell => Parallel.For(0, worldRep.Cells.Length, i => { var cell = worldRep.Cells[i]; cell.LightIndexCount = 0; cell.LightIndices.Clear(); // The first element of the light indices array is used to store how many // actual lights are in the list. Which is just LightIndexCount - 1... // Odd choice I know cell.LightIndexCount++; cell.LightIndices.Add(0); // If we have sunlight, then we just assume the sun has the potential to reach everything (ew) // The sun enabled option doesn't actually seem to do anything at runtime, it's purely about if // the cell has the sunlight idx on it. if (settings.Sunlight.Enabled) { cell.LightIndexCount++; cell.LightIndices.Add(0); cell.LightIndices[0]++; } // The OG lightmapper uses the cell traversal to work out all the cells that // are actually visited. We're a lot more coarse and just say if a cell is // in range then we potentially affect the lighting in the cell and add it // to the list. // There's a soft length limit here of 96 due to the runtime object shadow // cache, so we want this to be as minimal as possible. Additionally large // lists actually cause performance issues! var cellAabb = new MathUtils.Aabb(cell.Vertices); for (var j = 0; j < _lights.Count; j++) { var light = _lights[j]; if (!MathUtils.Intersects(new MathUtils.Sphere(light.Position, light.Radius), cellAabb)) { continue; } if (!lightVisibleCells[j].Contains(i)) { continue; } cell.LightIndexCount++; cell.LightIndices.Add((ushort)light.LightTableIndex); cell.LightIndices[0]++; } if (cell.LightIndexCount > 97) { Console.WriteLine($"WARNING: Too many lights in cell at ({cell.SphereCenter}): {cell.LightIndexCount - 1} / 96"); } }); { var overLit = 0; var maxLights = 0; foreach (var cell in worldRep.Cells) { if (cell.LightIndexCount > 97) { overLit++; } if (cell.LightIndexCount > maxLights) { maxLights = cell.LightIndexCount - 1; } } if (overLit > 0) { Console.WriteLine($"WARNING: Overlit cells detected: {overLit}/{worldRep.Cells.Length}"); } Console.WriteLine($"MaxLights: {maxLights} / 96"); } } private void TraceScene(Settings settings) { if (!_mission.TryGetChunk("WREXT", out var worldRep)) { return; } Parallel.ForEach(worldRep.Cells, cell => { // Reset cell AnimLight palette cell.AnimLightCount = 0; cell.AnimLights.Clear(); var numPolys = cell.PolyCount; var numRenderPolys = cell.RenderPolyCount; var numPortalPolys = cell.PortalPolyCount; // There's nothing to render // Portal polys can be render polys (e.g. water) but we're ignoring them for now if (numRenderPolys == 0 || numPortalPolys >= numPolys) { return; } var solidPolys = numPolys - numPortalPolys; var cellIdxOffset = 0; for (var polyIdx = 0; polyIdx < numRenderPolys; polyIdx++) { var poly = cell.Polys[polyIdx]; var plane = cell.Planes[poly.PlaneId]; var renderPoly = cell.RenderPolys[polyIdx]; var info = cell.LightList[polyIdx]; var lightmap = cell.Lightmaps[polyIdx]; info.AnimLightBitmask = 0; // We have to reset the lightmaps for water, but we don't want to do anything else var waterPoly = polyIdx >= solidPolys; if (!settings.LightmappedWater && waterPoly) { lightmap.Reset(Vector3.One * 255f, settings.Hdr); continue; } var ambientLight = settings.AmbientLight[cell.ZoneInfo.GetAmbientLightZoneIndex()]; lightmap.Reset(ambientLight, settings.Hdr); // Get world position of lightmap (0, 0) (+0.5 so we cast from the center of a pixel) var topLeft = cell.Vertices[cell.Indices[cellIdxOffset]]; topLeft -= renderPoly.TextureVectors.Item1 * (renderPoly.TextureBases.Item1 - info.Bases.Item1 * 0.25f); topLeft -= renderPoly.TextureVectors.Item2 * (renderPoly.TextureBases.Item2 - info.Bases.Item2 * 0.25f); var xDir = 0.25f * lightmap.Width * renderPoly.TextureVectors.Item1; var yDir = 0.25f * lightmap.Height * renderPoly.TextureVectors.Item2; var aabb = new MathUtils.Aabb([ topLeft, topLeft + xDir, topLeft + yDir, topLeft + xDir + yDir, ]); // Used for clipping points to poly var vs = new Vector3[poly.VertexCount]; for (var i = 0; i < poly.VertexCount; i++) { vs[i] = cell.Vertices[cell.Indices[cellIdxOffset + i]]; } var planeMapper = new MathUtils.PlanePointMapper(plane.Normal, vs[0], vs[1]); var v2ds = planeMapper.MapTo2d(vs); var (texU, texV) = renderPoly.TextureVectors; var (offsets, weights) = GetTraceOffsetsAndWeights(settings.MultiSampling, texU, texV, settings.MultiSamplingCenterWeight); var (quadOffsets, quadWeights) = settings.MultiSampling == SoftnessMode.HighFourPoint ? (offsets, weights) : GetTraceOffsetsAndWeights(SoftnessMode.HighFourPoint, texU, texV, settings.MultiSamplingCenterWeight); for (var y = 0; y < lightmap.Height; y++) { for (var x = 0; x < lightmap.Width; x++) { var pos = topLeft; pos += x * 0.25f * renderPoly.TextureVectors.Item1; pos += y * 0.25f * renderPoly.TextureVectors.Item2; // TODO: Handle quad lit lights better. Right now we're computing two sets of points for every // luxel. Maybe it's better to only compute if we encounter a quadlit light? var tracePoints = GetTracePoints(pos, offsets, renderPoly.Center, planeMapper, v2ds); var quadTracePoints = settings.MultiSampling == SoftnessMode.HighFourPoint ? tracePoints : GetTracePoints(pos, quadOffsets, renderPoly.Center, planeMapper, v2ds); // This is almost perfect now. Any issues seem to be related to Dark not carrying HSB strength correctly if (settings.Sunlight.Enabled) { // Check if plane normal is facing towards the light // If it's not then we're never going to be (directly) lit by this // light. var sunAngle = Vector3.Dot(-settings.Sunlight.Direction, plane.Normal); if (sunAngle > 0) { var strength = 0f; var targetPoints = settings.Sunlight.QuadLit ? quadTracePoints : tracePoints; var targetWeights = settings.Sunlight.QuadLit ? quadWeights : weights; for (var idx = 0; idx < targetPoints.Length; idx++) { var point = targetPoints[idx]; if (TraceSunRay(point, -settings.Sunlight.Direction)) { // Sunlight is a simpler lighting algorithm than normal lights so we can just // do it here strength += targetWeights[idx] * sunAngle; } } if (strength != 0f) { lightmap.AddLight(0, x, y, settings.Sunlight.Color, strength, settings.Hdr); } } } foreach (var lightIdx in cell.LightIndices) { if (lightIdx == 0) { continue; } var light = _lights[lightIdx - 1]; // Check if plane normal is facing towards the light // If it's not then we're never going to be (directly) lit by this // light. var centerDirection = renderPoly.Center - light.Position; if (Vector3.Dot(plane.Normal, centerDirection) >= 0) { continue; } // 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); if (planeDist > light.Radius) { continue; } // If the poly of the lightmap doesn't intersect the light radius then // none of the lightmap points will so we can discard. if (!MathUtils.Intersects(new MathUtils.Sphere(light.Position, light.Radius), aabb)) { continue; } var strength = 0f; var targetPoints = light.QuadLit ? quadTracePoints : tracePoints; var targetWeights = light.QuadLit ? quadWeights : weights; for (var idx = 0; idx < targetPoints.Length; idx++) { var point = targetPoints[idx]; // If we're out of range there's no point casting a ray // There's probably a better way to discard the entire lightmap // if we're massively out of range if ((point - light.Position).LengthSquared() > light.R2) { continue; } if (TraceRay(light.Position, point)) { strength += targetWeights[idx] * light.StrengthAtPoint(point, plane, settings.AnimLightCutoff); } } if (strength != 0f) { var layer = 0; // If we're an anim light there's a lot of stuff we need to update // Firstly we need to add the light to the cells anim light palette // Secondly we need to set the appropriate bit of the lightmap's // bitmask. Finally we need to check if the lightmap needs another layer // TODO: Handle too many lights for a layer if (light.Anim) { // TODO: Don't recalculate this for every point lol var paletteIdx = cell.AnimLights.IndexOf((ushort)light.LightTableIndex); if (paletteIdx == -1) { paletteIdx = cell.AnimLightCount; cell.AnimLightCount++; cell.AnimLights.Add((ushort)light.LightTableIndex); } info.AnimLightBitmask |= 1u << paletteIdx; layer = paletteIdx + 1; } lightmap.AddLight(layer, x, y, light.Color, strength, settings.Hdr); } } } } cellIdxOffset += poly.VertexCount; } }); } private static (Vector3[], float[]) GetTraceOffsetsAndWeights( SoftnessMode mode, Vector3 texU, Vector3 texV, float centerWeight) { var offsetScale = mode switch { SoftnessMode.HighFourPoint or SoftnessMode.HighFivePoint or SoftnessMode.HighNinePoint => 4f, SoftnessMode.MediumFourPoint or SoftnessMode.MediumFivePoint or SoftnessMode.MediumNinePoint => 8f, SoftnessMode.LowFourPoint => 16f, _ => 1f, }; var cw = centerWeight; var w = 1f - cw; texU /= offsetScale; texV /= offsetScale; return mode switch { SoftnessMode.LowFourPoint or SoftnessMode.MediumFourPoint or SoftnessMode.HighFourPoint => ( [-texU - texV, -texU - texV, -texU + texV, texU + texV], [0.25f, 0.25f, 0.25f, 0.25f]), SoftnessMode.MediumFivePoint or SoftnessMode.HighFivePoint => ( [Vector3.Zero, -texU - texV, texU - texV, -texU + texV, texU + texV], [cw, w * 0.25f, w * 0.25f, w * 0.25f, w * 0.25f]), SoftnessMode.MediumNinePoint or SoftnessMode.HighNinePoint => ( [Vector3.Zero, -texU - texV, texU - texV, -texU + texV, texU + texV, -texU, texU, -texV, texV], [cw, w * 0.125f, w * 0.125f, w * 0.125f, w * 0.125f, w * 0.125f, w * 0.125f, w * 0.125f, w * 0.125f]), _ => ( [Vector3.Zero], [1f]), }; } private Vector3[] GetTracePoints( Vector3 basePosition, Vector3[] offsets, Vector3 polyCenter, MathUtils.PlanePointMapper planeMapper, Vector2[] v2ds) { var tracePoints = new Vector3[offsets.Length]; for (var i = 0; i < offsets.Length; i++) { var offset = offsets[i]; var pos = basePosition + offset; // Embree has robustness issues when hitting poly edges which // results in false misses. To alleviate this we pre-push everything // slightly towards the center of the poly. var centerOffset = polyCenter - pos; if (centerOffset.LengthSquared() > MathUtils.Epsilon) { pos += Vector3.Normalize(centerOffset) * MathUtils.Epsilon; } // If we can't see our target point from the center of the poly // then it's outside the world. We need to clip the point to slightly // inside the poly and retrace to avoid three problems: // 1. Darkened spots from lightmap pixels whose center is outside // the polygon but is partially contained in the polygon // 2. Darkened spots from linear filtering of points outside the // polygon which have missed // 3. Darkened spots where centers are on the exact edge of a poly // which can sometimes cause Embree to miss casts var inPoly = TraceRay(polyCenter + planeMapper.Normal * 0.25f, pos); if (!inPoly) { var p2d = planeMapper.MapTo2d(pos); p2d = MathUtils.ClipPointToPoly2d(p2d, v2ds); pos = planeMapper.MapTo3d(p2d); } tracePoints[i] = pos; } return tracePoints; } private bool TraceRay(Vector3 origin, Vector3 target) { var hitDistanceFromTarget = float.MinValue; var hitSurfaceType = SurfaceType.Water; while (hitDistanceFromTarget < -MathUtils.Epsilon && hitSurfaceType == SurfaceType.Water) { var direction = target - origin; var hitResult = _scene.Trace(new Ray { Origin = origin, Direction = Vector3.Normalize(direction), }); hitDistanceFromTarget = hitResult.Distance - direction.Length(); hitSurfaceType = _triangleTypeMap[(int)hitResult.PrimId]; origin = hitResult.Position += direction * MathUtils.Epsilon; } return Math.Abs(hitDistanceFromTarget) < MathUtils.Epsilon; } // TODO: Can this be merged with the above? private bool TraceSunRay(Vector3 origin, Vector3 direction) { // Avoid self intersection origin += direction * MathUtils.Epsilon; var hitSurfaceType = SurfaceType.Water; while (hitSurfaceType == SurfaceType.Water) { var hitResult = _scene.Trace(new Ray { Origin = origin, Direction = Vector3.Normalize(direction), }); hitSurfaceType = _triangleTypeMap[(int)hitResult.PrimId]; origin = hitResult.Position += direction * MathUtils.Epsilon; } return hitSurfaceType == SurfaceType.Sky; } private void SetAnimLightCellMaps() { if (!_mission.TryGetChunk>("P$AnimLight", out var animLightChunk) || !_mission.TryGetChunk("WREXT", out var worldRep)) { return; } // Now that we've set all the per-cell stuff we need to aggregate the cell mappings // We can't do this in parallel which is why it's being done afterwards rather than // as we go var map = new Dictionary>(); for (ushort i = 0; i < worldRep.Cells.Length; i++) { var cell = worldRep.Cells[i]; for (ushort j = 0; j < cell.AnimLightCount; j++) { var animLightIdx = cell.AnimLights[j]; if (!map.TryGetValue(animLightIdx, out var value)) { value = []; map[animLightIdx] = value; } value.Add(new WorldRep.LightTable.AnimCellMap { CellIndex = i, LightIndex = j, }); } } foreach (var (lightIdx, animCellMaps) in map) { // We need to update the object property so it knows its mapping range // TODO: Handle nulls var light = _lights.Find(l => l.Anim && l.LightTableIndex == lightIdx); var prop = animLightChunk.properties.Find(p => p.objectId == light.ObjId); prop.LightTableLightIndex = lightIdx; prop.LightTableMapIndex = (ushort)worldRep.LightingTable.AnimMapCount; prop.CellsReached = (ushort)animCellMaps.Count; worldRep.LightingTable.AnimCellMaps.AddRange(animCellMaps); worldRep.LightingTable.AnimMapCount += animCellMaps.Count; } } }