Compare commits
11 Commits
fafae43c56
...
d4761ca682
Author | SHA1 | Date |
---|---|---|
|
d4761ca682 | |
|
fbc24b855a | |
|
1c869bb123 | |
|
170ec83fb1 | |
|
18abe30b03 | |
|
e78e53ff51 | |
|
cab85f3ea1 | |
|
bd3178398c | |
|
b0ceb0f845 | |
|
f4d5dcbd5a | |
|
4827ffb76b |
|
@ -56,23 +56,26 @@ public class Light
|
||||||
SpotlightDir = Vector3.Normalize(Vector3.Transform(vhotLightDir, scale * rotate));
|
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
|
// 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
|
// this is exact to Dark (I'm a genius??).
|
||||||
// falloff with diffuse angle, except we have to scale the length.
|
|
||||||
var dir = Position - point;
|
var dir = Position - point;
|
||||||
var angle = Vector3.Dot(Vector3.Normalize(dir), plane.Normal);
|
|
||||||
var len = dir.Length();
|
var len = dir.Length();
|
||||||
var slen = len / 4.0f;
|
dir = Vector3.Normalize(dir);
|
||||||
var strength = (angle + 1.0f) / slen;
|
|
||||||
|
// 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
|
// Inner radius starts a linear falloff to 0 at the radius
|
||||||
if (InnerRadius != 0 && len > InnerRadius)
|
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
|
// Anim lights have a (configurable) minimum light cutoff. This is checked before
|
||||||
// spotlight multipliers are applied so we don't cutoff the spot radius falloff.
|
// spotlight multipliers are applied so we don't cutoff the spot radius falloff.
|
||||||
if (Anim && strength * Brightness < lightCutoff)
|
if (Anim && strength * Brightness < lightCutoff)
|
||||||
|
@ -102,6 +105,7 @@ public class Light
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Interestingly DromEd doesn't apply attenuation here
|
||||||
spotlightMultiplier = (spotAngle - outer) / (inner - outer);
|
spotlightMultiplier = (spotAngle - outer) / (inner - outer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,19 @@ public class LightMapper
|
||||||
{
|
{
|
||||||
public Vector3[] AmbientLight;
|
public Vector3[] AmbientLight;
|
||||||
public bool Hdr;
|
public bool Hdr;
|
||||||
|
public float Attenuation;
|
||||||
|
public float Saturation;
|
||||||
public SoftnessMode MultiSampling;
|
public SoftnessMode MultiSampling;
|
||||||
public float MultiSamplingCenterWeight;
|
public float MultiSamplingCenterWeight;
|
||||||
public bool LightmappedWater;
|
public bool LightmappedWater;
|
||||||
public SunSettings Sunlight;
|
public SunSettings Sunlight;
|
||||||
public uint AnimLightCutoff;
|
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;
|
private ResourcePathManager.CampaignResources _campaign;
|
||||||
|
@ -86,12 +93,12 @@ public class LightMapper
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sunlightSettings = new SunSettings()
|
var sunlightSettings = new SunSettings
|
||||||
{
|
{
|
||||||
Enabled = rendParams.useSunlight,
|
Enabled = rendParams.useSunlight,
|
||||||
QuadLit = rendParams.sunlightMode is RendParams.SunlightMode.QuadUnshadowed or RendParams.SunlightMode.QuadObjcastShadows,
|
QuadLit = rendParams.sunlightMode is RendParams.SunlightMode.QuadUnshadowed or RendParams.SunlightMode.QuadObjcastShadows,
|
||||||
Direction = Vector3.Normalize(rendParams.sunlightDirection),
|
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();
|
var ambientLight = rendParams.ambientLightZones.ToList();
|
||||||
|
@ -106,15 +113,17 @@ public class LightMapper
|
||||||
{
|
{
|
||||||
Hdr = worldRep.DataHeader.LightmapFormat == 2,
|
Hdr = worldRep.DataHeader.LightmapFormat == 2,
|
||||||
AmbientLight = [..ambientLight],
|
AmbientLight = [..ambientLight],
|
||||||
|
Attenuation = lmParams.Attenuation,
|
||||||
|
Saturation = lmParams.Saturation,
|
||||||
MultiSampling = lmParams.ShadowSoftness,
|
MultiSampling = lmParams.ShadowSoftness,
|
||||||
MultiSamplingCenterWeight = lmParams.CenterWeight,
|
MultiSamplingCenterWeight = lmParams.CenterWeight,
|
||||||
LightmappedWater = lmParams.LightmappedWater,
|
LightmappedWater = lmParams.LightmappedWater,
|
||||||
Sunlight = sunlightSettings,
|
Sunlight = sunlightSettings,
|
||||||
AnimLightCutoff = lmParams.AnimLightCutoff,
|
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("Set Light Indices", () => SetCellLightIndices(settings));
|
||||||
Timing.TimeStage("Trace Scene", () => TraceScene(settings));
|
Timing.TimeStage("Trace Scene", () => TraceScene(settings));
|
||||||
Timing.TimeStage("Update AnimLight Cell Mapping", SetAnimLightCellMaps);
|
Timing.TimeStage("Update AnimLight Cell Mapping", SetAnimLightCellMaps);
|
||||||
|
@ -211,7 +220,7 @@ public class LightMapper
|
||||||
return (noObjMesh, fullMesh);
|
return (noObjMesh, fullMesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildLightList()
|
private void BuildLightList(Settings settings)
|
||||||
{
|
{
|
||||||
_lights.Clear();
|
_lights.Clear();
|
||||||
|
|
||||||
|
@ -231,31 +240,48 @@ public class LightMapper
|
||||||
switch (brush.media)
|
switch (brush.media)
|
||||||
{
|
{
|
||||||
case BrList.Brush.Media.Light:
|
case BrList.Brush.Media.Light:
|
||||||
ProcessBrushLight(worldRep.LightingTable, brush);
|
ProcessBrushLight(worldRep.LightingTable, brush, settings);
|
||||||
break;
|
break;
|
||||||
case BrList.Brush.Media.Object:
|
case BrList.Brush.Media.Object:
|
||||||
ProcessObjectLight(worldRep.LightingTable, brush);
|
ProcessObjectLight(worldRep.LightingTable, brush, settings);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckLightConfigurations();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckLightConfigurations()
|
||||||
|
{
|
||||||
var infinite = 0;
|
var infinite = 0;
|
||||||
foreach (var light in _lights)
|
foreach (var light in _lights)
|
||||||
{
|
{
|
||||||
if (light.Radius != float.MaxValue)
|
|
||||||
|
if (light.Radius == float.MaxValue)
|
||||||
{
|
{
|
||||||
continue;
|
if (light.ObjId != -1)
|
||||||
|
{
|
||||||
|
Log.Warning("Object {Id}: Infinite light radius.", light.ObjId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Warning("Brush at {Position}: Infinite light radius.", light.Position);
|
||||||
|
}
|
||||||
|
infinite++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (light.ObjId != -1)
|
// TODO: Extract magic number
|
||||||
|
if (light.InnerRadius > 0 && light.Radius - light.InnerRadius > 4)
|
||||||
{
|
{
|
||||||
Log.Warning("Infinite light from object {Id}", light.ObjId);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
Log.Warning("Infinite light from brush near {Position}", light.Position);
|
|
||||||
}
|
|
||||||
infinite++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (infinite > 0)
|
if (infinite > 0)
|
||||||
|
@ -265,7 +291,7 @@ public class LightMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check if this works (brush is a record type)
|
// 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
|
// For some reason the light table index on brush lights is 1 indexed
|
||||||
brush.brushInfo = (uint)lightTable.LightCount + 1;
|
brush.brushInfo = (uint)lightTable.LightCount + 1;
|
||||||
|
@ -278,10 +304,11 @@ public class LightMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
var brightness = Math.Min(sz.X, 255.0f);
|
var brightness = Math.Min(sz.X, 255.0f);
|
||||||
|
var saturation = sz.Z * settings.Saturation;
|
||||||
var light = new Light
|
var light = new Light
|
||||||
{
|
{
|
||||||
Position = brush.position,
|
Position = brush.position,
|
||||||
Color = Utils.HsbToRgb(sz.Y, sz.Z, brightness),
|
Color = Utils.HsbToRgb(sz.Y, saturation, brightness),
|
||||||
Brightness = brightness,
|
Brightness = brightness,
|
||||||
Radius = float.MaxValue,
|
Radius = float.MaxValue,
|
||||||
R2 = float.MaxValue,
|
R2 = float.MaxValue,
|
||||||
|
@ -294,7 +321,7 @@ public class LightMapper
|
||||||
lightTable.AddLight(light.ToLightData(32.0f));
|
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
|
// TODO: Handle PropSpotlightAndAmbient
|
||||||
var id = (int)brush.brushInfo;
|
var id = (int)brush.brushInfo;
|
||||||
|
@ -308,6 +335,8 @@ public class LightMapper
|
||||||
var propJointPos = _hierarchy.GetProperty<PropJointPos>(id, "P$JointPos");
|
var propJointPos = _hierarchy.GetProperty<PropJointPos>(id, "P$JointPos");
|
||||||
|
|
||||||
propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 };
|
propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 };
|
||||||
|
propLightColor.Saturation *= settings.Saturation;
|
||||||
|
|
||||||
var joints = propJointPos?.Positions ?? [0, 0, 0, 0, 0, 0];
|
var joints = propJointPos?.Positions ?? [0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
// Transform data
|
// Transform data
|
||||||
|
@ -375,6 +404,11 @@ public class LightMapper
|
||||||
lightTable.AddLight(light.ToLightData(32.0f), propAnimLight.Dynamic);
|
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)
|
if (propLight != null && propLight.Brightness != 0)
|
||||||
{
|
{
|
||||||
var light = new Light
|
var light = new Light
|
||||||
|
@ -439,72 +473,91 @@ 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 = new List<int[]>();
|
|
||||||
if (settings.UsePvs)
|
|
||||||
{
|
{
|
||||||
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 =>
|
||||||
{
|
{
|
||||||
var cellCount = worldRep.Cells.Length;
|
lightCellMap[i] = -1;
|
||||||
var aabbs = new MathUtils.Aabb[worldRep.Cells.Length];
|
var light = _lights[i];
|
||||||
Parallel.For(0, cellCount, i => aabbs[i] = new MathUtils.Aabb(worldRep.Cells[i].Vertices));
|
for (var j = 0; j < cellCount; j++)
|
||||||
|
|
||||||
var lightCellMap = new int[_lights.Count];
|
|
||||||
Parallel.For(0, _lights.Count, i =>
|
|
||||||
{
|
{
|
||||||
lightCellMap[i] = -1;
|
if (!MathUtils.Intersects(aabbs[j], light.Position))
|
||||||
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 pvs = new PotentiallyVisibleSet(worldRep.Cells);
|
|
||||||
Parallel.ForEach(lightCellMap, i =>
|
|
||||||
{
|
|
||||||
if (i != -1) pvs.ComputeVisibility(i);
|
|
||||||
});
|
|
||||||
|
|
||||||
var visibleCellMap = new List<int[]>(_lights.Count);
|
|
||||||
for (var i = 0; i < _lights.Count; i++)
|
|
||||||
{
|
|
||||||
var cellIdx = lightCellMap[i];
|
|
||||||
if (cellIdx == -1)
|
|
||||||
{
|
|
||||||
visibleCellMap.Add([]);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var visibleSet = pvs.GetVisible(lightCellMap[i]);
|
// Half-space contained
|
||||||
visibleCellMap.Add(visibleSet);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return visibleCellMap;
|
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.ComputeCellMightSee(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Parallel.For(0, _lights.Count, i =>
|
||||||
|
{
|
||||||
|
var cellIdx = lightCellMap[i];
|
||||||
|
if (cellIdx == -1)
|
||||||
|
{
|
||||||
|
visibleCellMap[i] = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// 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
|
||||||
|
@ -548,7 +601,7 @@ public class LightMapper
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.UsePvs && !lightVisibleCells[j].Contains(i))
|
if (!lightVisibleCells[j].Contains(i))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -582,7 +635,14 @@ public class LightMapper
|
||||||
|
|
||||||
if (overLit > 0)
|
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);
|
Log.Information("Max cell lights found ({Count}/96)", maxLights);
|
||||||
|
@ -774,7 +834,7 @@ public class LightMapper
|
||||||
|
|
||||||
if (!TraceOcclusion(_scene, 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, settings.Attenuation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,38 +35,12 @@ public class MeshBuilder
|
||||||
var polyVertices = new List<Vector3>();
|
var polyVertices = new List<Vector3>();
|
||||||
foreach (var cell in worldRep.Cells)
|
foreach (var cell in worldRep.Cells)
|
||||||
{
|
{
|
||||||
var numPolys = cell.PolyCount;
|
// We only care about polys representing solid terrain. We can't use RenderPolyCount because that includes
|
||||||
var numRenderPolys = cell.RenderPolyCount;
|
// water surfaces.
|
||||||
var numPortalPolys = cell.PortalPolyCount;
|
var solidPolys = cell.PolyCount - cell.PortalPolyCount;
|
||||||
var solidPolys = numPolys - numPortalPolys;
|
|
||||||
|
|
||||||
var cellIdxOffset = 0;
|
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];
|
var poly = cell.Polys[polyIdx];
|
||||||
polyVertices.Clear();
|
polyVertices.Clear();
|
||||||
polyVertices.EnsureCapacity(poly.VertexCount);
|
polyVertices.EnsureCapacity(poly.VertexCount);
|
||||||
|
@ -75,6 +49,7 @@ public class MeshBuilder
|
||||||
polyVertices.Add(cell.Vertices[cell.Indices[cellIdxOffset + i]]);
|
polyVertices.Add(cell.Vertices[cell.Indices[cellIdxOffset + i]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var primType = cell.RenderPolys[polyIdx].TextureId == 249 ? SurfaceType.Sky : SurfaceType.Solid;
|
||||||
AddPolygon(polyVertices, primType);
|
AddPolygon(polyVertices, primType);
|
||||||
cellIdxOffset += poly.VertexCount;
|
cellIdxOffset += poly.VertexCount;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ public class PotentiallyVisibleSet
|
||||||
{
|
{
|
||||||
private readonly struct Node(List<int> edgeIndices)
|
private readonly struct Node(List<int> edgeIndices)
|
||||||
{
|
{
|
||||||
public readonly HashSet<int> VisibleNodes = [];
|
|
||||||
public readonly List<int> EdgeIndices = edgeIndices;
|
public readonly List<int> EdgeIndices = edgeIndices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,15 +74,9 @@ public class PotentiallyVisibleSet
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Node[] _graph;
|
private readonly Node[] _graph;
|
||||||
private readonly bool[] _computedMap;
|
|
||||||
private readonly List<Edge> _edges;
|
private readonly List<Edge> _edges;
|
||||||
|
|
||||||
private const float Epsilon = 0.1f;
|
private const float Epsilon = MathUtils.Epsilon;
|
||||||
|
|
||||||
// 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];
|
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -99,12 +92,10 @@ public class PotentiallyVisibleSet
|
||||||
public PotentiallyVisibleSet(WorldRep.Cell[] cells)
|
public PotentiallyVisibleSet(WorldRep.Cell[] cells)
|
||||||
{
|
{
|
||||||
_graph = new Node[cells.Length];
|
_graph = new Node[cells.Length];
|
||||||
_computedMap = new bool[cells.Length];
|
|
||||||
|
|
||||||
var portalCount = 0;
|
var portalCount = 0;
|
||||||
for (var i = 0; i < cells.Length; i++)
|
for (var i = 0; i < cells.Length; i++)
|
||||||
{
|
{
|
||||||
_computedMap[i] = false;
|
|
||||||
portalCount += cells[i].PortalPolyCount;
|
portalCount += cells[i].PortalPolyCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,18 +147,119 @@ public class PotentiallyVisibleSet
|
||||||
// Parallel.ForEach(_edges, ComputeEdgeMightSee);
|
// Parallel.ForEach(_edges, ComputeEdgeMightSee);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int[] GetVisible(int cellIdx)
|
public HashSet<int> ComputeVisibilityFast(int cellIdx)
|
||||||
{
|
{
|
||||||
// TODO: Handle out of range indices
|
if (cellIdx >= _graph.Length)
|
||||||
var node = _graph[cellIdx];
|
|
||||||
if (_computedMap[cellIdx])
|
|
||||||
{
|
{
|
||||||
return [..node.VisibleNodes];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputeVisibility(cellIdx);
|
var visibleCells = new HashSet<int>();
|
||||||
_graph[cellIdx] = node;
|
foreach (var edgeIdx in _graph[cellIdx].EdgeIndices)
|
||||||
return [.._graph[cellIdx].VisibleNodes];
|
{
|
||||||
|
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)
|
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
|
private enum Side
|
||||||
{
|
{
|
||||||
Front,
|
Front,
|
||||||
|
@ -434,7 +340,7 @@ public class PotentiallyVisibleSet
|
||||||
|
|
||||||
// TODO: is this reference type poly going to fuck me?
|
// TODO: is this reference type poly going to fuck me?
|
||||||
// TODO: Should this and Poly be in MathUtils?
|
// 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;
|
var vertexCount = poly.Vertices.Count;
|
||||||
if (vertexCount == 0)
|
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
|
// 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
|
||||||
// var distances = new float[vertexCount];
|
var distances = new float[vertexCount];
|
||||||
// var sides = new Side[vertexCount];
|
var sides = new Side[vertexCount];
|
||||||
// var counts = new int[3];
|
var counts = new[] {0, 0, 0};
|
||||||
_clipDistances.Clear();
|
|
||||||
_clipSides.Clear();
|
|
||||||
_clipCounts[0] = 0;
|
|
||||||
_clipCounts[1] = 0;
|
|
||||||
_clipCounts[2] = 0;
|
|
||||||
for (var i = 0; i < vertexCount; i++)
|
for (var i = 0; i < vertexCount; i++)
|
||||||
{
|
{
|
||||||
var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]);
|
var distance = MathUtils.DistanceFromPlane(plane, poly.Vertices[i]);
|
||||||
_clipDistances.Add(distance);
|
distances[i] = distance;
|
||||||
_clipSides.Add(distance switch {
|
sides[i] = distance switch
|
||||||
|
{
|
||||||
> Epsilon => Side.Front,
|
> Epsilon => Side.Front,
|
||||||
<-Epsilon => Side.Back,
|
< -Epsilon => Side.Back,
|
||||||
_ => Side.On,
|
_ => Side.On,
|
||||||
});
|
};
|
||||||
_clipCounts[(int)_clipSides[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 (_clipCounts[(int)Side.Back] == 0 && _clipCounts[(int)Side.On] != vertexCount)
|
if (counts[(int)Side.Back] == 0 && counts[(int)Side.On] != vertexCount)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything is outside the half-space, so we clip everything
|
// 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();
|
poly.Vertices.Clear();
|
||||||
return;
|
return;
|
||||||
|
@ -483,11 +385,11 @@ public class PotentiallyVisibleSet
|
||||||
var i1 = (i + 1) % vertexCount;
|
var i1 = (i + 1) % vertexCount;
|
||||||
var v0 = poly.Vertices[i];
|
var v0 = poly.Vertices[i];
|
||||||
var v1 = poly.Vertices[i1];
|
var v1 = poly.Vertices[i1];
|
||||||
var side = _clipSides[i];
|
var side = sides[i];
|
||||||
var nextSide = _clipSides[i1];
|
var nextSide = sides[i1];
|
||||||
|
|
||||||
// Vertices that are inside/on the half-space don't get clipped
|
// 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);
|
vertices.Add(v0);
|
||||||
}
|
}
|
||||||
|
@ -501,7 +403,7 @@ public class PotentiallyVisibleSet
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is how far along the vector v0 -> v1 the front/back crossover occurs
|
// 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);
|
var splitVertex = v0 + frac * (v1 - v0);
|
||||||
vertices.Add(splitVertex);
|
vertices.Add(splitVertex);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,8 @@ public class LightCommand
|
||||||
[CliArgument(Description = "The name of the mission file including extension.")]
|
[CliArgument(Description = "The name of the mission file including extension.")]
|
||||||
public required string MissionName { get; set; }
|
public required string MissionName { get; set; }
|
||||||
|
|
||||||
[CliOption(Description = "Use a coarse PVS for tighter cell light indices.")]
|
[CliOption(Description = "Use a fast PVS calculation with looser cell light indices.")]
|
||||||
public bool Pvs { get; set; } = false;
|
public bool FastPvs { get; set; } = false;
|
||||||
|
|
||||||
[CliOption(Description = "Name of output file excluding extension.")]
|
[CliOption(Description = "Name of output file excluding extension.")]
|
||||||
public string OutputName { get; set; } = "kc_lit";
|
public string OutputName { get; set; } = "kc_lit";
|
||||||
|
@ -51,7 +51,7 @@ public class LightCommand
|
||||||
Timing.Reset();
|
Timing.Reset();
|
||||||
|
|
||||||
var lightMapper = new LightMapper(InstallPath, CampaignName, MissionName);
|
var lightMapper = new LightMapper(InstallPath, CampaignName, MissionName);
|
||||||
lightMapper.Light(Pvs);
|
lightMapper.Light(FastPvs);
|
||||||
lightMapper.Save(OutputName);
|
lightMapper.Save(OutputName);
|
||||||
|
|
||||||
Timing.LogAll();
|
Timing.LogAll();
|
||||||
|
|
Loading…
Reference in New Issue