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