653 lines
24 KiB
C++
653 lines
24 KiB
C++
#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 <SDL2/SDL.h>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<float> &pos, std::vector<float> &nrm, std::vector<uint32_t> &idx) {
|
|
eve::graphics::grass::makePlane(4.f, 4.f, 4, 4, pos, nrm, idx);
|
|
}
|
|
|
|
float minPairDist(const std::vector<Point> &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<int>(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<float> pos, nrm;
|
|
std::vector<uint32_t> 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<float> pos, nrm;
|
|
std::vector<uint32_t> 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<float> pos, nrm;
|
|
std::vector<uint32_t> 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<Point> 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<uint8_t> 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<std::string> 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<std::string> 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<uint8_t> 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<Point> 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<Renderable3D>() != nullptr) {
|
|
auto view = ecs::View<Renderable3D, Renderable3D::MeshRenderer>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [mr] = *it;
|
|
mr->visible = false;
|
|
}
|
|
}
|
|
if (ecs::current()->getManager<Camera3D>() != nullptr) {
|
|
auto camView = ecs::View<Camera3D, Camera3D::Data>();
|
|
for (auto it = camView.begin(); it != camView.end(); ++it) {
|
|
auto [data] = *it;
|
|
data->active = false;
|
|
}
|
|
}
|
|
if (ecs::current()->getManager<Light3D>() != nullptr) {
|
|
auto lightView = ecs::View<Light3D, Light3D::Data>();
|
|
for (auto it = lightView.begin(); it != lightView.end(); ++it) {
|
|
auto [d] = *it;
|
|
d->enabled = false;
|
|
}
|
|
}
|
|
if (ecs::current()->getManager<Renderable2D>() != nullptr) {
|
|
auto view = ecs::View<Renderable2D, Renderable2D::Sprite>();
|
|
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<float> pos, nrm;
|
|
std::vector<uint32_t> idx;
|
|
eve::graphics::grass::makePlane(sizeX, sizeZ, 12, 12, pos, nrm, idx);
|
|
std::vector<float> 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<const char *>(png->getData()),
|
|
static_cast<std::streamsize>(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<eve::image::ImageData> 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<uint8_t> 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();
|
|
}
|