using Godot; using Godot.NativeInterop; 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; [Tool] 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; // TODO: Make these 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() { 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); var material = new StandardMaterial3D { AlbedoTexture = ImageTexture.CreateFromImage(lightmap), TextureFilter = BaseMaterial3D.TextureFilterEnum.Nearest, }; MeshInstance3D mesh = GenerateMesh(surfaceArrays, material); // OccluderInstance3D occluderInstance = GenerateOccluder(vertices, indices); 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.TexUV].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.TexUV] = lightmapUvs; } return image; } private static 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 uvs = 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, uvs, 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] = uvs.ToArray(); surfacesArrays.Add(array); } return surfacesArrays; } private static OccluderInstance3D GenerateOccluder(List vertices, List indices) { 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, StandardMaterial3D material) { var arrMesh = new ArrayMesh(); for (var i = 0; i < surfaceArrays.Count; i++) { arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surfaceArrays[i]); arrMesh.SurfaceSetMaterial(i, material); } var meshInstance = new MeshInstance3D { Mesh = arrMesh, CastShadow = GeometryInstance3D.ShadowCastingSetting.On }; return meshInstance; } private static void CalcBaseUV( WorldRep.Cell cell, WorldRep.Cell.Poly poly, WorldRep.Cell.RenderPoly renderPoly, WorldRep.Cell.LightmapInfo light, 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 texU = renderPoly.TextureVectors.Item1.ToGodotVec3(); var texV = renderPoly.TextureVectors.Item2.ToGodotVec3(); var uu = texU.Dot(texU); var vv = texV.Dot(texV); var uv = texU.Dot(texV); var lmUScale = 4.0f / light.Width; var lmVScale = 4.0f / light.Height; var baseU = renderPoly.TextureBases.Item1; var baseV = renderPoly.TextureBases.Item2; var lmUBase = lmUScale * (baseU + (0.5f - light.Bases.Item1) / 4.0f); var lmVBase = lmVScale * (baseV + (0.5f - light.Bases.Item2) / 4.0f); var anchor = cell.Vertices[cell.Indices[cellIdxOffset + 0]].ToGodotVec3(); // TODO: This probably shouldn't be hardcoded idx 0 if (uv == 0.0) { 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 lmUV = new Vector2(delta.Dot(lmUVec) + lmUBase, delta.Dot(lmVVec) + lmVBase); lightmapUvs.Add(lmUV); } } else { var denom = 1.0f / (uu * vv - uv * 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 lmUV = new Vector2(lmUBase + lmVv * du - lmUvu * dv, lmVBase + lmUu * dv - lmUvv * du); lightmapUvs.Add(lmUV); } } } private void LoadTextures(TxList textureList) { // TODO: This has hardcoded .png extension and relies on you placing extracted and converted images in godot user directory 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 + ".png"; // Hardcoded extension! if (File.Exists(FileName + path)) { path = FileName + path; } else if (File.Exists(ProjectSettings.GlobalizePath($"user://textures{path}"))) { path = ProjectSettings.GlobalizePath($"user://textures{path}"); } else { path = "user://textures/jorge.png"; } if (Dump) GD.Print($"Loading texture: {path}"); // _textures.Add(ImageTexture.CreateFromImage(Image.LoadFromFile(path))); _textures.Add(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}"); } } }