thief-mission-viewer/project/code/TMV/Mission.cs

384 lines
11 KiB
C#

using Godot;
using KeepersCompound.LGS.Database;
using KeepersCompound.LGS.Database.Chunks;
using KeepersCompound.TMV.UI;
using RectpackSharp;
using System;
using System.Collections.Generic;
namespace KeepersCompound.TMV;
public partial class Mission : Node3D
{
private readonly struct LightmapRectData
{
public readonly int cellIndex;
public readonly int lightmapIndex;
public readonly int textureId;
public readonly int uvStart;
public readonly int uvEnd;
public LightmapRectData(int cellIndex, int lightmapIndex, int textureId, int uvStart, int uvEnd)
{
this.cellIndex = cellIndex;
this.lightmapIndex = lightmapIndex;
this.textureId = textureId;
this.uvStart = uvStart;
this.uvEnd = uvEnd;
}
}
[Export(PropertyHint.GlobalFile, "*.mis")]
public string FileName { get; set; }
[Export]
public bool Build = false;
[Export]
public bool Clear = false;
[Export]
public bool Dump = false;
InstallPaths _installPaths;
DbFile _file;
TextureLoader _textureLoader;
public override void _Ready()
{
var missionSelector = GetNode<Control>("%MissionSelector") as MissionSelector;
missionSelector.LoadMission += (string rootPath, string missionPath) =>
{
_installPaths = new InstallPaths(rootPath);
FileName = missionPath;
Build = true;
};
}
public override void _Process(double delta)
{
if (Build)
{
RebuildMap();
Build = false;
}
if (Clear)
{
ClearMap();
Clear = false;
}
}
public override void _Input(InputEvent @event)
{
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
{
if (keyEvent.Keycode == Key.R)
{
Build = true;
}
}
}
public void ClearMap()
{
foreach (var node in GetChildren())
{
node.QueueFree();
}
}
public void RebuildMap()
{
ClearMap();
_textureLoader = new TextureLoader(_installPaths.famPath, FileName.GetBaseDir());
_file = new(FileName);
UseChunk<TxList>("TXLIST", LoadTextures);
UseChunk<WorldRep>("WREXT", BuildWrMeshes);
}
private void UseChunk<T>(string name, Action<T> action)
{
if (_file.Chunks.TryGetValue(name, out var value))
{
action((T)value);
}
else
{
GD.Print($"No chunk found/loaded: {name}");
}
}
private void BuildWrMeshes(WorldRep worldRep)
{
var cells = worldRep.Cells;
var lmHdr = worldRep.DataHeader.LightmapFormat == 2;
GD.Print($"HDR Lightmap: {lmHdr}");
var packingRects = new List<PackingRectangle>();
var surfaceDataMap = new Dictionary<int, MeshSurfaceData>();
var rectDataMap = new Dictionary<int, LightmapRectData>();
for (var cellIdx = 0; cellIdx < cells.Length; cellIdx++)
{
var cell = cells[cellIdx];
var numPolys = cell.PolyCount;
var numRenderPolys = cell.RenderPolyCount;
var numPortalPolys = cell.PortalPolyCount;
// There's nothing to render
if (numRenderPolys == 0 || numPortalPolys >= numPolys)
{
continue;
}
// You'd think these would be the same number, but apparently not
// I think it's because water counts as a render poly and a portal poly
var maxPolyIdx = Math.Min(numRenderPolys, numPolys - numPortalPolys);
var cellIdxOffset = 0;
for (int polyIdx = 0; polyIdx < maxPolyIdx; polyIdx++)
{
var lightmapRectData = ProcessCellSurfaceData(surfaceDataMap, cell, cellIdx, polyIdx, cellIdxOffset);
rectDataMap.Add(packingRects.Count, lightmapRectData);
var light = cell.LightList[polyIdx];
var rect = new PackingRectangle(0, 0, light.Width, light.Height, packingRects.Count);
packingRects.Add(rect);
cellIdxOffset += cell.Polys[polyIdx].VertexCount;
}
}
var lightmapTexture = BuildLightmapTexture(cells, packingRects.ToArray(), rectDataMap, surfaceDataMap);
foreach (var (textureId, surface) in surfaceDataMap)
{
if (surface.Empty)
{
continue;
}
var albedoTexture = _textureLoader.Get(textureId);
var mesh = new ArrayMesh();
mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surface.BuildSurfaceArray());
mesh.SurfaceSetMaterial(0, BuildMaterial(albedoTexture, lightmapTexture, lmHdr));
var meshInstance = new MeshInstance3D { Mesh = mesh };
AddChild(meshInstance);
}
}
private LightmapRectData ProcessCellSurfaceData(Dictionary<int, MeshSurfaceData> surfaceDataMap, WorldRep.Cell cell, int cellIdx, int polyIdx, int indicesOffset)
{
var poly = cell.Polys[polyIdx];
var normal = cell.Planes[poly.PlaneId].Normal.ToGodotVec3();
var vertices = new List<Vector3>();
var textureUvs = new List<Vector2>();
var lightmapUvs = new List<Vector2>();
var numPolyVertices = poly.VertexCount;
for (var j = 0; j < numPolyVertices; j++)
{
var vertex = cell.Vertices[cell.Indices[indicesOffset + j]];
vertices.Add(vertex.ToGodotVec3());
}
var renderPoly = cell.RenderPolys[polyIdx];
var light = cell.LightList[polyIdx];
var textureId = CalcBaseUV(cell, poly, renderPoly, light, textureUvs, lightmapUvs, indicesOffset);
if (!surfaceDataMap.ContainsKey(textureId))
{
surfaceDataMap.Add(textureId, new MeshSurfaceData());
}
var surfaceData = surfaceDataMap[textureId];
var (start, end) = surfaceData.AddPolygon(vertices, normal, textureUvs, lightmapUvs);
if (polyIdx >= cell.Lightmaps.Length) GD.Print("HUH");
return new LightmapRectData(cellIdx, polyIdx, textureId, start, end);
}
private static Texture BuildLightmapTexture(WorldRep.Cell[] cells, PackingRectangle[] packingRects, Dictionary<int, LightmapRectData> rectDataMap, Dictionary<int, MeshSurfaceData> surfaceDataMap)
{
RectanglePacker.Pack(packingRects, out var bounds);
var image = Image.Create((int)bounds.Width, (int)bounds.Height, false, Image.Format.Rgba8);
foreach (var rect in packingRects)
{
if (!rectDataMap.ContainsKey(rect.Id)) GD.Print("Invalid rectDataMap key");
var info = rectDataMap[rect.Id];
if (info.cellIndex >= cells.Length) GD.Print($"CellIndex too big: {info.cellIndex}/{cells.Length}");
if (info.lightmapIndex >= cells[info.cellIndex].Lightmaps.Length) GD.Print($"LightmapIndex too big: {info.lightmapIndex}/{cells[info.cellIndex].Lightmaps.Length}");
var lightmap = cells[info.cellIndex].Lightmaps[info.lightmapIndex];
var layers = (uint)lightmap.Pixels.GetLength(0);
var height = (uint)lightmap.Pixels.GetLength(1);
var width = (uint)lightmap.Pixels.GetLength(2);
for (uint y = 0; y < height; y++)
{
for (uint x = 0; x < width; x++)
{
var rawColour = System.Numerics.Vector4.Zero;
for (uint l = 0; l < layers; l++)
{
rawColour += lightmap.GetPixel(l, x, y);
}
var colour = new Color(MathF.Min(rawColour.X, 1.0f), MathF.Min(rawColour.Y, 1.0f), MathF.Min(rawColour.Z, 1.0f), MathF.Min(rawColour.W, 1.0f));
image.SetPixel((int)(rect.X + x), (int)(rect.Y + y), colour);
}
}
if (!surfaceDataMap.ContainsKey(info.textureId)) GD.Print("Invalid SurfaceDataMap key");
surfaceDataMap[info.textureId].TransformLightmapUvs(info.uvStart, info.uvEnd, (uv) =>
{
var u = uv.X;
var v = uv.Y;
// Clamp uv range to [0..1]
u %= 1;
v %= 1;
if (u < 0) u = Math.Abs(u);
if (v < 0) v = Math.Abs(v);
// Transform!
u = (rect.X + rect.Width * u) / (int)bounds.Width;
v = (rect.Y + rect.Height * v) / (int)bounds.Height;
return new Vector2(u, v);
});
}
return ImageTexture.CreateFromImage(image);
}
private int CalcBaseUV(
WorldRep.Cell cell,
WorldRep.Cell.Poly poly,
WorldRep.Cell.RenderPoly renderPoly,
WorldRep.Cell.LightmapInfo light,
List<Vector2> textureUvs,
List<Vector2> lightmapUvs,
int cellIdxOffset)
{
// TODO: This is slightly hardcoded for ND. Check other stuff at some point. Should be handled in LG side imo
// TODO: This is a mess lol
var textureId = renderPoly.TextureId;
var texture = _textureLoader.Get(textureId);
var texU = renderPoly.TextureVectors.Item1.ToGodotVec3();
var texV = renderPoly.TextureVectors.Item2.ToGodotVec3();
var baseU = renderPoly.TextureBases.Item1;
var baseV = renderPoly.TextureBases.Item2;
var txUScale = 64.0f / texture.GetWidth();
var txVScale = 64.0f / texture.GetHeight();
var lmUScale = 4.0f / light.Width;
var lmVScale = 4.0f / light.Height;
var txUBase = baseU * txUScale;
var txVBase = baseV * txVScale;
var lmUBase = lmUScale * (baseU + (0.5f - light.Bases.Item1) / 4.0f);
var lmVBase = lmVScale * (baseV + (0.5f - light.Bases.Item2) / 4.0f);
var uu = texU.Dot(texU);
var vv = texV.Dot(texV);
var uv = texU.Dot(texV);
var anchor = cell.Vertices[cell.Indices[cellIdxOffset + 0]].ToGodotVec3(); // TODO: This probably shouldn't be hardcoded idx 0
if (uv == 0.0)
{
var txUVec = texU * txUScale / uu;
var txVVec = texV * txVScale / vv;
var lmUVec = texU * lmUScale / uu;
var lmVVec = texV * lmVScale / vv;
for (var i = 0; i < poly.VertexCount; i++)
{
var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3();
var delta = new Vector3(v.X - anchor.X, v.Y - anchor.Y, v.Z - anchor.Z);
var txUV = new Vector2(delta.Dot(txUVec) + txUBase, delta.Dot(txVVec) + txVBase);
var lmUV = new Vector2(delta.Dot(lmUVec) + lmUBase, delta.Dot(lmVVec) + lmVBase);
textureUvs.Add(txUV);
lightmapUvs.Add(lmUV);
}
}
else
{
var denom = 1.0f / (uu * vv - uv * uv);
var txUu = uu * txVScale * denom;
var txVv = vv * txUScale * denom;
var txUvu = txUScale * denom * uv;
var txUvv = txVScale * denom * uv;
var lmUu = uu * lmVScale * denom;
var lmVv = vv * lmUScale * denom;
var lmUvu = lmUScale * denom * uv;
var lmUvv = lmVScale * denom * uv;
for (var i = 0; i < poly.VertexCount; i++)
{
var v = cell.Vertices[cell.Indices[cellIdxOffset + i]].ToGodotVec3();
var delta = new Vector3(v.X - anchor.X, v.Y - anchor.Y, v.Z - anchor.Z);
var du = delta.Dot(texU);
var dv = delta.Dot(texV);
var txUV = new Vector2(txUBase + txVv * du - txUvu * dv, txVBase + txUu * dv - txUvv * du);
var lmUV = new Vector2(lmUBase + lmVv * du - lmUvu * dv, lmVBase + lmUu * dv - lmUvv * du);
textureUvs.Add(txUV);
lightmapUvs.Add(lmUV);
}
}
return textureId;
}
private void LoadTextures(TxList textureList)
{
// TODO: Use PathJoin
var count = textureList.ItemCount;
for (var i = 0; i < count; i++)
{
var item = textureList.Items[i];
var path = "/";
for (var j = 0; j < item.Tokens.Length; j++)
{
var token = item.Tokens[j];
if (token == 0)
{
break;
}
path += $"{textureList.Tokens[token - 1]}/";
}
path += item.Name;
if (!_textureLoader.Load(i, path))
{
GD.Print($"Failed to load texture: {path}");
}
}
if (Dump) DumpTextureList(textureList);
}
private static void DumpTextureList(TxList textureList)
{
GD.Print($"TXLIST:\n BlockSize: {textureList.BlockSize}\n ItemCount: {textureList.ItemCount}\n TokenCount: {textureList.TokenCount}\n Tokens:");
for (var i = 0; i < textureList.TokenCount; i++)
{
GD.Print($" {i}: {textureList.Tokens[i]}");
}
GD.Print($" Items:");
for (var i = 0; i < textureList.ItemCount; i++)
{
var item = textureList.Items[i];
GD.Print($" {i}:\n Tokens: [{item.Tokens[0]}, {item.Tokens[1]}, {item.Tokens[2]}, {item.Tokens[3]}]\n Name: {item.Name}");
}
}
const string MATERIAL_PATH = "res://project/materials/base.tres";
private static Material BuildMaterial(Texture albedoTexture, Texture lightmapTexture, bool lmHdr)
{
var material = ResourceLoader.Load<ShaderMaterial>(MATERIAL_PATH).Duplicate() as ShaderMaterial;
material.SetShaderParameter("texture_albedo", albedoTexture);
material.SetShaderParameter("lightmap_albedo", lightmapTexture);
material.SetShaderParameter("lightmap_2x", lmHdr);
return material;
}
}