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; using System.Linq; namespace KeepersCompound; public partial class Mission : Node3D { [Export(PropertyHint.GlobalFile, "*.mis")] public string FileName { get; set; } [Export] public bool Build = false; [Export] public bool Clear = false; [Export] public bool Dump = false; DbFile _file; List _textures; public override void _Ready() { _textures = new List(); var missionSelector = GetNode("%MissionSelector") as MissionSelector; missionSelector.LoadMission += (string path) => { FileName = path; 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() { _textures.Clear(); foreach (var node in GetChildren()) { node.QueueFree(); } } public void RebuildMap() { ClearMap(); _file = new(FileName); var textureList = (TxList)_file.Chunks["TXLIST"]; LoadTextures(textureList); if (Dump) DumpTextureList(textureList); var wr = (WorldRep)_file.Chunks["WREXT"]; foreach (var cell in wr.Cells) { BuildCellMesh(cell); } } private void BuildCellMesh(WorldRep.Cell cell) { var numPolys = cell.PolyCount; var numRenderPolys = cell.RenderPolyCount; var numPortalPolys = cell.PortalPolyCount; // There's nothing to render if (numRenderPolys == 0 || numPortalPolys >= numPolys) { return; } // 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 surfaceArrays = BuildSurfaceArrays(cell, maxPolyIdx, out var packingRects); Image lightmap = BuildLightmap(cell, packingRects, surfaceArrays); // !Hack: This should be somewhere else? var materials = new List(); for (var i = 0; i < maxPolyIdx; i++) { var textureId = cell.RenderPolys[i].TextureId; // !HACK: Sky textures :) if (textureId >= _textures.Count) { textureId = 0; } var material = ResourceLoader.Load("res://project/materials/base.tres").Duplicate() as ShaderMaterial; material.SetShaderParameter("texture_albedo", _textures[textureId]); material.SetShaderParameter("lightmap_albedo", ImageTexture.CreateFromImage(lightmap)); materials.Add(material); } MeshInstance3D mesh = GenerateMesh(surfaceArrays, materials); OccluderInstance3D occluderInstance = GenerateOccluder(surfaceArrays); var cellNode = new Node3D(); cellNode.AddChild(mesh); cellNode.AddChild(occluderInstance); AddChild(cellNode); } private static Image BuildLightmap(WorldRep.Cell cell, PackingRectangle[] packingRects, List surfaceArrays) { 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) { // Build lightmap var lightmap = cell.Lightmaps[rect.Id]; // TODO: Handle animlight layers 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); } } // Transform UVs var lightmapUvs = surfaceArrays[rect.Id][(int)Mesh.ArrayType.TexUV2].As(); for (var i = 0; i < lightmapUvs.Length; i++) { var uv = lightmapUvs[i]; 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; lightmapUvs[i] = new Vector2(u, v); } surfaceArrays[rect.Id][(int)Mesh.ArrayType.TexUV2] = lightmapUvs; } return image; } private List BuildSurfaceArrays( WorldRep.Cell cell, int maxPolyIdx, out PackingRectangle[] packingRects) { packingRects = new PackingRectangle[maxPolyIdx]; var surfacesArrays = new List(); var cellIdxOffset = 0; for (int i = 0; i < maxPolyIdx; i++) { var vertices = new List(); var normals = new List(); var indices = new List(); var textureUvs = new List(); var lightmapUvs = new List(); var poly = cell.Polys[i]; var normal = cell.Planes[poly.PlaneId].Normal.ToGodotVec3(); var numPolyVertices = poly.VertexCount; for (var j = 0; j < numPolyVertices; j++) { var vertex = cell.Vertices[cell.Indices[cellIdxOffset + j]]; vertices.Add(vertex.ToGodotVec3()); normals.Add(normal); } // Simple triangulation. Polys are always convex so we can just do a fan for (int j = 1; j < numPolyVertices - 1; j++) { indices.Add(0); indices.Add(j); indices.Add(j + 1); } // UVs var renderPoly = cell.RenderPolys[i]; var light = cell.LightList[i]; packingRects[i] = new PackingRectangle(0, 0, light.Width, light.Height, i); CalcBaseUV(cell, poly, renderPoly, light, textureUvs, lightmapUvs, cellIdxOffset); cellIdxOffset += poly.VertexCount; var array = new Godot.Collections.Array(); array.Resize((int)Mesh.ArrayType.Max); array[(int)Mesh.ArrayType.Vertex] = vertices.ToArray(); array[(int)Mesh.ArrayType.Normal] = normals.ToArray(); array[(int)Mesh.ArrayType.Index] = indices.ToArray(); array[(int)Mesh.ArrayType.TexUV] = textureUvs.ToArray(); array[(int)Mesh.ArrayType.TexUV2] = lightmapUvs.ToArray(); surfacesArrays.Add(array); } return surfacesArrays; } // TODO: This is broke? private static OccluderInstance3D GenerateOccluder(List surfaceArrays) { var vertices = new List(); var indices = new List(); foreach (var array in surfaceArrays) { var count = vertices.Count; vertices.AddRange(array[(int)Mesh.ArrayType.Vertex].As()); var surfaceIndices = array[(int)Mesh.ArrayType.Index].As(); foreach (var idx in surfaceIndices) { indices.Add(count + idx); } } var occluder = new ArrayOccluder3D(); occluder.SetArrays(vertices.ToArray(), indices.ToArray()); var occluderInstance = new OccluderInstance3D { Occluder = occluder, BakeSimplificationDistance = 0.0f }; return occluderInstance; } private static MeshInstance3D GenerateMesh(List surfaceArrays, List materials) { var arrMesh = new ArrayMesh(); for (var i = 0; i < surfaceArrays.Count; i++) { arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surfaceArrays[i]); arrMesh.SurfaceSetMaterial(i, materials[i]); } var meshInstance = new MeshInstance3D { Mesh = arrMesh, }; return meshInstance; } private void CalcBaseUV( WorldRep.Cell cell, WorldRep.Cell.Poly poly, WorldRep.Cell.RenderPoly renderPoly, WorldRep.Cell.LightmapInfo light, List textureUvs, List 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; // !HACK: Sky textures :) if (textureId >= _textures.Count) { textureId = 0; } var texture = _textures[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); } } } private void LoadTextures(TxList textureList) { static string PathToKey(string baseDir, string path) { return path.TrimPrefix(baseDir).GetBaseName().ToLower(); } // TODO: This has hardcoded .png extension and relies on you placing extracted and converted images in godot user directory // Collect all the fm textures here to help with case sensitivity :) // TODO: Only do this on case sensitive systems? var baseDir = FileName.GetBaseDir(); var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; var dirPaths = Directory.GetDirectories(baseDir, "fam", options); options.RecurseSubdirectories = true; var texturePaths = new Dictionary(); // Godot doesn't support runtime DDS :) // TODO: Load DDS BMP PCX GIF CEL string[] validExtensions = { "png", "tga" }; foreach (var dirPath in dirPaths) { foreach (var path in Directory.EnumerateFiles(dirPath, "*", options)) { if (validExtensions.Contains(path.GetExtension().ToLower())) { // TODO: This only adds the first one found rather than the highest priority texturePaths.TryAdd(PathToKey(baseDir, path), path); } } } // 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 (texturePaths.TryGetValue(path.ToLower(), out var newPath)) { path = newPath; } else if (File.Exists(ProjectSettings.GlobalizePath($"user://textures{path}.png"))) { path = ProjectSettings.GlobalizePath($"user://textures{path}.png"); } else { GD.Print($"Failed to find texture: {path}"); path = "user://textures/jorge.png"; } if (Dump) GD.Print($"Loading texture: {path}"); _textures.Add(ImageTexture.CreateFromImage(Image.LoadFromFile(path))); } } 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}"); } } }