Add glTF animation support

This commit is contained in:
Lars Mueller 2024-01-06 20:21:04 +01:00 committed by Lars Müller
parent d8274af670
commit 323fc0a798
9 changed files with 421 additions and 79 deletions

2
.gitattributes vendored
View file

@ -3,3 +3,5 @@
*.cpp diff=cpp *.cpp diff=cpp
*.h diff=cpp *.h diff=cpp
*.gltf binary

View file

@ -294,7 +294,7 @@ depends on by supplying a file with an equal name.
Only a subset of model file format features is supported: Only a subset of model file format features is supported:
Simple textured meshes (with multiple textures), optionally with normals. Simple textured meshes (with multiple textures), optionally with normals.
The .x and .b3d formats additionally support skeletal animation. The .x, .b3d and .gltf formats additionally support (a single) animation.
#### glTF #### glTF
@ -307,7 +307,10 @@ due to their space savings.
This means that many glTF features are not supported *yet*, including: This means that many glTF features are not supported *yet*, including:
* Animation * Animations
* Only a single animation is supported,
use frame ranges within this animation.
* Only integer frames are supported.
* Cameras * Cameras
* Materials * Materials
* Only base color textures are supported * Only base color textures are supported

View file

@ -1,4 +1,4 @@
glTF test model (and corresponding texture) licenses: The glTF test models (and corresponding textures) in this mod are all licensed freely:
* Spider (`gltf_spider.gltf`, `gltf_spider.png`): * Spider (`gltf_spider.gltf`, `gltf_spider.png`):
* By [archfan7411](https://github.com/archfan7411) * By [archfan7411](https://github.com/archfan7411)

View file

@ -27,8 +27,34 @@ do
}, },
}) })
end end
register_entity("snow_man", {"gltf_snow_man.png"}) register_entity("snow_man", {"gltf_snow_man.png"})
register_entity("spider", {"gltf_spider.png"}) register_entity("spider", {"gltf_spider.png"})
minetest.register_entity("gltf:spider_animated", {
initial_properties = {
visual = "mesh",
mesh = "gltf_spider_animated.gltf",
textures = {"gltf_spider.png"},
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 140}, 1)
end
})
minetest.register_entity("gltf:simple_skin", {
initial_properties = {
visual = "mesh",
visual_size = vector.new(5, 5, 5),
mesh = "gltf_simple_skin.gltf",
textures = {},
backface_culling = false
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 5.5}, 1)
end
})
-- Note: Model has an animation, but we can use it as a static test nevertheless -- 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: -- The claws rendering incorrectly from one side is expected behavior:
-- They use an unsupported double-sided material. -- They use an unsupported double-sided material.

View file

