mcjSlerp

for DS 1,2,3,4

mcjSlerp : Using Quaternions and Slerp() to fix camera animations

-----

the camera below had its movement "recorded"

with the the following keys on its rotation

( 0, 80, 0 )

( 0, 100, 0 )

( 0, 120, 0 )

this key was automatically changed to ( -180, 60, -180 ) by DazStudio

rendering the animation shows that it introduced a wobble movement

after processing by mcjSlerp we get camera movements that match what was happening at record time

example 2

the movement of the flying car comes from a capture using an early version of my "FlyCam" script ( see in the script repository )

although the flips look fun, they did not really occur in uhhhh real life, they are the result of the way rotation angles are processed in daz studio

in the "after" photo you can see that mcjSlerp corrected this

before

after

here's the source code

// DAZ Studio version 3.1 filetype DAZ Script
//=====================================================
// mcjSlerp
// by mCasual/Jacques
// MIT License
//=====================================================
// this script is meant to be used by people who understand
// the explanation below and figure out how to use it
// ( basically you select a camera that has a recorded animation )
// set the playrange to the range you want fixed and launch the script )
//=====================================================
// lets say you press DS's Timeline Play button and start moving a camera
// you are in fact recording the camera's movements
//
// we suppose there were no keys on this camera when recording started
// so there were no keys ahead of us at any moment during the recording
//
// the current values of XRotate YRotate and ZRotate during the recording
// were at the value we had left them upon the most recent keying
//
// example
// frame 80 : XRotate YRotate and ZRotate keyed with values ( 1, 1, 1 )
// frame 100: only YRotate is keyed with value 2,
// so the current angles are ( 1, 2, 1 )
// frame 120, XRotate YRotate and ZRotate keyed with values ( 3, 3, 3 )
//
// we stop recording
// now if we request to be shown the value of rotations at frame 100
// we get ( 2, 2, 2 )
// but we know that the real position at recording time was ( 1, 2, 1 )
//
// reason: XRot and ZRot were not keyed at frame 100,
// so we are shown interpolated values based on the values
// at frames 80 and 120
// FixPlayrange() corrects this
// it goes through all the keys in the playrange
// and generates keys with correct values
// in the example above,
// a key for XRotate is created at frame 100 with the value "left"
// by the XRotate key at frame 80
// a key for ZRotate is created at frame 100 with the value "left"
// by the ZRotate key at frame 80
// the keys at frame 100 are now ( 1, 2, 1 ) which correstponds to
// what was experienced during recording
// now we have keys we can build upon
// if you set your camera rotations to ( 0°, 100°, 0° )
// and rotate your mouse wheel to move the camera forward
// you'll notice that the rotations have been changed by Daz Studio
// to ( -180°, 80°, -180° )
// the camera is still pointed exactly like before, we just moved forth
// so those two sets of angles are equivalent
//
// but if you recorded/keyed ( 0°, 100°, 0° ) at frame 0
// and ( -180°, 80°, -180° ) at frame 30
// the animation doesnt show a camera moving forth while
// remaining pointed straight ahead
// it shows a camera doing a sort of quarter-spin while moving
// forth, in other situations you may
// even find your camera doing back flips
//
// Daz studio tends to force angles to remain within
// the range -180° to 180° for X and Z and -90° to 90° for Y
// ( probably due to the maths of quaternions-to-euler conversions )
//
// quaternions are a way to store and manipulate 3D space (XYZ) angles
// in fact they can store and manipulate 4D space angles, no kidding
// and Slerp() is a way to transition between two orientations
// expressed as quaternions
// the Quaternions/Slerp method is immune to the
// "spin and back flips" effect
//
// another method called squat() does the same thing but
// produces smoother results (splined interpolation)
// for now i'll implement Slerp and see if that's satisfactory
//
// SlerpPlayrange() goes through all the keys in the playrange
// and creates new keys for all the other frames in the playrange
// using Slerp()
//
// important: SlerpPlayrange() processes only between the first
// and last keys of the playrange, so normally you would set the
// playrange to the range of the recorded camera animation
//========================================
var node, xprop, yprop, zprop;
var playrange, timestep;
var iskey, iskeyX, iskeyY, iskeyZ;
var todegrees = 180 / Math.PI;
if( init() )
{
FixPlayrange();
SlerpPlayrange();
}
else
{
MessageBox.information( "You forgot to select a node.", "mcjSlerp", "&OK" );
}
//=============================================================
// get common variables and list of keyed frames
//=============================================================
function init()
{
node = Scene.getSelectedNodeList()[ 0 ];
if( !node )
{
return( false );
}
xprop = node.findProperty( "XRotate" );
yprop = node.findProperty( "YRotate" );
zprop = node.findProperty( "ZRotate" );
playrange = Scene.getPlayRange();
timestep = Scene.getTimeStep();
var numFrames = Math.round( playrange.end / timestep + 1 )
iskey = new Array( numFrames );
iskeyX = new Array( numFrames );
iskeyY = new Array( numFrames );
iskeyZ = new Array( numFrames );
for( var i = 0; i < numFrames; i++ )
{
iskeyX[i] = false;
iskeyY[i] = false;
iskeyZ[i] = false;
}
var i, fr, n;
n = xprop.getNumKeys();
for( i = 0; i < n; i++ )
{
fr = Math.round( xprop.getKeyTime( i ) / timestep );
iskeyX[fr] = true;
}
n = yprop.getNumKeys();
for( i = 0; i < n; i++ )
{
fr = Math.round( yprop.getKeyTime( i ) / timestep );
iskeyY[fr] = true;
}
n = zprop.getNumKeys();
for( i = 0; i < n; i++ )
{
fr = Math.round( zprop.getKeyTime( i ) / timestep );
iskeyZ[fr] = true;
}
for( i = 0; i < numFrames; i++ )
{
iskey[i] = iskeyX[i] | iskeyY[i] | iskeyZ[i];
}
return( true );
}
//=============================================================
// when the camera animation was recorded
// on some frames, only 1 or 2 of the 3 rotation channels
// got keyframed. This left 1 or 2 channel values "floating"
// later on if those "floating" channels get keyframed
// it changed the (interpolated) values for all the frames
// that were previously left "floating"
// here we fix those errors
//=============================================================
function FixPlayrange()
{
var tStart = playrange.start;
var frStart = Math.round( tStart / timestep );
var tEnd = playrange.end;
var frEnd = Math.round( tEnd / timestep );
var prevx = xprop.getValue( tStart );
var prevy = yprop.getValue( tStart );
var prevz = zprop.getValue( tStart );
for( var fr = frStart; fr <= frEnd; fr++ )
{
var t = fr * timestep;
if( iskey[fr] )
{
if( ! iskeyX[fr] )
{
xprop.setValue( t, prevx );
}
else
{
prevx = xprop.getValue( t );
}
if( ! iskeyY[fr] )
{
yprop.setValue( t, prevy );
}
else
{
prevy = yprop.getValue( t );
}
if( ! iskeyZ[fr] )
{
zprop.setValue( t, prevz );
}
else
{
prevz = zprop.getValue( t );
}
}
}
}
//=============================================================
//
//=============================================================
function getIndexForTime( t, prop )
{
var numKeys = prop.getNumKeys();
for( var i = 0; i < numKeys; i++ )
{
if( prop.getKeyTime( i ) == t ) // ToDo: too strict?
{
return ( i );
}
}
return ( 0 );
}
//=============================================================
// apply slerp() interpolation between keyframed orientations
//=============================================================
function SlerpPlayrange()
{
var index = new Number;
var isInbetween = false;
var prevQuat;
var nextQuat;
var prevT;
var nextT;
var q = new DzQuat();
for( var t = playrange.start; t <= playrange.end; t += timestep )
{
var fr = Math.round( t / timestep );
if( iskey[fr] )
{
index = getIndexForTime( t, xprop );
isInbetween = true;
prevQuat = node.getWSRot( t );
prevT = t;
var numXkeys = xprop.getNumKeys();
if( ( index + 1 ) >= numXkeys )
{
nextT = playrange.end
}
else
{
nextT = xprop.getKeyTime( index + 1 );
}
nextQuat = node.getWSRot( nextT );
nextQuat.makeClosest( prevQuat ); //**** important *****
//not using node.setWSRot to avoid keyframing
//Translate/Scale channels of the node
//node.setWSRot( nextT, nextQuat );
var vrot = nextQuat.getValue ( 0, 1, 2 );
//note: the y z x order is needed to prevent
//daz from meddling with our values
yprop.setValue( nextT, vrot.y * todegrees );
zprop.setValue( nextT, vrot.z * todegrees );
xprop.setValue( nextT, vrot.x * todegrees );
}
else
{
if( isInbetween )
{
var dt = nextT - prevT;
if( dt > 0 )
{
var f = ( t - prevT ) / dt;
q = slerp( f, prevQuat, nextQuat );
//not using node.setWSRot to avoid keyframing
//Translate/Scale channels of the node
//node.setWSRot( t, q );
var vrot = q.getValue ( 0, 1, 2 );
//note: the y z x order is needed to prevent
//daz from meddling with our values
yprop.setValue( t, vrot.y * todegrees );
zprop.setValue( t, vrot.z * todegrees );
xprop.setValue( t, vrot.x * todegrees );
}
}
}
}
}
//=============================================================
//
//=============================================================
function dotproduct( u, v )
{
return ( u.w * v.w + u.x * v.x + u.y * v.y + u.z * v.z );
}
//=============================================================
//
//=============================================================
function slerp( t, u, v )
{
var cosTheta = dotproduct( u, v );
var theta = Math.acos( cosTheta );
var sinTheta = Math.sin( theta );
var w1, w2;
if( sinTheta > 0.001 )
{
w1 = ( Math.sin( ( 1.0 - t ) * theta ) / sinTheta );
w2 = ( Math.sin( t * theta ) / sinTheta );
}
else
{
w1 = 1.0 - t;
w2 = t;
}
var r = new DzQuat(
u.x * w1 + v.x * w2,
u.y * w1 + v.y * w2,
u.z * w1 + v.z * w2,
u.w * w1 + v.w * w2
);
return ( r );
}