Skip to content
Permalink
Browse files

TinyGltfImporter: optimization for quaternion shortest-path lerp.

  • Loading branch information...
mosra committed Sep 5, 2018
1 parent 3ef0232 commit bba82bf37e13d5515afdeec8a8621d1da3b8330f
@@ -65,6 +65,8 @@ corrade_add_test(TinyGltfImporterTest
animation.bin
animation-embedded.gltf
animation-embedded.glb
animation-rotation-shortestpath.bin
animation-rotation-shortestpath.gltf
animation-wrong.gltf
camera.gltf
camera.glb
@@ -30,6 +30,7 @@
#include <Corrade/TestSuite/Tester.h>
#include <Corrade/TestSuite/Compare/Container.h>
#include <Corrade/Utility/Directory.h>
#include <Corrade/Utility/ConfigurationGroup.h>
#include <Magnum/Array.h>
#include <Magnum/Mesh.h>
#include <Magnum/PixelFormat.h>
@@ -65,6 +66,8 @@ struct TinyGltfImporterTest: TestSuite::Tester {
void animationWrongRotationType();
void animationWrongScalingType();
void animationUnsupportedPath();
void animationShortestPathOptimizationEnabled();
void animationShortestPathOptimizationDisabled();

void camera();

@@ -164,7 +167,10 @@ TinyGltfImporterTest::TinyGltfImporterTest() {
&TinyGltfImporterTest::animationWrongTranslationType,
&TinyGltfImporterTest::animationWrongRotationType,
&TinyGltfImporterTest::animationWrongScalingType,
&TinyGltfImporterTest::animationUnsupportedPath});
&TinyGltfImporterTest::animationUnsupportedPath,

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

addInstancedTests({&TinyGltfImporterTest::camera,

@@ -436,6 +442,129 @@ void TinyGltfImporterTest::animationUnsupportedPath() {
CORRADE_COMPARE(out.str(), "Trade::TinyGltfImporter::animation(): unsupported track target color\n");
}

void TinyGltfImporterTest::animationShortestPathOptimizationEnabled() {
std::unique_ptr<AbstractImporter> importer = _manager.instantiate("TinyGltfImporter");
/* Enabled by default */
CORRADE_VERIFY(importer->configuration().value<bool>("optimizeQuaternionShortestPath"));
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"animation-rotation-shortestpath.gltf")));

CORRADE_COMPARE(importer->animationCount(), 1);

auto animation = importer->animation(0);
CORRADE_VERIFY(animation);
CORRADE_VERIFY(animation->importerState());
CORRADE_COMPARE(animation->trackCount(), 1);

/* Rotation, linearly interpolated */
CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion);
CORRADE_COMPARE(animation->trackTarget(0), AnimationTrackTarget::Rotation3D);
Animation::TrackView<Float, Quaternion> track = animation->track<Quaternion>(0);
const Quaternion rotationValues[]{
{{0.0f, 0.0f, 0.92388f}, -0.382683f}, // 0 s: 225°
{{0.0f, 0.0f, 0.707107f}, -0.707107f}, // 1 s: 270°
{{0.0f, 0.0f, 0.382683f}, -0.92388f}, // 2 s: 315°
{{0.0f, 0.0f, 0.0f}, -1.0f}, // 3 s: 360° / 0°
{{0.0f, 0.0f, -0.382683f}, -0.92388f}, // 4 s: 45° (flipped)
{{0.0f, 0.0f, -0.707107f}, -0.707107f}, // 5 s: 90° (flipped)
{{0.0f, 0.0f, -0.92388f}, -0.382683f}, // 6 s: 135° (flipped back)
{{0.0f, 0.0f, -1.0f}, 0.0f}, // 7 s: 180° (flipped back)
{{0.0f, 0.0f, -0.92388f}, 0.382683f} // 8 s: 225° (flipped)
};
CORRADE_COMPARE_AS(track.values(), (Containers::StridedArrayView<const Quaternion>{rotationValues}), TestSuite::Compare::Container);

CORRADE_COMPARE(track.at(Math::slerp, 0.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 1.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 2.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 3.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 4.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 5.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 6.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 7.5f).axis(), -Vector3::zAxis());

/* Some are negated because of the flipped axis but other than that it's
nicely monotonic */
CORRADE_COMPARE(track.at(Math::slerp, 0.5f).angle(), 247.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 1.5f).angle(), 292.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 2.5f).angle(), 337.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 3.5f).angle(), 360.0_degf - 22.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 4.5f).angle(), 360.0_degf - 67.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 5.5f).angle(), 360.0_degf - 112.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 6.5f).angle(), 360.0_degf - 157.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 7.5f).angle(), 360.0_degf - 202.5_degf);
}

