I have a question regarding my quest on onderstanding IK solvers.
After viewing this movie on YouTube: https://www.youtube.com/watch?v=MvuO9ZHGr6k I started recreating it with JavaScript. Just 2d on canvas.
I have come to this point:
function IK(x, y, joints) {
this.location = new Vector(x, y);
// 0: Not visible
// 1: Just bones
// 2: Angle indicators
this.drawLevel = 2;
this.joints = joints;
this.target = new Vector(150, 420);
this.active = this.joints.length - 1;
this.run = true;
this.slow = true;
this.oneByOne = true;
this.attempts = 0;
}
IK.prototype.resetActive = function() {
this.active = this.joints.length - 1;
};
IK.prototype.getAngles = function() {
var activeToTargetAngle = Math.atan2(
this.target.y - (this.anchors[this.active].y + this.location.y),
this.target.x - (this.anchors[this.active].x + this.location.x)
) * (180 / Math.PI);
var endToActiveAngle = this.joints[this.active].angle * (180 / Math.PI);
if (this.active !== (this.anchors.length - 1)) {
endToActiveAngle = Math.atan2(
(this.location.y + this.anchors[this.anchors.length - 1].y) -
(this.location.y + this.anchors[this.active].y),
(this.location.x + this.anchors[this.anchors.length - 1].x) -
(this.location.x + this.anchors[this.active].x)
) * (180 / Math.PI);
}
return {
activeToTarget: activeToTargetAngle,
endToActive: endToActiveAngle
};
};
IK.prototype.makeAnchors = function() {
this.anchors = [];
this.anchors.push(new Vector(0, 0));
this.anchors.push(new Vector(
this.anchors[0].x + Math.cos(this.joints[0].angle) * this.joints[0].length,
this.anchors[0].y + Math.sin(this.joints[0].angle) * this.joints[0].length
));
this.anchors.push(new Vector(
this.anchors[1].x + Math.cos(this.joints[1].angle) * this.joints[1].length,
this.anchors[1].y + Math.sin(this.joints[1].angle) * this.joints[1].length
));
this.anchors.push(new Vector(
this.anchors[2].x + Math.cos(this.joints[2].angle) * this.joints[2].length,
this.anchors[2].y + Math.sin(this.joints[2].angle) * this.joints[2].length
));
this.anchors.push(new Vector(
this.anchors[3].x + Math.cos(this.joints[3].angle) * this.joints[3].length,
this.anchors[3].y + Math.sin(this.joints[3].angle) * this.joints[3].length
));
};
IK.prototype.onTarget = function() {
var endAffector = new Vector(
this.location.x + this.anchors[this.anchors.length -1].x,
this.location.y + this.anchors[this.anchors.length -1].y
);
var targetDifference = this.target.distance(endAffector);
if (targetDifference < 3) {
//console.log('On target, stopping!');
return true;
}
return false;
};
IK.prototype.notResolvable = function() {
if (this.attempts > 5000) {
console.log('Not resolvable, stopping!');
return true;
}
return false;
};
IK.prototype.shift = function() {
this.active -= 1;
if (this.active < 0) {
this.resetActive();
}
};
IK.prototype.step = function() {
this.makeAnchors();
var angle = this.getAngles();
// The angle is different, rotate!
if (angle.endToActive != angle.activeToTarget) {
for (var j = this.active; j < this.joints.length; j++) {
if (angle.endToActive < angle.activeToTarget) {
this.joints[j].angle += 0.01;
} else {
this.joints[j].angle -= 0.01;
}
}
// Done rotating towards
if (this.oneByOne) {
if (Math.floor(angle.endToActive) == Math.floor(angle.activeToTarget)) {
this.shift();
}
} else {
this.shift();
}
}
// Continue?
if (this.onTarget() || this.notResolvable()) {
this.run = false;
}
this.attempts += 1;
};
IK.prototype.direct = function() {
while (this.run) {
this.step();
}
};
IK.prototype.update = function() {
if (this.run) {
if (this.slow) {
this.step();
} else {
this.direct();
}
}
if (this.drawLevel > 0) {
this.draw();
}
};
IK.prototype.draw = function() {
this.drawStart();
this.drawBones();
this.drawConstraints();
this.drawAngleLines();
};
(I left out all the draw functions since make the example more convoluted than needed)
This IK object is initialized as follows:
var ik = new IK(centerX, centerY,
[{
angle: 3.5,
length: 50,
min: Math.PI + -0.2,
max: Math.PI + 0.2
}, {
angle: 2.7,
length: 100,
min: -1,
max: -.3
}, {
angle: 2.5,
length: 100,
min: -1,
max: 0
}, {
angle: 2.5,
length: 50,
min: -0.5,
max: 0.5
}]
);
So far so good. This is working like I see in the example on YouTube but now I wan't to take it a step further by adding angle constraints and this is where I'm getting stuck.
In the joints object that I send to the constructor I added min and max values that are added or subtracted from the current joint angle. Defining a valid rotation range.
After that I added this function, to check if the angle is inside the defined min / max range:
IK.prototype.withinConstraints = function() {
if (this.joints[this.active - 1] !== undefined) {
var min = (this.joints[this.active - 1].angle + this.joints[this.active].min) * (180 / Math.PI);
var max = (this.joints[this.active - 1].angle + this.joints[this.active].max) * (180 / Math.PI);
var angle = this.joints[this.active].angle * (180 / Math.PI);
if (angle < min || angle > max) {
this.shift();
return false;
}
}
}
I change the step function to:
IK.prototype.step = function() {
this.makeAnchors();
var angle = this.getAngles();
// The angle is different, rotate!
if (angle.endToActive != angle.activeToTarget) {
for (var j = this.active; j < this.joints.length; j++) {
this.joints[j].angle =
// Constraints not exeeded?
this.withinConstraints() ?
// Move towards or from target
(
angle.endToActive < angle.activeToTarget ?
this.joints[j].angle + 0.01 : this.joints[j].angle - 0.01
)
// Keep value
: this.joints[j].angle;
}
// Done rotating towards
if (this.oneByOne) {
if (Math.floor(angle.endToActive) == Math.floor(angle.activeToTarget)) {
this.shift();
}
} else {
this.shift();
}
}
// Continue?
if (this.onTarget() || this.notResolvable()) {
this.run = false;
}
this.attempts += 1;
};
This is just one of many attempts to make it work but I realize I simply don't know how to go about it.
This makes the rotation get stuck on one of the max or min angles. After that it won't continue or try to reach the target by going in the opposite direction. (which is logical with the above code).
I think I'm missing logics here:
// Move towards or from target
(
angle.endToActive < angle.activeToTarget ?
this.joints[j].angle + 0.01 : this.joints[j].angle - 0.01
)
The angle.endToActive < angle.activeToTarget is quite arbitrary.
Hopefully there is someone out there that can give me a push in the right direction.
---------- SOLUTION ---------
This is my solution for the constraints, if anyone might be interested.
I added the dir()
function, this helps me find the desired rotation direction.
IK.prototype.dir = function(a, t) {
var theta = t - a;
if (theta < 0) {
theta += 360;
}
if (theta > 180) {
return false;
}
return true;
};
With that I changed the step function:
IK.prototype.step = function() {
this.makeAnchors();
var angle = this.getAngles();
// The angle is different, rotate!
if (angle.endToActive != angle.activeToTarget) {
for (var j = this.active ; j < this.joints.length ; j++) {
var min = 0;
var max = 0;
if (this.joints[j - 1] !== undefined) {
min = this.joints[j - 1].angle + this.joints[j].min;
max = this.joints[j - 1].angle + this.joints[j].max;
} else {
// Start joint
min = this.joints[j].angle + this.joints[j].min;
max = this.joints[j].angle + this.joints[j].max;
}
var a = 0;
if (this.dir(angle.endToActive, angle.activeToTarget)) {
a = this.joints[j].angle + 0.01;
} else {
a = this.joints[j].angle - 0.01;
}
if ((a > min && a < max)) {
this.joints[j].angle = a;
} else {
this.shift();
}
}
// Done rotating towards
if (this.oneByOne) {
if (Math.floor(angle.endToActive) == Math.floor(angle.activeToTarget)) {
this.shift();
}
} else {
this.shift();
}
}
// Continue?
if (this.onTarget() || this.notResolvable()) {
this.run = false;
}
this.attempts += 1;
};
Now I have to optimize this script, I feel it's using more processor power than needed. But that's the next phase.