Add static glTF support (#14557)

Co-authored-by: Lars Mueller <appgurulars@gmx.de>
Co-authored-by: jordan4ibanez <jordan4ibanez@users.noreply.github.com>
Co-authored-by: sfan5 <sfan5@live.de>
Co-authored-by: SmallJoker <SmallJoker@users.noreply.github.com>
This commit is contained in:
JosiahWI 2024-09-02 07:50:30 -05:00 committed by GitHub
parent 8972c80d7d
commit ac11a14509
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 2863 additions and 28 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)

@ -274,7 +274,7 @@ Accepted formats are:
images: .png, .jpg, .tga, (deprecated:) .bmp
sounds: .ogg vorbis
models: .x, .b3d, .obj
models: .x, .b3d, .obj, .gltf (Minetest 5.10 or newer)
Other formats won't be sent to the client (e.g. you can store .blend files
in a folder for convenience, without the risk that such files are transferred)
@ -291,6 +291,43 @@ in one of its parents, the parent's file is used.
Although it is discouraged, a mod can overwrite a media file of any mod that it
depends on by supplying a file with an equal name.
Only a subset of model file format features is supported:
Simple textured meshes (with multiple textures), optionally with normals.
The .x and .b3d formats additionally support skeletal animation.
#### glTF
The glTF model file format for now only serves as a
more modern alternative to the other static model file formats;
it unlocks no special rendering features.
This means that many glTF features are not supported *yet*, including:
* Animation
* Cameras
* Materials
* Only base color textures are supported
* Backface culling is overridden
* Double-sided materials don't work
* Alternative means of supplying data
* Embedded images
* References to files via URIs
Textures are supplied solely via the same means as for the other model file formats:
The `textures` object property, the `tiles` node definition field and
the list of textures used in the `model[]` formspec element.
The order in which textures are to be supplied
is that in which they appear in the `textures` array in the glTF file.
Do not rely on glTF features not being supported; they may be supported in the future.
The backwards compatibility guarantee does not extend to ignoring unsupported features.
For example, if your model used an emissive material,
you should expect that a future version of Minetest may respect this,
and thus cause your model to render differently there.
Naming conventions
------------------

