Files
EVEngine/test/physics_cloth.cpp

215 lines
8.8 KiB
C++

#include "ScriptTest.h"
#include "physics/cloth/Cloth3D.h"
#include "physics/cloth/ClothModel.h"
#include <array>
#include <cmath>
extern const char* physics_cloth_content;
UnitSciptTest(PhysicsClothScriptTest, physics_cloth_content);
TEST_CASE_FIXTURE(PhysicsClothScriptTest, "PhysicsCloth.scriptBindings") {
CHECK(vm.callFunc(vm.findFunc("basic"), vm).toBool());
}
TEST_CASE("PhysicsCloth.modelGridCreatesReusableRuntime") {
auto baked = eve::physics::ClothModel::grid(4, 3, 0.5f, -1.f, 2.f, 3.f);
REQUIRE(baked.hasValue());
CHECK(baked.value().schemaVersion() == 1);
CHECK(baked.value().particles().size() == 12);
CHECK(baked.value().triangles().size() == 12);
CHECK(baked.value().gridCols() == 4);
CHECK(baked.value().particles()[0].inverseMass == 0.f);
CHECK(baked.value().particles()[4].inverseMass > 0.f);
eve::physics::Cloth3D first(baked.value());
eve::physics::Cloth3D second(baked.value());
first.setParticlePosition(8, 9.f, 8.f, 7.f);
CHECK(second.getParticleX(8) != 9.f);
first.reset();
CHECK(first.getParticleX(8) == -1.f);
CHECK(first.getParticleY(8) == 2.f);
CHECK(first.getParticleZ(8) == 4.f);
}
TEST_CASE("PhysicsCloth.modelBakesArbitraryTriangleTopologyTransactionally") {
const std::array<float, 12> positions = {0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 0.25f, 1.f, 0.f, 0.f, 1.f};
const std::array<int, 6> indices = {0, 3, 1, 3, 2, 1};
const std::array<float, 4> inverseMasses = {0.f, 0.f, 1.f, 1.f};
auto baked = eve::physics::ClothModel::fromTriangles(positions, indices, inverseMasses);
REQUIRE(baked.hasValue());
CHECK(baked.value().gridCols() == 0);
CHECK(baked.value().particles().size() == 4);
CHECK(baked.value().triangles().size() == 2);
CHECK(baked.value().foldConstraints().size() == 1);
eve::physics::Cloth3D cloth(baked.value());
CHECK(cloth.getParticleCount() == 4);
CHECK(cloth.isPinned(0));
CHECK(cloth.isPinned(1));
cloth.setGravity(0.f, -9.8f, 0.f);
cloth.update(1.f / 60.f);
CHECK(cloth.getParticleY(2) < 0.25f);
const std::array<int, 3> invalidIndices = {0, 1, 9};
auto invalid = eve::physics::ClothModel::fromTriangles(positions, invalidIndices);
CHECK(!invalid.hasValue());
}
TEST_CASE("PhysicsCloth.modelJsonRoundTripIsStrictAndVersioned") {
auto original = eve::physics::ClothModel::grid(3, 3, 0.25f, -2.f, 4.f, 1.f);
REQUIRE(original.hasValue());
auto json = original.value().toJson();
REQUIRE(json.hasValue());
auto restored = eve::physics::ClothModel::fromJson(json.value());
REQUIRE(restored.hasValue());
CHECK(restored.value().gridCols() == 3);
CHECK(restored.value().gridRows() == 3);
CHECK(restored.value().particles().size() == original.value().particles().size());
CHECK(restored.value().distanceConstraints().size() == original.value().distanceConstraints().size());
eve::Value unknownField = original.value().toValue();
unknownField.set("futureField", eve::Value(1));
auto rejectedUnknown = eve::physics::ClothModel::fromValue(unknownField);
CHECK(!rejectedUnknown.hasValue());
REQUIRE(rejectedUnknown.error() != nullptr);
CHECK(rejectedUnknown.error()->code() == eve::DiagnosticCode::ParseError);
eve::Value futureVersion = original.value().toValue();
futureVersion.set("schemaVersion", eve::Value(2));
auto rejectedVersion = eve::physics::ClothModel::fromValue(futureVersion);
CHECK(!rejectedVersion.hasValue());
REQUIRE(rejectedVersion.error() != nullptr);
CHECK(rejectedVersion.error()->code() == eve::DiagnosticCode::UnknownVersion);
}
TEST_CASE("PhysicsCloth.xpbdComplianceProducesTimeStepAwareMaterialResponse") {
auto model = eve::physics::ClothModel::grid(3, 4, 0.5f, 0.f, 0.f, 0.f);
REQUIRE(model.hasValue());
eve::physics::Cloth3D rigid(model.value());
eve::physics::Cloth3D compliant(model.value());
for (auto* cloth : {&rigid, &compliant}) {
cloth->setSelfCollision(false);
cloth->setFoldStiffness(0.f);
cloth->setGravity(0.f, -9.8f, 0.f);
cloth->setIterations(8);
}
compliant.setStretchCompliance(0.005f);
compliant.setShearCompliance(0.005f);
compliant.setBendCompliance(0.02f);
for (int frame = 0; frame < 30; ++frame) {
rigid.update(1.f / 60.f);
compliant.update(1.f / 60.f);
}
CHECK(compliant.getParticleY(10) < rigid.getParticleY(10) - 0.05f);
CHECK(std::isfinite(compliant.getParticleY(10)));
}
TEST_CASE("PhysicsCloth.modelBuildsGeodesicTethersAndRuntimeLimitsExtension") {
auto model = eve::physics::ClothModel::grid(3, 4, 0.5f, 0.f, 0.f, 0.f);
REQUIRE(model.hasValue());
CHECK(model.value().tetherConstraints().size() == 9);
eve::physics::Cloth3D cloth(model.value());
cloth.setGravity(0.f, 0.f, 0.f);
cloth.setStiffness(0.f);
cloth.setFoldStiffness(0.f);
cloth.setSelfCollision(false);
cloth.setTetherScale(1.f);
cloth.setTetherCompliance(0.f);
cloth.setParticlePosition(10, 0.5f, -10.f, 1.5f);
cloth.update(1.f / 60.f);
CHECK(cloth.getParticleY(10) > -2.f);
}
TEST_CASE("PhysicsCloth.closedModelPreservesAndInflatesVolume") {
const std::array<float, 12> positions = {0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f};
const std::array<int, 12> triangles = {0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3};
auto model = eve::physics::ClothModel::fromTriangles(positions, triangles);
REQUIRE(model.hasValue());
CHECK(model.value().isClosed());
CHECK(std::abs(model.value().restVolume() - 1.f / 6.f) < 1e-5f);
eve::physics::Cloth3D cloth(model.value());
cloth.setGravity(0.f, 0.f, 0.f);
cloth.setStiffness(0.f);
cloth.setFoldStiffness(0.f);
cloth.setSelfCollision(false);
cloth.setParticlePosition(3, 0.f, 0.f, 0.25f);
const float collapsed = cloth.getCurrentVolume();
cloth.setPressure(1.f);
cloth.setVolumeCompliance(0.f);
cloth.update(1.f / 60.f);
CHECK(std::abs(cloth.getCurrentVolume() - model.value().restVolume()) <
std::abs(collapsed - model.value().restVolume()));
const float preserved = cloth.getCurrentVolume();
cloth.setPressure(1.25f);
cloth.update(1.f / 60.f);
CHECK(cloth.getCurrentVolume() > preserved);
}
TEST_CASE("PhysicsCloth.skinBackstopAndAnimatedReferenceConstrainParticles") {
auto model = eve::physics::ClothModel::grid(2, 2, 0.5f, 0.f, 0.f, 0.f);
REQUIRE(model.hasValue());
eve::physics::Cloth3D cloth(model.value());
cloth.setGravity(0.f, 0.f, 0.f);
cloth.setStiffness(0.f);
cloth.setFoldStiffness(0.f);
cloth.setSelfCollision(false);
cloth.setSkinConstraint(2, 0.f, 0.f, 0.5f, 0.f, 1.f, 0.f, 0.25f, 0.f, 0.1f, 0.f);
CHECK(cloth.hasSkinConstraint(2));
cloth.setParticlePosition(2, 0.f, 0.f, 2.f);
cloth.update(1.f / 60.f);
const float dz = cloth.getParticleZ(2) - 0.5f;
CHECK(std::abs(dz) <= 0.251f);
cloth.updateSkinReference(2, 1.f, 0.f, 0.5f, 1.f, 0.f, 0.f);
cloth.setParticlePosition(2, 1.f, 0.f, 0.5f);
cloth.update(1.f / 60.f);
CHECK(cloth.getParticleX(2) >= 1.099f);
cloth.clearSkinConstraint(2);
CHECK(!cloth.hasSkinConstraint(2));
}
TEST_CASE("PhysicsCloth.attachmentTracksTargetAndDetachesWithVelocity") {
auto model = eve::physics::ClothModel::grid(2, 2, 0.5f, 0.f, 0.f, 0.f);
REQUIRE(model.hasValue());
eve::physics::Cloth3D cloth(model.value());
cloth.setGravity(0.f, 0.f, 0.f);
cloth.setStiffness(0.f);
cloth.setFoldStiffness(0.f);
cloth.setSelfCollision(false);
cloth.attachParticle(2, 2.f, 3.f, 4.f, 0.f);
CHECK(cloth.isAttached(2));
cloth.update(1.f / 60.f);
CHECK(std::abs(cloth.getParticleX(2) - 2.f) < 1e-5f);
CHECK(std::abs(cloth.getParticleY(2) - 3.f) < 1e-5f);
cloth.updateAttachment(2, 3.f, 4.f, 5.f);
cloth.update(1.f / 60.f);
CHECK(std::abs(cloth.getParticleZ(2) - 5.f) < 1e-5f);
cloth.detachParticle(2);
CHECK(!cloth.isAttached(2));
}
TEST_CASE("PhysicsCloth.runtimeTearingChangesAuthoritativeConstraintAndSurfaceTopology") {
auto model = eve::physics::ClothModel::grid(2, 2, 0.5f, 0.f, 0.f, 0.f);
REQUIRE(model.hasValue());
eve::physics::Cloth3D cloth(model.value());
const int originalLinks = cloth.getDistanceConstraintCount();
CHECK(cloth.getTriangleCount() == 2);
cloth.tearConstraint(0, 1);
CHECK(cloth.getTornConstraintCount() == 1);
CHECK(cloth.getDistanceConstraintCount() == originalLinks - 1);
CHECK(cloth.getTriangleCount() == 1);
eve::physics::Cloth3D automatic(model.value());
automatic.setGravity(0.f, 0.f, 0.f);
automatic.setSelfCollision(false);
automatic.setFoldStiffness(0.f);
automatic.setTearThreshold(1.2f);
automatic.setMaxTearsPerStep(1);
automatic.setParticlePosition(1, 4.f, 0.f, 0.f);
automatic.update(1.f / 60.f);
CHECK(automatic.getTornConstraintCount() >= 1);
}