Skip to content

Commit

Permalink
TinyGltfImporter: renormalize animation and transformation quaternions.
Browse files Browse the repository at this point in the history
As with the shortest-path patching it's controlled by a runtime option,
so it's possible to disable it when not desired.

Now I am *finally* able to load that effing AnimatedTriangle example.
Ugh, along with shortest-path quaternion interpolation this took me a
week.
  • Loading branch information
mosra committed Sep 9, 2018
1 parent 53c4c9d commit 0ce8456
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/MagnumPlugins/TinyGltfImporter/Test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ corrade_add_test(TinyGltfImporterTest
scene-nodefault.glb
object-transformation.gltf
object-transformation.glb
object-transformation-patching.gltf
texture.gltf
texture.glb
texture.png
Expand Down
107 changes: 104 additions & 3 deletions src/MagnumPlugins/TinyGltfImporter/Test/TinyGltfImporterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ struct TinyGltfImporterTest: TestSuite::Tester {
void animationWrongRotationType();
void animationWrongScalingType();
void animationUnsupportedPath();

void animationShortestPathOptimizationEnabled();
void animationShortestPathOptimizationDisabled();
void animationQuaternionNormalizationEnabled();
void animationQuaternionNormalizationDisabled();

void camera();

Expand All @@ -78,6 +81,9 @@ struct TinyGltfImporterTest: TestSuite::Tester {
void sceneNoDefault();
void objectTransformation();

void objectTransformationQuaternionNormalizationEnabled();
void objectTransformationQuaternionNormalizationDisabled();

void mesh();
void meshIndexed();
void meshUnknownAttribute();
Expand Down Expand Up @@ -170,7 +176,9 @@ TinyGltfImporterTest::TinyGltfImporterTest() {
&TinyGltfImporterTest::animationUnsupportedPath,

&TinyGltfImporterTest::animationShortestPathOptimizationEnabled,
&TinyGltfImporterTest::animationShortestPathOptimizationDisabled});
&TinyGltfImporterTest::animationShortestPathOptimizationDisabled,
&TinyGltfImporterTest::animationQuaternionNormalizationEnabled,
&TinyGltfImporterTest::animationQuaternionNormalizationDisabled});

addInstancedTests({&TinyGltfImporterTest::camera,

Expand All @@ -182,6 +190,9 @@ TinyGltfImporterTest::TinyGltfImporterTest() {
&TinyGltfImporterTest::objectTransformation},
Containers::arraySize(SingleFileData));

addTests({&TinyGltfImporterTest::objectTransformationQuaternionNormalizationEnabled,
&TinyGltfImporterTest::objectTransformationQuaternionNormalizationDisabled});

addInstancedTests({&TinyGltfImporterTest::mesh,
&TinyGltfImporterTest::meshIndexed,
&TinyGltfImporterTest::meshUnknownAttribute,
Expand Down Expand Up @@ -449,7 +460,7 @@ void TinyGltfImporterTest::animationShortestPathOptimizationEnabled() {
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"animation-patching.gltf")));

CORRADE_COMPARE(importer->animationCount(), 1);
CORRADE_COMPARE(importer->animationCount(), 2);
CORRADE_COMPARE(importer->animationName(0), "Quaternion shortest-path patching");

auto animation = importer->animation(0);
Expand Down Expand Up @@ -498,7 +509,7 @@ void TinyGltfImporterTest::animationShortestPathOptimizationDisabled() {
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"animation-patching.gltf")));

CORRADE_COMPARE(importer->animationCount(), 1);
CORRADE_COMPARE(importer->animationCount(), 2);
CORRADE_COMPARE(importer->animationName(0), "Quaternion shortest-path patching");

auto animation = importer->animation(0);
Expand Down Expand Up @@ -561,6 +572,58 @@ void TinyGltfImporterTest::animationShortestPathOptimizationDisabled() {
CORRADE_COMPARE(track.at(Math::slerp, 7.5f).angle(), 337.5_degf);
}

