52void apply_texture_conversion(bimg::ImageContainer* image,
const std::string& semantic,
bool inverse);
53void process_raw_texture_data(
const aiTexture* assimp_tex,
const fs::path& output_file,
54 const std::string& semantic,
bool inverse);
55void apply_specular_to_metallic_roughness_conversion(bimg::ImageContainer* image);
56auto atomic_image_save(
const fs::path& output_file, bimg::ImageContainer* image) -> bool;
64struct spec_gloss_factors_t
76void apply_diffuse_to_base_color_conversion(bimg::ImageContainer* diffuse_image,
77 const bimg::ImageContainer* specular_image,
78 const spec_gloss_factors_t& factors,
79 std::vector<uint8_t>* out_mr_rgba8 =
nullptr,
80 bool rewrite_diffuse_to_base_color =
true);
81auto perceived_brightness(
float r,
float g,
float b) -> float;
82auto solve_metallic(
float perceived_diffuse,
float perceived_specular,
float one_minus_specular_strength) -> float;
83auto convert_specular_gloss_to_metallic_roughness(
const aiColor3D& diffuse_color,
84 const aiColor3D& specular_color,
85 float glossiness_factor) -> std::tuple<aiColor3D, float, float>;
92inline auto is_supported_ldr_format(bimg::TextureFormat::Enum
format) ->
bool
98 uint32_t bpp = bimg::getBitsPerPixel(
format);
99 return bpp == 8 || bpp == 16 || bpp == 24 || bpp == 32;
107inline auto get_bimg_allocator() -> bx::AllocatorI*
109 static thread_local bx::DefaultAllocator allocator;
122auto ensure_rgba8(bimg::ImageContainer* image,
bool& owns_result) -> bimg::ImageContainer*
129 if(image->m_format == bimg::TextureFormat::RGBA8)
133 auto* converted = bimg::imageConvert(get_bimg_allocator(), bimg::TextureFormat::RGBA8, *image);
136 APPLOG_WARNING(
"Mesh Importer: Failed to convert image to RGBA8 (source format = {})",
137 bimg::getName(image->m_format));
148auto resize_rgba8_image_to(
const bimg::ImageContainer* src, uint32_t target_w, uint32_t target_h) -> bimg::ImageContainer*
150 if(!src || !src->m_data || target_w == 0 || target_h == 0)
154 if(src->m_width == target_w && src->m_height == target_h)
159 bimg::ImageContainer* src32f =
160 bimg::imageConvert(get_bimg_allocator(), bimg::TextureFormat::RGBA32F, *src,
false);
166 bimg::ImageContainer* dst32f = bimg::imageAlloc(get_bimg_allocator(),
167 bimg::TextureFormat::RGBA32F,
168 static_cast<uint16_t
>(target_w),
169 static_cast<uint16_t
>(target_h),
174 if(!dst32f || !bimg::imageResizeRgba32fLinear(dst32f, src32f))
176 bimg::imageFree(src32f);
179 bimg::imageFree(dst32f);
184 bimg::imageFree(src32f);
186 auto* dst8 = bimg::imageConvert(get_bimg_allocator(), bimg::TextureFormat::RGBA8, *dst32f,
false);
187 bimg::imageFree(dst32f);
191auto has_rotation_channel(
const aiAnimation* animation,
const std::string& nodeName) ->
bool
198 for(
unsigned int ch = 0; ch < animation->mNumChannels; ++ch)
200 aiNodeAnim* channel = animation->mChannels[ch];
207 if(std::string(channel->mNodeName.C_Str()) == nodeName)
210 if(channel->mNumRotationKeys > 1)
220auto has_rotation_channel(
const aiScene*
scene,
const std::string& nodeName) ->
bool
222 for(
unsigned int animIdx = 0; animIdx <
scene->mNumAnimations; ++animIdx)
224 aiAnimation* animation =
scene->mAnimations[animIdx];
226 if(has_rotation_channel(animation, nodeName))
234auto has_trannslation_channel(
const aiAnimation* animation,
const std::string& nodeName) ->
bool
241 for(
unsigned int ch = 0; ch < animation->mNumChannels; ++ch)
243 aiNodeAnim* channel = animation->mChannels[ch];
250 if(std::string(channel->mNodeName.C_Str()) == nodeName)
253 if(channel->mNumPositionKeys > 1)
265auto has_trannslation_channel(
const aiScene*
scene,
const std::string& nodeName) ->
bool
267 for(
unsigned int animIdx = 0; animIdx <
scene->mNumAnimations; ++animIdx)
269 aiAnimation* animation =
scene->mAnimations[animIdx];
271 if(has_trannslation_channel(animation, nodeName))
279enum channel_requirement
287auto find_first_animated_node_dfs(aiNode* node,
288 const aiScene*
scene,
289 const aiAnimation* animation,
290 channel_requirement req) -> aiNode*
297 case channel_requirement::translation:
299 if(has_trannslation_channel(animation, std::string(node->mName.C_Str())))
304 case channel_requirement::rotation:
306 if(has_rotation_channel(animation, std::string(node->mName.C_Str())))
313 if(has_trannslation_channel(animation, std::string(node->mName.C_Str())))
321 for(
unsigned int i = 0;
i < node->mNumChildren; ++
i)
323 aiNode* found = find_first_animated_node_dfs(node->mChildren[i],
scene, animation, req);
335auto find_root_motion_node_dfs(
const aiScene*
scene,
const aiAnimation* animation, channel_requirement req) -> aiNode*
342 return find_first_animated_node_dfs(
scene->mRootNode,
scene, animation, req);
346auto find_first_animated_node_bfs(
const aiScene*
scene,
const aiAnimation* animation, channel_requirement req)
354 std::queue<aiNode*> nodeQueue;
355 nodeQueue.push(
scene->mRootNode);
357 while(!nodeQueue.empty())
359 aiNode* current = nodeQueue.front();
364 case channel_requirement::translation:
366 if(has_trannslation_channel(animation, std::string(current->mName.C_Str())))
371 case channel_requirement::rotation:
373 if(has_rotation_channel(animation, std::string(current->mName.C_Str())))
380 if(has_trannslation_channel(animation, std::string(current->mName.C_Str())))
388 if(has_trannslation_channel(animation, std::string(current->mName.C_Str())))
394 for(
unsigned int i = 0;
i < current->mNumChildren; ++
i)
396 nodeQueue.push(current->mChildren[i]);
405auto find_root_motion_node_bfs(
const aiScene*
scene,
const aiAnimation* animation, channel_requirement req) -> aiNode*
407 return find_first_animated_node_bfs(
scene, animation, req);
410void apply_import_facing_correction_to_load_data(mesh::load_data& load_data)
412 if(!load_data.root_node)
417 load_data.root_node->local_transform.rotate(math::radians(math::vec3{0.0f, 180.0f, 0.0f}));
419 if(!load_data.bbox.is_populated())
425 correction.
set_rotation(glm::angleAxis(math::pi<float>(), math::vec3(0.0f, 1.0f, 0.0f)));
429void accumulate_bounds_from_armature(
const mesh::load_data& load_data,
math::bbox& out)
431 if(!load_data.root_node)
436 const std::function<void(
const mesh::armature_node&,
const math::transform&)> visit =
437 [&](
const mesh::armature_node& node,
const math::transform& parent_transform)
439 const math::transform world_transform = parent_transform * node.local_transform;
441 for(uint32_t submesh_index : node.submeshes)
443 if(submesh_index >= load_data.submeshes.size())
448 const auto transformed_bbox =
449 math::bbox::mul(load_data.submeshes[submesh_index].bbox, world_transform);
450 out.add_point(transformed_bbox.min);
451 out.add_point(transformed_bbox.max);
454 for(
const auto& child : node.children)
458 visit(*child, world_transform);
468auto get_texture_extension_from_texture(
const aiTexture* texture) -> std::string
470 if(texture->achFormatHint[0] !=
'\0')
472 return std::string(
".") + texture->achFormatHint;
477auto get_texture_extension(
const aiTexture* texture) -> std::string
479 auto extension = get_texture_extension_from_texture(texture);
481 if(extension ==
".jpg" || extension ==
".jpeg")
497auto normalize_assimp_path(
const std::string& path) -> fs::path
505 return fs::path(normalized_path).lexically_normal();
508auto normalize_assimp_path(
const fs::path& path) -> fs::path
514 return normalize_assimp_path(path.generic_string());
517auto normalize_assimp_path(
const char* path) -> fs::path
519 if(path ==
nullptr || path[0] ==
'\0')
523 return normalize_assimp_path(std::string(path));
530auto resolve_external_texture_path(
const fs::path& base_dir, fs::path relative_path) -> fs::path
532 relative_path = normalize_assimp_path(relative_path);
534 if(fs::exists(base_dir / relative_path, ec))
536 return relative_path;
540 const auto parent = relative_path.parent_path();
541 const auto stem = relative_path.stem().string();
544 for(
const auto& ext : extensions)
546 if(ext == requested_ext)
550 fs::path alternate = parent / (stem + ext);
551 if(fs::exists(base_dir / alternate, ec))
553 APPLOG_WARNING(
"Mesh Importer: Texture '{}' not found, using '{}' instead",
554 relative_path.generic_string(),
555 alternate.generic_string());
560 return relative_path;
563auto get_embedded_texture_name(
const aiTexture* texture,
566 const std::string& semantic) -> std::string
568 return fmt::format(
"[{}] {} {}{}",
index, semantic,
filename.string(), get_texture_extension(texture));
571auto process_matrix(
const aiMatrix4x4& assimp_matrix) -> math::mat4
575 matrix[0][0] = assimp_matrix.a1;
576 matrix[1][0] = assimp_matrix.a2;
577 matrix[2][0] = assimp_matrix.a3;
578 matrix[3][0] = assimp_matrix.a4;
580 matrix[0][1] = assimp_matrix.b1;
581 matrix[1][1] = assimp_matrix.b2;
582 matrix[2][1] = assimp_matrix.b3;
583 matrix[3][1] = assimp_matrix.b4;
585 matrix[0][2] = assimp_matrix.c1;
586 matrix[1][2] = assimp_matrix.c2;
587 matrix[2][2] = assimp_matrix.c3;
588 matrix[3][2] = assimp_matrix.c4;
590 matrix[0][3] = assimp_matrix.d1;
591 matrix[1][3] = assimp_matrix.d2;
592 matrix[2][3] = assimp_matrix.d3;
593 matrix[3][3] = assimp_matrix.d4;
598void process_vertices(aiMesh* mesh, mesh::load_data& load_data)
600 auto& submesh = load_data.submeshes.back();
603 bool has_position = load_data.vertex_format.has(gfx::attribute::Position);
604 bool has_normal = load_data.vertex_format.has(gfx::attribute::Normal);
605 bool has_bitangent = load_data.vertex_format.has(gfx::attribute::Bitangent);
606 bool has_tangent = load_data.vertex_format.has(gfx::attribute::Tangent);
607 bool has_texcoord0 = load_data.vertex_format.has(gfx::attribute::TexCoord0);
608 auto vertex_stride = load_data.vertex_format.getStride();
610 std::uint32_t current_vertex = load_data.vertex_count;
611 load_data.vertex_count += mesh->mNumVertices;
612 load_data.vertex_data.resize(load_data.vertex_count * vertex_stride);
614 std::uint8_t* current_vertex_ptr = load_data.vertex_data.data() + current_vertex * vertex_stride;
616 for(
size_t i = 0;
i < mesh->mNumVertices; ++
i, current_vertex_ptr += vertex_stride)
619 if(mesh->HasPositions() && has_position)
622 std::memcpy(
position, &mesh->mVertices[i],
sizeof(aiVector3D));
633 float textureCoords[4] = {0.0f, 0.0f, 0.0f, 0.0f};
634 if(mesh->HasTextureCoords(0))
636 std::memcpy(textureCoords, &mesh->mTextureCoords[0][i],
sizeof(aiVector2D));
640 gfx::attribute::TexCoord0,
641 load_data.vertex_format,
648 gfx::attribute::TexCoord0,
649 load_data.vertex_format,
657 if(mesh->HasNormals() && has_normal)
659 std::memcpy(math::value_ptr(
normal), &mesh->mNormals[i],
sizeof(aiVector3D));
663 gfx::attribute::Normal,
664 load_data.vertex_format,
668 math::vec4 tangent{};
672 if(mesh->HasTangentsAndBitangents())
674 std::memcpy(math::value_ptr(tangent), &mesh->mTangents[i],
sizeof(aiVector3D));
680 tangent = math::vec4(0.0f, 0.0f, 0.0f, 0.0f);
685 gfx::attribute::Tangent,
686 load_data.vertex_format,
692 math::vec4 bitangent{};
695 if(mesh->HasTangentsAndBitangents())
697 std::memcpy(math::value_ptr(bitangent), &mesh->mBitangents[i],
sizeof(aiVector3D));
701 bitangent = math::vec4(0.0f, 0.0f, 0.0f, 0.0f);
710 gfx::attribute::Bitangent,
711 load_data.vertex_format,
717void process_faces(aiMesh* mesh, std::uint32_t submesh_offset, mesh::load_data& load_data)
719 load_data.triangle_count += mesh->mNumFaces;
721 load_data.triangle_data.reserve(load_data.triangle_data.size() + mesh->mNumFaces);
723 for(
size_t i = 0;
i < mesh->mNumFaces; ++
i)
725 aiFace face = mesh->mFaces[
i];
727 auto& triangle = load_data.triangle_data.emplace_back();
728 triangle.data_group_id = mesh->mMaterialIndex;
730 auto num_indices = std::min<size_t>(face.mNumIndices, 3);
731 for(
size_t j = 0;
j < num_indices; ++
j)
733 triangle.indices[
j] = face.mIndices[
j] + submesh_offset;
738void process_bones(aiMesh* mesh, std::uint32_t submesh_offset, mesh::load_data& load_data)
742 auto& bone_influences = load_data.skin_data.get_bones();
744 for(
size_t i = 0;
i < mesh->mNumBones; ++
i)
746 aiBone* assimp_bone = mesh->mBones[
i];
747 const std::string bone_name = assimp_bone->mName.C_Str();
749 auto it = std::find_if(std::begin(bone_influences),
750 std::end(bone_influences),
751 [&bone_name](
const auto& bone)
753 return bone_name == bone.bone_id;
756 skin_bind_data::bone_influence* bone_ptr =
nullptr;
757 if(it != std::end(bone_influences))
763 const auto& assimp_matrix = assimp_bone->mOffsetMatrix;
764 skin_bind_data::bone_influence bone_influence;
765 bone_influence.bone_id = bone_name;
766 bone_influence.bind_pose_transform = process_matrix(assimp_matrix);
767 bone_influences.emplace_back(std::move(bone_influence));
768 bone_ptr = &bone_influences.back();
771 if(bone_ptr ==
nullptr)
776 for(
size_t j = 0;
j < assimp_bone->mNumWeights; ++
j)
778 aiVertexWeight assimp_influence = assimp_bone->mWeights[
j];
780 skin_bind_data::vertex_influence influence;
781 influence.vertex_index = assimp_influence.mVertexId + submesh_offset;
782 influence.weight = assimp_influence.mWeight;
784 bone_ptr->influences.emplace_back(influence);
790 if(assimp_influence.mVertexId < mesh->mNumVertices && assimp_influence.mWeight > 0.0f)
792 const auto& vertex = mesh->mVertices[assimp_influence.mVertexId];
793 const math::vec3 mesh_space_position(vertex.x, vertex.y, vertex.z);
794 const math::vec3 bone_space_position =
795 bone_ptr->bind_pose_transform.transform_coord(mesh_space_position);
796 bone_ptr->bounds.add_point(bone_space_position);
803auto make_stable_submesh_id(
const char*
name,
const mesh::load_data& load_data) -> uint32_t
807 uint32_t hash = 2166136261u;
809 for(
const char* c =
name; *
c !=
'\0'; ++
c)
811 hash ^=
static_cast<uint8_t
>(*c);
819 hash = 2166136261u ^
static_cast<uint32_t
>(load_data.submeshes.size() + 1);
826 auto collides = [&](uint32_t candidate)
828 return std::any_of(load_data.submeshes.begin(),
829 load_data.submeshes.end(),
830 [candidate](
const mesh::submesh& sm)
832 return sm.stable_id == candidate;
835 while(collides(hash))
837 hash = hash * 16777619u + 1u;
846void process_mesh(aiMesh* mesh, mesh::load_data& load_data)
848 load_data.submeshes.emplace_back();
849 auto& submesh = load_data.submeshes.back();
850 submesh.vertex_start = load_data.vertex_count;
851 submesh.vertex_count = mesh->mNumVertices;
852 submesh.face_start = load_data.triangle_count;
853 submesh.face_count = mesh->mNumFaces;
854 submesh.data_group_id = mesh->mMaterialIndex;
855 submesh.skinned = mesh->HasBones();
856 submesh.stable_id = make_stable_submesh_id(mesh->mName.C_Str(), load_data);
857 load_data.material_count = std::max(load_data.material_count, submesh.data_group_id + 1);
859 process_faces(mesh, submesh.vertex_start, load_data);
860 process_bones(mesh, submesh.vertex_start, load_data);
861 process_vertices(mesh, load_data);
864void process_meshes(
const aiScene*
scene, mesh::load_data& load_data)
866 for(
size_t i = 0;
i <
scene->mNumMeshes; ++
i)
868 aiMesh* mesh =
scene->mMeshes[
i];
869 process_mesh(mesh, load_data);
873void process_node(
const aiScene*
scene,
874 mesh::load_data& load_data,
876 const std::unique_ptr<mesh::armature_node>& armature_node,
878 std::unordered_map<std::string, unsigned int>& node_to_index_lut)
880 armature_node->name = node->mName.C_Str();
881 armature_node->local_transform = process_matrix(node->mTransformation);
882 armature_node->children.resize(node->mNumChildren);
883 armature_node->index = node_to_index_lut[armature_node->name];
884 auto resolved_transform = parent_transform * armature_node->local_transform;
886 for(uint32_t i = 0;
i < node->mNumMeshes; ++
i)
888 uint32_t submesh_index = node->mMeshes[
i];
889 armature_node->submeshes.emplace_back(submesh_index);
891 auto& submesh = load_data.submeshes[submesh_index];
893 auto transformed_bbox =
math::bbox::mul(submesh.bbox, resolved_transform);
894 load_data.bbox.add_point(transformed_bbox.min);
895 load_data.bbox.add_point(transformed_bbox.max);
898 for(
size_t i = 0;
i < node->mNumChildren; ++
i)
900 armature_node->children[
i] = std::make_unique<mesh::armature_node>();
904 armature_node->children[i],
910void process_nodes(
const aiScene*
scene,
911 mesh::load_data& load_data,
912 std::unordered_map<std::string, unsigned int>& node_to_index_lut)
915 if(
scene->mRootNode !=
nullptr)
918 load_data.root_node = std::make_unique<mesh::armature_node>();
927 auto get_axis = [&](
const std::string&
name, math::vec3 fallback)
929 if(!
scene->mMetaData)
935 if(!
scene->mMetaData->Get<
int>(
name, axis))
940 if(!
scene->mMetaData->Get<
int>(
name +
"Sign", axis_sign))
944 math::vec3 result{0.0f, 0.0f, 0.0f};
946 if(axis < 0 || axis >= 3)
951 result[
axis] = float(axis_sign);
955 auto x_axis = get_axis(
"CoordAxis", {1.0f, 0.0f, 0.0f});
956 auto y_axis = get_axis(
"UpAxis", {0.0f, 1.0f, 0.0f});
957 auto z_axis = get_axis(
"FrontAxis", {0.0f, 0.0f, 1.0f});
962void dfs_assign_indices(
const aiNode* node,
963 std::unordered_map<std::string, unsigned int>& node_indices,
964 unsigned int& current_index)
967 node_indices[node->mName.C_Str()] = current_index;
973 for(
unsigned int i = 0;
i < node->mNumChildren; ++
i)
975 dfs_assign_indices(node->mChildren[i], node_indices, current_index);
979auto assign_node_indices(
const aiScene*
scene) -> std::unordered_map<std::string, unsigned int>
981 std::unordered_map<std::string, unsigned int> node_indices;
982 unsigned int current_index = 0;
987 dfs_assign_indices(
scene->mRootNode, node_indices, current_index);
993auto is_node_a_bone(
const std::string& node_name,
const aiScene*
scene) ->
bool
995 for(
unsigned int i = 0;
i <
scene->mNumMeshes; ++
i)
997 const aiMesh* mesh =
scene->mMeshes[
i];
998 for(
unsigned int j = 0;
j < mesh->mNumBones; ++
j)
1000 if(mesh->mBones[j]->mName.C_Str() == node_name)
1009auto is_node_a_parent_of_bone(
const std::string& node_name,
const aiScene*
scene) ->
bool
1011 for(
unsigned int i = 0;
i <
scene->mNumMeshes; ++
i)
1013 const aiMesh* mesh =
scene->mMeshes[
i];
1014 for(
unsigned int j = 0;
j < mesh->mNumBones; ++
j)
1016 const aiNode* bone_node =
scene->mRootNode->FindNode(mesh->mBones[j]->mName);
1017 const aiNode* current_node = bone_node;
1019 while(current_node !=
nullptr)
1021 if(current_node->mName.C_Str() == node_name)
1025 current_node = current_node->mParent;
1032auto is_node_a_submesh(
const std::string& node_name,
const aiScene*
scene) ->
bool
1034 const aiNode* node =
scene->mRootNode->FindNode(node_name.c_str());
1035 return node !=
nullptr && node->mNumMeshes > 0;
1038auto is_node_a_parent_of_submesh(
const std::string& node_name,
const aiScene*
scene) ->
bool
1040 const aiNode* root =
scene->mRootNode;
1042 for(
unsigned int i = 0;
i <
scene->mNumMeshes; ++
i)
1044 const aiMesh* mesh =
scene->mMeshes[
i];
1045 const aiNode* submesh_node = root->FindNode(mesh->mName);
1046 const aiNode* current_node = submesh_node;
1048 while(current_node !=
nullptr)
1050 if(current_node->mName.C_Str() == node_name)
1054 current_node = current_node->mParent;
1060void process_animation(
const aiScene*
scene,
1062 const aiAnimation* assimp_anim,
1063 mesh::load_data& load_data,
1064 std::unordered_map<std::string, unsigned int>& node_to_index_lut,
1065 animation_clip& anim)
1068 anim.name = fixed_name;
1069 auto ticks_per_second = assimp_anim->mTicksPerSecond;
1070 if(ticks_per_second < 0.001)
1072 ticks_per_second = 25.0;
1075 auto ticks = assimp_anim->mDuration;
1077 anim.duration =
decltype(anim.duration)(ticks / ticks_per_second);
1079 if(assimp_anim->mNumChannels > 0)
1081 anim.channels.reserve(assimp_anim->mNumChannels);
1083 bool needs_sort =
false;
1086 for(
size_t i = 0;
i < assimp_anim->mNumChannels; ++
i)
1088 const aiNodeAnim* assimp_node_anim = assimp_anim->mChannels[
i];
1090 bool is_bone = is_node_a_bone(assimp_node_anim->mNodeName.C_Str(),
scene);
1091 bool is_parent_of_bone = is_node_a_parent_of_bone(assimp_node_anim->mNodeName.C_Str(),
scene);
1092 bool is_submesh = is_node_a_submesh(assimp_node_anim->mNodeName.C_Str(),
scene);
1093 bool is_parent_of_submesh = is_node_a_parent_of_submesh(assimp_node_anim->mNodeName.C_Str(),
scene);
1095 bool is_relevant = is_bone || is_parent_of_bone || is_submesh || is_parent_of_submesh;
1104 auto& node_anim = anim.channels.emplace_back();
1105 node_anim.node_name = assimp_node_anim->mNodeName.C_Str();
1106 node_anim.node_index = node_to_index_lut[node_anim.node_name];
1107 if(!needs_sort && anim.channels.size() > 1)
1109 auto& prev_node_anim = anim.channels[anim.channels.size() - 2];
1110 if(node_anim.node_index < prev_node_anim.node_index)
1116 if(assimp_node_anim->mNumPositionKeys > 0)
1118 node_anim.position_keys.resize(assimp_node_anim->mNumPositionKeys);
1121 for(
size_t idx = 0; idx < assimp_node_anim->mNumPositionKeys; ++idx)
1123 const auto& anim_key = assimp_node_anim->mPositionKeys[idx];
1124 auto&
key = node_anim.position_keys[idx];
1125 key.time =
decltype(
key.time)(anim_key.mTime / ticks_per_second);
1126 key.value.x = anim_key.mValue.x;
1127 key.value.y = anim_key.mValue.y;
1128 key.value.z = anim_key.mValue.z;
1131 if(assimp_node_anim->mNumRotationKeys > 0)
1133 node_anim.rotation_keys.resize(assimp_node_anim->mNumRotationKeys);
1136 for(
size_t idx = 0; idx < assimp_node_anim->mNumRotationKeys; ++idx)
1138 const auto& anim_key = assimp_node_anim->mRotationKeys[idx];
1139 auto&
key = node_anim.rotation_keys[idx];
1140 key.time =
decltype(
key.time)(anim_key.mTime / ticks_per_second);
1141 key.value.x = anim_key.mValue.x;
1142 key.value.y = anim_key.mValue.y;
1143 key.value.z = anim_key.mValue.z;
1144 key.value.w = anim_key.mValue.w;
1147 if(assimp_node_anim->mNumScalingKeys > 0)
1149 node_anim.scaling_keys.resize(assimp_node_anim->mNumScalingKeys);
1152 for(
size_t idx = 0; idx < assimp_node_anim->mNumScalingKeys; ++idx)
1154 const auto& anim_key = assimp_node_anim->mScalingKeys[idx];
1155 auto&
key = node_anim.scaling_keys[idx];
1156 key.time =
decltype(
key.time)(anim_key.mTime / ticks_per_second);
1157 key.value.x = anim_key.mValue.x;
1158 key.value.y = anim_key.mValue.y;
1159 key.value.z = anim_key.mValue.z;
1163 auto root_motion_translation_candidate =
1164 find_root_motion_node_bfs(
scene, assimp_anim, channel_requirement::translation);
1165 auto root_motion_rotation_candidate = find_root_motion_node_bfs(
scene, assimp_anim, channel_requirement::rotation);
1167 if(root_motion_translation_candidate)
1169 anim.root_motion.position_node_name = root_motion_translation_candidate->mName.C_Str();
1170 anim.root_motion.position_node_index = node_to_index_lut[anim.root_motion.position_node_name];
1172 if(root_motion_rotation_candidate)
1174 anim.root_motion.rotation_node_name = root_motion_rotation_candidate->mName.C_Str();
1175 anim.root_motion.rotation_node_index = node_to_index_lut[anim.root_motion.rotation_node_name];
1180 std::sort(anim.channels.begin(),
1181 anim.channels.end(),
1182 [](
const auto& lhs,
const auto& rhs)
1184 return lhs.node_index < rhs.node_index;
1188 APPLOG_TRACE(
"Mesh Importer : Animation {} discarded {} non relevat node keys", anim.name, skipped);
1190void process_animations(
const aiScene*
scene,
1192 mesh::load_data& load_data,
1193 std::unordered_map<std::string, unsigned int>& node_to_index_lut,
1194 std::vector<animation_clip>& animations)
1196 if(
scene->mNumAnimations > 0)
1198 animations.resize(
scene->mNumAnimations);
1201 for(
size_t i = 0;
i <
scene->mNumAnimations; ++
i)
1203 const aiAnimation* assimp_anim =
scene->mAnimations[
i];
1204 auto& anim = animations[
i];
1205 process_animation(
scene,
filename, assimp_anim, load_data, node_to_index_lut, anim);
1209void process_embedded_texture(
const aiTexture* assimp_tex,
1210 size_t assimp_tex_idx,
1213 std::vector<imported_texture>& textures)
1215 imported_texture texture{};
1217 auto rit = std::find_if(textures.rbegin(),
1219 [&](
const imported_texture& texture)
1221 return texture.embedded_index == static_cast<int>(assimp_tex_idx);
1223 if(rit != textures.rend())
1225 if(rit->process_count > 0)
1230 rit->process_count++;
1233 else if(assimp_tex->mFilename.length > 0)
1235 texture.name = normalize_assimp_path(assimp_tex->mFilename.C_Str()).filename().string();
1239 texture.name = get_embedded_texture_name(assimp_tex, assimp_tex_idx,
filename,
"Texture");
1242 fs::path output_file =
output_dir / texture.name;
1244 if(assimp_tex->pcData)
1246 bool compressed = assimp_tex->mHeight == 0;
1247 bool raw = assimp_tex->mHeight > 0;
1252 size_t texture_size = assimp_tex->mWidth;
1255 bimg::ImageContainer* image =
imageLoad(assimp_tex->pcData,
static_cast<uint32_t
>(texture_size));
1259 apply_texture_conversion(image, texture.semantic, texture.inverse);
1261 atomic_image_save(output_file, image);
1263 bimg::imageFree(image);
1270 process_raw_texture_data(assimp_tex, output_file, texture.semantic, texture.inverse);
1278namespace pixel_transforms
1285 inline auto to_uint8(
float value) -> uint8_t
1287 return static_cast<uint8_t
>(std::lround(math::clamp(value, 0.0f, 1.0f) * 255.0f));
1293 template<
typename TransformFunc>
1294 void transform_pixel(uint8_t* pixel_data, uint32_t bytes_per_pixel, TransformFunc transform_func)
1296 if (bytes_per_pixel >= 4)
1299 float r = pixel_data[0] / 255.0f;
1300 float g = pixel_data[1] / 255.0f;
1301 float b = pixel_data[2] / 255.0f;
1302 float a = pixel_data[3] / 255.0f;
1304 auto [new_r, new_g, new_b, new_a] = transform_func(r, g,
b,
a);
1306 pixel_data[0] = to_uint8(new_r);
1307 pixel_data[1] = to_uint8(new_g);
1308 pixel_data[2] = to_uint8(new_b);
1309 pixel_data[3] = to_uint8(new_a);
1311 else if (bytes_per_pixel >= 3)
1314 float r = pixel_data[0] / 255.0f;
1315 float g = pixel_data[1] / 255.0f;
1316 float b = pixel_data[2] / 255.0f;
1319 auto [new_r, new_g, new_b, new_a] = transform_func(r, g,
b,
a);
1321 pixel_data[0] = to_uint8(new_r);
1322 pixel_data[1] = to_uint8(new_g);
1323 pixel_data[2] = to_uint8(new_b);
1325 else if (bytes_per_pixel == 2)
1328 float luminance = pixel_data[0] / 255.0f;
1329 float a = pixel_data[1] / 255.0f;
1331 auto [new_r, new_g, new_b, new_a] = transform_func(luminance, luminance, luminance,
a);
1333 pixel_data[0] = to_uint8(new_r);
1334 pixel_data[1] = to_uint8(new_a);
1336 else if (bytes_per_pixel == 1)
1339 float luminance = pixel_data[0] / 255.0f;
1341 auto [new_r, new_g, new_b, new_a] = transform_func(luminance, luminance, luminance, 1.0f);
1343 pixel_data[0] = to_uint8(new_r);
1351 auto shininess_to_roughness_pixel(
float r,
float g,
float b,
float a) -> std::tuple<float, float, float, float>
1353 constexpr float k_reference_max_shininess = 128.0f;
1354 const float shininess = std::max(std::max({r, g,
b}) * k_reference_max_shininess, 1.0f);
1355 const float roughness = std::sqrt(2.0f / (shininess + 2.0f));
1356 return std::make_tuple(roughness, roughness, roughness, 1.0f);
1363 auto compute_metallic_from_specular(
float r,
float g,
float b) ->
float
1365 constexpr float assumed_diffuse = 0.5f;
1366 float max_specular = std::max({r, g,
b});
1367 float one_minus_specular_strength = 1.0f - max_specular;
1368 float perc_diffuse = perceived_brightness(assumed_diffuse, assumed_diffuse, assumed_diffuse);
1369 float perc_specular = perceived_brightness(r, g,
b);
1370 return solve_metallic(perc_diffuse, perc_specular, one_minus_specular_strength);
1377 auto specular_to_metallic_roughness_alpha_pixel(
float r,
float g,
float b,
float a) -> std::tuple<float, float, float, float>
1379 float metallic = compute_metallic_from_specular(r, g,
b);
1380 float roughness = 1.0f -
a;
1381 return std::make_tuple(1.0f, roughness, metallic, 1.0f);
1388 auto specular_to_metallic_roughness_intensity_pixel(
float r,
float g,
float b,
float a) -> std::tuple<float, float, float, float>
1390 float metallic = compute_metallic_from_specular(r, g,
b);
1391 float avg_specular = (r + g +
b) / 3.0f;
1392 float roughness = 1.0f - avg_specular;
1393 return std::make_tuple(1.0f, roughness, metallic, 1.0f);
1399 auto simple_invert_pixel(
float r,
float g,
float b,
float a) -> std::tuple<float, float, float, float>
1401 return std::make_tuple(1.0f - r, 1.0f - g, 1.0f -
b, 1.0f -
a);
1408void apply_texture_conversion(bimg::ImageContainer* image,
const std::string& semantic,
bool inverse)
1410 if(!image || !image->m_data)
1414 if(!is_supported_ldr_format(image->m_format))
1416 APPLOG_WARNING(
"Mesh Importer: Skipping {} conversion on unsupported texture format (compressed/float/non-byte-aligned)", semantic);
1420 uint8_t* image_data =
static_cast<uint8_t*
>(image->m_data);
1421 uint32_t pixel_count = image->m_width * image->m_height;
1422 uint32_t bpp = bimg::getBitsPerPixel(image->m_format);
1423 uint32_t bytes_per_pixel = bpp / 8;
1425 if(semantic ==
"SpecularToMetallicRoughness")
1427 apply_specular_to_metallic_roughness_conversion(image);
1430 else if(semantic ==
"ShininessToRoughness")
1432 for(uint32_t i = 0;
i < pixel_count; ++
i)
1434 uint32_t pixel_index =
i * bytes_per_pixel;
1435 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1436 pixel_transforms::shininess_to_roughness_pixel);
1438 APPLOG_TRACE(
"Mesh Importer: Applied ShininessToRoughness conversion to texture");
1440 else if(semantic ==
"ExtractMetallicChannel")
1443 for(uint32_t i = 0;
i < pixel_count; ++
i)
1445 uint32_t pixel_index =
i * bytes_per_pixel;
1446 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1447 [](
float r,
float g,
float b,
float a) {
1449 return std::make_tuple(
b,
b,
b, 1.0f);
1452 APPLOG_TRACE(
"Mesh Importer: Extracted metallic channel for debugging");
1454 else if(semantic ==
"ExtractRoughnessChannel")
1457 for(uint32_t i = 0;
i < pixel_count; ++
i)
1459 uint32_t pixel_index =
i * bytes_per_pixel;
1460 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1461 [](
float r,
float g,
float b,
float a) {
1463 return std::make_tuple(g, g, g, 1.0f);
1466 APPLOG_TRACE(
"Mesh Importer: Extracted roughness channel for debugging");
1471 for(uint32_t i = 0;
i < pixel_count; ++
i)
1473 uint32_t pixel_index =
i * bytes_per_pixel;
1474 pixel_transforms::transform_pixel(&image_data[pixel_index], bytes_per_pixel,
1475 pixel_transforms::simple_invert_pixel);
1477 APPLOG_TRACE(
"Mesh Importer: Applied simple inversion to texture");
1494void apply_specular_to_metallic_roughness_conversion(bimg::ImageContainer* image)
1496 if(!image || !image->m_data)
1500 if(!is_supported_ldr_format(image->m_format))
1502 APPLOG_WARNING(
"Mesh Importer: Skipping SpecularToMetallicRoughness conversion on unsupported texture format");
1506 uint8_t* image_data =
static_cast<uint8_t*
>(image->m_data);
1507 uint32_t pixel_count = image->m_width * image->m_height;
1508 uint32_t bpp = bimg::getBitsPerPixel(image->m_format);
1509 uint32_t bytes_per_pixel = bpp / 8;
1515 if(bytes_per_pixel < 3)
1517 APPLOG_WARNING(
"Mesh Importer: Skipping SpecularToMetallicRoughness conversion on <3-channel source (cannot pack R/G/B)");
1522 const bool alpha_has_gloss = (bytes_per_pixel >= 4);
1526 for(uint32_t i = 0;
i < pixel_count; ++
i)
1528 uint32_t pixel_index =
i * bytes_per_pixel;
1529 pixel_transforms::transform_pixel(&image_data[pixel_index],
1531 pixel_transforms::specular_to_metallic_roughness_alpha_pixel);
1533 APPLOG_TRACE(
"Mesh Importer: Applied SpecularToMetallicRoughness conversion (alpha=gloss) to texture");
1537 for(uint32_t i = 0;
i < pixel_count; ++
i)
1539 uint32_t pixel_index =
i * bytes_per_pixel;
1540 pixel_transforms::transform_pixel(&image_data[pixel_index],
1542 pixel_transforms::specular_to_metallic_roughness_intensity_pixel);
1544 APPLOG_TRACE(
"Mesh Importer: Applied SpecularToMetallicRoughness conversion (intensity) to texture");
1573void apply_diffuse_to_base_color_conversion(bimg::ImageContainer* diffuse_image,
1574 const bimg::ImageContainer* specular_image,
1575 const spec_gloss_factors_t& factors,
1576 std::vector<uint8_t>* out_mr_rgba8,
1577 bool rewrite_diffuse_to_base_color)
1579 if(!diffuse_image || !diffuse_image->m_data || !specular_image || !specular_image->m_data)
1583 if(diffuse_image->m_width != specular_image->m_width || diffuse_image->m_height != specular_image->m_height)
1585 APPLOG_WARNING(
"Mesh Importer: Diffuse/specular texture size mismatch for base color conversion");
1588 if(!is_supported_ldr_format(diffuse_image->m_format) || !is_supported_ldr_format(specular_image->m_format))
1590 APPLOG_WARNING(
"Mesh Importer: Diffuse-to-base-color conversion requires uncompressed LDR textures; skipping");
1594 uint32_t d_bpp = bimg::getBitsPerPixel(diffuse_image->m_format) / 8;
1595 uint32_t s_bpp = bimg::getBitsPerPixel(specular_image->m_format) / 8;
1596 if(d_bpp < 3 || s_bpp < 3)
1600 APPLOG_WARNING(
"Mesh Importer: Diffuse-to-base-color conversion requires RGB inputs (diffuse={} bpp, specular={} bpp); skipping",
1601 d_bpp * 8, s_bpp * 8);
1605 constexpr float dielectric_f0 = 0.04f;
1606 constexpr float epsilon = 1e-6f;
1608 uint32_t
width = diffuse_image->m_width;
1609 uint32_t
height = diffuse_image->m_height;
1611 auto* d_data =
static_cast<uint8_t*
>(diffuse_image->m_data);
1612 const auto* s_data =
static_cast<const uint8_t*
>(specular_image->m_data);
1617 bool spec_alpha_has_gloss = (s_bpp >= 4);
1619 if(out_mr_rgba8 !=
nullptr)
1621 out_mr_rgba8->assign(
static_cast<size_t>(pixel_count) * 4, 0);
1624 for(uint32_t i = 0;
i < pixel_count; ++
i)
1633 float dr =
static_cast<float>(d_data[
i * d_bpp + 0]) / 255.0f * factors.diffuse_r;
1634 float dg =
static_cast<float>(d_data[
i * d_bpp + 1]) / 255.0f * factors.diffuse_g;
1635 float db =
static_cast<float>(d_data[
i * d_bpp + 2]) / 255.0f * factors.diffuse_b;
1637 float sr =
static_cast<float>(s_data[
i * s_bpp + 0]) / 255.0f * factors.specular_r;
1638 float sg =
static_cast<float>(s_data[
i * s_bpp + 1]) / 255.0f * factors.specular_g;
1639 float sb =
static_cast<float>(s_data[
i * s_bpp + 2]) / 255.0f * factors.specular_b;
1640 float sa = (s_bpp >= 4) ?
static_cast<float>(s_data[i * s_bpp + 3]) / 255.0f * factors.glossiness
1641 : factors.glossiness;
1643 float max_specular = std::max({sr, sg, sb});
1644 float one_minus_spec_str = 1.0f - max_specular;
1645 float perc_d = perceived_brightness(dr, dg, db);
1646 float perc_s = perceived_brightness(sr, sg, sb);
1647 float metallic = solve_metallic(perc_d, perc_s, one_minus_spec_str);
1656 float denom = std::max(1.0f - metallic * dielectric_f0, epsilon);
1657 float spec_offset = dielectric_f0 * (1.0f - metallic);
1658 float t = metallic * metallic;
1660 auto base_d = [&](
float d) ->
float {
return d * (1.0f - dielectric_f0) / denom; };
1661 auto base_s = [&](
float s) ->
float {
return s - spec_offset; };
1663 if(rewrite_diffuse_to_base_color)
1665 float br = math::mix(base_d(dr), base_s(sr), t);
1666 float bg = math::mix(base_d(dg), base_s(sg), t);
1667 float bb = math::mix(base_d(db), base_s(sb), t);
1669 d_data[
i * d_bpp + 0] = pixel_transforms::to_uint8(br);
1670 d_data[
i * d_bpp + 1] = pixel_transforms::to_uint8(bg);
1671 d_data[
i * d_bpp + 2] = pixel_transforms::to_uint8(bb);
1677 float da =
static_cast<float>(d_data[
i * d_bpp + 3]) / 255.0f * factors.diffuse_a;
1678 d_data[
i * d_bpp + 3] = pixel_transforms::to_uint8(da);
1682 if(out_mr_rgba8 !=
nullptr)
1684 float roughness = spec_alpha_has_gloss
1686 : (1.0f - (sr + sg + sb) / 3.0f);
1688 uint8_t* mr = out_mr_rgba8->data() +
static_cast<size_t>(i) * 4;
1690 mr[1] = pixel_transforms::to_uint8(roughness);
1691 mr[2] = pixel_transforms::to_uint8(metallic);
1696 APPLOG_TRACE(
"Mesh Importer: Applied spec-gloss conversion{} (rewrite base color: {})",
1697 out_mr_rgba8 ?
" with sibling metallic-roughness map" :
"",
1698 rewrite_diffuse_to_base_color);
1705auto atomic_image_save(
const fs::path& output_file, bimg::ImageContainer* image) ->
bool
1710 [&](
const fs::path& temp)
1712 imageSave(temp.string().c_str(), image);
1732auto write_rgba8_png(
const fs::path& output_file,
1735 const uint8_t* rgba8_data) ->
bool
1740 [&](
const fs::path& temp)
1742 bx::FileWriter writer;
1744 if(!bx::open(&writer, temp.string().c_str(),
false, &err))
1748 bimg::imageWritePng(&writer,
1753 bimg::TextureFormat::RGBA8,
1765struct spec_gloss_pbr_result
1772auto convert_spec_gloss_to_pbr_textures(
const fs::path&
output_dir,
1775 bimg::ImageContainer* diffuse_img,
1776 const bimg::ImageContainer* specular_img,
1777 const spec_gloss_factors_t& factors,
1780 spec_gloss_pbr_result result{};
1782 if(!diffuse_img || !specular_img)
1790 bool diffuse_was_converted =
false;
1791 bool specular_was_converted =
false;
1792 bimg::ImageContainer* diffuse_rgba8 = ensure_rgba8(diffuse_img, diffuse_was_converted);
1793 bimg::ImageContainer* specular_rgba8 = ensure_rgba8(
const_cast<bimg::ImageContainer*
>(specular_img), specular_was_converted);
1795 bool specular_resized =
false;
1796 bimg::ImageContainer* specular_work = specular_rgba8;
1798 auto free_intermediates = [&]()
1800 if(specular_resized && specular_work)
1802 bimg::imageFree(specular_work);
1804 if(diffuse_was_converted && diffuse_rgba8)
1806 bimg::imageFree(diffuse_rgba8);
1808 if(specular_was_converted && specular_rgba8)
1810 bimg::imageFree(specular_rgba8);
1814 if(!diffuse_rgba8 || !specular_rgba8)
1816 APPLOG_WARNING(
"Mesh Importer: Spec-gloss conversion skipped — could not normalize inputs to RGBA8");
1817 free_intermediates();
1821 if(diffuse_rgba8->m_width != specular_rgba8->m_width || diffuse_rgba8->m_height != specular_rgba8->m_height)
1823 bimg::ImageContainer* resized =
1824 resize_rgba8_image_to(specular_rgba8, diffuse_rgba8->m_width, diffuse_rgba8->m_height);
1827 APPLOG_WARNING(
"Mesh Importer: Failed to resize specular {}x{} to match diffuse {}x{} for spec-gloss conversion",
1828 specular_rgba8->m_width,
1829 specular_rgba8->m_height,
1830 diffuse_rgba8->m_width,
1831 diffuse_rgba8->m_height);
1832 free_intermediates();
1835 specular_work = resized;
1836 specular_resized =
true;
1837 APPLOG_TRACE(
"Mesh Importer: Upscaled specular {}x{} -> {}x{} for spec-gloss pair conversion",
1838 specular_rgba8->m_width,
1839 specular_rgba8->m_height,
1840 diffuse_rgba8->m_width,
1841 diffuse_rgba8->m_height);
1844 std::vector<uint8_t> mr_buffer;
1845 apply_diffuse_to_base_color_conversion(diffuse_rgba8,
1852 if(mr_buffer.empty())
1854 APPLOG_WARNING(
"Mesh Importer: Spec-gloss conversion produced no output (size mismatch or unsupported format)");
1855 free_intermediates();
1862 diffuse_rgba8->m_width,
1863 diffuse_rgba8->m_height,
1864 static_cast<const uint8_t*
>(diffuse_rgba8->m_data)))
1867 free_intermediates();
1870 result.diffuse_converted =
true;
1874 if(write_rgba8_png(
output_dir /
mr_relative, diffuse_rgba8->m_width, diffuse_rgba8->m_height, mr_buffer.data()))
1880 APPLOG_WARNING(
"Mesh Importer: Failed to save sibling metallic-roughness texture for spec-gloss conversion");
1883 free_intermediates();
1890void process_raw_texture_data(
const aiTexture* assimp_tex,
const fs::path& output_file,
1891 const std::string& semantic,
bool inverse)
1894 uint32_t
width = assimp_tex->mWidth;
1895 uint32_t
height = assimp_tex->mHeight;
1899 std::memcpy(data.data(), assimp_tex->pcData,
width *
height * 4);
1902 if(semantic ==
"ShininessToRoughness" || semantic ==
"SpecularToMetallicRoughness")
1905 bimg::ImageContainer image;
1906 image.m_data = data.data();
1907 image.m_width =
width;
1910 image.m_format = bimg::TextureFormat::RGBA8;
1911 image.m_numMips = 1;
1912 image.m_hasAlpha =
true;
1914 apply_texture_conversion(&image, semantic, inverse);
1919 for(
size_t i = 0;
i < data.size(); ++
i)
1921 data[
i] = 255 - data[
i];
1928 write_rgba8_png(output_file,
width,
height, data.data());
1932void log_prop_value(aiMaterialProperty* prop,
const char* name1)
1934 auto data = (T*)prop->mData;
1936 auto count = prop->mDataLength /
sizeof(T);
1944 std::vector<T> vals(
count);
1945 std::memcpy(vals.data(), data,
count *
sizeof(T));
1950void log_materials(
const aiMaterial*
material)
1952 for(uint32_t i = 0;
i <
material->mNumProperties;
i++)
1959 if(prop->mDataLength > 0 && prop->mData)
1961 auto semantic = aiTextureType(prop->mSemantic);
1962 if(semantic != aiTextureType_NONE && semantic != aiTextureType_UNKNOWN)
1964 APPLOG_TRACE(
" semantic = {0}", aiTextureTypeToString(semantic));
1969 case aiPropertyTypeInfo::aiPTI_Float:
1971 log_prop_value<float>(prop,
"float");
1975 case aiPropertyTypeInfo::aiPTI_Double:
1977 log_prop_value<double>(prop,
"double");
1980 case aiPropertyTypeInfo::aiPTI_Integer:
1982 log_prop_value<int32_t>(prop,
"int");
1986 case aiPropertyTypeInfo::aiPTI_Buffer:
1988 log_prop_value<uint8_t>(prop,
"buffer");
1991 case aiPropertyTypeInfo::aiPTI_String:
1994 if(aiGetMaterialString(
material, prop->mKey.C_Str(), prop->mSemantic, prop->mIndex, &str) ==
2011enum class material_workflow
2015 khr_specular_glossiness,
2016 phong_specular_gloss,
2019auto phong_shininess_exponent_to_roughness(
float shininess) ->
float
2021 return math::clamp(std::sqrt(2.0f / (shininess + 2.0f)), 0.0f, 1.0f);
2027auto fbx_roughness_texture_is_native_roughness(
const aiMaterial*
material) ->
bool
2029 int use_glossiness = 0;
2031 &&
material->Get(
"$raw.3dsMax|main|useGlossiness", aiTextureType_NONE, 0, use_glossiness) == AI_SUCCESS
2032 && use_glossiness == 2;
2035auto material_workflow_label(material_workflow workflow) ->
const char*
2039 case material_workflow::metallic_roughness:
2040 return "Metallic/Roughness";
2041 case material_workflow::khr_specular_glossiness:
2042 return "KHR Specular/Glossiness";
2043 case material_workflow::phong_specular_gloss:
2044 return "Phong Specular/Gloss";
2050auto normalize_material_texture_path(
const fs::path& path) -> std::string
2055auto material_texture_paths_equal(
const fs::path&
left,
const fs::path&
right) ->
bool
2061 return normalize_material_texture_path(
left) == normalize_material_texture_path(
right);
2073auto base_color_texture_is_authoritative(
const aiMaterial*
material) ->
bool
2075 aiString base_path{};
2076 aiString diffuse_path{};
2077 const bool has_base =
2078 (
material->GetTexture(aiTextureType_BASE_COLOR, 0, &base_path) == AI_SUCCESS && base_path.length > 0)
2079 || (
material->GetTexture(AI_MATKEY_BASE_COLOR_TEXTURE, &base_path) == AI_SUCCESS && base_path.length > 0);
2080 const bool has_diffuse =
2081 material->GetTexture(aiTextureType_DIFFUSE, 0, &diffuse_path) == AI_SUCCESS && diffuse_path.length > 0;
2083 if(has_base && has_diffuse)
2085 return material_texture_paths_equal(normalize_assimp_path(base_path.C_Str()),
2086 normalize_assimp_path(diffuse_path.C_Str()));
2092auto texture_path_indicates_base_color(
const std::string& relative_path) ->
bool
2095 static constexpr std::array<const char*, 3> markers = {
2100 for(
const char* marker : markers)
2102 if(lower.find(marker) != std::string::npos)
2110auto albedo_texture_is_explicit_base_color(
const imported_texture& albedo_tex) ->
bool
2112 return albedo_tex.semantic ==
"BaseColor" || texture_path_indicates_base_color(albedo_tex.name);
2115auto material_shading_is_phong_family(aiShadingMode shading) ->
bool
2119 case aiShadingMode_Phong:
2120 case aiShadingMode_Blinn:
2121 case aiShadingMode_Minnaert:
2122 case aiShadingMode_Gouraud:
2123 case aiShadingMode_Flat:
2130auto material_has_pbr_brdf_shading(
const aiMaterial*
material) ->
bool
2132 aiShadingMode shading = aiShadingMode_Flat;
2133 return material->Get(AI_MATKEY_SHADING_MODEL, shading) == AI_SUCCESS && shading == aiShadingMode_PBR_BRDF;
2139auto material_has_glossiness_factor(
const aiMaterial*
material) ->
bool
2148auto should_reconstruct_base_color_for_spec_gloss_pair(material_workflow workflow,
2149 const aiMaterial*
material) ->
bool
2151 return workflow == material_workflow::khr_specular_glossiness
2152 && !base_color_texture_is_authoritative(
material);
2155auto string_ends_with(std::string_view value, std::string_view suffix) ->
bool
2157 return value.size() >= suffix.size()
2158 && value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0;
2164auto lumberyard_bistro_basecolor_specular_pair(
const aiMaterial*
material) ->
bool
2171 aiString diffuse_path{};
2172 aiString specular_path{};
2173 if(
material->GetTexture(aiTextureType_DIFFUSE, 0, &diffuse_path) != AI_SUCCESS || diffuse_path.length == 0)
2177 if(
material->GetTexture(aiTextureType_SPECULAR, 0, &specular_path) != AI_SUCCESS || specular_path.length == 0)
2182 const std::string diffuse_stem =
2184 const std::string specular_stem =
2187 if(!string_ends_with(diffuse_stem,
"_basecolor") || !string_ends_with(specular_stem,
"_specular"))
2192 constexpr std::string_view k_basecolor_suffix =
"_basecolor";
2193 constexpr std::string_view k_specular_suffix =
"_specular";
2194 const std::string diffuse_prefix =
2195 diffuse_stem.substr(0, diffuse_stem.size() - k_basecolor_suffix.size());
2196 const std::string specular_prefix =
2197 specular_stem.substr(0, specular_stem.size() - k_specular_suffix.size());
2198 return diffuse_prefix == specular_prefix;
2205auto specular_texture_path_looks_like_packed_mr(
const aiMaterial*
material) ->
bool
2212 if(lumberyard_bistro_basecolor_specular_pair(
material))
2218 if(
material->GetTexture(aiTextureType_SPECULAR, 0, &path) != AI_SUCCESS || path.length == 0)
2224 if(string_ends_with(stem,
"_spec"))
2228 return stem.find(
"_spec_") != std::string::npos;
2235auto material_has_packed_mr_in_specular_slot(
const aiMaterial*
material) ->
bool
2241 if(
material->GetTextureCount(aiTextureType_SPECULAR) == 0)
2245 if(
material->GetTextureCount(aiTextureType_SHININESS) > 0)
2250 const bool has_albedo_texture =
material->GetTextureCount(aiTextureType_DIFFUSE) > 0
2251 ||
material->GetTextureCount(aiTextureType_BASE_COLOR) > 0;
2252 if(!has_albedo_texture)
2259 return specular_texture_path_looks_like_packed_mr(
material);
2266auto has_metallic_roughness_texture_evidence(
const aiMaterial*
material) ->
bool
2269 if(
material->GetTexture(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, &path1) == AI_SUCCESS)
2275 if(
material->GetTexture(AI_MATKEY_METALLIC_TEXTURE, &path2) == AI_SUCCESS)
2280 if(
material->GetTextureCount(aiTextureType_METALNESS) > 0)
2285 if(
material->GetTextureCount(aiTextureType_GLTF_METALLIC_ROUGHNESS) > 0)
2290 if(
material->GetTextureCount(aiTextureType_DIFFUSE_ROUGHNESS) > 0)
2295 if(
material->GetTextureCount(aiTextureType_MAYA_SPECULAR_ROUGHNESS) > 0)
2301 if(
material->GetTextureCount(aiTextureType_UNKNOWN) > 0)
2303 ai_real metallic_dummy = 0.0f;
2304 if(material_has_pbr_brdf_shading(
material)
2305 ||
material->Get(AI_MATKEY_METALLIC_FACTOR, metallic_dummy) == AI_SUCCESS)
2311 if(material_has_packed_mr_in_specular_slot(
material))
2322auto has_native_mr_factor_evidence(
const aiMaterial*
material) ->
bool
2324 if(material_has_glossiness_factor(
material))
2328 if(!material_has_pbr_brdf_shading(
material))
2332 ai_real dummy = 0.0f;
2333 return material->Get(AI_MATKEY_METALLIC_FACTOR, dummy) == AI_SUCCESS
2334 ||
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, dummy) == AI_SUCCESS;
2340auto is_phong_legacy_material(
const aiMaterial*
material) ->
bool
2342 if(material_has_glossiness_factor(
material))
2346 if(has_metallic_roughness_texture_evidence(
material))
2350 if(has_native_mr_factor_evidence(
material))
2355 aiShadingMode shading = aiShadingMode_Flat;
2356 if(
material->Get(AI_MATKEY_SHADING_MODEL, shading) == AI_SUCCESS
2357 && material_shading_is_phong_family(shading))
2362 ai_real shininess = 0.0f;
2363 if(
material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
2368 if(
material->GetTextureCount(aiTextureType_SHININESS) > 0)
2373 if(!material_has_pbr_brdf_shading(
material) &&
material->GetTextureCount(aiTextureType_SPECULAR) > 0)
2384auto khr_needs_combined_specular_mr(
const aiMaterial*
material, material_workflow workflow) ->
bool
2386 if(workflow != material_workflow::khr_specular_glossiness)
2391 if(
material->GetTextureCount(aiTextureType_METALNESS) > 0
2392 ||
material->GetTextureCount(aiTextureType_GLTF_METALLIC_ROUGHNESS) > 0
2393 ||
material->GetTextureCount(aiTextureType_DIFFUSE_ROUGHNESS) > 0)
2398 if(
material->GetTextureCount(aiTextureType_SPECULAR) > 0)
2400 APPLOG_TRACE(
"Mesh Importer: KHR specular-only -> combined SpecularToMetallicRoughness conversion");
2412auto detect_material_workflow(
const aiMaterial*
material) -> material_workflow
2414 if(material_has_glossiness_factor(
material))
2416 APPLOG_TRACE(
"Mesh Importer: Workflow=KHR (AI_MATKEY_GLOSSINESS_FACTOR present)");
2417 return material_workflow::khr_specular_glossiness;
2420 if(has_metallic_roughness_texture_evidence(
material))
2422 if(material_has_packed_mr_in_specular_slot(
material))
2424 APPLOG_TRACE(
"Mesh Importer: Workflow=MR (packed metallic-roughness in aiTextureType_SPECULAR)");
2428 APPLOG_TRACE(
"Mesh Importer: Workflow=MR (dedicated metallic/roughness texture slots)");
2430 return material_workflow::metallic_roughness;
2433 if(has_native_mr_factor_evidence(
material))
2435 APPLOG_TRACE(
"Mesh Importer: Workflow=MR (PBR_BRDF + metallic/roughness factors, no glossiness)");
2436 return material_workflow::metallic_roughness;
2439 if(is_phong_legacy_material(
material))
2441 APPLOG_TRACE(
"Mesh Importer: Workflow=Phong (shininess/specular legacy signals)");
2442 return material_workflow::phong_specular_gloss;
2445 APPLOG_TRACE(
"Mesh Importer: Workflow=Unknown (no KHR/MR/Phong signals)");
2446 return material_workflow::unknown;
2449auto make_texture_catalog_key(
const imported_texture& tex) -> std::string
2451 if(tex.embedded_index >= 0)
2453 return fmt::format(
"e:{}:{}", tex.embedded_index, tex.semantic);
2455 return fmt::format(
"x:{}:{}", normalize_material_texture_path(normalize_assimp_path(tex.name)), tex.semantic);
2458auto needs_external_texture_conversion(
const imported_texture& tex) ->
bool
2460 return tex.embedded_index < 0 &&
2461 (tex.semantic ==
"ShininessToRoughness"
2462 || tex.semantic ==
"SpecularToMetallicRoughness" || tex.inverse);
2465auto make_converted_texture_name(
const std::string& original_name,
const std::string& semantic) -> std::string
2467 fs::path
p(original_name);
2468 const std::string suffix = semantic.empty() ?
"converted" : semantic;
2469 return (
p.parent_path() / (
p.stem().string() +
"_" + suffix +
".png")).generic_string();
2472auto build_converted_texture_name(
const fs::path&
filename,
2473 const aiScene*
scene,
2475 const std::string& source_relative,
2476 const std::string& target_semantic) -> std::string
2478 if(embedded_idx >= 0 && embedded_idx <
static_cast<int>(
scene->mNumTextures))
2480 return fmt::format(
"[{}] {} {}.png", embedded_idx, target_semantic,
filename.string());
2482 fs::path src(source_relative);
2483 return (src.parent_path() / (src.stem().string() +
"_" + target_semantic +
".png")).generic_string();
2486auto spec_gloss_factors_key(
const spec_gloss_factors_t& factors) -> std::string
2488 return fmt::format(
"{:.4f}_{:.4f}_{:.4f}_{:.4f}_{:.4f}_{:.4f}_{:.4f}_{:.4f}",
2496 factors.glossiness);
2499auto gather_spec_gloss_factors(
const aiMaterial*
material) -> spec_gloss_factors_t
2501 spec_gloss_factors_t factors{};
2507 aiColor4D diffuse_factor_color{1.0f, 1.0f, 1.0f, 1.0f};
2508 if(
material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse_factor_color) == AI_SUCCESS)
2510 factors.diffuse_r = diffuse_factor_color.r;
2511 factors.diffuse_g = diffuse_factor_color.g;
2512 factors.diffuse_b = diffuse_factor_color.b;
2513 factors.diffuse_a = diffuse_factor_color.a;
2516 aiColor3D specular_factor_color{1.0f, 1.0f, 1.0f};
2517 material->Get(AI_MATKEY_COLOR_SPECULAR, specular_factor_color);
2518 float specular_factor_scalar = 1.0f;
2519 material->Get(AI_MATKEY_SPECULAR_FACTOR, specular_factor_scalar);
2520 factors.specular_r = specular_factor_color.r * specular_factor_scalar;
2521 factors.specular_g = specular_factor_color.g * specular_factor_scalar;
2522 factors.specular_b = specular_factor_color.b * specular_factor_scalar;
2524 factors.glossiness = 1.0f;
2525 if(
material->Get(AI_MATKEY_GLOSSINESS_FACTOR, factors.glossiness) != AI_SUCCESS)
2527 float shininess = 32.0f;
2528 if(
material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
2530 factors.glossiness = math::clamp(1.0f - std::sqrt(2.0f / (shininess + 2.0f)), 0.0f, 1.0f);
2537class texture_catalog
2540 auto resolve(imported_texture& tex)
const ->
bool
2542 const auto it = entries_.find(make_texture_catalog_key(tex));
2543 if(it == entries_.end())
2547 tex.name = it->second.name;
2548 tex.flags = it->second.flags;
2549 tex.inverse = it->second.inverse;
2550 tex.process_count = it->second.process_count;
2551 tex.semantic = it->second.semantic;
2552 tex.embedded_index = it->second.embedded_index;
2556 void register_entry(
const imported_texture& lookup_key, imported_texture result)
2558 entries_[make_texture_catalog_key(lookup_key)] = std::move(result);
2561 void append_to_manifest(std::vector<imported_texture>& textures)
const
2563 for(
const auto& kvp : entries_)
2565 const auto&
entry = kvp.second;
2566 const auto exists = std::find_if(textures.begin(),
2568 [&](
const imported_texture& rhs)
2570 return rhs.embedded_index == entry.embedded_index
2571 && rhs.name == entry.name && rhs.semantic == entry.semantic;
2573 if(exists == textures.end())
2575 textures.push_back(
entry);
2580 void merge_from(
const texture_catalog& other)
2582 for(
const auto& kvp : other.entries_)
2584 entries_[kvp.first] = kvp.second;
2588 auto has_output_for_lookup(
const imported_texture& lookup_key,
const std::string& expected_output_relative)
const ->
bool
2590 imported_texture probe = lookup_key;
2595 return probe.name == expected_output_relative;
2599 std::unordered_map<std::string, imported_texture> entries_;
2602enum class texture_job_type : uint8_t
2621class texture_job_store
2624 auto jobs() const -> const
std::vector<texture_job>&
2629 auto try_add(texture_job job) ->
bool
2631 const std::string dedupe = make_dedupe_key(job);
2632 if(!dedupe_keys_.insert(dedupe).second)
2636 jobs_.push_back(std::move(job));
2641 static auto make_dedupe_key(
const texture_job& job) -> std::string
2645 case texture_job_type::embedded_extract:
2646 case texture_job_type::external_convert:
2647 return make_texture_catalog_key(job.desc);
2648 case texture_job_type::spec_gloss_pair:
2649 return fmt::format(
"sg:{}:{}:{}:{}",
2650 make_texture_catalog_key(job.desc),
2651 make_texture_catalog_key(job.specular_desc),
2652 spec_gloss_factors_key(job.spec_gloss_factors),
2653 job.bake_base_color ?
"bc" :
"mr");
2659 std::vector<texture_job> jobs_;
2660 std::unordered_set<std::string> dedupe_keys_;
2663struct material_import_env
2679auto load_image_for_texture_desc(
const aiScene*
scene,
2681 const imported_texture& tex,
2682 const char* assimp_path_cstr =
nullptr) -> bimg::ImageContainer*
2684 if(tex.embedded_index >= 0 && tex.embedded_index <
static_cast<int>(
scene->mNumTextures))
2686 const auto* embedded =
scene->mTextures[tex.embedded_index];
2687 if(embedded->pcData && embedded->mHeight == 0)
2689 return imageLoad(embedded->pcData,
static_cast<uint32_t
>(embedded->mWidth));
2694 if(assimp_path_cstr !=
nullptr && assimp_path_cstr[0] !=
'\0')
2696 const fs::path relative = resolve_external_texture_path(
output_dir, normalize_assimp_path(assimp_path_cstr));
2700 const fs::path relative = resolve_external_texture_path(
output_dir, normalize_assimp_path(tex.name));
2704auto try_synchronous_spec_gloss_pair_mr(
const fs::path&
output_dir,
2705 const aiScene*
scene,
2706 const imported_texture& pair_source,
2707 const imported_texture& specular_tex,
2708 const spec_gloss_factors_t& factors,
2709 const std::string& expected_mr,
2712 bimg::ImageContainer* diffuse_img = load_image_for_texture_desc(
scene,
output_dir, pair_source);
2713 bimg::ImageContainer* specular_img = load_image_for_texture_desc(
scene,
output_dir, specular_tex);
2717 APPLOG_WARNING(
"Mesh Importer: Bind-time pair MR could not load diffuse: {}", pair_source.name);
2721 APPLOG_WARNING(
"Mesh Importer: Bind-time pair MR could not load specular: {}", specular_tex.name);
2724 if(!diffuse_img || !specular_img)
2728 bimg::imageFree(diffuse_img);
2732 bimg::imageFree(specular_img);
2737 auto conv = convert_spec_gloss_to_pbr_textures(
output_dir,
2744 bimg::imageFree(diffuse_img);
2745 bimg::imageFree(specular_img);
2747 if(!conv.mr_relative.empty())
2749 APPLOG_TRACE(
"Mesh Importer: Bind-time pair metallic-roughness bake: {}", conv.mr_relative);
2751 return conv.mr_relative;
2754auto resolve_texture_on_disk(
const fs::path&
output_dir, fs::path relative) -> std::optional<fs::path>
2756 if(relative.empty())
2758 return std::nullopt;
2761 relative = resolve_external_texture_path(
output_dir, relative);
2764 fs::path absolute = relative.is_absolute() ? relative : (
output_dir / relative);
2765 absolute = fs::weakly_canonical(absolute, err);
2766 if(err || !fs::exists(absolute, err))
2768 return std::nullopt;
2774auto texture_file_exists(
const fs::path&
output_dir,
const std::string& relative) ->
bool
2776 return resolve_texture_on_disk(
output_dir, normalize_assimp_path(relative)).has_value();
2779auto try_make_texture_asset_key(
const fs::path&
output_dir,
const std::string& relative) -> std::optional<std::string>
2781 const auto absolute = resolve_texture_on_disk(
output_dir, normalize_assimp_path(relative));
2784 return std::nullopt;
2790 return std::nullopt;
2793 return key.generic_string();
2796auto find_spec_gloss_mr_relative(
const texture_catalog*
catalog,
2798 const imported_texture& specular_tex,
2799 const std::string& expected_mr) -> std::string
2803 imported_texture probe = specular_tex;
2804 if(
catalog->resolve(probe) && !probe.name.empty() && texture_file_exists(
output_dir, probe.name))
2809 if(texture_file_exists(
output_dir, expected_mr))
2816void mark_embedded_consumed_index(
int idx, std::unordered_set<int>& consumed, texture_catalog&
catalog)
2822 consumed.insert(idx);
2823 imported_texture
entry{};
2824 entry.embedded_index = idx;
2825 entry.process_count = 1;
2829auto get_texture_job_output_paths(
const texture_job& job) -> std::vector<std::string>
2831 std::vector<std::string> paths;
2834 case texture_job_type::embedded_extract:
2835 if(!job.desc.name.empty())
2837 paths.push_back(job.desc.name);
2840 case texture_job_type::external_convert:
2841 paths.push_back(make_converted_texture_name(job.desc.name, job.desc.semantic));
2843 case texture_job_type::spec_gloss_pair:
2844 if(!job.output_base_color_relative.empty())
2846 paths.push_back(job.output_base_color_relative);
2848 if(!job.output_mr_relative.empty())
2850 paths.push_back(job.output_mr_relative);
2859struct texture_job_disjoint_set
2876 void unite(
size_t a,
size_t b)
2889auto build_texture_job_composite_groups(
const std::vector<texture_job>& jobs) -> std::vector<std::vector<size_t>>
2896 texture_job_disjoint_set disjoint_set(jobs.size());
2897 std::unordered_map<std::string, size_t> path_to_job_index;
2899 for(
size_t job_index = 0; job_index < jobs.size(); ++job_index)
2901 for(
const auto& output_path : get_texture_job_output_paths(jobs[job_index]))
2903 if(output_path.empty())
2908 const auto existing = path_to_job_index.find(output_path);
2909 if(existing == path_to_job_index.end())
2911 path_to_job_index.emplace(output_path, job_index);
2915 disjoint_set.unite(job_index, existing->second);
2920 std::unordered_map<size_t, std::vector<size_t>> groups_by_root;
2921 groups_by_root.reserve(jobs.size());
2922 for(
size_t job_index = 0; job_index < jobs.size(); ++job_index)
2924 groups_by_root[disjoint_set.find(job_index)].push_back(job_index);
2927 std::vector<std::vector<size_t>> composites;
2928 composites.reserve(groups_by_root.size());
2929 for(
auto& kvp : groups_by_root)
2931 auto& group = kvp.second;
2932 std::sort(group.begin(), group.end());
2933 composites.push_back(std::move(group));
2936 std::sort(composites.begin(),
2938 [](
const std::vector<size_t>& lhs,
const std::vector<size_t>& rhs)
2940 return lhs.front() < rhs.front();
2946void sort_imported_textures(std::vector<imported_texture>& textures)
2948 std::sort(textures.begin(),
2950 [](
const imported_texture& lhs,
const imported_texture& rhs)
2952 if(lhs.embedded_index != rhs.embedded_index)
2954 return lhs.embedded_index < rhs.embedded_index;
2956 if(lhs.semantic != rhs.semantic)
2958 return lhs.semantic < rhs.semantic;
2960 return lhs.name < rhs.name;
2964void execute_texture_job(
const texture_job& job,
2967 const aiScene*
scene,
2969 std::unordered_set<int>& consumed_embedded)
2973 case texture_job_type::embedded_extract:
2975 imported_texture result = job.desc;
2976 std::vector<imported_texture> scratch;
2977 scratch.push_back(result);
2978 const auto* embedded =
scene->mTextures[result.embedded_index];
2979 process_embedded_texture(embedded,
static_cast<size_t>(result.embedded_index),
filename,
output_dir, scratch);
2980 result = scratch.back();
2981 catalog.register_entry(job.desc, result);
2982 consumed_embedded.insert(result.embedded_index);
2985 case texture_job_type::external_convert:
2987 imported_texture result = job.desc;
2988 fs::path original_file =
output_dir / result.name;
2989 const auto converted_name = make_converted_texture_name(result.name, result.semantic);
2990 fs::path converted_file =
output_dir / converted_name;
2991 bimg::ImageContainer* image =
imageLoad(bx::FilePath(original_file.string().c_str()));
2994 apply_texture_conversion(image, result.semantic, result.inverse);
2995 atomic_image_save(converted_file, image);
2996 bimg::imageFree(image);
2997 result.name = converted_name;
2998 APPLOG_TRACE(
"Mesh Importer: Applied {} conversion to external texture: {}", result.semantic, result.name);
3000 catalog.register_entry(job.desc, result);
3003 case texture_job_type::spec_gloss_pair:
3005 bimg::ImageContainer* diffuse_img = load_image_for_texture_desc(
scene,
output_dir, job.desc);
3006 bimg::ImageContainer* specular_img =
3007 load_image_for_texture_desc(
scene,
output_dir, job.specular_desc,
nullptr);
3011 APPLOG_WARNING(
"Mesh Importer: Spec-gloss pair job could not load diffuse texture: {}",
3016 APPLOG_WARNING(
"Mesh Importer: Spec-gloss pair job could not load specular texture: {}",
3017 job.specular_desc.name);
3020 if(diffuse_img && specular_img)
3022 auto conv = convert_spec_gloss_to_pbr_textures(
output_dir,
3023 job.output_base_color_relative,
3024 job.output_mr_relative,
3027 job.spec_gloss_factors,
3028 job.bake_base_color);
3029 if(conv.diffuse_converted)
3031 imported_texture base_result = job.desc;
3032 base_result.name = conv.base_color_relative;
3033 catalog.register_entry(job.desc, base_result);
3034 mark_embedded_consumed_index(job.desc.embedded_index, consumed_embedded,
catalog);
3036 if(!conv.mr_relative.empty())
3038 imported_texture mr_lookup = job.specular_desc;
3039 imported_texture mr_result = job.specular_desc;
3040 mr_result.name = conv.mr_relative;
3041 catalog.register_entry(mr_lookup, mr_result);
3042 mark_embedded_consumed_index(job.specular_desc.embedded_index, consumed_embedded,
catalog);
3043 APPLOG_TRACE(
"Mesh Importer: Wrote pair metallic-roughness: {}", conv.mr_relative);
3047 APPLOG_WARNING(
"Mesh Importer: Spec-gloss pair job produced no metallic-roughness output ({} + {})",
3049 job.specular_desc.name);
3054 bimg::imageFree(specular_img);
3058 bimg::imageFree(diffuse_img);
3067void execute_texture_job_composite(
const std::vector<texture_job>& jobs,
3068 const std::vector<size_t>& job_indices,
3071 const aiScene*
scene,
3073 std::unordered_set<int>& consumed_embedded)
3075 for(
const size_t job_index : job_indices)
3081void run_texture_jobs_parallel(texture_job_store& store,
3084 const aiScene*
scene,
3086 std::unordered_set<int>& consumed_embedded)
3088 const auto& jobs = store.jobs();
3094 const auto composites = build_texture_job_composite_groups(jobs);
3095 APPLOG_TRACE(
"Mesh Importer: Running {} texture job composites ({} jobs) in parallel",
3099 struct composite_result_t
3102 std::unordered_set<int> consumed_embedded;
3105 std::vector<composite_result_t> composite_results(composites.size());
3107 std::vector<size_t> composite_order(composites.size());
3108 std::iota(composite_order.begin(), composite_order.end(),
size_t{0});
3110 std::for_each(poolstl::par,
3111 composite_order.begin(),
3112 composite_order.end(),
3113 [&](
const size_t composite_index)
3115 auto& result = composite_results[composite_index];
3116 execute_texture_job_composite(jobs,
3117 composites[composite_index],
3122 result.consumed_embedded);
3125 for(
auto& result : composite_results)
3127 catalog.merge_from(result.catalog);
3128 consumed_embedded.insert(result.consumed_embedded.begin(), result.consumed_embedded.end());
3132void mark_embedded_consumed_from_textures(
const std::vector<imported_texture>& textures,
3133 std::unordered_set<int>& consumed_embedded)
3135 for(
const auto& tex : textures)
3137 if(tex.embedded_index >= 0 && tex.process_count > 0)
3139 consumed_embedded.insert(tex.embedded_index);
3144void collect_orphan_embedded_texture_jobs(
const aiScene*
scene,
3146 texture_job_store& store,
3147 const std::unordered_set<int>& consumed_embedded)
3149 for(
size_t i = 0;
i <
scene->mNumTextures; ++
i)
3151 if(consumed_embedded.count(
static_cast<int>(i)) > 0)
3156 imported_texture tex{};
3157 tex.embedded_index =
static_cast<int>(
i);
3158 tex.semantic =
"Texture";
3159 tex.name = get_embedded_texture_name(
scene->mTextures[i], i,
filename, tex.semantic);
3162 job.type = texture_job_type::embedded_extract;
3164 store.try_add(std::move(job));
3171template<
typename GetTextureFunc>
3172auto get_workflow_aware_texture(
const aiMaterial*
material,
3173 material_workflow workflow,
3174 const std::string& target_semantic,
3175 imported_texture& tex,
3176 GetTextureFunc get_imported_texture) ->
bool
3178 if(target_semantic ==
"BaseColor")
3181 if(workflow == material_workflow::khr_specular_glossiness
3182 || workflow == material_workflow::phong_specular_gloss)
3184 if(get_imported_texture(
material, aiTextureType_DIFFUSE, 0,
"BaseColor", tex))
3189 else if(get_imported_texture(
material, AI_MATKEY_BASE_COLOR_TEXTURE,
"BaseColor", tex))
3194 if(get_imported_texture(
material, aiTextureType_DIFFUSE, 0,
"BaseColor", tex))
3199 else if(target_semantic ==
"Metallic")
3202 if(workflow != material_workflow::khr_specular_glossiness)
3204 if(get_imported_texture(
material, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE,
"MetallicRoughness", tex))
3208 if(get_imported_texture(
material, AI_MATKEY_METALLIC_TEXTURE,
"Metallic", tex))
3214 if(workflow != material_workflow::phong_specular_gloss
3215 && get_imported_texture(
material, aiTextureType_UNKNOWN, 0,
"MetallicRoughness", tex))
3217 APPLOG_TRACE(
"Mesh Importer: Recovering metallic-roughness texture from aiTextureType_UNKNOWN slot");
3220 if(material_has_packed_mr_in_specular_slot(
material)
3221 && get_imported_texture(
material, aiTextureType_SPECULAR, 0,
"MetallicRoughness", tex))
3223 APPLOG_TRACE(
"Mesh Importer: Using packed metallic-roughness from aiTextureType_SPECULAR");
3228 else if(target_semantic ==
"Roughness")
3230 if(workflow != material_workflow::khr_specular_glossiness)
3232 if(get_imported_texture(
material, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE,
"MetallicRoughness", tex))
3236 if(get_imported_texture(
material, AI_MATKEY_ROUGHNESS_TEXTURE,
"Roughness", tex))
3240 if(workflow != material_workflow::phong_specular_gloss
3241 && get_imported_texture(
material, aiTextureType_UNKNOWN, 0,
"MetallicRoughness", tex))
3243 APPLOG_TRACE(
"Mesh Importer: Recovering metallic-roughness texture from aiTextureType_UNKNOWN slot");
3246 if(material_has_packed_mr_in_specular_slot(
material)
3247 && get_imported_texture(
material, aiTextureType_SPECULAR, 0,
"MetallicRoughness", tex))
3249 APPLOG_TRACE(
"Mesh Importer: Using packed metallic-roughness from aiTextureType_SPECULAR");
3254 if(workflow == material_workflow::phong_specular_gloss)
3256 if(fbx_roughness_texture_is_native_roughness(
material)
3257 && get_imported_texture(
material, aiTextureType_DIFFUSE_ROUGHNESS, 0,
"Roughness", tex))
3261 if(get_imported_texture(
material, aiTextureType_SHININESS, 0,
"ShininessToRoughness", tex))
3276void process_material_with_workflow_conversion(
const aiMaterial*
material,
3277 material_workflow workflow,
3278 aiColor3D& base_color,
3282 bool has_base_color = (
material->Get(AI_MATKEY_BASE_COLOR, base_color) == AI_SUCCESS);
3283 bool has_metallic = (
material->Get(AI_MATKEY_METALLIC_FACTOR, metallic) == AI_SUCCESS);
3284 bool has_roughness = (
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness) == AI_SUCCESS);
3287 if(workflow == material_workflow::khr_specular_glossiness)
3289 aiColor3D diffuse_color{1.0f, 1.0f, 1.0f};
3290 material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse_color);
3292 aiColor3D specular_color{0.04f, 0.04f, 0.04f};
3293 float specular_factor = 1.0f;
3294 material->Get(AI_MATKEY_COLOR_SPECULAR, specular_color);
3295 material->Get(AI_MATKEY_SPECULAR_FACTOR, specular_factor);
3296 specular_color.r *= specular_factor;
3297 specular_color.g *= specular_factor;
3298 specular_color.b *= specular_factor;
3303 float shininess = 32.0f;
3304 if(
material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
3306 glossiness = math::clamp(1.0f - std::sqrt(2.0f / (shininess + 2.0f)), 0.0f, 1.0f);
3310 auto [converted_base_color, converted_metallic, converted_roughness] =
3311 convert_specular_gloss_to_metallic_roughness(diffuse_color, specular_color,
glossiness);
3315 base_color = converted_base_color;
3316 APPLOG_TRACE(
"Mesh Importer: Converted base color from specular/diffuse workflow");
3318 metallic = converted_metallic;
3319 roughness = converted_roughness;
3320 APPLOG_TRACE(
"Mesh Importer: Converted PBR factors from KHR spec/gloss: metallic={:.3f}, roughness={:.3f}",
3321 metallic, roughness);
3323 else if(workflow == material_workflow::phong_specular_gloss)
3326 aiColor3D diffuse_tint{1.0f, 1.0f, 1.0f};
3327 if(
material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse_tint) != AI_SUCCESS)
3329 diffuse_tint = aiColor3D{1.0f, 1.0f, 1.0f};
3331 base_color = diffuse_tint;
3334 float shininess = 32.0f;
3335 if(
material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
3337 roughness = phong_shininess_exponent_to_roughness(shininess);
3343 APPLOG_TRACE(
"Mesh Importer: Phong -> MR factors: metallic={:.3f}, roughness={:.3f}",
3344 metallic, roughness);
3346 else if(workflow == material_workflow::metallic_roughness)
3350 if(
material->Get(AI_MATKEY_COLOR_DIFFUSE, base_color) != AI_SUCCESS)
3352 base_color = aiColor3D{1.0f, 1.0f, 1.0f};
3363 float shininess = 32.0f;
3364 if(
material->Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS)
3366 roughness = std::sqrt(2.0f / (shininess + 2.0f));
3378 if(
material->Get(AI_MATKEY_COLOR_DIFFUSE, base_color) != AI_SUCCESS)
3380 base_color = aiColor3D{1.0f, 1.0f, 1.0f};
3388 APPLOG_TRACE(
"Mesh Importer: Final PBR values - BaseColor: ({:.3f}, {:.3f}, {:.3f}), "
3389 "Metallic: {:.3f}, Roughness: {:.3f} [{}{}{}]",
3390 base_color.r, base_color.g, base_color.b, metallic, roughness,
3391 has_base_color ?
"B" :
"b",
3392 has_metallic ?
"M" :
"m",
3393 has_roughness ?
"R" :
"r");
3396auto is_material_two_sided(
const aiMaterial*
material) ->
bool
3405 bool two_sided =
false;
3406 if(
material->Get(AI_MATKEY_TWOSIDED, two_sided) == AI_SUCCESS && two_sided)
3413 aiString mat_name{};
3414 if(
material->Get(AI_MATKEY_NAME, mat_name) == AI_SUCCESS && mat_name.length > 0)
3417 static constexpr std::array<const char*, 6> markers = {
3425 for(
const char* marker : markers)
3427 if(
name.find(marker) != std::string::npos)
3437constexpr float k_import_opaque_opacity_threshold = 0.999f;
3439auto material_opacity_factor_suggests_cutout(
const aiMaterial*
material, ai_real& out_opacity) ->
bool
3443 &&
material->Get(AI_MATKEY_OPACITY, out_opacity) == AI_SUCCESS
3444 && out_opacity < k_import_opaque_opacity_threshold;
3447auto resolve_import_alpha_cutoff(
const aiMaterial*
material, ai_real fallback = 0.5f) -> ai_real
3449 ai_real cutoff = fallback;
3450 if(
material &&
material->Get(AI_MATKEY_GLTF_ALPHACUTOFF, cutoff) == AI_SUCCESS && cutoff > 0.0f)
3452 return math::clamp(cutoff, 0.0f, 1.0f);
3457constexpr float k_import_border_alpha_opaque_threshold = 0.95f;
3459auto compressed_texture_format_has_alpha(bimg::TextureFormat::Enum
format) ->
bool
3463 case bimg::TextureFormat::BC2:
3464 case bimg::TextureFormat::BC3:
3465 case bimg::TextureFormat::BC7:
3466 case bimg::TextureFormat::ETC2A:
3467 case bimg::TextureFormat::ETC2A1:
3468 case bimg::TextureFormat::PTC12A:
3469 case bimg::TextureFormat::PTC14A:
3470 case bimg::TextureFormat::ATCE:
3471 case bimg::TextureFormat::ATCI:
3477 if(
format >= bimg::TextureFormat::ASTC4x4 &&
format <= bimg::TextureFormat::ASTC12x12)
3485auto texture_format_has_alpha(bimg::TextureFormat::Enum
format,
bool parser_reported_alpha) ->
bool
3487 if(parser_reported_alpha)
3492 if(!bimg::isValid(
format))
3497 if(bimg::getBlockInfo(
format).aBits > 0)
3504 if(bimg::isCompressed(
format) && compressed_texture_format_has_alpha(
format))
3512auto image_mip_border_has_transparency(
const bimg::ImageMip& mip,
3513 bimg::TextureFormat::Enum
format,
3514 float opaque_threshold) ->
bool
3516 if(mip.m_width == 0 || mip.m_height == 0 || !mip.m_data)
3521 const bimg::UnpackFn unpack = bimg::getUnpack(
format);
3527 const uint32_t bpp = bimg::getBitsPerPixel(
format);
3528 if(bpp == 0 || (bpp % 8) != 0)
3533 const uint32_t bytes_per_pixel = bpp / 8;
3534 const uint32_t
width = mip.m_width;
3535 const uint32_t
height = mip.m_height;
3536 const uint32_t row_stride =
width * bytes_per_pixel;
3538 auto alpha_below_threshold = [&](uint32_t
x, uint32_t
y) ->
bool
3540 const uint8_t* pixel = mip.m_data + (
static_cast<size_t>(
y) * row_stride +
x * bytes_per_pixel);
3542 unpack(
rgba, pixel);
3543 return rgba[3] < opaque_threshold;
3546 for(uint32_t
x = 0;
x <
width; ++
x)
3548 if(alpha_below_threshold(
x, 0) || alpha_below_threshold(
x,
height - 1))
3554 for(uint32_t
y = 1;
y + 1 <
height; ++
y)
3556 if(alpha_below_threshold(0,
y) || alpha_below_threshold(
width - 1,
y))
3565auto image_border_has_transparency(
const bimg::ImageContainer& image,
float opaque_threshold) ->
bool
3567 if(image.m_width == 0 || image.m_height == 0 || !image.m_data)
3572 if(!texture_format_has_alpha(image.m_format, image.m_hasAlpha))
3577 bimg::ImageMip mip{};
3578 if(!bimg::imageGetRawData(image, 0, 0, image.m_data, image.m_size, mip))
3583 if(!bimg::isCompressed(image.m_format) && bimg::getUnpack(image.m_format) !=
nullptr)
3585 return image_mip_border_has_transparency(mip, image.m_format, opaque_threshold);
3588 if(bimg::isCompressed(image.m_format))
3590 const uint32_t
width = mip.m_width;
3591 const uint32_t
height = mip.m_height;
3597 std::vector<uint8_t> decoded(
static_cast<size_t>(
width) *
height * 4);
3598 bimg::imageDecodeToRgba8(get_bimg_allocator(),
3606 bimg::ImageMip decoded_mip{};
3607 decoded_mip.m_format = bimg::TextureFormat::RGBA8;
3608 decoded_mip.m_width =
width;
3609 decoded_mip.m_height =
height;
3610 decoded_mip.m_depth = 1;
3611 decoded_mip.m_bpp = 32;
3612 decoded_mip.m_hasAlpha =
true;
3613 decoded_mip.m_data = decoded.data();
3615 return image_mip_border_has_transparency(decoded_mip, bimg::TextureFormat::RGBA8, opaque_threshold);
3618 bimg::ImageContainer* converted =
3619 bimg::imageConvert(get_bimg_allocator(), bimg::TextureFormat::RGBA8, image,
false);
3625 const bool suggests_cutout = image_border_has_transparency(*converted, opaque_threshold);
3626 bimg::imageFree(converted);
3627 return suggests_cutout;
3630auto color_map_border_suggests_alpha_cutout(
const fs::path&
output_dir,
const std::string& relative) ->
bool
3632 if(relative.empty() || !texture_file_exists(
output_dir, relative))
3637 const fs::path filepath =
3639 const bx::FilePath bimg_path(filepath.string().c_str());
3641 bimg::ImageContainer header{};
3643 && !texture_format_has_alpha(header.m_format, header.m_hasAlpha))
3645 APPLOG_TRACE(
"Mesh Importer: Texture format does not have alpha: {}", relative);
3649 bimg::ImageContainer* loaded =
imageLoad(bimg_path);
3652 APPLOG_TRACE(
"Mesh Importer: Failed to load image: {}", relative);
3656 const bool suggests_cutout = image_border_has_transparency(*loaded, k_import_border_alpha_opaque_threshold);
3657 APPLOG_TRACE(
"Mesh Importer: Probing base color map border for transparency: {}", suggests_cutout);
3658 bimg::imageFree(loaded);
3659 return suggests_cutout;
3662void process_material(asset_manager& am,
3665 const aiScene*
scene,
3668 std::vector<imported_texture>& textures,
3669 material_import_env& env)
3676 const bool collecting = (env.phase == material_import_env::phase_t::collect);
3677 const bool binding = (env.phase == material_import_env::phase_t::bind);
3689 static constexpr std::array<slot_info, 21> slot_table = {{
3690 {aiTextureType_DIFFUSE,
"DIFFUSE"},
3691 {aiTextureType_SPECULAR,
"SPECULAR"},
3692 {aiTextureType_AMBIENT,
"AMBIENT"},
3693 {aiTextureType_EMISSIVE,
"EMISSIVE"},
3694 {aiTextureType_HEIGHT,
"HEIGHT"},
3695 {aiTextureType_NORMALS,
"NORMALS"},
3696 {aiTextureType_SHININESS,
"SHININESS"},
3697 {aiTextureType_OPACITY,
"OPACITY"},
3698 {aiTextureType_DISPLACEMENT,
"DISPLACEMENT"},
3699 {aiTextureType_LIGHTMAP,
"LIGHTMAP"},
3700 {aiTextureType_REFLECTION,
"REFLECTION"},
3701 {aiTextureType_BASE_COLOR,
"BASE_COLOR"},
3702 {aiTextureType_NORMAL_CAMERA,
"NORMAL_CAMERA"},
3703 {aiTextureType_EMISSION_COLOR,
"EMISSION_COLOR"},
3704 {aiTextureType_METALNESS,
"METALNESS"},
3705 {aiTextureType_DIFFUSE_ROUGHNESS,
"DIFFUSE_ROUGHNESS"},
3706 {aiTextureType_AMBIENT_OCCLUSION,
"AMBIENT_OCCLUSION"},
3707 {aiTextureType_SHEEN,
"SHEEN"},
3708 {aiTextureType_CLEARCOAT,
"CLEARCOAT"},
3709 {aiTextureType_TRANSMISSION,
"TRANSMISSION"},
3710 {aiTextureType_UNKNOWN,
"UNKNOWN"},
3713 std::string slot_log;
3714 for(
const auto& slot : slot_table)
3721 for(
unsigned int i = 0;
i <
count; ++
i)
3724 if(
material->GetTexture(slot.type, i, &path) == AI_SUCCESS && path.length > 0)
3726 if(!slot_log.empty())
3730 slot_log += fmt::format(
"{}[{}]={}", slot.name, i, normalize_assimp_path(path.C_Str()).generic_string());
3734 aiString mat_name{};
3735 material->Get(AI_MATKEY_NAME, mat_name);
3736 APPLOG_TRACE(
"Mesh Importer: Material '{}' texture slots: {}",
3737 mat_name.length > 0 ? mat_name.C_Str() :
"<unnamed>",
3738 slot_log.empty() ?
"<none>" : slot_log);
3742 auto workflow = detect_material_workflow(
material);
3744 APPLOG_TRACE(
"Mesh Importer: Material workflow detected: {}", material_workflow_label(workflow));
3748 auto get_imported_texture = [&](
const aiMaterial*
material,
3751 const std::string& semantic,
3752 imported_texture& tex) ->
bool
3755 aiTextureMapping mapping{};
3756 unsigned int uvindex{};
3759 aiTextureMapMode mapmode{};
3760 unsigned int flags{};
3763 aiReturn result = aiGetMaterialTexture(
material,
3777 auto tex_pair =
scene->GetEmbeddedTextureAndIndex(path.C_Str());
3779 const auto embedded_texture = tex_pair.first;
3780 if(embedded_texture)
3782 const auto index = tex_pair.second;
3785 tex.name = get_embedded_texture_name(embedded_texture,
index,
filename, semantic);
3786 tex.embedded_index =
index;
3790 const fs::path assimp_path = normalize_assimp_path(path.C_Str());
3791 tex.name = assimp_path.generic_string();
3793 const auto extension = assimp_path.extension().string();
3794 const auto texture_dir = assimp_path.parent_path();
3795 const auto texture_filename = assimp_path.filename().stem().string();
3797 if(fixed_name != texture_filename)
3799 fs::path fixed_relative = texture_dir / (fixed_name + extension);
3800 fs::path old_filepath =
output_dir / assimp_path;
3801 fs::path fixed_filepath =
output_dir / fixed_relative;
3804 if(fs::exists(old_filepath, ec))
3811 fixed_relative = resolve_external_texture_path(
output_dir, fixed_relative);
3812 fixed_filepath =
output_dir / fixed_relative;
3813 if(fs::exists(old_filepath, ec))
3818 tex.name = fixed_relative.generic_string();
3820 tex.name = resolve_external_texture_path(
output_dir, assimp_path).generic_string();
3822 if(!texture_file_exists(
output_dir, tex.name))
3824 APPLOG_WARNING(
"Mesh Importer: External texture '{}' not found on disk — skipping '{}'",
3825 assimp_path.generic_string(),
3830 tex.semantic = semantic;
3831 bool use_alpha = flags & aiTextureFlags_UseAlpha;
3832 bool ignore_alpha = flags & aiTextureFlags_IgnoreAlpha;
3833 bool invert = flags & aiTextureFlags_Invert;
3834 tex.inverse = invert;
3838 case aiTextureMapMode_Mirror:
3839 tex.flags = BGFX_SAMPLER_UVW_MIRROR;
3841 case aiTextureMapMode_Clamp:
3842 tex.flags = BGFX_SAMPLER_UVW_CLAMP;
3844 case aiTextureMapMode_Decal:
3845 tex.flags = BGFX_SAMPLER_UVW_BORDER;
3857 auto enqueue_simple_texture_job = [&](
const imported_texture& texture)
3865 if(texture.embedded_index >= 0)
3867 job.type = texture_job_type::embedded_extract;
3869 else if(needs_external_texture_conversion(texture))
3871 job.type = texture_job_type::external_convert;
3877 env.job_store->try_add(std::move(job));
3880 auto process_texture = [&](imported_texture& texture, std::vector<imported_texture>& textures_vec,
bool =
false)
3884 enqueue_simple_texture_job(texture);
3885 if(texture.embedded_index < 0 && !needs_external_texture_conversion(texture) && env.catalog
3886 && texture_file_exists(*env.output_dir, texture.name))
3888 env.catalog->register_entry(texture, texture);
3893 if(binding && env.catalog && env.catalog->resolve(texture))
3898 if(texture.embedded_index >= 0)
3900 auto it = std::find_if(std::begin(textures_vec),
3901 std::end(textures_vec),
3902 [&](
const imported_texture& rhs)
3904 return rhs.embedded_index == texture.embedded_index
3905 && rhs.semantic == texture.semantic;
3907 if(it != std::end(textures_vec))
3909 texture.name = it->name;
3910 texture.flags = it->flags;
3911 texture.inverse = it->inverse;
3912 texture.process_count = it->process_count;
3918 auto it = std::find_if(std::begin(textures_vec),
3919 std::end(textures_vec),
3920 [&](
const imported_texture& rhs)
3922 return rhs.embedded_index < 0 && rhs.name == texture.name
3923 && rhs.semantic == texture.semantic;
3925 if(it != std::end(textures_vec))
3927 if(needs_external_texture_conversion(texture))
3929 texture.name = make_converted_texture_name(texture.name, texture.semantic);
3935 textures_vec.emplace_back(texture);
3937 if(texture.embedded_index >= 0)
3939 const auto& embedded_texture =
scene->mTextures[texture.embedded_index];
3940 process_embedded_texture(embedded_texture, texture.embedded_index,
filename,
output_dir, textures_vec);
3942 else if(needs_external_texture_conversion(texture))
3944 fs::path original_file =
output_dir / texture.name;
3945 const auto converted_name = make_converted_texture_name(texture.name, texture.semantic);
3946 fs::path converted_file =
output_dir / converted_name;
3947 bimg::ImageContainer* image =
imageLoad(bx::FilePath(original_file.string().c_str()));
3950 apply_texture_conversion(image, texture.semantic, texture.inverse);
3951 atomic_image_save(converted_file, image);
3952 bimg::imageFree(image);
3953 texture.name = converted_name;
3954 APPLOG_TRACE(
"Mesh Importer: Applied {} conversion to external texture: {}", texture.semantic, texture.name);
3959 if(binding && is_material_two_sided(
material))
3965 std::string combined_mr_relative;
3966 std::string base_color_map_relative;
3968 bool khr_textures_baked =
false;
3969 bool spec_gloss_mr_baked =
false;
3973 imported_texture texture;
3974 if(get_workflow_aware_texture(
material, workflow,
"BaseColor", texture, get_imported_texture))
3976 if(workflow == material_workflow::khr_specular_glossiness)
3978 aiString specular_path{};
3979 bool has_specular = (
material->GetTexture(aiTextureType_SPECULAR, 0, &specular_path) == AI_SUCCESS)
3980 && specular_path.length > 0;
3982 const spec_gloss_factors_t factors = gather_spec_gloss_factors(
material);
3984 should_reconstruct_base_color_for_spec_gloss_pair(workflow,
material);
3986 imported_texture pair_albedo{};
3987 const bool has_pair_albedo =
3989 aiTextureType_DIFFUSE,
3993 const imported_texture& pair_source = has_pair_albedo ? pair_albedo : texture;
3995 imported_texture specular_tex{};
3997 && !get_imported_texture(
material, aiTextureType_SPECULAR, 0,
"Specular", specular_tex))
3999 APPLOG_WARNING(
"Mesh Importer: Material has SPECULAR slot but texture path could not be resolved");
4000 has_specular =
false;
4007 APPLOG_TRACE(
"Mesh Importer: {} - extension diffuse + specular pair -> PBR bake",
4008 material_workflow_label(workflow));
4012 APPLOG_TRACE(
"Mesh Importer: {} - diffuse pass-through, specular pair -> MR only",
4013 material_workflow_label(workflow));
4016 if(collecting && env.job_store)
4019 job.type = texture_job_type::spec_gloss_pair;
4020 job.desc = pair_source;
4021 job.specular_desc = specular_tex;
4022 job.spec_gloss_factors = factors;
4024 job.output_base_color_relative = build_converted_texture_name(
filename,
4026 pair_source.embedded_index,
4029 job.output_mr_relative = build_converted_texture_name(
filename,
4031 specular_tex.embedded_index,
4033 "MetallicRoughness");
4034 env.job_store->try_add(std::move(job));
4036 else if(binding && env.catalog)
4038 const std::string expected_base = build_converted_texture_name(
filename,
4040 pair_source.embedded_index,
4043 const std::string expected_mr = build_converted_texture_name(
filename,
4045 specular_tex.embedded_index,
4047 "MetallicRoughness");
4049 if(
bake_base_color && env.catalog->has_output_for_lookup(pair_source, expected_base))
4051 texture.name = expected_base;
4052 khr_textures_baked =
true;
4053 APPLOG_TRACE(
"Mesh Importer: Wrote converted base color (factors baked: D[{:.2f},{:.2f},{:.2f},{:.2f}] S[{:.2f},{:.2f},{:.2f}] G[{:.2f}]): {}",
4065 const std::string found_mr =
4066 find_spec_gloss_mr_relative(env.catalog,
output_dir, specular_tex, expected_mr);
4067 if(!found_mr.empty())
4069 combined_mr_relative = found_mr;
4070 spec_gloss_mr_baked =
true;
4071 if(found_mr == expected_mr)
4073 APPLOG_TRACE(
"Mesh Importer: Using pair metallic-roughness: {}", combined_mr_relative);
4077 APPLOG_TRACE(
"Mesh Importer: Using catalog metallic-roughness: {} (expected {})",
4078 combined_mr_relative,
4084 const std::string synced_mr = try_synchronous_spec_gloss_pair_mr(
output_dir,
4091 if(!synced_mr.empty())
4093 combined_mr_relative = synced_mr;
4094 spec_gloss_mr_baked =
true;
4095 imported_texture mr_result = specular_tex;
4096 mr_result.name = synced_mr;
4097 env.catalog->register_entry(specular_tex, mr_result);
4101 APPLOG_WARNING(
"Mesh Importer: Pair metallic-roughness not found (expected {}) and bind-time bake failed",
4106 if(!khr_textures_baked)
4108 process_texture(texture, textures);
4113 process_texture(texture, textures);
4118 process_texture(texture, textures);
4123 process_texture(texture, textures);
4126 base_color_map_relative = texture.name;
4130 if(
const auto key = try_make_texture_asset_key(
output_dir, texture.name))
4136 APPLOG_WARNING(
"Mesh Importer: Could not bind base color texture '{}'", texture.name);
4143 aiColor3D base_color_property{1.0f, 1.0f, 1.0f};
4144 float metallic_property = 0.0f;
4145 float roughness_property = 0.5f;
4147 if(khr_textures_baked)
4150 base_color_property = {1.0f, 1.0f, 1.0f};
4151 metallic_property = 1.0f;
4152 roughness_property = 1.0f;
4156 process_material_with_workflow_conversion(
material, workflow,
4157 base_color_property,
4159 roughness_property);
4160 if(spec_gloss_mr_baked)
4163 metallic_property = 1.0f;
4164 roughness_property = 1.0f;
4165 APPLOG_TRACE(
"Mesh Importer: MR texture drives shading — uniform multipliers: metallic={:.3f}, roughness={:.3f}",
4167 roughness_property);
4172 base_color = {base_color_property.r, base_color_property.g, base_color_property.b};
4173 base_color = math::clamp(base_color.value, 0.0f, 1.0f);
4174 mat.set_base_color(base_color);
4176 mat.set_metalness(math::clamp(metallic_property, 0.0f, 1.0f));
4177 mat.set_roughness(math::clamp(roughness_property, 0.0f, 1.0f));
4181 const bool khr_combined_specular_mr = khr_needs_combined_specular_mr(
material, workflow);
4182 bool has_metallic_tex =
false;
4183 bool has_roughness_tex =
false;
4185 if(!combined_mr_relative.empty())
4189 if(
const auto key = try_make_texture_asset_key(
output_dir, combined_mr_relative))
4193 mat.set_metalness_map(texture_asset);
4194 mat.set_roughness_map(texture_asset);
4195 has_metallic_tex =
true;
4196 has_roughness_tex =
true;
4198 APPLOG_TRACE(
"Mesh Importer: Using sibling metallic-roughness map from spec-gloss conversion: {}",
4199 combined_mr_relative);
4203 APPLOG_WARNING(
"Mesh Importer: Could not bind metallic-roughness texture '{}'", combined_mr_relative);
4207 else if(khr_combined_specular_mr && combined_mr_relative.empty())
4209 imported_texture combined_texture;
4210 if(get_imported_texture(
material, aiTextureType_SPECULAR, 0,
"SpecularToMetallicRoughness", combined_texture))
4212 process_texture(combined_texture, textures);
4216 if(
const auto key = try_make_texture_asset_key(
output_dir, combined_texture.name))
4220 mat.set_metalness_map(texture_asset);
4221 mat.set_roughness_map(texture_asset);
4222 has_metallic_tex =
true;
4223 has_roughness_tex =
true;
4225 APPLOG_TRACE(
"Mesh Importer: Converting single specular texture to combined metallic/roughness: {}",
4226 combined_texture.name);
4230 APPLOG_WARNING(
"Mesh Importer: Could not bind specular-to-MR texture '{}'",
4231 combined_texture.name);
4239 imported_texture texture;
4240 if(get_workflow_aware_texture(
material, workflow,
"Metallic", texture, get_imported_texture))
4242 process_texture(texture, textures);
4246 if(
const auto key = try_make_texture_asset_key(
output_dir, texture.name))
4248 mat.set_metalness_map(am.get_asset<
gfx::texture>(*key));
4249 has_metallic_tex =
true;
4253 APPLOG_WARNING(
"Mesh Importer: Could not bind metallic texture '{}'", texture.name);
4260 imported_texture texture;
4261 if(get_workflow_aware_texture(
material, workflow,
"Roughness", texture, get_imported_texture))
4263 process_texture(texture, textures);
4267 if(
const auto key = try_make_texture_asset_key(
output_dir, texture.name))
4269 mat.set_roughness_map(am.get_asset<
gfx::texture>(*key));
4270 has_roughness_tex =
true;
4272 if(texture.semantic ==
"ShininessToRoughness")
4274 APPLOG_TRACE(
"Mesh Importer: Converting shininess texture to roughness: {}", texture.name);
4279 APPLOG_WARNING(
"Mesh Importer: Could not bind roughness texture '{}'", texture.name);
4287 const bool packed_mr_specular = material_has_packed_mr_in_specular_slot(
material);
4288 if(has_metallic_tex && has_roughness_tex
4289 && (khr_textures_baked || spec_gloss_mr_baked || workflow == material_workflow::khr_specular_glossiness
4290 || packed_mr_specular))
4292 mat.set_metalness(1.0f);
4293 mat.set_roughness(1.0f);
4297 aiTextureType normals_type = aiTextureType_NORMALS;
4299 static const std::string semantic =
"Normals";
4301 imported_texture texture;
4302 bool has_texture =
false;
4306 has_texture |= get_imported_texture(
material, aiTextureType_NORMALS, 0, semantic, texture);
4311 has_texture |= get_imported_texture(
material, aiTextureType_NORMAL_CAMERA, 0, semantic, texture);
4315 normals_type = aiTextureType_NORMAL_CAMERA;
4321 process_texture(texture, textures);
4325 if(
const auto key = try_make_texture_asset_key(
output_dir, texture.name))
4331 APPLOG_WARNING(
"Mesh Importer: Could not bind normal texture '{}'", texture.name);
4339 bool has_property =
false;
4343 has_property |=
material->Get(AI_MATKEY_GLTF_TEXTURE_SCALE(normals_type, 0), property) == AI_SUCCESS;
4348 has_property |=
material->Get(AI_MATKEY_BUMPSCALING, property) == AI_SUCCESS;
4353 mat.set_bumpiness(property);
4358 aiTextureType occlusion_type = aiTextureType_AMBIENT_OCCLUSION;
4360 static const std::string semantic =
"Occlusion";
4362 imported_texture texture;
4363 bool has_texture =
false;
4367 has_texture |= get_imported_texture(
material, aiTextureType_AMBIENT_OCCLUSION, 0, semantic, texture);
4372 has_texture |= get_imported_texture(
material, aiTextureType_AMBIENT, 0, semantic, texture);
4376 occlusion_type = aiTextureType_AMBIENT;
4382 has_texture |= get_imported_texture(
material, aiTextureType_LIGHTMAP, 0, semantic, texture);
4385 occlusion_type = aiTextureType_LIGHTMAP;
4391 process_texture(texture, textures);
4395 if(
const auto key = try_make_texture_asset_key(
output_dir, texture.name))
4401 APPLOG_WARNING(
"Mesh Importer: Could not bind occlusion texture '{}'", texture.name);
4410 bool has_property =
false;
4414 has_property |=
material->Get(AI_MATKEY_GLTF_TEXTURE_STRENGTH(occlusion_type, 0), property) == AI_SUCCESS;
4424 static const std::string semantic =
"Emissive";
4426 imported_texture texture;
4427 bool has_texture =
false;
4431 has_texture |= get_imported_texture(
material, aiTextureType_EMISSION_COLOR, 0, semantic, texture);
4436 has_texture |= get_imported_texture(
material, aiTextureType_EMISSIVE, 0, semantic, texture);
4441 process_texture(texture, textures);
4445 if(
const auto key = try_make_texture_asset_key(
output_dir, texture.name))
4447 mat.set_emissive_map(am.get_asset<
gfx::texture>(*key));
4451 APPLOG_WARNING(
"Mesh Importer: Could not bind emissive texture '{}'", texture.name);
4464 aiColor3D
property{};
4465 bool has_property =
false;
4469 has_property |=
material->Get(AI_MATKEY_COLOR_EMISSIVE, property) == AI_SUCCESS;
4475 emissive = {
property.r,
property.g,
property.b};
4476 emissive = math::clamp(emissive.value, 0.0f, 1.0f);
4477 mat.set_emissive_color(emissive);
4482 ai_real intensity = 1.0f;
4483 if(
material->Get(AI_MATKEY_EMISSIVE_INTENSITY, intensity) == AI_SUCCESS)
4485 mat.set_emissive_intensity(math::clamp(intensity, 0.0f, 100.0f));
4488 ai_real texture_strength = 1.0f;
4489 if(
material->Get(AI_MATKEY_GLTF_TEXTURE_STRENGTH(aiTextureType_EMISSION_COLOR, 0), texture_strength) == AI_SUCCESS
4490 ||
material->Get(AI_MATKEY_GLTF_TEXTURE_STRENGTH(aiTextureType_EMISSIVE, 0), texture_strength) == AI_SUCCESS)
4492 mat.set_emissive_intensity(math::clamp(mat.get_emissive_intensity() * texture_strength, 0.0f, 100.0f));
4498 ai_real resolved_cutoff = 0.5f;
4500 aiString alpha_mode_str;
4501 const bool has_alpha_mode =
material->Get(AI_MATKEY_GLTF_ALPHAMODE, alpha_mode_str) == AI_SUCCESS;
4505 APPLOG_TRACE(
"Mesh Importer: glTF alphaMode: {}", alpha_mode_str.C_Str());
4507 if(alpha_mode_str == aiString(
"MASK"))
4510 material->Get(AI_MATKEY_GLTF_ALPHACUTOFF, resolved_cutoff);
4511 if(resolved_cutoff <= 0.0f)
4513 resolved_cutoff = 0.5f;
4516 else if(alpha_mode_str == aiString(
"BLEND"))
4523 ai_real opacity = 1.0f;
4524 if(material_opacity_factor_suggests_cutout(
material, opacity))
4527 resolved_cutoff = 1.0f - opacity;
4535 APPLOG_TRACE(
"Mesh Importer: Probing base color map border for transparency: {}", base_color_map_relative);
4536 if(color_map_border_suggests_alpha_cutout(
output_dir, base_color_map_relative))
4539 "Mesh Importer: Promoting to alpha cutout — base color map '{}' has transparent border pixels",
4540 base_color_map_relative);
4542 resolved_cutoff = resolve_import_alpha_cutoff(
material);
4546 mat.set_alpha_mode(resolved);
4549 mat.set_alpha_cutoff(math::clamp(resolved_cutoff, 0.0f, 1.0f));
4554void process_materials(asset_manager& am,
4557 const aiScene*
scene,
4558 std::vector<imported_material>& materials,
4559 std::vector<imported_texture>& textures)
4561 if(
scene->mNumMaterials == 0)
4566 materials.resize(
scene->mNumMaterials);
4570 std::unordered_set<int> consumed_embedded;
4571 std::vector<imported_texture> collect_scratch;
4573 material_import_env collect_env{};
4574 collect_env.phase = material_import_env::phase_t::collect;
4577 collect_env.scene =
scene;
4579 collect_env.catalog = &
catalog;
4581 APPLOG_TRACE(
"Mesh Importer: Collecting texture import jobs for {} materials ...",
scene->mNumMaterials);
4582 for(
size_t i = 0;
i <
scene->mNumMaterials; ++
i)
4585 process_material(am,
4589 scene->mMaterials[i],
4599 catalog.append_to_manifest(textures);
4600 sort_imported_textures(textures);
4602 material_import_env bind_env{};
4603 bind_env.phase = material_import_env::phase_t::bind;
4606 bind_env.scene =
scene;
4609 APPLOG_TRACE(
"Mesh Importer: Binding {} materials to textures ...",
scene->mNumMaterials);
4610 for(
size_t i = 0;
i <
scene->mNumMaterials; ++
i)
4612 const aiMaterial* assimp_mat =
scene->mMaterials[
i];
4614 auto mat = std::make_shared<pbr_material>();
4617 std::string assimp_mat_name = assimp_mat->GetName().C_Str();
4618 if(assimp_mat_name.empty())
4620 assimp_mat_name = fmt::format(
"Material {}",
filename.string());
4622 materials[
i].mat = mat;
4626 mark_embedded_consumed_from_textures(textures, consumed_embedded);
4628 texture_job_store orphan_job_store;
4629 collect_orphan_embedded_texture_jobs(
scene,
filename, orphan_job_store, consumed_embedded);
4630 if(!orphan_job_store.jobs().empty())
4632 APPLOG_TRACE(
"Mesh Importer: Running {} orphan embedded texture jobs ...", orphan_job_store.jobs().size());
4634 catalog.append_to_manifest(textures);
4637 sort_imported_textures(textures);
4640void process_embedded_textures(asset_manager& am,
4643 const aiScene*
scene,
4644 std::vector<imported_texture>& textures)
4646 if(
scene->mNumTextures > 0)
4648 for(
size_t i = 0;
i <
scene->mNumTextures; ++
i)
4650 const aiTexture* assimp_tex =
scene->mTextures[
i];
4657void process_imported_scene(asset_manager& am,
4660 const aiScene*
scene,
4661 mesh::load_data& load_data,
4662 std::vector<animation_clip>& animations,
4663 std::vector<imported_material>& materials,
4664 std::vector<imported_texture>& textures)
4666 int meshes_with_bones = 0;
4667 int meshes_without_bones = 0;
4673 auto name_to_index_lut = assign_node_indices(
scene);
4675 APPLOG_TRACE(
"Mesh Importer: Processing materials (collect jobs → run jobs → bind) ...");
4679 process_meshes(
scene, load_data);
4682 process_nodes(
scene, load_data, name_to_index_lut);
4684 APPLOG_TRACE(
"Mesh Importer: Processing animations ...");
4685 process_animations(
scene,
filename, load_data, name_to_index_lut, animations);
4690 if(!load_data.bbox.is_populated())
4692 load_data.bbox = {};
4693 accumulate_bounds_from_armature(load_data, load_data.bbox);
4696 apply_import_facing_correction_to_load_data(load_data);
4698 APPLOG_TRACE(
"Mesh Importer: bbox min {}, max {}", load_data.bbox.min, load_data.bbox.max);
4701auto read_file(Assimp::Importer& importer,
const fs::path& file, uint32_t flags) ->
const aiScene*
4704 return importer.ReadFile(file.string(), flags);
4711auto perceived_brightness(
float r,
float g,
float b) ->
float
4713 return std::sqrt(0.299f * r * r + 0.587f * g * g + 0.114f *
b *
b);
4729auto solve_metallic(
float perceived_diffuse,
float perceived_specular,
float one_minus_specular_strength) ->
float
4731 constexpr float dielectric_f0 = 0.04f;
4733 if(perceived_specular < dielectric_f0)
4738 float a = dielectric_f0;
4739 float b = perceived_diffuse * one_minus_specular_strength / (1.0f - dielectric_f0) + perceived_specular - 2.0f * dielectric_f0;
4740 float c = dielectric_f0 - perceived_specular;
4741 float discriminant = std::max(
b *
b - 4.0f *
a * c, 0.0f);
4743 return math::clamp((-
b + std::sqrt(discriminant)) / (2.0f *
a), 0.0f, 1.0f);
4751auto convert_specular_gloss_to_metallic_roughness(
const aiColor3D& diffuse_color,
4752 const aiColor3D& specular_color,
4753 float glossiness_factor) -> std::tuple<aiColor3D, float, float>
4755 constexpr float dielectric_f0 = 0.04f;
4756 constexpr float epsilon = 1e-6f;
4758 float max_specular = std::max({specular_color.r, specular_color.g, specular_color.b});
4759 float one_minus_specular_strength = 1.0f - max_specular;
4761 float perceived_diffuse = perceived_brightness(diffuse_color.r, diffuse_color.g, diffuse_color.b);
4762 float perceived_specular = perceived_brightness(specular_color.r, specular_color.g, specular_color.b);
4764 float metallic = solve_metallic(perceived_diffuse, perceived_specular, one_minus_specular_strength);
4770 float denom = std::max(1.0f - metallic * dielectric_f0, epsilon);
4771 float spec_offset = dielectric_f0 * (1.0f - metallic);
4773 auto base_from_diffuse = [&](
float d) ->
float {
return d * (1.0f - dielectric_f0) / denom; };
4774 auto base_from_specular = [&](
float s) ->
float {
return s - spec_offset; };
4776 float t = metallic * metallic;
4777 aiColor3D base_color;
4778 base_color.r = math::mix(base_from_diffuse(diffuse_color.r), base_from_specular(specular_color.r), t);
4779 base_color.g = math::mix(base_from_diffuse(diffuse_color.g), base_from_specular(specular_color.g), t);
4780 base_color.b = math::mix(base_from_diffuse(diffuse_color.b), base_from_specular(specular_color.b), t);
4782 float roughness = 1.0f - glossiness_factor;
4784 base_color.r = math::clamp(base_color.r, 0.0f, 1.0f);
4785 base_color.g = math::clamp(base_color.g, 0.0f, 1.0f);
4786 base_color.b = math::clamp(base_color.b, 0.0f, 1.0f);
4787 metallic = math::clamp(metallic, 0.0f, 1.0f);
4788 roughness = math::clamp(roughness, 0.0f, 1.0f);
4790 return std::make_tuple(base_color, metallic, roughness);
4799 struct log_stream :
public Assimp::LogStream
4801 log_stream(Assimp::Logger::ErrorSeverity s) : severity(s)
4805 void write(
const char* message)
override
4809 case Assimp::Logger::Info:
4812 case Assimp::Logger::Warn:
4815 case Assimp::Logger::Err:
4824 Assimp::Logger::ErrorSeverity severity{};
4839 const fs::path& path,
4842 std::vector<animation_clip>& animations,
4843 std::vector<imported_material>& materials,
4844 std::vector<imported_texture>& textures) ->
bool
4846 Assimp::Importer importer;
4848 int rvc_flags = aiComponent_CAMERAS | aiComponent_LIGHTS;
4850 if(!import_meta.model.import_meshes)
4852 rvc_flags |= aiComponent_MESHES;
4855 if(!import_meta.animations.import_animations)
4857 rvc_flags |= aiComponent_ANIMATIONS;
4860 if(!import_meta.materials.import_materials)
4862 rvc_flags |= aiComponent_MATERIALS;
4865 importer.SetPropertyInteger(AI_CONFIG_PP_RVC_FLAGS, rvc_flags);
4866 importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_LINE | aiPrimitiveType_POINT);
4867 importer.SetPropertyBool(AI_CONFIG_FBX_CONVERT_TO_M,
true);
4868 importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS,
false);
4870 fs::path file = path.stem();
4875 uint32_t flags = aiProcess_ConvertToLeftHanded |
4876 aiProcess_RemoveComponent |
4877 aiProcess_Triangulate |
4878 aiProcess_CalcTangentSpace |
4879 aiProcess_GenUVCoords |
4880 aiProcess_GenSmoothNormals |
4881 aiProcess_GenBoundingBoxes |
4882 aiProcess_ImproveCacheLocality |
4883 aiProcess_LimitBoneWeights |
4884 aiProcess_SortByPType |
4885 aiProcess_TransformUVCoords |
4886 aiProcess_GlobalScale;
4890 if(import_meta.model.weld_vertices)
4892 flags |= aiProcess_JoinIdenticalVertices;
4894 if(import_meta.model.optimize_meshes)
4896 flags |= aiProcess_OptimizeMeshes;
4898 if(import_meta.model.split_large_meshes)
4900 flags |= aiProcess_SplitLargeMeshes;
4902 if(import_meta.model.find_degenerates)
4904 flags |= aiProcess_FindDegenerates;
4906 if(import_meta.model.find_invalid_data)
4908 flags |= aiProcess_FindInvalidData;
4910 if(import_meta.materials.remove_redundant_materials)
4912 flags |= aiProcess_RemoveRedundantMaterials;
4915 APPLOG_TRACE(
"Mesh Importer: Loading {}", path.generic_string());
4917 const aiScene*
scene = read_file(importer, path, flags);
4919 if(
scene ==
nullptr)
4926 aiScene* modScene =
const_cast<aiScene*
>(
scene);
4929 process_imported_scene(am, file,
output_dir, modScene, load_data, animations, materials, textures);
4931 APPLOG_TRACE(
"Mesh Importer: Done with {}", path.generic_string());