Zweihänder: Midpoint-Anker mit Handballen-Korrektur (State C); Hover-Logs auf DEBUG

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 18:55:45 +02:00
parent 21c8af0105
commit 911b512208
3 changed files with 60 additions and 64 deletions

View File

@@ -222,39 +222,36 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
s.setLocalTranslation(weaponPos);
}
/**
* Zweihänder: Schwert dreht sich frei um Griff1 (rechte Handteller-Innenfläche) als Pivot;
* die Richtung wird so gewählt, dass Griff2 so nah wie möglich am linken Handteller liegt.
*/
private void applyTwoHandTransform(Spatial s, Vector3f bonePosInChar, Quaternion boneRotInChar,
ItemGrip grip, Quaternion charWorldRotInv, Vector3f charWorldPos) {
Quaternion gripRot = new Quaternion().fromAngles(
DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ());
Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar;
boolean bladeAtMinusZ3 = grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip3 = bladeAtMinusZ3
boolean bladeAtMinusZ = grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip = bladeAtMinusZ
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = animDelta.mult(bladeFlip3).mult(gripRot.inverse());
Quaternion weaponRot = animDelta.mult(bladeFlip).mult(gripRot.inverse());
// Griff-Achse im Modell-Space: Griff1 → Griff2
Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ());
Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ());
Vector3f modelGripAxis = g2.subtract(g1).normalizeLocal();
Vector3f currentGripAxis = weaponRot.mult(modelGripAxis);
Vector3f leftHandInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Quaternion leftBoneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
Vector3f leftHandballen = (palmOffsetLocal != null)
? leftHandInChar.subtract(leftBoneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: leftHandInChar;
// Zielachse: rechtes Handgelenk → linkes Handgelenk im Char-Space
Vector3f rightWristInChar = charWorldRotInv.mult(handNode.getWorldTranslation().subtract(charWorldPos));
Vector3f leftWristInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Vector3f targetGripAxis = leftWristInChar.subtract(rightWristInChar).normalizeLocal();
Vector3f currentGripDir = weaponRot.mult(g2.subtract(g1).normalizeLocal());
Vector3f targetDir = leftHandInChar.subtract(bonePosInChar).normalizeLocal();
float dot = currentGripDir.dot(targetDir);
if (dot > 0f) {
weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot);
}
Quaternion swingCorr = swingFromTo(currentGripAxis, targetGripAxis);
weaponRot = swingCorr.mult(weaponRot);
// Grip1 direkt am Handteller platzieren (bonePosInChar enthält palmOffset bereits).
// Kein zusätzlicher HAND_GRIP_OFFSET – wir haben die exakten Greifpositionen.
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1));
Vector3f midGrip = g1.add(g2).multLocal(0.5f);
Vector3f midTarget = bonePosInChar.add(leftHandballen).multLocal(0.5f);
Vector3f weaponPos = midTarget.subtract(weaponRot.mult(midGrip));
s.setLocalRotation(weaponRot);
s.setLocalTranslation(weaponPos);
}
@@ -262,15 +259,15 @@ public class NpcWeaponAttachmentControl extends AbstractControl {
private static Quaternion swingFromTo(Vector3f from, Vector3f to) {
float dot = com.jme3.math.FastMath.clamp(from.dot(to), -1f, 1f);
Vector3f axis = from.cross(to);
float axisLen = axis.length();
if (axisLen < 0.001f) {
float len = axis.length();
if (len < 0.001f) {
if (dot > 0f) return new Quaternion();
Vector3f perp = Math.abs(from.x) <= Math.abs(from.y)
? from.cross(Vector3f.UNIT_X).normalizeLocal()
: from.cross(Vector3f.UNIT_Y).normalizeLocal();
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.PI, perp);
}
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(axisLen));
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(len));
}
private static final Quaternion BACK_ROT_NORM = new Quaternion(0f, -(float) Math.sqrt(0.5), (float) Math.sqrt(0.5), 0f);

View File

