13#include <assimp/GltfMaterial.h>
14#include <assimp/material.h>
15#include <assimp/matrix4x4.h>
16#include <assimp/mesh.h>
17#include <assimp/scene.h>
28#include <unordered_map>
40Texture *textureFromImageData(IResourceFactory *gfx, image::ImageData *img) {
41 if (!img)
return nullptr;
43 return gfx->newTexture(img);
49std::string importedTextureContentKey(
const image::ImageData &image) {
52 constexpr uint64_t prime = 1099511628211ull;
53 uint64_t
a = 14695981039346656037ull;
54 uint64_t
b = 7809847782465536322ull;
55 const auto mix = [&](uint64_t &
hash, uint8_t byte) {
59 const auto feed32 = [&](uint64_t &
hash, uint32_t
value) {
60 for (
unsigned shift = 0; shift < 32; shift += 8) mix(
hash, uint8_t(
value >> shift));
62 feed32(
a, uint32_t(
image.getWidth()));
63 feed32(
a, uint32_t(
image.getHeight()));
64 feed32(
b, uint32_t(
image.getHeight()));
65 feed32(
b, uint32_t(
image.getWidth()));
66 const auto *
bytes =
static_cast<const uint8_t *
>(
image.getData());
67 const size_t count = size_t(
image.getWidth()) * size_t(
image.getHeight()) * 4;
68 for (
size_t i = 0; i <
count; ++i) {
72 return "model3d-rgba8:" + std::to_string(
image.getWidth()) +
"x" + std::to_string(
image.getHeight()) +
":" +
73 std::to_string(
a) +
":" + std::to_string(
b);
76Texture *loadEmbeddedTexture(IResourceFactory *gfx, ModelData *
model,
int idx) {
77 image::ImageData *img =
model->getEmbeddedTextureImageData(
idx);
81 auto shared = gfx->newSharedTexture(img, importedTextureContentKey(*img));
82 if (!shared)
throw eve::Exception(
"%s", shared.status().describe().c_str());
83 tex = &shared.value().get();
91std::string basenameOf(
const std::string &
path) {
92 const size_t slash =
path.find_last_of(
"/\\");
93 return slash == std::string::npos ?
path :
path.substr(slash + 1);
96Texture *loadExternalTexture(IResourceFactory *gfx,
const std::string &
path) {
97 auto *fs = filesystem::Filesystem::create();
98 std::unique_ptr<filesystem::FileData> fd;
100 fd.reset(fs->read(
path));
103 if (!fd || fd->getSize() == 0) {
104 const std::string base = basenameOf(
path);
106 fd.reset(fs->read(base));
109 if (!fd || fd->getSize() == 0) {
111 fd.reset(fs->read(
"Textures/" + base));
116 if (!fd || fd->getSize() == 0)
return nullptr;
118 image::ImageData *img = image::Image::create()->newImageData(fd.get());
119 Texture *tex = textureFromImageData(gfx, img);
127Texture* loadModelTexture(IResourceFactory* gfx, ModelData*
model,
const std::string&
path) {
131 if (
uri.rfind(
"file://", 0) == 0)
uri.erase(0, 7);
133 if (!
uri.empty() &&
uri.find(
"://") == std::string::npos) {
137 const auto resolved = (std::filesystem::path(
uri).parent_path() /
relative).lexically_normal();
138 if (
auto* texture = loadExternalTexture(gfx, resolved.generic_string()))
return texture;
140 return loadExternalTexture(gfx,
path);
143Texture* loadTextureSlot(IResourceFactory* gfx, ModelData*
model,
int matIndex,
const std::string&
type) {
144 if (
model->getMaterialTextureSlotCount(matIndex,
type) <= 0)
return nullptr;
145 const int embedded =
model->getMaterialTextureEmbeddedIndex(matIndex,
type, 0);
146 if (embedded >= 0)
return loadEmbeddedTexture(gfx,
model, embedded);
147 const std::string
path =
model->getMaterialTexturePath(matIndex,
type, 0);
148 if (
path.empty())
return nullptr;
149 return loadModelTexture(gfx,
model,
path);
166void extendedLook(IResourceFactory* gfx, ModelData*
model,
int index, TextureLook& look,
167 const ModelRenderOptions&
options) {
172 if (
material->Get(AI_MATKEY_GLTF_ALPHAMODE, alpha) != AI_SUCCESS)
return;
173 look.extendedPbr =
true;
174 look.ta =
model->getMaterialBaseColorA(
index);
176 auto scalar = [&](
const char*
key,
unsigned type,
unsigned slot,
float fallback) {
177 float value = fallback;
182 auto unit = [&](
const char*
key,
unsigned type,
unsigned slot,
float fallback) {
184 if (value < 0 || value > 1)
185 std::fprintf(stderr,
"[model3d] warning %s=%g clamped to [0,1]\n",
key,
double(
value));
186 return std::clamp(
value, 0.f, 1.f);
188 p.specularFactor =
unit(AI_MATKEY_SPECULAR_FACTOR, 1);
189 aiColor3D
color(1, 1, 1);
195 p.emissiveStrength = scalar(AI_MATKEY_EMISSIVE_INTENSITY, 1);
196 p.ior = scalar(AI_MATKEY_REFRACTI, 1.5f);
197 p.clearcoatFactor =
unit(AI_MATKEY_CLEARCOAT_FACTOR, 0);
198 p.clearcoatRoughness =
unit(AI_MATKEY_CLEARCOAT_ROUGHNESS_FACTOR, 0);
199 p.anisotropyStrength =
unit(AI_MATKEY_ANISOTROPY_FACTOR, 0);
200 p.normalScale = scalar(AI_MATKEY_GLTF_TEXTURE_SCALE(aiTextureType_NORMALS, 0), 1);
201 p.clearcoatNormalScale = scalar(AI_MATKEY_GLTF_TEXTURE_SCALE(aiTextureType_CLEARCOAT, 2), 1);
202 p.occlusionStrength =
unit(
"$tex.file.strength", aiTextureType_LIGHTMAP, 0, 1);
204 material->Get(AI_MATKEY_SHADING_MODEL, shading);
205 p.unlit = shading == aiShadingMode_Unlit;
206 const std::pair<aiTextureType, unsigned> roles[] = {
207 {aiTextureType_BASE_COLOR, 0}, {aiTextureType_UNKNOWN, 0}, {aiTextureType_NORMALS, 0},
208 {aiTextureType_LIGHTMAP, 0}, {aiTextureType_EMISSIVE, 0}, {aiTextureType_SPECULAR, 0},
209 {aiTextureType_SPECULAR, 1}, {aiTextureType_NONE, 0}, {aiTextureType_CLEARCOAT, 0},
210 {aiTextureType_CLEARCOAT, 1}, {aiTextureType_CLEARCOAT, 2}};
211 for (
size_t i = 0; i < 11; i++) {
212 if ((i == 0 && !
options.importAlbedo) || ((i == 2 || i == 10) && !
options.importNormalMaps))
continue;
213 const auto [
type, slot] = roles[i];
214 if (
type == aiTextureType_NONE)
continue;
217 aiTextureMapMode wrap[2] = {aiTextureMapMode_Wrap, aiTextureMapMode_Wrap};
218 if (
material->GetTexture(
type, slot, &
path,
nullptr, &
uv,
nullptr,
nullptr, wrap) != AI_SUCCESS)
continue;
220 if (i == 0 && look.albedo)
222 else if (i == 2 && look.normal)
224 else if (
path.length > 1 &&
path.C_Str()[0] ==
'*')
225 binding.texture = loadEmbeddedTexture(gfx,
model, std::stoi(
path.C_Str() + 1));
231 std::fprintf(stderr,
"[model3d] warning: missing material texture %s (model %s); using material factors\n",
236 auto wrapEnum = [](aiTextureMapMode mode) {
237 return mode == aiTextureMapMode_Clamp ? 33071u : mode == aiTextureMapMode_Mirror ? 33648u : 10497u;
239 binding.wrapS = wrapEnum(wrap[0]);
240 binding.wrapT = wrapEnum(wrap[1]);
247 aiUVTransform transform;
248 if (
material->Get(AI_MATKEY_UVTRANSFORM(
type, slot), transform) == AI_SUCCESS) {
250 if (
model->hasFlippedUvs()) {
251 transform.mRotation = -transform.mRotation;
252 transform.mTranslation.y = -transform.mTranslation.y;
254 const float r = -transform.mRotation,
c = std::cos(
r), sn = std::sin(
r);
255 const float sx = transform.mScaling.x,
sy = transform.mScaling.y;
258 binding.offset = {transform.mTranslation.x - .5f *
sx * (-
c + sn + 1),
259 1 -
sy - transform.mTranslation.y + .5f *
sy * (sn +
c - 1)};
262 look.albedo =
p.textures[0].texture;
263 look.normal =
p.textures[2].texture;
266TextureLook materialLook(IResourceFactory *gfx, ModelData *
model,
int matIndex,
const ModelRenderOptions &
options,
267 std::unordered_map<int, TextureLook> &cache) {
268 auto it = cache.find(matIndex);
269 if (it != cache.end())
return it->second;
273 look.tr =
model->getMaterialBaseColorR(matIndex);
274 look.tg =
model->getMaterialBaseColorG(matIndex);
275 look.tb =
model->getMaterialBaseColorB(matIndex);
276 look.ta =
model->getMaterialBaseColorA(matIndex);
277 look.metallic =
model->getMaterialMetallicFactor(matIndex);
278 look.roughness =
model->getMaterialRoughnessFactor(matIndex);
279 look.ta *=
model->getMaterialOpacity(matIndex);
280 look.alphaMode =
model->getMaterialAlphaMode(matIndex);
281 look.alphaCutoff =
model->getMaterialAlphaCutoff(matIndex);
282 look.doubleSided =
model->getMaterialTwoSided(matIndex);
284 look.albedo = loadTextureSlot(gfx,
model, matIndex,
"base_color");
285 if (!look.albedo) look.albedo = loadTextureSlot(gfx,
model, matIndex,
"diffuse");
288 look.normal = loadTextureSlot(gfx,
model, matIndex,
"normals");
290 look.height = loadTextureSlot(gfx,
model, matIndex,
"height");
293 cache.emplace(matIndex, look);
297Renderable3D *makeRenderable(IResourceFactory *gfx, ModelData *
model,
int meshIndex,
const aiMatrix4x4 &
world,
298 const ModelRenderOptions &
options, std::unordered_map<int, TextureLook> &cache) {
300 if (!scene || meshIndex < 0 ||
static_cast<unsigned>(meshIndex) >=
scene->mNumMeshes)
302 const aiMesh *ai =
scene->mMeshes[meshIndex];
303 if (!ai || ai->mNumVertices == 0 || ai->mNumFaces == 0)
return nullptr;
310 const bool bakeWorld =
options.bakeWorldTransform && !ai->HasBones();
311 Mesh *
mesh = bakeWorld ? gfx->newMeshFromAssimp(*ai,
world) : gfx->newMeshFromAssimp(*ai);
312 if (!
mesh)
return nullptr;
314 const int matIndex =
model->getMaterialIndex(meshIndex);
315 const TextureLook look = materialLook(gfx,
model, matIndex,
options, cache);
317 Renderable3D *ent = Renderable3D::create();
318 ent->meshRenderer()->visible =
true;
323 if (look.albedo) ent->setTexture(look.albedo);
324 if (look.normal) ent->setNormalTexture(look.normal);
325 if (look.height) ent->setHeightTexture(look.height);
326 ent->setTint(look.tr, look.tg, look.tb, look.ta);
327 ent->setMetallic(look.metallic);
328 ent->setRoughness(look.roughness);
332 material->setAlbedoTexture(look.albedo);
333 material->setNormalTexture(look.normal);
334 material->setHeightTexture(look.height);
335 material->setTint(look.tr, look.tg, look.tb, look.ta);
336 material->setMetallic(look.metallic);
337 material->setRoughness(look.roughness);
338 material->setDoubleSided(look.doubleSided);
339 material->setAlphaCutoff(look.alphaCutoff);
340 if (look.alphaMode ==
"MASK")
342 else if (look.alphaMode ==
"BLEND")
343 material->setSurfaceMode(
"transparent");
346 if (look.extendedPbr) {
347 auto configured =
material->setPbrSurface(look.pbr);
348 if (!configured)
throw eve::Exception(
"%s", configured.error()->message().c_str());
349 for (
unsigned channel = 0; channel < AI_MAX_NUMBER_OF_TEXTURECOORDS; channel++) {
350 if (!ai->mTextureCoords[channel])
continue;
351 std::vector<float>
values;
352 values.reserve(
size_t(ai->mNumVertices) * 2);
353 for (
unsigned v = 0;
v < ai->mNumVertices;
v++) {
354 values.push_back(ai->mTextureCoords[channel][
v].x);
355 values.push_back(
model->hasFlippedUvs() ? ai->mTextureCoords[channel][
v].y
356 : 1 - ai->mTextureCoords[channel][
v].
y);
358 auto attached =
mesh->setTexcoordSet(channel,
values);
359 if (!attached)
throw eve::Exception(
"%s", attached.error()->message().c_str());
366void walkNodes(
const aiNode *
node,
const aiMatrix4x4 &
parent, IResourceFactory *gfx, ModelData *
model,
367 const ModelRenderOptions &
options, std::vector<Renderable3D *> &out,
368 std::unordered_map<int, TextureLook> &cache) {
371 for (
unsigned i = 0; i <
node->mNumMeshes; ++i) {
374 if (ent) out.push_back(ent);
376 for (
unsigned c = 0;
c <
node->mNumChildren; ++
c)
380bool findMeshTransform(
const aiNode *
node,
const aiMatrix4x4 &
parent,
unsigned meshIndex,
382 if (!
node)
return false;
384 for (
unsigned i = 0; i <
node->mNumMeshes; ++i) {
385 if (
node->mMeshes[i] == meshIndex) {
390 for (
unsigned c = 0;
c <
node->mNumChildren; ++
c)
391 if (findMeshTransform(
node->mChildren[
c],
world, meshIndex, out))
return true;
399 if (!
model)
return nullptr;
401 if (!
scene || !
scene->mRootNode)
return nullptr;
404 if (!findMeshTransform(
scene->mRootNode, aiMatrix4x4(),
static_cast<unsigned>(meshIndex),
406 world = aiMatrix4x4();
408 std::unordered_map<int, TextureLook> cache;
414 std::vector<Renderable3D *> out;
415 if (!
model)
return out;
418 std::unordered_map<int, TextureLook> cache;
425 Mesh *
mesh = renderable.getMesh();
426 auto invalid = [](
const char *
message) {
430 if (!
scene || !
scene->mRootNode || meshIndex < 0 ||
unsigned(meshIndex) >=
scene->mNumMeshes || !
mesh)
431 return invalid(
"model, mesh index, and matching renderable are required");
434 return invalid(
"source and render mesh vertex counts must match");
435 if (
source->HasBones() ||
mesh->hasGpuSkinning())
436 return invalid(
"foliage deformation requires an unskinned static mesh");
439 if (!findMeshTransform(
scene->mRootNode, aiMatrix4x4(),
unsigned(meshIndex),
world))
world = aiMatrix4x4();
442 float minY = std::numeric_limits<float>::infinity();
443 float maxY = -std::numeric_limits<float>::infinity();
444 for (
unsigned i = 0; i <
source->mNumVertices; ++i) {
446 if (!std::isfinite(
p.x) || !std::isfinite(
p.y) || !std::isfinite(
p.z))
447 return invalid(
"foliage source positions must be finite");
449 minY = std::min(minY,
p.y);
450 maxY = std::max(maxY,
p.y);
452 const float height = maxY - minY;
453 if (!std::isfinite(
height) ||
height <= 1e-6f)
return invalid(
"foliage mesh must have positive height");
458 std::vector<float> factors;
459 factors.reserve(
size_t(
source->mNumVertices) * 9u);
460 for (
unsigned i = 0; i <
source->mNumVertices; ++i) {
462 const float normalizedHeight = std::clamp((
p.y - minY) /
height, 0.f, 1.f);
463 const float radial = std::clamp(std::hypot(
p.x,
p.z) /
radius, 0.f, 1.f);
464 const uint32_t
hash = (i + 1u) * 747796405u + 2891336453u;
465 const float variation = float((
hash >> 8u) & 0xffffu) / 65535.f;
466 factors.insert(factors.end(), {0.f, minY, 0.f, normalizedHeight * normalizedHeight, normalizedHeight * radial,
467 normalizedHeight, variation, height, radius});
469 return mesh->adoptVegetationDeformationFactors(std::move(factors));
graphics::Texture * albedo
std::map< std::string, Var > values
std::array< std::uint8_t, 32 > hash
std::vector< float > positions
eve::action::ActionVfxBinding binding
std::weak_ptr< PrimitiveScene > scene
const SquirrelValueOptions & options
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.
EVENGINE_API_FOUNDATION public API.
Move-only operation result carrying either a value or Status.
static Result failure(Status status)
Construct a failed result from a structured status.
Texture / mesh / shader / canvas creation and release.
Packages shading method + surface parameters into one attachable asset.
GPU mesh handle (+ optional CPU morph targets).
EVENGINE_API_BACKENDS public API.
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
CPU-side decoded 3D model (Assimp scene owned via medialoader::ModelScene). Does not upload to GPU — ...
Result< void > prepareFoliageDeformation(ModelData &model, int meshIndex, Renderable3D &renderable)
Renderable3D * buildRenderable(IResourceFactory &gfx, ModelData *model, int meshIndex, const ModelRenderOptions &options)
std::vector< Renderable3D * > buildRenderables(IResourceFactory &gfx, ModelData *model, const ModelRenderOptions &options)
ModelRenderOptions public API.