diff --git a/js/IKSolver.js b/js/IKSolver.js index 4a32aea..2f9e7f4 100644 --- a/js/IKSolver.js +++ b/js/IKSolver.js @@ -381,6 +381,21 @@ BaseSolver.JOINTTYPES = { OMNI: 0, HINGE: 1, BALLSOCKET: 2 }; // omni is just th class FABRIKSolver extends BaseSolver { + poleTarget = null; + poleAngle = 0; + poleRotationMatrix = new THREE.Matrix4(); + tmpVectors = [ + new THREE.Vector3(), + new THREE.Vector3(), + new THREE.Vector3(), + new THREE.Vector3(), + ] + setPoleTarget(obj){ + this.poleTarget = obj; + } + setPoleAngle(angle){ + this.poleAngle = angle; + } update ( ){ let bones = this.skeleton.bones; let positions = this._positions; @@ -455,6 +470,58 @@ class FABRIKSolver extends BaseSolver { targetPositions[ chain[0] ].copy( currTargetPoint ); + //pole Target + if(this.poleTarget && chain.length < 4){ + const polePos = this.tmpVectors[0]; + polePos.setFromMatrixPosition(this.poleTarget.matrixWorld); + + const midPoint = this.tmpVectors[1] + .addVectors(targetPositions[ chain[0] ], targetPositions[ chain[2] ]) + .multiplyScalar(0.5); + + const chainAxis = this.tmpVectors[2] + .subVectors(targetPositions[ chain[2] ], targetPositions[ chain[0] ]) + .normalize(); + + // Calculate pole direction with stability checks + const poleDirection = this.tmpVectors[3] + .subVectors(polePos, midPoint); + + // Check if pole position is too close to the chain axis (causes instability) + const poleDistanceFromAxis = poleDirection.length(); + if (poleDistanceFromAxis < 0.001) { + // Pole is too close to chain axis, skip pole constraint + continue; + } + + // Project pole direction onto plane perpendicular to chain axis + const axialComponent = poleDirection.dot(chainAxis); + poleDirection.addScaledVector(chainAxis, -axialComponent); + + // Check if projected pole direction is too small (near-perpendicular case) + if (poleDirection.lengthSq() < 0.000001) { + // Use a default perpendicular direction to avoid singularity + poleDirection.set(0, 1, 0); + if (Math.abs(chainAxis.dot(poleDirection)) > 0.9) { + poleDirection.set(1, 0, 0); + } + poleDirection.crossVectors(poleDirection, chainAxis); + } + + poleDirection.normalize(); + + const projectedLength = targetPositions[ chain[1] ].distanceTo(midPoint); + + if (this.poleAngle) { + const rotationMatrix = this.poleRotationMatrix.makeRotationAxis(chainAxis, THREE.MathUtils.degToRad(this.poleAngle)); + poleDirection.applyMatrix4(rotationMatrix).normalize(); + } + + targetPositions[ chain[1] ].copy( + midPoint.clone().add(poleDirection.multiplyScalar(projectedLength)) + ); + } + // compute rotations // from parent to effector diff --git a/js/app.js b/js/app.js index 238d86d..217b7f9 100644 --- a/js/app.js +++ b/js/app.js @@ -33,8 +33,8 @@ class App { this.models = [eva, lowPoly, Michelle]; this.currentModel = this.models[1]; - this.gui = new GUI(this); - + this.gui = new GUI(this); + } init() { @@ -119,9 +119,11 @@ class App { this.initLights(); + this.poleTarget = new THREE.Mesh(new THREE.IcosahedronGeometry(0.1)); + this.scene.add(this.poleTarget) //load models and add them to the scene this.initCharacter(); - + } initLights() { @@ -206,6 +208,13 @@ class App { } } + character.skeleton.bones[character.bonesIdxs["LeftForeArm"]].getWorldPosition(this.poleTarget.position) + this.poleTarget.position.z -= 0.2 + this.poleTarget.position.y -= 0.1 + this.poleTarget.userData = {poleAngle: 0} + character.poleTarget = this.poleTarget; + + //Add character to the scene and put it visible if it's the current model selected character.model.name = "Character_" + character.name; character.model.visible = this.currentModel.name == character.name; @@ -321,6 +330,13 @@ class App { transfControl.size = 0.6; transfControl.name = "control"+ chain.name; this.scene.add( transfControl ); + + + let transfControl2 = new TransformControls( this.camera, this.renderer.domElement ); + transfControl2.addEventListener( 'dragging-changed', ( event ) => { this.controls.enabled = ! event.value; } ); + transfControl2.attach( this.poleTarget ); + transfControl2.size = 0.6; + this.scene.add( transfControl2 ); } target.name = 'IKTarget' + chain.name; @@ -355,7 +371,11 @@ class App { } character.chains[ikChain.name] = ikChain; - if ( !character.FABRIKSolver ){ character.FABRIKSolver = new FABRIKSolver( character.skeleton ); } + if ( !character.FABRIKSolver ){ + character.FABRIKSolver = new FABRIKSolver( character.skeleton ); + character.FABRIKSolver.setPoleTarget(character.poleTarget); + character.FABRIKSolver.setPoleAngle(character.poleTarget.userData.poleAngle) + } character.FABRIKSolver.createChain(ikChain.bones, ikChain.constraints, ikChain.target, ikChain.name); if ( !character.CCDIKSolver ){ character.CCDIKSolver = new CCDIKSolver( character.skeleton ); } diff --git a/js/gui.js b/js/gui.js index f1654bb..1f9900e 100644 --- a/js/gui.js +++ b/js/gui.js @@ -209,6 +209,10 @@ class GUI { widgets.on_refresh(); }}); + widgets.addRange("pole Angle", 0, (value, event) => { + this.editor.currentModel.FABRIKSolver.poleAngle = value; + }, { min: -360, max: 360, step: 1, width: "100%" }); + if(newChain.target) { widgets.sameLine(2); widgets.addText("Target", newChain.target == true ? null : newChain.target, null, { width: '80%', disabled: true});