@@ -71,6 +71,7 @@ public class WeaponAttachmentControl extends AbstractControl {
private boolean lastDrawnState = false;
private Spatial weaponSpatial;
private boolean gripDiagLogged = false;
private int twoHandDiagFrame = 0;
private Item lastEquippedShield;
private Spatial shieldSpatial;
@@ -270,44 +271,42 @@ public class WeaponAttachmentControl extends AbstractControl {
weaponSpatial.setLocalTranslation(weaponPos);
}
/**
* Zweihänder-Constraint: orientiert die Waffe so, dass die Linie grip1→grip2 (Modell-Space)
* auf die Linie RightHand→LeftHand (Char-Space) zeigt. Roll bleibt aus gripRot erhalten.
*/
private void applyTwoHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar,
ItemGrip grip,
Quaternion charWorldRotInv, Vector3f charWorldPos) {
// Erst Standard-Einhand-Rotation berechnen (gibt Roll-Basis aus gripRot)
// Basis-Rotation aus rechter Hand (gibt Roll/Handflächenausrichtung)
Quaternion gripRot = new Quaternion().fromAngles(
DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ());
Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar;
boolean bladeAtMinusZ3 = grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip3 = bladeAtMinusZ3
boolean bladeAtMinusZ = grip.getGrip2PosZ() > grip.getPosZ();
Quaternion bladeFlip = bladeAtMinusZ
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
: new Quaternion();
Quaternion weaponRot = animDelta.mult(bladeFlip3).mult(gripRot.inverse());
Quaternion weaponRot = animDelta.mult(bladeFlip).mult(gripRot.inverse());
// Griff-Achse im Modell-Space: grip1 → grip2
// Griff-Achse g1→g2 in CharSpace; Ziel = rechteHand→linkeHand
Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ());
Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ());
Vector3f modelGripAxis = g2.subtract(g1).normalizeLocal();
Vector3f leftHandInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
// Linke Hand: Handballen-Korrektur (Palm-Offset entgegen Fingerrichtung) – nur für midTarget, nicht für targetDir
Quaternion leftBoneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
Vector3f leftHandballen = (palmOffsetLocal != null)
? leftHandInChar.subtract(leftBoneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: leftHandInChar;
// Aktuelle Griff-Achse im Char-Space (nach Einhand-Rotation)
Vector3f currentGripAxis = weaponRot.mult(modelGripAxis);
Vector3f currentGripDir = weaponRot.mult(g2.subtract(g1).normalizeLocal());
Vector3f targetDir = leftHandInChar.subtract(bonePosInChar).normalizeLocal();
float dot = currentGripDir.dot(targetDir);
// Ziel-Achse: rechtes Handgelenk → linkes Handgelenk im Char-Space
Vector3f rightWristInChar = charWorldRotInv.mult(handNode.getWorldTranslation().subtract(charWorldPos));
Vector3f leftWristInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Vector3f targetGripAxis = leftWristInChar.subtract(rightWristInChar).normalizeLocal();
if (dot > 0f) {
weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot);
}
// Minimale Swing-Rotation: dreht currentGripAxis → targetGripAxis, kein Roll-Einfluss
Quaternion swingCorr = swingFromTo(currentGripAxis, targetGripAxis);
weaponRot = swingCorr.mult(weaponRot);
// Grip1 direkt am Handteller platzieren (bonePosInChar enthält palmOffset bereits).
// Kein zusätzlicher HAND_GRIP_OFFSET – wir haben die exakten Greifpositionen.
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1));
// Midpoint-Anker: Mitte von g1–g2 auf Mitte beider Handballen legen
Vector3f midGrip = g1.add(g2).multLocal(0.5f);
Vector3f midTarget = bonePosInChar.add(leftHandballen).multLocal(0.5f);
Vector3f weaponPos = midTarget.subtract(weaponRot.mult(midGrip));
weaponSpatial.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(weaponPos);
}
@@ -315,15 +314,15 @@ public class WeaponAttachmentControl extends AbstractControl {
private static Quaternion swingFromTo(Vector3f from, Vector3f to) {
float dot = com.jme3.math.FastMath.clamp(from.dot(to), -1f, 1f);
Vector3f axis = from.cross(to);
float axisLen = axis.length();
if (axisLen < 0.001f) {
float len = axis.length();
if (len < 0.001f) {
if (dot > 0f) return new Quaternion();
Vector3f perp = Math.abs(from.x) <= Math.abs(from.y)
? from.cross(Vector3f.UNIT_X).normalizeLocal()
: from.cross(Vector3f.UNIT_Y).normalizeLocal();
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.PI, perp);
}
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(axisLen));
return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(len));
}
private static float round1(float v) { return Math.round(v * 10f) / 10f; }

View File