@ -0,0 +1 @@
{"scene":0,"scenes":[{"nodes":[0,1]}],"nodes":[{"skin":0,"mesh":0},{"children":[2]},{"translation":[0.0,1.0,0.0],"rotation":[0.0,0.0,0.0,1.0]}],"meshes":[{"primitives":[{"attributes":{"POSITION":1,"JOINTS_0":2,"WEIGHTS_0":3},"indices":0}]}],"skins":[{"inverseBindMatrices":4,"joints":[1,2]}],"animations":[{"channels":[{"sampler":0,"target":{"node":2,"path":"rotation"}}],"samplers":[{"input":5,"interpolation":"LINEAR","output":6}]}],"buffers":[{"uri":"data:application/gltf-buffer;base64,AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAvwAAAD8AAAAAAAAAPwAAAD8AAAAAAAAAvwAAgD8AAAAAAAAAPwAAgD8AAAAAAAAAvwAAwD8AAAAAAAAAPwAAwD8AAAAAAAAAvwAAAEAAAAAAAAAAPwAAAEAAAAAA","byteLength":168},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD4AAEA/AAAAAAAAAAAAAIA+AABAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAA=","byteLength":320},{"uri":"data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=","byteLength":128},{"uri":"data:application/gltf-buffer;base64,AAAAAAAAAD8AAIA/AADAPwAAAEAAACBAAABAQAAAYEAAAIBAAACQQAAAoEAAALBAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAPT9ND/0/TQ/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAkxjEPkSLbD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAPT9NL/0/TQ/AAAAAAAAAAD0/TS/9P00PwAAAAAAAAAAkxjEvkSLbD8AAAAAAAAAAAAAAAAAAIA/","byteLength":240}],"bufferViews":[{"buffer":0,"byteLength":48,"target":34963},{"buffer":0,"byteOffset":48,"byteLength":120,"target":34962},{"buffer":1,"byteLength":320,"byteStride":16},{"buffer":2,"byteLength":128},{"buffer":3,"byteLength":240}],"accessors":[{"bufferView":0,"componentType":5123,"count":24,"type":"SCALAR"},{"bufferView":1,"componentType":5126,"count":10,"type":"VEC3","max":[0.5,2.0,0.0],"min":[-0.5,0.0,0.0]},{"bufferView":2,"componentType":5123,"count":10,"type":"VEC4"},{"bufferView":2,"byteOffset":160,"componentType":5126,"count":10,"type":"VEC4"},{"bufferView":3,"componentType":5126,"count":2,"type":"MAT4"},{"bufferView":4,"componentType":5126,"count":12,"type":"SCALAR","max":[5.5],"min":[0.0]},{"bufferView":4,"byteOffset":48,"componentType":5126,"count":12,"type":"VEC4","max":[0.0,0.0,0.707,1.0],"min":[0.0,0.0,-0.707,0.707]}],"asset":{"version":"2.0"}}

File diff suppressed because one or more lines are too long

View file

@ -6,9 +6,9 @@
#include "SMaterialLayer.h" #include "SMaterialLayer.h"
#include "coreutil.h" #include "coreutil.h"
#include "CSkinnedMesh.h" #include "CSkinnedMesh.h"
#include "ISkinnedMesh.h" #include "IAnimatedMesh.h"
#include "irrTypes.h"
#include "IReadFile.h" #include "IReadFile.h"
#include "irrTypes.h"
#include "matrix4.h" #include "matrix4.h"
#include "path.h" #include "path.h"
#include "quaternion.h" #include "quaternion.h"
@ -23,9 +23,11 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <stdexcept> #include <stdexcept>
#include <tuple>
#include <utility> #include <utility>
#include <variant> #include <variant>
#include <vector> #include <vector>
#include <iostream>
namespace irr { namespace irr {
@ -51,6 +53,28 @@ core::vector3df convertHandedness(const core::vector3df &p)
return core::vector3df(p.X, p.Y, -p.Z); return core::vector3df(p.X, p.Y, -p.Z);
} }
template <>
core::quaternion convertHandedness(const core::quaternion &q)
{
return core::quaternion(q.X, q.Y, -q.Z, q.W);
}
template <>
core::matrix4 convertHandedness(const core::matrix4 &mat)
{
// Base transformation between left & right handed coordinate systems.
static const core::matrix4 invertZ = core::matrix4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1);
// Convert from left-handed to right-handed,
// then apply mat,
// then convert from right-handed to left-handed.
// Both conversions just invert Z.
return invertZ * mat * invertZ;
}
namespace scene { namespace scene {
using SelfType = CGLTFMeshFileLoader; using SelfType = CGLTFMeshFileLoader;
@ -196,6 +220,8 @@ ACCESSOR_PRIMITIVE(u16, UNSIGNED_SHORT)
ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT) ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT)
ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT) ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT)
ACCESSOR_TYPES(core::quaternion, VEC4, FLOAT)
ACCESSOR_TYPES(core::matrix4, MAT4, FLOAT)
template <class T> template <class T>
T SelfType::Accessor<T>::get(std::size_t i) const T SelfType::Accessor<T>::get(std::size_t i) const
@ -340,7 +366,7 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file)
auto *mesh = new CSkinnedMesh(); auto *mesh = new CSkinnedMesh();
MeshExtractor parser(std::move(model.value()), mesh); MeshExtractor parser(std::move(model.value()), mesh);
try { try {
parser.loadNodes(); parser.load();
} catch (std::runtime_error &e) { } catch (std::runtime_error &e) {
os::Printer::log("glTF loader", e.what(), ELL_ERROR); os::Printer::log("glTF loader", e.what(), ELL_ERROR);
mesh->drop(); mesh->drop();
@ -397,61 +423,134 @@ static video::E_TEXTURE_CLAMP convertTextureWrap(const Wrap wrap) {
} }
} }
/** void SelfType::MeshExtractor::addPrimitive(
* Load up the rawest form of the model. The vertex positions and indices. const tiniergltf::MeshPrimitive &primitive,
* Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes const std::optional<std::size_t> skinIdx,
* If material is undefined, then a default material MUST be used. CSkinnedMesh::SJoint *parent)
*/
void SelfType::MeshExtractor::loadMesh(
const std::size_t meshIdx,
ISkinnedMesh::SJoint *parent) const
{ {
for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) { auto vertices = getVertices(primitive);
const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi); if (!vertices.has_value())
auto vertices = getVertices(primitive); return; // "When positions are not specified, client implementations SHOULD skip primitives rendering"
if (!vertices.has_value())
continue; // "When positions are not specified, client implementations SHOULD skip primitives rendering"
// Excludes the max value for consistency. const auto n_vertices = vertices->size();
if (vertices->size() >= std::numeric_limits<u16>::max())
throw std::runtime_error("too many vertices");
// Apply the global transform along the parent chain. // Excludes the max value for consistency.
transformVertices(*vertices, parent->GlobalMatrix); if (n_vertices >= std::numeric_limits<u16>::max())
throw std::runtime_error("too many vertices");
auto maybeIndices = getIndices(primitive); // Apply the global transform along the parent chain.
std::vector<u16> indices; transformVertices(*vertices, parent->GlobalMatrix);
if (maybeIndices.has_value()) {
indices = std::move(*maybeIndices);
checkIndices(indices, vertices->size());
} else {
// Non-indexed geometry
indices = generateIndices(vertices->size());
}
m_irr_model->addMeshBuffer( auto maybeIndices = getIndices(primitive);
new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); std::vector<u16> indices;
auto *meshbuf = m_irr_model->getMeshBuffer(m_irr_model->getMeshBufferCount() - 1); if (maybeIndices.has_value()) {
auto &irr_mat = meshbuf->getMaterial(); indices = std::move(*maybeIndices);
checkIndices(indices, vertices->size());
} else {
// Non-indexed geometry
indices = generateIndices(vertices->size());
}
if (primitive.material.has_value()) { m_irr_model->addMeshBuffer(
const auto &material = m_gltf_model.materials->at(*primitive.material); new SSkinMeshBuffer(std::move(*vertices), std::move(indices)));
if (material.pbrMetallicRoughness.has_value()) { const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1;
const auto &texture = material.pbrMetallicRoughness->baseColorTexture; auto *meshbuf = m_irr_model->getMeshBuffer(meshbufNr);
if (texture.has_value()) {
const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1; if (primitive.material.has_value()) {
m_irr_model->setTextureSlot(meshbufNr, static_cast<u32>(texture->index)); const auto &material = m_gltf_model.materials->at(*primitive.material);
const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler; if (material.pbrMetallicRoughness.has_value()) {
if (samplerIdx.has_value()) { const auto &texture = material.pbrMetallicRoughness->baseColorTexture;
auto &sampler = m_gltf_model.samplers->at(*samplerIdx); if (texture.has_value()) {
auto &layer = irr_mat.TextureLayers[0]; m_irr_model->setTextureSlot(meshbufNr, static_cast<u32>(texture->index));
layer.TextureWrapU = convertTextureWrap(sampler.wrapS); const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler;
layer.TextureWrapV = convertTextureWrap(sampler.wrapT); if (samplerIdx.has_value()) {
} auto &sampler = m_gltf_model.samplers->at(*samplerIdx);
auto &layer = meshbuf->getMaterial().TextureLayers[0];
layer.TextureWrapU = convertTextureWrap(sampler.wrapS);
layer.TextureWrapV = convertTextureWrap(sampler.wrapT);
} }
} }
} }
} }
if (!skinIdx.has_value()) {
// No skin => all vertices belong entirely to their parent
for (std::size_t v = 0; v < n_vertices; ++v) {
auto *weight = m_irr_model->addWeight(parent);
weight->buffer_id = meshbufNr;
weight->vertex_id = v;
weight->strength = 1.0f;
}
return;
}
const auto &skin = m_gltf_model.skins->at(*skinIdx);
const auto &attrs = primitive.attributes;
const auto &joints = attrs.joints;
if (!joints.has_value())
return;
const auto &weights = attrs.weights;
for (std::size_t set = 0; set < joints->size(); ++set) {
const auto jointAccessor = ([&]() -> ArrayAccessorVariant<4, u8, u16> {
const auto idx = joints->at(set);
const auto &acc = m_gltf_model.accessors->at(idx);
switch (acc.componentType) {
case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE:
return Accessor<std::array<u8, 4>>::make(m_gltf_model, idx);
case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT:
return Accessor<std::array<u16, 4>>::make(m_gltf_model, idx);
default:
throw std::runtime_error("invalid component type");
}
})();
const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set));
for (std::size_t v = 0; v < n_vertices; ++v) {
std::array<u16, 4> jointIdxs;
if (std::holds_alternative<Accessor<std::array<u8, 4>>>(jointAccessor)) {
const auto jointIdxsU8 = std::get<Accessor<std::array<u8, 4>>>(jointAccessor).get(v);
jointIdxs = {jointIdxsU8[0], jointIdxsU8[1], jointIdxsU8[2], jointIdxsU8[3]};
} else if (std::holds_alternative<Accessor<std::array<u16, 4>>>(jointAccessor)) {
jointIdxs = std::get<Accessor<std::array<u16, 4>>>(jointAccessor).get(v);
}
std::array<f32, 4> strengths = getNormalizedValues(weightAccessor, v);
// 4 joints per set
for (std::size_t in_set = 0; in_set < 4; ++in_set) {
u16 jointIdx = jointIdxs[in_set];
f32 strength = strengths[in_set];
if (strength == 0)
continue;
CSkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx)));
weight->buffer_id = meshbufNr;
weight->vertex_id = v;
weight->strength = strength;
}
}
}
}
/**
* 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::deferAddMesh(
const std::size_t meshIdx,
const std::optional<std::size_t> skinIdx,
CSkinnedMesh::SJoint *parent)
{
m_mesh_loaders.emplace_back([=] {
for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) {
const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi);
addPrimitive(primitive, skinIdx, parent);
}
});
} }
// Base transformation between left & right handed coordinate systems. // Base transformation between left & right handed coordinate systems.
@ -464,51 +563,75 @@ static const core::matrix4 leftToRight = core::matrix4(
); );
static const core::matrix4 rightToLeft = leftToRight; static const core::matrix4 rightToLeft = leftToRight;
static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, CSkinnedMesh::SJoint *joint)
{ {
// Note: Under the hood, this casts these doubles to floats. // Note: Under the hood, this casts these doubles to floats.
return core::matrix4( core::matrix4 mat = convertHandedness(core::matrix4(
m[0], m[1], m[2], m[3], m[0], m[1], m[2], m[3],
m[4], m[5], m[6], m[7], m[4], m[5], m[6], m[7],
m[8], m[9], m[10], m[11], m[8], m[9], m[10], m[11],
m[12], m[13], m[14], m[15]); m[12], m[13], m[14], m[15]));
// Decompose the matrix into translation, scale, and rotation.
joint->Animatedposition = mat.getTranslation();
auto scale = mat.getScale();
joint->Animatedscale = scale;
core::matrix4 inverseScale;
inverseScale.setScale(core::vector3df(
scale.X == 0 ? 0 : 1 / scale.X,
scale.Y == 0 ? 0 : 1 / scale.Y,
scale.Z == 0 ? 0 : 1 / scale.Z));
core::matrix4 axisNormalizedMat = inverseScale * mat;
joint->Animatedrotation = axisNormalizedMat.getRotationDegrees();
// Invert the rotation because it is applied using `getMatrix_transposed`,
// which again inverts.
joint->Animatedrotation.makeInverse();
return mat;
} }
static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, CSkinnedMesh::SJoint *joint)
{ {
const auto &trans = trs.translation; const auto &trans = trs.translation;
const auto &rot = trs.rotation; const auto &rot = trs.rotation;
const auto &scale = trs.scale; const auto &scale = trs.scale;
core::matrix4 transMat; core::matrix4 transMat;
transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2])); joint->Animatedposition = convertHandedness(core::vector3df(trans[0], trans[1], trans[2]));
core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix(); transMat.setTranslation(joint->Animatedposition);
core::matrix4 rotMat;
joint->Animatedrotation = convertHandedness(core::quaternion(rot[0], rot[1], rot[2], rot[3]));
core::quaternion(joint->Animatedrotation).getMatrix_transposed(rotMat);
joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]);
core::matrix4 scaleMat; core::matrix4 scaleMat;
scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); scaleMat.setScale(joint->Animatedscale);
return transMat * rotMat * scaleMat; return transMat * rotMat * scaleMat;
} }
static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform) { static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform,
CSkinnedMesh::SJoint *joint) {
if (!transform.has_value()) { if (!transform.has_value()) {
return core::matrix4(); return core::matrix4();
} }
core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); return std::visit([joint](const auto &t) { return loadTransform(t, joint); }, *transform);
return rightToLeft * mat * leftToRight;
} }
void SelfType::MeshExtractor::loadNode( void SelfType::MeshExtractor::loadNode(
const std::size_t nodeIdx, const std::size_t nodeIdx,
ISkinnedMesh::SJoint *parent) const CSkinnedMesh::SJoint *parent)
{ {
const auto &node = m_gltf_model.nodes->at(nodeIdx); const auto &node = m_gltf_model.nodes->at(nodeIdx);
auto *joint = m_irr_model->addJoint(parent); auto *joint = m_irr_model->addJoint(parent);
const core::matrix4 transform = loadTransform(node.transform); const core::matrix4 transform = loadTransform(node.transform, joint);
joint->LocalMatrix = transform; joint->LocalMatrix = transform;
joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix;
if (node.name.has_value()) { if (node.name.has_value()) {
joint->Name = node.name->c_str(); joint->Name = node.name->c_str();
} }
m_loaded_nodes[nodeIdx] = joint;
if (node.mesh.has_value()) { if (node.mesh.has_value()) {
loadMesh(*node.mesh, joint); deferAddMesh(*node.mesh, node.skin, joint);
} }
if (node.children.has_value()) { if (node.children.has_value()) {
for (const auto &child : *node.children) { for (const auto &child : *node.children) {
@ -517,8 +640,10 @@ void SelfType::MeshExtractor::loadNode(
} }
} }
void SelfType::MeshExtractor::loadNodes() const void SelfType::MeshExtractor::loadNodes()
{ {
m_loaded_nodes = std::vector<CSkinnedMesh::SJoint *>(m_gltf_model.nodes->size());
std::vector<bool> isChild(m_gltf_model.nodes->size()); std::vector<bool> isChild(m_gltf_model.nodes->size());
for (const auto &node : *m_gltf_model.nodes) { for (const auto &node : *m_gltf_model.nodes) {
if (!node.children.has_value()) if (!node.children.has_value())
@ -536,6 +661,92 @@ void SelfType::MeshExtractor::loadNodes() const
} }
} }
void SelfType::MeshExtractor::loadSkins()
{
if (!m_gltf_model.skins.has_value())
return;
for (const auto &skin : *m_gltf_model.skins) {
if (!skin.inverseBindMatrices.has_value())
continue;
const auto accessor = Accessor<core::matrix4>::make(m_gltf_model, *skin.inverseBindMatrices);
if (accessor.getCount() < skin.joints.size())
throw std::runtime_error("accessor contains too few matrices");
for (std::size_t i = 0; i < skin.joints.size(); ++i) {
m_loaded_nodes.at(skin.joints[i])->GlobalInversedMatrix = convertHandedness(accessor.get(i));
}
}
}
void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx)
{
const auto &anim = m_gltf_model.animations->at(animIdx);
for (const auto &channel : anim.channels) {
const auto &sampler = anim.samplers.at(channel.sampler);
if (sampler.interpolation != tiniergltf::AnimationSampler::Interpolation::LINEAR)
throw std::runtime_error("unsupported interpolation");
const auto inputAccessor = Accessor<f32>::make(m_gltf_model, sampler.input);
const auto n_frames = inputAccessor.getCount();
if (!channel.target.node.has_value())
throw std::runtime_error("no animated node");
const auto &joint = m_loaded_nodes.at(*channel.target.node);
switch (channel.target.path) {
case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addPositionKey(joint);
key->frame = inputAccessor.get(i);
key->position = convertHandedness(outputAccessor.get(i));
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::ROTATION: {
const auto outputAccessor = Accessor<core::quaternion>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addRotationKey(joint);
key->frame = inputAccessor.get(i);
key->rotation = convertHandedness(outputAccessor.get(i));
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::SCALE: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);
for (std::size_t i = 0; i < n_frames; ++i) {
auto *key = m_irr_model->addScaleKey(joint);
key->frame = inputAccessor.get(i);
key->scale = outputAccessor.get(i);
}
break;
}
case tiniergltf::AnimationChannelTarget::Path::WEIGHTS:
throw std::runtime_error("no support for morph animations");
}
}
}
void SelfType::MeshExtractor::load()
{
loadNodes();
for (const auto &load_mesh : m_mesh_loaders) {
load_mesh();
}
loadSkins();
// Load the first animation, if there is one.
if (m_gltf_model.animations.has_value()) {
if (m_gltf_model.animations->size() > 1) {
os::Printer::log("glTF loader",
"multiple animations are not supported", ELL_WARNING);
}
loadAnimation(0);
m_irr_model->setAnimationSpeed(1);
}
m_irr_model->finalize();
}
/** /**
* Extracts GLTF mesh indices. * Extracts GLTF mesh indices.
*/ */
@ -722,4 +933,3 @@ std::optional<tiniergltf::GlTF> SelfType::tryParseGLTF(io::IReadFile* file)
} // namespace scene } // namespace scene
} // namespace irr } // namespace irr

