Merge branch 'quatinterp' into 'master'

Implement quadratic/TCB interpolation for quaternions (#2379)

Closes #2379

See merge request OpenMW/openmw!4802
This commit is contained in:
Alexei Kotov 2025-08-03 01:08:16 +03:00
commit 107778e13e
3 changed files with 89 additions and 5 deletions

View File

@ -94,6 +94,29 @@ namespace Misc
}
return 1 << depth;
}
inline osg::Quat quatexp(const osg::Quat& q)
{
const float imagNorm2 = q.asVec3().length2();
const float e = std::exp(q.w());
if (imagNorm2 < 1e-6f)
return { 0.f, 0.f, 0.f, e };
const float imagNorm = std::sqrt(imagNorm2);
const float f = e * std::sin(imagNorm) / imagNorm;
return { q.x() * f, q.y() * f, q.z() * f, e * std::cos(imagNorm) };
}
inline osg::Quat quatlog(const osg::Quat& q)
{
const float imagNorm2 = q.asVec3().length2();
const float norm = q.length();
if (imagNorm2 < 1e-6f)
return { 0.f, 0.f, 0.f, std::log(norm) };
const float f = std::acos(q.w() / norm) / std::sqrt(imagNorm2);
return { q.x() * f, q.y() * f, q.z() * f, std::log(norm) };
}
}
#endif

View File

@ -6,6 +6,8 @@
#include <utility>
#include <vector>
#include <components/misc/mathutil.hpp>
#include "exception.hpp"
#include "niffile.hpp"
#include "nifstream.hpp"
@ -28,7 +30,7 @@ namespace Nif
{
T mValue;
T mInTan; // Only for Quadratic interpolation, and never for QuaternionKeyList
T mOutTan; // Only for Quadratic interpolation, and never for QuaternionKeyList
T mOutTan; // For quaternions these are generated
};
template <typename T>
@ -107,6 +109,7 @@ namespace Nif
readQuadratic(*nif, key);
mKeys.emplace_back(time, key);
}
generateQuadraticTangents(mKeys);
}
else if (mInterpolationType == InterpolationType_TCB)
{
@ -155,6 +158,30 @@ namespace Nif
static void readQuadratic(NIFStream& nif, KeyT<osg::Quat>& key) { readValue(nif, key); }
template <typename U>
static void generateQuadraticTangents(std::vector<std::pair<float, KeyT<U>>>& keys)
{
// These are predefined for all types except quaternions
}
static void generateQuadraticTangents(std::vector<std::pair<float, KeyT<osg::Quat>>>& keys)
{
if (keys.size() <= 1)
return;
for (std::size_t i = 0; i < keys.size(); ++i)
{
KeyT<osg::Quat>& curr = keys[i].second;
const std::size_t prevIndex = i == 0 ? 0 : i - 1;
const std::size_t nextIndex = i == keys.size() - 1 ? i : i + 1;
const osg::Quat inv = curr.mValue.inverse();
const osg::Quat& prev = keys[prevIndex].second.mValue;
const osg::Quat& next = keys[nextIndex].second.mValue;
const osg::Quat len = Misc::quatlog(inv * prev) + Misc::quatlog(inv * next);
curr.mInTan = curr.mOutTan = curr.mValue * Misc::quatexp(len * -0.25f);
}
}
template <typename U>
static void generateTCBTangents(std::vector<TCBKey<U>>& keys)
{
@ -176,8 +203,8 @@ namespace Nif
const float w = (1.f - curr.mTension) * (1.f - curr.mContinuity) * (1.f - curr.mBias);
const U prevDelta = prev != nullptr ? curr.mValue - prev->mValue : next->mValue - curr.mValue;
const U nextDelta = next != nullptr ? next->mValue - curr.mValue : curr.mValue - prev->mValue;
curr.mInTan = (prevDelta * x + nextDelta * y) * prevLen / (prevLen + nextLen);
curr.mOutTan = (prevDelta * z + nextDelta * w) * nextLen / (prevLen + nextLen);
curr.mInTan = (prevDelta * x + nextDelta * y) * (prevLen / (prevLen + nextLen));
curr.mOutTan = (prevDelta * z + nextDelta * w) * (nextLen / (prevLen + nextLen));
}
}
@ -188,7 +215,32 @@ namespace Nif
static void generateTCBTangents(std::vector<TCBKey<osg::Quat>>& keys)
{
// TODO: implement TCB interpolation for quaternions
if (keys.size() <= 1)
return;
for (std::size_t i = 0; i < keys.size(); ++i)
{
TCBKey<osg::Quat>& curr = keys[i];
const TCBKey<osg::Quat>* prev = (i == 0) ? nullptr : &keys[i - 1];
const TCBKey<osg::Quat>* next = (i == keys.size() - 1) ? nullptr : &keys[i + 1];
const float prevLen = prev != nullptr && next != nullptr ? curr.mTime - prev->mTime : 1.f;
const float nextLen = prev != nullptr && next != nullptr ? next->mTime - curr.mTime : 1.f;
if (prevLen + nextLen == 0.f)
continue;
const float x = (1.f - curr.mTension) * (1.f - curr.mContinuity) * (1.f + curr.mBias);
const float y = (1.f - curr.mTension) * (1.f + curr.mContinuity) * (1.f - curr.mBias);
const float z = (1.f - curr.mTension) * (1.f + curr.mContinuity) * (1.f + curr.mBias);
const float w = (1.f - curr.mTension) * (1.f - curr.mContinuity) * (1.f - curr.mBias);
const osg::Quat inv = curr.mValue.inverse();
const osg::Quat prevDelta = prev != nullptr ? Misc::quatlog(prev->mValue.inverse() * curr.mValue)
: Misc::quatlog(inv * next->mValue);
const osg::Quat nextDelta = next != nullptr ? Misc::quatlog(inv * next->mValue)
: Misc::quatlog(prev->mValue.inverse() * curr.mValue);
const osg::Quat t1 = (prevDelta * x + nextDelta * y) * (nextLen / (prevLen + nextLen));
const osg::Quat t2 = (prevDelta * z + nextDelta * w) * (prevLen / (prevLen + nextLen));
curr.mInTan = curr.mValue * Misc::quatexp((prevDelta - t1) * 0.5f);
curr.mOutTan = curr.mValue * Misc::quatexp((t2 - nextDelta) * 0.5f);
}
}
};

View File

@ -165,7 +165,16 @@ namespace NifOsg
{
case Nif::InterpolationType_Constant:
return fraction > 0.5f ? b.mValue : a.mValue;
// TODO: Implement Quadratic and TBC interpolation
case Nif::InterpolationType_Quadratic:
case Nif::InterpolationType_TCB:
{
// Spherical quadrangle interpolation
osg::Quat from, to, result;
from.slerp(fraction, a.mValue, b.mValue);
to.slerp(fraction, a.mOutTan, b.mInTan);
result.slerp(2.f * fraction * (1.f - fraction), from, to);
return result;
}
default:
{
osg::Quat result;