@@ -0,0 +1,226 @@
package de.blight.game.animation ;
import com.jme3.anim.Armature ;
import com.jme3.anim.Joint ;
import com.jme3.math.FastMath ;
import com.jme3.math.Quaternion ;
import com.jme3.math.Vector3f ;
import com.jme3.renderer.RenderManager ;
import com.jme3.renderer.ViewPort ;
import com.jme3.scene.Spatial ;
import com.jme3.scene.control.AbstractControl ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
/**
* 2-Bone-Foot-IK – hält die Füße an ihrer World-Space-Position fest.
*
* Muss NACH AnimComposer und SkinningControl zur Spatial hinzugefügt werden,
* damit controlUpdate() nach der Animations-Anwendung läuft.
*/
public final class FootIKControl extends AbstractControl {
private static final Logger log = LoggerFactory . getLogger ( FootIKControl . class ) ;
private final Armature armature ;
private final Joint leftThigh , leftCalf , leftFoot ;
private final Joint rightThigh , rightCalf , rightFoot ;
// IK-Ziele in World-Space; null = IK inaktiv
private Vector3f leftWorldTarget = null ;
private Vector3f rightWorldTarget = null ;
private static final float MIN_CORRECTION_DIST = 0 . 005f ;
private int debugFramesLeft = 0 ;
public FootIKControl ( Armature armature ) {
this . armature = armature ;
leftThigh = findJoint ( armature , " L_Thigh " , " LeftUpLeg " , " mixamorig:LeftUpLeg " ) ;
leftCalf = findJoint ( armature , " L_Calf " , " LeftLeg " , " mixamorig:LeftLeg " ) ;
leftFoot = findJoint ( armature , " L_Foot " , " LeftFoot " , " mixamorig:LeftFoot " ) ;
rightThigh = findJoint ( armature , " R_Thigh " , " RightUpLeg " , " mixamorig:RightUpLeg " ) ;
rightCalf = findJoint ( armature , " R_Calf " , " RightLeg " , " mixamorig:RightLeg " ) ;
rightFoot = findJoint ( armature , " R_Foot " , " RightFoot " , " mixamorig:RightFoot " ) ;
log . info ( " [FootIK] Init: L={} R={} " ,
leftFoot ! = null ? leftFoot . getName ( ) : " n/a " ,
rightFoot ! = null ? rightFoot . getName ( ) : " n/a " ) ;
}
/**
* Fixiert die Füße an ihrer aktuellen World-Space-Position.
* Muss aufgerufen werden NACHDEM die neue Animation gesetzt wurde,
* aber BEVOR der kfOffset die Armature verschoben hat.
*/
public void lockFeetAtCurrentWorldPos ( ) {
Spatial sp = getSpatial ( ) ;
if ( sp = = null | | leftFoot = = null | | rightFoot = = null ) {
return ;
}
leftWorldTarget = sp . localToWorld (
leftFoot . getModelTransform ( ) . getTranslation ( ) . clone ( ) , new Vector3f ( ) ) ;
rightWorldTarget = sp . localToWorld (
rightFoot . getModelTransform ( ) . getTranslation ( ) . clone ( ) , new Vector3f ( ) ) ;
debugFramesLeft = 10 ;
log . info ( " [FootIK] LockWorld: L={} R={} " , fv ( leftWorldTarget ) , fv ( rightWorldTarget ) ) ;
}
public void releaseFeet ( ) {
leftWorldTarget = null ;
rightWorldTarget = null ;
debugFramesLeft = 0 ;
log . info ( " [FootIK] Freigegeben " ) ;
}
public boolean isActive ( ) {
return leftWorldTarget ! = null | | rightWorldTarget ! = null ;
}
// ── Control-Update ────────────────────────────────────────────────────────
@Override
protected void controlUpdate ( float tpf ) {
if ( leftWorldTarget = = null & & rightWorldTarget = = null ) {
return ;
}
Spatial sp = getSpatial ( ) ;
if ( sp = = null ) {
return ;
}
boolean dbg = debugFramesLeft > 0 ;
if ( dbg ) {
debugFramesLeft - - ;
}
if ( leftWorldTarget ! = null & & leftThigh ! = null & & leftCalf ! = null & & leftFoot ! = null ) {
Vector3f modelTarget = sp . worldToLocal ( leftWorldTarget , new Vector3f ( ) ) ;
solveLeg ( leftThigh , leftCalf , leftFoot , modelTarget , dbg , " L " ) ;
}
if ( rightWorldTarget ! = null & & rightThigh ! = null & & rightCalf ! = null & & rightFoot ! = null ) {
Vector3f modelTarget = sp . worldToLocal ( rightWorldTarget , new Vector3f ( ) ) ;
solveLeg ( rightThigh , rightCalf , rightFoot , modelTarget , dbg , " R " ) ;
}
armature . update ( ) ;
}
@Override
protected void controlRender ( RenderManager rm , ViewPort vp ) { }
// ── 2-Bone-IK-Solver ─────────────────────────────────────────────────────
private void solveLeg ( Joint thigh , Joint calf , Joint foot ,
Vector3f target , boolean dbg , String side ) {
Vector3f A = thigh . getModelTransform ( ) . getTranslation ( ) . clone ( ) ;
Vector3f B = calf . getModelTransform ( ) . getTranslation ( ) . clone ( ) ;
Vector3f C = foot . getModelTransform ( ) . getTranslation ( ) . clone ( ) ;
float L1 = A . distance ( B ) ;
float L2 = B . distance ( C ) ;
if ( L1 < 0 . 0001f | | L2 < 0 . 0001f ) {
return ;
}
float footErr = C . distance ( target ) ;
if ( dbg ) {
log . info ( " [FootIK][{}] A={} B={} C={} T={} err={} L1={} L2={} " ,
side , fv ( A ) , fv ( B ) , fv ( C ) , fv ( target ) ,
String . format ( " %.4f " , footErr ) ,
String . format ( " %.3f " , L1 ) , String . format ( " %.3f " , L2 ) ) ;
}
if ( footErr < MIN_CORRECTION_DIST ) {
return ;
}
float d = A . distance ( target ) ;
d = FastMath . clamp ( d , Math . abs ( L1 - L2 ) + 0 . 0001f , L1 + L2 - 0 . 0001f ) ;
float cosA = ( L1 * L1 + d * d - L2 * L2 ) / ( 2f * L1 * d ) ;
float angleA = FastMath . acos ( FastMath . clamp ( cosA , - 1f , 1f ) ) ;
Vector3f dirAT = target . subtract ( A ) . normalizeLocal ( ) ;
Vector3f kneeDir = B . subtract ( A ) . normalizeLocal ( ) ;
float proj = dirAT . dot ( kneeDir ) ;
Vector3f perp = kneeDir . subtract ( dirAT . mult ( proj ) ) ;
if ( perp . lengthSquared ( ) < 0 . 0001f ) {
perp = findPerp ( dirAT ) ;
}
perp . normalizeLocal ( ) ;
Vector3f B_new = A . add (
dirAT . mult ( L1 * FastMath . cos ( angleA ) ) . addLocal (
perp . mult ( L1 * FastMath . sin ( angleA ) ) )
) ;
// Oberschenkel rotieren
Vector3f curThighDir = B . subtract ( A ) . normalizeLocal ( ) ;
Vector3f dstThighDir = B_new . subtract ( A ) . normalizeLocal ( ) ;
Quaternion thighArc = rotArc ( curThighDir , dstThighDir ) ;
Quaternion thighModelRot = thigh . getModelTransform ( ) . getRotation ( ) . clone ( ) ;
Quaternion newThighModelRot = thighArc . mult ( thighModelRot ) ;
Joint thighParent = thigh . getParent ( ) ;
Quaternion parentRot = thighParent ! = null
? thighParent . getModelTransform ( ) . getRotation ( ) : new Quaternion ( ) ;
thigh . setLocalRotation ( parentRot . inverse ( ) . mult ( newThighModelRot ) ) ;
// Unterschenkel rotieren — Calf-Richtung NACH der Thigh-Rotation verwenden
Vector3f curCalfDirOrig = C . subtract ( B ) . normalizeLocal ( ) ;
Vector3f curCalfDirRotated = thighArc . mult ( curCalfDirOrig ) ;
Vector3f dstCalfDir = target . subtract ( B_new ) . normalizeLocal ( ) ;
Quaternion calfArc = rotArc ( curCalfDirRotated , dstCalfDir ) ;
Quaternion oldCalfLocalRot = calf . getLocalRotation ( ) . clone ( ) ;
Quaternion actualCalfModel = newThighModelRot . mult ( oldCalfLocalRot ) ;
Quaternion newCalfModelRot = calfArc . mult ( actualCalfModel ) ;
calf . setLocalRotation ( newThighModelRot . inverse ( ) . mult ( newCalfModelRot ) ) ;
if ( dbg ) {
float thighAngle = thighArc . toAngleAxis ( new Vector3f ( ) ) * FastMath . RAD_TO_DEG ;
float calfAngle = calfArc . toAngleAxis ( new Vector3f ( ) ) * FastMath . RAD_TO_DEG ;
log . info ( " [FootIK][{}] B_new={} thigh={}° calf={}° " ,
side , fv ( B_new ) ,
String . format ( " %.1f " , thighAngle ) ,
String . format ( " %.1f " , calfAngle ) ) ;
}
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private static Quaternion rotArc ( Vector3f from , Vector3f to ) {
float dot = FastMath . clamp ( from . dot ( to ) , - 1f , 1f ) ;
if ( dot > 0 . 9999f ) {
return new Quaternion ( ) ;
}
if ( dot < - 0 . 9999f ) {
return new Quaternion ( ) . fromAngleAxis ( FastMath . PI , findPerp ( from ) ) ;
}
Vector3f axis = from . cross ( to ) . normalizeLocal ( ) ;
Quaternion q = new Quaternion ( ) ;
q . fromAngleAxis ( FastMath . acos ( dot ) , axis ) ;
return q ;
}
private static Vector3f findPerp ( Vector3f v ) {
Vector3f p = v . cross ( Vector3f . UNIT_X ) ;
if ( p . lengthSquared ( ) < 0 . 0001f ) {
p = v . cross ( Vector3f . UNIT_Z ) ;
}
return p . normalizeLocal ( ) ;
}
private static Joint findJoint ( Armature arm , String . . . names ) {
for ( String n : names ) {
Joint j = arm . getJoint ( n ) ;
if ( j ! = null ) {
return j ;
}
}
return null ;
}
private static String fv ( Vector3f v ) {
return String . format ( " (%.3f,%.3f,%.3f) " , v . x , v . y , v . z ) ;
}
}