From d8775a3ae56476296f097eb8beaa3dbd0630a647 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 4 Nov 2021 16:23:04 +0100 Subject: [PATCH] Add Ragdoll Editor utilities for easier ragdoll setup and editing --- .../CustomEditors/Dedicated/RagdollEditor.cs | 300 +++++++++++++++++- .../SceneGraph/Actors/AnimatedModelNode.cs | 100 ++++-- 2 files changed, 360 insertions(+), 40 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs b/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs index 52fa409b9..6b6d71540 100644 --- a/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs @@ -1,6 +1,14 @@ // Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.SceneGraph; +using FlaxEditor.SceneGraph.Actors; using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.CustomEditors.Dedicated { @@ -16,13 +24,295 @@ namespace FlaxEditor.CustomEditors.Dedicated { base.Initialize(layout); - // Add info box - if (IsSingleObject && Values[0] is Ragdoll ragdoll) + var ragdoll = Values.Count == 1 ? Values[0] as Ragdoll : null; + if (ragdoll == null) + return; + + var editorGroup = layout.Group("Ragdoll Editor"); + editorGroup.Panel.Open(false); + + // Info + var text = $"Total mass: {Utils.RoundTo1DecimalPlace(ragdoll.TotalMass)}kg"; + var label = editorGroup.Label(text); + label.Label.AutoHeight = true; + + if (ragdoll.Parent is AnimatedModel animatedModel && animatedModel.SkinnedModel) { - var text = $"Total mass: {Utils.RoundTo1DecimalPlace(ragdoll.TotalMass)}kg"; - var label = layout.Label(text); - label.Label.AutoHeight = true; + // Builder + var grid = editorGroup.CustomContainer(); + var gridControl = grid.CustomControl; + gridControl.ClipChildren = false; + gridControl.Height = Button.DefaultHeight; + gridControl.SlotsHorizontally = 3; + gridControl.SlotsVertically = 1; + grid.Button("Rebuild").Button.ButtonClicked += OnRebuild; + grid.Button("Rebuild/Add bone").Button.ButtonClicked += OnRebuildBone; + grid.Button("Remove bone").Button.ButtonClicked += OnRemoveBone; } + + if (Presenter.Owner is Windows.PropertiesWindow || Presenter.Owner is Windows.Assets.PrefabWindow) + { + // Selection + var grid = editorGroup.CustomContainer(); + var gridControl = grid.CustomControl; + gridControl.ClipChildren = false; + gridControl.Height = Button.DefaultHeight; + gridControl.SlotsHorizontally = 3; + gridControl.SlotsVertically = 1; + grid.Button("Select all joints").Button.Clicked += OnSelectAllJoints; + grid.Button("Select all colliders").Button.Clicked += OnSelectAllColliders; + grid.Button("Select all bodies").Button.Clicked += OnSelectAllBodies; + } + } + + class RebuildContextMenu : ContextMenuBase + { + private Action _rebuild; + private AnimatedModelNode.RebuildOptions _options = new AnimatedModelNode.RebuildOptions(); + + public RebuildContextMenu(Action rebuild) + { + _rebuild = rebuild; + + const float width = 280.0f; + const float height = 220.0f; + Size = new Vector2(width, height); + + // Title + var title = new Label(2, 2, width - 4, 23.0f) + { + Font = new FontReference(FlaxEngine.GUI.Style.Current.FontLarge), + Text = "Ragdoll Options", + Parent = this + }; + + // Buttons + var rebuildButton = new Button(2.0f, title.Bottom + 2.0f, width - 4.0f, 20.0f) + { + Text = "Rebuild", + Parent = this + }; + rebuildButton.Clicked += OnRebuild; + + // Actual panel + var panel1 = new Panel(ScrollBars.Vertical) + { + Bounds = new Rectangle(0, rebuildButton.Bottom + 2.0f, width, height - rebuildButton.Bottom - 2.0f), + Parent = this + }; + var editor = new CustomEditorPresenter(null); + editor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop; + editor.Panel.IsScrollable = true; + editor.Panel.Parent = panel1; + + editor.Select(_options); + } + + private void OnRebuild() + { + Hide(); + _rebuild(_options); + } + + protected override void OnShow() + { + Focus(); + + base.OnShow(); + } + + public override void Hide() + { + if (!Visible) + return; + + Focus(null); + + base.Hide(); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (key == KeyboardKeys.Escape) + { + Hide(); + return true; + } + + return base.OnKeyDown(key); + } + } + + private void OnRebuild(Button button) + { + var cm = new RebuildContextMenu(Rebuild); + cm.Show(button.Parent, button.BottomLeft); + } + + private void Rebuild(AnimatedModelNode.RebuildOptions options) + { + var ragdoll = (Ragdoll)Values[0]; + var animatedModel = (AnimatedModel)ragdoll.Parent; + + // Remove existing bodies + var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive); + var actions = new List(); + foreach (var body in bodies) + { + var action = new Actions.DeleteActorsAction(new List { SceneGraphFactory.FindNode(body.ID) }); + action.Do(); + actions.Add(action); + } + Presenter.Undo?.AddAction(new MultiUndoAction(actions)); + + // Build ragdoll + SceneGraph.Actors.AnimatedModelNode.BuildRagdoll(animatedModel, options, ragdoll); + } + + private void OnRebuildBone(Button button) + { + PickBone(button, RebuildBone, false); + } + + private void RebuildBone(string name) + { + var ragdoll = (Ragdoll)Values[0]; + var animatedModel = (AnimatedModel)ragdoll.Parent; + + // Remove existing body + var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive); + var body = bodies.FirstOrDefault(x => x.Name == name); + if (body != null) + { + var action = new Actions.DeleteActorsAction(new List { SceneGraphFactory.FindNode(body.ID) }); + action.Do(); + Presenter.Undo?.AddAction(action); + } + + // Build ragdoll + SceneGraph.Actors.AnimatedModelNode.BuildRagdoll(animatedModel, new AnimatedModelNode.RebuildOptions(), ragdoll, name); + } + + private void OnRemoveBone(Button button) + { + PickBone(button, RemoveBone, true); + } + + private void RemoveBone(string name) + { + var ragdoll = (Ragdoll)Values[0]; + var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive); + var joints = bodies.SelectMany(y => y.Children).Where(z => z is Joint && z.IsActive).Cast(); + var body = bodies.First(x => x.Name == name); + var replacementJoint = joints.FirstOrDefault(x => x.Parent == body && x.Target != null); + + // Fix joints using this bone + foreach (var joint in joints) + { + if (joint.Target == body) + { + if (replacementJoint != null) + { + // Swap the joint target to the parent of the removed body to keep ragdoll connected + using (new UndoBlock(Presenter.Undo, joint, "Fix joint")) + { + joint.Target = replacementJoint.Target; + joint.EnableAutoAnchor = true; + } + } + else + { + // Remove joint that will no longer be valid + var action = new Actions.DeleteActorsAction(new List { SceneGraphFactory.FindNode(joint.ID) }); + action.Do(); + Presenter.Undo?.AddAction(action); + } + } + } + + // Remove body + { + var action = new Actions.DeleteActorsAction(new List { SceneGraphFactory.FindNode(body.ID) }); + action.Do(); + Presenter.Undo?.AddAction(action); + } + } + + private void PickBone(Button button, Action action, bool showOnlyExisting) + { + // Show context menu with list of bones to pick + var cm = new ItemsListContextMenu(280); + var ragdoll = (Ragdoll)Values[0]; + var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive); + var animatedModel = (AnimatedModel)ragdoll.Parent; + animatedModel.SkinnedModel.WaitForLoaded(); + var nodes = animatedModel.SkinnedModel.Nodes; + var bones = animatedModel.SkinnedModel.Bones; + foreach (var bone in bones) + { + var node = nodes[bone.NodeIndex]; + if (showOnlyExisting && !bodies.Any(x => x.Name == node.Name)) + continue; + string prefix = string.Empty, tooltip = node.Name; + var boneParentIndex = bone.ParentIndex; + while (boneParentIndex != -1) + { + prefix += " "; + tooltip = nodes[bones[boneParentIndex].NodeIndex].Name + " > " + tooltip; + boneParentIndex = bones[boneParentIndex].ParentIndex; + } + var item = new ItemsListContextMenu.Item + { + Name = prefix + node.Name, + TooltipText = tooltip, + Tag = node.Name, + }; + if (!showOnlyExisting && !bodies.Any(x => x.Name == node.Name)) + item.TintColor = new Color(1, 0.8f, 0.8f, 0.6f); + cm.AddItem(item); + } + cm.ItemClicked += item => action((string)item.Tag); + cm.SortChildren(); + cm.Show(button.Parent, button.BottomLeft); + } + + private void OnSelectAllJoints() + { + var ragdoll = (Ragdoll)Values[0]; + var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive); + var joints = bodies.SelectMany(y => y.Children).Where(z => z is Joint && z.IsActive); + Select(joints); + } + + private void OnSelectAllColliders() + { + var ragdoll = (Ragdoll)Values[0]; + var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive); + var colliders = bodies.SelectMany(y => y.Children).Where(z => z is Collider && z.IsActive); + Select(colliders); + } + + private void OnSelectAllBodies() + { + var ragdoll = (Ragdoll)Values[0]; + var bodies = ragdoll.Children.Where(x => x is RigidBody && x.IsActive); + Select(bodies); + } + + private void Select(IEnumerable list) + { + var selection = new List(); + foreach (var e in list) + { + var node = SceneGraphFactory.FindNode(e.ID); + if (node != null) + selection.Add(node); + } + if (Presenter.Owner is Windows.PropertiesWindow propertiesWindow) + propertiesWindow.Editor.SceneEditing.Select(selection); + else if (Presenter.Owner is Windows.Assets.PrefabWindow prefabWindow) + prefabWindow.Select(selection); } } } diff --git a/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs b/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs index 9bf80e971..022f80a0e 100644 --- a/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/AnimatedModelNode.cs @@ -1,6 +1,8 @@ // Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using FlaxEditor.GUI.ContextMenu; using FlaxEngine; @@ -34,12 +36,26 @@ namespace FlaxEditor.SceneGraph.Actors private void OnCreateRagdoll() { - // Settings - var minBoneSize = 20.0f; // The minimum size for the bone bounds to be used for physical bodies generation - var minValidSize = 0.0001f; // The minimum size of the bone bounds to be included (used to skip too small, degenerated or invalid bones) - var collisionMargin = 1.01f; // The scale of the collision body dimensions (relative to the visual dimensions of the bones) + BuildRagdoll((AnimatedModel)Actor, null); + TreeNode.ExpandAll(true); + } - var actor = (AnimatedModel)Actor; + internal class RebuildOptions + { + [DefaultValue(20.0f), Limit(0), Tooltip("The minimum size for the bone bounds to be used for physical bodies generation.")] + public float MinBoneSize = 20.0f; + + [DefaultValue(0.0001f), Limit(0), Tooltip("The minimum size of the bone bounds to be included (used to skip too small, degenerated or invalid bones).")] + public float MinValidSize = 0.0001f; + + [DefaultValue(1.01f), Limit(0.001f, 2.0f), Tooltip("The scale of the collision body dimensions (relative to the visual dimensions of the bones).")] + public float CollisionMargin = 1.01f; + } + + internal static void BuildRagdoll(AnimatedModel actor, RebuildOptions options = null, Ragdoll ragdoll = null, string boneNameToBuild = null) + { + if (options == null) + options = new RebuildOptions(); var model = actor.SkinnedModel; if (!model || model.WaitForLoaded()) { @@ -120,8 +136,12 @@ namespace FlaxEditor.SceneGraph.Actors } var boneBoxSize = (boneBounds.Size * 0.5f).Length; var boneMergedSize = bonesMergedSizes[boneIndex] += boneBoxSize; - if (boneMergedSize < minBoneSize && boneMergedSize >= minValidSize) + if (boneMergedSize < options.MinBoneSize && boneMergedSize >= options.MinValidSize) { + // Don't merge bone that was selected for rebuild + if (boneNameToBuild != null && boneNameToBuild == nodes[bone.NodeIndex].Name) + continue; + if (bone.ParentIndex != -1) { // Merge it into parent @@ -159,7 +179,7 @@ namespace FlaxEditor.SceneGraph.Actors int forcedRootBoneIndex = -1, firstParentBoneIndex = -1; for (int boneIndex = 0; boneIndex < bones.Length; ++boneIndex) { - if (bonesMergedSizes[boneIndex] > minBoneSize) + if (bonesMergedSizes[boneIndex] > options.MinBoneSize) { var parentIndex = bones[boneIndex].ParentIndex; if (parentIndex == -1) @@ -181,26 +201,45 @@ namespace FlaxEditor.SceneGraph.Actors // TODO: add undo support - // Spawn ragdoll actor - var ragdoll = new Ragdoll + var boneBodies = new RigidBody[bones.Length]; + + if (ragdoll == null) { - StaticFlags = StaticFlags.None, - Name = "Ragdoll", - Parent = actor, - }; + // Spawn ragdoll actor + ragdoll = new Ragdoll + { + StaticFlags = StaticFlags.None, + Name = "Ragdoll", + Parent = actor, + }; + } + else + { + // Reuse existing bones for joints + var children = ragdoll.Children; + for (int boneIndex = 0; boneIndex < bones.Length; ++boneIndex) + { + ref var bone = ref bones[boneIndex]; + var node = nodes[bone.NodeIndex]; + boneBodies[boneIndex] = (RigidBody)children.FirstOrDefault(x => x is RigidBody && x.Name == node.Name); + } + } // Spawn physical bodies for bones - var boneBodies = new RigidBody[bones.Length]; for (int boneIndex = 0; boneIndex < bones.Length; ++boneIndex) { ref var boneVertices = ref bonesVertices[boneIndex]; if (boneVertices == null || boneVertices.Count == 0) continue; var boneBounds = bonesBounds[boneIndex]; - if (bonesMergedSizes[boneIndex] < minBoneSize && boneIndex != forcedRootBoneIndex) + if (bonesMergedSizes[boneIndex] < options.MinBoneSize && boneIndex != forcedRootBoneIndex && boneNameToBuild == null) continue; ref var bone = ref bones[boneIndex]; ref var node = ref nodes[bone.NodeIndex]; + if (boneNameToBuild != null && boneNameToBuild != node.Name) + continue; + if (boneBodies[boneIndex] != null) + continue; // Calculate bone orientation based on the variance of the vertices var covarianceMatrix = CalculateCovarianceMatrix(boneVertices); @@ -234,13 +273,13 @@ namespace FlaxEditor.SceneGraph.Actors var collider = new BoxCollider { Name = "Box", - Size = boneLocalBoundsSize * collisionMargin, + Size = boneLocalBoundsSize * options.CollisionMargin, }; #elif false var collider = new SphereCollider { Name = "Sphere", - Radius = boneLocalBoundsSize.MaxValue * 0.5f * collisionMargin, + Radius = boneLocalBoundsSize.MaxValue * 0.5f * options.CollisionMargin, }; #elif true var collider = new CapsuleCollider @@ -249,20 +288,20 @@ namespace FlaxEditor.SceneGraph.Actors }; if (boneLocalBoundsSize.X > boneLocalBoundsSize.Y && boneLocalBoundsSize.X > boneLocalBoundsSize.Z) { - collider.Height = boneLocalBoundsSize.X * collisionMargin; - collider.Radius = Mathf.Max(boneLocalBoundsSize.Y, boneLocalBoundsSize.Z) * 0.5f * collisionMargin; + collider.Height = boneLocalBoundsSize.X * options.CollisionMargin; + collider.Radius = Mathf.Max(boneLocalBoundsSize.Y, boneLocalBoundsSize.Z) * 0.5f * options.CollisionMargin; } else if (boneLocalBoundsSize.Y > boneLocalBoundsSize.X && boneLocalBoundsSize.Y > boneLocalBoundsSize.Z) { collider.LocalOrientation = Quaternion.Euler(0, 0, 90); - collider.Height = boneLocalBoundsSize.Y * collisionMargin; - collider.Radius = Mathf.Max(boneLocalBoundsSize.X, boneLocalBoundsSize.Z) * 0.5f * collisionMargin; + collider.Height = boneLocalBoundsSize.Y * options.CollisionMargin; + collider.Radius = Mathf.Max(boneLocalBoundsSize.X, boneLocalBoundsSize.Z) * 0.5f * options.CollisionMargin; } else { collider.LocalOrientation = Quaternion.Euler(0, 90, 0); - collider.Height = boneLocalBoundsSize.Z * collisionMargin; - collider.Radius = Mathf.Max(boneLocalBoundsSize.X, boneLocalBoundsSize.Y) * 0.5f * collisionMargin; + collider.Height = boneLocalBoundsSize.Z * options.CollisionMargin; + collider.Radius = Mathf.Max(boneLocalBoundsSize.X, boneLocalBoundsSize.Y) * 0.5f * options.CollisionMargin; } collider.Height = Mathf.Max(collider.Height - collider.Radius * 2.0f, 0.0f); #endif @@ -286,16 +325,8 @@ namespace FlaxEditor.SceneGraph.Actors #else var joint = new D6Joint { - LimitSwing = new LimitConeRange - { - YLimitAngle = 45.0f, - ZLimitAngle = 45.0f, - }, - LimitTwist = new LimitAngularRange - { - Lower = -15.0f, - Upper = 15.0f, - }, + LimitSwing = new LimitConeRange(45.0f, 45.0f), + LimitTwist = new LimitAngularRange(-15.0f, 15.0f), }; joint.SetMotion(D6JointAxis.X, D6JointMotion.Locked); joint.SetMotion(D6JointAxis.Y, D6JointMotion.Locked); @@ -325,8 +356,7 @@ namespace FlaxEditor.SceneGraph.Actors } } - TreeNode.ExpandAll(true); - Editor.Instance.Scene.MarkSceneEdited(Root?.ParentScene); + Editor.Instance.Scene.MarkSceneEdited(actor.Scene); } private static unsafe Matrix CalculateCovarianceMatrix(List vertices)