void TinyGltfImporterTest::animationQuaternionNormalizationEnabled() {
std::unique_ptr<AbstractImporter> importer = _manager.instantiate("TinyGltfImporter");
/* Enabled by default */
CORRADE_VERIFY(importer->configuration().value<bool>("normalizeQuaternions"));
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"animation-patching.gltf")));
CORRADE_COMPARE(importer->animationCount(), 2);
CORRADE_COMPARE(importer->animationName(1), "Quaternion normalization patching");

Containers::Optional<Trade::AnimationData> animation;
std::ostringstream out;
{
Warning warningRedirection{&out};
animation = importer->animation(1);
}
CORRADE_VERIFY(animation);
CORRADE_COMPARE(out.str(), "Trade::TinyGltfImporter::animation(): quaternions in some rotation tracks were renormalized\n");
CORRADE_COMPARE(animation->trackCount(), 1);
CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion);

Animation::TrackView<Float, Quaternion> track = animation->track<Quaternion>(0);
const Quaternion rotationValues[]{
{{0.0f, 0.0f, 0.382683f}, 0.92388f}, // is normalized
{{0.0f, 0.0f, 0.707107f}, 0.707107f}, // is not, renormalized
{{0.0f, 0.0f, 0.382683f}, 0.92388f}, // is not, renormalized
};
CORRADE_COMPARE_AS(track.values(), (Containers::StridedArrayView<const Quaternion>{rotationValues}), TestSuite::Compare::Container);
}

void TinyGltfImporterTest::animationQuaternionNormalizationDisabled() {
std::unique_ptr<AbstractImporter> importer = _manager.instantiate("TinyGltfImporter");
/* Explicitly disable */
CORRADE_VERIFY(importer->configuration().setValue("normalizeQuaternions", false));
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"animation-patching.gltf")));
CORRADE_COMPARE(importer->animationCount(), 2);
CORRADE_COMPARE(importer->animationName(1), "Quaternion normalization patching");

auto animation = importer->animation(1);
CORRADE_VERIFY(animation);
CORRADE_COMPARE(animation->trackCount(), 1);
CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion);

Animation::TrackView<Float, Quaternion> track = animation->track<Quaternion>(0);
const Quaternion rotationValues[]{
Quaternion{{0.0f, 0.0f, 0.382683f}, 0.92388f}, // is normalized
Quaternion{{0.0f, 0.0f, 0.707107f}, 0.707107f}*2, // is not
Quaternion{{0.0f, 0.0f, 0.382683f}, 0.92388f}*2, // is not
};
CORRADE_COMPARE_AS(track.values(), (Containers::StridedArrayView<const Quaternion>{rotationValues}), TestSuite::Compare::Container);
}

void TinyGltfImporterTest::camera() {
auto&& data = SingleFileData[testCaseInstanceId()];
setTestCaseDescription(data.name);
Expand Down Expand Up @@ -859,6 +922,44 @@ void TinyGltfImporterTest::objectTransformation() {
}
}

void TinyGltfImporterTest::objectTransformationQuaternionNormalizationEnabled() {
std::unique_ptr<AbstractImporter> importer = _manager.instantiate("TinyGltfImporter");
/* Enabled by default */
CORRADE_VERIFY(importer->configuration().value<bool>("normalizeQuaternions"));
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"object-transformation-patching.gltf")));

CORRADE_COMPARE(importer->object3DCount(), 1);
CORRADE_COMPARE(importer->object3DName(0), "Non-normalized rotation");

std::unique_ptr<Trade::ObjectData3D> object;
std::ostringstream out;
{
Warning warningRedirection{&out};
object = importer->object3D(0);
}
CORRADE_VERIFY(object);
CORRADE_COMPARE(out.str(), "Trade::TinyGltfImporter::object3D(): rotation quaternion was renormalized\n");
CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling);
CORRADE_COMPARE(object->rotation(), Quaternion::rotation(45.0_degf, Vector3::yAxis()));
}

