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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
target = snapRootBoneXZ(target, sc.getArmature());
|
|
||||||
ac.addAnimClip(target);
|
ac.addAnimClip(target);
|
||||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
||||||
if (clipName.equals("sit_down")) {
|
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);
|
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)
|
// rotationNode als Drehpunkt (CharacterControl überschreibt wrapper-Rotation jeden Frame)
|
||||||
Node rotNode = new Node("charRot");
|
Node rotNode = new Node("charRot");
|
||||||
loaded.setLocalTranslation(0, offsetY, 0);
|
loaded.setLocalTranslation(0, offsetY, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user