From ac11a14509067604df23834d993a6251962db043 Mon Sep 17 00:00:00 2001 From: JosiahWI <41302989+JosiahWI@users.noreply.github.com> Date: Mon, 2 Sep 2024 07:50:30 -0500 Subject: [PATCH] Add static glTF support (#14557) Co-authored-by: Lars Mueller Co-authored-by: jordan4ibanez Co-authored-by: sfan5 Co-authored-by: SmallJoker --- CMakeLists.txt | 2 + doc/lua_api.md | 39 +- games/devtest/mods/gltf/LICENSE.md | 14 + games/devtest/mods/gltf/init.lua | 51 + games/devtest/mods/gltf/invalid/empty.gltf | 0 .../invalid/invalid_bufferview_bounds.gltf | 1 + .../mods/gltf/invalid/json_missing_brace.gltf | 1 + games/devtest/mods/gltf/mod.conf | 2 + .../mods/gltf/models/gltf_blender_cube.gltf | 1 + .../gltf_blender_cube_matrix_transform.gltf | 1 + .../gltf/models/gltf_blender_cube_scaled.gltf | 1 + games/devtest/mods/gltf/models/gltf_frog.gltf | 1 + .../gltf/models/gltf_minimal_triangle.gltf | 1 + .../models/gltf_simple_sparse_accessor.gltf | 1 + .../mods/gltf/models/gltf_snow_man.gltf | 1 + .../devtest/mods/gltf/models/gltf_spider.gltf | 1 + .../gltf_triangle_with_vertex_stride.gltf | 1 + .../models/gltf_triangle_without_indices.gltf | 1 + .../devtest/mods/gltf/textures/gltf_cube.png | Bin 0 -> 203 bytes .../devtest/mods/gltf/textures/gltf_frog.png | Bin 0 -> 272 bytes .../mods/gltf/textures/gltf_snow_man.png | Bin 0 -> 205 bytes .../mods/gltf/textures/gltf_spider.png | Bin 0 -> 10957 bytes irr/include/IMesh.h | 11 + irr/include/ISkinnedMesh.h | 3 + irr/include/SMesh.h | 14 + irr/include/SSkinMeshBuffer.h | 11 +- irr/include/irrArray.h | 4 + irr/include/irrString.h | 13 +- irr/src/CGLTFMeshFileLoader.cpp | 695 +++++++++ irr/src/CGLTFMeshFileLoader.h | 147 ++ irr/src/CMakeLists.txt | 10 +- irr/src/CSceneManager.cpp | 2 + irr/src/CSkinnedMesh.cpp | 17 + irr/src/CSkinnedMesh.h | 9 + lib/tiniergltf/.gitignore | 6 + lib/tiniergltf/CMakeLists.txt | 22 + lib/tiniergltf/Readme.md | 39 + lib/tiniergltf/tiniergltf.hpp | 1357 +++++++++++++++++ src/client/client.cpp | 2 +- src/client/content_cao.cpp | 21 +- src/client/content_mapblock.cpp | 5 +- src/client/mesh.cpp | 2 +- src/gui/guiFormSpecMenu.cpp | 9 +- src/server.cpp | 2 +- src/unittest/CMakeLists.txt | 2 +- src/unittest/test_irr_gltf_mesh_loader.cpp | 366 +++++ src/unittest/test_servermodmanager.cpp | 2 +- 47 files changed, 2863 insertions(+), 28 deletions(-) create mode 100644 games/devtest/mods/gltf/LICENSE.md create mode 100644 games/devtest/mods/gltf/init.lua create mode 100644 games/devtest/mods/gltf/invalid/empty.gltf create mode 100644 games/devtest/mods/gltf/invalid/invalid_bufferview_bounds.gltf create mode 100644 games/devtest/mods/gltf/invalid/json_missing_brace.gltf create mode 100644 games/devtest/mods/gltf/mod.conf create mode 100644 games/devtest/mods/gltf/models/gltf_blender_cube.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_blender_cube_matrix_transform.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_blender_cube_scaled.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_frog.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_minimal_triangle.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_simple_sparse_accessor.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_snow_man.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_spider.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_triangle_with_vertex_stride.gltf create mode 100644 games/devtest/mods/gltf/models/gltf_triangle_without_indices.gltf create mode 100644 games/devtest/mods/gltf/textures/gltf_cube.png create mode 100644 games/devtest/mods/gltf/textures/gltf_frog.png create mode 100644 games/devtest/mods/gltf/textures/gltf_snow_man.png create mode 100644 games/devtest/mods/gltf/textures/gltf_spider.png create mode 100644 irr/src/CGLTFMeshFileLoader.cpp create mode 100644 irr/src/CGLTFMeshFileLoader.h create mode 100644 lib/tiniergltf/.gitignore create mode 100644 lib/tiniergltf/CMakeLists.txt create mode 100644 lib/tiniergltf/Readme.md create mode 100644 lib/tiniergltf/tiniergltf.hpp create mode 100644 src/unittest/test_irr_gltf_mesh_loader.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6623fa828..54a830089 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -283,6 +283,8 @@ if(BUILD_UNITTESTS OR BUILD_BENCHMARKS) add_subdirectory(lib/catch2) endif() +add_subdirectory(lib/tiniergltf) + # Subdirectories # Be sure to add all relevant definitions above this add_subdirectory(src) diff --git a/doc/lua_api.md b/doc/lua_api.md index e6a351918..6989f3483 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -274,7 +274,7 @@ Accepted formats are: images: .png, .jpg, .tga, (deprecated:) .bmp sounds: .ogg vorbis - models: .x, .b3d, .obj + models: .x, .b3d, .obj, .gltf (Minetest 5.10 or newer) Other formats won't be sent to the client (e.g. you can store .blend files in a folder for convenience, without the risk that such files are transferred) @@ -291,6 +291,43 @@ in one of its parents, the parent's file is used. Although it is discouraged, a mod can overwrite a media file of any mod that it depends on by supplying a file with an equal name. +Only a subset of model file format features is supported: + +Simple textured meshes (with multiple textures), optionally with normals. +The .x and .b3d formats additionally support skeletal animation. + +#### glTF + +The glTF model file format for now only serves as a +more modern alternative to the other static model file formats; +it unlocks no special rendering features. + +This means that many glTF features are not supported *yet*, including: + +* Animation +* Cameras +* Materials + * Only base color textures are supported + * Backface culling is overridden + * Double-sided materials don't work +* Alternative means of supplying data + * Embedded images + * References to files via URIs + +Textures are supplied solely via the same means as for the other model file formats: +The `textures` object property, the `tiles` node definition field and +the list of textures used in the `model[]` formspec element. + +The order in which textures are to be supplied +is that in which they appear in the `textures` array in the glTF file. + +Do not rely on glTF features not being supported; they may be supported in the future. +The backwards compatibility guarantee does not extend to ignoring unsupported features. + +For example, if your model used an emissive material, +you should expect that a future version of Minetest may respect this, +and thus cause your model to render differently there. + Naming conventions ------------------ diff --git a/games/devtest/mods/gltf/LICENSE.md b/games/devtest/mods/gltf/LICENSE.md new file mode 100644 index 000000000..b0ae5fef5 --- /dev/null +++ b/games/devtest/mods/gltf/LICENSE.md @@ -0,0 +1,14 @@ +glTF test model (and corresponding texture) licenses: + +* Spider (`gltf_spider.gltf`, `gltf_spider.png`): + * By [archfan7411](https://github.com/archfan7411) + * Licensed under CC0, public domain "wherever public domain carries fewer rights or legal protections" +* Frog (`gltf_frog.gltf`, `gltf_frog.png`): + * By [Susybaka1234](https://sketchfab.com/3d-models/african-clawed-frog-v2-c81152c93948480c931c280d18957358) + * Licensed under CC-BY 4.0 +* Snow Man (`gltf_snow_man.gltf`, `gltf_snow_man.png`): + * By [jordan4ibanez](https://github.com/jordan4ibanez) + * Licensed under CC0 +* Minimal triangle, triangle without indices (`gltf_minimal_triangle.gltf`, `gltf_triangle_without_indices.gltf`) + * From [the glTF sample model collection](https://github.com/KhronosGroup/glTF-Sample-Models) + * Licensed under CC0 / public domain diff --git a/games/devtest/mods/gltf/init.lua b/games/devtest/mods/gltf/init.lua new file mode 100644 index 000000000..b5c2032bc --- /dev/null +++ b/games/devtest/mods/gltf/init.lua @@ -0,0 +1,51 @@ +local function register_entity(name, textures, backface_culling) + minetest.register_entity("gltf:" .. name, { + initial_properties = { + visual = "mesh", + mesh = "gltf_" .. name .. ".gltf", + textures = textures, + backface_culling = backface_culling, + }, + }) +end + +-- These do not have texture coordinates; they simple render as black surfaces. +register_entity("minimal_triangle", {}, false) +register_entity("triangle_with_vertex_stride", {}, false) +register_entity("triangle_without_indices", {}, false) +do + local cube_textures = {"gltf_cube.png"} + register_entity("blender_cube", cube_textures) + register_entity("blender_cube_scaled", cube_textures) + register_entity("blender_cube_matrix_transform", cube_textures) +end +register_entity("snow_man", {"gltf_snow_man.png"}) +register_entity("spider", {"gltf_spider.png"}) +-- Note: Model has an animation, but we can use it as a static test nevertheless +-- The claws rendering incorrectly from one side is expected behavior: +-- They use an unsupported double-sided material. +register_entity("frog", {"gltf_frog.png"}, false) + +minetest.register_node("gltf:frog", { + description = "glTF frog, but it's a node", + tiles = {{name = "gltf_frog.png", backface_culling = false}}, + drawtype = "mesh", + mesh = "gltf_frog.gltf", +}) + +minetest.register_chatcommand("show_model", { + params = " [textures]", + description = "Show a model (defaults to gltf models, for example '/show_model frog').", + func = function(name, param) + local model, textures = param:match"^(.-)%s+(.+)$" + if not model then + model = "gltf_" .. param .. ".gltf" + textures = "gltf_" .. param .. ".png" + end + minetest.show_formspec(name, "gltf:model", table.concat{ + "formspec_version[7]", + "size[10,10]", + "model[0,0;10,10;model;", model, ";", textures, ";0,0;true;true;0,0;0]", + }) + end, +}) diff --git a/games/devtest/mods/gltf/invalid/empty.gltf b/games/devtest/mods/gltf/invalid/empty.gltf new file mode 100644 index 000000000..e69de29bb diff --git a/games/devtest/mods/gltf/invalid/invalid_bufferview_bounds.gltf b/games/devtest/mods/gltf/invalid/invalid_bufferview_bounds.gltf new file mode 100644 index 000000000..2182861c6 --- /dev/null +++ b/games/devtest/mods/gltf/invalid/invalid_bufferview_bounds.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":0}}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA","byteLength":36}],"bufferViews":[{"buffer":0,"byteOffset":1,"byteLength":36,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/invalid/json_missing_brace.gltf b/games/devtest/mods/gltf/invalid/json_missing_brace.gltf new file mode 100644 index 000000000..98232c64f --- /dev/null +++ b/games/devtest/mods/gltf/invalid/json_missing_brace.gltf @@ -0,0 +1 @@ +{ diff --git a/games/devtest/mods/gltf/mod.conf b/games/devtest/mods/gltf/mod.conf new file mode 100644 index 000000000..3ec50d2ef --- /dev/null +++ b/games/devtest/mods/gltf/mod.conf @@ -0,0 +1,2 @@ +name = gltf +description = Hosts gltf test models, both for the C++ unit tests and for in-game viewing diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube.gltf b/games/devtest/mods/gltf/models/gltf_blender_cube.gltf new file mode 100644 index 000000000..041b4a1fc --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_blender_cube.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Cube","scale":[10,10,10]}],"meshes":[{"name":"Cube.004","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":288,"byteOffset":0},{"buffer":0,"byteLength":288,"byteOffset":288},{"buffer":0,"byteLength":192,"byteOffset":576},{"buffer":0,"byteLength":72,"byteOffset":768}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA"}]} diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube_matrix_transform.gltf b/games/devtest/mods/gltf/models/gltf_blender_cube_matrix_transform.gltf new file mode 100644 index 000000000..50235ceae --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_blender_cube_matrix_transform.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Cube","matrix":[1,0,0,0,0,2,0,0,0,0,3,0,4,5,6,1]}],"meshes":[{"name":"Cube.004","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":288,"byteOffset":0},{"buffer":0,"byteLength":288,"byteOffset":288},{"buffer":0,"byteLength":192,"byteOffset":576},{"buffer":0,"byteLength":72,"byteOffset":768}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA"}]} diff --git a/games/devtest/mods/gltf/models/gltf_blender_cube_scaled.gltf b/games/devtest/mods/gltf/models/gltf_blender_cube_scaled.gltf new file mode 100644 index 000000000..3b626b37e --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_blender_cube_scaled.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Cube","scale":[150,1,21.5]}],"meshes":[{"name":"Cube.004","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":288,"byteOffset":0},{"buffer":0,"byteLength":288,"byteOffset":288},{"buffer":0,"byteLength":192,"byteOffset":576},{"buffer":0,"byteLength":72,"byteOffset":768}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA"}]} diff --git a/games/devtest/mods/gltf/models/gltf_frog.gltf b/games/devtest/mods/gltf/models/gltf_frog.gltf new file mode 100644 index 000000000..201604fd3 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_frog.gltf @@ -0,0 +1 @@ +{"asset":{"version":"2.0","generator":"Blockbench 4.9.4 glTF exporter"},"scenes":[{"nodes":[20],"name":"blockbench_export"}],"scene":0,"nodes":[{"name":"cube","mesh":0},{"name":"cube","mesh":1},{"name":"cube","mesh":2},{"name":"body","children":[0,1,2]},{"translation":[0,0,-0.0625],"name":"cube","mesh":3},{"translation":[0.03125,0,-0.3125],"name":"cube","mesh":4},{"rotation":[0,-0.19509032201612825,0,0.9807852804032304],"translation":[0.01812248876854733,-0.0625,-0.25194388507103505],"name":"cube","mesh":5},{"translation":[0.0625,0,0.3125],"name":"leftleg","children":[4,5,6]},{"translation":[0.0625,0,-0.3125],"name":"cube","mesh":6},{"translation":[-0.03125,0,-0.3125],"name":"cube","mesh":7},{"rotation":[0,0.19509032201612825,0,0.9807852804032304],"translation":[-0.01812248876854733,-0.0625,-0.25194388507103505],"name":"cube","mesh":8},{"translation":[-0.0625,0,0.3125],"name":"rightleg","children":[8,9,10]},{"translation":[-0.125,-0.0625,0.125],"name":"cube","mesh":9},{"rotation":[0,0.5372996083468239,0,0.8433914458128857],"translation":[0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":10},{"rotation":[0,0.5372996083468239,0,0.8433914458128857],"translation":[0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":11},{"translation":[0.125,0.0625,-0.125],"name":"leftarm","children":[12,13,14]},{"translation":[0.125,-0.0625,0.125],"name":"cube","mesh":12},{"rotation":[0,-0.5372996083468239,0,0.8433914458128857],"translation":[-0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":13},{"rotation":[0,-0.5372996083468239,0,0.8433914458128857],"translation":[-0.10431178959951112,-0.0625,0.2349474087973531],"name":"cube","mesh":14},{"translation":[-0.125,0.0625,-0.125],"name":"rightarm","children":[16,17,18]},{"children":[3,7,11,15,19]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":840,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1128,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1416,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":1608,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":1680,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1968,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2256,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":2448,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":2520,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2808,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3096,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":3288,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":3360,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3648,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":3936,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":4128,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":4200,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":4488,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":4776,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":4968,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":5040,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":5328,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":5616,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":5808,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":5880,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":6168,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":6456,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":6648,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":6720,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7008,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7296,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":7488,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":7560,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":7848,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8136,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":8328,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":8400,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8688,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":8976,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":9168,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":9240,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":9528,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":9816,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":10008,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":10080,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":10368,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":10656,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":10848,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":10920,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":11208,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":11496,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":11688,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":11760,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":12048,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":12336,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":12528,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":12600,"byteLength":12},{"buffer":0,"byteOffset":12612,"byteLength":48},{"buffer":0,"byteOffset":12660,"byteLength":12},{"buffer":0,"byteOffset":12672,"byteLength":48},{"buffer":0,"byteOffset":12720,"byteLength":12},{"buffer":0,"byteOffset":12732,"byteLength":48},{"buffer":0,"byteOffset":12780,"byteLength":12},{"buffer":0,"byteOffset":12792,"byteLength":48},{"buffer":0,"byteOffset":12840,"byteLength":12},{"buffer":0,"byteOffset":12852,"byteLength":48},{"buffer":0,"byteOffset":12900,"byteLength":12},{"buffer":0,"byteOffset":12912,"byteLength":48},{"buffer":0,"byteOffset":12960,"byteLength":12},{"buffer":0,"byteOffset":12972,"byteLength":48},{"buffer":0,"byteOffset":13020,"byteLength":12},{"buffer":0,"byteOffset":13032,"byteLength":48},{"buffer":0,"byteOffset":13080,"byteLength":12},{"buffer":0,"byteOffset":13092,"byteLength":48},{"buffer":0,"byteOffset":13140,"byteLength":4},{"buffer":0,"byteOffset":13144,"byteLength":16},{"buffer":0,"byteOffset":13160,"byteLength":4},{"buffer":0,"byteOffset":13164,"byteLength":16}],"buffers":[{"byteLength":13180,"uri":"data:application/octet-stream;base64,AAAgPgAAAD4AAIA+AAAgPgAAAD4AAIC9AAAgPgAAAAAAAIA+AAAgPgAAAAAAAIC9AAAgvgAAAD4AAIC9AAAgvgAAAD4AAIA+AAAgvgAAAAAAAIC9AAAgvgAAAAAAAIA+AAAgvgAAAD4AAIC9AAAgPgAAAD4AAIC9AAAgvgAAAD4AAIA+AAAgPgAAAD4AAIA+AAAgvgAAAAAAAIA+AAAgPgAAAAAAAIA+AAAgvgAAAAAAAIC9AAAgPgAAAAAAAIC9AAAgvgAAAD4AAIA+AAAgPgAAAD4AAIA+AAAgvgAAAAAAAIA+AAAgPgAAAAAAAIA+AAAgPgAAAD4AAIC9AAAgvgAAAD4AAIC9AAAgPgAAAAAAAIC9AAAgvgAAAAAAAIC9AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgCAID4AgB8+AIAgPgAAADoAgF8+AIAfPgCAXz4AQKA+AIAgPgDA7z4AgCA+AECgPgCAXz4AwO8+AIBfPgDAnz4AgB8+AIAgPgCAHz4AwJ8+AAAAOgCAID4AAAA6AMDvPgAAADoAQKA+AAAAOgDA7z4AgB8+AECgPgCAHz4AQPA+AIAgPgDgHz8AgCA+AEDwPgCAXz4A4B8/AIBfPgCAID4AgCA+AMCfPgCAID4AgCA+AIBfPgDAnz4AgF8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAPgAAAD4AAIC9AAAAPgAAAD4AAKC+AAAAPgAAAAAAAIC9AAAAPgAAAAAAAKC+AAAAvgAAAD4AAKC+AAAAvgAAAD4AAIC9AAAAvgAAAAAAAKC+AAAAvgAAAAAAAIC9AAAAvgAAAD4AAKC+AAAAPgAAAD4AAKC+AAAAvgAAAD4AAIC9AAAAPgAAAD4AAIC9AAAAvgAAAAAAAIC9AAAAPgAAAAAAAIC9AAAAvgAAAAAAAKC+AAAAPgAAAAAAAKC+AAAAvgAAAD4AAIC9AAAAPgAAAD4AAIC9AAAAvgAAAAAAAIC9AAAAPgAAAAAAAIC9AAAAPgAAAD4AAKC+AAAAvgAAAD4AAKC+AAAAPgAAAAAAAKC+AAAAvgAAAAAAAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgBAsD4AAP89AECwPgAAADoAwM8+AAD/PQDAzz4AQIA+AECwPgDAvz4AQLA+AECAPgDAzz4AwL8+AMDPPgCAfz4AwK8+AIAAPgDArz4AgH8+AIBgPgCAAD4AgGA+AMC/PgCAYD4AQIA+AIBgPgDAvz4AwK8+AECAPgDArz4AQMA+AECwPgDA/z4AQLA+AEDAPgDAzz4AwP8+AMDPPgCAAD4AQLA+AIB/PgBAsD4AgAA+AMDPPgCAfz4AwM8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAPQAAAD4AAKA+AAAAPQAAAD4AAIA+AAAAPQAAAAAAAKA+AAAAPQAAAAAAAIA+AAAAvQAAAD4AAIA+AAAAvQAAAD4AAKA+AAAAvQAAAAAAAIA+AAAAvQAAAAAAAKA+AAAAvQAAAD4AAIA+AAAAPQAAAD4AAIA+AAAAvQAAAD4AAKA+AAAAPQAAAD4AAKA+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAAAAAAIA+AAAAPQAAAAAAAIA+AAAAvQAAAD4AAKA+AAAAPQAAAD4AAKA+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAPQAAAD4AAIA+AAAAvQAAAD4AAIA+AAAAPQAAAAAAAIA+AAAAvQAAAAAAAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDwPgAAAj0AwP8+AAACPQBA8D4AAL89AMD/PgAAvz0AIAg/AAACPQDgDz8AAAI9ACAIPwAAvz0A4A8/AAC/PQDgBz8AAPw8ACAAPwAA/DwA4Ac/AAAAOgAgAD8AAAA6AOAPPwAAADoAIAg/AAAAOgDgDz8AAPw8ACAIPwAA/DwAIBA/AAACPQDgFz8AAAI9ACAQPwAAvz0A4Bc/AAC/PQAgAD8AAAI9AOAHPwAAAj0AIAA/AAC/PQDgBz8AAL89AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAABgPgAAAD4AAEA+AABgPgAAAD4AAAAAAABgPgAAAAAAAEA+AABgPgAAAAAAAAAAAAAAvQAAAD4AAAAAAAAAvQAAAD4AAEA+AAAAvQAAAAAAAAAAAAAAvQAAAAAAAEA+AAAAvQAAAD4AAAAAAABgPgAAAD4AAAAAAAAAvQAAAD4AAEA+AABgPgAAAD4AAEA+AAAAvQAAAAAAAEA+AABgPgAAAAAAAEA+AAAAvQAAAAAAAAAAAABgPgAAAAAAAAAAAAAAvQAAAD4AAEA+AABgPgAAAD4AAEA+AAAAvQAAAAAAAEA+AABgPgAAAAAAAEA+AABgPgAAAD4AAAAAAAAAvQAAAD4AAAAAAABgPgAAAAAAAAAAAAAAvQAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDQPgBA0D4AwP8+AEDQPgBA0D4AwO8+AMD/PgDA7z4AICA/AEDQPgDgNz8AQNA+ACAgPwDA7z4A4Dc/AMDvPgDgHz8AwM8+ACAAPwDAzz4A4B8/AECgPgAgAD8AQKA+AOA/PwBAoD4AICA/AECgPgDgPz8AwM8+ACAgPwDAzz4AIDg/AEDQPgDgVz8AQNA+ACA4PwDA7z4A4Fc/AMDvPgAgAD8AQNA+AOAfPwBA0D4AIAA/AMDvPgDgHz8AwO8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAABgPgAAwD0AABA/AABgPgAAwD0AAKA+AABgPgAAAD0AABA/AABgPgAAAD0AAKA+AADAPQAAwD0AAKA+AADAPQAAwD0AABA/AADAPQAAAD0AAKA+AADAPQAAAD0AABA/AADAPQAAwD0AAKA+AABgPgAAwD0AAKA+AADAPQAAwD0AABA/AABgPgAAwD0AABA/AADAPQAAAD0AABA/AABgPgAAAD0AABA/AADAPQAAAD0AAKA+AABgPgAAAD0AAKA+AADAPQAAwD0AABA/AABgPgAAwD0AABA/AADAPQAAAD0AABA/AABgPgAAAD0AABA/AABgPgAAwD0AAKA+AADAPQAAwD0AAKA+AABgPgAAAD0AAKA+AADAPQAAAD0AAKA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AECgPgAgGD8AwN8+ACAYPwBAoD4A4B8/AMDfPgDgHz8AIAA/ACAYPwDgHz8AIBg/ACAAPwDgHz8A4B8/AOAfPwDA/z4A4Bc/AEDgPgDgFz8AwP8+AEDwPgBA4D4AQPA+AOAPPwBA8D4AIAA/AEDwPgDgDz8A4Bc/ACAAPwDgFz8AICA/ACAYPwDgLz8AIBg/ACAgPwDgHz8A4C8/AOAfPwBA4D4AIBg/AMD/PgAgGD8AQOA+AOAfPwDA/z4A4B8/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAMQjQPgAAQD4AAMA+MQjQPgAAQD4AAIA+MQjQPgAAgD0AAMA+MQjQPgAAgD0AAIA+AADQPgAAQD4AAIA+AADQPgAAQD4AAMA+AADQPgAAgD0AAIA+AADQPgAAgD0AAMA+AADQPgAAQD4AAIA+MQjQPgAAQD4AAIA+AADQPgAAQD4AAMA+MQjQPgAAQD4AAMA+AADQPgAAgD0AAMA+MQjQPgAAgD0AAMA+AADQPgAAgD0AAIA+MQjQPgAAgD0AAIA+AADQPgAAQD4AAMA+MQjQPgAAQD4AAMA+AADQPgAAgD0AAMA+MQjQPgAAgD0AAMA+MQjQPgAAQD4AAIA+AADQPgAAQD4AAIA+MQjQPgAAgD0AAIA+AADQPgAAgD0AAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgCAYD4AAH49AIBgPgAAADoAwI8+AAB+PQDAjz4AAIE9AIBgPgAA/z0AgGA+AACBPQDAjz4AAP89AMCPPgAAgT0AgF8+AAB+PQCAXz4AAIE9AIAgPgAAfj0AgCA+AACBPQCAID4AAH49AIAgPgAAgT0AgF8+AAB+PQCAXz4AgAA+AIBgPgAA/z0AgGA+AIAAPgDAjz4AAP89AMCPPgAAgT0AgGA+AAB+PQCAYD4AAIE9AMCPPgAAfj0AwI8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAvQAAAD4AAOA+AAAAvQAAAD4AAIA+AAAAvQAAAAAAAOA+AAAAvQAAAAAAAIA+AACQvgAAAD4AAIA+AACQvgAAAD4AAOA+AACQvgAAAAAAAIA+AACQvgAAAAAAAOA+AACQvgAAAD4AAIA+AAAAvQAAAD4AAIA+AACQvgAAAD4AAOA+AAAAvQAAAD4AAOA+AACQvgAAAAAAAOA+AAAAvQAAAAAAAOA+AACQvgAAAAAAAIA+AAAAvQAAAAAAAIA+AACQvgAAAD4AAOA+AAAAvQAAAD4AAOA+AACQvgAAAAAAAOA+AAAAvQAAAAAAAOA+AAAAvQAAAD4AAIA+AACQvgAAAD4AAIA+AAAAvQAAAAAAAIA+AACQvgAAAAAAAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgAgAD8AAL89ACAAPwAAADoA4A8/AAC/PQDgDz8AgGA+ACAAPwDAnz4AIAA/AIBgPgDgDz8AwJ8+AOAPPwCAXz4AwP8+AADBPQDA/z4AgF8+AEDQPgAAwT0AQNA+AMCvPgBA0D4AgGA+AEDQPgDArz4AwP8+AIBgPgDA/z4AQKA+ACAAPwDA3z4AIAA/AECgPgDgDz8AwN8+AOAPPwAAwT0AIAA/AIBfPgAgAD8AAME9AOAPPwCAXz4A4A8/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAADAvQAAwD0AABA/AADAvQAAwD0AAKA+AADAvQAAAD0AABA/AADAvQAAAD0AAKA+AABgvgAAwD0AAKA+AABgvgAAwD0AABA/AABgvgAAAD0AAKA+AABgvgAAAD0AABA/AABgvgAAwD0AAKA+AADAvQAAwD0AAKA+AABgvgAAwD0AABA/AADAvQAAwD0AABA/AABgvgAAAD0AABA/AADAvQAAAD0AABA/AABgvgAAAD0AAKA+AADAvQAAAD0AAKA+AABgvgAAwD0AABA/AADAvQAAwD0AABA/AABgvgAAAD0AABA/AADAvQAAAD0AABA/AADAvQAAwD0AAKA+AABgvgAAwD0AAKA+AADAvQAAAD0AAKA+AABgvgAAAD0AAKA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDwPgCAAD4A4Bc/AIAAPgBA8D4AgB8+AOAXPwCAHz4AICg/AIAAPgDgRz8AgAA+ACAoPwCAHz4A4Ec/AIAfPgDgJz8AAP89ACAYPwAA/z0A4Cc/AAAAOgAgGD8AAAA6AOA3PwAAADoAICg/AAAAOgDgNz8AAP89ACAoPwAA/z0AIEg/AIAAPgDgVz8AgAA+ACBIPwCAHz4A4Fc/AIAfPgAgGD8AgAA+AOAnPwCAAD4AIBg/AIAfPgDgJz8AgB8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAz/fPvgAAQD4AAMA+z/fPvgAAQD4AAIA+z/fPvgAAgD0AAMA+z/fPvgAAgD0AAIA+AADQvgAAQD4AAIA+AADQvgAAQD4AAMA+AADQvgAAgD0AAIA+AADQvgAAgD0AAMA+AADQvgAAQD4AAIA+z/fPvgAAQD4AAIA+AADQvgAAQD4AAMA+z/fPvgAAQD4AAMA+AADQvgAAgD0AAMA+z/fPvgAAgD0AAMA+AADQvgAAgD0AAIA+z/fPvgAAgD0AAIA+AADQvgAAQD4AAMA+z/fPvgAAQD4AAMA+AADQvgAAgD0AAMA+z/fPvgAAgD0AAMA+z/fPvgAAQD4AAIA+AADQvgAAQD4AAIA+z/fPvgAAgD0AAIA+AADQvgAAgD0AAIA+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgAAgT0AAH49AACBPQAAADoAAP89AAB+PQAA/z0AAIE9AACBPQAA/z0AAIE9AACBPQAA/z0AAP89AAD/PQAAgT0AAH49AAB+PQAAfj0AAIE9AAAAOgAAfj0AAAA6AACBPQAAADoAAH49AAAAOgAAgT0AAH49AAB+PQAAfj0AgAA+AACBPQAA/z0AAIE9AIAAPgAA/z0AAP89AAD/PQAAgT0AAIE9AAB+PQAAgT0AAIE9AAD/PQAAfj0AAP89AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACAPgAAwD0AAMC9AACAPgAAwD0AACC+AACAPgAAAD0AAMC9AACAPgAAAD0AACC+AAAAPgAAwD0AACC+AAAAPgAAwD0AAMC9AAAAPgAAAD0AACC+AAAAPgAAAD0AAMC9AAAAPgAAwD0AACC+AACAPgAAwD0AACC+AAAAPgAAwD0AAMC9AACAPgAAwD0AAMC9AAAAPgAAAD0AAMC9AACAPgAAAD0AAMC9AAAAPgAAAD0AACC+AACAPgAAAD0AACC+AAAAPgAAwD0AAMC9AACAPgAAwD0AAMC9AAAAPgAAAD0AAMC9AACAPgAAAD0AAMC9AACAPgAAwD0AACC+AAAAPgAAwD0AACC+AACAPgAAAD0AACC+AAAAPgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/ACAIPwBAkD4A4A8/AECQPgAgCD8AwJ8+AOAPPwDAnz4AICA/AECQPgDgJz8AQJA+ACAgPwDAnz4A4Cc/AMCfPgDgHz8AwI8+ACAQPwDAjz4A4B8/AECAPgAgED8AQIA+AOAvPwBAgD4AICA/AECAPgDgLz8AwI8+ACAgPwDAjz4AICg/AECQPgDgNz8AQJA+ACAoPwDAnz4A4Dc/AMCfPgAgED8AQJA+AOAfPwBAkD4AIBA/AMCfPgDgHz8AwJ8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACQPgAAwD0AAMC9AACQPgAAwD0AACC+AACQPgAAAD0AAMC9AACQPgAAAD0AACC+AABgPgAAwD0AACC+AABgPgAAwD0AAMC9AABgPgAAAD0AACC+AABgPgAAAD0AAMC9AABgPgAAwD0AACC+AACQPgAAwD0AACC+AABgPgAAwD0AAMC9AACQPgAAwD0AAMC9AABgPgAAAD0AAMC9AACQPgAAAD0AAMC9AABgPgAAAD0AACC+AACQPgAAAD0AACC+AABgPgAAwD0AAMC9AACQPgAAwD0AAMC9AABgPgAAAD0AAMC9AACQPgAAAD0AAMC9AACQPgAAwD0AACC+AABgPgAAwD0AACC+AACQPgAAAD0AACC+AABgPgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgBAoD4AAPw8AECgPgAAADoAwK8+AAD8PADArz4AAIE9AECgPgAAvz0AQKA+AACBPQDArz4AAL89AMCvPgAAfj0AwJ8+AAACPQDAnz4AAH49AECQPgAAAj0AQJA+AAC/PQBAkD4AAIE9AECQPgAAvz0AwJ8+AACBPQDAnz4AAME9AECgPgAA/z0AQKA+AADBPQDArz4AAP89AMCvPgAAAj0AQKA+AAB+PQBAoD4AAAI9AMCvPgAAfj0AwK8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACwPsUggD0AAMC9AACwPsUggD0AACC+AACwPgAAgD0AAMC9AACwPgAAgD0AACC+AACQPsUggD0AACC+AACQPsUggD0AAMC9AACQPgAAgD0AACC+AACQPgAAgD0AAMC9AACQPsUggD0AACC+AACwPsUggD0AACC+AACQPsUggD0AAMC9AACwPsUggD0AAMC9AACQPgAAgD0AAMC9AACwPgAAgD0AAMC9AACQPgAAgD0AACC+AACwPgAAgD0AACC+AACQPsUggD0AAMC9AACwPsUggD0AAMC9AACQPgAAgD0AAMC9AACwPgAAgD0AAMC9AACwPsUggD0AACC+AACQPsUggD0AACC+AACwPgAAgD0AACC+AACQPgAAgD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgCAID4AAPw8AIAgPgAAADoAgB8+AAD8PACAHz4AAIE9AIAgPgAAvz0AgCA+AACBPQCAHz4AAL89AIAfPgAAfj0AgB8+AAACPQCAHz4AAH49AIAAPgAAAj0AgAA+AAC/PQCAAD4AAIE9AIAAPgAAvz0AgB8+AACBPQCAHz4AAME9AIAgPgAA/z0AgCA+AADBPQCAHz4AAP89AIAfPgAAAj0AgCA+AAB+PQCAID4AAAI9AIAfPgAAfj0AgB8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAvgAAwD0AAMC9AAAAvgAAwD0AACC+AAAAvgAAAD0AAMC9AAAAvgAAAD0AACC+AACAvgAAwD0AACC+AACAvgAAwD0AAMC9AACAvgAAAD0AACC+AACAvgAAAD0AAMC9AACAvgAAwD0AACC+AAAAvgAAwD0AACC+AACAvgAAwD0AAMC9AAAAvgAAwD0AAMC9AACAvgAAAD0AAMC9AAAAvgAAAD0AAMC9AACAvgAAAD0AACC+AAAAvgAAAD0AACC+AACAvgAAwD0AAMC9AAAAvgAAwD0AAMC9AACAvgAAAD0AAMC9AAAAvgAAAD0AAMC9AAAAvgAAwD0AACC+AACAvgAAwD0AACC+AAAAvgAAAD0AACC+AACAvgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AEDAPgBAgD4AwM8+AECAPgBAwD4AwI8+AMDPPgDAjz4AQPA+AECAPgDA/z4AQIA+AEDwPgDAjz4AwP8+AMCPPgDA7z4AgH8+AEDQPgCAfz4AwO8+AIBgPgBA0D4AgGA+AOAHPwCAYD4AQPA+AIBgPgDgBz8AgH8+AEDwPgCAfz4AIAA/AECAPgDgDz8AQIA+ACAAPwDAjz4A4A8/AMCPPgBA0D4AQIA+AMDvPgBAgD4AQNA+AMCPPgDA7z4AwI8+AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAABgvgAAwD0AAMC9AABgvgAAwD0AACC+AABgvgAAAD0AAMC9AABgvgAAAD0AACC+AACQvgAAwD0AACC+AACQvgAAwD0AAMC9AACQvgAAAD0AACC+AACQvgAAAD0AAMC9AACQvgAAwD0AACC+AABgvgAAwD0AACC+AACQvgAAwD0AAMC9AABgvgAAwD0AAMC9AACQvgAAAD0AAMC9AABgvgAAAD0AAMC9AACQvgAAAD0AACC+AABgvgAAAD0AACC+AACQvgAAwD0AAMC9AABgvgAAwD0AAMC9AACQvgAAAD0AAMC9AABgvgAAAD0AAMC9AABgvgAAwD0AACC+AACQvgAAwD0AACC+AABgvgAAAD0AACC+AACQvgAAAD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAOgAAAj0AAPw8AAACPQAAADoAAH49AAD8PAAAfj0AAIE9AAACPQAAvz0AAAI9AACBPQAAfj0AAL89AAB+PQAAfj0AAPw8AAACPQAA/DwAAH49AAAAOgAAAj0AAAA6AAC/PQAAADoAAIE9AAAAOgAAvz0AAPw8AACBPQAA/DwAAME9AAACPQAA/z0AAAI9AADBPQAAfj0AAP89AAB+PQAAAj0AAAI9AAB+PQAAAj0AAAI9AAB+PQAAfj0AAH49AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAACQvsUggD0AAMC9AACQvsUggD0AACC+AACQvgAAgD0AAMC9AACQvgAAgD0AACC+AACwvsUggD0AACC+AACwvsUggD0AAMC9AACwvgAAgD0AACC+AACwvgAAgD0AAMC9AACwvsUggD0AACC+AACQvsUggD0AACC+AACwvsUggD0AAMC9AACQvsUggD0AAMC9AACwvgAAgD0AAMC9AACQvgAAgD0AAMC9AACwvgAAgD0AACC+AACQvgAAgD0AACC+AACwvsUggD0AAMC9AACQvsUggD0AAMC9AACwvgAAgD0AAMC9AACQvgAAgD0AAMC9AACQvsUggD0AACC+AACwvsUggD0AACC+AACQvgAAgD0AACC+AACwvgAAgD0AACC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACBPQAAAj0AAL89AAACPQAAgT0AAPw8AAC/PQAA/DwAgAA+AAACPQCAHz4AAAI9AIAAPgAA/DwAgB8+AAD8PAAA/z0AAPw8AADBPQAA/DwAAP89AAAAOgAAwT0AAAA6AIAfPgAAADoAgAA+AAAAOgCAHz4AAPw8AIAAPgAA/DwAgCA+AAACPQCAPz4AAAI9AIAgPgAA/DwAgD8+AAD8PAAAwT0AAAI9AAD/PQAAAj0AAME9AAD8PAAA/z0AAPw8AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUAAAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPxPyhT0AAAAAAAAAAK9zfz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAADug4Q+AAAAAOpGdz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAADug4S+AAAAAOpGdz8AAAAAIbWyvAAAAABn8H8/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAACoqAU+AAAAAFXPfT8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAwD4AAEA/AAAAAAAAAAAAAAAAAACAPwAAAACoqAW+AAAAAFXPfT8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAD+rqko/AAAAAAAAAAAAAAAAAACAPwAAAAAhtbI8AAAAAGfwfz8AAAAAx71QPAAAAACu+n8/AAAAAAAAAD+rqko/AAAAAAAAAAAAAAAAAACAPwAAAAAhtbK8AAAAAGfwfz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAQD8AAMA/AAAAAAAAAAAAAAAAAACAPwAAAAC2frK9AAAAAJ4Gfz8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAQD8AAMA/AAAAAAAAAAAAAAAAAACAPwAAAAC2frI9AAAAAJ4Gfz8AAAAAAAAAAAAAAAAAAIA/AAAAAKioBb4AAAAAAAAAAFXPfT8AAIA+qKgFvgAAAAAAAAAAVc99Pw=="}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[0.15625,0.125,0.25],"min":[-0.15625,0,-0.0625],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"max":[0.62451171875,0.21826171875],"min":[0.00048828125,0.00048828125],"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":4,"componentType":5126,"count":24,"max":[0.125,0.125,-0.0625],"min":[-0.125,0,-0.3125],"type":"VEC3"},{"bufferView":5,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":6,"componentType":5126,"count":24,"max":[0.49951171875,0.40576171875],"min":[0.00048828125,0.21923828125],"type":"VEC2"},{"bufferView":7,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":8,"componentType":5126,"count":24,"max":[0.03125,0.125,0.3125],"min":[-0.03125,0,0.25],"type":"VEC3"},{"bufferView":9,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":10,"componentType":5126,"count":24,"max":[0.59326171875,0.09326171875],"min":[0.46923828125,0.00048828125],"type":"VEC2"},{"bufferView":11,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":12,"componentType":5126,"count":24,"max":[0.21875,0.125,0.1875],"min":[-0.03125,0,0],"type":"VEC3"},{"bufferView":13,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":14,"componentType":5126,"count":24,"max":[0.84326171875,0.46826171875],"min":[0.40673828125,0.31298828125],"type":"VEC2"},{"bufferView":15,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":16,"componentType":5126,"count":24,"max":[0.21875,0.09375,0.5625],"min":[0.09375,0.03125,0.3125],"type":"VEC3"},{"bufferView":17,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":18,"componentType":5126,"count":24,"max":[0.68701171875,0.62451171875],"min":[0.31298828125,0.46923828125],"type":"VEC2"},{"bufferView":19,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":20,"componentType":5126,"count":24,"max":[0.406312495470047,0.1875,0.375],"min":[0.40625,0.0625,0.25],"type":"VEC3"},{"bufferView":21,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":22,"componentType":5126,"count":24,"max":[0.12548828125,0.28076171875],"min":[0.00048828125,0.15673828125],"type":"VEC2"},{"bufferView":23,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":24,"componentType":5126,"count":24,"max":[-0.03125,0.125,0.4375],"min":[-0.28125,0,0.25],"type":"VEC3"},{"bufferView":25,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":26,"componentType":5126,"count":24,"max":[0.43701171875,0.56201171875],"min":[0.00048828125,0.40673828125],"type":"VEC2"},{"bufferView":27,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":28,"componentType":5126,"count":24,"max":[-0.09375,0.09375,0.5625],"min":[-0.21875,0.03125,0.3125],"type":"VEC3"},{"bufferView":29,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":30,"componentType":5126,"count":24,"max":[0.84326171875,0.15576171875],"min":[0.46923828125,0.00048828125],"type":"VEC2"},{"bufferView":31,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":32,"componentType":5126,"count":24,"max":[-0.406187504529953,0.1875,0.375],"min":[-0.40625,0.0625,0.25],"type":"VEC3"},{"bufferView":33,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":34,"componentType":5126,"count":24,"max":[0.12548828125,0.12451171875],"min":[0.00048828125,0.00048828125],"type":"VEC2"},{"bufferView":35,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":36,"componentType":5126,"count":24,"max":[0.25,0.09375,-0.09375],"min":[0.125,0.03125,-0.15625],"type":"VEC3"},{"bufferView":37,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":38,"componentType":5126,"count":24,"max":[0.71826171875,0.31201171875],"min":[0.53173828125,0.25048828125],"type":"VEC2"},{"bufferView":39,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":40,"componentType":5126,"count":24,"max":[0.28125,0.09375,-0.09375],"min":[0.21875,0.03125,-0.15625],"type":"VEC3"},{"bufferView":41,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":42,"componentType":5126,"count":24,"max":[0.12451171875,0.34326171875],"min":[0.00048828125,0.28173828125],"type":"VEC2"},{"bufferView":43,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":44,"componentType":5126,"count":24,"max":[0.34375,0.0625625029206276,-0.09375],"min":[0.28125,0.0625,-0.15625],"type":"VEC3"},{"bufferView":45,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":46,"componentType":5126,"count":24,"max":[0.12451171875,0.15673828125],"min":[0.00048828125,0.12548828125],"type":"VEC2"},{"bufferView":47,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":48,"componentType":5126,"count":24,"max":[-0.125,0.09375,-0.09375],"min":[-0.25,0.03125,-0.15625],"type":"VEC3"},{"bufferView":49,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":50,"componentType":5126,"count":24,"max":[0.56201171875,0.28076171875],"min":[0.37548828125,0.21923828125],"type":"VEC2"},{"bufferView":51,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":52,"componentType":5126,"count":24,"max":[-0.21875,0.09375,-0.09375],"min":[-0.28125,0.03125,-0.15625],"type":"VEC3"},{"bufferView":53,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":54,"componentType":5126,"count":24,"max":[0.12451171875,0.06201171875],"min":[0.00048828125,0.00048828125],"type":"VEC2"},{"bufferView":55,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":56,"componentType":5126,"count":24,"max":[-0.28125,0.0625625029206276,-0.09375],"min":[-0.34375,0.0625,-0.15625],"type":"VEC3"},{"bufferView":57,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":58,"componentType":5126,"count":24,"max":[0.18701171875,0.03173828125],"min":[0.06298828125,0.00048828125],"type":"VEC2"},{"bufferView":59,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":60,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":61,"componentType":5126,"count":3,"max":[0.06540312618017197,0,0,1],"min":[0,0,0,0.9978589415550232],"type":"VEC4"},{"bufferView":62,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":63,"componentType":5126,"count":3,"max":[0,0.258819043636322,0,1],"min":[0,0,0,0.9659258127212524],"type":"VEC4"},{"bufferView":64,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":65,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.258819043636322,0,0.9659258127212524],"type":"VEC4"},{"bufferView":66,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":67,"componentType":5126,"count":3,"max":[0,0.13052618503570557,0,1],"min":[0,0,0,0.9914448857307434],"type":"VEC4"},{"bufferView":68,"componentType":5126,"count":3,"max":[0.75],"min":[0],"type":"SCALAR"},{"bufferView":69,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.13052618503570557,0,0.9914448857307434],"type":"VEC4"},{"bufferView":70,"componentType":5126,"count":3,"max":[0.7916666865348816],"min":[0],"type":"SCALAR"},{"bufferView":71,"componentType":5126,"count":3,"max":[0,0.02181488461792469,0,1],"min":[0,0,0,0.9997619986534119],"type":"VEC4"},{"bufferView":72,"componentType":5126,"count":3,"max":[0.7916666865348816],"min":[0],"type":"SCALAR"},{"bufferView":73,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.02181488461792469,0,0.9997619986534119],"type":"VEC4"},{"bufferView":74,"componentType":5126,"count":3,"max":[1.5],"min":[0],"type":"SCALAR"},{"bufferView":75,"componentType":5126,"count":3,"max":[0,0,0,1],"min":[0,-0.08715574443340302,0,0.9961947202682495],"type":"VEC4"},{"bufferView":76,"componentType":5126,"count":3,"max":[1.5],"min":[0],"type":"SCALAR"},{"bufferView":77,"componentType":5126,"count":3,"max":[0,0.08715574443340302,0,1],"min":[0,0,0,0.9961947202682495],"type":"VEC4"},{"bufferView":78,"componentType":5126,"count":1,"max":[0],"min":[0],"type":"SCALAR"},{"bufferView":79,"componentType":5126,"count":1,"max":[-0.13052618503570557,0,0,0.9914448857307434],"min":[-0.13052618503570557,0,0,0.9914448857307434],"type":"VEC4"},{"bufferView":80,"componentType":5126,"count":1,"max":[0.25],"min":[0.25],"type":"SCALAR"},{"bufferView":81,"componentType":5126,"count":1,"max":[-0.13052618503570557,0,0,0.9914448857307434],"min":[-0.13052618503570557,0,0,0.9914448857307434],"type":"VEC4"}],"materials":[{"pbrMetallicRoughness":{"metallicFactor":0,"roughnessFactor":1,"baseColorTexture":{"index":0}},"alphaMode":"MASK","alphaCutoff":0.05,"doubleSided":true}],"textures":[{"sampler":0}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"meshes":[{"primitives":[{"mode":4,"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":4,"NORMAL":5,"TEXCOORD_0":6},"indices":7,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":8,"NORMAL":9,"TEXCOORD_0":10},"indices":11,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":12,"NORMAL":13,"TEXCOORD_0":14},"indices":15,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":16,"NORMAL":17,"TEXCOORD_0":18},"indices":19,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":20,"NORMAL":21,"TEXCOORD_0":22},"indices":23,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":24,"NORMAL":25,"TEXCOORD_0":26},"indices":27,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":28,"NORMAL":29,"TEXCOORD_0":30},"indices":31,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":32,"NORMAL":33,"TEXCOORD_0":34},"indices":35,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":36,"NORMAL":37,"TEXCOORD_0":38},"indices":39,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":40,"NORMAL":41,"TEXCOORD_0":42},"indices":43,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":44,"NORMAL":45,"TEXCOORD_0":46},"indices":47,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":48,"NORMAL":49,"TEXCOORD_0":50},"indices":51,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":52,"NORMAL":53,"TEXCOORD_0":54},"indices":55,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":56,"NORMAL":57,"TEXCOORD_0":58},"indices":59,"material":0}]}],"animations":[{"name":"animation.model.walk","samplers":[{"input":60,"output":61,"interpolation":"LINEAR"},{"input":62,"output":63,"interpolation":"LINEAR"},{"input":64,"output":65,"interpolation":"LINEAR"},{"input":66,"output":67,"interpolation":"LINEAR"},{"input":68,"output":69,"interpolation":"LINEAR"}],"channels":[{"sampler":0,"target":{"node":3,"path":"rotation"}},{"sampler":1,"target":{"node":7,"path":"rotation"}},{"sampler":2,"target":{"node":11,"path":"rotation"}},{"sampler":3,"target":{"node":15,"path":"rotation"}},{"sampler":4,"target":{"node":19,"path":"rotation"}}]},{"name":"animation.model.idle","samplers":[{"input":70,"output":71,"interpolation":"LINEAR"},{"input":72,"output":73,"interpolation":"LINEAR"},{"input":74,"output":75,"interpolation":"LINEAR"},{"input":76,"output":77,"interpolation":"LINEAR"}],"channels":[{"sampler":0,"target":{"node":7,"path":"rotation"}},{"sampler":1,"target":{"node":11,"path":"rotation"}},{"sampler":2,"target":{"node":15,"path":"rotation"}},{"sampler":3,"target":{"node":19,"path":"rotation"}}]},{"name":"animation.model.back","samplers":[{"input":78,"output":79,"interpolation":"LINEAR"},{"input":80,"output":81,"interpolation":"LINEAR"}],"channels":[{"sampler":0,"target":{"node":15,"path":"rotation"}},{"sampler":1,"target":{"node":19,"path":"rotation"}}]}]} diff --git a/games/devtest/mods/gltf/models/gltf_minimal_triangle.gltf b/games/devtest/mods/gltf/models/gltf_minimal_triangle.gltf new file mode 100644 index 000000000..9a624f085 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_minimal_triangle.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":1},"indices":0}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=","byteLength":44}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":6,"target":34963},{"buffer":0,"byteOffset":8,"byteLength":36,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5123,"count":3,"type":"SCALAR","max":[2],"min":[0]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_simple_sparse_accessor.gltf b/games/devtest/mods/gltf/models/gltf_simple_sparse_accessor.gltf new file mode 100644 index 000000000..979896825 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_simple_sparse_accessor.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":1},"indices":0}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAAIAAcAAAABAAgAAQAJAAgAAQACAAkAAgAKAAkAAgADAAoAAwALAAoAAwAEAAsABAAMAAsABAAFAAwABQANAAwABQAGAA0AAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAQAAAAAAAAAAAAABAQAAAAAAAAAAAAACAQAAAAAAAAAAAAACgQAAAAAAAAAAAAADAQAAAAAAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAQAAAgD8AAAAAAABAQAAAgD8AAAAAAACAQAAAgD8AAAAAAACgQAAAgD8AAAAAAADAQAAAgD8AAAAACAAKAAwAAAAAAIA/AAAAQAAAAAAAAEBAAABAQAAAAAAAAKBAAACAQAAAAAA=","byteLength":284}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":72,"byteLength":168},{"buffer":0,"byteOffset":240,"byteLength":6},{"buffer":0,"byteOffset":248,"byteLength":36}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5123,"count":36,"type":"SCALAR","max":[13],"min":[0]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":14,"type":"VEC3","max":[6,4,0],"min":[0,0,0],"sparse":{"count":3,"indices":{"bufferView":2,"byteOffset":0,"componentType":5123},"values":{"bufferView":3,"byteOffset":0}}}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_snow_man.gltf b/games/devtest/mods/gltf/models/gltf_snow_man.gltf new file mode 100644 index 000000000..cd8c347d2 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_snow_man.gltf @@ -0,0 +1 @@ +{"asset":{"version":"2.0","generator":"Blockbench 4.6.0 glTF exporter"},"scenes":[{"nodes":[3],"name":"blockbench_export"}],"scene":0,"nodes":[{"name":"cube","mesh":0},{"name":"cube","mesh":1},{"name":"cube","mesh":2},{"children":[0,1,2]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":840,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1128,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1416,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":1608,"byteLength":72,"target":34963},{"buffer":0,"byteOffset":1680,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":1968,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":2256,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":2448,"byteLength":72,"target":34963}],"buffers":[{"byteLength":2520,"uri":"data:application/octet-stream;base64,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"}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[3,24,3],"min":[-3,18,-3],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"max":[0.8333333134651184,1],"min":[0,0.25],"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":4,"componentType":5126,"count":24,"max":[5,10,5],"min":[-5,0,-5],"type":"VEC3"},{"bufferView":5,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":6,"componentType":5126,"count":24,"max":[0.625,1],"min":[0,0.375],"type":"VEC2"},{"bufferView":7,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"},{"bufferView":8,"componentType":5126,"count":24,"max":[4,18,4],"min":[-4,10,-4],"type":"VEC3"},{"bufferView":9,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":10,"componentType":5126,"count":24,"max":[0.7083333134651184,0.7916666865348816],"min":[0.2083333283662796,0.25],"type":"VEC2"},{"bufferView":11,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"}],"materials":[{"pbrMetallicRoughness":{"metallicFactor":0,"roughnessFactor":1,"baseColorTexture":{"index":0,"texCoord":0}},"alphaMode":"MASK","alphaCutoff":0.05,"doubleSided":true}],"textures":[{}],"meshes":[{"primitives":[{"mode":4,"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":4,"NORMAL":5,"TEXCOORD_0":6},"indices":7,"material":0}]},{"primitives":[{"mode":4,"attributes":{"POSITION":8,"NORMAL":9,"TEXCOORD_0":10},"indices":11,"material":0}]}]} diff --git a/games/devtest/mods/gltf/models/gltf_spider.gltf b/games/devtest/mods/gltf/models/gltf_spider.gltf new file mode 100644 index 000000000..6698b6bb4 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_spider.gltf @@ -0,0 +1 @@ +{"asset":{"generator":"Khronos glTF Blender I/O v1.7.33","version":"2.0"},"scene":0,"scenes":[{"name":"Scene","nodes":[0]}],"nodes":[{"mesh":0,"name":"Spider"}],"materials":[{"doubleSided":true,"name":"Material.001","pbrMetallicRoughness":{}}],"meshes":[{"name":"Cube","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]}],"accessors":[{"bufferView":0,"componentType":5126,"count":1000,"max":[2.742279291152954,1.4045029878616333,2.0192716121673584],"min":[-2.742279291152954,-0.6434623599052429,-3.534085512161255],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":1000,"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":1000,"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":1500,"type":"SCALAR"}],"bufferViews":[{"buffer":0,"byteLength":12000,"byteOffset":0},{"buffer":0,"byteLength":12000,"byteOffset":12000},{"buffer":0,"byteLength":8000,"byteOffset":24000},{"buffer":0,"byteLength":3000,"byteOffset":32000}],"buffers":[{"byteLength":35000,"uri":"data:application/octet-stream;base64,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dQCGAHUAgACIAIsAkQCIAJEAjgCQAJIAnQCQAJ0AmwCcAJ8AmQCcAJkAlgCVAJcAjACVAIwAigCPAJoAlACPAJQAiQCeAJMAjQCeAI0AmACiAKgAqwCiAKsApQCnALQAtwCnALcAqgCyAKwArwCyAK8AtQCtAKAAowCtAKMAsACmAKEArgCmAK4AswC2ALEApAC2AKQAqQC6AMAAwwC6AMMAvQC/AMwAzgC/AM4AwQDKAMQAxwDKAMcAzQDGALkAuwDGALsAyAC+ALgAxQC+AMUAywDPAMkAvADPALwAwgDSANgA2wDSANsA1QDXAOQA5gDXAOYA2QDiANwA3wDiAN8A5QDeANEA0wDeANMA4ADWANAA3QDWAN0A4wDnAOEA1ADnANQA2gDpAOsA8QDpAPEA7wDwAPIA/QDwAP0A+wD8AP4A+AD8APgA9gD1APcA7AD1AOwA6gDuAPoA9ADuAPQA6AD/APMA7QD/AO0A+QAAAQMBCQEAAQkBBgEHAQoBFQEHARUBEgEUARcBEQEUAREBDgENARABBQENAQUBAgEIARMBDAEIAQwBAQEWAQsBBAEWAQQBDwEpAR0BIwEpASMBLwEoASYBGQEoARkBGwEtASsBJQEtASUBJwEhAR8BLAEhASwBLgEYASQBKgEYASoBHgEcARoBIAEcASABIgExATMBOgExAToBOAE3ATkBRQE3AUUBQwFEAUYBPwFEAT8BPQE+AUABNAE+ATQBMgE2AUIBPAE2ATwBMAFHATsBNQFHATUBQQFJAUsBUQFJAVEBTwFQAVIBXQFQAV0BWwFcAV4BWAFcAVgBVgFVAVcBTAFVAUwBSgFOAVoBVAFOAVQBSAFfAVMBTQFfAU0BWQFgAWMBaQFgAWkBZgFoAWoBdQFoAXUBcwF0AXcBcQF0AXEBbgFtAW8BZAFtAWQBYgFnAXIBbAFnAWwBYQF2AWsBZQF2AWUBcAGJAX0BgwGJAYMBjwGIAYYBeQGIAXkBewGNAYsBhQGNAYUBhwGBAX8BjAGBAYwBjgF4AYQBigF4AYoBfgF8AXoBgAF8AYABggGRAZMBmQGRAZkBlwGYAZoBpQGYAaUBowGkAaYBoAGkAaABngGdAZ8BlAGdAZQBkgGWAaIBnAGWAZwBkAGnAZsBlQGnAZUBoQGpAasBsQGpAbEBrwGwAbIBvQGwAb0BuwG8Ab4BuAG8AbgBtgG1AbcBrAG1AawBqgGuAboBtAGuAbQBqAG/AbMBrQG/Aa0BuQHAAcMByQHAAckBxgHIAcoB1QHIAdUB0wHUAdcB0QHUAdEBzgHNAc8BxAHNAcQBwgHHAdIBzAHHAcwBwQHWAcsBxQHWAcUB0AHpAd0B4wHpAeMB7wHoAeYB2QHoAdkB2wHtAesB5QHtAeUB5wHhAd8B7AHhAewB7gHYAeQB6gHYAeoB3gHcAdoB4AHcAeAB4gHxAfMB+QHxAfkB9wH4AfoBBQL4AQUCAwIEAgYCAAIEAgAC/gH9Af8B9AH9AfQB8gH2AQIC/AH2AfwB8AEHAvsB9QEHAvUBAQIJAgsCEQIJAhECDwIQAhICHQIQAh0CGwIcAh4CGAIcAhgCFgIVAhcCDAIVAgwCCgIOAhoCFAIOAhQCCAIfAhMCDQIfAg0CGQIgAiMCKQIgAikCJgIoAisCNgIoAjYCMwI0AjcCMQI0AjECLgIsAi8CJAIsAiQCIQInAjICLQInAi0CIgI1AioCJQI1AiUCMAJJAj0CQwJJAkMCTwJIAkYCOQJIAjkCOwJNAksCRQJNAkUCRwJBAj8CTAJBAkwCTgI4AkQCSgI4AkoCPgI8AjoCQAI8AkACQgJSAlQCWQJSAlkCVwJYAloCZgJYAmYCZAJjAmUCYAJjAmACXgJdAl8CUwJdAlMCUQJWAmICXAJWAlwCUAJnAlsCVQJnAlUCYQJpAm8CcwJpAnMCbQJuAnsCfwJuAn8CcgJ6AnQCeAJ6AngCfgJ1AmgCbAJ1AmwCeQJwAmoCdgJwAnYCfAJ9AncCawJ9AmsCcQKCAogCiwKCAosChQKHApQClwKHApcCigKSAowCjwKSAo8ClQKNAoACgwKNAoMCkAKGAoECjgKGAo4CkwKWApEChAKWAoQCiQKnAq0CoQKnAqECmwKoAp0CmQKoApkCpAKvAqkCpQKvAqUCqwKjAq4CqgKjAqoCnwKaAqACrAKaAqwCpgKcAqICngKcAp4CmAKxArYCugKxAroCtQK3AsMCxwK3AscCuwLCAr0CwQLCAsECxgK8ArACtAK8ArQCwAK4ArICvgK4Ar4CxALFAr8CswLFArMCuQLJAs8C0wLJAtMCzQLOAtsC3wLOAt8C0gLaAtQC2ALaAtgC3gLVAsgCzALVAswC2QLQAsoC1gLQAtYC3ALdAtcCywLdAssC0QLiAugC6wLiAusC5QLmAvMC9wLmAvcC6gLyAuwC7wLyAu8C9QLtAuAC5ALtAuQC8QLnAuEC7gLnAu4C9AL2AvAC4wL2AuMC6QIHAw0DAQMHAwED+wIIA/0C+QIIA/kCBAMPAwkDBQMPAwUDCwMDAw4DCgMDAwoD/wL6AgADDAP6AgwDBgP8AgID/gL8Av4C+AIRAxcDGwMRAxsDFQMWAyMDJwMWAycDGgMiAxwDIAMiAyADJgMdAxADFAMdAxQDIQMYAxIDHgMYAx4DJAMlAx8DEwMlAxMDGQMpAy8DMwMpAzMDLQMuAzsDPwMuAz8DMgM6AzQDOAM6AzgDPgM1AygDLAM1AywDOQMwAyoDNgMwAzYDPAM9AzcDKwM9AysDMQNCA0gDSwNCA0sDRQNGA1MDVwNGA1cDSgNSA0wDTwNSA08DVQNNA0ADRANNA0QDUQNHA0EDTgNHA04DVANWA1ADQwNWA0MDSQNnA20DYQNnA2EDWwNoA10DWQNoA1kDZANvA2kDZQNvA2UDawNjA24DagNjA2oDXwNaA2ADbANaA2wDZgNcA2IDXgNcA14DWANxA3cDewNxA3sDdQN2A4MDhwN2A4cDegOCA3wDgAOCA4ADhgN9A3ADdAN9A3QDgQN4A3IDfgN4A34DhAOFA38DcwOFA3MDeQOJA48DkwOJA5MDjQOOA5sDnwOOA58DkgOaA5QDmAOaA5gDngOVA4gDjAOVA4wDmQOQA4oDlgOQA5YDnAOdA5cDiwOdA4sDkQOiA6gDqwOiA6sDpQOmA7MDtgOmA7YDqQOyA6wDrwOyA68DtQOuA6EDpAOuA6QDsQOnA6ADrQOnA60DtAO3A7ADowO3A6MDqgPHA80DwQPHA8EDuwPIA70DuQPIA7kDxAPPA8kDxQPPA8UDywPDA84DygPDA8oDvwO6A8ADzAO6A8wDxgO8A8IDvgO8A74DuAPQA9cD2wPQA9sD1APWA+ID5gPWA+YD2gPjA9wD4APjA+AD5wPdA9ED1QPdA9UD4QPYA9ID3gPYA94D5APlA98D0wPlA9MD2QM="}]} diff --git a/games/devtest/mods/gltf/models/gltf_triangle_with_vertex_stride.gltf b/games/devtest/mods/gltf/models/gltf_triangle_with_vertex_stride.gltf new file mode 100644 index 000000000..feddfbb02 --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_triangle_with_vertex_stride.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":1},"indices":0}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAA=","byteLength":80}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":6,"target":34963},{"buffer":0,"byteOffset":8,"byteLength":72,"byteStride":24,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5123,"count":3,"type":"SCALAR","max":[2],"min":[0]},{"bufferView":1,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/models/gltf_triangle_without_indices.gltf b/games/devtest/mods/gltf/models/gltf_triangle_without_indices.gltf new file mode 100644 index 000000000..e91cc0e5a --- /dev/null +++ b/games/devtest/mods/gltf/models/gltf_triangle_without_indices.gltf @@ -0,0 +1 @@ +{"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":0}}]}],"buffers":[{"uri":"data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA","byteLength":36}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36,"target":34962}],"accessors":[{"bufferView":0,"byteOffset":0,"componentType":5126,"count":3,"type":"VEC3","max":[1,1,0],"min":[0,0,0]}],"asset":{"version":"2.0"}} diff --git a/games/devtest/mods/gltf/textures/gltf_cube.png b/games/devtest/mods/gltf/textures/gltf_cube.png new file mode 100644 index 0000000000000000000000000000000000000000..1d019108550471be78c1efa02d4367a5aaacdc92 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0P3?wHke>@jRu?6^qxc>kD|E9eB^b_}10Yw@- zT^vI+&hMRgm-B!EPs?MDz6TnTOJ_@LQFZ=u)N}1I`3qqhB2w{o=bEjB7#buN_JkT= zklJ;8HkW(z-bQ}jZ8vMquQFZKQ}e`}$+SSlVV20tH7!q0W{TZ3P_wxBxO0)kBPKcB zF#pE(4z9h&cK@~bm2E%&{ZU)l=bvI9miBHu&l=+9n`|96og3&D22WQ%mvv4FO#sL{ BPoDq) literal 0 HcmV?d00001 diff --git a/games/devtest/mods/gltf/textures/gltf_frog.png b/games/devtest/mods/gltf/textures/gltf_frog.png new file mode 100644 index 0000000000000000000000000000000000000000..552ae36493c59c6107de9eb9c08b85cf494d3647 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvi2$DvS0D`p(auuIo^pl3@)r** zx_@QWpC9+F^;c*BB^XPB{DK)Ap4~_Ta+Z0zIEF|_-aUA+uqi>H^`iAA)g?_HjC?@{ zj{W`jy*_R^t3pE7>UrIx=4=#B{D``d7B}@YEIM2ItZx z*QMOrdNy~*B)&;M7Pqfs_j>8Tuikej{qR=KR}%X?qh{`}?U|hNz{{a{{?cuXs%EB7 z-B&WO-uQeqJJq*WdS$6@&$mZzi4UVfYQC+ry|tl(VN2s7um6qnCOWNuCjLFyc#?Pi RV-=9cJYD@<);T3K0RXN~Zvg-R literal 0 HcmV?d00001 diff --git a/games/devtest/mods/gltf/textures/gltf_snow_man.png b/games/devtest/mods/gltf/textures/gltf_snow_man.png new file mode 100644 index 0000000000000000000000000000000000000000..7f2784358b94af2238337bc6a1555c32c06f9c80 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)I_yc@GT!AzY1Sgi{RrLJ-|3BuI z$}}LKu_VYZn8D%MjWi%9$J50z#G?1@g@e2e3Oub3*Et+w+0f|Y@aet%Wc9!^s~5)Y zx$%1XzC*^p(<>Q_8F+1fGx*F_*5a_@s<{yO`N)DPK`K|SZOcD!CZ=Hi>5HpcmN2|X z;k9~o)lFniy^Eslft!zXSM}YSuF%?e`lIZ5wHa=yVzLW>_A+?7`njxgN@xNA7I;nX literal 0 HcmV?d00001 diff --git a/games/devtest/mods/gltf/textures/gltf_spider.png b/games/devtest/mods/gltf/textures/gltf_spider.png new file mode 100644 index 0000000000000000000000000000000000000000..1e3d3ae8cbb31c80219081ed7ebdc1bb1ad78373 GIT binary patch literal 10957 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 literal 0 HcmV?d00001 diff --git a/irr/include/IMesh.h b/irr/include/IMesh.h index 6d06eb762..8ee180d5d 100644 --- a/irr/include/IMesh.h +++ b/irr/include/IMesh.h @@ -86,6 +86,17 @@ public: mesh buffer. */ virtual IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const = 0; + //! Minetest binds textures (node tiles, object textures) to models. + // glTF allows multiple primitives (mesh buffers) to reference the same texture. + // This is reflected here: This function gets the texture slot for a mesh buffer. + /** \param meshbufNr: Zero based index of the mesh buffer. The maximum value is + getMeshBufferCount() - 1; + \return number of texture slot to bind to the given mesh buffer */ + virtual u32 getTextureSlot(u32 meshbufNr) const + { + return meshbufNr; + } + //! Get an axis aligned bounding box of the mesh. /** \return Bounding box of this mesh. */ virtual const core::aabbox3d &getBoundingBox() const = 0; diff --git a/irr/include/ISkinnedMesh.h b/irr/include/ISkinnedMesh.h index 9cc7469cb..bb611bba2 100644 --- a/irr/include/ISkinnedMesh.h +++ b/irr/include/ISkinnedMesh.h @@ -199,6 +199,9 @@ public: //! Adds a new meshbuffer to the mesh, access it as last one virtual SSkinMeshBuffer *addMeshBuffer() = 0; + //! Adds a new meshbuffer to the mesh, access it as last one + virtual void addMeshBuffer(SSkinMeshBuffer *meshbuf) = 0; + //! Adds a new joint to the mesh, access it as last one virtual SJoint *addJoint(SJoint *parent = 0) = 0; diff --git a/irr/include/SMesh.h b/irr/include/SMesh.h index a391255a1..66e6ecc08 100644 --- a/irr/include/SMesh.h +++ b/irr/include/SMesh.h @@ -64,6 +64,17 @@ struct SMesh : public IMesh return nullptr; } + u32 getTextureSlot(u32 meshbufNr) const override + { + return TextureSlots.at(meshbufNr); + } + + void setTextureSlot(u32 meshbufNr, u32 textureSlot) + { + TextureSlots.at(meshbufNr) = textureSlot; + } + + //! returns an axis aligned bounding box const core::aabbox3d &getBoundingBox() const override { @@ -103,6 +114,7 @@ struct SMesh : public IMesh if (buf) { buf->grab(); MeshBuffers.push_back(buf); + TextureSlots.push_back(getMeshBufferCount() - 1); } } @@ -122,6 +134,8 @@ struct SMesh : public IMesh //! The meshbuffers of this mesh std::vector MeshBuffers; + //! Mapping from meshbuffer number to bindable texture slot + std::vector TextureSlots; //! The bounding box of this mesh core::aabbox3d BoundingBox; diff --git a/irr/include/SSkinMeshBuffer.h b/irr/include/SSkinMeshBuffer.h index fe9d78321..2b71cf6c5 100644 --- a/irr/include/SSkinMeshBuffer.h +++ b/irr/include/SSkinMeshBuffer.h @@ -6,6 +6,7 @@ #include "IMeshBuffer.h" #include "S3DVertex.h" +#include "irrArray.h" namespace irr { @@ -21,10 +22,14 @@ struct SSkinMeshBuffer : public IMeshBuffer PrimitiveType(EPT_TRIANGLES), HWBuffer(nullptr), MappingHint_Vertex(EHM_NEVER), MappingHint_Index(EHM_NEVER), BoundingBoxNeedsRecalculated(true) + {} + + //! Constructor for standard vertices + SSkinMeshBuffer(std::vector &&vertices, std::vector &&indices) : + SSkinMeshBuffer() { -#ifdef _DEBUG - setDebugName("SSkinMeshBuffer"); -#endif + Vertices_Standard = std::move(vertices); + Indices = std::move(indices); } //! Get Material of this buffer. diff --git a/irr/include/irrArray.h b/irr/include/irrArray.h index 66978048f..9f390e79b 100644 --- a/irr/include/irrArray.h +++ b/irr/include/irrArray.h @@ -45,6 +45,10 @@ public: { } + //! Move constructor + array(std::vector &&data) : + m_data(std::move(data)), is_sorted(false) {} + //! Reallocates the array, make it bigger or smaller. /** \param new_size New size of array. \param canShrink Specifies whether the array is reallocated even if diff --git a/irr/include/irrString.h b/irr/include/irrString.h index a583c9e4b..9d9b288d8 100644 --- a/irr/include/irrString.h +++ b/irr/include/irrString.h @@ -11,7 +11,6 @@ #include #include #include -#include /* HACK: import these string methods from MT's util/string.h */ extern std::wstring utf8_to_wide(std::string_view input); @@ -65,6 +64,7 @@ static inline u32 locale_upper(u32 x) template class string { + using stl_type = std::basic_string; public: typedef T char_type; @@ -79,6 +79,10 @@ public: *this = other; } + string(const stl_type &str) : str(str) {} + + string(stl_type &&str) : str(std::move(str)) {} + //! Constructor from other string types template string(const string &other) @@ -814,13 +818,6 @@ public: friend size_t wStringToUTF8(stringc &destination, const wchar_t *source); private: - typedef std::basic_string stl_type; - - //! Private constructor - string(stl_type &&str) : - str(str) - { - } //! strlen wrapper template diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp new file mode 100644 index 000000000..64bbc10f1 --- /dev/null +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -0,0 +1,695 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "CGLTFMeshFileLoader.h" + +#include "coreutil.h" +#include "CSkinnedMesh.h" +#include "ISkinnedMesh.h" +#include "irrTypes.h" +#include "IReadFile.h" +#include "matrix4.h" +#include "path.h" +#include "quaternion.h" +#include "vector3d.h" +#include "os.h" + +#include "tiniergltf.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace irr { + +/* Notes on the coordinate system. + * + * glTF uses a right-handed coordinate system where +Z is the + * front-facing axis, and Irrlicht uses a left-handed coordinate + * system where -Z is the front-facing axis. + * We convert between them by mirroring the mesh across the X axis. + * Doing this correctly requires negating the Z coordinate on + * vertex positions and normals, and reversing the winding order + * of the vertex indices. + */ + +// Right-to-left handedness conversions + +template +static inline T convertHandedness(const T &t); + +template <> +core::vector3df convertHandedness(const core::vector3df &p) +{ + return core::vector3df(p.X, p.Y, -p.Z); +} + +namespace scene { + +using SelfType = CGLTFMeshFileLoader; + +template +SelfType::Accessor +SelfType::Accessor::sparseIndices(const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseIndices &indices, + const std::size_t count) +{ + const auto &view = model.bufferViews->at(indices.bufferView); + const auto byteStride = view.byteStride.value_or(indices.elementSize()); + + const auto &buffer = model.buffers->at(view.buffer); + const auto source = buffer.data.data() + view.byteOffset + indices.byteOffset; + + return SelfType::Accessor(source, byteStride, count); +} + +template +SelfType::Accessor +SelfType::Accessor::sparseValues(const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseValues &values, + const std::size_t count, + const std::size_t defaultByteStride) +{ + const auto &view = model.bufferViews->at(values.bufferView); + const auto byteStride = view.byteStride.value_or(defaultByteStride); + + const auto &buffer = model.buffers->at(view.buffer); + const auto source = buffer.data.data() + view.byteOffset + values.byteOffset; + + return SelfType::Accessor(source, byteStride, count); +} + +template +SelfType::Accessor +SelfType::Accessor::base(const tiniergltf::GlTF &model, std::size_t accessorIdx) +{ + const auto &accessor = model.accessors->at(accessorIdx); + + if (!accessor.bufferView.has_value()) { + return Accessor(accessor.count); + } + + const auto &view = model.bufferViews->at(accessor.bufferView.value()); + const auto byteStride = view.byteStride.value_or(accessor.elementSize()); + + const auto &buffer = model.buffers->at(view.buffer); + const auto source = buffer.data.data() + view.byteOffset + accessor.byteOffset; + + return Accessor(source, byteStride, accessor.count); +} + +template +SelfType::Accessor +SelfType::Accessor::make(const tiniergltf::GlTF &model, std::size_t accessorIdx) +{ + const auto &accessor = model.accessors->at(accessorIdx); + if (accessor.componentType != getComponentType() || accessor.type != getType()) + throw std::runtime_error("invalid accessor"); + + const auto base = Accessor::base(model, accessorIdx); + + if (accessor.sparse.has_value()) { + std::vector vec(accessor.count); + for (std::size_t i = 0; i < accessor.count; ++i) { + vec[i] = base.get(i); + } + const auto overriddenCount = accessor.sparse->count; + const auto indicesAccessor = ([&]() -> AccessorVariant { + switch (accessor.sparse->indices.componentType) { + case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_BYTE: + return Accessor::sparseIndices(model, accessor.sparse->indices, overriddenCount); + case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_SHORT: + return Accessor::sparseIndices(model, accessor.sparse->indices, overriddenCount); + case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_INT: + return Accessor::sparseIndices(model, accessor.sparse->indices, overriddenCount); + } + throw std::logic_error("invalid enum value"); + })(); + + const auto valuesAccessor = Accessor::sparseValues(model, + accessor.sparse->values, overriddenCount, + accessor.bufferView.has_value() + ? model.bufferViews->at(*accessor.bufferView).byteStride.value_or(accessor.elementSize()) + : accessor.elementSize()); + + for (std::size_t i = 0; i < overriddenCount; ++i) { + u32 index; + std::visit([&](auto &&acc) { index = acc.get(i); }, indicesAccessor); + if (index >= accessor.count) + throw std::runtime_error("index out of bounds"); + vec[index] = valuesAccessor.get(i); + } + return Accessor(vec, accessor.count); + } + + return base; +} + +#define ACCESSOR_TYPES(T, U, V) \ + template <> \ + constexpr tiniergltf::Accessor::Type SelfType::Accessor::getType() \ + { \ + return tiniergltf::Accessor::Type::U; \ + } \ + template <> \ + constexpr tiniergltf::Accessor::ComponentType SelfType::Accessor::getComponentType() \ + { \ + return tiniergltf::Accessor::ComponentType::V; \ + } + +#define VEC_ACCESSOR_TYPES(T, U, N) \ + template <> \ + constexpr tiniergltf::Accessor::Type SelfType::Accessor>::getType() \ + { \ + return tiniergltf::Accessor::Type::VEC##N; \ + } \ + template <> \ + constexpr tiniergltf::Accessor::ComponentType SelfType::Accessor>::getComponentType() \ + { \ + return tiniergltf::Accessor::ComponentType::U; \ + } \ + template <> \ + std::array SelfType::rawget(const char *ptr) \ + { \ + std::array res; \ + for (int i = 0; i < N; ++i) \ + res[i] = rawget(ptr + sizeof(T) * i); \ + return res; \ + } + +#define ACCESSOR_PRIMITIVE(T, U) \ + ACCESSOR_TYPES(T, SCALAR, U) \ + VEC_ACCESSOR_TYPES(T, U, 2) \ + VEC_ACCESSOR_TYPES(T, U, 3) \ + VEC_ACCESSOR_TYPES(T, U, 4) + +ACCESSOR_PRIMITIVE(f32, FLOAT) +ACCESSOR_PRIMITIVE(u8, UNSIGNED_BYTE) +ACCESSOR_PRIMITIVE(u16, UNSIGNED_SHORT) +ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT) + +ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT) + +template +T SelfType::Accessor::get(std::size_t i) const +{ + // Buffer-based accessor: Read directly from the buffer. + if (std::holds_alternative(source)) { + const auto bufsrc = std::get(source); + return rawget(bufsrc.ptr + i * bufsrc.byteStride); + } + // Array-based accessor (used for sparse accessors): Read from array. + if (std::holds_alternative>(source)) { + return std::get>(source)[i]; + } + // Default-initialized accessor. + // We differ slightly from glTF here in that + // we default-initialize quaternions and matrices properly, + // but this does not cause any discrepancies for valid glTF models. + std::get>(source); + return T(); +} + +template +T SelfType::rawget(const char *ptr) +{ + T dest; + std::memcpy(&dest, ptr, sizeof(dest)); +#ifdef __BIG_ENDIAN__ + return os::Byteswap::byteswap(dest); +#else + return dest; +#endif +} + +// Note that these "more specialized templates" should win. + +template <> +core::matrix4 SelfType::rawget(const char *ptr) +{ + core::matrix4 mat; + for (u8 i = 0; i < 16; ++i) { + mat[i] = rawget(ptr + i * sizeof(f32)); + } + return mat; +} + +template <> +core::vector3df SelfType::rawget(const char *ptr) +{ + return core::vector3df( + rawget(ptr), + rawget(ptr + sizeof(f32)), + rawget(ptr + 2 * sizeof(f32))); +} + +template <> +core::quaternion SelfType::rawget(const char *ptr) +{ + return core::quaternion( + rawget(ptr), + rawget(ptr + sizeof(f32)), + rawget(ptr + 2 * sizeof(f32)), + rawget(ptr + 3 * sizeof(f32))); +} + +template +SelfType::NormalizedValuesAccessor +SelfType::createNormalizedValuesAccessor( + const tiniergltf::GlTF &model, + const std::size_t accessorIdx) +{ + const auto &acc = model.accessors->at(accessorIdx); + switch (acc.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: + return Accessor>::make(model, accessorIdx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: + return Accessor>::make(model, accessorIdx); + case tiniergltf::Accessor::ComponentType::FLOAT: + return Accessor>::make(model, accessorIdx); + default: + throw std::runtime_error("invalid component type"); + } +} + +template +std::array SelfType::getNormalizedValues( + const NormalizedValuesAccessor &accessor, + const std::size_t i) +{ + std::array values; + if (std::holds_alternative>>(accessor)) { + const auto u8s = std::get>>(accessor).get(i); + for (u8 i = 0; i < N; ++i) + values[i] = static_cast(u8s[i]) / std::numeric_limits::max(); + } else if (std::holds_alternative>>(accessor)) { + const auto u16s = std::get>>(accessor).get(i); + for (u8 i = 0; i < N; ++i) + values[i] = static_cast(u16s[i]) / std::numeric_limits::max(); + } else { + values = std::get>>(accessor).get(i); + for (u8 i = 0; i < N; ++i) { + if (values[i] < 0 || values[i] > 1) + throw std::runtime_error("invalid normalized value"); + } + } + return values; +} + +/** + * The most basic portion of the code base. This tells irllicht if this file has a .gltf extension. +*/ +bool SelfType::isALoadableFileExtension( + const io::path& filename) const +{ + return core::hasFileExtension(filename, "gltf"); +} + +/** + * Entry point into loading a GLTF model. +*/ +IAnimatedMesh* SelfType::createMesh(io::IReadFile* file) +{ + if (file->getSize() <= 0) { + return nullptr; + } + std::optional model = tryParseGLTF(file); + if (!model.has_value()) { + return nullptr; + } + + if (!(model->buffers.has_value() + && model->bufferViews.has_value() + && model->accessors.has_value() + && model->meshes.has_value() + && model->nodes.has_value())) { + os::Printer::log("glTF loader", "missing required fields", ELL_ERROR); + return nullptr; + } + + auto *mesh = new CSkinnedMesh(); + MeshExtractor parser(std::move(model.value()), mesh); + try { + parser.loadNodes(); + } catch (std::runtime_error &e) { + os::Printer::log("glTF loader", e.what(), ELL_ERROR); + mesh->drop(); + return nullptr; + } + if (model->images.has_value()) + os::Printer::log("glTF loader", "embedded images are not supported", ELL_WARNING); + return mesh; +} + +static void transformVertices(std::vector &vertices, const core::matrix4 &transform) +{ + for (auto &vertex : vertices) { + // Apply scaling, rotation and rotation (in that order) to the position. + transform.transformVect(vertex.Pos); + // For the normal, we do not want to apply the translation. + // TODO note that this also applies scaling; the Irrlicht method is misnamed. + transform.rotateVect(vertex.Normal); + // Renormalize (length might have been affected by scaling). + vertex.Normal.normalize(); + } +} + +static void checkIndices(const std::vector &indices, const std::size_t nVerts) +{ + for (u16 index : indices) { + if (index >= nVerts) + throw std::runtime_error("index out of bounds"); + } +} + +static std::vector generateIndices(const std::size_t nVerts) +{ + std::vector indices(nVerts); + for (std::size_t i = 0; i < nVerts; i += 3) { + // Reverse winding order per triangle + indices[i] = i + 2; + indices[i + 1] = i + 1; + indices[i + 2] = i; + } + return indices; +} + +/** + * Load up the rawest form of the model. The vertex positions and indices. + * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes + * If material is undefined, then a default material MUST be used. +*/ +void SelfType::MeshExtractor::loadMesh( + const std::size_t meshIdx, + ISkinnedMesh::SJoint *parent) const +{ + for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) { + const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi); + auto vertices = getVertices(primitive); + if (!vertices.has_value()) + continue; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" + + // Excludes the max value for consistency. + if (vertices->size() >= std::numeric_limits::max()) + throw std::runtime_error("too many vertices"); + + // Apply the global transform along the parent chain. + transformVertices(*vertices, parent->GlobalMatrix); + + auto maybeIndices = getIndices(primitive); + std::vector indices; + if (maybeIndices.has_value()) { + indices = std::move(*maybeIndices); + checkIndices(indices, vertices->size()); + } else { + // Non-indexed geometry + indices = generateIndices(vertices->size()); + } + + m_irr_model->addMeshBuffer( + new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); + + if (primitive.material.has_value()) { + const auto &material = m_gltf_model.materials->at(*primitive.material); + if (material.pbrMetallicRoughness.has_value()) { + const auto &texture = material.pbrMetallicRoughness->baseColorTexture; + if (texture.has_value()) { + const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1; + m_irr_model->setTextureSlot(meshbufNr, static_cast(texture->index)); + } + } + } + } +} + +// Base transformation between left & right handed coordinate systems. +// This just inverts the Z axis. +static const core::matrix4 leftToRight = core::matrix4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1 +); +static const core::matrix4 rightToLeft = leftToRight; + +static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) +{ + // Note: Under the hood, this casts these doubles to floats. + return core::matrix4( + m[0], m[1], m[2], m[3], + m[4], m[5], m[6], m[7], + m[8], m[9], m[10], m[11], + m[12], m[13], m[14], m[15]); +} + +static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) +{ + const auto &trans = trs.translation; + const auto &rot = trs.rotation; + const auto &scale = trs.scale; + core::matrix4 transMat; + transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2])); + core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix(); + core::matrix4 scaleMat; + scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); + return transMat * rotMat * scaleMat; +} + +static core::matrix4 loadTransform(std::optional> transform) { + if (!transform.has_value()) { + return core::matrix4(); + } + core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); + return rightToLeft * mat * leftToRight; +} + +void SelfType::MeshExtractor::loadNode( + const std::size_t nodeIdx, + ISkinnedMesh::SJoint *parent) const +{ + const auto &node = m_gltf_model.nodes->at(nodeIdx); + auto *joint = m_irr_model->addJoint(parent); + const core::matrix4 transform = loadTransform(node.transform); + joint->LocalMatrix = transform; + joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; + if (node.name.has_value()) { + joint->Name = node.name->c_str(); + } + if (node.mesh.has_value()) { + loadMesh(*node.mesh, joint); + } + if (node.children.has_value()) { + for (const auto &child : *node.children) { + loadNode(child, joint); + } + } +} + +void SelfType::MeshExtractor::loadNodes() const +{ + std::vector isChild(m_gltf_model.nodes->size()); + for (const auto &node : *m_gltf_model.nodes) { + if (!node.children.has_value()) + continue; + for (const auto &child : *node.children) { + isChild[child] = true; + } + } + // Load all nodes that aren't children. + // Children will be loaded by their parent nodes. + for (std::size_t i = 0; i < m_gltf_model.nodes->size(); ++i) { + if (!isChild[i]) { + loadNode(i, nullptr); + } + } +} + +/** + * Extracts GLTF mesh indices. + */ +std::optional> SelfType::MeshExtractor::getIndices( + const tiniergltf::MeshPrimitive &primitive) const +{ + const auto accessorIdx = primitive.indices; + if (!accessorIdx.has_value()) + return std::nullopt; // non-indexed geometry + + const auto accessor = ([&]() -> AccessorVariant { + const auto &acc = m_gltf_model.accessors->at(*accessorIdx); + switch (acc.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: + return Accessor::make(m_gltf_model, *accessorIdx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: + return Accessor::make(m_gltf_model, *accessorIdx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_INT: + return Accessor::make(m_gltf_model, *accessorIdx); + default: + throw std::runtime_error("invalid component type"); + } + })(); + const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor); + + std::vector indices; + for (std::size_t i = 0; i < count; ++i) { + // TODO (low-priority, maybe never) also reverse winding order based on determinant of global transform + // FIXME this hack also reverses triangle draw order + std::size_t elemIdx = count - i - 1; // reverse index order + u16 index; + // Note: glTF forbids the max value for each component type. + if (std::holds_alternative>(accessor)) { + index = std::get>(accessor).get(elemIdx); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + } else if (std::holds_alternative>(accessor)) { + index = std::get>(accessor).get(elemIdx); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + } else if (std::holds_alternative>(accessor)) { + u32 indexWide = std::get>(accessor).get(elemIdx); + // Use >= here for consistency. + if (indexWide >= std::numeric_limits::max()) + throw std::runtime_error("index too large (>= 65536)"); + index = static_cast(indexWide); + } + indices.push_back(index); + } + + return indices; +} + +/** + * Create a vector of video::S3DVertex (model data) from a mesh & primitive index. + */ +std::optional> SelfType::MeshExtractor::getVertices( + const tiniergltf::MeshPrimitive &primitive) const +{ + const auto &attributes = primitive.attributes; + const auto positionAccessorIdx = attributes.position; + if (!positionAccessorIdx.has_value()) { + // "When positions are not specified, client implementations SHOULD skip primitive's rendering" + return std::nullopt; + } + + std::vector vertices; + const auto vertexCount = m_gltf_model.accessors->at(*positionAccessorIdx).count; + vertices.resize(vertexCount); + copyPositions(*positionAccessorIdx, vertices); + + const auto normalAccessorIdx = attributes.normal; + if (normalAccessorIdx.has_value()) { + copyNormals(normalAccessorIdx.value(), vertices); + } + // TODO verify that the automatic normal recalculation done in Minetest indeed works correctly + + const auto &texcoords = attributes.texcoord; + if (texcoords.has_value()) { + const auto tCoordAccessorIdx = texcoords->at(0); + copyTCoords(tCoordAccessorIdx, vertices); + } + + return vertices; +} + +/** + * Get the amount of meshes that a model contains. +*/ +std::size_t SelfType::MeshExtractor::getMeshCount() const +{ + return m_gltf_model.meshes->size(); +} + +/** + * Get the amount of primitives that a mesh in a model contains. +*/ +std::size_t SelfType::MeshExtractor::getPrimitiveCount( + const std::size_t meshIdx) const +{ + return m_gltf_model.meshes->at(meshIdx).primitives.size(); +} + +/** + * Streams vertex positions raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void SelfType::MeshExtractor::copyPositions( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + const auto accessor = Accessor::make(m_gltf_model, accessorIdx); + for (std::size_t i = 0; i < accessor.getCount(); i++) { + vertices[i].Pos = convertHandedness(accessor.get(i)); + } +} + +/** + * Streams normals raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void SelfType::MeshExtractor::copyNormals( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + const auto accessor = Accessor::make(m_gltf_model, accessorIdx); + for (std::size_t i = 0; i < accessor.getCount(); ++i) { + vertices[i].Normal = convertHandedness(accessor.get(i)); + } +} + +/** + * Streams texture coordinate raw data into usable buffer via reference. + * Buffer: ref Vector +*/ +void SelfType::MeshExtractor::copyTCoords( + const std::size_t accessorIdx, + std::vector& vertices) const +{ + const auto accessor = createNormalizedValuesAccessor<2>(m_gltf_model, accessorIdx); + const auto count = std::visit([](auto &&a) { return a.getCount(); }, accessor); + for (std::size_t i = 0; i < count; ++i) { + const auto vals = getNormalizedValues(accessor, i); + vertices[i].TCoords = core::vector2df(vals[0], vals[1]); + } +} + +/** + * This is where the actual model's GLTF file is loaded and parsed by tiniergltf. +*/ +std::optional SelfType::tryParseGLTF(io::IReadFile* file) +{ + auto size = file->getSize(); + if (size < 0) // this can happen if `ftell` fails + return std::nullopt; + std::unique_ptr buf(new char[size + 1]); + if (file->read(buf.get(), size) != static_cast(size)) + return std::nullopt; + // We probably don't need this, but add it just to be sure. + buf[size] = '\0'; + Json::CharReaderBuilder builder; + const std::unique_ptr reader(builder.newCharReader()); + Json::Value json; + JSONCPP_STRING err; + if (!reader->parse(buf.get(), buf.get() + size, &json, &err)) { + return std::nullopt; + } + try { + return tiniergltf::GlTF(json); + } catch (const std::runtime_error &e) { + os::Printer::log("glTF loader", e.what(), ELL_ERROR); + return std::nullopt; + } catch (const std::out_of_range &e) { + os::Printer::log("glTF loader", e.what(), ELL_ERROR); + return std::nullopt; + } +} + +} // namespace scene + +} // namespace irr + diff --git a/irr/src/CGLTFMeshFileLoader.h b/irr/src/CGLTFMeshFileLoader.h new file mode 100644 index 000000000..39c3ea6dd --- /dev/null +++ b/irr/src/CGLTFMeshFileLoader.h @@ -0,0 +1,147 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "CSkinnedMesh.h" +#include "IMeshLoader.h" +#include "IReadFile.h" +#include "irrTypes.h" +#include "path.h" +#include "S3DVertex.h" + +#include + +#include +#include + +namespace irr +{ + +namespace scene +{ + +class CGLTFMeshFileLoader : public IMeshLoader +{ +public: + CGLTFMeshFileLoader() noexcept {}; + + bool isALoadableFileExtension(const io::path& filename) const override; + + IAnimatedMesh* createMesh(io::IReadFile* file) override; + +private: + template + static T rawget(const char *ptr); + + template + class Accessor + { + struct BufferSource + { + const char *ptr; + std::size_t byteStride; + }; + using Source = std::variant, std::tuple<>>; + + public: + static Accessor sparseIndices( + const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseIndices &indices, + const std::size_t count); + static Accessor sparseValues( + const tiniergltf::GlTF &model, + const tiniergltf::AccessorSparseValues &values, + const std::size_t count, + const std::size_t defaultByteStride); + static Accessor base( + const tiniergltf::GlTF &model, + std::size_t accessorIdx); + static Accessor make(const tiniergltf::GlTF &model, std::size_t accessorIdx); + static constexpr tiniergltf::Accessor::Type getType(); + static constexpr tiniergltf::Accessor::ComponentType getComponentType(); + std::size_t getCount() const { return count; } + T get(std::size_t i) const; + + private: + Accessor(const char *ptr, std::size_t byteStride, std::size_t count) : + source(BufferSource{ptr, byteStride}), count(count) {} + Accessor(std::vector vec, std::size_t count) : + source(vec), count(count) {} + Accessor(std::size_t count) : + source(std::make_tuple()), count(count) {} + // Directly from buffer, sparse, or default-initialized + const Source source; + const std::size_t count; + }; + + template + using AccessorVariant = std::variant...>; + + template + using ArrayAccessorVariant = std::variant>...>; + + template + using NormalizedValuesAccessor = ArrayAccessorVariant; + + template + static NormalizedValuesAccessor createNormalizedValuesAccessor( + const tiniergltf::GlTF &model, + const std::size_t accessorIdx); + + template + static std::array getNormalizedValues( + const NormalizedValuesAccessor &accessor, + const std::size_t i); + + class MeshExtractor { + public: + MeshExtractor(tiniergltf::GlTF &&model, + CSkinnedMesh *mesh) noexcept + : m_gltf_model(model), m_irr_model(mesh) {}; + + /* Gets indices for the given mesh/primitive. + * + * Values are return in Irrlicht winding order. + */ + std::optional> getIndices( + const tiniergltf::MeshPrimitive &primitive) const; + + std::optional> getVertices( + const tiniergltf::MeshPrimitive &primitive) const; + + std::size_t getMeshCount() const; + + std::size_t getPrimitiveCount(const std::size_t meshIdx) const; + + void loadNodes() const; + + private: + const tiniergltf::GlTF m_gltf_model; + CSkinnedMesh *m_irr_model; + + void copyPositions(const std::size_t accessorIdx, + std::vector& vertices) const; + + void copyNormals(const std::size_t accessorIdx, + std::vector& vertices) const; + + void copyTCoords(const std::size_t accessorIdx, + std::vector& vertices) const; + + void loadMesh( + std::size_t meshIdx, + ISkinnedMesh::SJoint *parentJoint) const; + + void loadNode( + const std::size_t nodeIdx, + ISkinnedMesh::SJoint *parentJoint) const; + }; + + std::optional tryParseGLTF(io::IReadFile* file); +}; + +} // namespace scene + +} // namespace irr + diff --git a/irr/src/CMakeLists.txt b/irr/src/CMakeLists.txt index 161a0c060..f5bc675e4 100644 --- a/irr/src/CMakeLists.txt +++ b/irr/src/CMakeLists.txt @@ -17,7 +17,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") set(CMAKE_CXX_FLAGS_RELEASE "-O3") set(CMAKE_CXX_FLAGS_DEBUG "-g") - add_compile_options(-Wall -pipe -fno-exceptions) + add_compile_options(-Wall -pipe) # Enable SSE for floating point math on 32-bit x86 by default # reasoning see minetest issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath @@ -298,6 +298,7 @@ set(link_includes set(IRRMESHLOADER CB3DMeshFileLoader.cpp + CGLTFMeshFileLoader.cpp COBJMeshFileLoader.cpp CXMeshFileLoader.cpp ) @@ -310,6 +311,8 @@ add_library(IRRMESHOBJ OBJECT ${IRRMESHLOADER} ) +target_link_libraries(IRRMESHOBJ PUBLIC tiniergltf::tiniergltf) + add_library(IRROBJ OBJECT CBillboardSceneNode.cpp CCameraSceneNode.cpp @@ -321,6 +324,10 @@ add_library(IRROBJ OBJECT CMeshCache.cpp ) +# Make sure IRROBJ gets the transitive include directories for +# tiniergltf from IRRMESHOBJ. +target_link_libraries(IRROBJ PRIVATE IRRMESHOBJ) + set(IRRDRVROBJ CNullDriver.cpp CGLXManager.cpp @@ -477,6 +484,7 @@ target_include_directories(IrrlichtMt # this needs to be here and not in a variable (like link_includes) due to issues # with the generator expressions on at least CMake 3.22, but not 3.28 or later target_link_libraries(IrrlichtMt PRIVATE + tiniergltf::tiniergltf ${ZLIB_LIBRARY} ${JPEG_LIBRARY} ${PNG_LIBRARY} diff --git a/irr/src/CSceneManager.cpp b/irr/src/CSceneManager.cpp index cd88e1a6e..c75a28a48 100644 --- a/irr/src/CSceneManager.cpp +++ b/irr/src/CSceneManager.cpp @@ -20,6 +20,7 @@ #include "CXMeshFileLoader.h" #include "COBJMeshFileLoader.h" #include "CB3DMeshFileLoader.h" +#include "CGLTFMeshFileLoader.h" #include "CBillboardSceneNode.h" #include "CAnimatedMeshSceneNode.h" #include "CCameraSceneNode.h" @@ -78,6 +79,7 @@ CSceneManager::CSceneManager(video::IVideoDriver *driver, MeshLoaderList.push_back(new CXMeshFileLoader(this)); MeshLoaderList.push_back(new COBJMeshFileLoader(this)); MeshLoaderList.push_back(new CB3DMeshFileLoader(this)); + MeshLoaderList.push_back(new CGLTFMeshFileLoader()); } //! destructor diff --git a/irr/src/CSkinnedMesh.cpp b/irr/src/CSkinnedMesh.cpp index eb7317309..5db027abc 100644 --- a/irr/src/CSkinnedMesh.cpp +++ b/irr/src/CSkinnedMesh.cpp @@ -6,6 +6,7 @@ #include #include "CBoneSceneNode.h" #include "IAnimatedMeshSceneNode.h" +#include "SSkinMeshBuffer.h" #include "os.h" namespace @@ -596,6 +597,15 @@ IMeshBuffer *CSkinnedMesh::getMeshBuffer(const video::SMaterial &material) const return 0; } +u32 CSkinnedMesh::getTextureSlot(u32 meshbufNr) const +{ + return TextureSlots.at(meshbufNr); +} + +void CSkinnedMesh::setTextureSlot(u32 meshbufNr, u32 textureSlot) { + TextureSlots.at(meshbufNr) = textureSlot; +} + //! returns an axis aligned bounding box const core::aabbox3d &CSkinnedMesh::getBoundingBox() const { @@ -1057,10 +1067,17 @@ void CSkinnedMesh::updateBoundingBox(void) scene::SSkinMeshBuffer *CSkinnedMesh::addMeshBuffer() { scene::SSkinMeshBuffer *buffer = new scene::SSkinMeshBuffer(); + TextureSlots.push_back(LocalBuffers.size()); LocalBuffers.push_back(buffer); return buffer; } +void CSkinnedMesh::addMeshBuffer(SSkinMeshBuffer *meshbuf) +{ + TextureSlots.push_back(LocalBuffers.size()); + LocalBuffers.push_back(meshbuf); +} + CSkinnedMesh::SJoint *CSkinnedMesh::addJoint(SJoint *parent) { SJoint *joint = new SJoint; diff --git a/irr/src/CSkinnedMesh.h b/irr/src/CSkinnedMesh.h index b0228c93b..4b4c5e3b7 100644 --- a/irr/src/CSkinnedMesh.h +++ b/irr/src/CSkinnedMesh.h @@ -61,6 +61,10 @@ public: NULL if there is no such mesh buffer. */ IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const override; + u32 getTextureSlot(u32 meshbufNr) const override; + + void setTextureSlot(u32 meshbufNr, u32 textureSlot); + //! returns an axis aligned bounding box const core::aabbox3d &getBoundingBox() const override; @@ -129,6 +133,9 @@ public: //! Adds a new meshbuffer to the mesh, access it as last one SSkinMeshBuffer *addMeshBuffer() override; + //! Adds a new meshbuffer to the mesh, access it as last one + void addMeshBuffer(SSkinMeshBuffer *meshbuf) override; + //! Adds a new joint to the mesh, access it as last one SJoint *addJoint(SJoint *parent = 0) override; @@ -184,6 +191,8 @@ private: core::array *SkinningBuffers; // Meshbuffer to skin, default is to skin localBuffers core::array LocalBuffers; + //! Mapping from meshbuffer number to bindable texture slot + std::vector TextureSlots; core::array AllJoints; core::array RootJoints; diff --git a/lib/tiniergltf/.gitignore b/lib/tiniergltf/.gitignore new file mode 100644 index 000000000..6d105f52b --- /dev/null +++ b/lib/tiniergltf/.gitignore @@ -0,0 +1,6 @@ +cmake +CMakeCache.txt +CMakeFiles +.cache +compile_commands.json +build \ No newline at end of file diff --git a/lib/tiniergltf/CMakeLists.txt b/lib/tiniergltf/CMakeLists.txt new file mode 100644 index 000000000..889203e49 --- /dev/null +++ b/lib/tiniergltf/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.12) + +project(tiniergltf + VERSION 1.0.0 + LANGUAGES CXX +) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(tiniergltf OBJECT tiniergltf.hpp) +add_library(tiniergltf::tiniergltf ALIAS tiniergltf) +set_target_properties(tiniergltf PROPERTIES LINKER_LANGUAGE CXX) + +target_include_directories(tiniergltf + INTERFACE + "$" + "${JSON_INCLUDE_DIR}" # Set in FindJson.cmake + "${CMAKE_SOURCE_DIR}/src" # util/base64.h +) + +target_link_libraries(tiniergltf) diff --git a/lib/tiniergltf/Readme.md b/lib/tiniergltf/Readme.md new file mode 100644 index 000000000..a8cc65c93 --- /dev/null +++ b/lib/tiniergltf/Readme.md @@ -0,0 +1,39 @@ +# TinierGLTF + +A safe, modern, tiny glTF loader for C++ 17. + +What this is: + +* A tiny glTF deserializer which maps JSON objects to appropriate C++ structures. +* Intended to be safe for loading untrusted input. +* Slightly tailored to the needs of [Minetest](https://github.com/minetest/minetest). + +What this doesn't and shouldn't do: + +* Serialization +* Loading images +* Resolving resources +* Support glTF extensions + +## TODOs + +- [ ] Add GLB support. +- [ ] Add further checks according to the specification. + - Everything in the JSON schema (+ indices and misc. stuff) is already validated. + Much of the code was generated by a Lua script from the JSON schemata. + +## License + +`tiniergltf.hpp` was written by Lars Müller and is licensed under the MIT license: + +> Copyright 2024 Lars Müller +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Bug Bounty + +I offer a reward of one (1) virtual headpat per valid bug report. diff --git a/lib/tiniergltf/tiniergltf.hpp b/lib/tiniergltf/tiniergltf.hpp new file mode 100644 index 000000000..6a861556e --- /dev/null +++ b/lib/tiniergltf/tiniergltf.hpp @@ -0,0 +1,1357 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "util/base64.h" + +namespace tiniergltf { + +static inline void check(bool cond) { + if (!cond) + throw std::runtime_error("invalid glTF"); +} + +template +static inline void checkIndex(const std::optional> &vec, + const std::optional &i) { + if (!i.has_value()) return; + check(vec.has_value()); + check(i < vec->size()); +} + +template +static inline void checkIndex(const std::vector &vec, + const std::optional &i) { + if (!i.has_value()) return; + check(i < vec.size()); +} + +template +static inline void checkForall(const std::optional> &vec, const F &cond) { + if (!vec.has_value()) + return; + for (const T &v : vec.value()) + cond(v); +} + +template +static inline void checkDuplicateFree(const std::vector &vec) { + check(std::unordered_set(vec.begin(), vec.end()).size() == vec.size()); +} + +template +static inline T as(const Json::Value &o); + +template<> +bool as(const Json::Value &o) { + check(o.isBool()); + return o.asBool(); +} + +template<> +double as (const Json::Value &o) { + check(o.isDouble()); + return o.asDouble(); +} + +template<> +std::size_t as(const Json::Value &o) { + check(o.isUInt64()); + auto u = o.asUInt64(); + check(u <= std::numeric_limits::max()); + return u; +} + +template<> +std::string as(const Json::Value &o) { + check(o.isString()); + return o.asString(); +} + +template +std::vector asVec(const Json::Value &o) { + check(o.isArray()); + std::vector res; + res.reserve(o.size()); + for (Json::ArrayIndex i = 0; i < o.size(); ++i) { + res.push_back(as(o[i])); + } + return res; +} + +template +std::array asArr(const Json::Value &o) { + check(o.isArray()); + check(o.size() == n); + std::array res; + for (Json::ArrayIndex i = 0; i < n; ++i) { + res[i] = as(o[i]); + } + return res; +} + +struct AccessorSparseIndices { + std::size_t bufferView; + std::size_t byteOffset; + // as defined in the glTF specification + enum class ComponentType { + UNSIGNED_BYTE, + UNSIGNED_SHORT, + UNSIGNED_INT, + }; + ComponentType componentType; + std::size_t componentSize() const { + switch (componentType) { + case ComponentType::UNSIGNED_BYTE: + return 1; + case ComponentType::UNSIGNED_SHORT: + return 2; + case ComponentType::UNSIGNED_INT: + return 4; + } + throw std::logic_error("invalid component type"); + } + std::size_t elementSize() const { + return componentSize(); + } + AccessorSparseIndices(const Json::Value &o) + : bufferView(as(o["bufferView"])) + , byteOffset(0) + { + check(o.isObject()); + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + } + { + static std::unordered_map map = { + {5121, ComponentType::UNSIGNED_BYTE}, + {5123, ComponentType::UNSIGNED_SHORT}, + {5125, ComponentType::UNSIGNED_INT}, + }; + const auto &v = o["componentType"]; check(v.isUInt64()); + componentType = map.at(v.asUInt64()); + } + } +}; +template<> AccessorSparseIndices as(const Json::Value &o) { return o; } + +struct AccessorSparseValues { + std::size_t bufferView; + std::size_t byteOffset; + AccessorSparseValues(const Json::Value &o) + : bufferView(as(o["bufferView"])) + , byteOffset(0) + { + check(o.isObject()); + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + } + } +}; +template<> AccessorSparseValues as(const Json::Value &o) { return o; } + +struct AccessorSparse { + std::size_t count; + AccessorSparseIndices indices; + AccessorSparseValues values; + AccessorSparse(const Json::Value &o) + : count(as(o["count"])) + , indices(as(o["indices"])) + , values(as(o["values"])) + { + check(o.isObject()); + check(count >= 1); + } +}; +template<> AccessorSparse as(const Json::Value &o) { return o; } + +struct Accessor { + std::optional bufferView; + std::size_t byteOffset; + // as defined in the glTF specification + enum class ComponentType { + BYTE, + UNSIGNED_BYTE, + SHORT, + UNSIGNED_SHORT, + UNSIGNED_INT, + FLOAT, + }; + ComponentType componentType; + std::size_t componentSize() const { + switch (componentType) { + case ComponentType::BYTE: + case ComponentType::UNSIGNED_BYTE: + return 1; + case ComponentType::SHORT: + case ComponentType::UNSIGNED_SHORT: + return 2; + case ComponentType::UNSIGNED_INT: + case ComponentType::FLOAT: + return 4; + } + throw std::logic_error("invalid component type"); + } + std::size_t count; + std::optional> max; + std::optional> min; + std::optional name; + bool normalized; + std::optional sparse; + enum class Type { + MAT2, + MAT3, + MAT4, + SCALAR, + VEC2, + VEC3, + VEC4, + }; + std::size_t typeCount() const { + switch (type) { + case Type::SCALAR: + return 1; + case Type::VEC2: + return 2; + case Type::VEC3: + return 3; + case Type::MAT2: + case Type::VEC4: + return 4; + case Type::MAT3: + return 9; + case Type::MAT4: + return 16; + } + throw std::logic_error("invalid type"); + } + Type type; + std::size_t elementSize() const { + return componentSize() * typeCount(); + } + Accessor(const Json::Value &o) + : byteOffset(0) + , count(as(o["count"])) + , normalized(false) + { + check(o.isObject()); + if (o.isMember("bufferView")) { + bufferView = as(o["bufferView"]); + } + { + static std::unordered_map map = { + {5120, ComponentType::BYTE}, + {5121, ComponentType::UNSIGNED_BYTE}, + {5122, ComponentType::SHORT}, + {5123, ComponentType::UNSIGNED_SHORT}, + {5125, ComponentType::UNSIGNED_INT}, + {5126, ComponentType::FLOAT}, + }; + const auto &v = o["componentType"]; check(v.isUInt64()); + componentType = map.at(v.asUInt64()); + } + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + check(byteOffset % componentSize() == 0); + } + check(count >= 1); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("normalized")) { + normalized = as(o["normalized"]); + } + if (o.isMember("sparse")) { + sparse = as(o["sparse"]); + } + { + static std::unordered_map map = { + {"MAT2", Type::MAT2}, + {"MAT3", Type::MAT3}, + {"MAT4", Type::MAT4}, + {"SCALAR", Type::SCALAR}, + {"VEC2", Type::VEC2}, + {"VEC3", Type::VEC3}, + {"VEC4", Type::VEC4}, + }; + const auto &v = o["type"]; check(v.isString()); + type = map.at(v.asString()); + } + if (o.isMember("max")) { + max = asVec(o["max"]); + check(max->size() == typeCount()); + } + if (o.isMember("min")) { + min = asVec(o["min"]); + check(min->size() == typeCount()); + } + } +}; +template<> Accessor as(const Json::Value &o) { return o; } + +struct AnimationChannelTarget { + std::optional node; + enum class Path { + ROTATION, + SCALE, + TRANSLATION, + WEIGHTS, + }; + Path path; + AnimationChannelTarget(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("node")) { + node = as(o["node"]); + } + { + static std::unordered_map map = { + {"rotation", Path::ROTATION}, + {"scale", Path::SCALE}, + {"translation", Path::TRANSLATION}, + {"weights", Path::WEIGHTS}, + }; + const auto &v = o["path"]; check(v.isString()); + path = map.at(v.asString()); + } + } +}; +template<> AnimationChannelTarget as(const Json::Value &o) { return o; } + +struct AnimationChannel { + std::size_t sampler; + AnimationChannelTarget target; + AnimationChannel(const Json::Value &o) + : sampler(as(o["sampler"])) + , target(as(o["target"])) + { + check(o.isObject()); + } +}; +template<> AnimationChannel as(const Json::Value &o) { return o; } + +struct AnimationSampler { + std::size_t input; + enum class Interpolation { + CUBICSPLINE, + LINEAR, + STEP, + }; + Interpolation interpolation; + std::size_t output; + AnimationSampler(const Json::Value &o) + : input(as(o["input"])) + , interpolation(Interpolation::LINEAR) + , output(as(o["output"])) + { + check(o.isObject()); + if (o.isMember("interpolation")) { + static std::unordered_map map = { + {"CUBICSPLINE", Interpolation::CUBICSPLINE}, + {"LINEAR", Interpolation::LINEAR}, + {"STEP", Interpolation::STEP}, + }; + const auto &v = o["interpolation"]; check(v.isString()); + interpolation = map.at(v.asString()); + } + } +}; +template<> AnimationSampler as(const Json::Value &o) { return o; } + +struct Animation { + std::vector channels; + std::optional name; + std::vector samplers; + Animation(const Json::Value &o) + : channels(asVec(o["channels"])) + , samplers(asVec(o["samplers"])) + { + check(o.isObject()); + check(channels.size() >= 1); + if (o.isMember("name")) { + name = as(o["name"]); + } + check(samplers.size() >= 1); + } +}; +template<> Animation as(const Json::Value &o) { return o; } + +struct Asset { + std::optional copyright; + std::optional generator; + std::optional minVersion; + std::string version; + Asset(const Json::Value &o) + : version(as(o["version"])) + { + check(o.isObject()); + if (o.isMember("copyright")) { + copyright = as(o["copyright"]); + } + if (o.isMember("generator")) { + generator = as(o["generator"]); + } + if (o.isMember("minVersion")) { + minVersion = as(o["minVersion"]); + } + } +}; +template<> Asset as(const Json::Value &o) { return o; } + +struct BufferView { + std::size_t buffer; + std::size_t byteLength; + std::size_t byteOffset; + std::optional byteStride; + std::optional name; + enum class Target { + ARRAY_BUFFER, + ELEMENT_ARRAY_BUFFER, + }; + std::optional target; + BufferView(const Json::Value &o) + : buffer(as(o["buffer"])) + , byteLength(as(o["byteLength"])) + , byteOffset(0) + { + check(o.isObject()); + check(byteLength >= 1); + if (o.isMember("byteOffset")) { + byteOffset = as(o["byteOffset"]); + check(byteOffset >= 0); + } + if (o.isMember("byteStride")) { + byteStride = as(o["byteStride"]); + check(byteStride.value() >= 4); + check(byteStride.value() <= 252); + check(byteStride.value() % 4 == 0); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("target")) { + static std::unordered_map map = { + {34962, Target::ARRAY_BUFFER}, + {34963, Target::ELEMENT_ARRAY_BUFFER}, + }; + const auto &v = o["target"]; check(v.isUInt64()); + target = map.at(v.asUInt64()); + } + } +}; +template<> BufferView as(const Json::Value &o) { return o; } + +struct Buffer { + std::size_t byteLength; + std::optional name; + std::string data; + Buffer(const Json::Value &o, + const std::function &resolveURI) + : byteLength(as(o["byteLength"])) + { + check(o.isObject()); + check(byteLength >= 1); + if (o.isMember("name")) { + name = as(o["name"]); + } + check(o.isMember("uri")); + bool dataURI = false; + const std::string uri = as(o["uri"]); + for (auto &prefix : std::array { + "data:application/octet-stream;base64,", + "data:application/gltf-buffer;base64," + }) { + if (std::string_view(uri).substr(0, prefix.length()) == prefix) { + auto view = std::string_view(uri).substr(prefix.length()); + check(base64_is_valid(view)); + data = base64_decode(view); + dataURI = true; + break; + } + } + if (!dataURI) + data = resolveURI(uri); + check(data.size() >= byteLength); + data.resize(byteLength); + } +}; + +struct CameraOrthographic { + double xmag; + double ymag; + double zfar; + double znear; + CameraOrthographic(const Json::Value &o) + : xmag(as(o["xmag"])) + , ymag(as(o["ymag"])) + , zfar(as(o["zfar"])) + , znear(as(o["znear"])) + { + check(o.isObject()); + check(zfar > 0); + check(znear >= 0); + } +}; +template<> CameraOrthographic as(const Json::Value &o) { return o; } + +struct CameraPerspective { + std::optional aspectRatio; + double yfov; + std::optional zfar; + double znear; + CameraPerspective(const Json::Value &o) + : yfov(as(o["yfov"])) + , znear(as(o["znear"])) + { + check(o.isObject()); + if (o.isMember("aspectRatio")) { + aspectRatio = as(o["aspectRatio"]); + check(aspectRatio.value() > 0); + } + check(yfov > 0); + if (o.isMember("zfar")) { + zfar = as(o["zfar"]); + check(zfar.value() > 0); + } + check(znear > 0); + } +}; +template<> CameraPerspective as(const Json::Value &o) { return o; } + +struct Camera { + std::optional name; + std::optional orthographic; + std::optional perspective; + enum class Type { + ORTHOGRAPHIC, + PERSPECTIVE, + }; + Type type; + Camera(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("orthographic")) { + orthographic = as(o["orthographic"]); + } + if (o.isMember("perspective")) { + perspective = as(o["perspective"]); + } + { + static std::unordered_map map = { + {"orthographic", Type::ORTHOGRAPHIC}, + {"perspective", Type::PERSPECTIVE}, + }; + const auto &v = o["type"]; check(v.isString()); + type = map.at(v.asString()); + } + } +}; +template<> Camera as(const Json::Value &o) { return o; } + +struct Image { + std::optional bufferView; + enum class MimeType { + IMAGE_JPEG, + IMAGE_PNG, + }; + std::optional mimeType; + std::optional name; + std::optional uri; + Image(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("bufferView")) { + bufferView = as(o["bufferView"]); + } + if (o.isMember("mimeType")) { + static std::unordered_map map = { + {"image/jpeg", MimeType::IMAGE_JPEG}, + {"image/png", MimeType::IMAGE_PNG}, + }; + const auto &v = o["mimeType"]; check(v.isString()); + mimeType = map.at(v.asString()); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("uri")) { + uri = as(o["uri"]); + } + } +}; +template<> Image as(const Json::Value &o) { return o; } + +struct TextureInfo { + std::size_t index; + std::size_t texCoord; + TextureInfo(const Json::Value &o) + : index(as(o["index"])) + , texCoord(0) + { + check(o.isObject()); + if (o.isMember("texCoord")) { + texCoord = as(o["texCoord"]); + check(texCoord >= 0); + } + } +}; +template<> TextureInfo as(const Json::Value &o) { return o; } + +struct MaterialNormalTextureInfo { + std::size_t index; + double scale; + std::size_t texCoord; + MaterialNormalTextureInfo(const Json::Value &o) + : index(as(o["index"])) + , scale(1) + , texCoord(0) + { + check(o.isObject()); + if (o.isMember("scale")) { + scale = as(o["scale"]); + } + if (o.isMember("texCoord")) { + texCoord = as(o["texCoord"]); + } + } +}; +template<> MaterialNormalTextureInfo as(const Json::Value &o) { return o; } + +struct MaterialOcclusionTextureInfo { + std::size_t index; + double strength; + std::size_t texCoord; + MaterialOcclusionTextureInfo(const Json::Value &o) + : index(as(o["index"])) + , strength(1) + , texCoord(0) + { + check(o.isObject()); + if (o.isMember("strength")) { + strength = as(o["strength"]); + check(strength >= 0); + check(strength <= 1); + } + if (o.isMember("texCoord")) { + texCoord = as(o["texCoord"]); + } + } +}; +template<> MaterialOcclusionTextureInfo as(const Json::Value &o) { return o; } + +struct MaterialPbrMetallicRoughness { + std::array baseColorFactor; + std::optional baseColorTexture; + double metallicFactor; + std::optional metallicRoughnessTexture; + double roughnessFactor; + MaterialPbrMetallicRoughness(const Json::Value &o) + : baseColorFactor{1, 1, 1, 1} + , metallicFactor(1) + , roughnessFactor(1) + { + check(o.isObject()); + if (o.isMember("baseColorFactor")) { + baseColorFactor = asArr(o["baseColorFactor"]); + for (auto v: baseColorFactor) { + check(v >= 0); + check(v <= 1); + } + } + if (o.isMember("baseColorTexture")) { + baseColorTexture = as(o["baseColorTexture"]); + } + if (o.isMember("metallicFactor")) { + metallicFactor = as(o["metallicFactor"]); + check(metallicFactor >= 0); + check(metallicFactor <= 1); + } + if (o.isMember("metallicRoughnessTexture")) { + metallicRoughnessTexture = as(o["metallicRoughnessTexture"]); + } + if (o.isMember("roughnessFactor")) { + roughnessFactor = as(o["roughnessFactor"]); + check(roughnessFactor >= 0); + check(roughnessFactor <= 1); + } + } +}; +template<> MaterialPbrMetallicRoughness as(const Json::Value &o) { return o; } + +struct Material { + double alphaCutoff; + enum class AlphaMode { + BLEND, + MASK, + OPAQUE, + }; + AlphaMode alphaMode; + bool doubleSided; + std::array emissiveFactor; + std::optional emissiveTexture; + std::optional name; + std::optional normalTexture; + std::optional occlusionTexture; + std::optional pbrMetallicRoughness; + Material(const Json::Value &o) + : alphaCutoff(0.5) + , alphaMode(AlphaMode::OPAQUE) + , doubleSided(false) + , emissiveFactor{0, 0, 0} + { + check(o.isObject()); + if (o.isMember("alphaCutoff")) { + alphaCutoff = as(o["alphaCutoff"]); + check(alphaCutoff >= 0); + } + if (o.isMember("alphaMode")){ + static std::unordered_map map = { + {"BLEND", AlphaMode::BLEND}, + {"MASK", AlphaMode::MASK}, + {"OPAQUE", AlphaMode::OPAQUE}, + }; + const auto &v = o["alphaMode"]; check(v.isString()); + alphaMode = map.at(v.asString()); + } + if (o.isMember("doubleSided")) { + doubleSided = as(o["doubleSided"]); + } + if (o.isMember("emissiveFactor")) { + emissiveFactor = asArr(o["emissiveFactor"]); + for (const auto &v: emissiveFactor) { + check(v >= 0); + check(v <= 1); + } + } + if (o.isMember("emissiveTexture")) { + emissiveTexture = as(o["emissiveTexture"]); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("normalTexture")) { + normalTexture = as(o["normalTexture"]); + } + if (o.isMember("occlusionTexture")) { + occlusionTexture = as(o["occlusionTexture"]); + } + if (o.isMember("pbrMetallicRoughness")) { + pbrMetallicRoughness = as(o["pbrMetallicRoughness"]); + } + } +}; +template<> Material as(const Json::Value &o) { return o; } + +struct MeshPrimitive { + static void enumeratedProps(const Json::Value &o, const std::string &name, std::optional> &attr) { + for (std::size_t i = 0;; ++i) { + const std::string s = name + "_" + std::to_string(i); + if (!o.isMember(s)) break; + if (i == 0) { + attr = std::vector(); + } + attr->push_back(as(o[s])); + } + } + struct Attributes { + std::optional position, normal, tangent; + std::optional> texcoord, color, joints, weights; + Attributes(const Json::Value &o) { + if (o.isMember("POSITION")) + position = as(o["POSITION"]); + if (o.isMember("NORMAL")) + normal = as(o["NORMAL"]); + if (o.isMember("TANGENT")) + tangent = as(o["TANGENT"]); + enumeratedProps(o, "TEXCOORD", texcoord); + enumeratedProps(o, "COLOR", color); + enumeratedProps(o, "JOINTS", joints); + enumeratedProps(o, "WEIGHTS", weights); + check(joints.has_value() == weights.has_value()); + if (joints.has_value()) { + check(joints->size() == weights->size()); + } + check(position.has_value() + || normal.has_value() + || tangent.has_value() + || texcoord.has_value() + || color.has_value() + || joints.has_value() + || weights.has_value()); + } + }; + Attributes attributes; + std::optional indices; + std::optional material; + enum class Mode { + POINTS, + LINES, + LINE_LOOP, + LINE_STRIP, + TRIANGLES, + TRIANGLE_STRIP, + TRIANGLE_FAN, + }; + Mode mode; + struct MorphTargets { + std::optional position, normal, tangent; + std::optional> texcoord, color; + MorphTargets(const Json::Value &o) { + if (o.isMember("POSITION")) + position = as(o["POSITION"]); + if (o.isMember("NORMAL")) + normal = as(o["NORMAL"]); + if (o.isMember("TANGENT")) + tangent = as(o["TANGENT"]); + enumeratedProps(o, "TEXCOORD", texcoord); + enumeratedProps(o, "COLOR", color); + check(position.has_value() + || normal.has_value() + || tangent.has_value() + || texcoord.has_value() + || color.has_value()); + } + }; + std::optional> targets; + MeshPrimitive(const Json::Value &o) + : attributes(Attributes(o["attributes"])) + , mode(Mode::TRIANGLES) + { + check(o.isObject()); + if (o.isMember("indices")) { + indices = as(o["indices"]); + } + if (o.isMember("material")) { + material = as(o["material"]); + } + if (o.isMember("mode")) { + static std::unordered_map map = { + {0, Mode::POINTS}, + {1, Mode::LINES}, + {2, Mode::LINE_LOOP}, + {3, Mode::LINE_STRIP}, + {4, Mode::TRIANGLES}, + {5, Mode::TRIANGLE_STRIP}, + {6, Mode::TRIANGLE_FAN}, + }; + const auto &v = o["mode"]; check(v.isUInt64()); + mode = map.at(v.asUInt64()); + } + if (o.isMember("targets")) { + targets = asVec(o["targets"]); + check(targets->size() >= 1); + } + } +}; +template<> MeshPrimitive::MorphTargets as(const Json::Value &o) { return o; } +template<> MeshPrimitive as(const Json::Value &o) { return o; } + +struct Mesh { + std::optional name; + std::vector primitives; + std::optional> weights; + Mesh(const Json::Value &o) + : primitives(asVec(o["primitives"])) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + check(primitives.size() >= 1); + if (o.isMember("weights")) { + weights = asVec(o["weights"]); + check(weights->size() >= 1); + } + } +}; +template<> Mesh as(const Json::Value &o) { return o; } + +struct Node { + std::optional camera; + std::optional> children; + typedef std::array Matrix; + struct TRS { + std::array translation = {0, 0, 0}; + std::array rotation = {0, 0, 0, 1}; + std::array scale = {1, 1, 1}; + }; + std::variant transform; + std::optional mesh; + std::optional name; + std::optional skin; + std::optional> weights; + Node(const Json::Value &o) + : transform(Matrix { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }) + { + check(o.isObject()); + if (o.isMember("camera")) { + camera = as(o["camera"]); + } + if (o.isMember("children")) { + children = asVec(o["children"]); + check(children->size() >= 1); + checkDuplicateFree(*children); + } + bool hasTRS = o.isMember("translation") || o.isMember("rotation") || o.isMember("scale"); + if (o.isMember("matrix")) { + check(!hasTRS); + transform = asArr(o["matrix"]); + } else if (hasTRS) { + TRS trs; + if (o.isMember("translation")) { + trs.translation = asArr(o["translation"]); + } + if (o.isMember("rotation")) { + trs.rotation = asArr(o["rotation"]); + for (auto v: trs.rotation) { + check(v >= -1); + check(v <= 1); + } + } + if (o.isMember("scale")) { + trs.scale = asArr(o["scale"]); + } + transform = trs; + } + if (o.isMember("mesh")) { + mesh = as(o["mesh"]); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("skin")) { + check(mesh.has_value()); + skin = as(o["skin"]); + } + if (o.isMember("weights")) { + weights = asVec(o["weights"]); + check(weights->size() >= 1); + } + } +}; +template<> Node as(const Json::Value &o) { return o; } + +struct Sampler { + enum class MagFilter { + NEAREST, + LINEAR, + }; + std::optional magFilter; + enum class MinFilter { + NEAREST, + LINEAR, + NEAREST_MIPMAP_NEAREST, + LINEAR_MIPMAP_NEAREST, + NEAREST_MIPMAP_LINEAR, + LINEAR_MIPMAP_LINEAR, + }; + std::optional minFilter; + std::optional name; + enum class WrapS { + REPEAT, + CLAMP_TO_EDGE, + MIRRORED_REPEAT, + }; + WrapS wrapS; + enum class WrapT { + REPEAT, + CLAMP_TO_EDGE, + MIRRORED_REPEAT, + }; + WrapT wrapT; + Sampler(const Json::Value &o) + : wrapS(WrapS::REPEAT) + , wrapT(WrapT::REPEAT) + { + check(o.isObject()); + if (o.isMember("magFilter")) { + static std::unordered_map map = { + {9728, MagFilter::NEAREST}, + {9729, MagFilter::LINEAR}, + }; + const auto &v = o["magFilter"]; check(v.isUInt64()); + magFilter = map.at(v.asUInt64()); + } + if (o.isMember("minFilter")) { + static std::unordered_map map = { + {9728, MinFilter::NEAREST}, + {9729, MinFilter::LINEAR}, + {9984, MinFilter::NEAREST_MIPMAP_NEAREST}, + {9985, MinFilter::LINEAR_MIPMAP_NEAREST}, + {9986, MinFilter::NEAREST_MIPMAP_LINEAR}, + {9987, MinFilter::LINEAR_MIPMAP_LINEAR}, + }; + const auto &v = o["minFilter"]; check(v.isUInt64()); + minFilter = map.at(v.asUInt64()); + } + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("wrapS")) { + static std::unordered_map map = { + {10497, WrapS::REPEAT}, + {33071, WrapS::CLAMP_TO_EDGE}, + {33648, WrapS::MIRRORED_REPEAT}, + }; + const auto &v = o["wrapS"]; check(v.isUInt64()); + wrapS = map.at(v.asUInt64()); + } + if (o.isMember("wrapT")) { + static std::unordered_map map = { + {10497, WrapT::REPEAT}, + {33071, WrapT::CLAMP_TO_EDGE}, + {33648, WrapT::MIRRORED_REPEAT}, + }; + const auto &v = o["wrapT"]; check(v.isUInt64()); + wrapT = map.at(v.asUInt64()); + } + } +}; +template<> Sampler as(const Json::Value &o) { return o; } + +struct Scene { + std::optional name; + std::optional> nodes; + Scene(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("nodes")) { + nodes = asVec(o["nodes"]); + check(nodes->size() >= 1); + checkDuplicateFree(*nodes); + } + } +}; +template<> Scene as(const Json::Value &o) { return o; } + +struct Skin { + std::optional inverseBindMatrices; + std::vector joints; + std::optional name; + std::optional skeleton; + Skin(const Json::Value &o) + : joints(asVec(o["joints"])) + { + check(o.isObject()); + if (o.isMember("inverseBindMatrices")) { + inverseBindMatrices = as(o["inverseBindMatrices"]); + } + check(joints.size() >= 1); + checkDuplicateFree(joints); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("skeleton")) { + skeleton = as(o["skeleton"]); + } + } +}; +template<> Skin as(const Json::Value &o) { return o; } + +struct Texture { + std::optional name; + std::optional sampler; + std::optional source; + Texture(const Json::Value &o) + { + check(o.isObject()); + if (o.isMember("name")) { + name = as(o["name"]); + } + if (o.isMember("sampler")) { + sampler = as(o["sampler"]); + } + if (o.isMember("source")) { + source = as(o["source"]); + } + } +}; +template<> Texture as(const Json::Value &o) { return o; } + +struct GlTF { + std::optional> accessors; + std::optional> animations; + Asset asset; + std::optional> bufferViews; + std::optional> buffers; + std::optional> cameras; + std::optional> extensionsRequired; + std::optional> extensionsUsed; + std::optional> images; + std::optional> materials; + std::optional> meshes; + std::optional> nodes; + std::optional> samplers; + std::optional scene; + std::optional> scenes; + std::optional> skins; + std::optional> textures; + static std::string uriError(const std::string &uri) { + // only base64 data URI support by default + throw std::runtime_error("unsupported URI: " + uri); + } + GlTF(const Json::Value &o, + const std::function &resolveURI = uriError) + : asset(as(o["asset"])) + { + check(o.isObject()); + if (o.isMember("accessors")) { + accessors = asVec(o["accessors"]); + check(accessors->size() >= 1); + } + if (o.isMember("animations")) { + animations = asVec(o["animations"]); + check(animations->size() >= 1); + } + if (o.isMember("bufferViews")) { + bufferViews = asVec(o["bufferViews"]); + check(bufferViews->size() >= 1); + } + if (o.isMember("buffers")) { + auto b = o["buffers"]; + check(b.isArray()); + std::vector bufs; + bufs.reserve(b.size()); + for (Json::ArrayIndex i = 0; i < b.size(); ++i) { + bufs.emplace_back(b[i], resolveURI); + } + check(bufs.size() >= 1); + buffers = std::move(bufs); + } + if (o.isMember("cameras")) { + cameras = asVec(o["cameras"]); + check(cameras->size() >= 1); + } + if (o.isMember("extensionsRequired")) { + extensionsRequired = asVec(o["extensionsRequired"]); + check(extensionsRequired->size() >= 1); + checkDuplicateFree(*extensionsRequired); + } + if (o.isMember("extensionsUsed")) { + extensionsUsed = asVec(o["extensionsUsed"]); + check(extensionsUsed->size() >= 1); + checkDuplicateFree(*extensionsUsed); + } + if (o.isMember("images")) { + images = asVec(o["images"]); + check(images->size() >= 1); + } + if (o.isMember("materials")) { + materials = asVec(o["materials"]); + check(materials->size() >= 1); + } + if (o.isMember("meshes")) { + meshes = asVec(o["meshes"]); + check(meshes->size() >= 1); + } + if (o.isMember("nodes")) { + nodes = asVec(o["nodes"]); + check(nodes->size() >= 1); + // Nodes must be a forest: + // 1. Each node should have indegree 0 or 1: + std::vector indeg(nodes->size()); + for (std::size_t i = 0; i < nodes->size(); ++i) { + auto children = nodes->at(i).children; + if (!children.has_value()) continue; + for (auto child : children.value()) { + ++indeg.at(child); + } + } + for (const auto deg : indeg) { + check(deg <= 1); + } + // 2. There should be no cycles: + std::vector visited(nodes->size()); + std::stack> toVisit; + for (std::size_t i = 0; i < nodes->size(); ++i) { + // Only start DFS in roots. + if (indeg[i] > 0) + continue; + + toVisit.push(i); + do { + std::size_t j = toVisit.top(); + check(!visited.at(j)); + visited[j] = true; + toVisit.pop(); + auto children = nodes->at(j).children; + if (!children.has_value()) + continue; + for (auto child : *children) { + toVisit.push(child); + } + } while (!toVisit.empty()); + } + } + if (o.isMember("samplers")) { + samplers = asVec(o["samplers"]); + check(samplers->size() >= 1); + } + if (o.isMember("scene")) { + scene = as(o["scene"]); + } + if (o.isMember("scenes")) { + scenes = asVec(o["scenes"]); + check(scenes->size() >= 1); + } + if (o.isMember("skins")) { + skins = asVec(o["skins"]); + check(skins->size() >= 1); + } + if (o.isMember("textures")) { + textures = asVec(o["textures"]); + check(textures->size() >= 1); + } + + // Validation + + checkForall(bufferViews, [&](const BufferView &view) { + check(buffers.has_value()); + const Buffer &buf = buffers->at(view.buffer); + // Be careful because of possible integer overflows. + check(view.byteOffset < buf.byteLength); + check(view.byteLength <= buf.byteLength); + check(view.byteOffset <= buf.byteLength - view.byteLength); + }); + + const auto checkAccessor = [&](const auto &accessor, + std::size_t bufferView, std::size_t byteOffset, std::size_t count) { + const BufferView &view = bufferViews->at(bufferView); + if (view.byteStride.has_value()) + check(*view.byteStride % accessor.componentSize() == 0); + check(byteOffset < view.byteLength); + // Use division to avoid overflows. + const auto effective_byte_stride = view.byteStride.value_or(accessor.elementSize()); + check(count <= (view.byteLength - byteOffset) / effective_byte_stride); + }; + checkForall(accessors, [&](const Accessor &accessor) { + if (accessor.bufferView.has_value()) + checkAccessor(accessor, *accessor.bufferView, accessor.byteOffset, accessor.count); + if (accessor.sparse.has_value()) { + const auto &indices = accessor.sparse->indices; + checkAccessor(indices, indices.bufferView, indices.byteOffset, accessor.sparse->count); + const auto &values = accessor.sparse->values; + checkAccessor(accessor, values.bufferView, values.byteOffset, accessor.sparse->count); + } + }); + + checkForall(images, [&](const Image &image) { + checkIndex(bufferViews, image.bufferView); + }); + + checkForall(meshes, [&](const Mesh &mesh) { + for (const auto &primitive : mesh.primitives) { + checkIndex(accessors, primitive.indices); + checkIndex(materials, primitive.material); + checkIndex(accessors, primitive.attributes.normal); + checkIndex(accessors, primitive.attributes.position); + checkIndex(accessors, primitive.attributes.tangent); + checkForall(primitive.attributes.texcoord, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(primitive.attributes.color, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(primitive.attributes.joints, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(primitive.attributes.weights, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + if (primitive.material.has_value()) { + const Material &material = materials->at(primitive.material.value()); + if (material.emissiveTexture.has_value()) { + check(primitive.attributes.texcoord.has_value()); + check(material.emissiveTexture->texCoord < primitive.attributes.texcoord->size()); + } + if (material.normalTexture.has_value()) { + check(primitive.attributes.texcoord.has_value()); + check(material.normalTexture->texCoord < primitive.attributes.texcoord->size()); + } + if (material.occlusionTexture.has_value()) { + check(primitive.attributes.texcoord.has_value()); + check(material.occlusionTexture->texCoord < primitive.attributes.texcoord->size()); + } + } + checkForall(primitive.targets, [&](const MeshPrimitive::MorphTargets &target) { + checkIndex(accessors, target.normal); + checkIndex(accessors, target.position); + checkIndex(accessors, target.tangent); + checkForall(target.texcoord, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + checkForall(target.color, [&](const std::size_t &i) { + checkIndex(accessors, i); + }); + }); + } + }); + + checkForall(nodes, [&](const Node &node) { + checkIndex(cameras, node.camera); + checkIndex(meshes, node.mesh); + checkIndex(skins, node.skin); + }); + + checkForall(scenes, [&](const Scene &scene) { + checkForall(scene.nodes, [&](const size_t &i) { + checkIndex(nodes, i); + }); + }); + + checkForall(skins, [&](const Skin &skin) { + checkIndex(accessors, skin.inverseBindMatrices); + for (const std::size_t &i : skin.joints) + checkIndex(nodes, i); + checkIndex(nodes, skin.skeleton); + }); + + checkForall(textures, [&](const Texture &texture) { + checkIndex(samplers, texture.sampler); + checkIndex(images, texture.source); + }); + + checkForall(animations, [&](const Animation &animation) { + for (const auto &sampler : animation.samplers) { + checkIndex(accessors, sampler.input); + const auto &accessor = accessors->at(sampler.input); + check(accessor.type == Accessor::Type::SCALAR); + check(accessor.componentType == Accessor::ComponentType::FLOAT); + checkIndex(accessors, sampler.output); + } + for (const auto &channel : animation.channels) { + checkIndex(nodes, channel.target.node); + checkIndex(animation.samplers, channel.sampler); + } + }); + + checkIndex(scenes, scene); + } +}; + +} diff --git a/src/client/client.cpp b/src/client/client.cpp index 9c12be8f2..3720ec54f 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -826,7 +826,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename, } const char *model_ext[] = { - ".x", ".b3d", ".obj", + ".x", ".b3d", ".obj", ".gltf", NULL }; name = removeStringEnd(filename, model_ext); diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 19bee6f7f..563fe0abd 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -844,14 +844,19 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr) if (m_animated_meshnode) { u32 mat_count = m_animated_meshnode->getMaterialCount(); + assert(mat_count == m_animated_meshnode->getMesh()->getMeshBufferCount()); + u32 max_tex_idx = 0; + for (u32 i = 0; i < mat_count; ++i) { + max_tex_idx = std::max(max_tex_idx, + m_animated_meshnode->getMesh()->getTextureSlot(i)); + } if (mat_count == 0 || m_prop.textures.empty()) { // nothing - } else if (mat_count > m_prop.textures.size()) { + } else if (max_tex_idx >= m_prop.textures.size()) { std::ostringstream oss; oss << "GenericCAO::addToScene(): Model " - << m_prop.mesh << " loaded with " << mat_count - << " mesh buffers but only " << m_prop.textures.size() - << " texture(s) specified, this is deprecated."; + << m_prop.mesh << " is missing " << (max_tex_idx + 1 - m_prop.textures.size()) + << " more texture(s), this is deprecated."; logOnce(oss, warningstream); video::ITexture *last = m_animated_meshnode->getMaterial(0).TextureLayers[0].Texture; @@ -1370,9 +1375,11 @@ void GenericCAO::updateTextures(std::string mod) else if (m_animated_meshnode) { if (m_prop.visual == "mesh") { - for (u32 i = 0; i < m_prop.textures.size() && - i < m_animated_meshnode->getMaterialCount(); ++i) { - std::string texturestring = m_prop.textures[i]; + for (u32 i = 0; i < m_animated_meshnode->getMaterialCount(); ++i) { + const auto texture_idx = m_animated_meshnode->getMesh()->getTextureSlot(i); + if (texture_idx >= m_prop.textures.size()) + continue; + std::string texturestring = m_prop.textures[texture_idx]; if (texturestring.empty()) continue; // Empty texture string means don't modify that material texturestring += mod; diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index 1de36c9c5..4f4056668 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include "content_mapblock.h" +#include "util/basic_macros.h" #include "util/numeric.h" #include "util/directiontables.h" #include "mapblock_mesh.h" @@ -1676,7 +1677,9 @@ void MapblockMeshGenerator::drawMeshNode() int mesh_buffer_count = mesh->getMeshBufferCount(); for (int j = 0; j < mesh_buffer_count; j++) { - useTile(j); + // Only up to 6 tiles are supported + const auto tile = mesh->getTextureSlot(j); + useTile(MYMIN(tile, 5)); scene::IMeshBuffer *buf = mesh->getMeshBuffer(j); video::S3DVertex *vertices = (video::S3DVertex *)buf->getVertices(); int vertex_count = buf->getVertexCount(); diff --git a/src/client/mesh.cpp b/src/client/mesh.cpp index 711f7e1c6..711712c33 100644 --- a/src/client/mesh.cpp +++ b/src/client/mesh.cpp @@ -397,8 +397,8 @@ scene::SMesh* cloneMesh(scene::IMesh *src_mesh) scene::IMeshBuffer *temp_buf = cloneMeshBuffer( src_mesh->getMeshBuffer(j)); dst_mesh->addMeshBuffer(temp_buf); + dst_mesh->setTextureSlot(j, src_mesh->getTextureSlot(j)); temp_buf->drop(); - } return dst_mesh; } diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 3f22fb3c4..1fd373b9c 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -2807,8 +2807,13 @@ void GUIFormSpecMenu::parseModel(parserData *data, const std::string &element) auto meshnode = e->setMesh(mesh); - for (u32 i = 0; i < textures.size() && i < meshnode->getMaterialCount(); ++i) - e->setTexture(i, m_tsrc->getTexture(unescape_string(textures[i]))); + for (u32 i = 0; i < meshnode->getMaterialCount(); ++i) { + const auto texture_idx = mesh->getTextureSlot(i); + if (texture_idx >= textures.size()) + warningstream << "Invalid model element: Not enough textures" << std::endl; + else + e->setTexture(i, m_tsrc->getTexture(unescape_string(textures[texture_idx]))); + } if (vec_rot.size() >= 2) e->setRotation(v2f(stof(vec_rot[0]), stof(vec_rot[1]))); diff --git a/src/server.cpp b/src/server.cpp index 609b7188b..c76155015 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -2465,7 +2465,7 @@ bool Server::addMediaFile(const std::string &filename, const char *supported_ext[] = { ".png", ".jpg", ".bmp", ".tga", ".ogg", - ".x", ".b3d", ".obj", + ".x", ".b3d", ".obj", ".gltf", // Custom translation file format ".tr", NULL diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt index ec52ee6bf..93803c912 100644 --- a/src/unittest/CMakeLists.txt +++ b/src/unittest/CMakeLists.txt @@ -49,7 +49,7 @@ set (UNITTEST_CLIENT_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_content_mapblock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_eventmanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_gameui.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_irr_gltf_mesh_loader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_mesh_compare.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_keycode.cpp PARENT_SCOPE) - diff --git a/src/unittest/test_irr_gltf_mesh_loader.cpp b/src/unittest/test_irr_gltf_mesh_loader.cpp new file mode 100644 index 000000000..8ab57e590 --- /dev/null +++ b/src/unittest/test_irr_gltf_mesh_loader.cpp @@ -0,0 +1,366 @@ +// Minetest +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "CSceneManager.h" +#include "content/subgames.h" +#include "filesys.h" + +#include "CReadFile.h" +#include "irr_v3d.h" +#include "irr_v2d.h" + +#include + +#include "catch.h" + +TEST_CASE("gltf") { + +const auto gamespec = findSubgame("devtest"); + +if (!gamespec.isValid()) + SKIP(); + +irr::scene::CSceneManager smgr(nullptr, nullptr, nullptr); +const auto loadMesh = [&smgr](const irr::io::path& filepath) { + irr::io::CReadFile file(filepath); + return smgr.getMesh(&file); +}; + +const static auto model_stem = gamespec.gamemods_path + + DIR_DELIM + "gltf" + DIR_DELIM + "models" + DIR_DELIM + "gltf_"; + +SECTION("error cases") { + const static auto invalid_model_path = gamespec.gamemods_path + DIR_DELIM + "gltf" + DIR_DELIM + "invalid" + DIR_DELIM; + + SECTION("empty gltf file") { + CHECK(loadMesh(invalid_model_path + "empty.gltf") == nullptr); + } + + SECTION("null file pointer") { + CHECK(smgr.getMesh(nullptr) == nullptr); + } + + SECTION("invalid JSON") { + CHECK(loadMesh(invalid_model_path + "json_missing_brace.gltf") == nullptr); + } + + // This is an example of something that should be validated by tiniergltf. + SECTION("invalid bufferview bounds") + { + CHECK(loadMesh(invalid_model_path + "invalid_bufferview_bounds.gltf") == nullptr); + } +} + +SECTION("minimal triangle") { + const auto path = GENERATE( + model_stem + "minimal_triangle.gltf", + model_stem + "triangle_with_vertex_stride.gltf", + // Test non-indexed geometry. + model_stem + "triangle_without_indices.gltf"); + INFO(path); + const auto mesh = loadMesh(path); + REQUIRE(mesh != nullptr); + REQUIRE(mesh->getMeshBufferCount() == 1); + + SECTION("vertex coordinates are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 3); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == v3f {0.0f, 0.0f, 0.0f}); + CHECK(vertices[1].Pos == v3f {1.0f, 0.0f, 0.0f}); + CHECK(vertices[2].Pos == v3f {0.0f, 1.0f, 0.0f}); + } + + SECTION("vertex indices are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getIndexCount() == 3); + auto indices = static_cast( + mesh->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 2); + CHECK(indices[1] == 1); + CHECK(indices[2] == 0); + } +} + +SECTION("blender cube") { + const auto mesh = loadMesh(model_stem + "blender_cube.gltf"); + REQUIRE(mesh != nullptr); + REQUIRE(mesh->getMeshBufferCount() == 1); + SECTION("vertex coordinates are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == v3f{-10.0f, -10.0f, -10.0f}); + CHECK(vertices[3].Pos == v3f{-10.0f, 10.0f, -10.0f}); + CHECK(vertices[6].Pos == v3f{-10.0f, -10.0f, 10.0f}); + CHECK(vertices[9].Pos == v3f{-10.0f, 10.0f, 10.0f}); + CHECK(vertices[12].Pos == v3f{10.0f, -10.0f, -10.0f}); + CHECK(vertices[15].Pos == v3f{10.0f, 10.0f, -10.0f}); + CHECK(vertices[18].Pos == v3f{10.0f, -10.0f, 10.0f}); + CHECK(vertices[21].Pos == v3f{10.0f, 10.0f, 10.0f}); + } + + SECTION("vertex indices are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 16); + CHECK(indices[1] == 5); + CHECK(indices[2] == 22); + CHECK(indices[35] == 0); + } + + SECTION("vertex normals are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Normal == v3f{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[1].Normal == v3f{0.0f, -1.0f, 0.0f}); + CHECK(vertices[2].Normal == v3f{0.0f, 0.0f, -1.0f}); + CHECK(vertices[3].Normal == v3f{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[6].Normal == v3f{-1.0f, 0.0f, 0.0f}); + CHECK(vertices[23].Normal == v3f{1.0f, 0.0f, 0.0f}); + + } + + SECTION("texture coords are correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].TCoords == v2f{0.375f, 1.0f}); + CHECK(vertices[1].TCoords == v2f{0.125f, 0.25f}); + CHECK(vertices[2].TCoords == v2f{0.375f, 0.0f}); + CHECK(vertices[3].TCoords == v2f{0.6250f, 1.0f}); + CHECK(vertices[6].TCoords == v2f{0.375f, 0.75f}); + } +} + +SECTION("blender cube scaled") { + const auto mesh = loadMesh(model_stem + "blender_cube_scaled.gltf"); + REQUIRE(mesh != nullptr); + REQUIRE(mesh->getMeshBufferCount() == 1); + + SECTION("Scaling is correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + + CHECK(vertices[0].Pos == v3f{-150.0f, -1.0f, -21.5f}); + CHECK(vertices[3].Pos == v3f{-150.0f, 1.0f, -21.5f}); + CHECK(vertices[6].Pos == v3f{-150.0f, -1.0f, 21.5f}); + CHECK(vertices[9].Pos == v3f{-150.0f, 1.0f, 21.5f}); + CHECK(vertices[12].Pos == v3f{150.0f, -1.0f, -21.5f}); + CHECK(vertices[15].Pos == v3f{150.0f, 1.0f, -21.5f}); + CHECK(vertices[18].Pos == v3f{150.0f, -1.0f, 21.5f}); + CHECK(vertices[21].Pos == v3f{150.0f, 1.0f, 21.5f}); + } +} + +SECTION("blender cube matrix transform") { + const auto mesh = loadMesh(model_stem + "blender_cube_matrix_transform.gltf"); + REQUIRE(mesh != nullptr); + REQUIRE(mesh->getMeshBufferCount() == 1); + + SECTION("Transformation is correct") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + const auto checkVertex = [&](const std::size_t i, v3f vec) { + // The transform scales by (1, 2, 3) and translates by (4, 5, 6). + CHECK(vertices[i].Pos == vec * v3f{1, 2, 3} + // The -6 is due to the coordinate system conversion. + + v3f{4, 5, -6}); + }; + checkVertex(0, v3f{-1, -1, -1}); + checkVertex(3, v3f{-1, 1, -1}); + checkVertex(6, v3f{-1, -1, 1}); + checkVertex(9, v3f{-1, 1, 1}); + checkVertex(12, v3f{1, -1, -1}); + checkVertex(15, v3f{1, 1, -1}); + checkVertex(18, v3f{1, -1, 1}); + checkVertex(21, v3f{1, 1, 1}); + } +} + +SECTION("snow man") { + const auto mesh = loadMesh(model_stem + "snow_man.gltf"); + REQUIRE(mesh != nullptr); + REQUIRE(mesh->getMeshBufferCount() == 3); + + SECTION("vertex coordinates are correct for all buffers") { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + { + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Pos == v3f{3.0f, 24.0f, -3.0f}); + CHECK(vertices[3].Pos == v3f{3.0f, 18.0f, 3.0f}); + CHECK(vertices[6].Pos == v3f{-3.0f, 18.0f, -3.0f}); + CHECK(vertices[9].Pos == v3f{3.0f, 24.0f, 3.0f}); + CHECK(vertices[12].Pos == v3f{3.0f, 18.0f, -3.0f}); + CHECK(vertices[15].Pos == v3f{-3.0f, 18.0f, 3.0f}); + CHECK(vertices[18].Pos == v3f{3.0f, 18.0f, -3.0f}); + CHECK(vertices[21].Pos == v3f{3.0f, 18.0f, 3.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(1)->getVertices()); + CHECK(vertices[2].Pos == v3f{5.0f, 10.0f, 5.0f}); + CHECK(vertices[3].Pos == v3f{5.0f, 0.0f, 5.0f}); + CHECK(vertices[7].Pos == v3f{-5.0f, 0.0f, 5.0f}); + CHECK(vertices[8].Pos == v3f{5.0f, 10.0f, -5.0f}); + CHECK(vertices[14].Pos == v3f{5.0f, 0.0f, 5.0f}); + CHECK(vertices[16].Pos == v3f{5.0f, 10.0f, -5.0f}); + CHECK(vertices[22].Pos == v3f{-5.0f, 10.0f, 5.0f}); + CHECK(vertices[23].Pos == v3f{-5.0f, 0.0f, 5.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(2)->getVertices()); + CHECK(vertices[1].Pos == v3f{4.0f, 10.0f, -4.0f}); + CHECK(vertices[2].Pos == v3f{4.0f, 18.0f, 4.0f}); + CHECK(vertices[3].Pos == v3f{4.0f, 10.0f, 4.0f}); + CHECK(vertices[10].Pos == v3f{-4.0f, 18.0f, -4.0f}); + CHECK(vertices[11].Pos == v3f{-4.0f, 18.0f, 4.0f}); + CHECK(vertices[12].Pos == v3f{4.0f, 10.0f, -4.0f}); + CHECK(vertices[17].Pos == v3f{-4.0f, 18.0f, -4.0f}); + CHECK(vertices[18].Pos == v3f{4.0f, 10.0f, -4.0f}); + } + } + + SECTION("vertex indices are correct for all buffers") { + { + REQUIRE(mesh->getMeshBuffer(0)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(0)->getIndices()); + CHECK(indices[0] == 23); + CHECK(indices[1] == 21); + CHECK(indices[2] == 22); + CHECK(indices[35] == 2); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(1)->getIndices()); + CHECK(indices[10] == 16); + CHECK(indices[11] == 18); + CHECK(indices[15] == 13); + CHECK(indices[27] == 5); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getIndexCount() == 36); + auto indices = static_cast( + mesh->getMeshBuffer(2)->getIndices()); + CHECK(indices[26] == 6); + CHECK(indices[27] == 5); + CHECK(indices[29] == 6); + CHECK(indices[32] == 2); + } + } + + + SECTION("vertex normals are correct for all buffers") { + { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[1].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[2].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[3].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[6].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[23].Normal == v3f{0.0f, 0.0f, 1.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(1)->getVertices()); + CHECK(vertices[0].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[1].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[3].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[6].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[7].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[22].Normal == v3f{0.0f, 0.0f, 1.0f}); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(2)->getVertices()); + CHECK(vertices[3].Normal == v3f{1.0f, 0.0f, -0.0f}); + CHECK(vertices[4].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[5].Normal == v3f{-1.0f, 0.0f, -0.0f}); + CHECK(vertices[10].Normal == v3f{0.0f, 1.0f, -0.0f}); + CHECK(vertices[11].Normal == v3f{0.0f, 1.0f, -0.0f}); + CHECK(vertices[19].Normal == v3f{0.0f, 0.0f, -1.0f}); + } + } + + + SECTION("texture coords are correct for all buffers") { + { + REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(0)->getVertices()); + CHECK(vertices[0].TCoords == v2f{0.583333313f, 0.791666686f}); + CHECK(vertices[1].TCoords == v2f{0.583333313f, 0.666666686f}); + CHECK(vertices[2].TCoords == v2f{0.708333313f, 0.791666686f}); + CHECK(vertices[5].TCoords == v2f{0.375f, 0.416666657f}); + CHECK(vertices[6].TCoords == v2f{0.5f, 0.291666657f}); + CHECK(vertices[19].TCoords == v2f{0.708333313f, 0.75f}); + } + { + REQUIRE(mesh->getMeshBuffer(1)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(1)->getVertices()); + + CHECK(vertices[1].TCoords == v2f{0.0f, 0.791666686f}); + CHECK(vertices[4].TCoords == v2f{0.208333328f, 0.791666686f}); + CHECK(vertices[5].TCoords == v2f{0.0f, 0.791666686f}); + CHECK(vertices[6].TCoords == v2f{0.208333328f, 0.583333313f}); + CHECK(vertices[12].TCoords == v2f{0.416666657f, 0.791666686f}); + CHECK(vertices[15].TCoords == v2f{0.208333328f, 0.583333313f}); + } + { + REQUIRE(mesh->getMeshBuffer(2)->getVertexCount() == 24); + auto vertices = static_cast( + mesh->getMeshBuffer(2)->getVertices()); + CHECK(vertices[10].TCoords == v2f{0.375f, 0.416666657f}); + CHECK(vertices[11].TCoords == v2f{0.375f, 0.583333313f}); + CHECK(vertices[12].TCoords == v2f{0.708333313f, 0.625f}); + CHECK(vertices[17].TCoords == v2f{0.541666687f, 0.458333343f}); + CHECK(vertices[20].TCoords == v2f{0.208333328f, 0.416666657f}); + CHECK(vertices[22].TCoords == v2f{0.375f, 0.416666657f}); + } + } +} + +// https://github.com/KhronosGroup/glTF-Sample-Models/tree/main/2.0/SimpleSparseAccessor +SECTION("simple sparse accessor") +{ + const auto mesh = loadMesh(model_stem + "simple_sparse_accessor.gltf"); + REQUIRE(mesh != nullptr); + const auto *vertices = reinterpret_cast( + mesh->getMeshBuffer(0)->getVertices()); + const std::array expectedPositions = { + // Lower + v3f(0, 0, 0), + v3f(1, 0, 0), + v3f(2, 0, 0), + v3f(3, 0, 0), + v3f(4, 0, 0), + v3f(5, 0, 0), + v3f(6, 0, 0), + // Upper + v3f(0, 1, 0), + v3f(1, 2, 0), // overridden + v3f(2, 1, 0), + v3f(3, 3, 0), // overridden + v3f(4, 1, 0), + v3f(5, 4, 0), // overridden + v3f(6, 1, 0), + }; + for (std::size_t i = 0; i < expectedPositions.size(); ++i) + CHECK(vertices[i].Pos == expectedPositions[i]); +} + +} diff --git a/src/unittest/test_servermodmanager.cpp b/src/unittest/test_servermodmanager.cpp index 90c29e125..f26734ab3 100644 --- a/src/unittest/test_servermodmanager.cpp +++ b/src/unittest/test_servermodmanager.cpp @@ -122,7 +122,7 @@ void TestServerModManager::testGetMods() ServerModManager sm(m_worlddir); const auto &mods = sm.getMods(); // `ls ./games/devtest/mods | wc -l` + 1 (test mod) - UASSERTEQ(std::size_t, mods.size(), 31 + 1); + UASSERTEQ(std::size_t, mods.size(), 32 + 1); // Ensure we found basenodes mod (part of devtest) // and test_mod (for testing MINETEST_MOD_PATH).