Add **Cloth** simulation with physics

This commit is contained in:
Wojtek Figat
2023-07-03 10:38:36 +02:00
parent 8818b3b07c
commit f81989a171
9 changed files with 1586 additions and 6 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -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<ModelInstanceActor>(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<void(const MeshBase*, MeshDeformationData&)> deformer;
deformer.Bind<Cloth, &Cloth::RunClothDeformer>(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<const Float4> 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<uint16>()[index];
i1 = indicesData.Get<uint16>()[index + 1];
i2 = indicesData.Get<uint16>()[index + 2];
}
else
{
i0 = indicesData.Get<uint32>()[index];
i1 = indicesData.Get<uint32>()[index + 1];
i2 = indicesData.Get<uint32>()[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<const Float4> 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<uint16>()[index];
i1 = indicesData.Get<uint16>()[index + 1];
i2 = indicesData.Get<uint16>()[index + 2];
}
else
{
i0 = indicesData.Get<uint32>()[index];
i1 = indicesData.Get<uint32>()[index + 1];
i2 = indicesData.Get<uint32>()[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<Cloth, &Cloth::DrawPhysicsDebug>(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<Cloth, &Cloth::DrawPhysicsDebug>(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<void(const MeshBase*, MeshDeformationData&)> deformer;
deformer.Bind<Cloth, &Cloth::RunClothDeformer>(this);
deformation->AddDeformer(mesh.LODIndex, mesh.MeshIndex, MeshBufferType::Vertex0, deformer);
_meshDeformation = deformation;
}
#endif
return false;
}
void Cloth::DestroyCloth()
{
#if WITH_CLOTH
if (_meshDeformation)
{
Function<void(const MeshBase*, MeshDeformationData&)> deformer;
deformer.Bind<Cloth, &Cloth::RunClothDeformer>(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<const Float4> 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<AnimatedModel>(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<Matrix> for readonly)
Array<Matrix> 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
}

View File

@@ -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"
/// <summary>
/// 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.
/// </summary>
API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Cloth\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API Cloth : public Actor
{
friend class PhysicsBackend;
DECLARE_SCENE_OBJECT(Cloth);
/// <summary>
/// Cloth response to forces settings.
/// </summary>
API_STRUCT() struct ForceSettings : ISerializable
{
DECLARE_SCRIPTING_TYPE_MINIMAL(ForceSettings);
API_AUTO_SERIALIZATION();
/// <summary>
/// Scale multiplier applied to the gravity of cloth particles (scales the global gravity force).
/// </summary>
API_FIELD() float GravityScale = 1.0f;
/// <summary>
/// Damping of cloth particle velocity. 0: velocity is unaffected. 1: velocity is zeroed.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float Damping = 0.4f;
/// <summary>
/// Portion of velocity applied to cloth particles. 0: cloth particles are unaffected. 1: damped global cloth particle velocity.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float LinearDrag = 0.2f;
/// <summary>
/// Portion of angular velocity applied to turning cloth particles. 0: cloth particles are unaffected. 1: damped global cloth particle angular velocity.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float AngularDrag = 0.2f;
/// <summary>
/// Portion of linear acceleration applied to cloth particles. 0: cloth particles are unaffected. 1: physically correct linear acceleration.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float LinearInertia = 1.0f;
/// <summary>
/// Portion of angular acceleration applied to turning cloth particles. 0: cloth particles are unaffected. 1: physically correct angular acceleration.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float AngularInertia = 1.0f;
/// <summary>
/// Portion of angular velocity applied to turning cloth particles. 0: cloth particles are unaffected. 1: physically correct angular velocity.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float CentrifugalInertia = 1.0f;
/// <summary>
/// Defines how much drag air applies to the cloth particles. Set to 0 to disable wind.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float AirDragCoefficient = 0.0f;
/// <summary>
/// Defines how much lift air applies to the cloth particles. Set to 0 to disable wind.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float AirLiftCoefficient = 0.0f;
/// <summary>
/// Defines fluid density of air used for drag and lift calculations.
/// </summary>
API_FIELD(Attributes="Limit(0)") float AirDensity = 1.0f;
};
/// <summary>
/// Cloth response to collisions settings.
/// </summary>
API_STRUCT() struct CollisionSettings : ISerializable
{
DECLARE_SCRIPTING_TYPE_MINIMAL(CollisionSettings);
API_AUTO_SERIALIZATION();
/// <summary>
/// Controls the amount of friction between cloth particles and colliders. 0: friction disabled.
/// </summary>
API_FIELD(Attributes="Limit(0)") float Friction = 0.1f;
/// <summary>
/// Controls how quickly cloth particle mass is increased during collisions. 0: mass scale disabled.
/// </summary>
API_FIELD(Attributes="Limit(0)") float MassScale = 0.0f;
/// <summary>
/// Enables collisions with scene geometry (both dynamic and static). Disable this to improve performance of cloth that doesn't need to collide.
/// </summary>
API_FIELD() bool SceneCollisions = true;
/// <summary>
/// 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.
/// </summary>
API_FIELD() bool ContinuousCollisionDetection = false;
/// <summary>
/// The minimum distance that the colliding cloth particles must maintain from each other in meters. 0: self collision disabled.
/// </summary>
API_FIELD(Attributes="Limit(0)") float SelfCollisionDistance = 0.0f;
/// <summary>
/// Stiffness for the self collision constraints. 0: self collision disabled.
/// </summary>
API_FIELD(Attributes="Limit(0)") float SelfCollisionStiffness = 0.2f;
};
/// <summary>
/// Cloth simulation settings.
/// </summary>
API_STRUCT() struct SimulationSettings : ISerializable
{
DECLARE_SCRIPTING_TYPE_MINIMAL(SimulationSettings);
API_AUTO_SERIALIZATION();
/// <summary>
/// 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.
/// </summary>
API_FIELD() float SolverFrequency = 300.0f;
/// <summary>
/// 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.
/// </summary>
API_FIELD() Vector3 WindVelocity = Vector3::Zero;
};
/// <summary>
/// Cloth's fabric settings (material's stiffness and compression response) for a single axis.
/// </summary>
API_STRUCT() struct FabricAxisSettings : ISerializable
{
DECLARE_SCRIPTING_TYPE_MINIMAL(FabricAxisSettings);
API_AUTO_SERIALIZATION();
/// <summary>
/// Stiffness value for stretch and compression constraints. 0: disables it.
/// </summary>
API_FIELD(Attributes="Limit(0)") float Stiffness = 1.0f;
/// <summary>
/// Scale value for stretch and compression constraints. 0: no stretch and compression constraints applied. 1: fully apply stretch and compression constraints.
/// </summary>
API_FIELD(Attributes="Limit(0, 1)") float StiffnessMultiplier = 1.0f;
/// <summary>
/// Compression limit for constraints.
/// </summary>
API_FIELD(Attributes="Limit(0)") float CompressionLimit = 1.0f;
/// <summary>
/// Stretch limit for constraints.
/// </summary>
API_FIELD(Attributes="Limit(0)") float StretchLimit = 1.0f;
};
/// <summary>
/// Cloth's fabric settings (material's stiffness and compression response).
/// </summary>
API_STRUCT() struct FabricSettings : ISerializable
{
DECLARE_SCRIPTING_TYPE_MINIMAL(FabricStiffnessSettings);
API_AUTO_SERIALIZATION();
/// <summary>
/// Vertical constraints for stretching or compression (along the gravity).
/// </summary>
API_FIELD() FabricAxisSettings Vertical;
/// <summary>
/// Horizontal constraints for stretching or compression (perpendicular to the gravity).
/// </summary>
API_FIELD() FabricAxisSettings Horizontal;
/// <summary>
/// Bending constraints for out-of-plane bending in angle-based formulation.
/// </summary>
API_FIELD() FabricAxisSettings Bending;
/// <summary>
/// Shearing constraints for plane shearing along (typically) diagonal edges.
/// </summary>
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:
/// <summary>
/// Gets the mesh to use for the cloth simulation (single mesh from specific LOD).
/// </summary>
/// <remarks></remarks>
API_PROPERTY(Attributes="EditorOrder(0), EditorDisplay(\"Cloth\")")
ModelInstanceActor::MeshReference GetMesh() const;
/// <summary>
/// Sets the mesh to use for the cloth simulation (single mesh from specific LOD).
/// </summary>
API_PROPERTY() void SetMesh(const ModelInstanceActor::MeshReference& value);
/// <summary>
/// Gets the cloth response to forces settings.
/// </summary>
API_PROPERTY(Attributes="EditorOrder(10), EditorDisplay(\"Cloth\")")
FORCE_INLINE ForceSettings GetForce() const
{
return _forceSettings;
}
/// <summary>
/// Sets the cloth response to forces settings.
/// </summary>
API_PROPERTY() void SetForce(const ForceSettings& value);
/// <summary>
/// Gets the cloth response to collisions settings.
/// </summary>
API_PROPERTY(Attributes="EditorOrder(20), EditorDisplay(\"Cloth\")")
FORCE_INLINE CollisionSettings GetCollision() const
{
return _collisionSettings;
}
/// <summary>
/// Sets the cloth response to collisions settings.
/// </summary>
API_PROPERTY() void SetCollision(const CollisionSettings& value);
/// <summary>
/// Gets the cloth simulation settings.
/// </summary>
API_PROPERTY(Attributes="EditorOrder(30), EditorDisplay(\"Cloth\")")
FORCE_INLINE SimulationSettings GetSimulation() const
{
return _simulationSettings;
}
/// <summary>
/// Sets the cloth simulation settings.
/// </summary>
API_PROPERTY() void SetSimulation(const SimulationSettings& value);
/// <summary>
/// Gets the cloth's fabric settings (material's stiffness and compression response).
/// </summary>
API_PROPERTY(Attributes="EditorOrder(40), EditorDisplay(\"Cloth\")")
FORCE_INLINE FabricSettings GetFabric() const
{
return _fabricSettings;
}
/// <summary>
/// Sets the cloth's fabric settings (material's stiffness and compression response).
/// </summary>
API_PROPERTY() void SetFabric(const FabricSettings& value);
public:
/// <summary>
/// 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).
/// </summary>
API_FUNCTION() void Rebuild();
/// <summary>
/// Sets the inertia derived from transform change to zero (once). It will reset any cloth object movement effects as it was teleported.
/// </summary>
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);
};

View File

@@ -38,6 +38,17 @@
#include <ThirdParty/PhysX/vehicle/PxVehicleUtilSetup.h>
#include <ThirdParty/PhysX/PxFiltering.h>
#endif
#if WITH_CLOTH
#include "Engine/Physics/Actors/Cloth.h"
#include <ThirdParty/NvCloth/Callbacks.h>
#include <ThirdParty/NvCloth/Factory.h>
#include <ThirdParty/NvCloth/Cloth.h>
#include <ThirdParty/NvCloth/Fabric.h>
#include <ThirdParty/NvCloth/Solver.h>
#include <ThirdParty/NvCloth/NvClothExt/ClothFabricCooker.h>
#define MAX_CLOTH_SPHERE_COUNT 32
#define MAX_CLOTH_PLANE_COUNT 32
#endif
#if WITH_PVD
#include <ThirdParty/PhysX/pvd/PxPvd.h>
#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<int32_t>::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<BoundingBox&>(Actor->GetBox()));
BoundingSphere::FromBox(Actor->GetBox(), const_cast<BoundingSphere&>(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<float> WheelTireTypes;
WheelFilterPhysX WheelRaycastFilter;
#endif
#if WITH_CLOTH
nv::cloth::Factory* ClothFactory = nullptr;
Dictionary<nv::cloth::Fabric*, FabricSettings> Fabrics;
Dictionary<nv::cloth::Cloth*, ClothSettings> 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<const PxVec4>(), 0, clothPhysX->getNumSpheres());
clothPhysX->setPlanes(nv::cloth::Range<const PxVec4>(), 0, clothPhysX->getNumPlanes());
clothPhysX->setTriangles(nv::cloth::Range<const PxVec3>(), 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<PxOverlapHit> 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<const PxVec4> 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<const PxVec4> 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<const uint32_t>(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>((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<const PxU32>(&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<const PxPlane&>(polygon.mPlane), convexToShapeInv).transform(shapeToCloth);
}
clothPhysX->setPlanes(nv::cloth::Range<const PxVec4>((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<const PxU32>(&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<PxVec3> 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<PxVec3> 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<const PxVec3>(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<int32_t>::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<Float4> 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<PxVec4> 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<const PxVec4>(), 0, clothPhysX->getNumSpheres());
clothPhysX->setPlanes(nv::cloth::Range<const PxVec4>(), 0, clothPhysX->getNumPlanes());
clothPhysX->setTriangles(nv::cloth::Range<const PxVec3>(), 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<nv::cloth::PhaseConfig> 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<const nv::cloth::PhaseConfig>(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<const Float4> PhysicsBackend::GetClothCurrentParticles(void* cloth)
{
auto clothPhysX = (const nv::cloth::Cloth*)cloth;
const nv::cloth::MappedRange<const PxVec4> range = clothPhysX->getCurrentParticles();
return Span<const Float4>((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);

View File

@@ -14,6 +14,16 @@ public class Physics : EngineModule
/// </summary>
public static bool WithCooking = true;
/// <summary>
/// Enables using vehicles simulation.
/// </summary>
public static bool WithVehicle = true;
/// <summary>
/// Enables using cloth simulation.
/// </summary>
public static bool WithCloth = true;
/// <summary>
/// Enables using PhysX library. Can be overriden by SetupPhysicsBackend.
/// </summary>
@@ -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
{

View File

@@ -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;
};
/// <summary>
/// Interface for the physical simulation backend implementation.
/// </summary>
@@ -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<const Float4> 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);

View File

@@ -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;

View File

@@ -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
{
/// <summary>
/// NVIDIA NvCloth
/// </summary>
/// <seealso cref="Flax.Deps.Dependency" />
class NvCloth : Dependency
{
private string root, nvCloth;
/// <inheritdoc />
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];
}
}
}
/// <inheritdoc />
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<string, string>();
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));
}
}
}
}