#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 #include #include #include #include #include #include #include #include #include #include // 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(png->getData()), static_cast(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 bytes(static_cast(size)); REQUIRE(input.read(reinterpret_cast(bytes.data()), size).good()); eve::data::ByteData source(bytes.data(), bytes.size()); std::unique_ptr 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(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 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(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(); }