载入中...
搜索中...
未找到
ModelRenderer.cpp
浏览该文件的文档.
2
3#include "model3d/ModelData.h"
4
8#include "graphics/Material.h"
10#include "graphics/Texture.h"
11#include "image/Image.h"
12
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>
18#include <algorithm>
19#include <cmath>
20#include <cstdio>
21#include "common/Exception.h"
22
23#include <algorithm>
24#include <filesystem>
25#include <limits>
26#include <memory>
27#include <string>
28#include <unordered_map>
29#include <vector>
30
31namespace eve::model3d {
32namespace {
33
39
40Texture *textureFromImageData(IResourceFactory *gfx, image::ImageData *img) {
41 if (!img) return nullptr;
42 try {
43 return gfx->newTexture(img);
44 } catch (...) {
45 return nullptr;
46 }
47}
48
49std::string importedTextureContentKey(const image::ImageData &image) {
50 // Two independently seeded FNV-1a lanes make accidental aliasing of imported
51 // pixels negligible while keeping model3d independent of the data module.
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) {
56 hash ^= byte;
57 hash *= prime;
58 };
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));
61 };
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) {
69 mix(a, bytes[i]);
70 mix(b, bytes[count - i - 1]);
71 }
72 return "model3d-rgba8:" + std::to_string(image.getWidth()) + "x" + std::to_string(image.getHeight()) + ":" +
73 std::to_string(a) + ":" + std::to_string(b);
74}
75
76Texture *loadEmbeddedTexture(IResourceFactory *gfx, ModelData *model, int idx) {
77 image::ImageData *img = model->getEmbeddedTextureImageData(idx);
78 Texture *tex = nullptr;
79 try {
80 if (img) {
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();
84 }
85 } catch (...) {
86 }
87 delete img;
88 return tex;
89}
90
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);
94}
95
96Texture *loadExternalTexture(IResourceFactory *gfx, const std::string &path) {
97 auto *fs = filesystem::Filesystem::create();
98 std::unique_ptr<filesystem::FileData> fd;
99 try {
100 fd.reset(fs->read(path));
101 } catch (...) {
102 }
103 if (!fd || fd->getSize() == 0) {
104 const std::string base = basenameOf(path);
105 try {
106 fd.reset(fs->read(base));
107 } catch (...) {
108 }
109 if (!fd || fd->getSize() == 0) {
110 try {
111 fd.reset(fs->read("Textures/" + base));
112 } catch (...) {
113 }
114 }
115 }
116 if (!fd || fd->getSize() == 0) return nullptr;
117 try {
118 image::ImageData *img = image::Image::create()->newImageData(fd.get());
119 Texture *tex = textureFromImageData(gfx, img);
120 delete img;
121 return tex;
122 } catch (...) {
123 return nullptr;
124 }
125}
126
127Texture* loadModelTexture(IResourceFactory* gfx, ModelData* model, const std::string& path) {
128 // Imported texture paths are relative to the source model, not the VFS root.
129 // Normalize parent segments (OBJ/MTL commonly uses ../textures/foo.png).
130 std::string uri = model->getUri();
131 if (uri.rfind("file://", 0) == 0) uri.erase(0, 7);
132 // ResourceManager cache keys are normalized VFS paths without a scheme.
133 if (!uri.empty() && uri.find("://") == std::string::npos) {
134 uri = uri.substr(0, uri.find('?'));
135 std::string relative = path;
136 std::replace(relative.begin(), relative.end(), '\\', '/');
137 const auto resolved = (std::filesystem::path(uri).parent_path() / relative).lexically_normal();
138 if (auto* texture = loadExternalTexture(gfx, resolved.generic_string())) return texture;
139 }
140 return loadExternalTexture(gfx, path);
141}
142
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);
150}
151
152struct TextureLook {
153 Texture* albedo = nullptr;
154 Texture* normal = nullptr;
155 Texture *height = nullptr;
156 float tr = 1.f, tg = 1.f, tb = 1.f, ta = 1.f;
157 float metallic = 0.f;
158 float roughness = 0.45f;
159 std::string alphaMode = "OPAQUE";
160 float alphaCutoff = 0.5f;
161 bool doubleSided = false;
162 bool extendedPbr = false;
163 graphics::PbrSurface pbr;
164};
165
166void extendedLook(IResourceFactory* gfx, ModelData* model, int index, TextureLook& look,
167 const ModelRenderOptions& options) {
168 const auto* scene = model->getScene();
169 if (!scene || index < 0 || unsigned(index) >= scene->mNumMaterials) return;
170 const auto* material = scene->mMaterials[index];
171 aiString alpha;
172 if (material->Get(AI_MATKEY_GLTF_ALPHAMODE, alpha) != AI_SUCCESS) return;
173 look.extendedPbr = true;
174 look.ta = model->getMaterialBaseColorA(index);
175 auto& p = look.pbr;
176 auto scalar = [&](const char* key, unsigned type, unsigned slot, float fallback) {
177 float value = fallback;
178 material->Get(key, type, slot, value);
179 if (!std::isfinite(value)) throw eve::Exception("nonfinite imported material factor %s", key);
180 return value;
181 };
182 auto unit = [&](const char* key, unsigned type, unsigned slot, float fallback) {
183 float value = scalar(key, type, slot, 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);
187 };
188 p.specularFactor = unit(AI_MATKEY_SPECULAR_FACTOR, 1);
189 aiColor3D color(1, 1, 1);
190 material->Get(AI_MATKEY_COLOR_SPECULAR, color);
191 p.specularColor = {color.r, color.g, color.b};
192 color = {0, 0, 0};
193 material->Get(AI_MATKEY_COLOR_EMISSIVE, color);
194 p.emissive = {color.r, color.g, color.b};
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);
203 int shading = 0;
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;
215 aiString path;
216 unsigned uv = 0;
217 aiTextureMapMode wrap[2] = {aiTextureMapMode_Wrap, aiTextureMapMode_Wrap};
218 if (material->GetTexture(type, slot, &path, nullptr, &uv, nullptr, nullptr, wrap) != AI_SUCCESS) continue;
219 auto& binding = p.textures[i];
220 if (i == 0 && look.albedo)
221 binding.texture = look.albedo;
222 else if (i == 2 && look.normal)
223 binding.texture = look.normal;
224 else if (path.length > 1 && path.C_Str()[0] == '*')
225 binding.texture = loadEmbeddedTexture(gfx, model, std::stoi(path.C_Str() + 1));
226 else
227 binding.texture = loadModelTexture(gfx, model, path.C_Str());
228 if (!binding.texture) {
229 // Model3D preview preserves the legacy factor-only material when an
230 // external resource is absent. Canonical asset admission remains strict.
231 std::fprintf(stderr, "[model3d] warning: missing material texture %s (model %s); using material factors\n",
232 path.C_Str(), model->getUri().c_str());
233 continue;
234 }
235 binding.texcoord = uv;
236 auto wrapEnum = [](aiTextureMapMode mode) {
237 return mode == aiTextureMapMode_Clamp ? 33071u : mode == aiTextureMapMode_Mirror ? 33648u : 10497u;
238 };
239 binding.wrapS = wrapEnum(wrap[0]);
240 binding.wrapT = wrapEnum(wrap[1]);
241 int filter = 9987;
242 material->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MIN(type, slot), filter);
243 binding.minFilter = filter;
244 filter = 9729;
245 material->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MAG(type, slot), filter);
246 binding.magFilter = filter;
247 aiUVTransform transform;
248 if (material->Get(AI_MATKEY_UVTRANSFORM(type, slot), transform) == AI_SUCCESS) {
249 // First undo the optional postprocess, then Assimp's glTF origin/center conversion.
250 if (model->hasFlippedUvs()) {
251 transform.mRotation = -transform.mRotation;
252 transform.mTranslation.y = -transform.mTranslation.y;
253 }
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;
256 binding.rotation = r;
257 binding.scale = {sx, sy};
258 binding.offset = {transform.mTranslation.x - .5f * sx * (-c + sn + 1),
259 1 - sy - transform.mTranslation.y + .5f * sy * (sn + c - 1)};
260 }
261 }
262 look.albedo = p.textures[0].texture;
263 look.normal = p.textures[2].texture;
264}
265
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;
270
271 TextureLook look;
272 if (matIndex >= 0) {
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);
283 if (options.importAlbedo) {
284 look.albedo = loadTextureSlot(gfx, model, matIndex, "base_color");
285 if (!look.albedo) look.albedo = loadTextureSlot(gfx, model, matIndex, "diffuse");
286 }
287 if (options.importNormalMaps)
288 look.normal = loadTextureSlot(gfx, model, matIndex, "normals");
289 if (options.importHeightMaps)
290 look.height = loadTextureSlot(gfx, model, matIndex, "height");
291 }
292 extendedLook(gfx, model, matIndex, look, options);
293 cache.emplace(matIndex, look);
294 return look;
295}
296
297Renderable3D *makeRenderable(IResourceFactory *gfx, ModelData *model, int meshIndex, const aiMatrix4x4 &world,
298 const ModelRenderOptions &options, std::unordered_map<int, TextureLook> &cache) {
299 const aiScene *scene = model->getScene();
300 if (!scene || meshIndex < 0 || static_cast<unsigned>(meshIndex) >= scene->mNumMeshes)
301 return nullptr;
302 const aiMesh *ai = scene->mMeshes[meshIndex];
303 if (!ai || ai->mNumVertices == 0 || ai->mNumFaces == 0) return nullptr;
304
305 // Skinned vertex positions and inverse-bind matrices share the model's
306 // bind-pose space. Baking the owning node transform into only the vertices
307 // makes the skin palette apply that transform a second time, separating
308 // modular body parts (notably KayKit heads, armour and limbs). Static
309 // meshes still use the established baked scene transform.
310 const bool bakeWorld = options.bakeWorldTransform && !ai->HasBones();
311 Mesh *mesh = bakeWorld ? gfx->newMeshFromAssimp(*ai, world) : gfx->newMeshFromAssimp(*ai);
312 if (!mesh) return nullptr;
313
314 const int matIndex = model->getMaterialIndex(meshIndex);
315 const TextureLook look = materialLook(gfx, model, matIndex, options, cache);
316
317 Renderable3D *ent = Renderable3D::create();
318 ent->meshRenderer()->visible = true;
319 ent->setMesh(mesh);
320 // Preserve the established Renderable3D inspection API. The Material below
321 // is authoritative for drawing, while these mirrored fields keep imported
322 // models compatible with callers that query meshRenderer()/getTexture().
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);
329 // Keep imported surface semantics in one Material instead of losing glTF
330 // alphaMode/alphaCutoff on the legacy renderer fields.
331 Material *material = new Material();
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")
341 material->setSurfaceMode("masked");
342 else if (look.alphaMode == "BLEND")
343 material->setSurfaceMode("transparent");
344 else
345 material->setSurfaceMode("opaque");
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);
357 }
358 auto attached = mesh->setTexcoordSet(channel, values);
359 if (!attached) throw eve::Exception("%s", attached.error()->message().c_str());
360 }
361 }
362 ent->setMaterial(material);
363 return ent;
364}
365
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) {
369 if (!node) return;
370 const aiMatrix4x4 world = parent * node->mTransformation;
371 for (unsigned i = 0; i < node->mNumMeshes; ++i) {
372 Renderable3D *ent =
373 makeRenderable(gfx, model, static_cast<int>(node->mMeshes[i]), world, options, cache);
374 if (ent) out.push_back(ent);
375 }
376 for (unsigned c = 0; c < node->mNumChildren; ++c)
377 walkNodes(node->mChildren[c], world, gfx, model, options, out, cache);
378}
379
380bool findMeshTransform(const aiNode *node, const aiMatrix4x4 &parent, unsigned meshIndex,
381 aiMatrix4x4 &out) {
382 if (!node) return false;
383 const aiMatrix4x4 world = parent * node->mTransformation;
384 for (unsigned i = 0; i < node->mNumMeshes; ++i) {
385 if (node->mMeshes[i] == meshIndex) {
386 out = world;
387 return true;
388 }
389 }
390 for (unsigned c = 0; c < node->mNumChildren; ++c)
391 if (findMeshTransform(node->mChildren[c], world, meshIndex, out)) return true;
392 return false;
393}
394
395} // namespace
396
397Renderable3D *buildRenderable(IResourceFactory &gfx, ModelData *model, int meshIndex,
399 if (!model) return nullptr;
400 const aiScene *scene = model->getScene();
401 if (!scene || !scene->mRootNode) return nullptr;
402
403 aiMatrix4x4 world;
404 if (!findMeshTransform(scene->mRootNode, aiMatrix4x4(), static_cast<unsigned>(meshIndex),
405 world)) {
406 world = aiMatrix4x4();
407 }
408 std::unordered_map<int, TextureLook> cache;
409 return makeRenderable(&gfx, model, meshIndex, world, options, cache);
410}
411
412std::vector<Renderable3D *> buildRenderables(IResourceFactory &gfx, ModelData *model,
414 std::vector<Renderable3D *> out;
415 if (!model) return out;
416 const aiScene *scene = model->getScene();
417 if (!scene || !scene->mRootNode) return out;
418 std::unordered_map<int, TextureLook> cache;
419 walkNodes(scene->mRootNode, aiMatrix4x4(), &gfx, model, options, out, cache);
420 return out;
421}
422
423Result<void> prepareFoliageDeformation(ModelData &model, int meshIndex, Renderable3D &renderable) {
424 const aiScene *scene = model.getScene();
425 Mesh *mesh = renderable.getMesh();
426 auto invalid = [](const char *message) {
428 Diagnostic::error(DiagnosticCode::InvalidArgument, message, "model3d.foliage-deformation"));
429 };
430 if (!scene || !scene->mRootNode || meshIndex < 0 || unsigned(meshIndex) >= scene->mNumMeshes || !mesh)
431 return invalid("model, mesh index, and matching renderable are required");
432 const aiMesh *source = scene->mMeshes[meshIndex];
433 if (!source || !source->mVertices || source->mNumVertices == 0 || mesh->gpuVertexCount != int(source->mNumVertices))
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");
437
438 aiMatrix4x4 world;
439 if (!findMeshTransform(scene->mRootNode, aiMatrix4x4(), unsigned(meshIndex), world)) world = aiMatrix4x4();
440 std::vector<aiVector3D> positions;
441 positions.reserve(source->mNumVertices);
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) {
445 const aiVector3D p = world * source->mVertices[i];
446 if (!std::isfinite(p.x) || !std::isfinite(p.y) || !std::isfinite(p.z))
447 return invalid("foliage source positions must be finite");
448 positions.push_back(p);
449 minY = std::min(minY, p.y);
450 maxY = std::max(maxY, p.y);
451 }
452 const float height = maxY - minY;
453 if (!std::isfinite(height) || height <= 1e-6f) return invalid("foliage mesh must have positive height");
454 float radius = 0.f;
455 for (const auto &p : positions) radius = std::max(radius, std::hypot(p.x, p.z));
456 radius = std::max(radius, 1e-6f);
457
458 std::vector<float> factors;
459 factors.reserve(size_t(source->mNumVertices) * 9u);
460 for (unsigned i = 0; i < source->mNumVertices; ++i) {
461 const auto &p = positions[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});
468 }
469 return mesh->adoptVegetationDeformationFactors(std::move(factors));
470}
471
472} // namespace eve::model3d
double value
float y
Definition AnimClip.cpp:738
Vec3 relative
Definition AnimSmr.cpp:164
float uv
graphics::Texture * albedo
glm::vec4 p[6]
std::map< std::string, Var > values
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
std::string message
std::uint32_t key
vk::UniqueImage image
double r
std::vector< float > positions
float v
std::int32_t c
std::uint32_t height
std::int32_t parent
std::uint64_t bytes
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float roughness
float tb
graphics::PbrSurface pbr
float tr
bool doubleSided
Texture * normal
std::string alphaMode
float tg
float alphaCutoff
float ta
float metallic
bool extendedPbr
eve::action::ActionVfxBinding binding
int idx
World3D * world
float radius
std::string path
Definition PlayHost.cpp:110
std::string uri
std::weak_ptr< PrimitiveScene > scene
Mesh * mesh
glm::mat4 model
Material * material
const RoadNode * node
std::string filter
std::uint32_t count
const SquirrelValueOptions & options
TacticalUnit * unit
uint32_t index
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.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Texture / mesh / shader / canvas creation and release.
Packages shading method + surface parameters into one attachable asset.
Definition Material.h:35
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:25
EVENGINE_API_BACKENDS public API.
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
CPU-side decoded 3D model (Assimp scene owned via medialoader::ModelScene). Does not upload to GPU — ...
Definition ModelData.h:39
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.
glm::vec4 color