mirror of
https://github.com/minetest/minetest.git
synced 2024-12-27 03:00:34 +01:00
Allow non-normalized weights in glTF models (#15310)
We are being lax here, but the glTF specification just requires that "when the weights are stored using float component type, their linear sum SHOULD be as close as reasonably possible to 1.0 for a given vertex" In particular weights > 1 and weight sums well below or above 1 can be observed in models exported by Blender if they aren't manually normalized. These fail the glTF validator but Irrlicht normalizes weights itself so we can support them just fine. The docs have been updated to recommend normalizing weights (as well as documenting the status of interpolation support). Weights < 0, most of them close to 0, also occur. Consistent with Irrlicht, we ignore them, but we also raise a warning.
This commit is contained in:
parent
3e10d9ccf5
commit
05d31222f7
@ -309,17 +309,24 @@ it unlocks no special rendering features.
|
|||||||
Binary glTF (`.glb`) files are supported and recommended over `.gltf` files
|
Binary glTF (`.glb`) files are supported and recommended over `.gltf` files
|
||||||
due to their space savings.
|
due to their space savings.
|
||||||
|
|
||||||
This means that many glTF features are not supported *yet*, including:
|
Bone weights should be normalized, e.g. using ["normalize all" in Blender](https://docs.blender.org/manual/en/4.2/grease_pencil/modes/weight_paint/weights_menu.html#normalize-all).
|
||||||
|
|
||||||
|
You can use the [Khronos glTF validator](https://github.com/KhronosGroup/glTF-Validator)
|
||||||
|
to check whether a model is a valid glTF file.
|
||||||
|
|
||||||
|
Many glTF features are not supported *yet*, including:
|
||||||
|
|
||||||
* Animations
|
* Animations
|
||||||
* Only a single animation is supported, use frame ranges within this animation.
|
* Only a single animation is supported, use frame ranges within this animation.
|
||||||
|
* Only linear interpolation is supported.
|
||||||
* Cameras
|
* Cameras
|
||||||
* Materials
|
* Materials
|
||||||
* Only base color textures are supported
|
* Only base color textures are supported
|
||||||
* Backface culling is overridden
|
* Backface culling is overridden
|
||||||
* Double-sided materials don't work
|
* Double-sided materials don't work
|
||||||
* Alternative means of supplying data
|
* Alternative means of supplying data
|
||||||
* Embedded images
|
* Embedded images. You can use `gltfutil.py` from the
|
||||||
|
[modding tools](https://github.com/minetest/modtools) to strip or extract embedded images.
|
||||||
* References to files via URIs
|
* References to files via URIs
|
||||||
|
|
||||||
Textures are supplied solely via the same means as for the other model file formats:
|
Textures are supplied solely via the same means as for the other model file formats:
|
||||||
|
@ -305,7 +305,7 @@ SelfType::createNormalizedValuesAccessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
template <std::size_t N>
|
template <std::size_t N, bool validate>
|
||||||
std::array<f32, N> SelfType::getNormalizedValues(
|
std::array<f32, N> SelfType::getNormalizedValues(
|
||||||
const NormalizedValuesAccessor<N> &accessor,
|
const NormalizedValuesAccessor<N> &accessor,
|
||||||
const std::size_t i)
|
const std::size_t i)
|
||||||
@ -313,17 +313,19 @@ std::array<f32, N> SelfType::getNormalizedValues(
|
|||||||
std::array<f32, N> values;
|
std::array<f32, N> values;
|
||||||
if (std::holds_alternative<Accessor<std::array<u8, N>>>(accessor)) {
|
if (std::holds_alternative<Accessor<std::array<u8, N>>>(accessor)) {
|
||||||
const auto u8s = std::get<Accessor<std::array<u8, N>>>(accessor).get(i);
|
const auto u8s = std::get<Accessor<std::array<u8, N>>>(accessor).get(i);
|
||||||
for (u8 i = 0; i < N; ++i)
|
for (std::size_t j = 0; j < N; ++j)
|
||||||
values[i] = static_cast<f32>(u8s[i]) / std::numeric_limits<u8>::max();
|
values[j] = static_cast<f32>(u8s[j]) / std::numeric_limits<u8>::max();
|
||||||
} else if (std::holds_alternative<Accessor<std::array<u16, N>>>(accessor)) {
|
} else if (std::holds_alternative<Accessor<std::array<u16, N>>>(accessor)) {
|
||||||
const auto u16s = std::get<Accessor<std::array<u16, N>>>(accessor).get(i);
|
const auto u16s = std::get<Accessor<std::array<u16, N>>>(accessor).get(i);
|
||||||
for (u8 i = 0; i < N; ++i)
|
for (std::size_t j = 0; j < N; ++j)
|
||||||
values[i] = static_cast<f32>(u16s[i]) / std::numeric_limits<u16>::max();
|
values[j] = static_cast<f32>(u16s[j]) / std::numeric_limits<u16>::max();
|
||||||
} else {
|
} else {
|
||||||
values = std::get<Accessor<std::array<f32, N>>>(accessor).get(i);
|
values = std::get<Accessor<std::array<f32, N>>>(accessor).get(i);
|
||||||
for (u8 i = 0; i < N; ++i) {
|
if constexpr (validate) {
|
||||||
if (values[i] < 0 || values[i] > 1)
|
for (std::size_t j = 0; j < N; ++j) {
|
||||||
throw std::runtime_error("invalid normalized value");
|
if (values[j] < 0 || values[j] > 1)
|
||||||
|
throw std::runtime_error("invalid normalized value");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
@ -493,6 +495,7 @@ void SelfType::MeshExtractor::addPrimitive(
|
|||||||
|
|
||||||
const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set));
|
const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set));
|
||||||
|
|
||||||
|
bool negative_weights = false;
|
||||||
for (std::size_t v = 0; v < n_vertices; ++v) {
|
for (std::size_t v = 0; v < n_vertices; ++v) {
|
||||||
std::array<u16, 4> jointIdxs;
|
std::array<u16, 4> jointIdxs;
|
||||||
if (std::holds_alternative<Accessor<std::array<u8, 4>>>(jointAccessor)) {
|
if (std::holds_alternative<Accessor<std::array<u8, 4>>>(jointAccessor)) {
|
||||||
@ -501,14 +504,18 @@ void SelfType::MeshExtractor::addPrimitive(
|
|||||||
} else if (std::holds_alternative<Accessor<std::array<u16, 4>>>(jointAccessor)) {
|
} else if (std::holds_alternative<Accessor<std::array<u16, 4>>>(jointAccessor)) {
|
||||||
jointIdxs = std::get<Accessor<std::array<u16, 4>>>(jointAccessor).get(v);
|
jointIdxs = std::get<Accessor<std::array<u16, 4>>>(jointAccessor).get(v);
|
||||||
}
|
}
|
||||||
std::array<f32, 4> strengths = getNormalizedValues(weightAccessor, v);
|
|
||||||
|
// Be lax: We can allow weights that aren't normalized. Irrlicht already normalizes them.
|
||||||
|
// The glTF spec only requires that these be "as close to 1 as reasonably possible".
|
||||||
|
auto strengths = getNormalizedValues<4, false>(weightAccessor, v);
|
||||||
|
|
||||||
// 4 joints per set
|
// 4 joints per set
|
||||||
for (std::size_t in_set = 0; in_set < 4; ++in_set) {
|
for (std::size_t in_set = 0; in_set < 4; ++in_set) {
|
||||||
u16 jointIdx = jointIdxs[in_set];
|
u16 jointIdx = jointIdxs[in_set];
|
||||||
f32 strength = strengths[in_set];
|
f32 strength = strengths[in_set];
|
||||||
if (strength == 0)
|
negative_weights = negative_weights || (strength < 0);
|
||||||
continue;
|
if (strength <= 0)
|
||||||
|
continue; // note: also ignores negative weights
|
||||||
|
|
||||||
SkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx)));
|
SkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx)));
|
||||||
weight->buffer_id = meshbufNr;
|
weight->buffer_id = meshbufNr;
|
||||||
@ -516,6 +523,8 @@ void SelfType::MeshExtractor::addPrimitive(
|
|||||||
weight->strength = strength;
|
weight->strength = strength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (negative_weights)
|
||||||
|
warn("negative weights");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ private:
|
|||||||
const tiniergltf::GlTF &model,
|
const tiniergltf::GlTF &model,
|
||||||
const std::size_t accessorIdx);
|
const std::size_t accessorIdx);
|
||||||
|
|
||||||
template <std::size_t N>
|
template <std::size_t N, bool validate = true>
|
||||||
static std::array<f32, N> getNormalizedValues(
|
static std::array<f32, N> getNormalizedValues(
|
||||||
const NormalizedValuesAccessor<N> &accessor,
|
const NormalizedValuesAccessor<N> &accessor,
|
||||||
const std::size_t i);
|
const std::size_t i);
|
||||||
@ -118,7 +118,7 @@ private:
|
|||||||
std::size_t getPrimitiveCount(const std::size_t meshIdx) const;
|
std::size_t getPrimitiveCount(const std::size_t meshIdx) const;
|
||||||
|
|
||||||
void load();
|
void load();
|
||||||
const std::vector<std::string> &getWarnings() {
|
const std::unordered_set<std::string> &getWarnings() {
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,9 +129,9 @@ private:
|
|||||||
std::vector<std::function<void()>> m_mesh_loaders;
|
std::vector<std::function<void()>> m_mesh_loaders;
|
||||||
std::vector<SkinnedMesh::SJoint *> m_loaded_nodes;
|
std::vector<SkinnedMesh::SJoint *> m_loaded_nodes;
|
||||||
|
|
||||||
std::vector<std::string> warnings;
|
std::unordered_set<std::string> warnings;
|
||||||
void warn(const std::string &warning) {
|
void warn(const std::string &warning) {
|
||||||
warnings.push_back(warning);
|
warnings.insert(warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
void copyPositions(const std::size_t accessorIdx,
|
void copyPositions(const std::size_t accessorIdx,
|
||||||
|
Loading…
Reference in New Issue
Block a user