Cogs.Core
GltfLoader.cpp
1#include "GltfLoader.h"
2
20#include "DracoMeshDecompressor.h"
21#include "MeshoptDecompressor.h"
22#include "Context.h"
23#include "ExtensionRegistry.h"
24
25#include "Services/Variables.h"
26#include "Platform/ResourceBufferBackedFileContents.h"
27#include "Resources/AnimationManager.h"
28#include "Resources/DefaultMaterial.h"
29#include "Resources/IModelLoader.h"
30#include "Resources/MaterialManager.h"
31#include "Resources/ResourceStore.h"
32#include "Resources/Skeleton.h"
33#include "Resources/Animation.h"
34#include "Foundation/HashSequence.h"
35#include "Foundation/Logging/Logger.h"
36#include "Foundation/Platform/IO.h"
37#include "Rendering/DataFormat.h"
38
39#if defined ( __clang__ )
40#pragma clang diagnostic push
41#pragma clang diagnostic ignored "-Wsign-compare"
42#endif
43
44#include "stb_image.h"
45
46#if defined ( __clang__ )
47#pragma clang diagnostic pop
48#endif
49
50
52{
53 Cogs::Logging::Log logger = Cogs::Logging::getLogger("GltfLoader");
54
55 // FIXME: use stringview
56 std::string getScheme(const std::string& uri)
57 {
58 std::string scheme = "file";
59 std::string::size_type spos = uri.find(':');
60
61 if (spos != std::string::npos) {
62 std::string s = uri.substr(0, spos);
63 bool use = true;
64 for (const char &c : s) {
65 if (!(std::isupper(c) || std::islower(c))) {
66 use = false;
67 break;
68 }
69 }
70 if (use) {
71 scheme = s;
72 }
73 }
74
75 return scheme;
76 }
77
78 void debugMessageOnce(GltfModelDefinition& loadData, const char* format, ...)
79 {
80 char buffer[2048];
81
82 va_list argptr;
83 va_start(argptr, format);
84 int n = std::vsnprintf(buffer, sizeof(buffer), format, argptr);
85 va_end(argptr);
86 if (0 < n && n < static_cast<int>(sizeof(buffer))) {
87 loadData.debugMessages[buffer] += 1;
88 }
89 }
90
91 template<size_t M>
92 void stridedCopy(uint8_t* dst, size_t dstStride, const uint8_t* src, size_t srcStride, size_t count)
93 {
94 for (size_t i = 0; i < count; i++) {
95 for (size_t k = 0; k < M; k++) {
96 dst[dstStride * i + k] = src[srcStride * i + k];
97 }
98 }
99 }
100
101 [[nodiscard]] bool checkIsUint(const GltfModelDefinition& loadData, const char* name, const Value& value)
102 {
103 if (!value.IsUint()) {
104 LOG_ERROR(logger, "%.*s: %s is not an uint", StringViewFormat(loadData.path), name);
105 return false;
106 }
107 return true;
108 }
109
110 [[nodiscard]] bool checkIsUint(const GltfModelDefinition& loadData, const Member& member)
111 {
112 return checkIsUint(loadData, member.name.GetString(), member.value);
113 }
114
115 [[nodiscard]] bool getUint(const GltfModelDefinition& loadData, uint32_t& result, const Member& member)
116 {
117 if (!checkIsUint(loadData, member.name.GetString(), member.value)) {
118 return false;
119 }
120 result = member.value.GetUint();
121 return true;
122 }
123
124 [[nodiscard]] bool checkIsBool(const GltfModelDefinition& loadData, const char* name, const Value& value)
125 {
126 if (!value.IsBool()) {
127 LOG_ERROR(logger, "%.*s: %s is not a bool", StringViewFormat(loadData.path), name);
128 return false;
129 }
130 return true;
131 }
132
133 [[nodiscard]] bool checkIsBool(const GltfModelDefinition& loadData, const Member& member)
134 {
135 return checkIsBool(loadData, member.name.GetString(), member.value);
136 }
137
138 [[nodiscard]] bool getBool(const GltfModelDefinition& loadData, bool& result, const Member& member)
139 {
140 if (!checkIsBool(loadData, member.name.GetString(), member.value)) return false;
141 result = member.value.GetBool();
142 return true;
143 }
144
145 [[nodiscard]] bool checkIsString(const GltfModelDefinition& loadData, const char* name, const Value& value)
146 {
147 if (!value.IsString()) {
148 LOG_ERROR(logger, "%.*s: %s is not a string", StringViewFormat(loadData.path), name);
149 return false;
150 }
151 return true;
152 }
153
154 [[nodiscard]] bool checkIsString(const GltfModelDefinition& loadData, const Member& member)
155 {
156 return checkIsString(loadData, member.name.GetString(), member.value);
157 }
158
159 [[nodiscard]] bool checkIsObject(const GltfModelDefinition& loadData, const char* name, const Value& value)
160 {
161 if (!value.IsObject()) {
162 LOG_ERROR(logger, "%.*s: %s is not an object", StringViewFormat(loadData.path), name);
163 return false;
164 }
165 return true;
166 }
167
168 [[nodiscard]] bool checkIsObject(const GltfModelDefinition& loadData, const Member& member)
169 {
170 return checkIsObject(loadData, member.name.GetString(), member.value);
171 }
172
173 [[nodiscard]] bool checkIsArray(const GltfModelDefinition& loadData, const Member& member)
174 {
175 if (!member.value.IsArray()) {
176 LOG_ERROR(logger, "%.*s: %s is not an array", StringViewFormat(loadData.path), member.name.GetString());
177 return false;
178 }
179 return true;
180 }
181
182 template<typename Vec>
183 [[nodiscard]] bool tryGetVecN(Vec& result, const GltfModelDefinition& loadData, const char* key, const Object& parent)
184 {
185 size_t N = result.length();
186
187 if (auto it = parent.FindMember(key); it != parent.MemberEnd()) {
188 if (!it->value.IsArray()) {
189 LOG_ERROR(logger, "%.*s: %s not an array", StringViewFormat(loadData.path), it->name.GetString());
190 return false;
191 }
192 const Array& arr = it->value.GetArray();
193 if (arr.Size() != N) {
194 LOG_ERROR(logger, "%.*s: %s not an array with length %zu", StringViewFormat(loadData.path), it->name.GetString(), N);
195 return false;
196 }
197 for (uint32_t i = 0; i < N; i++) {
198 if (!arr[i].IsNumber()) {
199 LOG_ERROR(logger, "%.*s: %s array element %u is not a float", StringViewFormat(loadData.path), it->name.GetString(), i);
200 return false;
201 }
202 result[i] = arr[i].GetFloat();
203 }
204 }
205 return true;
206 }
207
208 [[nodiscard]] bool tryGetBool(bool& result, const GltfModelDefinition& loadData, const char* key, const Object& parent)
209 {
210 if (auto it = parent.FindMember(key); it != parent.MemberEnd()) {
211 if (!it->value.IsBool()) {
212 LOG_ERROR(logger, "%.*s: %s not a bool", StringViewFormat(loadData.path), it->name.GetString());
213 return false;
214 }
215 result = it->value.GetBool();
216 }
217 return true;
218 }
219
220 [[nodiscard]] bool tryGetString(Cogs::StringView& result, const GltfModelDefinition& loadData, const char* key, const Object& parent)
221 {
222 if (auto it = parent.FindMember(key); it != parent.MemberEnd()) {
223 if (!it->value.IsString()) {
224 LOG_ERROR(logger, "%.*s: %s not a string", StringViewFormat(loadData.path), it->name.GetString());
225 return false;
226 }
227 result = toView(it->value);
228 }
229 return true;
230 }
231
232 [[nodiscard]] bool tryGetFloat(float& result, const GltfModelDefinition& loadData, const char* key, const Object& parent)
233 {
234 if (auto it = parent.FindMember(key); it != parent.MemberEnd()) {
235 if (!it->value.IsNumber()) {
236 LOG_ERROR(logger, "%.*s: %s not a float", StringViewFormat(loadData.path), it->name.GetString());
237 return false;
238 }
239 result = it->value.GetFloat();
240 }
241 return true;
242 }
243
244 [[nodiscard]] bool tryGetUint(uint32_t& result, const GltfModelDefinition& loadData, const char* key, const Object& parent)
245 {
246 if (auto it = parent.FindMember(key); it != parent.MemberEnd()) {
247 if (!it->value.IsUint()) {
248 LOG_ERROR(logger, "%.*s: %s not an uint", StringViewFormat(loadData.path), it->name.GetString());
249 return false;
250 }
251 result = it->value.GetUint();
252 }
253 return true;
254 }
255
256 [[nodiscard]] bool getString(std::string& result, const Cogs::StringView& path, const Member& member)
257 {
258 if (!member.value.IsString()) {
259 LOG_ERROR(logger, "%.*s: %s s not a string", StringViewFormat(path), member.name.GetString());
260 return false;
261 }
262 result = std::string(member.value.GetString());
263
264 // Fix at least space char encoded in URL. Fails Kronos demos.
265 // Todo - more URL decoding.
266 for (;;) {
267 size_t pos = result.find("%20");
268 if (pos == std::string::npos) {
269 break;
270 }
271 result.replace(pos, 3, " ");
272 }
273
274 return true;
275 }
276
277 [[nodiscard]] bool parseAsset(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& assetValue)
278 {
279 if (!assetValue.IsObject()) {
280 LOG_ERROR(logger, "%.*s: JSON asset is not an object value", StringViewFormat(path));
281 return false;
282 }
283
284 for (const auto& field : assetValue.GetObject()) {
285 if (field.name == "version") {
286 std::string v = field.value.GetString();
287 size_t pos = v.find('.');
288 std::string v_maj = v.substr(0, pos);
289 std::string v_min = v.substr(pos + 1, v.size() - pos);
290 int32_t major_version = atoi(v_maj.c_str());
291 int32_t minor_version = atoi(v_min.c_str());
292 loadData.version = major_version * 100 + minor_version;
293 }
294 else if (field.name == "minVersion") {
295 std::string v = field.value.GetString();
296 size_t pos = v.find('.');
297 std::string v_maj = v.substr(0, pos);
298 std::string v_min = v.substr(pos + 1, v.size() - pos);
299 int32_t major_version = atoi(v_maj.c_str());
300 int32_t minor_version = atoi(v_min.c_str());
301 loadData.min_version = major_version * 100 + minor_version;
302 }
303 else if (field.name == "generator") {
304 // Ignored
305 }
306 else if (field.name == "copyright") {
307 // Ignored
308 }
309 else if (field.name == "extras") {
310 // Ignored
311 }
312 else if(field.name == "extensions"){
313 // Ignored
314 }
315 else if(field.name == "extensionsRequired"){
316 // Ignored
317 }
318 else {
319 LOG_WARNING_ONCE(logger, "Unknown asset field: %s", field.name.GetString());
320 break;
321 }
322 }
323 if (loadData.min_version == -1) loadData.min_version = loadData.version;
324 if (loadData.min_version >= 300) {
325 LOG_ERROR(logger, "Could not load asset file %.*s. (Unsuported min version %d.)", StringViewFormat(path), loadData.min_version);
326 return false;
327 }
328 return true;
329 }
330
331 [[nodiscard]] bool parseBuffers(Context* context, GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& buffersValue, std::span<const uint8_t>& glbData)
332 {
333 if (!buffersValue.IsArray()) {
334 LOG_ERROR(logger, "%.*s: JSON buffers member is not an array value", StringViewFormat(path));
335 return false;
336 }
337
338 for (const auto& bufferValue : buffersValue.GetArray()) {
339 bool glb = true;
340 std::string uri;
341
342 for (const auto& field : bufferValue.GetObject()) {
343 if (field.name == "uri") {
344 uri = field.value.GetString();
345 if (uri.find("%") != std::string::npos) {
346 LOG_WARNING_ONCE(logger, "Unknown buffer field: %s", uri.c_str());
347 }
348 else if (uri.find("&") != std::string::npos) {
349 LOG_WARNING_ONCE(logger, "Unknown buffer field: %s", uri.c_str());
350 }
351 glb = false;
352 }
353 else if (field.name == "byteLength") {
354 // Ignored
355 }
356 else if (field.name == "extensions") {
357 auto exts = field.value.GetObject();
358 for (auto& ext : exts) {
359 if (std::string(ext.name.GetString()) == "EXT_meshopt_compression") {
360 // This section can contain a "fallback=true/false" value which indicates that this buffer shall be reserved
361 // for the decompressed data. This is ignored for now as we handle the decompressed data ourselves.
362 }
363 }
364 }
365 else {
366 LOG_WARNING_ONCE(logger, "Unknown buffer field: %s", field.name.GetString());
367 }
368 }
369
370 std::string scheme = getScheme(uri);
371
372 if (glb) {
373 loadData.buffer_data.push_back(glbData);
374 }
375 else if (scheme == "data") {
376 std::string::size_type spos = uri.find(':');
377 std::string::size_type mpos = uri.find(';');
378 std::string::size_type dpos = uri.find(',');
379 std::string media_type = uri.substr(spos + 1, mpos - (spos + 1));
380 std::string base_type = uri.substr(mpos + 1, dpos - (mpos + 1));
381 std::string base64 = uri.substr(dpos + 1);
382 LOG_TRACE(logger, "Loading data: media type \"%s\", base_type \"%s\"", media_type.c_str(), base_type.c_str());
383 std::string data = base64_decode(base64);
384
385 Cogs::Memory::MemoryBuffer buffer(data.size(), context->memory->ioAllocator);
386
387 std::memcpy(buffer.data(), &data[0], data.size());
388
389 auto& stored = loadData.buffer_data_store.emplace_back(ResourceBuffer{ std::make_shared<Cogs::Memory::MemoryBuffer>(std::move(buffer)) });
390
391 loadData.buffer_data.push_back(std::span<const uint8_t>(stored.data(), stored.data() + stored.size()));
392 }
393 else if (scheme == "file") {
394 std::string::size_type spos = uri.find(':');
395 if (spos != std::string::npos) {
396 uri = Cogs::IO::combine(Cogs::IO::parentPath(path), uri.substr(spos + 1));
397 }
398 else {
399 uri = Cogs::IO::combine(Cogs::IO::parentPath(path), uri);
400 }
401 auto& stored = loadData.buffer_data_store.emplace_back(context->resourceStore->getResourceContents(uri));
402 if (stored.empty()) {
403 LOG_ERROR(logger, "Failed or empty linked uri: %s files.", uri.c_str());
404 return false;
405 }
406 loadData.buffer_data.push_back(std::span<const uint8_t>(stored.data(), stored.data() + stored.size()));
407 }
408 else {
409 LOG_ERROR(logger, "%.*s: Loader cannot handle uri %s files.", StringViewFormat(path), uri.c_str());
410 return false;
411 }
412 }
413 return true;
414 }
415
416 [[nodiscard]] bool parseBufferViews(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& bufferViewsValue, MeshoptDecompressor & meshoptDecomp)
417 {
418 if (!bufferViewsValue.IsArray()) {
419 LOG_ERROR(logger, "%.*s: JSON bufferViews is not an array", StringViewFormat(path));
420 return false;
421 }
422
423 for (const auto& bufferViewValue : bufferViewsValue.GetArray()) {
424 GltfBufferView& tmp = loadData.buffer_views.emplace_back(GltfBufferView());
425
426 if (!bufferViewValue.IsObject()) {
427 LOG_ERROR(logger, "%.*s: JSON bufferViews element is not an object", StringViewFormat(path));
428 return false;
429 }
430
431 for (const auto& field : bufferViewValue.GetObject()) {
432 switch (toView(field.name).hash()) {
433 case Cogs::hash("buffer"):
434 if (!field.value.IsUint()) {
435 LOG_ERROR(logger, "%.*s: JSON bufferView buffer is not an uint", StringViewFormat(path));
436 return false;
437 }
438 tmp.buffer = field.value.GetUint();
439 break;
440
441 case Cogs::hash("byteLength"):
442 if (!field.value.IsUint()) {
443 LOG_ERROR(logger, "%.*s: JSON bufferView byteLength is not an uint", StringViewFormat(path));
444 return false;
445 }
446 tmp.byteLength = field.value.GetUint();
447 break;
448
449 case Cogs::hash("byteOffset"):
450 if (!field.value.IsUint()) {
451 LOG_ERROR(logger, "%.*s: JSON bufferView byteOffset is not an uint", StringViewFormat(path));
452 return false;
453 }
454 tmp.byteOffset = field.value.GetUint();
455 break;
456
457 case Cogs::hash("target"):
458 if (!field.value.IsUint()) {
459 LOG_ERROR(logger, "%.*s: JSON bufferView target is not an uint", StringViewFormat(path));
460 return false;
461 }
462 tmp.target = field.value.GetUint();
463 break;
464
465 case Cogs::hash("byteStride"):
466 if (!field.value.IsUint()) {
467 LOG_ERROR(logger, "%.*s: JSON bufferView byteStride is not an uint", StringViewFormat(path));
468 return false;
469 }
470 tmp.byteStride = field.value.GetUint();
471 break;
472 case Cogs::hash("name"):
473 // TODO
474 break;
475 case Cogs::hash("extensions"):
476 for (const auto& ext : field.value.GetObject()) {
477 if (std::string(ext.name.GetString()) == "EXT_meshopt_compression") {
478 meshoptDecomp.registerBufferViewCompression(uint32_t(loadData.buffer_views.size() - 1), ext.value.GetObject());
479 }
480 }
481 break;
482 default:
483 LOG_ERROR(logger, "%.*s: JSON unknown bufferView field: %s", StringViewFormat(path), field.name.GetString());
484 break;
485 }
486 }
487 }
488 return true;
489 }
490
491 void decodeImageData(Context* context, GltfImage& gltfImage, const Cogs::StringView& path, const Cogs::StringView& mimeType, const void* data, const size_t size)
492 {
493#if 0 // Debug image
494 const uint32_t img[2 * 2] = {
495 0xff00ff, 0x00ff00,
496 0x00ff00, 0xff00ff,
497 };
498 void* mem = malloc(sizeof(img));
499 std::memcpy(mem, img, sizeof(img));
500 gltfImage.decoded.data = reinterpret_cast<stbi_uc*>(mem);
501 gltfImage.decoded.width = 2;
502 gltfImage.decoded.height = 2;
503 gltfImage.decoded.comp = 4;
504 return;
505#endif
506
507 switch (mimeType.hash()) {
508 case Cogs::hash("image/png"):
509 case Cogs::hash("image/jpeg"):
510 stbi_set_flip_vertically_on_load_thread(0);
511 gltfImage.handle = context->textureManager->loadTextureFromMemory(data, size, "unnamed.extension", NoResourceId, Cogs::Core::TextureLoadFlags::None);
512 break;
513
514 case Cogs::hash("image/ktx2"):
515 {
516 gltfImage.handle = context->textureManager->loadTextureFromMemory(data, size, "unnamed.ktx2", NoResourceId, Cogs::Core::TextureLoadFlags::ColorSpaceFromLoadInfo);
517
518 break;
519 }
520
521 default:
522 LOG_ERROR(logger, "%.*s: Unsupported mimetype %.*s", StringViewFormat(path), StringViewFormat(mimeType));
523 gltfImage.decoded.failed = true;
524 }
525 }
526
527 void decodeImageData(Context* context, GltfImage& gltfImage, const Cogs::StringView& path, const Cogs::StringView& mimeType, const void* data, const size_t size, Cogs::Core::TextureLoadFlags loadFlags)
528 {
529#if 0 // Debug image
530 const uint32_t img[2 * 2] = {
531 0xff00ff, 0x00ff00,
532 0x00ff00, 0xff00ff,
533 };
534 void* mem = malloc(sizeof(img));
535 std::memcpy(mem, img, sizeof(img));
536 gltfImage.decoded.data = reinterpret_cast<stbi_uc*>(mem);
537 gltfImage.decoded.width = 2;
538 gltfImage.decoded.height = 2;
539 gltfImage.decoded.comp = 4;
540 return;
541#endif
542
543 switch (mimeType.hash()) {
544 case Cogs::hash("image/png"):
545 case Cogs::hash("image/jpeg"):
546 gltfImage.handle = context->textureManager->loadTextureFromMemory(data, size, "unnamed.extension", NoResourceId, loadFlags);
547 break;
548
549 case Cogs::hash("image/ktx2"):
550 {
551 gltfImage.handle = context->textureManager->loadTextureFromMemory(data, size, "unnamed.ktx2", NoResourceId, loadFlags | Cogs::Core::TextureLoadFlags::ColorSpaceFromLoadInfo);
552 break;
553 }
554
555 default:
556 LOG_ERROR(logger, "%.*s: Unsupported mimetype %.*s", StringViewFormat(path), StringViewFormat(mimeType));
557 gltfImage.decoded.failed = true;
558 }
559 }
560
561 [[nodiscard]] bool parseImages(Context* context, GltfModelDefinition& loadData, const Value& imagesValue)
562 {
563 if (!imagesValue.IsArray()) {
564 LOG_ERROR(logger, "%.*s: Images is not an array", StringViewFormat(loadData.path));
565 return false;
566 }
567
568 Array images = imagesValue.GetArray(); // Resize up-front so it doesn't move in mem, we use async tasks here
569 loadData.images.resize(images.Size());
570
571 for(uint32_t i=0; i<images.Size(); i++) {
572 const Value& imageValue = images[i];
573 if (!imageValue.IsObject()) {
574 LOG_ERROR(logger, "%.*s: Images array item is not an object", StringViewFormat(loadData.path));
575 return false;
576 }
577 auto imageObject = imageValue.GetObject();
578
579 GltfImage& image = loadData.images[i];
580
581 if (auto uriMember = imageObject.FindMember("uri"); uriMember != imageValue.MemberEnd()) {
582 if (!getString(image.uri, loadData.path, *uriMember)) {
583 return false;
584 }
585
586 const Cogs::StringView imageUri(image.uri);
587 if (imageUri.substr(0, 5) == "data:") {
588 size_t spos = imageUri.find_first_of(':');
589 size_t mpos = imageUri.find_first_of(';');
590 size_t dpos = imageUri.find_first_of(',');
591 Cogs::StringView mediaType = imageUri.substr(spos + 1, mpos - (spos + 1));
592 Cogs::StringView baseType = imageUri.substr(mpos + 1, dpos - (mpos + 1));
593 Cogs::StringView base64 = imageUri.substr(dpos + 1);
594 LOG_TRACE(logger, "%.*s: Loading embedded data: media type \"%.*s\", base_type \"%.*s\"",
595 StringViewFormat(loadData.path), StringViewFormat(mediaType), StringViewFormat(baseType));
596
597 image.kind = GltfImage::LoadFromMemory;
598 image.decoded.task = loadData.context->taskManager->enqueueChild(loadData.rootTask,
599 [&loadData, &image, mediaType, base64, context]() {
600 std::string data = base64_decode(base64.to_string());
601 decodeImageData(context, image, loadData.path, mediaType, data.c_str(), data.size());
602 });
603 }
604 else {
605 image.kind = GltfImage::LoadFromUri;
606 }
607 }
608
609 else if (auto bufferViewMember = imageObject.FindMember("bufferView"); bufferViewMember != imageValue.MemberEnd()) {
610 auto mimeTypeMember = imageObject.FindMember("mimeType");
611 if (mimeTypeMember == imageObject.MemberEnd()) {
612 LOG_ERROR(logger, "%.*s: Images array item has an bufferView item but no mimeType member", StringViewFormat(loadData.path));
613 return false;
614 }
615
616 uint32_t bufferViewIndex = 0;
617 if (!getUint(loadData, bufferViewIndex, *bufferViewMember)) {
618 return false;
619 }
620
621 std::string mimeType;
622 if (!getString(mimeType, loadData.path, *mimeTypeMember)) {
623 return false;
624 }
625
626 image.kind = GltfImage::LoadFromMemory;
627 image.from_memory_data.bufferViewIndex = bufferViewIndex;
628 image.from_memory_data.mimeType = mimeType;
629 }
630 else {
631 LOG_ERROR(logger, "%.*s: Images array item has neither an uri nor a bufferView member", StringViewFormat(loadData.path));
632 return false;
633 }
634 }
635
636 return true;
637 }
638
639 [[nodiscard]] TextureHandle getTextureHandle(GltfModelDefinition& loadData, const size_t imageIndex, const bool linearColor, const bool mipmapped)
640 {
641 if (loadData.images.size() <= imageIndex) {
642 LOG_ERROR(logger, "%.*s: Illegal image index %zu", StringViewFormat(loadData.path), imageIndex);
644 }
645
646 GltfImage& image = loadData.images[imageIndex];
647 const size_t cacheIndex = (linearColor ? 2 : 0) + (mipmapped ? 1 : 0);
648
649 if (!image.cache[cacheIndex].handle) {
650 switch (image.kind) {
651 case GltfImage::Unset:
652 LOG_WARNING_ONCE(logger, "%.*s: Image index %zu has no valid data", StringViewFormat(loadData.path), imageIndex);
653 image.cache[cacheIndex].handle = loadData.context->textureManager->white;
654 break;
655 case GltfImage::LoadFromUri:
656 image.cache[cacheIndex].handle = loadData.context->textureManager->loadTexture(Cogs::IO::combine(Cogs::IO::parentPath(loadData.path), image.uri),
657 NoResourceId,
660 break;
661 case GltfImage::LoadFromMemory:
662 assert(!image.cache[cacheIndex].proxy);
663 {
664 const GltfBufferView& bufferView = loadData.buffer_views[image.from_memory_data.bufferViewIndex];
665 std::span<const uint8_t> buffer = loadData.buffer_data[bufferView.buffer];
666
667 const uint8_t* ptr = &buffer[bufferView.byteOffset];
668 uint32_t size = bufferView.byteLength;
669 decodeImageData(loadData.context, image, loadData.path, image.from_memory_data.mimeType, ptr, size, (linearColor ? TextureLoadFlags::LinearColorSpace : TextureLoadFlags::None) |
671 }
672 image.cache[cacheIndex].handle = image.handle;
673 break;
674 default:
675 assert(false && "Invalid image kind");
676 }
677 }
678
679 return image.cache[cacheIndex].handle;
680 }
681
682 [[nodiscard]] bool parseSamplers(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& samplersValue)
683 {
684 if (!samplersValue.IsArray()) {
685 LOG_ERROR(logger, "%.*s: Samplers is not an array", StringViewFormat(path));
686 return false;
687 }
688 for (auto& samplerValue : samplersValue.GetArray()) {
689 if (!samplerValue.IsObject()) {
690 LOG_ERROR(logger, "%.*s: Sampler is not an object", StringViewFormat(path));
691 return false;
692 }
693
694 // Note: Cogs does not expose full filtermode flexibility.
695 bool linearFilter = true;
696 GltfTextureSampler& sampler = loadData.texture_samplers.emplace_back();
697
698 for (const auto& field : samplerValue.GetObject()) {
699 switch (toView(field.name).hash()) {
700 case Cogs::hash("magFilter"): {
701 uint32_t magFilter;
702 if (!getUint(loadData, magFilter, field)) {
703 return false;
704 }
705 switch (magFilter) {
706 case GLTF_NEAREST: break;
707 case GLTF_LINEAR: break;
708 default:
709 LOG_ERROR(logger, "%.*s: Unrecognized magFilter %u", StringViewFormat(path), magFilter);
710 return false;
711 }
712 break;
713 }
714
715 case Cogs::hash("minFilter"): {
716 uint32_t minFilter;
717 if (!getUint(loadData, minFilter, field)) return false;
718 switch (minFilter) {
719 case GLTF_NEAREST: sampler.mipmapped = false; linearFilter = false; break;
720 case GLTF_LINEAR: sampler.mipmapped = false; break;
721 case GLTF_NEAREST_MIPMAP_NEAREST: linearFilter = false; break;
722 case GLTF_LINEAR_MIPMAP_NEAREST: break;
723 case GLTF_NEAREST_MIPMAP_LINEAR: break;
724 case GLTF_LINEAR_MIPMAP_LINEAR: break;
725 default:
726 LOG_ERROR(logger, "%.*s: Unrecognized minFilter %u", StringViewFormat(path), minFilter);
727 return false;
728 }
729 break;
730 }
731
732 case Cogs::hash("wrapS"): {
733 uint32_t wrapS;
734 if (!getUint(loadData, wrapS, field)) return false;
735 switch (wrapS) {
736 case GLTF_CLAMP_TO_EDGE: sampler.sMode = Cogs::SamplerState::AddressMode::Clamp; break;
737 case GLTF_MIRRORED_REPEAT: sampler.sMode = Cogs::SamplerState::AddressMode::Mirror; break;
738 case GLTF_REPEAT: sampler.sMode = Cogs::SamplerState::AddressMode::Clamp; break;
739 default:
740 LOG_ERROR(logger, "%.*s: Unrecognized wrapS %u", StringViewFormat(path), wrapS);
741 return false;
742 }
743 break;
744 }
745
746 case Cogs::hash("wrapT"): {
747 uint32_t wrapT;
748 if (!getUint(loadData, wrapT, field)) return false;
749 switch (wrapT) {
750 case GLTF_CLAMP_TO_EDGE: sampler.tMode = Cogs::SamplerState::AddressMode::Clamp; break;
751 case GLTF_MIRRORED_REPEAT: sampler.tMode = Cogs::SamplerState::AddressMode::Mirror; break;
752 case GLTF_REPEAT: sampler.tMode = Cogs::SamplerState::AddressMode::Clamp; break;
753 default:
754 LOG_ERROR(logger, "%.*s: Unrecognized wrapT %u", StringViewFormat(path), wrapT);
755 return false;
756 }
757 break;
758 }
759
760 default:
761 LOG_WARNING(logger, "%.*s: Unrecognized sampler field %s", StringViewFormat(path), field.name.GetString());
762 break;
763 }
764 }
766 }
767 return true;
768 }
769
770 [[nodiscard]] bool parseTextures(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& texturesValue)
771 {
772 if (!texturesValue.IsArray()) {
773 LOG_ERROR(logger, "%.*s: Textures is not an array", StringViewFormat(path));
774 return false;
775 }
776 for (auto& texture : texturesValue.GetArray()) {
777 auto& texture_value = loadData.textures.emplace_back(GltfTexture{ -1, -1 });
778
779 for (const auto& field : texture.GetObject()) {
780 if (field.name == "sampler") {
781 texture_value.sampler = field.value.GetUint();
782 if (loadData.texture_samplers.size() <= (size_t)texture_value.sampler) {
783 LOG_ERROR(logger, "Illegal texture sampler index %u", texture_value.sampler);
784 return false;
785 }
786 }
787 else if (field.name == "source") {
788 texture_value.source = field.value.GetUint();
789 if (loadData.images.size() <= uint32_t(texture_value.source)) {
790 LOG_ERROR(logger, "Illegal texture source index %u", texture_value.source);
791 return false;
792 }
793 }
794 else if (field.name == "extensions") {
795 auto ext = field.value.GetObject();
796 if (ext.HasMember("KHR_texture_basisu")) {
797 auto data = ext["KHR_texture_basisu"].GetObject();
798 int altSource = data["source"].GetInt();
799 if (!texture.HasMember("source")) {
800 texture_value.source = altSource;
801 }
802 }
803 }
804 else {
805 LOG_WARNING_ONCE(logger, "Unknown texture field: %s", field.name.GetString());
806 }
807 }
808 }
809 return true;
810 }
811
812 [[nodiscard]] bool tryHandleTextureInfo(MaterialHandle material,
813 MaterialInstanceHandle instance,
814 GltfModelDefinition& loadData,
815 CachedModelMaterial& cachedModelMaterial,
816 const char* key,
817 const Object& parent,
818 const char* textureName,
819 bool linearColor,
820 bool* success,
821 const char* extraKey = nullptr,
822 float* extraValue = nullptr)
823 {
824 if (auto texInfoIt = parent.FindMember(key); texInfoIt != parent.MemberEnd()) {
825 if (!checkIsObject(loadData, *texInfoIt)) {
826 return false;
827 }
828 const Object& texInfoObject = texInfoIt->value.GetObject();
829
830 if (auto indexIt = texInfoObject.FindMember("index"); indexIt != texInfoObject.MemberEnd()) {
831 uint32_t index = 0;
832 if (!getUint(loadData, index, *indexIt)) {
833 LOG_ERROR(logger, "No 'index' uint element in texInfoObject");
834 return false;
835 }
836
837 uint32_t texcoord = 0;
838 if (!tryGetUint(texcoord, loadData, "texCoord", texInfoObject)) {
839 LOG_ERROR(logger, "No 'texCoord' uint element in texInfoObject");
840 return false;
841 }
842 cachedModelMaterial.texCoordSets = std::max(cachedModelMaterial.texCoordSets, texcoord + 1);
843
844 if (extraKey && !tryGetFloat(*extraValue, loadData, extraKey, texInfoObject)) {
845 LOG_ERROR(logger, "No '%s' float element in texInfoObject", extraKey);
846 return false;
847 }
848
849 if (loadData.textures.size() <= index) {
850 LOG_ERROR(logger, "%.*s: %s has illegal texture index %u", StringViewFormat(loadData.path), key, index);
851 return false;
852 }
853
854 const GltfTexture& textureDefinition = loadData.textures[index];
855 if (loadData.images.size() <= (size_t)textureDefinition.source) {
856 LOG_ERROR(logger, "%.*s: %s has illegal texture index source %u", StringViewFormat(loadData.path), key, textureDefinition.source);
857 return false;
858 }
859
860 GltfTextureSampler sampler;
861 if (textureDefinition.sampler != -1) {
862 if (loadData.texture_samplers.size() <= (size_t)textureDefinition.sampler) {
863 LOG_ERROR(logger, "%.*s: %s has illegal sampler index %u", StringViewFormat(loadData.path), key, textureDefinition.sampler);
864 return false;
865 }
866 sampler = loadData.texture_samplers[textureDefinition.sampler];
867 }
868 Cogs::Core::TextureHandle textureHandle = getTextureHandle(loadData, textureDefinition.source,
869 linearColor, sampler.mipmapped);
870
871 const std::string texmapName = std::string(textureName) + "Map";
872 if (instance->material->getVariantIndex(texmapName) != Material::NoVariantIndex) {
873 instance->setVariant(texmapName, true);
874 }
875
876 const std::string texUVChannelName = std::string(textureName) + "UVChannel";
877 if (instance->material->getVariantIndex(texUVChannelName) != Material::NoVariantIndex) {
878 instance->setVariant(texUVChannelName, int(texcoord));
879 }
880
881 const std::string texCoordName = std::string("TexCoord") + std::to_string(texcoord);
882 if (instance->material->getVariantIndex(texCoordName) != Material::NoVariantIndex) {
883 instance->setVariant(texCoordName, true);
884 }
885
886 std::string texKeyName = std::string(textureName) + "Map";
887 texKeyName[0] = char(std::tolower(texKeyName[0]));
888
889 const VariableKey texKey = material->getTextureKey(texKeyName);
890 instance->setTextureProperty(texKey, textureHandle);
891 instance->setTextureAddressMode(texKey, sampler.sMode, sampler.tMode, sampler.uMode);
892
893 //
894 // Check if we are using the KHR_texture_transform extension for this material. If so, register the transform
895 // and the texcoords will be preprocessed before uploaded to the GPU.
896 //
897 if (auto extIt = texInfoObject.FindMember("extensions"); extIt != texInfoObject.MemberEnd()) {
898 const Object& extObject = extIt->value.GetObject();
899 if (auto texTrIt = extObject.FindMember("KHR_texture_transform"); texTrIt != extObject.MemberEnd()) {
900 const Object& texTrObject = texTrIt->value.GetObject();
901 TextureTransform textureTransform;
902 if (texTrObject.HasMember("offset")) {
903 if (!texTrObject["offset"].IsArray() || texTrObject["offset"].Size() != 2) {
904 LOG_ERROR(logger, "The 'KHR_texture_transform' member 'offset' is not an array of size 2"); return false;
905 return false;
906 }
907 textureTransform.offset = glm::vec2(texTrObject["offset"][0].GetFloat(), texTrObject["offset"][1].GetFloat());
908 }
909 if (texTrObject.HasMember("scale")) {
910 if (!texTrObject["scale"].IsArray() || texTrObject["scale"].Size() != 2) {
911 LOG_ERROR(logger, "The 'KHR_texture_transform' member 'scale' is not an array of size 2"); return false;
912 return false;
913 }
914 textureTransform.scale = glm::vec2(texTrObject["scale"][0].GetFloat(), texTrObject["scale"][1].GetFloat());
915 }
916 if (texTrObject.HasMember("rotation")) {
917 textureTransform.rotation = texTrObject["rotation"].GetFloat();
918 }
919
920 loadData.textureTransforms.push_back(textureTransform);
921 }
922 }
923
924 if (success) {
925 *success = true;
926 }
927 }
928 else {
929 LOG_ERROR(logger, "%.*s: %s is missing required index property", StringViewFormat(loadData.path), texInfoIt->name.GetString());
930 return false;
931 }
932 }
933 else {
934 // NOTE: This message is printed A LOT due to varying strictness of glTF exporters. Nice to have while debugging the glTF-loader, though.
935 //LOG_DEBUG(logger, "Key '%s' not present in material object", key);
936 }
937
938 return true;
939 }
940
941 [[nodiscard]] bool handleMaterialPbrMetallicRoughness(GltfModelDefinition& loadData, CachedModelMaterial& cachedMaterial, MaterialHandle material, MaterialInstanceHandle instance, const Object& materialObject)
942 {
943 glm::vec4 baseColorFactor(1.f, 1.f, 1.f, 1.f);
944 float metallicFactor = 1.f;
945 float roughnessFactor = 1.f;
946
947 if (auto pbrMetallicValue = materialObject.FindMember("pbrMetallicRoughness"); pbrMetallicValue != materialObject.MemberEnd()) {
948 if (!checkIsObject(loadData, *pbrMetallicValue)) return false;
949 const Object& pbrMetallicObject = pbrMetallicValue->value.GetObject();
950
951 if (!tryGetVecN(baseColorFactor, loadData, "baseColorFactor", pbrMetallicObject)) return false;
952 if (!tryGetFloat(metallicFactor, loadData, "metallicFactor", pbrMetallicObject)) return false;
953 if (!tryGetFloat(roughnessFactor, loadData, "roughnessFactor", pbrMetallicObject)) return false;
954
955 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "baseColorTexture", pbrMetallicObject, "Albedo", false, nullptr)) return false;
956 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "metallicRoughnessTexture", pbrMetallicObject, "MetallicRoughness", true, nullptr)) return false;
957 }
958
959 // TODO: Create a new material exclusive to GLTF models instead of using StandardMaterial that can read color values 'as is'
960 for (int i = 0; i < 3; i++) {
961 baseColorFactor[i] = std::pow(std::abs(baseColorFactor[i]), 1.0f / 2.2f); // StandardMaterial expects albedo to be in sRGB color space
962 }
963
964 instance->setVec4Property(material->getVec4Key("albedo"), baseColorFactor);
965 instance->setFloatProperty(material->getFloatKey("metallic"), metallicFactor);
966 instance->setFloatProperty(material->getFloatKey("roughness"), roughnessFactor);
967
968 return true;
969 }
970
971 [[nodiscard]] bool handleMaterialGenericFields(GltfModelDefinition& loadData, CachedModelMaterial& cachedMaterial, MaterialHandle material, MaterialInstanceHandle instance, const Object& materialObject)
972 {
973 Cogs::StringView name;
974 if (!tryGetString(name, loadData, "name", materialObject)) return false;
975 if (name) {
976 instance->setName(name);
977 }
978
979 Cogs::StringView alphaMode = "OPAQUE";
980 if (!tryGetString(alphaMode, loadData, "alphaMode", materialObject)) return false;
981 switch (alphaMode.hash()) {
982 case Cogs::hash("OPAQUE"):
983 break;
984 case Cogs::hash("MASK"):
985 instance->setVariant("AlphaTest", "Discard");
986 break;
987 case Cogs::hash("BLEND"):
988 instance->setTransparent();
989 break;
990 default:
991 LOG_ERROR(logger, "%.*s: Unknown alphaMode '%.*s'", StringViewFormat(loadData.path), StringViewFormat(alphaMode));
992 return false;
993 }
994
995 float alphaCutoff = 0.5f;
996 if (!tryGetFloat(alphaCutoff, loadData, "alphaCutoff", materialObject)) return false;
997 instance->setFloatProperty(material->getFloatKey("alphaThreshold"), alphaCutoff);
998
999 bool doubleSided = false;
1000 if (!tryGetBool(doubleSided, loadData, "doubleSided", materialObject)) return false;
1001 instance->options.cullMode = doubleSided ? CullMode::None : CullMode::Back;
1002
1003 float normalMapFactor = 1.0f;
1004 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "normalTexture", materialObject, "Normal", true, &cachedMaterial.needsTangents, "scale", &normalMapFactor)) return false;
1005 instance->setFloatProperty(material->getFloatKey("normalMapFactor"), normalMapFactor);
1006
1007 float occlusionFactor = 1.0f;
1008 if (!tryGetFloat(occlusionFactor, loadData, "occlusionFactor", materialObject)) return false;
1009 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "occlusionTexture", materialObject, "Occlusion", true, nullptr, "strength", &occlusionFactor)) return false;
1010 instance->setFloatProperty(material->getFloatKey("occlusion"), occlusionFactor);
1011
1012 glm::vec3 emissiveFactor = glm::vec3(0.0f, 0.0f, 0.0f);
1013 if (!tryGetVecN(emissiveFactor, loadData, "emissiveFactor", materialObject)) return false;
1014 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "emissiveTexture", materialObject, "Emissive", false, nullptr)) return false;
1015 instance->setVec3Property(material->getVec3Key("emissive"), emissiveFactor);
1016
1017 return true;
1018 }
1019
1020 [[nodiscard]] bool handleMaterialPbrSpecularGlossiness(GltfModelDefinition& loadData, CachedModelMaterial& cachedMaterial, MaterialHandle material, MaterialInstanceHandle instance, const Object& specGlossObject)
1021 {
1022 glm::vec4 diffuseFactor = glm::vec4(1.f, 1.f, 1.f, 1.f);
1023 if (!tryGetVecN(diffuseFactor, loadData, "diffuseFactor", specGlossObject)) return false;
1024 // TODO: Create a new material exclusive to GLTF models instead of using StandardMaterial that can read color values 'as is'
1025 for (int i = 0; i < 3; i++) {
1026 diffuseFactor[i] = std::pow(std::abs(diffuseFactor[i]), 1.0f / 2.2f); // StandardMaterial expects albedo to be in sRGB color space
1027 }
1028 instance->setVec4Property(material->getVec4Key("albedo"), diffuseFactor);
1029
1030 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "diffuseTexture", specGlossObject, "Albedo", false, nullptr)) return false;
1031
1032 glm::vec3 specularFactor = glm::vec3(1.f, 1.f, 1.f);
1033 if (!tryGetVecN(specularFactor, loadData, "specularFactor", specGlossObject)) return false;
1034 instance->setVec3Property(material->getVec3Key("specular"), specularFactor);
1035
1036 float glossinessFactor = 1.f;
1037 if (!tryGetFloat(glossinessFactor, loadData, "glossinessFactor", specGlossObject)) return false;
1038 instance->setFloatProperty(material->getFloatKey("gloss"), glossinessFactor);
1039
1040 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "specularGlossinessTexture", specGlossObject, "SpecularGloss", false, nullptr)) return false;
1041 return true;
1042 }
1043
1044 [[nodiscard]] bool handleMaterialSpecular(GltfModelDefinition& loadData, CachedModelMaterial& cachedMaterial, MaterialHandle material, MaterialInstanceHandle instance, const Object& specularObject)
1045 {
1046 float specularFactor = 1.f;
1047 if (!tryGetFloat(specularFactor, loadData, "specularFactor", specularObject)) return false;
1048 instance->setFloatProperty(material->getFloatKey("specularFactor"), specularFactor);
1049
1050 glm::vec3 specularColorFactor = glm::vec3(1.f, 1.f, 1.f);
1051 if (!tryGetVecN(specularColorFactor, loadData, "specularColorFactor", specularObject)) return false;
1052 instance->setVec3Property(material->getVec3Key("specularColor"), specularColorFactor);
1053
1054 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "specularTexture", specularObject, "SpecularFactor", false, nullptr)) return false;
1055
1056 if (!tryHandleTextureInfo(material, instance, loadData, cachedMaterial, "specularColorTexture", specularObject, "SpecularColor", false, nullptr)) return false;
1057
1058 return true;
1059 }
1060
1061 [[nodiscard]] bool handleMaterialIdxOfRefraction(GltfModelDefinition& loadData, CachedModelMaterial& /* cachedMaterial */, MaterialHandle material, MaterialInstanceHandle instance, const Object& iorObject)
1062 {
1063 float idxOfRefraction = 1.5f;
1064 if (!tryGetFloat(idxOfRefraction, loadData, "ior", iorObject)) return false;
1065 instance->setFloatProperty(material->getFloatKey("ior"), idxOfRefraction);
1066 instance->setVariant("Ior", true);
1067
1068 return true;
1069 }
1070
1071 [[nodiscard]] bool handleMaterialExtensions(GltfModelDefinition& loadData, CachedModelMaterial& cachedMaterial, MaterialHandle material, MaterialInstanceHandle instance, const Object& materialObject)
1072 {
1073 if (auto extensionsIt = materialObject.FindMember("extensions"); extensionsIt != materialObject.MemberEnd()) {
1074 if (!checkIsObject(loadData, *extensionsIt)) {
1075 return false;
1076 }
1077
1078 const Object& extensionsObject = extensionsIt->value.GetObject();
1079
1080 if (auto specGlossIt = extensionsObject.FindMember("KHR_materials_pbrSpecularGlossiness"); specGlossIt != extensionsObject.MemberEnd()) {
1081 if (!checkIsObject(loadData, *specGlossIt)) {
1082 return false;
1083 }
1084 const Object& specGlossObject = specGlossIt->value.GetObject();
1085
1086 if (!handleMaterialPbrSpecularGlossiness(loadData, cachedMaterial, material, instance, specGlossObject)) {
1087 return false;
1088 }
1089
1090 return true;
1091 }
1092
1093 // None of the following extensions can be used if KHR_materials_pbrSpecularGlossiness is found
1094 if (auto specularIt = extensionsObject.FindMember("KHR_materials_specular"); specularIt != extensionsObject.MemberEnd()) {
1095 if (!checkIsObject(loadData, *specularIt)) {
1096 return false;
1097 }
1098 const Object& specObject = specularIt->value.GetObject();
1099 if (!handleMaterialSpecular(loadData, cachedMaterial, material, instance, specObject)) {
1100 return false;
1101 }
1102 }
1103
1104 if (auto iorIt = extensionsObject.FindMember("KHR_materials_ior"); iorIt != extensionsObject.MemberEnd()) {
1105 if (!checkIsObject(loadData, *iorIt)) {
1106 return false;
1107 }
1108 const Object& iorObject = iorIt->value.GetObject();
1109 if (!handleMaterialIdxOfRefraction(loadData, cachedMaterial, material, instance, iorObject)) {
1110 return false;
1111 }
1112 }
1113 }
1114 return true;
1115 }
1116
1117 [[nodiscard]] bool getMaterialHandle(GltfModelDefinition& loadData,
1118 Model& model,
1119 CachedModelMaterial*& cachedMaterial_,
1120 const int32_t gltfMaterialIx,
1121 const bool isSkinned,
1122 const bool albedoPerVertex,
1123 const bool hasBitangents)
1124 {
1125 if (loadData.materialsArray.size() < size_t(gltfMaterialIx + 1)) {
1126 LOG_ERROR(logger, "%.*s: Illegal gltf material index %d", StringViewFormat(loadData.path), gltfMaterialIx);
1127 return false;
1128 }
1129 if (loadData.modelMaterialsCache.empty()) {
1130 loadData.modelMaterialsCache.resize(8 * (loadData.materialsArray.size() + 1));
1131 }
1132
1133 // Get entry from cache
1134 size_t cacheIx = 8 * size_t(gltfMaterialIx + 1) + (isSkinned ? 4 : 0) + (albedoPerVertex ? 2 : 0) + (hasBitangents ? 1 : 0);
1135 assert(cacheIx < loadData.modelMaterialsCache.size());
1136 CachedModelMaterial& cachedMaterial = loadData.modelMaterialsCache[cacheIx];
1137 cachedMaterial_ = &cachedMaterial;
1138
1139 // Check if model is already been created
1140 if (cachedMaterial.modelMaterialIx != -1) {
1141 return true;
1142 }
1143
1144 MaterialInstanceHandle materialInstance = MaterialInstanceHandle(nullptr);
1145
1146 // Create material from specification
1147 if (gltfMaterialIx >= 0) {
1148 const Object& materialObject = loadData.materialsArray[gltfMaterialIx];
1149
1150 // Handle the special 'KHR_materials_unlit' extension which needs a more basic version of the material.
1151 if (materialObject.HasMember("extensions") && materialObject["extensions"].HasMember("KHR_materials_unlit")) {
1152 MaterialHandle material = loadData.context->materialManager->getMaterial("DefaultMaterial");
1153 materialInstance = loadData.context->materialInstanceManager->createMaterialInstance(material);
1154 materialInstance->setPermutation("Default");
1155 materialInstance->setVariant("DefaultHasTexCoord", true);
1156 materialInstance->setVariant("Textured", true);
1157 materialInstance->setVariant("LightModel", "BaseColor");
1158 materialInstance->setVariant("EnableLighting", false);
1159 materialInstance->setVariant("NoTonemap", true);
1160
1161 if (auto pbrMetallicValue = materialObject.FindMember("pbrMetallicRoughness"); pbrMetallicValue != materialObject.MemberEnd()) {
1162 if (!checkIsObject(loadData, *pbrMetallicValue)) {
1163 LOG_ERROR(logger, "The 'KHR_materials_unlit' material does not have a 'pbrMetallicRoughness' section.");
1164 return false;
1165 }
1166 const Object& pbrMetallicObject = pbrMetallicValue->value.GetObject();
1167 glm::vec4 baseColorFactor(1.0, 1.0, 1.0, 1.0);
1168 if (!tryGetVecN(baseColorFactor, loadData, "baseColorFactor", pbrMetallicObject)) {
1169 LOG_ERROR(logger, "The 'pbrMetallicRoughness' section does not have a 'baseColorFactor' value.");
1170 return false;
1171 }
1172 materialInstance->setVec4Property(DefaultMaterial::DiffuseColor, baseColorFactor);
1173
1174 if (!tryHandleTextureInfo(material, materialInstance, loadData, cachedMaterial, "baseColorTexture", pbrMetallicObject, "Diffuse", false, nullptr)) {
1175 // The KHR_materials_unlit section did not have any textures specified.
1176 LOG_ERROR(logger, "The 'pbrMetallicRoughness' section does not have a 'baseColorTexture' value.");
1177 return false;
1178 }
1179 }
1180
1181 // As this is an unlit material, we can safely remove all normals to save memory.
1182 cachedMaterial_->needsNormals = false;
1183 }
1184 else {
1185 MaterialHandle material = loadData.context->materialManager->getMaterial("StandardMaterial");
1186 materialInstance = loadData.context->materialInstanceManager->createMaterialInstance(material);
1187
1188 // Note, we set permuation first because switching permutation overwrites variant selectors
1189 materialInstance->setPermutation("Metallic");
1190 if (auto extensionsIt = materialObject.FindMember("extensions"); extensionsIt != materialObject.MemberEnd()) {
1191 if (!checkIsObject(loadData, *extensionsIt)) return false;
1192 const Object& extensionsObject = extensionsIt->value.GetObject();
1193 if (auto specGlossIt = extensionsObject.FindMember("KHR_materials_pbrSpecularGlossiness"); specGlossIt != extensionsObject.MemberEnd()) {
1194 materialInstance->setPermutation("Specular");
1195 }
1196 }
1197 // Enable shader variant that are disabled by defaout for performance
1198 if (auto extensionsIt = materialObject.FindMember("extensions"); extensionsIt != materialObject.MemberEnd()) {
1199 if (!checkIsObject(loadData, *extensionsIt)) return false;
1200 const Object& extensionsObject = extensionsIt->value.GetObject();
1201 if (auto iorIt = extensionsObject.FindMember("KHR_materials_ior"); iorIt != extensionsObject.MemberEnd()) {
1202 materialInstance->setVariant("Ior", true);
1203 }
1204 if (auto specularIt = extensionsObject.FindMember("KHR_materials_specular"); specularIt != extensionsObject.MemberEnd()) {
1205 materialInstance->setVariant("Specular", true);
1206 }
1207 }
1208
1209 materialInstance->setVariant("LightModel", "PBR");
1210 materialInstance->setVariant("GltfMaterialModel", true);
1211 if (isSkinned) materialInstance->setVariant("Skinned", true);
1212 if (albedoPerVertex) materialInstance->setVariant("AlbedoPerVertex", true);
1213 if (hasBitangents) materialInstance->setVariant("Bitangents", true);
1214
1215 if (!handleMaterialPbrMetallicRoughness(loadData, cachedMaterial, material, materialInstance, materialObject)) return false;
1216 if (!handleMaterialGenericFields(loadData, cachedMaterial, material, materialInstance, materialObject)) return false;
1217 if (!handleMaterialExtensions(loadData, cachedMaterial, material, materialInstance, materialObject)) return false;
1218
1219 cachedMaterial_->needsNormals = true;
1220 }
1221 }
1222 // Create default material
1223 else {
1224 MaterialHandle material = loadData.context->materialManager->getMaterial("StandardMaterial");
1225 materialInstance = loadData.context->materialInstanceManager->createMaterialInstance(material);
1226
1227 materialInstance->setPermutation("Metallic");
1228 materialInstance->setVariant("GltfMaterialModel", true);
1229 if (isSkinned) materialInstance->setVariant("Skinned", true);
1230 if (albedoPerVertex) materialInstance->setVariant("AlbedoPerVertex", true);
1231 if (hasBitangents) materialInstance->setVariant("Bitangents", true);
1232 materialInstance->setName("GLTF default material");
1233 materialInstance->setOption("CullMode", "Back");
1234 materialInstance->setVariant("GltfMaterialModel", true);
1235
1236 materialInstance->setVec4Property(material->getVec4Key("albedo"), glm::vec4(1.0, 1.0, 1.0, 1.0));
1237 materialInstance->setFloatProperty(material->getFloatKey("metallic"), 1.0f);
1238 materialInstance->setFloatProperty(material->getFloatKey("roughness"), 1.0f);
1239 materialInstance->setVec3Property(material->getVec3Key("emissive"), glm::vec3(0.0, 0.0, 0.0));
1240 materialInstance->setFloatProperty(material->getFloatKey("alphaThreshold"), 0.5f);
1241
1242 cachedMaterial_->needsNormals = true;
1243 }
1244
1245 assert(materialInstance && "Internal error");
1246
1247 cachedMaterial.modelMaterialIx = int32_t(model.materials.size());
1248 model.materials.push_back(materialInstance);
1249 return true;
1250 }
1251
1252 [[nodiscard]] bool parseScenes(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& scenesValue)
1253 {
1254 if (!scenesValue.IsArray()) {
1255 LOG_ERROR(logger, "%.*s: JSON scenes is not an array value", StringViewFormat(path));
1256 return false;
1257 }
1258
1259 for (const auto& sceneValue : scenesValue.GetArray()) {
1260 if (!sceneValue.IsObject()) {
1261 LOG_ERROR(logger, "%.*s: JSON scenes item is not an object", StringViewFormat(path));
1262 return false;
1263 }
1264
1265 GltfScene& scene = loadData.scenes.emplace_back();
1266 for (const auto& field : sceneValue.GetObject()) {
1267 switch (toView(field.name).hash()) {
1268 case Cogs::hash("name"): {
1269 if (!field.value.IsString()) {
1270 LOG_ERROR(logger, "%.*s: JSON scene name value is not a string", StringViewFormat(path));
1271 return false;
1272 }
1273 scene.name = field.value.GetString();
1274 break;
1275 }
1276 case Cogs::hash("nodes"):
1277 {
1278 if (!field.value.IsArray()) {
1279 LOG_ERROR(logger, "%.*s: JSON scene nodes value is not an array", StringViewFormat(path));
1280 return false;
1281 }
1282 for (const auto& node : field.value.GetArray()) {
1283 if (!node.IsUint()) {
1284 LOG_ERROR(logger, "%.*s: JSON scene nodes array item is not an uint", StringViewFormat(path));
1285 return false;
1286 }
1287 scene.nodes.push_back(node.GetUint());
1288 }
1289 break;
1290 }
1291 case Cogs::hash("extensions"):
1292 {
1293 if (!field.value.IsObject()) {
1294 LOG_ERROR(logger, "%.*s: JSON scene extensions item is not an object", StringViewFormat(path));
1295 return false;
1296 }
1297
1298 // Any extension declared anywhere in the json file will be hoisted and placed on the arrays
1299 // extensionsUsed and extensionsRequired and handled acordingly when reading those.
1300 break;
1301 }
1302
1303 case Cogs::hash("extras"): {
1304 // extras can have ANY type but usually appears as key/value pairs
1305 //https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras
1306
1307 auto type = field.value.GetType();
1308
1309 LOG_WARNING_ONCE(logger, "Unhandled application-specific node under scenes node: Extras[rapidjson::Type: %u]", type );
1310 break;
1311 }
1312 default:
1313 LOG_WARNING_ONCE(logger, "%.*s: JSON unknown scene field: %s", StringViewFormat(path), field.name.GetString());
1314 break;
1315 }
1316 }
1317 }
1318 return true;
1319 }
1320
1321 [[nodiscard]] bool parseNodes(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& nodesValue)
1322 {
1323 if (!nodesValue.IsArray()) {
1324 LOG_ERROR(logger, "%.*s: JSON nodes member is not an array value", StringViewFormat(path));
1325 return false;
1326 }
1327
1328 const auto nodeValues = nodesValue.GetArray();
1329 const uint32_t numNodes = nodeValues.Size();
1330 loadData.nodes.resize(numNodes);
1331
1332 for (uint32_t i = 0; i < numNodes; ++i) {
1333 GltfNode& node = loadData.nodes[i];
1334 node.node_index = i;
1335
1336 bool use_matrix_transform = false;
1337
1338 const Value& nodeValue = nodeValues[i];
1339 if (!nodeValue.IsObject()) {
1340 LOG_ERROR(logger, "%.*s: JSON nodes array item not an object", StringViewFormat(path));
1341 return false;
1342 }
1343
1344 for (const auto& field : nodeValue.GetObject()) {
1345 switch (toView(field.name).hash()) {
1346 case Cogs::hash("name"):
1347 if (!field.value.IsString()) {
1348 LOG_ERROR(logger, "%.*s: JSON node name is not a string", StringViewFormat(path));
1349 return false;
1350 }
1351 node.name = field.value.GetString();
1352 break;
1353
1354 case Cogs::hash("children"):
1355 if (!field.value.IsArray()) {
1356 LOG_ERROR(logger, "%.*s: JSON node children is not an array", StringViewFormat(path));
1357 return false;
1358 }
1359 for (const auto& child : field.value.GetArray()) {
1360 if (!child.IsUint()) {
1361 LOG_ERROR(logger, "%.*s: JSON children array element is not an uint", StringViewFormat(path));
1362 return false;
1363 }
1364 uint32_t childIndex = child.GetUint();
1365 node.children.push_back(childIndex);
1366 loadData.nodes[childIndex].parent_node = i;
1367 }
1368 break;
1369
1370 case Cogs::hash("rotation"):
1371 if (field.value.IsArray() && field.value.GetArray().Size() == 4) {
1372 auto arr = field.value.GetArray();
1373 node.rot = glm::quat(arr[3].GetFloat(), arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat());
1374 }
1375 else {
1376 LOG_ERROR(logger, "%.*s: JSON node rotation value is not an array with 4 elements", StringViewFormat(path));
1377 return false;
1378 }
1379 break;
1380
1381 case Cogs::hash("translation"):
1382 if (field.value.IsArray() && field.value.GetArray().Size() == 3) {
1383 auto arr = field.value.GetArray();
1384 node.pos = glm::vec3(arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat());
1385 }
1386 else {
1387 LOG_ERROR(logger, "%.*s: JSON node translation value is not an array with 3 elements", StringViewFormat(path));
1388 return false;
1389 }
1390 break;
1391
1392 case Cogs::hash("scale"):
1393 if (field.value.IsArray() && field.value.GetArray().Size() == 3) {
1394 auto arr = field.value.GetArray();
1395 node.scale = glm::vec3(arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat());
1396 }
1397 else {
1398 LOG_ERROR(logger, "%.*s: JSON node scale value is not an array with 3 elements", StringViewFormat(path));
1399 return false;
1400 }
1401 break;
1402
1403 case Cogs::hash("matrix"):
1404 if (field.value.IsArray() && field.value.GetArray().Size() == 16) {
1405 auto arr = field.value.GetArray();
1406 node.mat = glm::mat4(arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat(), arr[3].GetFloat(),
1407 arr[4].GetFloat(), arr[5].GetFloat(), arr[6].GetFloat(), arr[7].GetFloat(),
1408 arr[8].GetFloat(), arr[9].GetFloat(), arr[10].GetFloat(), arr[11].GetFloat(),
1409 arr[12].GetFloat(), arr[13].GetFloat(), arr[14].GetFloat(), arr[15].GetFloat());
1410 glm::vec3 skew;
1411 glm::vec4 perspective;
1412 glm::decompose(node.mat, node.scale, node.rot, node.pos, skew, perspective);
1413 use_matrix_transform = true;
1414 }
1415 else {
1416 LOG_ERROR(logger, "%.*s: JSON node matrix value is not an array with 16 elements", StringViewFormat(path));
1417 return false;
1418 }
1419 break;
1420
1421 case Cogs::hash("mesh"):
1422 if (field.value.IsUint()) {
1423 node.mesh = field.value.GetUint();
1424 }
1425 else {
1426 LOG_ERROR(logger, "%.*s: JSON node mesh value is not an uint", StringViewFormat(path));
1427 return false;
1428 }
1429 break;
1430
1431 case Cogs::hash("skin"):
1432 if (field.value.IsUint()) {
1433 node.skin = field.value.GetUint();
1434 }
1435 else {
1436 LOG_ERROR(logger, "%.*s: JSON node skin value is not an uint", StringViewFormat(path));
1437 return false;
1438 }
1439 break;
1440
1441 case Cogs::hash("camera"):
1442 // Ignore
1443 break;
1444 default:
1445 LOG_DEBUG(logger, "Unknown node field: %s", field.name.GetString());
1446 break;
1447 }
1448 }
1449
1450 if (!use_matrix_transform) {
1451 glm::mat4 ii(1.0f);
1452 node.mat = glm::translate(ii, node.pos) * glm::mat4_cast(node.rot) * glm::scale(ii, node.scale);
1453 }
1454 }
1455 return true;
1456 }
1457
1458 // There is an issue when an accessor-type is FLOAT but the BBox values are all integers (or the other way around).
1459 // The glTF spec requires accessor-type and bbox values-type to match, but this is not always the case...
1460 // We'll therefore just follow the actual type in the JSON document instead of assuming and cast to whatever the
1461 // accessor-type is.
1462 void parseAccessorMinArray(GltfAccessor & accessor, Array arr)
1463 {
1464 accessor.minCount = std::min(16u, arr.Size());
1465 for (uint32_t i = 0; i < accessor.minCount; i++) {
1466 if (arr[i].IsUint() || arr[i].IsInt()) {
1467 if (accessor.componentType == AccessorComponentType::Float) {
1468 accessor.min.s_float[i] = float(arr[i].GetInt());
1469 }
1470 else {
1471 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1472 accessor.componentType == AccessorComponentType::UnsignedShort ||
1473 accessor.componentType == AccessorComponentType::UnsignedInt) {
1474 accessor.min.s_uint[i] = arr[i].GetUint();
1475 }
1476 else {
1477 accessor.min.s_int[i] = arr[i].GetInt();
1478 }
1479 }
1480 }
1481 else {
1482 if (accessor.componentType == AccessorComponentType::Float) {
1483 accessor.min.s_float[i] = arr[i].GetFloat();
1484 }
1485 else {
1486 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1487 accessor.componentType == AccessorComponentType::UnsignedShort ||
1488 accessor.componentType == AccessorComponentType::UnsignedInt) {
1489 accessor.min.s_uint[i] = uint32_t(std::floor(arr[i].GetFloat()));
1490 }
1491 else {
1492 accessor.min.s_int[i] = int32_t(std::floor(arr[i].GetFloat()));
1493 }
1494 }
1495 }
1496 }
1497 }
1498
1499 // See comment above "parseAccessorMinArray()".
1500 void parseAccessorMaxArray(GltfAccessor& accessor, Array arr)
1501 {
1502 accessor.maxCount = std::min(16u, arr.Size());
1503 for (uint32_t i = 0; i < accessor.maxCount; i++) {
1504 if (arr[i].IsInt() || arr[i].IsUint()) {
1505 if (accessor.componentType == AccessorComponentType::Float) {
1506 accessor.max.s_float[i] = float(arr[i].GetInt());
1507 }
1508 else {
1509 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1510 accessor.componentType == AccessorComponentType::UnsignedShort ||
1511 accessor.componentType == AccessorComponentType::UnsignedInt) {
1512 accessor.max.s_uint[i] = arr[i].GetUint();
1513 }
1514 else {
1515 accessor.max.s_int[i] = arr[i].GetInt();
1516 }
1517 }
1518 }
1519 else {
1520 if (accessor.componentType == AccessorComponentType::Float) {
1521 accessor.max.s_float[i] = arr[i].GetFloat();
1522 }
1523 else {
1524 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1525 accessor.componentType == AccessorComponentType::UnsignedShort ||
1526 accessor.componentType == AccessorComponentType::UnsignedInt) {
1527 accessor.max.s_uint[i] = uint32_t(std::ceil(arr[i].GetFloat()));
1528 }
1529 else {
1530 accessor.max.s_int[i] = int32_t(std::ceil(arr[i].GetFloat()));
1531 }
1532 }
1533 }
1534 }
1535 }
1536
1537 [[nodiscard]] bool parseAccessors(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& accessorsValue)
1538 {
1539 assert(accessorsValue.IsArray());
1540 for (const auto& accessorValue : accessorsValue.GetArray()) {
1541 if (!checkIsObject(loadData, "accessor array item", accessorValue)) return false;
1542
1543 GltfAccessor& accessor = loadData.accessors.emplace_back();
1544 for (const auto& field : accessorValue.GetObject()) {
1545 switch (toView(field.name).hash()) {
1546 case Cogs::hash("bufferView"):
1547 if (!getUint(loadData, accessor.bufferView, field)) return false;
1548 break;
1549
1550 case Cogs::hash("byteOffset"):
1551 if (!getUint(loadData, accessor.byteOffset, field)) return false;
1552 break;
1553
1554 case Cogs::hash("componentType"):
1555 if (checkIsUint(loadData, field)) {
1556 switch (field.value.GetUint()) {
1557 case GLTF_BYTE: accessor.componentType = AccessorComponentType::Byte; break;
1558 case GLTF_UNSIGNED_BYTE: accessor.componentType = AccessorComponentType::UnsignedByte; break;
1559 case GLTF_SHORT: accessor.componentType = AccessorComponentType::Short; break;
1560 case GLTF_UNSIGNED_SHORT: accessor.componentType = AccessorComponentType::UnsignedShort; break;
1561 case GLTF_UNSIGNED_INT: accessor.componentType = AccessorComponentType::UnsignedInt; break;
1562 case GLTF_FLOAT: accessor.componentType = AccessorComponentType::Float; break;
1563 default:
1564 LOG_ERROR(logger, "%.*s:n Unrecognized accessor componentType %u", StringViewFormat(path), field.value.GetUint());
1565 return false;
1566 }
1567 }
1568 else return false;
1569 break;
1570
1571 case Cogs::hash("count"):
1572 if (!getUint(loadData, accessor.count, field)) return false;
1573 break;
1574
1575 case Cogs::hash("type"):
1576 if (checkIsString(loadData, field)) {
1577 switch (toView(field.value).hash()) {
1578 case Cogs::hash("SCALAR"): accessor.type = AccessorType::Scalar; break;
1579 case Cogs::hash("VEC2"): accessor.type = AccessorType::Vec2; break;
1580 case Cogs::hash("VEC3"): accessor.type = AccessorType::Vec3; break;
1581 case Cogs::hash("VEC4"): accessor.type = AccessorType::Vec4; break;
1582 case Cogs::hash("MAT2"): accessor.type = AccessorType::Mat2; break;
1583 case Cogs::hash("MAT3"): accessor.type = AccessorType::Mat3; break;
1584 case Cogs::hash("MAT4"): accessor.type = AccessorType::Mat4; break;
1585 default:
1586 LOG_ERROR(logger, "%.*s: Unrecognized accessor type '%s'", StringViewFormat(path), field.value.GetString());
1587 return false;
1588 }
1589 }
1590 else return false;
1591 break;
1592
1593 case Cogs::hash("min"):
1594 if (checkIsArray(loadData, field)) {
1595 assert(field.value.IsArray());
1596 parseAccessorMinArray(accessor, field.value.GetArray());
1597 }
1598 else return false;
1599 break;
1600
1601 case Cogs::hash("max"):
1602 if (checkIsArray(loadData, field)) {
1603 assert(field.value.IsArray());
1604 parseAccessorMaxArray(accessor, field.value.GetArray());
1605 }
1606 else {
1607 return false;
1608 }
1609 break;
1610
1611 case Cogs::hash("normalized"):
1612 if (!getBool(loadData, accessor.normalized, field)) return false;
1613 break;
1614
1615 case Cogs::hash("name"):
1616 // TODO
1617 break;
1618
1619 default:
1620 LOG_WARNING_ONCE(logger, "%.*s: Unknown accessor key '%s'", StringViewFormat(path), field.name.GetString());
1621 break;
1622 }
1623 }
1624 }
1625 return true;
1626 }
1627
1628 [[nodiscard]] bool parseSkins(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& skinsValue)
1629 {
1630 if (!skinsValue.IsArray()) {
1631 LOG_ERROR(logger, "%.*s: Skins is not an array", StringViewFormat(path));
1632 return false;
1633 }
1634 for (auto& skin_value : skinsValue.GetArray()) {
1635 auto& skin = loadData.skins.emplace_back();
1636
1637 for (const auto& field : skin_value.GetObject()) {
1638 if (field.name == "inverseBindMatrices")
1639 skin.inverseBindMatrices = field.value.GetUint();
1640 else if (field.name == "joints") {
1641 for (auto& joint : field.value.GetArray()) skin.joints.push_back(joint.GetUint());
1642 }
1643 else if (field.name == "skeleton")
1644 skin.skeleton = field.value.GetUint();
1645 else if (field.name == "name")
1646 skin.name = field.value.GetString();
1647 else {
1648 LOG_WARNING_ONCE(logger, "Unknown skin field: %s", field.name.GetString());
1649 }
1650 }
1651 }
1652 return true;
1653 }
1654
1655 [[nodiscard]] bool parseAnimations(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& animationsValue)
1656 {
1657 if (!animationsValue.IsArray()) {
1658 LOG_ERROR(logger, "%.*s: Animations is not an array", StringViewFormat(path));
1659 return false;
1660 }
1661 for (auto& animation_value : animationsValue.GetArray()) {
1662 auto& animation = loadData.animations.emplace_back();
1663
1664 for (const auto& field : animation_value.GetObject()) {
1665 if (field.name == "channels") {
1666 for (auto& channel_value : field.value.GetArray()) {
1667 auto& channel = animation.channels.emplace_back();
1668
1669 for (auto& channel_field : channel_value.GetObject()) {
1670 if (channel_field.name == "sampler")
1671 channel.sampler = channel_field.value.GetUint();
1672 else if (channel_field.name == "target") {
1673 for (auto& target : channel_field.value.GetObject()) {
1674 if (target.name == "node")
1675 channel.node = target.value.GetUint();
1676 else if (target.name == "path")
1677 channel.path = target.value.GetString();
1678 else {
1679 LOG_WARNING_ONCE(logger, "Unknown animation channel target field: %s", target.name.GetString());
1680 }
1681 }
1682 }
1683 else {
1684 LOG_WARNING_ONCE(logger, "Unknown animation channel field: %s", channel_field.name.GetString());
1685 }
1686 }
1687 }
1688 }
1689 else if (field.name == "samplers") {
1690 for (auto& sampler_value : field.value.GetArray()) {
1691 auto& sampler = animation.samplers.emplace_back();
1692
1693 for (auto& sampler_field : sampler_value.GetObject()) {
1694 if (sampler_field.name == "input")
1695 sampler.input = sampler_field.value.GetUint();
1696 else if (sampler_field.name == "interpolation")
1697 sampler.interpolation = sampler_field.value.GetString();
1698 else if (sampler_field.name == "output")
1699 sampler.output = sampler_field.value.GetUint();
1700 else {
1701 LOG_WARNING_ONCE(logger, "Unknown animation sampler field: %s", sampler_field.name.GetString());
1702 }
1703 }
1704 }
1705 }
1706 else if (field.name == "name") {
1707 animation.name = field.value.GetString();
1708 }
1709 else {
1710 LOG_WARNING_ONCE(logger, "Unknown animation field: %s", field.name.GetString());
1711 }
1712 }
1713 }
1714
1715 return true;
1716 }
1717
1718 uint32_t getAccessorTypeSize(const GltfAccessor& accessor)
1719 {
1720 assert(size_t(accessor.componentType) < size_t(AccessorComponentType::Count));
1721 assert(size_t(accessor.type) < size_t(AccessorType::Count));
1722 return accessorTypeSize[size_t(accessor.type)] * accessorComponentTypeSize[size_t(accessor.componentType)];
1723 }
1724
1725 [[nodiscard]] bool getBufferViewData(const GltfModelDefinition& loadData, BufferDataView& dataView, const GltfAccessor& accessor)
1726 {
1727 if (loadData.buffer_views.size() <= accessor.bufferView) {
1728 LOG_ERROR(logger, "%.*s: Illegal accessor buffer view index %u", StringViewFormat(loadData.path), accessor.bufferView);
1729 return false;
1730 }
1731 const GltfBufferView& bufferView = loadData.buffer_views[accessor.bufferView];
1732
1733 if (loadData.buffer_data.size() <= bufferView.buffer) {
1734 LOG_ERROR(logger, "%.*s: Illegal buffer view buffer index %u", StringViewFormat(loadData.path), bufferView.buffer);
1735 return false;
1736 }
1737 std::span<const uint8_t> buffer = loadData.buffer_data[bufferView.buffer];
1738
1739 size_t byteOffset = size_t(accessor.byteOffset) + bufferView.byteOffset;
1740 uint32_t stride = bufferView.byteStride != 0u ? bufferView.byteStride : getAccessorTypeSize(accessor);
1741
1742 if (buffer.size() < byteOffset + size_t(stride) * accessor.count) {
1743 LOG_ERROR(logger, "%.*s: Accessor references outside of buffer (bufferSize=%zd, idx=%zd)",
1744 StringViewFormat(loadData.path), buffer.size(), byteOffset + size_t(stride) * accessor.count);
1745 return false;
1746 }
1747
1748 dataView = {
1749 .data = buffer.data() + byteOffset,
1750 .stride = stride,
1751 .count = accessor.count
1752 };
1753
1754 return true;
1755 }
1756
1757 bool loadBone(GltfModelDefinition& definition,
1758 GltfNode& node,
1759 Skeleton& skeleton,
1760 size_t parent_bone,
1761 const glm::mat4& parent_transform)
1762 {
1763 const glm::mat4& localTransform = node.mat;
1764 const glm::mat4 globalTransform = parent_transform * localTransform;
1765
1766 uint32_t boneIndex = (uint32_t)skeleton.bones.size();
1767 node.bone_index = boneIndex;
1768
1769 auto& bone = skeleton.bones.emplace_back();
1770 bone.name = std::to_string(node.node_index) + "_" + node.name;
1771 bone.parentBone = parent_bone;
1772 bone.pos = node.pos;
1773 bone.scale = node.scale;
1774 bone.rot = node.rot;
1775 bone.relative = localTransform;
1776 bone.absolute = globalTransform;
1777 bone.inverseBindPose = glm::mat4(1.0);
1778 bone.hasOffset = false;
1779 bone.used = false;
1780 bone.animated = false;
1781
1782 skeleton.bonesByName[bone.name] = boneIndex;
1783
1784 for (uint32_t childIndex : node.children) {
1785 if (!loadBone(definition, definition.nodes[childIndex], skeleton, boneIndex, globalTransform)) {
1786 return false;
1787 }
1788 }
1789
1790 return true;
1791 }
1792
1793 bool loadJoints(GltfModelDefinition& definition,
1794 GltfNode& node,
1795 Skeleton& skeleton,
1796 MeshoptDecompressor& meshoptDecomp)
1797 {
1798 for (uint32_t childIndex : node.children) {
1799 if (!loadJoints(definition, definition.nodes[childIndex], skeleton, meshoptDecomp)) {
1800 return false;
1801 }
1802 }
1803
1804 if (node.skin != (uint32_t)-1) {
1805 skeleton.bindPose = glm::inverse(skeleton.bones[node.bone_index].absolute);
1806 GltfSkin& skin = definition.skins[node.skin];
1807
1808 if (definition.accessors.size() <= size_t(skin.inverseBindMatrices)) {
1809 return false;
1810 }
1811
1812 size_t accessorIdx = size_t(skin.inverseBindMatrices);
1813 GltfAccessor accessor = definition.accessors[accessorIdx];
1814
1815 // Meshopt compressed?
1816 if (meshoptDecomp.isCompressed(definition, int(accessorIdx))) {
1817 bool ok = meshoptDecomp.decompress(definition, int(accessorIdx));
1818 if (!ok) {
1819 return false;
1820 }
1821 }
1822
1823 BufferDataView dataView;
1824 if (!getBufferViewData(definition, dataView, accessor)) {
1825 return false;
1826 }
1827
1828 size_t i = 0;
1829 for (uint32_t jointIndex : skin.joints) {
1830 auto& joint = skeleton.bones[definition.nodes[jointIndex].bone_index];
1831 joint.inverseBindPose = *reinterpret_cast<const glm::mat4*>(dataView.data + dataView.stride * i++);
1832 joint.hasOffset = true;
1833 joint.used = true;
1834 }
1835 }
1836
1837 return true;
1838 }
1839
1840 [[nodiscard]] bool getVertexElement(GltfModelDefinition& loadData, VertexStreamsState& streamsState, VertexStream& vertexStream, const Cogs::StringView& key, const uint32_t accessorIx)
1841 {
1842 if (loadData.accessors.size() <= accessorIx) {
1843 LOG_ERROR(logger, "%.*s: Illegal access index %u", StringViewFormat(loadData.path), accessorIx);
1844 return false;
1845 }
1846 const GltfAccessor& accessor = loadData.accessors[accessorIx];
1847
1848 if (!getBufferViewData(loadData, vertexStream.dataView, accessor)) {
1849 return false;
1850 }
1851
1852 // Non-indexed attributes
1853 // ----------------------
1854
1855 switch (key.hash()) {
1856 case Cogs::hash("POSITION"):
1857 if (accessor.type == AccessorType::Vec3 &&
1858 (accessor.componentType == AccessorComponentType::Float ||
1859 accessor.componentType == AccessorComponentType::Byte ||
1860 accessor.componentType == AccessorComponentType::Short ||
1861 accessor.componentType == AccessorComponentType::UnsignedShort)) {
1862 streamsState.positionData = vertexStream.dataView;
1863 streamsState.positionPresent = true;
1864 streamsState.positionCount = accessor.count;
1865 vertexStream.semantic = Cogs::ElementSemantic::Position;
1866 vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT;
1867
1868 if (accessor.componentType != AccessorComponentType::Float) {
1869 // The standardshader material only accepts Float3 vertices. We'll therefore convert the data to keep things simple.
1870 loadData.positionsScratch.push_back(Memory::MemoryBuffer());
1871 vertexStream.dataView = convertVecBufferToFloats<glm::vec3>(vertexStream.dataView, accessor.componentType, &loadData.positionsScratch.back());
1872 }
1873 streamsState.positionData = vertexStream.dataView;
1874
1875 if (accessor.minCount == 3 && accessor.maxCount == 3) {
1876 if (accessor.componentType == AccessorComponentType::Float) {
1877 streamsState.bbox.min = glm::make_vec3(accessor.min.s_float);
1878 streamsState.bbox.max = glm::make_vec3(accessor.max.s_float);
1879 }
1880 else if (accessor.componentType == AccessorComponentType::UnsignedShort) {
1881 streamsState.bbox.min = glm::make_vec3(accessor.min.s_uint);
1882 streamsState.bbox.max = glm::make_vec3(accessor.max.s_uint);
1883 }
1884 else {
1885 streamsState.bbox.min = glm::make_vec3(accessor.min.s_int);
1886 streamsState.bbox.max = glm::make_vec3(accessor.max.s_int);
1887 }
1888 }
1889 else {
1890 LOG_WARNING_ONCE(logger, "%.*s: POSITION attribute has missing or malformed min and max values", StringViewFormat(loadData.path));
1891 streamsState.bbox.min = glm::vec3(std::numeric_limits<float>::max());
1892 streamsState.bbox.max = -streamsState.bbox.min;
1893 for (size_t i = 0; i < streamsState.positionCount; i++) {
1894 const glm::vec3& p = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * i);
1895 if (std::isfinite(p.x) && std::isfinite(p.y) && std::isfinite(p.z)) {
1896 streamsState.bbox.min = min(streamsState.bbox.min, p);
1897 streamsState.bbox.max = max(streamsState.bbox.max, p);
1898 }
1899 }
1900 }
1901
1902 return true;
1903 }
1904 LOG_ERROR(logger, "%.*s: Illegal POSITION accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
1905 return false;
1906
1907 case Cogs::hash("NORMAL"):
1908 if (accessor.type == AccessorType::Vec3 &&
1909 (accessor.componentType == AccessorComponentType::Float ||
1910 accessor.componentType == AccessorComponentType::Byte ||
1911 accessor.componentType == AccessorComponentType::Short)) {
1912 if (accessor.componentType != AccessorComponentType::Float) {
1913 // The standardshader material only accepts Float3 normals. We'll therefore convert the data to keep things simple.
1914 loadData.normalsScratch.push_back(Memory::MemoryBuffer());
1915 vertexStream.dataView = convertVecBufferToFloats<glm::vec3>(vertexStream.dataView, accessor.componentType, &loadData.normalsScratch.back());
1916 }
1917
1918 streamsState.normalData = vertexStream.dataView;
1919 streamsState.normalPresent = true;
1920 vertexStream.semantic = Cogs::ElementSemantic::Normal;
1921 vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT;
1922
1923 return true;
1924 }
1925 LOG_ERROR(logger, "%.*s: Illegal NORMAL accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
1926 return false;
1927
1928 case Cogs::hash("TANGENT"):
1929 if (accessor.type == AccessorType::Vec4 &&
1930 (accessor.componentType == AccessorComponentType::Float ||
1931 accessor.componentType == AccessorComponentType::Byte ||
1932 accessor.componentType == AccessorComponentType::Short)) {
1933 streamsState.tangentPresent = true;
1934 vertexStream.semantic = Cogs::ElementSemantic::Tangent;
1935 if (accessor.componentType != AccessorComponentType::Float) {
1936 // The standardshader material only accepts Float4 tangents. We'll therefore convert the data to keep things simple.
1937 loadData.tangentsScratch.push_back(Memory::MemoryBuffer());
1938 vertexStream.dataView = convertVecBufferToFloats<glm::vec4>(vertexStream.dataView, accessor.componentType, &loadData.tangentsScratch.back());
1939 }
1940 vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT;
1941
1942 return true;
1943 }
1944 LOG_ERROR(logger, "%.*s: Illegal TANGENT accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
1945 return false;
1946
1947 default:
1948
1949 // Indexed attributes
1950 // ------------------
1951
1952 if (size_t underscore = key.find_first_of('_');
1953 underscore != Cogs::StringView::NoPosition && // There is an underscore
1954 underscore + 1 < key.length() && // There is at least one character after the underscore
1955 '0' <= key[underscore + 1] && // The character is an digit
1956 key[underscore + 1] <= '9')
1957 {
1958 vertexStream.semanticIndex = 0;
1959 size_t digit = underscore + 1;
1960 for (; digit < key.size() && '0' <= key[digit] && key[digit] <= '9'; digit++) {
1961 vertexStream.semanticIndex = 10 * vertexStream.semanticIndex + uint32_t(key[digit] - '0');
1962 }
1963
1964 if (digit == key.length()) { // no garbage after last digit
1965 switch (key.substr(0, underscore).hash()) {
1966 case Cogs::hash("TEXCOORD"):
1967 if (vertexStream.semanticIndex == 0) {
1968 if (accessor.componentType != AccessorComponentType::Float) {
1969 // The standard-shader material only accepts Float2 texcoords. We'll therefore convert the data to keep things simple.
1970 loadData.texCoordsScratch.push_back(Memory::MemoryBuffer());
1971 vertexStream.dataView = convertVecBufferToFloats<glm::vec2>(vertexStream.dataView, accessor.componentType, &loadData.texCoordsScratch.back());
1972 }
1973 }
1974
1975 // Pre-calc texture coord transforms from a KHR_texture_transform setting?
1976 if (loadData.textureTransforms.size()) {
1977 const TextureTransform& textureTransform = loadData.textureTransforms.back(); // Always use the LAST texture-transform registered.
1978 float* buf = reinterpret_cast<float*>(const_cast<uint8_t*>(vertexStream.dataView.data));
1979 size_t num = vertexStream.dataView.count;
1980 const glm::mat3 translation = glm::mat3(1.0, 0, 0, 0, 1.0, 0, textureTransform.offset.x, textureTransform.offset.y, 1.0);
1981 const glm::mat3 rotation = glm::mat3(cos(textureTransform.rotation), sin(textureTransform.rotation), 0,
1982 -sin(textureTransform.rotation), cos(textureTransform.rotation), 0,
1983 0, 0, 1.0);
1984 const glm::mat3 scale = glm::mat3(textureTransform.scale.x, 0, 0, 0, textureTransform.scale.y, 0, 0, 0, 1.0);
1985 const glm::mat3 xform = translation * rotation * scale;
1986
1987 for (size_t i = 0; i < num; ++i) {
1988 const glm::vec3 result = xform * glm::vec3(buf[i * 2], buf[i * 2 + 1], 1.0);
1989 buf[i * 2] = result.x;
1990 buf[i * 2 + 1] = result.y;
1991 }
1992 }
1993
1994 streamsState.texcoordData = vertexStream.dataView;
1995 streamsState.texcoordStreamCount++;
1996 vertexStream.semantic = Cogs::ElementSemantic::TextureCoordinate;
1997 vertexStream.format = Cogs::DataFormat::R32G32_FLOAT;
1998 return true;
1999
2000 case Cogs::hash("COLOR"):
2001 streamsState.colorStreamCount++;
2002 vertexStream.semantic = Cogs::ElementSemantic::Color;
2003 if (accessor.type == AccessorType::Vec3) {
2004 switch (accessor.componentType) {
2005 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT; return true;
2006 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8_UNORM; return true;
2007 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16_UNORM; return true;
2008 default: break;
2009 }
2010 }
2011 else if (accessor.type == AccessorType::Vec4) {
2012 switch (accessor.componentType) {
2013 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32A32_FLOAT; return true;
2014 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8A8_UNORM; return true;
2015 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16A16_UNORM; return true;
2016 default: break;
2017 }
2018 }
2019 LOG_ERROR(logger, "%.*s: Illegal COLOR accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
2020 return false;
2021
2022 case Cogs::hash("JOINTS"):
2023 if (vertexStream.semanticIndex == 0) { // We only support one set of joints, at least for now
2024 streamsState.jointsStreamCount = 1;
2025 vertexStream.semantic = Cogs::ElementSemantic::Color;
2026 vertexStream.semanticIndex = 4;
2027 if (accessor.type == AccessorType::Vec4) {
2028 switch (accessor.componentType) {
2029 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8A8_UINT; return true;
2030 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16A16_UINT; return true;
2031 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32A32_FLOAT; return true;
2032 default: break;
2033 }
2034 }
2035 LOG_ERROR(logger, "%.*s: Illegal JOINTS accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
2036 return false;
2037 }
2038 break;
2039
2040 case Cogs::hash("WEIGHTS"):
2041 if (vertexStream.semanticIndex == 0) { // We only support one set of weights, at least for now
2042 vertexStream.semantic = Cogs::ElementSemantic::Color;
2043 vertexStream.semanticIndex = 5;
2044 if (accessor.type == AccessorType::Vec4) {
2045 switch (accessor.componentType) {
2046 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32A32_FLOAT; return true;
2047 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8A8_UNORM; return true;
2048 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16A16_UNORM; return true;
2049 default: break;
2050 }
2051 }
2052 LOG_ERROR(logger, "%.*s: Illegal WEIGHTS accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
2053 return false;
2054 }
2055 break;
2056
2057 default:
2058 break;
2059 }
2060 }
2061 }
2062 break;
2063 }
2064
2065 LOG_WARNING_ONCE(logger, "Unrecognized attribute semantic %.*s", StringViewFormat(key));
2066 return false;
2067 }
2068
2069 [[nodiscard]] bool getVertexAttributes(GltfModelDefinition& loadData, VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams, const Object& attributeObject, MeshoptDecompressor& meshoptDecomp)
2070 {
2071 for (const auto& attributeIt : attributeObject) {
2072 // Retrieve accessor
2073 uint32_t accessorIx = 0;
2074 if (!getUint(loadData, accessorIx, attributeIt)) {
2075 return false;
2076 }
2077
2078 // Meshopt compressed attribute?
2079 if (meshoptDecomp.isCompressed(loadData, accessorIx)) {
2080 meshoptDecomp.decompress(loadData, accessorIx);
2081 }
2082
2083 // Retrieve vertex specification
2084 VertexStream vertexStream;
2085 if (getVertexElement(loadData, streamsState, vertexStream, toView(attributeIt.name), accessorIx)) {
2086 vertexStreams.push_back(vertexStream);
2087 }
2088 }
2089
2090 if (!streamsState.positionPresent) {
2091 LOG_ERROR(logger, "%.*s: Primitive has no POSITION attribute", StringViewFormat(loadData.path));
2092 return false;
2093 }
2094
2095 for (const auto& stream : vertexStreams) {
2096 if (stream.dataView.count != streamsState.positionCount) {
2097 LOG_ERROR(logger, "%.*s: Primitive has attributes with variable counts", StringViewFormat(loadData.path));
2098 return false;
2099 }
2100 }
2101
2102 return true;
2103 }
2104
2105 [[nodiscard]] bool generateVertexTangents(GltfModelDefinition& loadData, VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams, Cogs::PrimitiveType::EPrimitiveType primitiveType, bool clockwise)
2106 {
2107 if (primitiveType != Cogs::PrimitiveType::TriangleList) {
2108 LOG_ERROR(logger, "%.*s: Vertex tangent generator only supports triangle lists (type is %d)", StringViewFormat(loadData.path), primitiveType);
2109 return false;
2110 }
2111 assert(streamsState.positionPresent);
2112 assert(streamsState.normalPresent);
2113 assert(streamsState.texcoordStreamCount);
2114
2115 const size_t byteSize = sizeof(glm::vec3) * streamsState.positionCount;
2116
2117 loadData.tangentsScratch.push_back(Memory::MemoryBuffer());
2118 Memory::MemoryBuffer * memBuf = &loadData.tangentsScratch.back();
2119 memBuf->resize(byteSize);
2120 glm::vec3* tangents = (glm::vec3*) memBuf->data();
2121 std::memset(static_cast<void*>(tangents), 0, byteSize);
2122
2123 size_t count = streamsState.indexType != AccessorComponentType::None ? streamsState.indexData.count : streamsState.positionCount;
2124 for (size_t i = 0; i + 2 < count; i += 3) {
2125 size_t ix[3] = { 0,0,0 };
2126 switch (streamsState.indexType) {
2127 case AccessorComponentType::None: for (size_t k = 0; k < 3; k++) { ix[k] = i + k; } break;
2128 case AccessorComponentType::UnsignedByte: for (size_t k = 0; k < 3; k++) { ix[k] = *(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2129 case AccessorComponentType::UnsignedShort: for (size_t k = 0; k < 3; k++) { ix[k] = *(const uint16_t*)(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2130 case AccessorComponentType::UnsignedInt: for (size_t k = 0; k < 3; k++) { ix[k] = *(const uint32_t*)(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2131 default: assert(false); return false;
2132 }
2133
2134 glm::vec3 a = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[0]);
2135 glm::vec3 b = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[1]);
2136 glm::vec3 c = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[2]);
2137 glm::vec2 ta = *(const glm::vec2*)(streamsState.texcoordData.data + streamsState.texcoordData.stride * ix[0]);
2138 glm::vec2 tb = *(const glm::vec2*)(streamsState.texcoordData.data + streamsState.texcoordData.stride * ix[1]);
2139 glm::vec2 tc = *(const glm::vec2*)(streamsState.texcoordData.data + streamsState.texcoordData.stride * ix[2]);
2140
2141 glm::vec2 u0, u1;
2142 glm::vec3 v0, v1;
2143 if (!clockwise) {
2144 u0 = tb - ta;
2145 u1 = tc - ta;
2146 v0 = b - a;
2147 v1 = c - a;
2148 }
2149 else {
2150 u0 = tc - ta;
2151 u1 = tb - ta;
2152 v0 = c - a;
2153 v1 = b - a;
2154 }
2155
2156 float den = 1.f / (u0.x * u1.y - u0.y * u1.x);
2157 glm::vec3 t0 = u1.y * den * v0 - u0.y * den * v1;
2158
2159 tangents[ix[0]] += t0;
2160 tangents[ix[1]] += t0;
2161 tangents[ix[2]] += t0;
2162 }
2163
2164 for (size_t i = 0; i < streamsState.positionCount; i++) {
2165 const glm::vec3 normal = *(const glm::vec3*)(streamsState.normalData.data + streamsState.normalData.stride * i);
2166 tangents[i] = glm::normalize(tangents[i] - glm::dot(normal, tangents[i]) * normal);
2167 }
2168
2169 vertexStreams.push_back(VertexStream{
2170 .dataView = {
2171 .data = (const uint8_t*) memBuf->data(),
2172 .stride = sizeof(glm::vec3),
2173 .count = streamsState.positionCount
2174 },
2175 .format = Cogs::DataFormat::R32G32B32_FLOAT,
2177 .semanticIndex = 0
2178 });
2179
2180 return true;
2181 }
2182
2183 [[nodiscard]] bool generateVertexNormals(GltfModelDefinition& loadData, VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams, Cogs::PrimitiveType::EPrimitiveType primitiveType, bool clockwise)
2184 {
2185 if (primitiveType != Cogs::PrimitiveType::TriangleList) {
2186 LOG_ERROR(logger, "%.*s: Vertex normal generator only supports triangle lists", StringViewFormat(loadData.path));
2187 return false;
2188 }
2189 assert(streamsState.positionPresent);
2190
2191 const size_t byteSize = sizeof(glm::vec3) * streamsState.positionCount;
2192 loadData.normalsScratch.push_back(Memory::MemoryBuffer());
2193 Memory::MemoryBuffer* memBuf = &loadData.normalsScratch.back();
2194
2195 memBuf->resize(byteSize);
2196 glm::vec3* normals = (glm::vec3*) memBuf->data();
2197 std::memset(static_cast<void*>(normals), 0, byteSize);
2198
2199 size_t count = streamsState.indexSize ? streamsState.indexData.count : streamsState.positionCount;
2200 for (size_t i = 0; i + 2 < count; i += 3) {
2201 size_t ix[3] = { 0,0,0 };
2202 switch (streamsState.indexSize) {
2203 case 0: for (size_t k = 0; k < 3; k++) { ix[k] = i + k; } break;
2204 case 1: for (size_t k = 0; k < 3; k++) { ix[k] = *(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2205 case 2: for (size_t k = 0; k < 3; k++) { ix[k] = *(const uint16_t*)(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2206 case 4: for (size_t k = 0; k < 3; k++) { ix[k] = *(const uint32_t*)(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2207 default: assert(false); return false;
2208 }
2209 glm::vec3 a = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[0]);
2210 glm::vec3 b = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[1]);
2211 glm::vec3 c = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[2]);
2212
2213 const glm::vec3 l1 = clockwise ? a - b : b - a;
2214 const glm::vec3 l2 = clockwise ? a - c : c - a;
2215 glm::vec3 n = glm::cross(l1, l2);
2216
2217 if (glm::length(n) <= 0.0) {
2218 LOG_WARNING_ONCE(logger, "Calculated normal is not valid (normalIdx=%zu)", i);
2219 n = glm::vec3(1, 0, 0);
2220 }
2221
2222 normals[ix[0]] += n;
2223 normals[ix[1]] += n;
2224 normals[ix[2]] += n;
2225 }
2226
2227 for (size_t i = 0; i < streamsState.positionCount; i++) {
2228 normals[i] = glm::normalize(normals[i]);
2229 }
2230
2231 vertexStreams.push_back(VertexStream{
2232 .dataView = {
2233 .data = (const uint8_t*) normals,
2234 .stride = sizeof(glm::vec3),
2235 .count = streamsState.positionCount
2236 },
2237 .format = Cogs::DataFormat::R32G32B32_FLOAT, // Let the shader add .w=1
2239 .semanticIndex = 0,
2240 });
2241
2242 return true;
2243 }
2244
2245 [[nodiscard]] bool setMeshIndices(GltfModelDefinition& loadData, MeshManager::ResourceProxy& mesh, VertexStreamsState& streamsState)
2246 {
2247 uint32_t dstStride = 0;
2248 switch (streamsState.indexType) {
2249 // NOTE: We'll be using stride=2 for bytes as Cogs.Rendering does not support strides of 1.
2250 case AccessorComponentType::UnsignedByte: dstStride = 2; streamsState.indexSize = 1; break;
2251 case AccessorComponentType::UnsignedShort: dstStride = 2; streamsState.indexSize = dstStride; break;
2252 case AccessorComponentType::UnsignedInt: dstStride = 4; streamsState.indexSize = dstStride; break;
2253 default:
2254 LOG_ERROR(logger, "%.*s: Illegal index data component type %s", StringViewFormat(loadData.path), accessorComponentTypeName[size_t(streamsState.indexType)]);
2255 return false;
2256 }
2257
2258 if (uint8_t* dst = mesh->mapStream(VertexDataType::Indexes, 0, streamsState.indexData.count, dstStride, true); dst) {
2259 switch (streamsState.indexType) {
2260 case AccessorComponentType::UnsignedByte: stridedCopy<1>(dst, 2, streamsState.indexData.data, streamsState.indexData.stride, streamsState.indexData.count); break;
2261 case AccessorComponentType::UnsignedShort: stridedCopy<2>(dst, 2, streamsState.indexData.data, streamsState.indexData.stride, streamsState.indexData.count); break;
2262 case AccessorComponentType::UnsignedInt: stridedCopy<4>(dst, 4, streamsState.indexData.data, streamsState.indexData.stride, streamsState.indexData.count); break;
2263 default: assert(false); break;
2264 }
2265 mesh->unmap(VertexDataType::Indexes);
2266 }
2267 mesh->setMeshFlag(MeshFlags::Indexed);
2268 mesh->setMeshFlag(MeshFlags::IndexesChanged);
2269 mesh->setCount(streamsState.indexData.count);
2270 return true;
2271 }
2272
2273 [[nodiscard]] bool setVertexAttributes(GltfModelDefinition& loadData, MeshManager::ResourceProxy& mesh, const VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams)
2274 {
2275 // Build vertex format
2276 Cogs::VertexFormatHandle vertexFormat;
2277
2278 for (VertexStream& vertexStream : vertexStreams) {
2279 const Cogs::FormatInfo* formatInfo = Cogs::getFormatInfo(vertexStream.format);
2280 assert(formatInfo);
2281 assert(formatInfo->blockSize);
2282 vertexStream.formatSize = formatInfo->blockSize;
2283 }
2284
2285 size_t offset = 0;
2286 std::vector<Cogs::VertexElement> vertexElements;
2287 for (VertexStream& vertexStream : vertexStreams) {
2288 offset = (offset + 3) & ~3;
2289 vertexStream.offset = offset;
2290 vertexElements.push_back(Cogs::VertexElement{
2291 .offset = uint16_t(offset),
2292 .format = vertexStream.format,
2293 .semantic = vertexStream.semantic,
2294 .semanticIndex = uint16_t(vertexStream.semanticIndex),
2295 .inputType = Cogs::InputType::VertexData,
2296 .instanceStep = 0
2297 });
2298 offset += vertexStream.formatSize;
2299 }
2300
2301 vertexFormat = Cogs::VertexFormats::createVertexFormat(vertexElements.data(), vertexElements.size());
2302 if (!HandleIsValid(vertexFormat)) {
2303 LOG_ERROR(logger, "%.*s: Failed to create vertex format with %zu elements", StringViewFormat(loadData.path), vertexElements.size());
2304 return false;
2305 }
2306
2307 // Store data
2308 size_t stride = Cogs::getSize(vertexFormat);
2309 if (uint8_t* base = mesh->mapStream(VertexDataType::Interleaved0, vertexFormat, 0, streamsState.positionCount, stride, true)) {
2310 for (const VertexStream& vertexStream : vertexStreams) {
2311 uint8_t* dst = base + vertexStream.offset;
2312 switch (vertexStream.format) {
2313 case Cogs::DataFormat::R32G32B32A32_FLOAT: // 4*4 = 16
2314 assert(vertexStream.formatSize == 16);
2315 stridedCopy<16>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2316 break;
2317
2318 case Cogs::DataFormat::R32G32B32_FLOAT: // 3*4 = 12
2319 assert(vertexStream.formatSize == 12);
2320 stridedCopy<12>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2321 break;
2322
2323 case Cogs::DataFormat::R32G32_FLOAT: // 2*4 = 8
2324 case Cogs::DataFormat::R16G16B16A16_UNORM: // 4*2 = 8
2325 case Cogs::DataFormat::R16G16B16A16_UINT: // 4*2 = 8
2326 assert(vertexStream.formatSize == 8);
2327 stridedCopy<8>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2328 break;
2329
2330 case Cogs::DataFormat::R16G16B16_UNORM: // 3*2 = 6
2331 case Cogs::DataFormat::R16G16B16_UINT:
2332 case Cogs::DataFormat::R16G16B16_SINT:
2333 case Cogs::DataFormat::R16G16B16_SNORM:
2334 assert(vertexStream.formatSize == 6);
2335 stridedCopy<6>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2336 break;
2337
2338 case Cogs::DataFormat::R16G16_UNORM: // 2*2 = 4
2339 case Cogs::DataFormat::R16G16_UINT:
2340 case Cogs::DataFormat::R16G16_SNORM:
2341 case Cogs::DataFormat::R16G16_SINT:
2342 case Cogs::DataFormat::R8G8B8A8_UNORM: // 4*1 = 4
2343 case Cogs::DataFormat::R8G8B8A8_UINT:
2344 assert(vertexStream.formatSize == 4);
2345 stridedCopy<4>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2346 break;
2347
2348 case Cogs::DataFormat::R8G8B8_UNORM: // 3*1 = 3
2349 case Cogs::DataFormat::R8G8B8_UINT:
2350 case Cogs::DataFormat::R8G8B8_SINT:
2351 case Cogs::DataFormat::R8G8B8_SNORM:
2352 assert(vertexStream.formatSize == 3);
2353 stridedCopy<3>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2354 break;
2355
2356 case Cogs::DataFormat::R8G8_UNORM: // 2*1 = 2
2357 assert(vertexStream.formatSize == 3);
2358 stridedCopy<2>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2359 break;
2360
2361 default:
2362 assert(false && "Unhandled DataFormat");
2363 break;
2364 }
2365 }
2366 mesh->unmap(VertexDataType::Interleaved0);
2367 }
2368 return true;
2369 }
2370
2371 [[nodiscard]] bool buildPrimitiveMesh(GltfModelDefinition& loadData, ModelMeshInstance& modelMeshInstance, Model& model, const Object& primitiveObject, int32_t skinIx, bool clockwise, MeshoptDecompressor & meshoptDecomp)
2372 {
2373 VertexStreamsState streamsState;
2374 std::vector<VertexStream> vertexStreams;
2375
2376 // Check if unlit
2377 bool needsNormals = true;
2378 if (auto materialIt = primitiveObject.FindMember("material"); materialIt != primitiveObject.MemberEnd()) {
2379 uint32_t materialIx = 0;
2380 if (!getUint(loadData, materialIx, *materialIt)) {
2381 LOG_ERROR(logger, "The 'meshes.primitives.material' is not an integer");
2382 return false;
2383 }
2384 if (materialIx >= 0) {
2385 const Object& materialObject = loadData.materialsArray[materialIx];
2386 if (materialObject.HasMember("extensions") && materialObject["extensions"].HasMember("KHR_materials_unlit")) {
2387 needsNormals = false;
2388 }
2389 }
2390 }
2391
2392 // Process attributes
2393 bool useDracoDecompression = false;
2395
2396 // Is this mesh using the DRACO extension?
2397 if (primitiveObject.HasMember("extensions") && primitiveObject["extensions"].HasMember("KHR_draco_mesh_compression")) {
2398 auto extSection = primitiveObject["extensions"].GetObject();
2399 auto dracoSection = extSection["KHR_draco_mesh_compression"].GetObject();
2400 draco.initAttributes(dracoSection);
2401 useDracoDecompression = true;
2402 }
2403
2404 if (const auto& attributesIt = primitiveObject.FindMember("attributes"); attributesIt != primitiveObject.MemberEnd()) {
2405 if (!checkIsObject(loadData, *attributesIt)) {
2406 return false;
2407 }
2408
2409 if (useDracoDecompression) {
2410 if (!draco.decompress(loadData, streamsState, vertexStreams, attributesIt->value.GetObject(), !needsNormals)) {
2411 return false;
2412 }
2413 }
2414 else {
2415 if (!getVertexAttributes(loadData, streamsState, vertexStreams, attributesIt->value.GetObject(), meshoptDecomp)) {
2416 return false;
2417 }
2418 }
2419 }
2420 else {
2421 LOG_ERROR(logger, "%.*s: Primitive has no attribute property", StringViewFormat(loadData.path));
2422 return false;
2423 }
2424
2425 // Process indices
2426 bool isIndexed = false;
2427 uint32_t indicesAccessorIx = 0;
2428
2429 if (useDracoDecompression) {
2430 isIndexed = true; // All index data has been set by the draco-decompressor
2431 }
2432 else {
2433 if (auto indicesIt = primitiveObject.FindMember("indices"); indicesIt != primitiveObject.MemberEnd()) {
2434 if (!getUint(loadData, indicesAccessorIx, *indicesIt)) {
2435 LOG_ERROR(logger, "The 'meshes.primitives.indices' is not an integer");
2436 return false;
2437 }
2438
2439 if (loadData.accessors.size() <= indicesAccessorIx) {
2440 LOG_ERROR(logger, "%.*s: Illegal accessor index %u", StringViewFormat(loadData.path), indicesAccessorIx);
2441 return false;
2442 }
2443
2444 const GltfAccessor& accessor = loadData.accessors[indicesAccessorIx];
2445 streamsState.indexType = accessor.componentType;
2446
2447 if (meshoptDecomp.isCompressed(loadData, indicesAccessorIx)) {
2448 bool ok = meshoptDecomp.decompress(loadData, indicesAccessorIx);
2449 if (!ok) {
2450 return false;
2451 }
2452 }
2453
2454 if (!getBufferViewData(loadData, streamsState.indexData, accessor)) {
2455 return false;
2456 }
2457
2458 isIndexed = true;
2459 }
2460 }
2461
2463 if (auto modeIt = primitiveObject.FindMember("mode"); modeIt != primitiveObject.MemberEnd()) {
2464 uint32_t mode = 0;
2465 if (!getUint(loadData, mode, *modeIt)) {
2466 return false;
2467 }
2468
2469 switch (mode) {
2470 case 0: primitiveType = Cogs::PrimitiveType::PointList; break;
2471 case 1: primitiveType = Cogs::PrimitiveType::LineList; break;
2472 case 3: primitiveType = Cogs::PrimitiveType::LineStrip; break;
2473 case 4: primitiveType = Cogs::PrimitiveType::TriangleList; break;
2474 case 5: primitiveType = Cogs::PrimitiveType::TriangleStrip; break;
2475 case 2: LOG_ERROR(logger, "%.*s: Unsupported primitive mode 2: LINE_LOOP", StringViewFormat(loadData.path)); return false;
2476 case 6: LOG_ERROR(logger, "%.*s: Unsupported primitive mode 6: TRIANGLE_FAN", StringViewFormat(loadData.path)); return false;
2477 default: LOG_ERROR(logger, "%.*s: Illegal primitive mode %u", StringViewFormat(loadData.path), mode); return false;
2478 }
2479 }
2480
2481 // targets
2482 // -------
2483 if (auto targetsIt = primitiveObject.FindMember("targets"); targetsIt != primitiveObject.MemberEnd()) {
2484 if (!checkIsArray(loadData, *targetsIt)) {
2485 return false;
2486 }
2487 if (!targetsIt->value.GetArray().Empty()) {
2488 // Not implemented yet.
2489 LOG_WARNING_ONCE(logger, "%.*s: Morph targets not implemented yet, ignoring.", StringViewFormat(loadData.path));
2490 }
2491 }
2492
2493 // Process materials.
2494 bool isSkinned = streamsState.jointsStreamCount != 0;
2495 bool albedoPerVertex = streamsState.colorStreamCount != 0;
2496 CachedModelMaterial* modelMaterial = nullptr;
2497 if (auto materialIt = primitiveObject.FindMember("material"); materialIt != primitiveObject.MemberEnd()) {
2498 uint32_t materialIx = 0;
2499 if (!getUint(loadData, materialIx, *materialIt)) {
2500 LOG_ERROR(logger, "The 'meshes.primitives.material' is not an integer");
2501 return false;
2502 }
2503 if (!getMaterialHandle(loadData, model, modelMaterial, materialIx, isSkinned, albedoPerVertex, false)) {
2504 return false;
2505 }
2506 }
2507 else {
2508 if (!getMaterialHandle(loadData, model, modelMaterial, -1, isSkinned, albedoPerVertex, false)) {
2509 return false;
2510 }
2511 }
2512 assert(modelMaterial);
2513 modelMeshInstance.materialIx = modelMaterial->modelMaterialIx;
2514
2515 // Handle missing vertex data
2516 if (streamsState.texcoordStreamCount < modelMaterial->texCoordSets) {
2517 LOG_ERROR(logger, "%.*s: Material requires texture coordinates, something which the mesh doesn't have", StringViewFormat(loadData.path));
2518 return false;
2519 }
2520
2521 // Handle missing normals
2522 if (modelMaterial->needsNormals && !streamsState.normalPresent) {
2523 if (!generateVertexNormals(loadData, streamsState, vertexStreams, primitiveType, clockwise)) {
2524 return false;
2525 }
2526 }
2527
2528 // Handle missing tangents
2529 if (modelMaterial->needsTangents && !streamsState.tangentPresent) {
2530 if (!generateVertexTangents(loadData, streamsState, vertexStreams, primitiveType, clockwise)) {
2531 return false;
2532 }
2533 }
2534
2535 // Optionally remove unused streams
2536 if (loadData.optimizationLevel >= 1) {
2537 std::vector<VertexStream> culledVertexStreams;
2538
2539 for (auto& stream : vertexStreams) {
2540 bool keep = false;
2541 switch (stream.semantic) {
2543 keep = stream.semanticIndex == 0;
2544 break;
2546 keep = modelMaterial->needsNormals ? stream.semanticIndex == 0 : false;
2547 break;
2549 if (stream.semanticIndex == 4 || stream.semanticIndex == 5) {
2550 keep = isSkinned;
2551 }
2552 else {
2553 keep = albedoPerVertex && stream.semanticIndex == 0;
2554 }
2555 break;
2557 keep = stream.semanticIndex < modelMaterial->texCoordSets;
2558 break;
2560 keep = stream.semanticIndex == 0;
2561 break;
2562 default:
2563 break;
2564 }
2565 if (keep) {
2566 culledVertexStreams.emplace_back(stream);
2567 }
2568 else {
2569 debugMessageOnce(loadData, "Culled unused vertex stream with semantic=%u:%u", unsigned(stream.semantic), stream.semanticIndex);
2570 }
2571 }
2572 vertexStreams = std::move(culledVertexStreams);
2573 }
2574
2575 { // Sort vertex streams
2576 size_t n = 0;
2577 size_t N = vertexStreams.size();
2578 std::vector<VertexStream> sortedStreams(N);
2579 for (const auto& stream : vertexStreams) {
2580 size_t i = n++;
2581 while(i && (
2582 (unsigned(stream.semantic) < unsigned(sortedStreams[i - 1].semantic)) ||
2583 ((unsigned(stream.semantic) == unsigned(sortedStreams[i - 1].semantic) && (stream.semanticIndex < sortedStreams[i - 1].semanticIndex)))))
2584 {
2585 sortedStreams[i] = sortedStreams[i - 1];
2586 --i;
2587 }
2588 sortedStreams[i] = stream;
2589 }
2590 vertexStreams = std::move(sortedStreams);
2591 }
2592
2593 // Calculate mesh hash. Active vertex streams is dependent on material,
2594 // so we hash those specifically, for indices the accessor index should
2595 // be sufficient
2596 size_t meshHash = Cogs::hashSequence(skinIx,
2597 primitiveType,
2598 clockwise ? uint32_t(1) : uint32_t(0),
2599 isIndexed ? uint32_t(~0) : indicesAccessorIx);
2600 for (const VertexStream& vertexStream : vertexStreams) {
2601 meshHash = hash(vertexStream, meshHash);
2602 }
2603
2604 if (auto it = loadData.modelMeshCache.find(meshHash); it != loadData.modelMeshCache.end()) {
2605 debugMessageOnce(loadData, "Found duplicate primitive description, recycling mesh");
2606 modelMeshInstance.mesh = it->second;
2607 }
2608 else {
2609 // Create mesh
2610 MeshManager::ResourceProxy mesh = loadData.context->meshManager->createLocked();
2611 modelMeshInstance.mesh.meshIx = uint32_t(model.meshes.size());
2612 model.meshes.push_back(mesh.getHandle());
2613 if (!setVertexAttributes(loadData, mesh, streamsState, vertexStreams)) {
2614 return false;
2615 }
2616
2617 if (clockwise) {
2618 mesh->setMeshFlag(MeshFlags::ClockwiseWinding);
2619 }
2620
2621 if (isSkinned) {
2622 mesh->setMeshFlag(MeshFlags::Skinned);
2623 }
2624
2625 mesh->primitiveType = primitiveType;
2626 mesh->setBounds(streamsState.bbox);
2627 modelMeshInstance.mesh.bboxIx = uint32_t(model.bounds.size());
2628 model.bounds.push_back(streamsState.bbox);
2629
2630 if (isIndexed) {
2631 if (!setMeshIndices(loadData, mesh, streamsState)) {
2632 return false;
2633 }
2634 }
2635 else {
2636 mesh->setCount(streamsState.positionCount);
2637 }
2638 modelMeshInstance.mesh.vertexCount = mesh->getCount();
2639
2640 // Skin
2641 // ----
2642 if (skinIx != -1) { // Assume check as been done when read
2643 assert(size_t(skinIx) < loadData.skins.size());
2644 const GltfSkin& skin = loadData.skins[skinIx];
2645 size_t count = skin.joints.size();
2646 if (kMaxBones < count) {
2647 LOG_WARNING_ONCE(logger, "%.*s: Unable to handle bone count %zu over %zu", StringViewFormat(loadData.path), count, kMaxBones);
2648 count = kMaxBones;
2649 }
2650
2651 std::span<uint32_t> poseIndexes = mesh->mapPoseIndexes(static_cast<uint32_t>(count));
2652 for (size_t i = 0; i < count; i++) {
2653 uint32_t jointIx = skin.joints[i];
2654 poseIndexes[i] = loadData.nodes[jointIx].bone_index;
2655 }
2656 }
2657
2658 loadData.modelMeshCache[meshHash] = modelMeshInstance.mesh;
2659 }
2660
2661 return true;
2662 }
2663
2664 [[nodiscard]] bool buildMeshes(GltfModelDefinition& loadData, std::vector<ModelMeshInstance>& modelMeshInstances, Model& model, int32_t skinIx, int32_t gltfMeshIx, bool clockwise, MeshoptDecompressor & meshoptDecomp)
2665 {
2666 if (loadData.meshesArray.size() < size_t(gltfMeshIx)) {
2667 LOG_ERROR(logger, "%.*s: Illegal gltf mesh index %u", StringViewFormat(loadData.path), gltfMeshIx);
2668 return false;
2669 }
2670
2671 const Object& meshObject = loadData.meshesArray[gltfMeshIx];
2672
2673 if (auto primitivesIt = meshObject.FindMember("primitives"); primitivesIt != meshObject.MemberEnd()) {
2674 if (!checkIsArray(loadData, *primitivesIt)) {
2675 return false;
2676 }
2677
2678 const Array& primitivesArray = primitivesIt->value.GetArray();
2679 for (auto& primitiveItem : primitivesArray) {
2680 if (!checkIsObject(loadData, "mesh primitive item", primitiveItem)) {
2681 return false;
2682 }
2683 if (!buildPrimitiveMesh(loadData, modelMeshInstances.emplace_back(), model, primitiveItem.GetObject(), skinIx, clockwise, meshoptDecomp)) {
2684 return false;
2685 }
2686 }
2687 }
2688 else {
2689 LOG_ERROR(logger, "%.*s: Mesh %u has no primitives property", StringViewFormat(loadData.path), gltfMeshIx);
2690 return false;
2691 }
2692
2693 return true;
2694 }
2695
2696 bool loadNode(Context* context,
2697 GltfModelDefinition& definition,
2698 GltfNode& node,
2699 Model& model,
2700 uint32_t parent_part,
2701 const glm::mat4& parent_transform,
2702 MeshoptDecompressor & meshoptDecomp)
2703 {
2704 const bool clockwise = glm::determinant(node.mat) < 0.0f;
2705 const glm::mat4& localTransform = node.mat;
2706 const glm::mat4 globalTransform = parent_transform * localTransform;
2707
2708 std::string nodeName = std::to_string(node.node_index) + "_" + node.name;
2709 uint32_t partIndex = (uint32_t)model.parts.size();
2710 node.part_index = partIndex;
2711
2712 ModelPart& nodePart = model.parts.emplace_back();
2713 nodePart.parentIndex = parent_part;
2714 model.setPartTransform(nodePart, localTransform);
2715 model.setPartName(nodePart, nodeName);
2716
2717 if (node.mesh != (uint32_t)-1) {
2718 std::vector<ModelMeshInstance> meshes;
2719 if (!buildMeshes(definition, meshes, model, node.skin, node.mesh, clockwise, meshoptDecomp)) {
2720 return false;
2721 }
2722
2723 // If it is just a single mesh, we attach the mesh directly to this node
2724 if (meshes.size() == 1) {
2725 nodePart.meshIndex = meshes[0].mesh.meshIx;
2726 nodePart.materialIndex = meshes[0].materialIx;
2727 nodePart.boundsIndex = meshes[0].mesh.bboxIx;
2728 nodePart.vertexCount = meshes[0].mesh.vertexCount;
2729 }
2730 else {
2731 // If there are multiple meshes, we create a child part for each mesh
2732 for (auto& instance : meshes) {
2733 ModelPart& part = model.parts.emplace_back();
2734 part.parentIndex = partIndex;
2735 part.meshIndex = instance.mesh.meshIx;
2736 part.materialIndex = instance.materialIx;
2737 part.boundsIndex = instance.mesh.bboxIx;
2738 part.vertexCount = instance.mesh.vertexCount;
2739 }
2740 }
2741 }
2742
2743 for (uint32_t child_index : node.children) {
2744 if (!loadNode(context, definition, definition.nodes[child_index], model, partIndex, globalTransform, meshoptDecomp)) {
2745 return false;
2746 }
2747 }
2748
2749 return true;
2750 }
2751
2752 bool loadAnimation(Context* context, GltfModelDefinition& definition, Model& model, MeshoptDecompressor& meshoptDecomp)
2753 {
2754 if (definition.animations.size()) model.animation = context->animationManager->create();
2755
2756 auto& animation = *model.animation.resolve();
2757 auto& skeleton = model.skeleton;
2758
2759 for (auto& animationValue : definition.animations) {
2760 auto& clip = animation.clips.emplace_back();
2761
2762 clip.name = animationValue.name;
2763 clip.duration = 0.0f;
2764 clip.resolution = 1.0f;
2765
2766 uint32_t prev_node = (decltype(prev_node))-1;
2767 for (uint32_t i = 0; i < animationValue.channels.size(); i++) {
2768 GltfChannel& channel = animationValue.channels[i];
2769 GltfSampler& sampler = animationValue.samplers[channel.sampler];
2770 uint32_t nodeIndex = channel.node;
2771 GltfNode& node = definition.nodes[nodeIndex];
2772 uint32_t boneIndex = node.bone_index;
2773
2774 if (sampler.interpolation != "" && sampler.interpolation != "LINEAR") // TODO
2775 LOG_ERROR(logger, "Unable to handle animation interpolation %s", sampler.interpolation.c_str());
2776
2777 auto& inputAccessor = definition.accessors[sampler.input];
2778
2779 // Meshopt compressed?
2780 if (meshoptDecomp.isCompressed(definition, sampler.input)) {
2781 bool ok = meshoptDecomp.decompress(definition, sampler.input);
2782 if (!ok) {
2783 return false;
2784 }
2785 }
2786
2787 BufferDataView dataView;
2788 if (!getBufferViewData(definition, dataView, inputAccessor)) {
2789 return false;
2790 }
2791
2792 if (!dataView.data || !dataView.count) {
2793 return false;
2794 }
2795
2796 float t_max = inputAccessor.max.s_float[0];
2797 //float t_min = input_accessor.min.s_float[0];
2798 clip.duration = std::max(t_max, clip.duration);
2799
2800 if (prev_node != nodeIndex) {
2801 clip.tracks.push_back({});
2802 }
2803 prev_node = nodeIndex;
2804 auto& track = clip.tracks.back();
2805 track.boneIndex = boneIndex;
2806
2807 skeleton.bones[boneIndex].animated = true;
2808
2809 auto getTime = [](uint32_t i, const uint8_t* iData, const size_t iStride) {
2810 return *reinterpret_cast<const float*>(iData + iStride * i);
2811 };
2812
2813 if (definition.accessors.size() <= sampler.output) {
2814 return false;
2815 }
2816
2817 const GltfAccessor& outputAccessor = definition.accessors[sampler.output];
2818
2819 // Meshopt compressed? Not supported yet.
2820 if (meshoptDecomp.isCompressed(definition, sampler.output)) {
2821 bool ok = meshoptDecomp.decompress(definition, sampler.output);
2822 if (!ok) {
2823 return false;
2824 }
2825 }
2826
2827 BufferDataView oDataView;
2828 if (!getBufferViewData(definition, oDataView, outputAccessor)) {
2829 return false;
2830 }
2831
2832 if (channel.path == "translation") {
2833 assert(outputAccessor.type == AccessorType::Vec3);
2834 track.translations.resize(dataView.count);
2835
2836 for (uint32_t j = 0; j < dataView.count; j++) {
2837 const float time = getTime(j, dataView.data, dataView.stride);
2838 glm::vec3 translation = *reinterpret_cast<const glm::vec3*>(oDataView.data + oDataView.stride * j);
2839 track.translations[j] = TranslationKey{ time, translation };
2840 }
2841 }
2842 else if (channel.path == "scale") {
2843 assert(outputAccessor.type == AccessorType::Vec3);
2844 track.scales.resize(dataView.count);
2845
2846 for (uint32_t j = 0; j < dataView.count; j++) {
2847 const float time = getTime(j, dataView.data, dataView.stride);
2848 glm::vec3 scale = *reinterpret_cast<const glm::vec3*>(oDataView.data + oDataView.stride * j);
2849 track.scales[j] = ScaleKey{ time, scale };
2850 }
2851 }
2852 else if (channel.path == "rotation") {
2853 assert(outputAccessor.type == AccessorType::Vec4);
2854 track.rotations.resize(dataView.count);
2855
2856 if (outputAccessor.componentType == AccessorComponentType::Float) {
2857 for (uint32_t j = 0; j < dataView.count; j++) {
2858 const float time = getTime(j, dataView.data, dataView.stride);
2859 glm::vec4 rot = *reinterpret_cast<const glm::vec4*>(oDataView.data + oDataView.stride * j);
2860 track.rotations[j] = RotationKey{ time, glm::quat(rot.w, rot.x, rot.y, rot.z) };
2861 }
2862 }
2863 }
2864 else {
2865 LOG_ERROR(logger, "Unknown animation channel path: %s", channel.path.c_str());
2866 }
2867 }
2868 }
2869 if (model.animation) {
2870 model.animation->skeleton = model.skeleton;
2871 }
2872 return true;
2873 }
2874};
2875
2876namespace Cogs::Core::GltfLoader {
2877 bool
2878 GltfLoader::canLoad(Context* context, const ModelLoadInfo& loadInfo)
2879 {
2880 // If loader is disabled, just return false.
2881 if (Variable* enable = context->variables->get(enableVariableName); !enable->isEmpty() && enable->getBool() == false) {
2882 return false;
2883 }
2884
2885 // If 'resourcePath' is an URL then it might contain parameters which will mess
2886 // up the extension-test. Remove all parameters -- if found.
2887 std::string resourcePath = loadInfo.resourcePath;
2888 if (resourcePath.starts_with("http")) {
2889 size_t paramPos = resourcePath.find('?');
2890 if (paramPos != std::string::npos) {
2891 resourcePath = resourcePath.substr(0, paramPos);
2892 }
2893 }
2894
2895 auto extension = Cogs::IO::extension(resourcePath);
2896 if (extension.starts_with(".gltf")) return true;
2897 if (extension.starts_with(".glb")) return true;
2898 if (extension.starts_with(".b3dm")) return true; // B3DM is just a wrapped glTF/glb file
2899 return false;
2900 }
2901
2909 size_t GltfLoader::getPotentialB3DMOffset(uint8_t* content, size_t size)
2910 {
2911 struct B3DMHeader {
2912 uint32_t magic;
2913 uint32_t version;
2914 uint32_t length;
2915 uint32_t featureTableJSONLength;
2916 uint32_t featureTableBinaryLength;
2917 uint32_t batchTableJSONLength;
2918 uint32_t batchTableBinaryLength;
2919 };
2920
2921 const uint32_t b3dmMagic = uint32_t('b') + uint32_t('3' << 8) + uint32_t('d' << 16) + uint32_t('m' << 24);
2922 B3DMHeader* header = reinterpret_cast<B3DMHeader*>(content);
2923
2924 if (header->magic != b3dmMagic) {
2925 return 0; // This is not a B3DM file
2926 }
2927
2928 assert(header->length <= size && "Invalid length value in header");
2929
2930 if (header->version != 1) {
2931 LOG_ERROR(logger, "The B3DM file is not version 1.0");
2932 return 0;
2933 }
2934
2935 uint32_t featureTableStart = sizeof(B3DMHeader);
2936 uint32_t batchTableStart = featureTableStart + header->featureTableJSONLength + header->featureTableBinaryLength;
2937 uint32_t glbStart = batchTableStart + header->batchTableJSONLength + header->batchTableBinaryLength;
2938
2939 assert(glbStart < size && "Offset out of bounds");
2940 return glbStart;
2941 }
2942
2943 bool GltfLoader::load(Context* context, const ModelLoadInfo& loadInfo, std::unique_ptr<Cogs::FileContents> contents)
2944 {
2945 LOG_TRACE(logger, "Loading model: '%s'", loadInfo.resourceName.c_str());
2946
2947 Cogs::Memory::MemoryBuffer content = contents->take();
2948
2949 if (!content.size()) {
2950 LOG_ERROR(logger, "Could not open file '%s'.", loadInfo.resourcePath.c_str());
2951 return false;
2952 }
2953
2954 // If this file is actually a B3DM file, we'll calculate the amount of bytes to skip to
2955 // load the actual GLTF data.
2956 const size_t fileOffset = getPotentialB3DMOffset(static_cast<uint8_t*>(content.data()), content.size());
2957 const uint8_t* dataPtr = (uint8_t*)(content.data()) + fileOffset;
2958
2959 struct GlbHeader
2960 {
2961 uint32_t magic;
2962 uint32_t version;
2963 uint32_t length;
2964 };
2965
2966 struct GlbChunkHeader
2967 {
2968 uint32_t length;
2969 uint32_t type;
2970 };
2971
2972 struct GlbChunk
2973 {
2974 GlbChunkHeader header;
2975 char data[1];
2976 };
2977
2978 JsonParseFlags flags = JsonParseFlags::NoCachedContent;
2979 const Cogs::StringView path = loadInfo.resourcePath;
2980
2981 Document document;
2982 const GlbHeader* header = (const GlbHeader*) dataPtr;
2983 GltfModelDefinition loadData(context, loadInfo.resourcePath);
2984 loadData.default_material = -1;
2985 if (auto var = context->variables->get(optimizeVariableName); !var->isEmpty()) {
2986 loadData.optimizationLevel = unsigned(std::max(0, var->getInt()));
2987 }
2988
2989 std::span<const uint8_t> glb_data;
2990 if (sizeof(GlbHeader) <= content.size() && header->magic == 0x46546C67) {
2991 loadData.version = header->version * 100;
2992
2993 if (header->version != 2) {
2994 LOG_ERROR(logger, "Unable to load glb format %d", loadData.version);
2995 return false;
2996 }
2997
2998 size_t offset = sizeof(GlbHeader);
2999 uint32_t i = 0;
3000 while (offset < (content.size() - fileOffset)) {
3001 const GlbChunk* chunk = (GlbChunk*)(reinterpret_cast<const char*>(dataPtr) + offset);
3002 if (chunk->header.type == 0x4E4F534A)
3003 document = parseJson(Cogs::StringView(chunk->data, chunk->header.length), flags);
3004 else if (chunk->header.type == 0x004E4942)
3005 glb_data = std::span((const uint8_t*)chunk->data, (const uint8_t*)chunk->data + chunk->header.length);
3006 else {
3007 LOG_ERROR(logger, "Unknown glb chunk type 0x%x (index %d)", chunk->header.type, i);
3008 }
3009 offset += chunk->header.length + sizeof(GlbChunkHeader);
3010 i++;
3011 }
3012 }
3013 else {
3014 document = parseJson(Cogs::StringView(reinterpret_cast<const char*>(dataPtr), content.size() - fileOffset), flags);
3015 }
3016
3017 if (!document.IsObject()) {
3018 LOG_ERROR(logger, "Could not load asset file %s: JSON root is not an object.", loadInfo.resourceName.c_str());
3019 return false;
3020 }
3021
3022 auto& modelManager = *context->modelManager;
3023 auto model = modelManager.lock(loadInfo.handle);
3024
3025 MeshoptDecompressor meshoptDecomp;
3026
3027 auto root = document.GetObject();
3028
3029 // Asset
3030 if (const auto& assetItem = root.FindMember("asset"); assetItem != root.MemberEnd()) {
3031 if (!parseAsset(loadData, path, assetItem->value)) return false;
3032 }
3033 else {
3034 LOG_ERROR(logger, "%.*s: No required JSON asset member", StringViewFormat(path));
3035 return false;
3036 }
3037
3038 if (const auto& buffersItem = root.FindMember("buffers"); buffersItem != root.MemberEnd()) {
3039 if (!parseBuffers(context, loadData, path, buffersItem->value, glb_data)) {
3040 return false;
3041 }
3042 }
3043
3044 if (const auto& bufferViewsItem = root.FindMember("bufferViews"); bufferViewsItem != root.MemberEnd()) {
3045 if (!parseBufferViews(loadData, path, bufferViewsItem->value, meshoptDecomp)) {
3046 return false;
3047 }
3048 }
3049
3050 if (const auto& imagesItem = root.FindMember("images"); imagesItem != root.MemberEnd()) {
3051 if (!parseImages(context, loadData, imagesItem->value)) {
3052 return false;
3053 }
3054 }
3055
3056 if (const auto& samplersItem = root.FindMember("samplers"); samplersItem != root.MemberEnd()) {
3057 if (!parseSamplers(loadData, path, samplersItem->value)) {
3058 return false;
3059 }
3060 }
3061
3062 if (const auto& texturesItem = root.FindMember("textures"); texturesItem != root.MemberEnd()) {
3063 if (!parseTextures(loadData, path, texturesItem->value)) {
3064 return false;
3065 }
3066 }
3067
3068 if (auto materialsItem = root.FindMember("materials"); materialsItem != root.MemberEnd()) {
3069 if (!checkIsArray(loadData, *materialsItem)) return false;
3070 for (const Value& materialsElement : materialsItem->value.GetArray()) {
3071 if (!checkIsObject(loadData, "materials element", materialsElement)) {
3072 return false;
3073 }
3074 loadData.materialsArray.emplace_back(materialsElement.GetObject());
3075 }
3076 }
3077
3078 if (auto meshesItem = root.FindMember("meshes"); meshesItem != root.MemberEnd()) {
3079 if (!checkIsArray(loadData, *meshesItem)) return false;
3080 for (const Value& meshesElement : meshesItem->value.GetArray()) {
3081 if (!checkIsObject(loadData, "meshes element", meshesElement)) {
3082 return false;
3083 }
3084 loadData.meshesArray.emplace_back(meshesElement.GetObject());
3085 }
3086 }
3087
3088 for (const auto& section : root) {
3089 switch (toView(section.name).hash()) {
3090 case Cogs::hash("asset"):
3091 case Cogs::hash("buffers"):
3092 case Cogs::hash("bufferViews"):
3093 case Cogs::hash("images"):
3094 case Cogs::hash("samplers"):
3095 case Cogs::hash("textures"):
3096 case Cogs::hash("materials"):
3097 case Cogs::hash("meshes"):
3098 case Cogs::hash("cameras"): // ignored for now
3099 break;
3100
3101 case Cogs::hash("nodes"):
3102 if (!parseNodes(loadData, path, section.value)) return false;
3103 break;
3104
3105 case Cogs::hash("scenes"):
3106 if (!parseScenes(loadData, path, section.value)) return false;
3107 break;
3108
3109 case Cogs::hash("scene"):
3110 loadData.load_scene = section.value.GetUint();
3111 break;
3112
3113 case Cogs::hash("accessors"):
3114 if (!checkIsArray(loadData, section)) return false;
3115 if (!parseAccessors(loadData, path, section.value)) return false;
3116 break;
3117
3118 case Cogs::hash("skins"):
3119 if (!parseSkins(loadData, path, section.value)) return false;
3120 break;
3121
3122 case Cogs::hash("animations"):
3123 if (!parseAnimations(loadData, path, section.value)) return false;
3124 break;
3125
3126 case Cogs::hash("extensionsRequired"):
3127 for (auto& extension : section.value.GetArray()) {
3128 if (extension.GetString() == Cogs::StringView("KHR_materials_pbrSpecularGlossiness") ||
3129 extension.GetString() == Cogs::StringView("KHR_draco_mesh_compression") ||
3130 extension.GetString() == Cogs::StringView("KHR_mesh_quantization") ||
3131 extension.GetString() == Cogs::StringView("KHR_materials_unlit") ||
3132 extension.GetString() == Cogs::StringView("KHR_texture_transform") ||
3133 extension.GetString() == Cogs::StringView("KHR_texture_basisu") || // NOTE: The KTX v2 image format is not supported yet.
3134 extension.GetString() == Cogs::StringView("EXT_meshopt_compression")) {
3135 // https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression
3136 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression
3137 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_unlit
3138 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization
3139 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform
3140 // LOG_DEBUG(logger, "Required extension used: %s", extension.GetString());
3141 }
3142 else {
3143 LOG_ERROR(logger, "Unknown required extension: %s", extension.GetString());
3144 return false;
3145 }
3146 }
3147 break;
3148
3149 case Cogs::hash("extensions"):
3150 case Cogs::hash("extensionsUsed"):
3151 if (section.value.IsArray()) {
3152 for (auto& extension : section.value.GetArray()) {
3153 if (extension.GetString() == Cogs::StringView("KHR_materials_pbrSpecularGlossiness") ||
3154 extension.GetString() == Cogs::StringView("KHR_materials_specular") ||
3155 extension.GetString() == Cogs::StringView("KHR_materials_ior") ||
3156 extension.GetString() == Cogs::StringView("KHR_draco_mesh_compression") ||
3157 extension.GetString() == Cogs::StringView("KHR_mesh_quantization") ||
3158 extension.GetString() == Cogs::StringView("KHR_texture_transform") ||
3159 extension.GetString() == Cogs::StringView("KHR_texture_basisu") || // NOTE: The KTX v2 image format is not supported yet.
3160 extension.GetString() == Cogs::StringView("EXT_meshopt_compression") ||
3161 extension.GetString() == Cogs::StringView("KHR_materials_unlit")) {
3162 // LOG_DEBUG(logger, "Extension used: %s", extension.GetString());
3163 }
3164 else {
3165 LOG_WARNING(logger, "Unknown extension: %s", extension.GetString());
3166 }
3167 }
3168 }
3169 else if (section.value.IsObject()) {
3170 for (const auto& extension : section.value.GetObject()) {
3171 LOG_WARNING(logger, "Unknown extension: %s", extension.name.GetString());
3172 }
3173 }
3174 else {
3175 LOG_WARNING(logger, "Unknown extension: %s", section.value.GetString());
3176 }
3177 break;
3178
3179 default:
3180 LOG_WARNING(logger, "%.*s: Unrecognized root key %s", StringViewFormat(path), section.name.GetString());
3181 break;
3182 }
3183 }
3184
3185 uint32_t sceneIndex = 0;
3186 for (auto& scene : loadData.scenes) {
3187 if (sceneIndex > 1) {
3188 LOG_ERROR(logger, "Loader unable to handle more than one scene.");
3189 break;
3190 }
3191
3192 model->setName(scene.name);
3193
3194 //NOTE: We insert a virtual root node rotating the models from the Y-up system used by glTF to our
3195 // Z-up convention. If other usage is desired we could replace this with a more configurable approach.
3196 auto& root_ = model->parts.emplace_back();
3197 model->setPartTransform(root_, glm::rotate(glm::mat4(1.0f), glm::half_pi<float>(), glm::vec3(1, 0, 0)));
3198 model->setPartName(root_, "CogsRoot");
3199
3200 // Leave the model root transform alone, used to calculate internal model transforms.
3201 glm::mat4 rootTransform(1.0f);
3202
3203 for (uint32_t nodeIndex : scene.nodes) {
3204 assert(nodeIndex < loadData.nodes.size());
3205
3206 if (!loadBone(loadData, loadData.nodes[nodeIndex], model->skeleton, (size_t)-1, rootTransform)) {
3207 return false;
3208 }
3209
3210 if (!loadJoints(loadData, loadData.nodes[nodeIndex], model->skeleton, meshoptDecomp)) {
3211 return false;
3212 }
3213
3214 // Load root nodes from glTF using our virtual root node with index==0 as parent.
3215 if (!loadNode(context, loadData, loadData.nodes[nodeIndex], *model, 0, rootTransform, meshoptDecomp)) {
3216 return false;
3217 }
3218 }
3219
3220 if (!loadAnimation(context, loadData, *model, meshoptDecomp)) {
3221 return false;
3222 }
3223 }
3224
3225 for (auto const & it : loadData.debugMessages) {
3226 LOG_TRACE(logger, "%s (x %u)", it.first.c_str(), it.second);
3227 }
3228 return true;
3229 }
3230
3231 bool GltfLoader::load(Context* context, const ModelLoadInfo& loadInfo)
3232 {
3233 JsonParseFlags flags = JsonParseFlags::NoCachedContent;
3234#ifdef EMSCRIPTEN
3235 const auto resourceFlags = ResourceStoreFlags::None;
3236#else
3237 const auto resourceFlags = (flags & JsonParseFlags::NoCachedContent) == JsonParseFlags::NoCachedContent
3239 : ResourceStoreFlags::None;
3240#endif
3241
3242 std::unique_ptr<Cogs::FileContents> contents;
3243#ifdef COGS_USE_MMAP
3244 if (loadInfo.protocol == ResourceProtocol::Archive) {
3245#endif
3246 ResourceBuffer buffer = context->resourceStore->getResourceContents(loadInfo.resourcePath);
3247 if (!buffer.buffer->data()) {
3248 LOG_ERROR(logger, "Could not load resource data for model %s.", loadInfo.resourcePath.c_str());
3249 return false;
3250 }
3251 contents = std::make_unique<Cogs::Platform::ResourceBufferBackedFileContents>(buffer, loadInfo.resourcePath, Cogs::FileContentsHints::None);
3252#ifdef COGS_USE_MMAP
3253 }
3254 else {
3255 Cogs::FileHandle::Ptr file = Cogs::IO::openFile(loadInfo.resourcePath, Cogs::FileHandle::OpenMode::OpenAlways, Cogs::FileHandle::AccessMode::Read);
3256 if (!file) {
3257 LOG_ERROR(logger, "Could not open file %s for mapping.", loadInfo.resourcePath.c_str());
3258 return false;
3259 }
3260
3261 size_t size = Cogs::IO::fileSize(file);
3262 if (!size) {
3263 LOG_ERROR(logger, "File size invalid: %zd.", size);
3264 return false;
3265 }
3266
3267 const uint8_t* ptr = static_cast<const uint8_t*>(Cogs::IO::mapFile(file, Cogs::FileHandle::ProtectionMode::ReadOnly, 0, size));
3268 if (!ptr) {
3269 LOG_ERROR(logger, "Could not map file %s for reading model data.", loadInfo.resourcePath.c_str());
3270 return false;
3271 }
3272
3273 contents = std::make_unique<Cogs::MMapBackedFileContents>(ptr, size, file, loadInfo.resourcePath, Cogs::FileContentsHints::None);
3274 }
3275#endif
3276
3277 const Cogs::StringView path = loadInfo.resourcePath;
3278 const ResourceBuffer content = context->resourceStore->getResourceContents(path, resourceFlags);
3279
3280 return load(context, loadInfo, std::move(contents));
3281 }
3282
3283 inline GltfModelDefinition::GltfModelDefinition(Context* context, Cogs::StringView path) :
3284 context(context),
3285 path(path)
3286 {
3287 timer = Cogs::Timer::startNew();
3288 rootTask = context->taskManager->createGroup(context->taskManager->ResourceQueue);
3289 }
3290
3291 inline GltfModelDefinition::~GltfModelDefinition()
3292 {
3293 context->taskManager->wait(rootTask);
3294 for (auto& image : images) {
3295 if (image.decoded.data) {
3296 stbi_image_free(image.decoded.data);
3297 image.decoded.data = nullptr;
3298 }
3299 }
3300 LOG_TRACE(logger, "Gltf elapsed: %f.", timer.elapsedSeconds());
3301 }
3302}
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
std::unique_ptr< struct MemoryContext > memory
Memory and allocation info.
Definition: Context.h:171
std::unique_ptr< class TaskManager > taskManager
TaskManager service instance.
Definition: Context.h:186
std::unique_ptr< class Variables > variables
Variables service instance.
Definition: Context.h:180
std::unique_ptr< class ResourceStore > resourceStore
ResourceStore service instance.
Definition: Context.h:210
size_t getPotentialB3DMOffset(uint8_t *content, size_t size)
bool registerBufferViewCompression(uint32_t bufferViewIdx, const GltfLoader::Object &properties)
Returns false if something failed.
ResourceProxy< Mesh, ResourceManager > ResourceProxy
Type of resource proxy objects, specialized on the type of resource.
Log implementation class.
Definition: LogManager.h:139
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:24
size_t find_first_of(char character, size_t pos=0) const noexcept
Find the first occurance of the given character from the specified starting position.
Definition: StringView.cpp:46
constexpr StringView substr(size_t offset, size_t count=NoPosition) const noexcept
Get the given sub string.
Definition: StringView.h:258
static constexpr size_t NoPosition
No position.
Definition: StringView.h:43
constexpr size_t hash() const noexcept
Get the hash code of the string.
Definition: StringView.h:200
@ NoCachedContent
Never use cached data.
bool HandleIsValid(const ResourceHandle_t< T > &handle)
Check if the given resource is valid, that is not equal to NoHandle or InvalidHandle.
TextureLoadFlags
Texture loading flags. May be combined with resource load flags.
Definition: ResourceFlags.h:47
@ LinearColorSpace
For textures with RGBA format without color space information, mark the data as being in linear color...
@ ColorSpaceFromLoadInfo
by default we want to retrieve colorspace info from the texture data, not from the format specified i...
@ NoMipMaps
Do not generate mipmaps.
@ Back
Back face of primitives discarded before rasterization.
@ None
No primitive culling performed.
uint16_t VariableKey
Used to lookup material properties.
Definition: Resources.h:46
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
Contains all Cogs related functionality.
Definition: FieldSetter.h:23
constexpr size_t hash() noexcept
Simple getter function that returns the initial value for fnv1a hashing.
Definition: HashFunctions.h:62
constexpr size_t hashSequence(const T &t, const U &u)
Hash the last two items in a sequence of objects.
Definition: HashSequence.h:8
@ VertexData
Per vertex data.
@ Position
Position semantic.
@ Tangent
Tangent semantic.
@ Normal
Normal semantic.
@ Color
Color semantic.
@ TextureCoordinate
Texture coordinate semantic.
void setFloatProperty(const VariableKey key, float value)
Set the float property with the given key to value.
void setTransparent()
Set the material instance to transparent, indicating to the renderer that blending should be enabled ...
void setVec3Property(const VariableKey key, glm::vec3 value)
Set the vec3 property with the given key to value.
void setOption(const StringView &key, const StringView &value)
Sets the option with the given key to a value parsed from the value string.
void setTextureAddressMode(const StringView &key, const StringView &addressMode)
Set texture address mode with textual name.
void setTextureProperty(const StringView &key, TextureHandle value)
Set the texture property with the given key to the texture resource held by value.
MaterialOptions options
Material rendering options used by this instance.
void setVec4Property(const VariableKey key, glm::vec4 value)
Set the vec4 property with the given key to value.
Material * material
Material resource this MaterialInstance is created from.
CullMode cullMode
Primitive culling mode to use.
@ IndexesChanged
The index data of the mesh changed.
Definition: Mesh.h:61
@ Indexed
The mesh should be drawn indexed, using index data to order the triangle vertexes.
Definition: Mesh.h:65
@ ClockwiseWinding
The mesh uses clockwise winding order for it's triangles as opposed to the counter-clockwise default.
Definition: Mesh.h:63
Model resources define a template for a set of connected entities, with resources such as meshes,...
Definition: Model.h:56
void setName(const StringView &name)
Set the user friendly name of the resource.
Definition: ResourceBase.h:298
static const ResourceHandle_t NoHandle
Handle representing a default (or none if default not present) resource.
ResourceType * resolve() const
Resolve the handle, returning a pointer to the actual resource.
std::string resourcePath
Resource path. Used to locate resource.
std::string resourceName
Desired resource name. If no name is given, a default name will be chosen.
ResourceHandleBase handle
Handle to resource structure for holding actual resource data.
Runtime control variable.
Definition: Variables.h:27
uint8_t blockSize
Bytesize of one block of data.
Definition: DataFormat.h:257
EPrimitiveType
Primitive type enumeration.
Definition: Common.h:114
@ LineStrip
Line strip.
Definition: Common.h:122
@ LineList
List of lines.
Definition: Common.h:120
@ TriangleStrip
Triangle strip.
Definition: Common.h:118
@ PointList
List of points.
Definition: Common.h:124
@ TriangleList
List of triangles.
Definition: Common.h:116
@ Clamp
Texture coordinates are clamped to the [0, 1] range.
Definition: SamplerState.h:17
@ Mirror
Texture coordinates are mirrored when outside [0, 1] range.
Definition: SamplerState.h:21
@ MinMagMipPoint
Point sampling for both minification and magnification.
Definition: SamplerState.h:33
@ MinMagMipLinear
Linear sampling for both minification and magnification.
Definition: SamplerState.h:35
Vertex element structure used to describe a single data element in a vertex for the input assembler.
Definition: VertexFormat.h:38
uint16_t offset
Offset in bytes from the vertex position in memory.
Definition: VertexFormat.h:39