@ -0,0 +1,14 @@
glTF test model (and corresponding texture) licenses:
* Spider (`gltf_spider.gltf`, `gltf_spider.png`):
* By [archfan7411](https://github.com/archfan7411)
* Licensed under CC0, public domain "wherever public domain carries fewer rights or legal protections"
* Frog (`gltf_frog.gltf`, `gltf_frog.png`):
* By [Susybaka1234](https://sketchfab.com/3d-models/african-clawed-frog-v2-c81152c93948480c931c280d18957358)
* Licensed under CC-BY 4.0
* Snow Man (`gltf_snow_man.gltf`, `gltf_snow_man.png`):
* By [jordan4ibanez](https://github.com/jordan4ibanez)
* Licensed under CC0
* Minimal triangle, triangle without indices (`gltf_minimal_triangle.gltf`, `gltf_triangle_without_indices.gltf`)
* From [the glTF sample model collection](https://github.com/KhronosGroup/glTF-Sample-Models)
* Licensed under CC0 / public domain

@ -0,0 +1,51 @@
local function register_entity(name, textures, backface_culling)
minetest.register_entity("gltf:" .. name, {
initial_properties = {
visual = "mesh",
mesh = "gltf_" .. name .. ".gltf",
textures = textures,
backface_culling = backface_culling,
},
})
end
-- These do not have texture coordinates; they simple render as black surfaces.
register_entity("minimal_triangle", {}, false)
register_entity("triangle_with_vertex_stride", {}, false)
register_entity("triangle_without_indices", {}, false)
do
local cube_textures = {"gltf_cube.png"}
register_entity("blender_cube", cube_textures)
register_entity("blender_cube_scaled", cube_textures)
register_entity("blender_cube_matrix_transform", cube_textures)
end
register_entity("snow_man", {"gltf_snow_man.png"})
register_entity("spider", {"gltf_spider.png"})
-- Note: Model has an animation, but we can use it as a static test nevertheless
-- The claws rendering incorrectly from one side is expected behavior:
-- They use an unsupported double-sided material.
register_entity("frog", {"gltf_frog.png"}, false)
minetest.register_node("gltf:frog", {
description = "glTF frog, but it's a node",
tiles = {{name = "gltf_frog.png", backface_culling = false}},
drawtype = "mesh",
mesh = "gltf_frog.gltf",
})
minetest.register_chatcommand("show_model", {
params = "<model> [textures]",
description = "Show a model (defaults to gltf models, for example '/show_model frog').",
func = function(name, param)
local model, textures = param:match"^(.-)%s+(.+)$"
if not model then
model = "gltf_" .. param .. ".gltf"
textures = "gltf_" .. param .. ".png"
end
minetest.show_formspec(name, "gltf:model", table.concat{
"formspec_version[7]",
"size[10,10]",
"model[0,0;10,10;model;", model, ";", textures, ";0,0;true;true;0,0;0]",
})
end,
})

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

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,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: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -86,6 +86,17 @@ public:
mesh buffer. */
virtual IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const = 0;
//! Minetest binds textures (node tiles, object textures) to models.
// glTF allows multiple primitives (mesh buffers) to reference the same texture.
// This is reflected here: This function gets the texture slot for a mesh buffer.
/** \param meshbufNr: Zero based index of the mesh buffer. The maximum value is
getMeshBufferCount() - 1;
\return number of texture slot to bind to the given mesh buffer */
virtual u32 getTextureSlot(u32 meshbufNr) const
{
return meshbufNr;
}
//! Get an axis aligned bounding box of the mesh.
/** \return Bounding box of this mesh. */
virtual const core::aabbox3d<f32> &getBoundingBox() const = 0;

@ -199,6 +199,9 @@ public:
//! Adds a new meshbuffer to the mesh, access it as last one
virtual SSkinMeshBuffer *addMeshBuffer() = 0;
//! Adds a new meshbuffer to the mesh, access it as last one
virtual void addMeshBuffer(SSkinMeshBuffer *meshbuf) = 0;
//! Adds a new joint to the mesh, access it as last one
virtual SJoint *addJoint(SJoint *parent = 0) = 0;

@ -64,6 +64,17 @@ struct SMesh : public IMesh
return nullptr;
}
u32 getTextureSlot(u32 meshbufNr) const override
{
return TextureSlots.at(meshbufNr);
}
void setTextureSlot(u32 meshbufNr, u32 textureSlot)
{
TextureSlots.at(meshbufNr) = textureSlot;
}
//! returns an axis aligned bounding box
const core::aabbox3d<f32> &getBoundingBox() const override
{
@ -103,6 +114,7 @@ struct SMesh : public IMesh
if (buf) {
buf->grab();
MeshBuffers.push_back(buf);
TextureSlots.push_back(getMeshBufferCount() - 1);
}
}
@ -122,6 +134,8 @@ struct SMesh : public IMesh
//! The meshbuffers of this mesh
std::vector<IMeshBuffer *> MeshBuffers;
//! Mapping from meshbuffer number to bindable texture slot
std::vector<u32> TextureSlots;
//! The bounding box of this mesh
core::aabbox3d<f32> BoundingBox;

@ -6,6 +6,7 @@
#include "IMeshBuffer.h"
#include "S3DVertex.h"
#include "irrArray.h"
namespace irr
{
@ -21,10 +22,14 @@ struct SSkinMeshBuffer : public IMeshBuffer
PrimitiveType(EPT_TRIANGLES), HWBuffer(nullptr),
MappingHint_Vertex(EHM_NEVER), MappingHint_Index(EHM_NEVER),
BoundingBoxNeedsRecalculated(true)
{}
//! Constructor for standard vertices
SSkinMeshBuffer(std::vector<video::S3DVertex> &&vertices, std::vector<u16> &&indices) :
SSkinMeshBuffer()
{
#ifdef _DEBUG
setDebugName("SSkinMeshBuffer");
#endif
Vertices_Standard = std::move(vertices);
Indices = std::move(indices);
}
//! Get Material of this buffer.

@ -45,6 +45,10 @@ public:
{
}
//! Move constructor
array(std::vector<T> &&data) :
m_data(std::move(data)), is_sorted(false) {}
//! Reallocates the array, make it bigger or smaller.
/** \param new_size New size of array.
\param canShrink Specifies whether the array is reallocated even if

@ -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,10 @@ public:
*this = other;
}
string(const stl_type &str) : str(str) {}
string(stl_type &&str) : str(std::move(str)) {}
//! Constructor from other string types
template <class B>
string(const string<B> &other)
@ -814,13 +818,6 @@ public:
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,695 @@
// Minetest
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "CGLTFMeshFileLoader.h"
#include "coreutil.h"
#include "CSkinnedMesh.h"
#include "ISkinnedMesh.h"
#include "irrTypes.h"
#include "IReadFile.h"
#include "matrix4.h"
#include "path.h"
#include "quaternion.h"
#include "vector3d.h"
#include "os.h"
#include "tiniergltf.hpp"
#include <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 char *ptr) \
{ \
std::array<T, N> res; \
for (int 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 char *ptr)
{
T dest;
std::memcpy(&dest, ptr, sizeof(dest));
#ifdef __BIG_ENDIAN__
return os::Byteswap::byteswap(dest);
#else
return dest;
#endif
}
// Note that these "more specialized templates" should win.
template <>
core::matrix4 SelfType::rawget(const char *ptr)
{
core::matrix4 mat;
for (u8 i = 0; i < 16; ++i) {
mat[i] = rawget<f32>(ptr + i * sizeof(f32));
}
return mat;
}
template <>
core::vector3df SelfType::rawget(const char *ptr)
{
return core::vector3df(
rawget<f32>(ptr),
rawget<f32>(ptr + sizeof(f32)),
rawget<f32>(ptr + 2 * sizeof(f32)));
}
template <>
core::quaternion SelfType::rawget(const char *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())) {
os::Printer::log("glTF loader", "missing required fields", ELL_ERROR);
return nullptr;
}
auto *mesh = new CSkinnedMesh();
MeshExtractor parser(std::move(model.value()), mesh);
try {
parser.loadNodes();
} catch (std::runtime_error &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
mesh->drop();
return nullptr;
}
if (model->images.has_value())
os::Printer::log("glTF loader", "embedded images are not supported", ELL_WARNING);
return mesh;
}
static void transformVertices(std::vector<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 pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) {
const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi);
auto vertices = getVertices(primitive);
if (!vertices.has_value())
continue; // "When positions are not specified, client implementations SHOULD skip 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(primitive);
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)));
if (primitive.material.has_value()) {
const auto &material = m_gltf_model.materials->at(*primitive.material);
if (material.pbrMetallicRoughness.has_value()) {
const auto &texture = material.pbrMetallicRoughness->baseColorTexture;
if (texture.has_value()) {
const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1;
m_irr_model->setTextureSlot(meshbufNr, static_cast<u32>(texture->index));
}
}
}
}
}
// Base transformation between left & right handed coordinate systems.
// This just inverts the Z axis.
static const core::matrix4 leftToRight = core::matrix4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1
);
static const core::matrix4 rightToLeft = leftToRight;
static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m)
{
// Note: Under the hood, this casts these doubles to floats.
return core::matrix4(
m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11],
m[12], m[13], m[14], m[15]);
}
static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs)
{
const auto &trans = trs.translation;
const auto &rot = trs.rotation;
const auto &scale = trs.scale;
core::matrix4 transMat;
transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2]));
core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix();
core::matrix4 scaleMat;
scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2]));
return transMat * rotMat * scaleMat;
}
static core::matrix4 loadTransform(std::optional<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 tiniergltf::MeshPrimitive &primitive) const
{
const auto accessorIdx = primitive.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 tiniergltf::MeshPrimitive &primitive) const
{
const auto &attributes = primitive.attributes;
const auto positionAccessorIdx = attributes.position;
if (!positionAccessorIdx.has_value()) {
// "When positions are not specified, client implementations SHOULD skip primitive's rendering"
return std::nullopt;
}
std::vector<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 = 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;
std::unique_ptr<char[]> buf(new 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) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
return std::nullopt;
} catch (const std::out_of_range &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR);
return std::nullopt;
}
}
} // namespace scene
} // namespace irr

@ -0,0 +1,147 @@
// Minetest
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "CSkinnedMesh.h"
#include "IMeshLoader.h"
#include "IReadFile.h"
#include "irrTypes.h"
#include "path.h"
#include "S3DVertex.h"
#include <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 char *ptr);
template <class T>
class Accessor
{
struct BufferSource
{
const char *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 char *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,
CSkinnedMesh *mesh) noexcept
: m_gltf_model(model), m_irr_model(mesh) {};
/* Gets indices for the given mesh/primitive.
*
* Values are return in Irrlicht winding order.
*/
std::optional<std::vector<u16>> getIndices(
const tiniergltf::MeshPrimitive &primitive) const;
std::optional<std::vector<video::S3DVertex>> getVertices(
const tiniergltf::MeshPrimitive &primitive) const;
std::size_t getMeshCount() const;
std::size_t getPrimitiveCount(const std::size_t meshIdx) const;
void loadNodes() const;
private:
const tiniergltf::GlTF m_gltf_model;
CSkinnedMesh *m_irr_model;
void copyPositions(const std::size_t accessorIdx,
std::vector<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

@ -17,7 +17,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
set(CMAKE_CXX_FLAGS_DEBUG "-g")
add_compile_options(-Wall -pipe -fno-exceptions)
add_compile_options(-Wall -pipe)
# Enable SSE for floating point math on 32-bit x86 by default
# reasoning see minetest issue #11810 and https://gcc.gnu.org/wiki/FloatingPointMath
@ -298,6 +298,7 @@ set(link_includes
set(IRRMESHLOADER
CB3DMeshFileLoader.cpp
CGLTFMeshFileLoader.cpp
COBJMeshFileLoader.cpp
CXMeshFileLoader.cpp
)
@ -310,6 +311,8 @@ add_library(IRRMESHOBJ OBJECT
${IRRMESHLOADER}
)
target_link_libraries(IRRMESHOBJ PUBLIC tiniergltf::tiniergltf)
add_library(IRROBJ OBJECT
CBillboardSceneNode.cpp
CCameraSceneNode.cpp
@ -321,6 +324,10 @@ add_library(IRROBJ OBJECT
CMeshCache.cpp
)
# Make sure IRROBJ gets the transitive include directories for
# tiniergltf from IRRMESHOBJ.
target_link_libraries(IRROBJ PRIVATE IRRMESHOBJ)
set(IRRDRVROBJ
CNullDriver.cpp
CGLXManager.cpp
@ -477,6 +484,7 @@ target_include_directories(IrrlichtMt
# this needs to be here and not in a variable (like link_includes) due to issues
# with the generator expressions on at least CMake 3.22, but not 3.28 or later
target_link_libraries(IrrlichtMt PRIVATE
tiniergltf::tiniergltf
${ZLIB_LIBRARY}
${JPEG_LIBRARY}
${PNG_LIBRARY}

@ -20,6 +20,7 @@
#include "CXMeshFileLoader.h"
#include "COBJMeshFileLoader.h"
#include "CB3DMeshFileLoader.h"
#include "CGLTFMeshFileLoader.h"
#include "CBillboardSceneNode.h"
#include "CAnimatedMeshSceneNode.h"
#include "CCameraSceneNode.h"
@ -78,6 +79,7 @@ CSceneManager::CSceneManager(video::IVideoDriver *driver,
MeshLoaderList.push_back(new CXMeshFileLoader(this));
MeshLoaderList.push_back(new COBJMeshFileLoader(this));
MeshLoaderList.push_back(new CB3DMeshFileLoader(this));
MeshLoaderList.push_back(new CGLTFMeshFileLoader());
}
//! destructor

@ -6,6 +6,7 @@
#include <optional>
#include "CBoneSceneNode.h"
#include "IAnimatedMeshSceneNode.h"
#include "SSkinMeshBuffer.h"
#include "os.h"
namespace
@ -596,6 +597,15 @@ IMeshBuffer *CSkinnedMesh::getMeshBuffer(const video::SMaterial &material) const
return 0;
}
u32 CSkinnedMesh::getTextureSlot(u32 meshbufNr) const
{
return TextureSlots.at(meshbufNr);
}
void CSkinnedMesh::setTextureSlot(u32 meshbufNr, u32 textureSlot) {
TextureSlots.at(meshbufNr) = textureSlot;
}
//! returns an axis aligned bounding box
const core::aabbox3d<f32> &CSkinnedMesh::getBoundingBox() const
{
@ -1057,10 +1067,17 @@ void CSkinnedMesh::updateBoundingBox(void)
scene::SSkinMeshBuffer *CSkinnedMesh::addMeshBuffer()
{
scene::SSkinMeshBuffer *buffer = new scene::SSkinMeshBuffer();
TextureSlots.push_back(LocalBuffers.size());
LocalBuffers.push_back(buffer);
return buffer;
}
void CSkinnedMesh::addMeshBuffer(SSkinMeshBuffer *meshbuf)
{
TextureSlots.push_back(LocalBuffers.size());
LocalBuffers.push_back(meshbuf);
}
CSkinnedMesh::SJoint *CSkinnedMesh::addJoint(SJoint *parent)
{
SJoint *joint = new SJoint;

@ -61,6 +61,10 @@ public:
NULL if there is no such mesh buffer. */
IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const override;
u32 getTextureSlot(u32 meshbufNr) const override;
void setTextureSlot(u32 meshbufNr, u32 textureSlot);
//! returns an axis aligned bounding box
const core::aabbox3d<f32> &getBoundingBox() const override;
@ -129,6 +133,9 @@ public:
//! Adds a new meshbuffer to the mesh, access it as last one
SSkinMeshBuffer *addMeshBuffer() override;
//! Adds a new meshbuffer to the mesh, access it as last one
void addMeshBuffer(SSkinMeshBuffer *meshbuf) override;
//! Adds a new joint to the mesh, access it as last one
SJoint *addJoint(SJoint *parent = 0) override;
@ -184,6 +191,8 @@ private:
core::array<SSkinMeshBuffer *> *SkinningBuffers; // Meshbuffer to skin, default is to skin localBuffers
core::array<SSkinMeshBuffer *> LocalBuffers;
//! Mapping from meshbuffer number to bindable texture slot
std::vector<u32> TextureSlots;
core::array<SJoint *> AllJoints;
core::array<SJoint *> RootJoints;

6
lib/tiniergltf/.gitignore vendored Normal file

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

@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.12)
project(tiniergltf
VERSION 1.0.0
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(tiniergltf OBJECT tiniergltf.hpp)
add_library(tiniergltf::tiniergltf ALIAS tiniergltf)
set_target_properties(tiniergltf PROPERTIES LINKER_LANGUAGE CXX)
target_include_directories(tiniergltf
INTERFACE
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
"${JSON_INCLUDE_DIR}" # Set in FindJson.cmake
"${CMAKE_SOURCE_DIR}/src" # util/base64.h
)
target_link_libraries(tiniergltf)

39
lib/tiniergltf/Readme.md Normal file

@ -0,0 +1,39 @@
# TinierGLTF
A safe, modern, tiny glTF loader for C++ 17.
What this is:
* A tiny glTF deserializer which maps JSON objects to appropriate C++ structures.
* Intended to be safe for loading untrusted input.
* Slightly tailored to the needs of [Minetest](https://github.com/minetest/minetest).
What this doesn't and shouldn't do:
* Serialization
* Loading images
* Resolving resources
* Support glTF extensions
## TODOs
- [ ] Add GLB support.
- [ ] Add further checks according to the specification.
- Everything in the JSON schema (+ indices and misc. stuff) is already validated.
Much of the code was generated by a Lua script from the JSON schemata.
## License
`tiniergltf.hpp` was written by Lars Müller and is licensed under the MIT license:
> Copyright 2024 Lars Müller
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## Bug Bounty
I offer a reward of one (1) virtual headpat per valid bug report.

File diff suppressed because it is too large Load Diff

@ -826,7 +826,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
}
const char *model_ext[] = {
".x", ".b3d", ".obj",
".x", ".b3d", ".obj", ".gltf",
NULL
};
name = removeStringEnd(filename, model_ext);

@ -844,14 +844,19 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr)
if (m_animated_meshnode) {
u32 mat_count = m_animated_meshnode->getMaterialCount();
assert(mat_count == m_animated_meshnode->getMesh()->getMeshBufferCount());
u32 max_tex_idx = 0;
for (u32 i = 0; i < mat_count; ++i) {
max_tex_idx = std::max(max_tex_idx,
m_animated_meshnode->getMesh()->getTextureSlot(i));
}
if (mat_count == 0 || m_prop.textures.empty()) {
// nothing
} else if (mat_count > m_prop.textures.size()) {
} else if (max_tex_idx >= m_prop.textures.size()) {
std::ostringstream oss;
oss << "GenericCAO::addToScene(): Model "
<< m_prop.mesh << " loaded with " << mat_count
<< " mesh buffers but only " << m_prop.textures.size()
<< " texture(s) specified, this is deprecated.";
<< m_prop.mesh << " is missing " << (max_tex_idx + 1 - m_prop.textures.size())
<< " more texture(s), this is deprecated.";
logOnce(oss, warningstream);
video::ITexture *last = m_animated_meshnode->getMaterial(0).TextureLayers[0].Texture;
@ -1370,9 +1375,11 @@ void GenericCAO::updateTextures(std::string mod)
else if (m_animated_meshnode) {
if (m_prop.visual == "mesh") {
for (u32 i = 0; i < m_prop.textures.size() &&
i < m_animated_meshnode->getMaterialCount(); ++i) {
std::string texturestring = m_prop.textures[i];
for (u32 i = 0; i < m_animated_meshnode->getMaterialCount(); ++i) {
const auto texture_idx = m_animated_meshnode->getMesh()->getTextureSlot(i);
if (texture_idx >= m_prop.textures.size())
continue;
std::string texturestring = m_prop.textures[texture_idx];
if (texturestring.empty())
continue; // Empty texture string means don't modify that material
texturestring += mod;

@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <cmath>
#include "content_mapblock.h"
#include "util/basic_macros.h"
#include "util/numeric.h"
#include "util/directiontables.h"
#include "mapblock_mesh.h"
@ -1676,7 +1677,9 @@ void MapblockMeshGenerator::drawMeshNode()
int mesh_buffer_count = mesh->getMeshBufferCount();
for (int j = 0; j < mesh_buffer_count; j++) {
useTile(j);
// Only up to 6 tiles are supported
const auto tile = mesh->getTextureSlot(j);
useTile(MYMIN(tile, 5));
scene::IMeshBuffer *buf = mesh->getMeshBuffer(j);
video::S3DVertex *vertices = (video::S3DVertex *)buf->getVertices();
int vertex_count = buf->getVertexCount();

@ -397,8 +397,8 @@ scene::SMesh* cloneMesh(scene::IMesh *src_mesh)
scene::IMeshBuffer *temp_buf = cloneMeshBuffer(
src_mesh->getMeshBuffer(j));
dst_mesh->addMeshBuffer(temp_buf);
dst_mesh->setTextureSlot(j, src_mesh->getTextureSlot(j));
temp_buf->drop();
}
return dst_mesh;
}

@ -2807,8 +2807,13 @@ void GUIFormSpecMenu::parseModel(parserData *data, const std::string &element)
auto meshnode = e->setMesh(mesh);
for (u32 i = 0; i < textures.size() && i < meshnode->getMaterialCount(); ++i)
e->setTexture(i, m_tsrc->getTexture(unescape_string(textures[i])));
for (u32 i = 0; i < meshnode->getMaterialCount(); ++i) {
const auto texture_idx = mesh->getTextureSlot(i);
if (texture_idx >= textures.size())
warningstream << "Invalid model element: Not enough textures" << std::endl;
else
e->setTexture(i, m_tsrc->getTexture(unescape_string(textures[texture_idx])));
}
if (vec_rot.size() >= 2)
e->setRotation(v2f(stof(vec_rot[0]), stof(vec_rot[1])));

@ -2465,7 +2465,7 @@ bool Server::addMediaFile(const std::string &filename,
const char *supported_ext[] = {
".png", ".jpg", ".bmp", ".tga",
".ogg",
".x", ".b3d", ".obj",
".x", ".b3d", ".obj", ".gltf",
// Custom translation file format
".tr",
NULL

@ -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,366 @@
// Minetest
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "CSceneManager.h"
#include "content/subgames.h"
#include "filesys.h"
#include "CReadFile.h"
#include "irr_v3d.h"
#include "irr_v2d.h"
#include <irrlicht.h>
#include "catch.h"
TEST_CASE("gltf") {
const auto gamespec = findSubgame("devtest");
if (!gamespec.isValid())
SKIP();
irr::scene::CSceneManager smgr(nullptr, nullptr, nullptr);
const auto loadMesh = [&smgr](const irr::io::path& filepath) {
irr::io::CReadFile file(filepath);
return smgr.getMesh(&file);
};
const static auto model_stem = gamespec.gamemods_path +
DIR_DELIM + "gltf" + DIR_DELIM + "models" + DIR_DELIM + "gltf_";
SECTION("error cases") {
const static auto invalid_model_path = gamespec.gamemods_path + DIR_DELIM + "gltf" + DIR_DELIM + "invalid" + DIR_DELIM;
SECTION("empty gltf file") {
CHECK(loadMesh(invalid_model_path + "empty.gltf") == nullptr);
}
SECTION("null file pointer") {
CHECK(smgr.getMesh(nullptr) == nullptr);
}
SECTION("invalid JSON") {
CHECK(loadMesh(invalid_model_path + "json_missing_brace.gltf") == nullptr);
}
// This is an example of something that should be validated by tiniergltf.
SECTION("invalid bufferview bounds")
{
CHECK(loadMesh(invalid_model_path + "invalid_bufferview_bounds.gltf") == nullptr);
}
}
SECTION("minimal triangle") {
const auto path = GENERATE(
model_stem + "minimal_triangle.gltf",
model_stem + "triangle_with_vertex_stride.gltf",
// Test non-indexed geometry.
model_stem + "triangle_without_indices.gltf");
INFO(path);
const auto mesh = loadMesh(path);
REQUIRE(mesh != nullptr);
REQUIRE(mesh->getMeshBufferCount() == 1);
SECTION("vertex coordinates are correct") {
REQUIRE(mesh->getMeshBuffer(0)->getVertexCount() == 3);
auto vertices = static_cast<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).