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 if (isSkinned) materialInstance->setVariant("Skinned", true);
1211 if (albedoPerVertex) materialInstance->setVariant("AlbedoPerVertex", true);
1212 if (hasBitangents) materialInstance->setVariant("Bitangents", true);
1213
1214 if (!handleMaterialPbrMetallicRoughness(loadData, cachedMaterial, material, materialInstance, materialObject)) return false;
1215 if (!handleMaterialGenericFields(loadData, cachedMaterial, material, materialInstance, materialObject)) return false;
1216 if (!handleMaterialExtensions(loadData, cachedMaterial, material, materialInstance, materialObject)) return false;
1217
1218 cachedMaterial_->needsNormals = true;
1219 }
1220 }
1221 // Create default material
1222 else {
1223 MaterialHandle material = loadData.context->materialManager->getMaterial("StandardMaterial");
1224 materialInstance = loadData.context->materialInstanceManager->createMaterialInstance(material);
1225
1226 materialInstance->setPermutation("Metallic");
1227 if (isSkinned) materialInstance->setVariant("Skinned", true);
1228 if (albedoPerVertex) materialInstance->setVariant("AlbedoPerVertex", true);
1229 if (hasBitangents) materialInstance->setVariant("Bitangents", true);
1230 materialInstance->setName("GLTF default material");
1231 materialInstance->setOption("CullMode", "Back");
1232
1233 materialInstance->setVec4Property(material->getVec4Key("albedo"), glm::vec4(1.0, 1.0, 1.0, 1.0));
1234 materialInstance->setFloatProperty(material->getFloatKey("metallic"), 1.0f);
1235 materialInstance->setFloatProperty(material->getFloatKey("roughness"), 1.0f);
1236 materialInstance->setVec3Property(material->getVec3Key("emissive"), glm::vec3(0.0, 0.0, 0.0));
1237 materialInstance->setFloatProperty(material->getFloatKey("alphaThreshold"), 0.5f);
1238
1239 cachedMaterial_->needsNormals = true;
1240 }
1241
1242 assert(materialInstance && "Internal error");
1243
1244 cachedMaterial.modelMaterialIx = int32_t(model.materials.size());
1245 model.materials.push_back(materialInstance);
1246 return true;
1247 }
1248
1249 [[nodiscard]] bool parseScenes(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& scenesValue)
1250 {
1251 if (!scenesValue.IsArray()) {
1252 LOG_ERROR(logger, "%.*s: JSON scenes is not an array value", StringViewFormat(path));
1253 return false;
1254 }
1255
1256 for (const auto& sceneValue : scenesValue.GetArray()) {
1257 if (!sceneValue.IsObject()) {
1258 LOG_ERROR(logger, "%.*s: JSON scenes item is not an object", StringViewFormat(path));
1259 return false;
1260 }
1261
1262 GltfScene& scene = loadData.scenes.emplace_back();
1263 for (const auto& field : sceneValue.GetObject()) {
1264 switch (toView(field.name).hash()) {
1265 case Cogs::hash("name"): {
1266 if (!field.value.IsString()) {
1267 LOG_ERROR(logger, "%.*s: JSON scene name value is not a string", StringViewFormat(path));
1268 return false;
1269 }
1270 scene.name = field.value.GetString();
1271 break;
1272 }
1273 case Cogs::hash("nodes"):
1274 {
1275 if (!field.value.IsArray()) {
1276 LOG_ERROR(logger, "%.*s: JSON scene nodes value is not an array", StringViewFormat(path));
1277 return false;
1278 }
1279 for (const auto& node : field.value.GetArray()) {
1280 if (!node.IsUint()) {
1281 LOG_ERROR(logger, "%.*s: JSON scene nodes array item is not an uint", StringViewFormat(path));
1282 return false;
1283 }
1284 scene.nodes.push_back(node.GetUint());
1285 }
1286 break;
1287 }
1288 case Cogs::hash("extensions"):
1289 {
1290 if (!field.value.IsObject()) {
1291 LOG_ERROR(logger, "%.*s: JSON scene extensions item is not an object", StringViewFormat(path));
1292 return false;
1293 }
1294
1295 // Any extension declared anywhere in the json file will be hoisted and placed on the arrays
1296 // extensionsUsed and extensionsRequired and handled acordingly when reading those.
1297 break;
1298 }
1299
1300 case Cogs::hash("extras"): {
1301 // extras can have ANY type but usually appears as key/value pairs
1302 //https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras
1303
1304 auto type = field.value.GetType();
1305
1306 LOG_WARNING_ONCE(logger, "Unhandled application-specific node under scenes node: Extras[rapidjson::Type: %u]", type );
1307 break;
1308 }
1309 default:
1310 LOG_WARNING_ONCE(logger, "%.*s: JSON unknown scene field: %s", StringViewFormat(path), field.name.GetString());
1311 break;
1312 }
1313 }
1314 }
1315 return true;
1316 }
1317
1318 [[nodiscard]] bool parseNodes(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& nodesValue)
1319 {
1320 if (!nodesValue.IsArray()) {
1321 LOG_ERROR(logger, "%.*s: JSON nodes member is not an array value", StringViewFormat(path));
1322 return false;
1323 }
1324
1325 const auto nodeValues = nodesValue.GetArray();
1326 const uint32_t numNodes = nodeValues.Size();
1327 loadData.nodes.resize(numNodes);
1328
1329 for (uint32_t i = 0; i < numNodes; ++i) {
1330 GltfNode& node = loadData.nodes[i];
1331 node.node_index = i;
1332
1333 bool use_matrix_transform = false;
1334
1335 const Value& nodeValue = nodeValues[i];
1336 if (!nodeValue.IsObject()) {
1337 LOG_ERROR(logger, "%.*s: JSON nodes array item not an object", StringViewFormat(path));
1338 return false;
1339 }
1340
1341 for (const auto& field : nodeValue.GetObject()) {
1342 switch (toView(field.name).hash()) {
1343 case Cogs::hash("name"):
1344 if (!field.value.IsString()) {
1345 LOG_ERROR(logger, "%.*s: JSON node name is not a string", StringViewFormat(path));
1346 return false;
1347 }
1348 node.name = field.value.GetString();
1349 break;
1350
1351 case Cogs::hash("children"):
1352 if (!field.value.IsArray()) {
1353 LOG_ERROR(logger, "%.*s: JSON node children is not an array", StringViewFormat(path));
1354 return false;
1355 }
1356 for (const auto& child : field.value.GetArray()) {
1357 if (!child.IsUint()) {
1358 LOG_ERROR(logger, "%.*s: JSON children array element is not an uint", StringViewFormat(path));
1359 return false;
1360 }
1361 uint32_t childIndex = child.GetUint();
1362 node.children.push_back(childIndex);
1363 loadData.nodes[childIndex].parent_node = i;
1364 }
1365 break;
1366
1367 case Cogs::hash("rotation"):
1368 if (field.value.IsArray() && field.value.GetArray().Size() == 4) {
1369 auto arr = field.value.GetArray();
1370 node.rot = glm::quat(arr[3].GetFloat(), arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat());
1371 }
1372 else {
1373 LOG_ERROR(logger, "%.*s: JSON node rotation value is not an array with 4 elements", StringViewFormat(path));
1374 return false;
1375 }
1376 break;
1377
1378 case Cogs::hash("translation"):
1379 if (field.value.IsArray() && field.value.GetArray().Size() == 3) {
1380 auto arr = field.value.GetArray();
1381 node.pos = glm::vec3(arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat());
1382 }
1383 else {
1384 LOG_ERROR(logger, "%.*s: JSON node translation value is not an array with 3 elements", StringViewFormat(path));
1385 return false;
1386 }
1387 break;
1388
1389 case Cogs::hash("scale"):
1390 if (field.value.IsArray() && field.value.GetArray().Size() == 3) {
1391 auto arr = field.value.GetArray();
1392 node.scale = glm::vec3(arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat());
1393 }
1394 else {
1395 LOG_ERROR(logger, "%.*s: JSON node scale value is not an array with 3 elements", StringViewFormat(path));
1396 return false;
1397 }
1398 break;
1399
1400 case Cogs::hash("matrix"):
1401 if (field.value.IsArray() && field.value.GetArray().Size() == 16) {
1402 auto arr = field.value.GetArray();
1403 node.mat = glm::mat4(arr[0].GetFloat(), arr[1].GetFloat(), arr[2].GetFloat(), arr[3].GetFloat(),
1404 arr[4].GetFloat(), arr[5].GetFloat(), arr[6].GetFloat(), arr[7].GetFloat(),
1405 arr[8].GetFloat(), arr[9].GetFloat(), arr[10].GetFloat(), arr[11].GetFloat(),
1406 arr[12].GetFloat(), arr[13].GetFloat(), arr[14].GetFloat(), arr[15].GetFloat());
1407 glm::vec3 skew;
1408 glm::vec4 perspective;
1409 glm::decompose(node.mat, node.scale, node.rot, node.pos, skew, perspective);
1410 use_matrix_transform = true;
1411 }
1412 else {
1413 LOG_ERROR(logger, "%.*s: JSON node matrix value is not an array with 16 elements", StringViewFormat(path));
1414 return false;
1415 }
1416 break;
1417
1418 case Cogs::hash("mesh"):
1419 if (field.value.IsUint()) {
1420 node.mesh = field.value.GetUint();
1421 }
1422 else {
1423 LOG_ERROR(logger, "%.*s: JSON node mesh value is not an uint", StringViewFormat(path));
1424 return false;
1425 }
1426 break;
1427
1428 case Cogs::hash("skin"):
1429 if (field.value.IsUint()) {
1430 node.skin = field.value.GetUint();
1431 }
1432 else {
1433 LOG_ERROR(logger, "%.*s: JSON node skin value is not an uint", StringViewFormat(path));
1434 return false;
1435 }
1436 break;
1437
1438 case Cogs::hash("camera"):
1439 // Ignore
1440 break;
1441 default:
1442 LOG_DEBUG(logger, "Unknown node field: %s", field.name.GetString());
1443 break;
1444 }
1445 }
1446
1447 if (!use_matrix_transform) {
1448 glm::mat4 ii(1.0f);
1449 node.mat = glm::translate(ii, node.pos) * glm::mat4_cast(node.rot) * glm::scale(ii, node.scale);
1450 }
1451 }
1452 return true;
1453 }
1454
1455 // There is an issue when an accessor-type is FLOAT but the BBox values are all integers (or the other way around).
1456 // The glTF spec requires accessor-type and bbox values-type to match, but this is not always the case...
1457 // We'll therefore just follow the actual type in the JSON document instead of assuming and cast to whatever the
1458 // accessor-type is.
1459 void parseAccessorMinArray(GltfAccessor & accessor, Array arr)
1460 {
1461 accessor.minCount = std::min(16u, arr.Size());
1462 for (uint32_t i = 0; i < accessor.minCount; i++) {
1463 if (arr[i].IsUint() || arr[i].IsInt()) {
1464 if (accessor.componentType == AccessorComponentType::Float) {
1465 accessor.min.s_float[i] = float(arr[i].GetInt());
1466 }
1467 else {
1468 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1469 accessor.componentType == AccessorComponentType::UnsignedShort ||
1470 accessor.componentType == AccessorComponentType::UnsignedInt) {
1471 accessor.min.s_uint[i] = arr[i].GetUint();
1472 }
1473 else {
1474 accessor.min.s_int[i] = arr[i].GetInt();
1475 }
1476 }
1477 }
1478 else {
1479 if (accessor.componentType == AccessorComponentType::Float) {
1480 accessor.min.s_float[i] = arr[i].GetFloat();
1481 }
1482 else {
1483 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1484 accessor.componentType == AccessorComponentType::UnsignedShort ||
1485 accessor.componentType == AccessorComponentType::UnsignedInt) {
1486 accessor.min.s_uint[i] = uint32_t(std::floor(arr[i].GetFloat()));
1487 }
1488 else {
1489 accessor.min.s_int[i] = int32_t(std::floor(arr[i].GetFloat()));
1490 }
1491 }
1492 }
1493 }
1494 }
1495
1496 // See comment above "parseAccessorMinArray()".
1497 void parseAccessorMaxArray(GltfAccessor& accessor, Array arr)
1498 {
1499 accessor.maxCount = std::min(16u, arr.Size());
1500 for (uint32_t i = 0; i < accessor.maxCount; i++) {
1501 if (arr[i].IsInt() || arr[i].IsUint()) {
1502 if (accessor.componentType == AccessorComponentType::Float) {
1503 accessor.max.s_float[i] = float(arr[i].GetInt());
1504 }
1505 else {
1506 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1507 accessor.componentType == AccessorComponentType::UnsignedShort ||
1508 accessor.componentType == AccessorComponentType::UnsignedInt) {
1509 accessor.max.s_uint[i] = arr[i].GetUint();
1510 }
1511 else {
1512 accessor.max.s_int[i] = arr[i].GetInt();
1513 }
1514 }
1515 }
1516 else {
1517 if (accessor.componentType == AccessorComponentType::Float) {
1518 accessor.max.s_float[i] = arr[i].GetFloat();
1519 }
1520 else {
1521 if (accessor.componentType == AccessorComponentType::UnsignedByte ||
1522 accessor.componentType == AccessorComponentType::UnsignedShort ||
1523 accessor.componentType == AccessorComponentType::UnsignedInt) {
1524 accessor.max.s_uint[i] = uint32_t(std::ceil(arr[i].GetFloat()));
1525 }
1526 else {
1527 accessor.max.s_int[i] = int32_t(std::ceil(arr[i].GetFloat()));
1528 }
1529 }
1530 }
1531 }
1532 }
1533
1534 [[nodiscard]] bool parseAccessors(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& accessorsValue)
1535 {
1536 assert(accessorsValue.IsArray());
1537 for (const auto& accessorValue : accessorsValue.GetArray()) {
1538 if (!checkIsObject(loadData, "accessor array item", accessorValue)) return false;
1539
1540 GltfAccessor& accessor = loadData.accessors.emplace_back();
1541 for (const auto& field : accessorValue.GetObject()) {
1542 switch (toView(field.name).hash()) {
1543 case Cogs::hash("bufferView"):
1544 if (!getUint(loadData, accessor.bufferView, field)) return false;
1545 break;
1546
1547 case Cogs::hash("byteOffset"):
1548 if (!getUint(loadData, accessor.byteOffset, field)) return false;
1549 break;
1550
1551 case Cogs::hash("componentType"):
1552 if (checkIsUint(loadData, field)) {
1553 switch (field.value.GetUint()) {
1554 case GLTF_BYTE: accessor.componentType = AccessorComponentType::Byte; break;
1555 case GLTF_UNSIGNED_BYTE: accessor.componentType = AccessorComponentType::UnsignedByte; break;
1556 case GLTF_SHORT: accessor.componentType = AccessorComponentType::Short; break;
1557 case GLTF_UNSIGNED_SHORT: accessor.componentType = AccessorComponentType::UnsignedShort; break;
1558 case GLTF_UNSIGNED_INT: accessor.componentType = AccessorComponentType::UnsignedInt; break;
1559 case GLTF_FLOAT: accessor.componentType = AccessorComponentType::Float; break;
1560 default:
1561 LOG_ERROR(logger, "%.*s:n Unrecognized accessor componentType %u", StringViewFormat(path), field.value.GetUint());
1562 return false;
1563 }
1564 }
1565 else return false;
1566 break;
1567
1568 case Cogs::hash("count"):
1569 if (!getUint(loadData, accessor.count, field)) return false;
1570 break;
1571
1572 case Cogs::hash("type"):
1573 if (checkIsString(loadData, field)) {
1574 switch (toView(field.value).hash()) {
1575 case Cogs::hash("SCALAR"): accessor.type = AccessorType::Scalar; break;
1576 case Cogs::hash("VEC2"): accessor.type = AccessorType::Vec2; break;
1577 case Cogs::hash("VEC3"): accessor.type = AccessorType::Vec3; break;
1578 case Cogs::hash("VEC4"): accessor.type = AccessorType::Vec4; break;
1579 case Cogs::hash("MAT2"): accessor.type = AccessorType::Mat2; break;
1580 case Cogs::hash("MAT3"): accessor.type = AccessorType::Mat3; break;
1581 case Cogs::hash("MAT4"): accessor.type = AccessorType::Mat4; break;
1582 default:
1583 LOG_ERROR(logger, "%.*s: Unrecognized accessor type '%s'", StringViewFormat(path), field.value.GetString());
1584 return false;
1585 }
1586 }
1587 else return false;
1588 break;
1589
1590 case Cogs::hash("min"):
1591 if (checkIsArray(loadData, field)) {
1592 assert(field.value.IsArray());
1593 parseAccessorMinArray(accessor, field.value.GetArray());
1594 }
1595 else return false;
1596 break;
1597
1598 case Cogs::hash("max"):
1599 if (checkIsArray(loadData, field)) {
1600 assert(field.value.IsArray());
1601 parseAccessorMaxArray(accessor, field.value.GetArray());
1602 }
1603 else {
1604 return false;
1605 }
1606 break;
1607
1608 case Cogs::hash("normalized"):
1609 if (!getBool(loadData, accessor.normalized, field)) return false;
1610 break;
1611
1612 case Cogs::hash("name"):
1613 // TODO
1614 break;
1615
1616 default:
1617 LOG_WARNING_ONCE(logger, "%.*s: Unknown accessor key '%s'", StringViewFormat(path), field.name.GetString());
1618 break;
1619 }
1620 }
1621 }
1622 return true;
1623 }
1624
1625 [[nodiscard]] bool parseSkins(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& skinsValue)
1626 {
1627 if (!skinsValue.IsArray()) {
1628 LOG_ERROR(logger, "%.*s: Skins is not an array", StringViewFormat(path));
1629 return false;
1630 }
1631 for (auto& skin_value : skinsValue.GetArray()) {
1632 auto& skin = loadData.skins.emplace_back();
1633
1634 for (const auto& field : skin_value.GetObject()) {
1635 if (field.name == "inverseBindMatrices")
1636 skin.inverseBindMatrices = field.value.GetUint();
1637 else if (field.name == "joints") {
1638 for (auto& joint : field.value.GetArray()) skin.joints.push_back(joint.GetUint());
1639 }
1640 else if (field.name == "skeleton")
1641 skin.skeleton = field.value.GetUint();
1642 else if (field.name == "name")
1643 skin.name = field.value.GetString();
1644 else {
1645 LOG_WARNING_ONCE(logger, "Unknown skin field: %s", field.name.GetString());
1646 }
1647 }
1648 }
1649 return true;
1650 }
1651
1652 [[nodiscard]] bool parseAnimations(GltfModelDefinition& loadData, const Cogs::StringView& path, const Value& animationsValue)
1653 {
1654 if (!animationsValue.IsArray()) {
1655 LOG_ERROR(logger, "%.*s: Animations is not an array", StringViewFormat(path));
1656 return false;
1657 }
1658 for (auto& animation_value : animationsValue.GetArray()) {
1659 auto& animation = loadData.animations.emplace_back();
1660
1661 for (const auto& field : animation_value.GetObject()) {
1662 if (field.name == "channels") {
1663 for (auto& channel_value : field.value.GetArray()) {
1664 auto& channel = animation.channels.emplace_back();
1665
1666 for (auto& channel_field : channel_value.GetObject()) {
1667 if (channel_field.name == "sampler")
1668 channel.sampler = channel_field.value.GetUint();
1669 else if (channel_field.name == "target") {
1670 for (auto& target : channel_field.value.GetObject()) {
1671 if (target.name == "node")
1672 channel.node = target.value.GetUint();
1673 else if (target.name == "path")
1674 channel.path = target.value.GetString();
1675 else {
1676 LOG_WARNING_ONCE(logger, "Unknown animation channel target field: %s", target.name.GetString());
1677 }
1678 }
1679 }
1680 else {
1681 LOG_WARNING_ONCE(logger, "Unknown animation channel field: %s", channel_field.name.GetString());
1682 }
1683 }
1684 }
1685 }
1686 else if (field.name == "samplers") {
1687 for (auto& sampler_value : field.value.GetArray()) {
1688 auto& sampler = animation.samplers.emplace_back();
1689
1690 for (auto& sampler_field : sampler_value.GetObject()) {
1691 if (sampler_field.name == "input")
1692 sampler.input = sampler_field.value.GetUint();
1693 else if (sampler_field.name == "interpolation")
1694 sampler.interpolation = sampler_field.value.GetString();
1695 else if (sampler_field.name == "output")
1696 sampler.output = sampler_field.value.GetUint();
1697 else {
1698 LOG_WARNING_ONCE(logger, "Unknown animation sampler field: %s", sampler_field.name.GetString());
1699 }
1700 }
1701 }
1702 }
1703 else if (field.name == "name") {
1704 animation.name = field.value.GetString();
1705 }
1706 else {
1707 LOG_WARNING_ONCE(logger, "Unknown animation field: %s", field.name.GetString());
1708 }
1709 }
1710 }
1711
1712 return true;
1713 }
1714
1715 uint32_t getAccessorTypeSize(const GltfAccessor& accessor)
1716 {
1717 assert(size_t(accessor.componentType) < size_t(AccessorComponentType::Count));
1718 assert(size_t(accessor.type) < size_t(AccessorType::Count));
1719 return accessorTypeSize[size_t(accessor.type)] * accessorComponentTypeSize[size_t(accessor.componentType)];
1720 }
1721
1722 [[nodiscard]] bool getBufferViewData(const GltfModelDefinition& loadData, BufferDataView& dataView, const GltfAccessor& accessor)
1723 {
1724 if (loadData.buffer_views.size() <= accessor.bufferView) {
1725 LOG_ERROR(logger, "%.*s: Illegal accessor buffer view index %u", StringViewFormat(loadData.path), accessor.bufferView);
1726 return false;
1727 }
1728 const GltfBufferView& bufferView = loadData.buffer_views[accessor.bufferView];
1729
1730 if (loadData.buffer_data.size() <= bufferView.buffer) {
1731 LOG_ERROR(logger, "%.*s: Illegal buffer view buffer index %u", StringViewFormat(loadData.path), bufferView.buffer);
1732 return false;
1733 }
1734 std::span<const uint8_t> buffer = loadData.buffer_data[bufferView.buffer];
1735
1736 size_t byteOffset = size_t(accessor.byteOffset) + bufferView.byteOffset;
1737 uint32_t stride = bufferView.byteStride != 0u ? bufferView.byteStride : getAccessorTypeSize(accessor);
1738
1739 if (buffer.size() < byteOffset + size_t(stride) * accessor.count) {
1740 LOG_ERROR(logger, "%.*s: Accessor references outside of buffer (bufferSize=%zd, idx=%zd)",
1741 StringViewFormat(loadData.path), buffer.size(), byteOffset + size_t(stride) * accessor.count);
1742 return false;
1743 }
1744
1745 dataView = {
1746 .data = buffer.data() + byteOffset,
1747 .stride = stride,
1748 .count = accessor.count
1749 };
1750
1751 return true;
1752 }
1753
1754 bool loadBone(GltfModelDefinition& definition,
1755 GltfNode& node,
1756 Skeleton& skeleton,
1757 size_t parent_bone,
1758 const glm::mat4& parent_transform)
1759 {
1760 const glm::mat4& localTransform = node.mat;
1761 const glm::mat4 globalTransform = parent_transform * localTransform;
1762
1763 uint32_t boneIndex = (uint32_t)skeleton.bones.size();
1764 node.bone_index = boneIndex;
1765
1766 auto& bone = skeleton.bones.emplace_back();
1767 bone.name = std::to_string(node.node_index) + "_" + node.name;
1768 bone.parentBone = parent_bone;
1769 bone.pos = node.pos;
1770 bone.scale = node.scale;
1771 bone.rot = node.rot;
1772 bone.relative = localTransform;
1773 bone.absolute = globalTransform;
1774 bone.inverseBindPose = glm::mat4(1.0);
1775 bone.hasOffset = false;
1776 bone.used = false;
1777 bone.animated = false;
1778
1779 skeleton.bonesByName[bone.name] = boneIndex;
1780
1781 for (uint32_t childIndex : node.children) {
1782 if (!loadBone(definition, definition.nodes[childIndex], skeleton, boneIndex, globalTransform)) {
1783 return false;
1784 }
1785 }
1786
1787 return true;
1788 }
1789
1790 bool loadJoints(GltfModelDefinition& definition,
1791 GltfNode& node,
1792 Skeleton& skeleton,
1793 MeshoptDecompressor& meshoptDecomp)
1794 {
1795 for (uint32_t childIndex : node.children) {
1796 if (!loadJoints(definition, definition.nodes[childIndex], skeleton, meshoptDecomp)) {
1797 return false;
1798 }
1799 }
1800
1801 if (node.skin != (uint32_t)-1) {
1802 skeleton.bindPose = glm::inverse(skeleton.bones[node.bone_index].absolute);
1803 GltfSkin& skin = definition.skins[node.skin];
1804
1805 if (definition.accessors.size() <= size_t(skin.inverseBindMatrices)) {
1806 return false;
1807 }
1808
1809 size_t accessorIdx = size_t(skin.inverseBindMatrices);
1810 GltfAccessor accessor = definition.accessors[accessorIdx];
1811
1812 // Meshopt compressed?
1813 if (meshoptDecomp.isCompressed(definition, int(accessorIdx))) {
1814 bool ok = meshoptDecomp.decompress(definition, int(accessorIdx));
1815 if (!ok) {
1816 return false;
1817 }
1818 }
1819
1820 BufferDataView dataView;
1821 if (!getBufferViewData(definition, dataView, accessor)) {
1822 return false;
1823 }
1824
1825 size_t i = 0;
1826 for (uint32_t jointIndex : skin.joints) {
1827 auto& joint = skeleton.bones[definition.nodes[jointIndex].bone_index];
1828 joint.inverseBindPose = *reinterpret_cast<const glm::mat4*>(dataView.data + dataView.stride * i++);
1829 joint.hasOffset = true;
1830 joint.used = true;
1831 }
1832 }
1833
1834 return true;
1835 }
1836
1837 [[nodiscard]] bool getVertexElement(GltfModelDefinition& loadData, VertexStreamsState& streamsState, VertexStream& vertexStream, const Cogs::StringView& key, const uint32_t accessorIx)
1838 {
1839 if (loadData.accessors.size() <= accessorIx) {
1840 LOG_ERROR(logger, "%.*s: Illegal access index %u", StringViewFormat(loadData.path), accessorIx);
1841 return false;
1842 }
1843 const GltfAccessor& accessor = loadData.accessors[accessorIx];
1844
1845 if (!getBufferViewData(loadData, vertexStream.dataView, accessor)) {
1846 return false;
1847 }
1848
1849 // Non-indexed attributes
1850 // ----------------------
1851
1852 switch (key.hash()) {
1853 case Cogs::hash("POSITION"):
1854 if (accessor.type == AccessorType::Vec3 &&
1855 (accessor.componentType == AccessorComponentType::Float ||
1856 accessor.componentType == AccessorComponentType::Byte ||
1857 accessor.componentType == AccessorComponentType::Short ||
1858 accessor.componentType == AccessorComponentType::UnsignedShort)) {
1859 streamsState.positionData = vertexStream.dataView;
1860 streamsState.positionPresent = true;
1861 streamsState.positionCount = accessor.count;
1862 vertexStream.semantic = Cogs::ElementSemantic::Position;
1863 vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT;
1864
1865 if (accessor.componentType != AccessorComponentType::Float) {
1866 // The standardshader material only accepts Float3 vertices. We'll therefore convert the data to keep things simple.
1867 loadData.positionsScratch.push_back(Memory::MemoryBuffer());
1868 vertexStream.dataView = convertVecBufferToFloats<glm::vec3>(vertexStream.dataView, accessor.componentType, &loadData.positionsScratch.back());
1869 }
1870 streamsState.positionData = vertexStream.dataView;
1871
1872 if (accessor.minCount == 3 && accessor.maxCount == 3) {
1873 if (accessor.componentType == AccessorComponentType::Float) {
1874 streamsState.bbox.min = glm::make_vec3(accessor.min.s_float);
1875 streamsState.bbox.max = glm::make_vec3(accessor.max.s_float);
1876 }
1877 else if (accessor.componentType == AccessorComponentType::UnsignedShort) {
1878 streamsState.bbox.min = glm::make_vec3(accessor.min.s_uint);
1879 streamsState.bbox.max = glm::make_vec3(accessor.max.s_uint);
1880 }
1881 else {
1882 streamsState.bbox.min = glm::make_vec3(accessor.min.s_int);
1883 streamsState.bbox.max = glm::make_vec3(accessor.max.s_int);
1884 }
1885 }
1886 else {
1887 LOG_WARNING_ONCE(logger, "%.*s: POSITION attribute has missing or malformed min and max values", StringViewFormat(loadData.path));
1888 streamsState.bbox.min = glm::vec3(std::numeric_limits<float>::max());
1889 streamsState.bbox.max = -streamsState.bbox.min;
1890 for (size_t i = 0; i < streamsState.positionCount; i++) {
1891 const glm::vec3& p = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * i);
1892 if (std::isfinite(p.x) && std::isfinite(p.y) && std::isfinite(p.z)) {
1893 streamsState.bbox.min = min(streamsState.bbox.min, p);
1894 streamsState.bbox.max = max(streamsState.bbox.max, p);
1895 }
1896 }
1897 }
1898
1899 return true;
1900 }
1901 LOG_ERROR(logger, "%.*s: Illegal POSITION accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
1902 return false;
1903
1904 case Cogs::hash("NORMAL"):
1905 if (accessor.type == AccessorType::Vec3 &&
1906 (accessor.componentType == AccessorComponentType::Float ||
1907 accessor.componentType == AccessorComponentType::Byte ||
1908 accessor.componentType == AccessorComponentType::Short)) {
1909 if (accessor.componentType != AccessorComponentType::Float) {
1910 // The standardshader material only accepts Float3 normals. We'll therefore convert the data to keep things simple.
1911 loadData.normalsScratch.push_back(Memory::MemoryBuffer());
1912 vertexStream.dataView = convertVecBufferToFloats<glm::vec3>(vertexStream.dataView, accessor.componentType, &loadData.normalsScratch.back());
1913 }
1914
1915 streamsState.normalData = vertexStream.dataView;
1916 streamsState.normalPresent = true;
1917 vertexStream.semantic = Cogs::ElementSemantic::Normal;
1918 vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT;
1919
1920 return true;
1921 }
1922 LOG_ERROR(logger, "%.*s: Illegal NORMAL accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
1923 return false;
1924
1925 case Cogs::hash("TANGENT"):
1926 if (accessor.type == AccessorType::Vec4 &&
1927 (accessor.componentType == AccessorComponentType::Float ||
1928 accessor.componentType == AccessorComponentType::Byte ||
1929 accessor.componentType == AccessorComponentType::Short)) {
1930 streamsState.tangentPresent = true;
1931 vertexStream.semantic = Cogs::ElementSemantic::Tangent;
1932 if (accessor.componentType != AccessorComponentType::Float) {
1933 // The standardshader material only accepts Float4 tangents. We'll therefore convert the data to keep things simple.
1934 loadData.tangentsScratch.push_back(Memory::MemoryBuffer());
1935 vertexStream.dataView = convertVecBufferToFloats<glm::vec4>(vertexStream.dataView, accessor.componentType, &loadData.tangentsScratch.back());
1936 }
1937 vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT;
1938
1939 return true;
1940 }
1941 LOG_ERROR(logger, "%.*s: Illegal TANGENT accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
1942 return false;
1943
1944 default:
1945
1946 // Indexed attributes
1947 // ------------------
1948
1949 if (size_t underscore = key.find_first_of('_');
1950 underscore != Cogs::StringView::NoPosition && // There is an underscore
1951 underscore + 1 < key.length() && // There is at least one character after the underscore
1952 '0' <= key[underscore + 1] && // The character is an digit
1953 key[underscore + 1] <= '9')
1954 {
1955 vertexStream.semanticIndex = 0;
1956 size_t digit = underscore + 1;
1957 for (; digit < key.size() && '0' <= key[digit] && key[digit] <= '9'; digit++) {
1958 vertexStream.semanticIndex = 10 * vertexStream.semanticIndex + uint32_t(key[digit] - '0');
1959 }
1960
1961 if (digit == key.length()) { // no garbage after last digit
1962 switch (key.substr(0, underscore).hash()) {
1963 case Cogs::hash("TEXCOORD"):
1964 if (vertexStream.semanticIndex == 0) {
1965 if (accessor.componentType != AccessorComponentType::Float) {
1966 // The standard-shader material only accepts Float2 texcoords. We'll therefore convert the data to keep things simple.
1967 loadData.texCoordsScratch.push_back(Memory::MemoryBuffer());
1968 vertexStream.dataView = convertVecBufferToFloats<glm::vec2>(vertexStream.dataView, accessor.componentType, &loadData.texCoordsScratch.back());
1969 }
1970 }
1971
1972 // Pre-calc texture coord transforms from a KHR_texture_transform setting?
1973 if (loadData.textureTransforms.size()) {
1974 const TextureTransform& textureTransform = loadData.textureTransforms.back(); // Always use the LAST texture-transform registered.
1975 float* buf = reinterpret_cast<float*>(const_cast<uint8_t*>(vertexStream.dataView.data));
1976 size_t num = vertexStream.dataView.count;
1977 const glm::mat3 translation = glm::mat3(1.0, 0, 0, 0, 1.0, 0, textureTransform.offset.x, textureTransform.offset.y, 1.0);
1978 const glm::mat3 rotation = glm::mat3(cos(textureTransform.rotation), sin(textureTransform.rotation), 0,
1979 -sin(textureTransform.rotation), cos(textureTransform.rotation), 0,
1980 0, 0, 1.0);
1981 const glm::mat3 scale = glm::mat3(textureTransform.scale.x, 0, 0, 0, textureTransform.scale.y, 0, 0, 0, 1.0);
1982 const glm::mat3 xform = translation * rotation * scale;
1983
1984 for (size_t i = 0; i < num; ++i) {
1985 const glm::vec3 result = xform * glm::vec3(buf[i * 2], buf[i * 2 + 1], 1.0);
1986 buf[i * 2] = result.x;
1987 buf[i * 2 + 1] = result.y;
1988 }
1989 }
1990
1991 streamsState.texcoordData = vertexStream.dataView;
1992 streamsState.texcoordStreamCount++;
1993 vertexStream.semantic = Cogs::ElementSemantic::TextureCoordinate;
1994 vertexStream.format = Cogs::DataFormat::R32G32_FLOAT;
1995 return true;
1996
1997 case Cogs::hash("COLOR"):
1998 streamsState.colorStreamCount++;
1999 vertexStream.semantic = Cogs::ElementSemantic::Color;
2000 if (accessor.type == AccessorType::Vec3) {
2001 switch (accessor.componentType) {
2002 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32_FLOAT; return true;
2003 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8_UNORM; return true;
2004 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16_UNORM; return true;
2005 default: break;
2006 }
2007 }
2008 else if (accessor.type == AccessorType::Vec4) {
2009 switch (accessor.componentType) {
2010 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32A32_FLOAT; return true;
2011 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8A8_UNORM; return true;
2012 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16A16_UNORM; return true;
2013 default: break;
2014 }
2015 }
2016 LOG_ERROR(logger, "%.*s: Illegal COLOR accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
2017 return false;
2018
2019 case Cogs::hash("JOINTS"):
2020 if (vertexStream.semanticIndex == 0) { // We only support one set of joints, at least for now
2021 streamsState.jointsStreamCount = 1;
2022 vertexStream.semantic = Cogs::ElementSemantic::Color;
2023 vertexStream.semanticIndex = 4;
2024 if (accessor.type == AccessorType::Vec4) {
2025 switch (accessor.componentType) {
2026 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8A8_UINT; return true;
2027 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16A16_UINT; return true;
2028 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32A32_FLOAT; return true;
2029 default: break;
2030 }
2031 }
2032 LOG_ERROR(logger, "%.*s: Illegal JOINTS accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
2033 return false;
2034 }
2035 break;
2036
2037 case Cogs::hash("WEIGHTS"):
2038 if (vertexStream.semanticIndex == 0) { // We only support one set of weights, at least for now
2039 vertexStream.semantic = Cogs::ElementSemantic::Color;
2040 vertexStream.semanticIndex = 5;
2041 if (accessor.type == AccessorType::Vec4) {
2042 switch (accessor.componentType) {
2043 case AccessorComponentType::Float: vertexStream.format = Cogs::DataFormat::R32G32B32A32_FLOAT; return true;
2044 case AccessorComponentType::UnsignedByte: vertexStream.format = Cogs::DataFormat::R8G8B8A8_UNORM; return true;
2045 case AccessorComponentType::UnsignedShort: vertexStream.format = Cogs::DataFormat::R16G16B16A16_UNORM; return true;
2046 default: break;
2047 }
2048 }
2049 LOG_ERROR(logger, "%.*s: Illegal WEIGHTS accessor type %s of %s", StringViewFormat(loadData.path), accessorTypeName[size_t(accessor.type)], accessorComponentTypeName[size_t(accessor.componentType)]);
2050 return false;
2051 }
2052 break;
2053
2054 default:
2055 break;
2056 }
2057 }
2058 }
2059 break;
2060 }
2061
2062 LOG_WARNING_ONCE(logger, "Unrecognized attribute semantic %.*s", StringViewFormat(key));
2063 return false;
2064 }
2065
2066 [[nodiscard]] bool getVertexAttributes(GltfModelDefinition& loadData, VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams, const Object& attributeObject, MeshoptDecompressor& meshoptDecomp)
2067 {
2068 for (const auto& attributeIt : attributeObject) {
2069 // Retrieve accessor
2070 uint32_t accessorIx = 0;
2071 if (!getUint(loadData, accessorIx, attributeIt)) {
2072 return false;
2073 }
2074
2075 // Meshopt compressed attribute?
2076 if (meshoptDecomp.isCompressed(loadData, accessorIx)) {
2077 meshoptDecomp.decompress(loadData, accessorIx);
2078 }
2079
2080 // Retrieve vertex specification
2081 VertexStream vertexStream;
2082 if (getVertexElement(loadData, streamsState, vertexStream, toView(attributeIt.name), accessorIx)) {
2083 vertexStreams.push_back(vertexStream);
2084 }
2085 }
2086
2087 if (!streamsState.positionPresent) {
2088 LOG_ERROR(logger, "%.*s: Primitive has no POSITION attribute", StringViewFormat(loadData.path));
2089 return false;
2090 }
2091
2092 for (const auto& stream : vertexStreams) {
2093 if (stream.dataView.count != streamsState.positionCount) {
2094 LOG_ERROR(logger, "%.*s: Primitive has attributes with variable counts", StringViewFormat(loadData.path));
2095 return false;
2096 }
2097 }
2098
2099 return true;
2100 }
2101
2102 [[nodiscard]] bool generateVertexTangents(GltfModelDefinition& loadData, VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams, Cogs::PrimitiveType primitiveType, bool clockwise)
2103 {
2104 if (primitiveType != Cogs::PrimitiveType::TriangleList) {
2105 LOG_ERROR(logger, "%.*s: Vertex tangent generator only supports triangle lists (type is %d)", StringViewFormat(loadData.path), primitiveType);
2106 return false;
2107 }
2108 assert(streamsState.positionPresent);
2109 assert(streamsState.normalPresent);
2110 assert(streamsState.texcoordStreamCount);
2111
2112 const size_t byteSize = sizeof(glm::vec3) * streamsState.positionCount;
2113
2114 loadData.tangentsScratch.push_back(Memory::MemoryBuffer());
2115 Memory::MemoryBuffer * memBuf = &loadData.tangentsScratch.back();
2116 memBuf->resize(byteSize);
2117 glm::vec3* tangents = (glm::vec3*) memBuf->data();
2118 std::memset(static_cast<void*>(tangents), 0, byteSize);
2119
2120 size_t count = streamsState.indexType != AccessorComponentType::None ? streamsState.indexData.count : streamsState.positionCount;
2121 for (size_t i = 0; i + 2 < count; i += 3) {
2122 size_t ix[3] = { 0,0,0 };
2123 switch (streamsState.indexType) {
2124 case AccessorComponentType::None: for (size_t k = 0; k < 3; k++) { ix[k] = i + k; } break;
2125 case AccessorComponentType::UnsignedByte: for (size_t k = 0; k < 3; k++) { ix[k] = *(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2126 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;
2127 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;
2128 default: assert(false); return false;
2129 }
2130
2131 glm::vec3 a = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[0]);
2132 glm::vec3 b = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[1]);
2133 glm::vec3 c = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[2]);
2134 glm::vec2 ta = *(const glm::vec2*)(streamsState.texcoordData.data + streamsState.texcoordData.stride * ix[0]);
2135 glm::vec2 tb = *(const glm::vec2*)(streamsState.texcoordData.data + streamsState.texcoordData.stride * ix[1]);
2136 glm::vec2 tc = *(const glm::vec2*)(streamsState.texcoordData.data + streamsState.texcoordData.stride * ix[2]);
2137
2138 glm::vec2 u0, u1;
2139 glm::vec3 v0, v1;
2140 if (!clockwise) {
2141 u0 = tb - ta;
2142 u1 = tc - ta;
2143 v0 = b - a;
2144 v1 = c - a;
2145 }
2146 else {
2147 u0 = tc - ta;
2148 u1 = tb - ta;
2149 v0 = c - a;
2150 v1 = b - a;
2151 }
2152
2153 float den = 1.f / (u0.x * u1.y - u0.y * u1.x);
2154 glm::vec3 t0 = u1.y * den * v0 - u0.y * den * v1;
2155
2156 tangents[ix[0]] += t0;
2157 tangents[ix[1]] += t0;
2158 tangents[ix[2]] += t0;
2159 }
2160
2161 for (size_t i = 0; i < streamsState.positionCount; i++) {
2162 const glm::vec3 normal = *(const glm::vec3*)(streamsState.normalData.data + streamsState.normalData.stride * i);
2163 tangents[i] = glm::normalize(tangents[i] - glm::dot(normal, tangents[i]) * normal);
2164 }
2165
2166 vertexStreams.push_back(VertexStream{
2167 .dataView = {
2168 .data = (const uint8_t*) memBuf->data(),
2169 .stride = sizeof(glm::vec3),
2170 .count = streamsState.positionCount
2171 },
2172 .format = Cogs::DataFormat::R32G32B32_FLOAT,
2174 .semanticIndex = 0
2175 });
2176
2177 return true;
2178 }
2179
2180 [[nodiscard]] bool generateVertexNormals(GltfModelDefinition& loadData, VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams, Cogs::PrimitiveType primitiveType, bool clockwise)
2181 {
2182 if (primitiveType != Cogs::PrimitiveType::TriangleList) {
2183 LOG_ERROR(logger, "%.*s: Vertex normal generator only supports triangle lists", StringViewFormat(loadData.path));
2184 return false;
2185 }
2186 assert(streamsState.positionPresent);
2187
2188 const size_t byteSize = sizeof(glm::vec3) * streamsState.positionCount;
2189 loadData.normalsScratch.push_back(Memory::MemoryBuffer());
2190 Memory::MemoryBuffer* memBuf = &loadData.normalsScratch.back();
2191
2192 memBuf->resize(byteSize);
2193 glm::vec3* normals = (glm::vec3*) memBuf->data();
2194 std::memset(static_cast<void*>(normals), 0, byteSize);
2195
2196 size_t count = streamsState.indexSize ? streamsState.indexData.count : streamsState.positionCount;
2197 for (size_t i = 0; i + 2 < count; i += 3) {
2198 size_t ix[3] = { 0,0,0 };
2199 switch (streamsState.indexSize) {
2200 case 0: for (size_t k = 0; k < 3; k++) { ix[k] = i + k; } break;
2201 case 1: for (size_t k = 0; k < 3; k++) { ix[k] = *(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2202 case 2: for (size_t k = 0; k < 3; k++) { ix[k] = *(const uint16_t*)(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2203 case 4: for (size_t k = 0; k < 3; k++) { ix[k] = *(const uint32_t*)(streamsState.indexData.data + streamsState.indexData.stride * (i + k)); } break;
2204 default: assert(false); return false;
2205 }
2206 glm::vec3 a = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[0]);
2207 glm::vec3 b = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[1]);
2208 glm::vec3 c = *(const glm::vec3*)(streamsState.positionData.data + streamsState.positionData.stride * ix[2]);
2209
2210 const glm::vec3 l1 = clockwise ? a - b : b - a;
2211 const glm::vec3 l2 = clockwise ? a - c : c - a;
2212 glm::vec3 n = glm::cross(l1, l2);
2213
2214 if (glm::length(n) <= 0.0) {
2215 LOG_WARNING_ONCE(logger, "Calculated normal is not valid (normalIdx=%zu)", i);
2216 n = glm::vec3(1, 0, 0);
2217 }
2218
2219 normals[ix[0]] += n;
2220 normals[ix[1]] += n;
2221 normals[ix[2]] += n;
2222 }
2223
2224 for (size_t i = 0; i < streamsState.positionCount; i++) {
2225 normals[i] = glm::normalize(normals[i]);
2226 }
2227
2228 vertexStreams.push_back(VertexStream{
2229 .dataView = {
2230 .data = (const uint8_t*) normals,
2231 .stride = sizeof(glm::vec3),
2232 .count = streamsState.positionCount
2233 },
2234 .format = Cogs::DataFormat::R32G32B32_FLOAT, // Let the shader add .w=1
2236 .semanticIndex = 0,
2237 });
2238
2239 return true;
2240 }
2241
2242 [[nodiscard]] bool setMeshIndices(GltfModelDefinition& loadData, MeshManager::ResourceProxy& mesh, VertexStreamsState& streamsState)
2243 {
2244 uint32_t dstStride = 0;
2245 switch (streamsState.indexType) {
2246 // NOTE: We'll be using stride=2 for bytes as Cogs.Rendering does not support strides of 1.
2247 case AccessorComponentType::UnsignedByte: dstStride = 2; streamsState.indexSize = 1; break;
2248 case AccessorComponentType::UnsignedShort: dstStride = 2; streamsState.indexSize = dstStride; break;
2249 case AccessorComponentType::UnsignedInt: dstStride = 4; streamsState.indexSize = dstStride; break;
2250 default:
2251 LOG_ERROR(logger, "%.*s: Illegal index data component type %s", StringViewFormat(loadData.path), accessorComponentTypeName[size_t(streamsState.indexType)]);
2252 return false;
2253 }
2254
2255 if (uint8_t* dst = mesh->mapStream(VertexDataType::Indexes, 0, streamsState.indexData.count, dstStride, true); dst) {
2256 switch (streamsState.indexType) {
2257 case AccessorComponentType::UnsignedByte: stridedCopy<1>(dst, 2, streamsState.indexData.data, streamsState.indexData.stride, streamsState.indexData.count); break;
2258 case AccessorComponentType::UnsignedShort: stridedCopy<2>(dst, 2, streamsState.indexData.data, streamsState.indexData.stride, streamsState.indexData.count); break;
2259 case AccessorComponentType::UnsignedInt: stridedCopy<4>(dst, 4, streamsState.indexData.data, streamsState.indexData.stride, streamsState.indexData.count); break;
2260 default: assert(false); break;
2261 }
2262 mesh->unmap(VertexDataType::Indexes);
2263 }
2264 mesh->setMeshFlag(MeshFlags::Indexed);
2265 mesh->setMeshFlag(MeshFlags::IndexesChanged);
2266 mesh->setCount(streamsState.indexData.count);
2267 return true;
2268 }
2269
2270 [[nodiscard]] bool setVertexAttributes(GltfModelDefinition& loadData, MeshManager::ResourceProxy& mesh, const VertexStreamsState& streamsState, std::vector<VertexStream>& vertexStreams)
2271 {
2272 // Build vertex format
2273 Cogs::VertexFormatHandle vertexFormat;
2274
2275 for (VertexStream& vertexStream : vertexStreams) {
2276 const Cogs::FormatInfo* formatInfo = Cogs::getFormatInfo(vertexStream.format);
2277 assert(formatInfo);
2278 assert(formatInfo->blockSize);
2279 vertexStream.formatSize = formatInfo->blockSize;
2280 }
2281
2282 size_t offset = 0;
2283 std::vector<Cogs::VertexElement> vertexElements;
2284 for (VertexStream& vertexStream : vertexStreams) {
2285 offset = (offset + 3) & ~3;
2286 vertexStream.offset = offset;
2287 vertexElements.push_back(Cogs::VertexElement{
2288 .offset = uint16_t(offset),
2289 .format = vertexStream.format,
2290 .semantic = vertexStream.semantic,
2291 .semanticIndex = uint16_t(vertexStream.semanticIndex),
2292 .inputType = Cogs::InputType::VertexData,
2293 .instanceStep = 0
2294 });
2295 offset += vertexStream.formatSize;
2296 }
2297
2298 vertexFormat = Cogs::VertexFormats::createVertexFormat(vertexElements.data(), vertexElements.size());
2299 if (!HandleIsValid(vertexFormat)) {
2300 LOG_ERROR(logger, "%.*s: Failed to create vertex format with %zu elements", StringViewFormat(loadData.path), vertexElements.size());
2301 return false;
2302 }
2303
2304 // Store data
2305 size_t stride = Cogs::getSize(vertexFormat);
2306 if (uint8_t* base = mesh->mapStream(VertexDataType::Interleaved0, vertexFormat, 0, streamsState.positionCount, stride, true)) {
2307 for (const VertexStream& vertexStream : vertexStreams) {
2308 uint8_t* dst = base + vertexStream.offset;
2309 switch (vertexStream.format) {
2310 case Cogs::DataFormat::R32G32B32A32_FLOAT: // 4*4 = 16
2311 assert(vertexStream.formatSize == 16);
2312 stridedCopy<16>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2313 break;
2314
2315 case Cogs::DataFormat::R32G32B32_FLOAT: // 3*4 = 12
2316 assert(vertexStream.formatSize == 12);
2317 stridedCopy<12>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2318 break;
2319
2320 case Cogs::DataFormat::R32G32_FLOAT: // 2*4 = 8
2321 case Cogs::DataFormat::R16G16B16A16_UNORM: // 4*2 = 8
2322 case Cogs::DataFormat::R16G16B16A16_UINT: // 4*2 = 8
2323 assert(vertexStream.formatSize == 8);
2324 stridedCopy<8>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2325 break;
2326
2327 case Cogs::DataFormat::R16G16B16_UNORM: // 3*2 = 6
2328 case Cogs::DataFormat::R16G16B16_UINT:
2329 case Cogs::DataFormat::R16G16B16_SINT:
2330 case Cogs::DataFormat::R16G16B16_SNORM:
2331 assert(vertexStream.formatSize == 6);
2332 stridedCopy<6>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2333 break;
2334
2335 case Cogs::DataFormat::R16G16_UNORM: // 2*2 = 4
2336 case Cogs::DataFormat::R16G16_UINT:
2337 case Cogs::DataFormat::R16G16_SNORM:
2338 case Cogs::DataFormat::R16G16_SINT:
2339 case Cogs::DataFormat::R8G8B8A8_UNORM: // 4*1 = 4
2340 case Cogs::DataFormat::R8G8B8A8_UINT:
2341 assert(vertexStream.formatSize == 4);
2342 stridedCopy<4>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2343 break;
2344
2345 case Cogs::DataFormat::R8G8B8_UNORM: // 3*1 = 3
2346 case Cogs::DataFormat::R8G8B8_UINT:
2347 case Cogs::DataFormat::R8G8B8_SINT:
2348 case Cogs::DataFormat::R8G8B8_SNORM:
2349 assert(vertexStream.formatSize == 3);
2350 stridedCopy<3>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2351 break;
2352
2353 case Cogs::DataFormat::R8G8_UNORM: // 2*1 = 2
2354 assert(vertexStream.formatSize == 3);
2355 stridedCopy<2>(dst, stride, vertexStream.dataView.data, vertexStream.dataView.stride, streamsState.positionCount);
2356 break;
2357
2358 default:
2359 assert(false && "Unhandled DataFormat");
2360 break;
2361 }
2362 }
2363 mesh->unmap(VertexDataType::Interleaved0);
2364 }
2365 return true;
2366 }
2367
2368 [[nodiscard]] bool buildPrimitiveMesh(GltfModelDefinition& loadData, ModelMeshInstance& modelMeshInstance, Model& model, const Object& primitiveObject, int32_t skinIx, bool clockwise, MeshoptDecompressor & meshoptDecomp)
2369 {
2370 VertexStreamsState streamsState;
2371 std::vector<VertexStream> vertexStreams;
2372
2373 // Check if unlit
2374 bool needsNormals = true;
2375 if (auto materialIt = primitiveObject.FindMember("material"); materialIt != primitiveObject.MemberEnd()) {
2376 uint32_t materialIx = 0;
2377 if (!getUint(loadData, materialIx, *materialIt)) {
2378 LOG_ERROR(logger, "The 'meshes.primitives.material' is not an integer");
2379 return false;
2380 }
2381 if (materialIx >= 0) {
2382 const Object& materialObject = loadData.materialsArray[materialIx];
2383 if (materialObject.HasMember("extensions") && materialObject["extensions"].HasMember("KHR_materials_unlit")) {
2384 needsNormals = false;
2385 }
2386 }
2387 }
2388
2389 // Process attributes
2390 bool useDracoDecompression = false;
2392
2393 // Is this mesh using the DRACO extension?
2394 if (primitiveObject.HasMember("extensions") && primitiveObject["extensions"].HasMember("KHR_draco_mesh_compression")) {
2395 auto extSection = primitiveObject["extensions"].GetObject();
2396 auto dracoSection = extSection["KHR_draco_mesh_compression"].GetObject();
2397 draco.initAttributes(dracoSection);
2398 useDracoDecompression = true;
2399 }
2400
2401 if (const auto& attributesIt = primitiveObject.FindMember("attributes"); attributesIt != primitiveObject.MemberEnd()) {
2402 if (!checkIsObject(loadData, *attributesIt)) {
2403 return false;
2404 }
2405
2406 if (useDracoDecompression) {
2407 if (!draco.decompress(loadData, streamsState, vertexStreams, attributesIt->value.GetObject(), !needsNormals)) {
2408 return false;
2409 }
2410 }
2411 else {
2412 if (!getVertexAttributes(loadData, streamsState, vertexStreams, attributesIt->value.GetObject(), meshoptDecomp)) {
2413 return false;
2414 }
2415 }
2416 }
2417 else {
2418 LOG_ERROR(logger, "%.*s: Primitive has no attribute property", StringViewFormat(loadData.path));
2419 return false;
2420 }
2421
2422 // Process indices
2423 bool isIndexed = false;
2424 uint32_t indicesAccessorIx = 0;
2425
2426 if (useDracoDecompression) {
2427 isIndexed = true; // All index data has been set by the draco-decompressor
2428 }
2429 else {
2430 if (auto indicesIt = primitiveObject.FindMember("indices"); indicesIt != primitiveObject.MemberEnd()) {
2431 if (!getUint(loadData, indicesAccessorIx, *indicesIt)) {
2432 LOG_ERROR(logger, "The 'meshes.primitives.indices' is not an integer");
2433 return false;
2434 }
2435
2436 if (loadData.accessors.size() <= indicesAccessorIx) {
2437 LOG_ERROR(logger, "%.*s: Illegal accessor index %u", StringViewFormat(loadData.path), indicesAccessorIx);
2438 return false;
2439 }
2440
2441 const GltfAccessor& accessor = loadData.accessors[indicesAccessorIx];
2442 streamsState.indexType = accessor.componentType;
2443
2444 if (meshoptDecomp.isCompressed(loadData, indicesAccessorIx)) {
2445 bool ok = meshoptDecomp.decompress(loadData, indicesAccessorIx);
2446 if (!ok) {
2447 return false;
2448 }
2449 }
2450
2451 if (!getBufferViewData(loadData, streamsState.indexData, accessor)) {
2452 return false;
2453 }
2454
2455 isIndexed = true;
2456 }
2457 }
2458
2460 if (auto modeIt = primitiveObject.FindMember("mode"); modeIt != primitiveObject.MemberEnd()) {
2461 uint32_t mode = 0;
2462 if (!getUint(loadData, mode, *modeIt)) {
2463 return false;
2464 }
2465
2466 switch (mode) {
2467 case 0: primitiveType = Cogs::PrimitiveType::PointList; break;
2468 case 1: primitiveType = Cogs::PrimitiveType::LineList; break;
2469 case 3: primitiveType = Cogs::PrimitiveType::LineStrip; break;
2470 case 4: primitiveType = Cogs::PrimitiveType::TriangleList; break;
2471 case 5: primitiveType = Cogs::PrimitiveType::TriangleStrip; break;
2472 case 2: LOG_ERROR(logger, "%.*s: Unsupported primitive mode 2: LINE_LOOP", StringViewFormat(loadData.path)); return false;
2473 case 6: LOG_ERROR(logger, "%.*s: Unsupported primitive mode 6: TRIANGLE_FAN", StringViewFormat(loadData.path)); return false;
2474 default: LOG_ERROR(logger, "%.*s: Illegal primitive mode %u", StringViewFormat(loadData.path), mode); return false;
2475 }
2476 }
2477
2478 // targets
2479 // -------
2480 if (auto targetsIt = primitiveObject.FindMember("targets"); targetsIt != primitiveObject.MemberEnd()) {
2481 if (!checkIsArray(loadData, *targetsIt)) {
2482 return false;
2483 }
2484 if (!targetsIt->value.GetArray().Empty()) {
2485 // Not implemented yet.
2486 LOG_WARNING_ONCE(logger, "%.*s: Morph targets not implemented yet, ignoring.", StringViewFormat(loadData.path));
2487 }
2488 }
2489
2490 // Process materials.
2491 bool isSkinned = streamsState.jointsStreamCount != 0;
2492 bool albedoPerVertex = streamsState.colorStreamCount != 0;
2493 CachedModelMaterial* modelMaterial = nullptr;
2494 if (auto materialIt = primitiveObject.FindMember("material"); materialIt != primitiveObject.MemberEnd()) {
2495 uint32_t materialIx = 0;
2496 if (!getUint(loadData, materialIx, *materialIt)) {
2497 LOG_ERROR(logger, "The 'meshes.primitives.material' is not an integer");
2498 return false;
2499 }
2500 if (!getMaterialHandle(loadData, model, modelMaterial, materialIx, isSkinned, albedoPerVertex, false)) {
2501 return false;
2502 }
2503 }
2504 else {
2505 if (!getMaterialHandle(loadData, model, modelMaterial, -1, isSkinned, albedoPerVertex, false)) {
2506 return false;
2507 }
2508 }
2509 assert(modelMaterial);
2510 modelMeshInstance.materialIx = modelMaterial->modelMaterialIx;
2511
2512 // Handle missing vertex data
2513 if (streamsState.texcoordStreamCount < modelMaterial->texCoordSets) {
2514 LOG_ERROR(logger, "%.*s: Material requires texture coordinates, something which the mesh doesn't have", StringViewFormat(loadData.path));
2515 return false;
2516 }
2517
2518 // Handle missing normals
2519 if (modelMaterial->needsNormals && !streamsState.normalPresent) {
2520 if (!generateVertexNormals(loadData, streamsState, vertexStreams, primitiveType, clockwise)) {
2521 return false;
2522 }
2523 }
2524
2525 // Handle missing tangents
2526 if (modelMaterial->needsTangents && !streamsState.tangentPresent) {
2527 if (!generateVertexTangents(loadData, streamsState, vertexStreams, primitiveType, clockwise)) {
2528 return false;
2529 }
2530 }
2531
2532 // Optionally remove unused streams
2533 if (loadData.optimizationLevel >= 1) {
2534 std::vector<VertexStream> culledVertexStreams;
2535
2536 for (auto& stream : vertexStreams) {
2537 bool keep = false;
2538 switch (stream.semantic) {
2540 keep = stream.semanticIndex == 0;
2541 break;
2543 keep = modelMaterial->needsNormals ? stream.semanticIndex == 0 : false;
2544 break;
2546 if (stream.semanticIndex == 4 || stream.semanticIndex == 5) {
2547 keep = isSkinned;
2548 }
2549 else {
2550 keep = albedoPerVertex && stream.semanticIndex == 0;
2551 }
2552 break;
2554 keep = stream.semanticIndex < modelMaterial->texCoordSets;
2555 break;
2557 keep = stream.semanticIndex == 0;
2558 break;
2559 default:
2560 break;
2561 }
2562 if (keep) {
2563 culledVertexStreams.emplace_back(stream);
2564 }
2565 else {
2566 debugMessageOnce(loadData, "Culled unused vertex stream with semantic=%u:%u", unsigned(stream.semantic), stream.semanticIndex);
2567 }
2568 }
2569 vertexStreams = std::move(culledVertexStreams);
2570 }
2571
2572 { // Sort vertex streams
2573 size_t n = 0;
2574 size_t N = vertexStreams.size();
2575 std::vector<VertexStream> sortedStreams(N);
2576 for (const auto& stream : vertexStreams) {
2577 size_t i = n++;
2578 while(i && (
2579 (unsigned(stream.semantic) < unsigned(sortedStreams[i - 1].semantic)) ||
2580 ((unsigned(stream.semantic) == unsigned(sortedStreams[i - 1].semantic) && (stream.semanticIndex < sortedStreams[i - 1].semanticIndex)))))
2581 {
2582 sortedStreams[i] = sortedStreams[i - 1];
2583 --i;
2584 }
2585 sortedStreams[i] = stream;
2586 }
2587 vertexStreams = std::move(sortedStreams);
2588 }
2589
2590 // Calculate mesh hash. Active vertex streams is dependent on material,
2591 // so we hash those specifically, for indices the accessor index should
2592 // be sufficient
2593 size_t meshHash = Cogs::hashSequence(skinIx,
2594 primitiveType,
2595 clockwise ? uint32_t(1) : uint32_t(0),
2596 isIndexed ? uint32_t(~0) : indicesAccessorIx);
2597 for (const VertexStream& vertexStream : vertexStreams) {
2598 meshHash = hash(vertexStream, meshHash);
2599 }
2600
2601 if (auto it = loadData.modelMeshCache.find(meshHash); it != loadData.modelMeshCache.end()) {
2602 debugMessageOnce(loadData, "Found duplicate primitive description, recycling mesh");
2603 modelMeshInstance.mesh = it->second;
2604 }
2605 else {
2606 // Create mesh
2607 MeshManager::ResourceProxy mesh = loadData.context->meshManager->createLocked();
2608 modelMeshInstance.mesh.meshIx = uint32_t(model.meshes.size());
2609 model.meshes.push_back(mesh.getHandle());
2610 if (!setVertexAttributes(loadData, mesh, streamsState, vertexStreams)) {
2611 return false;
2612 }
2613
2614 if (clockwise) {
2615 mesh->setMeshFlag(MeshFlags::ClockwiseWinding);
2616 }
2617
2618 if (isSkinned) {
2619 mesh->setMeshFlag(MeshFlags::Skinned);
2620 }
2621
2622 mesh->primitiveType = primitiveType;
2623 mesh->setBounds(streamsState.bbox);
2624 modelMeshInstance.mesh.bboxIx = uint32_t(model.bounds.size());
2625 model.bounds.push_back(streamsState.bbox);
2626
2627 if (isIndexed) {
2628 if (!setMeshIndices(loadData, mesh, streamsState)) {
2629 return false;
2630 }
2631 }
2632 else {
2633 mesh->setCount(streamsState.positionCount);
2634 }
2635 modelMeshInstance.mesh.vertexCount = mesh->getCount();
2636
2637 // Skin
2638 // ----
2639 if (skinIx != -1) { // Assume check as been done when read
2640 assert(size_t(skinIx) < loadData.skins.size());
2641 const GltfSkin& skin = loadData.skins[skinIx];
2642 size_t count = skin.joints.size();
2643 if (kMaxBones < count) {
2644 LOG_WARNING_ONCE(logger, "%.*s: Unable to handle bone count %zu over %zu", StringViewFormat(loadData.path), count, kMaxBones);
2645 count = kMaxBones;
2646 }
2647
2648 std::span<uint32_t> poseIndexes = mesh->mapPoseIndexes(static_cast<uint32_t>(count));
2649 for (size_t i = 0; i < count; i++) {
2650 uint32_t jointIx = skin.joints[i];
2651 poseIndexes[i] = loadData.nodes[jointIx].bone_index;
2652 }
2653 }
2654
2655 loadData.modelMeshCache[meshHash] = modelMeshInstance.mesh;
2656 }
2657
2658 return true;
2659 }
2660
2661 [[nodiscard]] bool buildMeshes(GltfModelDefinition& loadData, std::vector<ModelMeshInstance>& modelMeshInstances, Model& model, int32_t skinIx, int32_t gltfMeshIx, bool clockwise, MeshoptDecompressor & meshoptDecomp)
2662 {
2663 if (loadData.meshesArray.size() < size_t(gltfMeshIx)) {
2664 LOG_ERROR(logger, "%.*s: Illegal gltf mesh index %u", StringViewFormat(loadData.path), gltfMeshIx);
2665 return false;
2666 }
2667
2668 const Object& meshObject = loadData.meshesArray[gltfMeshIx];
2669
2670 if (auto primitivesIt = meshObject.FindMember("primitives"); primitivesIt != meshObject.MemberEnd()) {
2671 if (!checkIsArray(loadData, *primitivesIt)) {
2672 return false;
2673 }
2674
2675 const Array& primitivesArray = primitivesIt->value.GetArray();
2676 for (auto& primitiveItem : primitivesArray) {
2677 if (!checkIsObject(loadData, "mesh primitive item", primitiveItem)) {
2678 return false;
2679 }
2680 if (!buildPrimitiveMesh(loadData, modelMeshInstances.emplace_back(), model, primitiveItem.GetObject(), skinIx, clockwise, meshoptDecomp)) {
2681 return false;
2682 }
2683 }
2684 }
2685 else {
2686 LOG_ERROR(logger, "%.*s: Mesh %u has no primitives property", StringViewFormat(loadData.path), gltfMeshIx);
2687 return false;
2688 }
2689
2690 return true;
2691 }
2692
2693 bool loadNode(Context* context,
2694 GltfModelDefinition& definition,
2695 GltfNode& node,
2696 Model& model,
2697 uint32_t parent_part,
2698 const glm::mat4& parent_transform,
2699 MeshoptDecompressor & meshoptDecomp)
2700 {
2701 const bool clockwise = glm::determinant(node.mat) < 0.0f;
2702 const glm::mat4& localTransform = node.mat;
2703 const glm::mat4 globalTransform = parent_transform * localTransform;
2704
2705 std::string nodeName = std::to_string(node.node_index) + "_" + node.name;
2706 uint32_t partIndex = (uint32_t)model.parts.size();
2707 node.part_index = partIndex;
2708
2709 ModelPart& nodePart = model.parts.emplace_back();
2710 nodePart.parentIndex = parent_part;
2711 model.setPartTransform(nodePart, localTransform);
2712 model.setPartName(nodePart, nodeName);
2713
2714 if (node.mesh != (uint32_t)-1) {
2715 std::vector<ModelMeshInstance> meshes;
2716 if (!buildMeshes(definition, meshes, model, node.skin, node.mesh, clockwise, meshoptDecomp)) {
2717 return false;
2718 }
2719
2720 // If it is just a single mesh, we attach the mesh directly to this node
2721 if (meshes.size() == 1) {
2722 nodePart.meshIndex = meshes[0].mesh.meshIx;
2723 nodePart.materialIndex = meshes[0].materialIx;
2724 nodePart.boundsIndex = meshes[0].mesh.bboxIx;
2725 nodePart.vertexCount = meshes[0].mesh.vertexCount;
2726 }
2727 else {
2728 // If there are multiple meshes, we create a child part for each mesh
2729 for (auto& instance : meshes) {
2730 ModelPart& part = model.parts.emplace_back();
2731 part.parentIndex = partIndex;
2732 part.meshIndex = instance.mesh.meshIx;
2733 part.materialIndex = instance.materialIx;
2734 part.boundsIndex = instance.mesh.bboxIx;
2735 part.vertexCount = instance.mesh.vertexCount;
2736 }
2737 }
2738 }
2739
2740 for (uint32_t child_index : node.children) {
2741 if (!loadNode(context, definition, definition.nodes[child_index], model, partIndex, globalTransform, meshoptDecomp)) {
2742 return false;
2743 }
2744 }
2745
2746 return true;
2747 }
2748
2749 bool loadAnimation(Context* context, GltfModelDefinition& definition, Model& model, MeshoptDecompressor& meshoptDecomp)
2750 {
2751 if (definition.animations.size()) model.animation = context->animationManager->create();
2752
2753 auto& animation = *model.animation.resolve();
2754 auto& skeleton = model.skeleton;
2755
2756 for (auto& animationValue : definition.animations) {
2757 auto& clip = animation.clips.emplace_back();
2758
2759 clip.name = animationValue.name;
2760 clip.duration = 0.0f;
2761 clip.resolution = 1.0f;
2762
2763 uint32_t prev_node = (decltype(prev_node))-1;
2764 for (uint32_t i = 0; i < animationValue.channels.size(); i++) {
2765 GltfChannel& channel = animationValue.channels[i];
2766 GltfSampler& sampler = animationValue.samplers[channel.sampler];
2767 uint32_t nodeIndex = channel.node;
2768 GltfNode& node = definition.nodes[nodeIndex];
2769 uint32_t boneIndex = node.bone_index;
2770
2771 if (sampler.interpolation != "" && sampler.interpolation != "LINEAR") // TODO
2772 LOG_ERROR(logger, "Unable to handle animation interpolation %s", sampler.interpolation.c_str());
2773
2774 auto& inputAccessor = definition.accessors[sampler.input];
2775
2776 // Meshopt compressed?
2777 if (meshoptDecomp.isCompressed(definition, sampler.input)) {
2778 bool ok = meshoptDecomp.decompress(definition, sampler.input);
2779 if (!ok) {
2780 return false;
2781 }
2782 }
2783
2784 BufferDataView dataView;
2785 if (!getBufferViewData(definition, dataView, inputAccessor)) {
2786 return false;
2787 }
2788
2789 if (!dataView.data || !dataView.count) {
2790 return false;
2791 }
2792
2793 float t_max = inputAccessor.max.s_float[0];
2794 //float t_min = input_accessor.min.s_float[0];
2795 clip.duration = std::max(t_max, clip.duration);
2796
2797 if (prev_node != nodeIndex) {
2798 clip.tracks.push_back({});
2799 }
2800 prev_node = nodeIndex;
2801 auto& track = clip.tracks.back();
2802 track.boneIndex = boneIndex;
2803
2804 skeleton.bones[boneIndex].animated = true;
2805
2806 auto getTime = [](uint32_t i, const uint8_t* iData, const size_t iStride) {
2807 return *reinterpret_cast<const float*>(iData + iStride * i);
2808 };
2809
2810 if (definition.accessors.size() <= sampler.output) {
2811 return false;
2812 }
2813
2814 const GltfAccessor& outputAccessor = definition.accessors[sampler.output];
2815
2816 // Meshopt compressed? Not supported yet.
2817 if (meshoptDecomp.isCompressed(definition, sampler.output)) {
2818 bool ok = meshoptDecomp.decompress(definition, sampler.output);
2819 if (!ok) {
2820 return false;
2821 }
2822 }
2823
2824 BufferDataView oDataView;
2825 if (!getBufferViewData(definition, oDataView, outputAccessor)) {
2826 return false;
2827 }
2828
2829 if (channel.path == "translation") {
2830 assert(outputAccessor.type == AccessorType::Vec3);
2831 track.translations.resize(dataView.count);
2832
2833 for (uint32_t j = 0; j < dataView.count; j++) {
2834 const float time = getTime(j, dataView.data, dataView.stride);
2835 glm::vec3 translation = *reinterpret_cast<const glm::vec3*>(oDataView.data + oDataView.stride * j);
2836 track.translations[j] = TranslationKey{ time, translation };
2837 }
2838 }
2839 else if (channel.path == "scale") {
2840 assert(outputAccessor.type == AccessorType::Vec3);
2841 track.scales.resize(dataView.count);
2842
2843 for (uint32_t j = 0; j < dataView.count; j++) {
2844 const float time = getTime(j, dataView.data, dataView.stride);
2845 glm::vec3 scale = *reinterpret_cast<const glm::vec3*>(oDataView.data + oDataView.stride * j);
2846 track.scales[j] = ScaleKey{ time, scale };
2847 }
2848 }
2849 else if (channel.path == "rotation") {
2850 assert(outputAccessor.type == AccessorType::Vec4);
2851 track.rotations.resize(dataView.count);
2852
2853 if (outputAccessor.componentType == AccessorComponentType::Float) {
2854 for (uint32_t j = 0; j < dataView.count; j++) {
2855 const float time = getTime(j, dataView.data, dataView.stride);
2856 glm::vec4 rot = *reinterpret_cast<const glm::vec4*>(oDataView.data + oDataView.stride * j);
2857 track.rotations[j] = RotationKey{ time, glm::quat(rot.w, rot.x, rot.y, rot.z) };
2858 }
2859 }
2860 }
2861 else {
2862 LOG_ERROR(logger, "Unknown animation channel path: %s", channel.path.c_str());
2863 }
2864 }
2865 }
2866 if (model.animation) {
2867 model.animation->skeleton = model.skeleton;
2868 }
2869 return true;
2870 }
2871};
2872
2873namespace Cogs::Core::GltfLoader {
2874 bool
2875 GltfLoader::canLoad(Context* context, const ModelLoadInfo& loadInfo)
2876 {
2877 // If loader is disabled, just return false.
2878 if (Variable* enable = context->variables->get(enableVariableName); !enable->isEmpty() && enable->getBool() == false) {
2879 return false;
2880 }
2881
2882 // If 'resourcePath' is an URL then it might contain parameters which will mess
2883 // up the extension-test. Remove all parameters -- if found.
2884 std::string resourcePath = loadInfo.resourcePath;
2885 if (resourcePath.starts_with("http")) {
2886 size_t paramPos = resourcePath.find('?');
2887 if (paramPos != std::string::npos) {
2888 resourcePath = resourcePath.substr(0, paramPos);
2889 }
2890 }
2891
2892 auto extension = Cogs::IO::extension(resourcePath);
2893 if (extension.starts_with(".gltf")) return true;
2894 if (extension.starts_with(".glb")) return true;
2895 if (extension.starts_with(".b3dm")) return true; // B3DM is just a wrapped glTF/glb file
2896 return false;
2897 }
2898
2906 size_t GltfLoader::getPotentialB3DMOffset(uint8_t* content, size_t size)
2907 {
2908 struct B3DMHeader {
2909 uint32_t magic;
2910 uint32_t version;
2911 uint32_t length;
2912 uint32_t featureTableJSONLength;
2913 uint32_t featureTableBinaryLength;
2914 uint32_t batchTableJSONLength;
2915 uint32_t batchTableBinaryLength;
2916 };
2917
2918 const uint32_t b3dmMagic = uint32_t('b') + uint32_t('3' << 8) + uint32_t('d' << 16) + uint32_t('m' << 24);
2919 B3DMHeader* header = reinterpret_cast<B3DMHeader*>(content);
2920
2921 if (header->magic != b3dmMagic) {
2922 return 0; // This is not a B3DM file
2923 }
2924
2925 assert(header->length <= size && "Invalid length value in header");
2926
2927 if (header->version != 1) {
2928 LOG_ERROR(logger, "The B3DM file is not version 1.0");
2929 return 0;
2930 }
2931
2932 uint32_t featureTableStart = sizeof(B3DMHeader);
2933 uint32_t batchTableStart = featureTableStart + header->featureTableJSONLength + header->featureTableBinaryLength;
2934 uint32_t glbStart = batchTableStart + header->batchTableJSONLength + header->batchTableBinaryLength;
2935
2936 assert(glbStart < size && "Offset out of bounds");
2937 return glbStart;
2938 }
2939
2940 bool GltfLoader::load(Context* context, const ModelLoadInfo& loadInfo, std::unique_ptr<Cogs::FileContents> contents)
2941 {
2942 LOG_TRACE(logger, "Loading model: '%s'", loadInfo.resourceName.c_str());
2943
2944 Cogs::Memory::MemoryBuffer content = contents->take();
2945
2946 if (!content.size()) {
2947 LOG_ERROR(logger, "Could not open file '%s'.", loadInfo.resourcePath.c_str());
2948 return false;
2949 }
2950
2951 // If this file is actually a B3DM file, we'll calculate the amount of bytes to skip to
2952 // load the actual GLTF data.
2953 const size_t fileOffset = getPotentialB3DMOffset(static_cast<uint8_t*>(content.data()), content.size());
2954 const uint8_t* dataPtr = (uint8_t*)(content.data()) + fileOffset;
2955
2956 struct GlbHeader
2957 {
2958 uint32_t magic;
2959 uint32_t version;
2960 uint32_t length;
2961 };
2962
2963 struct GlbChunkHeader
2964 {
2965 uint32_t length;
2966 uint32_t type;
2967 };
2968
2969 struct GlbChunk
2970 {
2971 GlbChunkHeader header;
2972 char data[1];
2973 };
2974
2975 JsonParseFlags flags = JsonParseFlags::NoCachedContent;
2976 const Cogs::StringView path = loadInfo.resourcePath;
2977
2978 Document document;
2979 const GlbHeader* header = (const GlbHeader*) dataPtr;
2980 GltfModelDefinition loadData(context, loadInfo.resourcePath);
2981 loadData.default_material = -1;
2982 if (auto var = context->variables->get(optimizeVariableName); !var->isEmpty()) {
2983 loadData.optimizationLevel = unsigned(std::max(0, var->getInt()));
2984 }
2985
2986 std::span<const uint8_t> glb_data;
2987 if (sizeof(GlbHeader) <= content.size() && header->magic == 0x46546C67) {
2988 loadData.version = header->version * 100;
2989
2990 if (header->version != 2) {
2991 LOG_ERROR(logger, "Unable to load glb format %d", loadData.version);
2992 return false;
2993 }
2994
2995 size_t offset = sizeof(GlbHeader);
2996 uint32_t i = 0;
2997 while (offset < (content.size() - fileOffset)) {
2998 const GlbChunk* chunk = (GlbChunk*)(reinterpret_cast<const char*>(dataPtr) + offset);
2999 if (chunk->header.type == 0x4E4F534A)
3000 document = parseJson(Cogs::StringView(chunk->data, chunk->header.length), flags);
3001 else if (chunk->header.type == 0x004E4942)
3002 glb_data = std::span((const uint8_t*)chunk->data, (const uint8_t*)chunk->data + chunk->header.length);
3003 else {
3004 LOG_ERROR(logger, "Unknown glb chunk type 0x%x (index %d)", chunk->header.type, i);
3005 }
3006 offset += chunk->header.length + sizeof(GlbChunkHeader);
3007 i++;
3008 }
3009 }
3010 else {
3011 document = parseJson(Cogs::StringView(reinterpret_cast<const char*>(dataPtr), content.size() - fileOffset), flags);
3012 }
3013
3014 if (!document.IsObject()) {
3015 LOG_ERROR(logger, "Could not load asset file %s: JSON root is not an object.", loadInfo.resourceName.c_str());
3016 return false;
3017 }
3018
3019 auto& modelManager = *context->modelManager;
3020 auto model = modelManager.lock(loadInfo.handle);
3021
3022 MeshoptDecompressor meshoptDecomp;
3023
3024 auto root = document.GetObject();
3025
3026 // Asset
3027 if (const auto& assetItem = root.FindMember("asset"); assetItem != root.MemberEnd()) {
3028 if (!parseAsset(loadData, path, assetItem->value)) return false;
3029 }
3030 else {
3031 LOG_ERROR(logger, "%.*s: No required JSON asset member", StringViewFormat(path));
3032 return false;
3033 }
3034
3035 if (const auto& buffersItem = root.FindMember("buffers"); buffersItem != root.MemberEnd()) {
3036 if (!parseBuffers(context, loadData, path, buffersItem->value, glb_data)) {
3037 return false;
3038 }
3039 }
3040
3041 if (const auto& bufferViewsItem = root.FindMember("bufferViews"); bufferViewsItem != root.MemberEnd()) {
3042 if (!parseBufferViews(loadData, path, bufferViewsItem->value, meshoptDecomp)) {
3043 return false;
3044 }
3045 }
3046
3047 if (const auto& imagesItem = root.FindMember("images"); imagesItem != root.MemberEnd()) {
3048 if (!parseImages(context, loadData, imagesItem->value)) {
3049 return false;
3050 }
3051 }
3052
3053 if (const auto& samplersItem = root.FindMember("samplers"); samplersItem != root.MemberEnd()) {
3054 if (!parseSamplers(loadData, path, samplersItem->value)) {
3055 return false;
3056 }
3057 }
3058
3059 if (const auto& texturesItem = root.FindMember("textures"); texturesItem != root.MemberEnd()) {
3060 if (!parseTextures(loadData, path, texturesItem->value)) {
3061 return false;
3062 }
3063 }
3064
3065 if (auto materialsItem = root.FindMember("materials"); materialsItem != root.MemberEnd()) {
3066 if (!checkIsArray(loadData, *materialsItem)) return false;
3067 for (const Value& materialsElement : materialsItem->value.GetArray()) {
3068 if (!checkIsObject(loadData, "materials element", materialsElement)) {
3069 return false;
3070 }
3071 loadData.materialsArray.emplace_back(materialsElement.GetObject());
3072 }
3073 }
3074
3075 if (auto meshesItem = root.FindMember("meshes"); meshesItem != root.MemberEnd()) {
3076 if (!checkIsArray(loadData, *meshesItem)) return false;
3077 for (const Value& meshesElement : meshesItem->value.GetArray()) {
3078 if (!checkIsObject(loadData, "meshes element", meshesElement)) {
3079 return false;
3080 }
3081 loadData.meshesArray.emplace_back(meshesElement.GetObject());
3082 }
3083 }
3084
3085 for (const auto& section : root) {
3086 switch (toView(section.name).hash()) {
3087 case Cogs::hash("asset"):
3088 case Cogs::hash("buffers"):
3089 case Cogs::hash("bufferViews"):
3090 case Cogs::hash("images"):
3091 case Cogs::hash("samplers"):
3092 case Cogs::hash("textures"):
3093 case Cogs::hash("materials"):
3094 case Cogs::hash("meshes"):
3095 case Cogs::hash("cameras"): // ignored for now
3096 break;
3097
3098 case Cogs::hash("nodes"):
3099 if (!parseNodes(loadData, path, section.value)) return false;
3100 break;
3101
3102 case Cogs::hash("scenes"):
3103 if (!parseScenes(loadData, path, section.value)) return false;
3104 break;
3105
3106 case Cogs::hash("scene"):
3107 loadData.load_scene = section.value.GetUint();
3108 break;
3109
3110 case Cogs::hash("accessors"):
3111 if (!checkIsArray(loadData, section)) return false;
3112 if (!parseAccessors(loadData, path, section.value)) return false;
3113 break;
3114
3115 case Cogs::hash("skins"):
3116 if (!parseSkins(loadData, path, section.value)) return false;
3117 break;
3118
3119 case Cogs::hash("animations"):
3120 if (!parseAnimations(loadData, path, section.value)) return false;
3121 break;
3122
3123 case Cogs::hash("extensionsRequired"):
3124 for (auto& extension : section.value.GetArray()) {
3125 if (extension.GetString() == Cogs::StringView("KHR_materials_pbrSpecularGlossiness") ||
3126 extension.GetString() == Cogs::StringView("KHR_draco_mesh_compression") ||
3127 extension.GetString() == Cogs::StringView("KHR_mesh_quantization") ||
3128 extension.GetString() == Cogs::StringView("KHR_materials_unlit") ||
3129 extension.GetString() == Cogs::StringView("KHR_texture_transform") ||
3130 extension.GetString() == Cogs::StringView("KHR_texture_basisu") || // NOTE: The KTX v2 image format is not supported yet.
3131 extension.GetString() == Cogs::StringView("EXT_meshopt_compression")) {
3132 // https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression
3133 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression
3134 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_unlit
3135 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization
3136 // https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform
3137 // LOG_DEBUG(logger, "Required extension used: %s", extension.GetString());
3138 }
3139 else {
3140 LOG_ERROR(logger, "Unknown required extension: %s", extension.GetString());
3141 return false;
3142 }
3143 }
3144 break;
3145
3146 case Cogs::hash("extensions"):
3147 case Cogs::hash("extensionsUsed"):
3148 if (section.value.IsArray()) {
3149 for (auto& extension : section.value.GetArray()) {
3150 if (extension.GetString() == Cogs::StringView("KHR_materials_pbrSpecularGlossiness") ||
3151 extension.GetString() == Cogs::StringView("KHR_materials_specular") ||
3152 extension.GetString() == Cogs::StringView("KHR_materials_ior") ||
3153 extension.GetString() == Cogs::StringView("KHR_draco_mesh_compression") ||
3154 extension.GetString() == Cogs::StringView("KHR_mesh_quantization") ||
3155 extension.GetString() == Cogs::StringView("KHR_texture_transform") ||
3156 extension.GetString() == Cogs::StringView("KHR_texture_basisu") || // NOTE: The KTX v2 image format is not supported yet.
3157 extension.GetString() == Cogs::StringView("EXT_meshopt_compression") ||
3158 extension.GetString() == Cogs::StringView("KHR_materials_unlit")) {
3159 // LOG_DEBUG(logger, "Extension used: %s", extension.GetString());
3160 }
3161 else {
3162 LOG_WARNING(logger, "Unknown extension: %s", extension.GetString());
3163 }
3164 }
3165 }
3166 else if (section.value.IsObject()) {
3167 for (const auto& extension : section.value.GetObject()) {
3168 LOG_WARNING(logger, "Unknown extension: %s", extension.name.GetString());
3169 }
3170 }
3171 else {
3172 LOG_WARNING(logger, "Unknown extension: %s", section.value.GetString());
3173 }
3174 break;
3175
3176 default:
3177 LOG_WARNING(logger, "%.*s: Unrecognized root key %s", StringViewFormat(path), section.name.GetString());
3178 break;
3179 }
3180 }
3181
3182 uint32_t sceneIndex = 0;
3183 for (auto& scene : loadData.scenes) {
3184 if (sceneIndex > 1) {
3185 LOG_ERROR(logger, "Loader unable to handle more than one scene.");
3186 break;
3187 }
3188
3189 model->setName(scene.name);
3190
3191 //NOTE: We insert a virtual root node rotating the models from the Y-up system used by glTF to our
3192 // Z-up convention. If other usage is desired we could replace this with a more configurable approach.
3193 auto& root_ = model->parts.emplace_back();
3194 model->setPartTransform(root_, glm::rotate(glm::mat4(1.0f), glm::half_pi<float>(), glm::vec3(1, 0, 0)));
3195 model->setPartName(root_, "CogsRoot");
3196
3197 // Leave the model root transform alone, used to calculate internal model transforms.
3198 glm::mat4 rootTransform(1.0f);
3199
3200 for (uint32_t nodeIndex : scene.nodes) {
3201 assert(nodeIndex < loadData.nodes.size());
3202
3203 if (!loadBone(loadData, loadData.nodes[nodeIndex], model->skeleton, (size_t)-1, rootTransform)) {
3204 return false;
3205 }
3206
3207 if (!loadJoints(loadData, loadData.nodes[nodeIndex], model->skeleton, meshoptDecomp)) {
3208 return false;
3209 }
3210
3211 // Load root nodes from glTF using our virtual root node with index==0 as parent.
3212 if (!loadNode(context, loadData, loadData.nodes[nodeIndex], *model, 0, rootTransform, meshoptDecomp)) {
3213 return false;
3214 }
3215 }
3216
3217 if (!loadAnimation(context, loadData, *model, meshoptDecomp)) {
3218 return false;
3219 }
3220 }
3221
3222 for (auto const & it : loadData.debugMessages) {
3223 LOG_TRACE(logger, "%s (x %u)", it.first.c_str(), it.second);
3224 }
3225 return true;
3226 }
3227
3228 bool GltfLoader::load(Context* context, const ModelLoadInfo& loadInfo)
3229 {
3230 JsonParseFlags flags = JsonParseFlags::NoCachedContent;
3231#ifdef EMSCRIPTEN
3232 const auto resourceFlags = ResourceStoreFlags::None;
3233#else
3234 const auto resourceFlags = (flags & JsonParseFlags::NoCachedContent) == JsonParseFlags::NoCachedContent
3236 : ResourceStoreFlags::None;
3237#endif
3238
3239 std::unique_ptr<Cogs::FileContents> contents;
3240#ifdef COGS_USE_MMAP
3241 if (loadInfo.protocol == ResourceProtocol::Archive) {
3242#endif
3243 ResourceBuffer buffer = context->resourceStore->getResourceContents(loadInfo.resourcePath);
3244 if (!buffer.buffer->data()) {
3245 LOG_ERROR(logger, "Could not load resource data for model %s.", loadInfo.resourcePath.c_str());
3246 return false;
3247 }
3248 contents = std::make_unique<Cogs::Platform::ResourceBufferBackedFileContents>(buffer, loadInfo.resourcePath, Cogs::FileContentsHints::None);
3249#ifdef COGS_USE_MMAP
3250 }
3251 else {
3252 Cogs::FileHandle::Ptr file = Cogs::IO::openFile(loadInfo.resourcePath, Cogs::FileHandle::OpenMode::OpenAlways, Cogs::FileHandle::AccessMode::Read);
3253 if (!file) {
3254 LOG_ERROR(logger, "Could not open file %s for mapping.", loadInfo.resourcePath.c_str());
3255 return false;
3256 }
3257
3258 size_t size = Cogs::IO::fileSize(file);
3259 if (!size) {
3260 LOG_ERROR(logger, "File size invalid: %zd.", size);
3261 return false;
3262 }
3263
3264 const uint8_t* ptr = static_cast<const uint8_t*>(Cogs::IO::mapFile(file, Cogs::FileHandle::ProtectionMode::ReadOnly, 0, size));
3265 if (!ptr) {
3266 LOG_ERROR(logger, "Could not map file %s for reading model data.", loadInfo.resourcePath.c_str());
3267 return false;
3268 }
3269
3270 contents = std::make_unique<Cogs::MMapBackedFileContents>(ptr, size, file, loadInfo.resourcePath, Cogs::FileContentsHints::None);
3271 }
3272#endif
3273
3274 const Cogs::StringView path = loadInfo.resourcePath;
3275 const ResourceBuffer content = context->resourceStore->getResourceContents(path, resourceFlags);
3276
3277 return load(context, loadInfo, std::move(contents));
3278 }
3279
3280 inline GltfModelDefinition::GltfModelDefinition(Context* context, Cogs::StringView path) :
3281 context(context),
3282 path(path)
3283 {
3284 timer = Cogs::Timer::startNew();
3285 rootTask = context->taskManager->createGroup(context->taskManager->ResourceQueue);
3286 }
3287
3288 inline GltfModelDefinition::~GltfModelDefinition()
3289 {
3290 context->taskManager->wait(rootTask);
3291 for (auto& image : images) {
3292 if (image.decoded.data) {
3293 stbi_image_free(image.decoded.data);
3294 image.decoded.data = nullptr;
3295 }
3296 }
3297 LOG_TRACE(logger, "Gltf elapsed: %f.", timer.elapsedSeconds());
3298 }
3299}
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:140
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:50
@ 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:181
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.
PrimitiveType
Primitive types for interpreting vertex data sent to the graphics pipeline.
Definition: Common.h:112
@ PointList
List of points.
@ TriangleStrip
Triangle strip.
@ LineList
List of lines.
@ LineStrip
Line strip.
@ TriangleList
List of triangles.
@ 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
@ 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