void TinyGltfImporterTest::animationShortestPathOptimizationDisabled() {
std::unique_ptr<AbstractImporter> importer = _manager.instantiate("TinyGltfImporter");
/* Explicitly disable */
importer->configuration().setValue("optimizeQuaternionShortestPath", false);
CORRADE_VERIFY(importer->openFile(Utility::Directory::join(TINYGLTFIMPORTER_TEST_DIR,
"animation-rotation-shortestpath.gltf")));

CORRADE_COMPARE(importer->animationCount(), 1);

auto animation = importer->animation(0);
CORRADE_VERIFY(animation);
CORRADE_VERIFY(animation->importerState());
CORRADE_COMPARE(animation->trackCount(), 1);
CORRADE_COMPARE(animation->trackType(0), AnimationTrackType::Quaternion);
CORRADE_COMPARE(animation->trackTarget(0), AnimationTrackTarget::Rotation3D);
Animation::TrackView<Float, Quaternion> track = animation->track<Quaternion>(0);

/* Should be the same as in animation-rotation-shortestpath.bin.in */
const Quaternion rotationValues[]{
{{0.0f, 0.0f, 0.92388f}, -0.382683f}, // 0 s: 225°
{{0.0f, 0.0f, 0.707107f}, -0.707107f}, // 1 s: 270°
{{0.0f, 0.0f, 0.382683f}, -0.92388f}, // 2 s: 315°
{{0.0f, 0.0f, 0.0f}, -1.0f}, // 3 s: 360° / 0°
{{0.0f, 0.0f, 0.382683f}, 0.92388f}, // 4 s: 45° (longer path)
{{0.0f, 0.0f, 0.707107f}, 0.707107f}, // 5 s: 90°
{{0.0f, 0.0f, -0.92388f}, -0.382683f}, // 6 s: 135° (longer path)
{{0.0f, 0.0f, -1.0f}, 0.0f}, // 7 s: 180°
{{0.0f, 0.0f, 0.92388f}, -0.382683f} // 8 s: 225° (longer path)
};
CORRADE_COMPARE_AS(track.values(), (Containers::StridedArrayView<const Quaternion>{rotationValues}), TestSuite::Compare::Container);

CORRADE_COMPARE(track.at(Math::slerpShortestPath, 0.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 1.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 2.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 3.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 4.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 5.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 6.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 7.5f).axis(), Vector3::zAxis());

/* Some are negated because of the flipped axis but other than that it's
nicely monotonic because slerpShortestPath() ensures that */
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 0.5f).angle(), 247.5_degf);
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 1.5f).angle(), 292.5_degf);
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 2.5f).angle(), 337.5_degf);
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 3.5f).angle(), 22.5_degf);
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 4.5f).angle(), 67.5_degf);
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 5.5f).angle(), 360.0_degf - 112.5_degf);
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 6.5f).angle(), 360.0_degf - 157.5_degf);
CORRADE_COMPARE(track.at(Math::slerpShortestPath, 7.5f).angle(), 202.5_degf);

CORRADE_COMPARE(track.at(Math::slerp, 0.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 1.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 2.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 3.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 4.5f).axis(), Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 5.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 6.5f).axis(), -Vector3::zAxis());
CORRADE_COMPARE(track.at(Math::slerp, 7.5f).axis(), -Vector3::zAxis(1.00004f)); /* ?! */

/* Things are a complete chaos when using non-SP slerp */
CORRADE_COMPARE(track.at(Math::slerp, 0.5f).angle(), 247.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 1.5f).angle(), 292.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 2.5f).angle(), 337.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 3.5f).angle(), 202.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 4.5f).angle(), 67.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 5.5f).angle(), 67.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 6.5f).angle(), 202.5_degf);
CORRADE_COMPARE(track.at(Math::slerp, 7.5f).angle(), 337.5_degf);
}

