using System; using System.Collections.Generic; using System.IO; using JetBrains.Annotations; using Needle.Engine.Core; using Needle.Engine.Interfaces; using Needle.Engine.Utils; using Unity.Profiling; using UnityEngine; using Object = UnityEngine.Object; namespace Needle.Engine.Gltf { internal struct NestedGltfInfo { public readonly GameObject GameObject; public readonly string PreviousEditorTag; public GameObject Created; public readonly Action Resetting; public NestedGltfInfo(GameObject go, Action resetting = null) { this.GameObject = go; this.PreviousEditorTag = go.tag; Created = null; Resetting = resetting; } public void Reset() { if (Resetting != null) { try { Resetting?.Invoke(); } catch (Exception e) { Debug.LogException(e); } } GameObject.tag = PreviousEditorTag; Created.SafeDestroy(); } } /// /// When a gltf object is nested in another gltf export process it should not be embedded in the gltf directly /// But instead we want the nested gltf object be exported to its own file /// That way we can for example add already compressed gltf objects to the scene /// and on export they will just be copied to the output directory /// [UsedImplicitly] public class NestedGltfExportHandler : GltfExtensionHandlerBase { private readonly List _nestedGltfGameObjects = new List(); private static ProfilerMarker _exportNestedGltfMarker = new ProfilerMarker("Export Nested Gltf"); private static ProfilerMarker _exportNestedGltfCleanupMarker = new ProfilerMarker("Cleanup Exported Nested Gltf"); public override void OnBeforeExport(GltfExportContext context) { base.OnBeforeExport(context); if (ShouldRunNestedGlbExport(context)) { var root = context.Root; _nestedGltfGameObjects.Clear(); TraverseHierarchyForNestedGltfObjects(root, context, _nestedGltfGameObjects); } } public override void OnCleanup() { base.OnCleanup(); foreach (var nested in _nestedGltfGameObjects) { using var _ = _exportNestedGltfCleanupMarker.Auto(); nested.Reset(); } } private static bool ShouldRunNestedGlbExport(GltfExportContext context) { var isNestedGlbExport = context.ParentContext != null; if (isNestedGlbExport) { var parent = context.ParentContext; switch (parent) { // when we export from within a referenced scene (or an object via menu item) case ExportContext _: case ObjectExportContext _: case GltfExportContext _: return true; } } return false; } private static void TraverseHierarchyForNestedGltfObjects(Transform current, GltfExportContext context, List list) { // The object was marked as editor only by the user // Dont export it! if (current.CompareTag("EditorOnly")) return; // if this GameObject is a nested gltf we handle export // otherwise we walk recursively through its children if (!ReferenceEquals(current, context.Root) && current.TryGetComponent(out IExportableObject nested)) { if (ReferenceEquals(current, context.Root)) { // if this is the object we are exporting return; } var path = context.AssetsDirectory + "/" + nested.name.ToFileName() + context.GetExtension(current.gameObject); if (context.IsCurrentlyExportingToPath(path)) { Debug.LogError("Error exporting nested gltf: Can not export nested gltf object to the same path as the parent gltf object: " + path + " in " + context.Root, context.Root); return; } var prevSettings = context.Settings; try { using var _ = _exportNestedGltfMarker.Auto(); var exportSettings = context.Settings?.Clone() ?? new ExportSettings(); // export skybox if a camera is found exportSettings.ExportSkybox = current.GetComponentInChildren(true); // exportSettings.SetExtensionAllowed(UnityGltf_NEEDLE_lighting_settings.EXTENSION_NAME, false); context.Settings = exportSettings; if (nested.Export(path, false, context)) { var gltfObjectRoot = current; // if the gltf object was exported successfully // save the current editor tag and mark it as editor only // var onReset = new Action(() => gltfObjectRoot.ApplyTransform(transformData)); var cmd = new ExportNestedGltfCommand(gltfObjectRoot, context, path, nested); var info = new NestedGltfInfo(current.gameObject, () => cmd.Undo()); cmd.Perform(); info.Created = cmd.CreatedGameObject; list.Add(info); } } finally { context.Settings = prevSettings; } } // only traverse the hierarchy further if THIS GameObject is not a nested gltf else { foreach (Transform ch in current) { TraverseHierarchyForNestedGltfObjects(ch, context, list); } } } private static void RegisterStableGuid(Object obj, Object guidSource, GltfExportContext context) { if (!guidSource) { Debug.LogError( "Guid source for nested gltf export is not provided. This is probably because the exporting object is not a Unity component? Please report this as a bug with as much information as possible. The issue happened while exporting a nested gltf in " + context.Path); return; } var guid = guidSource.GetId(); // using the version with dashes because that's what Unity's guids look like too var stableGuid = GuidGenerator.GetGuid(guid); context.RegisterGuid(obj, stableGuid); } private class ExportNestedGltfCommand : ICommand { private readonly Transform gltfRoot; private readonly GltfExportContext context; private readonly string path; private readonly IExportableObject nested; public GameObject CreatedGameObject { get; private set; } public ExportNestedGltfCommand(Transform gltfRoot, GltfExportContext context, string path, IExportableObject nested) { this.gltfRoot = gltfRoot; this.context = context; this.path = path; this.nested = nested; } public void Perform() { var createdGo = new GameObject(gltfRoot.name); CreatedGameObject = createdGo; createdGo.hideFlags = HideFlags.DontSave; createdGo.transform.parent = gltfRoot.parent; createdGo.layer = gltfRoot.gameObject.layer; gltfRoot.tag = "EditorOnly"; createdGo.transform.ApplyTransform(gltfRoot.SaveTransform()); // gltfObjectRoot.SetLocalIdentity(); var nestedGltfComponent = createdGo.AddComponent(); nestedGltfComponent.FilePath = Path.GetFileName(path); // TODO: make filePaths relative from currently exported gltf so they can be resolved without relying on project structure. e.g. when exporting a hierarchy via context menu and then use it in another project we must search relative // if (!nestedGltfComponent.FilePath.StartsWith("./")) nestedGltfComponent.FilePath = "./" + nestedGltfComponent.FilePath; RegisterStableGuid(createdGo, gltfRoot, context); RegisterStableGuid(nestedGltfComponent, nested as Object, context); } public void Undo() { } } } }