3#include "morph_mesh_internal.h"
4#include "pixelbullet/assets/gltf_prefab_import.h"
5#include "pixelbullet/assets/material.h"
6#include "pixelbullet/filesystem/filesystem.h"
7#include "pixelbullet/image/bitmap.h"
8#include "pixelbullet/integration/assets/gltf_source_dependencies.h"
9#include "pixelbullet/logging/log.h"
10#include "pixelbullet/math/validation.h"
11#include "pixelbullet/scene/components/transform.h"
12#include "pixelbullet/scene/prefab_asset.h"
13#include "skinned_mesh_internal.h"
14#include "skinned_morph_mesh_internal.h"
15#include "static_mesh_internal.h"
18#include "ozz/animation/runtime/animation.h"
19#include "ozz/animation/runtime/skeleton.h"
20#include "ozz/base/memory/unique_ptr.h"
22#include <glm/gtc/matrix_transform.hpp>
23#include <glm/gtx/quaternion.hpp>
29#include <fmt/format.h>
33#include <unordered_map>
34#include <unordered_set>
38namespace pixelbullet::gltf_import_internal
40inline std::shared_ptr<log::Logger> GetAssetsLogger()
42 return logging::get(logging::names::core_assets);
45constexpr std::string_view k_assets_prefix =
"@assets/";
46constexpr std::string_view k_shared_prefix =
"@shared/";
50 std::string logical_root;
51 std::filesystem::path physical_root;
52 std::filesystem::path relative_source_path;
53 std::filesystem::path relative_source_stem;
60 std::vector<std::pair<VirtualPath, static_mesh_internal::LoadData>> meshes;
61 std::vector<std::pair<VirtualPath, morph_mesh_internal::LoadData>> morph_meshes;
62 std::vector<std::pair<VirtualPath, skinned_mesh_internal::LoadData>> skinned_meshes;
63 std::vector<std::pair<VirtualPath, skinned_morph_mesh_internal::LoadData>> skinned_morph_meshes;
64 std::vector<std::pair<VirtualPath, ozz::unique_ptr<ozz::animation::Skeleton>>> skeletons;
65 std::vector<std::pair<VirtualPath, ozz::unique_ptr<ozz::animation::Animation>>> animation_clips;
66 std::vector<std::pair<VirtualPath, Material>> materials;
67 std::vector<std::pair<VirtualPath, Bitmap>> textures;
73 std::vector<const cgltf_node*> joint_nodes_in_ozz_order;
74 std::vector<EntityId> joint_entities_in_ozz_order;
75 std::unordered_map<const cgltf_node*, std::size_t> joint_track_indices;
78enum class PrimitiveMeshKind
88 const cgltf_primitive* primitive =
nullptr;
89 const cgltf_skin* skin =
nullptr;
90 PrimitiveMeshKind kind = PrimitiveMeshKind::Static;
95 return lhs.primitive == rhs.primitive && lhs.skin == rhs.skin && lhs.kind == rhs.kind;
102 std::size_t hash = std::hash<const void*>{}(key.primitive);
103 hash ^= std::hash<const void*>{}(key.skin) + 0x9e3779b97f4a7c15ull + (hash << 6u) + (hash >> 2u);
104 hash ^= std::hash<std::size_t>{}(
static_cast<std::size_t
>(key.kind)) + 0x9e3779b97f4a7c15ull + (hash << 6u) + (hash >> 2u);
113 std::filesystem::path resolved_source_path;
115 cgltf_data* gltf =
nullptr;
117 std::unordered_map<const cgltf_image*, VirtualPath> image_paths;
118 std::unordered_map<const cgltf_material*, VirtualPath> material_paths;
119 std::unordered_map<PrimitiveMeshCacheKey, VirtualPath, PrimitiveMeshCacheKeyHash> primitive_mesh_path_cache;
120 std::unordered_map<const cgltf_node*, EntityId> imported_node_entities;
121 std::unordered_map<const cgltf_skin*, ImportedSkinAnimationData> imported_skins;
122 std::unordered_map<const cgltf_skin*, std::vector<EntityId>> imported_skin_owner_entities;
123 std::optional<VirtualPath> default_material_path;
124 std::unordered_map<std::string, std::size_t> mesh_name_counts;
125 std::unordered_map<std::string, std::size_t> material_name_counts;
126 std::unordered_map<std::string, std::size_t> texture_name_counts;
127 std::unordered_map<std::string, std::size_t> skeleton_name_counts;
128 std::unordered_map<std::string, std::size_t> animation_clip_name_counts;
129 std::vector<std::string> warning_messages;
130 std::unordered_set<std::string> seen_warning_messages;
131 std::string failure_message;
134inline bool StartsWith(
const std::string_view value,
const std::string_view prefix)
noexcept
136 return value.size() >= prefix.size() && value.substr(0, prefix.size()) == prefix;
139inline std::string ToLowerAscii(std::string value)
141 std::transform(value.begin(), value.end(), value.begin(),
142 [](
const unsigned char character) { return static_cast<char>(std::tolower(character)); });
146inline bool IsPathWithinRoot(
const std::filesystem::path& candidate,
const std::filesystem::path& root)
148 if (candidate.empty() || root.empty())
153 const std::filesystem::path normalized_candidate = candidate.lexically_normal();
154 const std::filesystem::path normalized_root = root.lexically_normal();
155 auto candidate_it = normalized_candidate.begin();
156 for (
auto root_it = normalized_root.begin(); root_it != normalized_root.end(); ++root_it, ++candidate_it)
158 if (candidate_it == normalized_candidate.end() ||
159 ToLowerAscii(candidate_it->generic_string()) != ToLowerAscii(root_it->generic_string()))
168inline std::optional<std::filesystem::path> ResolveGltfLocalDependencyPath(
const SourceRoot& source_root,
169 const std::filesystem::path& source_directory,
170 const std::string_view uri, std::string* error_message)
172 if (error_message !=
nullptr)
174 error_message->clear();
177 const integration::assets::GltfDependencyUri dependency_uri = integration::assets::ClassifyGltfDependencyUri(uri);
180 if (error_message !=
nullptr)
182 *error_message = dependency_uri.error_message;
187 if (!dependency_uri.is_local_file())
192 const std::filesystem::path dependency_path = (source_directory / dependency_uri.relative_path).lexically_normal();
193 if (!IsPathWithinRoot(dependency_path, source_root.physical_root))
195 if (error_message !=
nullptr)
197 *error_message = fmt::format(
"glTF dependency URI '{}' resolves outside the source asset root.", uri);
202 return dependency_path;
205inline bool ValidateGltfDependencyUris(
const cgltf_data& gltf,
const SourceRoot& source_root,
const std::filesystem::path& source_directory,
206 std::string* error_message)
208 const auto validate_uri = [&](
const char*
const uri,
const std::string_view label,
const std::size_t index) ->
bool
210 std::string dependency_error;
211 ResolveGltfLocalDependencyPath(source_root, source_directory, uri !=
nullptr ? std::string_view(uri) : std::string_view{},
213 if (!dependency_error.empty())
215 if (error_message !=
nullptr)
217 *error_message = fmt::format(
"Invalid glTF {} dependency URI at index {}: {}", label, index, dependency_error);
224 for (cgltf_size index = 0u; index < gltf.buffers_count; ++index)
226 if (!validate_uri(gltf.buffers[index].uri,
"buffer",
static_cast<std::size_t
>(index)))
232 for (cgltf_size index = 0u; index < gltf.images_count; ++index)
234 if (!validate_uri(gltf.images[index].uri,
"image",
static_cast<std::size_t
>(index)))
243inline std::string SanitizeStem(
const std::string_view value,
const std::string_view fallback)
245 std::string sanitized;
246 sanitized.reserve(value.size());
247 for (
const char ch : value)
249 if (std::isalnum(
static_cast<unsigned char>(ch)) || ch ==
'_' || ch ==
'-')
251 sanitized.push_back(
static_cast<char>(std::tolower(
static_cast<unsigned char>(ch))));
253 else if (ch ==
' ' || ch ==
'.' || ch ==
'/' || ch ==
'\\')
255 if (sanitized.empty() || sanitized.back() !=
'_')
257 sanitized.push_back(
'_');
262 while (!sanitized.empty() && sanitized.back() ==
'_')
264 sanitized.pop_back();
267 if (sanitized.empty())
269 sanitized = std::string(fallback);
275inline std::string MakeUniqueStem(std::unordered_map<std::string, std::size_t>& counts,
const std::string_view preferred,
276 const std::string_view fallback)
278 std::string stem = SanitizeStem(preferred.empty() ? fallback : preferred, fallback);
279 const auto [it, inserted] = counts.emplace(stem, 0u);
286 return stem +
"_" + std::to_string(it->second);
289inline void SetFailure(
ImportState& state, std::string message)
291 if (state.failure_message.empty())
293 state.failure_message = std::move(message);
297inline bool ValidateGltfFiniteComponents(
const float* values,
const std::size_t count,
const std::string_view label,
298 std::string* error_message)
300 for (std::size_t index = 0u; index < count; ++index)
302 if (!math::is_finite(values[index]))
306 *error_message = fmt::format(
"glTF {} contains non-finite values.", label);
316inline bool ValidateGltfFiniteValue(
const T& value,
const std::string_view label, std::string* error_message)
318 if (math::is_finite(value))
325 *error_message = fmt::format(
"glTF {} contains non-finite values.", label);
330template <
typename... Args>
331inline void RecordWarning(
ImportState& state, fmt::format_string<Args...> fmt_str, Args&&... args)
333 std::string message = fmt::format(fmt_str, std::forward<Args>(args)...);
334 if (state.seen_warning_messages.insert(message).second)
336 state.warning_messages.push_back(std::move(message));
337 GetAssetsLogger()->warn(
"{}", state.warning_messages.back());
341inline std::optional<SourceRoot> ResolveSourceRoot(
const Filesystem& filesystem,
const VirtualPath& source_path)
343 const std::string_view logical_path = source_path.logical_path();
344 if (StartsWith(logical_path, k_assets_prefix))
346 const std::filesystem::path relative = std::filesystem::path(std::string(logical_path.substr(k_assets_prefix.size())));
347 std::filesystem::path stem = relative;
348 stem.replace_extension();
350 .logical_root =
"@assets",
351 .physical_root = filesystem.resolve_writable(VirtualPath(
"@assets")),
352 .relative_source_path = std::move(relative),
353 .relative_source_stem = stem,
357 if (StartsWith(logical_path, k_shared_prefix))
359 const std::filesystem::path relative = std::filesystem::path(std::string(logical_path.substr(k_shared_prefix.size())));
360 std::filesystem::path stem = relative;
361 stem.replace_extension();
363 .logical_root =
"@shared",
364 .physical_root = filesystem.resolve_writable(VirtualPath(
"@shared")),
365 .relative_source_path = std::move(relative),
366 .relative_source_stem = stem,
373inline VirtualPath BuildGeneratedPath(
const ImportState& state,
const std::string_view type_root,
374 const std::filesystem::path& relative_path)
376 std::string logical = state.source_root.logical_root;
377 logical.push_back(
'/');
378 logical.append(type_root);
379 logical.push_back(
'/');
380 logical.append(relative_path.generic_string());
381 return VirtualPath(std::move(logical));
384inline std::filesystem::path BuildImportedNamespace(
const ImportState& state)
386 return std::filesystem::path(
"imported") / state.source_root.relative_source_stem;
389inline VirtualPath BuildGeneratedPrefabPath(
const ImportState& state)
391 std::filesystem::path relative = BuildImportedNamespace(state);
392 relative +=
".prefab.yaml";
393 return BuildGeneratedPath(state,
"prefabs", relative);
396inline VirtualPath BuildGeneratedMeshPath(
const ImportState& state,
const std::string_view stem)
398 return BuildGeneratedPath(state,
"models", BuildImportedNamespace(state) / (std::string(stem) +
".mesh.bin"));
401inline VirtualPath BuildGeneratedSkinnedMeshPath(
const ImportState& state,
const std::string_view stem)
403 return BuildGeneratedPath(state,
"models", BuildImportedNamespace(state) / (std::string(stem) +
".skinned_mesh.bin"));
406inline VirtualPath BuildGeneratedMorphMeshPath(
const ImportState& state,
const std::string_view stem)
408 return BuildGeneratedPath(state,
"models", BuildImportedNamespace(state) / (std::string(stem) +
".morph_mesh.bin"));
411inline VirtualPath BuildGeneratedSkeletonPath(
const ImportState& state,
const std::string_view stem)
413 return BuildGeneratedPath(state,
"animations", BuildImportedNamespace(state) / (std::string(stem) +
".skeleton.bin"));
416inline VirtualPath BuildGeneratedAnimationClipPath(
const ImportState& state,
const std::string_view stem)
418 return BuildGeneratedPath(state,
"animations", BuildImportedNamespace(state) / (std::string(stem) +
".animation_clip.bin"));
421inline VirtualPath BuildGeneratedSkinnedMorphMeshPath(
const ImportState& state,
const std::string_view stem)
423 return BuildGeneratedPath(state,
"models", BuildImportedNamespace(state) / (std::string(stem) +
".skinned_morph_mesh.bin"));
426inline VirtualPath BuildGeneratedMaterialPath(
const ImportState& state,
const std::string_view stem)
428 return BuildGeneratedPath(state,
"materials", BuildImportedNamespace(state) / (std::string(stem) +
".material.yaml"));
431inline VirtualPath BuildGeneratedTexturePath(
const ImportState& state,
const std::string_view stem)
433 return BuildGeneratedPath(state,
"textures", BuildImportedNamespace(state) / (std::string(stem) +
".png"));
436inline std::optional<glm::mat4> BuildNodeLocalMatrix(
const cgltf_node& node, std::string* error_message)
440 if (!ValidateGltfFiniteComponents(node.matrix, 16u,
"node matrix transform", error_message))
445 glm::mat4 matrix(1.0f);
446 std::memcpy(&matrix[0][0], node.matrix,
sizeof(node.matrix));
450 if (node.has_translation && !ValidateGltfFiniteComponents(node.translation, 3u,
"node translation transform", error_message))
454 if (node.has_rotation && !ValidateGltfFiniteComponents(node.rotation, 4u,
"node rotation transform", error_message))
458 if (node.has_scale && !ValidateGltfFiniteComponents(node.scale, 3u,
"node scale transform", error_message))
463 const glm::vec3 translation =
464 node.has_translation ? glm::vec3(node.translation[0], node.translation[1], node.translation[2]) : glm::vec3(0.0f);
465 const glm::quat rotation =
466 node.has_rotation ? glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]) : glm::quat(glm::vec3(0.0f));
467 const glm::vec3 scale = node.has_scale ? glm::vec3(node.scale[0], node.scale[1], node.scale[2]) : glm::vec3(1.0f);
468 return glm::translate(glm::mat4(1.0f), translation) * glm::toMat4(rotation) * glm::scale(glm::mat4(1.0f), scale);
471inline std::optional<Transform> BuildNodeTransform(
const cgltf_node& node, std::string* error_message)
473 const std::optional<glm::mat4> local_matrix = BuildNodeLocalMatrix(node, error_message);
474 if (!local_matrix.has_value())
480 if (!TryDecomposeTransform(*local_matrix, transform))
487inline bool ReadFloatAccessorElement(
const cgltf_accessor& accessor,
const cgltf_size index,
const cgltf_size expected_components,
490 std::fill(values, values + expected_components, 0.0f);
491 return cgltf_accessor_read_float(&accessor, index, values, expected_components) != 0;
494inline bool ReadUintAccessorElement(
const cgltf_accessor& accessor,
const cgltf_size index,
const cgltf_size expected_components,
497 std::fill(values, values + expected_components, 0u);
498 return cgltf_accessor_read_uint(&accessor, index, values, expected_components) != 0;
501inline std::size_t FindMaterialIndex(
const ImportState& state,
const cgltf_material& material)
noexcept
503 return static_cast<std::size_t
>(&material - state.gltf->materials);
506inline std::size_t FindImageIndex(
const ImportState& state,
const cgltf_image& image)
noexcept
508 return static_cast<std::size_t
>(&image - state.gltf->images);
511inline std::size_t FindMeshIndex(
const ImportState& state,
const cgltf_mesh& mesh)
noexcept
513 return static_cast<std::size_t
>(&mesh - state.gltf->meshes);
516inline std::size_t FindNodeIndex(
const ImportState& state,
const cgltf_node& node)
noexcept
518 return static_cast<std::size_t
>(&node - state.gltf->nodes);
521inline std::optional<uint32_t> ResolveTextureCoordinateIndex(
const cgltf_texture_view& view, std::string* error_message)
525 error_message->clear();
528 if (view.texture ==
nullptr)
533 const cgltf_int texcoord = view.has_transform && view.transform.has_texcoord ? view.transform.texcoord : view.texcoord;
545 *error_message =
"glTF textures that reference TEXCOORD_2+ are not supported.";
550inline MaterialTextureUvSet ResolveTextureUvSet(
const cgltf_texture_view& view, std::string* error_message)
552 const std::optional<uint32_t> texcoord_index = ResolveTextureCoordinateIndex(view, error_message);
553 if (!texcoord_index.has_value())
555 return MaterialTextureUvSet::Uv0;
558 return *texcoord_index == 1u ? MaterialTextureUvSet::Uv1 : MaterialTextureUvSet::Uv0;
561inline MaterialTextureTransform ResolveTextureUvTransform(
const cgltf_texture_view& view)
noexcept
563 MaterialTextureTransform transform;
564 if (!view.has_transform)
569 transform.offset = glm::vec2(view.transform.offset[0], view.transform.offset[1]);
570 transform.scale = glm::vec2(view.transform.scale[0], view.transform.scale[1]);
571 transform.rotation_degrees = glm::degrees(view.transform.rotation);
575[[nodiscard]] std::optional<VirtualPath> GetOrCreateMaterialPath(
ImportState& state,
const cgltf_material* material);
576[[nodiscard]] std::optional<VirtualPath> GetOrCreatePrimitiveMeshPath(
ImportState& state,
const cgltf_mesh& mesh,
577 const cgltf_primitive& primitive, std::size_t primitive_index);
578[[nodiscard]] std::optional<VirtualPath> GetOrCreatePrimitiveSkinnedMeshPath(
ImportState& state,
const cgltf_mesh& mesh,
579 const cgltf_primitive& primitive,
const cgltf_skin& skin,
580 std::size_t primitive_index);
581[[nodiscard]] std::optional<VirtualPath> GetOrCreatePrimitiveMorphMeshPath(
ImportState& state,
const cgltf_mesh& mesh,
582 const cgltf_primitive& primitive, std::size_t primitive_index);
583[[nodiscard]] std::optional<VirtualPath> GetOrCreatePrimitiveSkinnedMorphMeshPath(
ImportState& state,
const cgltf_mesh& mesh,
584 const cgltf_primitive& primitive,
const cgltf_skin& skin,
585 std::size_t primitive_index);
586[[nodiscard]] std::optional<std::vector<float>> ResolveNodeMorphWeights(
const cgltf_node& node,
const cgltf_mesh& mesh,
587 std::string* error_message);
589 std::string* error_message);
590bool ImportNodeAnimations(
ImportState& state, PrefabAsset& prefab, std::string* error_message);
591bool BuildImportedPrefab(
ImportState& state, std::string* error_message);
592bool CommitGeneratedFiles(
ImportState& state, GltfPrefabImportResult& result);
Definition filesystem.h:19
Definition virtual_path.h:10
Definition prefab_asset.h:9
Definition gltf_import_shared_internal.h:57
Definition gltf_import_shared_internal.h:110
Definition gltf_import_shared_internal.h:71
Definition gltf_import_shared_internal.h:99
Definition gltf_import_shared_internal.h:87
Definition gltf_import_shared_internal.h:49