void TinyGltfImporterTest::camera() {
auto&& data = SingleFileData[testCaseInstanceId()];
setTestCaseDescription(data.name);
@@ -0,0 +1,16 @@
type = "<9f 36f"
input = [
# time
0, 1, 2, 3, 4, 5, 6, 7, 8,

# rotation around Z
0, 0, 0.92388, -0.382683, # 225°
0, 0, 0.707107, -0.707107, # 270°
0, 0, 0.382683, -0.92388, # 315°
0, 0, 0, -1, # 360°
0, 0, 0.382683, 0.92388, # 45°
0, 0, 0.707107, 0.707107, # 90°
0, 0, -0.92388, -0.382683, # 135°
0, 0, -1, 0, # 180°
0, 0, 0.92388, -0.382683, # 225°
]
@@ -0,0 +1,59 @@
{
"asset": {
"version": "2.0"
},
"animations": [
{
"channels": [
{
"sampler": 0,
"target": {
"node": 1337,
"path": "rotation"
}
}
],
"samplers": [
{
"input": 0,
"interpolation": "LINEAR",
"output": 1
}
]
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5126,
"count": 9,
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 9,
"type": "VEC4"
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 36
},
{
"buffer": 0,
"byteOffset": 36,
"byteLength": 140
}
],
"buffers": [
{
"byteLength": 180,
"uri": "animation-rotation-shortestpath.bin"
}
]
}
@@ -3,7 +3,7 @@
set -e

# in -> bin
for i in animation external-data mesh-colors mesh-primitives mesh; do
for i in animation animation-rotation-shortestpath external-data mesh-colors mesh-primitives mesh; do
./in2bin.py ${i}.bin.in
done

@@ -1,3 +1,11 @@
depends=AnyImageImporter
provides=GltfImporter
provides=GlbImporter

# [config]
[configuration]

# Optimize imported linearly-interpolated quaternion animation tracks to
# ensure shortest path is always chosen
optimizeQuaternionShortestPath=true
# [config]
@@ -31,6 +31,7 @@
#include <limits>
#include <unordered_map>
#include <Corrade/Containers/ArrayView.h>
#include <Corrade/Utility/ConfigurationGroup.h>
#include <Corrade/Utility/Directory.h>
#include <Corrade/Utility/String.h>
#include <Magnum/Mesh.h>
@@ -122,11 +123,26 @@ struct TinyGltfImporter::Document {
bool open = false;
};

TinyGltfImporter::TinyGltfImporter() = default;
namespace {

void fillDefaultConfiguration(Utility::ConfigurationGroup& conf) {
/** @todo horrible workaround, fix this properly */
conf.setValue("optimizeQuaternionShortestPath", true);
}

}

TinyGltfImporter::TinyGltfImporter() {
/** @todo horrible workaround, fix this properly */
fillDefaultConfiguration(configuration());
}

TinyGltfImporter::TinyGltfImporter(PluginManager::AbstractManager& manager, const std::string& plugin): AbstractImporter{manager, plugin} {}

TinyGltfImporter::TinyGltfImporter(PluginManager::Manager<AbstractImporter>& manager): AbstractImporter{manager} {}
TinyGltfImporter::TinyGltfImporter(PluginManager::Manager<AbstractImporter>& manager): AbstractImporter{manager} {
/** @todo horrible workaround, fix this properly */
fillDefaultConfiguration(configuration());
}

TinyGltfImporter::~TinyGltfImporter() = default;

@@ -329,7 +345,16 @@ Containers::Optional<AnimationData> TinyGltfImporter::doAnimation(UnsignedInt id
/* View on the value data */
const auto outputDataFound = samplerData.find(sampler.output);
CORRADE_INTERNAL_ASSERT(outputDataFound != samplerData.end());
const auto values = Containers::arrayCast<const Quaternion>(data.slice(outputDataFound->second.second, outputDataFound->second.second + outputDataFound->second.first.size()));
const auto values = Containers::arrayCast<Quaternion>(data.slice(outputDataFound->second.second, outputDataFound->second.second + outputDataFound->second.first.size()));

/* Patch the data to ensure shortest path is always chosen */
if(configuration().value<bool>("optimizeQuaternionShortestPath")) {
Float flip = 1.0f;
for(std::size_t i = 0; i != values.size() - 1; ++i) {
if(Math::dot(values[i], values[i + 1]*flip) < 0) flip = -flip;
values[i + 1] *= flip;
}
}

/* Populate track metadata */
type = AnimationTrackType::Quaternion;
@@ -174,6 +174,20 @@ Import of skeleton, skin and morph data is not supported at the moment.
@ref SamplerMipmap::Linear
- Wrapping (all axes): @ref SamplerWrapping::Repeat
@section Trade-TinyGltfImporter-configuration Plugin-specific config
It's possible to tune various output options through @ref configuration(). See
below for all options and their default values. In particular, the
enabled-by-default @cb{.ini} optimizeQuaternionShortestPath @ce option makes it
possible to use the faster
@ref Math::lerp(const Quaternion<T>&, const Quaternion<T>&, T) "Math::lerp()" /
@ref Math::slerp(const Quaternion<T>&, const Quaternion<T>&, T) "Math::slerp()"
functions instead of
@ref Math::lerpShortestPath(const Quaternion<T>&, const Quaternion<T>&, T) "Math::lerpShortestPath()" /
@ref Math::slerpShortestPath(const Quaternion<T>&, const Quaternion<T>&, T) "Math::slerpShortestPath()".
@snippet MagnumPlugins/TinyGltfImporter/TinyGltfImporter.conf config
@section Trade-TinyGltfImporter-state Access to internal importer state
Access to the underlying TinyGLTF structures it is provided through

0 comments on commit bba82bf

Please sign in to comment.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.