Compare commits

...

51 Commits

Author SHA1 Message Date
JosiahWI
dde1eb76fe
Merge 6be3c1a0f0c3a4b5f129ab915bab4edad49a04cf into 9a1501ae89ffe79c38dbd6756c9e7ed647dd7dc1 2024-06-28 00:22:29 -03:00
grorp
9a1501ae89
CIrrDeviceSDL: Fix numpad key events not having correct KeyInput.Char (#14780)
Allows you to change viewing range using numpad +/- again. This fix also works with the current unreleased version of SDL 3.

The keycodes for numpad keys (both SDL keycodes and Irrlicht keycodes) are not the same as the keycodes for the equivalent non-numpad keys and don't correspond to chars, so I mapped them to chars manually.

Since I think the resolution of https://github.com/minetest/minetest/issues/13770 was "just disable numlock", I made sure to only do this for the numpad number keys if numlock is enabled.
2024-06-27 14:44:44 +02:00
Lars Mueller
6be3c1a0f0 Rename (& minify) files to follow naming conventions 2024-06-12 23:45:31 +02:00
Lars Mueller
196bb56dfd Fix invalid bufferview bounds test 2024-06-12 23:42:36 +02:00
Lars Mueller
3467d91730 Fix sparse accessor handling in tiniergltf 2024-06-12 20:35:36 +02:00
Lars Mueller
35466b227a Add applicable tests from animated branch 2024-06-12 20:18:46 +02:00
Lars Mueller
8547a33cc7 Use SKIP now that we can 2024-06-12 18:08:44 +02:00
Lars Mueller
31ab21ffd5 Update devtest mod count, add mod.conf 2024-06-12 17:57:08 +02:00
Lars Mueller
0ce6b3f8c8 Implement clang-tidy suggestions in tiniergltf 2024-06-12 17:57:08 +02:00
Lars Mueller
616a27a966 Work around lack of SKIP 2024-06-12 17:57:08 +02:00
Lars Mueller
39691664ba Add spider model 2024-06-12 17:57:08 +02:00
Lars Mueller
d6b6eec1bf Address minor TODOs in tiniergltf 2024-06-12 17:57:08 +02:00
Lars Mueller
997926461e Test fixes (get rid of device creation, fix error case tests) 2024-06-12 17:57:08 +02:00
Lars Mueller
37c3edfc38 Move test assets to devtest, add corresponding entities 2024-06-12 17:57:08 +02:00
Lars Mueller
be03a435b8 Add copy & move constructor from STL string to Irrlicht string 2024-06-12 17:57:08 +02:00
Lars Mueller
580a71554d Improve docs 2024-06-12 17:57:08 +02:00
Lars Mueller
1d185c1ce7 Fix triangle with vertex stride test case 2024-06-12 17:57:08 +02:00
Lars Mueller
451978dc2c Move everything to sections of one big "gltf" test case 2024-06-12 17:57:08 +02:00
Lars Mueller
59a173b51b Clean up unit tests 2024-06-12 17:57:08 +02:00
Lars Mueller
50c8604de1 Fix snow man test texture coordinates
The current texture coordinates specified in the test did not use sufficient precision.
It is unfortunately common for software to round away the last bit when displaying floats.
This issue was previously masked by Irrlicht using a float comparison with tolerance for vectors,
which was changed in 3452857 to an exact float comparison for unrelated reasons.
Manual inspection of the buffer reveals that the values produced by the reader are correct.
2024-06-12 17:57:08 +02:00
Lars Mueller
50d20a854c Fix -Wsign-compare warning 2024-06-12 17:57:08 +02:00
Josiah VanderZee
d5450c62bf Add f postfix to floating point test values 2024-06-12 17:57:08 +02:00
Josiah VanderZee
2a9e570cbc Fix strict aliasing violations in glTF tests 2024-06-12 17:57:08 +02:00
Josiah VanderZee
8f6c24cbe9 Fix improper unit tests paths again 2024-06-12 17:57:08 +02:00
Josiah VanderZee
d4d9035ee5 Fix improper unit test paths 2024-06-12 17:57:08 +02:00
Josiah VanderZee
0eef3b07b8 Fix minetestserver build 2024-06-12 17:57:08 +02:00
Josiah VanderZee
b45aa1cd72 Get unit tests to build 2024-06-12 17:57:08 +02:00
Lars Mueller
8c35461af2 Fix/improve reading from accessors
- Alignment issues
- Use os::Byteswap::byteswap
- Directly write into matrices
2024-06-12 17:57:08 +02:00
Lars Mueller
be375cdfb6 Do not stack-allocate SSkinMeshBuffer 2024-06-12 17:57:08 +02:00
Josiah VanderZee
b419d0e445 Use __BIG_ENDIAN_
This also corrects the function name to be snake_case because it is
not a method.
2024-06-12 17:57:08 +02:00
Josiah VanderZee
3a4dde48e8 Remove unneeded header 2024-06-12 17:57:08 +02:00
JosiahWI
28299e7910 Program more defensively
Co-authored-by: sfan5 <sfan5@live.de>
2024-06-12 17:57:08 +02:00
Lars Mueller
942d46d37c Remove unused includes 2024-06-12 17:57:08 +02:00
Lars Mueller
88383e40db Reduce verbosity by introducing SelfType alias 2024-06-12 17:57:08 +02:00
Lars Mueller
d4de89cbe7 Remove unnecessary constructor 2024-06-12 17:57:07 +02:00
Lars Mueller
b734119d05 Move data directly rather than copying 2024-06-12 17:57:07 +02:00
Lars Mueller
818ac9ea3d Replace some spaces with tabs 2024-06-12 17:57:07 +02:00
Lars Mueller
a372397416 Remove now obsolete getters for accessor indices 2024-06-12 17:57:07 +02:00
Lars Mueller
a2311dc734 Check that enough bytes could be read 2024-06-12 17:57:07 +02:00
Lars Mueller
00d23d65e0 Add SPDX-License-Identifiers 2024-06-12 17:57:07 +02:00
Josiah VanderZee
315c03b99a Change "reflecting" to "mirroring" for readability 2024-06-12 17:57:07 +02:00
Lars Mueller
411969c87c Bring accessor implementation from other branch over 2024-06-12 17:57:07 +02:00
Josiah VanderZee
8a2cae1065 Mark transformation matrices as const 2024-06-12 17:57:07 +02:00
Josiah VanderZee
5bd1d052ad Remove unused method isSparseAccessor 2024-06-12 17:57:07 +02:00
Josiah VanderZee
b95dcb7e7a Preallocate the indices vector 2024-06-12 17:57:07 +02:00
Josiah VanderZee
da722a833f Compile with runtime type information 2024-06-12 17:57:07 +02:00
Josiah VanderZee
c803a9c895 Add comment clarifying linking of object libraries 2024-06-12 17:57:07 +02:00
Josiah VanderZee
b9b03557c6 Use main project cxx standard for unit tests 2024-06-12 17:57:07 +02:00
Josiah VanderZee
1baa3f1e34 Clean up code style 2024-06-12 17:57:07 +02:00
Josiah VanderZee
26276729ed Remove EAMT_GLTF as it serves no purpose 2024-06-12 17:57:07 +02:00
JosiahWI
ec5950033f Add glTF loader for static meshes 2024-06-12 17:57:07 +02:00
43 changed files with 2979 additions and 29 deletions

@ -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)

