Files
EVEngine/test/fluids_volume_emitter.cpp

867 lines
39 KiB
C++

#include <glm/geometric.hpp>
#include <limits>
#include "fluids/VolumeFluidCodec.h"
#include "fluids/VolumeFluidEmitter.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 void removeV12MaterialFields(eve::Value& description) {
auto* prototype = description.find("prototype");
const auto* source = prototype ? prototype->find("material") : nullptr;
REQUIRE(source != nullptr);
eve::Value material(eve::Value::Object{});
for (const auto& key : source->keys())
if (key != "stickiness" && key != "stickDistance" && key != "frictionCombine" && key != "stickinessCombine")
material.set(key, *source->find(key));
prototype->set("material", std::move(material));
}
TEST_CASE("fluids.volumeEmitter.shapesRepeatableAndAtomic") {
for (auto shape : {VolumeFluidEmissionShape::Edge, VolumeFluidEmissionShape::Square, VolumeFluidEmissionShape::Disk,
VolumeFluidEmissionShape::Sphere, VolumeFluidEmissionShape::Cube}) {
auto ar = VolumeFluid::create({}), br = VolumeFluid::create({});
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidEmission e;
e.shape = shape;
e.direction = {0.f, 0.f, 1.f};
e.extent = {0.2f, 0.2f, 0.2f};
e.seed = 42;
e.jitter = 0.2f;
REQUIRE(emitVolumeFluidBurst(*a, e, 100).ok());
REQUIRE(emitVolumeFluidBurst(*b, e, 100).ok());
auto pa = a->particles(), pb = b->particles();
for (size_t i = 0; i < pa.size(); ++i) {
REQUIRE(glm::length(pa[i].position - pb[i].position) == 0.f);
REQUIRE(glm::length(pa[i].velocity - pb[i].velocity) == 0.f);
const auto offset = pa[i].position - e.origin;
REQUIRE(std::abs(offset.x) <= .20001f);
REQUIRE(std::abs(offset.y) <= .20001f);
REQUIRE(std::abs(offset.z) <= .20001f);
if (shape == VolumeFluidEmissionShape::Disk || shape == VolumeFluidEmissionShape::Sphere)
REQUIRE(glm::length(offset) <= .20001f);
if (shape == VolumeFluidEmissionShape::Edge) REQUIRE(offset.y == 0.f);
if (shape != VolumeFluidEmissionShape::Sphere && shape != VolumeFluidEmissionShape::Cube)
REQUIRE(offset.z == 0.f);
}
e.origin.y = -10.f;
REQUIRE(!emitVolumeFluidBurst(*a, e, 2).ok());
REQUIRE(a->particleCount() == 100);
}
}
TEST_CASE("fluids.volumeEmitter.orderedShapeSetComposition") {
VolumeFluidEmission base;
base.origin = {9.f, 8.f, 7.f};
base.direction = {1.f, 0.f, 0.f};
base.speed = 2.f;
base.prototype.color = {.2f, .3f, .4f, .5f};
VolumeFluidEmission first;
first.shape = VolumeFluidEmissionShape::Distribution;
first.origin = {1.f, 2.f, 3.f};
first.direction = {1.f, 0.f, 0.f};
first.distribution = {{{0.f, 1.f, 0.f}, {1.f, 0.f, 0.f, 1.f}, {0.f, 0.f, 1.f}}};
VolumeFluidEmission second;
second.shape = VolumeFluidEmissionShape::Distribution;
second.origin = {-1.f, 0.f, 0.f};
second.direction = {0.f, 0.f, 1.f};
const std::vector shapes{first, second};
auto result = composeVolumeFluidEmitterShapes(base, shapes);
REQUIRE(result.ok());
const auto& combined = result.value();
REQUIRE(combined.shape == VolumeFluidEmissionShape::Distribution);
REQUIRE(combined.distribution.size() == 2);
REQUIRE(glm::length(combined.distribution[0].position - glm::vec3(1.f, 3.f, 3.f)) < .00001f);
REQUIRE(glm::length(combined.distribution[0].direction - glm::vec3(1.f, 0.f, 0.f)) < .00001f);
REQUIRE(combined.distribution[0].color == glm::vec4(1.f, 0.f, 0.f, 1.f));
REQUIRE(combined.distribution[1].position == second.origin);
REQUIRE(combined.distribution[1].direction == second.direction);
REQUIRE(combined.speed == base.speed);
REQUIRE(combined.prototype.color == base.prototype.color);
auto fallback = composeVolumeFluidEmitterShapes(base, {});
REQUIRE(fallback.ok());
REQUIRE(fallback.value().distribution.size() == 1);
REQUIRE(fallback.value().distribution[0].position == base.origin);
REQUIRE(fallback.value().distribution[0].direction == glm::vec3(1.f, 0.f, 0.f));
first.shape = VolumeFluidEmissionShape::Cube;
const std::vector invalid{first};
REQUIRE(!composeVolumeFluidEmitterShapes(base, invalid).ok());
REQUIRE(combined.distribution.size() == 2);
VolumeFluidEmission dense = second;
dense.distribution.assign(4096, {});
const std::vector excessivePoints{dense, second};
REQUIRE(!composeVolumeFluidEmitterShapes(base, excessivePoints).ok());
const std::vector excessiveShapes(65, second);
REQUIRE(!composeVolumeFluidEmitterShapes(base, excessiveShapes).ok());
}
TEST_CASE("fluids.volumeEmitter.granularRadiusRandomness") {
VolumeFluidSettings settings;
settings.spacing = .1f;
auto ar = VolumeFluid::create(settings), br = VolumeFluid::create(settings);
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidEmission emission;
emission.seed = 91;
emission.granularRadiusRandomness = 100.f;
emission.prototype.material.phase = VolumeFluidPhase::Granular;
REQUIRE(emitVolumeFluidBurst(*a, emission, 64).ok());
REQUIRE(emitVolumeFluidBurst(*b, emission, 64).ok());
bool varied = false;
for (size_t i = 0; i < a->particleCount(); ++i) {
const auto ra = a->particles()[i].radii, rb = b->particles()[i].radii;
REQUIRE(ra == rb);
REQUIRE(ra.x == ra.y);
REQUIRE(ra.y == ra.z);
REQUIRE(ra.x >= .001f);
REQUIRE(ra.x <= .051001f);
varied |= std::abs(ra.x - .05f) > .001f;
}
REQUIRE(varied);
auto liquid = emission;
liquid.prototype.material.phase = VolumeFluidPhase::Liquid;
liquid.prototype.radii = glm::vec3(.03f);
auto lr = VolumeFluid::create(settings);
REQUIRE(lr.ok());
auto liquidSolver = std::move(lr).takeValue();
REQUIRE(emitVolumeFluidBurst(*liquidSolver, liquid, 1).ok());
REQUIRE(liquidSolver->particles()[0].radii == glm::vec3(.03f));
emission.granularRadiusRandomness = 100.01f;
REQUIRE(!emitVolumeFluidBurst(*a, emission, 1).ok());
}
TEST_CASE("fluids.volumeEmitter.streamPoolAndBudget") {
auto created = VolumeFluid::create(VolumeFluidSettings{.capacity = 10});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmitter stream;
VolumeFluidEmission e;
e.prototype.life = .01f;
auto result = stream.advance(*solver, e, 1.f / 60.f, 6000.f, 3);
REQUIRE(result.ok());
REQUIRE(result.value() == 3);
// Whole credit beyond the explicit work cap must not become a catch-up burst.
result = stream.advance(*solver, e, 0.f, 0.f, 3);
REQUIRE(result.ok());
REQUIRE(result.value() == 0);
for (int i = 0; i < 3; ++i) REQUIRE(stream.advance(*solver, e, 1.f / 60.f, 6000.f, 3).ok());
REQUIRE(solver->particleCount() == 10);
REQUIRE(stream.advance(*solver, e, 1.f / 60.f, 6000.f, 3).value() == 0);
REQUIRE(solver->step(1.f / 60.f).ok());
REQUIRE(solver->particleCount() == 0);
REQUIRE(stream.advance(*solver, e, 1.f / 60.f, 6000.f, 3).value() == 3);
}
TEST_CASE("fluids.volumeEmitter.zeroSpeedStopsWithoutCatchupDebt") {
VolumeFluidSettings settings;
settings.capacity = 16;
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmission emission;
emission.shape = VolumeFluidEmissionShape::Edge;
emission.extent = glm::vec3(0.f);
VolumeFluidEmitter stream;
REQUIRE(stream.advance(*solver, emission, .006f, 100.f, 1, 0.f).value() == 0);
emission.speed = 0.f;
REQUIRE(stream.advance(*solver, emission, 1.f / 30.f, 100.f, 1, 0.f).value() == 0);
REQUIRE(solver->particleCount() == 0);
REQUIRE(stream.snapshot().phase == "0");
emission.speed = 1.f;
REQUIRE(stream.advance(*solver, emission, .006f, 100.f, 1, 0.f).value() == 0);
REQUIRE(stream.advance(*solver, emission, .006f, 100.f, 1, 0.f).value() == 1);
solver->clear();
VolumeFluidJetEmitter jet;
REQUIRE(jet.advance(*solver, emission, .026f, 1, 0.f).value() == 0);
emission.speed = 0.f;
REQUIRE(jet.advance(*solver, emission, 1.f / 30.f, 1, 0.f).value() == 0);
REQUIRE(jet.snapshot().phase == "0");
emission.speed = 1.f;
for (int i = 0; i < 3; ++i) REQUIRE(jet.advance(*solver, emission, .026f, 1, 0.f).value() == 0);
REQUIRE(jet.advance(*solver, emission, .026f, 1, 0.f).value() == 1);
}
TEST_CASE("fluids.volumeEmitter.burstWaitsForActorDeathAndPoolRecovery") {
VolumeFluidSettings settings;
settings.capacity = 4;
settings.gravity = {0.f, 0.f, 0.f};
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmitter emitter;
VolumeFluidEmission emission;
emission.seed = 17;
emission.jitter = .25f;
emission.prototype.actorGroup = 7;
emission.prototype.life = .01f;
auto first = emitter.advanceBurst(*solver, emission, 2, 0.f);
REQUIRE(first.ok());
REQUIRE(first.value() == 2);
REQUIRE(solver->particleCount() == 2);
auto waiting = emitter.advanceBurst(*solver, emission, 2, 0.f);
REQUIRE(waiting.ok());
REQUIRE(waiting.value() == 0);
REQUIRE(solver->particleCount() == 2);
VolumeFluidEmission other = emission;
other.prototype.actorGroup = 8;
other.prototype.life = 0.f;
REQUIRE(emitVolumeFluidBurst(*solver, other, 2).ok());
REQUIRE(solver->killParticle(1).ok());
REQUIRE(solver->killParticle(0).ok());
REQUIRE(solver->particleCount() == 2);
auto actorPoolIgnoresOtherActorPressure = emitter.advanceBurst(*solver, emission, 2, .5f);
REQUIRE(actorPoolIgnoresOtherActorPressure.ok());
REQUIRE(actorPoolIgnoresOtherActorPressure.value() == 2);
auto actorStillAlive = emitter.advanceBurst(*solver, emission, 2, 0.f);
REQUIRE(actorStillAlive.ok());
REQUIRE(actorStillAlive.value() == 0);
REQUIRE(solver->particleCount() == 4);
}
TEST_CASE("fluids.volumeEmitter.burstFailureIsAtomicAndSnapshotContinuesSequence") {
VolumeFluidSettings settings;
settings.capacity = 8;
settings.gravity = {0.f, 0.f, 0.f};
auto ar = VolumeFluid::create(settings), br = VolumeFluid::create(settings);
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidEmitter source, restored;
VolumeFluidEmission emission;
emission.shape = VolumeFluidEmissionShape::Sphere;
emission.seed = 91;
emission.jitter = .2f;
emission.prototype.actorGroup = 11;
emission.prototype.life = .01f;
REQUIRE(source.advanceBurst(*a, emission, 2, 0.f).ok());
REQUIRE(a->killParticle(1).ok());
REQUIRE(a->killParticle(0).ok());
const auto saved = source.snapshot();
const auto particleCount = a->particleCount();
REQUIRE(!source.advanceBurst(*a, emission, 0, 0.f).ok());
REQUIRE(!source.advanceBurst(*a, emission, 2, 1.01f).ok());
REQUIRE(source.snapshot().sequence == saved.sequence);
REQUIRE(source.snapshot().phase == saved.phase);
REQUIRE(source.snapshot().emitting == saved.emitting);
REQUIRE(a->particleCount() == particleCount);
REQUIRE(restored.restore(saved).ok());
auto nextA = source.advanceBurst(*a, emission, 2, 0.f);
auto nextB = restored.advanceBurst(*b, emission, 2, 0.f);
REQUIRE(nextA.ok());
REQUIRE(nextB.ok());
REQUIRE(nextA.value() == 2);
REQUIRE(nextB.value() == 2);
for (size_t i = 0; i < 2; ++i) {
REQUIRE(glm::length(a->particles()[i].position - b->particles()[i].position) == 0.f);
REQUIRE(glm::length(a->particles()[i].velocity - b->particles()[i].velocity) == 0.f);
}
}
TEST_CASE("fluids.volumeEmitter.failurePreservesStream") {
auto ar = VolumeFluid::create({}), br = VolumeFluid::create({});
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidEmitter sa, sb;
VolumeFluidEmission e;
REQUIRE(sa.advance(*a, e, .01f, 50.f).ok());
REQUIRE(sb.advance(*b, e, .01f, 50.f).ok());
auto bad = e;
bad.origin.y = -20.f;
REQUIRE(!sa.advance(*a, bad, .02f, 50.f).ok());
REQUIRE(sa.advance(*a, e, .02f, 50.f).ok());
REQUIRE(sb.advance(*b, e, .02f, 50.f).ok());
REQUIRE(a->particleCount() == 1);
REQUIRE(glm::length(a->particles()[0].position - b->particles()[0].position) == 0.f);
REQUIRE(!sa.advance(*a, e, std::numeric_limits<float>::quiet_NaN(), 50.f).ok());
}
TEST_CASE("fluids.volumeEmitter.restartThreshold") {
auto created = VolumeFluid::create(VolumeFluidSettings{.capacity = 10});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
std::vector<VolumeFluidParticle> initial(5);
for (size_t i = 0; i < initial.size(); ++i) {
initial[i].position = {float(i) * .1f, 1.f, 0.f};
initial[i].life = .01f;
}
REQUIRE(solver->emit(initial).ok());
VolumeFluidEmitter stream;
VolumeFluidEmission e;
e.actorCapacity = 10;
auto stopped = stream.advance(*solver, e, 1.f / 60.f, 600.f, 2, .5f);
REQUIRE(stopped.ok());
REQUIRE(stopped.value() == 0);
REQUIRE(solver->step(1.f / 60.f).ok());
auto restarted = stream.advance(*solver, e, 1.f / 60.f, 600.f, 2, .5f);
REQUIRE(restarted.ok());
REQUIRE(restarted.value() == 2);
}
#include "fluids/VolumeFluidCodec.h"
TEST_CASE("fluids.volumeEmitter.strictDescriptionCodec") {
VolumeFluidEmission original;
original.seed = 123;
original.speed = 3.f;
auto value = encodeVolumeFluidEmission(original);
auto decoded = decodeVolumeFluidEmission(value);
REQUIRE(decoded.ok());
REQUIRE(decoded.value().seed == 123);
REQUIRE(decoded.value().speed == 3.f);
value.set("version", 99);
REQUIRE(!decodeVolumeFluidEmission(value).ok());
value = encodeVolumeFluidEmission(original);
value.set("extra", 1);
REQUIRE(!decodeVolumeFluidEmission(value).ok());
value = encodeVolumeFluidEmission(original);
auto description = *value.find("description");
description.set("extra", 1);
value.set("description", description);
REQUIRE(!decodeVolumeFluidEmission(value).ok());
REQUIRE(!decodeVolumeFluidEmission(eve::Value(3)).ok());
}
TEST_CASE("fluids.volumeEmitter.batchDecodeIsIndependent") {
VolumeFluidSnapshot state;
VolumeFluidParticle particle;
particle.position = {0.f, 1.f, 0.f};
state.particles.push_back(particle);
auto encoded = encodeVolumeFluid(state);
auto batch = decodeVolumeFluidParticles(*encoded.find("particles"));
REQUIRE(batch.ok());
REQUIRE(batch.value().size() == 1);
state.particles.clear();
REQUIRE(batch.value()[0].position.y == 1.f);
REQUIRE(!decodeVolumeFluidParticles(eve::Value(1)).ok());
eve::Value::Array malformed;
malformed.emplace_back(eve::Value::Object{});
REQUIRE(!decodeVolumeFluidParticles(eve::Value(std::move(malformed))).ok());
}
TEST_CASE("fluids.volumeEmitter.jetSpacingAndTemporalLayers") {
auto created = VolumeFluid::create({});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidJetEmitter jet;
VolumeFluidEmission e;
e.direction = {0.f, 0.f, 1.f};
e.extent = {.1f, .1f, .1f};
e.speed = 12.f;
auto result = jet.advance(*solver, e, .025f, 32, 0.f);
REQUIRE(result.ok());
REQUIRE(result.value() == 15);
const auto particles = solver->particles();
for (size_t i = 0; i < particles.size(); ++i)
for (size_t j = i + 1; j < particles.size(); ++j)
REQUIRE(glm::length(particles[i].position - particles[j].position) >= .09999f);
for (const auto& p : particles) {
REQUIRE(p.position.z >= 0.f);
REQUIRE(p.position.z <= .20001f);
}
}
TEST_CASE("fluids.volumeEmitter.jetRejectsAndPreservesPhase") {
auto ar = VolumeFluid::create({}), br = VolumeFluid::create({});
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidJetEmitter ja, jb;
VolumeFluidEmission e;
e.speed = 3.f;
REQUIRE(ja.advance(*a, e, .02f, 16).ok());
REQUIRE(jb.advance(*b, e, .02f, 16).ok());
auto bad = e;
bad.origin.y = -10.f;
REQUIRE(!ja.advance(*a, bad, .02f, 16).ok());
REQUIRE(!ja.advance(*a, e, .02f, 1).ok());
auto ra = ja.advance(*a, e, .02f, 16), rb = jb.advance(*b, e, .02f, 16);
REQUIRE(ra.ok());
REQUIRE(rb.ok());
REQUIRE(ra.value() == 5);
REQUIRE(glm::length(a->particles()[0].position - b->particles()[0].position) == 0.f);
bad = e;
bad.shape = VolumeFluidEmissionShape::Cube;
REQUIRE(!ja.advance(*a, bad, .02f, 16).ok());
}
TEST_CASE("fluids.volumeEmitter.directEmittingStateMatchesSnapshotWithoutProjection") {
auto created = VolumeFluid::create({});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmission emission;
VolumeFluidEmitter rate;
VolumeFluidJetEmitter jet;
CHECK(!rate.isEmitting());
CHECK(!jet.isEmitting());
CHECK(rate.isEmitting() == rate.snapshot().emitting);
CHECK(jet.isEmitting() == jet.snapshot().emitting);
REQUIRE(rate.advance(*solver, emission, .02f, 100.f, 1, 0.f).value() == 1);
CHECK(rate.isEmitting());
CHECK(rate.isEmitting() == rate.snapshot().emitting);
solver->clear();
REQUIRE(jet.advance(*solver, emission, 1.f / 30.f, 8, 0.f).ok());
CHECK(jet.isEmitting());
CHECK(jet.isEmitting() == jet.snapshot().emitting);
auto saved = jet.snapshot();
saved.emitting = false;
REQUIRE(jet.restore(saved).ok());
CHECK(!jet.isEmitting());
}
TEST_CASE("fluids.volumeEmitter.imageMaskColorAndAdmission") {
const glm::vec4 pixels[] = {{1, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 1}, {1, 1, 1, 0}};
auto built = buildVolumeFluidImageDistribution(pixels, 2, 2, .1f, 1.f, .05f, .5f);
REQUIRE(built.ok());
REQUIRE(!built.value().empty());
for (const auto& p : built.value()) {
REQUIRE(p.color.a > .5f);
REQUIRE(p.position.z == 0.f);
}
VolumeFluidEmission e;
e.shape = VolumeFluidEmissionShape::Distribution;
e.direction = {0, 0, 1};
e.distribution = built.value();
e.prototype.color = {1, 1, 1, 1};
auto created = VolumeFluid::create({});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
REQUIRE(emitVolumeFluidBurst(*solver, e, unsigned(e.distribution.size())).ok());
auto particles = solver->particles();
for (size_t i = 0; i < particles.size(); ++i) {
REQUIRE(glm::length(particles[i].position - e.origin - e.distribution[i].position) < 1e-6f);
REQUIRE(particles[i].color.r == e.distribution[i].color.r);
}
auto decoded = decodeVolumeFluidEmission(encodeVolumeFluidEmission(e));
REQUIRE(decoded.ok());
REQUIRE(decoded.value().distribution.size() == e.distribution.size());
REQUIRE(!buildVolumeFluidImageDistribution(pixels, 3, 2, .1f, 1.f, .05f, .5f).ok());
REQUIRE(!buildVolumeFluidImageDistribution(pixels, 2, 2, .1f, 1.f, .00001f, .5f).ok());
}
TEST_CASE("fluids.volumeEmitter.imageThresholdAndAspect") {
std::vector<glm::vec4> pixels(8, glm::vec4(1.f));
REQUIRE(buildVolumeFluidImageDistribution(pixels, 4, 2, 1.f, 2.f, .5f, 1.f).value().empty());
auto points = buildVolumeFluidImageDistribution(pixels, 4, 2, 1.f, 2.f, .5f, 0.f);
REQUIRE(points.ok());
REQUIRE(points.value().size() == 8);
REQUIRE(points.value().front().position.x == -1.f);
REQUIRE(points.value().front().position.y == -.5f);
}
TEST_CASE("fluids.volumeEmitter.descriptionV1Migration") {
auto current = encodeVolumeFluidEmission(VolumeFluidEmission{});
eve::Value legacyDescription(eve::Value::Object{});
const auto& description = *current.find("description");
for (const auto& key : description.keys()) {
if (key == "distribution" || key == "granularRadiusRandomness" || key == "randomVelocity" ||
key == "useShapeColor" || key == "actorCapacity")
continue;
if (key != "prototype") {
legacyDescription.set(key, *description.find(key));
continue;
}
eve::Value oldPrototype(eve::Value::Object{});
const auto& prototype = *description.find(key);
for (const auto& field : prototype.keys())
if (field != "collisionFilter" && field != "radii" && field != "orientation" && field != "actorGroup" &&
field != "selfCollide")
oldPrototype.set(field, *prototype.find(field));
legacyDescription.set(key, withoutRecentMaterialFields(std::move(oldPrototype)));
}
current.set("version", 1);
current.set("description", legacyDescription);
auto migrated = decodeVolumeFluidEmission(current);
REQUIRE(migrated.ok());
REQUIRE(migrated.value().distribution.empty());
REQUIRE(migrated.value().prototype.collisionFilter == 0xffff0001u);
REQUIRE(migrated.value().prototype.radii == glm::vec3(0.f));
REQUIRE(migrated.value().prototype.orientation.w == 1.f);
REQUIRE(migrated.value().prototype.actorGroup == 0);
REQUIRE(migrated.value().prototype.selfCollide);
REQUIRE(migrated.value().prototype.material.atmosphericPressure == 0.f);
REQUIRE(migrated.value().prototype.material.smoothing == 2.f);
REQUIRE(!migrated.value().prototype.material.rollingContacts);
REQUIRE(migrated.value().prototype.material.rollingFriction == 0.f);
REQUIRE(migrated.value().prototype.material.dynamicFriction == .2f);
REQUIRE(migrated.value().prototype.material.staticFriction == .2f);
REQUIRE(migrated.value().prototype.angularVelocity == glm::vec3(0.f));
REQUIRE(migrated.value().granularRadiusRandomness == 0.f);
REQUIRE(migrated.value().randomVelocity == 0.f);
REQUIRE(migrated.value().useShapeColor);
REQUIRE(migrated.value().actorCapacity == 1000);
legacyDescription.set("unrecognized", 1);
current.set("description", legacyDescription);
REQUIRE(!decodeVolumeFluidEmission(current).ok());
}
TEST_CASE("fluids.volumeEmitter.distributionStreamVisitsEveryPoint") {
auto created = VolumeFluid::create({});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmission e;
e.shape = VolumeFluidEmissionShape::Distribution;
e.direction = {0, 0, 1};
e.distribution = {{{-.2f, 0, 0}, {1, 0, 0, 1}}, {{0, 0, 0}, {0, 1, 0, 1}}, {{.2f, 0, 0}, {0, 0, 1, 1}}};
VolumeFluidEmitter emitter;
for (int i = 0; i < 3; ++i) REQUIRE(emitter.advance(*solver, e, 1.f / 30.f, 30.f, 1, 0.f).ok());
const auto p = solver->particles();
REQUIRE(p.size() == 3);
REQUIRE(p[0].position.x == -.2f);
REQUIRE(p[1].position.x == 0.f);
REQUIRE(p[2].position.x == .2f);
}
TEST_CASE("fluids.volumeEmitter.emptyImageDistribution") {
auto created = VolumeFluid::create({});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmission e;
e.shape = VolumeFluidEmissionShape::Distribution;
VolumeFluidEmitter emitter;
auto result = emitter.advance(*solver, e, 1.f / 60.f, 100.f, 16);
REQUIRE(result.ok());
REQUIRE(result.value() == 0);
REQUIRE(emitVolumeFluidBurst(*solver, e, 0).ok());
REQUIRE(!emitVolumeFluidBurst(*solver, e, 1).ok());
}
TEST_CASE("fluids.volumeEmitter.restoreContinuesExactSequence") {
auto ar = VolumeFluid::create({}), br = VolumeFluid::create({});
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidEmission e;
e.jitter = .1f;
e.seed = 57;
VolumeFluidEmitter first, restored;
REQUIRE(first.advance(*a, e, .017f, 73.f, 16).ok());
auto encoded = encodeVolumeFluidEmitterState(first.snapshot());
auto decoded = decodeVolumeFluidEmitterState(encoded);
REQUIRE(decoded.ok());
REQUIRE(restored.restore(decoded.value()).ok());
REQUIRE(b->restore(a->snapshot()).ok());
for (int i = 0; i < 20; ++i) {
auto ra = first.advance(*a, e, .011f, 73.f, 16), rb = restored.advance(*b, e, .011f, 73.f, 16);
REQUIRE(ra.ok());
REQUIRE(rb.ok());
REQUIRE(ra.value() == rb.value());
}
const auto pa = a->particles(), pb = b->particles();
REQUIRE(pa.size() == pb.size());
for (size_t i = 0; i < pa.size(); ++i) {
REQUIRE(glm::length(pa[i].position - pb[i].position) == 0.f);
REQUIRE(glm::length(pa[i].velocity - pb[i].velocity) == 0.f);
}
auto bad = first.snapshot();
bad.phase = "nan";
const auto saved = first.snapshot();
REQUIRE(!first.restore(bad).ok());
REQUIRE(first.snapshot().phase == saved.phase);
bad = saved;
bad.kind = 1;
REQUIRE(!first.restore(bad).ok());
encoded.set("unexpected", 1);
REQUIRE(!decodeVolumeFluidEmitterState(encoded).ok());
}
TEST_CASE("fluids.volumeEmitter.distributionJetEmitsWholeColoredLayers") {
auto created = VolumeFluid::create({});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmission e;
e.shape = VolumeFluidEmissionShape::Distribution;
e.origin = {0, 1, 0};
e.direction = {0, 0, 1};
e.speed = 3.f;
e.prototype.color = {.5f, .5f, .5f, 1.f};
e.distribution = {{{-.2f, 0, 0}, {1, 0, 0, 1}}, {{.2f, 0, 0}, {0, 1, 0, .5f}}};
VolumeFluidJetEmitter jet;
auto emitted = jet.advance(*solver, e, 1.f / 30.f, 4, 0.f);
REQUIRE(emitted.ok());
REQUIRE(emitted.value() == 2);
const auto particles = solver->particles();
REQUIRE(particles.size() == 2);
REQUIRE(std::abs(particles[0].position.x + .2f) < 1e-6f);
REQUIRE(std::abs(particles[1].position.x - .2f) < 1e-6f);
REQUIRE(particles[0].color == glm::vec4(1, 0, 0, 1));
REQUIRE(particles[1].color == glm::vec4(0, 1, 0, .5f));
auto before = jet.snapshot();
REQUIRE(!jet.advance(*solver, e, 1.f / 30.f, 1, 0.f).ok());
auto after = jet.snapshot();
REQUIRE(after.phase == before.phase);
REQUIRE(after.sequence == before.sequence);
}
TEST_CASE("fluids.volumeEmitter.sphereAndCubeDistributionsMatchSurfaceDirections") {
auto sphereVolume = buildVolumeFluidSphereDistribution(.2f, .1f, false);
auto sphereSurface = buildVolumeFluidSphereDistribution(.2f, .1f, true);
auto cubeVolume = buildVolumeFluidCubeDistribution({.2f, .3f, .4f}, .1f, false);
auto cubeSurface = buildVolumeFluidCubeDistribution({.2f, .3f, .4f}, .1f, true);
REQUIRE(sphereVolume.ok());
REQUIRE(sphereSurface.ok());
REQUIRE(cubeVolume.ok());
REQUIRE(cubeSurface.ok());
REQUIRE(!sphereVolume.value().empty());
REQUIRE(!sphereSurface.value().empty());
REQUIRE(cubeVolume.value().size() == 60);
REQUIRE(cubeSurface.value().size() == 54);
for (const auto& point : sphereVolume.value()) REQUIRE(point.direction == glm::vec3(0, 0, 1));
for (const auto& point : sphereSurface.value()) {
REQUIRE(std::abs(glm::length(point.position) - .2f) < 1e-5f);
REQUIRE(glm::dot(glm::normalize(point.position), point.direction) > .9999f);
}
for (const auto& point : cubeSurface.value()) REQUIRE(std::abs(glm::length(point.direction) - 1.f) < 1e-6f);
REQUIRE(!buildVolumeFluidSphereDistribution(.2f, .0001f, false).ok());
REQUIRE(!buildVolumeFluidCubeDistribution({1, 1, 1}, .001f, true).ok());
}
TEST_CASE("fluids.volumeEmitter.edgeRadialAndDiskEdgeDistributions") {
auto edge = buildVolumeFluidEdgeDistribution(.5f, .1f, 30.f);
REQUIRE(edge.ok());
REQUIRE(edge.value().size() == 5);
REQUIRE(std::abs(edge.value().front().position.x + .25f) < 1e-6f);
REQUIRE(edge.value().front().direction == glm::vec3(0, 0, 1));
REQUIRE(edge.value()[3].direction.y < -.9999f);
REQUIRE(std::abs(edge.value()[3].direction.z) < 1e-5f);
auto diskEdge = buildVolumeFluidDiskDistribution(.2f, .1f, true);
auto diskFilled = buildVolumeFluidDiskDistribution(.2f, .1f, false);
REQUIRE(diskEdge.ok());
REQUIRE(diskFilled.ok());
REQUIRE(!diskEdge.value().empty());
REQUIRE(diskFilled.value().size() > diskEdge.value().size());
for (const auto& point : diskEdge.value()) {
REQUIRE(std::abs(glm::length(point.position) - .2f) < 1e-5f);
REQUIRE(glm::dot(glm::normalize(point.position), point.direction) > .9999f);
}
REQUIRE(diskFilled.value().front().position == glm::vec3(0));
for (const auto& point : diskFilled.value()) REQUIRE(point.direction == glm::vec3(0, 0, 1));
REQUIRE(!buildVolumeFluidEdgeDistribution(.5f, 0.f, 0.f).ok());
REQUIRE(!buildVolumeFluidDiskDistribution(.2f, 0.f, false).ok());
REQUIRE(!buildVolumeFluidEdgeDistribution(10000.f, .001f, 0.f).ok());
}
TEST_CASE("fluids.volumeEmitter.distributionDirectionsDriveBurstStreamAndJet") {
VolumeFluidSettings settings;
settings.gravity = {0, 0, 0};
settings.capacity = 32;
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidEmission emission;
emission.shape = VolumeFluidEmissionShape::Distribution;
emission.origin = {0, 1, 0};
emission.direction = {0, 0, 1};
emission.speed = 6.f;
emission.distribution = {{{0, 0, 0}, {1, 1, 1, 1}, {1, 0, 0}}, {{.1f, 0, 0}, {1, 1, 1, 1}, {0, 1, 0}}};
REQUIRE(emitVolumeFluidBurst(*solver, emission, 2).ok());
auto particles = solver->particles();
REQUIRE(particles[0].velocity.x > 5.99f);
REQUIRE(particles[1].velocity.y > 5.99f);
solver->clear();
VolumeFluidEmitter stream;
REQUIRE(stream.advance(*solver, emission, .025f, 100.f, 2, 0.f).value() == 2);
particles = solver->particles();
REQUIRE(particles[0].velocity.x > 5.99f);
REQUIRE(particles[1].velocity.y > 5.99f);
solver->clear();
VolumeFluidJetEmitter jet;
REQUIRE(jet.advance(*solver, emission, 1.f / 30.f, 2, 0.f).value() == 2);
particles = solver->particles();
REQUIRE(particles[0].velocity.x > 5.99f);
REQUIRE(particles[1].velocity.y > 5.99f);
}
TEST_CASE("fluids.volumeEmitter.Fluid3DUseShapeColorCoversBurstStreamAndJet") {
VolumeFluidSettings settings;
settings.gravity = {0, 0, 0};
settings.capacity = 32;
VolumeFluidEmission emission;
emission.shape = VolumeFluidEmissionShape::Distribution;
emission.direction = {0, 0, 1};
emission.speed = 6.f;
emission.prototype.color = {.8f, .6f, .4f, 1.f};
emission.distribution = {{{0, 0, 0}, {.5f, .25f, .1f, .5f}, {0, 0, 1}}};
auto coloredResult = VolumeFluid::create(settings);
REQUIRE(coloredResult.ok());
auto colored = std::move(coloredResult).takeValue();
REQUIRE(emitVolumeFluidBurst(*colored, emission, 1).value() == 1);
REQUIRE(glm::length(colored->particles()[0].color - emission.distribution[0].color) < 1e-6f);
auto coloredStreamResult = VolumeFluid::create(settings), coloredJetResult = VolumeFluid::create(settings);
REQUIRE(coloredStreamResult.ok());
REQUIRE(coloredJetResult.ok());
auto coloredStreamSolver = std::move(coloredStreamResult).takeValue();
auto coloredJetSolver = std::move(coloredJetResult).takeValue();
VolumeFluidEmitter coloredStream;
REQUIRE(coloredStream.advance(*coloredStreamSolver, emission, .02f, 100.f, 1, 0.f).value() == 1);
REQUIRE(glm::length(coloredStreamSolver->particles()[0].color - emission.distribution[0].color) < 1e-6f);
VolumeFluidJetEmitter coloredJet;
REQUIRE(coloredJet.advance(*coloredJetSolver, emission, 1.f / 30.f, 4, 0.f).value() > 0);
for (const auto& particle : coloredJetSolver->particles())
REQUIRE(glm::length(particle.color - emission.distribution[0].color) < 1e-6f);
emission.useShapeColor = false;
auto streamResult = VolumeFluid::create(settings), jetResult = VolumeFluid::create(settings);
REQUIRE(streamResult.ok());
REQUIRE(jetResult.ok());
auto streamSolver = std::move(streamResult).takeValue(), jetSolver = std::move(jetResult).takeValue();
VolumeFluidEmitter stream;
REQUIRE(stream.advance(*streamSolver, emission, .02f, 100.f, 1, 0.f).value() == 1);
REQUIRE(glm::length(streamSolver->particles()[0].color - emission.prototype.color) < 1e-6f);
VolumeFluidJetEmitter jet;
REQUIRE(jet.advance(*jetSolver, emission, 1.f / 30.f, 4, 0.f).value() > 0);
for (const auto& particle : jetSolver->particles())
REQUIRE(glm::length(particle.color - emission.prototype.color) < 1e-6f);
}
TEST_CASE("fluids.volumeEmitter.descriptionV12AddsUseShapeColor") {
auto encoded = encodeVolumeFluidEmission(VolumeFluidEmission{});
encoded.set("version", 12);
const auto& current = *encoded.find("description");
eve::Value legacy(eve::Value::Object{});
for (const auto& key : current.keys())
if (key != "useShapeColor" && key != "actorCapacity") legacy.set(key, *current.find(key));
encoded.set("description", legacy);
auto migrated = decodeVolumeFluidEmission(encoded);
REQUIRE(migrated.ok());
REQUIRE(migrated.value().useShapeColor);
legacy.set("useShapeColor", false);
encoded.set("description", legacy);
REQUIRE(!decodeVolumeFluidEmission(encoded).ok());
}
TEST_CASE("fluids.volumeEmitter.descriptionV9AddsDistributionDirection") {
VolumeFluidEmission emission;
emission.shape = VolumeFluidEmissionShape::Distribution;
emission.distribution = {{{.1f, .2f, .3f}, {.4f, .5f, .6f, .7f}, {1, 0, 0}}};
auto legacy = encodeVolumeFluidEmission(emission);
legacy.set("version", 9);
auto currentDescription = *legacy.find("description");
eve::Value description(eve::Value::Object{});
for (const auto& key : currentDescription.keys())
if (key != "randomVelocity" && key != "useShapeColor" && key != "actorCapacity")
description.set(key, *currentDescription.find(key));
removeV12MaterialFields(description);
auto distribution = *description.find("distribution");
const auto point = distribution.at(0);
eve::Value legacyPoint(eve::Value::Object{});
legacyPoint.set("position", *point.find("position"));
legacyPoint.set("color", *point.find("color"));
eve::Value::Array points;
points.push_back(std::move(legacyPoint));
description.set("distribution", eve::Value(std::move(points)));
legacy.set("description", description);
auto migrated = decodeVolumeFluidEmission(legacy);
REQUIRE(migrated.ok());
REQUIRE(migrated.value().distribution[0].direction == glm::vec3(0, 0, 1));
REQUIRE(migrated.value().randomVelocity == 0.f);
REQUIRE(migrated.value().useShapeColor);
REQUIRE(encodeVolumeFluidEmission(emission).find("version")->asInt() == 14);
}
TEST_CASE("fluids.volumeEmitter.Fluid3DRandomVelocityBlendAndV10Migration") {
VolumeFluidSettings settings;
settings.gravity = {0, 0, 0};
settings.capacity = 8;
auto ar = VolumeFluid::create(settings), br = VolumeFluid::create(settings);
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidEmission emission;
emission.direction = {0, 0, 1};
emission.speed = 2;
emission.seed = 77;
emission.randomVelocity = 1;
REQUIRE(emitVolumeFluidBurst(*a, emission, 1).ok());
const auto randomized = a->particles()[0].velocity;
REQUIRE(std::abs(glm::length(randomized) - 2.f) < 1e-5f);
REQUIRE(glm::length(randomized - glm::vec3(0, 0, 2)) > .01f);
emission.randomVelocity = 0;
REQUIRE(emitVolumeFluidBurst(*b, emission, 1).ok());
REQUIRE(glm::length(b->particles()[0].velocity - glm::vec3(0, 0, 2)) < 1e-6f);
auto encoded = encodeVolumeFluidEmission(emission);
encoded.set("version", 10);
auto current = *encoded.find("description");
eve::Value legacy(eve::Value::Object{});
for (const auto& key : current.keys())
if (key != "randomVelocity" && key != "useShapeColor" && key != "actorCapacity")
legacy.set(key, *current.find(key));
removeV12MaterialFields(legacy);
encoded.set("description", std::move(legacy));
auto migrated = decodeVolumeFluidEmission(encoded);
REQUIRE(migrated.ok());
REQUIRE(migrated.value().randomVelocity == 0.f);
REQUIRE(migrated.value().useShapeColor);
emission.randomVelocity = 1.01f;
REQUIRE(!emitVolumeFluidBurst(*a, emission, 1).ok());
REQUIRE(a->particleCount() == 1);
}
TEST_CASE("fluids.volumeEmitter.jetRestorePreservesLayerPhase") {
auto ar = VolumeFluid::create({}), br = VolumeFluid::create({});
REQUIRE(ar.ok());
REQUIRE(br.ok());
auto a = std::move(ar).takeValue(), b = std::move(br).takeValue();
VolumeFluidJetEmitter first, restored;
VolumeFluidEmission e;
e.speed = 3.f;
REQUIRE(first.advance(*a, e, .017f, 16).ok());
auto decoded = decodeVolumeFluidEmitterState(encodeVolumeFluidEmitterState(first.snapshot()));
REQUIRE(decoded.ok());
REQUIRE(restored.restore(decoded.value()).ok());
REQUIRE(first.advance(*a, e, .021f, 16).ok());
REQUIRE(restored.advance(*b, e, .021f, 16).ok());
REQUIRE(a->particleCount() == b->particleCount());
REQUIRE(a->particleCount() == 5);
REQUIRE(glm::length(a->particles()[0].position - b->particles()[0].position) == 0.f);
}
TEST_CASE("fluids.volumeEmitter.movingNozzleSamplesEmissionTime") {
auto created = VolumeFluid::create({});
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidJetEmitter jet;
VolumeFluidEmission e;
e.shape = VolumeFluidEmissionShape::Edge;
e.extent = {0, 0, 0};
e.speed = 12.f;
VolumeFluidNozzlePose begin{{0, 1, 0}, {0, 0, 0, 1}}, end{{.3f, 1, 0}, {0, 0, 0, 1}};
auto result = jet.advanceMoving(*solver, e, begin, end, .025f, 16, 0.f, 0.f);
REQUIRE(result.ok());
REQUIRE(result.value() == 3);
const auto particles = solver->particles();
REQUIRE(std::abs(particles[0].position.x - .3f) < 1e-5f);
REQUIRE(std::abs(particles[1].position.x - .2f) < 1e-5f);
REQUIRE(std::abs(particles[2].position.x - .1f) < 1e-5f);
REQUIRE(std::abs(particles[2].position.z - .2f) < 1e-5f);
}