645 lines
25 KiB
C++
645 lines
25 KiB
C++
#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 <SDL2/SDL.h>
|
|
|
|
#include <cmath>
|
|
#include <memory>
|
|
|
|
using namespace eve::physics;
|
|
using namespace eve::graphics;
|
|
|
|
TEST_CASE("softbody.cloth.fallsAndPinsHold") {
|
|
std::unique_ptr<Cloth> 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> 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> 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> 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> 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> 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> 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> 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> 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<Cloth3D> 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<Cloth3D> 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") {
|
|
std::unique_ptr<Cloth3D> cloth(eve::cloth::Cloth::create()->newCloth3D(10, 8, 0.5f, -1.f, 2.f, -1.f));
|
|
cloth->setGravity(0.f, -2.5f, 0.f);
|
|
cloth->setBounds(-1.f, -2.f, -1.f, 6.f, 6.f, 6.f);
|
|
cloth->setStiffness(0.9f);
|
|
cloth->setMaxFoldAngle(130.f);
|
|
const float minDist = cloth->getParticleSize() * 2.f;
|
|
for (int f = 0; f < 240; ++f) {
|
|
if ((f / 40) % 2 == 0)
|
|
cloth->applyForce(2.f, 0.f, 0.f);
|
|
else
|
|
cloth->applyForce(-2.f, 0.f, 0.f);
|
|
cloth->interactAt(float(f % 60) * 0.04f, 1.2f, 0.f, 90.f, -30.f);
|
|
cloth->update(1.f / 60.f);
|
|
}
|
|
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);
|
|
const float dz = cloth->getParticleZ(j) - cloth->getParticleZ(i);
|
|
minSeen = std::min(minSeen, std::sqrt(dx * dx + dy * dy + dz * dz));
|
|
}
|
|
}
|
|
REQUIRE_GT(minSeen, minDist * 0.5f);
|
|
}
|
|
|
|
TEST_CASE("softbody.cloth3d.foldAngleLimited") {
|
|
std::unique_ptr<Cloth3D> 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<World3D> 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<Cloth3D> 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<World3D> 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<Cloth3D> 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<Cloth3D> 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<int> 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<int>(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<Fluid2D> 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<Fluid2D> 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> 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<Fluid2D> 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<Cloth3D> 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();
|
|
}
|