11#include <assimp/Importer.hpp>
12#include <assimp/postprocess.h>
13#include <assimp/scene.h>
18 std::string get_diffuse_path(
const Kakshya::MeshData& mesh_data)
20 if (!mesh_data.submeshes.has_value())
22 const auto& regions = mesh_data.submeshes->regions;
25 const auto& attrs = regions.front().attributes;
26 auto it = attrs.find(
"diffuse_path");
27 if (it == attrs.end())
29 const auto* s = std::any_cast<std::string>(&it->second);
30 if (!s || s->empty() || (*s)[0] ==
'*')
42 const aiScene*
scene {
nullptr };
50 : m_impl(
std::make_unique<
Impl>())
65 if (!
open(filepath)) {
80 const aiScene* s =
m_impl->scene;
81 std::vector<Kakshya::MeshData> result;
82 result.reserve(s->mNumMeshes);
84 for (
unsigned int i = 0; i < s->mNumMeshes; ++i) {
85 const aiMesh* mesh = s->mMeshes[i];
87 std::string mesh_name(mesh->mName.C_Str());
90 if (s->mNumMaterials > 0 && mesh->mMaterialIndex < s->mNumMaterials) {
92 s->mMaterials[mesh->mMaterialIndex]->Get(AI_MATKEY_NAME, ai_mat);
93 mat_name = ai_mat.C_Str();
97 if (mesh_data.is_valid()) {
98 result.push_back(std::move(mesh_data));
101 "ModelReader: mesh '{}' produced invalid MeshData, skipped",
102 mesh_name.empty() ?
"<unnamed>" : mesh_name);
107 "ModelReader: extracted {}/{} meshes",
108 result.size(), s->mNumMeshes);
113std::vector<std::shared_ptr<Buffers::MeshBuffer>>
118 std::vector<std::shared_ptr<Buffers::MeshBuffer>> result;
119 result.reserve(meshes.size());
121 for (
auto& mesh_data : meshes) {
122 if (!mesh_data.is_valid()) {
124 "ModelReader::create_mesh_buffers: skipping invalid MeshData");
128 auto buf = std::make_shared<Buffers::MeshBuffer>(mesh_data);
131 const auto raw = get_diffuse_path(mesh_data);
133 auto image = resolver(raw);
135 buf->bind_diffuse_texture(
image);
138 "ModelReader::create_mesh_buffers: resolver returned null for '{}'",
144 result.push_back(std::move(buf));
148 "ModelReader::create_mesh_buffers: created {}/{} MeshBuffers",
149 result.size(), meshes.size());
154std::shared_ptr<Nodes::Network::MeshNetwork>
162 auto net = std::make_shared<Nodes::Network::MeshNetwork>();
163 const aiScene* s =
m_impl->scene;
165 for (
unsigned int i = 0; i < s->mNumMeshes; ++i) {
166 const aiMesh* ai_mesh = s->mMeshes[i];
168 std::string mesh_name(ai_mesh->mName.C_Str());
169 if (mesh_name.empty())
170 mesh_name =
"mesh_" + std::to_string(i);
172 std::string mat_name;
173 if (s->mNumMaterials > 0 && ai_mesh->mMaterialIndex < s->mNumMaterials) {
175 s->mMaterials[ai_mesh->mMaterialIndex]->Get(AI_MATKEY_NAME, ai_mat);
176 mat_name = ai_mat.C_Str();
180 if (!mesh_data.is_valid()) {
182 "ModelReader::create_mesh_network: skipping invalid mesh '{}'", mesh_name);
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);
193 auto node = std::make_shared<Nodes::GpuSync::MeshWriterNode>(vertex_count);
195 std::span<const Nodes::MeshVertex>(
198 std::span<const uint32_t>(ib->data(), ib->size()));
200 auto slot_idx = net->add_slot(mesh_name, node);
203 const auto raw = get_diffuse_path(mesh_data);
205 auto image = resolver(raw);
207 net->get_slot(slot_idx).diffuse_texture = std::move(
image);
210 "ModelReader::create_mesh_network: resolver returned null for '{}' (slot '{}')",
218 "ModelReader::create_mesh_network: {} slots", net->slot_count());
229 const auto ext = std::filesystem::path(filepath)
238 const std::string lower = [&] {
239 std::string s = ext.substr(1);
240 std::ranges::transform(s, s.begin(), ::tolower);
244 return std::ranges::find(supported, lower) != supported.end();
253 if (!std::filesystem::exists(resolved)) {
254 set_error(
"File not found: " + filepath);
260 constexpr unsigned int flags = aiProcess_Triangulate
261 | aiProcess_GenSmoothNormals
262 | aiProcess_CalcTangentSpace
264 | aiProcess_JoinIdenticalVertices
265 | aiProcess_SortByPType;
267 m_impl->scene =
m_impl->importer.ReadFile(resolved, flags);
270 || (
m_impl->scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE)
271 || !
m_impl->scene->mRootNode) {
274 "ModelReader: Assimp import failed — {}",
m_last_error);
281 "ModelReader: opened '{}' — {} meshes, {} materials",
282 std::filesystem::path(resolved).filename().
string(),
283 m_impl->scene->mNumMeshes,
284 m_impl->scene->mNumMaterials);
292 m_impl->importer.FreeScene();
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);
334 m_last_error =
"Mesh data does not use SignalSourceContainer. Use load() instead.";
339 std::shared_ptr<Kakshya::SignalSourceContainer> )
341 m_last_error =
"Mesh data does not use SignalSourceContainer. Use load() instead.";
375 const void* ai_mesh_ptr,
376 const void* ai_scene,
377 std::string_view mesh_name,
378 std::string_view material_name)
const
380 const auto* mesh =
static_cast<const aiMesh*
>(ai_mesh_ptr);
384 std::vector<V> verts;
385 verts.reserve(mesh->mNumVertices);
387 for (
unsigned int v = 0; v < mesh->mNumVertices; ++v) {
391 mesh->mVertices[v].x,
392 mesh->mVertices[v].y,
396 if (mesh->HasNormals()) {
403 vert.normal = { 0.0F, 1.0F, 0.0F };
406 if (mesh->HasTangentsAndBitangents()) {
408 mesh->mTangents[v].x,
409 mesh->mTangents[v].y,
413 vert.tangent = { 1.0F, 0.0F, 0.0F };
416 if (mesh->HasTextureCoords(0)) {
418 mesh->mTextureCoords[0][v].x,
419 mesh->mTextureCoords[0][v].y
422 vert.uv = { 0.0F, 0.0F };
425 if (mesh->HasVertexColors(0)) {
427 mesh->mColors[0][v].r,
428 mesh->mColors[0][v].g,
429 mesh->mColors[0][v].b
432 vert.color = { 0.8F, 0.8F, 0.8F };
436 verts.push_back(vert);
439 std::vector<uint32_t> indices;
440 indices.reserve(
static_cast<size_t>(mesh->mNumFaces) * 3);
442 for (
unsigned int f = 0; f < mesh->mNumFaces; ++f) {
443 const aiFace& face = mesh->mFaces[f];
444 if (face.mNumIndices != 3) {
447 indices.push_back(face.mIndices[0]);
448 indices.push_back(face.mIndices[1]);
449 indices.push_back(face.mIndices[2]);
452 if (verts.empty() || indices.empty()) {
453 set_error(
"Mesh has no usable geometry after extraction");
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),
466 auto access = ins.
build();
468 set_error(
"MeshInsertion::build() failed");
471 data.layout = access->layout;
475 sub.
index_count =
static_cast<uint32_t
>(indices.size());
477 sub.
name = std::string(mesh_name);
480 const auto* s =
static_cast<const aiScene*
>(ai_scene);
482 if (s->mNumMaterials > 0 && mesh->mMaterialIndex < s->mNumMaterials) {
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();
491 "ModelReader: mesh '{}' diffuse={} embedded={}",
492 std::string(mesh_name),
500 data.submeshes = std::move(rg);
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());
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
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 ®ion) 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.
@ FileIO
Filesystem I/O operations.
@ IO
Networking, file handling, streaming.
Generic region descriptor for any file type.
Assimp::Importer importer
static MeshData empty()
Construct an empty MeshData with the canonical 60-byte mesh layout.
Owning CPU-side representation of a loaded or generated mesh.
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)
std::string material_name
Byte-range descriptor for one submesh within the shared index buffer.
void add_region(const Region ®ion)
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)