LNXSDK/leenkx/Sources/iron/object/BoneAnimation.hx

1169 lines
33 KiB
Haxe
Raw Normal View History

2025-01-22 16:18:30 +01:00
package iron.object;
import iron.object.Animation.ActionSampler;
#if lnx_skin
import kha.FastFloat;
import kha.arrays.Float32Array;
import iron.math.Vec4;
import iron.math.Mat4;
import iron.math.Quat;
import iron.data.MeshData;
import iron.data.SceneFormat;
import iron.data.Armature;
import iron.data.Data;
import iron.math.Ray;
class BoneAnimation extends Animation {
public static var skinMaxBones = 128;
// Skinning
public var object: MeshObject;
public var armatureObject: Object;
public var data: MeshData;
public var skinBuffer: Float32Array;
var updateAnimation: Array<Mat4>->Void = null;
public var skeletonBones(default, null): Array<TObj> = null;
public var skeletonMats(default, null): Array<Mat4> = null;
//var skeletonBonesBlend: Array<TObj> = null;
var absMats: Array<Mat4> = null;
var applyParent: Array<Bool> = null;
var matsFast: Array<Mat4> = [];
var matsFastSort: Array<Int> = [];
var matsFastBlend: Array<Mat4> = [];
var matsFastBlendSort: Array<Int> = [];
var rootMotionCacheInit: Bool = false;
public var rootMotion(default, null): TObj = null;
public var rootMotionVelocity(default, null): Vec4 = null;
public var rootMotionRotation(default, null): Quat = null;
var rootMotionIndex: Int = -1;
var rootMotionLockX: Bool = false;
var rootMotionLockY: Bool = false;
var rootMotionLockZ: Bool = false;
var delta: FastFloat = 0;
var boneChildren: Map<String, Array<Object>> = null; // Parented to bone
var constraintTargets: Array<Object> = null;
var constraintTargetsI: Array<Mat4> = null;
var constraintMats: Map<TObj, Mat4> = null;
var relativeBoneConstraints: Bool = false;
static var m = Mat4.identity(); // Skinning matrix
static var m1 = Mat4.identity();
static var m2 = Mat4.identity();
static var bm = Mat4.identity(); // Absolute bone matrix
static var wm = Mat4.identity();
static var vpos = new Vec4();
static var vpos2 = new Vec4();
static var vpos3 = new Vec4();
static var vscl = new Vec4();
static var vscl2 = new Vec4();
static var vscl3 = new Vec4();
static var q1 = new Quat();
static var q2 = new Quat();
static var q3 = new Quat();
static var q4 = new Quat();
static var v1 = new Vec4();
static var v2 = new Vec4();
public function new(armatureUid: Int, armatureObject: Object) {
super();
this.isSampled = false;
this.armatureObject = armatureObject;
for (a in Scene.active.armatures) {
if (a.uid == armatureUid) {
this.armature = a;
break;
}
}
}
public function initMatsEmpty(): Array<Mat4> {
var mats = [];
for(i in 0...skeletonMats.length) mats.push(Mat4.identity());
return mats;
}
public inline function getNumBones(): Int {
if (skeletonBones == null) return 0;
return skeletonBones.length;
}
public function setSkin(mo: MeshObject) {
this.object = mo;
this.data = mo != null ? mo.data : null;
this.isSkinned = data != null ? data.isSkinned : false;
if (this.isSkinned) {
var boneSize = 12; // Dual-quat skinning + scaling
this.skinBuffer = new Float32Array(skinMaxBones * boneSize);
for (i in 0...this.skinBuffer.length) this.skinBuffer[i] = 0;
// Rotation is already applied to skin at export
object.transform.rot.set(0, 0, 0, 1);
object.transform.buildMatrix();
var refs = mo.parent.raw.bone_actions;
if (refs != null && refs.length > 0) {
Data.getSceneRaw(refs[0], function(action: TSceneFormat) { play(action.name); });
}
}
if (armatureObject.raw.relative_bone_constraints) relativeBoneConstraints = true;
}
public function addBoneChild(bone: String, o: Object) {
if (boneChildren == null) boneChildren = new Map();
var ar = boneChildren.get(bone);
if (ar == null) {
ar = [];
boneChildren.set(bone, ar);
}
ar.push(o);
}
public function removeBoneChild(bone: String, o: Object) {
if (boneChildren != null) {
var ar = boneChildren.get(bone);
if (ar != null) ar.remove(o);
}
}
@:access(iron.object.Transform)
function updateBoneChildren(bone: TObj, bm: Mat4) {
var ar = boneChildren.get(bone.name);
if (ar == null) return;
for (o in ar) {
var t = o.transform;
if (t.boneParent == null) t.boneParent = Mat4.identity();
if (o.raw.parent_bone_tail != null) {
if (o.raw.parent_bone_connected || isSkinned) {
var v = o.raw.parent_bone_tail;
t.boneParent.initTranslate(v[0], v[1], v[2]);
t.boneParent.multmat(bm);
}
else {
var v = o.raw.parent_bone_tail_pose;
t.boneParent.setFrom(bm);
t.boneParent.translate(v[0], v[1], v[2]);
}
}
else t.boneParent.setFrom(bm);
t.buildMatrix();
}
}
function numParents(b: TObj): Int {
var i = 0;
var p = b.parent;
while (p != null) {
i++;
p = p.parent;
}
return i;
}
function setMats() {
while (matsFast.length < skeletonBones.length) {
matsFast.push(Mat4.identity());
matsFastSort.push(matsFastSort.length);
}
// Calc bones with 0 parents first
matsFastSort.sort(function(a, b) {
var i = numParents(skeletonBones[a]);
var j = numParents(skeletonBones[b]);
return i < j ? -1 : i > j ? 1 : 0;
});
}
function setAction(action: String) {
armature.initMats();
var a = armature.getAction(action);
skeletonBones = a.bones;
skeletonMats = a.mats;
if(! rootMotionCacheInit) skeletonMats.push(Mat4.identity());
rootMotionCacheInit = true;
setMats();
}
function getAction(action: String): Array<TObj> {
armature.initMats();
return armature.getAction(action).bones;
}
function multParent(i: Int, fasts: Array<Mat4>, bones: Array<TObj>, mats: Array<Mat4>) {
var f = fasts[i];
if (applyParent != null && !applyParent[i]) {
f.setFrom(mats[i]);
return;
}
var p = bones[i].parent;
var bi = getBoneIndex(p, bones);
(p == null || bi == -1) ? f.setFrom(mats[i]) : f.multmats(fasts[bi], mats[i]);
}
inline function multVecs(vec1: Vec4, vec2: Vec4): Vec4 {
var res = new Vec4().setFrom(vec1);
res.x *= vec2.x;
res.y *= vec2.y;
res.z *= vec2.z;
res.w *= vec2.w;
return res;
}
// Do animation here
public function setAnimationLoop(f: Array<Mat4>->Void) {
updateAnimation = f;
}
override public function play(action = "", onComplete: Void->Void = null, blendTime = 0.2, speed = 1.0, loop = true) {
super.play(action, onComplete, blendTime, speed, loop);
if (action != "") {
setAction(action);
var tempAnimParam = new ActionSampler(action);
registerAction("tempAction", tempAnimParam);
updateAnimation = function(mats){
sampleAction(tempAnimParam, mats);
}
}
}
override public function update(delta: FastFloat) {
this.delta = delta;
if (!isSkinned && skeletonBones == null) setAction(armature.actions[0].name);
if (object != null && (!object.visible || object.culled)) return;
if (skeletonBones == null || skeletonBones.length == 0) return;
#if lnx_debug
Animation.beginProfile();
#end
super.update(delta);
if(updateAnimation != null) {
updateAnimation(skeletonMats);
}
updateConstraints();
// Do forward kinematics and inverse kinematics here
if (onUpdates != null) {
var i = 0;
var l = onUpdates.length;
while (i < l) {
onUpdates[i]();
l <= onUpdates.length ? i++ : l = onUpdates.length;
}
}
// Calc absolute bones
for (i in 0...skeletonBones.length) {
// Take bones with 0 parents first
multParent(matsFastSort[i], matsFast, skeletonBones, skeletonMats);
}
if (isSkinned) updateSkinGpu();
else updateBonesOnly();
#if lnx_debug
Animation.endProfile();
#end
}
public function evaluateRootMotion(actionMats: Array<Mat4>): Vec4{
if(rootMotionIndex < 0) return new Vec4();
var scl = armatureObject.transform.scale;
wm = getRootMotionWorldMat(actionMats, rootMotion);
wm.decompose(vpos, q1, vscl);
vpos = multVecs(vpos, scl);
rootMotionVelocity.setFrom(vpos);
rootMotionRotation.setFrom(q1);
return new Vec4().setFrom(rootMotionVelocity);
}
public function setRootMotion(bone: TObj, lockX: Bool = false, lockY: Bool = false, lockZ: Bool = false) {
rootMotion = bone;
rootMotionIndex = getBoneIndex(rootMotion);
rootMotionLockX = lockX;
rootMotionLockY = lockY;
rootMotionLockZ = lockZ;
rootMotionVelocity = new Vec4();
rootMotionRotation = new Quat();
}
function multParents(m: Mat4, i: Int, bones: Array<TObj>, mats: Array<Mat4>) {
var bone = bones[i];
var p = bone.parent;
while (p != null) {
var i = getBoneIndex(p, bones);
if (i == -1) continue;
m.multmat(mats[i]);
p = p.parent;
}
}
function getConstraintsFromScene(cs: Array<TConstraint>) {
// Init constraints
if (constraintTargets == null) {
constraintTargets = [];
constraintTargetsI = [];
for (c in cs) {
var o = Scene.active.getChild(c.target);
constraintTargets.push(o);
var m: Mat4 = null;
if (o != null) {
m = Mat4.identity().setFrom(o.transform.world);
m.getInverse(m);
}
constraintTargetsI.push(m);
}
constraintMats = new Map();
}
}
function getConstraintsFromParentRelative(cs: Array<TConstraint>) {
// Init constraints
if (constraintTargets == null) {
constraintTargets = [];
constraintTargetsI = [];
// MeshObject -> ArmatureObject -> Collection/Empty
var conParent = armatureObject.parent;
if (conParent == null) return;
for (c in cs) {
var o = conParent.getChild(c.target);
constraintTargets.push(o);
var m: Mat4 = null;
if (o != null) {
m = Mat4.identity().setFrom(o.transform.world);
m.getInverse(m);
}
constraintTargetsI.push(m);
}
constraintMats = new Map();
}
}
function updateConstraints() {
if (data == null) return;
var cs = data.raw.skin.constraints;
if (cs == null) return;
if (relativeBoneConstraints) {
getConstraintsFromParentRelative(cs);
}
else {
getConstraintsFromScene(cs);
}
// Update matrices
for (i in 0...cs.length) {
var c = cs[i];
var bone = getBone(c.bone);
if (bone == null) continue;
var o = constraintTargets[i];
if (o == null) continue;
if (c.type == "CHILD_OF") {
var m = constraintMats.get(bone);
if (m == null) {
m = Mat4.identity();
constraintMats.set(bone, m);
}
m.setFrom(armatureObject.transform.world); // Armature transform
m.multmat(constraintTargetsI[i]); // Roll back initial hitbox transform
m.multmat(o.transform.world); // Current hitbox transform
m1.getInverse(armatureObject.transform.world); // Roll back armature transform
m.multmat(m1);
}
}
}
var onUpdates: Array<Void->Void> = null;
public function notifyOnUpdate(f: Void->Void) {
if (onUpdates == null) onUpdates = [];
onUpdates.push(f);
}
public function removeUpdate(f: Void->Void) {
onUpdates.remove(f);
}
override public function updateActionTrack(sampler: ActionSampler) {
if(sampler.paused) return;
if(! sampler.actionDataInit) {
var bones = getAction(sampler.action);
sampler.setBoneAction(bones);
}
var bones = sampler.getBoneAction();
for(b in bones){
if (b.anim != null) {
updateTrack(b.anim, sampler);
break;
}
}
}
public function sampleAction(sampler: ActionSampler, actionMats: Array<Mat4>) {
if(! sampler.actionDataInit) {
var bones = getAction(sampler.action);
sampler.setBoneAction(bones);
}
var bones = sampler.getBoneAction();
actionMats[skeletonBones.length].setIdentity();
var rootMotionEnabled = sampler.rootMotionPos || sampler.rootMotionRot;
for (i in 0...bones.length) {
if (i == rootMotionIndex && rootMotionEnabled){
updateAnimSampledRootMotion(bones[i].anim, actionMats[i], actionMats[skeletonBones.length], sampler);
}
else {
updateAnimSampled(bones[i].anim, actionMats[i], sampler);
}
}
}
function updateAnimSampled(anim: TAnimation, mm: Mat4, sampler: ActionSampler) {
if(anim == null) return;
var track = anim.tracks[0];
var sign = sampler.speed > 0 ? 1 : -1;
var t = sampler.time;
//t = t < 0 ? 0.1 : t;
var ti = sampler.offset;
//ti = ti < 0 ? 1 : ti;
interpolateSample(track, mm, t, ti, sign);
}
function updateAnimSampledRootMotion(anim: TAnimation, mm: Mat4, rm: Mat4, sampler: ActionSampler) {
if(anim == null) return;
var track = anim.tracks[0];
var sign = sampler.speed > 0 ? 1 : -1;
var t0 = speed > 0 ? 0 : track.frames.length - 1;
var t = sampler.time;
var ti = sampler.offset;
if(sampler.trackEnd || !sampler.cacheInit) {
interpolateSample(track, bm, t, ti, sign);
//bm.setF32(track.values, t0);
sampler.setActionCache(bm);
}
// Interpolated action for current frame
interpolateSample(track, m1, t, ti, sign);
// Action at first frame
m.setF32(track.values, t0);
// Action at previous frame
sampler.getActionCache(m2);
m.decompose(vpos, q1, vscl);
m1.decompose(vpos2, q2, vscl2);
m2.decompose(vpos3, q3, vscl3);
// Compose
if(sampler.rootMotionRot) {
// Bone matrix
mm.fromQuat(q1);
mm.scale(vscl2);
// Root motion matrix
q2.mult(q4.inverse(q3));
rm.fromQuat(q2);
}
else {
// Bone matrix
mm.fromQuat(q2);
mm.scale(vscl2);
}
if(sampler.rootMotionPos) {
// Bone matrix
mm._30 = vpos.x;
mm._31 = vpos.y;
mm._32 = vpos.z;
// Root motion matrix
rm._30 = vpos2.x - vpos3.x;
rm._31 = vpos2.y - vpos3.y;
rm._32 = vpos2.z - vpos3.z;
}
else {
// Bone matrix
mm._30 = vpos2.x;
mm._31 = vpos2.y;
mm._32 = vpos2.z;
}
sampler.setActionCache(m1);
}
inline function interpolateSample(track: TTrack, m: Mat4, t: FastFloat, ti: Int, sign: Int) {
var t1 = track.frames[ti] * frameTime;
var t2 = track.frames[ti + sign] * frameTime;
var s: FastFloat = (t - t1) / (t2 - t1); // Linear
m1.setF32(track.values, ti * 16); // Offset to 4x4 matrix array
m2.setF32(track.values, (ti + sign) * 16);
// Decompose
m1.decompose(vpos, q1, vscl);
m2.decompose(vpos2, q2, vscl2);
// Lerp
v1.lerp(vpos, vpos2, s);
v2.lerp(vscl, vscl2, s);
q3.lerp(q1, q2, s);
// Compose
m.fromQuat(q3);
m.scale(v2);
m._30 = v1.x;
m._31 = v1.y;
m._32 = v1.z;
}
function updateBonesOnly() {
if (boneChildren != null) {
for (i in 0...skeletonBones.length) {
var b = skeletonBones[i]; // TODO: blendTime > 0
m.setFrom(matsFast[i]);
updateBoneChildren(b, m);
}
}
}
function updateSkinGpu() {
var bones = skeletonBones;
// Update skin buffer
for (i in 0...bones.length) {
if (constraintMats != null) {
var m = constraintMats.get(bones[i]);
if (m != null) {
updateSkinBuffer(m, i);
continue;
}
}
m.setFrom(matsFast[i]);
if (absMats != null && i < absMats.length) absMats[i].setFrom(m);
if (boneChildren != null) updateBoneChildren(bones[i], m);
m.multmats(m, data.geom.skeletonTransformsI[i]);
updateSkinBuffer(m, i);
}
}
function updateSkinBuffer(m: Mat4, i: Int) {
// Dual quat skinning
m.decompose(vpos, q1, vscl);
q1.normalize();
q2.set(vpos.x, vpos.y, vpos.z, 0.0);
q2.multquats(q2, q1);
skinBuffer[i * 12] = q1.x; // Real
skinBuffer[i * 12 + 1] = q1.y;
skinBuffer[i * 12 + 2] = q1.z;
skinBuffer[i * 12 + 3] = q1.w;
skinBuffer[i * 12 + 4] = q2.x * 0.5; // Dual
skinBuffer[i * 12 + 5] = q2.y * 0.5;
skinBuffer[i * 12 + 6] = q2.z * 0.5;
skinBuffer[i * 12 + 7] = q2.w * 0.5;
skinBuffer[i * 12 + 8] = vscl.x;
skinBuffer[i * 12 + 9] = vscl.y;
skinBuffer[i * 12 + 10] = vscl.z;
skinBuffer[i * 12 + 11] = 1.0;
}
public override function getTotalFrames(sampler: ActionSampler): Int {
var bones = getAction(sampler.action);
var track = bones[0].anim.tracks[0];
return Std.int(track.frames[track.frames.length - 1] - track.frames[0]);
}
public function getBone(name: String): TObj {
if (skeletonBones == null) return null;
for (b in skeletonBones) if (b.name == name) return b;
return null;
}
function getBoneIndex(bone: TObj, bones: Array<TObj> = null): Int {
if (bones == null) bones = skeletonBones;
if (bones != null) for (i in 0...bones.length) if (bones[i] == bone) return i;
return -1;
}
public function getBoneMat(actionMats: Array<Mat4>, bone: TObj): Mat4 {
return actionMats != null ? actionMats[getBoneIndex(bone)] : null;
}
function getWorldMat(actionMats: Array<Mat4>, bone: TObj): Mat4 {
if (applyParent == null) {
applyParent = [];
for (m in actionMats) applyParent.push(true);
}
var i = getBoneIndex(bone);
wm.setFrom(actionMats[i]);
multParents(wm, i, skeletonBones, actionMats);
return wm;
}
function getRootMotionWorldMat(actionMats: Array<Mat4>, bone: TObj): Mat4 {
// Get root motion bone index
var i = getBoneIndex(bone);
// Store current bone matrix in temp
var tempMat = Mat4.identity().setFrom(actionMats[i]);
// Move root motion cache to bone position
actionMats[i].setFrom(actionMats[skeletonBones.length]);
// Calculate world matrix
wm.setFrom(getWorldMat(actionMats, bone));
// Revert to old value
actionMats[i].setFrom(tempMat);
return wm;
}
// Returns bone matrix in world space
public function getAbsWorldMat(actionMats: Array<Mat4>, bone: TObj): Mat4 {
var wm = getWorldMat(actionMats, bone);
wm.multmat(armatureObject.transform.world);
return wm;
}
// Returns an array of bone matrices in world space
public function getWorldMatsFast(actionMats: Array<Mat4>, tip: TObj, chainLength: Int): Array<Mat4> {
var wmArray: Array<Mat4> = [];
var root = tip;
var numP = chainLength;
for (i in 0...chainLength) {
var wm = getAbsWorldMat(actionMats, root);
wmArray[chainLength - 1 - i] = wm.clone();
root = root.parent;
numP--;
}
// Root bone at [0]
return wmArray;
}
// Set bone transforms in world space
public function setBoneMatFromWorldMat(actionMats: Array<Mat4>, wm: Mat4, bone: TObj) {
var invMat = Mat4.identity();
var tempMat = wm.clone();
invMat.getInverse(armatureObject.transform.world);
tempMat.multmat(invMat);
var bones: Array<TObj> = [];
var pBone = bone;
while (pBone.parent != null) {
bones.push(pBone.parent);
pBone = pBone.parent;
}
for (i in 0...bones.length) {
var x = bones.length - 1;
invMat.getInverse(getBoneMat(actionMats, bones[x - i]));
tempMat.multmat(invMat);
}
getBoneMat(actionMats, bone).setFrom(tempMat);
}
public function blendAction(actionMats1: Array<Mat4>, actionMats2: Array<Mat4>, resultMat: Array<Mat4>, factor: FastFloat = 0.0, layerMask: Int = -1, threshold: FastFloat = 0.1) {
if(factor < threshold) {
for(i in 0...actionMats1.length){
// Use Action 1
resultMat[i].setFrom(actionMats1[i]);
}
}
else if(factor > 1.0 - threshold){
for(i in 0...actionMats2.length){
// If root motion cache -> use root motion index, else use current index
var j = i == skeletonBones.length ? rootMotionIndex : i;
// Skip if root motion is disabled
if(j < 0) continue;
// Use Action 2
if(skeletonBones[j].bone_layers[layerMask] || layerMask < 0){
resultMat[i].setFrom(actionMats2[i]);
}
// Use Action 1 if not in layer
else {
resultMat[i].setFrom(actionMats1[i]);
}
}
}
else {
for(i in 0...actionMats1.length){
// If root motion cache -> use root motion index, else use current index
var j = i == skeletonBones.length ? rootMotionIndex : i;
// Skip if root motion is disabled
if(j < 0) continue;
// Blend
if(skeletonBones[j].bone_layers[j] || layerMask < 0) {
// Decompose
m.setFrom(actionMats1[i]);
m1.setFrom(actionMats2[i]);
m.decompose(vpos, q1, vscl);
m1.decompose(vpos2, q2, vscl2);
// Lerp
v1.lerp(vpos, vpos2, factor);
v2.lerp(vscl, vscl2, factor);
q3.lerp(q1, q2, factor);
// Compose
m2.fromQuat(q3);
m2.scale(v2);
m2._30 = v1.x;
m2._31 = v1.y;
m2._32 = v1.z;
// Lock root motion to one action on certain axis to conserve looping
if(i == skeletonBones.length){
m2._30 = rootMotionLockX ? vpos2.x : m2._30;
m2._31 = rootMotionLockY ? vpos2.y : m2._31;
m2._32 = rootMotionLockZ ? vpos2.z : m2._32;
}
// Return Result
resultMat[i].setFrom(m2);
}
// Use Action 1 if not in layer
else {
resultMat[i].setFrom(actionMats1[i]);
}
}
}
}
public function additiveBlendAction(baseActionMats: Array<Mat4>, addActionMats: Array<Mat4>, restPoseMats: Array<Mat4>, resultMat: Array<Mat4>, factor: FastFloat, layerMask: Int = -1, threshold: FastFloat = 0.1) {
if(factor < threshold) {
for(i in 0...baseActionMats.length){
resultMat[i].setFrom(baseActionMats[i]);
}
}
else{
for(i in 0...baseActionMats.length){
if(skeletonBones[i].bone_layers[layerMask] || layerMask < 0) {
// Decompose
m.setFrom(baseActionMats[i]);
m1.setFrom(addActionMats[i]);
bm.setFrom(restPoseMats[i]);
m.decompose(vpos, q1, vscl);
m1.decompose(vpos2, q2, vscl2);
bm.decompose(vpos3, q3, vscl3);
// Add Transforms
v1.setFrom(vpos);
v2.setFrom(vpos2);
v2.sub(vpos3);
v2.mult(factor);
v1.add(v2);
// Add Scales
vscl2.mult(factor);
v2.set(1-factor, 1-factor, 1-factor, 1);
v2.add(vscl2);
v2.x *= vscl.x;
v2.y *= vscl.y;
v2.z *= vscl.z;
v2.w = 1.0;
// Add rotations
q2.lerp(q3, q2, factor);
wm.fromQuat(q3);
wm.getInverse(wm);
q3.fromMat(wm).normalize();
q3.multquats(q3, q2);
q3.multquats(q1, q3);
// Compose
m2.fromQuat(q3);
m2.scale(v2);
m2._30 = v1.x;
m2._31 = v1.y;
m2._32 = v1.z;
// Return Result
resultMat[i].setFrom(m2);
}
else{
resultMat[i].setFrom(baseActionMats[i]);
}
}
}
}
public function solveIK(actionMats: Array<Mat4>, effector: TObj, goal: Vec4, precision = 0.01, maxIterations = 100, chainLenght = 100, pole: Vec4 = null, rollAngle = 0.0) {
// Array of bones to solve IK for, effector at 0
var bones: Array<TObj> = [];
// Array of bones lengths, effector length at 0
var lengths: Array<FastFloat> = [];
// Array of bones matrices in world coordinates, effector at 0
var boneWorldMats: Array<Mat4>;
var tempLoc = new Vec4();
var tempRot = new Quat();
var tempRot2 = new Quat();
var tempScl = new Vec4();
var roll = new Quat().fromEuler(0, rollAngle, 0);
// Store all bones and lengths in array
var tip = effector;
bones.push(tip);
var root = tip;
while (root.parent != null) {
if (bones.length > chainLenght - 1) break;
bones.push(root.parent);
root = root.parent;
}
// Get all bone mats in world space
boneWorldMats = getWorldMatsFast(actionMats, effector, bones.length);
var tempIndex = 0;
for(b in bones){
lengths.push(b.bone_length * boneWorldMats[boneWorldMats.length - 1 - tempIndex].getScale().x);
tempIndex++;
}
// Root bone
root = bones[bones.length - 1];
// World matrix of root bone
var rootWorldMat = getAbsWorldMat(actionMats, root).clone();
// Distance from root to goal
var dist = Vec4.distance(goal, rootWorldMat.getLoc());
// Total bones length
var totalLength: FastFloat = 0.0;
for (l in lengths) totalLength += l;
// Unreachable distance
if (dist > totalLength) {
// Calculate unit vector from root to goal
var newLook = goal.clone();
newLook.sub(rootWorldMat.getLoc());
newLook.normalize();
// Rotate root bone to point at goal
rootWorldMat.decompose(tempLoc, tempRot, tempScl);
tempRot2.fromTo(rootWorldMat.look().normalize(), newLook);
tempRot2.mult(tempRot);
tempRot2.mult(roll);
rootWorldMat.compose(tempLoc, tempRot2, tempScl);
// Set bone matrix in local space from world space
setBoneMatFromWorldMat(actionMats, rootWorldMat, root);
// Set child bone rotations to zero
for (i in 0...bones.length - 1) {
getBoneMat(actionMats, bones[i]).decompose(tempLoc, tempRot, tempScl);
getBoneMat(actionMats, bones[i]).compose(tempLoc, roll, tempScl);
}
return;
}
// Array of bone locations in world space, root location at [0]
var boneWorldLocs: Array<Vec4> = [];
for (b in boneWorldMats) boneWorldLocs.push(b.getLoc());
// Solve FABRIK
var vec = new Vec4();
var startLoc = boneWorldLocs[0].clone();
var l = boneWorldLocs.length;
var testLength = 0;
for (iter in 0...maxIterations) {
// Backward
vec.setFrom(goal);
vec.sub(boneWorldLocs[l - 1]);
vec.normalize();
vec.mult(lengths[0]);
boneWorldLocs[l - 1].setFrom(goal);
boneWorldLocs[l - 1].sub(vec);
for (j in 1...l) {
vec.setFrom(boneWorldLocs[l - 1 - j]);
vec.sub(boneWorldLocs[l - j]);
vec.normalize();
vec.mult(lengths[j]);
boneWorldLocs[l - 1 - j].setFrom(boneWorldLocs[l - j]);
boneWorldLocs[l - 1 - j].add(vec);
}
// Forward
boneWorldLocs[0].setFrom(startLoc);
for (j in 1...l) {
vec.setFrom(boneWorldLocs[j]);
vec.sub(boneWorldLocs[j - 1]);
vec.normalize();
vec.mult(lengths[l - j]);
boneWorldLocs[j].setFrom(boneWorldLocs[j - 1]);
boneWorldLocs[j].add(vec);
}
if (Vec4.distance(boneWorldLocs[l - 1], goal) - lengths[0] <= precision) break;
}
// Pole rotation implementation
if (pole != null) {
for (i in 1...boneWorldLocs.length - 1) {
boneWorldLocs[i] = moveTowardPole(boneWorldLocs[i - 1].clone(), boneWorldLocs[i].clone(), boneWorldLocs[i + 1].clone(), pole.clone());
}
}
// Correct rotations
// Applying locations and rotations
var tempLook = new Vec4();
var tempLoc2 = new Vec4();
for (i in 0...l - 1){
// Decompose matrix
boneWorldMats[i].decompose(tempLoc, tempRot, tempScl);
// Rotate to point to parent bone
tempLoc2.setFrom(boneWorldLocs[i + 1]);
tempLoc2.sub(boneWorldLocs[i]);
tempLoc2.normalize();
tempLook.setFrom(boneWorldMats[i].look());
tempLook.normalize();
tempRot2.fromTo(tempLook, tempLoc2);
tempRot2.mult(tempRot);
tempRot2.mult(roll);
// Compose matrix with new rotation and location
boneWorldMats[i].compose(boneWorldLocs[i], tempRot2, tempScl);
// Set bone matrix in local space from world space
setBoneMatFromWorldMat(actionMats, boneWorldMats[i], bones[bones.length - 1 - i]);
}
// Decompose matrix
boneWorldMats[l - 1].decompose(tempLoc, tempRot, tempScl);
// Rotate to point to goal
tempLoc2.setFrom(goal);
tempLoc2.sub(tempLoc);
tempLoc2.normalize();
tempLook.setFrom(boneWorldMats[l - 1].look());
tempLook.normalize();
tempRot2.fromTo(tempLook, tempLoc2);
tempRot2.mult(tempRot);
tempRot2.mult(roll);
// Compose matrix with new rotation and location
boneWorldMats[l - 1].compose(boneWorldLocs[l - 1], tempRot2, tempScl);
// Set bone matrix in local space from world space
setBoneMatFromWorldMat(actionMats, boneWorldMats[l - 1], bones[0]);
}
public function moveTowardPole(bone0Pos: Vec4, bone1Pos: Vec4, bone2Pos: Vec4, polePos: Vec4): Vec4 {
// Setup projection plane at current bone's parent
var plane = new Plane();
// Plane normal from parent of current bone to child of current bone
var planeNormal = new Vec4().setFrom(bone2Pos);
planeNormal.sub(bone0Pos);
planeNormal.normalize();
plane.set(planeNormal, bone0Pos);
// Create and project ray from current bone to plane
var rayPos = new Vec4();
rayPos.setFrom(bone1Pos);
var rayDir = new Vec4();
rayDir.sub(planeNormal);
rayDir.normalize();
var rayBone = new Ray(rayPos, rayDir);
// Projection point of current bone on plane
// If pole does not project on the plane
if (!rayBone.intersectsPlane(plane)) {
rayBone.direction = planeNormal;
}
var bone1Proj = rayBone.intersectPlane(plane);
// Create and project ray from pole to plane
rayPos.setFrom(polePos);
var rayPole = new Ray(rayPos, rayDir);
// If pole does not project on the plane
if (!rayPole.intersectsPlane(plane)) {
rayPole.direction = planeNormal;
}
// Projection point of pole on plane
var poleProj = rayPole.intersectPlane(plane);
// Caclulate unit vectors from pole projection to parent bone
var poleProjNormal = new Vec4();
poleProjNormal.setFrom(bone0Pos);
poleProjNormal.sub(poleProj);
poleProjNormal.normalize();
// Calculate unit vector from current bone projection to parent bone
var bone1ProjNormal = new Vec4();
bone1ProjNormal.setFrom(bone0Pos);
bone1ProjNormal.sub(bone1Proj);
bone1ProjNormal.normalize();
// Calculate rotation quaternion
var rotQuat = new Quat();
rotQuat.fromTo(bone1ProjNormal, poleProjNormal);
// Apply quaternion to current bone location
var bone1Res = new Vec4().setFrom(bone1Pos);
bone1Res.sub(bone0Pos);
bone1Res.applyQuat(rotQuat);
bone1Res.add(bone0Pos);
// Return new location of current bone
return bone1Res;
}
public function solveTwoBoneIK(actionMats : Array<Mat4>, effector: TObj, goal: Vec4, pole: Vec4 = null, rollAngle = 0.0) {
var roll = new Quat().fromEuler(0, rollAngle, 0);
var root = effector.parent;
// Get bone transforms in world space
var effectorMat = getAbsWorldMat(actionMats, effector).clone();
var rootMat = getAbsWorldMat(actionMats, root).clone();
// Get bone lenghts
var effectorLen = effector.bone_length * effectorMat.getScale().x;
var rootLen = root.bone_length * rootMat.getScale().x;
// Get distance form root to goal
var goalLen = Math.abs(Vec4.distance(rootMat.getLoc(), goal));
var totalLength = effectorLen + rootLen;
// Get tip location of effector bone
var effectorTipPos = new Vec4().setFrom(effectorMat.look()).normalize();
effectorTipPos.mult(effectorLen);
effectorTipPos.add(effectorMat.getLoc());
// Get unit vector from root to effector tip
var vectorRootEffector = new Vec4().setFrom(effectorTipPos).sub(rootMat.getLoc());
vectorRootEffector.normalize();
// Get unit vector from root to goal
var vectorGoal = new Vec4().setFrom(goal).sub(rootMat.getLoc());
vectorGoal.normalize();
// Get unit vector of root bone
var vectorRoot = new Vec4().setFrom(rootMat.look()).normalize();
// Get unit vector of effector bone
var vectorEffector = new Vec4().setFrom(effectorMat.look()).normalize();
// Get dot product of vectors
var dot = new Vec4().setFrom(vectorRootEffector).dot(vectorRoot);
// Calmp between -1 and 1
dot = dot < -1.0 ? -1.0 : dot > 1.0 ? 1.0 : dot;
// Gat angle A1
var angleA1 = Math.acos(dot);
// Get angle A2
dot = new Vec4().setFrom(vectorRoot).mult(-1.0).dot(vectorEffector);
dot = dot < -1.0 ? -1.0 : dot > 1.0 ? 1.0 : dot;
var angleA2 = Math.acos(dot);
// Get angle A3
dot = new Vec4().setFrom(vectorRootEffector).dot(vectorGoal);
dot = dot < -1.0 ? -1.0 : dot > 1.0 ? 1.0 : dot;
var angleA3 = Math.acos(dot);
// Get angle B1
dot = (effectorLen * effectorLen - rootLen * rootLen - goalLen * goalLen) / (-2 * rootLen * goalLen);
dot = dot < -1.0 ? -1.0 : dot > 1.0 ? 1.0 : dot;
var angleB1 = Math.acos(dot);
// Get angle B2
dot = (goalLen * goalLen - rootLen * rootLen - effectorLen * effectorLen) / (-2 * rootLen * effectorLen);
dot = dot < -1.0 ? -1.0 : dot > 1.0 ? 1.0 : dot;
var angleB2 = Math.acos(dot);
// Calculate rotation axes
var axis0 = new Vec4().setFrom(vectorRootEffector).cross(vectorRoot).normalize();
var axis1 = new Vec4().setFrom(vectorRootEffector).cross(vectorGoal).normalize();
// Apply rotations to effector bone
vpos.setFrom(effectorMat.getLoc());
effectorMat.setLoc(new Vec4());
effectorMat.applyQuat(new Quat().fromAxisAngle(axis0, angleB2 - angleA2));
effectorMat.setLoc(vpos);
setBoneMatFromWorldMat(actionMats, effectorMat, effector);
// Apply rotations to root bone
vpos.setFrom(rootMat.getLoc());
rootMat.setLoc(new Vec4());
rootMat.applyQuat(new Quat().fromAxisAngle(axis0, angleB1 - angleA1));
rootMat.applyQuat(new Quat().fromAxisAngle(axis1, angleA3));
rootMat.setLoc(vpos);
setBoneMatFromWorldMat(actionMats, rootMat, root);
// Recalculate new effector matrix
effectorMat.setFrom(getAbsWorldMat(actionMats, effector));
// Check if pole present
if((pole != null) && (goalLen < totalLength)) {
// Calculate new effector tip position
vscl.setFrom(effectorMat.look()).normalize();
vscl.mult(effectorLen);
vscl.add(effectorMat.getLoc());
// Calculate new effector position from pole
vpos2 = moveTowardPole(rootMat.getLoc(), effectorMat.getLoc(), vscl, pole);
// Orient root bone to new effector position
vpos.setFrom(rootMat.getLoc());
rootMat.setLoc(new Vec4());
vpos3.setFrom(vpos2).sub(vpos).normalize();
rootMat.applyQuat(new Quat().fromTo(rootMat.look().normalize(), vpos3));
rootMat.setLoc(vpos);
// Orient effector bone to new position
vpos.setFrom(effectorMat.getLoc());
effectorMat.setLoc(new Vec4());
vpos3.setFrom(vscl).sub(vpos2).normalize();
effectorMat.applyQuat(new Quat().fromTo(effectorMat.look().normalize(), vpos3));
effectorMat.setLoc(vpos2);
}
// Apply roll to root bone
vpos.setFrom(rootMat.getLoc());
rootMat.setLoc(new Vec4());
rootMat.applyQuat(new Quat().fromAxisAngle(rootMat.look().normalize(), rollAngle));
rootMat.setLoc(vpos);
// Apply roll to effector bone
vpos.setFrom(effectorMat.getLoc());
effectorMat.setLoc(new Vec4());
effectorMat.applyQuat(new Quat().fromAxisAngle(effectorMat.look().normalize(), rollAngle));
effectorMat.setLoc(vpos);
// Finally set root and effector matrices in local space
setBoneMatFromWorldMat(actionMats, rootMat, root);
setBoneMatFromWorldMat(actionMats, effectorMat, effector);
}
}
#end