There is a metod with very good trade-off between speed and accuracy how to increment a quaterniom representing rotational state ( i.e. integrate differential equation of rotational motion) by small vector increment of angle dphi
( which is vector angular velocity omega
mulptipliad by time step dt
).
The Exact (and slow) method of rotation of quaternion by vector:
void rotate_quaternion_by_vector_vec ( double [] dphi, double [] q ) {
double x = dphi[0];
double y = dphi[1];
double z = dphi[2];
double r2 = x*x + y*y + z*z;
double norm = Math.sqrt( r2 );
double halfAngle = norm * 0.5d;
double sa = Math.sin( halfAngle )/norm; // we normalize it here to save multiplications
double ca = Math.cos( halfAngle );
x*=sa; y*=sa; z*=sa;
double qx = q[0];
double qy = q[1];
double qz = q[2];
double qw = q[3];
q[0] = x*qw + y*qz - z*qy + ca*qx;
q[1] = -x*qz + y*qw + z*qx + ca*qy;
q[2] = x*qy - y*qx + z*qw + ca*qz;
q[3] = -x*qx - y*qy - z*qz + ca*qw;
}
The problem is that you have to compute slow functions like cos, sin, sqrt
. Instead you can obtain considerable speed gain and reasonable accuracy for small angles ( which is the case if the time step of your simulation is reasonable small ) by approximating sin
and cos
by taylor expansion expressed just using norm^2
instead of norm
.
Like this Fast method of rotation of quaternion by vector:
void rotate_quaternion_by_vector_Fast ( double [] dphi, double [] q ) {
double x = dphi[0];
double y = dphi[1];
double z = dphi[2];
double r2 = x*x + y*y + z*z;
// derived from second order taylor expansion
// often this is accuracy is sufficient
final double c3 = 1.0d/(6 * 2*2*2 ) ; // evaulated in compile time
final double c2 = 1.0d/(2 * 2*2) ; // evaulated in compile time
double sa = 0.5d - c3*r2 ;
double ca = 1 - c2*r2 ;
x*=sa;
y*=sa;
z*=sa;
double qx = q[0];
double qy = q[1];
double qz = q[2];
double qw = q[3];
q[0] = x*qw + y*qz - z*qy + ca*qx;
q[1] = -x*qz + y*qw + z*qx + ca*qy;
q[2] = x*qy - y*qx + z*qw + ca*qz;
q[3] = -x*qx - y*qy - z*qz + ca*qw;
}
you can increase accuracy by doing half o angle which 5 more multiplications:
final double c3 = 1.0d/( 6.0 *4*4*4 ) ; // evaulated in compile time
final double c2 = 1.0d/( 2.0 *4*4 ) ; // evaulated in compile time
double sa_ = 0.25d - c3*r2 ;
double ca_ = 1 - c2*r2 ;
double ca = ca_*ca_ - sa_*sa_*r2 ;
double sa = 2*ca_*sa_ ;
or even more accurate by an other spliting angle to halfs:
final double c3 = 1.0d/( 6 *8*8*8 ); // evaulated in compile time
final double c2 = 1.0d/( 2 *8*8 ); // evaulated in compile time
double sa = ( 0.125d - c3*r2 ) ;
double ca = 1 - c2*r2 ;
double ca_ = ca*ca - sa*sa*r2;
double sa_ = 2*ca*sa;
ca = ca_*ca_ - sa_*sa_*r2;
sa = 2*ca_*sa_;
NOTE: If you use more sophisticated integration scheme than just verlet (like Runge-Kutta ) you would probably need a differential of the quaternion, rather than the new ( updated ) quaternion.
this is possible to see in the code here
q[0] = x*qw + y*qz - z*qy + ca*qx;
q[1] = -x*qz + y*qw + z*qx + ca*qy;
q[2] = x*qy - y*qx + z*qw + ca*qz;
q[3] = -x*qx - y*qy - z*qz + ca*qw;
it could be interpreted as multiplication of the old (not updated ) quaternion by ca
( cosine of half angle ) which is approximativelly ca ~ 1
for small angles and adding the rest (some cross interactions). So the differential simply:
dq[0] = x*qw + y*qz - z*qy + (1-ca)*qx;
dq[1] = -x*qz + y*qw + z*qx + (1-ca)*qy;
dq[2] = x*qy - y*qx + z*qw + (1-ca)*qz;
dq[3] = -x*qx - y*qy - z*qz + (1-ca)*qw;
where term ( 1 - ca ) ~ 0
for small angles and could be sometimes neglected ( basically it just renormalize the quternion).