void TinyGltfImporterTest::objectTransformationQuaternionNormalizationDisabled() {
std::unique_ptr<AbstractImporter> importer = _manager.instantiate("TinyGltfImporter");
/* Explicity disable */
importer->configuration().setValue("normalizeQuaternions", false);
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"object-transformation-patching.gltf")));

CORRADE_COMPARE(importer->object3DCount(), 1);
CORRADE_COMPARE(importer->object3DName(0), "Non-normalized rotation");

auto object = importer->object3D(0);
CORRADE_VERIFY(object);
CORRADE_COMPARE(object->flags(), ObjectFlag3D::HasTranslationRotationScaling);
CORRADE_COMPARE(object->rotation(), Quaternion::rotation(45.0_degf, Vector3::yAxis())*2.0f);
}

void TinyGltfImporterTest::mesh() {
auto&& data = MultiFileData[testCaseInstanceId()];
setTestCaseDescription(data.name);
Expand Down
Binary file modified src/MagnumPlugins/TinyGltfImporter/Test/animation-patching.bin
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
type = "<9f 36f"
type = "<9f 36f 12f"
input = [
# time
# time shared by everything
0, 1, 2, 3, 4, 5, 6, 7, 8,

# rotation around Z
# quaternion shortest-path optimization, rotation around Z
0, 0, 0.92388, -0.382683, # 225°
0, 0, 0.707107, -0.707107, # 270°
0, 0, 0.382683, -0.92388, # 315°
Expand All @@ -13,4 +13,9 @@ input = [
0, 0, -0.92388, -0.382683, # 135°
0, 0, -1, 0, # 180°
0, 0, 0.92388, -0.382683, # 225°

# quaternion normalization
0, 0, 0.382683, 0.92388, # 45°
0, 0, 0.707107*2, 0.707107*2, # 90°, denormalized
0, 0, 0.382683*2, 0.92388*2, # 45°, denormalized
]
30 changes: 28 additions & 2 deletions src/MagnumPlugins/TinyGltfImporter/Test/animation-patching.gltf
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@
"output": 1
}
]
},
{
"name": "Quaternion normalization patching",
"channels": [
{
"sampler": 0,
"target": {
"node": 1337,
"path": "rotation"
}
}
],
"samplers": [
{
"input": 0,
"interpolation": "LINEAR",
"output": 2
}
]
}
],
"accessors": [
Expand All @@ -37,6 +56,13 @@
"componentType": 5126,
"count": 9,
"type": "VEC4"
},
{
"bufferView": 1,
"byteOffset": 144,
"componentType": 5126,
"count": 3,
"type": "VEC4"
}
],
"bufferViews": [
Expand All @@ -48,12 +74,12 @@
{
"buffer": 0,
"byteOffset": 36,
"byteLength": 140
"byteLength": 188
}
],
"buffers": [
{
"byteLength": 180,
"byteLength": 228,
"uri": "animation-patching.bin"
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"asset": {
"version": "2.0"
},
"nodes": [
{
"name": "Non-normalized rotation",
"rotation": [
0.0,
0.765366,
0.0,
1.84776
]
}
]
}
9 changes: 8 additions & 1 deletion src/MagnumPlugins/TinyGltfImporter/TinyGltfImporter.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ provides=GlbImporter
[configuration]

# Optimize imported linearly-interpolated quaternion animation tracks to
# ensure shortest path is always chosen
# ensure shortest path is always chosen. This can be controlled separately for
# each animation import.
optimizeQuaternionShortestPath=true

# Normalize transformation quaternions and linearly-interpolated quaternion
# animation tracks, if they are not already. Note that spline-interpolated
# quaternion animation tracks are not patched. This can be controlled
# separately for each object/animation import.
normalizeQuaternions=true
# [config]
22 changes: 21 additions & 1 deletion src/MagnumPlugins/TinyGltfImporter/TinyGltfImporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ namespace {
void fillDefaultConfiguration(Utility::ConfigurationGroup& conf) {
/** @todo horrible workaround, fix this properly */
conf.setValue("optimizeQuaternionShortestPath", true);
conf.setValue("normalizeQuaternions", true);
}

}
Expand Down Expand Up @@ -288,6 +289,7 @@ Containers::Optional<AnimationData> TinyGltfImporter::doAnimation(UnsignedInt id
}

/* Import all tracks */
bool hadToRenormalize = false;
Containers::Array<Trade::AnimationTrackData> tracks{animation.channels.size()};
for(std::size_t i = 0; i != animation.channels.size(); ++i) {
const tinygltf::AnimationChannel& channel = animation.channels[i];
Expand Down Expand Up @@ -362,6 +364,16 @@ Containers::Optional<AnimationData> TinyGltfImporter::doAnimation(UnsignedInt id
}
}

/* Normalize the quaternions if not already. Don't attempt to
normalize every time to avoid tiny differences, only when the
quaternion looks to be off. */
if(configuration().value<bool>("normalizeQuaternions")) {
for(auto& i: values) if(!i.isNormalized()) {
i = i.normalized();
hadToRenormalize = true;
}
}

/* Populate track metadata */
type = AnimationTrackType::Quaternion;
target = AnimationTrackTarget::Rotation3D;
Expand Down Expand Up @@ -392,6 +404,9 @@ Containers::Optional<AnimationData> TinyGltfImporter::doAnimation(UnsignedInt id
tracks[i] = AnimationTrackData{type, target, UnsignedInt(channel.target_node), track};
}

if(hadToRenormalize)
Warning{} << "Trade::TinyGltfImporter::animation(): quaternions in some rotation tracks were renormalized";

return AnimationData{std::move(data), std::move(tracks), &animation};
}