View file

@ -10,9 +10,11 @@
#include "path.h" #include "path.h"
#include "S3DVertex.h" #include "S3DVertex.h"
#include <tiniergltf.hpp> #include "tiniergltf.hpp"
#include <functional>
#include <cstddef> #include <cstddef>
#include <tuple>
#include <vector> #include <vector>
namespace irr namespace irr
@ -26,9 +28,9 @@ class CGLTFMeshFileLoader : public IMeshLoader
public: public:
CGLTFMeshFileLoader() noexcept {}; CGLTFMeshFileLoader() noexcept {};
bool isALoadableFileExtension(const io::path& filename) const override; bool isALoadableFileExtension(const io::path &filename) const override;
IAnimatedMesh* createMesh(io::IReadFile* file) override; IAnimatedMesh *createMesh(io::IReadFile *file) override;
private: private:
template <typename T> template <typename T>
@ -94,7 +96,8 @@ private:
const NormalizedValuesAccessor<N> &accessor, const NormalizedValuesAccessor<N> &accessor,
const std::size_t i); const std::size_t i);
class MeshExtractor { class MeshExtractor
{
public: public:
MeshExtractor(tiniergltf::GlTF &&model, MeshExtractor(tiniergltf::GlTF &&model,
CSkinnedMesh *mesh) noexcept CSkinnedMesh *mesh) noexcept
@ -114,12 +117,15 @@ private:
std::size_t getPrimitiveCount(const std::size_t meshIdx) const; std::size_t getPrimitiveCount(const std::size_t meshIdx) const;
void loadNodes() const; void load();
private: private:
const tiniergltf::GlTF m_gltf_model; const tiniergltf::GlTF m_gltf_model;
CSkinnedMesh *m_irr_model; CSkinnedMesh *m_irr_model;
std::vector<std::function<void()>> m_mesh_loaders;
std::vector<CSkinnedMesh::SJoint *> m_loaded_nodes;
void copyPositions(const std::size_t accessorIdx, void copyPositions(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const; std::vector<video::S3DVertex>& vertices) const;
@ -129,16 +135,24 @@ private:
void copyTCoords(const std::size_t accessorIdx, void copyTCoords(const std::size_t accessorIdx,
std::vector<video::S3DVertex>& vertices) const; std::vector<video::S3DVertex>& vertices) const;
void loadMesh( void addPrimitive(const tiniergltf::MeshPrimitive &primitive,
std::size_t meshIdx, const std::optional<std::size_t> skinIdx,
ISkinnedMesh::SJoint *parentJoint) const; CSkinnedMesh::SJoint *parent);
void loadNode( void deferAddMesh(const std::size_t meshIdx,
const std::size_t nodeIdx, const std::optional<std::size_t> skinIdx,
ISkinnedMesh::SJoint *parentJoint) const; CSkinnedMesh::SJoint *parentJoint);
void loadNode(const std::size_t nodeIdx, CSkinnedMesh::SJoint *parentJoint);
void loadNodes();
void loadSkins();
void loadAnimation(const std::size_t animIdx);
}; };
std::optional<tiniergltf::GlTF> tryParseGLTF(io::IReadFile* file); std::optional<tiniergltf::GlTF> tryParseGLTF(io::IReadFile *file);
}; };
} // namespace scene } // namespace scene

