diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 728758736..e5c4c5a09 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -690,12 +690,12 @@ void AnimatedModel::OnActiveInTreeChanged() void AnimatedModel::UpdateBounds() { - auto model = SkinnedModel.Get(); + const auto model = SkinnedModel.Get(); if (CustomBounds.GetSize().LengthSquared() > 0.01f) { BoundingBox::Transform(CustomBounds, _transform, _box); } - else if (model && model->IsLoaded()) + else if (model && model->IsLoaded() && model->LODs.Count() != 0) { BoundingBox box = model->LODs[0].GetBox(_transform, _deformation); if (GraphInstance.NodesPose.Count() != 0) diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index 17d5ca805..3ab88cdce 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -270,8 +270,8 @@ void StaticModel::OnModelResidencyChanged() void StaticModel::UpdateBounds() { - auto model = Model.Get(); - if (model && model->IsLoaded()) + const auto model = Model.Get(); + if (model && model->IsLoaded() && model->LODs.Count() != 0) { Transform transform = _transform; transform.Scale *= _boundsScale; diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp new file mode 100644 index 000000000..a16f5b8af --- /dev/null +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -0,0 +1,492 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#include "Cloth.h" +#include "Engine/Core/Log.h" +#include "Engine/Graphics/Models/MeshBase.h" +#include "Engine/Graphics/Models/MeshDeformation.h" +#include "Engine/Physics/PhysicsBackend.h" +#include "Engine/Physics/PhysicsScene.h" +#include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Serialization/Serialization.h" +#include "Engine/Level/Actors/AnimatedModel.h" +#if USE_EDITOR +#include "Engine/Level/Scene/SceneRendering.h" +#include "Engine/Debug/DebugDraw.h" +#endif + +Cloth::Cloth(const SpawnParams& params) + : Actor(params) +{ + // Use the first mesh by default + _mesh.LODIndex = _mesh.MeshIndex = 0; +} + +ModelInstanceActor::MeshReference Cloth::GetMesh() const +{ + auto value = _mesh; + value.Actor = Cast(GetParent()); // Force to use cloth's parent only + return value; +} + +void Cloth::SetMesh(const ModelInstanceActor::MeshReference& value) +{ + if (_mesh.LODIndex == value.LODIndex && _mesh.MeshIndex == value.MeshIndex) + return; + + // Remove mesh deformer (mesh index/lod changes) + if (_meshDeformation) + { + Function deformer; + deformer.Bind(this); + _meshDeformation->RemoveDeformer(_mesh.LODIndex, _mesh.MeshIndex, MeshBufferType::Vertex0, deformer); + _meshDeformation = nullptr; + } + + _mesh = value; + _mesh.Actor = nullptr; // Don't store this reference + Rebuild(); +} + +void Cloth::SetForce(const ForceSettings& value) +{ + _forceSettings = value; +#if WITH_CLOTH + if (_cloth) + PhysicsBackend::SetClothForceSettings(_cloth, &value); +#endif +} + +void Cloth::SetCollision(const CollisionSettings& value) +{ + _collisionSettings = value; +#if WITH_CLOTH + if (_cloth) + PhysicsBackend::SetClothCollisionSettings(_cloth, &value); +#endif +} + +void Cloth::SetSimulation(const SimulationSettings& value) +{ + _simulationSettings = value; +#if WITH_CLOTH + if (_cloth) + PhysicsBackend::SetClothSimulationSettings(_cloth, &value); +#endif +} + +void Cloth::SetFabric(const FabricSettings& value) +{ + _fabricSettings = value; +#if WITH_CLOTH + if (_cloth) + PhysicsBackend::SetClothFabricSettings(_cloth, &value); +#endif +} + +void Cloth::Rebuild() +{ +#if WITH_CLOTH + if (_cloth) + { + // Remove old + if (IsDuringPlay()) + PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); + DestroyCloth(); + + // Create new + CreateCloth(); + if (IsDuringPlay()) + PhysicsBackend::AddCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); + } +#endif +} + +void Cloth::ClearInteria() +{ +#if WITH_CLOTH + if (_cloth) + PhysicsBackend::ClearClothInertia(_cloth); +#endif +} + +void Cloth::Serialize(SerializeStream& stream, const void* otherObj) +{ + Actor::Serialize(stream, otherObj); + + SERIALIZE_GET_OTHER_OBJ(Cloth); + + SERIALIZE_MEMBER(Mesh, _mesh); + SERIALIZE_MEMBER(Force, _forceSettings); + SERIALIZE_MEMBER(Collision, _collisionSettings); + SERIALIZE_MEMBER(Simulation, _simulationSettings); + SERIALIZE_MEMBER(Fabric, _fabricSettings); +} + +void Cloth::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) +{ + Actor::Deserialize(stream, modifier); + + DESERIALIZE_MEMBER(Mesh, _mesh); + _mesh.Actor = nullptr; // Don't store this reference + DESERIALIZE_MEMBER(Force, _forceSettings); + DESERIALIZE_MEMBER(Collision, _collisionSettings); + DESERIALIZE_MEMBER(Simulation, _simulationSettings); + DESERIALIZE_MEMBER(Fabric, _fabricSettings); +} + +#if USE_EDITOR + +void Cloth::DrawPhysicsDebug(RenderView& view) +{ +#if WITH_CLOTH + if (_cloth) + { + const ModelInstanceActor::MeshReference mesh = GetMesh(); + if (mesh.Actor == nullptr) + return; + BytesContainer indicesData; + int32 indicesCount = 0; + if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + return; + PhysicsBackend::LockClothParticles(_cloth); + const Span particles = PhysicsBackend::GetClothCurrentParticles(_cloth); + const Transform transform = GetTransform(); + const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); + const int32 trianglesCount = indicesCount / 3; + 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])); + // TODO: highlight immovable cloth particles with a different color + DEBUG_DRAW_TRIANGLE(v0, v1, v2, Color::Pink, 0, true); + } + PhysicsBackend::UnlockClothParticles(_cloth); + } +#endif +} + +void Cloth::OnDebugDrawSelected() +{ +#if WITH_CLOTH + if (_cloth) + { + DEBUG_DRAW_WIRE_BOX(_box, Color::Violet.RGBMultiplied(0.8f), 0, true); + const ModelInstanceActor::MeshReference mesh = GetMesh(); + if (mesh.Actor == nullptr) + return; + BytesContainer indicesData; + int32 indicesCount = 0; + if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + return; + PhysicsBackend::LockClothParticles(_cloth); + const Span particles = PhysicsBackend::GetClothCurrentParticles(_cloth); + const Transform transform = GetTransform(); + const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); + const int32 trianglesCount = indicesCount / 3; + 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])); + // 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); + } + PhysicsBackend::UnlockClothParticles(_cloth); + } +#endif + + Actor::OnDebugDrawSelected(); +} + +#endif + +void Cloth::BeginPlay(SceneBeginData* data) +{ + if (CreateCloth()) + { + LOG(Error, "Failed to create cloth '{0}'", GetNamePath()); + } + + Actor::BeginPlay(data); +} + +void Cloth::EndPlay() +{ + Actor::EndPlay(); + + if (_cloth) + { + DestroyCloth(); + } +} + +void Cloth::OnEnable() +{ +#if USE_EDITOR + GetSceneRendering()->AddPhysicsDebug(this); +#endif +#if WITH_CLOTH + if (_cloth) + { + PhysicsBackend::AddCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); + } +#endif + + Actor::OnEnable(); +} + +void Cloth::OnDisable() +{ + Actor::OnDisable(); + +#if WITH_CLOTH + if (_cloth) + { + PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); + } +#endif +#if USE_EDITOR + GetSceneRendering()->RemovePhysicsDebug(this); +#endif +} + +void Cloth::OnParentChanged() +{ + Actor::OnParentChanged(); + + Rebuild(); +} + +void Cloth::OnTransformChanged() +{ + Actor::OnTransformChanged(); + +#if WITH_CLOTH + if (_cloth) + { + // Move cloth but consider this as teleport if the position delta is significant + const float minTeleportDistanceSq = Math::Square(1000.0f); + const bool teleport = Vector3::DistanceSquared(_cachedPosition, _transform.Translation) >= minTeleportDistanceSq; + _cachedPosition = _transform.Translation; + PhysicsBackend::SetClothTransform(_cloth, _transform, teleport); + } + else +#endif + { + _box = BoundingBox(_transform.Translation); + _sphere = BoundingSphere(_transform.Translation, 0.0f); + } +} + +void Cloth::OnPhysicsSceneChanged(PhysicsScene* previous) +{ + Actor::OnPhysicsSceneChanged(previous); + +#if WITH_CLOTH + if (_cloth) + { + PhysicsBackend::RemoveCloth(previous->GetPhysicsScene(), _cloth); + void* scene = GetPhysicsScene()->GetPhysicsScene(); + PhysicsBackend::AddCloth(scene, _cloth); + } +#endif +} + +bool Cloth::CreateCloth() +{ +#if WITH_CLOTH + PROFILE_CPU(); + + // Get mesh data + // TODO: consider making it via async task so physics can wait on the cloth setup from mesh data just before next fixed update which gives more time when loading scene + const ModelInstanceActor::MeshReference mesh = GetMesh(); + if (mesh.Actor == nullptr) + return false; + PhysicsClothDesc desc; + desc.Actor = this; + BytesContainer data; + int32 count; + if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, data, count)) + return true; + desc.VerticesData = data.Get(); + desc.VerticesCount = count; + desc.VerticesStride = data.Length() / count; + if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, data, count)) + return true; + desc.IndicesData = data.Get(); + desc.IndicesCount = count; + desc.IndicesStride = data.Length() / count; + + // Create cloth + ASSERT(_cloth == nullptr); + _cloth = PhysicsBackend::CreateCloth(desc); + if (_cloth == nullptr) + return true; + _cachedPosition = _transform.Translation; + PhysicsBackend::SetClothForceSettings(_cloth, &_forceSettings); + PhysicsBackend::SetClothCollisionSettings(_cloth, &_collisionSettings); + PhysicsBackend::SetClothSimulationSettings(_cloth, &_simulationSettings); + PhysicsBackend::SetClothFabricSettings(_cloth, &_fabricSettings); + PhysicsBackend::SetClothTransform(_cloth, _transform, true); + PhysicsBackend::ClearClothInertia(_cloth); + + // Add cloth mesh deformer + if (auto* deformation = mesh.Actor->GetMeshDeformation()) + { + Function deformer; + deformer.Bind(this); + deformation->AddDeformer(mesh.LODIndex, mesh.MeshIndex, MeshBufferType::Vertex0, deformer); + _meshDeformation = deformation; + } +#endif + + return false; +} + +void Cloth::DestroyCloth() +{ +#if WITH_CLOTH + if (_meshDeformation) + { + Function deformer; + deformer.Bind(this); + _meshDeformation->RemoveDeformer(_mesh.LODIndex, _mesh.MeshIndex, MeshBufferType::Vertex0, deformer); + _meshDeformation = nullptr; + } + PhysicsBackend::DestroyCloth(_cloth); + _cloth = nullptr; +#endif +} + +void Cloth::OnUpdated() +{ + if (_meshDeformation) + { + // Mark mesh as dirty + const Matrix invWorld = Matrix::Invert(_transform.GetWorld()); + BoundingBox localBounds; + BoundingBox::Transform(_box, invWorld, localBounds); + _meshDeformation->Dirty(_mesh.LODIndex, _mesh.MeshIndex, MeshBufferType::Vertex0, localBounds); + + // Update bounds (for mesh culling) + auto* actor = (ModelInstanceActor*)GetParent(); + actor->UpdateBounds(); + } +} + +void Cloth::RunClothDeformer(const MeshBase* mesh, MeshDeformationData& deformation) +{ +#if WITH_CLOTH + PROFILE_CPU_NAMED("Cloth"); + PhysicsBackend::LockClothParticles(_cloth); + const Span particles = PhysicsBackend::GetClothCurrentParticles(_cloth); + + // Update mesh vertices based on the cloth particles positions + auto vbData = deformation.VertexBuffer.Data.Get(); + auto vbCount = (uint32)mesh->GetVertexCount(); + auto vbStride = (uint32)deformation.VertexBuffer.Data.Count() / vbCount; + // TODO: add support for mesh vertex data layout descriptor instead hardcoded position data at the beginning of VB0 + ASSERT((uint32)particles.Length() >= vbCount); + if (auto* animatedModel = Cast(GetParent())) + { + if (animatedModel->GraphInstance.NodesPose.IsEmpty()) + { + // Delay unit skinning data is ready + PhysicsBackend::UnlockClothParticles(_cloth); + _meshDeformation->Dirty(_mesh.LODIndex, _mesh.MeshIndex, MeshBufferType::Vertex0); + return; + } + + // TODO: optimize memory allocs (eg. get pose as Span for readonly) + Array pose; + animatedModel->GetCurrentPose(pose); + const SkeletonData& skeleton = animatedModel->SkinnedModel->Skeleton; + + // Animated model uses skinning thus requires to set vertex position inverse to skeleton bones + ASSERT(vbStride == sizeof(VB0SkinnedElementType)); + for (uint32 i = 0; i < vbCount; i++) + { + VB0SkinnedElementType& vb0 = *(VB0SkinnedElementType*)vbData; + + // Calculate skinned vertex matrix from bones blending + const Float4 blendWeights = vb0.BlendWeights.ToFloat4(); + // TODO: optimize this or use _skinningData from AnimatedModel to access current mesh bones data directly + Matrix matrix; + const SkeletonBone& bone0 = skeleton.Bones[vb0.BlendIndices.R]; + Matrix::Multiply(bone0.OffsetMatrix, pose[bone0.NodeIndex], matrix); + Matrix boneMatrix = matrix * blendWeights.X; + if (blendWeights.Y > 0.0f) + { + const SkeletonBone& bone1 = skeleton.Bones[vb0.BlendIndices.G]; + Matrix::Multiply(bone1.OffsetMatrix, pose[bone1.NodeIndex], matrix); + boneMatrix += matrix * blendWeights.Y; + } + if (blendWeights.Z > 0.0f) + { + const SkeletonBone& bone2 = skeleton.Bones[vb0.BlendIndices.B]; + Matrix::Multiply(bone2.OffsetMatrix, pose[bone2.NodeIndex], matrix); + boneMatrix += matrix * blendWeights.Z; + } + if (blendWeights.W > 0.0f) + { + const SkeletonBone& bone3 = skeleton.Bones[vb0.BlendIndices.A]; + Matrix::Multiply(bone3.OffsetMatrix, pose[bone3.NodeIndex], matrix); + boneMatrix += matrix * blendWeights.W; + } + + // Set vertex position so it will match cloth particle pos after skinning with bone matrix + Matrix boneMatrixInv; + Matrix::Invert(boneMatrix, boneMatrixInv); + Float3 pos = *(Float3*)&particles.Get()[i]; + vb0.Position = Float3::Transform(pos, boneMatrixInv); + + vbData += vbStride; + } + } + else + { + for (uint32 i = 0; i < vbCount; i++) + { + *((Float3*)vbData) = *(Float3*)&particles.Get()[i]; + vbData += vbStride; + } + } + + // Mark whole mesh as modified + deformation.DirtyMinIndex = 0; + deformation.DirtyMaxIndex = vbCount; + + PhysicsBackend::UnlockClothParticles(_cloth); +#endif +} diff --git a/Source/Engine/Physics/Actors/Cloth.h b/Source/Engine/Physics/Actors/Cloth.h new file mode 100644 index 000000000..588a4f4fd --- /dev/null +++ b/Source/Engine/Physics/Actors/Cloth.h @@ -0,0 +1,307 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Level/Actor.h" +#include "Engine/Level/Actors/ModelInstanceActor.h" + +/// +/// Physical simulation actor for cloth objects made of vertices that are simulated as cloth particles with physical properties, forces, and constraints to affect cloth behavior. +/// +API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Cloth\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API Cloth : public Actor +{ + friend class PhysicsBackend; + DECLARE_SCENE_OBJECT(Cloth); + + /// + /// Cloth response to forces settings. + /// + API_STRUCT() struct ForceSettings : ISerializable + { + DECLARE_SCRIPTING_TYPE_MINIMAL(ForceSettings); + API_AUTO_SERIALIZATION(); + + /// + /// Scale multiplier applied to the gravity of cloth particles (scales the global gravity force). + /// + API_FIELD() float GravityScale = 1.0f; + + /// + /// Damping of cloth particle velocity. 0: velocity is unaffected. 1: velocity is zeroed. + /// + API_FIELD(Attributes="Limit(0, 1)") float Damping = 0.4f; + + /// + /// Portion of velocity applied to cloth particles. 0: cloth particles are unaffected. 1: damped global cloth particle velocity. + /// + API_FIELD(Attributes="Limit(0, 1)") float LinearDrag = 0.2f; + + /// + /// Portion of angular velocity applied to turning cloth particles. 0: cloth particles are unaffected. 1: damped global cloth particle angular velocity. + /// + API_FIELD(Attributes="Limit(0, 1)") float AngularDrag = 0.2f; + + /// + /// Portion of linear acceleration applied to cloth particles. 0: cloth particles are unaffected. 1: physically correct linear acceleration. + /// + API_FIELD(Attributes="Limit(0, 1)") float LinearInertia = 1.0f; + + /// + /// Portion of angular acceleration applied to turning cloth particles. 0: cloth particles are unaffected. 1: physically correct angular acceleration. + /// + API_FIELD(Attributes="Limit(0, 1)") float AngularInertia = 1.0f; + + /// + /// Portion of angular velocity applied to turning cloth particles. 0: cloth particles are unaffected. 1: physically correct angular velocity. + /// + API_FIELD(Attributes="Limit(0, 1)") float CentrifugalInertia = 1.0f; + + /// + /// Defines how much drag air applies to the cloth particles. Set to 0 to disable wind. + /// + API_FIELD(Attributes="Limit(0, 1)") float AirDragCoefficient = 0.0f; + + /// + /// Defines how much lift air applies to the cloth particles. Set to 0 to disable wind. + /// + API_FIELD(Attributes="Limit(0, 1)") float AirLiftCoefficient = 0.0f; + + /// + /// Defines fluid density of air used for drag and lift calculations. + /// + API_FIELD(Attributes="Limit(0)") float AirDensity = 1.0f; + }; + + /// + /// Cloth response to collisions settings. + /// + API_STRUCT() struct CollisionSettings : ISerializable + { + DECLARE_SCRIPTING_TYPE_MINIMAL(CollisionSettings); + API_AUTO_SERIALIZATION(); + + /// + /// Controls the amount of friction between cloth particles and colliders. 0: friction disabled. + /// + API_FIELD(Attributes="Limit(0)") float Friction = 0.1f; + + /// + /// Controls how quickly cloth particle mass is increased during collisions. 0: mass scale disabled. + /// + API_FIELD(Attributes="Limit(0)") float MassScale = 0.0f; + + /// + /// Enables collisions with scene geometry (both dynamic and static). Disable this to improve performance of cloth that doesn't need to collide. + /// + API_FIELD() bool SceneCollisions = true; + + /// + /// Enables Continuous Collision Detection (CCD) that improves collision by computing the time of impact between cloth particles and colliders. The increase in quality can impact performance. + /// + API_FIELD() bool ContinuousCollisionDetection = false; + + /// + /// The minimum distance that the colliding cloth particles must maintain from each other in meters. 0: self collision disabled. + /// + API_FIELD(Attributes="Limit(0)") float SelfCollisionDistance = 0.0f; + + /// + /// Stiffness for the self collision constraints. 0: self collision disabled. + /// + API_FIELD(Attributes="Limit(0)") float SelfCollisionStiffness = 0.2f; + }; + + /// + /// Cloth simulation settings. + /// + API_STRUCT() struct SimulationSettings : ISerializable + { + DECLARE_SCRIPTING_TYPE_MINIMAL(SimulationSettings); + API_AUTO_SERIALIZATION(); + + /// + /// Target cloth solver iterations per second. The executed number of iterations per second may vary dependent on many performance factors. However, at least one iteration per frame is solved regardless of the value set. + /// + API_FIELD() float SolverFrequency = 300.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. + /// + API_FIELD() Vector3 WindVelocity = Vector3::Zero; + }; + + /// + /// Cloth's fabric settings (material's stiffness and compression response) for a single axis. + /// + API_STRUCT() struct FabricAxisSettings : ISerializable + { + DECLARE_SCRIPTING_TYPE_MINIMAL(FabricAxisSettings); + API_AUTO_SERIALIZATION(); + + /// + /// Stiffness value for stretch and compression constraints. 0: disables it. + /// + API_FIELD(Attributes="Limit(0)") float Stiffness = 1.0f; + + /// + /// Scale value for stretch and compression constraints. 0: no stretch and compression constraints applied. 1: fully apply stretch and compression constraints. + /// + API_FIELD(Attributes="Limit(0, 1)") float StiffnessMultiplier = 1.0f; + + /// + /// Compression limit for constraints. + /// + API_FIELD(Attributes="Limit(0)") float CompressionLimit = 1.0f; + + /// + /// Stretch limit for constraints. + /// + API_FIELD(Attributes="Limit(0)") float StretchLimit = 1.0f; + }; + + /// + /// Cloth's fabric settings (material's stiffness and compression response). + /// + API_STRUCT() struct FabricSettings : ISerializable + { + DECLARE_SCRIPTING_TYPE_MINIMAL(FabricStiffnessSettings); + API_AUTO_SERIALIZATION(); + + /// + /// Vertical constraints for stretching or compression (along the gravity). + /// + API_FIELD() FabricAxisSettings Vertical; + + /// + /// Horizontal constraints for stretching or compression (perpendicular to the gravity). + /// + API_FIELD() FabricAxisSettings Horizontal; + + /// + /// Bending constraints for out-of-plane bending in angle-based formulation. + /// + API_FIELD() FabricAxisSettings Bending; + + /// + /// Shearing constraints for plane shearing along (typically) diagonal edges. + /// + API_FIELD() FabricAxisSettings Shearing; + }; + +private: + void* _cloth = nullptr; + ForceSettings _forceSettings; + CollisionSettings _collisionSettings; + SimulationSettings _simulationSettings; + FabricSettings _fabricSettings; + Vector3 _cachedPosition = Vector3::Zero; + ModelInstanceActor::MeshReference _mesh; + MeshDeformation* _meshDeformation = nullptr; + +public: + /// + /// Gets the mesh to use for the cloth simulation (single mesh from specific LOD). + /// + /// + API_PROPERTY(Attributes="EditorOrder(0), EditorDisplay(\"Cloth\")") + ModelInstanceActor::MeshReference GetMesh() const; + + /// + /// Sets the mesh to use for the cloth simulation (single mesh from specific LOD). + /// + API_PROPERTY() void SetMesh(const ModelInstanceActor::MeshReference& value); + + /// + /// Gets the cloth response to forces settings. + /// + API_PROPERTY(Attributes="EditorOrder(10), EditorDisplay(\"Cloth\")") + FORCE_INLINE ForceSettings GetForce() const + { + return _forceSettings; + } + + /// + /// Sets the cloth response to forces settings. + /// + API_PROPERTY() void SetForce(const ForceSettings& value); + + /// + /// Gets the cloth response to collisions settings. + /// + API_PROPERTY(Attributes="EditorOrder(20), EditorDisplay(\"Cloth\")") + FORCE_INLINE CollisionSettings GetCollision() const + { + return _collisionSettings; + } + + /// + /// Sets the cloth response to collisions settings. + /// + API_PROPERTY() void SetCollision(const CollisionSettings& value); + + /// + /// Gets the cloth simulation settings. + /// + API_PROPERTY(Attributes="EditorOrder(30), EditorDisplay(\"Cloth\")") + FORCE_INLINE SimulationSettings GetSimulation() const + { + return _simulationSettings; + } + + /// + /// Sets the cloth simulation settings. + /// + API_PROPERTY() void SetSimulation(const SimulationSettings& value); + + /// + /// Gets the cloth's fabric settings (material's stiffness and compression response). + /// + API_PROPERTY(Attributes="EditorOrder(40), EditorDisplay(\"Cloth\")") + FORCE_INLINE FabricSettings GetFabric() const + { + return _fabricSettings; + } + + /// + /// Sets the cloth's fabric settings (material's stiffness and compression response). + /// + API_PROPERTY() void SetFabric(const FabricSettings& value); + +public: + /// + /// Recreates the cloth by removing current instance data and creating a new physical cloth object. Does nothing if cloth was not created (eg. no parent mesh). + /// + API_FUNCTION() void Rebuild(); + + /// + /// Sets the inertia derived from transform change to zero (once). It will reset any cloth object movement effects as it was teleported. + /// + API_FUNCTION() void ClearInteria(); + +public: + // [Actor] + void Serialize(SerializeStream& stream, const void* otherObj) override; + void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; + +protected: + // [Actor] +#if USE_EDITOR + void OnDebugDrawSelected() override; +#endif + void BeginPlay(SceneBeginData* data) override; + void EndPlay() override; + void OnEnable() override; + void OnDisable() override; + void OnParentChanged() override; + void OnTransformChanged() override; + void OnPhysicsSceneChanged(PhysicsScene* previous) override; + +private: +#if USE_EDITOR + void DrawPhysicsDebug(RenderView& view); +#endif + bool CreateCloth(); + void DestroyCloth(); + 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 41430fb4d..e33c66efe 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -38,6 +38,17 @@ #include #include #endif +#if WITH_CLOTH +#include "Engine/Physics/Actors/Cloth.h" +#include +#include +#include +#include +#include +#include +#define MAX_CLOTH_SPHERE_COUNT 32 +#define MAX_CLOTH_PLANE_COUNT 32 +#endif #if WITH_PVD #include #endif @@ -79,6 +90,9 @@ struct ScenePhysX PxBatchQueryExt* WheelRaycastBatchQuery = nullptr; int32 WheelRaycastBatchQuerySize = 0; #endif +#if WITH_CLOTH + nv::cloth::Solver* ClothSolver = nullptr; +#endif }; class AllocatorPhysX : public PxAllocatorCallback @@ -103,6 +117,67 @@ class ErrorPhysX : public PxErrorCallback } }; +#if WITH_CLOTH + +class AssertPhysX : public nv::cloth::PxAssertHandler +{ +public: + void operator()(const char* exp, const char* file, int line, bool& ignore) override + { + Platform::Error(String(exp)); + Platform::Crash(line, file); + } +}; + +class ProfilerPhysX : public physx::PxProfilerCallback +{ +public: + void* zoneStart(const char* eventName, bool detached, uint64_t contextId) override + { + return nullptr; + } + + void zoneEnd(void* profilerData, const char* eventName, bool detached, uint64_t contextId) override + { + } +}; + +struct FabricSettings +{ + int32 Refs; + nv::cloth::Vector::Type PhraseTypes; +}; + +struct ClothSettings +{ + bool SceneCollisions = false; + float GravityScale = 1.0f; + Cloth* Actor; + + void UpdateBounds(const nv::cloth::Cloth* clothPhysX) const + { + // Get cloth particles bounds (in local-space) + const PxVec3& clothBoundsPos = clothPhysX->getBoundingBoxCenter(); + const PxVec3& clothBoundsSize = clothPhysX->getBoundingBoxScale(); + BoundingBox localBounds; + BoundingBox::FromPoints(P2C(clothBoundsPos - clothBoundsSize), P2C(clothBoundsPos + clothBoundsSize), localBounds); + + // Transform local-space bounds into world-space + const PxTransform clothPose(clothPhysX->getTranslation(), clothPhysX->getRotation()); + const Transform clothTrans(P2C(clothPose.p), P2C(clothPose.q)); + Vector3 boundsCorners[8]; + localBounds.GetCorners(boundsCorners); + for (Vector3& c : boundsCorners) + clothTrans.LocalToWorld(c, c); + + // Setup bounds + BoundingBox::FromPoints(boundsCorners, 8, const_cast(Actor->GetBox())); + BoundingSphere::FromBox(Actor->GetBox(), const_cast(Actor->GetSphere())); + } +}; + +#endif + class QueryFilterPhysX : public PxQueryFilterCallback { PxQueryHitType::Enum preFilter(const PxFilterData& filterData, const PxShape* shape, const PxRigidActor* actor, PxHitFlags& queryFlags) override @@ -379,6 +454,10 @@ namespace PxMaterial* DefaultMaterial = nullptr; AllocatorPhysX AllocatorCallback; ErrorPhysX ErrorCallback; +#if WITH_CLOTH + AssertPhysX AssertCallback; + ProfilerPhysX ProfilerCallback; +#endif PxTolerancesScale ToleranceScale; QueryFilterPhysX QueryFilter; CharacterQueryFilterPhysX CharacterQueryFilter; @@ -402,6 +481,22 @@ namespace Array WheelTireTypes; WheelFilterPhysX WheelRaycastFilter; #endif + +#if WITH_CLOTH + nv::cloth::Factory* ClothFactory = nullptr; + Dictionary Fabrics; + Dictionary Cloths; +#endif +} + +FORCE_INLINE PxPlane transform(const PxPlane& plane, const PxMat33& inverse) +{ + PxPlane result; + result.n.x = plane.n.x * inverse.column0.x + plane.n.y * inverse.column0.y + plane.n.z * inverse.column0.z; + result.n.y = plane.n.x * inverse.column1.x + plane.n.y * inverse.column1.y + plane.n.z * inverse.column1.z; + result.n.z = plane.n.x * inverse.column2.x + plane.n.y * inverse.column2.y + plane.n.z * inverse.column2.z; + result.d = plane.d; + return result; } PxShapeFlags GetShapeFlags(bool isTrigger, bool isEnabled) @@ -777,6 +872,13 @@ void PhysicsBackend::Shutdown() RELEASE_PHYSX(DefaultMaterial); // Shutdown PhysX +#if WITH_CLOTH + if (ClothFactory) + { + NvClothDestroyFactory(ClothFactory); + ClothFactory = nullptr; + } +#endif #if WITH_VEHICLE if (VehicleSDKInitialized) { @@ -881,6 +983,13 @@ void PhysicsBackend::DestroyScene(void* scene) #if WITH_VEHICLE RELEASE_PHYSX(scenePhysX->WheelRaycastBatchQuery); scenePhysX->WheelRaycastBatchQuerySize = 0; +#endif +#if WITH_CLOTH + if (scenePhysX->ClothSolver) + { + NV_CLOTH_DELETE(scenePhysX->ClothSolver); + scenePhysX->ClothSolver = nullptr; + } #endif RELEASE_PHYSX(scenePhysX->ControllerManager); SAFE_DELETE(scenePhysX->CpuDispatcher); @@ -1249,6 +1358,203 @@ void PhysicsBackend::EndSimulateScene(void* scene) } } +#if WITH_CLOTH + nv::cloth::Solver* clothSolver = scenePhysX->ClothSolver; + if (clothSolver && clothSolver->getNumCloths() != 0) + { + PROFILE_CPU_NAMED("Physics.Cloth"); + const int32 clothsCount = scenePhysX->ClothSolver->getNumCloths(); + nv::cloth::Cloth* const* cloths = scenePhysX->ClothSolver->getClothList(); + // TODO: jobify to run in async (eg. with vehicles update and events setup) + + { + PROFILE_CPU_NAMED("Collisions"); + const bool hitTriggers = false; + const bool blockSingle = false; + PxQueryFilterData filterData; + filterData.flags |= PxQueryFlag::ePREFILTER; + filterData.data.word1 = blockSingle ? 1 : 0; + filterData.data.word2 = hitTriggers ? 1 : 0; + for (int32 i = 0; i < clothsCount; i++) + { + auto clothPhysX = (nv::cloth::Cloth*)cloths[i]; + const auto& clothSettings = Cloths[clothPhysX]; + + // Setup automatic scene collisions with colliders around the cloth + if (clothSettings.SceneCollisions) + { + // Reset existing colliders + clothPhysX->setSpheres(nv::cloth::Range(), 0, clothPhysX->getNumSpheres()); + clothPhysX->setPlanes(nv::cloth::Range(), 0, clothPhysX->getNumPlanes()); + clothPhysX->setTriangles(nv::cloth::Range(), 0, clothPhysX->getNumTriangles()); + + filterData.data.word0 = Physics::LayerMasks[clothSettings.Actor->GetLayer()]; + const PxTransform clothPose(clothPhysX->getTranslation(), clothPhysX->getRotation()); + const PxVec3 clothBoundsPos = clothPhysX->getBoundingBoxCenter(); + const PxVec3 clothBoundsSize = clothPhysX->getBoundingBoxScale(); + const PxTransform overlapPose(clothPose.transform(clothBoundsPos), clothPose.q); + const float boundsMargin = 1.6f; + const PxSphereGeometry overlapGeo(clothBoundsSize.magnitude() * boundsMargin); + + // Find any colliders around the cloth + DynamicHitBuffer buffer; + if (scenePhysX->Scene->overlap(overlapGeo, overlapPose, buffer, filterData, &QueryFilter)) + { + for (uint32 j = 0; j < buffer.getNbTouches(); j++) + { + const auto& hit = buffer.getTouch(j); + if (hit.shape) + { + const PxGeometry& geo = hit.shape->getGeometry(); + const PxTransform shapeToCloth = clothPose.transformInv(hit.actor->getGlobalPose().transform(hit.shape->getLocalPose())); + // TODO: maybe use shared spheres/planes buffers for batched assigning? + switch (geo.getType()) + { + case PxGeometryType::eSPHERE: + { + const PxSphereGeometry& geoSphere = (const PxSphereGeometry&)geo; + const PxVec4 packedSphere(shapeToCloth.p, geoSphere.radius); + const nv::cloth::Range sphereRange(&packedSphere, &packedSphere + 1); + const uint32_t spheresCount = clothPhysX->getNumSpheres(); + if (spheresCount + 1 > MAX_CLOTH_SPHERE_COUNT) + break; + clothPhysX->setSpheres(sphereRange, spheresCount, spheresCount); + break; + } + case PxGeometryType::eCAPSULE: + { + const PxCapsuleGeometry& geomCapsule = (const PxCapsuleGeometry&)geo; + const PxVec4 packedSpheres[2] = { + PxVec4(shapeToCloth.transform(PxVec3(+geomCapsule.halfHeight, 0, 0)), geomCapsule.radius), + PxVec4(shapeToCloth.transform(PxVec3(-geomCapsule.halfHeight, 0, 0)), geomCapsule.radius) + }; + const nv::cloth::Range sphereRange(packedSpheres, packedSpheres + 2); + const uint32_t spheresCount = clothPhysX->getNumSpheres(); + if (spheresCount + 2 > MAX_CLOTH_SPHERE_COUNT) + break; + clothPhysX->setSpheres(sphereRange, spheresCount, spheresCount); + const uint32_t packedCapsules[2] = { spheresCount, spheresCount + 1 }; + const int32 capsulesCount = clothPhysX->getNumCapsules(); + clothPhysX->setCapsules(nv::cloth::Range(packedCapsules, packedCapsules + 2), capsulesCount, capsulesCount); + break; + } + case PxGeometryType::eBOX: + { + const PxBoxGeometry& geomBox = (const PxBoxGeometry&)geo; + const uint32_t planesCount = clothPhysX->getNumPlanes(); + if (planesCount + 6 > MAX_CLOTH_PLANE_COUNT) + break; + const PxPlane packedPlanes[6] = { + PxPlane(PxVec3(1, 0, 0), -geomBox.halfExtents.x).transform(shapeToCloth), + PxPlane(PxVec3(-1, 0, 0), -geomBox.halfExtents.x).transform(shapeToCloth), + PxPlane(PxVec3(0, 1, 0), -geomBox.halfExtents.y).transform(shapeToCloth), + PxPlane(PxVec3(0, -1, 0), -geomBox.halfExtents.y).transform(shapeToCloth), + PxPlane(PxVec3(0, 0, 1), -geomBox.halfExtents.z).transform(shapeToCloth), + PxPlane(PxVec3(0, 0, -1), -geomBox.halfExtents.z).transform(shapeToCloth) + }; + clothPhysX->setPlanes(nv::cloth::Range((const PxVec4*)packedPlanes, (const PxVec4*)packedPlanes + 6), planesCount, planesCount); + const PxU32 convexMask = PxU32(0x3f << planesCount); + const uint32_t convexesCount = clothPhysX->getNumConvexes(); + clothPhysX->setConvexes(nv::cloth::Range(&convexMask, &convexMask + 1), convexesCount, convexesCount); + break; + } + case PxGeometryType::eCONVEXMESH: + { + const PxConvexMeshGeometry& geomConvexMesh = (const PxConvexMeshGeometry&)geo; + const PxU32 convexPlanesCount = geomConvexMesh.convexMesh->getNbPolygons(); + const uint32_t planesCount = clothPhysX->getNumPlanes(); + if (planesCount + convexPlanesCount > MAX_CLOTH_PLANE_COUNT) + break; + const PxMat33 convexToShapeInv = geomConvexMesh.scale.toMat33().getInverse(); + // TODO: merge convexToShapeInv with shapeToCloth to have a single matrix multiplication + PxPlane planes[MAX_CLOTH_PLANE_COUNT]; + for (PxU32 k = 0; k < convexPlanesCount; k++) + { + PxHullPolygon polygon; + geomConvexMesh.convexMesh->getPolygonData(k, polygon); + planes[k] = transform(reinterpret_cast(polygon.mPlane), convexToShapeInv).transform(shapeToCloth); + } + clothPhysX->setPlanes(nv::cloth::Range((const PxVec4*)planes, (const PxVec4*)planes + convexPlanesCount), planesCount, planesCount); + const PxU32 convexMask = PxU32(((1 << convexPlanesCount) - 1) << planesCount); + const uint32_t convexesCount = clothPhysX->getNumConvexes(); + clothPhysX->setConvexes(nv::cloth::Range(&convexMask, &convexMask + 1), convexesCount, convexesCount); + break; + } + // Cloth vs Triangle collisions are too slow for real-time use +#if 0 + case PxGeometryType::eTRIANGLEMESH: + { + const PxTriangleMeshGeometry& geomTriangleMesh = (const PxTriangleMeshGeometry&)geo; + if (geomTriangleMesh.triangleMesh->getNbTriangles() >= 1024) + break; // Ignore too-tessellated meshes due to poor solver performance + // TODO: use shared memory allocators maybe? maybe per-frame stack allocator? + Array vertices; + vertices.Add(geomTriangleMesh.triangleMesh->getVertices(), geomTriangleMesh.triangleMesh->getNbVertices()); + const PxMat33 triangleMeshToShape = geomTriangleMesh.scale.toMat33(); + // TODO: merge triangleMeshToShape with shapeToCloth to have a single matrix multiplication + for (int32 k = 0; k < vertices.Count(); k++) + { + PxVec3& v = vertices.Get()[k]; + v = shapeToCloth.transform(triangleMeshToShape.transform(v)); + } + Array triangles; + triangles.Resize(geomTriangleMesh.triangleMesh->getNbTriangles() * 3); + if (geomTriangleMesh.triangleMesh->getTriangleMeshFlags() & PxTriangleMeshFlag::e16_BIT_INDICES) + { + auto indices = (const uint16*)geomTriangleMesh.triangleMesh->getTriangles(); + for (int32 k = 0; k < triangles.Count(); k++) + triangles.Get()[k] = vertices.Get()[indices[k]]; + } + else + { + auto indices = (const uint32*)geomTriangleMesh.triangleMesh->getTriangles(); + for (int32 k = 0; k < triangles.Count(); k++) + triangles.Get()[k] = vertices.Get()[indices[k]]; + } + const uint32_t trianglesCount = clothPhysX->getNumTriangles(); + clothPhysX->setTriangles(nv::cloth::Range(triangles.begin(), triangles.end()), trianglesCount, trianglesCount); + break; + } + case PxGeometryType::eHEIGHTFIELD: + { + const PxHeightFieldGeometry& geomHeightField = (const PxHeightFieldGeometry&)geo; + // TODO: heightfield collisions (gather triangles only nearby cloth to not blow sim perf) + break; + } +#endif + } + } + } + } + } + } + } + + { + PROFILE_CPU_NAMED("Simulation"); + if (clothSolver->beginSimulation(scenePhysX->LastDeltaTime)) + { + const int32 count = clothSolver->getSimulationChunkCount(); + for (int32 i = 0; i < count; i++) + { + clothSolver->simulateChunk(i); + } + clothSolver->endSimulation(); + } + } + + { + PROFILE_CPU_NAMED("Post"); + for (int32 i = 0; i < clothsCount; i++) + { + auto clothPhysX = (nv::cloth::Cloth*)cloths[i]; + const auto& clothSettings = Cloths[clothPhysX]; + clothSettings.UpdateBounds(clothPhysX); + clothSettings.Actor->OnUpdated(); + } + } + } + { PROFILE_CPU_NAMED("Physics.SendEvents"); @@ -1257,6 +1563,7 @@ void PhysicsBackend::EndSimulateScene(void* scene) scenePhysX->EventsCallback.SendCollisionEvents(); scenePhysX->EventsCallback.SendJointEvents(); } +#endif } Vector3 PhysicsBackend::GetSceneGravity(void* scene) @@ -1269,6 +1576,19 @@ void PhysicsBackend::SetSceneGravity(void* scene, const Vector3& value) { auto scenePhysX = (ScenePhysX*)scene; scenePhysX->Scene->setGravity(C2P(value)); +#if WITH_CLOTH + if (scenePhysX->ClothSolver) + { + const int32 clothsCount = scenePhysX->ClothSolver->getNumCloths(); + nv::cloth::Cloth* const* cloths = scenePhysX->ClothSolver->getClothList(); + for (int32 i = 0; i < clothsCount; i++) + { + auto clothPhysX = (nv::cloth::Cloth*)cloths[i]; + const auto& clothSettings = Cloths[clothPhysX]; + clothPhysX->setGravity(C2P(value * clothSettings.GravityScale)); + } + } +#endif } bool PhysicsBackend::GetSceneEnableCCD(void* scene) @@ -1302,6 +1622,7 @@ void PhysicsBackend::SetSceneOrigin(void* scene, const Vector3& oldOrigin, const scenePhysX->Origin = newOrigin; scenePhysX->Scene->shiftOrigin(shift); scenePhysX->ControllerManager->shiftOrigin(shift); +#if WITH_VEHICLE WheelVehiclesCache.Clear(); for (auto wheelVehicle : scenePhysX->WheelVehicles) { @@ -1312,6 +1633,18 @@ void PhysicsBackend::SetSceneOrigin(void* scene, const Vector3& oldOrigin, const WheelVehiclesCache.Add(drive); } PxVehicleShiftOrigin(shift, WheelVehiclesCache.Count(), WheelVehiclesCache.Get()); +#endif +#if WITH_CLOTH + if (scenePhysX->ClothSolver) + { + const int32 clothsCount = scenePhysX->ClothSolver->getNumCloths(); + nv::cloth::Cloth* const* cloths = scenePhysX->ClothSolver->getClothList(); + for (int32 i = 0; i < clothsCount; i++) + { + cloths[i]->teleport(shift); + } + } +#endif SceneOrigins[scenePhysX->Scene] = newOrigin; } @@ -2931,6 +3264,220 @@ void PhysicsBackend::RemoveVehicle(void* scene, WheeledVehicle* actor) #endif +#if WITH_CLOTH + +void* PhysicsBackend::CreateCloth(const PhysicsClothDesc& desc) +{ + // Lazy-init NvCloth + if (ClothFactory == nullptr) + { + nv::cloth::InitializeNvCloth(&AllocatorCallback, &ErrorCallback, &AssertCallback, &ProfilerCallback); + ClothFactory = NvClothCreateFactoryCPU(); + ASSERT(ClothFactory); + } + + // Cook fabric from the mesh data + nv::cloth::ClothMeshDesc meshDesc; + meshDesc.points.data = desc.VerticesData; + meshDesc.points.stride = desc.VerticesStride; + meshDesc.points.count = desc.VerticesCount; + 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) + nv::cloth::Fabric* fabric = NvClothCookFabricFromMesh(ClothFactory, meshDesc, gravity.Raw, &phaseTypeInfo); + if (!fabric) + { + LOG(Error, "NvClothCookFabricFromMesh failed"); + return nullptr; + } + + // 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 + const nv::cloth::Range initialParticlesRange((PxVec4*)initialState.Get(), (PxVec4*)initialState.Get() + initialState.Count()); + nv::cloth::Cloth* clothPhysX = ClothFactory->createCloth(initialParticlesRange, *fabric); + fabric->decRefCount(); + if (!clothPhysX) + { + LOG(Error, "createCloth failed"); + return nullptr; + } + + // Setup settings + FabricSettings fabricSettings; + fabricSettings.Refs = 1; + fabricSettings.PhraseTypes.swap(phaseTypeInfo); + Fabrics.Add(fabric, fabricSettings); + ClothSettings clothSettings; + clothSettings.Actor = desc.Actor; + clothSettings.UpdateBounds(clothPhysX); + Cloths.Add(clothPhysX, clothSettings); + + return clothPhysX; +} + +void PhysicsBackend::DestroyCloth(void* cloth) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + if (--Fabrics[&clothPhysX->getFabric()].Refs == 0) + Fabrics.Remove(&clothPhysX->getFabric()); + Cloths.Remove(clothPhysX); + NV_CLOTH_DELETE(clothPhysX); +} + +void PhysicsBackend::SetClothForceSettings(void* cloth, const void* settingsPtr) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + const auto& settings = *(const Cloth::ForceSettings*)settingsPtr; + clothPhysX->setGravity(C2P(PhysicsSettings::Get()->DefaultGravity * settings.GravityScale)); + clothPhysX->setDamping(PxVec3(settings.Damping)); + clothPhysX->setLinearDrag(PxVec3(settings.LinearDrag)); + clothPhysX->setAngularDrag(PxVec3(settings.AngularDrag)); + clothPhysX->setLinearInertia(PxVec3(settings.LinearInertia)); + clothPhysX->setAngularInertia(PxVec3(settings.AngularInertia)); + clothPhysX->setCentrifugalInertia(PxVec3(settings.CentrifugalInertia)); + clothPhysX->setDragCoefficient(Math::Saturate(settings.AirDragCoefficient)); + clothPhysX->setLiftCoefficient(Math::Saturate(settings.AirLiftCoefficient)); + clothPhysX->setFluidDensity(Math::Max(settings.AirDensity, ZeroTolerance)); + auto& clothSettings = Cloths[clothPhysX]; + clothSettings.GravityScale = settings.GravityScale; +} + +void PhysicsBackend::SetClothCollisionSettings(void* cloth, const void* settingsPtr) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + const auto& settings = *(const Cloth::CollisionSettings*)settingsPtr; + clothPhysX->setFriction(settings.Friction); + clothPhysX->setCollisionMassScale(settings.MassScale); + clothPhysX->enableContinuousCollision(settings.ContinuousCollisionDetection); + clothPhysX->setSelfCollisionDistance(settings.SelfCollisionDistance); + clothPhysX->setSelfCollisionStiffness(settings.SelfCollisionStiffness); + auto& clothSettings = Cloths[clothPhysX]; + if (clothSettings.SceneCollisions != settings.SceneCollisions && !settings.SceneCollisions) + { + // Remove colliders + clothPhysX->setSpheres(nv::cloth::Range(), 0, clothPhysX->getNumSpheres()); + clothPhysX->setPlanes(nv::cloth::Range(), 0, clothPhysX->getNumPlanes()); + clothPhysX->setTriangles(nv::cloth::Range(), 0, clothPhysX->getNumTriangles()); + } + clothSettings.SceneCollisions = settings.SceneCollisions; +} + +void PhysicsBackend::SetClothSimulationSettings(void* cloth, const void* settingsPtr) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + const auto& settings = *(const Cloth::SimulationSettings*)settingsPtr; + clothPhysX->setSolverFrequency(settings.SolverFrequency); + clothPhysX->setWindVelocity(C2P(settings.WindVelocity)); +} + +void PhysicsBackend::SetClothFabricSettings(void* cloth, const void* settingsPtr) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + const auto& settings = *(const Cloth::FabricSettings*)settingsPtr; + const auto& fabricSettings = Fabrics[&clothPhysX->getFabric()]; + Array configs; + configs.Resize(fabricSettings.PhraseTypes.size()); + for (int32 i = 0; i < configs.Count(); i++) + { + auto& config = configs[i]; + config.mPhaseIndex = i; + const Cloth::FabricAxisSettings* axisSettings; + switch (fabricSettings.PhraseTypes[i]) + { + case nv::cloth::ClothFabricPhaseType::eVERTICAL: + axisSettings = &settings.Vertical; + break; + case nv::cloth::ClothFabricPhaseType::eHORIZONTAL: + axisSettings = &settings.Horizontal; + break; + case nv::cloth::ClothFabricPhaseType::eBENDING: + axisSettings = &settings.Bending; + break; + case nv::cloth::ClothFabricPhaseType::eSHEARING: + axisSettings = &settings.Shearing; + break; + } + config.mStiffness = axisSettings->Stiffness; + config.mStiffnessMultiplier = axisSettings->StiffnessMultiplier; + config.mCompressionLimit = axisSettings->CompressionLimit; + config.mStretchLimit = axisSettings->StretchLimit; + } + clothPhysX->setPhaseConfig(nv::cloth::Range(configs.begin(), configs.end())); +} + +void PhysicsBackend::SetClothTransform(void* cloth, const Transform& transform, bool teleport) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + if (teleport) + { + clothPhysX->teleportToLocation(C2P(transform.Translation), C2P(transform.Orientation)); + } + else + { + clothPhysX->setTranslation(C2P(transform.Translation)); + clothPhysX->setRotation(C2P(transform.Orientation)); + } + const auto& clothSettings = Cloths[clothPhysX]; + clothSettings.UpdateBounds(clothPhysX); +} + +void PhysicsBackend::ClearClothInertia(void* cloth) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + clothPhysX->clearInertia(); +} + +void PhysicsBackend::LockClothParticles(void* cloth) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + clothPhysX->lockParticles(); +} + +void PhysicsBackend::UnlockClothParticles(void* cloth) +{ + auto clothPhysX = (nv::cloth::Cloth*)cloth; + clothPhysX->unlockParticles(); +} + +Span PhysicsBackend::GetClothCurrentParticles(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::AddCloth(void* scene, void* cloth) +{ + auto scenePhysX = (ScenePhysX*)scene; + auto clothPhysX = (nv::cloth::Cloth*)cloth; + if (scenePhysX->ClothSolver == nullptr) + { + scenePhysX->ClothSolver = ClothFactory->createSolver(); + ASSERT(scenePhysX->ClothSolver); + } + scenePhysX->ClothSolver->addCloth(clothPhysX); +} + +void PhysicsBackend::RemoveCloth(void* scene, void* cloth) +{ + auto scenePhysX = (ScenePhysX*)scene; + auto clothPhysX = (nv::cloth::Cloth*)cloth; + scenePhysX->ClothSolver->removeCloth(clothPhysX); +} + +#endif + void* PhysicsBackend::CreateConvexMesh(byte* data, int32 dataSize, BoundingBox& localBounds) { PxDefaultMemoryInputData input(data, dataSize); diff --git a/Source/Engine/Physics/Physics.Build.cs b/Source/Engine/Physics/Physics.Build.cs index d31e0d271..2a5911245 100644 --- a/Source/Engine/Physics/Physics.Build.cs +++ b/Source/Engine/Physics/Physics.Build.cs @@ -14,6 +14,16 @@ public class Physics : EngineModule /// public static bool WithCooking = true; + /// + /// Enables using vehicles simulation. + /// + public static bool WithVehicle = true; + + /// + /// Enables using cloth simulation. + /// + public static bool WithCloth = true; + /// /// Enables using PhysX library. Can be overriden by SetupPhysicsBackend. /// @@ -42,6 +52,8 @@ public class Physics : EngineModule if (WithPhysX) { options.PrivateDependencies.Add("PhysX"); + if (WithCloth && options.Platform.Target != TargetPlatform.PS4) // TODO: build nvcloth for ps4 with vs2017 + options.PrivateDependencies.Add("NvCloth"); } else { diff --git a/Source/Engine/Physics/PhysicsBackend.h b/Source/Engine/Physics/PhysicsBackend.h index f72c7f679..5e8743d69 100644 --- a/Source/Engine/Physics/PhysicsBackend.h +++ b/Source/Engine/Physics/PhysicsBackend.h @@ -22,8 +22,9 @@ enum class D6JointDriveType; class IPhysicsActor; class PhysicalMaterial; class JsonAsset; +class JsonAsset; -struct FLAXENGINE_API PhysicsJointDesc +struct PhysicsJointDesc { Joint* Joint; void* Actor0; @@ -34,6 +35,17 @@ struct FLAXENGINE_API PhysicsJointDesc Vector3 Pos1; }; +struct PhysicsClothDesc +{ + class Cloth* Actor; + void* VerticesData; + void* IndicesData; + uint32 VerticesCount; + uint32 VerticesStride; + uint32 IndicesCount; + uint32 IndicesStride; +}; + /// /// Interface for the physical simulation backend implementation. /// @@ -256,6 +268,23 @@ public: static void AddVehicle(void* scene, WheeledVehicle* actor); static void RemoveVehicle(void* scene, WheeledVehicle* actor); #endif + +#if WITH_CLOTH + // Cloth + static void* CreateCloth(const PhysicsClothDesc& desc); + static void DestroyCloth(void* cloth); + static void SetClothForceSettings(void* cloth, const void* settingsPtr); + static void SetClothCollisionSettings(void* cloth, const void* settingsPtr); + static void SetClothSimulationSettings(void* cloth, const void* settingsPtr); + static void SetClothFabricSettings(void* cloth, const void* settingsPtr); + static void SetClothTransform(void* cloth, const Transform& transform, bool teleport); + static void ClearClothInertia(void* cloth); + static void LockClothParticles(void* cloth); + static void UnlockClothParticles(void* cloth); + static Span GetClothCurrentParticles(void* cloth); + static void AddCloth(void* scene, void* cloth); + static void RemoveCloth(void* scene, void* cloth); +#endif // Resources static void* CreateConvexMesh(byte* data, int32 dataSize, BoundingBox& localBounds); diff --git a/Source/ThirdParty/PhysX/PhysX.Build.cs b/Source/ThirdParty/PhysX/PhysX.Build.cs index 7b598f8b1..31d6ec2db 100644 --- a/Source/ThirdParty/PhysX/PhysX.Build.cs +++ b/Source/ThirdParty/PhysX/PhysX.Build.cs @@ -38,7 +38,7 @@ public class PhysX : DepsModule bool useDynamicLinking = false; bool usePVD = false; - bool useVehicle = true; + bool useVehicle = Physics.WithVehicle; bool usePhysicsCooking = Physics.WithCooking; var depsRoot = options.DepsFolder; diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/NvCloth.cs b/Source/Tools/Flax.Build/Deps/Dependencies/NvCloth.cs new file mode 100644 index 000000000..7288add63 --- /dev/null +++ b/Source/Tools/Flax.Build/Deps/Dependencies/NvCloth.cs @@ -0,0 +1,193 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Flax.Build; +using Flax.Build.Platforms; + +namespace Flax.Deps.Dependencies +{ + /// + /// NVIDIA NvCloth + /// + /// + class NvCloth : Dependency + { + private string root, nvCloth; + + /// + public override TargetPlatform[] Platforms + { + get + { + switch (BuildPlatform) + { + case TargetPlatform.Windows: + return new[] + { + TargetPlatform.Windows, + TargetPlatform.XboxOne, + TargetPlatform.XboxScarlett, + TargetPlatform.PS4, + TargetPlatform.PS5, + TargetPlatform.Switch, + TargetPlatform.Android, + }; + case TargetPlatform.Linux: + return new[] + { + TargetPlatform.Linux, + }; + case TargetPlatform.Mac: + return new[] + { + TargetPlatform.Mac, + TargetPlatform.iOS, + }; + default: return new TargetPlatform[0]; + } + } + } + + /// + public override void Build(BuildOptions options) + { + root = options.IntermediateFolder; + nvCloth = Path.Combine(root, "NvCloth"); + + // Get the source + CloneGitRepoSingleBranch(root, "https://github.com/FlaxEngine/NvCloth.git", "master"); + + foreach (var platform in options.Platforms) + { + switch (platform) + { + case TargetPlatform.Windows: + Build(options, platform, TargetArchitecture.x64); + break; + case TargetPlatform.XboxOne: + case TargetPlatform.XboxScarlett: + Build(options, platform, TargetArchitecture.x64); + break; + case TargetPlatform.PS4: + case TargetPlatform.PS5: + Utilities.DirectoryCopy(Path.Combine(GetBinariesFolder(options, platform), "Data", "NvCloth"), root, true, true); + Build(options, platform, TargetArchitecture.x64); + break; + case TargetPlatform.Switch: + Utilities.DirectoryCopy(Path.Combine(GetBinariesFolder(options, platform), "Data", "NvCloth"), root, true, true); + Build(options, platform, TargetArchitecture.ARM64); + break; + case TargetPlatform.Android: + Build(options, platform, TargetArchitecture.ARM64); + break; + } + } + + // Copy header files + var dstIncludePath = Path.Combine(options.ThirdPartyFolder, "NvCloth"); + Directory.GetFiles(dstIncludePath, "*.h", SearchOption.AllDirectories).ToList().ForEach(File.Delete); + Utilities.FileCopy(Path.Combine(nvCloth, "license.txt"), Path.Combine(dstIncludePath, "License.txt")); + Utilities.DirectoryCopy(Path.Combine(nvCloth, "include", "NvCloth"), dstIncludePath, true, true, "*.h"); + Utilities.DirectoryCopy(Path.Combine(nvCloth, "extensions", "include"), dstIncludePath, true, true, "*.h"); + } + + private void Build(BuildOptions options, TargetPlatform platform, TargetArchitecture architecture) + { + // Peek options + var binariesPrefix = string.Empty; + var binariesPostfix = string.Empty; + var cmakeArgs = "-DNV_CLOTH_ENABLE_DX11=0 -DNV_CLOTH_ENABLE_CUDA=0 -DPX_GENERATE_GPU_PROJECTS=0"; + var cmakeName = string.Empty; + var buildFolder = Path.Combine(nvCloth, "compiler", platform.ToString() + '_' + architecture.ToString()); + var envVars = new Dictionary(); + envVars["GW_DEPS_ROOT"] = root; + switch (platform) + { + case TargetPlatform.Windows: + case TargetPlatform.XboxOne: + case TargetPlatform.XboxScarlett: + cmakeArgs += " -DTARGET_BUILD_PLATFORM=windows -DSTATIC_WINCRT=0"; + cmakeName = "windows"; + binariesPostfix = "_x64"; + break; + case TargetPlatform.PS4: + cmakeArgs += " -DTARGET_BUILD_PLATFORM=ps4"; + cmakeArgs += $" -DCMAKE_TOOLCHAIN_FILE=\"{Path.Combine(nvCloth, "Externals/CMakeModules/ps4/PS4Toolchain.txt").Replace('\\', '/')}\""; + cmakeName = "ps4"; + binariesPrefix = "lib"; + break; + case TargetPlatform.PS5: + cmakeArgs += " -DTARGET_BUILD_PLATFORM=ps5"; + cmakeArgs += $" -DCMAKE_TOOLCHAIN_FILE=\"{Path.Combine(nvCloth, "Externals/CMakeModules/ps5/PS5Toolchain.txt").Replace('\\', '/')}\""; + cmakeName = "ps5"; + binariesPrefix = "lib"; + break; + case TargetPlatform.Switch: + cmakeArgs += " -DTARGET_BUILD_PLATFORM=NX64"; + cmakeName = "switch"; + binariesPrefix = "lib"; + envVars.Add("NintendoSdkRoot", Sdk.Get("SwitchSdk").RootPath + '\\'); + break; + case TargetPlatform.Android: + cmakeArgs += " -DTARGET_BUILD_PLATFORM=android"; + cmakeName = "android"; + binariesPrefix = "lib"; + if (AndroidNdk.Instance.IsValid) + { + envVars.Add("ANDROID_NDK_HOME", AndroidNdk.Instance.RootPath); + envVars.Add("PM_ANDROIDNDK_PATH", AndroidNdk.Instance.RootPath); + } + break; + default: throw new InvalidPlatformException(platform); + } + var cmakeFolder = Path.Combine(nvCloth, "compiler", "cmake", cmakeName); + + // Setup build environment variables for the build system + switch (BuildPlatform) + { + case TargetPlatform.Windows: + { + GetMsBuildForPlatform(platform, out var vsVersion, out var msBuild); + if (File.Exists(msBuild)) + { + envVars.Add("PATH", Path.GetDirectoryName(msBuild)); + } + break; + } + } + + // Print the NvCloth version + Log.Info($"Building {File.ReadAllLines(Path.Combine(root, "README.md"))[0].Trim()} to {platform} {architecture}"); + + // Generate project files + SetupDirectory(buildFolder, false); + Utilities.FileDelete(Path.Combine(cmakeFolder, "CMakeCache.txt")); + cmakeArgs += $" -DPX_STATIC_LIBRARIES=1 -DPX_OUTPUT_DLL_DIR=\"{Path.Combine(buildFolder, "bin")}\" -DPX_OUTPUT_LIB_DIR=\"{Path.Combine(buildFolder, "lib")}\" -DPX_OUTPUT_EXE_DIR=\"{Path.Combine(buildFolder, "bin")}\""; + RunCmake(cmakeFolder, platform, architecture, " -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF " + cmakeArgs, envVars); + + // Run build + Utilities.Run("cmake", "--build . --config Release", null, cmakeFolder, Utilities.RunOptions.ThrowExceptionOnError, envVars); + + // Deploy binaries + var libs = new[] + { + "NvCloth", + }; + var dstBinaries = GetThirdPartyFolder(options, platform, architecture); + var srcBinaries = Path.Combine(buildFolder, "lib"); + var platformObj = Platform.GetPlatform(platform); + var binariesExtension = platformObj.StaticLibraryFileExtension; + foreach (var lib in libs) + { + var filename = binariesPrefix + lib + binariesPostfix + binariesExtension; + Utilities.FileCopy(Path.Combine(srcBinaries, filename), Path.Combine(dstBinaries, filename)); + + var filenamePdb = Path.ChangeExtension(filename, "pdb"); + if (File.Exists(Path.Combine(srcBinaries, filenamePdb))) + Utilities.FileCopy(Path.Combine(srcBinaries, filenamePdb), Path.Combine(dstBinaries, filenamePdb)); + } + } + } +}