13#include <assimp/material.h>
14#include <assimp/matrix4x4.h>
15#include <assimp/scene.h>
16#include <assimp/texture.h>
29#include <unordered_map>
30#include <unordered_set>
35std::string esc(
const std::string &
v) { std::string o;
for (
char c :
v) {
if (
c ==
'\\' ||
c ==
'"') o +=
'\\'; o +=
c; }
return o; }
37graphics::Texture *textureFromEmbedded(graphics::Graphics *gfx,
const aiTexture *
source) {
39 if (
source->mHeight == 0) {
41 std::unique_ptr<image::ImageData> decoded(image::Image::create()->newImageData(&
bytes));
44 std::vector<uint8_t> rgba(
size_t(
source->mWidth) *
size_t(
source->mHeight) * 4);
45 for (
size_t pixel = 0; pixel < size_t(
source->mWidth) * size_t(
source->mHeight); ++pixel) {
46 rgba[pixel * 4 + 0] =
source->pcData[pixel].r;
47 rgba[pixel * 4 + 1] =
source->pcData[pixel].g;
48 rgba[pixel * 4 + 2] =
source->pcData[pixel].b;
49 rgba[pixel * 4 + 3] =
source->pcData[pixel].a;
51 return gfx->newTexture(
int(
source->mWidth), int(
source->mHeight), rgba.data(),
55graphics::Texture *textureFromFile(graphics::Graphics *gfx,
const std::string &modelPath,
56 const std::string &texturePath) {
57 if (texturePath.empty())
return nullptr;
58 std::filesystem::path resolved(texturePath);
59 if (resolved.is_relative()) resolved = std::filesystem::path(modelPath).parent_path() / resolved;
61 if (std::filesystem::is_regular_file(resolved)) {
62 std::ifstream
input(resolved, std::ios::binary | std::ios::ate);
63 const std::streamsize
size =
input.tellg();
64 if (
size <= 0)
return nullptr;
65 input.seekg(0, std::ios::beg);
66 std::vector<uint8_t>
bytes(
static_cast<size_t>(
size));
67 if (!
input.read(
reinterpret_cast<char *
>(
bytes.data()),
size))
return nullptr;
69 std::unique_ptr<image::ImageData> decoded(
70 image::Image::create()->newImageData(&
source));
73 return gfx->newTextureFromFile(resolved.lexically_normal().string());
79graphics::Texture *assimpTexture(graphics::Graphics *gfx,
const aiScene *scene,
80 const aiMaterial *
material,
const std::string &modelPath,
81 aiTextureType primary, aiTextureType fallback) {
82 if (!scene || !
material)
return nullptr;
84 if (
material->GetTexture(primary, 0, &texturePath) != AI_SUCCESS &&
85 material->GetTexture(fallback, 0, &texturePath) != AI_SUCCESS)
87 const std::string
path = texturePath.C_Str();
88 if (
path.empty())
return nullptr;
89 if (
path.front() ==
'*') {
90 const int index = std::atoi(
path.c_str() + 1);
91 if (
index < 0 ||
static_cast<unsigned>(
index) >=
scene->mNumTextures)
return nullptr;
93 return textureFromEmbedded(gfx,
scene->mTextures[
index]);
98 return textureFromFile(gfx, modelPath,
path);
112 [[nodiscard]] model3d::ModelData *get() const noexcept {
119ModelHandle loadModel(model3d::Model3D *models,
const std::string &
path) {
120 if (!std::filesystem::is_regular_file(std::filesystem::path(
path))) {
123 model3d::ModelData *cached = models->newModelDataFromFile(
path);
124 if (cached ==
nullptr)
return {};
126 if (!
pinned.ok())
return {};
131 std::ifstream
input(
path, std::ios::binary | std::ios::ate);
132 if (!
input)
throw std::runtime_error(
"cannot open model: " +
path);
133 const std::streamsize
size =
input.tellg();
134 if (
size <= 0)
throw std::runtime_error(
"model is empty: " +
path);
135 input.seekg(0, std::ios::beg);
136 std::vector<uint8_t>
bytes(
static_cast<size_t>(
size));
138 throw std::runtime_error(
"cannot read model: " +
path);
141 handle.owned.reset(models->newModelData(&
source, std::filesystem::path(
path).extension().string()));
162 std::ostringstream o;
163 o <<
"{\"schema\":\"eve.house-layout\",\"version\":1,\"seed\":" <<
seed <<
",\"moduleSize\":" <<
moduleSize
166 <<
",\"footprintDepth\":" <<
footprintDepth <<
",\"footprintMask\":\"";
168 o <<
"\",\"instances\":[";
169 for (
size_t i = 0; i <
instances.size(); ++i) {
const auto &
v =
instances[i];
if (i) o <<
','; o <<
"{\"componentId\":\"" << esc(
v.componentId) <<
"\",\"x\":" <<
v.x <<
",\"y\":" <<
v.y <<
",\"z\":" <<
v.z <<
",\"rotationDeg\":" <<
v.rotationDeg <<
'}'; }
170 o <<
"],\"rooms\":[";
171 for (
size_t i = 0; i <
rooms.size(); ++i) {
const auto &
v =
rooms[i];
if (i) o <<
','; o <<
"{\"type\":\"" << esc(
v.type) <<
"\",\"x\":" <<
v.x <<
",\"y\":" <<
v.y <<
",\"z\":" <<
v.z <<
",\"width\":" <<
v.width <<
",\"depth\":" <<
v.depth <<
'}'; }
172 o <<
"],\"diagnostics\":[";
173 for (
size_t i = 0; i <
diagnostics.size(); ++i) {
if (i) o <<
','; o <<
'"' << esc(
diagnostics[i]) <<
'"'; }
180 std::string parseError;
190 const std::string schema = o.getString(
"schema",
"eve.house-layout");
191 const int version = o.getInt(
"version", 1);
192 if (schema !=
"eve.house-layout" || version != 1)
194 "unsupported house layout schema or version",
"schema",
195 {},
"housegen.layout"));
198 parsed.
seed =
static_cast<unsigned>(o.getInt(
"seed", 1));
199 parsed.
moduleSize = o.getFloat(
"moduleSize", 1.f);
200 parsed.
floorHeight = o.getFloat(
"floorHeight", 3.f);
201 parsed.
footprintStyle = o.getString(
"footprintStyle",
"rectangle");
202 parsed.
roofStyle = o.getString(
"roofStyle",
"gable");
203 parsed.
entranceSide = o.getString(
"entranceSide",
"north");
206 const std::string footprintBits = o.getString(
"footprintMask",
"");
210 "invalid canonical footprint mask",
"footprintMask",
211 {},
"housegen.layout"));
212 for (
const char bit : footprintBits) {
213 if (bit !=
'0' && bit !=
'1')
215 "footprint mask must contain only 0 or 1",
216 "footprintMask", {},
"housegen.layout"));
229 for (
size_t i = 0; i <
instances.size(); ++i) {
232 if (!
v.has(
"componentId") || !
v.has(
"x") || !
v.has(
"y") || !
v.has(
"z"))
234 "instance needs componentId, x, y and z",
235 "instances", {},
"housegen.layout"));
236 parsed.
instances.push_back({
v.getString(
"componentId"),
v.getInt(
"x"),
v.getInt(
"y"),
237 v.getInt(
"z"),
v.getInt(
"rotationDeg", 0)});
241 for (
size_t i = 0; i <
rooms.size(); ++i) {
243 if (!
v.has(
"type") || !
v.has(
"x") || !
v.has(
"y") || !
v.has(
"width") || !
v.has(
"depth"))
245 "room needs type, x, y, width and depth",
"rooms",
246 {},
"housegen.layout"));
247 parsed.
rooms.push_back({
v.getString(
"type"),
v.getInt(
"x"),
v.getInt(
"y"),
248 v.getInt(
"z", 0),
v.getInt(
"width"),
v.getInt(
"depth")});
251 parsed.
diagnostics = o.getStringArray(
"diagnostics");
252 *
this = std::move(parsed);
264 "house layout has no canonical footprint mask",
265 "footprintMask", {},
"housegen.layout"));
271 instance.componentId, {},
"housegen.layout"));
273 if (room.x < 0 || room.y < 0 || room.width <= 0 || room.depth <= 0 || room.x + room.width >
footprintWidth ||
276 "room lies outside the canonical footprint",
277 "rooms", {},
"housegen.layout"));
278 for (
int y = room.y;
y < room.y + room.depth; ++
y)
279 for (
int x = room.x;
x < room.x + room.width; ++
x)
282 "room contains an inactive footprint cell",
283 "rooms", {},
"housegen.layout"));
285 std::unordered_set<std::string>
occupied;
286 std::unordered_set<std::string> floorCells, roofCells;
287 std::vector<std::tuple<int, int, int>> floors;
288 auto cellKey = [](
int x,
int y,
int z) {
289 return std::to_string(
x) +
":" + std::to_string(
y) +
":" + std::to_string(
z);
291 bool entrance =
false, roof =
false;
293 const auto component = library.
find(i.componentId);
296 "unknown component: " + i.componentId,
297 "componentId", {},
"housegen.layout"));
299 const int normalizedRotation = (i.rotationDeg % 360 + 360) % 360;
300 if (normalizedRotation % 90 != 0)
303 const bool rotationAllowed = std::any_of(
c.rotations.begin(),
c.rotations.end(), [&](
int allowed) {
304 return (allowed % 360 + 360) % 360 == normalizedRotation;
306 if (!rotationAllowed)
308 "component does not allow the requested rotation",
309 "rotationDeg", {},
"housegen.layout"));
310 const bool quarter = (normalizedRotation / 90) % 2 != 0;
311 const int w = quarter ?
c.depth :
c.width,
d = quarter ?
c.width :
c.depth;
312 for (
int y = 0;
y <
d; ++
y)
for (
int x = 0;
x <
w; ++
x) {
314 const bool orientedBoundary =
c.category ==
"wall" ||
c.category ==
"door" ||
315 c.category ==
"interior_wall" ||
c.category ==
"interior_door";
316 const std::string orientation =
317 orientedBoundary ?
":" + std::to_string((i.rotationDeg % 360 + 360) % 360) :
"";
318 const std::string
key = std::to_string(i.x +
x) +
":" + std::to_string(i.y +
y) +
":" +
319 std::to_string(i.z) +
":" +
c.category + orientation;
322 "overlapping " +
c.category +
" components",
323 {}, {},
"housegen.layout"));
324 if (
c.category ==
"floor") {
325 floorCells.insert(cellKey(i.x +
x, i.y +
y, i.z));
326 floors.emplace_back(i.x +
x, i.y +
y, i.z);
327 }
else if (
c.category ==
"roof") {
328 roofCells.insert(cellKey(i.x +
x, i.y +
y, i.z));
331 entrance = entrance || (
c.category ==
"door" && i.z == 0);
332 roof = roof ||
c.category ==
"roof";
336 "house has no entrance", {}, {},
"housegen.layout"));
339 "house has no roof", {}, {},
"housegen.layout"));
340 for (
const auto &[
x,
y,
z] : floors) {
341 if (
z > 0 && !floorCells.contains(cellKey(
x,
y,
z - 1))) {
343 "upper floor has no structural support", {}, {},
346 if (!floorCells.contains(cellKey(
x,
y,
z + 1)) &&
347 !roofCells.contains(cellKey(
x,
y,
z + 1))) {
361 if (instance.x < 0 || instance.y < 0 || instance.z < 0)
364 instance.componentId, {},
"housegen.layout"));
369 "house layout has no canonical footprint mask",
370 "footprintMask", {},
"housegen.layout"));
381 candidate.
setDetail(instance.x, instance.y,
382 std::min(255, std::max(candidate.
getDetail(instance.x, instance.y), instance.z + 1)));
384 candidate.
setMeta(
"domain",
"house.footprint");
389 out = std::move(candidate);
401 "house layout has no canonical footprint mask",
402 "footprintMask", {},
"housegen.layout"));
404 const int rotation = (instance.rotationDeg % 360 + 360) % 360;
405 if (instance.x < 0 || instance.y < 0 || instance.z < 0 || instance.x >=
footprintWidth ||
409 "invalid house component point placement",
410 instance.componentId, {},
"housegen.layout"));
414 std::uint64_t ordinal = 0;
415 const std::uint64_t pointNamespace = (std::uint64_t(
seed) << 32u) | 0x484f5553u;
417 const int index = candidate.
add(
float(instance.x) * moduleSize,
float(instance.z) * floorHeight,
418 float(instance.y) * moduleSize);
419 candidate.
setYaw(
index,
float(instance.rotationDeg));
421 candidate.
setBounds(
index, 0.f, 0.f, 0.f, moduleSize, floorHeight, moduleSize);
433 out = std::move(candidate);
439 const auto valid = validate(library);
441 std::vector<ecs::EntityHandle>
entities;
442 const auto destroyCreated = [&
entities]()
noexcept {
444 if (
auto *entity = ecs::try_get(
handle)) ecs::DestroyEntity(entity);
448 std::unordered_map<std::string, ModelHandle> data;
454 float r = 1.f,
g = 1.f,
b = 1.f,
a = 1.f;
456 float parallaxScale = 0.f, parallaxMinLayers = 8.f, parallaxMaxLayers = 32.f;
457 float cellBombScale = 4.f, cellBombStrength = 0.f, cellBombRotation = 1.f;
459 std::unordered_map<std::string, std::vector<CachedPart>>
parts;
462 const auto component = library.
find(i.componentId);
467 "componentId", {},
"housegen.layout"));
471 auto modelIt = data.find(
c.modelPath);
472 if (modelIt == data.end()) modelIt = data.emplace(
c.modelPath, loadModel(&models,
c.modelPath)).first;
476 auto &cached =
parts[
c.id];
477 if (cached.empty()) {
479 if (!
scene || !
scene->mRootNode)
throw std::runtime_error(
"model has no scene root: " +
c.modelPath);
480 std::function<void(
const aiNode *,
const aiMatrix4x4 &)> walk =
481 [&](
const aiNode *
node,
const aiMatrix4x4 &
parent) {
483 for (
unsigned ni = 0; ni <
node->mNumMeshes; ++ni) {
484 const unsigned mi =
node->mMeshes[ni];
485 if (mi >=
scene->mNumMeshes || !
scene->mMeshes[mi])
continue;
491 aiColor4D
color(1.f, 1.f, 1.f, 1.f);
495 material->Get(AI_MATKEY_METALLIC_FACTOR, part.metallic);
496 material->Get(AI_MATKEY_ROUGHNESS_FACTOR, part.roughness);
498 aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE);
499 part.normalTexture = assimpTexture(&gfx,
scene,
material,
c.modelPath,
500 aiTextureType_NORMALS, aiTextureType_NORMAL_CAMERA);
501 part.heightTexture = assimpTexture(&gfx,
scene,
material,
c.modelPath,
502 aiTextureType_HEIGHT, aiTextureType_DISPLACEMENT);
504 if (
c.material.hasBaseColor) {
505 part.r =
c.material.baseColorR;
506 part.g =
c.material.baseColorG;
507 part.b =
c.material.baseColorB;
508 part.a =
c.material.baseColorA;
510 if (!
c.material.baseColorTexture.empty())
511 part.texture = textureFromFile(&gfx,
c.modelPath,
c.material.baseColorTexture);
512 if (!
c.material.normalTexture.empty())
513 part.normalTexture = textureFromFile(&gfx,
c.modelPath,
c.material.normalTexture);
514 if (!
c.material.heightTexture.empty())
515 part.heightTexture = textureFromFile(&gfx,
c.modelPath,
c.material.heightTexture);
516 if (
c.material.hasMetallic) part.metallic =
c.material.metallic;
517 if (
c.material.hasRoughness) part.roughness =
c.material.roughness;
518 part.parallaxScale =
c.material.parallaxScale;
519 part.parallaxMinLayers =
c.material.parallaxMinLayers;
520 part.parallaxMaxLayers =
c.material.parallaxMaxLayers;
521 part.cellBombScale =
c.material.cellBombScale;
522 part.cellBombStrength =
c.material.cellBombStrength;
523 part.cellBombRotation =
c.material.cellBombRotation;
524 cached.push_back(part);
526 for (
unsigned child = 0; child <
node->mNumChildren; ++child)
529 walk(
scene->mRootNode, aiMatrix4x4());
531 for (
const auto &part : cached) {
532 auto *e = graphics::Renderable3D::create();
537 {},
"housegen.layout"));
539 e->setMesh(part.mesh);
540 e->setPosition(i.x * moduleSize, i.z * floorHeight, i.y * moduleSize);
541 e->setYaw(
float(i.rotationDeg) * 3.14159265358979323846f / 180.f);
542 e->setTint(part.r, part.g, part.b, part.a);
543 if (part.texture) e->setTexture(part.texture);
544 if (part.normalTexture) e->setNormalTexture(part.normalTexture);
545 if (part.heightTexture) e->setHeightTexture(part.heightTexture);
546 e->setMetallic(part.metallic);
547 e->setRoughness(part.roughness);
548 e->setTexCellBomb(part.cellBombScale, part.cellBombStrength,
549 part.cellBombRotation);
550 if (part.heightTexture && part.parallaxScale > 0.f)
551 e->setParallax(part.parallaxScale, part.parallaxMinLayers,
552 part.parallaxMaxLayers);
553 entities.push_back(ecs::handle_of(e));
556 }
catch (
const std::exception &e) {
std::vector< eve::artifact::PartView > parts
std::vector< BuildingInstanceSnapshot > instances
wgpu::PopErrorScopeStatus status
std::array< float, 4 > rotation
std::vector< ecs::EntityHandle > entities
std::weak_ptr< PrimitiveScene > scene
const UnitySourceAsset & source
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
eve::Result< ResourcePin > pin(Resource *resource)
Keeps one cached resource alive across unload()/clear()/reload.
static ResourceManager & getInstance()
Returns the instance.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
The canonical owning dynamic value used by data-facing protocols.
bool isObject() const noexcept
Return true when this value is an object.
Value & at(std::size_t index)
Return a bounds-checked array element.
virtual Mesh * newMeshFromAssimp(const ::aiMesh &mesh)=0
Creates a mesh from assimp. @ownership Caller deletes unless documented otherwise.
GPU mesh handle (+ optional CPU morph targets).
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
房屋组件注册表(按 id 索引,支持分类/风格查询)。
eve::OptionalRef< const HouseComponent > find(std::string_view id) const
按 id 查询组件;缺失是正常查询结果。
一次生成的房屋布局:实例 + 房间 + 元信息,可 JSON 序列化 / 实例化。
std::string footprintStyle
布局风格结果。
eve::Result< void > validate(const HouseComponentLibrary &library) const
校验布局是否满足组件库规则。
std::string toJson() const
Serialize or restore the eve.house-layout version 1 JSON schema.
std::vector< HouseRoom > rooms
std::vector< uint8_t > footprintMask
eve::Result< void > writeComponentPoints(procgen::PointSet &out) const
Export component placements as canonical attributed procgen points.
eve::Result< void > fromJson(std::string_view json)
From json.
int footprintWidth
Canonical base-floor occupancy mask owned by this layout.
eve::Result< void > writeFootprintGrid(procgen::Grid2D &out) const
Export the generated footprint as the canonical procgen grid value.
std::vector< std::string > diagnostics
std::vector< HouseInstance > instances
组件实例 / 房间 / 诊断信息。
EVENGINE_API_FOUNDATION public API.
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
EVENGINE_API_FOUNDATION public API.
Resource module for decoding 3D models via medialoader (Assimp). Produces ModelData; GPU upload is gr...
CPU-side decoded 3D model (Assimp scene owned via medialoader::ModelScene). Does not upload to GPU — ...
Intermediate 2D generation result. cells store semantic ids (see Semantic.h), not tile GIDs — convert...
void setDetail(int x, int y, int value)
Per-cell detail layer (0..255), parallel to cells. Semantics stay in cells (palette/GID compatible); ...
int getDetail(int x, int y) const
Returns the detail.
void resize(int width, int height)
Resize.
void fill(int semantic)
Fill.
void setCell(int x, int y, int semantic)
Sets the cell.
void setMeta(const std::string &key, const std::string &value)
Sets the meta.
Script-friendly collection of attributed 3D samples.
Result< void > trySetPointId(int index, std::uint64_t id)
Assign a unique non-zero point identity without mutating on failure.
int add(float x, float y, float z)
Result< void > trySetStringAttribute(int index, const std::string &name, const std::string &value)
Canonical checked string metadata write.
void setBounds(int index, float minX, float minY, float minZ, float maxX, float maxY, float maxZ)
Set local-space point bounds before scale and rotation are applied.
void setYaw(int index, float yaw)
Sets the yaw.
void setPointSeed(int index, uint32_t seed)
Sets the point seed.
Result< void > trySetIntAttribute(int index, const std::string &name, std::int64_t value)
Canonical checked signed integer metadata write.
void reserve(std::size_t count)
Reserve point storage without changing point or attribute row counts.
RAII keep-alive for one RuntimeObjectRegistry entry.
T * get() const noexcept
Borrows the pinned object; valid until the pin is released.
std::uint64_t derivePointId(std::uint64_t namespaceId, std::uint64_t ordinal)
Deterministically derive a non-zero stable point identity.
std::unique_ptr< T > Owned
C++ owning storage used by an object factory.
static TextureCreateInfo withMipmaps(bool aniso=true, float maxAniso=16.f)
With mipmaps.