mirror of
https://github.com/minetest/minetest.git
synced 2024-06-30 21:20:36 +02:00
Compare commits
51 Commits
de87f79a14
...
dde1eb76fe
Author | SHA1 | Date | |
---|---|---|---|
|
dde1eb76fe | ||
|
9a1501ae89 | ||
|
6be3c1a0f0 | ||
|
196bb56dfd | ||
|
3467d91730 | ||
|
35466b227a | ||
|
8547a33cc7 | ||
|
31ab21ffd5 | ||
|
0ce6b3f8c8 | ||
|
616a27a966 | ||
|
39691664ba | ||
|
d6b6eec1bf | ||
|
997926461e | ||
|
37c3edfc38 | ||
|
be03a435b8 | ||
|
580a71554d | ||
|
1d185c1ce7 | ||
|
451978dc2c | ||
|
59a173b51b | ||
|
50c8604de1 | ||
|
50d20a854c | ||
|
d5450c62bf | ||
|
2a9e570cbc | ||
|
8f6c24cbe9 | ||
|
d4d9035ee5 | ||
|
0eef3b07b8 | ||
|
b45aa1cd72 | ||
|
8c35461af2 | ||
|
be375cdfb6 | ||
|
b419d0e445 | ||
|
3a4dde48e8 | ||
|
28299e7910 | ||
|
942d46d37c | ||
|
88383e40db | ||
|
d4de89cbe7 | ||
|
b734119d05 | ||
|
818ac9ea3d | ||
|
a372397416 | ||
|
a2311dc734 | ||
|
00d23d65e0 | ||
|
315c03b99a | ||
|
411969c87c | ||
|
8a2cae1065 | ||
|
5bd1d052ad | ||
|
b95dcb7e7a | ||
|
da722a833f | ||
|
c803a9c895 | ||
|
b9b03557c6 | ||
|
1baa3f1e34 | ||
|
26276729ed | ||
|
ec5950033f |
@ -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
|
||||
------------------
|
||||
|
||||
|
11
games/devtest/mods/gltf/LICENSE.md
Normal file
11
games/devtest/mods/gltf/LICENSE.md
Normal file
@ -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
|
22
games/devtest/mods/gltf/init.lua
Normal file
22
games/devtest/mods/gltf/init.lua
Normal file
@ -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
games/devtest/mods/gltf/invalid/empty.gltf
Normal file
0
games/devtest/mods/gltf/invalid/empty.gltf
Normal file
@ -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"}}
|
1
games/devtest/mods/gltf/invalid/json_missing_brace.gltf
Normal file
1
games/devtest/mods/gltf/invalid/json_missing_brace.gltf
Normal file
@ -0,0 +1 @@
|
||||
{
|
2
games/devtest/mods/gltf/mod.conf
Normal file
2
games/devtest/mods/gltf/mod.conf
Normal file
@ -0,0 +1,2 @@
|
||||
name = gltf
|
||||
description = Hosts gltf test models, both for the C++ unit tests and for in-game viewing
|
1
games/devtest/mods/gltf/models/gltf_blender_cube.gltf
Normal file
1
games/devtest/mods/gltf/models/gltf_blender_cube.gltf
Normal file
@ -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"}}
|
1
games/devtest/mods/gltf/models/gltf_snow_man.gltf
Normal file
1
games/devtest/mods/gltf/models/gltf_snow_man.gltf
Normal file
File diff suppressed because one or more lines are too long
1
games/devtest/mods/gltf/models/gltf_spider.gltf
Normal file
1
games/devtest/mods/gltf/models/gltf_spider.gltf
Normal file
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"}}
|
BIN
games/devtest/mods/gltf/textures/gltf_snow_man.png
Normal file
BIN
games/devtest/mods/gltf/textures/gltf_snow_man.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 297 B |
BIN
games/devtest/mods/gltf/textures/gltf_spider.png
Normal file
BIN
games/devtest/mods/gltf/textures/gltf_spider.png
Normal file
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>
|
||||
|
679
irr/src/CGLTFMeshFileLoader.cpp
Normal file
679
irr/src/CGLTFMeshFileLoader.cpp
Normal file
@ -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 primitive’s 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
|
||||
|
147
irr/src/CGLTFMeshFileLoader.h
Normal file
147
irr/src/CGLTFMeshFileLoader.h
Normal file
@ -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;
|
||||
|
||||
|
27
irr/src/tests/CMakeLists.txt
Normal file
27
irr/src/tests/CMakeLists.txt
Normal file
@ -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
6
lib/tiniergltf/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
cmake
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
.cache
|
||||
compile_commands.json
|
||||
build
|
20
lib/tiniergltf/CMakeLists.txt
Normal file
20
lib/tiniergltf/CMakeLists.txt
Normal file
@ -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
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
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
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);
|
1355
lib/tiniergltf/tiniergltf.hpp
Normal file
1355
lib/tiniergltf/tiniergltf.hpp
Normal file
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)
|
||||
|
||||
|
368
src/unittest/test_irr_gltf_mesh_loader.cpp
Normal file
368
src/unittest/test_irr_gltf_mesh_loader.cpp
Normal file
@ -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).
|
||||
|
Loading…
Reference in New Issue
Block a user