Unravel Engine C++ Reference
Loading...
Searching...
No Matches
mesh_importer.cpp
Go to the documentation of this file.
1#include "mesh_importer.h"
2#include "bimg/bimg.h"
3
4#include <graphics/graphics.h>
5#include <logging/logging.h>
6#include <math/math.h>
8
9#include <assimp/DefaultLogger.hpp>
10#include <assimp/GltfMaterial.h>
11#include <assimp/IOStream.hpp>
12#include <assimp/IOSystem.hpp>
13#include <assimp/Importer.hpp>
14#include <assimp/LogStream.hpp>
15#include <assimp/ProgressHandler.hpp>
16#include <assimp/material.h>
17#include <assimp/postprocess.h>
18#include <assimp/scene.h>
19#include <bx/file.h>
21
22#include <algorithm>
24#include <queue>
25#include <tuple>
26
27namespace unravel
28{
29namespace importer
30{
31namespace
32{
33
34// Forward declarations
35void apply_texture_conversion(bimg::ImageContainer* image, const std::string& semantic, bool inverse);
36void process_raw_texture_data(const aiTexture* assimp_tex, const fs::path& output_file,
37 const std::string& semantic, bool inverse);
38void apply_specular_to_metallic_roughness_conversion(bimg::ImageContainer* image);
39auto convert_specular_gloss_to_metallic_roughness(const aiColor3D& diffuse_color,
40 const aiColor3D& specular_color,
41 float glossiness_factor) -> std::tuple<aiColor3D, float, float>;
42
43auto has_rotation_channel(const aiAnimation* animation, const std::string& nodeName) -> bool
44{
45 if(!animation)
46 {
47 return false;
48 }
49
50 for(unsigned int ch = 0; ch < animation->mNumChannels; ++ch)
51 {
52 aiNodeAnim* channel = animation->mChannels[ch];
53 if(!channel)
54 {
55 continue;
56 }
57
58 // Compare the channel's node name with the given nodeName.
59 if(std::string(channel->mNodeName.C_Str()) == nodeName)
60 {
61 // If the channel has position keys, then the node is animated in translation.
62 if(channel->mNumRotationKeys > 1)
63 {
64 return true;
65 }
66 }
67 }
68
69 return false;
70}
71
72auto has_rotation_channel(const aiScene* scene, const std::string& nodeName) -> bool
73{
74 for(unsigned int animIdx = 0; animIdx < scene->mNumAnimations; ++animIdx)
75 {
76 aiAnimation* animation = scene->mAnimations[animIdx];
77
78 if(has_rotation_channel(animation, nodeName))
79 {
80 return true;
81 }
82 }
83 return false;
84}
85
86auto has_trannslation_channel(const aiAnimation* animation, const std::string& nodeName) -> bool
87{
88 if(!animation)
89 {
90 return false;
91 }
92
93 for(unsigned int ch = 0; ch < animation->mNumChannels; ++ch)
94 {
95 aiNodeAnim* channel = animation->mChannels[ch];
96 if(!channel)
97 {
98 continue;
99 }
100
101 // Compare the channel's node name with the given nodeName.
102 if(std::string(channel->mNodeName.C_Str()) == nodeName)
103 {
104 // If the channel has position keys, then the node is animated in translation.
105 if(channel->mNumPositionKeys > 1)
106 {
107 return true;
108 }
109 }
110 }
111
112 return false;
113}
114
115// Helper function to check whether a given node name has an animation channel
116// with translation (position) keys.
117auto has_trannslation_channel(const aiScene* scene, const std::string& nodeName) -> bool
118{
119 for(unsigned int animIdx = 0; animIdx < scene->mNumAnimations; ++animIdx)
120 {
121 aiAnimation* animation = scene->mAnimations[animIdx];
122
123 if(has_trannslation_channel(animation, nodeName))
124 {
125 return true;
126 }
127 }
128 return false;
129}
130
131enum channel_requirement
132{
133 translation,
134 rotation
135};
136
137// Recursive, top-down search: returns the first node (in a depth-first search)
138// that has an animation channel with translation keys.
139auto find_first_animated_node_dfs(aiNode* node,
140 const aiScene* scene,
141 const aiAnimation* animation,
142 channel_requirement req) -> aiNode*
143{
144 if(!node)
145 return nullptr;
146
147 switch(req)
148 {
149 case channel_requirement::translation:
150 // Check if the current node is animated (i.e. has translation keys).
151 if(has_trannslation_channel(animation, std::string(node->mName.C_Str())))
152 {
153 return node;
154 }
155 break;
156 case channel_requirement::rotation:
157 // Check if the current node is animated (i.e. has translation keys).
158 if(has_rotation_channel(animation, std::string(node->mName.C_Str())))
159 {
160 return node;
161 }
162 break;
163 default:
164 // Check if the current node is animated (i.e. has translation keys).
165 if(has_trannslation_channel(animation, std::string(node->mName.C_Str())))
166 {
167 return node;
168 }
169 break;
170 }
171
172 // Recursively search in the children.
173 for(unsigned int i = 0; i < node->mNumChildren; ++i)
174 {
175 aiNode* found = find_first_animated_node_dfs(node->mChildren[i], scene, animation, req);
176 if(found)
177 {
178 return found;
179 }
180 }
181
182 // No matching node found in this branch.
183 return nullptr;
184}
185
186// Top-level function that starts at the scene's root node.
187auto find_root_motion_node_dfs(const aiScene* scene, const aiAnimation* animation, channel_requirement req) -> aiNode*
188{
189 if(!scene || !scene->mRootNode)
190 {
191 return nullptr;
192 }
193
194 return find_first_animated_node_dfs(scene->mRootNode, scene, animation, req);
195}
196
197// Breadth-first search to find the first node (level-by-level) with translation animation.
198auto find_first_animated_node_bfs(const aiScene* scene, const aiAnimation* animation, channel_requirement req)
199 -> aiNode*
200{
201 if(!scene || !scene->mRootNode)
202 {
203 return nullptr;
204 }
205
206 std::queue<aiNode*> nodeQueue;
207 nodeQueue.push(scene->mRootNode);
208
209 while(!nodeQueue.empty())
210 {
211 aiNode* current = nodeQueue.front();
212 nodeQueue.pop();
213
214 switch(req)
215 {
216 case channel_requirement::translation:
217 // Check if the current node is animated (i.e. has translation keys).
218 if(has_trannslation_channel(animation, std::string(current->mName.C_Str())))
219 {
220 return current;
221 }
222 break;
223 case channel_requirement::rotation:
224 // Check if the current node is animated (i.e. has translation keys).
225 if(has_rotation_channel(animation, std::string(current->mName.C_Str())))
226 {
227 return current;
228 }
229 break;
230 default:
231 // Check if the current node is animated (i.e. has translation keys).
232 if(has_trannslation_channel(animation, std::string(current->mName.C_Str())))
233 {
234 return current;
235 }
236 break;
237 }
238
239 // Check if the current node is animated (i.e. has translation keys).
240 if(has_trannslation_channel(animation, std::string(current->mName.C_Str())))
241 {
242 return current;
243 }
244
245 // Enqueue all children of the current node.
246 for(unsigned int i = 0; i < current->mNumChildren; ++i)
247 {
248 nodeQueue.push(current->mChildren[i]);
249 }
250 }
251
252 // If no matching node is found, return nullptr.
253 return nullptr;
254}
255
256// Top-level function that returns the root motion node using breadth-first search.
257auto find_root_motion_node_bfs(const aiScene* scene, const aiAnimation* animation, channel_requirement req) -> aiNode*
258{
259 return find_first_animated_node_bfs(scene, animation, req);
260}
261
262// Helper function to interpolate between two keyframes for position
263auto interpolate_position(float animation_time, const aiNodeAnim* node_anim) -> aiVector3D
264{
265 if(node_anim->mNumPositionKeys == 1)
266 {
267 return node_anim->mPositionKeys[0].mValue;
268 }
269
270 for(unsigned int i = 0; i < node_anim->mNumPositionKeys - 1; ++i)
271 {
272 if(animation_time < (float)node_anim->mPositionKeys[i + 1].mTime)
273 {
274 float time1 = (float)node_anim->mPositionKeys[i].mTime;
275 float time2 = (float)node_anim->mPositionKeys[i + 1].mTime;
276 float factor = (animation_time - time1) / (time2 - time1);
277 const aiVector3D& start = node_anim->mPositionKeys[i].mValue;
278 const aiVector3D& end = node_anim->mPositionKeys[i + 1].mValue;
279 aiVector3D delta = end - start;
280 return start + factor * delta;
281 }
282 }
283 return node_anim->mPositionKeys[0].mValue; // Default to first position
284}
285
286// Helper function to interpolate between two keyframes for rotation
287auto interpolate_rotation(float animation_time, const aiNodeAnim* node_anim) -> aiQuaternion
288{
289 if(node_anim->mNumRotationKeys == 1)
290 {
291 return node_anim->mRotationKeys[0].mValue;
292 }
293
294 for(unsigned int i = 0; i < node_anim->mNumRotationKeys - 1; ++i)
295 {
296 if(animation_time < (float)node_anim->mRotationKeys[i + 1].mTime)
297 {
298 float time1 = (float)node_anim->mRotationKeys[i].mTime;
299 float time2 = (float)node_anim->mRotationKeys[i + 1].mTime;
300 float factor = (animation_time - time1) / (time2 - time1);
301 const aiQuaternion& start = node_anim->mRotationKeys[i].mValue;
302 const aiQuaternion& end = node_anim->mRotationKeys[i + 1].mValue;
303 aiQuaternion result;
304 aiQuaternion::Interpolate(result, start, end, factor);
305 return result.Normalize();
306 }
307 }
308 return node_anim->mRotationKeys[0].mValue; // Default to first rotation
309}
310
311// Helper function to interpolate between two keyframes for scaling
312auto interpolate_scaling(float animation_time, const aiNodeAnim* node_anim) -> aiVector3D
313{
314 if(node_anim->mNumScalingKeys == 1)
315 {
316 return node_anim->mScalingKeys[0].mValue;
317 }
318
319 for(unsigned int i = 0; i < node_anim->mNumScalingKeys - 1; ++i)
320 {
321 if(animation_time < (float)node_anim->mScalingKeys[i + 1].mTime)
322 {
323 float time1 = (float)node_anim->mScalingKeys[i].mTime;
324 float time2 = (float)node_anim->mScalingKeys[i + 1].mTime;
325 float factor = (animation_time - time1) / (time2 - time1);
326 const aiVector3D& start = node_anim->mScalingKeys[i].mValue;
327 const aiVector3D& end = node_anim->mScalingKeys[i + 1].mValue;
328 aiVector3D delta = end - start;
329 return start + factor * delta;
330 }
331 }
332 return node_anim->mScalingKeys[0].mValue; // Default to first scaling
333}
334
335// Find the animation channel that matches the node name (bone)
336auto find_node_anim(const aiAnimation* animation, const aiString& node_name) -> const aiNodeAnim*
337{
338 for(unsigned int i = 0; i < animation->mNumChannels; ++i)
339 {
340 const aiNodeAnim* node_anim = animation->mChannels[i];
341 if(std::string(node_anim->mNodeName.C_Str()) == node_name.C_Str())
342 {
343 return node_anim;
344 }
345 }
346 return nullptr;
347}
348
349// Recursively calculate the bone transform for the current node (bone)
350auto calculate_bone_transform(const aiNode* node,
351 const aiString& bone_name,
352 const aiAnimation* animation,
353 float animation_time,
354 const aiMatrix4x4& parent_transform) -> aiMatrix4x4
355{
356 std::string node_name(node->mName.C_Str());
357
358 // Find the corresponding animation channel for this bone/node
359 const aiNodeAnim* node_anim = find_node_anim(animation, node->mName);
360
361 // Local transformation matrix
362 aiMatrix4x4 local_transform = node->mTransformation;
363
364 // If we have animation data for this node, interpolate the transformation
365 if(node_anim)
366 {
367 // Interpolate translation, rotation, and scaling
368 aiVector3D interpolated_position = interpolate_position(animation_time, node_anim);
369 aiQuaternion interpolated_rotation = interpolate_rotation(animation_time, node_anim);
370 aiVector3D interpolated_scaling = interpolate_scaling(animation_time, node_anim);
371
372 // Build the transformation matrix from interpolated values
373 aiMatrix4x4 position_matrix;
374 aiMatrix4x4::Translation(interpolated_position, position_matrix);
375
376 aiMatrix4x4 rotation_matrix = aiMatrix4x4(interpolated_rotation.GetMatrix());
377
378 aiMatrix4x4 scaling_matrix;
379 aiMatrix4x4::Scaling(interpolated_scaling, scaling_matrix);
380
381 // Combine them into a single local transformation matrix
382 local_transform = position_matrix * rotation_matrix * scaling_matrix;
383 }
384
385 // Combine with parent transformation
386 aiMatrix4x4 global_transform = parent_transform * local_transform;
387
388 // If this node is the bone we're looking for, return the global transformation
389 if(node_name == bone_name.C_Str())
390 {
391 return global_transform;
392 }
393
394 // Recursively calculate the bone transform for all child nodes
395 for(unsigned int i = 0; i < node->mNumChildren; ++i)
396 {
397 auto child_transform =
398 calculate_bone_transform(node->mChildren[i], bone_name, animation, animation_time, global_transform);
399 if(child_transform != aiMatrix4x4())
400 {
401 return child_transform;
402 }
403 }
404
405 // If not found, return identity matrix
406 return aiMatrix4x4();
407}
408
409using animation_bounding_box_map = std::unordered_map<const aiAnimation*, std::vector<math::bbox>>;
410
411auto transform_point(const aiMatrix4x4& transform, const aiVector3D& point) -> math::vec3
412{
413 aiVector3D transformed_point = transform * point;
414 return math::vec3(transformed_point.x, transformed_point.y, transformed_point.z);
415}
416
417auto get_transformed_vertices(const aiMesh* mesh,
418 const aiScene* scene,
419 float time_in_seconds,
420 const aiAnimation* animation) -> std::vector<math::vec3>
421{
422 std::vector<math::vec3> transformed_vertices(mesh->mNumVertices, math::vec3(0.0f));
423
424 // Iterate over bones in the mesh using parallel execution
425 std::for_each(
426 // std::execution::par,
427 mesh->mBones,
428 mesh->mBones + mesh->mNumBones,
429 [&](const aiBone* bone)
430 {
431 aiMatrix4x4 bone_offset = bone->mOffsetMatrix;
432
433 // Calculate or retrieve the cached bone transformation for this frame
434 aiMatrix4x4 bone_transform =
435 calculate_bone_transform(scene->mRootNode, bone->mName, animation, time_in_seconds, aiMatrix4x4());
436
437 // Apply the bone transformation to vertices influenced by this bone
438 std::for_each(bone->mWeights,
439 bone->mWeights + bone->mNumWeights,
440 [&](const aiVertexWeight& weight)
441 {
442 unsigned int vertex_id = weight.mVertexId;
443 float weight_value = weight.mWeight;
444
445 aiVector3D position = mesh->mVertices[vertex_id];
446 math::vec3 transformed_pos = transform_point(bone_transform * bone_offset, position);
447
448 // Accumulate the influence of this bone for each vertex
449 transformed_vertices[vertex_id] += transformed_pos * weight_value;
450 });
451 });
452
453 return transformed_vertices;
454}
455
456// Calculate the bounding box in parallel
457auto calculate_bounding_box(const std::vector<math::vec3>& vertices) -> math::bbox
458{
460
461 // Use parallel execution to find the min/max extents of the bounding box
462 std::for_each(vertices.begin(),
463 vertices.end(),
464 [&](const math::vec3& vertex)
465 {
466 box.add_point(vertex);
467 });
468
469 return box;
470}
471
472// Recursive function to propagate bone influence to child nodes
473void propagate_bone_influence(const aiNode* node, std::unordered_set<std::string>& affected_bones)
474{
475 // Mark this node as affected
476 affected_bones.insert(node->mName.C_Str());
477
478 // Recursively propagate to all child nodes
479 for(unsigned int i = 0; i < node->mNumChildren; ++i)
480 {
481 propagate_bone_influence(node->mChildren[i], affected_bones);
482 }
483}
484
485// Helper function to collect directly and indirectly affected bones (nodes) by the animation
486auto get_affected_bones_and_children(const aiScene* scene, const aiAnimation* animation)
487 -> std::unordered_set<std::string>
488{
489 std::unordered_set<std::string> affected_bones;
490
491 // Step 1: Collect directly affected bones (from animation channels)
492 for(unsigned int i = 0; i < animation->mNumChannels; ++i)
493 {
494 const aiNodeAnim* node_anim = animation->mChannels[i];
495 affected_bones.insert(node_anim->mNodeName.C_Str());
496
497 // Step 2: Find the corresponding node in the scene and propagate influence to its children
498 const aiNode* affected_node = scene->mRootNode->FindNode(node_anim->mNodeName);
499 if(affected_node)
500 {
501 propagate_bone_influence(affected_node, affected_bones); // Recursively mark all children
502 }
503 }
504
505 return affected_bones;
506}
507
508// Function to check if a mesh is affected by the animation (directly or indirectly)
509auto is_mesh_affected_by_animation(const aiMesh* mesh, const std::unordered_set<std::string>& affected_bones) -> bool
510{
511 for(unsigned int i = 0; i < mesh->mNumBones; ++i)
512 {
513 if(affected_bones.find(mesh->mBones[i]->mName.C_Str()) != affected_bones.end())
514 {
515 return true; // This mesh is influenced by at least one bone affected by the animation
516 }
517 }
518 return false; // No bones from this mesh are affected by the animation
519}
520
521auto get_affected_meshes(const aiScene* scene,
522 const aiAnimation* animation,
523 const std::unordered_set<std::string>& affected_bones)
524{
525 std::vector<const aiMesh*> affected_meshes;
526 for(unsigned int mesh_index = 0; mesh_index < scene->mNumMeshes; ++mesh_index)
527 {
528 const aiMesh* mesh = scene->mMeshes[mesh_index];
529
530 // Skip the mesh if it is not affected by the animation
531 if(is_mesh_affected_by_animation(mesh, affected_bones))
532 {
533 affected_meshes.emplace_back(mesh);
534 }
535 }
536
537 return affected_meshes;
538}
539
540// Main function to compute bounding boxes for animations, skipping unaffected meshes
541auto compute_bounding_boxes_for_animations(const aiScene* scene, float sample_interval = 0.2f)
542 -> animation_bounding_box_map
543{
544 APPLOG_TRACE_PERF(std::chrono::seconds);
545
546 animation_bounding_box_map animation_bounding_boxes;
547
548 if(!scene->HasAnimations())
549 {
550 return animation_bounding_boxes;
551 }
552
553 float total_steps = 0;
554 for(unsigned int anim_index = 0; anim_index < scene->mNumAnimations; ++anim_index)
555 {
556 const aiAnimation* animation = scene->mAnimations[anim_index];
557
558 animation_bounding_boxes[animation].clear();
559
560 float animation_duration = (float)animation->mDuration;
561 float ticks_per_second = (animation->mTicksPerSecond != 0.0f) ? (float)animation->mTicksPerSecond : 25.0f;
562 float steps = animation_duration / (sample_interval * ticks_per_second);
563 total_steps += steps;
564 }
565
566 std::atomic<size_t> current_steps = 0;
567
568 std::for_each(
569 // std::execution::par,
570 scene->mAnimations,
571 scene->mAnimations + scene->mNumAnimations,
572 [&](const aiAnimation* animation)
573 {
574 float animation_duration = (float)animation->mDuration;
575 float ticks_per_second = (animation->mTicksPerSecond != 0.0f) ? (float)animation->mTicksPerSecond : 25.0f;
576 float steps = animation_duration / (sample_interval * ticks_per_second);
577
578 auto& boxes = animation_bounding_boxes[animation];
579 boxes.reserve(size_t(steps));
580
581 // Collect the bones affected by the animation (both direct and indirect)
582 auto affected_bones = get_affected_bones_and_children(scene, animation);
583 auto affected_meshes = get_affected_meshes(scene, animation, affected_bones);
584 // For each keyframe (or sample the animation at regular intervals)
585 // for(float time = 0.0f; time <= animation_duration; time += (sample_interval * ticks_per_second))
586 {
587 float time = 0.0f;
588 float percent = (float(current_steps) / total_steps) * 100.0f;
589
590 for(const auto& mesh : affected_meshes)
591 {
592 // Get transformed vertices for this time/frame
593 auto transformed_vertices = get_transformed_vertices(mesh, scene, time, animation);
594
595 // Compute the bounding box for this frame
596 auto frame_bounding_box = calculate_bounding_box(transformed_vertices);
597
598 // Inflate the box by some margin to account for skipped frames
599 frame_bounding_box.inflate(frame_bounding_box.get_extents() * 0.05f);
600
601 // Store the bounding box (for later use)
602 boxes.push_back(frame_bounding_box);
603 }
604
605 // APPLOG_TRACE("Mesh Importer : Animation precompute bounding box progress {:.2f}%", percent);
606 current_steps++;
607 }
608 });
609
610 return animation_bounding_boxes;
611}
612
613// Helper function to get the file extension from the compressed texture format
614
615auto get_texture_extension_from_texture(const aiTexture* texture) -> std::string
616{
617 if(texture->achFormatHint[0] != '\0')
618 {
619 return std::string(".") + texture->achFormatHint;
620 }
621 return ".tga"; // Fallback extension raw
622}
623
624auto get_texture_extension(const aiTexture* texture) -> std::string
625{
626 auto extension = get_texture_extension_from_texture(texture);
627
628 if(extension == ".jpg" || extension == ".jpeg")
629 {
630 extension = ".dds";
631 }
632
633 return extension;
634}
635
636auto get_embedded_texture_name(const aiTexture* texture,
637 size_t index,
638 const fs::path& filename,
639 const std::string& semantic) -> std::string
640{
641 return fmt::format("[{}] {} {}{}", index, semantic, filename.string(), get_texture_extension(texture));
642}
643
644auto process_matrix(const aiMatrix4x4& assimp_matrix) -> math::mat4
645{
646 math::mat4 matrix;
647
648 matrix[0][0] = assimp_matrix.a1;
649 matrix[1][0] = assimp_matrix.a2;
650 matrix[2][0] = assimp_matrix.a3;
651 matrix[3][0] = assimp_matrix.a4;
652
653 matrix[0][1] = assimp_matrix.b1;
654 matrix[1][1] = assimp_matrix.b2;
655 matrix[2][1] = assimp_matrix.b3;
656 matrix[3][1] = assimp_matrix.b4;
657
658 matrix[0][2] = assimp_matrix.c1;
659 matrix[1][2] = assimp_matrix.c2;
660 matrix[2][2] = assimp_matrix.c3;
661 matrix[3][2] = assimp_matrix.c4;
662
663 matrix[0][3] = assimp_matrix.d1;
664 matrix[1][3] = assimp_matrix.d2;
665 matrix[2][3] = assimp_matrix.d3;
666 matrix[3][3] = assimp_matrix.d4;
667
668 return matrix;
669}
670
671void process_vertices(aiMesh* mesh, mesh::load_data& load_data)
672{
673 auto& submesh = load_data.submeshes.back();
674
675 // Determine the correct offset to any relevant elements in the vertex
676 bool has_position = load_data.vertex_format.has(gfx::attribute::Position);
677 bool has_normal = load_data.vertex_format.has(gfx::attribute::Normal);
678 bool has_bitangent = load_data.vertex_format.has(gfx::attribute::Bitangent);
679 bool has_tangent = load_data.vertex_format.has(gfx::attribute::Tangent);
680 bool has_texcoord0 = load_data.vertex_format.has(gfx::attribute::TexCoord0);
681 auto vertex_stride = load_data.vertex_format.getStride();
682
683 std::uint32_t current_vertex = load_data.vertex_count;
684 load_data.vertex_count += mesh->mNumVertices;
685 load_data.vertex_data.resize(load_data.vertex_count * vertex_stride);
686
687 std::uint8_t* current_vertex_ptr = load_data.vertex_data.data() + current_vertex * vertex_stride;
688
689 for(size_t i = 0; i < mesh->mNumVertices; ++i, current_vertex_ptr += vertex_stride)
690 {
691 // position
692 if(mesh->HasPositions() && has_position)
693 {
694 float position[4];
695 std::memcpy(position, &mesh->mVertices[i], sizeof(aiVector3D));
696
697 gfx::vertex_pack(position, false, gfx::attribute::Position, load_data.vertex_format, current_vertex_ptr);
698
699 submesh.bbox.add_point(math::vec3(position[0], position[1], position[2]));
700 }
701
702 // tex coords
703 if(mesh->HasTextureCoords(0) && has_texcoord0)
704 {
705 float textureCoords[4];
706 std::memcpy(textureCoords, &mesh->mTextureCoords[0][i], sizeof(aiVector2D));
707
708 gfx::vertex_pack(textureCoords,
709 true,
710 gfx::attribute::TexCoord0,
711 load_data.vertex_format,
712 current_vertex_ptr);
713 }
714
716 math::vec4 normal;
717 if(mesh->HasNormals() && has_normal)
718 {
719 std::memcpy(math::value_ptr(normal), &mesh->mNormals[i], sizeof(aiVector3D));
720
721 gfx::vertex_pack(math::value_ptr(normal),
722 true,
723 gfx::attribute::Normal,
724 load_data.vertex_format,
725 current_vertex_ptr);
726 }
727
728 math::vec4 tangent;
729 // tangents
730 if(mesh->HasTangentsAndBitangents() && has_tangent)
731 {
732 std::memcpy(math::value_ptr(tangent), &mesh->mTangents[i], sizeof(aiVector3D));
733 tangent.w = 1.0f;
734
735 gfx::vertex_pack(math::value_ptr(tangent),
736 true,
737 gfx::attribute::Tangent,
738 load_data.vertex_format,
739 current_vertex_ptr);
740 }
741
742 // binormals
743 math::vec4 bitangent;
744 if(mesh->HasTangentsAndBitangents() && has_bitangent)
745 {
746 std::memcpy(math::value_ptr(bitangent), &mesh->mBitangents[i], sizeof(aiVector3D));
747 float handedness =
748 math::dot(math::vec3(bitangent), math::normalize(math::cross(math::vec3(normal), math::vec3(tangent))));
749 tangent.w = handedness;
750
751 gfx::vertex_pack(math::value_ptr(bitangent),
752 true,
753 gfx::attribute::Bitangent,
754 load_data.vertex_format,
755 current_vertex_ptr);
756 }
757 }
758}
759
760void process_faces(aiMesh* mesh, std::uint32_t submesh_offset, mesh::load_data& load_data)
761{
762 load_data.triangle_count += mesh->mNumFaces;
763
764 load_data.triangle_data.reserve(load_data.triangle_data.size() + mesh->mNumFaces);
765
766 for(size_t i = 0; i < mesh->mNumFaces; ++i)
767 {
768 aiFace face = mesh->mFaces[i];
769
770 auto& triangle = load_data.triangle_data.emplace_back();
771 triangle.data_group_id = mesh->mMaterialIndex;
772
773 auto num_indices = std::min<size_t>(face.mNumIndices, 3);
774 for(size_t j = 0; j < num_indices; ++j)
775 {
776 triangle.indices[j] = face.mIndices[j] + submesh_offset;
777 }
778 }
779}
780
781void process_bones(aiMesh* mesh, std::uint32_t submesh_offset, mesh::load_data& load_data)
782{
783 if(mesh->HasBones())
784 {
785 auto& bone_influences = load_data.skin_data.get_bones();
786
787 for(size_t i = 0; i < mesh->mNumBones; ++i)
788 {
789 aiBone* assimp_bone = mesh->mBones[i];
790 const std::string bone_name = assimp_bone->mName.C_Str();
791
792 auto it = std::find_if(std::begin(bone_influences),
793 std::end(bone_influences),
794 [&bone_name](const auto& bone)
795 {
796 return bone_name == bone.bone_id;
797 });
798
799 skin_bind_data::bone_influence* bone_ptr = nullptr;
800 if(it != std::end(bone_influences))
801 {
802 bone_ptr = &(*it);
803 }
804 else
805 {
806 const auto& assimp_matrix = assimp_bone->mOffsetMatrix;
807 skin_bind_data::bone_influence bone_influence;
808 bone_influence.bone_id = bone_name;
809 bone_influence.bind_pose_transform = process_matrix(assimp_matrix);
810 bone_influences.emplace_back(std::move(bone_influence));
811 bone_ptr = &bone_influences.back();
812 }
813
814 if(bone_ptr == nullptr)
815 {
816 continue;
817 }
818
819 for(size_t j = 0; j < assimp_bone->mNumWeights; ++j)
820 {
821 aiVertexWeight assimp_influence = assimp_bone->mWeights[j];
822
823 skin_bind_data::vertex_influence influence;
824 influence.vertex_index = assimp_influence.mVertexId + submesh_offset;
825 influence.weight = assimp_influence.mWeight;
826
827 bone_ptr->influences.emplace_back(influence);
828 }
829 }
830 }
831}
832
833void process_mesh(aiMesh* mesh, mesh::load_data& load_data)
834{
835 load_data.submeshes.emplace_back();
836 auto& submesh = load_data.submeshes.back();
837 submesh.vertex_start = load_data.vertex_count;
838 submesh.vertex_count = mesh->mNumVertices;
839 submesh.face_start = load_data.triangle_count;
840 submesh.face_count = mesh->mNumFaces;
841 submesh.data_group_id = mesh->mMaterialIndex;
842 submesh.skinned = mesh->HasBones();
843 load_data.material_count = std::max(load_data.material_count, submesh.data_group_id + 1);
844
845 process_faces(mesh, submesh.vertex_start, load_data);
846 process_bones(mesh, submesh.vertex_start, load_data);
847 process_vertices(mesh, load_data);
848}
849
850void process_meshes(const aiScene* scene, mesh::load_data& load_data)
851{
852 for(size_t i = 0; i < scene->mNumMeshes; ++i)
853 {
854 aiMesh* mesh = scene->mMeshes[i];
855 process_mesh(mesh, load_data);
856 }
857}
858
859void process_node(const aiScene* scene,
860 mesh::load_data& load_data,
861 const aiNode* node,
862 const std::unique_ptr<mesh::armature_node>& armature_node,
863 const math::transform& parent_transform,
864 std::unordered_map<std::string, unsigned int>& node_to_index_lut)
865{
866 armature_node->name = node->mName.C_Str();
867 armature_node->local_transform = process_matrix(node->mTransformation);
868 armature_node->children.resize(node->mNumChildren);
869 armature_node->index = node_to_index_lut[armature_node->name];
870 auto resolved_transform = parent_transform * armature_node->local_transform;
871
872 for(uint32_t i = 0; i < node->mNumMeshes; ++i)
873 {
874 uint32_t submesh_index = node->mMeshes[i];
875 armature_node->submeshes.emplace_back(submesh_index);
876
877 auto& submesh = load_data.submeshes[submesh_index];
878 submesh.node_id = node->mName.C_Str();
879
880 auto transformed_bbox = math::bbox::mul(submesh.bbox, resolved_transform);
881 load_data.bbox.add_point(transformed_bbox.min);
882 load_data.bbox.add_point(transformed_bbox.max);
883 }
884
885 for(size_t i = 0; i < node->mNumChildren; ++i)
886 {
887 armature_node->children[i] = std::make_unique<mesh::armature_node>();
888 process_node(scene,
889 load_data,
890 node->mChildren[i],
891 armature_node->children[i],
892 resolved_transform,
893 node_to_index_lut);
894 }
895}
896
897void process_nodes(const aiScene* scene,
898 mesh::load_data& load_data,
899 std::unordered_map<std::string, unsigned int>& node_to_index_lut)
900{
901 size_t index = 0;
902 if(scene->mRootNode != nullptr)
903 {
904 load_data.bbox = {};
905 load_data.root_node = std::make_unique<mesh::armature_node>();
906
907 process_node(scene,
908 load_data,
909 scene->mRootNode,
910 load_data.root_node,
912 node_to_index_lut);
913
914 auto get_axis = [&](const std::string& name, math::vec3 fallback)
915 {
916 if(!scene->mMetaData)
917 {
918 return fallback;
919 }
920
921 int axis = 0;
922 if(!scene->mMetaData->Get<int>(name, axis))
923 {
924 return fallback;
925 }
926 int axis_sign = 1;
927 if(!scene->mMetaData->Get<int>(name + "Sign", axis_sign))
928 {
929 return fallback;
930 }
931 math::vec3 result{0.0f, 0.0f, 0.0f};
932
933 if(axis < 0 || axis >= 3)
934 {
935 return fallback;
936 }
937
938 result[axis] = float(axis_sign);
939
940 return result;
941 };
942 auto x_axis = get_axis("CoordAxis", {1.0f, 0.0f, 0.0f});
943 auto y_axis = get_axis("UpAxis", {0.0f, 1.0f, 0.0f});
944 auto z_axis = get_axis("FrontAxis", {0.0f, 0.0f, 1.0f});
945 // load_data.root_node->local_transform.set_rotation(x_axis, y_axis, z_axis);
946 }
947}
948
949void dfs_assign_indices(const aiNode* node,
950 std::unordered_map<std::string, unsigned int>& node_indices,
951 unsigned int& current_index)
952{
953 // Assign the current index to this node
954 node_indices[node->mName.C_Str()] = current_index;
955
956 // Increment the index for the next node
957 current_index++;
958
959 // Recursively visit all children (DFS)
960 for(unsigned int i = 0; i < node->mNumChildren; ++i)
961 {
962 dfs_assign_indices(node->mChildren[i], node_indices, current_index);
963 }
964}
965
966auto assign_node_indices(const aiScene* scene) -> std::unordered_map<std::string, unsigned int>
967{
968 std::unordered_map<std::string, unsigned int> node_indices;
969 unsigned int current_index = 0;
970
971 // Start DFS traversal from the root node
972 if(scene->mRootNode)
973 {
974 dfs_assign_indices(scene->mRootNode, node_indices, current_index);
975 }
976
977 return node_indices;
978}
979
980auto is_node_a_bone(const std::string& node_name, const aiScene* scene) -> bool
981{
982 for(unsigned int i = 0; i < scene->mNumMeshes; ++i)
983 {
984 const aiMesh* mesh = scene->mMeshes[i];
985 for(unsigned int j = 0; j < mesh->mNumBones; ++j)
986 {
987 if(mesh->mBones[j]->mName.C_Str() == node_name)
988 {
989 return true;
990 }
991 }
992 }
993 return false;
994}
995
996auto is_node_a_parent_of_bone(const std::string& node_name, const aiScene* scene) -> bool
997{
998 for(unsigned int i = 0; i < scene->mNumMeshes; ++i)
999 {
1000 const aiMesh* mesh = scene->mMeshes[i];
1001 for(unsigned int j = 0; j < mesh->mNumBones; ++j)
1002 {
1003 const aiNode* bone_node = scene->mRootNode->FindNode(mesh->mBones[j]->mName);
1004 const aiNode* current_node = bone_node;
1005
1006 while(current_node != nullptr)
1007 {
1008 if(current_node->mName.C_Str() == node_name)
1009 {
1010 return true;
1011 }
1012 current_node = current_node->mParent;
1013 }
1014 }
1015 }
1016 return false;
1017}
1018
1019auto is_node_a_submesh(const std::string& node_name, const aiScene* scene) -> bool
1020{
1021 const aiNode* node = scene->mRootNode->FindNode(node_name.c_str());
1022 return node != nullptr && node->mNumMeshes > 0;
1023}
1024
1025auto is_node_a_parent_of_submesh(const std::string& node_name, const aiScene* scene) -> bool
1026{
1027 const aiNode* root = scene->mRootNode;
1028
1029 for(unsigned int i = 0; i < scene->mNumMeshes; ++i)
1030 {
1031 const aiMesh* mesh = scene->mMeshes[i];
1032 const aiNode* submesh_node = root->FindNode(mesh->mName);
1033 const aiNode* current_node = submesh_node;
1034
1035 while(current_node != nullptr)
1036 {
1037 if(current_node->mName.C_Str() == node_name)
1038 {
1039 return true;
1040 }
1041 current_node = current_node->mParent;
1042 }
1043 }
1044 return false;
1045}
1046
1047void process_animation(const aiScene* scene,
1048 const fs::path& filename,
1049 const aiAnimation* assimp_anim,
1050 mesh::load_data& load_data,
1051 std::unordered_map<std::string, unsigned int>& node_to_index_lut,
1052 animation_clip& anim)
1053{
1054 auto fixed_name = filename.string() + "_" + string_utils::replace(assimp_anim->mName.C_Str(), ".", "_");
1055 anim.name = fixed_name;
1056 auto ticks_per_second = assimp_anim->mTicksPerSecond;
1057 if(ticks_per_second < 0.001)
1058 {
1059 ticks_per_second = 25.0;
1060 }
1061
1062 auto ticks = assimp_anim->mDuration;
1063
1064 anim.duration = decltype(anim.duration)(ticks / ticks_per_second);
1065
1066 if(assimp_anim->mNumChannels > 0)
1067 {
1068 anim.channels.reserve(assimp_anim->mNumChannels);
1069 }
1070 bool needs_sort = false;
1071
1072 size_t skipped = 0;
1073 for(size_t i = 0; i < assimp_anim->mNumChannels; ++i)
1074 {
1075 const aiNodeAnim* assimp_node_anim = assimp_anim->mChannels[i];
1076
1077 bool is_bone = is_node_a_bone(assimp_node_anim->mNodeName.C_Str(), scene);
1078 bool is_parent_of_bone = is_node_a_parent_of_bone(assimp_node_anim->mNodeName.C_Str(), scene);
1079 bool is_submesh = is_node_a_submesh(assimp_node_anim->mNodeName.C_Str(), scene);
1080 bool is_parent_of_submesh = is_node_a_parent_of_submesh(assimp_node_anim->mNodeName.C_Str(), scene);
1081
1082 bool is_relevant = is_bone || is_parent_of_bone || is_submesh || is_parent_of_submesh;
1083
1084 // skip frames for non relevant nodes
1085 if(!is_relevant)
1086 {
1087 skipped++;
1088 continue;
1089 }
1090
1091 auto& node_anim = anim.channels.emplace_back();
1092 node_anim.node_name = assimp_node_anim->mNodeName.C_Str();
1093 node_anim.node_index = node_to_index_lut[node_anim.node_name];
1094 if(!needs_sort && anim.channels.size() > 1)
1095 {
1096 auto& prev_node_anim = anim.channels[anim.channels.size() - 2];
1097 if(node_anim.node_index < prev_node_anim.node_index)
1098 {
1099 needs_sort = true;
1100 }
1101 }
1102
1103 if(assimp_node_anim->mNumPositionKeys > 0)
1104 {
1105 node_anim.position_keys.resize(assimp_node_anim->mNumPositionKeys);
1106 }
1107
1108 for(size_t idx = 0; idx < assimp_node_anim->mNumPositionKeys; ++idx)
1109 {
1110 const auto& anim_key = assimp_node_anim->mPositionKeys[idx];
1111 auto& key = node_anim.position_keys[idx];
1112 key.time = decltype(key.time)(anim_key.mTime / ticks_per_second);
1113 key.value.x = anim_key.mValue.x;
1114 key.value.y = anim_key.mValue.y;
1115 key.value.z = anim_key.mValue.z;
1116 }
1117
1118 if(assimp_node_anim->mNumRotationKeys > 0)
1119 {
1120 node_anim.rotation_keys.resize(assimp_node_anim->mNumRotationKeys);
1121 }
1122
1123 for(size_t idx = 0; idx < assimp_node_anim->mNumRotationKeys; ++idx)
1124 {
1125 const auto& anim_key = assimp_node_anim->mRotationKeys[idx];
1126 auto& key = node_anim.rotation_keys[idx];
1127 key.time = decltype(key.time)(anim_key.mTime / ticks_per_second);
1128 key.value.x = anim_key.mValue.x;
1129 key.value.y = anim_key.mValue.y;
1130 key.value.z = anim_key.mValue.z;
1131 key.value.w = anim_key.mValue.w;
1132 }
1133
1134 if(assimp_node_anim->mNumScalingKeys > 0)
1135 {
1136 node_anim.scaling_keys.resize(assimp_node_anim->mNumScalingKeys);
1137 }
1138
1139 for(size_t idx = 0; idx < assimp_node_anim->mNumScalingKeys; ++idx)
1140 {
1141 const auto& anim_key = assimp_node_anim->mScalingKeys[idx];
1142 auto& key = node_anim.scaling_keys[idx];
1143 key.time = decltype(key.time)(anim_key.mTime / ticks_per_second);
1144 key.value.x = anim_key.mValue.x;
1145 key.value.y = anim_key.mValue.y;
1146 key.value.z = anim_key.mValue.z;
1147 }
1148 }
1149
1150 auto root_motion_translation_candidate =
1151 find_root_motion_node_bfs(scene, assimp_anim, channel_requirement::translation);
1152 auto root_motion_rotation_candidate = find_root_motion_node_bfs(scene, assimp_anim, channel_requirement::rotation);
1153
1154 if(root_motion_translation_candidate)
1155 {
1156 anim.root_motion.position_node_name = root_motion_translation_candidate->mName.C_Str();
1157 anim.root_motion.position_node_index = node_to_index_lut[anim.root_motion.position_node_name];
1158 }
1159 if(root_motion_rotation_candidate)
1160 {
1161 anim.root_motion.rotation_node_name = root_motion_rotation_candidate->mName.C_Str();
1162 anim.root_motion.rotation_node_index = node_to_index_lut[anim.root_motion.rotation_node_name];
1163 }
1164
1165 if(needs_sort)
1166 {
1167 std::sort(anim.channels.begin(),
1168 anim.channels.end(),
1169 [](const auto& lhs, const auto& rhs)
1170 {
1171 return lhs.node_index < rhs.node_index;
1172 });
1173 }
1174
1175 APPLOG_TRACE("Mesh Importer : Animation {} discarded {} non relevat node keys", anim.name, skipped);
1176}
1177void process_animations(const aiScene* scene,
1178 const fs::path& filename,
1179 mesh::load_data& load_data,
1180 std::unordered_map<std::string, unsigned int>& node_to_index_lut,
1181 std::vector<animation_clip>& animations)
1182{
1183 if(scene->mNumAnimations > 0)
1184 {
1185 animations.resize(scene->mNumAnimations);
1186 }
1187
1188 for(size_t i = 0; i < scene->mNumAnimations; ++i)
1189 {
1190 const aiAnimation* assimp_anim = scene->mAnimations[i];
1191 auto& anim = animations[i];
1192 process_animation(scene, filename, assimp_anim, load_data, node_to_index_lut, anim);
1193 }
1194}
1195
1196void process_embedded_texture(const aiTexture* assimp_tex,
1197 size_t assimp_tex_idx,
1198 const fs::path& filename,
1199 const fs::path& output_dir,
1200 std::vector<imported_texture>& textures)
1201{
1202 imported_texture texture{};
1203 auto it = std::find_if(std::begin(textures),
1204 std::end(textures),
1205 [&](const imported_texture& texture)
1206 {
1207 return texture.embedded_index == assimp_tex_idx;
1208 });
1209 if(it != std::end(textures))
1210 {
1211 if(it->process_count > 0)
1212 {
1213 return;
1214 }
1215
1216 it->process_count++;
1217 texture = *it;
1218 }
1219 else if(assimp_tex->mFilename.length > 0)
1220 {
1221 texture.name = fs::path(assimp_tex->mFilename.C_Str()).filename().string();
1222 }
1223 else
1224 {
1225 texture.name = get_embedded_texture_name(assimp_tex, assimp_tex_idx, filename, "Texture");
1226 }
1227
1228 fs::path output_file = output_dir / texture.name;
1229
1230 if(assimp_tex->pcData)
1231 {
1232 bool compressed = assimp_tex->mHeight == 0;
1233 bool raw = assimp_tex->mHeight > 0;
1234
1235 if(compressed)
1236 {
1237 // Compressed texture (e.g., PNG, JPEG)
1238 size_t texture_size = assimp_tex->mWidth;
1239
1240 // Parse the image using bimg
1241 bimg::ImageContainer* image = imageLoad(assimp_tex->pcData, static_cast<uint32_t>(texture_size));
1242 if(image)
1243 {
1244 // Apply workflow-specific texture conversions
1245 apply_texture_conversion(image, texture.semantic, texture.inverse);
1246
1247 imageSave(output_file.string().c_str(), image);
1248
1249 bimg::imageFree(image);
1250 }
1251 }
1252 else if(raw)
1253 {
1254 // Uncompressed texture (e.g., raw RGBA)
1255 // For raw data, we need to process it differently
1256 process_raw_texture_data(assimp_tex, output_file, texture.semantic, texture.inverse);
1257 }
1258 }
1259}
1260
1264namespace pixel_transforms
1265{
1269 template<typename TransformFunc>
1270 void transform_pixel(uint8_t* pixel_data, uint32_t bytes_per_pixel, TransformFunc transform_func)
1271 {
1272 if (bytes_per_pixel >= 4)
1273 {
1274 // RGBA format
1275 float r = pixel_data[0] / 255.0f;
1276 float g = pixel_data[1] / 255.0f;
1277 float b = pixel_data[2] / 255.0f;
1278 float a = pixel_data[3] / 255.0f;
1279
1280 auto [new_r, new_g, new_b, new_a] = transform_func(r, g, b, a);
1281
1282 pixel_data[0] = static_cast<uint8_t>(math::clamp(new_r, 0.0f, 1.0f) * 255.0f);
1283 pixel_data[1] = static_cast<uint8_t>(math::clamp(new_g, 0.0f, 1.0f) * 255.0f);
1284 pixel_data[2] = static_cast<uint8_t>(math::clamp(new_b, 0.0f, 1.0f) * 255.0f);
1285 pixel_data[3] = static_cast<uint8_t>(math::clamp(new_a, 0.0f, 1.0f) * 255.0f);
1286 }
1287 else if (bytes_per_pixel >= 3)
1288 {
1289 // RGB format
1290 float r = pixel_data[0] / 255.0f;
1291 float g = pixel_data[1] / 255.0f;
1292 float b = pixel_data[2] / 255.0f;
1293 float a = 1.0f; // Default alpha
1294
1295 auto [new_r, new_g, new_b, new_a] = transform_func(r, g, b, a);
1296
1297 pixel_data[0] = static_cast<uint8_t>(math::clamp(new_r, 0.0f, 1.0f) * 255.0f);
1298 pixel_data[1] = static_cast<uint8_t>(math::clamp(new_g, 0.0f, 1.0f) * 255.0f);
1299 pixel_data[2] = static_cast<uint8_t>(math::clamp(new_b, 0.0f, 1.0f) * 255.0f);
1300 }
1301 else if (bytes_per_pixel == 2)
1302 {
1303 // Grayscale + Alpha format
1304 float luminance = pixel_data[0] / 255.0f;
1305 float a = pixel_data[1] / 255.0f;
1306
1307 auto [new_r, new_g, new_b, new_a] = transform_func(luminance, luminance, luminance, a);
1308
1309 pixel_data[0] = static_cast<uint8_t>(math::clamp(new_r, 0.0f, 1.0f) * 255.0f); // Use red as luminance
1310 pixel_data[1] = static_cast<uint8_t>(math::clamp(new_a, 0.0f, 1.0f) * 255.0f);
1311 }
1312 else if (bytes_per_pixel == 1)
1313 {
1314 // Grayscale format
1315 float luminance = pixel_data[0] / 255.0f;
1316
1317 auto [new_r, new_g, new_b, new_a] = transform_func(luminance, luminance, luminance, 1.0f);
1318
1319 pixel_data[0] = static_cast<uint8_t>(math::clamp(new_r, 0.0f, 1.0f) * 255.0f);
1320 }
1321 }
1322
1326 auto specular_to_metallic_pixel(float r, float g, float b, float a) -> std::tuple<float, float, float, float>
1327 {
1328 // Calculate metallic from specular using Khronos official approach
1329 float max_specular = std::max({r, g, b});
1330 float avg_specular = (r + g + b) / 3.0f;
1331 float color_variance = std::abs(r - avg_specular) + std::abs(g - avg_specular) + std::abs(b - avg_specular);
1332
1333 float metallic = 0.0f;
1334 const float dielectric_f0 = 0.04f; // Official dielectric F0 threshold
1335
1336 if (max_specular <= dielectric_f0)
1337 {
1338 metallic = 0.0f; // Definitely dielectric
1339 }
1340 else if (max_specular >= 0.9f)
1341 {
1342 metallic = 1.0f; // Definitely metallic
1343 }
1344 else
1345 {
1346 // Use smooth transition based on official approach
1347 float normalized_specular = (max_specular - dielectric_f0) / (1.0f - dielectric_f0);
1348 metallic = math::clamp(normalized_specular, 0.0f, 1.0f);
1349
1350 // If specular has significant color (not grayscale), boost metallic
1351 if (color_variance > 0.1f && avg_specular > 0.3f)
1352 {
1353 metallic = std::max(metallic, 0.8f); // Strong indication of metal
1354 }
1355 }
1356
1357 // Store metallic value in RGB channels
1358 return std::make_tuple(metallic, metallic, metallic, 1.0f);
1359 }
1360
1364 auto gloss_to_roughness_pixel(float r, float g, float b, float a) -> std::tuple<float, float, float, float>
1365 {
1366 // Convert glossiness to roughness: Roughness = 1 - Glossiness
1367
1368 // Check if it's a greyscale glossiness map
1369 if (std::abs(r - g) < 0.01f && std::abs(g - b) < 0.01f)
1370 {
1371 // Convert all RGB channels
1372 float roughness = 1.0f - r;
1373 return std::make_tuple(roughness, roughness, roughness, a);
1374 }
1375 else
1376 {
1377 // Check alpha channel for glossiness (common in specular/gloss workflows)
1378 if (a < 1.0f)
1379 {
1380 float roughness = 1.0f - a;
1381 return std::make_tuple(r, g, b, roughness);
1382 }
1383 else
1384 {
1385 // Assume green channel contains glossiness (common convention)
1386 float roughness = 1.0f - g;
1387 return std::make_tuple(r, roughness, b, a);
1388 }
1389 }
1390 }
1391
1395 auto specular_to_roughness_pixel(float r, float g, float b, float a) -> std::tuple<float, float, float, float>
1396 {
1397 float roughness = 0.0f;
1398
1399 // Method 1: If alpha channel has meaningful data, use it as gloss and invert
1400 if (a < 1.0f)
1401 {
1402 roughness = 1.0f - a; // Roughness = 1 - Gloss
1403 }
1404 else
1405 {
1406 // Method 2: Convert specular intensity to roughness estimate
1407 float specular_intensity = (r + g + b) / 3.0f;
1408 roughness = 1.0f - specular_intensity;
1409 }
1410
1411 return std::make_tuple(roughness, roughness, roughness, 1.0f);
1412 }
1413
1417 auto specular_to_metallic_roughness_pixel(float r, float g, float b, float a) -> std::tuple<float, float, float, float>
1418 {
1419 // Calculate metallic from specular using Khronos official approach
1420 float max_specular = std::max({r, g, b});
1421 float avg_specular = (r + g + b) / 3.0f;
1422 float color_variance = std::abs(r - avg_specular) + std::abs(g - avg_specular) + std::abs(b - avg_specular);
1423
1424 float metallic = 0.0f;
1425 const float dielectric_f0 = 0.04f; // Official dielectric F0 threshold
1426
1427 if (max_specular <= dielectric_f0)
1428 {
1429 metallic = 0.0f; // Definitely dielectric
1430 }
1431 else if (max_specular >= 0.9f)
1432 {
1433 metallic = 1.0f; // Definitely metallic
1434 }
1435 else
1436 {
1437 // Use smooth transition based on official approach
1438 float normalized_specular = (max_specular - dielectric_f0) / (1.0f - dielectric_f0);
1439 metallic = math::clamp(normalized_specular, 0.0f, 1.0f);
1440
1441 // If specular has significant color (not grayscale), boost metallic
1442 if (color_variance > 0.1f && avg_specular > 0.3f)
1443 {
1444 metallic = std::max(metallic, 0.8f); // Strong indication of metal
1445 }
1446 }
1447
1448 // Calculate roughness from alpha (gloss) or intensity
1449 float roughness = 0.0f;
1450 if (a < 1.0f) // Alpha channel has gloss data
1451 {
1452 roughness = 1.0f - a; // Roughness = 1 - Gloss
1453 }
1454 else
1455 {
1456 // Fallback: use inverse of specular intensity
1457 roughness = 1.0f - avg_specular;
1458 }
1459
1460 // Store in glTF convention: R=Occlusion(unused), G=Roughness, B=Metallic, A=1.0
1461 return std::make_tuple(1.0f, roughness, metallic, 1.0f);
1462 }
1463
1467 auto simple_invert_pixel(float r, float g, float b, float a) -> std::tuple<float, float, float, float>
1468 {
1469 return std::make_tuple(1.0f - r, 1.0f - g, 1.0f - b, 1.0f - a);
1470 }
1471}
1472
1476void apply_texture_conversion(bimg::ImageContainer* image, const std::string& semantic, bool inverse)
1477{
1478 if(!image || !image->m_data)
1479 {
1480 return;
1481 }
1482
1483 uint8_t* image_data = static_cast<uint8_t*>(image->m_data);
1484 uint32_t pixel_count = image->m_width * image->m_height;
1485 uint32_t bpp = bimg::getBitsPerPixel(image->m_format);
1486 uint32_t bytes_per_pixel = bpp / 8;
1487
1488 if(semantic == "SpecularToMetallicRoughness")
1489 {
1490 // Use the combined conversion function
1491 apply_specular_to_metallic_roughness_conversion(image);
1492 return;
1493 }
1494 else if(semantic == "GlossToRoughness")
1495 {
1496 for(uint32_t i = 0; i < pixel_count; ++i)
1497 {
1498 uint32_t pixel_index = i * bytes_per_pixel;
1499 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1500 pixel_transforms::gloss_to_roughness_pixel);
1501 }
1502 APPLOG_TRACE("Mesh Importer: Applied GlossToRoughness conversion to texture");
1503 }
1504 else if(semantic == "SpecularToRoughness")
1505 {
1506 for(uint32_t i = 0; i < pixel_count; ++i)
1507 {
1508 uint32_t pixel_index = i * bytes_per_pixel;
1509 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1510 pixel_transforms::specular_to_roughness_pixel);
1511 }
1512 APPLOG_TRACE("Mesh Importer: Applied SpecularToRoughness conversion to texture");
1513 }
1514 else if(semantic == "SpecularToMetallic")
1515 {
1516 for(uint32_t i = 0; i < pixel_count; ++i)
1517 {
1518 uint32_t pixel_index = i * bytes_per_pixel;
1519 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1520 pixel_transforms::specular_to_metallic_pixel);
1521 }
1522 APPLOG_TRACE("Mesh Importer: Applied SpecularToMetallic conversion to texture");
1523 }
1524 else if(semantic == "ExtractMetallicChannel")
1525 {
1526 // Extract metallic channel from combined texture (Blue channel in glTF standard)
1527 for(uint32_t i = 0; i < pixel_count; ++i)
1528 {
1529 uint32_t pixel_index = i * bytes_per_pixel;
1530 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1531 [](float r, float g, float b, float a) {
1532 // Extract metallic from blue channel and make it grayscale
1533 return std::make_tuple(b, b, b, 1.0f);
1534 });
1535 }
1536 APPLOG_TRACE("Mesh Importer: Extracted metallic channel for debugging");
1537 }
1538 else if(semantic == "ExtractRoughnessChannel")
1539 {
1540 // Extract roughness channel from combined texture (Green channel in glTF standard)
1541 for(uint32_t i = 0; i < pixel_count; ++i)
1542 {
1543 uint32_t pixel_index = i * bytes_per_pixel;
1544 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1545 [](float r, float g, float b, float a) {
1546 // Extract roughness from green channel and make it grayscale
1547 return std::make_tuple(g, g, g, 1.0f);
1548 });
1549 }
1550 APPLOG_TRACE("Mesh Importer: Extracted roughness channel for debugging");
1551 }
1552 else if(inverse)
1553 {
1554 // Simple inversion for other cases where inverse flag is set
1555 for(uint32_t i = 0; i < pixel_count; ++i)
1556 {
1557 uint32_t pixel_index = i * bytes_per_pixel;
1558 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1559 pixel_transforms::simple_invert_pixel);
1560 }
1561 APPLOG_TRACE("Mesh Importer: Applied simple inversion to texture");
1562 }
1563}
1564
1568void apply_specular_to_metallic_roughness_conversion(bimg::ImageContainer* image)
1569{
1570 if(!image || !image->m_data)
1571 {
1572 return;
1573 }
1574
1575 uint8_t* image_data = static_cast<uint8_t*>(image->m_data);
1576 uint32_t pixel_count = image->m_width * image->m_height;
1577 uint32_t bpp = bimg::getBitsPerPixel(image->m_format);
1578 uint32_t bytes_per_pixel = bpp / 8;
1579
1580 for(uint32_t i = 0; i < pixel_count; ++i)
1581 {
1582 uint32_t pixel_index = i * bytes_per_pixel;
1583 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1584 pixel_transforms::specular_to_metallic_roughness_pixel);
1585 }
1586
1587 APPLOG_TRACE("Mesh Importer: Applied SpecularToMetallicRoughness conversion to texture");
1588}
1589
1593void process_raw_texture_data(const aiTexture* assimp_tex, const fs::path& output_file,
1594 const std::string& semantic, bool inverse)
1595{
1596 // For raw textures, we need to create a temporary image container to apply conversions
1597 uint32_t width = assimp_tex->mWidth;
1598 uint32_t height = assimp_tex->mHeight;
1599
1600 // Create a copy of the raw data to modify
1601 std::vector<uint8_t> data(width * height * 4);
1602 std::memcpy(data.data(), assimp_tex->pcData, width * height * 4);
1603
1604 // Apply conversions to the copied data
1605 if(semantic == "GlossToRoughness" || semantic == "SpecularToRoughness" || semantic == "SpecularToMetallic" || semantic == "SpecularToMetallicRoughness")
1606 {
1607 // Create a temporary image container for conversion
1608 bimg::ImageContainer image;
1609 image.m_data = data.data();
1610 image.m_width = width;
1611 image.m_height = height;
1612 image.m_depth = 1;
1613 image.m_format = bimg::TextureFormat::RGBA8;
1614 image.m_numMips = 1;
1615 image.m_hasAlpha = true;
1616
1617 apply_texture_conversion(&image, semantic, inverse);
1618 }
1619 else if(inverse)
1620 {
1621 // Simple inversion
1622 for(size_t i = 0; i < data.size(); ++i)
1623 {
1624 data[i] = 255 - data[i];
1625 }
1626 }
1627
1628 // Write the processed data
1629 bx::FileWriter writer;
1630 bx::Error err;
1631
1632 if(bx::open(&writer, output_file.string().c_str(), false, &err))
1633 {
1634 bimg::imageWriteTga(&writer,
1635 width,
1636 height,
1637 width * 4,
1638 data.data(),
1639 false,
1640 false,
1641 &err);
1642 bx::close(&writer);
1643 }
1644}
1645
1646template<typename T>
1647void log_prop_value(aiMaterialProperty* prop, const char* name1)
1648{
1649 auto data = (T*)prop->mData;
1650
1651 auto count = prop->mDataLength / sizeof(T);
1652
1653 if(count == 1)
1654 {
1655 APPLOG_TRACE(" {} = {}", name1, data[0]);
1656 }
1657 else
1658 {
1659 std::vector<T> vals(count);
1660 std::memcpy(vals.data(), data, count * sizeof(T));
1661 APPLOG_TRACE(" {}[{}] = {}", name1, count, vals);
1662 }
1663}
1664
1665void log_materials(const aiMaterial* material)
1666{
1667 for(uint32_t i = 0; i < material->mNumProperties; i++)
1668 {
1669 auto prop = material->mProperties[i];
1670
1671 APPLOG_TRACE("Material Property:");
1672 APPLOG_TRACE(" name = {0}", prop->mKey.C_Str());
1673
1674 if(prop->mDataLength > 0 && prop->mData)
1675 {
1676 auto semantic = aiTextureType(prop->mSemantic);
1677 if(semantic != aiTextureType_NONE && semantic != aiTextureType_UNKNOWN)
1678 {
1679 APPLOG_TRACE(" semantic = {0}", aiTextureTypeToString(semantic));
1680 }
1681
1682 switch(prop->mType)
1683 {
1684 case aiPropertyTypeInfo::aiPTI_Float:
1685 {
1686 log_prop_value<float>(prop, "float");
1687 break;
1688 }
1689
1690 case aiPropertyTypeInfo::aiPTI_Double:
1691 {
1692 log_prop_value<double>(prop, "double");
1693 break;
1694 }
1695 case aiPropertyTypeInfo::aiPTI_Integer:
1696 {
1697 log_prop_value<int32_t>(prop, "int");
1698 break;
1699 }
1700
1701 case aiPropertyTypeInfo::aiPTI_Buffer:
1702 {
1703 log_prop_value<uint8_t>(prop, "buffer");
1704 break;
1705 }
1706 case aiPropertyTypeInfo::aiPTI_String:
1707 {
1708 aiString str;
1709 if(aiGetMaterialString(material, prop->mKey.C_Str(), prop->mSemantic, prop->mIndex, &str) ==
1710 AI_SUCCESS)
1711 {
1712 APPLOG_TRACE(" string = {0}", str.C_Str());
1713 }
1714 break;
1715 }
1716 default:
1717 {
1718 break;
1719 }
1720 }
1721 }
1722 }
1723}
1724
1725// Add workflow detection and conversion functions
1726enum class material_workflow
1727{
1728 unknown,
1729 metallic_roughness,
1730 specular_gloss
1731};
1732
1736auto detect_duplicate_specular_usage(const aiMaterial* material, material_workflow workflow) -> bool
1737{
1738 if(workflow != material_workflow::specular_gloss)
1739 {
1740 return false;
1741 }
1742
1743 // Check if we have dedicated metallic/roughness textures - if so, no duplication
1744 bool has_metallic_texture = (material->GetTextureCount(aiTextureType_METALNESS) > 0) ||
1745 (material->GetTextureCount(aiTextureType_GLTF_METALLIC_ROUGHNESS) > 0);
1746 bool has_roughness_texture = (material->GetTextureCount(aiTextureType_DIFFUSE_ROUGHNESS) > 0) ||
1747 (material->GetTextureCount(aiTextureType_GLTF_METALLIC_ROUGHNESS) > 0);
1748 bool has_glossiness_texture = (material->GetTextureCount(aiTextureType_SHININESS) > 0);
1749
1750 // If we have dedicated textures, no need for specular conversion
1751 if(has_metallic_texture || has_roughness_texture || has_glossiness_texture)
1752 {
1753 return false;
1754 }
1755
1756 // Check if we have a specular texture that would be used for both conversions
1757 bool has_specular_texture = (material->GetTextureCount(aiTextureType_SPECULAR) > 0);
1758
1759 if(has_specular_texture)
1760 {
1761 // The logic in get_workflow_aware_texture would use the same specular texture for:
1762 // 1. "Metallic" -> SpecularToMetallic conversion
1763 // 2. "Roughness" -> SpecularToRoughness conversion
1764 // This is a duplication that should use SpecularToMetallicRoughness instead
1765
1766 APPLOG_TRACE("Mesh Importer: Detected duplicate specular usage - same texture would be used for both metallic and roughness conversion");
1767 return true;
1768 }
1769
1770 return false;
1771}
1772
1776auto detect_material_workflow(const aiMaterial* material) -> material_workflow
1777{
1778 // Check for metallic/roughness workflow indicators
1779 bool has_metallic_factor = false;
1780 bool has_roughness_factor = false;
1781 bool has_base_color_factor = false;
1782 bool has_metallic_texture = false;
1783 bool has_roughness_texture = false;
1784 bool has_metallic_roughness_texture = false;
1785 bool has_base_color_texture = false;
1786
1787 // Check for specular/gloss workflow indicators
1788 bool has_specular_factor = false;
1789 bool has_glossiness_factor = false;
1790 bool has_diffuse_color = false;
1791 bool has_specular_color = false;
1792 bool has_specular_texture = false;
1793 bool has_glossiness_texture = false;
1794 bool has_diffuse_texture = false;
1795
1796 // Check for legacy indicators
1797 bool has_shininess = false;
1798 bool has_reflectivity = false;
1799
1800 ai_real dummy_value{};
1801 aiColor3D dummy_color{};
1802 aiString path{};
1803
1804 // Check for metallic/roughness properties
1805 has_metallic_factor = material->Get(AI_MATKEY_METALLIC_FACTOR, dummy_value) == AI_SUCCESS;
1806 has_roughness_factor = material->Get(AI_MATKEY_ROUGHNESS_FACTOR, dummy_value) == AI_SUCCESS;
1807 has_base_color_factor = material->Get(AI_MATKEY_BASE_COLOR, dummy_color) == AI_SUCCESS;
1808
1809 // Check for specular/gloss properties
1810 has_specular_factor = material->Get(AI_MATKEY_SPECULAR_FACTOR, dummy_value) == AI_SUCCESS;
1811 has_glossiness_factor = material->Get(AI_MATKEY_GLOSSINESS_FACTOR, dummy_value) == AI_SUCCESS;
1812 has_diffuse_color = material->Get(AI_MATKEY_COLOR_DIFFUSE, dummy_color) == AI_SUCCESS;
1813 has_specular_color = material->Get(AI_MATKEY_COLOR_SPECULAR, dummy_color) == AI_SUCCESS;
1814
1815 // Check for legacy properties
1816 has_shininess = material->Get(AI_MATKEY_SHININESS, dummy_value) == AI_SUCCESS;
1817 has_reflectivity = material->Get(AI_MATKEY_REFLECTIVITY, dummy_value) == AI_SUCCESS;
1818
1819 // Check for metallic/roughness textures
1820 has_metallic_roughness_texture = material->GetTexture(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, &path) == AI_SUCCESS;
1821 has_metallic_texture = material->GetTexture(AI_MATKEY_METALLIC_TEXTURE, &path) == AI_SUCCESS;
1822 has_roughness_texture = material->GetTexture(AI_MATKEY_ROUGHNESS_TEXTURE, &path) == AI_SUCCESS;
1823 has_base_color_texture = material->GetTexture(AI_MATKEY_BASE_COLOR_TEXTURE, &path) == AI_SUCCESS;
1824
1825 // Check for specular/gloss textures
1826 has_specular_texture = material->GetTexture(aiTextureType_SPECULAR, 0, &path) == AI_SUCCESS;
1827 has_glossiness_texture = material->GetTexture(aiTextureType_SHININESS, 0, &path) == AI_SUCCESS;
1828 has_diffuse_texture = material->GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS;
1829
1830 // Calculate confidence scores with improved weighting
1831 int metallic_roughness_score = 0;
1832 int specular_gloss_score = 0;
1833
1834 // Metallic/Roughness indicators (higher scores for strong indicators)
1835 if(has_metallic_factor) metallic_roughness_score += 8;
1836 if(has_roughness_factor) metallic_roughness_score += 8;
1837 if(has_base_color_factor) metallic_roughness_score += 4;
1838 if(has_metallic_roughness_texture) metallic_roughness_score += 12; // Combined texture is very strong indicator
1839 if(has_metallic_texture) metallic_roughness_score += 10; // Standalone metallic texture is strong
1840 if(has_roughness_texture) metallic_roughness_score += 6;
1841 if(has_base_color_texture) metallic_roughness_score += 3;
1842
1843 // Specular/Gloss indicators (higher scores for strong indicators)
1844 if(has_specular_factor) specular_gloss_score += 8;
1845 if(has_glossiness_factor) specular_gloss_score += 8;
1846 if(has_diffuse_color) specular_gloss_score += 4;
1847 if(has_specular_color) specular_gloss_score += 6;
1848 if(has_specular_texture) specular_gloss_score += 10; // Specular texture is very strong indicator
1849 if(has_glossiness_texture) specular_gloss_score += 10; // Gloss/Shininess texture is very strong indicator
1850 if(has_diffuse_texture) specular_gloss_score += 6; // Diffuse texture in specular workflow
1851
1852 // Legacy indicators (could be either, but lean towards specular)
1853 if(has_shininess) specular_gloss_score += 4;
1854 if(has_reflectivity) specular_gloss_score += 3;
1855
1856 // Additional heuristics: check for specific combinations
1857 // Strong metallic/roughness combination
1858 if(has_metallic_factor && has_roughness_factor)
1859 {
1860 metallic_roughness_score += 5;
1861 }
1862
1863 // Strong specular/gloss combination
1864 if(has_specular_texture && has_diffuse_texture)
1865 {
1866 specular_gloss_score += 8; // Classic specular workflow combo
1867 }
1868
1869 if(has_specular_color && has_diffuse_color)
1870 {
1871 specular_gloss_score += 6; // Classic specular workflow combo
1872 }
1873
1874 // Log detection details for debugging
1875 APPLOG_TRACE("Mesh Importer: Material workflow detection scores - Metallic/Roughness: {}, Specular/Gloss: {}",
1876 metallic_roughness_score, specular_gloss_score);
1877
1878 // Determine workflow based on scores with minimum threshold
1879 if(metallic_roughness_score > specular_gloss_score && metallic_roughness_score >= 5)
1880 {
1881 return material_workflow::metallic_roughness;
1882 }
1883 else if(specular_gloss_score >= 5)
1884 {
1885 return material_workflow::specular_gloss;
1886 }
1887
1888 return material_workflow::unknown;
1889}
1890
1894auto convert_specular_to_metallic(float specular) -> float
1895{
1896 // Enhanced conversion based on PBR principles and official Khronos guidelines:
1897 const float dielectric_f0 = 0.04f; // Standard F0 for dielectrics
1898
1899 if (specular <= dielectric_f0)
1900 {
1901 return 0.0f; // Definitely dielectric
1902 }
1903 else if (specular >= 0.9f)
1904 {
1905 return 1.0f; // Definitely metallic
1906 }
1907 else
1908 {
1909 // Smooth transition using the official approach
1910 float normalized_specular = (specular - dielectric_f0) / (1.0f - dielectric_f0);
1911 return math::clamp(normalized_specular, 0.0f, 1.0f);
1912 }
1913}
1914
1918auto is_specular_color_metallic(const aiColor3D& specular_color) -> bool
1919{
1920 // Metals typically have colored specular reflections
1921 // Dielectrics usually have white/grey specular
1922 float avg_specular = (specular_color.r + specular_color.g + specular_color.b) / 3.0f;
1923 float color_variance = std::abs(specular_color.r - avg_specular) +
1924 std::abs(specular_color.g - avg_specular) +
1925 std::abs(specular_color.b - avg_specular);
1926
1927 // If specular color has significant color variation, it's likely metallic
1928 return color_variance > 0.1f && avg_specular > 0.3f;
1929}
1930
1934auto convert_specular_color_to_base_color(const aiColor3D& specular_color, float metallic) -> aiColor3D
1935{
1936 if(metallic > 0.5f)
1937 {
1938 // For metals, specular color becomes the base color
1939 return specular_color;
1940 }
1941 else
1942 {
1943 // For dielectrics, base color should be white or from diffuse
1944 return aiColor3D(1.0f, 1.0f, 1.0f);
1945 }
1946}
1947
1951template<typename GetTextureFunc>
1952auto get_workflow_aware_texture(const aiMaterial* material,
1953 material_workflow workflow,
1954 const std::string& target_semantic,
1955 imported_texture& tex,
1956 GetTextureFunc get_imported_texture,
1957 bool use_combined_specular = false) -> bool
1958{
1959 if(target_semantic == "BaseColor")
1960 {
1961 // Try base color first, then diffuse as fallback
1962 if(get_imported_texture(material, AI_MATKEY_BASE_COLOR_TEXTURE, "BaseColor", tex))
1963 {
1964 return true;
1965 }
1966 else if(get_imported_texture(material, aiTextureType_DIFFUSE, 0, "BaseColor", tex))
1967 {
1968 return true;
1969 }
1970 }
1971 else if(target_semantic == "Metallic")
1972 {
1973 // For metallic, check if we have a combined metallic/roughness texture
1974 if(get_imported_texture(material, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, "MetallicRoughness", tex))
1975 {
1976 return true;
1977 }
1978 // Otherwise try standalone metallic
1979 else if(get_imported_texture(material, AI_MATKEY_METALLIC_TEXTURE, "Metallic", tex))
1980 {
1981 return true;
1982 }
1983 // For specular workflow, check if we should use combined processing
1984 else if(workflow == material_workflow::specular_gloss)
1985 {
1986 if(use_combined_specular)
1987 {
1988 // Skip individual processing - combined processing will handle this
1989 return false;
1990 }
1991 else if(get_imported_texture(material, aiTextureType_SPECULAR, 0, "SpecularToMetallic", tex))
1992 {
1993 tex.inverse = false; // Special processing: extract metallic info from specular
1994 return true;
1995 }
1996 }
1997 }
1998 else if(target_semantic == "Roughness")
1999 {
2000 // For roughness, check if we have a combined metallic/roughness texture
2001 if(get_imported_texture(material, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, "MetallicRoughness", tex))
2002 {
2003 return true;
2004 }
2005 // Try standalone roughness
2006 else if(get_imported_texture(material, AI_MATKEY_ROUGHNESS_TEXTURE, "Roughness", tex))
2007 {
2008 return true;
2009 }
2010 // For specular/gloss workflow, convert gloss to roughness
2011 else if(workflow == material_workflow::specular_gloss)
2012 {
2013 if(use_combined_specular)
2014 {
2015 // Skip individual processing - combined processing will handle this
2016 return false;
2017 }
2018 else if(get_imported_texture(material, aiTextureType_SHININESS, 0, "GlossToRoughness", tex))
2019 {
2020 tex.inverse = true; // Roughness = 1 - Gloss
2021 return true;
2022 }
2023 // Try specular texture as fallback (may contain gloss in alpha)
2024 else if(get_imported_texture(material, aiTextureType_SPECULAR, 0, "SpecularToRoughness", tex))
2025 {
2026 tex.inverse = true;
2027 return true;
2028 }
2029 }
2030 }
2031
2032 return false;
2033}
2034
2039void process_material_with_workflow_conversion(const aiMaterial* material,
2040 material_workflow workflow,
2041 aiColor3D& base_color,
2042 float& metallic,
2043 float& roughness)
2044{
2045 // Step 1: Try to get the actual PBR properties first
2046 bool has_base_color = (material->Get(AI_MATKEY_BASE_COLOR, base_color) == AI_SUCCESS);
2047 bool has_metallic = (material->Get(AI_MATKEY_METALLIC_FACTOR, metallic) == AI_SUCCESS);
2048 bool has_roughness = (material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness) == AI_SUCCESS);
2049
2050 // Step 2: Fill in missing properties based on available data and workflow
2051
2052 // Handle Base Color
2053 if (!has_base_color)
2054 {
2055 // Try diffuse color as fallback
2056 if (material->Get(AI_MATKEY_COLOR_DIFFUSE, base_color) != AI_SUCCESS)
2057 {
2058 base_color = aiColor3D{1.0f, 1.0f, 1.0f}; // Default white
2059 }
2060
2061 // If we're converting from specular workflow, we might need to adjust base color
2062 if (workflow == material_workflow::specular_gloss)
2063 {
2064 aiColor3D diffuse_color = base_color;
2065 aiColor3D specular_color{0.04f, 0.04f, 0.04f};
2066 float specular_factor = 1.0f;
2067
2068 material->Get(AI_MATKEY_COLOR_SPECULAR, specular_color);
2069 material->Get(AI_MATKEY_SPECULAR_FACTOR, specular_factor);
2070
2071 specular_color.r *= specular_factor;
2072 specular_color.g *= specular_factor;
2073 specular_color.b *= specular_factor;
2074
2075 // Use Khronos conversion for base color calculation
2076 float glossiness = 0.5f;
2077 if(material->Get(AI_MATKEY_GLOSSINESS_FACTOR, glossiness) != AI_SUCCESS)
2078 {
2079 float shininess = 32.0f;
2080 if(material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
2081 {
2082 glossiness = math::clamp(std::sqrt((shininess + 2.0f) / 1024.0f), 0.0f, 1.0f);
2083 }
2084 }
2085
2086 auto [converted_base_color, _, __] =
2087 convert_specular_gloss_to_metallic_roughness(diffuse_color, specular_color, glossiness);
2088 base_color = converted_base_color;
2089
2090 APPLOG_TRACE("Mesh Importer: Converted base color from specular/diffuse workflow");
2091 }
2092 }
2093
2094 // Handle Metallic Factor
2095 if (!has_metallic)
2096 {
2097 if (workflow == material_workflow::specular_gloss)
2098 {
2099 // Convert from specular workflow
2100 aiColor3D diffuse_color = base_color;
2101 aiColor3D specular_color{0.04f, 0.04f, 0.04f};
2102 float specular_factor = 1.0f;
2103 float glossiness = 0.5f;
2104
2105 material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse_color);
2106 material->Get(AI_MATKEY_COLOR_SPECULAR, specular_color);
2107 material->Get(AI_MATKEY_SPECULAR_FACTOR, specular_factor);
2108
2109 specular_color.r *= specular_factor;
2110 specular_color.g *= specular_factor;
2111 specular_color.b *= specular_factor;
2112
2113 if(material->Get(AI_MATKEY_GLOSSINESS_FACTOR, glossiness) != AI_SUCCESS)
2114 {
2115 float shininess = 32.0f;
2116 if(material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
2117 {
2118 glossiness = math::clamp(std::sqrt((shininess + 2.0f) / 1024.0f), 0.0f, 1.0f);
2119 }
2120 }
2121
2122 auto [_, converted_metallic, __] =
2123 convert_specular_gloss_to_metallic_roughness(diffuse_color, specular_color, glossiness);
2124 metallic = converted_metallic;
2125
2126 APPLOG_TRACE("Mesh Importer: Converted metallic factor from specular workflow: {:.3f}", metallic);
2127 }
2128 else
2129 {
2130 // Try legacy reflectivity as fallback
2131 if (material->Get(AI_MATKEY_REFLECTIVITY, metallic) != AI_SUCCESS)
2132 {
2133 metallic = 0.0f; // Default dielectric
2134 }
2135 }
2136 }
2137
2138 // Handle Roughness Factor
2139 if (!has_roughness)
2140 {
2141 if (workflow == material_workflow::specular_gloss)
2142 {
2143 // Convert from glossiness
2144 float glossiness = 0.5f;
2145
2146 if(material->Get(AI_MATKEY_GLOSSINESS_FACTOR, glossiness) == AI_SUCCESS)
2147 {
2148 roughness = 1.0f - glossiness;
2149 APPLOG_TRACE("Mesh Importer: Converted roughness from glossiness: {:.3f} -> {:.3f}",
2150 glossiness, roughness);
2151 }
2152 else
2153 {
2154 // Try shininess conversion
2155 float shininess = 32.0f;
2156 if(material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
2157 {
2158 // Convert to glossiness first, then to roughness
2159 glossiness = math::clamp(std::sqrt((shininess + 2.0f) / 1024.0f), 0.0f, 1.0f);
2160 roughness = 1.0f - glossiness;
2161 APPLOG_TRACE("Mesh Importer: Converted roughness from shininess: {:.1f} -> {:.3f}",
2162 shininess, roughness);
2163 }
2164 else
2165 {
2166 // Use specular workflow conversion as final fallback
2167 aiColor3D diffuse_color = base_color;
2168 aiColor3D specular_color{0.04f, 0.04f, 0.04f};
2169 float specular_factor = 1.0f;
2170
2171 material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse_color);
2172 material->Get(AI_MATKEY_COLOR_SPECULAR, specular_color);
2173 material->Get(AI_MATKEY_SPECULAR_FACTOR, specular_factor);
2174
2175 specular_color.r *= specular_factor;
2176 specular_color.g *= specular_factor;
2177 specular_color.b *= specular_factor;
2178
2179 auto [_, __, converted_roughness] =
2180 convert_specular_gloss_to_metallic_roughness(diffuse_color, specular_color, glossiness);
2181 roughness = converted_roughness;
2182
2183 APPLOG_TRACE("Mesh Importer: Converted roughness from full specular workflow: {:.3f}", roughness);
2184 }
2185 }
2186 }
2187 else
2188 {
2189 // Try shininess conversion for legacy materials
2190 float shininess = 32.0f;
2191 if(material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
2192 {
2193 roughness = std::sqrt(2.0f / (shininess + 2.0f));
2194 APPLOG_TRACE("Mesh Importer: Converted roughness from legacy shininess: {:.1f} -> {:.3f}",
2195 shininess, roughness);
2196 }
2197 else
2198 {
2199 roughness = 0.5f; // Default mid-range roughness
2200 }
2201 }
2202 }
2203
2204 // Log final values
2205 APPLOG_TRACE("Mesh Importer: Final PBR values - BaseColor: ({:.3f}, {:.3f}, {:.3f}), "
2206 "Metallic: {:.3f}, Roughness: {:.3f} [{}{}{}]",
2207 base_color.r, base_color.g, base_color.b, metallic, roughness,
2208 has_base_color ? "B" : "b",
2209 has_metallic ? "M" : "m",
2210 has_roughness ? "R" : "r");
2211}
2212
2213void process_material(asset_manager& am,
2214 const fs::path& filename,
2215 const fs::path& output_dir,
2216 const aiScene* scene,
2217 const aiMaterial* material,
2218 pbr_material& mat,
2219 std::vector<imported_texture>& textures)
2220{
2221 if(!material)
2222 {
2223 return;
2224 }
2225
2226 // Detect the material workflow before processing
2227 auto workflow = detect_material_workflow(material);
2228
2229 APPLOG_TRACE("Mesh Importer: Material workflow detected: {}",
2230 workflow == material_workflow::metallic_roughness ? "Metallic/Roughness" :
2231 workflow == material_workflow::specular_gloss ? "Specular/Gloss" : "Unknown");
2232
2233 // log_materials(material);
2234
2235 auto get_imported_texture = [&](const aiMaterial* material,
2236 aiTextureType type,
2237 unsigned int index,
2238 const std::string& semantic,
2239 imported_texture& tex) -> bool
2240 {
2241 aiString path{};
2242 aiTextureMapping mapping{};
2243 unsigned int uvindex{};
2244 float blend{};
2245 aiTextureOp op{};
2246 aiTextureMapMode mapmode{};
2247 unsigned int flags{};
2248
2249 // Call the function
2250 aiReturn result = aiGetMaterialTexture(material, // The material pointer
2251 type, // The type of texture (e.g., diffuse)
2252 index, // The texture index
2253 &path // The path where the texture file path will be stored
2254 // &mapping, // The mapping method
2255 // &uvindex, // The UV index
2256 // &blend, // The blend factor
2257 // &op, // The texture operation
2258 // &mapmode, // The texture map mode
2259 // &flags // Additional flags
2260 );
2261
2262 if(path.length > 0)
2263 {
2264 auto tex_pair = scene->GetEmbeddedTextureAndIndex(path.C_Str());
2265
2266 const auto embedded_texture = tex_pair.first;
2267 if(embedded_texture)
2268 {
2269 const auto index = tex_pair.second;
2270
2271 // std::string s = aiTextureTypeToString(type);
2272 tex.name = get_embedded_texture_name(embedded_texture, index, filename, semantic);
2273 tex.embedded_index = index;
2274 }
2275 else
2276 {
2277 tex.name = path.C_Str();
2278 auto texture_filepath = fs::path(tex.name);
2279
2280 auto extension = texture_filepath.extension().string();
2281 auto texture_dir = texture_filepath.parent_path();
2282 auto texture_filename = texture_filepath.filename().stem().string();
2283 auto fixed_name = string_utils::replace(texture_filename, ".", "_");
2284 if(fixed_name != texture_filename)
2285 {
2286 auto old_filepath = output_dir / tex.name;
2287 auto fixed_relative = texture_dir / (fixed_name + extension);
2288 auto fixed_filepath = output_dir / fixed_relative;
2289
2290 fs::error_code ec;
2291 if(fs::exists(old_filepath, ec))
2292 {
2293 fs::rename(old_filepath, fixed_filepath, ec);
2294 }
2295 else
2296 {
2297 // doesnt exist. so try to import it
2298 fs::copy_file(old_filepath, fixed_filepath, ec);
2299 }
2300 tex.name = fixed_relative.generic_string();
2301 }
2302 }
2303 tex.semantic = semantic;
2304 bool use_alpha = flags & aiTextureFlags_UseAlpha;
2305 bool ignore_alpha = flags & aiTextureFlags_IgnoreAlpha;
2306 bool invert = flags & aiTextureFlags_Invert;
2307 tex.inverse = invert;
2308
2309 switch(mapmode)
2310 {
2311 case aiTextureMapMode_Mirror:
2312 tex.flags = BGFX_SAMPLER_UVW_MIRROR;
2313 case aiTextureMapMode_Clamp:
2314 tex.flags = BGFX_SAMPLER_UVW_CLAMP;
2315 case aiTextureMapMode_Decal:
2316 tex.flags = BGFX_SAMPLER_UVW_BORDER;
2317 default:
2318 break;
2319 }
2320
2321 return true;
2322 }
2323
2324 return false;
2325 };
2326
2327 auto process_texture = [&](imported_texture& texture, std::vector<imported_texture>& textures, bool force_process = false)
2328 {
2329 if(texture.embedded_index >= 0)
2330 {
2331 auto it = std::find_if(std::begin(textures),
2332 std::end(textures),
2333 [&](const imported_texture& rhs)
2334 {
2335 return rhs.embedded_index == texture.embedded_index;
2336 });
2337 if(it != std::end(textures))
2338 {
2339 texture.name = it->name;
2340 texture.flags = it->flags;
2341 texture.inverse = it->inverse;
2342 texture.process_count = it->process_count;
2343 return;
2344 }
2345 }
2346
2347 textures.emplace_back(texture);
2348
2349 if(texture.embedded_index >= 0)
2350 {
2351 const auto& embedded_texture = scene->mTextures[texture.embedded_index];
2352 process_embedded_texture(embedded_texture, texture.embedded_index, filename, output_dir, textures);
2353 }
2354 };
2355
2356 // technically there is a difference between MASK and BLEND mode
2357 // but for our purposes it's enough if we sort properly
2358 // aiString alpha_mode;
2359 // material->Get(AI_MATKEY_GLTF_ALPHAMODE, alpha_mode);
2360 // aiString alpha_mode_opaque;
2361 // alpha_mode_opaque.Set("OPAQUE");
2362
2363 // out.blend = alphaMode != alphaModeOpaque;
2364
2365 // bool double_sided{};
2366 // if(material->Get(AI_MATKEY_TWOSIDED, double_sided) == AI_SUCCESS)
2367 // {
2368 // mat.set_cull_type(double_sided ? cull_type::none : cull_type::counter_clockwise);
2369 // }
2370
2371 // BASE COLOR TEXTURE - Use workflow-aware detection
2372 {
2373 imported_texture texture;
2374 if(get_workflow_aware_texture(material, workflow, "BaseColor", texture, get_imported_texture, false))
2375 {
2376 process_texture(texture, textures);
2377
2378 auto key = fs::convert_to_protocol(output_dir / texture.name);
2379 mat.set_color_map(am.get_asset<gfx::texture>(key.generic_string()));
2380 }
2381 }
2382 // BASE COLOR PROPERTY - Use workflow-aware conversion
2383 {
2384 aiColor3D base_color_property{1.0f, 1.0f, 1.0f};
2385 float metallic_property = 0.0f;
2386 float roughness_property = 0.5f;
2387
2388 // Use workflow-aware conversion to get proper values
2389 process_material_with_workflow_conversion(material, workflow,
2390 base_color_property,
2391 metallic_property,
2392 roughness_property);
2393
2394 // Set the converted base color
2395 math::color base_color{};
2396 base_color = {base_color_property.r, base_color_property.g, base_color_property.b};
2397 base_color = math::clamp(base_color.value, 0.0f, 1.0f);
2398 mat.set_base_color(base_color);
2399
2400 // Store converted values for later use
2401 mat.set_metalness(math::clamp(metallic_property, 0.0f, 1.0f));
2402 mat.set_roughness(math::clamp(roughness_property, 0.0f, 1.0f));
2403 }
2404
2405 // METALLIC & ROUGHNESS TEXTURES - Check for duplicate specular usage first
2406 bool uses_duplicate_specular = detect_duplicate_specular_usage(material, workflow);
2407
2408 if(uses_duplicate_specular)
2409 {
2410 // Use combined processing for the same specular texture
2411 imported_texture combined_texture;
2412 if(get_imported_texture(material, aiTextureType_SPECULAR, 0, "SpecularToMetallicRoughness", combined_texture))
2413 {
2414 process_texture(combined_texture, textures);
2415
2416 auto key = fs::convert_to_protocol(output_dir / combined_texture.name);
2417 auto texture_asset = am.get_asset<gfx::texture>(key.generic_string());
2418
2419 // Use the same texture for both metallic and roughness (glTF style channels)
2420 mat.set_metalness_map(texture_asset);
2421 mat.set_roughness_map(texture_asset);
2422
2423 APPLOG_TRACE("Mesh Importer: Converting single specular texture to combined metallic/roughness: {}", combined_texture.name);
2424 }
2425 }
2426 else
2427 {
2428 // Process metallic and roughness textures separately
2429
2430 // METALLIC TEXTURE - Use workflow-aware detection with conversion support
2431 {
2432 imported_texture texture;
2433 if(get_workflow_aware_texture(material, workflow, "Metallic", texture, get_imported_texture, uses_duplicate_specular))
2434 {
2435 process_texture(texture, textures);
2436
2437 auto key = fs::convert_to_protocol(output_dir / texture.name);
2438 mat.set_metalness_map(am.get_asset<gfx::texture>(key.generic_string()));
2439
2440 // Log conversion info for debugging
2441 if(texture.semantic == "SpecularToMetallic")
2442 {
2443 APPLOG_TRACE("Mesh Importer: Converting specular texture to metallic: {}", texture.name);
2444 }
2445 }
2446 }
2447
2448 // ROUGHNESS TEXTURE - Use workflow-aware detection with conversion support
2449 {
2450 imported_texture texture;
2451 if(get_workflow_aware_texture(material, workflow, "Roughness", texture, get_imported_texture, uses_duplicate_specular))
2452 {
2453 process_texture(texture, textures);
2454
2455 auto key = fs::convert_to_protocol(output_dir / texture.name);
2456 mat.set_roughness_map(am.get_asset<gfx::texture>(key.generic_string()));
2457
2458 // Log conversion info for debugging
2459 if(texture.semantic == "GlossToRoughness")
2460 {
2461 APPLOG_TRACE("Mesh Importer: Converting gloss texture to roughness: {}", texture.name);
2462 }
2463 else if(texture.semantic == "SpecularToRoughness")
2464 {
2465 APPLOG_TRACE("Mesh Importer: Converting specular texture to roughness: {}", texture.name);
2466 }
2467 }
2468 }
2469 }
2470
2471 // METALLIC & ROUGHNESS PROPERTIES - Now handled by workflow conversion above
2472 // The metallic and roughness values are already set by process_material_with_workflow_conversion()
2473 // which properly converts between specular/gloss and metallic/roughness workflows
2474
2475 // ROUGHNESS PROPERTY - Now handled by workflow conversion above
2476 // The roughness value is already set by process_material_with_workflow_conversion()
2477 // which properly converts between specular/gloss and metallic/roughness workflows
2478
2479 // NORMAL TEXTURE
2480 aiTextureType normals_type = aiTextureType_NORMALS;
2481 {
2482 static const std::string semantic = "Normals";
2483
2484 imported_texture texture;
2485 bool has_texture = false;
2486
2487 if(!has_texture)
2488 {
2489 has_texture |= get_imported_texture(material, aiTextureType_NORMALS, 0, semantic, texture);
2490 }
2491
2492 if(!has_texture)
2493 {
2494 has_texture |= get_imported_texture(material, aiTextureType_NORMAL_CAMERA, 0, semantic, texture);
2495
2496 if(has_texture)
2497 {
2498 normals_type = aiTextureType_NORMAL_CAMERA;
2499 }
2500 }
2501
2502 if(has_texture)
2503 {
2504 process_texture(texture, textures);
2505
2506 auto key = fs::convert_to_protocol(output_dir / texture.name);
2507 mat.set_normal_map(am.get_asset<gfx::texture>(key.generic_string()));
2508 }
2509 }
2510 // NORMAL BUMP PROPERTY
2511 {
2512 ai_real property{};
2513 bool has_property = false;
2514
2515 if(!has_property)
2516 {
2517 has_property |= material->Get(AI_MATKEY_GLTF_TEXTURE_SCALE(normals_type, 0), property) == AI_SUCCESS;
2518 }
2519
2520 if(has_property)
2521 {
2522 mat.set_bumpiness(property);
2523 }
2524 }
2525
2526 // OCCLUSION TEXTURE
2527 aiTextureType occlusion_type = aiTextureType_AMBIENT_OCCLUSION;
2528 {
2529 static const std::string semantic = "Occlusion";
2530
2531 imported_texture texture;
2532 bool has_texture = false;
2533
2534 if(!has_texture)
2535 {
2536 has_texture |= get_imported_texture(material, aiTextureType_AMBIENT_OCCLUSION, 0, semantic, texture);
2537 }
2538
2539 if(!has_texture)
2540 {
2541 has_texture |= get_imported_texture(material, aiTextureType_AMBIENT, 0, semantic, texture);
2542
2543 if(has_texture)
2544 {
2545 occlusion_type = aiTextureType_AMBIENT;
2546 }
2547 }
2548
2549 if(!has_texture)
2550 {
2551 has_texture |= get_imported_texture(material, aiTextureType_LIGHTMAP, 0, semantic, texture);
2552 if(has_texture)
2553 {
2554 occlusion_type = aiTextureType_LIGHTMAP;
2555 }
2556 }
2557
2558 if(has_texture)
2559 {
2560 process_texture(texture, textures);
2561
2562 auto key = fs::convert_to_protocol(output_dir / texture.name);
2563 mat.set_ao_map(am.get_asset<gfx::texture>(key.generic_string()));
2564 }
2565 }
2566
2567 // OCCLUSION STERNGTH PROPERTY
2568 {
2569 ai_real property{};
2570 bool has_property = false;
2571
2572 if(!has_property)
2573 {
2574 has_property |= material->Get(AI_MATKEY_GLTF_TEXTURE_STRENGTH(occlusion_type, 0), property) == AI_SUCCESS;
2575 }
2576
2577 if(has_property)
2578 {
2579 }
2580 }
2581
2582 // EMISSIVE TEXTURE
2583 {
2584 static const std::string semantic = "Emissive";
2585
2586 imported_texture texture;
2587 bool has_texture = false;
2588
2589 if(!has_texture)
2590 {
2591 has_texture |= get_imported_texture(material, aiTextureType_EMISSION_COLOR, 0, semantic, texture);
2592 }
2593
2594 if(!has_texture)
2595 {
2596 has_texture |= get_imported_texture(material, aiTextureType_EMISSIVE, 0, semantic, texture);
2597 }
2598
2599 if(has_texture)
2600 {
2601 process_texture(texture, textures);
2602
2603 auto key = fs::convert_to_protocol(output_dir / texture.name);
2604 mat.set_emissive_map(am.get_asset<gfx::texture>(key.generic_string()));
2605 }
2606 }
2607 // EMISSIVE COLOR PROPERTY
2608 {
2609 aiColor3D property{};
2610 bool has_property = false;
2611
2612 if(!has_property)
2613 {
2614 has_property |= material->Get(AI_MATKEY_COLOR_EMISSIVE, property) == AI_SUCCESS;
2615 }
2616
2617 if(has_property)
2618 {
2619 math::color emissive{};
2620 emissive = {property.r, property.g, property.b};
2621 emissive = math::clamp(emissive.value, 0.0f, 1.0f);
2622 mat.set_emissive_color(emissive);
2623 }
2624 }
2625}
2626
2627void process_materials(asset_manager& am,
2628 const fs::path& filename,
2629 const fs::path& output_dir,
2630 const aiScene* scene,
2631 std::vector<imported_material>& materials,
2632 std::vector<imported_texture>& textures)
2633{
2634 if(scene->mNumMaterials > 0)
2635 {
2636 materials.resize(scene->mNumMaterials);
2637 }
2638
2639 for(size_t i = 0; i < scene->mNumMaterials; ++i)
2640 {
2641 const aiMaterial* assimp_mat = scene->mMaterials[i];
2642
2643 auto mat = std::make_shared<pbr_material>();
2644 process_material(am, filename, output_dir, scene, assimp_mat, *mat, textures);
2645 std::string assimp_mat_name = assimp_mat->GetName().C_Str();
2646 if(assimp_mat_name.empty())
2647 {
2648 assimp_mat_name = fmt::format("Material {}", filename.string());
2649 }
2650 materials[i].mat = mat;
2651 materials[i].name = string_utils::replace(fmt::format("[{}] {}", i, assimp_mat_name), ".", "_");
2652 }
2653}
2654
2655void process_embedded_textures(asset_manager& am,
2656 const fs::path& filename,
2657 const fs::path& output_dir,
2658 const aiScene* scene,
2659 std::vector<imported_texture>& textures)
2660{
2661 if(scene->mNumTextures > 0)
2662 {
2663 for(size_t i = 0; i < scene->mNumTextures; ++i)
2664 {
2665 const aiTexture* assimp_tex = scene->mTextures[i];
2666
2667 process_embedded_texture(assimp_tex, i, filename, output_dir, textures);
2668 }
2669 }
2670}
2671
2672void process_imported_scene(asset_manager& am,
2673 const fs::path& filename,
2674 const fs::path& output_dir,
2675 const aiScene* scene,
2676 mesh::load_data& load_data,
2677 std::vector<animation_clip>& animations,
2678 std::vector<imported_material>& materials,
2679 std::vector<imported_texture>& textures)
2680{
2681 int meshes_with_bones = 0;
2682 int meshes_without_bones = 0;
2683
2684 APPLOG_TRACE_PERF_NAMED(std::chrono::milliseconds, "Mesh Importer: Parse Imported Data");
2685
2686 load_data.vertex_format = gfx::mesh_vertex::get_layout();
2687
2688 auto name_to_index_lut = assign_node_indices(scene);
2689
2690 APPLOG_TRACE("Mesh Importer: Processing materials ...");
2691 process_materials(am, filename, output_dir, scene, materials, textures);
2692
2693 APPLOG_TRACE("Mesh Importer: Processing embedded textures ...");
2694 process_embedded_textures(am, filename, output_dir, scene, textures);
2695
2696 APPLOG_TRACE("Mesh Importer: Processing meshes ...");
2697 process_meshes(scene, load_data);
2698
2699 APPLOG_TRACE("Mesh Importer: Processing nodes ...");
2700 process_nodes(scene, load_data, name_to_index_lut);
2701
2702 APPLOG_TRACE("Mesh Importer: Processing animations ...");
2703 process_animations(scene, filename, load_data, name_to_index_lut, animations);
2704
2705 APPLOG_TRACE("Mesh Importer: Processing animations bounding boxes ...");
2706 auto boxes = compute_bounding_boxes_for_animations(scene);
2707
2708 if(!boxes.empty())
2709 {
2710 load_data.bbox = {};
2711 for(const auto& kvp : boxes)
2712 {
2713 for(const auto& box : kvp.second)
2714 {
2715 load_data.bbox.add_point(box.min);
2716 load_data.bbox.add_point(box.max);
2717 }
2718 }
2719 }
2720 else if(!load_data.bbox.is_populated())
2721 {
2722 for(const auto& submesh : load_data.submeshes)
2723 {
2724 load_data.bbox.add_point(submesh.bbox.min);
2725 load_data.bbox.add_point(submesh.bbox.max);
2726 }
2727 }
2728
2729 APPLOG_TRACE("Mesh Importer: bbox min {}, max {}", load_data.bbox.min, load_data.bbox.max);
2730}
2731
2732auto read_file(Assimp::Importer& importer, const fs::path& file, uint32_t flags) -> const aiScene*
2733{
2734 APPLOG_TRACE_PERF_NAMED(std::chrono::milliseconds, "Importer Read File");
2735 return importer.ReadFile(file.string(), flags);
2736}
2737
2742auto convert_specular_gloss_to_metallic_roughness(const aiColor3D& diffuse_color,
2743 const aiColor3D& specular_color,
2744 float glossiness_factor) -> std::tuple<aiColor3D, float, float>
2745{
2746 // Official Khronos conversion formulas from glTF specification appendix
2747
2748 // Step 1: Calculate the maximum specular component for metallic detection
2749 float max_specular = std::max({specular_color.r, specular_color.g, specular_color.b});
2750
2751 // Step 2: Calculate metallic factor based on specular intensity
2752 // The official formula uses a threshold approach:
2753 // - If max(specular) > 0.04 (typical dielectric F0), likely metallic
2754 // - Use a sigmoid-like transition for smooth conversion
2755 float metallic = 0.0f;
2756 const float dielectric_f0 = 0.04f; // Typical F0 for dielectrics
2757
2758 if (max_specular > dielectric_f0)
2759 {
2760 // Enhanced metallic detection using official approach
2761 float specular_above_dielectric = max_specular - dielectric_f0;
2762 float specular_range = 1.0f - dielectric_f0;
2763
2764 // Use a smooth transition function instead of sharp cutoff
2765 metallic = math::clamp(specular_above_dielectric / specular_range, 0.0f, 1.0f);
2766
2767 // Additional check: colored specular strongly indicates metal
2768 float avg_specular = (specular_color.r + specular_color.g + specular_color.b) / 3.0f;
2769 float color_variance = std::abs(specular_color.r - avg_specular) +
2770 std::abs(specular_color.g - avg_specular) +
2771 std::abs(specular_color.b - avg_specular);
2772
2773 // If specular has significant color (not grayscale), boost metallic
2774 if (color_variance > 0.1f && avg_specular > 0.3f)
2775 {
2776 metallic = std::max(metallic, 0.8f); // Strong indication of metal
2777 }
2778 }
2779
2780 // Step 3: Calculate base color using official formula
2781 // For metals: base color comes from specular color
2782 // For dielectrics: base color comes from diffuse color
2783 aiColor3D base_color;
2784
2785 if (metallic > 0.5f)
2786 {
2787 // For metallic materials, specular color becomes base color
2788 // Apply the diffuse contribution formula: c_diff = diffuse * (1 - max(specular))
2789 float specular_influence = 1.0f - max_specular;
2790 base_color.r = specular_color.r + (diffuse_color.r * specular_influence * (1.0f - metallic));
2791 base_color.g = specular_color.g + (diffuse_color.g * specular_influence * (1.0f - metallic));
2792 base_color.b = specular_color.b + (diffuse_color.b * specular_influence * (1.0f - metallic));
2793 }
2794 else
2795 {
2796 // For dielectric materials, use diffuse as base color
2797 base_color = diffuse_color;
2798 }
2799
2800 // Step 4: Convert glossiness to roughness using official formula
2801 // α = (1 - glossiness)² where α is the roughness parameter used in BRDF
2802 // However, for texture storage, we typically use linear roughness
2803 float roughness = 1.0f - glossiness_factor;
2804
2805 // Clamp all values to valid ranges
2806 base_color.r = math::clamp(base_color.r, 0.0f, 1.0f);
2807 base_color.g = math::clamp(base_color.g, 0.0f, 1.0f);
2808 base_color.b = math::clamp(base_color.b, 0.0f, 1.0f);
2809 metallic = math::clamp(metallic, 0.0f, 1.0f);
2810 roughness = math::clamp(roughness, 0.0f, 1.0f);
2811
2812 return std::make_tuple(base_color, metallic, roughness);
2813}
2814
2815
2816
2817} // namespace
2818
2820{
2821 struct log_stream : public Assimp::LogStream
2822 {
2823 log_stream(Assimp::Logger::ErrorSeverity s) : severity(s)
2824 {
2825 }
2826
2827 void write(const char* message) override
2828 {
2829 switch(severity)
2830 {
2831 case Assimp::Logger::Info:
2832 APPLOG_INFO("Mesh Importer: {0}", message);
2833 break;
2834 case Assimp::Logger::Warn:
2835 APPLOG_WARNING("Mesh Importer: {0}", message);
2836 break;
2837 case Assimp::Logger::Err:
2838 APPLOG_ERROR("Mesh Importer: {0}", message);
2839 break;
2840 default:
2841 APPLOG_TRACE("Mesh Importer: {0}", message);
2842 break;
2843 }
2844 }
2845
2846 Assimp::Logger::ErrorSeverity severity{};
2847 };
2848
2849 // if(Assimp::DefaultLogger::isNullLogger())
2850 // {
2851 // auto logger = Assimp::DefaultLogger::create("", Assimp::Logger::VERBOSE);
2852
2853 // logger->attachStream(new log_stream(Assimp::Logger::Debugging), Assimp::Logger::Debugging);
2854 // logger->attachStream(new log_stream(Assimp::Logger::Info), Assimp::Logger::Info);
2855 // logger->attachStream(new log_stream(Assimp::Logger::Warn), Assimp::Logger::Warn);
2856 // logger->attachStream(new log_stream(Assimp::Logger::Err), Assimp::Logger::Err);
2857 // }
2858}
2859
2861 const fs::path& path,
2862 const mesh_importer_meta& import_meta,
2863 mesh::load_data& load_data,
2864 std::vector<animation_clip>& animations,
2865 std::vector<imported_material>& materials,
2866 std::vector<imported_texture>& textures) -> bool
2867{
2868 Assimp::Importer importer;
2869
2870 int rvc_flags = aiComponent_CAMERAS | aiComponent_LIGHTS;
2871
2872 if(!import_meta.model.import_meshes)
2873 {
2874 rvc_flags |= aiComponent_MESHES;
2875 }
2876
2877 if(!import_meta.animations.import_animations)
2878 {
2879 rvc_flags |= aiComponent_ANIMATIONS;
2880 }
2881
2882 if(!import_meta.materials.import_materials)
2883 {
2884 rvc_flags |= aiComponent_MATERIALS;
2885 }
2886
2887 importer.SetPropertyInteger(AI_CONFIG_PP_RVC_FLAGS, rvc_flags);
2888 importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_LINE | aiPrimitiveType_POINT);
2889 importer.SetPropertyBool(AI_CONFIG_FBX_CONVERT_TO_M, true);
2890 importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
2891
2892 fs::path file = path.stem();
2893 fs::path output_dir = path.parent_path();
2894
2895 // clang-format off
2896
2897 uint32_t flags = aiProcess_FlipUVs |
2898 aiProcess_RemoveComponent |
2899 aiProcess_Triangulate |
2900 aiProcess_CalcTangentSpace |
2901 aiProcess_GenUVCoords |
2902 aiProcess_GenSmoothNormals |
2903 aiProcess_GenBoundingBoxes |
2904 aiProcess_ImproveCacheLocality |
2905 aiProcess_LimitBoneWeights |
2906 aiProcess_SortByPType |
2907 aiProcess_TransformUVCoords |
2908 aiProcess_GlobalScale;
2909
2910 // clang-format on
2911
2912 if(import_meta.model.weld_vertices)
2913 {
2914 flags |= aiProcess_JoinIdenticalVertices;
2915 }
2916 if(import_meta.model.optimize_meshes)
2917 {
2918 flags |= aiProcess_OptimizeMeshes;
2919 }
2920 if(import_meta.model.split_large_meshes)
2921 {
2922 flags |= aiProcess_SplitLargeMeshes;
2923 }
2924 if(import_meta.model.find_degenerates)
2925 {
2926 flags |= aiProcess_FindDegenerates;
2927 }
2928 if(import_meta.model.find_invalid_data)
2929 {
2930 flags |= aiProcess_FindInvalidData;
2931 }
2932 if(import_meta.materials.remove_redundant_materials)
2933 {
2934 flags |= aiProcess_RemoveRedundantMaterials;
2935 }
2936
2937 APPLOG_TRACE("Mesh Importer: Loading {}", path.generic_string());
2938
2939 const aiScene* scene = read_file(importer, path, flags);
2940
2941 if(scene == nullptr)
2942 {
2943 APPLOG_ERROR(importer.GetErrorString());
2944 return false;
2945 }
2946
2947 // We need to modify the scene, so we cast away const (be cautious in production).
2948 aiScene* modScene = const_cast<aiScene*>(scene);
2949
2950 //CollapseAssimpFBXPivotsAndAnimations(modScene);
2951 process_imported_scene(am, file, output_dir, modScene, load_data, animations, materials, textures);
2952
2953 APPLOG_TRACE("Mesh Importer: Done with {}", path.generic_string());
2954
2955 return true;
2956}
2957} // namespace importer
2958
2959} // namespace unravel
bimg::ImageContainer * imageLoad(const void *data, uint32_t size, bgfx::TextureFormat::Enum _dstFormat)
bool imageSave(const char *saveAs, bimg::ImageContainer *image)
entt::handle b
manifold_type type
btVector3 normal
entt::handle a
General purpose transformation class designed to maintain each component of the transformation separa...
Definition transform.hpp:27
static auto identity() noexcept -> const transform_t &
Get the identity transform.
Manages assets, including loading, unloading, and storage.
Main class representing a 3D mesh with support for different LODs, submeshes, and skinning.
Definition mesh.h:310
std::string name
Definition hub.cpp:27
#define APPLOG_WARNING(...)
Definition logging.h:19
#define APPLOG_ERROR(...)
Definition logging.h:20
#define APPLOG_TRACE_PERF(T)
Definition logging.h:90
#define APPLOG_INFO(...)
Definition logging.h:18
#define APPLOG_TRACE_PERF_NAMED(T, name)
Definition logging.h:95
#define APPLOG_TRACE(...)
Definition logging.h:17
path convert_to_protocol(const path &_path)
Oposite of the resolve_protocol this function tries to convert to protocol path from an absolute one.
void vertex_pack(const float _input[4], bool _inputNormalized, attribute _attr, const vertex_layout &_decl, void *_data, uint32_t _index)
Definition graphics.cpp:192
auto replace(const std::string &str, const std::string &search, const std::string &replace) -> std::string
Definition utils.cpp:28
auto load_mesh_data_from_file(asset_manager &am, const fs::path &path, const mesh_importer_meta &import_meta, mesh::load_data &load_data, std::vector< animation_clip > &animations, std::vector< imported_material > &materials, std::vector< imported_texture > &textures) -> bool
@ box
Box type reflection probe.
glm::vec3 transform_point(const glm::mat4 &mat, const glm::vec3 &point)
auto blend(const math::transform &lhs, const math::transform &rhs, float factor) -> math::transform
static auto get_layout() -> const vertex_layout &
Definition vertex_decl.h:15
Storage for box vector values and wraps up common functionality.
Definition bbox.h:21
bbox & mul(const transform &t)
Transforms an axis aligned bounding box by the specified matrix.
Definition bbox.cpp:876
Struct used for mesh construction.
Definition mesh.h:388
Represents a scene in the ACE framework, managing entities and their relationships.
Definition scene.h:21