diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs index f0db64fca..3d41d0904 100644 --- a/Source/Editor/SceneGraph/Actors/SplineNode.cs +++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs @@ -314,6 +314,7 @@ namespace FlaxEditor.SceneGraph.Actors contextMenu.AddButton("Add spline model", OnAddSplineModel); contextMenu.AddButton("Add spline collider", OnAddSplineCollider); + contextMenu.AddButton("Add spline rope body", OnAddSplineRopeBody); } private void OnAddSplineModel() @@ -337,6 +338,16 @@ namespace FlaxEditor.SceneGraph.Actors Editor.Instance.SceneEditing.Spawn(actor, Actor); } + private void OnAddSplineRopeBody() + { + var actor = new SplineRopeBody + { + StaticFlags = StaticFlags.None, + Transform = Actor.Transform, + }; + Editor.Instance.SceneEditing.Spawn(actor, Actor); + } + internal static void OnSplineEdited(Spline spline) { var collider = spline.GetChild(); diff --git a/Source/Editor/SceneGraph/SceneGraphFactory.cs b/Source/Editor/SceneGraph/SceneGraphFactory.cs index 9abf44af3..071cb6e5b 100644 --- a/Source/Editor/SceneGraph/SceneGraphFactory.cs +++ b/Source/Editor/SceneGraph/SceneGraphFactory.cs @@ -68,6 +68,7 @@ namespace FlaxEditor.SceneGraph CustomNodesTypes.Add(typeof(Spline), typeof(SplineNode)); CustomNodesTypes.Add(typeof(SplineModel), typeof(ActorNode)); CustomNodesTypes.Add(typeof(SplineCollider), typeof(ColliderNode)); + CustomNodesTypes.Add(typeof(SplineRopeBody), typeof(ActorNode)); } /// diff --git a/Source/Engine/Physics/Actors/SplineRopeBody.cpp b/Source/Engine/Physics/Actors/SplineRopeBody.cpp new file mode 100644 index 000000000..acdc8ef59 --- /dev/null +++ b/Source/Engine/Physics/Actors/SplineRopeBody.cpp @@ -0,0 +1,213 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#include "SplineRopeBody.h" +#include "Engine/Level/Actors/Spline.h" +#include "Engine/Level/Scene/Scene.h" +#include "Engine/Physics/Physics.h" +#include "Engine/Engine/Time.h" +#include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Serialization/Serialization.h" + +SplineRopeBody::SplineRopeBody(const SpawnParams& params) + : Actor(params) +{ +} + +void SplineRopeBody::Tick() +{ + if (!_spline || _spline->GetSplinePointsCount() < 2) + return; + PROFILE_CPU(); + + // TODO: add as configs + const float SubstepTime = 0.02f; + const int32 SolverIterations = 1; + + // Cache data + const Vector3 gravity = Physics::GetGravity() * GravityScale; + auto& keyframes = _spline->Curve.GetKeyframes(); + const Transform splineTransform = _spline->GetTransform(); + const int32 keyframesCount = keyframes.Count(); + const float substepTime = SubstepTime; + const float substepTimeSqr = substepTime * substepTime; + bool splineDirty = false; + + // Synchronize spline keyframes with simulated masses + if (_masses.Count() > keyframesCount) + _masses.Resize(keyframesCount); + else + { + _masses.EnsureCapacity(keyframesCount); + while (_masses.Count() < keyframesCount) + { + const int32 i = _masses.Count(); + auto& mass = _masses.AddOne(); + mass.PrevPosition = splineTransform.LocalToWorld(keyframes[i].Value.Translation); + if (i != 0) + mass.SegmentLength = Vector3::Distance(mass.PrevPosition, _masses[i - 1].PrevPosition); + else + mass.SegmentLength = 0.0f; + } + } + { + // Rope start + auto& mass = _masses.First(); + mass.Position = mass.PrevPosition = GetPosition(); + mass.Unconstrained = false; + if (splineTransform.LocalToWorld(keyframes.First().Value.Translation) != mass.Position) + splineDirty = true; + } + for (int32 i = 1; i < keyframesCount; i++) + { + auto& mass = _masses[i]; + mass.Unconstrained = true; + mass.Position = splineTransform.LocalToWorld(keyframes[i].Value.Translation); + } + if (AttachEnd) + { + // Rope end + auto& mass = _masses.Last(); + mass.Position = mass.PrevPosition = AttachEnd->GetPosition(); + mass.Unconstrained = false; + if (splineTransform.LocalToWorld(keyframes.Last().Value.Translation) != mass.Position) + splineDirty = true; + } + + // Perform simulation in substeps to have better stability + _time += Time::Update.DeltaTime.GetTotalSeconds(); + while (_time > substepTime) + { + // Verlet integration + // [Reference: https://en.wikipedia.org/wiki/Verlet_integration] + const Vector3 force = gravity + AdditionalForce; + for (int32 i = 0; i < keyframesCount; i++) + { + auto& mass = _masses[i]; + if (mass.Unconstrained) + { + const Vector3 velocity = mass.Position - mass.PrevPosition; + mass.PrevPosition = mass.Position; + mass.Position = mass.Position + velocity + (substepTimeSqr * force); + keyframes[i].Value.Translation = splineTransform.WorldToLocal(mass.Position); + } + } + + // Constraints solving + for (int32 iteration = 0; iteration < SolverIterations; iteration++) + { + // Distance constraint + for (int32 i = 1; i < keyframesCount; i++) + { + auto& massA = _masses[i - 1]; + auto& massB = _masses[i]; + Vector3 offset = massB.Position - massA.Position; + const float distance = offset.Length(); + const float scale = (distance - massB.SegmentLength) / Math::Max(distance, ZeroTolerance); + if (massA.Unconstrained && massB.Unconstrained) + { + offset *= scale * 0.5f; + massA.Position += offset; + massB.Position -= offset; + } + else if (massA.Unconstrained) + { + massA.Position += scale * offset; + } + else if (massB.Unconstrained) + { + massB.Position -= scale * offset; + } + } + + // Stiffness constraint + if (EnableStiffness) + { + for (int32 i = 2; i < keyframesCount; i++) + { + auto& massA = _masses[i - 2]; + auto& massB = _masses[i]; + Vector3 offset = massB.Position - massA.Position; + const float distance = offset.Length(); + const float scale = (distance - massB.SegmentLength * 2.0f) / Math::Max(distance, ZeroTolerance); + if (massA.Unconstrained && massB.Unconstrained) + { + offset *= scale * 0.5f; + massA.Position += offset; + massB.Position -= offset; + } + else if (massA.Unconstrained) + { + massA.Position += scale * offset; + } + else if (massB.Unconstrained) + { + massB.Position -= scale * offset; + } + } + } + } + + _time -= substepTime; + splineDirty = true; + } + + // Update spline and relevant components (eg. spline model) + if (splineDirty) + { + for (int32 i = 0; i < keyframesCount; i++) + keyframes[i].Value.Translation = splineTransform.WorldToLocal(_masses[i].Position); + + _spline->UpdateSpline(); + } +} + +void SplineRopeBody::Serialize(SerializeStream& stream, const void* otherObj) +{ + Actor::Serialize(stream, otherObj); + + SERIALIZE_GET_OTHER_OBJ(SplineRopeBody); + + SERIALIZE(AttachEnd); + SERIALIZE(GravityScale); + SERIALIZE(AdditionalForce); + SERIALIZE(EnableStiffness); +} + +void SplineRopeBody::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) +{ + Actor::Deserialize(stream, modifier); + + DESERIALIZE(AttachEnd); + DESERIALIZE(GravityScale); + DESERIALIZE(AdditionalForce); + DESERIALIZE(EnableStiffness); +} + +void SplineRopeBody::OnEnable() +{ + GetScene()->Ticking.Update.AddTick(this); + + Actor::OnEnable(); +} + +void SplineRopeBody::OnDisable() +{ + Actor::OnDisable(); + + GetScene()->Ticking.Update.RemoveTick(this); +} + +void SplineRopeBody::OnParentChanged() +{ + Actor::OnParentChanged(); + + _spline = Cast(_parent); +} + +void SplineRopeBody::OnTransformChanged() +{ + Actor::OnTransformChanged(); + + _box = BoundingBox(_transform.Translation, _transform.Translation); + _sphere = BoundingSphere(_transform.Translation, 0.0f); +} diff --git a/Source/Engine/Physics/Actors/SplineRopeBody.h b/Source/Engine/Physics/Actors/SplineRopeBody.h new file mode 100644 index 000000000..5ac0506a5 --- /dev/null +++ b/Source/Engine/Physics/Actors/SplineRopeBody.h @@ -0,0 +1,70 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Level/Actor.h" +#include "Engine/Scripting/ScriptingObjectReference.h" + +class Spline; + +/// +/// Physical simulation actor for ropes, chains and cables represented by a spline. +/// +/// +API_CLASS() class FLAXENGINE_API SplineRopeBody : public Actor +{ +DECLARE_SCENE_OBJECT(SplineRopeBody); +private: + + struct Mass + { + Vector3 Position; + float SegmentLength; + Vector3 PrevPosition; + bool Unconstrained; + }; + + Spline* _spline = nullptr; + float _time = 0.0f; + Array _masses; + +public: + + /// + /// The target actor too attach the rope end to. If unset the rope end will run freely. + /// + API_FIELD(Attributes="EditorOrder(0), DefaultValue(null), EditorDisplay(\"Rope\")") + ScriptingObjectReference AttachEnd; + + /// + /// The world gravity scale applied to the rope. Can be used to adjust gravity force or disable it. + /// + API_FIELD(Attributes="EditorOrder(10), EditorDisplay(\"Rope\")") + float GravityScale = 1.0f; + + /// + /// The additional, external force applied to rope (world-space). This can be eg. wind force. + /// + API_FIELD(Attributes="EditorOrder(20), EditorDisplay(\"Rope\")") + Vector3 AdditionalForce = Vector3::Zero; + + /// + /// If checked, the physics solver will use stiffness constraint for rope. It will be less likely to bend over and will preserve more it's shape. + /// + API_FIELD(Attributes="EditorOrder(30), EditorDisplay(\"Rope\")") + bool EnableStiffness = false; + +private: + + void Tick(); + +public: + + // [Actor] + void Serialize(SerializeStream& stream, const void* otherObj) override; + void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; + void OnEnable() override; + void OnDisable() override; + void OnTransformChanged() override; + void OnParentChanged() override; +};