Files
EVEngine/test/particles_playback.cpp
T

255 lines
9.3 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "graphics/Graphics.h"
#include "particles/ParticleEmitter.h"
#include "particles/ParticleSystem.h"
#include "particles/Particles.h"
#include "window/Window.h"
#include <cmath>
#include <cstdint>
#include <filesystem>
using namespace eve::particles;
namespace {
void configureDeterministicEmitter(ParticleEmitter* emitter, int seed) {
emitter->setRandomSeed(seed);
emitter->setPosition(12.f, 34.f);
emitter->setParticleLifetime(0.5f, 2.f);
emitter->setDirection(0.7f);
emitter->setSpread(1.2f);
emitter->setSpeed(10.f, 80.f);
emitter->setSizeVariation(0.5f);
emitter->setStartRotation(-90.f, 90.f);
}
} // namespace
TEST_CASE("particles.playback.seedIsReproducible") {
auto* particles = Particles::create();
auto* a = particles->newEmitter(16);
auto* b = particles->newEmitter(16);
configureDeterministicEmitter(a, 1337);
configureDeterministicEmitter(b, 1337);
a->emit(8);
b->emit(8);
REQUIRE(a->getCount() == b->getCount());
for (int i = 0; i < a->getCount(); ++i) {
const Particle& pa = a->sim()->particles[size_t(i)];
const Particle& pb = b->sim()->particles[size_t(i)];
CHECK(std::abs(pa.life - pb.life) < 1e-6f);
CHECK(std::abs(pa.vx - pb.vx) < 1e-6f);
CHECK(std::abs(pa.vy - pb.vy) < 1e-6f);
CHECK(std::abs(pa.size - pb.size) < 1e-6f);
CHECK(std::abs(pa.rot - pb.rot) < 1e-6f);
}
}
TEST_CASE("particles.playback.startRewindsFixedSeed") {
auto* emitter = Particles::create()->newEmitter(8);
configureDeterministicEmitter(emitter, 42);
emitter->start();
emitter->emit(1);
const Particle first = emitter->sim()->particles[0];
emitter->reset();
emitter->start();
emitter->emit(1);
const Particle replay = emitter->sim()->particles[0];
CHECK(std::abs(first.life - replay.life) < 1e-6f);
CHECK(std::abs(first.vx - replay.vx) < 1e-6f);
CHECK(std::abs(first.vy - replay.vy) < 1e-6f);
}
TEST_CASE("particles.playback.fixedStepAndSpeed") {
auto* emitter = Particles::create()->newEmitter(4);
emitter->setRandomSeed(7);
emitter->setParticleLifetime(10.f, 10.f);
emitter->setDirection(0.f);
emitter->setSpread(0.f);
emitter->setSpeed(10.f, 10.f);
emitter->setFixedTimeStep(0.1f, 4);
emitter->setPlaybackSpeed(2.f);
emitter->emit(1);
advanceEmitterSim(*emitter->config(), *emitter->sim(), 0.025f);
CHECK(std::abs(emitter->sim()->particles[0].x) < 1e-6f);
advanceEmitterSim(*emitter->config(), *emitter->sim(), 0.025f);
CHECK(std::abs(emitter->sim()->particles[0].x - 1.f) < 1e-5f);
}
TEST_CASE("particles.playback.loopResetsTimelineAndBursts") {
auto* emitter = Particles::create()->newEmitter(16);
emitter->setRandomSeed(9);
emitter->setParticleLifetime(10.f, 10.f);
emitter->setEmitterLifetime(0.1f);
emitter->setLooping(true);
emitter->addBurst(0.f, 2);
emitter->start();
advanceEmitterSim(*emitter->config(), *emitter->sim(), 0.11f);
CHECK(emitter->isActive());
CHECK_EQ(emitter->getCount(), 2);
CHECK(emitter->sim()->emitterAge < 0.1f);
advanceEmitterSim(*emitter->config(), *emitter->sim(), 0.01f);
CHECK_EQ(emitter->getCount(), 4);
}
TEST_CASE("particles.playback.distanceEmissionIsEvenlySpaced") {
auto* emitter = Particles::create()->newEmitter(16);
emitter->setRandomSeed(10);
emitter->setParticleLifetime(10.f, 10.f);
emitter->setSpeed(0.f, 0.f);
emitter->setEmissionRateOverDistance(0.5f);
emitter->setPosition(0.f, 0.f);
emitter->start();
advanceEmitterSim(*emitter->config(), *emitter->sim(), 0.01f);
emitter->setPosition(10.f, 0.f);
advanceEmitterSim(*emitter->config(), *emitter->sim(), 0.01f);
REQUIRE(emitter->getCount() == 5);
for (int i = 0; i < 5; ++i) {
CHECK(std::abs(emitter->sim()->particles[size_t(i)].x - float((i + 1) * 2)) < 1e-5f);
CHECK(std::abs(emitter->sim()->particles[size_t(i)].y) < 1e-6f);
}
}
TEST_CASE("particles.playback.configAppliesProfessionalControls") {
auto* emitter = Particles::create()->newEmitter(8);
const char* json = R"({
"emissionRateOverDistance": 3.5,
"emitterLife": 2.0,
"looping": true,
"playbackSpeed": 0.5,
"fixedTimeStep": 0.0166667,
"maxSubSteps": 6,
"randomSeed": 1234,
"autoRandomSeed": false
})";
REQUIRE(emitter->applyConfig(json));
CHECK(std::abs(emitter->getEmissionRateOverDistance() - 3.5f) < 1e-6f);
CHECK(emitter->getLooping());
CHECK(std::abs(emitter->getPlaybackSpeed() - 0.5f) < 1e-6f);
CHECK(std::abs(emitter->getFixedTimeStep() - 0.0166667f) < 1e-6f);
CHECK_EQ(emitter->config()->maxSubSteps, 6);
CHECK_EQ(emitter->getRandomSeed(), 1234);
CHECK(!emitter->getAutoRandomSeed());
}
TEST_CASE("particles.playback.exposedParametersDriveRuntimeScales") {
auto* emitter = Particles::create()->newEmitter(32);
emitter->setRandomSeed(91);
emitter->setEmissionRate(10.f);
emitter->setParticleLifetime(5.f, 5.f);
emitter->setSpeed(20.f, 20.f);
emitter->setSizeVariation(0.f);
emitter->setFloatParameter("intensity", 2.f);
emitter->bindFloatParameter("intensity", "emission");
emitter->bindFloatParameter("intensity", "speed", 0.5f, 0.f);
emitter->bindFloatParameter("intensity", "size", 0.75f, 0.f);
emitter->start();
advanceEmitterSim(*emitter->config(), *emitter->sim(), 0.1f);
REQUIRE_EQ(emitter->getCount(), 2);
CHECK(std::abs(emitter->sim()->particles[0].size - 1.5f) < 1e-5f);
const auto& particle = emitter->sim()->particles[0];
CHECK(std::abs(std::sqrt(particle.vx * particle.vx + particle.vy * particle.vy) - 20.f) < 1e-4f);
emitter->bindFloatParameter("freeze", "playback");
emitter->setFloatParameter("freeze", 0.f);
const float life = emitter->sim()->particles[0].life;
advanceEmitterSim(*emitter->config(), *emitter->sim(), 1.f);
CHECK(std::abs(emitter->sim()->particles[0].life - life) < 1e-5f);
}
TEST_CASE("particles.playback.renderDistanceTrailAndLoopingBurst") {
auto* window = eve::window::Window::create();
auto* gfx = eve::graphics::Graphics::create();
REQUIRE(window != nullptr);
REQUIRE(gfx != nullptr);
eve::window::WindowSettings settings;
settings.width = 640;
settings.height = 420;
settings.centered = true;
REQUIRE(window->setWindowSettings(settings));
gfx->setScreenReadbackEnabled(true);
auto* particles = Particles::create();
const uint8_t whitePixel[4] = {255, 255, 255, 255};
auto* whiteTexture = gfx->newTexture(1, 1, whitePixel);
auto* trail = particles->newEmitter(900);
trail->setTexture(whiteTexture);
trail->setRandomSeed(20260826);
trail->setEmissionRateOverDistance(0.38f);
trail->setParticleLifetime(0.65f, 1.15f);
trail->setParticleSize(22.f, 22.f);
trail->setSizes(1.f, 0.45f);
trail->setSizeVariation(0.35f);
trail->setSpeed(0.f, 0.f);
trail->setSpread(6.2831853f);
trail->setGravity(0.f, 0.f);
trail->setDamping(0.25f);
trail->setBlendMode("alpha");
trail->setColorStart(0.35f, 0.9f, 1.f, 1.f);
trail->setColorEnd(0.45f, 0.15f, 1.f, 0.35f);
trail->setRibbon(0.55f, 2.f);
trail->setFixedTimeStep(1.f / 120.f, 8);
trail->setPosition(320.f, 210.f);
trail->start();
auto* burst = particles->newEmitter(600);
burst->setTexture(whiteTexture);
burst->setRandomSeed(424242);
burst->setPosition(320.f, 210.f);
burst->setEmitterLifetime(0.9f);
burst->setLooping(true);
burst->addBurst(0.f, 72);
burst->setParticleLifetime(0.7f, 1.4f);
burst->setParticleSize(18.f, 18.f);
burst->setSizes(1.2f, 0.4f);
burst->setSpeed(80.f, 210.f);
burst->setSpread(6.2831853f);
burst->setGravity(0.f, 95.f);
burst->setDamping(0.18f);
burst->setBlendMode("alpha");
burst->clearColorGradient();
burst->addColorStop(0.f, 1.f, 0.96f, 0.65f, 1.f);
burst->addColorStop(0.35f, 1.f, 0.35f, 0.08f, 0.9f);
burst->addColorStop(1.f, 0.3f, 0.04f, 0.01f, 0.3f);
burst->setFixedTimeStep(1.f / 120.f, 8);
burst->start();
gfx->setBackgroundColorRGBA(0.025f, 0.035f, 0.065f, 1.f);
constexpr float dt = 1.f / 60.f;
for (int frame = 0; frame < 120; ++frame) {
const float t = float(frame) * dt;
trail->setPosition(320.f + std::cos(t * 1.35f) * 240.f, 210.f + std::sin(t * 2.05f) * 125.f);
ParticleSimSystem::update(dt);
gfx->clearScreen();
gfx->drawSolidRectRGBA(28.f, 26.f, 584.f, 368.f, 0.04f, 0.065f, 0.12f, 1.f);
ParticleRenderSystem::render(gfx);
gfx->present();
}
REQUIRE_GT(trail->getCount(), 20);
REQUIRE_GT(burst->getCount(), 20);
int particlesInView = 0;
for (auto* emitter : {trail, burst}) {
for (int i = 0; i < emitter->getCount(); ++i) {
const Particle& p = emitter->sim()->particles[size_t(i)];
if (p.x >= 0.f && p.x < 640.f && p.y >= 0.f && p.y < 420.f) ++particlesInView;
}
}
REQUIRE_GT(particlesInView, 20);
const std::string output = std::string(EVENGINE_TEST_BINARY_DIR) + "/particle_playback_lab.png";
CHECK(gfx->saveFramePng(output));
CHECK(std::filesystem::exists(output));
REQUIRE(std::filesystem::file_size(output) > uintmax_t(1024));
window->close();
}