@ -273,7 +273,7 @@ Accepted formats are:
images: .png, .jpg, .tga, (deprecated:) .bmp
sounds: .ogg vorbis
models: .x, .b3d, .obj
models: .x, .b3d, .obj, (5.9+:) .gltf
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)
@ -290,6 +290,18 @@ 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.
Of the model file formats, only a subset is supported:
Simple textured meshes (with multiple textures), optionally with normals.
The .x and .b3d formats additionally support skeletal animation.
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
------------------

@ -0,0 +1,11 @@
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"
* 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

@ -0,0 +1,22 @@
local function register_entity(name, textures)
minetest.register_entity("gltf:" .. name, {
initial_properties = {
visual = "mesh",
mesh = "gltf_" .. name .. ".gltf",
textures = textures,
},
})
end
-- These do not have texture coordinates; they simple render as black surfaces.
register_entity("minimal_triangle", {})
register_entity("triangle_with_vertex_stride", {})
register_entity("triangle_without_indices", {})
do
local cube_textures = {"no_texture.png"} -- TODO provide proper textures
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"})

@ -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"}}

@ -0,0 +1 @@
{

@ -0,0 +1,2 @@
name = gltf
description = Hosts gltf test models, both for the C++ unit tests and for in-game viewing

@ -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"}]}

@ -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"}]}

@ -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"}]}

@ -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"}}

@ -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"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -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"}}

