Files

527 lines
23 KiB
C++

#include "fluids/FluidSurfaceRenderer.h"
#include "fluids/VolumeFluid.h"
#include "fluids/VolumeFluidCodec.h"
#include "graphics/Graphics.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <cmath>
using namespace eve::fluids;
TEST_CASE("fluids.ssf.uniformVolumeColorOnlyGpuPath") {
auto* graphics = eve::graphics::Graphics::create();
REQUIRE(graphics != nullptr);
graphics->initHeadless(64, 64);
VolumeFluidSettings settings;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidParticle particle;
particle.position = glm::vec3(0.f, 1.f, 0.f);
particle.color = glm::vec4(.8f, .1f, .05f, .5f);
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
FluidSurfaceParams params;
params.width = 64;
params.height = 64;
params.eye = glm::vec3(0.f, 1.f, 3.f);
params.target = glm::vec3(0.f, 1.f, 0.f);
params.smoothIterations = 0;
FluidSurfaceRenderer renderer(params, true);
const auto depthBefore = renderer.depth();
renderer.renderVolumeColorOnly(*solver);
REQUIRE(renderer.usingGpu());
const size_t middle = (32u * 64u + 32u) * 4u;
REQUIRE(renderer.color()[middle + 3u] > 0);
REQUIRE(renderer.color()[middle] > renderer.color()[middle + 1u]);
REQUIRE(renderer.depth() == depthBefore);
const auto colorBefore = renderer.color();
std::vector<float> sceneDepth(64u * 64u, 1e30f);
REQUIRE(!renderer.occludeWithSceneDepth(sceneDepth).ok());
REQUIRE(renderer.color() == colorBefore);
}
TEST_CASE("fluids.ssf.customMaterialUniformColorOnlyRemainsGpuFastPath") {
auto* graphics = eve::graphics::Graphics::create();
REQUIRE(graphics != nullptr);
graphics->initHeadless(64, 64);
VolumeFluidSettings settings;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidParticle particle;
particle.position = glm::vec3(0.f, 1.f, 0.f);
particle.color = glm::vec4(.65f, .2f, .08f, .75f);
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
FluidSurfaceParams params;
params.width = params.height = 64;
params.eye = glm::vec3(0.f, 1.f, 3.f);
params.target = glm::vec3(0.f, 1.f, 0.f);
params.smoothIterations = 0;
FluidSurfaceRenderer cpu(params, false), gpu(params, true);
for (auto* renderer : {&cpu, &gpu}) {
REQUIRE(renderer->configureSurface(1.f, .2f, .08f, 0).ok());
REQUIRE(renderer->configureMaterial(true, .35f, .4f, 1.25f, .3f, .7f).ok());
REQUIRE(renderer->configureColors(glm::vec3(.1f, .2f, .3f), glm::vec3(.2f, .7f, .9f)).ok());
}
cpu.renderVolume(*solver);
const auto depthBefore = gpu.depth();
gpu.renderVolumeColorOnly(*solver);
REQUIRE(gpu.usingGpu());
REQUIRE(gpu.depth() == depthBefore);
const size_t middle = (32u * 64u + 32u) * 4u;
for (size_t channel = 0; channel < 4; ++channel)
CHECK(std::abs(int(cpu.color()[middle + channel]) - int(gpu.color()[middle + channel])) <= 3);
}
TEST_CASE("fluids.ssf.multicolorVolumeColorOnlyRemainsDeviceLocal") {
auto* graphics = eve::graphics::Graphics::create();
REQUIRE(graphics != nullptr);
graphics->initHeadless(64, 64);
VolumeFluidSettings settings;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidParticle particles[2];
particles[0].position = glm::vec3(-.025f, 1.f, 0.f);
particles[0].color = glm::vec4(.9f, .1f, .05f, .8f);
particles[0].radii = glm::vec3(.1f, .25f, .1f);
particles[0].orientation = glm::vec4(0.f, 0.f, .70710677f, .70710677f);
particles[1].position = glm::vec3(.025f, 1.f, 0.f);
particles[1].color = glm::vec4(.05f, .2f, .9f, .6f);
particles[1].radii = glm::vec3(.1f, .25f, .1f);
particles[1].orientation = particles[0].orientation;
REQUIRE(solver->emit(particles).ok());
FluidSurfaceParams params;
params.width = params.height = 64;
params.eye = glm::vec3(0.f, 1.f, 3.f);
params.target = glm::vec3(0.f, 1.f, 0.f);
params.smoothIterations = 0;
FluidSurfaceRenderer cpu(params, false), gpu(params, true);
REQUIRE(cpu.configureAnisotropy(true).ok());
REQUIRE(gpu.configureAnisotropy(true).ok());
cpu.renderVolume(*solver);
const auto depthBefore = gpu.depth();
gpu.renderVolumeColorOnly(*solver);
REQUIRE(gpu.usingGpu());
REQUIRE(gpu.depth() == depthBefore);
const size_t middle = (32u * 64u + 32u) * 4u;
REQUIRE(gpu.color()[middle + 3u] > 0);
for (size_t channel = 0; channel < 4; ++channel)
CHECK(std::abs(int(cpu.color()[middle + channel]) - int(gpu.color()[middle + channel])) <= 4);
}
TEST_CASE("fluids.ssf.volumeRenderUsesFixedStepInterpolationWithoutReplacingOnFailure") {
VolumeFluidSettings settings;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidParticle particle;
particle.position = {-.5f, 1.f, 0.f};
particle.velocity = {60.f, 0.f, 0.f};
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
REQUIRE(solver->step(1.f / 120.f, 1).ok());
FluidSurfaceParams params;
params.width = params.height = 65;
params.eye = {0.f, 1.f, 3.f};
params.target = {0.f, 1.f, 0.f};
params.orthographic = true;
params.orthographicSize = 1.f;
params.smoothIterations = 0;
FluidSurfaceRenderer renderer(params, false);
REQUIRE(renderer.renderVolumeInterpolated(*solver, 0.f).ok());
auto centroid = [](const std::vector<uint8_t>& rgba) {
double sum = 0, weight = 0;
for (size_t i = 0; i < rgba.size() / 4; ++i) {
const double a = rgba[i * 4 + 3];
sum += double(i % 65) * a;
weight += a;
}
return sum / weight;
};
const double previousX = centroid(renderer.color());
REQUIRE(renderer.renderVolumeInterpolated(*solver, 1.f).ok());
const double currentX = centroid(renderer.color());
REQUIRE(currentX > previousX + 10.0);
const auto preserved = renderer.color();
REQUIRE(!renderer.renderVolumeInterpolated(*solver, -.01f).ok());
REQUIRE(renderer.color() == preserved);
}
TEST_CASE("fluids.ssf.gasVolumeSelectsGasAndAccumulatesOpacity") {
VolumeFluidSettings settings;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidParticle particles[2];
particles[0].position = {0.f, 1.f, 0.f};
particles[0].color = {.9f, .15f, .05f, .8f};
particles[0].material.phase = VolumeFluidPhase::Gas;
particles[1].position = {.8f, 1.f, 0.f};
particles[1].color = {.05f, .1f, .95f, 1.f};
particles[1].material.phase = VolumeFluidPhase::Liquid;
REQUIRE(solver->emit(particles).ok());
FluidSurfaceParams params;
params.width = 65;
params.height = 65;
params.eye = {0.f, 1.f, 3.f};
params.target = {0.f, 1.f, 0.f};
FluidSurfaceRenderer renderer(params, false);
auto rendered = renderer.renderGasVolume(*solver, 5.f);
REQUIRE(rendered.ok());
const size_t center = (32u * 65u + 32u) * 4u;
REQUIRE(renderer.color()[center + 3u] > 0);
CHECK(renderer.color()[center] > renderer.color()[center + 2u]);
CHECK(glm::length(renderer.normals()[32u * 65u + 32u]) == 0.f);
CHECK(!renderer.renderGasVolume(*solver, 0.f).ok());
}
TEST_CASE("fluids.ssf.generateSurfaceToggleSelectsExistingBoundedRenderPaths") {
VolumeFluidSettings settings;
settings.gravity = glm::vec3(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.color = {.8f, .2f, .1f, 1.f};
REQUIRE(solver->emit(std::span(&particle, 1)).ok());
FluidSurfaceParams params;
params.width = params.height = 65;
params.eye = {0.f, 1.f, 3.f};
params.target = {0.f, 1.f, 0.f};
params.smoothIterations = 0;
FluidSurfaceRenderer renderer(params, false);
REQUIRE(renderer.configureSurfaceEnabled(false).ok());
REQUIRE(renderer.renderConfiguredVolume(*solver).ok());
const size_t center = 32u * 65u + 32u;
REQUIRE(renderer.color()[center * 4u + 3u] > 0);
REQUIRE(glm::length(renderer.normals()[center]) == 0.f);
REQUIRE(renderer.configureSurfaceEnabled(true).ok());
REQUIRE(renderer.renderConfiguredVolume(*solver).ok());
REQUIRE(renderer.color()[center * 4u + 3u] > 0);
REQUIRE(glm::length(renderer.normals()[center]) > .5f);
}
TEST_CASE("fluids.ssf.completeFluid3DRendererSettingsApplyAtomically") {
FluidSurfaceParams params;
params.width = params.height = 32;
FluidSurfaceRenderer renderer(params, false);
FluidRendererSettings settings;
REQUIRE(settings.blendSource == 5);
REQUIRE(settings.blendDestination == 10);
REQUIRE(settings.thicknessCutoff == 1.2f);
REQUIRE(settings.thicknessDownsample == 2);
REQUIRE(settings.blurRadius == .02f);
settings.generateSurface = false;
settings.generateReflection = false;
settings.generateRefraction = false;
settings.generateFoam = false;
settings.particleZWrite = true;
settings.surfaceDownsample = 3;
settings.refractionDownsample = 4;
REQUIRE(renderer.configureRendererSettings(settings).ok());
const auto applied = renderer.rendererSettings();
REQUIRE(!applied.generateSurface);
REQUIRE(!applied.generateReflection);
REQUIRE(!applied.generateRefraction);
REQUIRE(!applied.generateFoam);
REQUIRE(applied.particleZWrite);
REQUIRE(applied.surfaceDownsample == 3);
REQUIRE(applied.refractionDownsample == 4);
auto encoded = encodeFluidRendererSettings(settings);
REQUIRE(decodeFluidRendererSettings(encoded).ok());
encoded.set("futureField", int64_t(1));
REQUIRE(!decodeFluidRendererSettings(encoded).ok());
settings.foamDownsample = 5;
REQUIRE(!renderer.configureRendererSettings(settings).ok());
REQUIRE(renderer.rendererSettings().foamDownsample == applied.foamDownsample);
REQUIRE(renderer.rendererSettings().surfaceDownsample == applied.surfaceDownsample);
}
TEST_CASE("fluids.ssf.gasVolumeParticleBudgetPreservesFrame") {
VolumeFluidSettings settings;
settings.capacity = 65537;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
std::vector<VolumeFluidParticle> particles(settings.capacity);
for (auto& particle : particles) {
particle.position = {0.f, 1.f, 0.f};
particle.material.phase = VolumeFluidPhase::Gas;
}
REQUIRE(solver->emit(particles).ok());
FluidSurfaceParams params;
params.width = params.height = 16;
FluidSurfaceRenderer renderer(params, false);
renderer.render(std::vector<glm::vec3>{{0.f, 0.f, 0.f}}, .1f);
const auto previous = renderer.color();
auto rendered = renderer.renderGasVolume(*solver, 5.f);
REQUIRE(!rendered.ok());
CHECK(renderer.color() == previous);
}
TEST_CASE("fluids.ssf.sphericalDepthAndNormalParity") {
auto* graphics = eve::graphics::Graphics::create();
REQUIRE(graphics != nullptr);
graphics->initHeadless(64, 64);
for (int passes : {0, 1, 2}) {
FluidSurfaceParams params;
params.width = 64;
params.height = 64;
params.eye = {0, 0, 3};
params.target = {0, 0, 0};
params.smoothIterations = passes;
FluidSurfaceRenderer cpu(params, false), gpu(params, true);
const std::vector<glm::vec3> particles{{0, 0, 0}};
cpu.render(particles, .6f);
gpu.render(particles, .6f);
REQUIRE(gpu.usingGpu());
const size_t middle = 32 * 64 + 32, left = 32 * 64 + 26, right = 32 * 64 + 38;
const size_t top = 26 * 64 + 32, bottom = 38 * 64 + 32;
REQUIRE(cpu.depth()[middle] < 2.42f);
REQUIRE(cpu.depth()[right] > cpu.depth()[middle] + .06f);
REQUIRE(gpu.normals()[left].x < -.2f);
REQUIRE(gpu.normals()[right].x > .2f);
REQUIRE(gpu.normals()[top].y > .2f);
REQUIRE(gpu.normals()[bottom].y < -.2f);
REQUIRE(gpu.normals()[middle].z > .95f);
for (size_t i = 0; i < cpu.depth().size(); ++i) {
if (cpu.depth()[i] >= 1e29f) {
REQUIRE(gpu.depth()[i] >= 1e29f);
continue;
}
REQUIRE(std::abs(cpu.depth()[i] - gpu.depth()[i]) < .00002f);
REQUIRE(glm::length(cpu.normals()[i] - gpu.normals()[i]) < .002f);
// GPU thickness uses 1/256-unit fixed-point accumulation.
REQUIRE(std::abs(cpu.thickness()[i] - gpu.thickness()[i]) < 1.f / 256.f + .000001f);
}
gpu.render({}, .6f);
REQUIRE(gpu.depth()[middle] >= 1e29f);
REQUIRE(glm::length(gpu.normals()[middle]) == 0.f);
REQUIRE(gpu.thickness()[middle] == 0.f);
REQUIRE(gpu.color()[middle * 4 + 3] == 0);
}
}
TEST_CASE("fluids.ssf.worldSpaceBlurRadiusUsesExistingBoundedPass") {
auto* graphics = eve::graphics::Graphics::create();
REQUIRE(graphics != nullptr);
graphics->initHeadless(64, 64);
FluidSurfaceParams params;
params.width = params.height = 64;
params.eye = {0, 0, 3};
params.target = {0, 0, 0};
params.smoothIterations = 1;
const std::vector<glm::vec3> particles{{-.18f, 0, 0}, {.18f, 0, -.12f}};
FluidSurfaceRenderer unblurred(params, false);
REQUIRE(unblurred.configureSurfaceBlurRadius(0.f).ok());
REQUIRE(!unblurred.configureSurfaceBlurRadius(.1001f).ok());
unblurred.render(particles, .35f);
auto noPassParams = params;
noPassParams.smoothIterations = 0;
FluidSurfaceRenderer noPass(noPassParams, false);
noPass.render(particles, .35f);
REQUIRE(unblurred.depth() == noPass.depth());
params.smoothIterations = 1;
FluidSurfaceRenderer cpu(params, false), gpu(params, true);
REQUIRE(cpu.configureSurfaceBlurRadius(.02f).ok());
REQUIRE(gpu.configureSurfaceBlurRadius(.02f).ok());
cpu.render(particles, .35f);
gpu.render(particles, .35f);
REQUIRE(gpu.usingGpu());
for (size_t i = 0; i < cpu.depth().size(); ++i) {
if (cpu.depth()[i] >= 1e29f || gpu.depth()[i] >= 1e29f) continue;
REQUIRE(std::abs(cpu.depth()[i] - gpu.depth()[i]) < 1e-4f);
}
}
TEST_CASE("fluids.ssf.configuredSurfaceBlendSupportsUnityFactorsAtomically") {
FluidSurfaceParams params;
params.width = params.height = 17;
params.eye = {0.f, 0.f, 3.f};
params.target = {0.f, 0.f, 0.f};
params.smoothIterations = 0;
FluidSurfaceRenderer renderer(params, false);
renderer.render(std::vector<glm::vec3>{{0.f, 0.f, 0.f}}, .8f);
const size_t center = (8u * 17u + 8u) * 4u;
const auto source = renderer.color();
std::vector<uint8_t> scene(source.size(), 0u);
for (size_t i = 0; i < scene.size(); i += 4u) {
scene[i + 0u] = 128u;
scene[i + 1u] = 64u;
scene[i + 2u] = 32u;
scene[i + 3u] = 255u;
}
REQUIRE(renderer.configureSurfaceBlend(2, 0).ok()); // DstColor, Zero.
REQUIRE(renderer.compositeConfiguredSurfaceBlend(scene).ok());
CHECK(renderer.color()[center] == uint8_t((unsigned(source[center]) * 128u + 127u) / 255u));
CHECK(renderer.color()[center + 1u] == uint8_t((unsigned(source[center + 1u]) * 64u + 127u) / 255u));
const auto preserved = renderer.color();
REQUIRE(!renderer.configureSurfaceBlend(-1, 10).ok());
REQUIRE(!renderer.configureSurfaceBlend(5, 11).ok());
REQUIRE(!renderer.compositeConfiguredSurfaceBlend(std::span<const uint8_t>(scene.data(), 4)).ok());
CHECK(renderer.color() == preserved);
}
TEST_CASE("fluids.ssf.particleBlendAndDepthWriteMatchFluid3DColorPass") {
VolumeFluidSettings settings;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidParticle particles[2];
particles[0].position = {-.05f, .5f, 0.f};
particles[1].position = {.05f, .5f, 0.f};
particles[0].color = {.8f, .5f, .25f, .3f};
particles[1].color = {.5f, .25f, .8f, .7f};
REQUIRE(solver->emit(particles).ok());
FluidSurfaceParams params;
params.width = params.height = 33;
params.eye = {0.f, .5f, 3.f};
params.target = {0.f, .5f, 0.f};
params.smoothIterations = 0;
FluidSurfaceRenderer renderer(params, false);
REQUIRE(renderer.configureMaterial(false, 0.f, 0.f, 0.f, 0.f, 1.f).ok());
REQUIRE(renderer.configureParticleBlend(2, 0, false).ok());
renderer.renderVolume(*solver);
const size_t center = (16u * 33u + 16u) * 4u;
CHECK(std::abs(int(renderer.color()[center + 0u]) - int(255.f * .8f * .5f)) <= 1);
CHECK(std::abs(int(renderer.color()[center + 1u]) - int(255.f * .5f * .25f)) <= 1);
CHECK(std::abs(int(renderer.color()[center + 2u]) - int(255.f * .25f * .8f)) <= 1);
REQUIRE(renderer.configureParticleBlend(2, 0, true).ok());
renderer.renderVolume(*solver);
CHECK(std::abs(int(renderer.color()[center + 0u]) - int(255.f * .8f)) <= 1);
CHECK(std::abs(int(renderer.color()[center + 1u]) - int(255.f * .5f)) <= 1);
CHECK(std::abs(int(renderer.color()[center + 2u]) - int(255.f * .25f)) <= 1);
const auto preserved = renderer.color();
REQUIRE(!renderer.configureParticleBlend(11, 0, false).ok());
renderer.renderVolume(*solver);
CHECK(renderer.color() == preserved);
}
TEST_CASE("fluids.ssf.orthographicDepthIndependentCoverageAndGpuParity") {
auto* graphics = eve::graphics::Graphics::create();
REQUIRE(graphics != nullptr);
graphics->initHeadless(64, 64);
FluidSurfaceParams params;
params.width = 64;
params.height = 64;
params.eye = {0, 0, 3};
params.target = {0, 0, 0};
params.smoothIterations = 0;
params.orthographic = true;
params.orthographicSize = 1.f;
FluidSurfaceRenderer nearCpu(params, false), farCpu(params, false), gpu(params, true);
nearCpu.render(std::vector<glm::vec3>{{0, 0, 0}}, .25f);
farCpu.render(std::vector<glm::vec3>{{0, 0, -1}}, .25f);
gpu.render(std::vector<glm::vec3>{{0, 0, 0}}, .25f);
REQUIRE(gpu.usingGpu());
size_t nearCoverage = 0, farCoverage = 0;
for (size_t i = 0; i < nearCpu.depth().size(); ++i) {
nearCoverage += nearCpu.depth()[i] < 1e29f;
farCoverage += farCpu.depth()[i] < 1e29f;
if (nearCpu.depth()[i] >= 1e29f) {
REQUIRE(gpu.depth()[i] >= 1e29f);
continue;
}
REQUIRE(std::abs(nearCpu.depth()[i] - gpu.depth()[i]) < .00002f);
REQUIRE(glm::length(nearCpu.normals()[i] - gpu.normals()[i]) < .002f);
}
CHECK(nearCoverage == farCoverage);
CHECK(nearCoverage > 150);
const auto previous = nearCpu.depth();
REQUIRE(!nearCpu.configureProjection(true, 0.f).ok());
nearCpu.render(std::vector<glm::vec3>{{0, 0, 0}}, .25f);
CHECK(nearCpu.depth() == previous);
}
TEST_CASE("fluids.ssf.orthographicVolumeTintAndGasCoverageAreDepthIndependent") {
auto makeSolver = [&](float depthOffset, VolumeFluidPhase phase) {
VolumeFluidSettings settings;
settings.gravity = glm::vec3(0.f);
auto created = VolumeFluid::create(settings);
REQUIRE(created.ok());
auto solver = std::move(created).takeValue();
VolumeFluidParticle particles[2];
particles[0].position = {-.2f, .5f, depthOffset};
particles[0].color = {1.f, .1f, .05f, .8f};
particles[0].material.phase = phase;
particles[1].position = {.2f, .5f, depthOffset};
particles[1].color = {.05f, .2f, 1.f, .8f};
particles[1].material.phase = phase;
REQUIRE(solver->emit(particles).ok());
return solver;
};
FluidSurfaceParams params;
params.width = params.height = 65;
params.eye = {0.f, .5f, 3.f};
params.target = {0.f, .5f, 0.f};
params.orthographic = true;
params.orthographicSize = 1.f;
params.smoothIterations = 0;
auto nearLiquid = makeSolver(0.f, VolumeFluidPhase::Liquid);
auto farLiquid = makeSolver(-.8f, VolumeFluidPhase::Liquid);
FluidSurfaceRenderer nearLiquidRenderer(params, false), farLiquidRenderer(params, false);
nearLiquidRenderer.renderVolume(*nearLiquid);
farLiquidRenderer.renderVolume(*farLiquid);
CHECK(nearLiquidRenderer.color() == farLiquidRenderer.color());
auto nearGas = makeSolver(0.f, VolumeFluidPhase::Gas);
auto farGas = makeSolver(-.8f, VolumeFluidPhase::Gas);
FluidSurfaceRenderer nearGasRenderer(params, false), farGasRenderer(params, false);
REQUIRE(nearGasRenderer.renderGasVolume(*nearGas, 5.f).ok());
REQUIRE(farGasRenderer.renderGasVolume(*farGas, 5.f).ok());
CHECK(nearGasRenderer.color() == farGasRenderer.color());
}
TEST_CASE("fluids.ssf.occlusionDoesNotTiltForegroundNormals") {
auto* graphics = eve::graphics::Graphics::create();
REQUIRE(graphics != nullptr);
graphics->initHeadless(64, 64);
FluidSurfaceParams params;
params.width = 64;
params.height = 64;
params.eye = {0, 0, 3};
params.target = {0, 0, 0};
params.smoothIterations = 0;
FluidSurfaceRenderer foreground(params, false), combined(params, true), reference(params, false);
foreground.render(std::vector<glm::vec3>{{0, 0, 0}}, .6f);
const std::vector<glm::vec3> particles{{0, 0, 0}, {.9f, 0, -2.f}};
combined.render(particles, .6f);
reference.render(particles, .6f);
REQUIRE(combined.usingGpu());
unsigned occludedEdges = 0;
for (size_t i = 0; i < foreground.depth().size(); ++i) {
if (foreground.depth()[i] >= 1e29f) continue;
REQUIRE(std::abs(combined.depth()[i] - foreground.depth()[i]) < .00002f);
REQUIRE(glm::length(combined.normals()[i] - reference.normals()[i]) < .002f);
const size_t x = i % 64;
if (x < 63 && foreground.depth()[i + 1] >= 1e29f && combined.depth()[i + 1] < 1e29f) {
++occludedEdges;
REQUIRE(glm::length(combined.normals()[i] - foreground.normals()[i]) < .002f);
}
}
REQUIRE(occludedEdges >= 8);
}