851 lines
38 KiB
C++
851 lines
38 KiB
C++
#include <cmath>
|
|
#include <glm/geometric.hpp>
|
|
#include <limits>
|
|
#include "fluids/VolumeFluid.h"
|
|
#include "fluids/VolumeFluidCodec.h"
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
using namespace eve::fluids;
|
|
|
|
static eve::Value withoutRecentMaterialFields(eve::Value particle) {
|
|
const auto* current = particle.find("material");
|
|
eve::Value material(eve::Value::Object{});
|
|
for (const auto& key : current->keys())
|
|
if (key != "atmosphericPressure" && key != "smoothing" && key != "rollingContacts" &&
|
|
key != "rollingFriction" && key != "dynamicFriction" && key != "staticFriction" && key != "stickiness" &&
|
|
key != "stickDistance" && key != "frictionCombine" && key != "stickinessCombine")
|
|
material.set(key, *current->find(key));
|
|
eve::Value legacy(eve::Value::Object{});
|
|
for (const auto& key : particle.keys())
|
|
if (key != "angularVelocity") legacy.set(key, *particle.find(key));
|
|
legacy.set("material", std::move(material));
|
|
return legacy;
|
|
}
|
|
static eve::Value withoutSdfColliders(const eve::Value& snapshot) {
|
|
eve::Value legacy(eve::Value::Object{});
|
|
for (const auto& key : snapshot.keys()) {
|
|
if (key == "sdfColliders" || key == "heightFieldColliders" || key == "stitches") continue;
|
|
if (key != "colliders") {
|
|
legacy.set(key, *snapshot.find(key));
|
|
continue;
|
|
}
|
|
eve::Value::Array colliders;
|
|
const auto* source = snapshot.find(key);
|
|
for (size_t i = 0; i < source->arraySize(); ++i) {
|
|
eve::Value collider(eve::Value::Object{});
|
|
for (const auto& field : source->at(i).keys())
|
|
if (field != "isTrigger" && field != "staticFriction" && field != "rollingFriction" &&
|
|
field != "stickiness" && field != "stickDistance" && field != "frictionCombine" &&
|
|
field != "stickinessCombine" && field != "rollingContacts")
|
|
collider.set(field, *source->at(i).find(field));
|
|
colliders.push_back(std::move(collider));
|
|
}
|
|
legacy.set(key, eve::Value(std::move(colliders)));
|
|
}
|
|
return legacy;
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.renderCopyOwnership") {
|
|
auto created = VolumeFluid::create({});
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle p{{0.f, 1.f, 0.f}, {0.f, 0.f, 0.f}};
|
|
p.radii = {.04f, .08f, .12f};
|
|
p.orientation = {0.f, 0.f, .70710677f, .70710677f};
|
|
REQUIRE(solver->emit(std::span(&p, 1)).ok());
|
|
std::vector<glm::vec3> positions;
|
|
std::vector<glm::vec4> colors;
|
|
REQUIRE(solver->copyRenderData(positions, colors) == 0.1f);
|
|
REQUIRE(solver->particleCount() == 1);
|
|
REQUIRE(positions[0].y == 1.f);
|
|
const auto view = solver->particleView();
|
|
REQUIRE(view.size() == 1);
|
|
REQUIRE(view[0].position == p.position);
|
|
std::vector<glm::vec3> radii;
|
|
std::vector<glm::vec4> orientations;
|
|
REQUIRE(solver->copySurfaceRenderData(positions, colors, radii, orientations) == .1f);
|
|
REQUIRE(radii[0] == p.radii);
|
|
REQUIRE(glm::length(orientations[0] - p.orientation) < 1e-6f);
|
|
const auto* radiiStorage = radii.data();
|
|
const auto* storage = positions.data();
|
|
REQUIRE(solver->step(1.f / 60.f).ok());
|
|
REQUIRE(positions[0].y == 1.f);
|
|
REQUIRE(solver->copyRenderData(positions, colors) == 0.1f);
|
|
REQUIRE(positions.data() == storage);
|
|
REQUIRE(solver->copySurfaceRenderData(positions, colors, radii, orientations) == .1f);
|
|
REQUIRE(radii.data() == radiiStorage);
|
|
REQUIRE(positions[0].y < 1.f);
|
|
solver->clear();
|
|
REQUIRE(solver->particleCount() == 0);
|
|
REQUIRE(solver->copyRenderData(positions, colors) == 0.1f);
|
|
REQUIRE(positions.empty());
|
|
REQUIRE(colors.empty());
|
|
REQUIRE(solver->copySurfaceRenderData(positions, colors, radii, orientations) == .1f);
|
|
REQUIRE(radii.empty());
|
|
REQUIRE(orientations.empty());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.renderInterpolationTracksCompactionTeleportAndRestore") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0.f, 0.f, 0.f};
|
|
settings.capacity = 4;
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
std::vector<VolumeFluidParticle> particles(3);
|
|
particles[0].position = {-1.f, 1.f, 0.f};
|
|
particles[0].velocity = {1.f, 0.f, 0.f};
|
|
particles[1].position = {0.f, 1.f, 0.f};
|
|
particles[1].velocity = {2.f, 0.f, 0.f};
|
|
particles[1].life = .001f;
|
|
particles[2].position = {1.f, 1.f, 0.f};
|
|
particles[2].velocity = {3.f, 0.f, 0.f};
|
|
REQUIRE(solver->emit(particles).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
std::vector<glm::vec3> positions;
|
|
REQUIRE(solver->copyInterpolatedPositions(0.f, positions).ok());
|
|
REQUIRE(positions.size() == 2);
|
|
REQUIRE(positions[0] == glm::vec3(-1.f, 1.f, 0.f));
|
|
REQUIRE(positions[1] == glm::vec3(1.f, 1.f, 0.f));
|
|
REQUIRE(solver->copyInterpolatedPositions(.5f, positions).ok());
|
|
REQUIRE(std::abs(positions[0].x - (-1.f + .5f / 120.f)) < 1e-5f);
|
|
REQUIRE(std::abs(positions[1].x - (1.f + 1.5f / 120.f)) < 1e-5f);
|
|
const auto preserved = positions;
|
|
REQUIRE(!solver->copyInterpolatedPositions(1.01f, positions).ok());
|
|
REQUIRE(positions == preserved);
|
|
REQUIRE(solver->killParticle(0).ok());
|
|
REQUIRE(solver->copyInterpolatedPositions(0.f, positions).ok());
|
|
REQUIRE(positions[0].x == 1.f);
|
|
REQUIRE(solver->teleportActor({0, 0, 0}, {0, 0, 0, 1}, {.5f, 0, 0}, {0, 0, 0, 1}).ok());
|
|
REQUIRE(solver->copyInterpolatedPositions(0.f, positions).ok());
|
|
REQUIRE(positions[0] == solver->particleView()[0].position);
|
|
const auto saved = solver->snapshot();
|
|
REQUIRE(solver->restore(saved).ok());
|
|
REQUIRE(solver->copyInterpolatedPositions(0.f, positions).ok());
|
|
REQUIRE(positions[0] == saved.particles[0].position);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.killParticleRemapsOwnedLinksAtomically") {
|
|
VolumeFluidSnapshot state;
|
|
state.settings.gravity = {0, 0, 0};
|
|
state.settings.capacity = 8;
|
|
state.particles.resize(3);
|
|
for (unsigned i = 0; i < 3; ++i) state.particles[i].position = {-.3f + .3f * float(i), 1.f, 0.f};
|
|
state.particles[2].material.phase = VolumeFluidPhase::Solid;
|
|
VolumeFluidCollider collider;
|
|
collider.label = 7;
|
|
state.colliders.push_back(collider);
|
|
state.attachments.push_back({2, 7, {0, 1, 0}, {0, 0, 0, 1}});
|
|
state.simplexes = {{{0, 2, 0}, 2}, {{1, 2, 0}, 2}};
|
|
auto created = VolumeFluid::create(state.settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
REQUIRE(solver->restore(state).ok());
|
|
|
|
REQUIRE(solver->killParticle(1).ok());
|
|
const auto after = solver->snapshot();
|
|
REQUIRE(after.particles.size() == 2);
|
|
REQUIRE(after.settings.capacity == 8);
|
|
REQUIRE(after.attachments.size() == 1);
|
|
REQUIRE(after.attachments[0].particleIndex == 1);
|
|
REQUIRE(after.simplexes.size() == 1);
|
|
REQUIRE(after.simplexes[0].particleIndices[0] == 0);
|
|
REQUIRE(after.simplexes[0].particleIndices[1] == 1);
|
|
REQUIRE(solver->availableCapacity() == 6);
|
|
|
|
const auto beforeInvalid = solver->snapshot();
|
|
REQUIRE(!solver->killParticle(2).ok());
|
|
const auto unchanged = solver->snapshot();
|
|
REQUIRE(unchanged.particles.size() == beforeInvalid.particles.size());
|
|
REQUIRE(unchanged.attachments.size() == beforeInvalid.attachments.size());
|
|
REQUIRE(unchanged.simplexes.size() == beforeInvalid.simplexes.size());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.killActorParticlesCompactsOnceAndRemapsOwnedLinks") {
|
|
VolumeFluidSnapshot state;
|
|
state.settings.gravity = {0, 0, 0};
|
|
state.settings.capacity = 8;
|
|
state.particles.resize(4);
|
|
for (unsigned i = 0; i < 4; ++i) {
|
|
state.particles[i].position = {-.6f + .4f * float(i), 1.f, 0.f};
|
|
state.particles[i].actorGroup = (i == 0 || i == 2) ? 7 : 8;
|
|
}
|
|
state.particles[3].material.phase = VolumeFluidPhase::Solid;
|
|
VolumeFluidCollider collider;
|
|
collider.label = 71;
|
|
state.colliders.push_back(collider);
|
|
state.attachments.push_back({3, 71, state.particles[3].position, {0, 0, 0, 1}});
|
|
state.stitches.push_back({1, 3, .01f});
|
|
state.simplexes = {{{0, 1, 0}, 2}, {{1, 3, 0}, 2}};
|
|
auto created = VolumeFluid::create(state.settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
REQUIRE(solver->restore(state).ok());
|
|
REQUIRE(solver->killActorParticles(7).value() == 2);
|
|
const auto after = solver->snapshot();
|
|
REQUIRE(after.particles.size() == 2);
|
|
CHECK(after.particles[0].actorGroup == 8);
|
|
CHECK(after.particles[1].actorGroup == 8);
|
|
REQUIRE(after.attachments.size() == 1);
|
|
CHECK(after.attachments[0].particleIndex == 1);
|
|
REQUIRE(after.stitches.size() == 1);
|
|
CHECK(after.stitches[0].particleIndex1 == 0);
|
|
CHECK(after.stitches[0].particleIndex2 == 1);
|
|
REQUIRE(after.simplexes.size() == 1);
|
|
CHECK(after.simplexes[0].particleIndices[0] == 0);
|
|
CHECK(after.simplexes[0].particleIndices[1] == 1);
|
|
CHECK(solver->availableCapacity() == 6);
|
|
const auto unchanged = solver->snapshot();
|
|
CHECK(!solver->killActorParticles(7).ok());
|
|
CHECK(solver->snapshot().particles.size() == unchanged.particles.size());
|
|
CHECK(solver->snapshot().attachments.size() == unchanged.attachments.size());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.freeFall") {
|
|
auto result = VolumeFluid::create({});
|
|
REQUIRE(result.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
const VolumeFluidParticle p{{0.f, 2.f, 0.f}, {0.f, 0.f, 0.f}};
|
|
REQUIRE(solver->emit(std::span(&p, 1)).ok());
|
|
for (int i = 0; i < 12; ++i) REQUIRE(solver->step(1.f / 60.f).ok());
|
|
const auto out = solver->particles();
|
|
REQUIRE(std::abs(out[0].velocity.y + 9.81f * 0.2f) < 0.001f);
|
|
REQUIRE(std::abs(out[0].position.y - (2.f - 0.5f * 9.81f * 0.04f)) < 0.005f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.worldSpaceGravityUpdatesAtomically") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0.f, 0.f, 0.f};
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particle;
|
|
particle.position = {0.f, 1.f, 0.f};
|
|
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
|
|
REQUIRE(solver->setGravity({0.f, 9.81f, 0.f}).ok());
|
|
REQUIRE(solver->gravity() == glm::vec3(0.f, 9.81f, 0.f));
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(solver->particleView()[0].velocity.y > 0.f);
|
|
const auto before = solver->snapshot();
|
|
REQUIRE(!solver->setGravity({std::numeric_limits<float>::quiet_NaN(), 0.f, 0.f}).ok());
|
|
REQUIRE(!solver->setGravity({1001.f, 0.f, 0.f}).ok());
|
|
REQUIRE(solver->gravity() == before.settings.gravity);
|
|
REQUIRE(solver->snapshot().settings.gravity == before.settings.gravity);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.actorMassPropertiesAreWeightedAndExplicit") {
|
|
VolumeFluidSettings settings;
|
|
settings.spacing = .1f;
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particles[3];
|
|
particles[0].position = {0.f, 1.f, 0.f};
|
|
particles[0].actorGroup = 7;
|
|
particles[0].material.density = 1000.f;
|
|
particles[1].position = {1.f, 1.f, 0.f};
|
|
particles[1].actorGroup = 7;
|
|
particles[1].material.density = 500.f;
|
|
particles[2].position = {1.5f, 1.f, 0.f};
|
|
particles[2].actorGroup = 8;
|
|
particles[2].material.density = 1000.f;
|
|
REQUIRE(solver->emit(particles).ok());
|
|
|
|
auto properties = solver->actorMassProperties(7);
|
|
REQUIRE(properties.ok());
|
|
CHECK(properties.value().particleCount == 2);
|
|
CHECK(std::abs(properties.value().mass - 1.5f) < 1e-6f);
|
|
CHECK(std::abs(properties.value().centerOfMass.x - 1.f / 3.f) < 1e-6f);
|
|
CHECK(properties.value().centerOfMass.y == 1.f);
|
|
CHECK(!solver->actorMassProperties(9).ok());
|
|
CHECK(!solver->actorMassProperties(0x01000000u).ok());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.particleDragIsBoundedConstantWork") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0.f, 0.f, 0.f};
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particle;
|
|
particle.position = {0.f, 1.f, 0.f};
|
|
particle.velocity = {1.f, 0.f, 0.f};
|
|
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
|
|
|
|
REQUIRE(solver->applyParticleDrag(0, {1.f, 1.f, 0.f}, 12.f, 2.f, .01f).ok());
|
|
CHECK(std::abs(solver->particleView()[0].velocity.x - 1.1f) < 1e-6f);
|
|
const auto before = solver->snapshot();
|
|
CHECK(!solver->applyParticleDrag(1, {1.f, 1.f, 0.f}, 12.f, 2.f, .01f).ok());
|
|
CHECK(!solver->applyParticleDrag(0, {100.f, 1.f, 0.f}, 10000.f, 0.f, 1.f / 30.f).ok());
|
|
CHECK(!solver->applyParticleDrag(0, {1.f, 1.f, 0.f}, -1.f, 2.f, .01f).ok());
|
|
CHECK(solver->snapshot().particles[0].velocity == before.particles[0].velocity);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.admissionAtomic") {
|
|
VolumeFluidSettings settings;
|
|
settings.capacity = 1;
|
|
auto result = VolumeFluid::create(settings);
|
|
REQUIRE(result.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
const VolumeFluidParticle batch[] = {{{0.f, 1.f, 0.f}, {}}, {{0.f, 2.f, 0.f}, {}}};
|
|
REQUIRE(!solver->emit(batch).ok());
|
|
REQUIRE(solver->particles().empty());
|
|
REQUIRE(solver->emit(std::span(batch, 1)).ok());
|
|
REQUIRE(!solver->step(std::numeric_limits<float>::quiet_NaN()).ok());
|
|
REQUIRE(!solver->step(0.03f, 1).ok());
|
|
REQUIRE(solver->particles()[0].position.y == 1.f);
|
|
auto invalid = VolumeFluid::create(VolumeFluidSettings{.capacity = 0});
|
|
REQUIRE(!invalid.ok());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.damBreakContainedRepeatable") {
|
|
VolumeFluidSettings settings;
|
|
settings.spacing = 0.12f;
|
|
auto result = VolumeFluid::create(settings);
|
|
auto second = VolumeFluid::create(settings);
|
|
REQUIRE(result.ok());
|
|
REQUIRE(second.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
auto replay = std::move(second).takeValue();
|
|
std::vector<VolumeFluidParticle> batch;
|
|
for (int z = 0; z < 5; ++z)
|
|
for (int y = 0; y < 9; ++y)
|
|
for (int x = 0; x < 5; ++x)
|
|
batch.push_back({{-1.7f + x * 0.12f, 0.1f + y * 0.12f, -0.24f + z * 0.12f}, {}});
|
|
REQUIRE(solver->emit(batch).ok());
|
|
REQUIRE(replay->emit(batch).ok());
|
|
for (int i = 0; i < 60; ++i) {
|
|
REQUIRE(solver->step(1.f / 60.f).ok());
|
|
REQUIRE(replay->step(1.f / 60.f).ok());
|
|
}
|
|
const auto a = solver->particles(), b = replay->particles();
|
|
float maxX = -2.f;
|
|
for (size_t i = 0; i < a.size(); ++i) {
|
|
REQUIRE(std::isfinite(a[i].position.x));
|
|
REQUIRE(a[i].position.y >= 0.06f);
|
|
REQUIRE(a[i].position.y <= 2.94f);
|
|
REQUIRE(a[i].position.x >= -1.94f);
|
|
REQUIRE(a[i].position.x <= 1.94f);
|
|
REQUIRE(a[i].position.z == b[i].position.z);
|
|
REQUIRE(a[i].position.x == b[i].position.x);
|
|
REQUIRE(a[i].position.y == b[i].position.y);
|
|
maxX = std::max(maxX, a[i].position.x);
|
|
}
|
|
REQUIRE(maxX > -1.f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.gasDiffusionLifetimeAndGranules") {
|
|
auto result = VolumeFluid::create({});
|
|
REQUIRE(result.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
VolumeFluidParticle pair[2];
|
|
pair[0].position = {0.f, 1.f, 0.f};
|
|
pair[1].position = {0.08f, 1.f, 0.f};
|
|
pair[0].material.phase = pair[1].material.phase = VolumeFluidPhase::Gas;
|
|
pair[0].material.buoyancy = pair[1].material.buoyancy = -1.f;
|
|
pair[0].material.diffusion = pair[1].material.diffusion = 20.f;
|
|
pair[0].color = {1.f, 0.f, 0.f, 1.f};
|
|
pair[1].color = {0.f, 0.f, 1.f, 1.f};
|
|
pair[0].life = 0.1f;
|
|
REQUIRE(solver->emit(pair).ok());
|
|
REQUIRE(solver->step(1.f / 60.f).ok());
|
|
auto state = solver->particles();
|
|
REQUIRE(state[0].velocity.y > 0.f);
|
|
REQUIRE(state[0].color.b > 0.f);
|
|
REQUIRE(state[1].color.r > 0.f);
|
|
REQUIRE(std::abs(state[0].color.r + state[1].color.r - 1.f) < 1e-5f);
|
|
for (int i = 0; i < 8; ++i) REQUIRE(solver->step(1.f / 60.f).ok());
|
|
REQUIRE(solver->particles().size() == 1);
|
|
solver->clear();
|
|
pair[0].life = 0.f;
|
|
pair[0].material.phase = pair[1].material.phase = VolumeFluidPhase::Granular;
|
|
REQUIRE(solver->emit(pair).ok());
|
|
REQUIRE(solver->step(1.f / 60.f).ok());
|
|
state = solver->particles();
|
|
const auto diff = state[0].position - state[1].position;
|
|
REQUIRE(std::sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z) >= 0.099f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.phaseRenderCopyIsBoundedAndReportsTotal") {
|
|
auto created = VolumeFluid::create({});
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particles[3];
|
|
for (auto& particle : particles) particle.position.y = 1.f;
|
|
particles[0].material.phase = VolumeFluidPhase::Gas;
|
|
particles[0].position.x = .1f;
|
|
particles[1].material.phase = VolumeFluidPhase::Liquid;
|
|
particles[1].position.x = .2f;
|
|
particles[2].material.phase = VolumeFluidPhase::Gas;
|
|
particles[2].position.x = .3f;
|
|
REQUIRE(solver->emit(particles).ok());
|
|
std::vector<glm::vec3> positions;
|
|
std::vector<glm::vec4> colors;
|
|
CHECK(solver->copyPhaseRenderData(VolumeFluidPhase::Gas, positions, colors, 1) == 2);
|
|
REQUIRE(positions.size() == 1);
|
|
CHECK(positions[0].x == .1f);
|
|
CHECK(colors.size() == 1);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.granularAngularIntegrationAndRollingContact") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0, 0, 0};
|
|
settings.iterations = 1;
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle granular;
|
|
granular.position = {0, .54f, 0};
|
|
granular.velocity = {1, 0, 0};
|
|
granular.angularVelocity = {0, 0, 3};
|
|
granular.material.phase = VolumeFluidPhase::Granular;
|
|
granular.material.rollingContacts = true;
|
|
granular.material.rollingFriction = 0.f;
|
|
VolumeFluidCollider floor;
|
|
floor.shape = VolumeFluidColliderShape::Box;
|
|
floor.center = {0, 0, 0};
|
|
floor.halfExtent = {2, .5f, 2};
|
|
floor.friction = 1.f;
|
|
REQUIRE(solver->setColliders(std::span(&floor, 1)).ok());
|
|
REQUIRE(solver->emit(std::span(&granular, 1)).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto contacted = solver->particles()[0];
|
|
REQUIRE(std::abs(contacted.angularVelocity.z - 3.f) > .1f);
|
|
REQUIRE(std::abs(contacted.orientation.z) > .001f);
|
|
|
|
solver->clear();
|
|
granular.velocity = {0, 0, 0};
|
|
granular.angularVelocity = {0, 0, 10};
|
|
granular.material.rollingFriction = 1.f;
|
|
REQUIRE(solver->emit(std::span(&granular, 1)).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(glm::length(solver->particles()[0].angularVelocity) < .001f);
|
|
|
|
solver->clear();
|
|
auto liquid = granular;
|
|
liquid.position = {0, 1, 0};
|
|
liquid.material.phase = VolumeFluidPhase::Liquid;
|
|
liquid.material.rollingContacts = false;
|
|
liquid.angularVelocity = {0, 0, 3};
|
|
const auto original = liquid.orientation;
|
|
REQUIRE(solver->setColliders({}).ok());
|
|
REQUIRE(solver->emit(std::span(&liquid, 1)).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(solver->particles()[0].orientation == original);
|
|
auto invalid = solver->snapshot();
|
|
invalid.particles[0].material.rollingFriction = 1.01f;
|
|
REQUIRE(!solver->restore(invalid).ok());
|
|
REQUIRE(solver->snapshot().particles[0].material.rollingFriction == 1.f);
|
|
solver->clear();
|
|
granular.angularVelocity = {0, 0, 1001};
|
|
REQUIRE(!solver->emit(std::span(&granular, 1)).ok());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.granularPairStaticDynamicAndRollingFriction") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0, 0, 0};
|
|
settings.iterations = 1;
|
|
auto run = [&](float friction, bool rolling) {
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle pair[2];
|
|
pair[0].position = {-.045f, 1, 0};
|
|
pair[1].position = {.045f, 1, 0};
|
|
pair[0].velocity = {0, 0, 1};
|
|
pair[1].velocity = {0, 0, -1};
|
|
for (auto& p : pair) {
|
|
p.material.phase = VolumeFluidPhase::Granular;
|
|
p.material.cohesion = 0;
|
|
p.material.dynamicFriction = friction;
|
|
p.material.staticFriction = friction;
|
|
p.material.rollingContacts = rolling;
|
|
p.material.rollingFriction = 0;
|
|
}
|
|
REQUIRE(solver->emit(pair).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
return solver;
|
|
};
|
|
auto frictionless = run(0, false), frictional = run(1, true);
|
|
const auto freeRelative =
|
|
std::abs(frictionless->particles()[0].velocity.z - frictionless->particles()[1].velocity.z);
|
|
const auto frictionRelative =
|
|
std::abs(frictional->particles()[0].velocity.z - frictional->particles()[1].velocity.z);
|
|
REQUIRE(frictionRelative < freeRelative - .1f);
|
|
REQUIRE(std::abs(frictional->particles()[0].angularVelocity.y) > .1f);
|
|
REQUIRE(std::abs(frictional->particles()[1].angularVelocity.y) > .1f);
|
|
auto rolling = run(0, true);
|
|
auto rollingState = rolling->snapshot();
|
|
rollingState.particles[0].position = {-.045f, 1, 0};
|
|
rollingState.particles[1].position = {.045f, 1, 0};
|
|
rollingState.particles[0].velocity = rollingState.particles[1].velocity = glm::vec3(0.f);
|
|
rollingState.particles[0].angularVelocity = {0, 10, 0};
|
|
rollingState.particles[1].angularVelocity = {0, -10, 0};
|
|
for (auto& p : rollingState.particles) p.material.rollingFriction = 1.f;
|
|
REQUIRE(rolling->restore(rollingState).ok());
|
|
REQUIRE(rolling->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(glm::length(rolling->particles()[0].angularVelocity - rolling->particles()[1].angularVelocity) < .001f);
|
|
auto invalid = rolling->snapshot();
|
|
invalid.particles[0].material.staticFriction = 1.01f;
|
|
REQUIRE(!rolling->restore(invalid).ok());
|
|
REQUIRE(rolling->snapshot().particles[0].material.staticFriction == 0.f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.atmosphericSurfacePressureDragAndMigration") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0, 0, 0};
|
|
settings.iterations = 1;
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle pair[2];
|
|
pair[0].position = {-.075f, 1, 0};
|
|
pair[1].position = {.075f, 1, 0};
|
|
for (auto& p : pair) {
|
|
p.material.cohesion = 0;
|
|
p.material.viscosity = 0;
|
|
p.material.atmosphericPressure = 10.f;
|
|
}
|
|
REQUIRE(solver->emit(pair).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto pressured = solver->particles();
|
|
REQUIRE(pressured[0].velocity.x > 0.f);
|
|
REQUIRE(pressured[1].velocity.x < 0.f);
|
|
REQUIRE(glm::length(pressured[0].velocity + pressured[1].velocity) < 1e-6f);
|
|
|
|
solver->clear();
|
|
VolumeFluidParticle exposed;
|
|
exposed.position = {0, 1, 0};
|
|
exposed.velocity = {1, 0, 0};
|
|
exposed.material.cohesion = 0;
|
|
exposed.material.viscosity = 0;
|
|
exposed.material.drag = 60.f;
|
|
REQUIRE(solver->emit(std::span(&exposed, 1)).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(solver->particles()[0].velocity.x > 0.f);
|
|
REQUIRE(solver->particles()[0].velocity.x < .7f);
|
|
|
|
auto state = solver->snapshot();
|
|
auto encoded = encodeVolumeFluid(state);
|
|
auto particle = encoded.find("particles")->at(0);
|
|
encoded.set("particles", eve::Value(eve::Value::Array{withoutRecentMaterialFields(std::move(particle))}));
|
|
encoded = withoutSdfColliders(encoded);
|
|
encoded.set("version", 9);
|
|
auto migrated = decodeVolumeFluid(encoded);
|
|
REQUIRE(migrated.ok());
|
|
REQUIRE(migrated.value().version == 20);
|
|
REQUIRE(migrated.value().particles[0].material.atmosphericPressure == 0.f);
|
|
auto invalid = state;
|
|
invalid.particles[0].material.atmosphericPressure = 1001.f;
|
|
REQUIRE(!solver->restore(invalid).ok());
|
|
REQUIRE(solver->snapshot().particles[0].material.atmosphericPressure == 0.f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.transientPerParticleWind") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0, 0, 0};
|
|
settings.iterations = 1;
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particle;
|
|
particle.position = {0, 1, 0};
|
|
particle.material.cohesion = 0;
|
|
particle.material.viscosity = 0;
|
|
particle.material.drag = 60;
|
|
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
|
|
const glm::vec3 wind[] = {glm::vec3(6, 0, 0)};
|
|
REQUIRE(solver->setParticleWinds(wind).ok());
|
|
const glm::vec3 invalid[] = {glm::vec3(0), glm::vec3(0)};
|
|
REQUIRE(!solver->setParticleWinds(invalid).ok());
|
|
REQUIRE(!solver->step(-1.f, 1).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const float driven = solver->particles()[0].velocity.x;
|
|
REQUIRE(driven > 2.f);
|
|
REQUIRE(driven < 3.f);
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(solver->particles()[0].velocity.x > 0.f);
|
|
REQUIRE(solver->particles()[0].velocity.x < driven);
|
|
REQUIRE(solver->setParticleWinds(wind).ok());
|
|
REQUIRE(solver->setParticleWinds({}).ok());
|
|
const float before = solver->particles()[0].velocity.x;
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(solver->particles()[0].velocity.x < before);
|
|
const glm::vec3 nonfinite[] = {glm::vec3(std::numeric_limits<float>::quiet_NaN(), 0, 0)};
|
|
REQUIRE(!solver->setParticleWinds(nonfinite).ok());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.boundedFluid3DWindZones") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0, 0, 0};
|
|
settings.iterations = 1;
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particle;
|
|
particle.position = {1, 1, 0};
|
|
particle.material.cohesion = 0;
|
|
particle.material.viscosity = 0;
|
|
particle.material.drag = 60;
|
|
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
|
|
VolumeFluidWindZone ambient;
|
|
ambient.direction = {0, 1, 0};
|
|
ambient.intensity = 2;
|
|
VolumeFluidWindZone sphere;
|
|
sphere.type = VolumeFluidWindZoneType::Spherical;
|
|
sphere.center = {0, 1, 0};
|
|
sphere.radius = 2;
|
|
sphere.intensity = 4;
|
|
sphere.radial = true;
|
|
const VolumeFluidWindZone zones[] = {ambient, sphere};
|
|
REQUIRE(solver->accumulateWindZones(zones, 0.f).ok());
|
|
auto invalid = sphere;
|
|
invalid.radius = 0;
|
|
REQUIRE(!solver->accumulateWindZones(std::span(&invalid, 1), 0.f).ok());
|
|
REQUIRE(!solver->accumulateWindZones(zones, -1.f).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto velocity = solver->particles()[0].velocity;
|
|
REQUIRE(velocity.x > 0.f);
|
|
REQUIRE(velocity.y > 0.f);
|
|
REQUIRE(std::abs(velocity.x / velocity.y - 1.5f) < .001f);
|
|
|
|
eve::Value zoneValue = eve::fluids::encodeVolumeFluidWindZone(ambient);
|
|
auto decoded = decodeVolumeFluidWindZones(eve::Value(eve::Value::Array{zoneValue}));
|
|
REQUIRE(decoded.ok());
|
|
REQUIRE(decoded.value().size() == 1);
|
|
REQUIRE(decoded.value()[0].intensity == 2.f);
|
|
zoneValue.set("unknown", 1);
|
|
REQUIRE(!decodeVolumeFluidWindZones(eve::Value(eve::Value::Array{zoneValue})).ok());
|
|
REQUIRE(!decodeVolumeFluidWindZones(eve::Value(eve::Value::Array(65, eve::Value{}))).ok());
|
|
|
|
VolumeFluidSettings largeSettings;
|
|
largeSettings.capacity = 65536;
|
|
auto largeCreated = VolumeFluid::create(largeSettings);
|
|
REQUIRE(largeCreated.ok());
|
|
auto large = std::move(largeCreated).takeValue();
|
|
std::vector<VolumeFluidParticle> particles(62501);
|
|
for (auto& p : particles) p.position = {0, 1, 0};
|
|
REQUIRE(large->emit(particles).ok());
|
|
std::vector<VolumeFluidWindZone> spherical(64, sphere);
|
|
REQUIRE(!large->accumulateWindZones(spherical, 0.f).ok());
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.externalForcesUseInverseMassAndClearAfterSuccessfulStep") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0.f, 0.f, 0.f};
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particles[2];
|
|
particles[0].position = {-.2f, 1.f, 0.f};
|
|
particles[1].position = {.2f, 1.f, 0.f};
|
|
particles[0].material.density = 1000.f;
|
|
particles[1].material.density = 2000.f;
|
|
REQUIRE(solver->emit(particles).ok());
|
|
const glm::vec3 forces[2] = {{8.f, 0.f, 0.f}, {8.f, 0.f, 0.f}};
|
|
REQUIRE(solver->setParticleExternalForces(forces).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto first = solver->particles();
|
|
REQUIRE(std::abs(first[0].velocity.x - 2.f * first[1].velocity.x) < 1e-5f);
|
|
REQUIRE(first[0].velocity.x > 0.f);
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto second = solver->particles();
|
|
REQUIRE(std::abs(second[0].velocity.x - first[0].velocity.x) < 1e-5f);
|
|
REQUIRE(std::abs(second[1].velocity.x - first[1].velocity.x) < 1e-5f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.externalForceZonesAreBoundedAndAtomic") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0.f, 0.f, 0.f};
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle particle;
|
|
particle.position = {0.f, 1.f, 0.f};
|
|
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
|
|
VolumeFluidWindZone zone;
|
|
zone.direction = {1.f, 0.f, 0.f};
|
|
zone.intensity = 8.f;
|
|
REQUIRE(solver->accumulateExternalForceZones(std::span(&zone, 1), 0.f).ok());
|
|
auto invalid = zone;
|
|
invalid.intensity = 1001.f;
|
|
REQUIRE(!solver->accumulateExternalForceZones(std::span(&invalid, 1), 0.f).ok());
|
|
REQUIRE(!solver->step(1.f / 120.f, 0).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(solver->particles()[0].velocity.x > 0.f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.perParticleSmoothingRadiusAndMigration") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = {0, 0, 0};
|
|
settings.iterations = 1;
|
|
auto run = [&](float firstSmoothing, float secondSmoothing) {
|
|
auto created = VolumeFluid::create(settings);
|
|
REQUIRE(created.ok());
|
|
auto solver = std::move(created).takeValue();
|
|
VolumeFluidParticle pair[2];
|
|
pair[0].position = {-.125f, 1, 0};
|
|
pair[1].position = {.125f, 1, 0};
|
|
pair[0].velocity = {1, 0, 0};
|
|
pair[1].velocity = {-1, 0, 0};
|
|
for (auto& p : pair) {
|
|
p.material.cohesion = 0;
|
|
p.material.viscosity = 100;
|
|
}
|
|
pair[0].material.smoothing = firstSmoothing;
|
|
pair[1].material.smoothing = secondSmoothing;
|
|
REQUIRE(solver->emit(pair).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
return solver;
|
|
};
|
|
auto compact = run(2, 2), mixed = run(1, 4);
|
|
REQUIRE(std::abs(compact->particles()[0].velocity.x - 1.f) < 1e-6f);
|
|
REQUIRE(mixed->particles()[0].velocity.x < .999f);
|
|
REQUIRE(std::abs(mixed->particles()[0].velocity.x + mixed->particles()[1].velocity.x) < 1e-6f);
|
|
|
|
auto current = mixed->snapshot();
|
|
auto encoded = encodeVolumeFluid(current);
|
|
eve::Value::Array particles;
|
|
for (size_t i = 0; i < encoded.find("particles")->arraySize(); ++i) {
|
|
const auto& currentParticle = encoded.find("particles")->at(i);
|
|
const auto* source = currentParticle.find("material");
|
|
eve::Value material(eve::Value::Object{});
|
|
for (const auto& key : source->keys())
|
|
if (key != "smoothing" && key != "rollingContacts" && key != "rollingFriction" &&
|
|
key != "dynamicFriction" && key != "staticFriction" && key != "stickiness" && key != "stickDistance" &&
|
|
key != "frictionCombine" && key != "stickinessCombine")
|
|
material.set(key, *source->find(key));
|
|
eve::Value particle(eve::Value::Object{});
|
|
for (const auto& key : currentParticle.keys())
|
|
if (key != "material" && key != "angularVelocity") particle.set(key, *currentParticle.find(key));
|
|
particle.set("material", std::move(material));
|
|
particles.push_back(std::move(particle));
|
|
}
|
|
encoded.set("particles", eve::Value(std::move(particles)));
|
|
encoded = withoutSdfColliders(encoded);
|
|
encoded.set("version", 10);
|
|
auto migrated = decodeVolumeFluid(encoded);
|
|
REQUIRE(migrated.ok());
|
|
REQUIRE(migrated.value().version == 20);
|
|
REQUIRE(migrated.value().particles[0].material.smoothing == 2.f);
|
|
REQUIRE(!migrated.value().particles[0].material.rollingContacts);
|
|
REQUIRE(migrated.value().particles[0].material.rollingFriction == 0.f);
|
|
REQUIRE(migrated.value().particles[0].material.dynamicFriction == .2f);
|
|
REQUIRE(migrated.value().particles[0].material.staticFriction == .2f);
|
|
REQUIRE(migrated.value().particles[0].angularVelocity == glm::vec3(0.f));
|
|
auto invalid = current;
|
|
invalid.particles[0].material.smoothing = 4.01f;
|
|
REQUIRE(!mixed->restore(invalid).ok());
|
|
REQUIRE(mixed->snapshot().particles[0].material.smoothing == 1.f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.colliderSolidifyMelt") {
|
|
auto result = VolumeFluid::create({});
|
|
REQUIRE(result.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
VolumeFluidCollider collider;
|
|
collider.label = 9;
|
|
collider.solidify = true;
|
|
REQUIRE(solver->setColliders(std::span(&collider, 1)).ok());
|
|
VolumeFluidParticle p;
|
|
p.position = {0.f, 1.25f, 0.f};
|
|
p.actorGroup = 23;
|
|
REQUIRE(solver->emit(std::span(&p, 1)).ok());
|
|
REQUIRE(solver->step(1.f / 60.f).ok());
|
|
REQUIRE(!solver->contacts().empty());
|
|
REQUIRE(solver->contacts()[0].colliderLabel == 9);
|
|
REQUIRE(solver->contacts()[0].particleIndex == 0);
|
|
REQUIRE(solver->contacts()[0].actorGroup == 23);
|
|
REQUIRE(solver->contacts()[0].impulse.y < 0.f);
|
|
REQUIRE(solver->particles()[0].material.phase == VolumeFluidPhase::Solid);
|
|
const auto frozen = solver->particles()[0].position;
|
|
REQUIRE(solver->step(1.f / 60.f).ok());
|
|
REQUIRE(solver->particles()[0].position.y == frozen.y);
|
|
REQUIRE(solver->setColliders({}).ok());
|
|
REQUIRE(solver->paint(frozen, 0.1f, {}).ok());
|
|
REQUIRE(solver->step(1.f / 60.f).ok());
|
|
REQUIRE(solver->particles()[0].position.y < frozen.y);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.solidifyStopsFurtherColliderProjection") {
|
|
auto result = VolumeFluid::create({});
|
|
REQUIRE(result.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
VolumeFluidCollider first;
|
|
first.label = 1;
|
|
first.solidify = true;
|
|
VolumeFluidCollider second;
|
|
second.label = 2;
|
|
second.center = {0.1f, 1.3f, 0.f};
|
|
const VolumeFluidCollider colliders[] = {first, second};
|
|
REQUIRE(solver->setColliders(colliders).ok());
|
|
VolumeFluidParticle particle;
|
|
particle.position = {0.f, 1.25f, 0.f};
|
|
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto frozen = solver->particles()[0];
|
|
REQUIRE(frozen.material.phase == VolumeFluidPhase::Solid);
|
|
REQUIRE(frozen.position.x == 0.f);
|
|
REQUIRE(solver->contacts().size() == 1);
|
|
REQUIRE(solver->contacts()[0].colliderLabel == 1);
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
REQUIRE(solver->particles()[0].position == frozen.position);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.solidifyDoesNotInjectViscousVelocity") {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = glm::vec3(0.f);
|
|
auto result = VolumeFluid::create(settings);
|
|
REQUIRE(result.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
VolumeFluidCollider collider;
|
|
collider.solidify = true;
|
|
REQUIRE(solver->setColliders(std::span(&collider, 1)).ok());
|
|
VolumeFluidParticle particles[2];
|
|
particles[0].position = {0.f, 1.25f, 0.f};
|
|
particles[1].position = {0.f, 1.44f, 0.f};
|
|
for (auto& particle : particles) {
|
|
particle.material.viscosity = 50.f;
|
|
particle.material.cohesion = 0.f;
|
|
}
|
|
REQUIRE(solver->emit(particles).ok());
|
|
REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto state = solver->particles();
|
|
REQUIRE(state[0].material.phase == VolumeFluidPhase::Solid);
|
|
REQUIRE(state[1].material.phase == VolumeFluidPhase::Liquid);
|
|
REQUIRE(glm::length(state[1].velocity) < 1e-6f);
|
|
}
|
|
|
|
TEST_CASE("fluids.volume.viscosityDampsShearWithoutBulkDrag") {
|
|
const auto run = [&](float viscosity) {
|
|
VolumeFluidSettings settings;
|
|
settings.gravity = glm::vec3(0.f);
|
|
auto result = VolumeFluid::create(settings);
|
|
REQUIRE(result.ok());
|
|
auto solver = std::move(result).takeValue();
|
|
VolumeFluidParticle particles[2];
|
|
particles[0].position = {-0.06f, 1.f, 0.f};
|
|
particles[1].position = {0.06f, 1.f, 0.f};
|
|
particles[0].velocity = {0.f, 0.04f, 0.1f};
|
|
particles[1].velocity = {0.f, -0.04f, 0.1f};
|
|
for (auto& particle : particles) {
|
|
particle.material.viscosity = viscosity;
|
|
particle.material.cohesion = 0.f;
|
|
}
|
|
REQUIRE(solver->emit(particles).ok());
|
|
for (int tick = 0; tick < 60; ++tick) REQUIRE(solver->step(1.f / 120.f, 1).ok());
|
|
const auto state = solver->particles();
|
|
const auto mean = (state[0].velocity + state[1].velocity) * 0.5f;
|
|
// Viscosity damps relative motion, not the common translation.
|
|
REQUIRE(glm::length(mean - glm::vec3(0.f, 0.f, 0.1f)) < 1e-4f);
|
|
return glm::length(state[0].velocity - state[1].velocity);
|
|
};
|
|
const float inviscid = run(0.f);
|
|
const float medium = run(10.f);
|
|
const float viscous = run(100.f);
|
|
REQUIRE(std::abs(inviscid - 0.08f) < 1e-4f);
|
|
REQUIRE(medium < inviscid * 0.95f);
|
|
REQUIRE(viscous < medium * 0.5f);
|
|
}
|