Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
ModernGltfMesh.cpp
Go to the documentation of this file.
1#include "ModernGltfMesh.hpp"
3
4#include <fastgltf/core.hpp>
5#include <fastgltf/math.hpp>
6#include <fastgltf/tools.hpp>
7#include <fastgltf/types.hpp>
8
9#include <algorithm>
10#include <cmath>
11#include <limits>
12#include <new>
13#include <stdexcept>
14#include <variant>
15#include <unordered_map>
16#include <utility>
17
18namespace monopoly::data
19{
20 namespace
21 {
22 [[nodiscard]] MeshRuntimeError error(
24 std::string detail)
25 {
26 MeshRuntimeError result;
27 result.code = code;
28 result.detail = std::move(detail);
29 return result;
30 }
31
32
33 [[nodiscard]] MeshMaterial materialFactors(
34 const fastgltf::Asset& asset,
35 const fastgltf::Primitive& primitive)
36 {
37 MeshMaterial result;
39 if (!primitive.materialIndex ||
40 *primitive.materialIndex >= asset.materials.size())
41 return result;
42
43 const auto& material =
44 asset.materials[*primitive.materialIndex];
45 const auto& factor = material.pbrData.baseColorFactor;
46
47 for (std::size_t index = 0; index < 4; ++index)
48 result.diffuse[index] =
49 static_cast<float>(factor[index]);
50 result.metallic = static_cast<float>(
51 material.pbrData.metallicFactor);
52 result.roughness = static_cast<float>(
53 material.pbrData.roughnessFactor);
54 for (std::size_t index = 0; index < 3; ++index)
55 result.emissive[index] =
56 static_cast<float>(material.emissiveFactor[index]);
57 result.emissiveStrength =
58 static_cast<float>(material.emissiveStrength);
59 result.doubleSided = material.doubleSided;
60
61 const auto channel = [](float value)
62 {
63 return static_cast<std::uint32_t>(
64 std::lround(
65 std::clamp(value, 0.0F, 1.0F) * 255.0F));
66 };
67 result.rawDiffuse =
68 channel(result.diffuse[0]) |
69 (channel(result.diffuse[1]) << 8U) |
70 (channel(result.diffuse[2]) << 16U);
71 return result;
72 }
73
74
75 struct YawRotation
76 {
77 float sine;
78 float cosine;
79 };
80
81 [[nodiscard]] YawRotation yawRotation(float yawDegrees) noexcept
82 {
83 constexpr float DegreesToRadians =
84 3.14159265358979323846F / 180.0F;
85 const float angle = yawDegrees * DegreesToRadians;
86 return {std::sin(angle), std::cos(angle)};
87 }
88
89 [[nodiscard]] fastgltf::math::fvec3 rotateAroundY(
90 fastgltf::math::fvec3 value,
91 const YawRotation& rotation) noexcept
92 {
93 return fastgltf::math::fvec3(
94 rotation.cosine * value[0] + rotation.sine * value[2],
95 value[1],
96 -rotation.sine * value[0] + rotation.cosine * value[2]);
97 }
98
99
100 [[nodiscard]] fastgltf::math::fvec3 transformPosition(
101 const fastgltf::math::fmat4x4& world,
102 fastgltf::math::fvec3 position,
103 float unitsPerMeter,
104 const YawRotation& rotation) noexcept
105 {
106 const auto transformed = world * fastgltf::math::fvec4(
107 position[0], position[1], position[2], 1.0F);
108 auto rotated = rotateAroundY(
109 fastgltf::math::fvec3(
110 transformed[0],
111 transformed[1],
112 transformed[2]),
113 rotation);
114 return rotated * unitsPerMeter;
115 }
116
117
118 [[nodiscard]] fastgltf::math::fvec3 transformNormal(
119 const fastgltf::math::fmat3x3& normalMatrix,
120 fastgltf::math::fvec3 normal,
121 const YawRotation& rotation) noexcept
122 {
123 return fastgltf::math::normalize(
125 }
126
127
128 void extendBounds(
129 MeshBounds& bounds,
130 const MeshVertex& vertex,
131 bool& initialized) noexcept
132 {
133 if (!initialized)
134 {
135 bounds.minimum = vertex.position;
136 bounds.maximum = vertex.position;
137 initialized = true;
138 return;
139 }
140 for (std::size_t axis = 0; axis < 3; ++axis)
141 {
142 bounds.minimum[axis] =
143 std::min(bounds.minimum[axis], vertex.position[axis]);
144 bounds.maximum[axis] =
145 std::max(bounds.maximum[axis], vertex.position[axis]);
146 }
147 }
148
149
150 [[nodiscard]] bool validBufferView(const fastgltf::Asset& asset,
151 std::size_t index)
152 {
153 if (index >= asset.bufferViews.size()) return false;
154 const auto& view = asset.bufferViews[index];
155 if (view.bufferIndex >= asset.buffers.size()) return false;
156 const auto bytes = std::visit(fastgltf::visitor{
157 [](const auto&) -> std::size_t { return 0; },
158 [](const fastgltf::sources::Array& value) { return value.bytes.size_bytes(); },
159 [](const fastgltf::sources::Vector& value) { return value.bytes.size(); },
160 [](const fastgltf::sources::ByteView& value) { return value.bytes.size(); }
161 }, asset.buffers[view.bufferIndex].data);
162 return view.byteOffset <= bytes && view.byteLength <= bytes - view.byteOffset;
163 }
164
165 [[nodiscard]] bool validAccessor(const fastgltf::Asset& asset,
166 std::size_t index, fastgltf::AccessorType type)
167 {
168 if (index >= asset.accessors.size()) return false;
169 const auto& accessor = asset.accessors[index];
170 if (accessor.type != type || accessor.count == 0 ||
171 accessor.componentType == fastgltf::ComponentType::Invalid)
172 return false;
173 const auto elementBytes = fastgltf::getElementByteSize(
174 accessor.type, accessor.componentType);
175 const auto fits = [&](std::size_t viewIndex, std::size_t offset,
176 std::size_t count, std::size_t size,
177 bool strided)
178 {
179 if (!validBufferView(asset, viewIndex)) return false;
180 const auto& view = asset.bufferViews[viewIndex];
181 const auto stride = strided ? view.byteStride.value_or(size) : size;
182 if (size == 0 || stride < size || offset > view.byteLength)
183 return false;
184 const auto available = view.byteLength - offset;
185 return count == 0 || (size <= available &&
186 count - 1 <= (available - size) / stride);
187 };
188 if (accessor.bufferViewIndex &&
189 !fits(*accessor.bufferViewIndex, accessor.byteOffset,
190 accessor.count, elementBytes, true)) return false;
191 if (!accessor.bufferViewIndex && !accessor.sparse) return false;
192 if (accessor.sparse)
193 {
194 const auto& sparse = *accessor.sparse;
195 if (sparse.count > accessor.count ||
196 (sparse.indexComponentType != fastgltf::ComponentType::UnsignedByte &&
197 sparse.indexComponentType != fastgltf::ComponentType::UnsignedShort &&
198 sparse.indexComponentType != fastgltf::ComponentType::UnsignedInt) ||
199 !fits(sparse.indicesBufferView, sparse.indicesByteOffset,
200 sparse.count, fastgltf::getComponentByteSize(sparse.indexComponentType), false) ||
201 !fits(sparse.valuesBufferView, sparse.valuesByteOffset,
202 sparse.count, elementBytes, false)) return false;
203 }
204 return true;
205 }
206
207 // Validate the hierarchy before fastgltf's recursive traversal. A deep
208 // or cyclic document must not exhaust the stack before geometry checks.
209 [[nodiscard]] bool validHierarchy(const fastgltf::Asset& asset)
210 {
211 struct Entry { std::size_t node; std::size_t nextChild; };
212 std::vector<std::uint8_t> colors(asset.nodes.size());
213 std::vector<std::size_t> depths(asset.nodes.size(), 1);
214 std::vector<Entry> stack;
215 for (std::size_t root = 0; root < asset.nodes.size(); ++root)
216 {
217 if (colors[root] != 0) continue;
218 colors[root] = 1;
219 stack.push_back({root, 0});
220 while (!stack.empty())
221 {
222 auto& entry = stack.back();
223 const auto& children = asset.nodes[entry.node].children;
224 if (entry.nextChild == children.size())
225 {
226 for (const auto child : children)
227 depths[entry.node] = std::max(depths[entry.node], depths[child] + 1);
228 if (depths[entry.node] > 1024) return false;
229 colors[entry.node] = 2;
230 stack.pop_back();
231 continue;
232 }
233 const auto child = children[entry.nextChild++];
234 if (child >= asset.nodes.size() || colors[child] == 1)
235 return false;
236 if (colors[child] == 2) continue;
237 if (stack.size() >= 1024) return false;
238 colors[child] = 1;
239 stack.push_back({child, 0});
240 }
241 }
242 for (const auto& scene : asset.scenes)
244 if (node >= asset.nodes.size()) return false;
245 return true;
246 }
247
248 using ImageCache = std::unordered_map<std::size_t,
249 std::shared_ptr<const ModernTextureImage>>;
250 constexpr std::size_t MaximumMeshImageBytes = 256U * 1024U * 1024U;
251
252 [[nodiscard]] std::expected<ModernTextureBinding, MeshRuntimeError> textureBindingFor(
253 const fastgltf::Asset& asset, const fastgltf::TextureInfo& info,
254 ModernTextureColorSpace colorSpace, float scale,
255 ImageCache& images, std::size_t& decodedImageBytes)
256 {
257 if (info.texCoordIndex != 0 || info.transform)
259 "modern GLB maps require TEXCOORD_0 without texture transforms"));
260 if (!std::isfinite(scale) || info.textureIndex >= asset.textures.size())
261 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
262 "modern GLB texture reference or map scale is invalid"));
263 const auto& texture = asset.textures[info.textureIndex];
264 if (!texture.imageIndex || *texture.imageIndex >= asset.images.size())
265 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
266 "modern GLB texture requires a valid image reference"));
267 ModernTextureBinding result;
268 result.colorSpace = colorSpace;
269 result.scale = scale;
270 if (texture.samplerIndex)
271 {
272 if (*texture.samplerIndex >= asset.samplers.size())
273 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
274 "modern GLB sampler reference is out of range"));
275 const auto& sampler = asset.samplers[*texture.samplerIndex];
276 const auto wrap = [](fastgltf::Wrap source) -> std::optional<ModernTextureWrap>
277 {
278 switch (source)
279 {
280 case fastgltf::Wrap::Repeat: return ModernTextureWrap::Repeat;
281 case fastgltf::Wrap::ClampToEdge: return ModernTextureWrap::ClampToEdge;
282 case fastgltf::Wrap::MirroredRepeat: return ModernTextureWrap::MirroredRepeat;
283 default: return std::nullopt;
284 }
285 };
286 const auto filter = [](fastgltf::Filter source) -> std::optional<ModernTextureFilter>
287 {
288 switch (source)
289 {
290 case fastgltf::Filter::Nearest: return ModernTextureFilter::Nearest;
291 case fastgltf::Filter::Linear: return ModernTextureFilter::Linear;
292 case fastgltf::Filter::NearestMipMapNearest: return ModernTextureFilter::NearestMipmapNearest;
293 case fastgltf::Filter::LinearMipMapNearest: return ModernTextureFilter::LinearMipmapNearest;
294 case fastgltf::Filter::NearestMipMapLinear: return ModernTextureFilter::NearestMipmapLinear;
295 case fastgltf::Filter::LinearMipMapLinear: return ModernTextureFilter::LinearMipmapLinear;
296 default: return std::nullopt;
297 }
298 };
299 const auto wrapS = wrap(sampler.wrapS), wrapT = wrap(sampler.wrapT);
300 const auto min = filter(sampler.minFilter.value_or(fastgltf::Filter::Linear));
301 const auto mag = filter(sampler.magFilter.value_or(fastgltf::Filter::Linear));
302 if (!wrapS || !wrapT || !min || !mag ||
304 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
305 "modern GLB sampler wrap or filter is invalid"));
306 result.sampler = {*wrapS, *wrapT, *min, *mag};
307 }
308 const auto imageIndex = *texture.imageIndex;
309 if (const auto cached = images.find(imageIndex); cached != images.end())
310 {
311 result.image = cached->second;
312 return result;
313 }
314 const auto* source = std::get_if<fastgltf::sources::BufferView>(
315 &asset.images[imageIndex].data);
316 if (!source)
318 "modern GLB images must be embedded buffer views; external and data URIs are unsupported"));
319 if (!validBufferView(asset, source->bufferViewIndex))
320 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
321 "modern GLB image buffer view is outside its embedded payload"));
323 if (source->mimeType == fastgltf::MimeType::PNG) encoding = ModernImageEncoding::Png;
324 else if (source->mimeType == fastgltf::MimeType::JPEG) encoding = ModernImageEncoding::Jpeg;
325 else return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetUnsupported,
326 "modern GLB embedded image MIME must be PNG or JPEG"));
327 const auto bytes = fastgltf::DefaultBufferDataAdapter{}(asset, source->bufferViewIndex);
328 ModernImageDecodeLimits limits;
329 limits.maximumDecodedBytes = std::min(limits.maximumDecodedBytes,
331 auto decoded = decodeModernImage(std::span<const std::byte>(bytes.data(), bytes.size()),
332 encoding, limits);
333 if (!decoded) return std::unexpected(decoded.error());
334 result.image = *decoded;
335 decodedImageBytes += result.image->rgba.size();
336 images.emplace(imageIndex, result.image);
337 return result;
338 }
339
340 [[nodiscard]] std::expected<MeshMaterial, MeshRuntimeError> materialFor(
341 const fastgltf::Asset& asset, const fastgltf::Primitive& primitive,
342 ImageCache& images, std::size_t& decodedImageBytes)
343 {
344 auto result = materialFactors(asset, primitive);
345 if (!primitive.materialIndex) return result;
346 const auto& material = asset.materials[*primitive.materialIndex];
347 if (material.alphaMode == fastgltf::AlphaMode::Blend)
349 "modern GLB alpha blending requires transparent sorting and pipelines"));
350 result.alphaMode = material.alphaMode == fastgltf::AlphaMode::Mask ?
352 result.alphaCutoff = static_cast<float>(material.alphaCutoff);
353 if (!std::isfinite(result.alphaCutoff) || result.alphaCutoff < 0.0F)
354 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
355 "modern GLB alpha cutoff must be finite and nonnegative"));
356 const auto bind = [&](const auto& source, std::optional<ModernTextureBinding>& target,
357 ModernTextureColorSpace colorSpace, float scale = 1.0F)
358 -> std::expected<void, MeshRuntimeError>
359 {
360 if (!source) return {};
361 auto binding = textureBindingFor(asset, *source, colorSpace, scale, images, decodedImageBytes);
362 if (!binding) return std::unexpected(binding.error());
363 target = std::move(*binding);
364 return {};
365 };
366 if (auto bound = bind(material.pbrData.baseColorTexture, result.baseColorTexture,
367 ModernTextureColorSpace::Srgb); !bound) return std::unexpected(bound.error());
368 if (auto bound = bind(material.pbrData.metallicRoughnessTexture, result.metallicRoughnessTexture,
369 ModernTextureColorSpace::Linear); !bound) return std::unexpected(bound.error());
370 if (auto bound = bind(material.normalTexture, result.normalTexture, ModernTextureColorSpace::Linear,
371 material.normalTexture ? static_cast<float>(material.normalTexture->scale) : 1.0F);
372 !bound) return std::unexpected(bound.error());
373 if (auto bound = bind(material.emissiveTexture, result.emissiveTexture,
374 ModernTextureColorSpace::Srgb); !bound) return std::unexpected(bound.error());
375 if (auto bound = bind(material.occlusionTexture, result.occlusionTexture, ModernTextureColorSpace::Linear,
376 material.occlusionTexture ? static_cast<float>(material.occlusionTexture->strength) : 1.0F);
377 !bound) return std::unexpected(bound.error());
378 if (result.occlusionTexture &&
379 (result.occlusionTexture->scale < 0.0F || result.occlusionTexture->scale > 1.0F))
380 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
381 "modern GLB occlusion strength must be between zero and one"));
382 if ((result.baseColorTexture || result.metallicRoughnessTexture || result.normalTexture ||
383 result.emissiveTexture || result.occlusionTexture) &&
384 primitive.findAttribute("TEXCOORD_0") == primitive.attributes.end())
385 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
386 "modern GLB textured primitive requires TEXCOORD_0"));
387 return result;
388 }
389 }
390
391
392 std::expected<
393 std::shared_ptr<const MeshRenderData>,
394 MeshRuntimeError>
396 const std::filesystem::path& path,
397 ModernGltfLoadOptions options) try
398 {
399 if (path.empty() || options.unitsPerMeter <= 0.0F ||
400 !std::isfinite(options.unitsPerMeter) ||
401 !std::isfinite(options.yawDegrees) ||
402 !std::ranges::all_of(options.localOffset,
403 [](float value) { return std::isfinite(value); }))
404 return std::unexpected(error(
406 "invalid modern GLB path or world scale"));
407
408 // GltfFileStream's constructor calls throwing file_size before isOpen.
409 // Validate missing/unreadable paths and cap parser input before opening.
410 std::error_code fileError;
411 const bool regular = std::filesystem::is_regular_file(path, fileError);
412 const auto inputBytes = regular && !fileError
413 ? std::filesystem::file_size(path, fileError) : 0;
414 if (!regular || fileError)
415 return std::unexpected(error(MeshRuntimeErrorCode::SourceLoadFailed,
416 "modern GLB file is unavailable: " + path.string()));
417 constexpr std::uintmax_t MaxGlbInputBytes = 256U * 1024U * 1024U;
419 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
420 "modern GLB input exceeds the 256 MiB parser budget"));
421
422 fastgltf::GltfFileStream stream(path);
423 if (!stream.isOpen())
424 return std::unexpected(error(
426 "modern GLB file cannot be opened: " +
427 path.string()));
428
429 fastgltf::Parser parser(fastgltf::Extensions::KHR_texture_transform |
430 fastgltf::Extensions::KHR_materials_emissive_strength);
431 auto loaded = parser.loadGltfBinary(
432 stream,
433 path.parent_path(),
434 fastgltf::Options::None);
435 if (loaded.error() != fastgltf::Error::None ||
436 loaded.get_if() == nullptr)
437 return std::unexpected(error(
439 std::string("fastgltf: ") +
440 std::string(fastgltf::getErrorMessage(
441 loaded.error()))));
442
443 auto asset = std::move(loaded.get());
444 if (!asset.animations.empty())
446 "modern GLB animations require the animation adapter"));
447 // Sparse reference/byte checks alone do not validate ordered, in-range
448 // sparse indices. Keep the static bridge explicit until that semantic
449 // validation is supported before accessor decoding.
450 if (std::ranges::any_of(asset.accessors,
451 [](const auto& accessor) { return accessor.sparse.has_value(); }))
453 "modern GLB sparse accessors require bounded semantic validation"));
454 if (!validHierarchy(asset))
455 return std::unexpected(error(
457 "modern GLB node hierarchy is invalid or too deep"));
458 if (asset.scenes.empty() || asset.nodes.empty())
459 return std::unexpected(error(
461 "modern GLB has no scene nodes"));
462
463 auto result = std::make_shared<MeshRenderData>();
464 bool boundsInitialized = false;
465 bool failed = false;
468 std::size_t decodedImageBytes{};
469
470 const auto sceneIndex =
471 asset.defaultScene.value_or(std::size_t{0});
472 if (sceneIndex >= asset.scenes.size())
473 return std::unexpected(error(
475 "modern GLB default scene is out of range"));
476
477 const auto rotation = yawRotation(options.yawDegrees);
478 fastgltf::iterateSceneNodes(
479 asset,
481 fastgltf::math::fmat4x4(1.0F),
482 [&](fastgltf::Node& node,
483 const fastgltf::math::fmat4x4& world)
484 {
485 if (failed || !node.meshIndex)
486 return;
487 if (node.skinIndex)
488 {
489 failed = true;
491 "modern GLB skins require the animation pipeline");
492 return;
493 }
494 if (*node.meshIndex >= asset.meshes.size())
495 {
496 failed = true;
497 failure = error(
499 "modern GLB node references an invalid mesh");
500 return;
501 }
502
503 const auto& mesh = asset.meshes[*node.meshIndex];
504 // Constant for this node: retain the original vertex arithmetic
505 // and singular/nonfinite guards, but avoid millions of inverses.
506 const auto linear = fastgltf::math::fmat3x3(world);
507 const auto normalMatrix = fastgltf::math::transpose(
508 fastgltf::math::inverse(linear));
509 const auto determinant = fastgltf::math::determinant(linear);
510 for (const auto& primitive : mesh.primitives)
511 {
512 if (failed) return;
513 if (primitive.materialIndex &&
514 *primitive.materialIndex >= asset.materials.size())
515 {
516 failed = true;
518 "modern GLB material reference is out of range");
519 return;
520 }
521 if (primitive.type != fastgltf::PrimitiveType::Triangles)
522 {
523 failed = true;
524 failure = error(
526 "modern GLB currently supports triangle primitives only");
527 return;
528 }
529 if (!primitive.targets.empty())
530 {
531 failed = true;
532 failure = error(
534 "modern GLB morph targets are reserved for the animation pass");
535 return;
536 }
537 auto material = materialFor(asset, primitive, images, decodedImageBytes);
538 if (!material)
539 {
540 failed = true;
541 failure = std::move(material.error());
542 return;
543 }
544
545 const auto position = primitive.findAttribute("POSITION");
546 const auto normal = primitive.findAttribute("NORMAL");
547 if (position == primitive.attributes.end() ||
548 normal == primitive.attributes.end() ||
549 !validAccessor(asset, position->accessorIndex,
550 fastgltf::AccessorType::Vec3) ||
551 !validAccessor(asset, normal->accessorIndex,
552 fastgltf::AccessorType::Vec3))
553 {
554 failed = true;
555 failure = error(
557 "modern GLB primitive requires valid POSITION and NORMAL accessors");
558 return;
559 }
560
561 const auto& positionAccessor =
562 asset.accessors[position->accessorIndex];
563 const auto& normalAccessor =
564 asset.accessors[normal->accessorIndex];
565 if (positionAccessor.count != normalAccessor.count ||
566 positionAccessor.type != fastgltf::AccessorType::Vec3 ||
567 normalAccessor.type != fastgltf::AccessorType::Vec3)
568 {
569 failed = true;
570 failure = error(
572 "modern GLB position/normal counts differ");
573 return;
574 }
575
576 if (positionAccessor.count > options.limits.maximumVertices ||
577 result->vertices.size() >
578 options.limits.maximumVertices -
579 positionAccessor.count ||
580 positionAccessor.count >
581 std::numeric_limits<std::uint32_t>::max() -
582 result->vertices.size())
583 {
584 failed = true;
585 failure = error(
587 "modern GLB exceeds mesh vertex budget");
588 return;
589 }
590
591 const auto baseVertex = result->vertices.size();
592 result->vertices.resize(
594
595 const auto uv = primitive.findAttribute("TEXCOORD_0");
596 const auto tangent = primitive.findAttribute("TANGENT");
597 if (tangent != primitive.attributes.end() &&
598 (!validAccessor(asset, tangent->accessorIndex, fastgltf::AccessorType::Vec4) ||
599 asset.accessors[tangent->accessorIndex].count != positionAccessor.count))
600 {
601 failed = true;
603 "modern GLB tangent accessor must match the vertex count");
604 return;
605 }
606 if (uv != primitive.attributes.end() &&
607 (!validAccessor(asset, uv->accessorIndex,
608 fastgltf::AccessorType::Vec2) ||
609 asset.accessors[uv->accessorIndex].count !=
610 positionAccessor.count))
611 {
612 failed = true;
614 "modern GLB UV accessor must match the vertex count");
615 return;
616 }
617 for (std::size_t index = 0;
618 index < positionAccessor.count;
619 ++index)
620 {
621 const auto sourcePosition =
622 fastgltf::getAccessorElement<
623 fastgltf::math::fvec3>(
624 asset, positionAccessor, index);
625 const auto sourceNormal =
626 fastgltf::getAccessorElement<
627 fastgltf::math::fvec3>(
628 asset, normalAccessor, index);
629
630 const auto worldPosition = transformPosition(
631 world,
633 options.unitsPerMeter,
634 rotation);
635 const auto worldNormal = transformNormal(
638 rotation);
639 for (std::size_t axis = 0; axis < 3; ++axis)
640 {
641 if (!std::isfinite(worldPosition[axis]) ||
642 !std::isfinite(worldNormal[axis]))
643 {
644 failed = true;
646 "modern GLB transform or normal is nonfinite or singular");
647 return;
648 }
649 }
650
651 auto& vertex = result->vertices[baseVertex + index];
652 vertex.position = {
653 worldPosition[0],
654 worldPosition[1],
655 worldPosition[2]};
656 vertex.normal = {
657 worldNormal[0],
658 worldNormal[1],
659 worldNormal[2]};
660
661 if (tangent != primitive.attributes.end())
662 {
663 const auto sourceTangent = fastgltf::getAccessorElement<fastgltf::math::fvec4>(
664 asset, asset.accessors[tangent->accessorIndex], index);
665 auto direction = rotateAroundY(linear * fastgltf::math::fvec3(
667 direction -= worldNormal * fastgltf::math::dot(worldNormal, direction);
668 const auto length = fastgltf::math::length(direction);
669 if (!std::isfinite(length) || length <= 0.0F ||
670 !std::isfinite(determinant) || determinant == 0.0F ||
671 (sourceTangent[3] != -1.0F && sourceTangent[3] != 1.0F))
672 {
673 failed = true;
675 "modern GLB authored tangent is invalid or singular");
676 return;
677 }
678 direction /= length;
679 vertex.tangent = {direction[0], direction[1], direction[2],
680 sourceTangent[3] * (determinant < 0.0F ? -1.0F : 1.0F)};
681 }
682
683 if (uv != primitive.attributes.end())
684 {
685 const auto sourceUv =
686 fastgltf::getAccessorElement<
687 fastgltf::math::fvec2>(
688 asset,
689 asset.accessors[uv->accessorIndex],
690 index);
691 vertex.uv = {
692 sourceUv[0],
693 sourceUv[1]};
694 if (!std::isfinite(sourceUv[0]) || !std::isfinite(sourceUv[1]))
695 {
696 failed = true;
698 "modern GLB UV coordinates are nonfinite");
699 return;
700 }
701 }
703 result->bounds,
704 vertex,
706 }
707
708 const fastgltf::Accessor* indexAccessor = nullptr;
709 if (primitive.indicesAccessor)
710 {
711 if (!validAccessor(asset, *primitive.indicesAccessor,
712 fastgltf::AccessorType::Scalar))
713 {
714 failed = true;
716 "modern GLB index accessor is invalid");
717 return;
718 }
719 indexAccessor = &asset.accessors[*primitive.indicesAccessor];
720 if (indexAccessor->normalized ||
721 (indexAccessor->componentType != fastgltf::ComponentType::UnsignedByte &&
722 indexAccessor->componentType != fastgltf::ComponentType::UnsignedShort &&
723 indexAccessor->componentType != fastgltf::ComponentType::UnsignedInt))
724 {
725 failed = true;
727 "modern GLB indices must be unsigned integers");
728 return;
729 }
730 }
731 const auto indexCount = indexAccessor ? indexAccessor->count : positionAccessor.count;
732 if (indexCount % 3 != 0)
733 {
734 failed = true;
736 "modern GLB triangle indices must be scalar triplets");
737 return;
738 }
739 if (indexCount > options.limits.maximumIndices ||
740 result->indices.size() >
741 options.limits.maximumIndices -
742 indexCount)
743 {
744 failed = true;
745 failure = error(
747 "modern GLB exceeds mesh index budget");
748 return;
749 }
750 if (result->batches.size() >=
751 options.limits.maximumGroups)
752 {
753 failed = true;
754 failure = error(
756 "modern GLB exceeds mesh batch budget");
757 return;
758 }
759
760 const auto firstIndex = result->indices.size();
761 result->indices.reserve(
762 firstIndex + indexCount);
763 for (std::size_t index = 0;
764 index < indexCount;
765 ++index)
766 {
767 const auto sourceIndex =
768 indexAccessor ? fastgltf::getAccessorElement<std::uint32_t>(
769 asset, *indexAccessor, index) : static_cast<std::uint32_t>(index);
770 if (sourceIndex >= positionAccessor.count)
771 {
772 failed = true;
773 failure = error(
775 "modern GLB index exceeds primitive vertex count");
776 return;
777 }
778 result->indices.push_back(
779 static_cast<std::uint32_t>(
781 }
782
783 result->batches.push_back(MeshRenderBatch{
784 firstIndex,
785 indexCount,
786 std::move(*material),
787 std::nullopt
788 });
789 }
790 });
791
792 if (failed)
793 return std::unexpected(std::move(failure));
794 if (!boundsInitialized ||
795 result->vertices.empty() ||
796 result->indices.empty() ||
797 result->batches.empty())
798 return std::unexpected(error(
800 "modern GLB contains no renderable triangle geometry"));
801
802 std::array<float, 3> offset = options.localOffset;
803 if (options.groundToZero)
804 offset[1] -= result->bounds.minimum[1];
805 for (std::size_t axis = 0; axis < 3; ++axis)
806 if (!std::isfinite(offset[axis]) ||
807 !std::isfinite(result->bounds.minimum[axis] + offset[axis]) ||
808 !std::isfinite(result->bounds.maximum[axis] + offset[axis]))
809 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
810 "modern GLB offset exceeds finite world coordinates"));
811
812 if (offset != std::array<float, 3>{})
813 {
814 for (auto& vertex : result->vertices)
815 for (std::size_t axis = 0; axis < 3; ++axis)
816 vertex.position[axis] += offset[axis];
817 for (std::size_t axis = 0; axis < 3; ++axis)
818 {
819 result->bounds.minimum[axis] += offset[axis];
820 result->bounds.maximum[axis] += offset[axis];
821 }
822 }
823
824 return std::const_pointer_cast<const MeshRenderData>(result);
825 }
826 catch (const std::filesystem::filesystem_error& failure)
827 {
828 return std::unexpected(error(MeshRuntimeErrorCode::SourceLoadFailed,
829 "modern GLB file access failed: " + std::string(failure.what())));
830 }
831 catch (const std::bad_alloc&)
832 {
833 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
834 "modern GLB allocation failed"));
835 }
836 catch (const std::length_error&)
837 {
838 return std::unexpected(error(MeshRuntimeErrorCode::ModernAssetInvalid,
839 "modern GLB allocation exceeds container limits"));
840 }
841}
std::size_t count
std::size_t offset
std::size_t root
std::vector< std::byte > bytes
std::shared_ptr< const data::HmdTextureImage > source
SDL_GPUTexture * texture
float cosine
float sine
unsigned index
std::string path
std::string error
std::uint16_t value
Definition Timers.cpp:16
std::expected< std::shared_ptr< const MeshRenderData >, MeshRuntimeError > loadModernGltfMesh(const std::filesystem::path &path, ModernGltfLoadOptions options)
std::expected< std::shared_ptr< const ModernTextureImage >, MeshRuntimeError > decodeModernImage(std::span< const std::byte > bytes, ModernImageEncoding encoding, ModernImageDecodeLimits limits)
std::size_t size()