Files
EVEngine/test/procgen_lsystem.cpp
T

234 lines
7.4 KiB
C++

#include "procgen/PointSet.h"
#include "procgen/Procgen.h"
#include "procgen/algorithms/LSystem.h"
#include "graphics/Graphics.h"
#include "graphics/Mesh.h"
#include "graphics/RenderSystem.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/Texture.h"
#include "image/Image.h"
#include "window/Window.h"
#include "RenderImageAudit.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <memory>
#include <set>
using namespace eve::procgen;
using namespace eve::graphics;
TEST_CASE("procgen.lsystem.renderDump") {
const char *outputPath = std::getenv("EVENGINE_LSYSTEM_RENDER_PNG");
if (!outputPath || !outputPath[0]) return;
auto *win = eve::window::Window::create();
auto *gfx = Graphics::create();
REQUIRE(win != nullptr);
REQUIRE(gfx != nullptr);
eve::window::WindowSettings ws;
ws.width = 900;
ws.height = 700;
ws.centered = true;
REQUIRE(win->setWindowSettings(ws));
Params params;
params.setSeed(20260826);
const char *styleOverride = std::getenv("EVENGINE_LSYSTEM_STYLE");
params.setString("style", styleOverride ? styleOverride : "tree");
params.setInt("iterations", 5);
params.setString("leafMode", "cards");
params.setFloat("tropism", 0.05f);
params.setFloat("leafSize", 0.5f);
Procgen generator;
auto paramsResult = Procgen::newParamsHandle();
REQUIRE(paramsResult.ok());
auto paramsHandle = std::move(paramsResult).takeValue();
auto paramsView = Procgen::resolve(paramsHandle);
REQUIRE(paramsView.isBound());
*paramsView = params;
auto lsMeshView = generator.generateMeshBorrowed("mesh.lsystem", paramsHandle, gfx);
REQUIRE(lsMeshView.isBound());
Mesh *lsMesh = lsMeshView.get();
// 4px bark/foliage atlas; UVs are partitioned by the mesh recipe (left = bark).
const uint8_t atlasPixels[] = {
111, 70, 42, 255, 128, 82, 47, 255,
64, 119, 57, 255, 82, 145, 67, 255,
};
Texture *atlas = gfx->newTexture(4, 1, atlasPixels);
REQUIRE(atlas != nullptr);
auto *plant = Renderable3D::create();
plant->setMesh(lsMesh);
plant->setTexture(atlas);
plant->setTint(1.f, 1.f, 1.f, 1.f);
plant->setRoughness(0.88f);
auto *camera = Camera3D::createCamera();
camera->setEye(7.5f, 5.2f, 9.5f);
camera->setTarget(0.f, 3.2f, 0.f);
camera->setUp(0.f, 1.f, 0.f);
camera->setFov(36.f);
camera->setAmbient(0.30f, 0.34f, 0.28f);
camera->setActive(true);
gfx->setScreenReadbackEnabled(true);
gfx->setBackgroundColor(Color(0.075f, 0.105f, 0.095f, 1.f));
RenderSystem3D::setDirectionalLight(-0.55f, -1.f, -0.35f, 1.45f, 1.32f, 1.08f);
for (int frame = 0; frame < 4; ++frame) {
RenderSystem3D::render(*gfx);
RenderSystem::render(*gfx);
}
std::unique_ptr<eve::image::ImageData> image(gfx->newImageData());
REQUIRE(image.get() != nullptr);
REQUIRE(saveImagePng(*image, outputPath));
std::printf("lsystem render saved: %s\n", outputPath);
REQUIRE(Procgen::release(paramsHandle).ok());
win->close();
}
TEST_CASE("procgen.lsystem.deterministicExpansion") {
LSystem ls;
ls.setAxiom("F");
ls.addRule('F', "F[+F]F[-F]F");
ls.setIterations(3);
ls.setSeed(42);
const std::string a = ls.derive();
const std::string b = ls.derive();
CHECK(!a.empty());
CHECK_EQ(a, b);
// More iterations must grow the word monotonically.
ls.setIterations(4);
const std::string c = ls.derive();
CHECK(c.size() > a.size());
}
TEST_CASE("procgen.lsystem.stochasticRulesUseSeed") {
LSystem ls;
ls.setAxiom("A");
// Branching grammar so different seeds produce a rich, distinct outcome space.
ls.addRules('A', {"F[+A]A", "F[-A]A", "FA"}, {1.f, 1.f, 1.f});
ls.setIterations(4);
// Determinism for a fixed seed.
ls.setSeed(7);
const std::string a = ls.derive();
ls.setSeed(7);
const std::string b = ls.derive();
CHECK_EQ(a, b);
CHECK(!a.empty());
// Across several seeds the stochastic grammar should not all collapse to one.
std::set<std::string> seen;
for (int s = 1; s <= 8; ++s) {
ls.setSeed(s);
seen.insert(ls.derive());
}
CHECK(seen.size() >= 2);
}
TEST_CASE("procgen.lsystem.turtleProducesSegments") {
LSystem ls;
ls.setAxiom("F[+F]F[-F]F");
ls.addRule('F', "F");
ls.setIterations(1);
ls.setAngle(25.f);
ls.setStep(1.f);
ls.setSeed(1);
LSystemResult result;
ls.generate(result);
CHECK(!result.segments.empty());
CHECK(!result.derivation.empty());
// Bracketed depth increases then returns: trunk segment is at depth 0.
bool sawDepthZero = false;
bool sawDeep = false;
for (const auto& seg : result.segments) {
if (seg.depth == 0) sawDepthZero = true;
if (seg.depth > 0) sawDeep = true;
if (seg.leaf) CHECK(seg.leafSize > 0.f);
}
CHECK(sawDepthZero);
CHECK(sawDeep);
}
TEST_CASE("procgen.lsystem.meshRecipeIsDeterministic") {
Procgen proc;
Params p1;
p1.setSeed(123);
p1.setString("style", "tree");
p1.setInt("iterations", 4);
p1.setString("leafMode", "cards");
Params p2 = p1;
auto p1Result = Procgen::newParamsHandle();
auto p2Result = Procgen::newParamsHandle();
REQUIRE(p1Result.ok());
REQUIRE(p2Result.ok());
auto p1Handle = std::move(p1Result).takeValue();
auto p2Handle = std::move(p2Result).takeValue();
*Procgen::resolve(p1Handle) = p1;
*Procgen::resolve(p2Handle) = p2;
auto m1Result = proc.buildMeshHandle("mesh.lsystem", p1Handle);
auto m2Result = proc.buildMeshHandle("mesh.lsystem", p2Handle);
REQUIRE(m1Result.ok());
REQUIRE(m2Result.ok());
auto m1Handle = std::move(m1Result).takeValue();
auto m2Handle = std::move(m2Result).takeValue();
auto m1 = proc.resolveMeshBuild(m1Handle);
auto m2 = proc.resolveMeshBuild(m2Handle);
REQUIRE(m1.isBound());
REQUIRE(m2.isBound());
CHECK(m1->getVertexCount() > 0);
CHECK_EQ(m1->getVertexCount(), m2->getVertexCount());
CHECK_EQ(m1->getIndexCount(), m2->getIndexCount());
CHECK_EQ(m1->getMeta("recipe", ""), std::string("mesh.lsystem"));
REQUIRE(proc.releaseMeshBuild(m2Handle).ok());
REQUIRE(proc.releaseMeshBuild(m1Handle).ok());
REQUIRE(Procgen::release(p2Handle).ok());
REQUIRE(Procgen::release(p1Handle).ok());
}
TEST_CASE("procgen.lsystem.meshRecipeReportsErrors") {
Procgen proc;
Params params;
params.setSeed(1);
params.setString("style", "bogus");
auto paramsResult = Procgen::newParamsHandle();
REQUIRE(paramsResult.ok());
auto paramsHandle = std::move(paramsResult).takeValue();
*Procgen::resolve(paramsHandle) = params;
auto meshResult = proc.buildMeshHandle("mesh.lsystem", paramsHandle);
REQUIRE(!meshResult.ok());
CHECK(meshResult.status().describe().find("unknown style") != std::string::npos);
REQUIRE(Procgen::release(paramsHandle).ok());
}
TEST_CASE("procgen.lsystem.scriptFacade") {
Procgen proc;
auto result = proc.newLSystemHandle();
REQUIRE(result.ok());
auto handle = std::move(result).takeValue();
auto ls = proc.resolveLSystem(handle);
REQUIRE(ls.isBound());
ls->setAxiom("F");
ls->addRule('F', "F[+F]F[-F]F");
ls->setIterations(2);
ls->setSeed(5);
PointSet points;
ls->toPointSet(points);
CHECK(points.getCount() > 0);
REQUIRE(proc.release(handle).ok());
}