Sicherstellen des Zustands der passt

This commit is contained in:
2026-06-28 21:31:00 +02:00
parent cd350a92fa
commit 6d061cd621
15 changed files with 260 additions and 9 deletions

View File

@@ -1,18 +1,19 @@
{
"clips": [
"get_up_sitting",
"alive_again",
"idle",
"idle_jump",
"pickup",
"running",
"running_jump",
"sitting",
"sit_down_bench",
"sitting_bench",
"sitting_floor",
"sprinting",
"stand_up",
"stand_up_bench",
"tpose",
"walking",
"sit_down_bench"
"walking"
],
"actionMap": {
"DEFAULT": "tpose",
@@ -20,12 +21,13 @@
"WALK": "walking",
"RUN": "running",
"SPRINT": "sprinting",
"RUNNING_JUMP": "running_jump",
"JUMP": "idle_jump",
"RUNNING_JUMP": "running_jump",
"PICK_UP": "pickup",
"SITTING": "sitting",
"SIT_DOWN": "sit_down_bench",
"SIT_UP": "get_up_sitting"
"SIT_UP": "stand_up_bench",
"SITTING": "sitting_bench",
"REVIVE": "alive_again"
},
"previewModelPath": "Models/Chars/mainchar.j3o",
"motionKeyframes": {

View File

@@ -25,6 +25,7 @@
<logger name="com.jme3.util.TangentBinormalGenerator" level="ERROR"/>
<!-- Material warnt bei linear-color-space Texturen ohne passenden Parameter bekannt, kein Fehler -->
<logger name="com.jme3.material.Material" level="ERROR"/>
<logger name="de.blight.game.animation.FootIKControl" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>

View File

@@ -0,0 +1,22 @@
package de.blight.game.animation;
/**
* Positions-/Rotations-Versatz für eine blockierende Animation.
*
* TX/TZ: Versatz in Charakter-lokalem Raum (TX=seitlich, TZ=vorwärts relativ zur Blickrichtung).
* TY: Versatz in Welt-Y (hoch/runter).
* RX/RY/RZ: Additiver Rotations-Versatz in Grad (Euler XYZ, relativ zur Startrotation).
*/
public class AnimOffset {
public float tx, ty, tz; // Positions-Versatz (Meter)
public float rx, ry, rz; // Rotations-Versatz (Grad)
public AnimOffset() {}
public AnimOffset(float tx, float ty, float tz,
float rx, float ry, float rz) {
this.tx = tx; this.ty = ty; this.tz = tz;
this.rx = rx; this.ry = ry; this.rz = rz;
}
}

View File

@@ -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);
}
}

View File

@@ -557,7 +557,7 @@ public class PlayerInputControl {
public void clearKfOffset() {
log.info("[KF] clearKfOffset() → Lerp zu 0, current=({},{},{})", kfOffsetCurrent.x, kfOffsetCurrent.y, kfOffsetCurrent.z);
kfOffsetTarget.set(0, 0, 0);
kfOffsetSpeed = 5.0f; // 0.25m in ~3 Frames bei 60fps
kfOffsetSpeed = 5.0f;
}
private void clearMotionKfOffset() {

View File

@@ -2,7 +2,7 @@
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>