View file

@ -8,6 +8,7 @@
#include "irr_v2d.h" #include "irr_v2d.h"
#include "irr_ptr.h" #include "irr_ptr.h"
#include "ISkinnedMesh.h"
#include <irrlicht.h> #include <irrlicht.h>
#include "catch.h" #include "catch.h"
@ -371,7 +372,91 @@ SECTION("simple sparse accessor")
CHECK(vertices[i].Pos == expectedPositions[i]); CHECK(vertices[i].Pos == expectedPositions[i]);
} }
// https://github.com/KhronosGroup/glTF-Sample-Models/tree/main/2.0/SimpleSkin
SECTION("simple skin")
{
using ISkinnedMesh = irr::scene::ISkinnedMesh;
const auto mesh = loadMesh(model_stem + "simple_skin.gltf");
REQUIRE(mesh != nullptr);
auto csm = dynamic_cast<const ISkinnedMesh*>(mesh);
const auto joints = csm->getAllJoints();
REQUIRE(joints.size() == 3);
const auto findJoint = [&](const std::function<bool(ISkinnedMesh::SJoint*)> &predicate) {
for (std::size_t i = 0; i < joints.size(); ++i) {
if (predicate(joints[i])) {
return joints[i];
}
}
throw std::runtime_error("joint not found");
};
// Check the node hierarchy
const auto parent = findJoint([](auto joint) {
return !joint->Children.empty();
});
REQUIRE(parent->Children.size() == 1);
const auto child = parent->Children[0];
REQUIRE(child != parent);
SECTION("transformations are correct")
{
CHECK(parent->Animatedposition == v3f(0, 0, 0));
CHECK(parent->Animatedrotation == irr::core::quaternion());
CHECK(parent->Animatedscale == v3f(1, 1, 1));
CHECK(parent->GlobalInversedMatrix == irr::core::matrix4());
const v3f childTranslation(0, 1, 0);
CHECK(child->Animatedposition == childTranslation);
CHECK(child->Animatedrotation == irr::core::quaternion());
CHECK(child->Animatedscale == v3f(1, 1, 1));
irr::core::matrix4 inverseBindMatrix;
inverseBindMatrix.setInverseTranslation(childTranslation);
CHECK(child->GlobalInversedMatrix == inverseBindMatrix);
}
SECTION("weights are correct")
{
const auto weights = [&](const ISkinnedMesh::SJoint *joint) {
std::unordered_map<irr::u32, irr::f32> weights;
for (std::size_t i = 0; i < joint->Weights.size(); ++i) {
const auto weight = joint->Weights[i];
REQUIRE(weight.buffer_id == 0);
weights[weight.vertex_id] = weight.strength;
}
return weights;
};
const auto parentWeights = weights(parent);
const auto childWeights = weights(child);
const auto checkWeights = [&](irr::u32 index, irr::f32 parentWeight, irr::f32 childWeight) {
const auto getWeight = [](auto weights, auto index) {
const auto it = weights.find(index);
return it == weights.end() ? 0.0f : it->second;
};
CHECK(getWeight(parentWeights, index) == parentWeight);
CHECK(getWeight(childWeights, index) == childWeight);
};
checkWeights(0, 1.00, 0.00);
checkWeights(1, 1.00, 0.00);
checkWeights(2, 0.75, 0.25);
checkWeights(3, 0.75, 0.25);
checkWeights(4, 0.50, 0.50);
checkWeights(5, 0.50, 0.50);
checkWeights(6, 0.25, 0.75);
checkWeights(7, 0.25, 0.75);
checkWeights(8, 0.00, 1.00);
checkWeights(9, 0.00, 1.00);
}
SECTION("there should be a third node not involved in skinning")
{
const auto other = findJoint([&](auto joint) {
return joint != child && joint != parent;
});
CHECK(other->Weights.empty());
}
}
driver->closeDevice(); driver->closeDevice();
driver->drop(); driver->drop();
} }