@ -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"}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@ -199,6 +199,9 @@ class ISkinnedMesh : public IAnimatedMesh
//! 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;

@ -6,6 +6,7 @@
#include "IMeshBuffer.h"
#include "S3DVertex.h"
#include "irrArray.h"
namespace irr
{
@ -22,10 +23,14 @@ struct SSkinMeshBuffer : public IMeshBuffer
MappingHint_Vertex(EHM_NEVER), MappingHint_Index(EHM_NEVER),
HWBuffer(NULL),
BoundingBoxNeedsRecalculated(true)
{}
//! Constructor for standard vertices
SSkinMeshBuffer(core::array<video::S3DVertex> &&vertices, core::array<u16> &&indices) :
SSkinMeshBuffer()
{
#ifdef _DEBUG
setDebugName("SSkinMeshBuffer");
#endif
Vertices_Standard = vertices;
Indices = indices;
}
//! Get Material of this buffer.
@ -301,7 +306,7 @@ struct SSkinMeshBuffer : public IMeshBuffer
}
//! append the vertices and indices to the current buffer
void append(const void *const vertices, u32 numVertices, const u16 *const indices, u32 numIndices) override {}
void append(const void* const vertices, u32 numVertices, const u16* const indices, u32 numIndices) override {}
//! get the current hardware mapping hint for vertex buffers
E_HARDWARE_MAPPING getHardwareMappingHint_Vertex() const override

