#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "graphics/AmbientOcclusion.h" #include "graphics/AntiAliasing.h" #include "graphics/Canvas.h" #include "graphics/DrawItem2D.h" #include "graphics/Font.h" #include "graphics/GBuffer.h" #include "graphics/GlobalIllumination.h" #include "graphics/Graphics.h" #include "graphics/Grass.h" #include "graphics/Light.h" #include "graphics/Material.h" #include "graphics/Mesh.h" #include "graphics/Outline.h" #include "graphics/Quad.h" #include "graphics/RenderControl.h" #include "graphics/ScreenSpaceReflection.h" #include "graphics/Shader.h" #include "graphics/Texture.h" #include "graphics/Volumetric.h" #include "graphics/Water.h" #include "graphics/Waterfall.h" #include "physics/Body.h" #include "physics/Body3D.h" #include "physics/cloth/Cloth.h" #include "physics/cloth/ClothModule.h" #include "physics/cloth/Cloth3D.h" #include "physics/Fluid2D.h" #include "physics/Physics.h" #include "physics/World.h" #include "physics/World3D.h" #include "window/Window.h" #include #include #include using namespace eve::physics; using namespace eve::graphics; TEST_CASE("softbody.cloth.fallsAndPinsHold") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(8, 6, 10.f, 100.f, 40.f)); REQUIRE(cloth.get() != nullptr); CHECK_EQ(cloth->getCols(), 8); CHECK_EQ(cloth->getRows(), 6); CHECK_EQ(cloth->getParticleCount(), 48); CHECK(cloth->isPinned(0)); CHECK(cloth->isPinned(7)); // Pinned top row holds under gravity. const float pinY0 = cloth->getParticleY(0); const float pinX0 = cloth->getParticleX(0); cloth->setBounds(0.f, 0.f, 800.f, 600.f); for (int i = 0; i < 60; ++i) cloth->update(1.f / 60.f); CHECK(std::fabs(cloth->getParticleY(0) - pinY0) < 0.5f); CHECK(std::fabs(cloth->getParticleX(0) - pinX0) < 0.5f); // Free-fall: unpin everything and verify gravity moves particles down. for (int i = 0; i < cloth->getParticleCount(); ++i) cloth->unpin(i); const float freeY0 = cloth->getParticleY(20); for (int i = 0; i < 45; ++i) cloth->update(1.f / 60.f); CHECK_GT(cloth->getParticleY(20), freeY0 + 20.f); } TEST_CASE("softbody.cloth.grabMovesParticle") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(6, 6, 12.f, 50.f, 50.f)); // Unpin a mid particle so it can be grabbed. const int idx = 3 * 6 + 3; cloth->unpin(idx); const float x = cloth->getParticleX(idx); const float y = cloth->getParticleY(idx); int grabbed = cloth->grabAt(x, y, 20.f); CHECK_EQ(grabbed, idx); CHECK(cloth->isGrabbing()); cloth->moveGrab(x + 40.f, y + 30.f); for (int i = 0; i < 10; ++i) cloth->update(1.f / 60.f); CHECK(std::fabs(cloth->getParticleX(idx) - (x + 40.f)) < 1.f); CHECK(std::fabs(cloth->getParticleY(idx) - (y + 30.f)) < 1.f); cloth->releaseGrab(); CHECK(!cloth->isGrabbing()); } TEST_CASE("softbody.cloth.selfCollisionSeparates") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(10, 8, 12.f, 20.f, 20.f)); cloth->setGravity(0.f, 320.f); cloth->setBounds(0.f, 0.f, 600.f, 600.f); cloth->setParticleSize(3.f); cloth->setStiffness(0.9f); cloth->setMaxFoldAngle(120.f); const float minDist = cloth->getParticleSize() * 2.f; for (int f = 0; f < 240; ++f) { // Alternating wind + pointer repulsion folds the sheet onto itself. if ((f / 40) % 2 == 0) cloth->applyForce(260.f, 0.f); else cloth->applyForce(-260.f, 0.f); cloth->interactAt(300.f + float(f % 60) * 1.5f, 220.f, 90.f, -2600.f); cloth->update(1.f / 60.f); } // Non-adjacent particles must never interpenetrate beyond solver slack. float minSeen = 1e9f; for (int i = 0; i < cloth->getParticleCount(); ++i) { for (int j = i + 1; j < cloth->getParticleCount(); ++j) { const float dx = cloth->getParticleX(j) - cloth->getParticleX(i); const float dy = cloth->getParticleY(j) - cloth->getParticleY(i); minSeen = std::min(minSeen, std::sqrt(dx * dx + dy * dy)); } } REQUIRE_GT(minSeen, minDist * 0.5f); } TEST_CASE("softbody.cloth.foldAngleLimited") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(3, 3, 10.f, 0.f, 0.f)); cloth->setGravity(0.f, 0.f); cloth->setMaxFoldAngle(30.f); cloth->setFoldStiffness(1.f); cloth->setDamping(0.2f); cloth->setSelfCollision(false); // isolate the fold limit from proximity // Pin the left/right columns and fold the middle column sharply upward. for (int r = 0; r < 3; ++r) { cloth->pin(r * 3); cloth->pin(r * 3 + 2); cloth->unpin(r * 3 + 1); cloth->setParticlePosition(r * 3, 0.f, 0.f); cloth->setParticlePosition(r * 3 + 1, 10.f, -14.f); cloth->setParticlePosition(r * 3 + 2, 20.f, 0.f); } for (int i = 0; i < 120; ++i) cloth->update(1.f / 60.f); // The sharp -14 fold must be pushed back inside the 30° cone around the // straight line (|y| <= 10*tan(15°) ≈ 2.7, plus solver slack). REQUIRE_GT(cloth->getParticleY(4), -5.f); REQUIRE_LT(cloth->getParticleY(4), 5.f); REQUIRE_GT(cloth->getParticleX(4), 9.5f); REQUIRE_LT(cloth->getParticleX(4), 11.0f); } TEST_CASE("softbody.cloth.collidesWithWorld") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld(0.f, 0.f, true)); auto *ground = world->newBody("static", 400.f, 390.f); REQUIRE(ground != nullptr); ground->newRectangleFixture(800.f, 20.f); std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(6, 6, 10.f, 300.f, 60.f)); for (int i = 0; i < cloth->getParticleCount(); ++i) cloth->unpin(i); cloth->setGravity(0.f, 600.f); cloth->setCollideWorld(world.get()); const float surfaceY = 400.f; for (int i = 0; i < 120; ++i) cloth->update(1.f / 60.f); for (int i = 0; i < cloth->getParticleCount(); ++i) REQUIRE_LE(cloth->getParticleY(i), surfaceY + cloth->getParticleSize() + 1.f); // Sanity: the sheet actually fell onto the box top. REQUIRE_GT(cloth->getParticleY(5 * 6 + 3), 300.f); } TEST_CASE("softbody.cloth.interactAtRepels") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(6, 6, 12.f, 50.f, 50.f)); for (int i = 0; i < cloth->getParticleCount(); ++i) cloth->unpin(i); cloth->setGravity(0.f, 0.f); float avg0 = 0.f; for (int i = 0; i < cloth->getParticleCount(); ++i) { const float dx = cloth->getParticleX(i) - 86.f; const float dy = cloth->getParticleY(i) - 86.f; avg0 += std::sqrt(dx * dx + dy * dy); } avg0 /= float(cloth->getParticleCount()); for (int i = 0; i < 60; ++i) { cloth->interactAt(86.f, 86.f, 90.f, -3600.f); cloth->update(1.f / 60.f); } float avg1 = 0.f; for (int i = 0; i < cloth->getParticleCount(); ++i) { const float dx = cloth->getParticleX(i) - 86.f; const float dy = cloth->getParticleY(i) - 86.f; avg1 += std::sqrt(dx * dx + dy * dy); } avg1 /= float(cloth->getParticleCount()); REQUIRE_GT(avg1, avg0 + 4.f); } TEST_CASE("softbody.cloth.momentumExchangeScalesWithMass") { auto *mod = Physics::create(); const auto runCase = [&](float density) { std::unique_ptr world(mod->newWorld(0.f, 0.f, true)); auto *box = world->newBody("dynamic", 400.f, 120.f); REQUIRE(box != nullptr); box->newRectangleFixture(40.f, 40.f, density, 0.2f, 0.f); std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(6, 4, 10.f, 340.f, 20.f)); for (int i = 0; i < cloth->getParticleCount(); ++i) cloth->unpin(i); cloth->setGravity(0.f, 600.f); cloth->setCollideWorld(world.get()); for (int i = 0; i < 120; ++i) { world->update(1.f / 60.f); cloth->update(1.f / 60.f); } return box->getLinearVelocityY(); }; const float vLight = runCase(0.05f); const float vHeavy = runCase(5.f); // Same cloth impact must push the light body much harder than the heavy one. REQUIRE_GT(vLight, 30.f); REQUIRE_GT(vLight, vHeavy + 20.f); } TEST_CASE("softbody.cloth3d.pinsHoldUnderGravity") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth3D(8, 6, 0.5f, 0.f, 3.f, 0.f)); REQUIRE(cloth.get() != nullptr); REQUIRE_EQ(cloth->getParticleCount(), 48); REQUIRE(cloth->isPinned(0)); REQUIRE(cloth->isPinned(7)); const float y0 = cloth->getParticleY(0); for (int i = 0; i < 90; ++i) cloth->update(1.f / 60.f); REQUIRE(std::fabs(cloth->getParticleY(0) - y0) < 0.01f); REQUIRE_LT(cloth->getParticleY(5 * 8 + 3), y0 - 0.5f); } TEST_CASE("softbody.cloth3d.grabMovesParticle") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth3D(6, 6, 0.5f, 0.f, 2.f, 0.f)); const int idx = 3 * 6 + 3; cloth->unpin(idx); const float x = cloth->getParticleX(idx); const float y = cloth->getParticleY(idx); const float z = cloth->getParticleZ(idx); int grabbed = cloth->grabAt(x, y, z, 0.3f); REQUIRE_EQ(grabbed, idx); REQUIRE(cloth->isGrabbing()); cloth->moveGrab(x + 0.4f, y + 0.3f, z + 0.2f); for (int i = 0; i < 10; ++i) cloth->update(1.f / 60.f); REQUIRE(std::fabs(cloth->getParticleX(idx) - (x + 0.4f)) < 0.02f); REQUIRE(std::fabs(cloth->getParticleY(idx) - (y + 0.3f)) < 0.02f); REQUIRE(std::fabs(cloth->getParticleZ(idx) - (z + 0.2f)) < 0.02f); cloth->releaseGrab(); REQUIRE(!cloth->isGrabbing()); } TEST_CASE("softbody.cloth3d.selfCollisionSeparates") { for (bool enabled : {true, false}) { std::unique_ptr cloth( eve::cloth::Cloth::create()->newCloth3D(4, 4, 0.5f, 0.f, 0.f, 0.f)); cloth->setGravity(0.f, 0.f, 0.f); cloth->setStiffness(0.f); cloth->setFoldStiffness(0.f); cloth->setDamping(1.f); cloth->setSelfCollision(enabled); for (int i = 0; i < cloth->getParticleCount(); ++i) cloth->pin(i); const int moving = cloth->getParticleCount() - 1; cloth->unpin(moving); // Opposite corners are not linked. Put the free corner inside the pinned // corner's collision radius; springs cannot separate them in this fixture. cloth->setParticlePosition(moving, 0.05f, 0.f, 0.f); const float minimum = cloth->getParticleSize() * 2.f; REQUIRE(0.05f < minimum * 0.5f); for (int frame = 0; frame < 240; ++frame) cloth->update(1.f / 60.f); const float x = cloth->getParticleX(moving); const float y = cloth->getParticleY(moving); const float z = cloth->getParticleZ(moving); const float distance = std::sqrt(x * x + y * y + z * z); if (enabled) REQUIRE(distance > minimum * 0.5f); else REQUIRE(std::fabs(distance - 0.05f) < 1e-4f); REQUIRE(cloth->getParticleX(0) == 0.f); REQUIRE(cloth->getParticleY(0) == 0.f); REQUIRE(cloth->getParticleZ(0) == 0.f); } } TEST_CASE("softbody.cloth3d.foldAngleLimited") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth3D(3, 3, 0.5f, 0.f, 2.f, 0.f)); cloth->setGravity(0.f, 0.f, 0.f); cloth->setMaxFoldAngle(30.f); cloth->setFoldStiffness(1.f); cloth->setSelfCollision(false); // isolate the fold limit from proximity for (int c = 0; c < 3; ++c) { cloth->setParticlePosition(c, float(c) * 0.5f, 2.4f, 0.f); cloth->setParticlePosition(3 + c, float(c) * 0.5f, 1.6f, 0.f); cloth->setParticlePosition(6 + c, float(c) * 0.5f, 2.4f, 0.f); } for (int i = 0; i < 120; ++i) cloth->update(1.f / 60.f); // The folded middle row should be pushed back toward the flat pose (y = 2.4). REQUIRE_GT(cloth->getParticleY(4), 2.0f); } TEST_CASE("softbody.cloth3d.collidesWithWorld3D") { auto *mod = Physics::create(); std::unique_ptr world3(mod->newWorld3D(0.f, -9.8f, 0.f, true)); auto *ground = world3->newBody("static", 0.f, -0.5f, 0.f); REQUIRE(ground != nullptr); ground->newBoxShape(8.f, 1.f, 8.f); std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth3D(8, 6, 0.5f, 0.f, 4.f, 0.f)); for (int i = 0; i < cloth->getParticleCount(); ++i) cloth->unpin(i); cloth->setGravity(0.f, -6.f, 0.f); cloth->setCollideWorld(world3.get()); const float surfaceY = 0.f; for (int i = 0; i < 180; ++i) cloth->update(1.f / 60.f); for (int i = 0; i < cloth->getParticleCount(); ++i) REQUIRE_GE(cloth->getParticleY(i), surfaceY - cloth->getParticleSize() - 0.05f); // Sanity: the sheet fell from y = 4 toward the ground. REQUIRE_LT(cloth->getParticleY(5 * 8 + 3), 3.5f); } TEST_CASE("softbody.cloth3d.momentumExchangeScalesWithMass") { auto *mod = Physics::create(); const auto runCase = [&](float density) { std::unique_ptr world3(mod->newWorld3D(0.f, 0.f, 0.f, true)); auto *box = world3->newBody("dynamic", 0.f, 1.2f, 0.f); REQUIRE(box != nullptr); box->newBoxShape(1.f, 0.8f, 1.f, density, 0.2f, 0.f); std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth3D(6, 4, 0.4f, -0.6f, 3.f, -0.6f)); for (int i = 0; i < cloth->getParticleCount(); ++i) cloth->unpin(i); cloth->setGravity(0.f, -5.f, 0.f); cloth->setCollideWorld(world3.get()); for (int i = 0; i < 180; ++i) { world3->update(1.f / 60.f); cloth->update(1.f / 60.f); } return box->getLinearVelocityY(); }; const float vLight = runCase(0.05f); const float vHeavy = runCase(5.f); // +Y up: the light box is pushed downward noticeably, the heavy one barely. REQUIRE_LT(vLight, -0.2f); REQUIRE_LT(vLight, vHeavy - 0.1f); } namespace { float testClosestPointTriangle(float ax, float ay, float az, float bx, float by, float bz, float cx, float cy, float cz, float px, float py, float pz, float &qx, float &qy, float &qz) { // Mirrors Cloth3D's Ericson closest-point-on-triangle (test-only). const float abx = bx - ax, aby = by - ay, abz = bz - az; const float acx = cx - ax, acy = cy - ay, acz = cz - az; const float apx = px - ax, apy = py - ay, apz = pz - az; const float d1 = abx * apx + aby * apy + abz * apz; const float d2 = acx * apx + acy * apy + acz * apz; if (d1 <= 0.f && d2 <= 0.f) { qx = ax; qy = ay; qz = az; return std::sqrt((px - ax) * (px - ax) + (py - ay) * (py - ay) + (pz - az) * (pz - az)); } const float bpx = px - bx, bpy = py - by, bpz = pz - bz; const float d3 = abx * bpx + aby * bpy + abz * bpz; const float d4 = acx * bpx + acy * bpy + acz * bpz; if (d3 >= 0.f && d4 <= d3) { qx = bx; qy = by; qz = bz; return std::sqrt((px - bx) * (px - bx) + (py - by) * (py - by) + (pz - bz) * (pz - bz)); } const float vc = d1 * d4 - d3 * d2; if (vc <= 0.f && d1 >= 0.f && d3 <= 0.f) { const float t = d1 / (d1 - d3); qx = ax + abx * t; qy = ay + aby * t; qz = az + abz * t; return std::sqrt((px - qx) * (px - qx) + (py - qy) * (py - qy) + (pz - qz) * (pz - qz)); } const float cpx = px - cx, cpy = py - cy, cpz = pz - cz; const float d5 = abx * cpx + aby * cpy + abz * cpz; const float d6 = acx * cpx + acy * cpy + acz * cpz; if (d6 >= 0.f && d5 <= d6) { qx = cx; qy = cy; qz = cz; return std::sqrt((px - cx) * (px - cx) + (py - cy) * (py - cy) + (pz - cz) * (pz - cz)); } const float vb = d5 * d2 - d1 * d6; if (vb <= 0.f && d2 >= 0.f && d6 <= 0.f) { const float t = d2 / (d2 - d6); qx = ax + acx * t; qy = ay + acy * t; qz = az + acz * t; return std::sqrt((px - qx) * (px - qx) + (py - qy) * (py - qy) + (pz - qz) * (pz - qz)); } const float va = d3 * d6 - d5 * d4; if (va <= 0.f && (d4 - d3) >= 0.f && (d5 - d6) >= 0.f) { const float t = (d4 - d3) / ((d4 - d3) + (d5 - d6)); qx = bx + (cx - bx) * t; qy = by + (cy - by) * t; qz = bz + (cz - bz) * t; return std::sqrt((px - qx) * (px - qx) + (py - qy) * (py - qy) + (pz - qz) * (pz - qz)); } const float denom = 1.f / (va + vb + vc); const float v = vb * denom; const float w = vc * denom; qx = ax + abx * v + acx * w; qy = ay + aby * v + acy * w; qz = az + abz * v + acz * w; return std::sqrt((px - qx) * (px - qx) + (py - qy) * (py - qy) + (pz - qz) * (pz - qz)); } } // namespace TEST_CASE("softbody.cloth3d.triangleSelfCollision") { std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth3D(10, 8, 0.5f, -2.f, 2.f, -2.f)); cloth->setGravity(0.f, -3.f, 0.f); cloth->setBounds(-2.f, -3.f, -2.f, 8.f, 8.f, 8.f); cloth->setStiffness(0.5f); // Let the sheet fold onto itself: no fold resistance, only self-collision. cloth->setMaxFoldAngle(175.f); cloth->setFoldStiffness(0.f); cloth->setParticleSize(0.12f); const float thickness = cloth->getParticleSize() * 2.f; for (int f = 0; f < 240; ++f) { if ((f / 40) % 2 == 0) cloth->applyForce(1.5f, 0.f, 0.f); else cloth->applyForce(-1.5f, 0.f, 0.f); // Pinch the sheet upward through itself: strong attraction above the sheet. cloth->interactAt(0.f, 3.2f, 0.f, 3.5f, 80.f); cloth->update(1.f / 60.f); } // Rebuild the quad triangles (same winding as Cloth3D) and verify that // non-adjacent triangles keep at least half the cloth thickness apart. const int cols = cloth->getCols(); const int rows = cloth->getRows(); std::vector triVerts; auto pushTri = [&](int a, int b, int c) { triVerts.push_back(a); triVerts.push_back(b); triVerts.push_back(c); }; for (int r = 0; r + 1 < rows; ++r) { for (int c = 0; c + 1 < cols; ++c) { const int a = r * cols + c; const int b = r * cols + c + 1; const int cc = (r + 1) * cols + c + 1; const int d = (r + 1) * cols + c; pushTri(a, d, b); pushTri(d, cc, b); } } const int triCount = static_cast(triVerts.size()) / 3; auto sharesVertex = [&](int t0, int t1) { for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) if (triVerts[t0 * 3 + i] == triVerts[t1 * 3 + j]) return true; return false; }; const auto vx = [&](int vi) { return cloth->getParticleX(vi); }; const auto vy = [&](int vi) { return cloth->getParticleY(vi); }; const auto vz = [&](int vi) { return cloth->getParticleZ(vi); }; float minTriDist = 1e9f; for (int t0 = 0; t0 < triCount; ++t0) { for (int t1 = t0 + 1; t1 < triCount; ++t1) { if (sharesVertex(t0, t1)) continue; for (int side = 0; side < 2; ++side) { const int ta = side == 0 ? t0 : t1; const int tb = side == 0 ? t1 : t0; for (int k = 0; k < 3; ++k) { float qx, qy, qz; const float d = testClosestPointTriangle( vx(triVerts[tb * 3 + 0]), vy(triVerts[tb * 3 + 0]), vz(triVerts[tb * 3 + 0]), vx(triVerts[tb * 3 + 1]), vy(triVerts[tb * 3 + 1]), vz(triVerts[tb * 3 + 1]), vx(triVerts[tb * 3 + 2]), vy(triVerts[tb * 3 + 2]), vz(triVerts[tb * 3 + 2]), vx(triVerts[ta * 3 + k]), vy(triVerts[ta * 3 + k]), vz(triVerts[ta * 3 + k]), qx, qy, qz); minTriDist = std::min(minTriDist, d); } } } } REQUIRE_GT(minTriDist, thickness * 0.5f); } TEST_CASE("softbody.fluid.emitAndSettle") { auto *mod = Physics::create(); std::unique_ptr fluid(mod->newFluid2D(256)); REQUIRE(fluid.get() != nullptr); fluid->setBounds(0.f, 0.f, 400.f, 400.f); fluid->setGravity(0.f, 900.f); int added = fluid->emit(200.f, 80.f, 40, 0.f, 0.f); CHECK_EQ(added, 40); CHECK_EQ(fluid->getParticleCount(), 40); float y0 = 0.f; for (int i = 0; i < fluid->getParticleCount(); ++i) y0 += fluid->getParticleY(i); y0 /= float(fluid->getParticleCount()); for (int i = 0; i < 120; ++i) fluid->update(1.f / 60.f); float y1 = 0.f; for (int i = 0; i < fluid->getParticleCount(); ++i) y1 += fluid->getParticleY(i); y1 /= float(fluid->getParticleCount()); CHECK_GT(y1, y0 + 10.f); // Stay inside container. for (int i = 0; i < fluid->getParticleCount(); ++i) { CHECK_GE(fluid->getParticleX(i), -1.f); CHECK_LE(fluid->getParticleX(i), 401.f); CHECK_GE(fluid->getParticleY(i), -1.f); CHECK_LE(fluid->getParticleY(i), 401.f); } } TEST_CASE("softbody.fluid.interactRepels") { auto *mod = Physics::create(); std::unique_ptr fluid(mod->newFluid2D(128)); fluid->setBounds(0.f, 0.f, 300.f, 300.f); fluid->setGravity(0.f, 0.f); fluid->emit(150.f, 150.f, 25, 0.f, 0.f); float avgDist0 = 0.f; for (int i = 0; i < fluid->getParticleCount(); ++i) { const float dx = fluid->getParticleX(i) - 150.f; const float dy = fluid->getParticleY(i) - 150.f; avgDist0 += std::sqrt(dx * dx + dy * dy); } avgDist0 /= float(fluid->getParticleCount()); for (int i = 0; i < 45; ++i) { fluid->interactAt(150.f, 150.f, 80.f, -4000.f); fluid->update(1.f / 60.f); } float avgDist1 = 0.f; for (int i = 0; i < fluid->getParticleCount(); ++i) { const float dx = fluid->getParticleX(i) - 150.f; const float dy = fluid->getParticleY(i) - 150.f; avgDist1 += std::sqrt(dx * dx + dy * dy); } avgDist1 /= float(fluid->getParticleCount()); CHECK_GT(avgDist1, avgDist0 + 2.f); } TEST_CASE("softbody.render.clothAndFluid2DPreview") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 720; s.height = 420; s.centered = true; REQUIRE(win->setWindowSettings(s)); auto *mod = Physics::create(); std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth(14, 10, 12.f, 40.f, 30.f)); cloth->setBounds(0.f, 0.f, 720.f, 420.f); cloth->setColor(0.78f, 0.84f, 0.98f, 1.f); std::unique_ptr fluid(mod->newFluid2D(400)); fluid->setBounds(360.f, 40.f, 320.f, 340.f); fluid->setGravity(0.f, 980.f); fluid->setColor(0.2f, 0.55f, 0.95f, 0.9f); fluid->emit(520.f, 90.f, 120, 0.f, 40.f); gfx->setBackgroundColorRGBA(0.07f, 0.08f, 0.11f, 1.f); for (int frame = 0; frame < 90; ++frame) { if (frame == 30) cloth->grabAt(cloth->getParticleX(5 * 14 + 7), cloth->getParticleY(5 * 14 + 7), 30.f); if (frame >= 30 && frame < 60) cloth->moveGrab(180.f + float(frame - 30) * 2.f, 220.f); if (frame == 60) cloth->releaseGrab(); if ((frame % 15) == 0) fluid->emit(500.f, 70.f, 8, 20.f, 60.f); fluid->interactAt(520.f, 200.f, 70.f, -2500.f); cloth->update(1.f / 60.f); fluid->update(1.f / 60.f); gfx->clearScreen(); // Fluid2D tank outline. gfx->drawSolidRectRGBA(360.f, 40.f, 320.f, 340.f, 0.12f, 0.14f, 0.18f, 1.f); cloth->draw(gfx); fluid->draw(gfx); gfx->present(); SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) break; } SDL_Delay(16); } CHECK_GT(cloth->getParticleY(5 * 14 + 7), 40.f); CHECK_GT(fluid->getParticleCount(), 100); win->close(); } TEST_CASE("softbody.render.cloth3DPreview") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 720; s.height = 420; s.centered = true; REQUIRE(win->setWindowSettings(s)); std::unique_ptr cloth(eve::cloth::Cloth::create()->newCloth3D(14, 10, 0.4f, -1.5f, 2.5f, -1.f)); cloth->setColor(0.78f, 0.84f, 0.98f, 1.f); cloth->setFoldStiffness(0.7f); gfx->setBackgroundColorRGBA(0.07f, 0.08f, 0.11f, 1.f); for (int frame = 0; frame < 45; ++frame) { if (frame == 15) cloth->grabAt(cloth->getParticleX(5 * 14 + 7), cloth->getParticleY(5 * 14 + 7), cloth->getParticleZ(5 * 14 + 7), 0.4f); if (frame >= 15 && frame < 30) cloth->moveGrab(0.8f, 1.2f, -0.4f); if (frame == 30) cloth->releaseGrab(); cloth->applyForce(0.8f, 0.f, 0.f); cloth->update(1.f / 60.f); gfx->begin3DFrame(); cloth->draw(gfx); gfx->present(); SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) break; } SDL_Delay(8); } REQUIRE_LT(cloth->getParticleY(5 * 14 + 7), 2.2f); win->close(); }