#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "filesystem/FileData.h" #include "graphics/AmbientOcclusion.h" #include "graphics/AntiAliasing.h" #include "graphics/Canvas.h" #include "graphics/DrawItem2D.h" #include "graphics/Font.h" #include "graphics/GBuffer.h" #include "graphics/GlobalIllumination.h" #include "graphics/Graphics.h" #include "graphics/Grass.h" #include "graphics/Material.h" #include "graphics/Outline.h" #include "graphics/Quad.h" #include "graphics/ScreenSpaceReflection.h" #include "graphics/Texture.h" #include "graphics/Volumetric.h" #include "graphics/Water.h" #include "graphics/Waterfall.h" // Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form. using eve::graphics::Color; #include "graphics/Light.h" #include "graphics/Mesh.h" #include "graphics/RenderControl.h" #include "graphics/RenderSystem.h" #include "graphics/RenderSystem3D.h" #include "graphics/Shader.h" #include "graphics/shaders/mesh3d_grass_frag_spv.inc" #include "graphics/shaders/mesh3d_grass_vert_spv.inc" #include "image/Image.h" #include "image/ImageData.h" #include "window/Window.h" #include #include #include #include #include #include #include #include #include using eve::graphics::Camera3D; using eve::graphics::Graphics; using eve::graphics::GrassField; using eve::graphics::Light3D; using eve::graphics::Mesh; using eve::graphics::Renderable2D; using eve::graphics::Renderable3D; using eve::graphics::RenderSystem; using eve::graphics::RenderSystem3D; using eve::graphics::Shader; using eve::graphics::Texture; using eve::graphics::grass::BillboardMesh; using eve::graphics::grass::Point; using eve::graphics::grass::PackedAtlasInfo; using eve::graphics::grass::SampleParams; namespace { #include "PathBesideSource.h" EVE_DEFINE_PATH_BESIDE_SOURCE() void makeTestPlane(std::vector &pos, std::vector &nrm, std::vector &idx) { eve::graphics::grass::makePlane(4.f, 4.f, 4, 4, pos, nrm, idx); } float minPairDist(const std::vector &pts) { float best = 1e9f; for (size_t i = 0; i < pts.size(); ++i) { for (size_t j = i + 1; j < pts.size(); ++j) { const glm::vec3 d = pts[i].position - pts[j].position; best = std::min(best, glm::length(d)); } } return best; } } // namespace TEST_CASE("graphics.Grass.bindDefaults") { Shader sh; sh.setKind(Shader::Kind::eMesh3D); eve::graphics::grass::bindDefaults(&sh); CHECK(sh.hasUniform("time")); CHECK(sh.hasUniform("frameDuration")); CHECK(sh.hasUniform("lightGreen")); CHECK(sh.hasUniform("darkGreen")); CHECK(sh.hasUniform("alwaysDark")); CHECK_EQ(sh.getUniformIndex("time"), 0); CHECK_EQ(sh.getUniformIndex("alwaysDark"), 5); CHECK_EQ(sh.getUniformIndex("lightGreen"), 6); CHECK_EQ(sh.getUniformIndex("darkGreen"), 9); CHECK_EQ(sh.getUniformIndex("frameCount"), 12); CHECK_EQ(sh.getUniformIndex("atlasCols"), 13); CHECK_EQ(sh.getUniformIndex("atlasRows"), 14); CHECK_EQ(sh.getUniformIndex("grassVariantCount"), 15); CHECK_EQ(sh.getUniformIndex("leafVariantCount"), 16); CHECK_EQ(sh.getUniformIndex("leafRowOffset"), 17); CHECK_EQ(sh.getUniformIndex("windGlobals"), 18); CHECK_EQ(sh.getUniformIndex("windPhaseDistance"), 22); CHECK_EQ(sh.getUniformIndex("windFlex"), 24); CHECK_EQ(sh.getUniformIndex("windFrequency"), 27); CHECK_EQ(sh.getUniformIndex("windSineTime"), 30); CHECK_EQ(sh.pushConstantData()[23], 0.f); CHECK_EQ(sh.usedFloats(), 32); } TEST_CASE("graphics.Grass.paramNames") { CHECK_EQ(eve::graphics::grass::paramCount(), 32); CHECK_EQ(eve::graphics::grass::paramName(0), std::string("time")); CHECK_EQ(eve::graphics::grass::paramName(5), std::string("alwaysDark")); CHECK_EQ(eve::graphics::grass::paramName(12), std::string("frameCount")); CHECK_EQ(eve::graphics::grass::paramName(13), std::string("atlasCols")); CHECK_EQ(eve::graphics::grass::paramName(17), std::string("leafRowOffset")); } TEST_CASE("graphics.Grass.foliageProfileReusesStablePushLayout") { Shader sh; sh.setKind(Shader::Kind::eMesh3D); eve::graphics::grass::GrassFoliageSettings settings; settings.baseR = 0.2f; settings.baseG = 0.4f; settings.baseB = 0.6f; settings.normalStrength = 1.7f; settings.renderDistance = 91.f; settings.fadeRange = 13.f; settings.snowMinimumHeight = 8.f; settings.snowFadeDistance = 17.f; settings.snowProgress = 0.75f; eve::graphics::grass::bindFoliage(&sh, settings); CHECK_EQ(sh.usedFloats(), 32); CHECK_EQ(sh.pushConstantData()[0], 8.f); CHECK_EQ(sh.pushConstantData()[1], 1.7f); CHECK_EQ(sh.pushConstantData()[6], 0.2f); CHECK_EQ(sh.pushConstantData()[12], -1.f); CHECK_EQ(sh.pushConstantData()[13], 91.f); CHECK_EQ(sh.pushConstantData()[14], 13.f); CHECK_EQ(sh.pushConstantData()[15], 17.f); CHECK_EQ(sh.pushConstantData()[16], 0.75f); CHECK_EQ(sh.pushConstantData()[17], 105.5f); CHECK_EQ(sh.getUniformIndex("windGlobals"), 18); } TEST_CASE("graphics.Grass.terrainDetailOverwriteKeepsThreeDistanceSemantics") { eve::graphics::grass::GrassFoliageSettings foliage; eve::graphics::grass::TerrainDetailOverwriteSettings settings; settings.pcgDetailDistance = -4; settings.pcgFadeoutDistance = 12; settings.unityDetailDistance = 140; settings.unityDetailDensity = 0.35F; settings.detailResolutionPerPatch = 4; auto applied = eve::graphics::grass::applyTerrainDetailOverwrite(foliage, settings); REQUIRE(applied.ok()); CHECK(applied.value() == static_cast(eve::graphics::grass::TerrainDetailQuality::VeryHigh4)); CHECK(foliage.renderDistance == 0); CHECK(foliage.fadeRange == 12); CHECK(foliage.hardRenderDistance == 140); CHECK(foliage.density == 0.35F); auto before = foliage; settings.unityDetailDensity = 2; CHECK(!eve::graphics::grass::applyTerrainDetailOverwrite(foliage, settings).ok()); CHECK(foliage.renderDistance == before.renderDistance); CHECK(eve::graphics::grass::terrainDetailQuality(7) == eve::graphics::grass::TerrainDetailQuality::VeryLow64); } TEST_CASE("graphics.Grass.photoModeAuthorityOwnsFourRuntimeMultipliers") { auto* gfx=Graphics::create();auto* field=gfx->newGrassField();field->setPhotoModeAuthority(true); REQUIRE(field->applyPhotoModeField({"m_globalGrassDensity",eve::PhotoModeDomain::Grass,2.f}).ok()); REQUIRE(field->applyPhotoModeField({"m_globalGrassDistance",eve::PhotoModeDomain::Grass,3.f}).ok()); REQUIRE(field->applyPhotoModeField({"m_cameraCellDistance",eve::PhotoModeDomain::Grass,1.5f}).ok()); REQUIRE(field->applyPhotoModeField({"m_cameraCellSubdivision",eve::PhotoModeDomain::Grass,int64_t{2}}).ok()); CHECK_EQ(field->getPhotoModeDensity(),2.f);CHECK_EQ(field->getPhotoModeDistance(),3.f); CHECK_EQ(field->getPhotoModeCellDistance(),1.5f);CHECK_EQ(field->getPhotoModeCellSubdivision(),2); CHECK_EQ(field->getTerrainDetailHardDistance(),472.5f);CHECK_EQ(field->getTerrainDetailDensity(),1.f); CHECK(!field->applyPhotoModeField({"m_cameraCellSubdivision",eve::PhotoModeDomain::Grass,int64_t{9}}).ok()); CHECK_EQ(field->getPhotoModeCellSubdivision(),2);delete field; } TEST_CASE("graphics.Grass.spvMagic") { CHECK(mesh3d_grass_vert_spv_count > 0); CHECK(mesh3d_grass_frag_spv_count > 0); CHECK_EQ(mesh3d_grass_vert_spv[0], 0x07230203u); CHECK_EQ(mesh3d_grass_frag_spv[0], 0x07230203u); } TEST_CASE("graphics.Grass.poissonMinDistance") { std::vector pos, nrm; std::vector idx; makeTestPlane(pos, nrm, idx); SampleParams p; p.radius = 0.45f; p.maxPoints = 200; p.seed = 7; p.minSlopeDot = 0.1f; const auto pts = eve::graphics::grass::samplePoisson(pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), p); CHECK(pts.size() >= 8); CHECK(int(pts.size()) <= p.maxPoints); const float minD = minPairDist(pts); CHECK(minD + 1e-4f >= p.radius); for (const auto &pt : pts) { CHECK(std::abs(pt.position.y) < 1e-4f); CHECK(pt.position.x >= -2.01f); CHECK(pt.position.x <= 2.01f); CHECK(pt.position.z >= -2.01f); CHECK(pt.position.z <= 2.01f); } } TEST_CASE("graphics.Grass.haltonCountAndCoverage") { std::vector pos, nrm; std::vector idx; makeTestPlane(pos, nrm, idx); const auto pts = eve::graphics::grass::sampleHalton( pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), 64, 3, 0.1f); CHECK_EQ(int(pts.size()), 64); CHECK_EQ(pts.front().id, 0u); CHECK_EQ(pts.back().id, 63u); float minX = 1e9f, maxX = -1e9f, minZ = 1e9f, maxZ = -1e9f; for (const auto &pt : pts) { minX = std::min(minX, pt.position.x); maxX = std::max(maxX, pt.position.x); minZ = std::min(minZ, pt.position.z); maxZ = std::max(maxZ, pt.position.z); } CHECK(maxX - minX > 2.f); CHECK(maxZ - minZ > 2.f); } TEST_CASE("graphics.Grass.sparseIsSparser") { std::vector pos, nrm; std::vector idx; makeTestPlane(pos, nrm, idx); SampleParams dense; dense.radius = 0.3f; dense.maxPoints = 400; dense.seed = 1; SampleParams sparse = dense; sparse.radius = 1.2f; sparse.maxPoints = 80; sparse.seed = 99; const auto d = eve::graphics::grass::samplePoisson( pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), dense); const auto s = eve::graphics::grass::samplePoisson( pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), sparse); CHECK(d.size() > s.size()); CHECK(s.size() >= 1); } TEST_CASE("graphics.Grass.billboardRootPivot") { std::vector pts(1); pts[0].position = glm::vec3(3.f, 1.5f, -2.f); pts[0].id = 11; pts[0].scale = 1.25f; const BillboardMesh mesh = eve::graphics::grass::buildBillboards(pts, 0.4f, 0.8f); CHECK_EQ(int(mesh.posXYZ.size()), 12); CHECK_EQ(int(mesh.uvST.size()), 8); CHECK_EQ(int(mesh.indices.size()), 6); for (int i = 0; i < 4; ++i) { CHECK(std::abs(mesh.posXYZ[size_t(i * 3 + 0)] - 3.f) < 1e-5f); CHECK(std::abs(mesh.posXYZ[size_t(i * 3 + 1)] - 1.5f) < 1e-5f); CHECK(std::abs(mesh.posXYZ[size_t(i * 3 + 2)] + 2.f) < 1e-5f); CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 0)] - 11.f) < 1e-5f); CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 1)] - 1.25f) < 1e-5f); CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 2)]) < 1e-5f); } // Bottom edge UVs sit at v=0; their midpoint is the root (u=0.5). CHECK(std::abs(mesh.uvST[1]) < 1e-5f); CHECK(std::abs(mesh.uvST[3]) < 1e-5f); CHECK(std::abs(0.5f * (mesh.uvST[0] + mesh.uvST[2]) - 0.5f) < 1e-5f); } TEST_CASE("graphics.Grass.swayDesyncByInstanceId") { const int a = eve::graphics::grass::swayFrame(0.f, 0.12f, 0, 4); const int b = eve::graphics::grass::swayFrame(0.f, 0.12f, 1, 4); const int c = eve::graphics::grass::swayFrame(0.f, 0.12f, 2, 4); CHECK(a >= 0); CHECK(a < 4); CHECK(b >= 0); CHECK(b < 4); CHECK(c >= 0); CHECK(c < 4); CHECK((a != b || b != c)); const int later = eve::graphics::grass::swayFrame(0.36f, 0.12f, 0, 4); CHECK(later != a); } TEST_CASE("graphics.Grass.swayAtlasFourFrames") { std::vector rgba; eve::graphics::grass::makeSwayAtlasRGBA(32, 32, 4, rgba); CHECK_EQ(eve::graphics::grass::swayAtlasWidth(32, 4), 128); CHECK_EQ(eve::graphics::grass::swayAtlasHeight(32), 32); CHECK_EQ(int(rgba.size()), 128 * 32 * 4); int opaque = 0; int perFrame[4] = {}; for (int y = 0; y < 32; ++y) { for (int x = 0; x < 128; ++x) { const uint8_t a = rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u]; if (a > 16) { ++opaque; perFrame[x / 32]++; } } } CHECK(opaque > 80); for (int f = 0; f < 4; ++f) CHECK(perFrame[f] > 10); // Filled tuft: the root band is denser than the tips (not five 1px sticks). int baseOpaque = 0, tipOpaque = 0; for (int x = 0; x < 32; ++x) { for (int y = 24; y < 32; ++y) { if (rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u] > 16) ++baseOpaque; } for (int y = 0; y < 8; ++y) { if (rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u] > 16) ++tipOpaque; } } CHECK(baseOpaque > tipOpaque); CHECK(baseOpaque > 40); // Frames actually differ (wind lean). int diffs = 0; for (int y = 0; y < 32; ++y) { for (int x = 0; x < 32; ++x) { const uint8_t a0 = rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u]; const uint8_t a3 = rgba[(size_t(y) * 128u + size_t(x + 96)) * 4u + 3u]; if (a0 != a3) ++diffs; } } CHECK(diffs > 8); } TEST_CASE("graphics.Grass.fileSwayAtlasPacksFourGrassAndTwoLeaf") { const std::vector grass = { pathBesideThisSource("Texture/grass_sway_0.png"), pathBesideThisSource("Texture/grass_sway_1.png"), pathBesideThisSource("Texture/grass_sway_2.png"), pathBesideThisSource("Texture/grass_sway_3.png"), }; const std::vector leaf = { pathBesideThisSource("Texture/leaf_sway_0.png"), pathBesideThisSource("Texture/leaf_sway_1.png"), }; for (const auto &p : grass) REQUIRE(std::filesystem::exists(p)); for (const auto &p : leaf) REQUIRE(std::filesystem::exists(p)); std::vector rgba; PackedAtlasInfo packed; eve::graphics::grass::packSwayAtlasRGBA(grass, leaf, rgba, packed); CHECK_EQ(packed.grassVariants, 4); CHECK_EQ(packed.leafVariants, 2); CHECK_EQ(packed.atlasCols, 8); CHECK_EQ(packed.atlasRows, 4); CHECK_EQ(packed.leafRowOffset, 2); CHECK_EQ(packed.frames, 4); CHECK(packed.width >= 512); CHECK(packed.height >= 512); CHECK_EQ(int(rgba.size()), packed.width * packed.height * 4); auto opaqueInCell = [&](int col, int row) { const int cellW = packed.width / packed.atlasCols; const int cellH = packed.height / packed.atlasRows; int n = 0; for (int y = row * cellH; y < (row + 1) * cellH; ++y) { for (int x = col * cellW; x < (col + 1) * cellW; ++x) { if (rgba[(size_t(y) * size_t(packed.width) + size_t(x)) * 4u + 3u] > 16) ++n; } } return n; }; // Each grass variant is a 2x2 block on the top two frame-rows. for (int v = 0; v < 4; ++v) { CHECK(opaqueInCell(v * 2, 0) + opaqueInCell(v * 2 + 1, 0) + opaqueInCell(v * 2, 1) + opaqueInCell(v * 2 + 1, 1) > 80); } // Leaf variants occupy the bottom two frame-rows. CHECK(opaqueInCell(0, 2) + opaqueInCell(1, 2) + opaqueInCell(0, 3) + opaqueInCell(1, 3) > 80); CHECK(opaqueInCell(2, 2) + opaqueInCell(3, 2) + opaqueInCell(2, 3) + opaqueInCell(3, 3) > 80); } TEST_CASE("graphics.Grass.layerFlag") { Shader sh; sh.setKind(Shader::Kind::eMesh3D); eve::graphics::grass::bindDefaults(&sh); eve::graphics::grass::bindLayer(&sh, true); float dark = 0.f; REQUIRE_EQ(sh.getFromVar("alwaysDark", &dark, sizeof(dark)), int(sizeof(dark))); CHECK(dark > 0.5f); eve::graphics::grass::bindLayer(&sh, false); REQUIRE_EQ(sh.getFromVar("alwaysDark", &dark, sizeof(dark)), int(sizeof(dark))); CHECK(dark < 0.5f); } TEST_CASE("graphics.Grass.billboardAlwaysDarkFlag") { std::vector pts(1); pts[0].position = glm::vec3(0.f, 0.f, 0.f); pts[0].id = 3; pts[0].scale = 1.f; const BillboardMesh dark = eve::graphics::grass::buildBillboards(pts, 0.4f, 0.8f, true); CHECK_EQ(int(dark.nrmXYZ.size()), 12); for (int i = 0; i < 4; ++i) CHECK(dark.nrmXYZ[size_t(i * 3 + 2)] > 0.5f); } namespace { void resetGrassScene() { if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [mr] = *it; mr->visible = false; } } if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; data->active = false; } } if (ecs::current()->getManager() != nullptr) { auto lightView = ecs::View(); for (auto it = lightView.begin(); it != lightView.end(); ++it) { auto [d] = *it; d->enabled = false; } } if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [sp] = *it; sp->visible = false; } } } Texture *makeSolidTex(Graphics *gfx, uint8_t r, uint8_t g, uint8_t b) { const uint8_t px[4] = {r, g, b, 255}; return gfx->newTexture(1, 1, px); } Mesh *makeGroundMesh(Graphics *gfx, float sizeX, float sizeZ) { std::vector pos, nrm; std::vector idx; eve::graphics::grass::makePlane(sizeX, sizeZ, 12, 12, pos, nrm, idx); std::vector uv(size_t(pos.size() / 3) * 2u, 0.f); for (size_t i = 0; i < pos.size() / 3; ++i) { uv[i * 2] = pos[i * 3] / sizeX + 0.5f; uv[i * 2 + 1] = pos[i * 3 + 2] / sizeZ + 0.5f; } return gfx->newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(), int(idx.size())); } void savePng(eve::image::ImageData *frame, const std::string &path) { REQUIRE(frame != nullptr); [[maybe_unused]] auto *const imageModule = eve::image::Image::create(); eve::filesystem::FileData *png = frame->encode(medialoader::FormatHandler::ENCODED_PNG, "grass.png", false); REQUIRE(png != nullptr); REQUIRE(png->getSize() > 0); std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); { std::ofstream out(path, std::ios::binary); REQUIRE(out.good()); out.write(static_cast(png->getData()), static_cast(png->getSize())); REQUIRE(out.good()); } delete png; std::printf("\ngrass render saved: %s\n", path.c_str()); } } // namespace TEST_CASE("graphics.Grass.gpuRenderScreenshot") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); gfx->setMsaaSamples(0); eve::window::WindowSettings s; s.width = 960; s.height = 540; s.centered = true; REQUIRE(win->setWindowSettings(s)); resetGrassScene(); auto *rc = gfx->getRenderControl(); REQUIRE(rc != nullptr); rc->disable("aa"); rc->disable("msaa"); rc->disable("ao"); rc->disable("gi"); rc->enable("shadow"); rc->enable("forward"); rc->compile(); constexpr float kField = 10.f; auto *ground = Renderable3D::create(); ground->setMesh(makeGroundMesh(gfx, kField, kField)); ground->setTexture(makeSolidTex(gfx, 68, 54, 30)); ground->setPosition(0.f, 0.f, 0.f); ground->setMetallic(0.f); ground->setRoughness(0.92f); ground->setCastShadow(false); ground->setReceiveShadow(true); GrassField::BakeParams bake; bake.denseRadius = 0.10f; bake.sparseRadius = 0.46f; bake.maxDense = 7000; bake.maxSparse = 220; bake.width = 0.58f; bake.height = 0.98f; bake.seed = 11; bake.minSlopeDot = 0.2f; bake.grassAtlasFiles = { pathBesideThisSource("Texture/grass_sway_0.png"), pathBesideThisSource("Texture/grass_sway_1.png"), pathBesideThisSource("Texture/grass_sway_2.png"), pathBesideThisSource("Texture/grass_sway_3.png"), }; bake.leafAtlasFiles = { pathBesideThisSource("Texture/leaf_sway_0.png"), pathBesideThisSource("Texture/leaf_sway_1.png"), }; GrassField *field = gfx->newGrassField(); REQUIRE(field != nullptr); field->bakePlane(kField, kField, 16, 16, bake); field->setTime(0.22f); std::printf("\ngrass dense=%d sparse=%d\n", field->getDenseCount(), field->getSparseCount()); REQUIRE(field->getDenseCount() >= 800); REQUIRE(field->getDenseMesh() != nullptr); REQUIRE(field->getShader() != nullptr); REQUIRE(field->getAtlas() != nullptr); auto *dense = Renderable3D::create(); dense->setMesh(field->getDenseMesh()); dense->setTexture(field->getAtlas()); dense->setShader(field->getShader()); dense->setCastShadow(false); dense->setReceiveShadow(true); if (field->getSparseMesh()) { auto *sparse = Renderable3D::create(); sparse->setMesh(field->getSparseMesh()); sparse->setTexture(field->getAtlas()); sparse->setShader(field->getShader()); sparse->setCastShadow(false); sparse->setReceiveShadow(true); } auto *post = Renderable3D::create(); post->setMesh(gfx->newMeshCylinder(18, 1, true)); post->setTexture(makeSolidTex(gfx, 168, 148, 122)); post->setPosition(1.35f, 0.95f, -0.4f); post->setScale(0.28f, 0.95f, 0.28f); post->setMetallic(0.f); post->setRoughness(0.7f); post->setCastShadow(true); post->setReceiveShadow(false); auto *cam = Camera3D::createCamera(); cam->setEye(4.8f, 2.9f, 5.9f); cam->setTarget(0.f, 0.45f, 0.f); cam->setAmbient(0.18f, 0.20f, 0.16f); cam->data()->nearZ = 0.1f; cam->data()->farZ = 80.f; auto *sun = Light3D::createLight("dir"); sun->setDirection(0.62f, 1.05f, 0.28f); sun->setColor(1.f, 0.97f, 0.88f, 2.4f); sun->setCastShadow(true); sun->setShadowStrength(1.f); sun->setShadowBias(0.003f); auto *hud = Renderable2D::create(); hud->transform()->x = 0; hud->transform()->y = 0; hud->sprite()->width = 2; hud->sprite()->height = 2; hud->sprite()->r = 0.f; hud->sprite()->g = 0.f; hud->sprite()->b = 0.f; hud->sprite()->a = 0.f; hud->sprite()->visible = true; gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColor(::Color(0.52f, 0.72f, 0.92f, 1.f)); for (int i = 0; i < 4; ++i) { RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); } [[maybe_unused]] auto *const imageModule = eve::image::Image::create(); eve::image::ImageData *frame = gfx->newImageData(); REQUIRE(frame != nullptr); std::unique_ptr ownedFrame(frame); int greenish = 0; int darkGreen = 0; int litGreen = 0; const int w = gfx->getWidth(); const int h = gfx->getHeight(); for (int y = 0; y < h; y += 2) { for (int x = 0; x < w; x += 2) { const auto c = frame->getPixel(x, y); if (c.g > c.r + 0.04f && c.g > c.b + 0.02f) { ++greenish; const float luma = (c.r + c.g + c.b) / 3.f; if (luma < 0.28f) ++darkGreen; if (luma > 0.38f) ++litGreen; } } } // This fixed-size scene contains a broad field spanning multiple cascades. // A WebGPU regression that left empty cascade layers uncleared turned the // whole field dark while still satisfying the old handful-of-pixels check. CHECK(greenish > 50000); CHECK(darkGreen > 100); CHECK(litGreen > 50000); const std::string outDir = std::string(EVENGINE_TEST_BINARY_DIR) + "/out"; savePng(frame, outDir + "/grass_field.png"); savePng(frame, "/opt/cursor/artifacts/grass_field.png"); std::vector atlasRgba; PackedAtlasInfo atlasInfo; eve::graphics::grass::packSwayAtlasRGBA(bake.grassAtlasFiles, bake.leafAtlasFiles, atlasRgba, atlasInfo); auto *atlasImg = new eve::image::ImageData(atlasInfo.width, atlasInfo.height, "RGBA8", atlasRgba.data(), false); savePng(atlasImg, outDir + "/grass_atlas.png"); savePng(atlasImg, "/opt/cursor/artifacts/grass_atlas.png"); delete atlasImg; win->close(); }