UNPKG

@dannadori/three-ik

Version:

inverse kinematics for three.js

264 lines (221 loc) 7.64 kB
import { Vector3 } from 'three'; import IKJoint from './IKJoint.js'; import { getCentroid } from './utils.js'; /** * Class representing an IK chain, comprising multiple IKJoints. */ class IKChain { /** * Create an IKChain. */ constructor() { this.isIKChain = true; this.totalLengths = 0; this.base = null; this.effector = null; this.effectorIndex = null; this.chains = new Map(); /* THREE.Vector3 world position of base node */ this.origin = null; this.iterations = 100; this.tolerance = 0.01; this._depth = -1; this._targetPosition = new Vector3(); } /** * Add an IKJoint to the end of this chain. * * @param {IKJoint} joint * @param {Object} config * @param {THREE.Object3D} [config.target] */ add(joint, { target } = {}) { if (this.effector) { throw new Error('Cannot add additional joints to a chain with an end effector.'); } if (!joint.isIKJoint) { if (joint.isBone) { joint = new IKJoint(joint); } else { throw new Error('Invalid joint in an IKChain. Must be an IKJoint or a THREE.Bone.'); } } this.joints = this.joints || []; this.joints.push(joint); // If this is the first joint, set as base. if (this.joints.length === 1) { this.base = this.joints[0]; this.origin = new Vector3().copy(this.base._getWorldPosition()); } // Otherwise, calculate the distance for the previous joint, // and update the total length. else { const previousJoint = this.joints[this.joints.length - 2]; previousJoint._updateMatrixWorld(); previousJoint._updateWorldPosition(); joint._updateWorldPosition(); const distance = previousJoint._getWorldDistance(joint); if (distance === 0) { throw new Error('bone with 0 distance between adjacent bone found'); }; joint._setDistance(distance); joint._updateWorldPosition(); const direction = previousJoint._getWorldDirection(joint); previousJoint._originalDirection = new Vector3().copy(direction); joint._originalDirection = new Vector3().copy(direction); this.totalLengths += distance; } if (target) { this.effector = joint; this.effectorIndex = joint; this.target = target; } return this; } /** * Returns a boolean indicating whether or not this chain has an end effector. * * @private * @return {boolean} */ _hasEffector() { return !!this.effector; } /** * Returns the distance from the end effector to the target. Returns -1 if * this chain does not have an end effector. * * @private * @return {number} */ _getDistanceFromTarget() { return this._hasEffector() ? this.effector._getWorldDistance(this.target) : -1; } /** * Connects another IKChain to this chain. The additional chain's root * joint must be a member of this chain. * * @param {IKChain} chain */ connect(chain) { if (!chain.isIKChain) { throw new Error('Invalid connection in an IKChain. Must be an IKChain.'); } if (!chain.base.isIKJoint) { throw new Error('Connecting chain does not have a base joint.'); } const index = this.joints.indexOf(chain.base); // If we're connecting to the last joint in the chain, ensure we don't // already have an effector. if (this.target && index === this.joints.length - 1) { throw new Error('Cannot append a chain to an end joint in a chain with a target.'); } if (index === -1) { throw new Error('Cannot connect chain that does not have a base joint in parent chain.'); } this.joints[index]._setIsSubBase(); let chains = this.chains.get(index); if (!chains) { chains = []; this.chains.set(index, chains); } chains.push(chain); return this; } /** * Update joint world positions for this chain. * * @private */ _updateJointWorldPositions() { for (let joint of this.joints) { joint._updateWorldPosition(); } } /** * Runs the forward pass of the FABRIK algorithm. * * @private */ _forward() { // Copy the origin so the forward step can use before `_backward()` // modifies it. this.origin.copy(this.base._getWorldPosition()); // Set the effector's position to the target's position. if (this.target) { this._targetPosition.setFromMatrixPosition(this.target.matrixWorld); this.effector._setWorldPosition(this._targetPosition); } else if (!this.joints[this.joints.length - 1]._isSubBase) { // If this chain doesn't have additional chains or a target, // not much to do here. return; } // Apply sub base positions for all joints except the base, // as we want to possibly write to the base's sub base positions, // not read from it. for (let i = 1; i < this.joints.length; i++) { const joint = this.joints[i]; if (joint._isSubBase) { joint._applySubBasePositions(); } } for (let i = this.joints.length - 1; i > 0; i--) { const joint = this.joints[i]; const prevJoint = this.joints[i - 1]; const direction = prevJoint._getWorldDirection(joint); const worldPosition = direction.multiplyScalar(joint.distance).add(joint._getWorldPosition()); // If this chain's base is a sub base, set it's position in // `_subBaseValues` so that the forward step of the parent chain // can calculate the centroid and clear the values. // @TODO Could this have an issue if a subchain `x`'s base // also had its own subchain `y`, rather than subchain `x`'s // parent also being subchain `y`'s parent? if (prevJoint === this.base && this.base._isSubBase) { this.base._subBasePositions.push(worldPosition); } else { prevJoint._setWorldPosition(worldPosition); } } } /** * Runs the backward pass of the FABRIK algorithm. * * @private */ _backward() { // If base joint is a sub base, don't reset it's position back // to the origin, but leave it where the parent chain left it. if (!this.base._isSubBase) { this.base._setWorldPosition(this.origin); } for (let i = 0; i < this.joints.length - 1; i++) { const joint = this.joints[i]; const nextJoint = this.joints[i + 1]; const jointWorldPosition = joint._getWorldPosition(); const direction = nextJoint._getWorldDirection(joint); joint._setDirection(direction); joint._applyConstraints(); direction.copy(joint._direction); // Now apply the world position to the three.js matrices. We need // to do this before the next joint iterates so it can generate rotations // in local space from its parent's matrixWorld. // If this is a chain sub base, let the parent chain apply the world position if (!(this.base === joint && joint._isSubBase)) { joint._applyWorldPosition(); } nextJoint._setWorldPosition(direction.multiplyScalar(nextJoint.distance).add(jointWorldPosition)); // Since we don't iterate over the last joint, handle the applying of // the world position. If it's also a non-effector, then we must orient // it to its parent rotation since otherwise it has nowhere to point to. if (i === this.joints.length - 2) { if (nextJoint !== this.effector) { nextJoint._setDirection(direction); } nextJoint._applyWorldPosition(); } } return this._getDistanceFromTarget(); } } export default IKChain;