using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using GLTFast; using UnityEngine; using Object = UnityEngine.Object; // A mod package's visual assembly. It does not decide hull cells or hardpoint roles. public static class ShipModVisual { public sealed class Instance { public GameObject Root; public IReadOnlyDictionary Anchors; public Mesh LineMesh; public Material LineMaterial; public void Destroy() { DestroyObject(Root); DestroyObject(LineMesh); DestroyObject(LineMaterial); } } private sealed class Nodes : GameObjectInstantiator { public Nodes(IGltfReadable gltf, Transform parent) : base(gltf, parent) { } public Transform Get(uint index) => m_Nodes.TryGetValue(index, out var node) ? node.transform : null; } public static async Task LoadAsync(string shipPath, Transform parent) { var package = ShipModCatalog.ReadPackage(shipPath); var ship = package.Visual; var root = new GameObject("mod-ship:" + ship.Id); root.SetActive(false); root.transform.SetParent(parent, false); var result = new Instance { Root = root }; try { // glTFast's default defer agent creates a DontDestroyOnLoad object, which is // unavailable in edit-mode previews. Play mode keeps its frame-budgeted agent. var gltf = new GltfImport(deferAgent: Application.isPlaying ? null : new UninterruptedDeferAgent()); if (!await gltf.LoadFile(package.ModelPath)) throw new InvalidOperationException($"{ship.Id}: GLB import failed."); var nodes = new Nodes(gltf, root.transform); if (!await gltf.InstantiateMainSceneAsync(nodes)) throw new InvalidOperationException($"{ship.Id}: GLB scene instantiation failed."); result.Anchors = ship.Anchors.ToDictionary(anchor => anchor.Id, anchor => nodes.Get(package.NodeIndices[anchor.ModelNodeId]) ?? throw new InvalidOperationException($"{ship.Id}: GLB scene omitted node {anchor.ModelNodeId}."), StringComparer.Ordinal); result.LineMesh = BuildLineMesh(ship.SchematicLines); if (result.LineMesh != null) { var lines = new GameObject("Schematic Lines"); lines.transform.SetParent(root.transform, false); lines.AddComponent().sharedMesh = result.LineMesh; var shader = Shader.Find("Sprites/Default") ?? throw new InvalidOperationException("Sprites/Default shader is unavailable for ship lines."); result.LineMaterial = new Material(shader) { name = "Ship Lines" }; lines.AddComponent().sharedMaterial = result.LineMaterial; } root.SetActive(true); return result; } catch { result.Destroy(); throw; } } private static void DestroyObject(Object target) { if (target == null) return; if (Application.isPlaying) Object.Destroy(target); else Object.DestroyImmediate(target); } // Blender Z-up to Unity Y-up through Blender's glTF export and glTFast's handedness conversion. // Segment topology preserves every authored point; display width is currently one screen pixel. public static Mesh BuildLineMesh(IReadOnlyList lines) { if (lines == null || lines.Count == 0) return null; var vertices = new List(); var colors = new List(); var segments = new List(); foreach (var line in lines) { var count = line.Points.Length / 3; var start = vertices.Count; for (var i = 0; i < count; i++) { var offset = i * 3; vertices.Add(new Vector3(-line.Points[offset], line.Points[offset + 2], -line.Points[offset + 1])); var rgba = line.Color; colors.Add(new Color(rgba?[0] ?? 1, rgba?[1] ?? 1, rgba?[2] ?? 1, (rgba?[3] ?? 1) * (line.Opacities?[i] ?? 1))); if (i > 0) { segments.Add(start + i - 1); segments.Add(start + i); } } if (line.Cyclic && count > 2) { segments.Add(start + count - 1); segments.Add(start); } } var mesh = new Mesh { name = "Captured Ship Lines", indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 }; mesh.SetVertices(vertices); mesh.SetColors(colors); mesh.SetIndices(segments, MeshTopology.Lines, 0); mesh.RecalculateBounds(); return mesh; } }