Compare commits

...

11 Commits

5 changed files with 405 additions and 58 deletions

View File

@ -330,6 +330,112 @@ public class PropLight : Property
} }
} }
public class PropAnimLight : Property
{
public enum AnimMode
{
FlipMinMax,
SlideSmoothly,
Random,
MinBrightness,
MaxBrightness,
ZeroBrightness,
SmoothlyBrighten,
SmoothlyDim,
RandomButCoherent,
FlickerMinMax,
}
// Standard light props
public float Brightness;
public Vector3 Offset;
public float Radius;
public float InnerRadius;
public bool QuadLit;
public bool Dynamic;
// Animation
public AnimMode Mode;
public int MsToBrighten;
public int MsToDim;
public float MinBrightness;
public float MaxBrightness;
// Start state
public float CurrentBrightness;
public bool Rising;
public int Timer;
public bool Inactive;
// World rep info
public bool Refresh; // Not relevant to us. It's used to tell dromed it needs to relight
public ushort LightTableMapIndex;
public ushort LightTableLightIndex;
public ushort CellsReached;
private int _unknown;
public override void Read(BinaryReader reader)
{
base.Read(reader);
Brightness = reader.ReadSingle();
Offset = reader.ReadVec3();
Refresh = reader.ReadBoolean();
reader.ReadBytes(3);
LightTableMapIndex = reader.ReadUInt16();
CellsReached = reader.ReadUInt16();
LightTableLightIndex = reader.ReadUInt16();
Mode = (AnimMode)reader.ReadUInt16();
MsToBrighten = reader.ReadInt32();
MsToDim = reader.ReadInt32();
MinBrightness = reader.ReadSingle();
MaxBrightness = reader.ReadSingle();
CurrentBrightness = reader.ReadSingle();
Rising = reader.ReadBoolean();
reader.ReadBytes(3);
Timer = reader.ReadInt32();
Inactive = reader.ReadBoolean();
reader.ReadBytes(3);
Radius = reader.ReadSingle();
_unknown = reader.ReadInt32();
QuadLit = reader.ReadBoolean();
reader.ReadBytes(3);
InnerRadius = reader.ReadSingle();
Dynamic = reader.ReadBoolean();
reader.ReadBytes(3);
}
public override void Write(BinaryWriter writer)
{
base.Write(writer);
writer.Write(Brightness);
writer.WriteVec3(Offset);
writer.Write(Refresh);
writer.Write(new byte[3]);
writer.Write(LightTableMapIndex);
writer.Write(CellsReached);
writer.Write(LightTableLightIndex);
writer.Write((ushort)Mode);
writer.Write(MsToBrighten);
writer.Write(MsToDim);
writer.Write(MinBrightness);
writer.Write(MaxBrightness);
writer.Write(CurrentBrightness);
writer.Write(Rising);
writer.Write(new byte[3]);
writer.Write(Timer);
writer.Write(Inactive);
writer.Write(new byte[3]);
writer.Write(Radius);
writer.Write(_unknown);
writer.Write(QuadLit);
writer.Write(new byte[3]);
writer.Write(InnerRadius);
writer.Write(Dynamic);
writer.Write(new byte[3]);
}
}
public class PropLightColor : Property public class PropLightColor : Property
{ {
public float Hue; public float Hue;

View File

@ -1,5 +1,3 @@
using System;
using System.IO;
using System.Numerics; using System.Numerics;
namespace KeepersCompound.LGS.Database.Chunks; namespace KeepersCompound.LGS.Database.Chunks;
@ -419,9 +417,179 @@ public class WorldRep : IChunk
} }
} }
public struct BspTree
{
public struct Node
{
int parentIndex; // TODO: Split the flags out of this
int cellId;
int planeId;
uint insideIndex;
uint outsideIndex;
public Node(BinaryReader reader)
{
parentIndex = reader.ReadInt32();
cellId = reader.ReadInt32();
planeId = reader.ReadInt32();
insideIndex = reader.ReadUInt32();
outsideIndex = reader.ReadUInt32();
}
public readonly void Write(BinaryWriter writer)
{
writer.Write(parentIndex);
writer.Write(cellId);
writer.Write(planeId);
writer.Write(insideIndex);
writer.Write(outsideIndex);
}
}
public uint PlaneCount;
public uint NodeCount;
public Plane[] Planes;
public Node[] Nodes;
public BspTree(BinaryReader reader)
{
PlaneCount = reader.ReadUInt32();
Planes = new Plane[PlaneCount];
for (var i = 0; i < PlaneCount; i++)
{
Planes[i] = new Plane(reader.ReadVec3(), reader.ReadSingle());
}
NodeCount = reader.ReadUInt32();
Nodes = new Node[NodeCount];
for (var i = 0; i < NodeCount; i++)
{
Nodes[i] = new Node(reader);
}
}
public readonly void Write(BinaryWriter writer)
{
writer.Write(PlaneCount);
foreach (var plane in Planes)
{
writer.WriteVec3(plane.Normal);
writer.Write(plane.D);
}
writer.Write(NodeCount);
foreach (var node in Nodes)
{
node.Write(writer);
}
}
}
public struct LightTable
{
public struct LightData
{
public Vector3 Location;
public Vector3 Direction;
public Vector3 Color;
float InnerAngle; // I'm pretty sure these are the spotlight angles
float OuterAngle;
float Radius;
public LightData(BinaryReader reader)
{
Location = reader.ReadVec3();
Direction = reader.ReadVec3();
Color = reader.ReadVec3();
InnerAngle = reader.ReadSingle();
OuterAngle = reader.ReadSingle();
Radius = reader.ReadSingle();
}
public readonly void Write(BinaryWriter writer)
{
writer.WriteVec3(Location);
writer.WriteVec3(Direction);
writer.WriteVec3(Color);
writer.Write(InnerAngle);
writer.Write(OuterAngle);
writer.Write(Radius);
}
}
public struct AnimCellMap
{
public ushort CellIndex;
public ushort LightIndex;
public AnimCellMap(BinaryReader reader)
{
CellIndex = reader.ReadUInt16();
LightIndex = reader.ReadUInt16();
}
public readonly void Write(BinaryWriter writer)
{
writer.Write(CellIndex);
writer.Write(LightIndex);
}
}
public int LightCount;
public int DynamicLightCount;
public int AnimMapCount;
public LightData[] Lights;
public LightData[] ScratchpadLights;
public AnimCellMap[] AnimCellMaps;
// TODO: Support olddark
public LightTable(BinaryReader reader)
{
LightCount = reader.ReadInt32();
DynamicLightCount = reader.ReadInt32();
Lights = new LightData[LightCount + DynamicLightCount];
for (var i = 0; i < Lights.Length; i++)
{
Lights[i] = new LightData(reader);
}
ScratchpadLights = new LightData[32];
for (var i = 0; i < ScratchpadLights.Length; i++)
{
ScratchpadLights[i] = new LightData(reader);
}
AnimMapCount = reader.ReadInt32();
AnimCellMaps = new AnimCellMap[AnimMapCount];
for (var i = 0; i < AnimCellMaps.Length; i++)
{
AnimCellMaps[i] = new AnimCellMap(reader);
}
}
public readonly void Write(BinaryWriter writer)
{
writer.Write(LightCount);
writer.Write(DynamicLightCount);
foreach (var light in Lights)
{
light.Write(writer);
}
foreach (var light in ScratchpadLights)
{
light.Write(writer);
}
writer.Write(AnimMapCount);
foreach (var map in AnimCellMaps)
{
map.Write(writer);
}
}
}
public ChunkHeader Header { get; set; } public ChunkHeader Header { get; set; }
public WrHeader DataHeader { get; set; } public WrHeader DataHeader { get; set; }
public Cell[] Cells { get; set; } public Cell[] Cells { get; set; }
public BspTree Bsp { get; set; }
public LightTable LightingTable { get; set; }
private byte[] _unknown;
private byte[] _unreadData; private byte[] _unreadData;
public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry) public void ReadData(BinaryReader reader, DbFile.TableOfContents.Entry entry)
@ -435,6 +603,12 @@ public class WorldRep : IChunk
Cells[i] = new Cell(reader, bpp); Cells[i] = new Cell(reader, bpp);
} }
Bsp = new BspTree(reader);
// TODO: Work out what this is
_unknown = reader.ReadBytes(Cells.Length);
LightingTable = new LightTable(reader);
// TODO: All the other info lol // TODO: All the other info lol
var length = entry.Offset + entry.Size + 24 - reader.BaseStream.Position; var length = entry.Offset + entry.Size + 24 - reader.BaseStream.Position;
_unreadData = reader.ReadBytes((int)length); _unreadData = reader.ReadBytes((int)length);
@ -447,6 +621,9 @@ public class WorldRep : IChunk
{ {
cell.Write(writer); cell.Write(writer);
} }
Bsp.Write(writer);
writer.Write(_unknown);
LightingTable.Write(writer);
writer.Write(_unreadData); writer.Write(_unreadData);
} }
} }

