// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Runtime.InteropServices; using FlaxEditor.Content; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Previews; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Windows.Search; using Object = FlaxEngine.Object; // ReSharper disable UnusedMember.Local // ReSharper disable UnusedMember.Global // ReSharper disable MemberCanBePrivate.Local namespace FlaxEditor.Windows.Assets { /// /// Animation Graph window allows to view and edit asset. /// /// /// /// public sealed class AnimationGraphWindow : VisjectSurfaceWindow, ISearchWindow { internal static Guid BaseModelId = new Guid(1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); private sealed class AnimationGraphPreview : AnimationPreview { private readonly AnimationGraphWindow _window; public AnimationGraphPreview(AnimationGraphWindow window) : base(true) { _window = window; ShowFloor = true; } /// public override void Draw() { base.Draw(); var style = Style.Current; if (_window.Asset == null || !_window.Asset.IsLoaded) { Render2D.DrawText(style.FontLarge, "Loading...", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center); } } } /// /// The graph properties proxy object. /// private sealed class PropertiesProxy { private SkinnedModel _baseModel; [EditorOrder(10), EditorDisplay("General"), Tooltip("The base model used to preview the animation and prepare the graph (skeleton bones structure must match in instanced AnimationModel actors)")] public SkinnedModel BaseModel { get => _baseModel; set { if (_baseModel != value) { _baseModel = value; if (Window != null) Window.PreviewActor.SkinnedModel = _baseModel; } } } [EditorOrder(1000), EditorDisplay("Parameters"), CustomEditor(typeof(ParametersEditor)), NoSerialize] // ReSharper disable once UnusedAutoPropertyAccessor.Local public AnimationGraphWindow Window { get; set; } [HideInEditor, Serialize] // ReSharper disable once UnusedMember.Local public List Parameters { get => Window.Surface.Parameters; set => throw new Exception("No setter."); } /// /// Gathers parameters from the graph. /// /// The graph window. public void OnLoad(AnimationGraphWindow window) { Window = window; var surfaceParam = window.Surface.GetParameter(BaseModelId); if (surfaceParam != null) BaseModel = FlaxEngine.Content.LoadAsync((Guid)surfaceParam.Value); else BaseModel = window.PreviewActor.GetParameterValue(BaseModelId) as SkinnedModel; } /// /// Saves the properties to the graph. /// /// The graph window. public void OnSave(AnimationGraphWindow window) { var model = window.PreviewActor; model.SetParameterValue(BaseModelId, BaseModel); var surfaceParam = window.Surface.GetParameter(BaseModelId); if (surfaceParam != null) surfaceParam.Value = BaseModel?.ID ?? Guid.Empty; } /// /// Clears temporary data. /// public void OnClean() { // Unlink Window = null; } } /// /// The graph parameters preview proxy object. /// private sealed class PreviewProxy { [EditorDisplay("Parameters"), CustomEditor(typeof(Editor)), NoSerialize] // ReSharper disable once UnusedAutoPropertyAccessor.Local public AnimationGraphWindow Window; private class Editor : CustomEditor { public override DisplayStyle Style => DisplayStyle.InlineIntoParent; public override void Initialize(LayoutElementsContainer layout) { var window = (AnimationGraphWindow)Values[0]; var parameters = window.PreviewActor.Parameters; var data = SurfaceUtils.InitGraphParameters(parameters); SurfaceUtils.DisplayGraphParameters(layout, data, (instance, parameter, tag) => ((AnimationGraphWindow)instance).PreviewActor.GetParameterValue(parameter.Identifier), (instance, value, parameter, tag) => ((AnimationGraphWindow)instance).PreviewActor.SetParameterValue(parameter.Identifier, value), Values); } } } [StructLayout(LayoutKind.Sequential)] private unsafe struct AnimGraphDebugFlowInfo { public uint NodeId; public int BoxId; public fixed uint NodePath[8]; } private FlaxObjectRefPickerControl _debugPicker; private NavigationBar _navigationBar; private PropertiesProxy _properties; private ToolStripButton _showNodesButton; private Tab _previewTab; private readonly List _debugFlows = new List(); /// /// Gets the animated model actor used for the animation preview. /// public AnimatedModel PreviewActor => _preview.PreviewActor; /// public AnimationGraphWindow(Editor editor, AssetItem item) : base(editor, item, true) { // Asset preview _preview = new AnimationGraphPreview(this) { ViewportCamera = new FPSCamera(), ScaleToFit = false, PlayAnimation = true, Parent = _split2.Panel1 }; // Asset properties proxy _properties = new PropertiesProxy(); // Preview properties editor _previewTab = new Tab("Preview"); _previewTab.Presenter.Select(new PreviewProxy { Window = this, }); _tabs.AddTab(_previewTab); // Surface _surface = new AnimGraphSurface(this, Save, _undo) { Parent = _split1.Panel1, Enabled = false }; _surface.ContextChanged += OnSurfaceContextChanged; // Toolstrip _showNodesButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Bone64, () => _preview.ShowNodes = !_preview.ShowNodes).LinkTooltip("Show animated model nodes debug view"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/anim-graph/index.html")).LinkTooltip("See documentation to learn more"); // Debug picker var debugPickerContainer = new ContainerControl(); var debugPickerLabel = new Label { AnchorPreset = AnchorPresets.VerticalStretchLeft, VerticalAlignment = TextAlignment.Center, HorizontalAlignment = TextAlignment.Far, Parent = debugPickerContainer, Size = new Float2(50.0f, _toolstrip.Height), Text = "Debug:", TooltipText = "The current animated model actor to preview. Pick the player to debug it's playback. Leave empty to debug the model from the preview panel.", }; _debugPicker = new FlaxObjectRefPickerControl { Location = new Float2(debugPickerLabel.Right + 4.0f, 8.0f), Width = 120.0f, Type = new ScriptType(typeof(AnimatedModel)), Parent = debugPickerContainer, }; debugPickerContainer.Width = _debugPicker.Right + 2.0f; debugPickerContainer.Size = new Float2(_debugPicker.Right + 2.0f, _toolstrip.Height); debugPickerContainer.Parent = _toolstrip; _debugPicker.CheckValid = OnCheckValid; // Navigation bar _navigationBar = new NavigationBar { Parent = this }; Animations.DebugFlow += OnDebugFlow; } private void OnSurfaceContextChanged(VisjectSurfaceContext context) { _surface.UpdateNavigationBar(_navigationBar, _toolstrip); } private bool OnCheckValid(Object obj, ScriptType type) { return obj is AnimatedModel player && player.AnimationGraph == OriginalAsset; } private unsafe void OnDebugFlow(Animations.DebugFlowInfo flowInfo) { // Filter the flow if (_debugPicker.Value != null) { if (flowInfo.Asset != OriginalAsset || _debugPicker.Value != flowInfo.Instance) return; } else { if (flowInfo.Asset != Asset || _preview.PreviewActor != flowInfo.Instance) return; } // Register flow to show it in UI on a surface var flow = new AnimGraphDebugFlowInfo { NodeId = flowInfo.NodeId, BoxId = (int)flowInfo.BoxId }; Utils.MemoryCopy(new IntPtr(flow.NodePath), new IntPtr(flowInfo.NodePath0), sizeof(uint) * 8ul); lock (_debugFlows) { _debugFlows.Add(flow); } } /// public override IEnumerable NewParameterTypes => Editor.CodeEditing.VisualScriptPropertyTypes.Get(); /// public override void SetParameter(int index, object value) { try { var param = Surface.Parameters[index]; PreviewActor.SetParameterValue(param.ID, value); } catch { // Ignored } base.SetParameter(index, value); } /// protected override void UnlinkItem() { _properties.OnClean(); if (PreviewActor != null) PreviewActor.AnimationGraph = null; base.UnlinkItem(); } /// protected override void OnAssetLinked() { PreviewActor.AnimationGraph = _asset; base.OnAssetLinked(); } /// public override string SurfaceName => "Anim Graph"; /// public override byte[] SurfaceData { get => _asset.LoadSurface(); set { if (value == null) { Editor.LogError("Failed to save surface data"); return; } if (_asset.SaveSurface(value)) { _surface.MarkAsEdited(); Editor.LogError("Failed to save surface data"); return; } _asset.Reload(); _asset.WaitForLoaded(); _preview.PreviewActor.ResetLocalTransform(); _previewTab.Presenter.BuildLayoutOnUpdate(); } } /// protected override bool LoadSurface() { // Load surface graph if (_surface.Load()) { Editor.LogError("Failed to load animation graph surface."); return true; } // Init properties and parameters proxy _properties.OnLoad(this); _previewTab.Presenter.BuildLayoutOnUpdate(); // Update navbar _surface.UpdateNavigationBar(_navigationBar, _toolstrip); return false; } /// protected override bool SaveSurface() { // Sync edited parameters _properties.OnSave(this); // Save surface (will call SurfaceData setter) _surface.Save(); return false; } /// protected override void OnSurfaceEditingStart() { _propertiesEditor.Select(_properties); base.OnSurfaceEditingStart(); } /// protected override void PerformLayoutBeforeChildren() { base.PerformLayoutBeforeChildren(); if (_navigationBar != null && _debugPicker?.Parent != null && Parent != null) { _navigationBar.Bounds = new Rectangle ( new Float2(_debugPicker.Parent.Right + 8.0f, 0), new Float2(Parent.Width - X - 8.0f, 32) ); } } /// public override unsafe void OnUpdate() { // Extract animations playback state from the events tracing var debugActor = _debugPicker.Value as AnimatedModel; if (debugActor == null) debugActor = _preview.PreviewActor; if (debugActor != null) { debugActor.EnableTracing = true; Surface.LastTraceEvents = debugActor.TraceEvents; } base.OnUpdate(); // Update graph execution flow debugging visualization lock (_debugFlows) { foreach (var debugFlow in _debugFlows) { var context = Surface.FindContext(new Span(debugFlow.NodePath, 8)); var node = context?.FindNode(debugFlow.NodeId); var box = node?.GetBox(debugFlow.BoxId); box?.HighlightConnections(); } _debugFlows.Clear(); } _showNodesButton.Checked = _preview.ShowNodes; } /// public override void OnDestroy() { if (IsDisposing) return; Animations.DebugFlow -= OnDebugFlow; _properties = null; _navigationBar = null; _debugPicker = null; _showNodesButton = null; _previewTab = null; base.OnDestroy(); } /// public SearchAssetTypes AssetType => SearchAssetTypes.AnimGraph; } }