// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI.ContextMenu;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport.Previews
{
///
/// Animated model asset preview editor viewport.
///
///
public class AnimatedModelPreview : AssetPreview
{
private AnimatedModel _previewModel;
private ContextMenuButton _showNodesButton, _showBoundsButton, _showFloorButton, _showNodesNamesButton;
private bool _showNodes, _showBounds, _showFloor, _showNodesNames;
private StaticModel _floorModel;
private bool _playAnimation, _playAnimationOnce;
private float _playSpeed = 1.0f;
///
/// Snaps the preview actor to the world origin.
///
protected bool _snapToOrigin = true;
///
/// Gets or sets the skinned model asset to preview.
///
public SkinnedModel SkinnedModel
{
get => _previewModel.SkinnedModel;
set => _previewModel.SkinnedModel = value;
}
///
/// Gets the skinned model actor used to preview selected asset.
///
public AnimatedModel PreviewActor => _previewModel;
///
/// Gets or sets a value indicating whether play the animation in editor.
///
public bool PlayAnimation
{
get => _playAnimation;
set
{
if (_playAnimation == value)
return;
if (!value)
_playSpeed = _previewModel.UpdateSpeed;
_playAnimation = value;
_previewModel.UpdateSpeed = value ? _playSpeed : 0.0f;
PlayAnimationChanged?.Invoke();
}
}
///
/// Occurs when animation playback state gets changed.
///
public event Action PlayAnimationChanged;
///
/// Gets or sets the animation playback speed.
///
public float PlaySpeed
{
get => _playAnimation ? _previewModel.UpdateSpeed : _playSpeed;
set
{
if (_playAnimation)
_previewModel.UpdateSpeed = value;
else
_playSpeed = value;
}
}
///
/// Gets or sets a value indicating whether show animated model skeleton nodes debug view.
///
public bool ShowNodes
{
get => _showNodes;
set
{
if (_showNodes == value)
return;
_showNodes = value;
if (value)
ShowDebugDraw = true;
if (_showNodesButton != null)
_showNodesButton.Checked = value;
}
}
///
/// Gets or sets a value indicating whether show animated model skeleton nodes names debug view.
///
public bool ShowNodesNames
{
get => _showNodesNames;
set
{
if (_showNodesNames == value)
return;
_showNodesNames = value;
if (value)
ShowDebugDraw = true;
if (_showNodesNamesButton != null)
_showNodesNamesButton.Checked = value;
}
}
///
/// Gets or sets a value indicating whether show animated model bounding box debug view.
///
public bool ShowBounds
{
get => _showBounds;
set
{
if (_showBounds == value)
return;
_showBounds = value;
if (value)
ShowDebugDraw = true;
if (_showBoundsButton != null)
_showBoundsButton.Checked = value;
}
}
///
/// Gets or sets a value indicating whether show floor model.
///
public bool ShowFloor
{
get => _showFloor;
set
{
if (_showFloor == value)
return;
_showFloor = value;
if (value && !_floorModel)
{
_floorModel = new StaticModel
{
Position = new Vector3(0, -25, 0),
Scale = new Float3(5, 0.5f, 5),
Model = FlaxEngine.Content.LoadAsync(StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/Primitives/Cube.flax")),
};
}
if (value)
Task.AddCustomActor(_floorModel);
else
Task.RemoveCustomActor(_floorModel);
if (_showFloorButton != null)
_showFloorButton.Checked = value;
}
}
///
/// Gets or sets a value indicating whether scale the model to the normalized bounds.
///
public bool ScaleToFit { get; set; } = true;
///
/// Gets or sets the custom mask for the skeleton nodes. Nodes missing from this list will be skipped during rendering. Works only if is set to true and the array matches the attached nodes hierarchy.
///
public bool[] NodesMask { get; set; }
///
/// Initializes a new instance of the class.
///
/// if set to true use widgets.
public AnimatedModelPreview(bool useWidgets)
: base(useWidgets)
{
Task.Begin += OnBegin;
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
// Setup preview scene
_previewModel = new AnimatedModel
{
UseTimeScale = false,
UpdateWhenOffscreen = true,
BoundsScale = 100.0f,
UpdateMode = AnimatedModel.AnimationUpdateMode.Manual,
};
Task.AddCustomActor(_previewModel);
if (useWidgets)
{
// Show Bounds
_showBoundsButton = ViewWidgetShowMenu.AddButton("Bounds", () => ShowBounds = !ShowBounds);
_showBoundsButton.CloseMenuOnClick = false;
// Show Skeleton
_showNodesButton = ViewWidgetShowMenu.AddButton("Skeleton", () => ShowNodes = !ShowNodes);
_showNodesButton.CloseMenuOnClick = false;
// Show Skeleton Names
_showNodesNamesButton = ViewWidgetShowMenu.AddButton("Skeleton Names", () => ShowNodesNames = !ShowNodesNames);
_showNodesNamesButton.CloseMenuOnClick = false;
// Show Floor
_showFloorButton = ViewWidgetShowMenu.AddButton("Floor", button => ShowFloor = !ShowFloor);
_showFloorButton.IndexInParent = 1;
_showFloorButton.CloseMenuOnClick = false;
}
// Enable shadows
PreviewLight.ShadowsMode = ShadowsCastingMode.All;
PreviewLight.CascadeCount = 3;
PreviewLight.ShadowsDistance = 2000.0f;
Task.ViewFlags |= ViewFlags.Shadows;
}
///
/// Starts the animation playback.
///
public void Play()
{
PlayAnimation = true;
}
///
/// Pauses the animation playback.
///
public void Pause()
{
PlayAnimation = false;
}
///
/// Stops the animation playback.
///
public void Stop()
{
PlayAnimation = false;
_previewModel.ResetAnimation();
_previewModel.UpdateAnimation();
}
///
/// Sets the weight of the blend shape and updates the preview model (if not animated right now).
///
/// The blend shape name.
/// The normalized weight of the blend shape (in range -1:1).
public void SetBlendShapeWeight(string name, float value)
{
_previewModel.SetBlendShapeWeight(name, value);
_playAnimationOnce = true;
}
///
/// Gets the skinned model bounds. Handles skeleton-only assets.
///
/// The local bounds.
public BoundingBox GetBounds()
{
var box = BoundingBox.Zero;
var skinnedModel = SkinnedModel;
if (skinnedModel && skinnedModel.IsLoaded)
{
if (skinnedModel.LODs.Length != 0)
{
// Use model geometry bounds
box = skinnedModel.GetBox();
}
else
{
// Use skeleton bounds
_previewModel.GetCurrentPose(out var pose);
if (pose != null && pose.Length != 0)
{
var point = pose[0].TranslationVector;
box = new BoundingBox(point, point);
for (int i = 1; i < pose.Length; i++)
{
point = pose[i].TranslationVector;
box.Minimum = Vector3.Min(box.Minimum, point);
box.Maximum = Vector3.Max(box.Maximum, point);
}
}
}
}
return box;
}
private void OnBegin(RenderTask task, GPUContext context)
{
if (!ScaleToFit)
{
if (_snapToOrigin)
{
_previewModel.Scale = Vector3.One;
_previewModel.Position = Vector3.Zero;
}
return;
}
// Update preview model scale to fit the preview
var skinnedModel = SkinnedModel;
if (skinnedModel && skinnedModel.IsLoaded)
{
float targetSize = 50.0f;
BoundingBox box = GetBounds();
float maxSize = Mathf.Max(0.001f, (float)box.Size.MaxValue);
float scale = targetSize / maxSize;
_previewModel.Scale = new Vector3(scale);
_previewModel.Position = box.Center * (-0.5f * scale) + new Vector3(0, -10, 0);
}
}
private void OnScriptsReloadBegin()
{
// Prevent any crashes due to dangling references to anim events
_previewModel.ResetAnimation();
}
///
protected override void OnDebugDraw(GPUContext context, ref RenderContext renderContext)
{
base.OnDebugDraw(context, ref renderContext);
// Draw skeleton nodes
if (_showNodes || _showNodesNames)
{
_previewModel.GetCurrentPose(out var pose, true);
var nodes = _previewModel.SkinnedModel?.Nodes;
if (pose != null && pose.Length != 0 && nodes != null)
{
var nodesMask = NodesMask != null && NodesMask.Length == nodes.Length ? NodesMask : null;
if (_showNodes)
{
// Draw bounding box at the node locations
var localBox = new OrientedBoundingBox(new Vector3(-1.0f), new Vector3(1.0f));
for (int nodeIndex = 0; nodeIndex < pose.Length; nodeIndex++)
{
if (nodesMask != null && !nodesMask[nodeIndex])
continue;
var transform = pose[nodeIndex];
transform.Decompose(out var scale, out Matrix _, out _);
transform = Matrix.Invert(Matrix.Scaling(scale)) * transform;
var box = localBox * transform;
DebugDraw.DrawWireBox(box, Color.Green, 0, false);
}
// Nodes connections
for (int nodeIndex = 0; nodeIndex < nodes.Length; nodeIndex++)
{
int parentIndex = nodes[nodeIndex].ParentIndex;
if (parentIndex != -1)
{
if (nodesMask != null && (!nodesMask[nodeIndex] || !nodesMask[parentIndex]))
continue;
var parentPos = pose[parentIndex].TranslationVector;
var bonePos = pose[nodeIndex].TranslationVector;
DebugDraw.DrawLine(parentPos, bonePos, Color.Green, 0, false);
}
}
}
if (_showNodesNames)
{
// Nodes names
for (int nodeIndex = 0; nodeIndex < nodes.Length; nodeIndex++)
{
if (nodesMask != null && !nodesMask[nodeIndex])
continue;
//var t = new Transform(pose[nodeIndex].TranslationVector, Quaternion.Identity, new Float3(0.1f));
DebugDraw.DrawText(nodes[nodeIndex].Name, pose[nodeIndex].TranslationVector, Color.White, 20, 0.0f, 0.1f);
}
}
}
}
// Draw bounds
if (_showBounds)
{
DebugDraw.DrawWireBox(_previewModel.Box, Color.Violet.RGBMultiplied(0.8f), 0, false);
}
}
///
public override void Update(float deltaTime)
{
base.Update(deltaTime);
// Manually update animation
if (PlayAnimation || _playAnimationOnce)
{
_playAnimationOnce = false;
_previewModel.UpdateAnimation();
}
else
{
// Invalidate playback timer (preserves playback state, clears ticking info in LastUpdateTime)
_previewModel.IsActive = !_previewModel.IsActive;
_previewModel.IsActive = !_previewModel.IsActive;
}
}
///
/// Resets the camera to focus on a object.
///
public void ResetCamera()
{
ViewportCamera.SetArcBallView(_previewModel.Box);
}
///
public override bool OnKeyDown(KeyboardKeys key)
{
switch (key)
{
case KeyboardKeys.F:
ResetCamera();
return true;
case KeyboardKeys.Spacebar:
PlayAnimation = !PlayAnimation;
return true;
}
return base.OnKeyDown(key);
}
///
public override void OnDestroy()
{
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
Object.Destroy(ref _floorModel);
Object.Destroy(ref _previewModel);
NodesMask = null;
_showNodesButton = null;
_showBoundsButton = null;
_showFloorButton = null;
_showNodesNamesButton = null;
base.OnDestroy();
}
}
}