View File

@ -32,13 +32,13 @@ public class DbFile
} }
} }
public readonly struct TableOfContents public struct TableOfContents
{ {
public readonly struct Entry public struct Entry
{ {
public string Name { get; } public string Name;
public uint Offset { get; } public uint Offset;
public uint Size { get; } public uint Size;
public Entry(BinaryReader reader) public Entry(BinaryReader reader)
{ {
@ -47,10 +47,9 @@ public class DbFile
Size = reader.ReadUInt32(); Size = reader.ReadUInt32();
} }
public override string ToString() public override readonly string ToString()
{ {
// return $"Name: {Name}, Offset: {O}" return $"Name: {Name}, Offset: {Offset}, Size: {Size}";
return base.ToString();
} }
public readonly void Write(BinaryWriter writer) public readonly void Write(BinaryWriter writer)
@ -110,18 +109,28 @@ public class DbFile
public void Save(string filename) public void Save(string filename)
{ {
// !HACK: Right now we don't need to adjust TOC offset or anything because we're only
// overwriting data, not writing new lengths of data
using var stream = File.Open(filename, FileMode.Create); using var stream = File.Open(filename, FileMode.Create);
using var writer = new BinaryWriter(stream, Encoding.UTF8, false); using var writer = new BinaryWriter(stream, Encoding.UTF8, false);
Header.Write(writer); Header.Write(writer);
foreach (var (name, chunk) in Chunks) for (var i = 0; i < Toc.ItemCount; i++)
{ {
var item = Toc.Items[i];
var pos = stream.Position;
var chunk = Chunks[item.Name];
chunk.Write(writer); chunk.Write(writer);
var size = stream.Position - pos - 24;
item.Offset = (uint)pos;
item.Size = (uint)size;
Toc.Items[i] = item;
} }
var tocOffset = (uint)stream.Position;
Toc.Write(writer); Toc.Write(writer);
stream.Seek(0, SeekOrigin.Begin);
writer.Write(tocOffset);
} }
private static IChunk NewChunk(string entryName) private static IChunk NewChunk(string entryName)
@ -143,6 +152,7 @@ public class DbFile
"P$OTxtRepr2" => new PropertyChunk<PropString>(), "P$OTxtRepr2" => new PropertyChunk<PropString>(),
"P$OTxtRepr3" => new PropertyChunk<PropString>(), "P$OTxtRepr3" => new PropertyChunk<PropString>(),
"P$Light" => new PropertyChunk<PropLight>(), "P$Light" => new PropertyChunk<PropLight>(),
"P$AnimLight" => new PropertyChunk<PropAnimLight>(),
"P$LightColo" => new PropertyChunk<PropLightColor>(), "P$LightColo" => new PropertyChunk<PropLightColor>(),
"P$Spotlight" => new PropertyChunk<PropSpotlight>(), "P$Spotlight" => new PropertyChunk<PropSpotlight>(),
"P$RenderAlp" => new PropertyChunk<PropFloat>(), "P$RenderAlp" => new PropertyChunk<PropFloat>(),

View File

@ -101,11 +101,13 @@ public class ObjectHierarchy
AddProp<PropString>("P$OTxtRepr3"); AddProp<PropString>("P$OTxtRepr3");
AddProp<PropFloat>("P$RenderAlp"); AddProp<PropFloat>("P$RenderAlp");
AddProp<PropLight>("P$Light"); AddProp<PropLight>("P$Light");
AddProp<PropAnimLight>("P$AnimLight");
AddProp<PropLightColor>("P$LightColo"); AddProp<PropLightColor>("P$LightColo");
AddProp<PropSpotlight>("P$Spotlight"); AddProp<PropSpotlight>("P$Spotlight");
} }
public T GetProperty<T>(int objectId, string propName) where T : Property // TODO: Work out if there's some nice way to automatically decide if we inherit
public T GetProperty<T>(int objectId, string propName, bool inherit = true) where T : Property
{ {
if (!_objects.ContainsKey(objectId)) if (!_objects.ContainsKey(objectId))
{ {
@ -121,7 +123,7 @@ public class ObjectHierarchy
} }
var prop = obj.GetProperty<T>(propName); var prop = obj.GetProperty<T>(propName);
if (prop != null) if (prop != null || !inherit)
{ {
return prop; return prop;
} }

View File

@ -22,6 +22,9 @@ class Program
public Vector3 spotlightDir; public Vector3 spotlightDir;
public float spotlightInnerAngle; public float spotlightInnerAngle;
public float spotlightOuterAngle; public float spotlightOuterAngle;
public bool anim;
public int animLightTableIndex;
} }
static void Main(string[] args) static void Main(string[] args)
@ -33,18 +36,16 @@ class Program
var campaignName = "JAYRUDE_Tests"; var campaignName = "JAYRUDE_Tests";
var missionName = "lm_test.cow"; var missionName = "lm_test.cow";
// campaignName = "TDP20AC_a_burrick_in_a_room";
// missionName = "miss20.mis";
// Setup extract path // Setup extract path
var tmpDir = Directory.CreateTempSubdirectory("KCLightmapper"); var tmpDir = Directory.CreateTempSubdirectory("KCLightmapper");
Console.WriteLine(tmpDir.FullName);
var resPathManager = new ResourcePathManager(tmpDir.FullName); var resPathManager = new ResourcePathManager(tmpDir.FullName);
resPathManager.Init(installPath); resPathManager.Init(installPath);
var campaign = resPathManager.GetCampaign(campaignName); var campaign = resPathManager.GetCampaign(campaignName);
var misPath = campaign.GetResourcePath(ResourceType.Mission, missionName); var misPath = campaign.GetResourcePath(ResourceType.Mission, missionName);
// misPath = "/stuff/Games/thief/drive_c/GOG Games/TG ND 1.27 (MAPPING)/FMs/JAYRUDE_Tests/lm_test.cow";
// misPath = "/stuff/Games/thief/drive_c/GOG Games/TG ND 1.27 (MAPPING)/FMs/AtdV/miss20.mis";
// misPath = "/stuff/Games/thief/drive_c/GOG Games/TG ND 1.27 (MAPPING)/FMs/TDP20AC_a_burrick_in_a_room/miss20.mis";
Timing.TimeStage("Total", () => LightmapMission(campaign, misPath)); Timing.TimeStage("Total", () => LightmapMission(campaign, misPath));
Timing.LogAll(); Timing.LogAll();
@ -132,24 +133,17 @@ class Program
{ {
// TODO: Handle PropSpotlightAndAmbient // TODO: Handle PropSpotlightAndAmbient
var id = (int)brush.brushInfo; var id = (int)brush.brushInfo;
var propLight = hierarchy.GetProperty<PropLight>(id, "P$Light"); var propAnimLight = hierarchy.GetProperty<PropAnimLight>(id, "P$AnimLight", false);
var propLight = hierarchy.GetProperty<PropLight>(id, "P$Light", false);
var propLightColor = hierarchy.GetProperty<PropLightColor>(id, "P$LightColo"); var propLightColor = hierarchy.GetProperty<PropLightColor>(id, "P$LightColo");
var propSpotlight = hierarchy.GetProperty<PropSpotlight>(id, "P$Spotlight"); var propSpotlight = hierarchy.GetProperty<PropSpotlight>(id, "P$Spotlight");
var propModelname = hierarchy.GetProperty<PropLabel>(id, "P$ModelName"); var propModelname = hierarchy.GetProperty<PropLabel>(id, "P$ModelName");
if (propLight != null)
{
propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 }; propLightColor ??= new PropLightColor { Hue = 0, Saturation = 0 };
// TODO: There's still some lights that aren't positioned right such as Streetlamp. var baseLight = new Light
// Perhaps there's a light point specified in model files?
var light = new Light
{ {
position = brush.position + propLight.Offset, position = brush.position,
color = HsbToRgb(propLightColor.Hue, propLightColor.Saturation, propLight.Brightness),
innerRadius = propLight.InnerRadius,
radius = propLight.Radius,
r2 = propLight.Radius * propLight.Radius,
spotlightDir = -Vector3.UnitZ, spotlightDir = -Vector3.UnitZ,
}; };
@ -157,31 +151,46 @@ class Program
{ {
var resName = $"{propModelname.value.ToLower()}.bin"; var resName = $"{propModelname.value.ToLower()}.bin";
var modelPath = campaign.GetResourcePath(ResourceType.Object, resName); var modelPath = campaign.GetResourcePath(ResourceType.Object, resName);
if (modelPath != null)
{
// TODO: Handle failing to find model more gracefully
var model = new ModelFile(modelPath); var model = new ModelFile(modelPath);
if (model.TryGetVhot(ModelFile.VhotId.LightPosition, out var vhot)) if (model.TryGetVhot(ModelFile.VhotId.LightPosition, out var vhot))
{ {
light.position += vhot.Position; baseLight.position += vhot.Position;
} }
if (model.TryGetVhot(ModelFile.VhotId.LightDirection, out vhot)) if (model.TryGetVhot(ModelFile.VhotId.LightDirection, out vhot))
{ {
light.spotlightDir = vhot.Position; baseLight.spotlightDir = vhot.Position;
} }
} }
}
if (propSpotlight != null) if (propSpotlight != null)
{ {
// TODO: Some objects seem to have spotlight direction embedded in the model file
var rot = Matrix4x4.Identity; var rot = Matrix4x4.Identity;
rot *= Matrix4x4.CreateRotationX(float.DegreesToRadians(brush.angle.X)); rot *= Matrix4x4.CreateRotationX(float.DegreesToRadians(brush.angle.X));
rot *= Matrix4x4.CreateRotationY(float.DegreesToRadians(brush.angle.Y)); rot *= Matrix4x4.CreateRotationY(float.DegreesToRadians(brush.angle.Y));
rot *= Matrix4x4.CreateRotationZ(float.DegreesToRadians(brush.angle.Z)); rot *= Matrix4x4.CreateRotationZ(float.DegreesToRadians(brush.angle.Z));
light.spotlight = true; baseLight.spotlight = true;
light.spotlightDir = Vector3.Transform(light.spotlightDir, rot); baseLight.spotlightDir = Vector3.Transform(baseLight.spotlightDir, rot);
light.spotlightInnerAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.InnerAngle)); baseLight.spotlightInnerAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.InnerAngle));
light.spotlightOuterAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.OuterAngle)); baseLight.spotlightOuterAngle = (float)Math.Cos(float.DegreesToRadians(propSpotlight.OuterAngle));
} }
if (propLight != null)
{
var light = new Light
{
position = baseLight.position + propLight.Offset,
color = HsbToRgb(propLightColor.Hue, propLightColor.Saturation, propLight.Brightness),
innerRadius = propLight.InnerRadius,
radius = propLight.Radius,
r2 = propLight.Radius * propLight.Radius,
};
if (propLight.Radius == 0) if (propLight.Radius == 0)
{ {
light.radius = float.MaxValue; light.radius = float.MaxValue;
@ -190,6 +199,27 @@ class Program
lights.Add(light); lights.Add(light);
} }
if (propAnimLight != null)
{
var light = new Light
{
position = baseLight.position + propAnimLight.Offset,
color = HsbToRgb(propLightColor.Hue, propLightColor.Saturation, propAnimLight.MaxBrightness),
innerRadius = propAnimLight.InnerRadius,
radius = propAnimLight.Radius,
r2 = propAnimLight.Radius * propAnimLight.Radius,
anim = true,
animLightTableIndex = propAnimLight.LightTableLightIndex,
};
if (propAnimLight.Radius == 0)
{
light.radius = float.MaxValue;
light.r2 = float.MaxValue;
}
lights.Add(light);
}
} }
} }
} }
@ -323,6 +353,28 @@ class Program
foreach (var light in lights) foreach (var light in lights)
{ {
var layer = 0;
if (light.anim)
{
var paletteIdx = -1;
for (var i = 0; i < cell.AnimLightCount; i++)
{
var id = cell.AnimLights[i];
if (id == light.animLightTableIndex)
{
paletteIdx = i;
}
}
if (paletteIdx == -1 || (info.AnimLightBitmask & (1 << paletteIdx)) == 0)
{
continue;
}
var mask = info.AnimLightBitmask & ((1 << (paletteIdx + 1)) - 1);
layer = BitOperations.PopCount((uint)mask);
}
// Check if plane normal is facing towards the light // Check if plane normal is facing towards the light
// If it's not then we're never going to be (directly) lit by this // If it's not then we're never going to be (directly) lit by this
// light. // light.
@ -391,7 +443,7 @@ class Program
if (hit) if (hit)
{ {
var strength = CalculateLightStrengthAtPoint(light, pos, plane); var strength = CalculateLightStrengthAtPoint(light, pos, plane);
lightmap.AddLight(0, x, y, light.color, strength, hdr); lightmap.AddLight(layer, x, y, light.color, strength, hdr);
} }
} }
} }