@@ -246,7 +246,7 @@ public class WorldItemsState extends BaseAppState {
boolean doLog = hoverLogTimer >= 1f && !items.isEmpty();
if (hoverLogTimer >= 1f) hoverLogTimer = 0f;
if (doLog) {
log.info("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
log.debug("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin);
}
@@ -261,39 +261,39 @@ public class WorldItemsState extends BaseAppState {
// Distanz vom Charakter (nicht von der Kamera)
float charDist = worldPos.distance(charPos);
if (doLog) log.info("[Hover] item[{}] id={} charDist={}", i, pi.itemId(), charDist);
if (doLog) log.debug("[Hover] item[{}] id={} charDist={}", i, pi.itemId(), charDist);
if (charDist > INTERACT_RANGE) {
if (doLog) log.info("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE);
if (doLog) log.debug("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE);
continue;
}
// Muss vor der Kamera liegen
Vector3f camToItem = worldPos.subtract(cam.getLocation());
if (camToItem.dot(cam.getDirection()) <= 0f) {
if (doLog) log.info("[Hover] → skip: hinter der Kamera");
if (doLog) log.debug("[Hover] → skip: hinter der Kamera");
continue;
}
// Bildschirmprojektion (von der Kamera aus)
Vector3f screen = cam.getScreenCoordinates(worldPos);
float hDiff = Math.abs(screen.x - centerX);
if (doLog) log.info("[Hover] screen=({},{},{}) hDiff={} hMargin={}",
if (doLog) log.debug("[Hover] screen=({},{},{}) hDiff={} hMargin={}",
(int)screen.x, (int)screen.y, screen.z, (int)hDiff, (int)hMargin);
if (hDiff > hMargin) {
if (doLog) log.info("[Hover] → skip: horizontal außerhalb Mitte");
if (doLog) log.debug("[Hover] → skip: horizontal außerhalb Mitte");
continue;
}
if (screen.y < 0f || screen.y > screenH) {
if (doLog) log.info("[Hover] → skip: vertikal außerhalb Bildschirm");
if (doLog) log.debug("[Hover] → skip: vertikal außerhalb Bildschirm");
continue;
}
// Verdeckungsprüfung: Strahl von Kamera zum Item
float camDist = camToItem.length();
if (isOccluded(camToItem.normalize(), camDist, i, doLog)) {
if (doLog) log.info("[Hover] → skip: verdeckt");
if (doLog) log.debug("[Hover] → skip: verdeckt");
continue;
}
@@ -303,7 +303,7 @@ public class WorldItemsState extends BaseAppState {
}
}
if (doLog) log.info("[Hover] → hoveredIdx={}", bestIdx);
if (doLog) log.debug("[Hover] → hoveredIdx={}", bestIdx);
setHovered(bestIdx);
}
@@ -312,25 +312,25 @@ public class WorldItemsState extends BaseAppState {
CollisionResults results = new CollisionResults();
rootNode.collideWith(ray, results);
if (doLog) log.info("[Hover] occlusion: {} Treffer, itemDist={:.2f}", results.size(), itemDist);
if (doLog) log.debug("[Hover] occlusion: {} Treffer, itemDist={:.2f}", results.size(), itemDist);
for (int j = 0; j < results.size(); j++) {
CollisionResult cr = results.getCollision(j);
float d = cr.getDistance();
if (d < 1.5f) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → Nah-Clip skip ({})", j, d, cr.getGeometry().getName());
if (doLog) log.debug("[Hover] [{}] d={:.2f} → Nah-Clip skip ({})", j, d, cr.getGeometry().getName());
continue;
}
if (d > itemDist - 0.15f) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → am/hinter Item, kein Blocker ({})", j, d, cr.getGeometry().getName());
if (doLog) log.debug("[Hover] [{}] d={:.2f} → am/hinter Item, kein Blocker ({})", j, d, cr.getGeometry().getName());
continue;
}
int vidx = findVisualIndex(cr.getGeometry());
if (vidx == itemIdx) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → Item selbst ({})", j, d, cr.getGeometry().getName());
if (doLog) log.debug("[Hover] [{}] d={:.2f} → Item selbst ({})", j, d, cr.getGeometry().getName());
continue;
}
if (doLog) log.info("[Hover] [{}] d={:.2f} → BLOCKIERT durch '{}'", j, d, cr.getGeometry().getName());
if (doLog) log.debug("[Hover] [{}] d={:.2f} → BLOCKIERT durch '{}'", j, d, cr.getGeometry().getName());
return true;
}
return false;