// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using FlaxEditor.Content; using FlaxEditor.GUI.Drag; using FlaxEditor.GUI.Timeline.Tracks; using FlaxEditor.SceneGraph; using FlaxEngine; namespace FlaxEditor.GUI.Timeline { /// /// The timeline editor for scene animation asset. /// /// public sealed class SceneAnimationTimeline : Timeline { private sealed class Proxy : ProxyBase { /// public Proxy(SceneAnimationTimeline timeline) : base(timeline) { } } private SceneAnimationPlayer _player; private bool _showSelected3dTrack = true; internal Guid _id; /// /// Gets or sets the animation player actor used for the timeline preview. /// public SceneAnimationPlayer Player { get => _player != null ? _player : null; set { if (_player == value) return; _player = value; UpdatePlaybackState(); PlayerChanged?.Invoke(); } } /// /// Occurs when the selected player gets changed. /// public event Action PlayerChanged; /// /// Gets or sets the selected 3D track showing option value. /// public bool ShowSelected3dTrack { get => _showSelected3dTrack; set { if (_showSelected3dTrack == value) return; _showSelected3dTrack = value; ShowSelected3dTrackChanged?.Invoke(); } } /// /// Occurs when selected 3D track showing option gets changed. /// public event Action ShowSelected3dTrackChanged; /// /// Initializes a new instance of the class. /// /// The undo/redo to use for the history actions recording. Optional, can be null to disable undo support. public SceneAnimationTimeline(FlaxEditor.Undo undo) : base(PlaybackButtons.Play | PlaybackButtons.Stop | PlaybackButtons.Navigation, undo) { PlaybackState = PlaybackStates.Seeking; PropertiesEditObject = new Proxy(this); // Setup track types TrackArchetypes.Add(FolderTrack.GetArchetype()); TrackArchetypes.Add(PostProcessMaterialTrack.GetArchetype()); TrackArchetypes.Add(NestedSceneAnimationTrack.GetArchetype()); TrackArchetypes.Add(ScreenFadeTrack.GetArchetype()); TrackArchetypes.Add(AudioTrack.GetArchetype()); TrackArchetypes.Add(AudioVolumeTrack.GetArchetype()); TrackArchetypes.Add(ActorTrack.GetArchetype()); TrackArchetypes.Add(ScriptTrack.GetArchetype()); TrackArchetypes.Add(KeyframesPropertyTrack.GetArchetype()); TrackArchetypes.Add(CurvePropertyTrack.GetArchetype()); TrackArchetypes.Add(StringPropertyTrack.GetArchetype()); TrackArchetypes.Add(ObjectReferencePropertyTrack.GetArchetype()); TrackArchetypes.Add(StructPropertyTrack.GetArchetype()); TrackArchetypes.Add(ObjectPropertyTrack.GetArchetype()); TrackArchetypes.Add(EventTrack.GetArchetype()); TrackArchetypes.Add(CameraCutTrack.GetArchetype()); } /// protected override void SetupDragDrop() { base.SetupDragDrop(); DragHandlers.Add(new DragHandler(new DragActors(IsValidActor), OnDragActor)); DragHandlers.Add(new DragHandler(new DragScripts(IsValidScript), OnDragScript)); DragHandlers.Add(new DragHandler(new DragAssets(IsValidAsset), OnDragAsset)); } private static bool IsValidActor(ActorNode actorNode) { return actorNode.Actor; } private static void OnDragActor(Timeline timeline, DragHelper drag) { foreach (var actorNode in ((DragActors)drag).Objects) { ActorTrack track; if (actorNode.Actor is Camera) track = (CameraCutTrack)timeline.NewTrack(CameraCutTrack.GetArchetype()); else track = (ActorTrack)timeline.NewTrack(ActorTrack.GetArchetype()); track.Actor = actorNode.Actor; track.Rename(actorNode.Name); timeline.AddTrack(track); } } private static bool IsValidScript(Script script) { return script && script.Actor; } private static void OnDragScript(Timeline timeline, DragHelper drag) { foreach (var script in ((DragScripts)drag).Objects) { var actor = script.Actor; var track = (ActorTrack)timeline.NewTrack(ActorTrack.GetArchetype()); track.Actor = actor; track.Rename(actor.Name); timeline.AddTrack(track); track.AddScriptTrack(script); } } private static bool IsValidAsset(AssetItem assetItem) { if (assetItem is BinaryAssetItem binaryAssetItem) { if (typeof(MaterialBase).IsAssignableFrom(binaryAssetItem.Type)) { var material = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID); if (material && !material.WaitForLoaded() && material.IsPostFx) return true; } else if (typeof(SceneAnimation).IsAssignableFrom(binaryAssetItem.Type)) { var sceneAnimation = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID); if (sceneAnimation) return true; } else if (typeof(AudioClip).IsAssignableFrom(binaryAssetItem.Type)) { var audioClip = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID); if (audioClip) return true; } } return false; } private static void OnDragAsset(Timeline timeline, DragHelper drag) { foreach (var assetItem in ((DragAssets)drag).Objects) { if (assetItem is BinaryAssetItem binaryAssetItem) { if (typeof(MaterialBase).IsAssignableFrom(binaryAssetItem.Type)) { var material = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID); if (material && !material.WaitForLoaded() && material.IsPostFx) { var track = (PostProcessMaterialTrack)timeline.NewTrack(PostProcessMaterialTrack.GetArchetype()); track.Asset = material; track.Rename(assetItem.ShortName); timeline.AddTrack(track); } } else if (typeof(SceneAnimation).IsAssignableFrom(binaryAssetItem.Type)) { var sceneAnimation = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID); if (!sceneAnimation || sceneAnimation.WaitForLoaded()) continue; var track = (NestedSceneAnimationTrack)timeline.NewTrack(NestedSceneAnimationTrack.GetArchetype()); track.Asset = sceneAnimation; track.TrackMedia.DurationFrames = (int)(sceneAnimation.Duration * timeline.FramesPerSecond); track.Rename(assetItem.ShortName); timeline.AddTrack(track); } else if (typeof(AudioClip).IsAssignableFrom(binaryAssetItem.Type)) { var audioClip = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID); if (!audioClip || audioClip.WaitForLoaded()) continue; var track = (AudioTrack)timeline.NewTrack(AudioTrack.GetArchetype()); track.Asset = audioClip; track.TrackMedia.DurationFrames = (int)(audioClip.Length * timeline.FramesPerSecond); track.Rename(assetItem.ShortName); timeline.AddTrack(track); } } } } private void UpdatePlaybackState() { PlaybackStates state; if (!_player) state = PlaybackStates.Seeking; else if (_player.IsPlaying) state = PlaybackStates.Playing; else if (_player.IsPaused) state = PlaybackStates.Paused; else if (_player.IsStopped) state = PlaybackStates.Stopped; else state = PlaybackStates.Disabled; PlaybackState = state; if (_player && _player.Animation) { _player.Animation.WaitForLoaded(); CurrentFrame = (int)(_player.Time * _player.Animation.FramesPerSecond); } } /// public override void OnPlay() { var time = CurrentTime; _player.Play(); _player.Time = time; base.OnPlay(); } /// public override void OnPause() { _player.Pause(); base.OnPause(); } /// public override void OnStop() { _player.Stop(); base.OnStop(); } /// public override void OnSeek(int frame) { if (_player != null && _player.Animation) { _player.Animation.WaitForLoaded(); _player.Time = frame / _player.Animation.FramesPerSecond; } else { CurrentFrame = frame; } base.OnSeek(frame); } /// public override void Update(float deltaTime) { base.Update(deltaTime); UpdatePlaybackState(); // Draw all selected 3D position tracks as Bezier curve in editor viewport if (ShowSelected3dTrack) { foreach (var track in SelectedTracks) { if ( track is CurvePropertyTrack curveTrack && curveTrack.ValueSize == Vector3.SizeInBytes && curveTrack.MemberTypeName == typeof(Vector3).FullName && string.Equals(curveTrack.MemberName, "position", StringComparison.OrdinalIgnoreCase) ) { var curve = (BezierCurveEditor)curveTrack.Curve; var keyframes = curve.Keyframes; for (var i = 0; i < keyframes.Count; i++) { var k = keyframes[i]; DebugDraw.DrawSphere(new BoundingSphere(k.Value, 10.0f), Color.Red); DebugDraw.DrawSphere(new BoundingSphere(k.Value, 9.5f), new Color(1, 0, 0, 0.1f), 0, false); if (!k.TangentIn.IsZero) { var t = k.Value + k.TangentIn; DebugDraw.DrawLine(k.Value, t, Color.Yellow); DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(0.1f), 0, false); var box = BoundingBox.FromSphere(new BoundingSphere(t, 6.0f)); DebugDraw.DrawBox(box, Color.AliceBlue); DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(0.1f), 0, false); } if (!k.TangentOut.IsZero) { var t = k.Value + k.TangentOut; DebugDraw.DrawLine(k.Value, t, Color.Yellow); DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(0.1f), 0, false); var box = BoundingBox.FromSphere(new BoundingSphere(t, 6.0f)); DebugDraw.DrawBox(box, Color.AliceBlue); DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(0.1f), 0, false); } if (i != 0) { var l = keyframes[i - 1]; float d = (k.Time - l.Time) / 3.0f; DebugDraw.DrawBezier(l.Value, l.Value + d * l.TangentOut, k.Value + d * k.TangentIn, k.Value, Color.Red); } } } } } } /// protected override void OnShowViewContextMenu(ContextMenu.ContextMenu menu) { base.OnShowViewContextMenu(menu); menu.AddButton("Show selected 3D tracks", () => ShowSelected3dTrack = !ShowSelected3dTrack).Checked = ShowSelected3dTrack; } /// /// Loads the timeline from the specified asset. /// /// The asset. public void Load(SceneAnimation asset) { Profiler.BeginEvent("Asset.LoadTimeline"); var data = asset.LoadTimeline(); Profiler.EndEvent(); Profiler.BeginEvent("Timeline.Load"); Load(data); Profiler.EndEvent(); } /// /// Saves the timeline data to the asset. /// /// The asset. public void Save(SceneAnimation asset) { var data = Save(); asset.SaveTimeline(data); asset.Reload(); } } }