559 lines
25 KiB
C++
559 lines
25 KiB
C++
#include "PathBesideSource.h"
|
|
#include "housegen/HouseComponentLibrary.h"
|
|
#include "housegen/HouseGenerator.h"
|
|
#include "housegen/HouseLayout.h"
|
|
|
|
#include "data/ByteData.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/Light.h"
|
|
#include "graphics/Material.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "graphics/Outline.h"
|
|
#include "graphics/Quad.h"
|
|
#include "graphics/RenderControl.h"
|
|
#include "graphics/RenderSystem.h"
|
|
#include "graphics/RenderSystem3D.h"
|
|
#include "graphics/ScreenSpaceReflection.h"
|
|
#include "graphics/Shader.h"
|
|
#include "graphics/Texture.h"
|
|
#include "graphics/Volumetric.h"
|
|
#include "graphics/Water.h"
|
|
#include "graphics/Waterfall.h"
|
|
#include "image/Image.h"
|
|
#include "image/ImageData.h"
|
|
#include "medialoader/model/ModelLoader.h"
|
|
#include "model3d/Model3D.h"
|
|
#include "window/Window.h"
|
|
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <assimp/scene.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
|
|
using eve::graphics::Color;
|
|
|
|
using namespace eve::graphics;
|
|
using namespace eve::housegen;
|
|
|
|
namespace {
|
|
|
|
int envInt(const char *name, int fallback) {
|
|
const char *value = std::getenv(name);
|
|
if (!value || !*value) return fallback;
|
|
try {
|
|
return std::stoi(value);
|
|
} catch (...) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
std::string envString(const char *name, const char *fallback) {
|
|
const char *value = std::getenv(name);
|
|
return value && *value ? value : fallback;
|
|
}
|
|
|
|
struct Palette {
|
|
float wallR, wallG, wallB;
|
|
float trimR, trimG, trimB;
|
|
float roofR, roofG, roofB;
|
|
float glassR, glassG, glassB;
|
|
};
|
|
|
|
Palette gPalette{0.76f, 0.58f, 0.38f, 0.82f, 0.64f, 0.42f,
|
|
0.42f, 0.16f, 0.13f, 0.18f, 0.55f, 0.72f};
|
|
|
|
void selectPalette(uint32_t seed) {
|
|
switch (seed % 3u) {
|
|
case 0: gPalette = {0.78f, 0.80f, 0.76f, 0.25f, 0.28f, 0.30f,
|
|
0.16f, 0.20f, 0.25f, 0.22f, 0.48f, 0.64f}; break;
|
|
case 1: gPalette = {0.76f, 0.58f, 0.38f, 0.82f, 0.64f, 0.42f,
|
|
0.42f, 0.16f, 0.13f, 0.18f, 0.55f, 0.72f}; break;
|
|
default: gPalette = {0.64f, 0.70f, 0.78f, 0.92f, 0.90f, 0.82f,
|
|
0.20f, 0.28f, 0.36f, 0.16f, 0.52f, 0.70f}; break;
|
|
}
|
|
}
|
|
|
|
// A neutral unit cube is enough for this preview: HouseLayout supplies all placement and the
|
|
// renderer supplies material tint, lighting and perspective. Real GLB components use the same
|
|
// transforms through HouseLayout::instantiate().
|
|
constexpr char kCubeObj[] =
|
|
"v -0.5 -0.5 0.5\nv 0.5 -0.5 0.5\nv 0.5 0.5 0.5\nv -0.5 0.5 0.5\n"
|
|
"v 0.5 -0.5 -0.5\nv -0.5 -0.5 -0.5\nv -0.5 0.5 -0.5\nv 0.5 0.5 -0.5\n"
|
|
"vt 0 0\nvt 1 0\nvt 1 1\nvt 0 1\n"
|
|
"vn 0 0 1\nvn 0 0 -1\nvn 1 0 0\nvn -1 0 0\nvn 0 1 0\nvn 0 -1 0\n"
|
|
"f 1/1/1 2/2/1 3/3/1\nf 1/1/1 3/3/1 4/4/1\n"
|
|
"f 5/1/2 6/2/2 7/3/2\nf 5/1/2 7/3/2 8/4/2\n"
|
|
"f 2/1/3 5/2/3 8/3/3\nf 2/1/3 8/3/3 3/4/3\n"
|
|
"f 6/1/4 1/2/4 4/3/4\nf 6/1/4 4/3/4 7/4/4\n"
|
|
"f 4/1/5 3/2/5 8/3/5\nf 4/1/5 8/3/5 7/4/5\n"
|
|
"f 6/1/6 5/2/6 2/3/6\nf 6/1/6 2/3/6 1/4/6\n";
|
|
|
|
// Unit triangular prism whose base starts at y=0. Scaling it produces a continuous gable end;
|
|
// unlike stacked cubes it cannot poke through the sloped roof or leave stair-step notches.
|
|
constexpr char kGableObj[] =
|
|
"v -0.5 0.0 0.5\nv 0.5 0.0 0.5\nv 0.0 1.0 0.5\n"
|
|
"v -0.5 0.0 -0.5\nv 0.5 0.0 -0.5\nv 0.0 1.0 -0.5\n"
|
|
"f 1 2 3\nf 6 5 4\nf 1 4 5\nf 1 5 2\n"
|
|
"f 2 5 6\nf 2 6 3\nf 3 6 4\nf 3 4 1\n";
|
|
|
|
Mesh *previewCube(Graphics *gfx) {
|
|
medialoader::ModelLoader loader;
|
|
auto scene = loader.loadFromMemory(kCubeObj, sizeof(kCubeObj) - 1, ".obj");
|
|
REQUIRE(!scene.empty());
|
|
REQUIRE(scene->mNumMeshes > 0);
|
|
return gfx->newMeshFromAssimp(*scene->mMeshes[0]);
|
|
}
|
|
|
|
Mesh *previewGable(Graphics *gfx) {
|
|
medialoader::ModelLoader loader;
|
|
auto scene = loader.loadFromMemory(kGableObj, sizeof(kGableObj) - 1, ".obj");
|
|
REQUIRE(!scene.empty());
|
|
REQUIRE(scene->mNumMeshes > 0);
|
|
return gfx->newMeshFromAssimp(*scene->mMeshes[0]);
|
|
}
|
|
|
|
Renderable3D *box(Mesh *mesh, float x, float y, float z, float sx, float sy, float sz,
|
|
float r, float g, float b, float roll = 0.f) {
|
|
auto *entity = Renderable3D::create();
|
|
entity->setMesh(mesh);
|
|
entity->setPosition(x, y, z);
|
|
entity->setScale(sx, sy, sz);
|
|
entity->setRotation(0.f, 0.f, roll);
|
|
entity->setTint(r, g, b, 1.f);
|
|
entity->setRoughness(0.72f);
|
|
return entity;
|
|
}
|
|
|
|
Texture *gBrickColor = nullptr;
|
|
Texture *gBrickNormal = nullptr;
|
|
Texture *gBrickHeight = nullptr;
|
|
|
|
Renderable3D *wallSurfaceBox(Mesh *mesh, float x, float y, float z, float sx, float sy,
|
|
float sz) {
|
|
auto *entity = box(mesh, x, y, z, sx, sy, sz, 1.f, 1.f, 1.f);
|
|
if (gBrickColor) entity->setTexture(gBrickColor);
|
|
if (gBrickNormal) entity->setNormalTexture(gBrickNormal);
|
|
if (gBrickHeight) {
|
|
entity->setHeightTexture(gBrickHeight);
|
|
entity->setParallax(0.018f, 8.f, 20.f);
|
|
}
|
|
entity->setMetallic(0.f);
|
|
entity->setRoughness(0.86f);
|
|
entity->setTexCellBomb(4.f, 0.05f, 0.f);
|
|
return entity;
|
|
}
|
|
|
|
void addWallModule(Mesh *mesh, const HouseInstance &instance, float floorHeight) {
|
|
const float outX = instance.rotationDeg == 90 ? 1.f : instance.rotationDeg == 270 ? -1.f : 0.f;
|
|
const float outZ = instance.rotationDeg == 180 ? 1.f : instance.rotationDeg == 0 ? -1.f : 0.f;
|
|
const float x = float(instance.x) + outX * 0.50f;
|
|
const float y0 = float(instance.z) * floorHeight;
|
|
const float z = float(instance.y) + outZ * 0.50f;
|
|
const bool alongX = instance.rotationDeg == 0 || instance.rotationDeg == 180;
|
|
const float sx = alongX ? 1.02f : 0.12f;
|
|
const float sz = alongX ? 0.12f : 1.02f;
|
|
|
|
if (instance.componentId == "wall.window") {
|
|
wallSurfaceBox(mesh, x, y0 + 0.40f, z, sx, 0.80f, sz);
|
|
wallSurfaceBox(mesh, x, y0 + 2.08f, z, sx, 0.64f, sz);
|
|
const float glassX = x + outX * 0.015f, glassZ = z + outZ * 0.015f;
|
|
box(mesh, glassX, y0 + 1.28f, glassZ, alongX ? 0.72f : 0.07f, 0.92f,
|
|
alongX ? 0.07f : 0.72f,
|
|
gPalette.glassR, gPalette.glassG, gPalette.glassB);
|
|
for (float side : {-0.40f, 0.40f})
|
|
box(mesh, glassX + (alongX ? side : 0.f), y0 + 1.28f,
|
|
glassZ + (alongX ? 0.f : side), alongX ? 0.07f : 0.08f, 1.08f,
|
|
alongX ? 0.08f : 0.07f, gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
for (float side : {0.76f, 1.80f})
|
|
box(mesh, glassX, y0 + side, glassZ, alongX ? 0.86f : 0.08f, 0.07f,
|
|
alongX ? 0.08f : 0.86f, gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
return;
|
|
}
|
|
wallSurfaceBox(mesh, x, y0 + floorHeight * 0.5f, z, sx, floorHeight, sz);
|
|
}
|
|
|
|
void addDoorModule(Mesh *mesh, const HouseInstance &instance, float floorHeight) {
|
|
float x = float(instance.x);
|
|
const float y0 = float(instance.z) * floorHeight;
|
|
float z = float(instance.y);
|
|
const bool alongX = instance.rotationDeg == 0 || instance.rotationDeg == 180;
|
|
const float outX = instance.rotationDeg == 90 ? 1.f : instance.rotationDeg == 270 ? -1.f : 0.f;
|
|
const float outZ = instance.rotationDeg == 180 ? 1.f : instance.rotationDeg == 0 ? -1.f : 0.f;
|
|
x += outX * 0.50f; z += outZ * 0.50f;
|
|
const float leafX = alongX ? 0.58f : 0.12f, leafZ = alongX ? 0.12f : 0.58f;
|
|
box(mesh, x, y0 + 0.92f, z, leafX, 1.84f, leafZ, 0.16f, 0.24f, 0.20f);
|
|
for (float side : {-0.39f, 0.39f})
|
|
box(mesh, x + (alongX ? side : 0.f), y0 + floorHeight * 0.5f,
|
|
z + (alongX ? 0.f : side),
|
|
alongX ? 0.15f : 0.16f, floorHeight, alongX ? 0.16f : 0.15f,
|
|
gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
box(mesh, x, y0 + 2.25f, z, alongX ? 0.64f : 0.16f, 0.30f, alongX ? 0.16f : 0.64f,
|
|
gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
box(mesh, x + (alongX ? 0.20f : outX * 0.075f), y0 + 0.95f,
|
|
z + (alongX ? outZ * 0.075f : 0.20f), 0.055f, 0.055f, 0.055f,
|
|
0.92f, 0.72f, 0.20f);
|
|
|
|
// A small porch makes entrance-side variation readable from a distance.
|
|
const float porchX = x + outX * 0.55f, porchZ = z + outZ * 0.55f;
|
|
box(mesh, porchX, y0 + 0.04f, porchZ, alongX ? 1.55f : 0.80f, 0.10f,
|
|
alongX ? 0.80f : 1.55f, 0.38f, 0.28f, 0.19f);
|
|
box(mesh, porchX, y0 + 2.48f, porchZ, alongX ? 1.75f : 0.92f, 0.10f,
|
|
alongX ? 0.92f : 1.75f, gPalette.roofR, gPalette.roofG, gPalette.roofB);
|
|
for (float side : {-0.67f, 0.67f})
|
|
box(mesh, porchX + (alongX ? side : 0.f), y0 + 1.24f,
|
|
porchZ + (alongX ? 0.f : side), 0.07f, 2.42f, 0.07f,
|
|
gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
}
|
|
|
|
void saveFrame(Graphics *gfx, const std::string &name) {
|
|
[[maybe_unused]] auto *const imageModule = eve::image::Image::create();
|
|
auto *frame = gfx->newImageData();
|
|
REQUIRE(frame != nullptr);
|
|
auto *png = frame->encode(medialoader::FormatHandler::ENCODED_PNG, name.c_str(), false);
|
|
REQUIRE(png != nullptr);
|
|
const std::filesystem::path outDir =
|
|
std::filesystem::path(EVENGINE_TEST_BINARY_DIR) / "out";
|
|
std::filesystem::create_directories(outDir);
|
|
const auto outPath = outDir / name;
|
|
std::ofstream out(outPath, std::ios::binary);
|
|
REQUIRE(out.good());
|
|
out.write(static_cast<const char *>(png->getData()),
|
|
static_cast<std::streamsize>(png->getSize()));
|
|
REQUIRE(out.good());
|
|
std::printf("housegen render saved: %s\n", outPath.string().c_str());
|
|
delete png;
|
|
delete frame;
|
|
}
|
|
|
|
Texture *loadPreviewTexture(Graphics *gfx, const std::filesystem::path &path) {
|
|
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
|
REQUIRE(input.good());
|
|
const std::streamsize size = input.tellg();
|
|
REQUIRE(size > 0);
|
|
input.seekg(0, std::ios::beg);
|
|
std::vector<uint8_t> bytes(static_cast<size_t>(size));
|
|
REQUIRE(input.read(reinterpret_cast<char *>(bytes.data()), size).good());
|
|
eve::data::ByteData source(bytes.data(), bytes.size());
|
|
std::unique_ptr<eve::image::ImageData> decoded(
|
|
eve::image::Image::create()->newImageData(&source));
|
|
TextureCreateInfo info = TextureCreateInfo::withMipmaps(true);
|
|
info.sampler.repeatU = true;
|
|
info.sampler.repeatV = true;
|
|
return gfx->newTexture(decoded.get(), info);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("housegen.renderPreview") {
|
|
constexpr char kit[] = R"({"components":[
|
|
{"id":"foundation","model":"foundation.glb","category":"foundation"},
|
|
{"id":"floor","model":"floor.glb","category":"floor"},
|
|
{"id":"wall.solid","model":"wall.glb","category":"wall","weight":2},
|
|
{"id":"wall.window","model":"window.glb","category":"wall","weight":5,"tags":["window"]},
|
|
{"id":"door","model":"door.glb","category":"door"},
|
|
{"id":"roof","model":"roof.glb","category":"roof"}
|
|
]})";
|
|
|
|
HouseComponentLibrary library;
|
|
auto kitResult = library.loadFromJson(kit);
|
|
REQUIRE(kitResult.ok());
|
|
HouseRequest request;
|
|
request.seed = static_cast<unsigned>(envInt("EVHOUSE_PREVIEW_SEED", 20260815));
|
|
request.width = envInt("EVHOUSE_PREVIEW_WIDTH", 7);
|
|
request.depth = envInt("EVHOUSE_PREVIEW_DEPTH", 6);
|
|
request.floors = envInt("EVHOUSE_PREVIEW_FLOORS", 2);
|
|
request.footprint = envString("EVHOUSE_PREVIEW_FOOTPRINT", "auto");
|
|
request.roof = envString("EVHOUSE_PREVIEW_ROOF", "auto");
|
|
request.entrance = envString("EVHOUSE_PREVIEW_ENTRANCE", "auto");
|
|
request.floorHeight = 2.4f;
|
|
HouseLayout layout;
|
|
HouseGenerator generator(library);
|
|
auto generated = generator.generate(request, layout);
|
|
REQUIRE(generated.ok());
|
|
|
|
auto *window = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
REQUIRE(window != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
eve::window::WindowSettings settings;
|
|
settings.width = 960;
|
|
settings.height = 640;
|
|
settings.centered = true;
|
|
REQUIRE(window->setWindowSettings(settings));
|
|
|
|
Mesh *cube = previewCube(gfx);
|
|
Mesh *gable = previewGable(gfx);
|
|
REQUIRE(cube != nullptr);
|
|
REQUIRE(gable != nullptr);
|
|
const std::filesystem::path brickDir =
|
|
eve_test_path::pathBesideTestDir(__FILE__, "assets/housegen/materials/ambientcg-bricks001");
|
|
gBrickColor = loadPreviewTexture(gfx, brickDir / "bricks001-color.jpg");
|
|
gBrickNormal = loadPreviewTexture(gfx, brickDir / "bricks001-normal-gl.jpg");
|
|
gBrickHeight = loadPreviewTexture(gfx, brickDir / "bricks001-height.jpg");
|
|
REQUIRE(gBrickColor != nullptr);
|
|
REQUIRE(gBrickNormal != nullptr);
|
|
REQUIRE(gBrickHeight != nullptr);
|
|
selectPalette(request.seed);
|
|
gfx->setScreenReadbackEnabled(true);
|
|
gfx->setBackgroundColor(Color(0.58f, 0.75f, 0.90f, 1.f));
|
|
|
|
// Lawn, foundation/floors, then facade modules selected by the generated layout.
|
|
const float plotCenterX = float(request.width - 1) * 0.5f;
|
|
const float plotCenterZ = float(request.depth - 1) * 0.5f;
|
|
box(cube, plotCenterX, -0.30f, plotCenterZ, float(request.width + 4), 0.25f,
|
|
float(request.depth + 4), 0.20f, 0.42f, 0.18f);
|
|
for (const auto &instance : layout.instances) {
|
|
const auto component = library.find(instance.componentId);
|
|
REQUIRE(component.has_value());
|
|
if (component->get().category == "foundation") {
|
|
box(cube, float(instance.x), -0.08f, float(instance.y), 0.96f, 0.16f, 0.96f,
|
|
0.35f, 0.34f, 0.33f);
|
|
} else if (component->get().category == "floor") {
|
|
box(cube, float(instance.x), float(instance.z) * request.floorHeight - 0.03f,
|
|
float(instance.y), 0.96f, 0.06f, 0.96f, 0.48f, 0.33f, 0.22f);
|
|
} else if (component->get().category == "wall") {
|
|
addWallModule(cube, instance, request.floorHeight);
|
|
} else if (component->get().category == "door") {
|
|
addDoorModule(cube, instance, request.floorHeight);
|
|
}
|
|
}
|
|
|
|
// One coherent gable roof replaces the per-cell debug roof blocks while still deriving its
|
|
// footprint and elevation from the generated request/layout.
|
|
int roofLevel = 0;
|
|
for (const auto &instance : layout.instances) {
|
|
const auto component = library.find(instance.componentId);
|
|
if (component && component->get().category == "roof") roofLevel = std::max(roofLevel, instance.z);
|
|
}
|
|
int roofMinX = request.width, roofMaxX = 0, roofMinZ = request.depth, roofMaxZ = 0;
|
|
for (const auto &instance : layout.instances) {
|
|
const auto component = library.find(instance.componentId);
|
|
if (component && component->get().category == "roof" && instance.z == roofLevel) {
|
|
roofMinX = std::min(roofMinX, instance.x);
|
|
roofMaxX = std::max(roofMaxX, instance.x);
|
|
roofMinZ = std::min(roofMinZ, instance.y);
|
|
roofMaxZ = std::max(roofMaxZ, instance.y);
|
|
}
|
|
}
|
|
const float roofWidth = float(roofMaxX - roofMinX + 1);
|
|
const float roofDepth = float(roofMaxZ - roofMinZ + 1);
|
|
const float roofCenterX = float(roofMinX + roofMaxX) * 0.5f;
|
|
const float roofCenterZ = float(roofMinZ + roofMaxZ) * 0.5f;
|
|
const float eaveY = roofLevel * request.floorHeight;
|
|
std::unordered_set<int> topRoofCells;
|
|
for (const auto &instance : layout.instances) {
|
|
const auto component = library.find(instance.componentId);
|
|
if (component && component->get().category == "roof" && instance.z == roofLevel)
|
|
topRoofCells.insert(instance.y * request.width + instance.x);
|
|
}
|
|
auto hasTopRoof = [&](int x, int z) {
|
|
return x >= 0 && z >= 0 && x < request.width && z < request.depth &&
|
|
topRoofCells.contains(z * request.width + x);
|
|
};
|
|
// Roof instances below the main roof are mandatory covers for cells exposed by a setback.
|
|
for (const auto &instance : layout.instances) {
|
|
const auto component = library.find(instance.componentId);
|
|
if (!component || component->get().category != "roof" || instance.z == roofLevel) continue;
|
|
box(cube, float(instance.x), instance.z * request.floorHeight, float(instance.y),
|
|
1.02f, 0.14f, 1.02f, gPalette.roofR, gPalette.roofG, gPalette.roofB);
|
|
}
|
|
if (layout.roofStyle == "gable" && layout.footprintStyle == "rectangle") {
|
|
const float halfRun = roofWidth * 0.5f + 0.30f;
|
|
const float rise = roofWidth * 0.22f;
|
|
const float angle = std::atan2(rise, halfRun);
|
|
const float panelLength = std::hypot(halfRun, rise);
|
|
const float panelOffset = halfRun * 0.5f;
|
|
const float roofY = eaveY + rise * 0.5f;
|
|
box(cube, roofCenterX - panelOffset, roofY, roofCenterZ, panelLength, 0.18f,
|
|
roofDepth + 0.65f, gPalette.roofR, gPalette.roofG, gPalette.roofB, angle);
|
|
box(cube, roofCenterX + panelOffset, roofY, roofCenterZ, panelLength, 0.18f,
|
|
roofDepth + 0.65f, gPalette.roofR, gPalette.roofG, gPalette.roofB, -angle);
|
|
const float wallRise = roofWidth * 0.5f * std::tan(angle) - 0.05f;
|
|
for (float z : {float(roofMinZ) - 0.50f, float(roofMaxZ) + 0.50f})
|
|
box(gable, roofCenterX, eaveY - 0.02f, z, roofWidth, wallRise, 0.12f,
|
|
gPalette.wallR, gPalette.wallG, gPalette.wallB);
|
|
} else {
|
|
// Tile-wise flat/shed roofs preserve L/T-shaped silhouettes instead of filling their
|
|
// missing quadrants with a rectangular cap.
|
|
const float shedAngle = layout.roofStyle == "shed" ? -0.16f : 0.f;
|
|
const float roofY = eaveY + (layout.roofStyle == "shed" ? roofWidth * 0.08f : 0.f);
|
|
for (const auto &instance : layout.instances) {
|
|
const auto component = library.find(instance.componentId);
|
|
if (!component || component->get().category != "roof" || instance.z != roofLevel) continue;
|
|
const float slopeY = layout.roofStyle == "shed"
|
|
? (roofCenterX - float(instance.x)) * 0.16f : 0.f;
|
|
box(cube, float(instance.x), roofY + slopeY, float(instance.y),
|
|
1.08f, layout.roofStyle == "flat" ? 0.20f : 0.14f, 1.08f,
|
|
gPalette.roofR, gPalette.roofG, gPalette.roofB, shedAngle);
|
|
}
|
|
// Close every actual roof boundary. Flat roofs receive a complete parapet; shed roofs
|
|
// receive stepped wall infill up to the sloped plane.
|
|
const int dx[] = {0, 1, 0, -1}, dz[] = {-1, 0, 1, 0};
|
|
for (int z = roofMinZ; z <= roofMaxZ; ++z) for (int x = roofMinX; x <= roofMaxX; ++x) {
|
|
if (!hasTopRoof(x, z)) continue;
|
|
const float tileY = roofY + (layout.roofStyle == "shed" ? (roofCenterX - float(x)) * 0.16f : 0.f);
|
|
for (int side = 0; side < 4; ++side) {
|
|
if (hasTopRoof(x + dx[side], z + dz[side])) continue;
|
|
const bool alongX = side == 0 || side == 2;
|
|
const float px = float(x) + float(dx[side]) * 0.50f;
|
|
const float pz = float(z) + float(dz[side]) * 0.50f;
|
|
if (layout.roofStyle == "flat") {
|
|
box(cube, px, eaveY + 0.20f, pz, alongX ? 1.02f : 0.10f, 0.34f,
|
|
alongX ? 0.10f : 1.02f, gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
} else {
|
|
const float fillHeight = std::max(0.02f, tileY - eaveY - 0.14f);
|
|
wallSurfaceBox(cube, px, eaveY + fillHeight * 0.5f - 0.07f, pz,
|
|
alongX ? 1.02f : 0.11f, fillHeight,
|
|
alongX ? 0.11f : 1.02f);
|
|
box(cube, px, tileY - 0.10f, pz, alongX ? 1.06f : 0.13f, 0.18f,
|
|
alongX ? 0.13f : 1.06f, gPalette.roofR, gPalette.roofG, gPalette.roofB);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Some seeds receive a front balcony; it is an optional decorative grammar production.
|
|
if (request.floors >= 2 && request.seed % 2u == 0u) {
|
|
const float balconyZ = -0.88f;
|
|
box(cube, plotCenterX, request.floorHeight + 0.14f, balconyZ, 2.8f, 0.14f, 0.74f,
|
|
0.38f, 0.28f, 0.19f);
|
|
box(cube, plotCenterX, request.floorHeight + 0.72f, balconyZ - 0.30f, 2.8f, 0.08f, 0.08f,
|
|
gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
for (float side : {-1.30f, 0.f, 1.30f})
|
|
box(cube, plotCenterX + side, request.floorHeight + 0.44f, balconyZ - 0.30f,
|
|
0.07f, 0.60f, 0.07f, gPalette.trimR, gPalette.trimG, gPalette.trimB);
|
|
}
|
|
|
|
auto *camera = Camera3D::createCamera();
|
|
const float plotSpan = float(std::max(request.width, request.depth));
|
|
camera->setTarget(plotCenterX, request.floors * request.floorHeight * 0.52f, plotCenterZ);
|
|
float eyeX = plotCenterX + plotSpan * 1.10f;
|
|
float eyeZ = plotCenterZ - plotSpan * 1.55f;
|
|
if (layout.entranceSide == "east") {
|
|
eyeX = plotCenterX + plotSpan * 1.55f; eyeZ = plotCenterZ - plotSpan * 1.10f;
|
|
} else if (layout.entranceSide == "south") {
|
|
eyeX = plotCenterX - plotSpan * 1.10f; eyeZ = plotCenterZ + plotSpan * 1.55f;
|
|
} else if (layout.entranceSide == "west") {
|
|
eyeX = plotCenterX - plotSpan * 1.55f; eyeZ = plotCenterZ - plotSpan * 1.10f;
|
|
}
|
|
camera->setEye(eyeX, request.floors * request.floorHeight + plotSpan * 0.48f, eyeZ);
|
|
camera->setAmbient(0.34f, 0.34f, 0.38f);
|
|
camera->data()->farZ = 80.f;
|
|
auto *sun = Light3D::createLight("dir");
|
|
sun->setDirection(-0.7f, -1.0f, -0.45f);
|
|
sun->setColor(1.f, 0.94f, 0.84f, 1.55f);
|
|
sun->setCastShadow(true);
|
|
sun->setShadowStrength(0.82f);
|
|
|
|
// RenderSystem closes the 3D pass and presents. The tiny off-screen-ish HUD also exercises
|
|
// the normal mixed 3D/2D game frame path.
|
|
auto *hud = Renderable2D::create();
|
|
hud->transform()->x = 0.f;
|
|
hud->transform()->y = 0.f;
|
|
hud->sprite()->width = 1.f;
|
|
hud->sprite()->height = 1.f;
|
|
hud->sprite()->a = 0.f;
|
|
for (int frame = 0; frame < 4; ++frame) {
|
|
RenderSystem3D::render(*gfx);
|
|
RenderSystem::render(*gfx);
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {}
|
|
}
|
|
|
|
saveFrame(gfx, envString("EVHOUSE_PREVIEW_NAME", "housegen_seed_20260815.png"));
|
|
window->close();
|
|
}
|
|
|
|
TEST_CASE("housegen.materialPreview") {
|
|
const std::filesystem::path assetDir =
|
|
eve_test_path::pathBesideTestDir(__FILE__, "assets/housegen/kenney-modular-buildings");
|
|
HouseComponentLibrary library;
|
|
auto loaded = library.loadFromFile((assetDir / "components.json").string());
|
|
REQUIRE(loaded.ok());
|
|
REQUIRE_EQ(library.count(), 4);
|
|
for (const auto &id : library.ids()) {
|
|
const auto component = library.find(id);
|
|
REQUIRE(component.has_value());
|
|
REQUIRE(std::filesystem::is_regular_file(component->get().modelPath));
|
|
}
|
|
|
|
auto *window = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
auto *models = eve::model3d::Model3D::create();
|
|
REQUIRE(window != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
REQUIRE(models != nullptr);
|
|
eve::window::WindowSettings settings;
|
|
settings.width = 960;
|
|
settings.height = 640;
|
|
settings.centered = true;
|
|
REQUIRE(window->setWindowSettings(settings));
|
|
|
|
gfx->setScreenReadbackEnabled(true);
|
|
gfx->setBackgroundColor(Color(0.56f, 0.72f, 0.86f, 1.f));
|
|
Mesh *cube = previewCube(gfx);
|
|
REQUIRE(cube != nullptr);
|
|
box(cube, 2.1f, -0.12f, 0.f, 7.2f, 0.18f, 3.2f, 0.22f, 0.38f, 0.18f);
|
|
|
|
HouseLayout layout;
|
|
layout.moduleSize = 1.4f;
|
|
layout.floorHeight = 2.f;
|
|
layout.instances = {
|
|
{"kenney.wall.block", 0, 0, 0, 0},
|
|
{"kenney.wall.window", 1, 0, 0, 0},
|
|
{"kenney.door", 2, 0, 0, 0},
|
|
{"kenney.roof.gable", 3, 0, 0, 0},
|
|
};
|
|
auto instantiated = layout.instantiate(*gfx, *models, library);
|
|
REQUIRE(instantiated.ok());
|
|
auto entities = std::move(instantiated).takeValue();
|
|
REQUIRE(entities.size() >= layout.instances.size());
|
|
auto *first = static_cast<Renderable3D *>(ecs::try_get(entities.front()));
|
|
REQUIRE(first != nullptr);
|
|
REQUIRE(first->meshRenderer()->texture != nullptr);
|
|
REQUIRE(first->meshRenderer()->normalTexture != nullptr);
|
|
REQUIRE(first->meshRenderer()->heightTexture != nullptr);
|
|
REQUIRE(first->meshRenderer()->parallaxScale > 0.f);
|
|
|
|
auto *camera = Camera3D::createCamera();
|
|
camera->setTarget(2.1f, 0.55f, 0.f);
|
|
camera->setEye(4.9f, 2.8f, 5.0f);
|
|
camera->setAmbient(0.30f, 0.30f, 0.34f);
|
|
camera->data()->farZ = 60.f;
|
|
auto *sun = Light3D::createLight("dir");
|
|
sun->setDirection(-0.7f, -1.f, -0.5f);
|
|
sun->setColor(1.f, 0.94f, 0.84f, 1.6f);
|
|
sun->setCastShadow(true);
|
|
sun->setShadowStrength(0.78f);
|
|
auto *hud = Renderable2D::create();
|
|
hud->sprite()->a = 0.f;
|
|
for (int frame = 0; frame < 4; ++frame) {
|
|
RenderSystem3D::render(*gfx);
|
|
RenderSystem::render(*gfx);
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {}
|
|
}
|
|
saveFrame(gfx, envString("EVHOUSE_MATERIAL_PREVIEW_NAME", "housegen_materials.png"));
|
|
window->close();
|
|
}
|