MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ModelReader.cpp
Go to the documentation of this file.
1#include "ModelReader.hpp"
2
4
6
8
10
11#include <assimp/Importer.hpp>
12#include <assimp/postprocess.h>
13#include <assimp/scene.h>
14
15namespace MayaFlux::IO {
16
17namespace {
18 std::string get_diffuse_path(const Kakshya::MeshData& mesh_data)
19 {
20 if (!mesh_data.submeshes.has_value())
21 return {};
22 const auto& regions = mesh_data.submeshes->regions;
23 if (regions.empty())
24 return {};
25 const auto& attrs = regions.front().attributes;
26 auto it = attrs.find("diffuse_path");
27 if (it == attrs.end())
28 return {};
29 const auto* s = std::any_cast<std::string>(&it->second);
30 if (!s || s->empty() || (*s)[0] == '*')
31 return {};
32 return *s;
33 }
34}
35
36// =============================================================================
37// PIMPL — hides Assimp headers from the public interface
38// =============================================================================
39
41 Assimp::Importer importer;
42 const aiScene* scene { nullptr };
43};
44
45// =============================================================================
46// Construction
47// =============================================================================
48
50 : m_impl(std::make_unique<Impl>())
51{
52}
53
58
59// =============================================================================
60// Primary API
61// =============================================================================
62
63std::vector<Kakshya::MeshData> ModelReader::load(const std::string& filepath)
64{
65 if (!open(filepath)) {
66 return {};
67 }
68 auto result = extract_meshes();
69 close();
70 return result;
71}
72
73std::vector<Kakshya::MeshData> ModelReader::extract_meshes() const
74{
75 if (!m_impl->scene) {
76 set_error("No scene loaded");
77 return {};
78 }
79
80 const aiScene* s = m_impl->scene;
81 std::vector<Kakshya::MeshData> result;
82 result.reserve(s->mNumMeshes);
83
84 for (unsigned int i = 0; i < s->mNumMeshes; ++i) {
85 const aiMesh* mesh = s->mMeshes[i];
86
87 std::string mesh_name(mesh->mName.C_Str());
88
89 std::string mat_name;
90 if (s->mNumMaterials > 0 && mesh->mMaterialIndex < s->mNumMaterials) {
91 aiString ai_mat;
92 s->mMaterials[mesh->mMaterialIndex]->Get(AI_MATKEY_NAME, ai_mat);
93 mat_name = ai_mat.C_Str();
94 }
95
96 auto mesh_data = extract_single_mesh(mesh, s, mesh_name, mat_name);
97 if (mesh_data.is_valid()) {
98 result.push_back(std::move(mesh_data));
99 } else {
101 "ModelReader: mesh '{}' produced invalid MeshData, skipped",
102 mesh_name.empty() ? "<unnamed>" : mesh_name);
103 }
104 }
105
107 "ModelReader: extracted {}/{} meshes",
108 result.size(), s->mNumMeshes);
109
110 return result;
111}
112
113std::vector<std::shared_ptr<Buffers::MeshBuffer>>
115{
116 auto meshes = extract_meshes();
117
118 std::vector<std::shared_ptr<Buffers::MeshBuffer>> result;
119 result.reserve(meshes.size());
120
121 for (auto& mesh_data : meshes) {
122 if (!mesh_data.is_valid()) {
124 "ModelReader::create_mesh_buffers: skipping invalid MeshData");
125 continue;
126 }
127
128 auto buf = std::make_shared<Buffers::MeshBuffer>(mesh_data);
129
130 if (resolver) {
131 const auto raw = get_diffuse_path(mesh_data);
132 if (!raw.empty()) {
133 auto image = resolver(raw);
134 if (image) {
135 buf->bind_diffuse_texture(image);
136 } else {
138 "ModelReader::create_mesh_buffers: resolver returned null for '{}'",
139 raw);
140 }
141 }
142 }
143
144 result.push_back(std::move(buf));
145 }
146
148 "ModelReader::create_mesh_buffers: created {}/{} MeshBuffers",
149 result.size(), meshes.size());
150
151 return result;
152}
153
154std::shared_ptr<Nodes::Network::MeshNetwork>
156{
157 if (!m_impl->scene) {
158 set_error("No scene loaded");
159 return nullptr;
160 }
161
162 auto net = std::make_shared<Nodes::Network::MeshNetwork>();
163 const aiScene* s = m_impl->scene;
164
165 for (unsigned int i = 0; i < s->mNumMeshes; ++i) {
166 const aiMesh* ai_mesh = s->mMeshes[i];
167
168 std::string mesh_name(ai_mesh->mName.C_Str());
169 if (mesh_name.empty())
170 mesh_name = "mesh_" + std::to_string(i);
171
172 std::string mat_name;
173 if (s->mNumMaterials > 0 && ai_mesh->mMaterialIndex < s->mNumMaterials) {
174 aiString ai_mat;
175 s->mMaterials[ai_mesh->mMaterialIndex]->Get(AI_MATKEY_NAME, ai_mat);
176 mat_name = ai_mat.C_Str();
177 }
178
179 auto mesh_data = extract_single_mesh(ai_mesh, s, mesh_name, mat_name);
180 if (!mesh_data.is_valid()) {
182 "ModelReader::create_mesh_network: skipping invalid mesh '{}'", mesh_name);
183 continue;
184 }
185
186 const auto* vb = std::get_if<std::vector<uint8_t>>(&mesh_data.vertex_variant);
187 const auto* ib = std::get_if<std::vector<uint32_t>>(&mesh_data.index_variant);
188
189 if (!vb || !ib)
190 continue;
191
192 const size_t vertex_count = vb->size() / sizeof(Nodes::MeshVertex);
193 auto node = std::make_shared<Nodes::GpuSync::MeshWriterNode>(vertex_count);
194 node->set_mesh(
195 std::span<const Nodes::MeshVertex>(
196 reinterpret_cast<const Nodes::MeshVertex*>(vb->data()),
197 vertex_count),
198 std::span<const uint32_t>(ib->data(), ib->size()));
199
200 auto slot_idx = net->add_slot(mesh_name, node);
201
202 if (resolver) {
203 const auto raw = get_diffuse_path(mesh_data);
204 if (!raw.empty()) {
205 auto image = resolver(raw);
206 if (image) {
207 net->get_slot(slot_idx).diffuse_texture = std::move(image);
208 } else {
210 "ModelReader::create_mesh_network: resolver returned null for '{}' (slot '{}')",
211 raw, mesh_name);
212 }
213 }
214 }
215 }
216
218 "ModelReader::create_mesh_network: {} slots", net->slot_count());
219
220 return net;
221}
222
223// =============================================================================
224// FileReader interface
225// =============================================================================
226
227bool ModelReader::can_read(const std::string& filepath) const
228{
229 const auto ext = std::filesystem::path(filepath)
230 .extension()
231 .string();
232
233 if (ext.empty()) {
234 return false;
235 }
236
237 const auto supported = get_supported_extensions();
238 const std::string lower = [&] {
239 std::string s = ext.substr(1);
240 std::ranges::transform(s, s.begin(), ::tolower);
241 return s;
242 }();
243
244 return std::ranges::find(supported, lower) != supported.end();
245}
246
247bool ModelReader::open(const std::string& filepath, FileReadOptions /*options*/)
248{
249 close();
250
251 auto resolved = resolve_path(filepath);
252
253 if (!std::filesystem::exists(resolved)) {
254 set_error("File not found: " + filepath);
256 "ModelReader: {}", m_last_error);
257 return false;
258 }
259
260 constexpr unsigned int flags = aiProcess_Triangulate
261 | aiProcess_GenSmoothNormals
262 | aiProcess_CalcTangentSpace
263 | aiProcess_FlipUVs
264 | aiProcess_JoinIdenticalVertices
265 | aiProcess_SortByPType;
266
267 m_impl->scene = m_impl->importer.ReadFile(resolved, flags);
268
269 if (!m_impl->scene
270 || (m_impl->scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE)
271 || !m_impl->scene->mRootNode) {
272 set_error(m_impl->importer.GetErrorString());
274 "ModelReader: Assimp import failed — {}", m_last_error);
275 m_impl->scene = nullptr;
276 return false;
277 }
278
279 m_is_open = true;
281 "ModelReader: opened '{}' — {} meshes, {} materials",
282 std::filesystem::path(resolved).filename().string(),
283 m_impl->scene->mNumMeshes,
284 m_impl->scene->mNumMaterials);
285
286 return true;
287}
288
290{
291 if (m_impl->scene) {
292 m_impl->importer.FreeScene();
293 m_impl->scene = nullptr;
294 }
295 m_is_open = false;
296}
297
298std::optional<FileMetadata> ModelReader::get_metadata() const
299{
300 if (!m_impl->scene) {
301 return std::nullopt;
302 }
303
304 FileMetadata meta;
305 meta.format = "model";
306 meta.mime_type = "model/gltf-binary"; // reasonable default; format-agnostic
307
308 const aiScene* s = m_impl->scene;
309 meta.attributes["mesh_count"] = static_cast<uint64_t>(s->mNumMeshes);
310 meta.attributes["material_count"] = static_cast<uint64_t>(s->mNumMaterials);
311 meta.attributes["animation_count"] = static_cast<uint64_t>(s->mNumAnimations);
312 meta.attributes["texture_count"] = static_cast<uint64_t>(s->mNumTextures);
313
314 return meta;
315}
316
317std::vector<FileRegion> ModelReader::get_regions() const
318{
319 return {};
320}
321
322std::vector<Kakshya::DataVariant> ModelReader::read_all()
323{
324 return {};
325}
326
327std::vector<Kakshya::DataVariant> ModelReader::read_region(const FileRegion& /*region*/)
328{
329 return {};
330}
331
332std::shared_ptr<Kakshya::SignalSourceContainer> ModelReader::create_container()
333{
334 m_last_error = "Mesh data does not use SignalSourceContainer. Use load() instead.";
335 return nullptr;
336}
337
339 std::shared_ptr<Kakshya::SignalSourceContainer> /*container*/)
340{
341 m_last_error = "Mesh data does not use SignalSourceContainer. Use load() instead.";
342 return false;
343}
344
345std::vector<uint64_t> ModelReader::get_read_position() const
346{
347 return { 0 };
348}
349
350bool ModelReader::seek(const std::vector<uint64_t>& /*position*/)
351{
352 return true;
353}
354
355std::vector<std::string> ModelReader::get_supported_extensions() const
356{
357 return {
358 "gltf", "glb",
359 "obj",
360 "fbx",
361 "ply",
362 "stl",
363 "dae",
364 "3ds",
365 "x3d",
366 "blend"
367 };
368}
369
370// =============================================================================
371// Private: single-mesh extraction
372// =============================================================================
373
375 const void* ai_mesh_ptr,
376 const void* ai_scene,
377 std::string_view mesh_name,
378 std::string_view material_name) const
379{
380 const auto* mesh = static_cast<const aiMesh*>(ai_mesh_ptr);
381
382 using V = Nodes::MeshVertex;
383
384 std::vector<V> verts;
385 verts.reserve(mesh->mNumVertices);
386
387 for (unsigned int v = 0; v < mesh->mNumVertices; ++v) {
388 V vert {};
389
390 vert.position = {
391 mesh->mVertices[v].x,
392 mesh->mVertices[v].y,
393 mesh->mVertices[v].z
394 };
395
396 if (mesh->HasNormals()) {
397 vert.normal = {
398 mesh->mNormals[v].x,
399 mesh->mNormals[v].y,
400 mesh->mNormals[v].z
401 };
402 } else {
403 vert.normal = { 0.0F, 1.0F, 0.0F };
404 }
405
406 if (mesh->HasTangentsAndBitangents()) {
407 vert.tangent = {
408 mesh->mTangents[v].x,
409 mesh->mTangents[v].y,
410 mesh->mTangents[v].z
411 };
412 } else {
413 vert.tangent = { 1.0F, 0.0F, 0.0F };
414 }
415
416 if (mesh->HasTextureCoords(0)) {
417 vert.uv = {
418 mesh->mTextureCoords[0][v].x,
419 mesh->mTextureCoords[0][v].y
420 };
421 } else {
422 vert.uv = { 0.0F, 0.0F };
423 }
424
425 if (mesh->HasVertexColors(0)) {
426 vert.color = {
427 mesh->mColors[0][v].r,
428 mesh->mColors[0][v].g,
429 mesh->mColors[0][v].b
430 };
431 } else {
432 vert.color = { 0.8F, 0.8F, 0.8F };
433 }
434
435 vert.weight = 0.0F;
436 verts.push_back(vert);
437 }
438
439 std::vector<uint32_t> indices;
440 indices.reserve(static_cast<size_t>(mesh->mNumFaces) * 3);
441
442 for (unsigned int f = 0; f < mesh->mNumFaces; ++f) {
443 const aiFace& face = mesh->mFaces[f];
444 if (face.mNumIndices != 3) {
445 continue;
446 }
447 indices.push_back(face.mIndices[0]);
448 indices.push_back(face.mIndices[1]);
449 indices.push_back(face.mIndices[2]);
450 }
451
452 if (verts.empty() || indices.empty()) {
453 set_error("Mesh has no usable geometry after extraction");
454 return {};
455 }
456
457 auto data = Kakshya::MeshData::empty();
458 Kakshya::MeshInsertion ins(data.vertex_variant, data.index_variant);
459 ins.insert_flat(
460 std::span<const uint8_t>(
461 reinterpret_cast<const uint8_t*>(verts.data()),
462 verts.size() * sizeof(V)),
463 std::span<const uint32_t>(indices),
465
466 auto access = ins.build();
467 if (!access) {
468 set_error("MeshInsertion::build() failed");
469 return {};
470 }
471 data.layout = access->layout;
472
474 sub.index_start = 0;
475 sub.index_count = static_cast<uint32_t>(indices.size());
476 sub.vertex_offset = 0;
477 sub.name = std::string(mesh_name);
478 sub.material_name = std::string(material_name);
479
480 const auto* s = static_cast<const aiScene*>(ai_scene);
481
482 if (s->mNumMaterials > 0 && mesh->mMaterialIndex < s->mNumMaterials) {
483 aiString tex_path;
484 const aiMaterial* mat = s->mMaterials[mesh->mMaterialIndex];
485 if (mat->GetTexture(aiTextureType_DIFFUSE, 0, &tex_path) == AI_SUCCESS) {
486 sub.diffuse_path = std::filesystem::path(tex_path.C_Str()).generic_string();
487 sub.diffuse_embedded = (!sub.diffuse_path.empty()
488 && sub.diffuse_path[0] == '*');
489
491 "ModelReader: mesh '{}' diffuse={} embedded={}",
492 std::string(mesh_name),
493 sub.diffuse_path,
494 sub.diffuse_embedded);
495 }
496 }
497
498 Kakshya::RegionGroup rg("submeshes");
499 rg.add_region(sub.to_region());
500 data.submeshes = std::move(rg);
501
503 "ModelReader: extracted mesh '{}' mat='{}' — {} verts, {} indices",
504 mesh_name.empty() ? "<unnamed>" : mesh_name,
505 material_name.empty() ? "<none>" : material_name,
506 verts.size(), indices.size());
507
508 return data;
509}
510
511} // namespace MayaFlux::IO
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
IO::ImageData image
static std::string resolve_path(const std::string &filepath)
Resolve a filepath against the project source root if not found as-is.
std::unique_ptr< Impl > m_impl
bool seek(const std::vector< uint64_t > &position) override
Seek to a specific position in the file.
std::optional< FileMetadata > get_metadata() const override
Get metadata from the open file.
std::shared_ptr< Kakshya::SignalSourceContainer > create_container() override
No-op.
std::vector< Kakshya::DataVariant > read_region(const FileRegion &region) override
Read a specific region of data.
Kakshya::MeshData extract_single_mesh(const void *ai_mesh, const void *ai_scene, std::string_view mesh_name, std::string_view material_name) const
void set_error(std::string msg) const
std::vector< Kakshya::MeshData > extract_meshes() const
Load all meshes after open() has already been called.
std::vector< FileRegion > get_regions() const override
Get semantic regions from the file.
std::vector< std::shared_ptr< Buffers::MeshBuffer > > create_mesh_buffers(const TextureResolver &resolver=nullptr) const
Construct one MeshBuffer per mesh in the currently loaded scene.
bool can_read(const std::string &filepath) const override
Check if a file can be read by this reader.
std::shared_ptr< Nodes::Network::MeshNetwork > create_mesh_network(const TextureResolver &resolver=nullptr) const
Construct a MeshNetwork from all meshes in the currently loaded scene.
void close() override
Close the currently open file.
bool load_into_container(std::shared_ptr< Kakshya::SignalSourceContainer > container) override
No-op.
std::vector< std::string > get_supported_extensions() const override
Get supported file extensions for this reader.
std::vector< Kakshya::MeshData > load(const std::string &filepath)
Load all meshes from a file in one call.
std::vector< Kakshya::DataVariant > read_all() override
Read all data from the file into memory.
std::vector< uint64_t > get_read_position() const override
Get current read position in primary dimension.
bool open(const std::string &filepath, FileReadOptions options=FileReadOptions::ALL) override
Open a file for reading.
std::optional< MeshAccess > build() const
Produce a MeshAccess over the current variant contents.
void insert_flat(std::span< const uint8_t > vertex_bytes, std::span< const uint32_t > index_data, const VertexLayout &layout)
Insert a single flat mesh (no submesh tracking).
Write counterpart to MeshAccess.
FileReadOptions
Generic options for file reading behavior.
std::function< std::shared_ptr< Core::VKImage >(const std::string &path)> TextureResolver
Callable that maps a raw material texture path to a GPU image.
Definition Creator.hpp:17
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
std::unordered_map< std::string, std::any > attributes
Type-specific metadata stored as key-value pairs (e.g., sample rate, channels)
std::string format
File format identifier (e.g., "wav", "mp3", "hdf5")
std::string mime_type
MIME type if applicable (e.g., "audio/wav")
Generic metadata structure for any file type.
Generic region descriptor for any file type.
static MeshData empty()
Construct an empty MeshData with the canonical 60-byte mesh layout.
Definition MeshData.hpp:49
Owning CPU-side representation of a loaded or generated mesh.
Definition MeshData.hpp:33
Region to_region() const
Convert this subrange to a Region for use in RegionGroup.
uint32_t vertex_offset
Base vertex added to each index (large-mesh batching)
Byte-range descriptor for one submesh within the shared index buffer.
void add_region(const Region &region)
Organizes related signal regions into a categorized collection.
static VertexLayout for_meshes(uint32_t stride=60)
Factory: layout for MeshVertex (position, color, weight, uv, normal, tangent)
Vertex type for indexed triangle mesh primitives (TRIANGLE_LIST topology)