Expand Down Expand Up @@ -560,8 +575,13 @@ std::unique_ptr<ObjectData3D> TinyGltfImporter::doObject3D(UnsignedInt id) {
flags |= ObjectFlag3D::HasTranslationRotationScaling;
if(node.translation.size() == 3)
translation = Vector3{Vector3d::from(node.translation.data())};
if(node.rotation.size() == 4)
if(node.rotation.size() == 4) {
rotation = Quaternion{Vector3{Vector3d::from(node.rotation.data())}, Float(node.rotation[3])};
if(!rotation.isNormalized() && configuration().value<bool>("normalizeQuaternions")) {
rotation = rotation.normalized();
Warning{} << "Trade::TinyGltfImporter::object3D(): rotation quaternion was renormalized";
}
}
if(node.scale.size() == 3)
scaling = Vector3{Vector3d::from(node.scale.data())};
}
Expand Down
8 changes: 8 additions & 0 deletions src/MagnumPlugins/TinyGltfImporter/TinyGltfImporter.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ Import of skeleton, skin and morph data is not supported at the moment.
- At the moment, only constant and linear animation interpolation can be
imported
- If linear quaternion rotation tracks are not normalized, the importer
prints a warning and normalizes them. Can be disabled per-animation with
the @cb{.ini} normalizeQuaternions @ce option, see
@ref Trade-TinyGltfImporter-configuration "below".
- Skinning and morph targets are not supported
- Animation tracks are always imported with
@ref Animation::Extrapolation::Constant, because glTF doesn't support
Expand All @@ -107,6 +111,10 @@ Import of skeleton, skin and morph data is not supported at the moment.
- In case object transformation is set via separate
translation/rotation/scaling properties in the source file,
@ref ObjectData3D is created with @ref ObjectFlag3D::HasTranslationRotationScaling and these separate properties accessible
- If object rotation quaternion is not normalized, the importer prints a
warning and normalizes it. Can be disabled per-object with the
@cb{.ini} normalizeQuaternions @ce option, see
@ref Trade-TinyGltfImporter-configuration "below".
@subsection Trade-TinyGltfImporter-limitations-camera Camera import
Expand Down

0 comments on commit 0ce8456

Please sign in to comment.