Files
EVEngine/test/voxel_render_scenes.cpp

318 lines
12 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "VoxelRenderFixtures.h"
static uint32_t voxelHash(uint32_t x) {
x ^= x >> 16;
x *= 0x7feb352du;
x ^= x >> 15;
x *= 0x846ca68bu;
x ^= x >> 16;
return x;
}
static float voxelH21(int x, int z) {
return float(voxelHash(uint32_t(x) * 0x9E3779B9u ^ uint32_t(z) * 0x85EBCA6Bu) & 0xFFFFu) / 65535.f;
}
static float voxelValueNoise(float x, float z) {
const int x0 = int(std::floor(x));
const int z0 = int(std::floor(z));
const float tx = x - float(x0);
const float tz = z - float(z0);
const float sx = tx * tx * (3.f - 2.f * tx);
const float sz = tz * tz * (3.f - 2.f * tz);
const float a = voxelH21(x0, z0);
const float b = voxelH21(x0 + 1, z0);
const float c = voxelH21(x0, z0 + 1);
const float d = voxelH21(x0 + 1, z0 + 1);
return (a * (1.f - sx) + b * sx) * (1.f - sz) + (c * (1.f - sx) + d * sx) * sz;
}
static float voxelFbm(float x, float z) {
float sum = 0.f, amp = 1.f, freq = 1.f, norm = 0.f;
for (int i = 0; i < 4; ++i) {
sum += amp * voxelValueNoise(x * freq, z * freq);
norm += amp;
amp *= 0.5f;
freq *= 2.03f;
}
return sum / std::max(norm, 1e-4f);
}
static Texture *makeEarthAtlas(Graphics *gfx) {
static const uint8_t pal[16][3] = {
{20, 22, 28}, {86, 158, 62}, {145, 96, 55}, {128, 128, 132}, {210, 186, 120}, {48, 96, 168},
{118, 78, 42}, {46, 118, 48}, {236, 240, 245}, {96, 96, 100}, {168, 52, 42}, {210, 196, 168},
{72, 48, 32}, {148, 72, 56}, {212, 168, 64}, {64, 68, 76},
};
std::vector<uint8_t> px(16u * 4u);
for (int i = 0; i < 16; ++i) {
px[size_t(i) * 4u + 0] = pal[i][0];
px[size_t(i) * 4u + 1] = pal[i][1];
px[size_t(i) * 4u + 2] = pal[i][2];
px[size_t(i) * 4u + 3] = 255;
}
return gfx->newTexture(4, 4, px.data());
}
static void fillBox(VoxelWorld *w, int x0, int y0, int z0, int x1, int y1, int z1, uint8_t tex) {
for (int z = z0; z < z1; ++z)
for (int y = y0; y < y1; ++y)
for (int x = x0; x < x1; ++x) w->setVoxel(x, y, z, tex);
}
static void fillHollowBox(VoxelWorld *w, int x0, int y0, int z0, int x1, int y1, int z1, uint8_t tex) {
fillBox(w, x0, y0, z0, x1, y1, z1, tex);
if (x1 - x0 > 2 && y1 - y0 > 1 && z1 - z0 > 2) fillBox(w, x0 + 1, y0, z0 + 1, x1 - 1, y1, z1 - 1, 0);
}
static void placeTree(VoxelWorld *w, int x, int groundY, int z) {
const int h = 3 + int(voxelH21(x, z) * 3.f);
for (int y = 1; y <= h; ++y) w->setVoxel(x, groundY + y, z, 6);
const int cy = groundY + h;
for (int dz = -2; dz <= 2; ++dz) {
for (int dx = -2; dx <= 2; ++dx) {
for (int dy = 0; dy <= 2; ++dy) {
if (dx * dx + dy * dy + dz * dz > 6) continue;
w->setVoxel(x + dx, cy + dy, z + dz, 7);
}
}
}
}
static void placeHouse(VoxelWorld *w, int x, int z, int wdt, int dpt, int hgt, uint8_t wall, uint8_t roof) {
fillHollowBox(w, x, 1, z, x + wdt, 1 + hgt, z + dpt, wall);
fillBox(w, x, 1 + hgt, z, x + wdt, 1 + hgt + 1, z + dpt, roof);
const int ridgeZ = z + dpt / 2;
for (int i = 0; i < wdt; ++i) w->setVoxel(x + i, 2 + hgt, ridgeZ, roof);
}
static std::string voxelSceneOutDir() { return std::string(EVENGINE_TEST_BINARY_DIR) + "/out/voxel_scenes"; }
static void renderVoxelScene(Graphics *gfx, VoxelWorld *world, Texture *atlas, const glm::vec3 &eye,
const glm::vec3 &target, float farZ, float viewRange) {
const float aspect = float(std::max(1, gfx->getPixelWidth())) / float(std::max(1, gfx->getPixelHeight()));
const glm::mat4 view = glm::lookAtRH(eye, target, glm::vec3(0.f, 1.f, 0.f));
const glm::mat4 proj = perspectiveVulkanRH_ZO(glm::radians(48.f), aspect, 0.2f, farZ);
const glm::mat4 vp = proj * view;
world->selectVisible(&vp[0][0], eye.x, eye.y, eye.z, viewRange, true);
const Color sky(0.42f, 0.58f, 0.78f, 1.f);
gfx->setBackgroundColor(sky);
gfx->clear(sky, std::nullopt, std::nullopt);
gfx->begin3DFrame();
if (!gfx->had3DThisFrame()) return;
gfx->setMesh3DViewProj(vp);
gfx->setMesh3DCameraPos(eye);
world->drawVisible(gfx, atlas, 4);
RenderSystem::render(*gfx);
}
static void saveVoxelScenePng(Graphics *gfx, const std::string &name) {
std::unique_ptr<ImageData> img(gfx->newImageData());
REQUIRE(img.get() != nullptr);
const std::string dir = voxelSceneOutDir();
std::error_code ec;
std::filesystem::create_directories(dir, ec);
const std::string path = dir + "/" + name + ".png";
REQUIRE(saveImagePng(*img, path));
std::printf("voxel scene: %s\n", path.c_str());
}
TEST_CASE("voxel.render.sceneHillsIsland") {
hideLeftover3D();
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx, 960, 540);
tinyHud(gfx);
gfx->setScreenReadbackEnabled(true);
std::unique_ptr<VoxelWorld> world(new VoxelWorld());
constexpr int kN = 160;
constexpr int kSea = 6;
for (int z = 0; z < kN; ++z) {
for (int x = 0; x < kN; ++x) {
const float n = voxelFbm(float(x) * 0.035f, float(z) * 0.035f);
const float ridge = std::fabs(voxelFbm(float(x) * 0.018f + 40.f, float(z) * 0.018f) * 2.f - 1.f);
const float h01 = n * 0.62f + (1.f - ridge) * 0.38f;
const float cx = (float(x) - 80.f) / 88.f;
const float cz = (float(z) - 80.f) / 88.f;
float island = std::clamp(1.12f - std::sqrt(cx * cx + cz * cz), 0.f, 1.f);
island *= island;
int h = int(2.f + h01 * 20.f * island);
if (h < 1) h = 1;
if (h > 24) h = 24;
for (int y = 0; y <= h; ++y) {
uint8_t tex = 3;
if (y == h) {
if (h <= kSea + 1)
tex = 4;
else if (h >= 18)
tex = 8;
else
tex = 1;
} else if (y >= h - 2) {
tex = 2;
}
world->setVoxel(x, y, z, tex);
}
if (h < kSea) {
for (int y = h + 1; y <= kSea; ++y) world->setVoxel(x, y, z, 5);
}
}
}
for (int z = 8; z < kN - 8; z += 11) {
for (int x = 8; x < kN - 8; x += 13) {
if (voxelH21(x * 3, z * 5) < 0.78f) continue;
uint8_t top = 0;
int gy = 24;
while (gy > 0 && (top = world->getVoxel(x, gy, z)) == 0) --gy;
if (top != 1) continue;
placeTree(world.get(), x, gy, z);
}
}
const int remeshed = world->remeshDirty();
CHECK(remeshed >= 20);
CHECK(world->getChunkCount() >= 20);
Texture *atlas = makeEarthAtlas(gfx);
REQUIRE(atlas != nullptr);
const glm::vec3 eye(78.f, 52.f, 168.f);
const glm::vec3 target(80.f, 8.f, 78.f);
for (int i = 0; i < 2; ++i) renderVoxelScene(gfx, world.get(), atlas, eye, target, 420.f, 360.f);
CHECK(world->getVisibleRectCount() > 80);
const Color sky(0.42f, 0.58f, 0.78f, 1.f);
CHECK(countNonBgSamples(gfx, sky) >= 20);
Color mid = gfx->getPixel(gfx->getWidth() / 2, gfx->getHeight() / 2);
CHECK(luma(mid) > 0.06f);
saveVoxelScenePng(gfx, "hills_island");
}
TEST_CASE("voxel.render.sceneWalledTown") {
hideLeftover3D();
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx, 960, 540);
tinyHud(gfx);
gfx->setScreenReadbackEnabled(true);
std::unique_ptr<VoxelWorld> world(new VoxelWorld());
constexpr int kN = 96;
fillBox(world.get(), 0, 0, 0, kN, 1, kN, 2);
fillBox(world.get(), 2, 1, 2, kN - 2, 2, kN - 2, 9);
const int wallH = 8;
for (int i = 4; i < kN - 4; ++i) {
for (int y = 1; y <= wallH; ++y) {
world->setVoxel(i, y, 4, 3);
world->setVoxel(i, y, kN - 5, 3);
world->setVoxel(4, y, i, 3);
world->setVoxel(kN - 5, y, i, 3);
}
if ((i % 2) == 0) {
world->setVoxel(i, wallH + 1, 4, 3);
world->setVoxel(i, wallH + 1, kN - 5, 3);
world->setVoxel(4, wallH + 1, i, 3);
world->setVoxel(kN - 5, wallH + 1, i, 3);
}
}
// Gate on +Z wall
fillBox(world.get(), 44, 1, 4, 52, wallH + 2, 6, 0);
fillBox(world.get(), 43, 1, 3, 44, wallH + 2, 7, 12);
fillBox(world.get(), 52, 1, 3, 53, wallH + 2, 7, 12);
auto tower = [&](int x, int z) {
fillHollowBox(world.get(), x, 1, z, x + 7, 14, z + 7, 3);
fillBox(world.get(), x, 14, z, x + 7, 15, z + 7, 10);
fillBox(world.get(), x + 1, 15, z + 1, x + 6, 16, z + 6, 10);
fillBox(world.get(), x + 2, 16, z + 2, x + 5, 17, z + 5, 10);
world->setVoxel(x + 3, 17, z + 3, 14);
};
tower(2, 2);
tower(kN - 9, 2);
tower(2, kN - 9);
tower(kN - 9, kN - 9);
fillHollowBox(world.get(), 36, 1, 36, 60, 16, 60, 13);
fillBox(world.get(), 36, 16, 36, 60, 17, 60, 10);
fillBox(world.get(), 38, 17, 38, 58, 18, 58, 10);
fillBox(world.get(), 46, 18, 46, 50, 22, 50, 12);
world->setVoxel(48, 22, 48, 14);
fillBox(world.get(), 46, 1, 36, 50, 8, 37, 0);
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 4; ++col) {
const int x = 10 + col * 8;
const int z = 12 + row * 8;
if (x + 6 >= 34) continue;
placeHouse(world.get(), x, z, 6, 6, 4 + ((col + row) % 3), 11, 10);
}
}
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
placeHouse(world.get(), 64 + col * 8, 12 + row * 9, 7, 7, 3 + (row % 2), 11, 10);
}
}
fillBox(world.get(), 12, 1, 70, 30, 2, 88, 4);
for (int i = 0; i < 5; ++i) placeTree(world.get(), 16 + i * 3, 1, 74 + (i % 2) * 4);
const int remeshed = world->remeshDirty();
CHECK(remeshed >= 4);
CHECK(world->getChunkCount() >= 4);
Texture *atlas = makeEarthAtlas(gfx);
REQUIRE(atlas != nullptr);
const glm::vec3 eye(118.f, 68.f, 118.f);
const glm::vec3 target(48.f, 6.f, 48.f);
for (int i = 0; i < 2; ++i) renderVoxelScene(gfx, world.get(), atlas, eye, target, 320.f, 280.f);
CHECK(world->getVisibleRectCount() > 40);
const Color sky(0.42f, 0.58f, 0.78f, 1.f);
CHECK(countNonBgSamples(gfx, sky) >= 20);
Color mid = gfx->getPixel(gfx->getWidth() / 2, gfx->getHeight() / 2);
CHECK(luma(mid) > 0.06f);
saveVoxelScenePng(gfx, "walled_town");
}
TEST_CASE("voxel.render.registryMultiFaceTex") {
hideLeftover3D();
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx, 320, 240);
tinyHud(gfx);
gfx->setScreenReadbackEnabled(true);
// grass:顶面绿色 tile(1),其余面红色 tile(0);方向性炉子:前脸蓝色 tile(2)
CubeTypeRegistry reg;
CubeType grass;
grass.name = "grass";
for (int i = 0; i < 6; ++i) grass.faceTex[i] = 0;
grass.faceTex[2] = 1; // 顶 = green
reg.add(grass);
CubeType furnace;
furnace.name = "furnace";
for (int i = 0; i < 6; ++i) furnace.faceTex[i] = 0;
furnace.faceTex[0] = 2; // +X = blue
furnace.directional = true;
reg.add(furnace);
VoxelWorld world(reg);
for (int z = 0; z < 3; ++z)
for (int y = 0; y < 3; ++y)
for (int x = 0; x < 3; ++x) world.setVoxelByName(x, y, z, "grass");
world.remeshDirty();
Chunk *c = world.getChunk(0, 0, 0);
REQUIRE(c != nullptr);
CHECK_EQ(c->faceRects(FaceDir::PosY)[0].tex(), 1); // 顶面解析为 green
CHECK_EQ(c->faceRects(FaceDir::PosX)[0].tex(), 0); // 侧面 red
// 4×4 图集:tile0=red, tile1=green, tile2=blue
Texture *atlas = makeTileAtlas(gfx, 4, 4);
const glm::vec3 eye(1.5f, 1.5f, 10.f);
const glm::vec3 target(1.5f, 1.5f, 1.5f);
renderVoxelFrame(gfx, &world, atlas, 4, eye, target, 100.f, true);
Color mid = gfx->getPixel(gfx->getWidth() / 2, gfx->getHeight() / 2);
CHECK(luma(mid) > 0.04f);
}