12#include <assimp/material.h>
13#include <assimp/matrix4x4.h>
14#include <assimp/scene.h>
15#include <assimp/texture.h>
27#include <unordered_map>
28#include <unordered_set>
32std::string esc(
const std::string &
v) { std::string o;
for (
char c :
v) {
if (
c ==
'\\' ||
c ==
'"') o +=
'\\'; o +=
c; }
return o; }
34graphics::Texture *textureFromEmbedded(graphics::Graphics *gfx,
const aiTexture *source) {
35 if (!source || !source->pcData)
return nullptr;
36 if (source->mHeight == 0) {
37 data::ByteData bytes(source->pcData,
static_cast<size_t>(source->mWidth));
38 std::unique_ptr<image::ImageData> decoded(image::Image::create()->newImageData(&bytes));
41 std::vector<uint8_t> rgba(
size_t(source->mWidth) *
size_t(source->mHeight) * 4);
42 for (
size_t pixel = 0; pixel < size_t(source->mWidth) * size_t(source->mHeight); ++pixel) {
43 rgba[pixel * 4 + 0] = source->pcData[pixel].r;
44 rgba[pixel * 4 + 1] = source->pcData[pixel].g;
45 rgba[pixel * 4 + 2] = source->pcData[pixel].b;
46 rgba[pixel * 4 + 3] = source->pcData[pixel].a;
48 return gfx->newTexture(
int(source->mWidth), int(source->mHeight), rgba.data(),
52graphics::Texture *textureFromFile(graphics::Graphics *gfx,
const std::string &modelPath,
53 const std::string &texturePath) {
54 if (texturePath.empty())
return nullptr;
55 std::filesystem::path resolved(texturePath);
56 if (resolved.is_relative()) resolved = std::filesystem::path(modelPath).parent_path() / resolved;
58 if (std::filesystem::is_regular_file(resolved)) {
59 std::ifstream input(resolved, std::ios::binary | std::ios::ate);
60 const std::streamsize size = input.tellg();
61 if (size <= 0)
return nullptr;
62 input.seekg(0, std::ios::beg);
63 std::vector<uint8_t> bytes(
static_cast<size_t>(size));
64 if (!input.read(
reinterpret_cast<char *
>(bytes.data()), size))
return nullptr;
65 data::ByteData source(bytes.data(), bytes.size());
66 std::unique_ptr<image::ImageData> decoded(
67 image::Image::create()->newImageData(&source));
70 return gfx->newTextureFromFile(resolved.lexically_normal().string());
76graphics::Texture *assimpTexture(graphics::Graphics *gfx,
const aiScene *scene,
77 const aiMaterial *
material,
const std::string &modelPath,
78 aiTextureType primary, aiTextureType fallback) {
79 if (!scene || !
material)
return nullptr;
81 if (
material->GetTexture(primary, 0, &texturePath) != AI_SUCCESS &&
82 material->GetTexture(fallback, 0, &texturePath) != AI_SUCCESS)
84 const std::string path = texturePath.C_Str();
85 if (path.empty())
return nullptr;
86 if (path.front() ==
'*') {
87 const int index = std::atoi(path.c_str() + 1);
88 if (index < 0 ||
static_cast<unsigned>(index) >= scene->mNumTextures)
return nullptr;
90 return textureFromEmbedded(gfx, scene->mTextures[index]);
95 return textureFromFile(gfx, modelPath, path);
99 if (!std::filesystem::is_regular_file(std::filesystem::path(path)))
100 return models->newModelDataFromFile(path);
101 std::ifstream input(path, std::ios::binary | std::ios::ate);
102 if (!input)
throw std::runtime_error(
"cannot open model: " + path);
103 const std::streamsize size = input.tellg();
104 if (size <= 0)
throw std::runtime_error(
"model is empty: " + path);
105 input.seekg(0, std::ios::beg);
106 std::vector<uint8_t> bytes(
static_cast<size_t>(size));
107 if (!input.read(
reinterpret_cast<char *
>(bytes.data()), size))
108 throw std::runtime_error(
"cannot read model: " + path);
109 data::ByteData source(bytes.data(), bytes.size());
110 return models->newModelData(&source, std::filesystem::path(path).extension().
string());
117 std::ostringstream o;
120 <<
"\",\"entranceSide\":\"" << esc(
entranceSide) <<
"\",\"instances\":[";
121 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 <<
'}'; }
122 o <<
"],\"rooms\":[";
123 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 <<
",\"width\":" <<
v.width <<
",\"depth\":" <<
v.depth <<
'}'; }
124 o <<
"],\"diagnostics\":[";
125 for (
size_t i = 0; i <
diagnostics.size(); ++i) {
if (i) o <<
','; o <<
'"' << esc(
diagnostics[i]) <<
'"'; }
133 if (!doc.
valid())
return false;
134 const Value o = doc.
root();
135 if (!o.isObject()) {
if (
error) *
error =
"layout must be an object";
return false; }
138 parsed.
seed =
static_cast<unsigned>(o.getInt(
"seed", 1));
139 parsed.
moduleSize = o.getFloat(
"moduleSize", 1.f);
140 parsed.
floorHeight = o.getFloat(
"floorHeight", 3.f);
141 parsed.
footprintStyle = o.getString(
"footprintStyle",
"rectangle");
142 parsed.
roofStyle = o.getString(
"roofStyle",
"gable");
143 parsed.
entranceSide = o.getString(
"entranceSide",
"north");
145 const Value
instances = o.get(
"instances");
147 for (
size_t i = 0; i <
instances.size(); ++i) {
150 if (!
v.has(
"componentId") || !
v.has(
"x") || !
v.has(
"y") || !
v.has(
"z")) {
151 if (
error) *
error =
"instance needs componentId, x, y and z";
154 parsed.
instances.push_back({
v.getString(
"componentId"),
v.getInt(
"x"),
v.getInt(
"y"),
155 v.getInt(
"z"),
v.getInt(
"rotationDeg", 0)});
158 const Value
rooms = o.get(
"rooms");
159 for (
size_t i = 0; i <
rooms.size(); ++i) {
160 const Value
v =
rooms.at(i);
161 if (!
v.has(
"type") || !
v.has(
"x") || !
v.has(
"y") || !
v.has(
"width") || !
v.has(
"depth")) {
162 if (
error) *
error =
"room needs type, x, y, width and depth";
165 parsed.
rooms.push_back({
v.getString(
"type"),
v.getInt(
"x"),
v.getInt(
"y"),
166 v.getInt(
"width"),
v.getInt(
"depth")});
169 parsed.
diagnostics = o.getStringArray(
"diagnostics");
170 *
this = std::move(parsed);
175 std::unordered_set<std::string> occupied;
176 std::unordered_set<std::string> floorCells, roofCells;
177 std::vector<std::tuple<int, int, int>> floors;
178 auto cellKey = [](
int x,
int y,
int z) {
179 return std::to_string(
x) +
":" + std::to_string(
y) +
":" + std::to_string(
z);
181 bool entrance =
false, roof =
false;
183 const auto *
c = library.
find(i.componentId);
184 if (!
c) {
if (
error) *
error =
"unknown component: " + i.componentId;
return false; }
185 if (i.rotationDeg % 90 != 0) {
if (
error) *
error =
"non-cardinal rotation";
return false; }
186 const bool quarter = (i.rotationDeg / 90) % 2 != 0;
187 const int w = quarter ?
c->
depth :
c->width,
d = quarter ?
c->width :
c->depth;
188 for (
int y = 0;
y <
d; ++
y)
for (
int x = 0;
x <
w; ++
x) {
190 const std::string orientation = (
c->category ==
"wall" ||
c->category ==
"door")
191 ?
":" + std::to_string((i.rotationDeg % 360 + 360) % 360)
193 const std::string key = std::to_string(i.x +
x) +
":" + std::to_string(i.y +
y) +
":" + std::to_string(i.z) +
":" +
c->category + orientation;
194 if (!occupied.insert(key).second) {
if (
error) *
error =
"overlapping " +
c->category +
" components";
return false; }
195 if (
c->category ==
"floor") {
196 floorCells.insert(cellKey(i.x +
x, i.y +
y, i.z));
197 floors.emplace_back(i.x +
x, i.y +
y, i.z);
198 }
else if (
c->category ==
"roof") {
199 roofCells.insert(cellKey(i.x +
x, i.y +
y, i.z));
202 entrance = entrance || (
c->category ==
"door" && i.z == 0);
203 roof = roof ||
c->category ==
"roof";
205 if (!entrance) {
if (
error) *
error =
"house has no entrance";
return false; }
206 if (!roof) {
if (
error) *
error =
"house has no roof";
return false; }
207 for (
const auto &[
x,
y,
z] : floors) {
208 if (
z > 0 && !floorCells.contains(cellKey(
x,
y,
z - 1))) {
209 if (
error) *
error =
"upper floor has no structural support";
212 if (!floorCells.contains(cellKey(
x,
y,
z + 1)) &&
213 !roofCells.contains(cellKey(
x,
y,
z + 1))) {
214 if (
error) *
error =
"floor cell has no roof coverage";
222 std::vector<graphics::Renderable3D *> entities;
223 if (!gfx || !models) {
if (
error) *
error =
"graphics and model3d are required";
return entities; }
224 std::unordered_map<std::string, eve::ref<model3d::ModelData>>
data;
230 float r = 1.f, g = 1.f,
b = 1.f,
a = 1.f;
232 float parallaxScale = 0.f, parallaxMinLayers = 8.f, parallaxMaxLayers = 32.f;
233 float cellBombScale = 4.f, cellBombStrength = 0.f, cellBombRotation = 1.f;
235 std::unordered_map<std::string, std::vector<CachedPart>> parts;
238 const auto *
c = library.
find(i.componentId);
if (!
c)
continue;
240 auto modelIt =
data.find(
c->modelPath);
241 if (modelIt ==
data.end())
242 modelIt =
data.emplace(
c->modelPath, loadModel(models,
c->modelPath)).first;
246 auto &cached = parts[
c->id];
247 if (cached.empty()) {
249 if (!scene || !scene->mRootNode)
throw std::runtime_error(
"model has no scene root: " +
c->modelPath);
250 std::function<void(
const aiNode *,
const aiMatrix4x4 &)> walk =
251 [&](
const aiNode *node,
const aiMatrix4x4 &
parent) {
252 const aiMatrix4x4 world =
parent * node->mTransformation;
253 for (
unsigned ni = 0; ni < node->mNumMeshes; ++ni) {
254 const unsigned mi = node->mMeshes[ni];
255 if (mi >= scene->mNumMeshes || !scene->mMeshes[mi])
continue;
256 const aiMesh *source = scene->mMeshes[mi];
259 if (scene->mMaterials && source->mMaterialIndex < scene->mNumMaterials) {
260 const aiMaterial *
material = scene->mMaterials[source->mMaterialIndex];
261 aiColor4D
color(1.f, 1.f, 1.f, 1.f);
265 material->Get(AI_MATKEY_METALLIC_FACTOR, part.metallic);
266 material->Get(AI_MATKEY_ROUGHNESS_FACTOR, part.roughness);
267 part.texture = assimpTexture(gfx, scene,
material,
c->modelPath,
268 aiTextureType_BASE_COLOR,
269 aiTextureType_DIFFUSE);
270 part.normalTexture = assimpTexture(gfx, scene,
material,
c->modelPath,
271 aiTextureType_NORMALS,
272 aiTextureType_NORMAL_CAMERA);
273 part.heightTexture = assimpTexture(gfx, scene,
material,
c->modelPath,
274 aiTextureType_HEIGHT,
275 aiTextureType_DISPLACEMENT);
277 if (
c->material.hasBaseColor) {
278 part.r =
c->material.baseColorR; part.g =
c->material.baseColorG;
279 part.b =
c->material.baseColorB; part.a =
c->material.baseColorA;
281 if (!
c->material.baseColorTexture.empty())
282 part.texture = textureFromFile(gfx,
c->modelPath,
283 c->material.baseColorTexture);
284 if (!
c->material.normalTexture.empty())
285 part.normalTexture = textureFromFile(gfx,
c->modelPath,
286 c->material.normalTexture);
287 if (!
c->material.heightTexture.empty())
288 part.heightTexture = textureFromFile(gfx,
c->modelPath,
289 c->material.heightTexture);
290 if (
c->material.hasMetallic) part.metallic =
c->material.metallic;
291 if (
c->material.hasRoughness) part.roughness =
c->material.roughness;
292 part.parallaxScale =
c->material.parallaxScale;
293 part.parallaxMinLayers =
c->material.parallaxMinLayers;
294 part.parallaxMaxLayers =
c->material.parallaxMaxLayers;
295 part.cellBombScale =
c->material.cellBombScale;
296 part.cellBombStrength =
c->material.cellBombStrength;
297 part.cellBombRotation =
c->material.cellBombRotation;
298 cached.push_back(part);
300 for (
unsigned child = 0; child < node->mNumChildren; ++child)
301 walk(node->mChildren[child], world);
303 walk(scene->mRootNode, aiMatrix4x4());
305 for (
const auto &part : cached) {
306 auto *e = graphics::Renderable3D::create();
307 e->setMesh(part.mesh);
309 e->setYaw(
float(i.rotationDeg) * 3.14159265358979323846f / 180.f);
310 e->setTint(part.r, part.g, part.b, part.a);
311 if (part.texture) e->setTexture(part.texture);
312 if (part.normalTexture) e->setNormalTexture(part.normalTexture);
313 if (part.heightTexture) e->setHeightTexture(part.heightTexture);
314 e->setMetallic(part.metallic);
315 e->setRoughness(part.roughness);
316 e->setTexCellBomb(part.cellBombScale, part.cellBombStrength,
317 part.cellBombRotation);
318 if (part.heightTexture && part.parallaxScale > 0.f)
319 e->setParallax(part.parallaxScale, part.parallaxMinLayers,
320 part.parallaxMaxLayers);
321 entities.push_back(e);
324 }
catch (
const std::exception &e) {
if (
error) *
error = e.what(); }
image::ImageData::Colorf color
virtual Mesh * newMeshFromAssimp(const ::aiMesh &mesh)=0
GPU mesh handle (+ optional CPU morph targets).
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
房屋组件注册表(按 id 索引,支持分类/风格查询)。
const HouseComponent * find(const std::string &id) const
按 id 查询组件。
一次生成的房屋布局:实例 + 房间 + 元信息,可 JSON 序列化 / 实例化。
std::string footprintStyle
布局风格结果。
std::string toJson() const
序列化为 JSON / 从 JSON 恢复。
std::vector< HouseRoom > rooms
bool validate(const HouseComponentLibrary &library, std::string *error=nullptr) const
校验布局是否满足组件库规则。
std::vector< graphics::Renderable3D * > instantiate(graphics::Graphics *gfx, model3d::Model3D *models, const HouseComponentLibrary &library, std::string *error=nullptr) const
把布局实例化为场景中的 Renderable3D(调用方持有)。
bool fromJson(const std::string &json, std::string *error=nullptr)
std::vector< std::string > diagnostics
std::vector< HouseInstance > instances
组件实例 / 房间 / 诊断信息。
static Document parse(const std::string &text, std::string *error=nullptr)
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 — ...
const aiScene * getScene() const
ref is a smart pointer that automatically calls ref() and unref() on the object. Type T must be a sub...
static TextureCreateInfo withMipmaps(bool aniso=true, float maxAniso=16.f)