Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hinge Constraints #12

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion build/three-ik.js
Original file line number Diff line number Diff line change
@@ -47,7 +47,7 @@ function setQuaternionFromDirection(direction, up, target) {
var el = m1.elements;
z.copy(direction);
x.crossVectors(up, z);
if (x.lengthSq() === 0) {
if (x.lengthSq() == 0) {
if (Math.abs(up.z) === 1) {
z.x += 0.0001;
} else {
@@ -367,11 +367,14 @@ var IKJoint = function () {
this.bone = bone;
this.distance = 0;
this._originalDirection = new three.Vector3();
this._originalHinge = new three.Vector3();
this._direction = new three.Vector3();
this._worldPosition = new three.Vector3();
this._isSubBase = false;
this._subBasePositions = null;
this.isIKJoint = true;
this._originalUp = new three.Vector3(0, 1, 0);
this._originalUp.applyQuaternion(this.bone.quaternion).normalize();
this._updateWorldPosition();
}
createClass(IKJoint, [{
@@ -553,6 +556,7 @@ var IKChain = function () {
}
else {
var previousJoint = this.joints[this.joints.length - 2];
var previousPreviousJoint = this.joints[this.joints.length - 3];
previousJoint._updateMatrixWorld();
previousJoint._updateWorldPosition();
joint._updateWorldPosition();
@@ -565,6 +569,9 @@ var IKChain = function () {
var direction = previousJoint._getWorldDirection(joint);
previousJoint._originalDirection = new three.Vector3().copy(direction);
joint._originalDirection = new three.Vector3().copy(direction);
if (previousPreviousJoint) {
previousJoint._originalHinge = previousJoint._worldToLocalDirection(previousJoint._originalDirection.clone().cross(previousPreviousJoint._originalDirection).normalize());
}
this.totalLengths += distance;
}
if (target) {
@@ -847,6 +854,44 @@ var IK = function () {
return IK;
}();

var Z_AXIS$1 = new three.Vector3(0, 0, -1);
var X_AXIS = new three.Vector3(1, 0, 0);
var t1$1 = new three.Vector3();
var t2$1 = new three.Vector3();
var t3$1 = new three.Vector3();
var t4 = new three.Vector3();
var RAD2DEG$1 = three.Math.RAD2DEG;
var IKHingeConstraint = function () {
function IKHingeConstraint(angle) {
classCallCheck(this, IKHingeConstraint);
this.angle = angle;
this.rotationPlane = new three.Plane();
}
createClass(IKHingeConstraint, [{
key: '_apply',
value: function _apply(joint) {
var direction = new three.Vector3().copy(joint._getDirection());
var parentDirection = joint._localToWorldDirection(t1$1.copy(Z_AXIS$1)).normalize();
var rotationPlaneNormal = joint._localToWorldDirection(t2$1.copy(joint._originalHinge)).normalize();
this.rotationPlane.normal = rotationPlaneNormal;
var projectedDir = this.rotationPlane.projectPoint(direction, new three.Vector3());
var parentDirectionProjected = this.rotationPlane.projectPoint(parentDirection, t3$1);
var currentAngle = projectedDir.angleTo(parentDirectionProjected) * RAD2DEG$1;
var cross = t4.crossVectors(projectedDir, parentDirectionProjected);
if (cross.dot(rotationPlaneNormal) > 0) {
currentAngle += 180;
}
if (currentAngle > this.angle) {
parentDirectionProjected.applyAxisAngle(rotationPlaneNormal, this.angle / RAD2DEG$1);
joint._setDirection(parentDirectionProjected);
} else {
joint._setDirection(projectedDir);
}
}
}]);
return IKHingeConstraint;
}();

var BoneHelper = function (_Object3D) {
inherits(BoneHelper, _Object3D);
function BoneHelper(height, boneSize, axesSize) {
@@ -1169,13 +1214,15 @@ if (typeof window !== 'undefined' && _typeof(window.THREE) === 'object') {
window.THREE.IKChain = IKChain;
window.THREE.IKJoint = IKJoint;
window.THREE.IKBallConstraint = IKBallConstraint;
window.THREE.IKHingeConstraint = IKHingeConstraint;
window.THREE.IKHelper = IKHelper;
}

exports.IK = IK;
exports.IKChain = IKChain;
exports.IKJoint = IKJoint;
exports.IKBallConstraint = IKBallConstraint;
exports.IKHingeConstraint = IKHingeConstraint;
exports.IKHelper = IKHelper;

Object.defineProperty(exports, '__esModule', { value: true });
52 changes: 49 additions & 3 deletions build/three-ik.module.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxesHelper, Color, ConeBufferGeometry, Math as Math$1, Matrix4, Mesh, MeshBasicMaterial, Object3D, Vector3 } from 'three';
import { AxesHelper, Color, ConeBufferGeometry, Math as Math$1, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Vector3 } from 'three';

var t1 = new Vector3();
var t2 = new Vector3();
@@ -43,7 +43,7 @@ function setQuaternionFromDirection(direction, up, target) {
var el = m1.elements;
z.copy(direction);
x.crossVectors(up, z);
if (x.lengthSq() === 0) {
if (x.lengthSq() == 0) {
if (Math.abs(up.z) === 1) {
z.x += 0.0001;
} else {
@@ -363,11 +363,14 @@ var IKJoint = function () {
this.bone = bone;
this.distance = 0;
this._originalDirection = new Vector3();
this._originalHinge = new Vector3();
this._direction = new Vector3();
this._worldPosition = new Vector3();
this._isSubBase = false;
this._subBasePositions = null;
this.isIKJoint = true;
this._originalUp = new Vector3(0, 1, 0);
this._originalUp.applyQuaternion(this.bone.quaternion).normalize();
this._updateWorldPosition();
}
createClass(IKJoint, [{
@@ -549,6 +552,7 @@ var IKChain = function () {
}
else {
var previousJoint = this.joints[this.joints.length - 2];
var previousPreviousJoint = this.joints[this.joints.length - 3];
previousJoint._updateMatrixWorld();
previousJoint._updateWorldPosition();
joint._updateWorldPosition();
@@ -561,6 +565,9 @@ var IKChain = function () {
var direction = previousJoint._getWorldDirection(joint);
previousJoint._originalDirection = new Vector3().copy(direction);
joint._originalDirection = new Vector3().copy(direction);
if (previousPreviousJoint) {
previousJoint._originalHinge = previousJoint._worldToLocalDirection(previousJoint._originalDirection.clone().cross(previousPreviousJoint._originalDirection).normalize());
}
this.totalLengths += distance;
}
if (target) {
@@ -843,6 +850,44 @@ var IK = function () {
return IK;
}();

var Z_AXIS$1 = new Vector3(0, 0, -1);
var X_AXIS = new Vector3(1, 0, 0);
var t1$1 = new Vector3();
var t2$1 = new Vector3();
var t3$1 = new Vector3();
var t4 = new Vector3();
var RAD2DEG$1 = Math$1.RAD2DEG;
var IKHingeConstraint = function () {
function IKHingeConstraint(angle) {
classCallCheck(this, IKHingeConstraint);
this.angle = angle;
this.rotationPlane = new Plane();
}
createClass(IKHingeConstraint, [{
key: '_apply',
value: function _apply(joint) {
var direction = new Vector3().copy(joint._getDirection());
var parentDirection = joint._localToWorldDirection(t1$1.copy(Z_AXIS$1)).normalize();
var rotationPlaneNormal = joint._localToWorldDirection(t2$1.copy(joint._originalHinge)).normalize();
this.rotationPlane.normal = rotationPlaneNormal;
var projectedDir = this.rotationPlane.projectPoint(direction, new Vector3());
var parentDirectionProjected = this.rotationPlane.projectPoint(parentDirection, t3$1);
var currentAngle = projectedDir.angleTo(parentDirectionProjected) * RAD2DEG$1;
var cross = t4.crossVectors(projectedDir, parentDirectionProjected);
if (cross.dot(rotationPlaneNormal) > 0) {
currentAngle += 180;
}
if (currentAngle > this.angle) {
parentDirectionProjected.applyAxisAngle(rotationPlaneNormal, this.angle / RAD2DEG$1);
joint._setDirection(parentDirectionProjected);
} else {
joint._setDirection(projectedDir);
}
}
}]);
return IKHingeConstraint;
}();

var BoneHelper = function (_Object3D) {
inherits(BoneHelper, _Object3D);
function BoneHelper(height, boneSize, axesSize) {
@@ -1165,7 +1210,8 @@ if (typeof window !== 'undefined' && _typeof(window.THREE) === 'object') {
window.THREE.IKChain = IKChain;
window.THREE.IKJoint = IKJoint;
window.THREE.IKBallConstraint = IKBallConstraint;
window.THREE.IKHingeConstraint = IKHingeConstraint;
window.THREE.IKHelper = IKHelper;
}

export { IK, IKChain, IKJoint, IKBallConstraint, IKHelper };
export { IK, IKChain, IKJoint, IKBallConstraint, IKHingeConstraint, IKHelper };
125 changes: 125 additions & 0 deletions examples/hinge-constraints.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>THREE.IK - hinge constraint test</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
font-family: Monospace;
background-color: #000;
color: #fff;
margin: 0px;
overflow: hidden;
}

#info {
color: #fff;
position: absolute;
top: 10px;
width: 100%;
text-align: center;
z-index: 100;
display: block;
}

#info a {
color: #046;
font-weight: bold;
}

</style>
</head>

<body>
<script src="node_modules/dat.gui/build/dat.gui.js"></script>
<script src="node_modules/three/build/three.js"></script>
<script src="node_modules/three/examples/js/controls/OrbitControls.js"></script>
<script src="node_modules/three/examples/js/loaders/FBXLoader.js"></script>
<script src="scripts/IKApp.js"></script>
<script src="scripts/AxisUtils.js"></script>

<script src="../build/three-ik.js"></script>
<script>
class SingleTargetApp extends IKApp {
setupGUI() {
this.config.constraintType = 'ball';
this.config.constraintAngle = 90;

}

setupIK() {

var fbxLoader = new THREE.FBXLoader();
fbxLoader.load('assets/rigTest-02.fbx', (rigGroup) => {
this.scene.add(rigGroup)
rigGroup.updateMatrixWorld()

var rigMesh = rigGroup.children[0];
var rootBone = rigGroup.children[1].children[0]

/**
This model's bind matrices are -Z forward, while the IK system operates in +Z Forward -- assuming
the parent's +Z forward faces the child. This utility helps us recalculate the models bone matrices
to be consistent with the IK system.
**/
setZForward(rootBone);
//must rebind the skeleton to reset the bind matrix.
rigMesh.bind(rigMesh.skeleton)

rigMesh.material = new THREE.MeshPhysicalMaterial({
skinning: true,
wireframe: true
})

this.target = new THREE.Mesh(
new THREE.SphereGeometry(0.5),
new THREE.MeshBasicMaterial({wireframe: true})
);
this.target.position.set(1,0,5.5)
this.scene.add(this.target)

var boneGroup = rootBone;
var ik = new IK.IK();
const chain = new IK.IKChain();
var currentBone = boneGroup;
var constraintArray = [
new IK.IKBallConstraint(90),
new IK.IKHingeConstraint(180),
new IK.IKHingeConstraint(180),
new IK.IKHingeConstraint(100),
];

for (let i = 0; i < 5; i++) {
const target = i === 4 ? this.target : null;
const constraints = [constraintArray[i]];
chain.add(new IK.IKJoint(currentBone, { constraints }), { target });
currentBone = currentBone.children[0]
}
ik.add(chain);
const helper = new IK.IKHelper(ik);
this.scene.add(helper);
this.iks.push(ik)

}, console.log, console.log);
}

onChange() {
super.onChange();
}

update() {
if(this.target){
this.target.position.y = 2 + 2 * Math.sin(0.003 * performance.now())
this.target.position.x = 1 + 2 * Math.sin(0.003 * performance.now())
}
}
};

window.app = new SingleTargetApp();

</script>
</body>

</html>
3 changes: 2 additions & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
@@ -248,7 +248,8 @@ <h1><a href="http://jsantell.github.io/THREE.IK">THREE<span>.IK</span></a></h1>
"single-target",
"multi-effector",
"readme-example",
"skinned-mesh"
"skinned-mesh",
"hinge-constraints"
]
};
function extractQuery() {
104 changes: 68 additions & 36 deletions examples/scripts/AxisUtils.js
Original file line number Diff line number Diff line change
@@ -7,10 +7,10 @@
*
**/

const t = new THREE.Vector3();
const q = new THREE.Quaternion();
const p = new THREE.Plane();
const FORWARD = new THREE.Vector3(0,0,1);
const t1 = new THREE.Vector3();
const t2 = new THREE.Vector3();
const t3 = new THREE.Vector3();
const m1 = new THREE.Matrix4();
var RESETQUAT = new THREE.Quaternion();

/**
@@ -23,18 +23,17 @@ var RESETQUAT = new THREE.Quaternion();
* @param {THREE.BONE} rootBone
*/

function setZForward(rootBone) {
var worldPos = {};
getOriginalWorldPositions(rootBone, worldPos);
updateTransformations(rootBone, worldPos);
function setZForward(rootBone, scene) {
var worldPos = {}
getOriginalWorldPositions(rootBone, worldPos)
updateTransformations(rootBone, worldPos, scene);
}

function updateTransformations(parentBone, worldPos) {

function updateTransformations(parentBone, worldPos, scene) {
var averagedDir = new THREE.Vector3();
parentBone.children.forEach((childBone) => {
//average the child bone world pos
var childBonePosWorld = worldPos[childBone.id];
var childBonePosWorld = worldPos[childBone.id][0];
averagedDir.add(childBonePosWorld);
});

@@ -43,41 +42,74 @@ function updateTransformations(parentBone, worldPos) {
//set quaternion
parentBone.quaternion.copy(RESETQUAT);
parentBone.updateMatrixWorld();

//get the child bone position in local coordinates
var childBoneDir = parentBone.worldToLocal(averagedDir).normalize();
//set the direction to child bone to the forward direction
var quat = getAlignmentQuaternion(FORWARD, childBoneDir);
if (quat) {
//rotate parent bone towards child bone
parentBone.quaternion.premultiply(quat);
parentBone.updateMatrixWorld();
//set child bone position relative to the new parent matrix.
parentBone.children.forEach((childBone) => {
var childBonePosWorld = worldPos[childBone.id].clone();
parentBone.worldToLocal(childBonePosWorld);
childBone.position.copy(childBonePosWorld);
});
}
var childBoneDir = parentBone.worldToLocal(averagedDir.clone()).normalize();

//set direction to face child
setQuaternionFromDirection(childBoneDir, Y_AXIS, parentBone.quaternion)
parentBone.updateMatrixWorld();

//set child bone position relative to the new parent matrix.
parentBone.children.forEach((childBone) => {
updateTransformations(childBone, worldPos);
var childBonePosWorld = worldPos[childBone.id][0].clone();
parentBone.worldToLocal(childBonePosWorld);
childBone.position.copy(childBonePosWorld);
});

parentBone.children.forEach((childBone) => {
updateTransformations(childBone, worldPos, scene);
})
}

function getAlignmentQuaternion(fromDir, toDir) {
const adjustAxis = t.crossVectors(fromDir, toDir).normalize();
const adjustAngle = fromDir.angleTo(toDir);
if (adjustAngle) {
const adjustQuat = q.setFromAxisAngle(adjustAxis, adjustAngle);
return adjustQuat;
//borrowing this from utils.js , not sure how to import it
function setQuaternionFromDirection(direction, up, target) {
const x = t1;
const y = t2;
const z = t3;
const m = m1;
const el = m1.elements;

z.copy(direction);
x.crossVectors(up, z);

if (x.lengthSq() === 0) {
// parallel
if (Math.abs(up.z) === 1) {
z.x += 0.0001;
} else {
z.z += 0.0001;
}
z.normalize();
x.crossVectors(up, z);
}
return null;

x.normalize();
y.crossVectors(z, x);

el[ 0 ] = x.x; el[ 4 ] = y.x; el[ 8 ] = z.x;
el[ 1 ] = x.y; el[ 5 ] = y.y; el[ 9 ] = z.y;
el[ 2 ] = x.z; el[ 6 ] = y.z; el[ 10 ] = z.z;

target.setFromRotationMatrix(m);
}

function getOriginalWorldPositions(rootBone, worldPos) {
var rootBoneWorldPos = rootBone.getWorldPosition(new THREE.Vector3())
worldPos[rootBone.id] = [rootBoneWorldPos];
rootBone.children.forEach((child) => {
var childWorldPos = child.getWorldPosition(new THREE.Vector3());
worldPos[child.id] = childWorldPos;
getOriginalWorldPositions(child, worldPos);
getOriginalWorldPositions(child, worldPos)
})
}

function _worldToLocalDirection(direction, parent) {
const inverseParent = new THREE.Matrix4().getInverse(parent.matrixWorld);
direction.transformDirection(inverseParent);
return direction;
}

function _localToWorldDirection(direction, parent) {
const parentMat = parent.matrixWorld;
direction.transformDirection(parentMat);
return direction;
}
5 changes: 5 additions & 0 deletions src/IKChain.js
Original file line number Diff line number Diff line change
@@ -60,6 +60,8 @@ class IKChain {
// and update the total length.
else {
const previousJoint = this.joints[this.joints.length - 2];
const previousPreviousJoint = this.joints[this.joints.length - 3];

previousJoint._updateMatrixWorld();
previousJoint._updateWorldPosition();
joint._updateWorldPosition();
@@ -75,6 +77,9 @@ class IKChain {
previousJoint._originalDirection = new Vector3().copy(direction);
joint._originalDirection = new Vector3().copy(direction);

if(previousPreviousJoint){
previousJoint._originalHinge = previousJoint._worldToLocalDirection(previousJoint._originalDirection.clone().cross(previousPreviousJoint._originalDirection).normalize());
}
this.totalLengths += distance;
}

62 changes: 62 additions & 0 deletions src/IKHingeConstraint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Quaternion, Vector3, Plane, Math as ThreeMath } from 'three';

const Z_AXIS = new Vector3(0, 0, -1);
const X_AXIS = new Vector3(1, 0, 0);

const t1 = new Vector3();
const t2 = new Vector3();
const t3 = new Vector3();
const t4 = new Vector3();

const { DEG2RAD, RAD2DEG } = ThreeMath;

/**
* A class for a constraint.
*/
class IKHingeConstraint {
/**
* Pass in an angle value in degrees,
* Axis of rotation for the constraint is calculated from initial bone positions.
*
* @param {number} angle
*/
constructor(angle) {
this.angle = angle;
this.rotationPlane = new Plane();
}

/**
* Applies a hinge constraint to passed in IKJoint. The direction will always be updated
* with this constraint, because it will always be projected onto the rotation plane.
* Additionally, an angle constraint will be applied if necessary.
*
* @param {IKJoint} joint
* @private
*/
_apply(joint) {
// Get direction of joint and parent in world space
const direction = new Vector3().copy(joint._getDirection());
const parentDirection = joint._localToWorldDirection(t1.copy(Z_AXIS)).normalize();
const rotationPlaneNormal = joint._localToWorldDirection(t2.copy(joint._originalHinge)).normalize();
this.rotationPlane.normal = rotationPlaneNormal;
var projectedDir = this.rotationPlane.projectPoint(direction, new Vector3())

var parentDirectionProjected = this.rotationPlane.projectPoint(parentDirection, t3)
var currentAngle = projectedDir.angleTo(parentDirectionProjected) * RAD2DEG;

//apply adjustment to angle if it is "negative"
var cross = t4.crossVectors(projectedDir, parentDirectionProjected);
if(cross.dot(rotationPlaneNormal) > 0){
currentAngle += 180;
}

if(currentAngle > this.angle){
parentDirectionProjected.applyAxisAngle(rotationPlaneNormal, this.angle/RAD2DEG);
joint._setDirection(parentDirectionProjected);
} else {
joint._setDirection(projectedDir);
}
}
}

export default IKHingeConstraint;
4 changes: 3 additions & 1 deletion src/IKJoint.js
Original file line number Diff line number Diff line change
@@ -21,12 +21,15 @@ class IKJoint {
this.distance = 0;

this._originalDirection = new Vector3();
this._originalHinge = new Vector3();
this._direction = new Vector3();
this._worldPosition = new Vector3();
this._isSubBase = false;
this._subBasePositions = null;
this.isIKJoint = true;

this._originalUp = new Vector3(0,1,0);
this._originalUp.applyQuaternion(this.bone.quaternion).normalize();
this._updateWorldPosition();
}

@@ -179,7 +182,6 @@ class IKJoint {

this._worldToLocalDirection(direction);
setQuaternionFromDirection(direction, Y_AXIS, this.bone.quaternion);

} else {
this.bone.position.copy(position);
}
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import IK from './IK.js';
import IKChain from './IKChain.js';
import IKJoint from './IKJoint.js';
import IKBallConstraint from './IKBallConstraint.js';
import IKHingeConstraint from './IKHingeConstraint.js';
import IKHelper from './IKHelper.js';

// If this is being included via script tag and using THREE
@@ -11,9 +12,10 @@ if (typeof window !== 'undefined' && typeof window.THREE === 'object') {
window.THREE.IKChain = IKChain;
window.THREE.IKJoint = IKJoint;
window.THREE.IKBallConstraint = IKBallConstraint;
window.THREE.IKHingeConstraint = IKHingeConstraint;
window.THREE.IKHelper = IKHelper;
}

export {
IK, IKChain, IKJoint, IKBallConstraint, IKHelper
IK, IKChain, IKJoint, IKBallConstraint, IKHingeConstraint, IKHelper
};
1 change: 1 addition & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ export function setQuaternionFromDirection(direction, up, target) {
z.copy(direction);
x.crossVectors(up, z);


if (x.lengthSq() === 0) {
// parallel
if (Math.abs(up.z) === 1) {