From 197a820ef9cb8de80cc2d757aa2fcbb93ac73f7e Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 21 Jun 2026 11:35:12 +0200 Subject: [PATCH] =?UTF-8?q?Option=20B:=20RootDriftCompensatorControl=20ver?= =?UTF-8?q?hindert=20Mesh-Drift=20ohne=20Animation=20zu=20ver=C3=A4ndern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues Control auf dem Modell-Spatial (nach AnimComposer hinzugefügt): - Läuft nach AnimComposer aber vor SkinningControl → liest aktuellen Frame - Findet den flachsten Knochen mit nicht-nullem Model-Space XZ (= Hüft-Bone) - Verschiebt den Modell-Spatial in die entgegengesetzte Richtung - Mesh bleibt immer über der Physik-Kapsel; Y (sit_down/Jump) wird nicht berührt - Animations-Daten, J3Os und AnimationLibrary bleiben unverändert Co-Authored-By: Claude Sonnet 4.6 --- .../game/animation/AnimationLibrary.java | 1 - .../RootDriftCompensatorControl.java | 103 ++++++++++++++++++ .../java/de/blight/game/scene/WorldScene.java | 4 + 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 blight-game/src/main/java/de/blight/game/animation/RootDriftCompensatorControl.java diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java index 0155f1a..844ff31 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java @@ -117,7 +117,6 @@ public class AnimationLibrary extends BaseAppState { return false; } - target = snapRootBoneXZ(target, sc.getArmature()); ac.addAnimClip(target); log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName()); if (clipName.equals("sit_down")) { diff --git a/blight-game/src/main/java/de/blight/game/animation/RootDriftCompensatorControl.java b/blight-game/src/main/java/de/blight/game/animation/RootDriftCompensatorControl.java new file mode 100644 index 0000000..3d8c7fe --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/animation/RootDriftCompensatorControl.java @@ -0,0 +1,103 @@ +package de.blight.game.animation; + +import com.jme3.anim.Armature; +import com.jme3.anim.Joint; +import com.jme3.anim.SkinningControl; +import com.jme3.math.Vector3f; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.control.AbstractControl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Verhindert visuellen Root-Motion-Drift, ohne Animations-Daten zu verändern. + * + * Dieses Control muss auf dem selben Spatial wie AnimComposer liegen und NACH + * AnimComposer hinzugefügt werden. Die Update-Reihenfolge ist dann: + * 1. AnimComposer – setzt Joint-Locals und propagiert Model-Transforms + * 2. RootDriftCompensator – liest aktuellen Frame-Knochen-XZ, korrigiert Spatial-XZ + * 3. SkinningControl (Kind) – skinnt mit korrekten Model-Transforms + * + * Mechanismus: Wenn der "Hüft-Knochen" (tiefster sinnvoller Root) im Model-Space + * in XZ driftet, verschiebt dieses Control den Spatial in die entgegengesetzte + * Richtung. Netto bleibt das Mesh immer über der Physik-Kapsel. + * Y (Höhenachse für sit_down/Jump) wird NICHT angefasst – applyBoneAnchorOffset + * in PlayerInputControl ist weiterhin allein zuständig. + */ +public class RootDriftCompensatorControl extends AbstractControl { + + private static final Logger log = LoggerFactory.getLogger(RootDriftCompensatorControl.class); + + private final float offsetY; // Y-Versatz des Modells (Füße auf Kapsel-Unterkante) + private Armature armature; + private Joint locomotionRoot; + + /** @param offsetY Wert aus {@code loaded.setLocalTranslation(0, offsetY, 0)} */ + public RootDriftCompensatorControl(float offsetY) { + this.offsetY = offsetY; + } + + @Override + protected void controlUpdate(float tpf) { + if (!ensureArmature()) return; + + Joint root = getOrFindLocomotionRoot(); + if (root == null) return; + + // Model-Transform enthält die akkumulierte Position des Knochens im Model-Space. + // AnimComposer hat diesen bereits für den aktuellen Frame gesetzt (läuft vor uns). + Vector3f modelXZ = root.getModelTransform().getTranslation(); + float scale = spatial.getLocalScale().x; // uniform scale + + // Spatial (= das geladene Modell) in die entgegengesetzte Richtung verschieben: + // mesh_world_X = capsule_X + spatial_local_X + bone_model_X * scale + // = capsule_X + (−bone_model_X*scale) + bone_model_X*scale = capsule_X ✓ + spatial.setLocalTranslation(-modelXZ.x * scale, offsetY, -modelXZ.z * scale); + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) {} + + private boolean ensureArmature() { + if (armature != null) return true; + SkinningControl sc = RetargetingSystem.findSkinningControl(spatial); + if (sc == null) return false; + armature = sc.getArmature(); + return armature != null; + } + + /** + * Sucht den flachsten Joint, dessen Model-Transform-XZ von 0 abweicht. + * Dieser Joint ist der "Hüft-Knochen", der den Gesamtdrift verursacht. + * Wird gecacht sobald gefunden; bis dahin jeden Frame erneut versucht. + */ + private Joint getOrFindLocomotionRoot() { + if (locomotionRoot != null) return locomotionRoot; + + int minDepth = Integer.MAX_VALUE; + for (int i = 0; i < armature.getJointCount(); i++) { + Joint j = armature.getJoint(i); + Vector3f mt = j.getModelTransform().getTranslation(); + if (Math.abs(mt.x) > 0.005f || Math.abs(mt.z) > 0.005f) { + int d = depth(j); + if (d < minDepth) { + minDepth = d; + locomotionRoot = j; + } + } + } + if (locomotionRoot != null) { + log.info("[RootDrift] Locomotion-Root: '{}' Tiefe={}", + locomotionRoot.getName(), minDepth); + } + return locomotionRoot; + } + + private static int depth(Joint j) { + int d = 0; + Joint p = j.getParent(); + while (p != null) { d++; p = p.getParent(); } + return d; + } +} diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 9d6cd91..56a62f3 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -338,6 +338,10 @@ public class WorldScene extends BaseAppState { log.info("[WorldScene] Kein Scale möglich (height={}), Fallback-Offset", modelHeight); } + // RootDriftCompensator nach dem Laden hinzufügen: läuft nach AnimComposer + // (gleicher Spatial, später hinzugefügt) und korrigiert XZ-Drift des Meshes. + loaded.addControl(new de.blight.game.animation.RootDriftCompensatorControl(offsetY)); + // rotationNode als Drehpunkt (CharacterControl überschreibt wrapper-Rotation jeden Frame) Node rotNode = new Node("charRot"); loaded.setLocalTranslation(0, offsetY, 0);