@ -45,6 +45,9 @@ class array
{
}
//! Move constructor
array(std::vector<T> &&data) : m_data(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

@ -11,7 +11,6 @@
#include <cstdio>
#include <cstring>
#include <cwchar>
#include <locale>
/* 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 <typename T>
class string
{
using stl_type = std::basic_string<T>;
public:
typedef T char_type;
@ -79,6 +79,12 @@ class string
*this = other;
}
//! Copy (from standard library string) constructor
string(const stl_type &str) : str(str) {}
//! Move (from standard library string) constructor
string(stl_type &&str) : str(str) {}
//! Constructor from other string types
template <class B>
string(const string<B> &other)
@ -814,13 +820,6 @@ class string
friend size_t wStringToUTF8(stringc &destination, const wchar_t *source);
private:
typedef std::basic_string<T> stl_type;
//! Private constructor
string(stl_type &&str) :
str(str)
{
}
//! strlen wrapper
template <typename U>

@ -0,0 +1,679 @@
// 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 <array>
#include <cstddef>
#include <cstring>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <variant>
#include <vector>
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 <typename T>
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 <class T>
SelfType::Accessor<T>
SelfType::Accessor<T>::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<T>(source, byteStride, count);
}
template <class T>
SelfType::Accessor<T>
SelfType::Accessor<T>::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<T>(source, byteStride, count);
}
template <class T>
SelfType::Accessor<T>
SelfType::Accessor<T>::base(const tiniergltf::GlTF &model, std::size_t accessorIdx)
{
const auto &accessor = model.accessors->at(accessorIdx);
if (!accessor.bufferView.has_value()) {
return Accessor<T>(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<T>(source, byteStride, accessor.count);
}
template <class T>
SelfType::Accessor<T>
SelfType::Accessor<T>::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<T>::base(model, accessorIdx);
if (accessor.sparse.has_value()) {
std::vector<T> 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<u8, u16, u32> {
switch (accessor.sparse->indices.componentType) {
case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_BYTE:
return Accessor<u8>::sparseIndices(model, accessor.sparse->indices, overriddenCount);
case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_SHORT:
return Accessor<u16>::sparseIndices(model, accessor.sparse->indices, overriddenCount);
case tiniergltf::AccessorSparseIndices::ComponentType::UNSIGNED_INT:
return Accessor<u32>::sparseIndices(model, accessor.sparse->indices, overriddenCount);
}
throw std::logic_error("invalid enum value");
})();
const auto valuesAccessor = Accessor<T>::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<T>(vec, accessor.count);
}
return base;
}
#define ACCESSOR_TYPES(T, U, V) \
template <> \
constexpr tiniergltf::Accessor::Type SelfType::Accessor<T>::getType() \
{ \
return tiniergltf::Accessor::Type::U; \
} \
template <> \
constexpr tiniergltf::Accessor::ComponentType SelfType::Accessor<T>::getComponentType() \
{ \
return tiniergltf::Accessor::ComponentType::V; \
}
#define VEC_ACCESSOR_TYPES(T, U, N) \
template <> \
constexpr tiniergltf::Accessor::Type SelfType::Accessor<std::array<T, N>>::getType() \
{ \
return tiniergltf::Accessor::Type::VEC##N; \
} \
template <> \
constexpr tiniergltf::Accessor::ComponentType SelfType::Accessor<std::array<T, N>>::getComponentType() \
{ \
return tiniergltf::Accessor::ComponentType::U; \
} \
template <> \
std::array<T, N> SelfType::rawget(const u8 *ptr) \
{ \
std::array<T, N> res; \
for (u8 i = 0; i < N; ++i) \
res[i] = rawget<T>(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 <class T>
T SelfType::Accessor<T>::get(std::size_t i) const
{
// Buffer-based accessor: Read directly from the buffer.
if (std::holds_alternative<BufferSource>(source)) {
const auto bufsrc = std::get<BufferSource>(source);
return rawget<T>(bufsrc.ptr + i * bufsrc.byteStride);
}
// Array-based accessor (used for sparse accessors): Read from array.
if (std::holds_alternative<std::vector<T>>(source)) {
return std::get<std::vector<T>>(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<std::tuple<>>(source);
return T();
}
template <typename T>
T SelfType::rawget(const u8 *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 u8 *ptr)
{
core::matrix4 mat;
for (u8 i = 0; i < 16; ++i) {
mat[i] = rawget<f32>(ptr + i * sizeof(f32));
}
return mat;
}
template <>
core::vector3df SelfType::rawget(const u8 *ptr)
{
return core::vector3df(
rawget<f32>(ptr),
rawget<f32>(ptr + sizeof(f32)),
rawget<f32>(ptr + 2 * sizeof(f32)));
}
template <>
core::quaternion SelfType::rawget(const u8 *ptr)
{
return core::quaternion(
rawget<f32>(ptr),
rawget<f32>(ptr + sizeof(f32)),
rawget<f32>(ptr + 2 * sizeof(f32)),
rawget<f32>(ptr + 3 * sizeof(f32)));
}
template <std::size_t N>
SelfType::NormalizedValuesAccessor<N>
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<std::array<u8, N>>::make(model, accessorIdx);
case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT:
return Accessor<std::array<u16, N>>::make(model, accessorIdx);
case tiniergltf::Accessor::ComponentType::FLOAT:
return Accessor<std::array<f32, N>>::make(model, accessorIdx);
default:
throw std::runtime_error("invalid component type");
}
}
template <std::size_t N>
std::array<f32, N> SelfType::getNormalizedValues(
const NormalizedValuesAccessor<N> &accessor,
const std::size_t i)
{
std::array<f32, N> values;
if (std::holds_alternative<Accessor<std::array<u8, N>>>(accessor)) {
const auto u8s = std::get<Accessor<std::array<u8, N>>>(accessor).get(i);
for (u8 i = 0; i < N; ++i)
values[i] = static_cast<f32>(u8s[i]) / std::numeric_limits<u8>::max();
} else if (std::holds_alternative<Accessor<std::array<u16, N>>>(accessor)) {
const auto u16s = std::get<Accessor<std::array<u16, N>>>(accessor).get(i);
for (u8 i = 0; i < N; ++i)
values[i] = static_cast<f32>(u16s[i]) / std::numeric_limits<u16>::max();
} else {
values = std::get<Accessor<std::array<f32, N>>>(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<tiniergltf::GlTF> 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())) {
return nullptr;
}
ISkinnedMesh *mesh = new CSkinnedMesh();
MeshExtractor parser(std::move(model.value()), mesh);
try {
parser.loadNodes();
} catch (std::runtime_error &e) {
mesh->drop();
return nullptr;
}
return mesh;
}
static void transformVertices(std::vector<video::S3DVertex> &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<u16> &indices, const std::size_t nVerts)
{
for (u16 index : indices) {
if (index >= nVerts)
throw std::runtime_error("index out of bounds");
}
}
static std::vector<u16> generateIndices(const std::size_t nVerts)
{
std::vector<u16> 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 j = 0; j < getPrimitiveCount(meshIdx); ++j) {
auto vertices = getVertices(meshIdx, j);
if (!vertices.has_value())
continue; // "When positions are not specified, client implementations SHOULD skip primitives rendering"
// Excludes the max value for consistency.
if (vertices->size() >= std::numeric_limits<u16>::max())
throw std::runtime_error("too many vertices");
// Apply the global transform along the parent chain.
transformVertices(*vertices, parent->GlobalMatrix);
auto maybeIndices = getIndices(meshIdx, j);
std::vector<u16> 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)));
}
}
// 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<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> 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<bool> 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<std::vector<u16>> SelfType::MeshExtractor::getIndices(
const std::size_t meshIdx,
const std::size_t primitiveIdx) const
{
const auto accessorIdx = m_gltf_model.meshes->at(meshIdx).primitives.at(primitiveIdx).indices;
if (!accessorIdx.has_value())
return std::nullopt; // non-indexed geometry
const auto accessor = ([&]() -> AccessorVariant<u8, u16, u32> {
const auto &acc = m_gltf_model.accessors->at(*accessorIdx);
switch (acc.componentType) {
case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE:
return Accessor<u8>::make(m_gltf_model, *accessorIdx);
case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT:
return Accessor<u16>::make(m_gltf_model, *accessorIdx);
case tiniergltf::Accessor::ComponentType::UNSIGNED_INT:
return Accessor<u32>::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<u16> 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<u8>>(accessor)) {
index = std::get<Accessor<u8>>(accessor).get(elemIdx);
if (index == std::numeric_limits<u8>::max())
throw std::runtime_error("invalid index");
} else if (std::holds_alternative<Accessor<u16>>(accessor)) {
index = std::get<Accessor<u16>>(accessor).get(elemIdx);
if (index == std::numeric_limits<u16>::max())
throw std::runtime_error("invalid index");
} else if (std::holds_alternative<Accessor<u32>>(accessor)) {
u32 indexWide = std::get<Accessor<u32>>(accessor).get(elemIdx);
// Use >= here for consistency.
if (indexWide >= std::numeric_limits<u16>::max())
throw std::runtime_error("index too large (>= 65536)");
index = static_cast<u16>(indexWide);
}
indices.push_back(index);
}
return indices;
}
/**
* Create a vector of video::S3DVertex (model data) from a mesh & primitive index.
*/
std::optional<std::vector<video::S3DVertex>> SelfType::MeshExtractor::getVertices(
const std::size_t meshIdx,
const std::size_t primitiveIdx) const
{
const auto &attributes = m_gltf_model.meshes->at(meshIdx).primitives.at(primitiveIdx).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<video::S3DVertex> 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 = m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].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<video::S3DVertex>
*/
void SelfType::MeshExtractor::copyPositions(
const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const
{
const auto accessor = Accessor<core::vector3df>::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<video::S3DVertex>
*/
void SelfType::MeshExtractor::copyNormals(
const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const
{
const auto accessor = Accessor<core::vector3df>::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<video::S3DVertex>
*/
void SelfType::MeshExtractor::copyTCoords(
const std::size_t accessorIdx,
std::vector<video::S3DVertex>& 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<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
{
auto size = file->getSize();
if (size < 0) // this can happen if `ftell` fails
return std::nullopt;
auto buf = std::make_unique<char[]>(size + 1);
if (file->read(buf.get(), size) != static_cast<std::size_t>(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<Json::CharReader> 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) {
return std::nullopt;
} catch (const std::out_of_range &e) {
return std::nullopt;
}
}
} // namespace scene
} // namespace irr

@ -0,0 +1,147 @@
// Minetest
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "ISkinnedMesh.h"
#include "IMeshLoader.h"
#include "IReadFile.h"
#include "irrTypes.h"
#include "path.h"
#include "S3DVertex.h"
#include <tiniergltf.hpp>
#include <cstddef>
#include <vector>
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 <typename T>
static T rawget(const u8 *ptr);
template <class T>
class Accessor
{
struct BufferSource
{
const u8 *ptr;
std::size_t byteStride;
};
using Source = std::variant<BufferSource, std::vector<T>, 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 u8 *ptr, std::size_t byteStride, std::size_t count) :
source(BufferSource{ptr, byteStride}), count(count) {}
Accessor(std::vector<T> 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 <typename... Ts>
using AccessorVariant = std::variant<Accessor<Ts>...>;
template <std::size_t N, typename... Ts>
using ArrayAccessorVariant = std::variant<Accessor<std::array<Ts, N>>...>;
template <std::size_t N>
using NormalizedValuesAccessor = ArrayAccessorVariant<N, u8, u16, f32>;
template <std::size_t N>
static NormalizedValuesAccessor<N> createNormalizedValuesAccessor(
const tiniergltf::GlTF &model,
const std::size_t accessorIdx);
template <std::size_t N>
static std::array<f32, N> getNormalizedValues(
const NormalizedValuesAccessor<N> &accessor,
const std::size_t i);
class MeshExtractor {
public:
MeshExtractor(tiniergltf::GlTF &&model,
ISkinnedMesh *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<std::vector<u16>> getIndices(const std::size_t meshIdx,
const std::size_t primitiveIdx) const;
std::optional<std::vector<video::S3DVertex>> getVertices(std::size_t meshIdx,
const std::size_t primitiveIdx) 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;
ISkinnedMesh *m_irr_model;
void copyPositions(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const;
void copyNormals(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const;
void copyTCoords(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& 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<tiniergltf::GlTF> tryParseGLTF(io::IReadFile* file);
};
} // namespace scene
} // namespace irr

@ -129,9 +129,9 @@ EM_BOOL CIrrDeviceSDL::MouseLeaveCallback(int eventType, const EmscriptenMouseEv
}
#endif
bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE key)
bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE irrlichtKey)
{
switch (key) {
switch (irrlichtKey) {
// keys which are known to have safe special character interpretation
// could need changes over time (removals and additions!)
case KEY_RETURN:
@ -189,24 +189,68 @@ bool CIrrDeviceSDL::keyIsKnownSpecial(EKEY_CODE key)
}
}
int CIrrDeviceSDL::findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key)
int CIrrDeviceSDL::findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtKey, bool numlock)
{
switch (irrlichtKey) {
// special cases that always return a char regardless of how the SDL keycode
// looks
switch (key) {
case KEY_RETURN:
case KEY_ESCAPE:
return (int)key;
return (int)irrlichtKey;
// This is necessary for keys on the numpad because they don't use the same
// keycodes as their non-numpad versions (whose keycodes correspond to chars),
// but have their own SDL keycodes and their own Irrlicht keycodes (which
// don't correspond to chars).
case KEY_MULTIPLY:
return '*';
case KEY_ADD:
return '+';
case KEY_SUBTRACT:
return '-';
case KEY_DIVIDE:
return '/';
default:
break;
}
if (numlock) {
// Number keys on the numpad are also affected, but we only want them
// to produce number chars when numlock is enabled.
switch (irrlichtKey) {
case KEY_NUMPAD0:
return '0';
case KEY_NUMPAD1:
return '1';
case KEY_NUMPAD2:
return '2';
case KEY_NUMPAD3:
return '3';
case KEY_NUMPAD4:
return '4';
case KEY_NUMPAD5:
return '5';
case KEY_NUMPAD6:
return '6';
case KEY_NUMPAD7:
return '7';
case KEY_NUMPAD8:
return '8';
case KEY_NUMPAD9:
return '9';
default:
break;
}
}
// SDL in-place ORs values with no character representation with 1<<30
// https://wiki.libsdl.org/SDL2/SDLKeycodeLookup
if (assumedChar & (1 << 30))
// This also affects the numpad keys btw.
if (sdlKey & (1 << 30))
return 0;
switch (key) {
switch (irrlichtKey) {
case KEY_PRIOR:
case KEY_NEXT:
case KEY_HOME:
@ -218,7 +262,7 @@ int CIrrDeviceSDL::findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key)
case KEY_NUMLOCK:
return 0;
default:
return assumedChar;
return sdlKey;
}
}
@ -825,7 +869,8 @@ bool CIrrDeviceSDL::run()
irrevent.KeyInput.PressedDown = (SDL_event.type == SDL_KEYDOWN);
irrevent.KeyInput.Shift = (SDL_event.key.keysym.mod & KMOD_SHIFT) != 0;
irrevent.KeyInput.Control = (SDL_event.key.keysym.mod & KMOD_CTRL) != 0;
irrevent.KeyInput.Char = findCharToPassToIrrlicht(mp.SDLKey, key);
irrevent.KeyInput.Char = findCharToPassToIrrlicht(mp.SDLKey, key,
(SDL_event.key.keysym.mod & KMOD_NUM) != 0);
postEventFromUser(irrevent);
} break;

@ -273,10 +273,10 @@ class CIrrDeviceSDL : public CIrrDeviceStub
#endif
// Check if a key is a known special character with no side effects on text boxes.
static bool keyIsKnownSpecial(EKEY_CODE key);
static bool keyIsKnownSpecial(EKEY_CODE irrlichtKey);
// Return the Char that should be sent to Irrlicht for the given key (either the one passed in or 0).
static int findCharToPassToIrrlicht(int assumedChar, EKEY_CODE key);
static int findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtKey, bool numlock);
// Check if a text box is in focus. Enable or disable SDL_TEXTINPUT events only if in focus.
void resetReceiveTextInputEvents();

@ -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
@ -316,6 +316,7 @@ set(link_includes
set(IRRMESHLOADER
CB3DMeshFileLoader.cpp
CGLTFMeshFileLoader.cpp
COBJMeshFileLoader.cpp
CXMeshFileLoader.cpp
)
@ -328,6 +329,8 @@ add_library(IRRMESHOBJ OBJECT
${IRRMESHLOADER}
)
target_link_libraries(IRRMESHOBJ PUBLIC tiniergltf::tiniergltf)
add_library(IRROBJ OBJECT
CBillboardSceneNode.cpp
CCameraSceneNode.cpp
@ -339,6 +342,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
@ -503,6 +510,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}

@ -18,6 +18,7 @@
#include "CXMeshFileLoader.h"
#include "COBJMeshFileLoader.h"
#include "CB3DMeshFileLoader.h"
#include "CGLTFMeshFileLoader.h"
#include "CBillboardSceneNode.h"
#include "CAnimatedMeshSceneNode.h"
#include "CCameraSceneNode.h"
@ -76,6 +77,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

@ -6,6 +6,7 @@
#include <optional>
#include "CBoneSceneNode.h"
#include "IAnimatedMeshSceneNode.h"
#include "SSkinMeshBuffer.h"
#include "os.h"
namespace
@ -1061,6 +1062,11 @@ scene::SSkinMeshBuffer *CSkinnedMesh::addMeshBuffer()
return buffer;
}
void CSkinnedMesh::addMeshBuffer(SSkinMeshBuffer *meshbuf)
{
LocalBuffers.push_back(meshbuf);
}
CSkinnedMesh::SJoint *CSkinnedMesh::addJoint(SJoint *parent)
{
SJoint *joint = new SJoint;

@ -129,6 +129,9 @@ class CSkinnedMesh : public ISkinnedMesh
//! 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;

@ -0,0 +1,27 @@
add_executable(tests
testCGLTFMeshFileLoader.cpp
"${PROJECT_SOURCE_DIR}/source/Irrlicht/CReadFile.cpp"
)
target_compile_options(tests
PRIVATE
"$<$<CXX_COMPILER_ID:GNU>:-Wall>"
)
target_include_directories(tests
PRIVATE
# For CReadFile
"${PROJECT_SOURCE_DIR}/source/Irrlicht"
)
target_link_libraries(tests
PRIVATE
Catch2::Catch
IrrlichtMt::IrrlichtMt
)
add_test(
NAME tests
COMMAND "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tests"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
)

6
lib/tiniergltf/.gitignore vendored Normal file

@ -0,0 +1,6 @@
cmake
CMakeCache.txt
CMakeFiles
.cache
compile_commands.json
build

@ -0,0 +1,20 @@
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 base64.cpp base64.h)
add_library(tiniergltf::tiniergltf ALIAS tiniergltf)
target_include_directories(tiniergltf
INTERFACE
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
"${JSON_INCLUDE_DIR}" # Set in FindJson.cmake
)
target_link_libraries(tiniergltf)

67
lib/tiniergltf/Readme.md Normal file

@ -0,0 +1,67 @@
# 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.
- [ ] Consider base64 rewrite.
## 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.
The base64 library used is licensed as follows:
> base64.cpp and base64.h
>
> Copyright (C) 2004-2008 René Nyffenegger
> Modified by the Minetest Contributors.
>
> This source code is provided 'as-is', without any express or implied
> warranty. In no event will the author be held liable for any damages
> arising from the use of this software.
>
> Permission is granted to anyone to use this software for any purpose,
> including commercial applications, and to alter it and redistribute it
> freely, subject to the following restrictions:
>
> 1. The origin of this source code must not be misrepresented; you must not
> claim that you wrote the original source code. If you use this source code
> in a product, an acknowledgment in the product documentation would be
> appreciated but is not required.
>
> 2. Altered source versions must be plainly marked as such, and must not be
> misrepresented as being the original source code.
>
> 3. This notice may not be removed or altered from any source distribution.
>
> René Nyffenegger rene.nyffenegger@adp-gmbh.ch
## Bug Bounty
I offer a reward of one (1) virtual headpat per valid bug report.

112
lib/tiniergltf/base64.cpp Normal file

@ -0,0 +1,112 @@
/*
base64.cpp and base64.h
Copyright (C) 2004-2008 René Nyffenegger
Modified by the Minetest Contributors.
This source code is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this source code must not be misrepresented; you must not
claim that you wrote the original source code. If you use this source code
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original source code.
3. This notice may not be removed or altered from any source distribution.
René Nyffenegger rene.nyffenegger@adp-gmbh.ch
*/
#include "base64.h"
#include <string>
#include <string_view>
static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
static const std::string base64_chars_padding_1 = "AEIMQUYcgkosw048";
static const std::string base64_chars_padding_2 = "AQgw";
static inline bool is_base64(unsigned char c)
{
return (c >= '0' && c <= '9')
|| (c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z')
|| c == '+' || c == '/';
}
bool base64_is_valid(const std::string_view &s)
{
size_t i = 0;
for (; i < s.size(); ++i)
if (!is_base64(s[i]))
break;
unsigned char padding = 3 - ((i + 3) % 4);
if ((padding == 1 && base64_chars_padding_1.find(s[i - 1]) == std::string::npos)
|| (padding == 2 && base64_chars_padding_2.find(s[i - 1]) == std::string::npos)
|| padding == 3)
return false;
int actual_padding = s.size() - i;
// omission of padding characters is allowed
if (actual_padding == 0)
return true;
// remaining characters (max. 2) may only be padding
for (; i < s.size(); ++i)
if (s[i] != '=')
return false;
// number of padding characters needs to match
return padding == actual_padding;
}
std::vector<unsigned char> base64_decode(const std::string_view &encoded_string) {
std::size_t in_len = encoded_string.size();
std::size_t i = 0, j = 0, in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::vector<unsigned char> ret;
ret.reserve(3 * in_len / 4 + 1);
while (in_len-- && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
char_array_4[i++] = encoded_string[in_]; in_++;
if (i ==4) {
for (i = 0; i <4; i++)
char_array_4[i] = base64_chars.find(char_array_4[i]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; (i < 3); i++)
ret.push_back(char_array_3[i]);
i = 0;
}
}
if (i) {
for (j = i; j <4; j++)
char_array_4[j] = 0;
for (j = 0; j <4; j++)
char_array_4[j] = base64_chars.find(char_array_4[j]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (j = 0; (j < i - 1); j++)
ret.push_back(char_array_3[j]);
}
return ret;
}

35
lib/tiniergltf/base64.h Normal file

@ -0,0 +1,35 @@
/*
base64.cpp and base64.h
Copyright (C) 2004-2008 René Nyffenegger
Minor modifications by the Minetest Contributors.
This source code is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this source code must not be misrepresented; you must not
claim that you wrote the original source code. If you use this source code
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original source code.
3. This notice may not be removed or altered from any source distribution.
René Nyffenegger rene.nyffenegger@adp-gmbh.ch
*/
#pragma once
#include <vector>
#include <string_view>
bool base64_is_valid(const std::string_view &s);
std::vector<unsigned char> base64_decode(const std::string_view &s);

File diff suppressed because it is too large Load Diff

@ -833,7 +833,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);

@ -2527,7 +2527,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

@ -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)

@ -0,0 +1,368 @@
// Minetest
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "CSceneManager.h"
#include "content/subgames.h"
#include "filesys.h"
#include "CReadFile.h"
#include "vector3d.h"
#include <irrlicht.h>
#include "catch.h"
using v3f = irr::core::vector3df;
using v2f = irr::core::vector2df;
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<const irr::video::S3DVertex *>(
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<const irr::u16 *>(
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<const irr::video::S3DVertex *>(
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<const irr::u16 *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::u16 *>(
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<const irr::u16 *>(
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<const irr::u16 *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<const irr::video::S3DVertex *>(
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<irr::video::S3DVertex *>(
mesh->getMeshBuffer(0)->getVertices());
const std::array<v3f, 14> 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]);
}
}

@ -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).