using Godot; using KeepersCompound.LGS; using KeepersCompound.LGS.Database; using KeepersCompound.LGS.Database.Chunks; using KeepersCompound.TMV.UI; using RectpackSharp; using System; using System.Collections.Generic; using System.IO; 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; string _campaignName; string _missionName; ResourcePathManager _installPaths; DbFile _file; TextureLoader _textureLoader; ModelLoader _modelLoader; public override void _Ready() { var extractPath = ProjectSettings.GlobalizePath($"user://extracted/tmp"); _installPaths = new ResourcePathManager(extractPath); _modelLoader = new ModelLoader(_installPaths); var missionSelector = GetNode("%MissionSelector") as MissionSelector; missionSelector.pathManager = _installPaths; missionSelector.MissionSelected += (string campaign, string mission) => { _campaignName = campaign; _missionName = mission; FileName = _installPaths.GetMissionPath(campaign, mission); Build = true; }; } public override void _Process(double delta) { if (Build) { Timing.TimeStage("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(_campaignName); Timing.TimeStage("DbFile Parse", () => _file = new(FileName)); Timing.TimeStage("Load FAM", () => UseChunk("TXLIST", LoadTextures)); Timing.TimeStage("Build WR", () => UseChunk("WREXT", BuildWrMeshes)); if (_file.Chunks.TryGetValue("BRLIST", out var brList)) { var objHierarchy = Timing.TimeStage("Hierarchy", BuildHierarchy); Timing.TimeStage("Object Placement", () => PlaceObjects((BrList)brList, objHierarchy)); } } // TODO: Make this less of a mess private ObjectHierarchy BuildHierarchy() { ObjectHierarchy objHierarchy; if (_file.Chunks.TryGetValue("GAM_FILE", out var gamFileChunk)) { var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; var name = ((GamFile)gamFileChunk).fileName; var paths = Directory.GetFiles(FileName.GetBaseDir(), name, options); if (!paths.IsEmpty()) { objHierarchy = new ObjectHierarchy(_file, new DbFile(paths[0])); } else { objHierarchy = new ObjectHierarchy(_file); } } else { objHierarchy = new ObjectHierarchy(_file); } return objHierarchy; } private void UseChunk(string name, Action action) { if (_file.Chunks.TryGetValue(name, out var value)) { action((T)value); } else { GD.Print($"No chunk found/loaded: {name}"); } } private void PlaceObjects(BrList brList, ObjectHierarchy objHierarchy) { foreach (var brush in brList.Brushes) { if (brush.media != BrList.Brush.Media.Object) { continue; } var id = (int)brush.brushInfo; var modelNameProp = objHierarchy.GetProperty(id, "P$ModelName"); var scaleProp = objHierarchy.GetProperty(id, "P$Scale"); var renderTypeProp = objHierarchy.GetProperty(id, "P$RenderTyp"); var renderMode = renderTypeProp == null ? PropRenderType.Mode.Normal : renderTypeProp.mode; if (modelNameProp == null || renderMode == PropRenderType.Mode.NotRendered) { continue; } // Let's try and place an object :) var modelName = modelNameProp.modelName + ".bin"; var pos = brush.position.ToGodotVec3(); var rawRot = brush.angle; var rot = new Vector3(rawRot.Y, rawRot.Z, rawRot.X) * 360 / ushort.MaxValue; var scale = scaleProp == null ? Vector3.One : scaleProp.scale.ToGodotVec3(false); var model = _modelLoader.Load(_campaignName, modelName); if (model != null) { model.Position = pos; model.RotationDegrees = rot; model.Scale = scale; AddChild(model); } } } private void BuildWrMeshes(WorldRep worldRep) { var cells = worldRep.Cells; var lmHdr = worldRep.DataHeader.LightmapFormat == 2; GD.Print($"HDR Lightmap: {lmHdr}"); var packingRects = new List(); var surfaceDataMap = new Dictionary(); var rectDataMap = new Dictionary(); 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, worldRep, 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 = Timing.TimeStage("Build Lightmap", () => { return 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 surfaceDataMap, WorldRep worldRep, int cellIdx, int polyIdx, int indicesOffset) { var cell = worldRep.Cells[cellIdx]; var poly = cell.Polys[polyIdx]; var normal = cell.Planes[poly.PlaneId].Normal.ToGodotVec3(); var vertices = new List(); var textureUvs = new List(); var lightmapUvs = new List(); 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 lightmapScale = worldRep.DataHeader.LightmapScaleMultiplier(); var textureId = CalcBaseUV(cell, poly, renderPoly, light, textureUvs, lightmapUvs, lightmapScale, 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 Texture2DArray BuildLightmapTexture( WorldRep.Cell[] cells, PackingRectangle[] packingRects, Dictionary rectDataMap, Dictionary surfaceDataMap) { var bounds = Timing.TimeStage("RectPack", () => { RectanglePacker.Pack(packingRects, out var bounds, PackingHints.FindBest, 0); return bounds; }); GD.Print($"Creating lightmap with bounds: ({bounds.Width}, {bounds.Height})"); var lightmapFormat = Image.Format.Rgba8; var lmLayerCount = 33; var lmImages = new Godot.Collections.Array(); lmImages.Resize(lmLayerCount); for (var i = 0; i < lmLayerCount; i++) { lmImages[i] = Image.CreateEmpty((int)bounds.Width, (int)bounds.Height, false, lightmapFormat); } 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 width = lightmap.Width; var height = lightmap.Height; var layerCount = lightmap.Layers; var srcRect = new Rect2I(0, 0, width, height); var dst = new Vector2I((int)rect.X, (int)rect.Y); for (var i = 0; i < layerCount; i++) { var cellLm = Image.CreateFromData(width, height, false, lightmapFormat, lightmap.AsBytesRgba(i)); lmImages[i].BlitRect(cellLm, srcRect, dst); } if (!surfaceDataMap.ContainsKey(info.textureId)) GD.Print("Invalid SurfaceDataMap key"); surfaceDataMap[info.textureId].TransformUv2s(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); }); } var lightmapTexture = new Texture2DArray(); lightmapTexture.CreateFromImages(lmImages); return lightmapTexture; } private int CalcBaseUV( WorldRep.Cell cell, WorldRep.Cell.Poly poly, WorldRep.Cell.RenderPoly renderPoly, WorldRep.Cell.LightmapInfo light, List textureUvs, List lightmapUvs, float lightmapScale, 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 / lightmapScale / texture.GetWidth(); var txVScale = 64.0f / lightmapScale / 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(_installPaths, 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(Texture2D albedoTexture, Texture2DArray lightmapTexture, bool lmHdr) { var material = ResourceLoader.Load(MATERIAL_PATH).Duplicate() as ShaderMaterial; material.SetShaderParameter("texture_albedo", albedoTexture); material.SetShaderParameter("lightmap_albedo", lightmapTexture); material.SetShaderParameter("lightmap_2x", lmHdr); return material; } }