Compare commits

..

14 Commits

8 changed files with 516 additions and 216 deletions

View File

@ -500,3 +500,27 @@ public class PropSpotlightAndAmbient : Property
writer.Write(SpotBrightness); writer.Write(SpotBrightness);
} }
} }
public class PropJointPos : Property
{
public float[] Positions;
public override void Read(BinaryReader reader)
{
base.Read(reader);
Positions = new float[6];
for (var i = 0; i < 6; i++)
{
Positions[i] = reader.ReadSingle();
}
}
public override void Write(BinaryWriter writer)
{
base.Write(writer);
foreach (var position in Positions)
{
writer.Write(position);
}
}
}

View File

@ -16,7 +16,7 @@ public class RendParams : IChunk
public string palette; public string palette;
public Vector3 ambientLight; public Vector3 ambientLight;
public int useSunlight; public bool useSunlight;
public SunlightMode sunlightMode; public SunlightMode sunlightMode;
public Vector3 sunlightDirection; public Vector3 sunlightDirection;
public float sunlightHue; public float sunlightHue;
@ -31,7 +31,8 @@ public class RendParams : IChunk
{ {
palette = reader.ReadNullString(16); palette = reader.ReadNullString(16);
ambientLight = reader.ReadVec3(); ambientLight = reader.ReadVec3();
useSunlight = reader.ReadInt32(); useSunlight = reader.ReadBoolean();
reader.ReadBytes(3);
sunlightMode = (SunlightMode)reader.ReadUInt32(); sunlightMode = (SunlightMode)reader.ReadUInt32();
sunlightDirection = reader.ReadVec3(); sunlightDirection = reader.ReadVec3();
sunlightHue = reader.ReadSingle(); sunlightHue = reader.ReadSingle();
@ -58,6 +59,7 @@ public class RendParams : IChunk
writer.WriteNullString(palette, 16); writer.WriteNullString(palette, 16);
writer.WriteVec3(ambientLight); writer.WriteVec3(ambientLight);
writer.Write(useSunlight); writer.Write(useSunlight);
writer.Write(new byte[3]);
writer.Write((uint)sunlightMode); writer.Write((uint)sunlightMode);
writer.WriteVec3(sunlightDirection); writer.WriteVec3(sunlightDirection);
writer.Write(sunlightHue); writer.Write(sunlightHue);

View File

@ -155,6 +155,9 @@ public class DbFile
"P$ModelName" => new PropertyChunk<PropLabel>(), "P$ModelName" => new PropertyChunk<PropLabel>(),
"P$Scale" => new PropertyChunk<PropVector>(), "P$Scale" => new PropertyChunk<PropVector>(),
"P$RenderTyp" => new PropertyChunk<PropRenderType>(), "P$RenderTyp" => new PropertyChunk<PropRenderType>(),
"P$JointPos" => new PropertyChunk<PropJointPos>(),
"P$Immobile" => new PropertyChunk<PropBool>(),
"P$StatShad" => new PropertyChunk<PropBool>(),
"P$OTxtRepr0" => new PropertyChunk<PropString>(), "P$OTxtRepr0" => new PropertyChunk<PropString>(),
"P$OTxtRepr1" => new PropertyChunk<PropString>(), "P$OTxtRepr1" => new PropertyChunk<PropString>(),
"P$OTxtRepr2" => new PropertyChunk<PropString>(), "P$OTxtRepr2" => new PropertyChunk<PropString>(),

View File

@ -91,6 +91,9 @@ public class ObjectHierarchy
AddProp<PropLabel>("P$ModelName"); AddProp<PropLabel>("P$ModelName");
AddProp<PropVector>("P$Scale"); AddProp<PropVector>("P$Scale");
AddProp<PropRenderType>("P$RenderTyp"); AddProp<PropRenderType>("P$RenderTyp");
AddProp<PropJointPos>("P$JointPos");
AddProp<PropBool>("P$Immobile");
AddProp<PropBool>("P$StatShad");
AddProp<PropString>("P$OTxtRepr0"); AddProp<PropString>("P$OTxtRepr0");
AddProp<PropString>("P$OTxtRepr1"); AddProp<PropString>("P$OTxtRepr1");
AddProp<PropString>("P$OTxtRepr2"); AddProp<PropString>("P$OTxtRepr2");

View File

@ -92,6 +92,54 @@ public class ModelFile
} }
} }
public struct SubObject
{
public string Name;
public byte Type;
public int Joint;
public float MinJointValue;
public float MaxJointValue;
public Matrix4x4 Transform;
public short Child;
public short Next;
public ushort VhotIdx;
public ushort VhotCount;
public ushort PointIdx;
public ushort PointCount;
public ushort LightIdx;
public ushort LightCount;
public ushort NormalIdx;
public ushort NormalCount;
public ushort NodeIdx;
public ushort NodeCount;
public SubObject(BinaryReader reader)
{
Name = reader.ReadNullString(8);
Type = reader.ReadByte();
Joint = reader.ReadInt32();
MinJointValue = reader.ReadSingle();
MaxJointValue = reader.ReadSingle();
var v1 = reader.ReadVec3();
var v2 = reader.ReadVec3();
var v3 = reader.ReadVec3();
var v4 = reader.ReadVec3();
Transform = new Matrix4x4(v1.X, v1.Y, v1.Z, 0, v2.X, v2.Y, v2.Z, 0, v3.X, v3.Y, v3.Z, 0, v4.X, v4.Y, v4.Z, 1);
Child = reader.ReadInt16();
Next = reader.ReadInt16();
VhotIdx = reader.ReadUInt16();
VhotCount = reader.ReadUInt16();
PointIdx = reader.ReadUInt16();
PointCount = reader.ReadUInt16();
LightIdx = reader.ReadUInt16();
LightCount = reader.ReadUInt16();
NormalIdx = reader.ReadUInt16();
NormalCount = reader.ReadUInt16();
NodeIdx = reader.ReadUInt16();
NodeCount = reader.ReadUInt16();
}
}
public struct Polygon public struct Polygon
{ {
public ushort Index; public ushort Index;
@ -183,6 +231,7 @@ public class ModelFile
public Polygon[] Polygons { get; } public Polygon[] Polygons { get; }
public Material[] Materials { get; } public Material[] Materials { get; }
public VHot[] VHots { get; } public VHot[] VHots { get; }
public SubObject[] Objects { get; }
public ModelFile(string filename) public ModelFile(string filename)
{ {
@ -231,6 +280,12 @@ public class ModelFile
{ {
VHots[i] = new VHot(reader); VHots[i] = new VHot(reader);
} }
stream.Seek(Header.ObjectOffset, SeekOrigin.Begin);
Objects = new SubObject[Header.ObjectCount];
for (var i = 0; i < Objects.Length; i++)
{
Objects[i] = new SubObject(reader);
}
} }
public bool TryGetVhot(VhotId id, out VHot vhot) public bool TryGetVhot(VhotId id, out VHot vhot)

View File

@ -8,20 +8,23 @@ namespace KeepersCompound.Lightmapper;
public class LightMapper public class LightMapper
{ {
private enum SurfaceType // The objcast element of sunlight is ignored, we just care if it's quadlit
private struct SunSettings
{ {
Solid, public bool Enabled;
Sky, public bool QuadLit;
Water, public Vector3 Direction;
public Vector3 Color;
} }
private class Settings private struct Settings
{ {
public Vector3 AmbientLight; public Vector3 AmbientLight;
public bool Hdr; public bool Hdr;
public SoftnessMode MultiSampling; public SoftnessMode MultiSampling;
public float MultiSamplingCenterWeight; public float MultiSamplingCenterWeight;
public bool LightmappedWater; public bool LightmappedWater;
public SunSettings Sunlight;
} }
private ResourcePathManager.CampaignResources _campaign; private ResourcePathManager.CampaignResources _campaign;
@ -30,7 +33,7 @@ public class LightMapper
private ObjectHierarchy _hierarchy; private ObjectHierarchy _hierarchy;
private Raytracer _scene; private Raytracer _scene;
private List<Light> _lights; private List<Light> _lights;
private List<SurfaceType> _triangleTypeMap; private SurfaceType[] _triangleTypeMap;
public LightMapper( public LightMapper(
string installPath, string installPath,
@ -42,9 +45,17 @@ public class LightMapper
_misPath = _campaign.GetResourcePath(ResourceType.Mission, missionName); _misPath = _campaign.GetResourcePath(ResourceType.Mission, missionName);
_mission = Timing.TimeStage("Parse DB", () => new DbFile(_misPath)); _mission = Timing.TimeStage("Parse DB", () => new DbFile(_misPath));
_hierarchy = Timing.TimeStage("Build Hierarchy", BuildHierarchy); _hierarchy = Timing.TimeStage("Build Hierarchy", BuildHierarchy);
_triangleTypeMap = [];
_scene = Timing.TimeStage("Build Scene", BuildRaytracingScene);
_lights = []; _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() public void Light()
@ -57,6 +68,14 @@ public class LightMapper
return; return;
} }
var sunlightSettings = new SunSettings()
{
Enabled = rendParams.useSunlight,
QuadLit = rendParams.sunlightMode is RendParams.SunlightMode.QuadUnshadowed or RendParams.SunlightMode.QuadObjcastShadows,
Direction = rendParams.sunlightDirection,
Color = Utils.HsbToRgb(rendParams.sunlightHue, rendParams.sunlightSaturation, rendParams.sunlightBrightness),
};
// TODO: lmParams LightmappedWater doesn't mean the game will actually *use* the lightmapped water hmm // TODO: lmParams LightmappedWater doesn't mean the game will actually *use* the lightmapped water hmm
var settings = new Settings var settings = new Settings
{ {
@ -65,6 +84,7 @@ public class LightMapper
MultiSampling = lmParams.ShadowSoftness, MultiSampling = lmParams.ShadowSoftness,
MultiSamplingCenterWeight = lmParams.CenterWeight, MultiSamplingCenterWeight = lmParams.CenterWeight,
LightmappedWater = lmParams.LightmappedWater, LightmappedWater = lmParams.LightmappedWater,
Sunlight = sunlightSettings,
}; };
Timing.TimeStage("Gather Lights", BuildLightList); Timing.TimeStage("Gather Lights", BuildLightList);
@ -109,70 +129,21 @@ public class LightMapper
return new ObjectHierarchy(_mission); return new ObjectHierarchy(_mission);
} }
private Raytracer BuildRaytracingScene() private Mesh BuildMesh()
{ {
var meshBuilder = new MeshBuilder();
// TODO: Should this throw? // TODO: Should this throw?
if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep)) // TODO: Only do object polys if objcast lighting?
if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep) ||
!_mission.TryGetChunk<BrList>("BRLIST", out var brList))
{ {
return null; return meshBuilder.Build();
} }
var vertices = new List<Vector3>(); meshBuilder.AddWorldRepPolys(worldRep);
var indices = new List<int>(); meshBuilder.AddObjectPolys(brList, _hierarchy, _campaign);
foreach (var cell in worldRep.Cells) return meshBuilder.Build();
{
var numPolys = cell.PolyCount;
var numRenderPolys = cell.RenderPolyCount;
var numPortalPolys = cell.PortalPolyCount;
// There's nothing to render
if (numRenderPolys == 0 || numPortalPolys >= numPolys)
{
continue;
}
var solidPolys = numPolys - numPortalPolys;
var cellIdxOffset = 0;
for (var polyIdx = 0; polyIdx < numRenderPolys; polyIdx++)
{
var poly = cell.Polys[polyIdx];
var meshIndexOffset = vertices.Count;
var numPolyVertices = poly.VertexCount;
for (var j = 0; j < numPolyVertices; j++)
{
var vertex = cell.Vertices[cell.Indices[cellIdxOffset + j]];
vertices.Add(vertex);
}
// We need to know what type of surface this poly is so we can map Embree primitive IDs to surface
// types
var renderPoly = cell.RenderPolys[polyIdx];
var primType = SurfaceType.Solid;
if (renderPoly.TextureId == 249)
{
primType = SurfaceType.Sky;
} else if (polyIdx >= solidPolys)
{
primType = SurfaceType.Water;
}
// Cell polygons are n-sided, but fortunately they're convex so we can just do a fan triangulation
for (var j = 1; j < numPolyVertices - 1; j++)
{
indices.Add(meshIndexOffset);
indices.Add(meshIndexOffset + j);
indices.Add(meshIndexOffset + j + 1);
_triangleTypeMap.Add(primType);
}
cellIdxOffset += cell.Polys[polyIdx].VertexCount;
}
}
var rt = new Raytracer();
rt.AddMesh(new TriangleMesh([.. vertices], [.. indices]));
rt.CommitScene();
return rt;
} }
private void BuildLightList() private void BuildLightList()
@ -334,6 +305,8 @@ public class LightMapper
private void SetCellLightIndices() private void SetCellLightIndices()
{ {
// TODO: Doors aren't blocking lights. Need to do some cell traversal to remove light indices :(
if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep)) if (!_mission.TryGetChunk<WorldRep>("WREXT", out var worldRep))
return; return;
@ -354,12 +327,14 @@ public class LightMapper
// The OG lightmapper uses the cell traversal to work out all the cells that // 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 // 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 // in range then we potentially affect the lighting in the cell and add it
// to the list. Cells already contain their sphere bounds so we just use // to the list.
// that for now, but a tighter AABB is another option. // There's a soft length limit here of 96 due to the runtime object shadow
var cellSphere = new MathUtils.Sphere(cell.SphereCenter, cell.SphereRadius); // 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);
foreach (var light in _lights) foreach (var light in _lights)
{ {
if (MathUtils.Intersects(cellSphere, new MathUtils.Sphere(light.Position, light.Radius))) if (MathUtils.Intersects(new MathUtils.Sphere(light.Position, light.Radius), cellAabb))
{ {
cell.LightIndexCount++; cell.LightIndexCount++;
cell.LightIndices.Add((ushort)light.LightTableIndex); cell.LightIndices.Add((ushort)light.LightTableIndex);
@ -437,6 +412,57 @@ public class LightMapper
var planeMapper = new MathUtils.PlanePointMapper(plane.Normal, vs[0], vs[1]); var planeMapper = new MathUtils.PlanePointMapper(plane.Normal, vs[0], vs[1]);
var v2ds = planeMapper.MapTo2d(vs); 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);
// TODO: This isn't quite right yet. It seems to be too bright
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 light in _lights) foreach (var light in _lights)
{ {
var layer = 0; var layer = 0;
@ -466,18 +492,28 @@ public class LightMapper
continue; continue;
} }
for (var y = 0; y < lightmap.Height; y++) var strength = 0f;
var targetPoints = light.QuadLit ? quadTracePoints : tracePoints;
var targetWeights = light.QuadLit ? quadWeights : weights;
for (var idx = 0; idx < targetPoints.Length; idx++)
{ {
for (var x = 0; x < lightmap.Width; x++) var point = targetPoints[idx];
{
var pos = topLeft;
pos += x * 0.25f * renderPoly.TextureVectors.Item1;
pos += y * 0.25f * renderPoly.TextureVectors.Item2;
if (TracePixelMultisampled( // If we're out of range there's no point casting a ray
settings.MultiSampling, light, pos, renderPoly.Center, plane, planeMapper, v2ds, // There's probably a better way to discard the entire lightmap
renderPoly.TextureVectors.Item1, renderPoly.TextureVectors.Item2, // if we're massively out of range
settings.MultiSamplingCenterWeight, out var strength)) if ((point - light.Position).LengthSquared() > light.R2)
{
continue;
}
if (TraceRay(light.Position, point))
{
strength += targetWeights[idx] * light.StrengthAtPoint(point, plane);
}
}
if (strength != 0f)
{ {
// If we're an anim light there's a lot of stuff we need to update // 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 // Firstly we need to add the light to the cells anim light palette
@ -507,83 +543,54 @@ public class LightMapper
}); });
} }
public bool TracePixelMultisampled( private static (Vector3[], float[]) GetTraceOffsetsAndWeights(
SoftnessMode mode, SoftnessMode mode,
Light light,
Vector3 pos,
Vector3 polyCenter,
Plane plane,
MathUtils.PlanePointMapper planeMapper,
Vector2[] v2ds,
Vector3 texU, Vector3 texU,
Vector3 texV, Vector3 texV,
float centerWeight, float centerWeight)
out float strength)
{ {
bool FourPoint(float offsetScale, out float strength) var offsetScale = mode switch
{ {
var hit = false; SoftnessMode.HighFourPoint or SoftnessMode.HighFivePoint or SoftnessMode.HighNinePoint => 4f,
var xOffset = texU / offsetScale; SoftnessMode.MediumFourPoint or SoftnessMode.MediumFivePoint or SoftnessMode.MediumNinePoint => 8f,
var yOffset = texV / offsetScale; SoftnessMode.LowFourPoint => 16f,
hit |= TracePixel(light, pos - xOffset - yOffset, polyCenter, plane, planeMapper, v2ds, out var strength1); _ => 1f,
hit |= TracePixel(light, pos + xOffset - yOffset, polyCenter, plane, planeMapper, v2ds, out var strength2); };
hit |= TracePixel(light, pos - xOffset + yOffset, polyCenter, plane, planeMapper, v2ds, out var strength3);
hit |= TracePixel(light, pos + xOffset + yOffset, polyCenter, plane, planeMapper, v2ds, out var strength4);
strength = (strength1 + strength2 + strength3 + strength4) / 4f;
return hit;
}
bool FivePoint(float offsetScale, out float strength) var cw = centerWeight;
{ var w = 1f - cw;
var hit = false; texU /= offsetScale;
hit |= TracePixel(light, pos, polyCenter, plane, planeMapper, v2ds, out var centerStrength); texV /= offsetScale;
hit |= FourPoint(offsetScale, out strength);
strength = (1.0f - centerWeight) * strength + centerWeight * centerStrength;
return hit;
}
bool NinePoint(float offsetScale, out float strength) return mode switch
{ {
var hit = false; SoftnessMode.LowFourPoint or SoftnessMode.MediumFourPoint or SoftnessMode.HighFourPoint => (
var xOffset = texU / offsetScale; [-texU - texV, -texU - texV, -texU + texV, texU + texV],
var yOffset = texV / offsetScale; [0.25f, 0.25f, 0.25f, 0.25f]),
hit |= TracePixel(light, pos - xOffset - yOffset, polyCenter, plane, planeMapper, v2ds, out var s1); SoftnessMode.MediumFivePoint or SoftnessMode.HighFivePoint => (
hit |= TracePixel(light, pos + xOffset - yOffset, polyCenter, plane, planeMapper, v2ds, out var s2); [Vector3.Zero, -texU - texV, texU - texV, -texU + texV, texU + texV],
hit |= TracePixel(light, pos - xOffset + yOffset, polyCenter, plane, planeMapper, v2ds, out var s3); [cw, w * 0.25f, w * 0.25f, w * 0.25f, w * 0.25f]),
hit |= TracePixel(light, pos + xOffset + yOffset, polyCenter, plane, planeMapper, v2ds, out var s4); SoftnessMode.MediumNinePoint or SoftnessMode.HighNinePoint => (
hit |= TracePixel(light, pos - xOffset, polyCenter, plane, planeMapper, v2ds, out var s5); [Vector3.Zero, -texU - texV, texU - texV, -texU + texV, texU + texV, -texU, texU, -texV, texV],
hit |= TracePixel(light, pos + xOffset, polyCenter, plane, planeMapper, v2ds, out var s6); [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]),
hit |= TracePixel(light, pos - yOffset, polyCenter, plane, planeMapper, v2ds, out var s7); _ => (
hit |= TracePixel(light, pos + yOffset, polyCenter, plane, planeMapper, v2ds, out var s8); [Vector3.Zero],
hit |= TracePixel(light, pos, polyCenter, plane, planeMapper, v2ds, out var centerStrength); [1f]),
strength = (s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8) / 8f;
strength = (1.0f - centerWeight) * strength + centerWeight * centerStrength;
return hit;
}
return mode switch {
SoftnessMode.Standard when light.QuadLit => FourPoint(4f, out strength),
SoftnessMode.HighFourPoint => FourPoint(4f, out strength),
SoftnessMode.HighFivePoint => FivePoint(4f, out strength),
SoftnessMode.HighNinePoint => NinePoint(4f, out strength),
SoftnessMode.MediumFourPoint => FourPoint(8f, out strength),
SoftnessMode.MediumFivePoint => FivePoint(8f, out strength),
SoftnessMode.MediumNinePoint => NinePoint(8f, out strength),
SoftnessMode.LowFourPoint => FourPoint(16f, out strength),
_ => TracePixel(light, pos, polyCenter, plane, planeMapper, v2ds, out strength)
}; };
} }
private bool TracePixel( private Vector3[] GetTracePoints(
Light light, Vector3 basePosition,
Vector3 pos, Vector3[] offsets,
Vector3 polyCenter, Vector3 polyCenter,
Plane plane,
MathUtils.PlanePointMapper planeMapper, MathUtils.PlanePointMapper planeMapper,
Vector2[] v2ds, Vector2[] v2ds)
out float strength)
{ {
strength = 0f; 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 // Embree has robustness issues when hitting poly edges which
// results in false misses. To alleviate this we pre-push everything // results in false misses. To alleviate this we pre-push everything
@ -603,7 +610,7 @@ public class LightMapper
// polygon which have missed // polygon which have missed
// 3. Darkened spots where centers are on the exact edge of a poly // 3. Darkened spots where centers are on the exact edge of a poly
// which can sometimes cause Embree to miss casts // which can sometimes cause Embree to miss casts
var inPoly = TraceRay(polyCenter + plane.Normal * 0.25f, pos); var inPoly = TraceRay(polyCenter + planeMapper.Normal * 0.25f, pos);
if (!inPoly) if (!inPoly)
{ {
var p2d = planeMapper.MapTo2d(pos); var p2d = planeMapper.MapTo2d(pos);
@ -611,22 +618,10 @@ public class LightMapper
pos = planeMapper.MapTo3d(p2d); pos = planeMapper.MapTo3d(p2d);
} }
// If we're out of range there's no point casting a ray tracePoints[i] = pos;
// There's probably a better way to discard the entire lightmap
// if we're massively out of range
if ((pos - light.Position).LengthSquared() > light.R2)
{
return false;
} }
// We cast from the light to the pixel because the light has return tracePoints;
// no mesh in the scene to hit
var hit = TraceRay(light.Position, pos);
if (hit)
{
strength += light.StrengthAtPoint(pos, plane);
}
return hit;
} }
private bool TraceRay(Vector3 origin, Vector3 target) private bool TraceRay(Vector3 origin, Vector3 target)
@ -650,6 +645,28 @@ public class LightMapper
return Math.Abs(hitDistanceFromTarget) < 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() private void SetAnimLightCellMaps()
{ {
if (!_mission.TryGetChunk<PropertyChunk<PropAnimLight>>("P$AnimLight", out var animLightChunk) || if (!_mission.TryGetChunk<PropertyChunk<PropAnimLight>>("P$AnimLight", out var animLightChunk) ||

View File

@ -0,0 +1,194 @@
using System.Numerics;
using KeepersCompound.LGS;
using KeepersCompound.LGS.Database;
using KeepersCompound.LGS.Database.Chunks;
namespace KeepersCompound.Lightmapper;
// TODO: Rename to CastSurfaceType?
public enum SurfaceType
{
Solid,
Sky,
Water,
Air,
}
public class Mesh(int triangleCount, List<Vector3> vertices, List<int> indices, List<SurfaceType> triangleSurfaceMap)
{
public int TriangleCount { get; } = triangleCount;
public Vector3[] Vertices { get; } = [..vertices];
public int[] Indices { get; } = [..indices];
public SurfaceType[] TriangleSurfaceMap { get; } = [..triangleSurfaceMap];
}
public class MeshBuilder
{
private int _triangleCount = 0;
private readonly List<Vector3> _vertices = [];
private readonly List<int> _indices = [];
private readonly List<SurfaceType> _primSurfaceMap = [];
public void AddWorldRepPolys(WorldRep worldRep)
{
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;
var cellIdxOffset = 0;
for (var polyIdx = 0; polyIdx < numPolys; polyIdx++)
{
// There's 3 types of poly that we need to include in the mesh:
// - Terrain
// - Water surfaces
// - Door vision blockers
//
// Door vision blockers are the interesting one. They're not RenderPolys at all, and we only include
// them in the mesh if the cell only has two of them (otherwise the door is in the middle of the air)
SurfaceType primType;
if (polyIdx < solidPolys)
{
primType = cell.RenderPolys[polyIdx].TextureId == 249 ? SurfaceType.Sky : SurfaceType.Solid;
}
else if (polyIdx < numRenderPolys)
{
primType = SurfaceType.Water;
}
else if (cell is { Flags: 24, PortalPolyCount: 2 }) // TODO: Work out what these flags are!!
{
primType = SurfaceType.Solid;
}
else
{
continue;
}
var poly = cell.Polys[polyIdx];
polyVertices.Clear();
polyVertices.EnsureCapacity(poly.VertexCount);
for (var i = 0; i < poly.VertexCount; i++)
{
polyVertices.Add(cell.Vertices[cell.Indices[cellIdxOffset + i]]);
}
AddPolygon(polyVertices, primType);
cellIdxOffset += poly.VertexCount;
}
}
}
public void AddObjectPolys(
BrList brushList,
ObjectHierarchy hierarchy,
ResourcePathManager.CampaignResources campaignResources)
{
var polyVertices = new List<Vector3>();
foreach (var brush in brushList.Brushes)
{
if (brush.media != BrList.Brush.Media.Object)
{
continue;
}
var id = (int)brush.brushInfo;
var modelNameProp = hierarchy.GetProperty<PropLabel>(id, "P$ModelName");
var scaleProp = hierarchy.GetProperty<PropVector>(id, "P$Scale");
var renderTypeProp = hierarchy.GetProperty<PropRenderType>(id, "P$RenderTyp");
var jointPosProp = hierarchy.GetProperty<PropJointPos>(id, "P$JointPos");
var immobileProp = hierarchy.GetProperty<PropBool>(id, "P$Immobile");
var staticShadowProp = hierarchy.GetProperty<PropBool>(id, "P$StatShad");
var joints = jointPosProp?.Positions ?? [0, 0, 0, 0, 0, 0];
var castsShadows = (immobileProp?.value ?? false) || (staticShadowProp?.value ?? false);
var renderMode = renderTypeProp?.mode ?? PropRenderType.Mode.Normal;
// TODO: Check which rendermodes cast shadows :)
if (modelNameProp == null || !castsShadows || renderMode == PropRenderType.Mode.CoronaOnly)
{
continue;
}
// Let's try and place an object :)
var modelName = modelNameProp.value.ToLower() + ".bin";
var modelPath = campaignResources.GetResourcePath(ResourceType.Object, modelName);
if (modelPath == null)
{
continue;
}
// TODO: Handle failing to find model more gracefully
var pos = brush.position;
var rot = brush.angle;
var scale = scaleProp?.value ?? Vector3.One;
var model = new ModelFile(modelPath);
pos -= model.Header.Center;
// for each object modify the vertices
// TODO: Almost perfect transform!
// TODO: Handle nested sub objects
foreach (var subObj in model.Objects)
{
var jointTrans = Matrix4x4.Identity;
if (subObj.Joint != -1)
{
var ang = float.DegreesToRadians(joints[subObj.Joint]);
var jointRot = Matrix4x4.CreateFromYawPitchRoll(0, ang, 0);
var objTrans = subObj.Transform;
jointTrans = jointRot * objTrans;
}
var scalePart = Matrix4x4.CreateScale(scale);
var rotPart = Matrix4x4.CreateFromYawPitchRoll(float.DegreesToRadians(rot.Y), float.DegreesToRadians(rot.X),
float.DegreesToRadians(rot.Z));
var transPart = Matrix4x4.CreateTranslation(pos);
var transform = jointTrans * scalePart * rotPart * transPart;
var start = subObj.PointIdx;
var end = start + subObj.PointCount;
for (var i = start; i < end; i++)
{
var v = model.Vertices[i];
model.Vertices[i] = Vector3.Transform(v, transform);
}
}
// for each polygon slam its vertices and indices :)
foreach (var poly in model.Polygons)
{
polyVertices.Clear();
polyVertices.EnsureCapacity(poly.VertexCount);
foreach (var idx in poly.VertexIndices)
{
polyVertices.Add(model.Vertices[idx]);
}
AddPolygon(polyVertices, SurfaceType.Solid);
}
}
}
private void AddPolygon(List<Vector3> vertices, SurfaceType surfaceType)
{
var vertexCount = vertices.Count;
var indexOffset = _vertices.Count;
// Polygons are n-sided, but fortunately they're convex so we can just do a fan triangulation
_vertices.AddRange(vertices);
for (var i = 1; i < vertexCount - 1; i++)
{
_indices.Add(indexOffset);
_indices.Add(indexOffset + i);
_indices.Add(indexOffset + i + 1);
_primSurfaceMap.Add(surfaceType);
_triangleCount++;
}
}
public Mesh Build()
{
return new Mesh(_triangleCount, _vertices, _indices, _primSurfaceMap);
}
}

View File

@ -90,12 +90,14 @@ public static class MathUtils
public record PlanePointMapper public record PlanePointMapper
{ {
public Vector3 Normal { get; }
Vector3 _origin; Vector3 _origin;
Vector3 _xAxis; Vector3 _xAxis;
Vector3 _yAxis; Vector3 _yAxis;
public PlanePointMapper(Vector3 normal, Vector3 p0, Vector3 p1) public PlanePointMapper(Vector3 normal, Vector3 p0, Vector3 p1)
{ {
Normal = normal;
_origin = p0; _origin = p0;
_xAxis = p1 - _origin; _xAxis = p1 - _origin;
_yAxis = Vector3.Cross(normal, _xAxis); _yAxis = Vector3.Cross(normal, _xAxis);