mirror of
https://github.com/minetest/minetest.git
synced 2025-03-06 20:48:40 +01:00
Add glTF animation support
This commit is contained in:
parent
d8274af670
commit
323fc0a798
9 changed files with 421 additions and 79 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -3,3 +3,5 @@
|
||||||
|
|
||||||
*.cpp diff=cpp
|
*.cpp diff=cpp
|
||||||
*.h diff=cpp
|
*.h diff=cpp
|
||||||
|
|
||||||
|
*.gltf binary
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
1
games/devtest/mods/gltf/models/gltf_simple_skin.gltf
Normal file
1
games/devtest/mods/gltf/models/gltf_simple_skin.gltf
Normal 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"}}
|
1
games/devtest/mods/gltf/models/gltf_spider_animated.gltf
Normal file
1
games/devtest/mods/gltf/models/gltf_spider_animated.gltf
Normal file
File diff suppressed because one or more lines are too long
|
@ -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 primitive’s rendering"
|
||||||
if (!vertices.has_value())
|
|
||||||
continue; // "When positions are not specified, client implementations SHOULD skip primitive’s 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue