diff --git a/Source/Editor/CustomEditors/Dedicated/ClothEditor.cs b/Source/Editor/CustomEditors/Dedicated/ClothEditor.cs new file mode 100644 index 000000000..999631991 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/ClothEditor.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using FlaxEditor.Gizmo; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.GUI; +using FlaxEngine.Tools; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + /// + [CustomEditor(typeof(Cloth)), DefaultEditor] + class ClothEditor : ActorEditor + { + private ClothPaintingGizmoMode _gizmoMode; + private Viewport.Modes.EditorGizmoMode _prevMode; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + if (Values.Count != 1) + return; + + // Add gizmo painting mode to the viewport + var owner = Presenter.Owner; + if (owner == null) + return; + var gizmoOwner = owner as IGizmoOwner ?? owner.PresenterViewport as IGizmoOwner; + if (gizmoOwner == null) + return; + var gizmos = gizmoOwner.Gizmos; + _gizmoMode = new ClothPaintingGizmoMode(); + gizmos.AddMode(_gizmoMode); + _prevMode = gizmos.ActiveMode; + gizmos.ActiveMode = _gizmoMode; + _gizmoMode.Gizmo.SetPaintCloth((Cloth)Values[0]); + + // Insert gizmo mode options to properties editing + var paintGroup = layout.Group("Cloth Painting"); + var paintValue = new ReadOnlyValueContainer(new ScriptType(typeof(ClothPaintingGizmoMode)), _gizmoMode); + paintGroup.Object(paintValue); + { + var grid = paintGroup.CustomContainer(); + var gridControl = grid.CustomControl; + gridControl.ClipChildren = false; + gridControl.Height = Button.DefaultHeight; + gridControl.SlotsHorizontally = 2; + gridControl.SlotsVertically = 1; + grid.Button("Fill", "Fills the cloth particles with given paint value.").Button.Clicked += _gizmoMode.Gizmo.Fill; + grid.Button("Reset", "Clears the cloth particles paint.").Button.Clicked += _gizmoMode.Gizmo.Reset; + } + } + + /// + protected override void Deinitialize() + { + // Cleanup gizmos + if (_gizmoMode != null) + { + var gizmos = _gizmoMode.Owner.Gizmos; + if (gizmos.ActiveMode == _gizmoMode) + gizmos.ActiveMode = _prevMode; + gizmos.RemoveMode(_gizmoMode); + _gizmoMode.Dispose(); + _gizmoMode = null; + } + _prevMode = null; + + base.Deinitialize(); + } + } +} diff --git a/Source/Editor/Gizmo/GizmosCollection.cs b/Source/Editor/Gizmo/GizmosCollection.cs index beb653826..0df516d32 100644 --- a/Source/Editor/Gizmo/GizmosCollection.cs +++ b/Source/Editor/Gizmo/GizmosCollection.cs @@ -75,6 +75,10 @@ namespace FlaxEditor.Gizmo /// public event Action ActiveModeChanged; + /// + /// Init. + /// + /// The gizmos owner interface. public GizmosCollection(IGizmoOwner owner) { _owner = owner; diff --git a/Source/Editor/Gizmo/IGizmoOwner.cs b/Source/Editor/Gizmo/IGizmoOwner.cs index 893e9a44a..a28810179 100644 --- a/Source/Editor/Gizmo/IGizmoOwner.cs +++ b/Source/Editor/Gizmo/IGizmoOwner.cs @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System.Collections.Generic; using FlaxEngine; namespace FlaxEditor.Gizmo @@ -94,5 +95,11 @@ namespace FlaxEditor.Gizmo /// Gets the root tree node for the scene graph. /// SceneGraph.RootNode SceneGraphRoot { get; } + + /// + /// Selects the scene objects. + /// + /// The nodes to select + void Select(List nodes); } } diff --git a/Source/Editor/Tools/ClothPainting.cs b/Source/Editor/Tools/ClothPainting.cs new file mode 100644 index 000000000..2ad3b94a1 --- /dev/null +++ b/Source/Editor/Tools/ClothPainting.cs @@ -0,0 +1,359 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using FlaxEditor; +using FlaxEditor.Gizmo; +using FlaxEditor.SceneGraph; +using FlaxEditor.Viewport.Modes; + +namespace FlaxEngine.Tools +{ + sealed class ClothPaintingGizmoMode : EditorGizmoMode + { + [HideInEditor] + public ClothPaintingGizmo Gizmo; + +#pragma warning disable CS0649 + /// + /// Brush radius (world-space). + /// + public float BrushSize = 50.0f; + + /// + /// Brush paint intensity. + /// + public float BrushStrength = 2.0f; + + /// + /// Brush paint falloff. Hardens or softens painting. + /// + public float BrushFalloff = 1.5f; + + /// + /// Value to paint with. Hold Ctrl hey to paint with inverse value (1 - value). + /// + public float PaintValue = 0.0f; + + /// + /// Enables continuous painting, otherwise single paint on click. + /// + public bool ContinuousPaint; +#pragma warning restore CS0649 + + public override void Init(IGizmoOwner owner) + { + base.Init(owner); + + Gizmo = new ClothPaintingGizmo(owner, this); + } + + public override void OnActivated() + { + base.OnActivated(); + + Owner.Gizmos.Active = Gizmo; + } + } + + sealed class ClothPaintingGizmo : GizmoBase + { + private Model _brushModel; + private MaterialInstance _brushMaterial; + private ClothPaintingGizmoMode _gizmoMode; + private bool _isPainting; + private int _paintUpdateCount; + private bool _hasHit; + private Vector3 _hitLocation; + private Vector3 _hitNormal; + private Cloth _cloth; + private Float3[] _clothParticles; + private float[] _clothPaint; + private EditClothPaintAction _undoAction; + + public bool IsPainting => _isPainting; + + public ClothPaintingGizmo(IGizmoOwner owner, ClothPaintingGizmoMode mode) + : base(owner) + { + _gizmoMode = mode; + } + + public void SetPaintCloth(Cloth cloth) + { + if (_cloth == cloth) + return; + PaintEnd(); + _cloth = cloth; + _clothParticles = cloth?.GetParticles(); + _clothPaint = null; + _hasHit = false; + } + + public void Fill() + { + PaintEnd(); + PaintStart(); + var clothPaint = _clothPaint; + var paintValue = Mathf.Saturate(_gizmoMode.PaintValue); + for (int i = 0; i < clothPaint.Length; i++) + clothPaint[i] = paintValue; + _cloth.SetPaint(clothPaint); + PaintEnd(); + } + + public void Reset() + { + PaintEnd(); + PaintStart(); + var clothPaint = _clothPaint; + for (int i = 0; i < clothPaint.Length; i++) + clothPaint[i] = 1.0f; + _cloth.SetPaint(clothPaint); + PaintEnd(); + } + + private void PaintStart() + { + if (IsPainting) + return; + + if (Editor.Instance.Undo.Enabled) + _undoAction = new EditClothPaintAction(_cloth); + _isPainting = true; + _paintUpdateCount = 0; + + // Get initial cloth paint state + var clothParticles = _clothParticles; + var clothPaint = _cloth.GetPaint(); + if (clothPaint == null || clothPaint.Length != clothParticles.Length) + { + _clothPaint = clothPaint = new float[clothParticles.Length]; + for (int i = 0; i < clothPaint.Length; i++) + clothPaint[i] = 1.0f; + } + _clothPaint = clothPaint; + } + + private void PaintUpdate() + { + if (!_gizmoMode.ContinuousPaint && _paintUpdateCount > 0) + return; + Profiler.BeginEvent("Cloth Paint"); + + // Edit the cloth paint + var clothParticles = _clothParticles; + var clothPaint = _clothPaint; + if (clothParticles == null || clothPaint == null) + throw new Exception(); + var instanceTransform = _cloth.Transform; + var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize); + var paintValue = Mathf.Saturate(_gizmoMode.PaintValue); + if (Owner.IsControlDown) + paintValue = 1.0f - paintValue; + var modified = false; + for (int i = 0; i < clothParticles.Length; i++) + { + var pos = instanceTransform.LocalToWorld(clothParticles[i]); + var dst = Vector3.Distance(ref pos, ref brushSphere.Center); + if (dst > brushSphere.Radius) + continue; + float strength = _gizmoMode.BrushStrength * Mathf.Lerp(1.0f, 1.0f - (float)dst / (float)brushSphere.Radius, _gizmoMode.BrushFalloff); + if (strength > Mathf.Epsilon) + { + // Paint the particle + ref var paint = ref clothPaint[i]; + paint = Mathf.Saturate(Mathf.Lerp(paint, paintValue, Mathf.Saturate(strength))); + modified = true; + } + } + _paintUpdateCount++; + if (modified) + { + // Update cloth particles state + _cloth.SetPaint(clothPaint); + } + + Profiler.EndEvent(); + } + + private void PaintEnd() + { + if (!IsPainting) + return; + + if (_undoAction != null) + { + _undoAction.RecordEnd(); + Editor.Instance.Undo.AddAction(_undoAction); + _undoAction = null; + } + _isPainting = false; + _paintUpdateCount = 0; + _clothPaint = null; + } + + public override bool IsControllingMouse => IsPainting; + + public override BoundingSphere FocusBounds => _cloth?.Sphere ?? base.FocusBounds; + + public override void Update(float dt) + { + _hasHit = false; + if (!IsActive) + { + SetPaintCloth(null); + return; + } + var cloth = _cloth; + if (cloth == null) + return; + + // Perform detailed tracing to find cursor location for the brush + var ray = Owner.MouseRay; + if (cloth.IntersectsItself(ray, out var closest, out var hitNormal)) + { + // Cursor hit cloth + _hasHit = true; + _hitLocation = ray.GetPoint(closest); + _hitNormal = hitNormal; + } + else + { + // Cursor hit other object or nothing + PaintEnd(); + return; + } + + // Handle painting + if (Owner.IsLeftMouseButtonDown) + PaintStart(); + else + PaintEnd(); + if (IsPainting) + PaintUpdate(); + } + + public override void Pick() + { + var ray = Owner.MouseRay; + var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); + var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives; + var hit = Owner.SceneGraphRoot.RayCast(ref ray, ref view, out _, rayCastFlags); + if (hit != null && hit is ActorNode) + Owner.Select(new List { hit }); + } + + public override void Draw(ref RenderContext renderContext) + { + if (!IsActive || !_cloth) + return; + + base.Draw(ref renderContext); + + // TODO: impl this + if (_hasHit) + { + var viewOrigin = renderContext.View.Origin; + + // Draw paint brush + if (!_brushModel) + _brushModel = Content.LoadAsyncInternal("Editor/Primitives/Sphere"); + if (!_brushMaterial) + _brushMaterial = Content.LoadAsyncInternal(EditorAssets.FoliageBrushMaterial)?.CreateVirtualInstance(); + if (_brushModel && _brushMaterial) + { + _brushMaterial.SetParameterValue("Color", new Color(1.0f, 0.85f, 0.0f)); // TODO: expose to editor options + _brushMaterial.SetParameterValue("DepthBuffer", Owner.RenderTask.Buffers.DepthBuffer); + Quaternion rotation = RootNode.RaycastNormalRotation(ref _hitNormal); + Matrix transform = Matrix.Scaling(_gizmoMode.BrushSize * 0.01f) * Matrix.RotationQuaternion(rotation) * Matrix.Translation(_hitLocation - viewOrigin); + _brushModel.Draw(ref renderContext, _brushMaterial, ref transform); + } + } + } + + public override void OnActivated() + { + base.OnActivated(); + + _hasHit = false; + } + + public override void OnDeactivated() + { + base.OnDeactivated(); + + PaintEnd(); + SetPaintCloth(null); + Object.Destroy(ref _brushMaterial); + _brushModel = null; + } + } + + sealed class EditClothPaintAction : IUndoAction + { + private Guid _actorId; + private string _before, _after; + + public EditClothPaintAction(Cloth cloth) + { + _actorId = cloth.ID; + _before = GetState(cloth); + } + + public static bool IsValidState(string state) + { + return state != null && state.Contains("\"Paint\":"); + } + + public static string GetState(Cloth cloth) + { + var json = cloth.ToJson(); + var start = json.IndexOf("\"Paint\":"); + if (start == -1) + return null; + var end = json.IndexOf('\"', json.IndexOf('\"', start + 8) + 1); + json = "{" + json.Substring(start, end - start) + "\"}"; + return json; + } + + public static void SetState(Cloth cloth, string state) + { + if (state == null) + cloth.SetPaint(null); + else + Editor.Internal_DeserializeSceneObject(Object.GetUnmanagedPtr(cloth), state); + } + + public void RecordEnd() + { + var cloth = Object.Find(ref _actorId); + _after = GetState(cloth); + Editor.Instance.Scene.MarkSceneEdited(cloth.Scene); + } + + private void Set(string state) + { + var cloth = Object.Find(ref _actorId); + SetState(cloth, state); + Editor.Instance.Scene.MarkSceneEdited(cloth.Scene); + } + + public string ActionString => "Edit Cloth Paint"; + + public void Do() + { + Set(_after); + } + + public void Undo() + { + Set(_before); + } + + public void Dispose() + { + _before = _after = null; + } + } +} diff --git a/Source/Editor/Tools/VertexPainting.cs b/Source/Editor/Tools/VertexPainting.cs index bb6b2c716..30bb0c84f 100644 --- a/Source/Editor/Tools/VertexPainting.cs +++ b/Source/Editor/Tools/VertexPainting.cs @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FlaxEditor.CustomEditors; @@ -596,18 +597,12 @@ namespace FlaxEditor.Tools /// public override void Pick() { - // Get mouse ray and try to hit any object var ray = Owner.MouseRay; var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); var rayCastFlags = SceneGraphNode.RayCastData.FlagTypes.SkipColliders | SceneGraphNode.RayCastData.FlagTypes.SkipEditorPrimitives; - var hit = Editor.Instance.Scene.Root.RayCast(ref ray, ref view, out _, rayCastFlags); - - // Update selection - var sceneEditing = Editor.Instance.SceneEditing; + var hit = Owner.SceneGraphRoot.RayCast(ref ray, ref view, out _, rayCastFlags); if (hit != null && hit is ActorNode actorNode && actorNode.Actor is StaticModel model) - { - sceneEditing.Select(hit); - } + Owner.Select(new List { hit }); } /// diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs index 2a99080ed..9a4fd34a2 100644 --- a/Source/Editor/Viewport/EditorGizmoViewport.cs +++ b/Source/Editor/Viewport/EditorGizmoViewport.cs @@ -1,6 +1,8 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System.Collections.Generic; using FlaxEditor.Gizmo; +using FlaxEditor.SceneGraph; using FlaxEditor.Viewport.Cameras; using FlaxEngine; using FlaxEngine.GUI; @@ -12,7 +14,7 @@ namespace FlaxEditor.Viewport /// /// /// - public class EditorGizmoViewport : EditorViewport, IGizmoOwner + public abstract class EditorGizmoViewport : EditorViewport, IGizmoOwner { private UpdateDelegate _update; @@ -79,6 +81,9 @@ namespace FlaxEditor.Viewport /// public SceneGraph.RootNode SceneGraphRoot { get; } + /// + public abstract void Select(List nodes); + /// protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false; diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 77032ff80..9591915ab 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -1150,6 +1150,12 @@ namespace FlaxEditor.Viewport return result; } + /// + public override void Select(List nodes) + { + _editor.SceneEditing.Select(nodes); + } + /// public override void OnDestroy() { diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index e8289d214..99d7cb8e6 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -301,7 +301,7 @@ namespace FlaxEditor.Viewport /// public void ShowSelectedActors() { - var orient = Viewport.ViewOrientation; + var orient = ViewOrientation; ((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orient); } @@ -345,7 +345,10 @@ namespace FlaxEditor.Viewport public RootNode SceneGraphRoot => _window.Graph.Root; /// - public EditorViewport Viewport => this; + public void Select(List nodes) + { + _window.Select(nodes); + } /// protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false; diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index e292a0133..0f1c73e2c 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -2209,14 +2209,14 @@ void VisualScript::GetMethodSignature(int32 index, String& name, byte& flags, St Span VisualScript::GetMetaData(int32 typeID) { auto meta = Graph.Meta.GetEntry(typeID); - return meta ? ToSpan(meta->Data.Get(), meta->Data.Count()) : Span(nullptr, 0); + return meta ? ToSpan(meta->Data) : Span(nullptr, 0); } Span VisualScript::GetMethodMetaData(int32 index, int32 typeID) { auto& method = _methods[index]; auto meta = method.Node->Meta.GetEntry(typeID); - return meta ? ToSpan(meta->Data.Get(), meta->Data.Count()) : Span(nullptr, 0); + return meta ? ToSpan(meta->Data) : Span(nullptr, 0); } #endif diff --git a/Source/Engine/Core/Math/Vector3.cpp b/Source/Engine/Core/Math/Vector3.cpp index d73cbd6d5..1bf03c2ef 100644 --- a/Source/Engine/Core/Math/Vector3.cpp +++ b/Source/Engine/Core/Math/Vector3.cpp @@ -312,7 +312,7 @@ void Float3::FindBestAxisVectors(Float3& firstAxis, Float3& secondAxis) const template<> float Float3::TriangleArea(const Float3& v0, const Float3& v1, const Float3& v2) { - return (v2 - v0 ^ v1 - v0).Length() * 0.5f; + return ((v2 - v0) ^ (v1 - v0)).Length() * 0.5f; } template<> @@ -626,7 +626,7 @@ void Double3::FindBestAxisVectors(Double3& firstAxis, Double3& secondAxis) const template<> double Double3::TriangleArea(const Double3& v0, const Double3& v1, const Double3& v2) { - return (v2 - v0 ^ v1 - v0).Length() * 0.5; + return ((v2 - v0) ^ (v1 - v0)).Length() * 0.5; } template<> diff --git a/Source/Engine/Core/Types/Span.h b/Source/Engine/Core/Types/Span.h index e7bea5fbd..e0518ec77 100644 --- a/Source/Engine/Core/Types/Span.h +++ b/Source/Engine/Core/Types/Span.h @@ -115,6 +115,12 @@ inline Span ToSpan(const T* ptr, int32 length) return Span(ptr, length); } +template +inline Span ToSpan(const Array& data) +{ + return Span((U*)data.Get(), data.Count()); +} + template inline bool SpanContains(const Span span, const T& value) { diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp index a16f5b8af..5bf8d264e 100644 --- a/Source/Engine/Physics/Actors/Cloth.cpp +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -2,6 +2,7 @@ #include "Cloth.h" #include "Engine/Core/Log.h" +#include "Engine/Core/Math/Ray.h" #include "Engine/Graphics/Models/MeshBase.h" #include "Engine/Graphics/Models/MeshDeformation.h" #include "Engine/Physics/PhysicsBackend.h" @@ -109,6 +110,160 @@ void Cloth::ClearInteria() #endif } +Array Cloth::GetParticles() const +{ + Array result; +#if WITH_CLOTH + if (_cloth) + { + PROFILE_CPU(); + PhysicsBackend::LockClothParticles(_cloth); + const Span particles = PhysicsBackend::GetClothParticles(_cloth); + result.Resize(particles.Length()); + const Float4* src = particles.Get(); + Float3* dst = result.Get(); + for (int32 i = 0; i < particles.Length(); i++) + dst[i] = Float3(src[i]); + PhysicsBackend::UnlockClothParticles(_cloth); + } +#endif + return result; +} + +void Cloth::SetParticles(Span value) +{ + PROFILE_CPU(); +#if !BUILD_RELEASE + { + // Sanity check + const Float3* src = value.Get(); + bool allValid = true; + for (int32 i = 0; i < value.Length(); i++) + allValid &= !src[i].IsNanOrInfinity(); + ASSERT(allValid); + } +#endif +#if WITH_CLOTH + if (_cloth) + { + // Update cloth particles + PhysicsBackend::LockClothParticles(_cloth); + PhysicsBackend::SetClothParticles(_cloth, Span(), value, Span()); + PhysicsBackend::UnlockClothParticles(_cloth); + } +#endif +} + +Span Cloth::GetPaint() const +{ + return ToSpan(_paint); +} + +void Cloth::SetPaint(Span value) +{ + PROFILE_CPU(); + if (value.IsInvalid()) + { + // Remove paint when set to empty + _paint.SetCapacity(0); +#if WITH_CLOTH + if (_cloth) + { + PhysicsBackend::SetClothPaint(_cloth, value); + } +#endif + return; + } +#if !BUILD_RELEASE + { + // Sanity check + const float* src = value.Get(); + bool allValid = true; + for (int32 i = 0; i < value.Length(); i++) + allValid &= !isnan(src[i]) && !isinf(src[i]); + ASSERT(allValid); + } +#endif + _paint.Set(value.Get(), value.Length()); +#if WITH_CLOTH + if (_cloth) + { + // Update cloth particles + Array invMasses; + CalculateInvMasses(invMasses); + PhysicsBackend::LockClothParticles(_cloth); + PhysicsBackend::SetClothParticles(_cloth, Span(), Span(), ToSpan(invMasses)); + PhysicsBackend::UnlockClothParticles(_cloth); + PhysicsBackend::SetClothPaint(_cloth, value); + } +#endif +} + +bool Cloth::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) +{ +#if USE_PRECISE_MESH_INTERSECTS + if (!Actor::IntersectsItself(ray, distance, normal)) + return false; +#if WITH_CLOTH + if (_cloth) + { + // Precise per-triangle intersection + const ModelInstanceActor::MeshReference mesh = GetMesh(); + if (mesh.Actor == nullptr) + return false; + BytesContainer indicesData; + int32 indicesCount; + if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + return false; + PhysicsBackend::LockClothParticles(_cloth); + const Span particles = PhysicsBackend::GetClothParticles(_cloth); + const Transform transform = GetTransform(); + const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); + const int32 trianglesCount = indicesCount / 3; + bool result = false; + distance = MAX_Real; + for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + { + const int32 index = triangleIndex * 3; + int32 i0, i1, i2; + if (indices16bit) + { + i0 = indicesData.Get()[index]; + i1 = indicesData.Get()[index + 1]; + i2 = indicesData.Get()[index + 2]; + } + else + { + i0 = indicesData.Get()[index]; + i1 = indicesData.Get()[index + 1]; + i2 = indicesData.Get()[index + 2]; + } + const Vector3 v0 = transform.LocalToWorld(Vector3(particles[i0])); + const Vector3 v1 = transform.LocalToWorld(Vector3(particles[i1])); + const Vector3 v2 = transform.LocalToWorld(Vector3(particles[i2])); + Real d; + if (CollisionsHelper::RayIntersectsTriangle(ray, v0, v1, v2, d) && d < distance) + { + result = true; + normal = Vector3::Normalize((v1 - v0) ^ (v2 - v0)); + distance = d; + + // Flip normal if needed as cloth is two-sided + const Vector3 hitPos = ray.GetPoint(d); + if (Vector3::DistanceSquared(hitPos + normal, ray.Position) > Math::Square(d)) + normal = -normal; + } + } + PhysicsBackend::UnlockClothParticles(_cloth); + return result; + } +#endif + return false; +#else + return Actor::IntersectsItself(ray, distance, normal); +#endif +} + void Cloth::Serialize(SerializeStream& stream, const void* otherObj) { Actor::Serialize(stream, otherObj); @@ -120,6 +275,12 @@ void Cloth::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(Collision, _collisionSettings); SERIALIZE_MEMBER(Simulation, _simulationSettings); SERIALIZE_MEMBER(Fabric, _fabricSettings); + if (Serialization::ShouldSerialize(_paint, other ? &other->_paint : nullptr)) + { + // Serialize as Base64 + stream.JKEY("Paint"); + stream.Blob(_paint.Get(), _paint.Count() * sizeof(float)); + } } void Cloth::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -132,24 +293,29 @@ void Cloth::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) DESERIALIZE_MEMBER(Collision, _collisionSettings); DESERIALIZE_MEMBER(Simulation, _simulationSettings); DESERIALIZE_MEMBER(Fabric, _fabricSettings); + DESERIALIZE_MEMBER(Paint, _paint); + + // Refresh cloth when settings were changed + if (IsDuringPlay()) + Rebuild(); } #if USE_EDITOR void Cloth::DrawPhysicsDebug(RenderView& view) { -#if WITH_CLOTH +#if WITH_CLOTH && COMPILE_WITH_DEBUG_DRAW if (_cloth) { const ModelInstanceActor::MeshReference mesh = GetMesh(); if (mesh.Actor == nullptr) return; BytesContainer indicesData; - int32 indicesCount = 0; + int32 indicesCount; if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) return; PhysicsBackend::LockClothParticles(_cloth); - const Span particles = PhysicsBackend::GetClothCurrentParticles(_cloth); + const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); const int32 trianglesCount = indicesCount / 3; @@ -172,7 +338,6 @@ void Cloth::DrawPhysicsDebug(RenderView& view) const Vector3 v0 = transform.LocalToWorld(Vector3(particles[i0])); const Vector3 v1 = transform.LocalToWorld(Vector3(particles[i1])); const Vector3 v2 = transform.LocalToWorld(Vector3(particles[i2])); - // TODO: highlight immovable cloth particles with a different color DEBUG_DRAW_TRIANGLE(v0, v1, v2, Color::Pink, 0, true); } PhysicsBackend::UnlockClothParticles(_cloth); @@ -182,7 +347,7 @@ void Cloth::DrawPhysicsDebug(RenderView& view) void Cloth::OnDebugDrawSelected() { -#if WITH_CLOTH +#if WITH_CLOTH && COMPILE_WITH_DEBUG_DRAW if (_cloth) { DEBUG_DRAW_WIRE_BOX(_box, Color::Violet.RGBMultiplied(0.8f), 0, true); @@ -190,11 +355,11 @@ void Cloth::OnDebugDrawSelected() if (mesh.Actor == nullptr) return; BytesContainer indicesData; - int32 indicesCount = 0; + int32 indicesCount; if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) return; PhysicsBackend::LockClothParticles(_cloth); - const Span particles = PhysicsBackend::GetClothCurrentParticles(_cloth); + const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); const int32 trianglesCount = indicesCount / 3; @@ -217,10 +382,16 @@ void Cloth::OnDebugDrawSelected() const Vector3 v0 = transform.LocalToWorld(Vector3(particles[i0])); const Vector3 v1 = transform.LocalToWorld(Vector3(particles[i1])); const Vector3 v2 = transform.LocalToWorld(Vector3(particles[i2])); - // TODO: highlight immovable cloth particles with a different color - DEBUG_DRAW_LINE(v0, v1, Color::White, 0, false); - DEBUG_DRAW_LINE(v1, v2, Color::White, 0, false); - DEBUG_DRAW_LINE(v2, v0, Color::White, 0, false); + Color c0 = Color::White, c1 = Color::White, c2 = Color::White; + if (_paint.Count() == particles.Length()) + { + c0 = Color::Lerp(Color::Red, Color::White, _paint[i0]); + c1 = Color::Lerp(Color::Red, Color::White, _paint[i1]); + c2 = Color::Lerp(Color::Red, Color::White, _paint[i2]); + } + DebugDraw::DrawLine(v0, v1, c0, c1, 0, false); + DebugDraw::DrawLine(v1, v2, c1, c2, 0, false); + DebugDraw::DrawLine(v2, v0, c2, c0, 0, false); } PhysicsBackend::UnlockClothParticles(_cloth); } @@ -233,10 +404,11 @@ void Cloth::OnDebugDrawSelected() void Cloth::BeginPlay(SceneBeginData* data) { +#if WITH_CLOTH if (CreateCloth()) - { LOG(Error, "Failed to create cloth '{0}'", GetNamePath()); - } + +#endif Actor::BeginPlay(data); } @@ -245,10 +417,10 @@ void Cloth::EndPlay() { Actor::EndPlay(); +#if WITH_CLOTH if (_cloth) - { DestroyCloth(); - } +#endif } void Cloth::OnEnable() @@ -258,9 +430,7 @@ void Cloth::OnEnable() #endif #if WITH_CLOTH if (_cloth) - { PhysicsBackend::AddCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); - } #endif Actor::OnEnable(); @@ -272,9 +442,7 @@ void Cloth::OnDisable() #if WITH_CLOTH if (_cloth) - { PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); - } #endif #if USE_EDITOR GetSceneRendering()->RemovePhysicsDebug(this); @@ -347,6 +515,12 @@ bool Cloth::CreateCloth() desc.IndicesData = data.Get(); desc.IndicesCount = count; desc.IndicesStride = data.Length() / count; + Array invMasses; + CalculateInvMasses(invMasses); + desc.InvMassesData = invMasses.Count() == desc.VerticesCount ? invMasses.Get() : nullptr; + desc.InvMassesStride = sizeof(float); + desc.MaxDistancesData = _paint.Count() == desc.VerticesCount ? _paint.Get() : nullptr; + desc.MaxDistancesStride = sizeof(float); // Create cloth ASSERT(_cloth == nullptr); @@ -389,6 +563,95 @@ void Cloth::DestroyCloth() #endif } +void Cloth::CalculateInvMasses(Array& invMasses) +{ + // Use per-particle max distance to evaluate which particles are immovable +#if WITH_CLOTH + if (_paint.IsEmpty()) + return; + + // Get mesh data + const ModelInstanceActor::MeshReference mesh = GetMesh(); + if (mesh.Actor == nullptr) + return; + BytesContainer verticesData; + int32 verticesCount; + if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, verticesData, verticesCount)) + return; + BytesContainer indicesData; + int32 indicesCount; + if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + return; + const int32 verticesStride = verticesData.Length() / verticesCount; + const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); + const int32 trianglesCount = indicesCount / 3; + + // Sum triangle area for each influenced particle + invMasses.Resize(verticesCount); + for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + { + const int32 index = triangleIndex * 3; + int32 i0, i1, i2; + if (indices16bit) + { + i0 = indicesData.Get()[index]; + i1 = indicesData.Get()[index + 1]; + i2 = indicesData.Get()[index + 2]; + } + else + { + i0 = indicesData.Get()[index]; + i1 = indicesData.Get()[index + 1]; + i2 = indicesData.Get()[index + 2]; + } +#define GET_POS(i) *(Float3*)((byte*)verticesData.Get() + i * verticesStride) + const Float3 v0(GET_POS(i0)); + const Float3 v1(GET_POS(i1)); + const Float3 v2(GET_POS(i2)); +#undef GET_POS + const float area = Float3::TriangleArea(v0, v1, v2); + invMasses.Get()[i0] += area; + invMasses.Get()[i1] += area; + invMasses.Get()[i2] += area; + } + + // Count fixed vertices which max movement distance is zero + int32 fixedCount = 0; + float massSum = 0; + for (int32 i = 0; i < verticesCount; i++) + { + float& mass = invMasses[i]; + const float maxDistance = _paint[i]; + if (maxDistance < 0.01f) + { + // Fixed + fixedCount++; + mass = 0.0f; + } + else + { + // Kinetic so include it's mass contribution + massSum += mass; + } + } + + if (massSum > ZeroTolerance) + { + // Normalize and inverse particles mass + const float massScale = (float)(verticesCount - fixedCount) / massSum; + for (int32 i = 0; i < verticesCount; i++) + { + float& mass = invMasses[i]; + if (mass > 0.0f) + { + mass *= massScale; + mass = 1.0f / mass; + } + } + } +#endif +} + void Cloth::OnUpdated() { if (_meshDeformation) @@ -410,7 +673,7 @@ void Cloth::RunClothDeformer(const MeshBase* mesh, MeshDeformationData& deformat #if WITH_CLOTH PROFILE_CPU_NAMED("Cloth"); PhysicsBackend::LockClothParticles(_cloth); - const Span particles = PhysicsBackend::GetClothCurrentParticles(_cloth); + const Span particles = PhysicsBackend::GetClothParticles(_cloth); // Update mesh vertices based on the cloth particles positions auto vbData = deformation.VertexBuffer.Data.Get(); @@ -478,7 +741,7 @@ void Cloth::RunClothDeformer(const MeshBase* mesh, MeshDeformationData& deformat { for (uint32 i = 0; i < vbCount; i++) { - *((Float3*)vbData) = *(Float3*)&particles.Get()[i]; + *(Float3*)vbData = *(Float3*)&particles.Get()[i]; vbData += vbStride; } } diff --git a/Source/Engine/Physics/Actors/Cloth.h b/Source/Engine/Physics/Actors/Cloth.h index dcc6544a8..ac87bf5ee 100644 --- a/Source/Engine/Physics/Actors/Cloth.h +++ b/Source/Engine/Physics/Actors/Cloth.h @@ -129,6 +129,11 @@ API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Cloth\"), ActorToolbox(\"Ph /// API_FIELD() float SolverFrequency = 300.0f; + /// + /// The maximum distance cloth particles can move from the original location (within local-space of the actor). Scaled by painted per-particle value (0-1) to restrict movement of certain particles. + /// + API_FIELD() float MaxDistance = 1000.0f; + /// /// Wind velocity vector (direction and magnitude) in world coordinates. A greater magnitude applies a stronger wind force. Ensure that Air Drag and Air Lift coefficients are non-zero in order to apply wind force. /// @@ -202,6 +207,7 @@ private: Vector3 _cachedPosition = Vector3::Zero; ModelInstanceActor::MeshReference _mesh; MeshDeformation* _meshDeformation = nullptr; + Array _paint; public: /// @@ -283,8 +289,29 @@ public: /// API_FUNCTION() void ClearInteria(); + /// + /// Gets the cloth particles data with per-particle XYZ position (in local cloth-space). + /// + API_FUNCTION() Array GetParticles() const; + + /// + /// Sets the cloth particles data with per-particle XYZ position (in local cloth-space). The size of the input data had to match the cloth size. + /// + API_FUNCTION() void SetParticles(Span value); + + /// + /// Gets the cloth particles paint data with per-particle max distance (normalized 0-1, 0 makes particle immovable). Returned value is empty if cloth was not initialized or doesn't use paint feature. + /// + API_FUNCTION() Span GetPaint() const; + + /// + /// Sets the cloth particles paint data with per-particle max distance (normalized 0-1, 0 makes particle immovable). The size of the input data had to match the cloth size. Set to empty to remove paint. + /// + API_FUNCTION() void SetPaint(Span value); + public: // [Actor] + bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override; void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; @@ -307,6 +334,7 @@ private: #endif bool CreateCloth(); void DestroyCloth(); + void CalculateInvMasses(Array& invMasses); void OnUpdated(); void RunClothDeformer(const MeshBase* mesh, struct MeshDeformationData& deformation); }; diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 57ef4ce17..da393f529 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -129,7 +129,7 @@ public: } }; -class ProfilerPhysX : public physx::PxProfilerCallback +class ProfilerPhysX : public PxProfilerCallback { public: void* zoneStart(const char* eventName, bool detached, uint64_t contextId) override @@ -162,6 +162,7 @@ struct ClothSettings const PxVec3& clothBoundsSize = clothPhysX->getBoundingBoxScale(); BoundingBox localBounds; BoundingBox::FromPoints(P2C(clothBoundsPos - clothBoundsSize), P2C(clothBoundsPos + clothBoundsSize), localBounds); + CHECK(!localBounds.Minimum.IsNanOrInfinity() && !localBounds.Maximum.IsNanOrInfinity()); // Transform local-space bounds into world-space const PxTransform clothPose(clothPhysX->getTranslation(), clothPhysX->getRotation()); @@ -3340,12 +3341,14 @@ void* PhysicsBackend::CreateCloth(const PhysicsClothDesc& desc) meshDesc.points.data = desc.VerticesData; meshDesc.points.stride = desc.VerticesStride; meshDesc.points.count = desc.VerticesCount; + meshDesc.invMasses.data = desc.InvMassesData; + meshDesc.invMasses.stride = desc.InvMassesStride; + meshDesc.invMasses.count = desc.InvMassesData ? desc.VerticesCount : 0; meshDesc.triangles.data = desc.IndicesData; meshDesc.triangles.stride = desc.IndicesStride * 3; meshDesc.triangles.count = desc.IndicesCount / 3; if (desc.IndicesStride == sizeof(uint16)) meshDesc.flags |= nv::cloth::MeshFlag::e16_BIT_INDICES; - // TODO: provide invMasses data const Float3 gravity(PhysicsSettings::Get()->DefaultGravity); nv::cloth::Vector::Type phaseTypeInfo; // TODO: automatically reuse fabric from existing cloths (simply check for input data used for computations to improve perf when duplicating cloths or with prefab) @@ -3359,10 +3362,17 @@ void* PhysicsBackend::CreateCloth(const PhysicsClothDesc& desc) // Create cloth object static_assert(sizeof(Float4) == sizeof(PxVec4), "Size mismatch"); Array initialState; - // TODO: provide initial state for cloth from the owner (eg. current skinned mesh position) initialState.Resize((int32)desc.VerticesCount); - for (uint32 i = 0; i < desc.VerticesCount; i++) - initialState.Get()[i] = Float4(*(Float3*)((byte*)desc.VerticesData + i * desc.VerticesStride), 1.0f); // TODO: set .w to invMass of that vertex + if (desc.InvMassesData) + { + for (uint32 i = 0; i < desc.VerticesCount; i++) + initialState.Get()[i] = Float4(*(Float3*)((byte*)desc.VerticesData + i * desc.VerticesStride), *(float*)((byte*)desc.InvMassesData + i * desc.InvMassesStride)); + } + else + { + for (uint32 i = 0; i < desc.VerticesCount; i++) + initialState.Get()[i] = Float4(*(Float3*)((byte*)desc.VerticesData + i * desc.VerticesStride), 1.0f); + } const nv::cloth::Range initialParticlesRange((PxVec4*)initialState.Get(), (PxVec4*)initialState.Get() + initialState.Count()); nv::cloth::Cloth* clothPhysX = ClothFactory->createCloth(initialParticlesRange, *fabric); fabric->decRefCount(); @@ -3371,6 +3381,14 @@ void* PhysicsBackend::CreateCloth(const PhysicsClothDesc& desc) LOG(Error, "createCloth failed"); return nullptr; } + if (desc.MaxDistancesData) + { + nv::cloth::Range motionConstraints = clothPhysX->getMotionConstraints(); + ASSERT(motionConstraints.size() == desc.VerticesCount); + for (uint32 i = 0; i < desc.VerticesCount; i++) + motionConstraints.begin()[i] = PxVec4(*(PxVec3*)((byte*)desc.VerticesData + i * desc.VerticesStride), *(float*)((byte*)desc.MaxDistancesData + i * desc.MaxDistancesStride)); + } + clothPhysX->setUserData(desc.Actor); // Setup settings FabricSettings fabricSettings; @@ -3438,6 +3456,7 @@ void PhysicsBackend::SetClothSimulationSettings(void* cloth, const void* setting auto clothPhysX = (nv::cloth::Cloth*)cloth; const auto& settings = *(const Cloth::SimulationSettings*)settingsPtr; clothPhysX->setSolverFrequency(settings.SolverFrequency); + clothPhysX->setMotionConstraintScaleBias(settings.MaxDistance, 0.0f); clothPhysX->setWindVelocity(C2P(settings.WindVelocity)); } @@ -3510,13 +3529,65 @@ void PhysicsBackend::UnlockClothParticles(void* cloth) clothPhysX->unlockParticles(); } -Span PhysicsBackend::GetClothCurrentParticles(void* cloth) +Span PhysicsBackend::GetClothParticles(void* cloth) { auto clothPhysX = (const nv::cloth::Cloth*)cloth; const nv::cloth::MappedRange range = clothPhysX->getCurrentParticles(); return Span((const Float4*)range.begin(), (int32)range.size()); } +void PhysicsBackend::SetClothParticles(void* cloth, Span value, Span positions, Span invMasses) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + nv::cloth::MappedRange range = clothPhysX->getCurrentParticles(); + const uint32_t size = range.size(); + PxVec4* dst = range.begin(); + if (value.IsValid()) + { + // Set XYZW + CHECK((uint32_t)value.Length() >= size); + Platform::MemoryCopy(dst, value.Get(), size * sizeof(Float4)); + } + if (positions.IsValid()) + { + // Set XYZ + CHECK((uint32_t)positions.Length() >= size); + const Float3* src = positions.Get(); + for (uint32 i = 0; i < size; i++) + dst[i] = PxVec4(C2P(src[i]), dst[i].w); + } + if (invMasses.IsValid()) + { + // Set W + CHECK((uint32_t)invMasses.Length() >= size); + const float* src = invMasses.Get(); + for (uint32 i = 0; i < size; i++) + dst[i].w = src[i]; + + // Apply previous particles too + nv::cloth::MappedRange range2 = clothPhysX->getPreviousParticles(); + for (uint32 i = 0; i < size; i++) + range2.begin()[i].w = src[i]; + } +} + +void PhysicsBackend::SetClothPaint(void* cloth, Span value) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + if (value.IsValid()) + { + const nv::cloth::MappedRange range = ((const nv::cloth::Cloth*)clothPhysX)->getCurrentParticles(); + nv::cloth::Range motionConstraints = clothPhysX->getMotionConstraints(); + ASSERT(motionConstraints.size() <= (uint32)value.Length()); + for (int32 i = 0; i < value.Length(); i++) + motionConstraints.begin()[i] = PxVec4(range[i].getXYZ(), value[i]); + } + else + { + clothPhysX->clearMotionConstraints(); + } +} + void PhysicsBackend::AddCloth(void* scene, void* cloth) { auto scenePhysX = (ScenePhysX*)scene; diff --git a/Source/Engine/Physics/PhysicsBackend.h b/Source/Engine/Physics/PhysicsBackend.h index 5e8743d69..2de719183 100644 --- a/Source/Engine/Physics/PhysicsBackend.h +++ b/Source/Engine/Physics/PhysicsBackend.h @@ -40,10 +40,14 @@ struct PhysicsClothDesc class Cloth* Actor; void* VerticesData; void* IndicesData; + float* InvMassesData; + float* MaxDistancesData; uint32 VerticesCount; uint32 VerticesStride; uint32 IndicesCount; uint32 IndicesStride; + uint32 InvMassesStride; + uint32 MaxDistancesStride; }; /// @@ -281,7 +285,9 @@ public: static void ClearClothInertia(void* cloth); static void LockClothParticles(void* cloth); static void UnlockClothParticles(void* cloth); - static Span GetClothCurrentParticles(void* cloth); + static Span GetClothParticles(void* cloth); + static void SetClothParticles(void* cloth, Span value, Span positions, Span invMasses); + static void SetClothPaint(void* cloth, Span value); static void AddCloth(void* scene, void* cloth); static void RemoveCloth(void* scene, void* cloth); #endif