PixelBullet  0.0.1
A C++ game engine
Loading...
Searching...
No Matches
gltf_import_shared_internal.h
1#pragma once
2
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"
16
17#include "cgltf.h"
18#include "ozz/animation/runtime/animation.h"
19#include "ozz/animation/runtime/skeleton.h"
20#include "ozz/base/memory/unique_ptr.h"
21
22#include <glm/gtc/matrix_transform.hpp>
23#include <glm/gtx/quaternion.hpp>
24
25#include <algorithm>
26#include <cctype>
27#include <cstring>
28#include <filesystem>
29#include <fmt/format.h>
30#include <memory>
31#include <optional>
32#include <string_view>
33#include <unordered_map>
34#include <unordered_set>
35#include <utility>
36#include <vector>
37
38namespace pixelbullet::gltf_import_internal
39{
40inline std::shared_ptr<log::Logger> GetAssetsLogger()
41{
42 return logging::get(logging::names::core_assets);
43}
44
45constexpr std::string_view k_assets_prefix = "@assets/";
46constexpr std::string_view k_shared_prefix = "@shared/";
47
49{
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;
54};
55
57{
58 PrefabAsset prefab;
59 VirtualPath prefab_path;
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;
68};
69
71{
72 VirtualPath skeleton_path;
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;
76};
77
78enum class PrimitiveMeshKind
79{
80 Static,
81 Skinned,
82 Morph,
83 SkinnedMorph,
84};
85
87{
88 const cgltf_primitive* primitive = nullptr;
89 const cgltf_skin* skin = nullptr;
90 PrimitiveMeshKind kind = PrimitiveMeshKind::Static;
91};
92
93inline bool operator==(const PrimitiveMeshCacheKey& lhs, const PrimitiveMeshCacheKey& rhs) noexcept
94{
95 return lhs.primitive == rhs.primitive && lhs.skin == rhs.skin && lhs.kind == rhs.kind;
96}
97
99{
100 std::size_t operator()(const PrimitiveMeshCacheKey& key) const noexcept
101 {
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);
105 return hash;
106 }
107};
108
110{
111 const Filesystem* filesystem = nullptr;
112 VirtualPath source_path;
113 std::filesystem::path resolved_source_path;
114 SourceRoot source_root;
115 cgltf_data* gltf = nullptr;
116 GeneratedFiles generated;
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;
132};
133
134inline bool StartsWith(const std::string_view value, const std::string_view prefix) noexcept
135{
136 return value.size() >= prefix.size() && value.substr(0, prefix.size()) == prefix;
137}
138
139inline std::string ToLowerAscii(std::string value)
140{
141 std::transform(value.begin(), value.end(), value.begin(),
142 [](const unsigned char character) { return static_cast<char>(std::tolower(character)); });
143 return value;
144}
145
146inline bool IsPathWithinRoot(const std::filesystem::path& candidate, const std::filesystem::path& root)
147{
148 if (candidate.empty() || root.empty())
149 {
150 return false;
151 }
152
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)
157 {
158 if (candidate_it == normalized_candidate.end() ||
159 ToLowerAscii(candidate_it->generic_string()) != ToLowerAscii(root_it->generic_string()))
160 {
161 return false;
162 }
163 }
164
165 return true;
166}
167
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)
171{
172 if (error_message != nullptr)
173 {
174 error_message->clear();
175 }
176
177 const integration::assets::GltfDependencyUri dependency_uri = integration::assets::ClassifyGltfDependencyUri(uri);
178 if (!dependency_uri)
179 {
180 if (error_message != nullptr)
181 {
182 *error_message = dependency_uri.error_message;
183 }
184 return std::nullopt;
185 }
186
187 if (!dependency_uri.is_local_file())
188 {
189 return std::nullopt;
190 }
191
192 const std::filesystem::path dependency_path = (source_directory / dependency_uri.relative_path).lexically_normal();
193 if (!IsPathWithinRoot(dependency_path, source_root.physical_root))
194 {
195 if (error_message != nullptr)
196 {
197 *error_message = fmt::format("glTF dependency URI '{}' resolves outside the source asset root.", uri);
198 }
199 return std::nullopt;
200 }
201
202 return dependency_path;
203}
204
205inline bool ValidateGltfDependencyUris(const cgltf_data& gltf, const SourceRoot& source_root, const std::filesystem::path& source_directory,
206 std::string* error_message)
207{
208 const auto validate_uri = [&](const char* const uri, const std::string_view label, const std::size_t index) -> bool
209 {
210 std::string dependency_error;
211 ResolveGltfLocalDependencyPath(source_root, source_directory, uri != nullptr ? std::string_view(uri) : std::string_view{},
212 &dependency_error);
213 if (!dependency_error.empty())
214 {
215 if (error_message != nullptr)
216 {
217 *error_message = fmt::format("Invalid glTF {} dependency URI at index {}: {}", label, index, dependency_error);
218 }
219 return false;
220 }
221 return true;
222 };
223
224 for (cgltf_size index = 0u; index < gltf.buffers_count; ++index)
225 {
226 if (!validate_uri(gltf.buffers[index].uri, "buffer", static_cast<std::size_t>(index)))
227 {
228 return false;
229 }
230 }
231
232 for (cgltf_size index = 0u; index < gltf.images_count; ++index)
233 {
234 if (!validate_uri(gltf.images[index].uri, "image", static_cast<std::size_t>(index)))
235 {
236 return false;
237 }
238 }
239
240 return true;
241}
242
243inline std::string SanitizeStem(const std::string_view value, const std::string_view fallback)
244{
245 std::string sanitized;
246 sanitized.reserve(value.size());
247 for (const char ch : value)
248 {
249 if (std::isalnum(static_cast<unsigned char>(ch)) || ch == '_' || ch == '-')
250 {
251 sanitized.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
252 }
253 else if (ch == ' ' || ch == '.' || ch == '/' || ch == '\\')
254 {
255 if (sanitized.empty() || sanitized.back() != '_')
256 {
257 sanitized.push_back('_');
258 }
259 }
260 }
261
262 while (!sanitized.empty() && sanitized.back() == '_')
263 {
264 sanitized.pop_back();
265 }
266
267 if (sanitized.empty())
268 {
269 sanitized = std::string(fallback);
270 }
271
272 return sanitized;
273}
274
275inline std::string MakeUniqueStem(std::unordered_map<std::string, std::size_t>& counts, const std::string_view preferred,
276 const std::string_view fallback)
277{
278 std::string stem = SanitizeStem(preferred.empty() ? fallback : preferred, fallback);
279 const auto [it, inserted] = counts.emplace(stem, 0u);
280 if (inserted)
281 {
282 return stem;
283 }
284
285 ++it->second;
286 return stem + "_" + std::to_string(it->second);
287}
288
289inline void SetFailure(ImportState& state, std::string message)
290{
291 if (state.failure_message.empty())
292 {
293 state.failure_message = std::move(message);
294 }
295}
296
297inline bool ValidateGltfFiniteComponents(const float* values, const std::size_t count, const std::string_view label,
298 std::string* error_message)
299{
300 for (std::size_t index = 0u; index < count; ++index)
301 {
302 if (!math::is_finite(values[index]))
303 {
304 if (error_message)
305 {
306 *error_message = fmt::format("glTF {} contains non-finite values.", label);
307 }
308 return false;
309 }
310 }
311
312 return true;
313}
314
315template <typename T>
316inline bool ValidateGltfFiniteValue(const T& value, const std::string_view label, std::string* error_message)
317{
318 if (math::is_finite(value))
319 {
320 return true;
321 }
322
323 if (error_message)
324 {
325 *error_message = fmt::format("glTF {} contains non-finite values.", label);
326 }
327 return false;
328}
329
330template <typename... Args>
331inline void RecordWarning(ImportState& state, fmt::format_string<Args...> fmt_str, Args&&... args)
332{
333 std::string message = fmt::format(fmt_str, std::forward<Args>(args)...);
334 if (state.seen_warning_messages.insert(message).second)
335 {
336 state.warning_messages.push_back(std::move(message));
337 GetAssetsLogger()->warn("{}", state.warning_messages.back());
338 }
339}
340
341inline std::optional<SourceRoot> ResolveSourceRoot(const Filesystem& filesystem, const VirtualPath& source_path)
342{
343 const std::string_view logical_path = source_path.logical_path();
344 if (StartsWith(logical_path, k_assets_prefix))
345 {
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();
349 return SourceRoot{
350 .logical_root = "@assets",
351 .physical_root = filesystem.resolve_writable(VirtualPath("@assets")),
352 .relative_source_path = std::move(relative),
353 .relative_source_stem = stem,
354 };
355 }
356
357 if (StartsWith(logical_path, k_shared_prefix))
358 {
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();
362 return SourceRoot{
363 .logical_root = "@shared",
364 .physical_root = filesystem.resolve_writable(VirtualPath("@shared")),
365 .relative_source_path = std::move(relative),
366 .relative_source_stem = stem,
367 };
368 }
369
370 return std::nullopt;
371}
372
373inline VirtualPath BuildGeneratedPath(const ImportState& state, const std::string_view type_root,
374 const std::filesystem::path& relative_path)
375{
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));
382}
383
384inline std::filesystem::path BuildImportedNamespace(const ImportState& state)
385{
386 return std::filesystem::path("imported") / state.source_root.relative_source_stem;
387}
388
389inline VirtualPath BuildGeneratedPrefabPath(const ImportState& state)
390{
391 std::filesystem::path relative = BuildImportedNamespace(state);
392 relative += ".prefab.yaml";
393 return BuildGeneratedPath(state, "prefabs", relative);
394}
395
396inline VirtualPath BuildGeneratedMeshPath(const ImportState& state, const std::string_view stem)
397{
398 return BuildGeneratedPath(state, "models", BuildImportedNamespace(state) / (std::string(stem) + ".mesh.bin"));
399}
400
401inline VirtualPath BuildGeneratedSkinnedMeshPath(const ImportState& state, const std::string_view stem)
402{
403 return BuildGeneratedPath(state, "models", BuildImportedNamespace(state) / (std::string(stem) + ".skinned_mesh.bin"));
404}
405
406inline VirtualPath BuildGeneratedMorphMeshPath(const ImportState& state, const std::string_view stem)
407{
408 return BuildGeneratedPath(state, "models", BuildImportedNamespace(state) / (std::string(stem) + ".morph_mesh.bin"));
409}
410
411inline VirtualPath BuildGeneratedSkeletonPath(const ImportState& state, const std::string_view stem)
412{
413 return BuildGeneratedPath(state, "animations", BuildImportedNamespace(state) / (std::string(stem) + ".skeleton.bin"));
414}
415
416inline VirtualPath BuildGeneratedAnimationClipPath(const ImportState& state, const std::string_view stem)
417{
418 return BuildGeneratedPath(state, "animations", BuildImportedNamespace(state) / (std::string(stem) + ".animation_clip.bin"));
419}
420
421inline VirtualPath BuildGeneratedSkinnedMorphMeshPath(const ImportState& state, const std::string_view stem)
422{
423 return BuildGeneratedPath(state, "models", BuildImportedNamespace(state) / (std::string(stem) + ".skinned_morph_mesh.bin"));
424}
425
426inline VirtualPath BuildGeneratedMaterialPath(const ImportState& state, const std::string_view stem)
427{
428 return BuildGeneratedPath(state, "materials", BuildImportedNamespace(state) / (std::string(stem) + ".material.yaml"));
429}
430
431inline VirtualPath BuildGeneratedTexturePath(const ImportState& state, const std::string_view stem)
432{
433 return BuildGeneratedPath(state, "textures", BuildImportedNamespace(state) / (std::string(stem) + ".png"));
434}
435
436inline std::optional<glm::mat4> BuildNodeLocalMatrix(const cgltf_node& node, std::string* error_message)
437{
438 if (node.has_matrix)
439 {
440 if (!ValidateGltfFiniteComponents(node.matrix, 16u, "node matrix transform", error_message))
441 {
442 return std::nullopt;
443 }
444
445 glm::mat4 matrix(1.0f);
446 std::memcpy(&matrix[0][0], node.matrix, sizeof(node.matrix));
447 return matrix;
448 }
449
450 if (node.has_translation && !ValidateGltfFiniteComponents(node.translation, 3u, "node translation transform", error_message))
451 {
452 return std::nullopt;
453 }
454 if (node.has_rotation && !ValidateGltfFiniteComponents(node.rotation, 4u, "node rotation transform", error_message))
455 {
456 return std::nullopt;
457 }
458 if (node.has_scale && !ValidateGltfFiniteComponents(node.scale, 3u, "node scale transform", error_message))
459 {
460 return std::nullopt;
461 }
462
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);
469}
470
471inline std::optional<Transform> BuildNodeTransform(const cgltf_node& node, std::string* error_message)
472{
473 const std::optional<glm::mat4> local_matrix = BuildNodeLocalMatrix(node, error_message);
474 if (!local_matrix.has_value())
475 {
476 return std::nullopt;
477 }
478
479 Transform transform;
480 if (!TryDecomposeTransform(*local_matrix, transform))
481 {
482 return Transform{};
483 }
484 return transform;
485}
486
487inline bool ReadFloatAccessorElement(const cgltf_accessor& accessor, const cgltf_size index, const cgltf_size expected_components,
488 float* values)
489{
490 std::fill(values, values + expected_components, 0.0f);
491 return cgltf_accessor_read_float(&accessor, index, values, expected_components) != 0;
492}
493
494inline bool ReadUintAccessorElement(const cgltf_accessor& accessor, const cgltf_size index, const cgltf_size expected_components,
495 uint32_t* values)
496{
497 std::fill(values, values + expected_components, 0u);
498 return cgltf_accessor_read_uint(&accessor, index, values, expected_components) != 0;
499}
500
501inline std::size_t FindMaterialIndex(const ImportState& state, const cgltf_material& material) noexcept
502{
503 return static_cast<std::size_t>(&material - state.gltf->materials);
504}
505
506inline std::size_t FindImageIndex(const ImportState& state, const cgltf_image& image) noexcept
507{
508 return static_cast<std::size_t>(&image - state.gltf->images);
509}
510
511inline std::size_t FindMeshIndex(const ImportState& state, const cgltf_mesh& mesh) noexcept
512{
513 return static_cast<std::size_t>(&mesh - state.gltf->meshes);
514}
515
516inline std::size_t FindNodeIndex(const ImportState& state, const cgltf_node& node) noexcept
517{
518 return static_cast<std::size_t>(&node - state.gltf->nodes);
519}
520
521inline std::optional<uint32_t> ResolveTextureCoordinateIndex(const cgltf_texture_view& view, std::string* error_message)
522{
523 if (error_message)
524 {
525 error_message->clear();
526 }
527
528 if (view.texture == nullptr)
529 {
530 return 0u;
531 }
532
533 const cgltf_int texcoord = view.has_transform && view.transform.has_texcoord ? view.transform.texcoord : view.texcoord;
534 if (texcoord == 0)
535 {
536 return 0u;
537 }
538 if (texcoord == 1)
539 {
540 return 1u;
541 }
542
543 if (error_message)
544 {
545 *error_message = "glTF textures that reference TEXCOORD_2+ are not supported.";
546 }
547 return std::nullopt;
548}
549
550inline MaterialTextureUvSet ResolveTextureUvSet(const cgltf_texture_view& view, std::string* error_message)
551{
552 const std::optional<uint32_t> texcoord_index = ResolveTextureCoordinateIndex(view, error_message);
553 if (!texcoord_index.has_value())
554 {
555 return MaterialTextureUvSet::Uv0;
556 }
557
558 return *texcoord_index == 1u ? MaterialTextureUvSet::Uv1 : MaterialTextureUvSet::Uv0;
559}
560
561inline MaterialTextureTransform ResolveTextureUvTransform(const cgltf_texture_view& view) noexcept
562{
563 MaterialTextureTransform transform;
564 if (!view.has_transform)
565 {
566 return transform;
567 }
568
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);
572 return transform;
573}
574
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);
588[[nodiscard]] const ImportedSkinAnimationData* GetOrCreateImportedSkinAnimationData(ImportState& state, const cgltf_skin& skin,
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);
593} // namespace pixelbullet::gltf_import_internal
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