blob: fc75ff0cf39cef95aea77f8d7813799f3cb45a37 [file] [log] [blame]
#include "rive/constraints/ik_constraint.hpp"
#include "rive/bones/bone.hpp"
#include "rive/artboard.hpp"
#include <algorithm>
#include <math.h> // M_PI
using namespace rive;
void IKConstraint::buildDependencies() {
Super::buildDependencies();
// IK Constraint needs to depend on the target so that world transform
// changes can propagate to the bones (and they can be reset before IK
// runs).
if (m_Target != nullptr) {
m_Target->addDependent(this);
}
}
StatusCode IKConstraint::onAddedClean(CoreContext* context) {
if (!parent()->is<Bone>()) {
return StatusCode::InvalidObject;
}
auto boneCount = parentBoneCount();
auto bone = parent()->as<Bone>();
std::vector<Bone*> bones;
bones.push_back(bone);
// Get the reverse FK chain of bones (from tip up).
while (bone->parent()->is<Bone>() && boneCount > 0) {
boneCount--;
bone = bone->parent()->as<Bone>();
bone->addPeerConstraint(this);
bones.push_back(bone);
}
int numBones = (int)bones.size();
m_FkChain.resize(numBones);
// Now put them in FK order (top to bottom).
int idx = 0;
for (auto boneItr = bones.rbegin(); boneItr != bones.rend(); ++boneItr) {
BoneChainLink& link = m_FkChain[idx];
link.index = idx++;
link.bone = *boneItr;
link.angle = 0.0f;
}
// Make sure all of the first level children of each bone depend on the
// tip (constrainedComponent).
auto tip = parent()->as<Bone>();
auto artboard = static_cast<Artboard*>(context);
// Find all children of this bone (we don't directly build up
// hierarchy at runtime, so we have to traverse everything and check
// parents).
for (auto core : artboard->objects()) {
if (core == nullptr || !core->is<TransformComponent>()) {
continue;
}
auto transformComponent = core->as<TransformComponent>();
for (int i = 1; i < numBones; i++) {
auto bone = bones[i];
if (transformComponent->parent() == bone &&
std::find(bones.begin(), bones.end(), transformComponent) == bones.end())
{
tip->addDependent(transformComponent);
}
}
}
return Super::onAddedClean(context);
}
void IKConstraint::markConstraintDirty() {
Super::markConstraintDirty();
// We automatically propagate dirt to the parent constrained bone, but we
// also need to make sure the other bones we influence above it rebuild
// their transforms.
for (int i = 0, length = (int)m_FkChain.size() - 1; i < length; i++) {
m_FkChain[i].bone->markTransformDirty();
}
}
void IKConstraint::solve1(BoneChainLink* fk1, const Vec2D& worldTargetTranslation) {
Mat2D iworld = fk1->parentWorldInverse;
Vec2D pA;
fk1->bone->worldTranslation(pA);
Vec2D pBT(worldTargetTranslation);
// To target in worldspace
const Vec2D toTarget = pBT - pA;
// Note this is directional, hence not transformMat2d
Vec2D toTargetLocal = Vec2D::transformDir(toTarget, iworld);
float r = std::atan2(toTargetLocal[1], toTargetLocal[0]);
constrainRotation(*fk1, r);
fk1->angle = r;
}
void IKConstraint::solve2(BoneChainLink* fk1,
BoneChainLink* fk2,
const Vec2D& worldTargetTranslation) {
Bone* b1 = fk1->bone;
Bone* b2 = fk2->bone;
BoneChainLink* firstChild = &(m_FkChain[fk1->index + 1]);
const Mat2D& iworld = fk1->parentWorldInverse;
Vec2D pA, pC, pB;
b1->worldTranslation(pA);
firstChild->bone->worldTranslation(pC);
b2->tipWorldTranslation(pB);
Vec2D pBT(worldTargetTranslation);
pA = iworld * pA;
pC = iworld * pC;
pB = iworld * pB;
pBT = iworld * pBT;
// http://mathworld.wolfram.com/LawofCosines.html
Vec2D av = pB - pC, bv = pC - pA, cv = pBT - pA;
float a = av.length(), b = bv.length(), c = cv.length();
float A = std::acos(std::max(-1.0f, std::min(1.0f, (-a * a + b * b + c * c) / (2.0f * b * c))));
float C = std::acos(std::max(-1.0f, std::min(1.0f, (a * a + b * b - c * c) / (2.0f * a * b))));
float r1, r2;
if (b2->parent() != b1) {
BoneChainLink& secondChild = m_FkChain[fk1->index + 2];
const Mat2D& secondChildWorldInverse = secondChild.parentWorldInverse;
firstChild->bone->worldTranslation(pC);
b2->tipWorldTranslation(pB);
Vec2D avLocal = Vec2D::transformDir(pB - pC, secondChildWorldInverse);
float angleCorrection = -std::atan2(avLocal[1], avLocal[0]);
if (invertDirection()) {
r1 = std::atan2(cv[1], cv[0]) - A;
r2 = -C + M_PI + angleCorrection;
} else {
r1 = A + std::atan2(cv[1], cv[0]);
r2 = C - M_PI + angleCorrection;
}
} else if (invertDirection()) {
r1 = std::atan2(cv[1], cv[0]) - A;
r2 = -C + M_PI;
} else {
r1 = A + std::atan2(cv[1], cv[0]);
r2 = C - M_PI;
}
constrainRotation(*fk1, r1);
constrainRotation(*firstChild, r2);
if (firstChild != fk2) {
Bone* bone = fk2->bone;
bone->mutableWorldTransform() = getParentWorld(*bone) * bone->transform();
}
// Simple storage, need this for interpolation.
fk1->angle = r1;
firstChild->angle = r2;
}
void IKConstraint::constrainRotation(BoneChainLink& fk, float rotation) {
Bone* bone = fk.bone;
const Mat2D& parentWorld = getParentWorld(*bone);
Mat2D& transform = bone->mutableTransform();
TransformComponents& c = fk.transformComponents;
transform = Mat2D::fromRotation(rotation);
// Translate
transform[4] = c.x();
transform[5] = c.y();
// Scale
float scaleX = c.scaleX();
float scaleY = c.scaleY();
transform[0] *= scaleX;
transform[1] *= scaleX;
transform[2] *= scaleY;
transform[3] *= scaleY;
// Skew
const float skew = c.skew();
if (skew != 0.0f) {
transform[2] = transform[0] * skew + transform[2];
transform[3] = transform[1] * skew + transform[3];
}
bone->mutableWorldTransform() = parentWorld * transform;
}
void IKConstraint::constrain(TransformComponent* component) {
if (m_Target == nullptr) {
return;
}
Vec2D worldTargetTranslation;
m_Target->worldTranslation(worldTargetTranslation);
// Decompose the chain.
for (BoneChainLink& item : m_FkChain) {
auto bone = item.bone;
const Mat2D& parentWorld = getParentWorld(*bone);
item.parentWorldInverse = parentWorld.invertOrIdentity();
Mat2D& boneTransform = bone->mutableTransform();
boneTransform = item.parentWorldInverse * bone->worldTransform();
item.transformComponents = boneTransform.decompose();
}
int count = (int)m_FkChain.size();
switch (count) {
case 1:
solve1(&m_FkChain[0], worldTargetTranslation);
break;
case 2:
solve2(&m_FkChain[0], &m_FkChain[1], worldTargetTranslation);
break;
default: {
auto last = count - 1;
BoneChainLink* tip = &m_FkChain[last];
for (int i = 0; i < last; i++) {
BoneChainLink* item = &m_FkChain[i];
solve2(item, tip, worldTargetTranslation);
for (int j = item->index + 1, end = m_FkChain.size() - 1; j < end; j++) {
BoneChainLink& fk = m_FkChain[j];
fk.parentWorldInverse = getParentWorld(*fk.bone).invertOrIdentity();
}
}
break;
}
}
// At the end, mix the FK angle with the IK angle by strength
if (strength() != 1.0f) {
for (BoneChainLink& fk : m_FkChain) {
float fromAngle = std::fmod(fk.transformComponents.rotation(), (float)M_PI * 2);
float toAngle = std::fmod(fk.angle, (float)M_PI * 2);
float diff = toAngle - fromAngle;
if (diff > M_PI) {
diff -= M_PI * 2;
} else if (diff < -M_PI) {
diff += M_PI * 2;
}
float angle = fromAngle + diff * strength();
constrainRotation(fk, angle);
}
}
}