/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace NvCloth { AZ_CVAR(float, cloth_DistanceToTeleport, 0.5f, nullptr, AZ::ConsoleFunctorFlags::Null, "The amount of meters the entity has to move in a frame to consider it a teleport for cloth."); AZ_CVAR(float, cloth_SecondsToDelaySimulationOnActorSpawned, 0.25f, nullptr, AZ::ConsoleFunctorFlags::Null, "The amount of time in seconds the cloth simulation will be delayed to avoid sudden impulses when actors are spawned."); // Helper class to map an RPI buffer from a buffer asset view. template class MappedBuffer { public: MappedBuffer( const AZ::RPI::BufferAssetView* bufferAssetView, const size_t expectedElementCount, const AZ::RHI::Format expectedElementFormat); ~MappedBuffer(); T* GetBuffer() { return m_buffer; } private: AZ::Data::Instance m_rpiBuffer; T* m_buffer = nullptr; }; template MappedBuffer::MappedBuffer( const AZ::RPI::BufferAssetView* bufferAssetView, [[maybe_unused]] const size_t expectedElementCount, [[maybe_unused]] const AZ::RHI::Format expectedElementFormat) { if (!bufferAssetView) { return; } const AZ::RHI::BufferViewDescriptor& bufferViewDescriptor = bufferAssetView->GetBufferViewDescriptor(); AZ_Assert(bufferViewDescriptor.m_elementCount == expectedElementCount, "Unexpected buffer size: expected is %d but descriptor's is %d", expectedElementCount, bufferViewDescriptor.m_elementCount); AZ_Assert(bufferViewDescriptor.m_elementSize == sizeof(T), "Unexpected buffer element size: expected is %d but descriptor's is %d", sizeof(T), bufferViewDescriptor.m_elementSize); AZ_Assert(bufferViewDescriptor.m_elementFormat == expectedElementFormat, "Unexpected buffer format: expected is %d but descriptor's is %d", expectedElementFormat, bufferViewDescriptor.m_elementFormat); const AZ::Data::Asset& bufferAsset = bufferAssetView->GetBufferAsset(); m_rpiBuffer = AZ::RPI::Buffer::FindOrCreate(bufferAsset); if (m_rpiBuffer == nullptr) { AZ_Error("ClothComponentMesh", false, "Failed to find or create RPI buffer from buffer asset '%s'", bufferAsset.GetHint().c_str()); return; } const uint64_t byteCount = aznumeric_cast(bufferViewDescriptor.m_elementCount) * aznumeric_cast(bufferViewDescriptor.m_elementSize); const uint64_t byteOffset = aznumeric_cast(bufferViewDescriptor.m_elementOffset) * aznumeric_cast(bufferViewDescriptor.m_elementSize); m_buffer = static_cast(m_rpiBuffer->Map(byteCount, byteOffset)); } template MappedBuffer::~MappedBuffer() { if (m_buffer) { m_rpiBuffer->Unmap(); } } ClothComponentMesh::ClothComponentMesh(AZ::EntityId entityId, const ClothConfiguration& config) : m_preSimulationEventHandler( [this](ClothId clothId, float deltaTime) { this->OnPreSimulation(clothId, deltaTime); }) , m_postSimulationEventHandler( [this](ClothId clothId, float deltaTime, const AZStd::vector& updatedParticles) { this->OnPostSimulation(clothId, deltaTime, updatedParticles); }) { Setup(entityId, config); } ClothComponentMesh::~ClothComponentMesh() { TearDown(); } void ClothComponentMesh::UpdateConfiguration(AZ::EntityId entityId, const ClothConfiguration& config) { if (m_entityId != entityId || m_config.m_meshNode != config.m_meshNode || m_config.m_removeStaticTriangles != config.m_removeStaticTriangles) { Setup(entityId, config); } else if (m_cloth) { m_config = config; ApplyConfigurationToCloth(); // Update the cloth constraints parameters m_clothConstraints->SetMotionConstraintMaxDistance(m_config.m_motionConstraintsMaxDistance); m_clothConstraints->SetBackstopMaxRadius(m_config.m_backstopRadius); m_clothConstraints->SetBackstopMaxOffsets(m_config.m_backstopBackOffset, m_config.m_backstopFrontOffset); UpdateSimulationConstraints(); // Subscribe to WindNotificationsBus only if custom wind velocity flag is not set if (m_config.IsUsingWindBus()) { Physics::WindNotificationsBus::Handler::BusConnect(); } else { Physics::WindNotificationsBus::Handler::BusDisconnect(); } } } void ClothComponentMesh::Setup(AZ::EntityId entityId, const ClothConfiguration& config) { TearDown(); m_entityId = entityId; m_config = config; if (!CreateCloth()) { TearDown(); return; } // Initialize render data m_renderDataBufferIndex = 0; { auto& renderData = GetRenderData(); renderData.m_particles = m_meshClothInfo.m_particles; renderData.m_tangents = m_meshClothInfo.m_tangents; renderData.m_bitangents = m_meshClothInfo.m_bitangents; renderData.m_normals = m_meshClothInfo.m_normals; } UpdateRenderData(m_cloth->GetParticles()); // Copy the first initialized element to the rest of the buffer for (AZ::u32 i = 1; i < RenderDataBufferSize; ++i) { m_renderDataBuffer[i] = m_renderDataBuffer[0]; } // It will return a valid instance if it's an actor with cloth colliders in it. m_actorClothColliders = ActorClothColliders::Create(m_entityId); // It will return a valid instance if it's an actor with skinning data. m_actorClothSkinning = ActorClothSkinning::Create( m_entityId, m_meshNodeInfo, m_meshClothInfo.m_particles, m_cloth->GetParticles().size(), m_meshRemappedVertices); m_timeClothSkinningUpdates = 0.0f; m_clothConstraints = ClothConstraints::Create( m_meshClothInfo.m_motionConstraints, m_config.m_motionConstraintsMaxDistance, m_meshClothInfo.m_backstopData, m_config.m_backstopRadius, m_config.m_backstopBackOffset, m_config.m_backstopFrontOffset, m_cloth->GetParticles(), m_cloth->GetInitialIndices(), m_meshRemappedVertices); AZ_Assert(m_clothConstraints, "Failed to create cloth constraints"); UpdateSimulationConstraints(); #ifndef RELEASE m_clothDebugDisplay = AZStd::make_unique(this); #endif AZ::TransformNotificationBus::Handler::BusConnect(m_entityId); AZ::TickBus::Handler::BusConnect(); m_cloth->ConnectPreSimulationEventHandler(m_preSimulationEventHandler); m_cloth->ConnectPostSimulationEventHandler(m_postSimulationEventHandler); if (m_config.IsUsingWindBus()) { Physics::WindNotificationsBus::Handler::BusConnect(); } } void ClothComponentMesh::TearDown() { if (m_cloth) { Physics::WindNotificationsBus::Handler::BusDisconnect(); AZ::TickBus::Handler::BusDisconnect(); AZ::TransformNotificationBus::Handler::BusDisconnect(); m_preSimulationEventHandler.Disconnect(); m_postSimulationEventHandler.Disconnect(); AZ::Interface::Get()->RemoveCloth(m_cloth); AZ::Interface::Get()->DestroyCloth(m_cloth); } m_entityId.SetInvalid(); m_renderDataBuffer = {}; m_meshRemappedVertices.clear(); m_meshNodeInfo = {}; m_meshClothInfo = {}; m_actorClothColliders.reset(); m_actorClothSkinning.reset(); m_clothConstraints.reset(); m_motionConstraints.clear(); m_separationConstraints.clear(); m_clothDebugDisplay.reset(); } void ClothComponentMesh::OnPreSimulation( [[maybe_unused]] ClothId clothId, float deltaTime) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); UpdateSimulationCollisions(); if (m_actorClothSkinning) { UpdateSimulationSkinning(deltaTime); UpdateSimulationConstraints(); } } void ClothComponentMesh::OnPostSimulation( [[maybe_unused]] ClothId clothId, [[maybe_unused]] float deltaTime, const AZStd::vector& updatedParticles) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); // Next buffer index of the render data m_renderDataBufferIndex = (m_renderDataBufferIndex + 1) % RenderDataBufferSize; UpdateRenderData(updatedParticles); } void ClothComponentMesh::OnTransformChanged([[maybe_unused]] const AZ::Transform& local, const AZ::Transform& world) { // At the moment there is no way to distinguish "move" from "teleport". // As a workaround we will consider a teleport if the position has changed considerably. bool teleport = (m_worldPosition.GetDistance(world.GetTranslation()) >= cloth_DistanceToTeleport); if (teleport) { TeleportCloth(world); } else { MoveCloth(world); } } void ClothComponentMesh::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { CopyRenderDataToModel(); } int ClothComponentMesh::GetTickOrder() { return AZ::TICK_PRE_RENDER; } void ClothComponentMesh::OnGlobalWindChanged() { m_cloth->GetClothConfigurator()->SetWindVelocity(GetWindBusVelocity()); } void ClothComponentMesh::OnWindChanged([[maybe_unused]] const AZ::Aabb& aabb) { OnGlobalWindChanged(); } ClothComponentMesh::RenderData& ClothComponentMesh::GetRenderData() { return const_cast( static_cast(*this).GetRenderData()); } const ClothComponentMesh::RenderData& ClothComponentMesh::GetRenderData() const { return m_renderDataBuffer[m_renderDataBufferIndex]; } void ClothComponentMesh::UpdateSimulationCollisions() { if (m_actorClothColliders) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); m_actorClothColliders->Update(); const auto& spheres = m_actorClothColliders->GetSpheres(); m_cloth->GetClothConfigurator()->SetSphereColliders(spheres); const auto& capsuleIndices = m_actorClothColliders->GetCapsuleIndices(); m_cloth->GetClothConfigurator()->SetCapsuleColliders(capsuleIndices); } } void ClothComponentMesh::UpdateSimulationSkinning(float deltaTime) { if (m_actorClothSkinning) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); m_actorClothSkinning->UpdateSkinning(); // Since component activation order is not trivial, the actor's pose might not be updated // immediately. Because of this cloth will receive a sudden impulse when changing from // T pose to animated pose. To avoid this undesired effect we will override cloth simulation during // a short amount of time. m_timeClothSkinningUpdates += deltaTime; // While the actor is not visible the skinned joints are not updated. Then when // it becomes visible the jump to the new skinned positions causes a sudden // impulse to cloth simulation. To avoid this undesired effect we will override cloth simulation during // a short amount of time. m_actorClothSkinning->UpdateActorVisibility(); if (!m_actorClothSkinning->WasActorVisible() && m_actorClothSkinning->IsActorVisible()) { m_timeClothSkinningUpdates = 0.0f; } if (m_timeClothSkinningUpdates <= cloth_SecondsToDelaySimulationOnActorSpawned) { // Update skinning for all particles and apply it to cloth AZStd::vector particles = m_cloth->GetParticles(); m_actorClothSkinning->ApplySkinning(m_cloth->GetInitialParticles(), particles); m_cloth->SetParticles(AZStd::move(particles)); m_cloth->DiscardParticleDelta(); } } } void ClothComponentMesh::UpdateSimulationConstraints() { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); m_motionConstraints = m_clothConstraints->GetMotionConstraints(); m_separationConstraints = m_clothConstraints->GetSeparationConstraints(); if (m_actorClothSkinning) { m_actorClothSkinning->ApplySkinning(m_clothConstraints->GetMotionConstraints(), m_motionConstraints); m_actorClothSkinning->ApplySkinning(m_clothConstraints->GetSeparationConstraints(), m_separationConstraints); } m_cloth->GetClothConfigurator()->SetMotionConstraints(m_motionConstraints); if (!m_separationConstraints.empty()) { m_cloth->GetClothConfigurator()->SetSeparationConstraints(m_separationConstraints); } } void ClothComponentMesh::UpdateRenderData(const AZStd::vector& particles) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); if (!m_cloth) { return; } auto& renderData = GetRenderData(); if (m_actorClothSkinning) { // Apply skinning to the non-simulated part of the mesh. m_actorClothSkinning->ApplySkinningOnNonSimulatedVertices(m_meshClothInfo, renderData); } // Calculate normals of the cloth particles (simplified mesh). AZStd::vector normals; [[maybe_unused]] bool normalsCalculated = AZ::Interface::Get()->CalculateNormals(particles, m_cloth->GetInitialIndices(), normals); AZ_Assert(normalsCalculated, "Cloth component mesh failed to calculate normals."); // Copy particles and normals to render data. // Since cloth's vertices were welded together, // the full mesh will result in smooth normals. for (size_t index = 0; index < m_meshRemappedVertices.size(); ++index) { const int remappedIndex = m_meshRemappedVertices[index]; if (remappedIndex >= 0) { renderData.m_particles[index] = particles[remappedIndex]; // For static particles only use the updated normal when indicated in the configuration. const bool useSimulatedClothParticleNormal = m_meshClothInfo.m_particles[index].GetW() != 0.0f || m_config.m_updateNormalsOfStaticParticles; if (useSimulatedClothParticleNormal) { renderData.m_normals[index] = normals[remappedIndex]; } } } // Calculate tangents and bitangents for the full mesh. [[maybe_unused]] bool tangentsAndBitangentsCalculated = AZ::Interface::Get()->CalculateTangentsAndBitagents( renderData.m_particles, m_meshClothInfo.m_indices, m_meshClothInfo.m_uvs, renderData.m_normals, renderData.m_tangents, renderData.m_bitangents); AZ_Assert(tangentsAndBitangentsCalculated, "Cloth component mesh failed to calculate tangents and bitangents."); } void ClothComponentMesh::CopyRenderDataToModel() { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::Cloth); // Previous buffer index of the render data const AZ::u32 previousBufferIndex = (m_renderDataBufferIndex + RenderDataBufferSize - 1) % RenderDataBufferSize; // Workaround to sync debug drawing with cloth rendering as // the Entity Debug Display Bus renders on the next frame. const bool isDebugDrawEnabled = m_clothDebugDisplay && m_clothDebugDisplay->IsDebugDrawEnabled(); const RenderData& renderData = (isDebugDrawEnabled) ? m_renderDataBuffer[previousBufferIndex] : m_renderDataBuffer[m_renderDataBufferIndex]; const auto& renderParticles = renderData.m_particles; const auto& renderNormals = renderData.m_normals; const auto& renderTangents = renderData.m_tangents; const auto& renderBitangents = renderData.m_bitangents; // Since Atom has a 1:1 relation with between ModelAsset buffers and Model buffers, // internally it created a new asset for the model instance. So it's important to // get the asset from the model when we want to write to them, instead of getting the // ModelAsset directly from the bus (which returns the original asset shared by all entities). AZ::Data::Instance model; AZ::Render::MeshComponentRequestBus::EventResult(model, m_entityId, &AZ::Render::MeshComponentRequestBus::Events::GetModel); if (!model) { return; } AZ::Data::Asset modelAsset = model->GetModelAsset(); if (!modelAsset.IsReady()) { return; } if (modelAsset->GetLodCount() < m_meshNodeInfo.m_lodLevel) { AZ_Error("ClothComponentMesh", false, "Unable to access lod %d from model asset '%s' as it only has %d lod levels.", m_meshNodeInfo.m_lodLevel, modelAsset.GetHint().c_str(), modelAsset->GetLodCount()); return; } const AZ::Data::Asset& modelLodAsset = modelAsset->GetLodAssets()[m_meshNodeInfo.m_lodLevel]; if (!modelLodAsset.GetId().IsValid()) { AZ_Error("ClothComponentMesh", false, "Model asset '%s' returns an invalid lod asset '%s' (lod level %d).", modelAsset.GetHint().c_str(), modelLodAsset.GetHint().c_str(), m_meshNodeInfo.m_lodLevel); return; } const AZ::Name positionSemantic("POSITION"); const AZ::Name normalSemantic("NORMAL"); const AZ::Name tangentSemantic("TANGENT"); const AZ::Name bitangentSemantic("BITANGENT"); // For each submesh... for (const auto& subMeshInfo : m_meshNodeInfo.m_subMeshes) { if (modelLodAsset->GetMeshes().size() < subMeshInfo.m_primitiveIndex) { AZ_Error("ClothComponentMesh", false, "Unable to access submesh %d from lod asset '%s' as it only has %d submeshes.", subMeshInfo.m_primitiveIndex, modelAsset.GetHint().c_str(), modelLodAsset->GetMeshes().size()); continue; } const AZ::RPI::ModelLodAsset::Mesh& subMesh = modelLodAsset->GetMeshes()[subMeshInfo.m_primitiveIndex]; const int numVertices = subMeshInfo.m_numVertices; const int firstVertex = subMeshInfo.m_verticesFirstIndex; if (subMesh.GetVertexCount() != numVertices) { AZ_Error("ClothComponentMesh", false, "Render mesh to be modified doesn't have the same number of vertices (%d) as the cloth's submesh (%d).", subMesh.GetVertexCount(), numVertices); continue; } AZ_Assert(firstVertex >= 0, "Invalid first vertex index %d", firstVertex); AZ_Assert((firstVertex + numVertices) <= static_cast(renderParticles.size()), "Submesh number of vertices (%d) reaches outside the particles (%zu)", (firstVertex + numVertices), renderParticles.size()); MappedBuffer destVertices(subMesh.GetSemanticBufferAssetView(positionSemantic), numVertices, AZ::RHI::Format::R32G32B32_FLOAT); MappedBuffer destNormals(subMesh.GetSemanticBufferAssetView(normalSemantic), numVertices, AZ::RHI::Format::R32G32B32_FLOAT); MappedBuffer destTangents(subMesh.GetSemanticBufferAssetView(tangentSemantic), numVertices, AZ::RHI::Format::R32G32B32A32_FLOAT); MappedBuffer destBitangents(subMesh.GetSemanticBufferAssetView(bitangentSemantic), numVertices, AZ::RHI::Format::R32G32B32_FLOAT); auto* destVerticesBuffer = destVertices.GetBuffer(); auto* destNormalsBuffer = destNormals.GetBuffer(); auto* destTangentsBuffer = destTangents.GetBuffer(); auto* destBitangentsBuffer = destBitangents.GetBuffer(); if (!destVerticesBuffer) { AZ_Error("ClothComponentMesh", AZ::RHI::IsNullRenderer(), "Invalid vertex position buffer obtained from the render mesh to be modified."); continue; } for (size_t index = 0; index < numVertices; ++index) { const int renderVertexIndex = firstVertex + index; const SimParticleFormat& renderParticle = renderParticles[renderVertexIndex]; destVerticesBuffer[index].Set( renderParticle.GetX(), renderParticle.GetY(), renderParticle.GetZ()); if (destNormalsBuffer) { const AZ::Vector3& renderNormal = renderNormals[renderVertexIndex]; destNormalsBuffer[index].Set( renderNormal.GetX(), renderNormal.GetY(), renderNormal.GetZ()); } if (destTangentsBuffer) { const AZ::Vector3& renderTangent = renderTangents[renderVertexIndex]; destTangentsBuffer[index].Set( renderTangent, 1.0f); } if (destBitangentsBuffer) { const AZ::Vector3& renderBitangent = renderBitangents[renderVertexIndex]; destBitangentsBuffer[index].Set( renderBitangent.GetX(), renderBitangent.GetY(), renderBitangent.GetZ()); } } } } bool ClothComponentMesh::CreateCloth() { AZStd::unique_ptr assetHelper = AssetHelper::CreateAssetHelper(m_entityId); if (!assetHelper) { return false; } // Obtain cloth mesh info bool clothInfoObtained = assetHelper->ObtainClothMeshNodeInfo(m_config.m_meshNode, m_meshNodeInfo, m_meshClothInfo); if (!clothInfoObtained) { return false; } // Generate a simplified mesh for simulation AZStd::vector meshSimplifiedParticles; AZStd::vector meshSimplifiedIndices; AZ::Interface::Get()->SimplifyMesh( m_meshClothInfo.m_particles, m_meshClothInfo.m_indices, meshSimplifiedParticles, meshSimplifiedIndices, m_meshRemappedVertices, m_config.m_removeStaticTriangles); if (meshSimplifiedParticles.empty() || meshSimplifiedIndices.empty()) { return false; } // Cook Fabric AZStd::optional cookedData = AZ::Interface::Get()->CookFabric(meshSimplifiedParticles, meshSimplifiedIndices); if (!cookedData) { return false; } // Create cloth instance m_cloth = AZ::Interface::Get()->CreateCloth(meshSimplifiedParticles, *cookedData); if (!m_cloth) { return false; } // Set initial Position and Rotation AZ::Transform transform = AZ::Transform::CreateIdentity(); AZ::TransformBus::EventResult(transform, m_entityId, &AZ::TransformInterface::GetWorldTM); TeleportCloth(transform); ApplyConfigurationToCloth(); // Add cloth to default solver to be simulated AZ::Interface::Get()->AddCloth(m_cloth); return true; } void ClothComponentMesh::ApplyConfigurationToCloth() { IClothConfigurator* clothConfig = m_cloth->GetClothConfigurator(); // Mass clothConfig->SetMass(m_config.m_mass); // Gravity and scale if (m_config.IsUsingWorldBusGravity()) { AZ::Vector3 gravity = AzPhysics::DefaultGravity; if (auto* sceneInterface = AZ::Interface::Get()) { AzPhysics::SceneHandle defaultScene = sceneInterface->GetSceneHandle(AzPhysics::DefaultPhysicsSceneName); if (defaultScene != AzPhysics::InvalidSceneHandle) { gravity = sceneInterface->GetGravity(defaultScene); } } clothConfig->SetGravity(gravity * m_config.m_gravityScale); } else { clothConfig->SetGravity(m_config.m_customGravity * m_config.m_gravityScale); } // Stiffness Frequency clothConfig->SetStiffnessFrequency(m_config.m_stiffnessFrequency); // Motion constraints parameters clothConfig->SetMotionConstraintsScale(m_config.m_motionConstraintsScale); clothConfig->SetMotionConstraintsBias(m_config.m_motionConstraintsBias); clothConfig->SetMotionConstraintsStiffness(m_config.m_motionConstraintsStiffness); // Damping parameters clothConfig->SetDamping(m_config.m_damping); clothConfig->SetDampingLinearDrag(m_config.m_linearDrag); clothConfig->SetDampingAngularDrag(m_config.m_angularDrag); // Inertia parameters clothConfig->SetLinearInertia(m_config.m_linearInteria); clothConfig->SetAngularInertia(m_config.m_angularInteria); clothConfig->SetCentrifugalInertia(m_config.m_centrifugalInertia); // Wind parameters if (m_config.IsUsingWindBus()) { clothConfig->SetWindVelocity(GetWindBusVelocity()); } else { clothConfig->SetWindVelocity(m_config.m_windVelocity); } clothConfig->SetWindDragCoefficient(m_config.m_airDragCoefficient); clothConfig->SetWindLiftCoefficient(m_config.m_airLiftCoefficient); clothConfig->SetWindFluidDensity(m_config.m_fluidDensity); // Collision parameters clothConfig->SetCollisionFriction(m_config.m_collisionFriction); clothConfig->SetCollisionMassScale(m_config.m_collisionMassScale); clothConfig->EnableContinuousCollision(m_config.m_continuousCollisionDetection); clothConfig->SetCollisionAffectsStaticParticles(m_config.m_collisionAffectsStaticParticles); // Self Collision parameters clothConfig->SetSelfCollisionDistance(m_config.m_selfCollisionDistance); clothConfig->SetSelfCollisionStiffness(m_config.m_selfCollisionStiffness); // Tether Constraints parameters clothConfig->SetTetherConstraintStiffness(m_config.m_tetherConstraintStiffness); clothConfig->SetTetherConstraintScale(m_config.m_tetherConstraintScale); // Quality parameters clothConfig->SetSolverFrequency(m_config.m_solverFrequency); clothConfig->SetAcceleationFilterWidth(m_config.m_accelerationFilterIterations); // Fabric Phases clothConfig->SetVerticalPhaseConfig( m_config.m_verticalStiffness, m_config.m_verticalStiffnessMultiplier, m_config.m_verticalCompressionLimit, m_config.m_verticalStretchLimit); clothConfig->SetHorizontalPhaseConfig( m_config.m_horizontalStiffness, m_config.m_horizontalStiffnessMultiplier, m_config.m_horizontalCompressionLimit, m_config.m_horizontalStretchLimit); clothConfig->SetBendingPhaseConfig( m_config.m_bendingStiffness, m_config.m_bendingStiffnessMultiplier, m_config.m_bendingCompressionLimit, m_config.m_bendingStretchLimit); clothConfig->SetShearingPhaseConfig( m_config.m_shearingStiffness, m_config.m_shearingStiffnessMultiplier, m_config.m_shearingCompressionLimit, m_config.m_shearingStretchLimit); } void ClothComponentMesh::MoveCloth(const AZ::Transform& worldTransform) { m_worldPosition = worldTransform.GetTranslation(); m_cloth->GetClothConfigurator()->SetTransform(worldTransform); if (m_config.IsUsingWindBus()) { // Wind velocity is affected by world position m_cloth->GetClothConfigurator()->SetWindVelocity(GetWindBusVelocity()); } } void ClothComponentMesh::TeleportCloth(const AZ::Transform& worldTransform) { MoveCloth(worldTransform); // By clearing inertia the cloth won't be affected by the sudden translation caused when teleporting the entity. m_cloth->GetClothConfigurator()->ClearInertia(); } AZ::Vector3 ClothComponentMesh::GetWindBusVelocity() { const Physics::WindRequests* windRequests = AZ::Interface::Get(); if (windRequests) { const AZ::Vector3 globalWind = windRequests->GetGlobalWind(); const AZ::Vector3 localWind = windRequests->GetWind(m_worldPosition); return globalWind + localWind; } return AZ::Vector3::CreateZero(); } } // namespace NvCloth