using System.Collections.Generic; using System.IO; using Godot; using KeepersCompound.LGS; namespace KeepersCompound.TMV; // TODO: Work out a way to share base game models again in the cache public class ModelLoader { public struct MeshDetails(int jointIdx, Transform3D transform, MeshInstance3D mesh) { public readonly int JointIdx = jointIdx; public readonly Transform3D Transform = transform; public readonly MeshInstance3D Mesh = mesh; } private readonly Dictionary<(string, string), MeshDetails[]> _cache = new(); public MeshDetails[] Load(string modelName, bool forceLoad = false) { var campaignResources = Context.Instance.CampaignResources; var campaignName = campaignResources.name; campaignName ??= ""; if (!forceLoad) { if (_cache.TryGetValue((campaignName, modelName), out var fmDetails)) { return fmDetails; } if (_cache.TryGetValue(("", modelName), out var omDetails)) { return omDetails; } } var details = Timing.TimeStage("Load Models", () => LoadModel(modelName)); _cache[(campaignName, modelName)] = details; return details; } public static MeshInstance3D[] TransformMeshes(float[] joints, MeshDetails[] meshDetails) { var meshes = new List(); foreach (var details in meshDetails) { var mesh = details.Mesh.Duplicate() as MeshInstance3D; if (details.JointIdx != -1) { var ang = float.DegreesToRadians(joints[details.JointIdx]); var r1 = new Quaternion(new Vector3(0, 0, 1), ang); var r2 = details.Transform.Basis.GetRotationQuaternion(); var basis = new Basis(r2 * r1); mesh.SetTransform(new Transform3D(basis, details.Transform.Origin)); } else { mesh.SetTransform(details.Transform); } meshes.Add(mesh); } return [..meshes]; } public static MeshDetails[] LoadModel(string modelName) { var campaignResources = Context.Instance.CampaignResources; var modelPath = campaignResources.GetResourcePath(ResourceType.Object, modelName); if (modelPath == null) { return []; } var modelFile = new ModelFile(modelPath); if (modelFile == null) { GD.Print($"Failed to load model file: {modelPath}"); return []; } var materials = new List(); foreach (var material in modelFile.Materials) { if (material.Type == 0) { var convertedName = ResourcePathManager.ConvertSeparator(material.Name); var resName = Path.GetFileNameWithoutExtension(convertedName); var path = campaignResources.GetResourcePath(ResourceType.ObjectTexture, resName); if (path == null) { // Might fail in exported projects path = "res://project/jorge.png"; GD.Print($"Failed to load model texture: {material.Name}"); } var mat = new StandardMaterial3D { AlbedoTexture = TextureLoader.LoadTexture(path), Transparency = BaseMaterial3D.TransparencyEnum.AlphaDepthPrePass, }; var name = material.Name.ToLower(); for (var i = 0; i < 4; i++) { if (name.Contains($"replace{i}")) { mat.SetMeta($"TxtRepl{i}", true); } } materials.Add(mat); } else { var b = (material.Handle) & 0xff; var g = (material.Handle >> 8) & 0xff; var r = (material.Handle >> 16) & 0xff; var colour = new Color(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); materials.Add(new StandardMaterial3D { AlbedoColor = colour }); } } var objCount = modelFile.Objects.Length; var meshDetails = new MeshDetails[objCount]; for (var i = 0; i < objCount; i++) { var subObj = modelFile.Objects[i]; var jointIdx = subObj.Joint; var transform = subObj.Type == 0 ? Transform3D.Identity : subObj.Transform.ToGodotTransform3D(); var surfaceDataMap = new Dictionary(); foreach (var poly in modelFile.Polygons) { var v0 = poly.VertexIndices[0]; if (v0 < subObj.PointIdx || v0 >= subObj.PointIdx + subObj.PointCount) { continue; } var vertices = new List(); var normal = modelFile.Normals[poly.Normal].ToGodotVec3(); var uvs = new List(); for (var j = 0; j < poly.VertexCount; j++) { var vertex = modelFile.Vertices[poly.VertexIndices[j]]; vertices.Add(vertex.ToGodotVec3()); if (j < poly.UvIndices.Length) { var uv = modelFile.Uvs[poly.UvIndices[j]]; uvs.Add(new Vector2(uv.X, uv.Y)); } else { uvs.Add(Vector2.Zero); } } if (!surfaceDataMap.ContainsKey(poly.Data)) { surfaceDataMap.Add(poly.Data, new MeshSurfaceData()); } surfaceDataMap[poly.Data].AddPolygon(vertices, normal, uvs, uvs); } var mesh = new ArrayMesh(); foreach (var (materialId, surfaceData) in surfaceDataMap) { var array = surfaceData.BuildSurfaceArray(); var surfaceIdx = mesh.GetSurfaceCount(); mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, array); for (var j = 0; j < materials.Count; j++) { var m = modelFile.Materials[j]; if (m.Slot == materialId) { mesh.SurfaceSetMaterial(surfaceIdx, materials[j]); break; } } } var pos = -modelFile.Header.Center.ToGodotVec3(); var meshInstance = new MeshInstance3D { Mesh = mesh, Position = pos }; meshDetails[i] = new MeshDetails(jointIdx, transform, meshInstance); } return meshDetails; } }