Option B: RootDriftCompensatorControl verhindert Mesh-Drift ohne Animation zu verändern
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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")) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user