| #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); | |
| Mat2D::invert(item.parentWorldInverse, parentWorld); | |
| Mat2D& boneTransform = bone->mutableTransform(); | |
| boneTransform = item.parentWorldInverse * bone->worldTransform(); | |
| Mat2D::decompose(item.transformComponents, boneTransform); | |
| } | |
| 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]; | |
| Mat2D::invert(fk.parentWorldInverse, getParentWorld(*fk.bone)); | |
| } | |
| } | |
| 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); | |
| } | |
| } | |
| } |