using System.Collections.Generic; using JetBrains.Annotations; using Needle.Engine.Gltf; using Needle.Engine.Settings; using Needle.Engine.Utils; using UnityEngine; using UnityEngine.Playables; namespace Needle.Engine.Timeline { internal struct TimelineAnimationKey { public object owner; public AnimationClip clip; } // Timeline serialization must run before components so that the clips have been added [UsedImplicitly, Priority(100)] public class PlayableDirectorExtension : GltfExtensionHandlerBase { /// /// Map to only export an timeline animationclip once, the key contains the information used to check if the key was exported with the specific info /// private readonly Dictionary exportedAnimations = new Dictionary(); private readonly List foundTimelines = new List(); public override void OnBeforeExport(GltfExportContext context) { base.OnBeforeExport(context); exportedAnimations.Clear(); foundTimelines.Clear(); } public override void OnAfterNodeExport(GltfExportContext context, Transform transform, int nodeId) { base.OnAfterNodeExport(context, transform, nodeId); if (!transform.TryGetComponent(out PlayableDirector dir)) return; if (!dir.playableAsset) return; // create export context var directorExport = new PlayableDirectorExportContext(dir, context); context.RegisterValueResolver(new TimelineValueResolver(directorExport)); foundTimelines.Add(directorExport); // var ext = new TimelineAssetExtension(directorExport); // context.Bridge.AddNodeExtension(nodeId, TimelineAssetExtension.EXTENSION_NAME, ext); } public override void OnAfterExport(GltfExportContext context) { base.OnAfterExport(context); foreach (var directorExport in foundTimelines) { var dir = directorExport.Director; if (ExporterProjectSettings.instance.debugMode) Debug.Log($"Export Timeline: {dir.name}", dir.gameObject); // first export all animations in timeline (make sure to only export clips once) foreach (var info in TimelineUtils.EnumerateAnimationClips(dir)) { var clip = info.TimelineClip; var owner = info.Owner; if (EditorUtils.IsEditorOnly(owner, context.Root)) continue; var animationClip = info.AnimationClip; // we need to get the animation index when building the timeline model // and we only want to export either each clip once of each infinite track once object clipMapKey = info.IsInfiniteClip ? info.AnimationClip : (object)info.TimelineClip; if (clip != null && directorExport.ClipMap.ContainsKey(clipMapKey)) continue; var key = new TimelineAnimationKey() { owner = owner, clip = animationClip, }; // check if this exact animation has been exported before, if so just reuse the index if (!exportedAnimations.TryGetValue(key, out var index) && animationClip) { try { TimelinePreview.DisableTimelinePreview(); if (ExporterProjectSettings.instance.debugMode) Debug.Log($"Add timeline animation: {owner.name}/{animationClip.name}", animationClip); index = context.Bridge.AddAnimationClip(animationClip, owner.transform, 1); if (index < 0) { Debug.LogError( $"Could not export animation clip: {owner.name}/{animationClip.name}. The object might be disabled/missing or it might require KHR_animation_pointer support.", owner); continue; } exportedAnimations.Add(key, index); } finally { TimelinePreview.ResetState(); } } directorExport.ClipMap.Add(clipMapKey, index); } } } } }