+/* apply a rotation from the perspective of root */
+VG_STATIC void keyframe_rotate_around( mdl_keyframe *kf,
+ v3f origin, v3f offset, v4f q )
+{
+ v3f v0, co;
+ v3_add( kf->co, offset, co );
+ v3_sub( co, origin, v0 );
+ q_mulv( q, v0, v0 );
+ v3_add( v0, origin, co );
+ v3_sub( co, offset, kf->co );
+
+ q_mul( q, kf->q, kf->q );
+ q_normalize( kf->q );
+}
+
+/*
+ * Lerp between two sets of keyframes and store in dest. Rotations use Nlerp.
+ */
+VG_STATIC void keyframe_lerp_pose( mdl_keyframe *kfa, mdl_keyframe *kfb,
+ float t, mdl_keyframe *kfd, int count )
+{
+ if( t <= 0.0001f ){
+ keyframe_copy_pose( kfa, kfd, count );
+ return;
+ }
+ else if( t >= 0.9999f ){
+ keyframe_copy_pose( kfb, kfd, count );
+ return;
+ }
+
+ for( int i=0; i<count; i++ ){
+ v3_lerp( kfa[i].co, kfb[i].co, t, kfd[i].co );
+ q_nlerp( kfa[i].q, kfb[i].q, t, kfd[i].q );
+ v3_lerp( kfa[i].s, kfb[i].s, t, kfd[i].s );
+ }
+}
+
+VG_STATIC
+void skeleton_lerp_pose( struct skeleton *skele,
+ mdl_keyframe *kfa, mdl_keyframe *kfb, float t,
+ mdl_keyframe *kfd )
+{
+ keyframe_lerp_pose( kfa, kfb, t, kfd, skele->bone_count-1 );
+}
+
+VG_STATIC void skeleton_copy_pose( struct skeleton *skele,
+ mdl_keyframe *kfa, mdl_keyframe *kfd )
+{
+ keyframe_copy_pose( kfa, kfd, skele->bone_count-1 );
+}
+
+/*
+ * Sample animation between 2 closest frames using time value. Output is a
+ * keyframe buffer that is allocated with an appropriate size
+ */
+VG_STATIC void skeleton_sample_anim( struct skeleton *skele,
+ struct skeleton_anim *anim,
+ float time,
+ mdl_keyframe *output )
+{
+ f32 animtime = fmodf( time*anim->rate, anim->length ),
+ animframe = floorf( animtime ),
+ t = animtime - animframe;
+
+ u32 frame = (u32)animframe % anim->length,
+ next = (frame+1) % anim->length;