@@ -18,9 +18,6 @@ import com.jme3.anim.AnimClip;
import com.jme3.anim.AnimComposer ;
import com.jme3.anim.SkinningControl ;
import com.jme3.anim.tween.action.Action ;
import com.jme3.anim.tween.action.BlendAction ;
import com.jme3.anim.tween.action.BlendableAction ;
import com.jme3.anim.tween.action.LinearBlendSpace ;
import com.jme3.app.Application ;
import com.jme3.app.SimpleApplication ;
import com.jme3.app.state.BaseAppState ;
@@ -89,10 +86,7 @@ public class AnimPreviewState extends BaseAppState {
private float transitionElapsed = 0 . 15f ;
private static final float TRANSITION_DURATION = 0 . 15f ;
private LinearBlendSpace blendSpace = null ;
private String pendingClipName = null ;
private float blendElapsed = 0f ;
private static final float BLEND_DURATION = TRANSITION_DURATION ; // Pose- und Offset-Blend synchron
private de . blight . game . animation . AnimPlayer animPlayer ;
private Node axesNode ;
@@ -216,12 +210,20 @@ public class AnimPreviewState extends BaseAppState {
try { currentAction . setSpeed ( input . animPreviewSpeed ) ; } catch ( Exception ignored ) { }
}
// Blend voranschreiten: BlendWeight 0→1 über BLEND_DURATION
if ( pendingClipName ! = null & & blendSpace ! = null & & currentModel ! = null ) {
blendElapsed + = tpf ;
float bt = Math . min ( 1f , blendElapsed / BLEND_DURATION ) ;
blendSpace . s etValue ( bt ) ;
if ( bt > = 1f ) finishBlend ( ) ;
// Blend voranschreiten via gemeinsamer AnimPlayer-Codebasis
if ( animPlayer ! = null & & currentModel ! = null ) {
Action prevAction = animPlayer . getCurrentAction ( ) ;
animPlayer . update ( tpf ) ;
Action newAction = animPlayer . g etCurrentAction ( ) ;
if ( newAction ! = prevAction & & newAction ! = null ) {
// Blend abgeschlossen oder direkter Wechsel abgeschlossen
currentAction = newAction ;
currentClipName = animPlayer . getCurrentClip ( ) ;
if ( currentAction ! = null ) {
try { currentAction . setSpeed ( input . animPreviewSpeed ) ; } catch ( Exception ignored ) { }
input . animPreviewCurrentClipDuration = ( float ) currentAction . getLength ( ) ;
}
}
}
// Loop-Steuerung: AnimComposer spielt einmal ab – wir starten manuell neu
@@ -360,10 +362,12 @@ public class AnimPreviewState extends BaseAppState {
previewAC . setCurrentAction ( " __tpose__ " ) ;
LOG . info ( " [AnimPreview] T-Pose aktiviert " ) ;
}
animPlayer = new de . blight . game . animation . AnimPlayer ( previewAC , previewSC ) ;
} else {
LOG . warn ( " [AnimPreview] T-Pose NICHT möglich: previewAC={}, previewSC={} " ,
previewAC ! = null ? " ok " : " NULL " ,
previewSC ! = null ? " ok " : " NULL " ) ;
animPlayer = null ;
}
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox.
@@ -454,49 +458,21 @@ public class AnimPreviewState extends BaseAppState {
input . animPreviewOffsetTz = 0f ;
input . animPreviewOffsetActive = false ;
}
// SkinningControls auf dem gesamten Modell aktivieren – AnimComposer und
// SkinningControl sitzen oft auf verschiedenen Geschwisterknoten.
setSkinningEnabled ( currentModel , true ) ;
// Wenn schon ein Clip läuft: Freeze-Pose einfrieren + BlendAction.
// blend.setTransitionLength(0) verhindert den internen 0.4s-Cross-Fade der
// BlendableAction, der sonst parallel zu unserem blendSpace.setValue läuft
// und ein "hin und her" erzeugt.
AnimComposer ac = findControl ( currentModel , AnimComposer . class ) ;
SkinningControl sc = findControl ( currentModel , SkinningControl . class ) ;
if ( ac ! = null & & sc ! = null & & ac . getAnimClipsNames ( ) . contains ( clipName )
& & ( currentClipName ! = null | | pendingClipName ! = null ) ) {
AnimClip freeze = buildFreezeClip ( sc . getArmature ( ) ) ;
if ( freeze ! = null ) {
try {
if ( ac . hasAnimClip ( " __freeze__ " ) ) ac . removeAnimClip ( ac . getAnimClip ( " __freeze__ " ) ) ;
ac . addAnimClip ( freeze ) ;
if ( ac . hasAction ( " __blend__ " ) ) ac . removeAction ( " __blend__ " ) ;
LinearBlendSpace space = new LinearBlendSpace ( 0f , 1f ) ;
space . setValue ( 0f ) ;
BlendAction blend = ac . actionBlended ( " __blend__ " , space , " __freeze__ " , clipName ) ;
blend . clearSpeedFactors ( ) ; // N/0=Infinity für length=0 Freeze-Clip vermeiden
blend . setTransitionLength ( 0 . 0 ) ; // keinen 2. internen Cross-Fade doppelt laufen lassen
ac . getLayer ( AnimComposer . DEFAULT_LAYER ) . setCurrentAction ( blend ) ;
blendSpace = space ;
pendingClipName = clipName ;
blendElapsed = 0f ;
currentAction = blend ;
currentClipName = null ;
return ;
} catch ( Exception e ) {
LOG . warn ( " [AnimPreview] BlendAction fehlgeschlagen, direktes Wechseln: {} " , e . getMessage ( ) ) ;
blendSpace = null ;
pendingClipName = null ;
}
if ( animPlayer ! = null & & animPlayer . play ( clipName , de . blight . game . animation . AnimPlayer . BLEND_DURATION ) ) {
currentAction = animPlayer . getCurrentAction ( ) ;
currentClipName = animPlayer . getCurrentClip ( ) ;
if ( currentAction ! = null ) {
try { currentAction . setSpeed ( input . animPreviewSpeed ) ; } catch ( Exception ignored ) { }
input . animPreviewCurrentClipDuration = ( float ) currentAction . getLength ( ) ;
}
} else {
// Fallback: animPlayer nicht verfügbar (kein SkinningControl)
currentAction = null ;
currentClipName = null ;
playOnSpatial ( currentModel , clipName ) ;
}
currentAction = null ;
currentClipName = null ;
pendingClipName = null ;
blendSpace = null ;
playOnSpatial ( currentModel , clipName ) ;
}
private boolean playOnSpatial ( Spatial s , String clipName ) {
@@ -523,6 +499,10 @@ public class AnimPreviewState extends BaseAppState {
String rStr = ( rr ! = null & & rr . length > 0 ) ? rr [ 0 ] . toString ( ) : " null " ;
LOG . info ( " [DIAG] Track '{}' target='{}' frames={} t[0]={} r[0]={} " ,
clipName , j . getName ( ) , tt . getTimes ( ) . length , tStr , rStr ) ;
if ( tr ! = null & & tr . length > 0 ) {
float modelOffY = input . animPreviewOffsetActive ? input . animPreviewOffsetTy : 0f ;
logHipPos ( clipName , tr [ 0 ] , tr [ tr . length - 1 ] , modelOffY ) ;
}
}
}
// DIAG: Modell-Hips localTransform + IBM
@@ -557,6 +537,7 @@ public class AnimPreviewState extends BaseAppState {
private void stopAll ( ) {
currentAction = null ;
currentClipName = null ;
if ( animPlayer ! = null ) animPlayer . stop ( ) ;
if ( currentModel ! = null ) {
stopOnSpatial ( currentModel ) ;
// Zurück zur T-Pose: __tpose__ wiedergeben (leerer Clip = Bind-Pose)
@@ -568,6 +549,14 @@ public class AnimPreviewState extends BaseAppState {
}
}
private void logHipPos ( String clipName , com . jme3 . math . Vector3f first , com . jme3 . math . Vector3f last , float modelOffY ) {
LOG . info ( " [HipPos] Clip='{}' modelOffY={} | START x={} y={} z={} | END x={} y={} z={} " ,
clipName ,
String . format ( " %.4f " , modelOffY ) ,
String . format ( " %.4f " , first . x ) , String . format ( " %.4f " , first . y ) , String . format ( " %.4f " , first . z ) ,
String . format ( " %.4f " , last . x ) , String . format ( " %.4f " , last . y ) , String . format ( " %.4f " , last . z ) ) ;
}
private void stopOnSpatial ( Spatial s ) {
AnimComposer ac = s . getControl ( AnimComposer . class ) ;
if ( ac ! = null ) {
@@ -718,7 +707,14 @@ public class AnimPreviewState extends BaseAppState {
saved + + ;
}
// Für den aktuellen Preview auch auf das Modell anwenden (wenn geladen)
if ( targetAC ! = null ) targetAC . addAnimClip ( toSave ) ;
if ( targetAC ! = null ) {
AnimClip forPreview = toSave ;
if ( snapArm ! = null & & input . animPreviewLocomotionClips . contains ( forPreview . getName ( ) ) ) {
AnimClip snapped = de . blight . game . animation . AnimationLibrary . snapRootBoneXZ ( forPreview , snapArm ) ;
if ( snapped ! = forPreview ) forPreview = snapped ;
}
targetAC . addAnimClip ( forPreview ) ;
}
}
// Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt – nicht externe Dateien, nie FBX)
if ( saved > 0 & & animAssetPath . matches ( " (?i).* \\ .(glb|gltf)$ " )
@@ -1204,51 +1200,6 @@ public class AnimPreviewState extends BaseAppState {
return clip ;
}
@SuppressWarnings ( " rawtypes " )
private static AnimClip buildFreezeClip ( com . jme3 . anim . Armature arm ) {
int n = arm . getJointCount ( ) ;
if ( n = = 0 ) return null ;
AnimClip clip = new AnimClip ( " __freeze__ " ) ;
com . jme3 . anim . AnimTrack [ ] tracks = new com . jme3 . anim . AnimTrack [ n ] ;
for ( int i = 0 ; i < n ; i + + ) {
com . jme3 . anim . Joint j = arm . getJoint ( i ) ;
com . jme3 . math . Transform lt = j . getLocalTransform ( ) ;
tracks [ i ] = new com . jme3 . anim . TransformTrack (
j , new float [ ] { 0f } ,
new com . jme3 . math . Vector3f [ ] { lt . getTranslation ( ) . clone ( ) } ,
new com . jme3 . math . Quaternion [ ] { lt . getRotation ( ) . clone ( ) } ,
new com . jme3 . math . Vector3f [ ] { lt . getScale ( ) . clone ( ) } ) ;
}
clip . setTracks ( tracks ) ;
return clip ;
}
private void finishBlend ( ) {
String clipName = pendingClipName ;
float elapsed = blendElapsed ;
pendingClipName = null ;
blendSpace = null ;
if ( currentModel = = null | | clipName = = null ) return ;
AnimComposer ac = findControl ( currentModel , AnimComposer . class ) ;
if ( ac = = null ) return ;
if ( ac . hasAction ( " __blend__ " ) ) ac . removeAction ( " __blend__ " ) ;
try {
// transitionLength=0 verhindert den Default-0.4s-Cross-Fade nach dem Blend
Action next = ac . action ( clipName ) ;
if ( next instanceof BlendableAction ba ) ba . setTransitionLength ( 0 . 0 ) ;
currentAction = ac . setCurrentAction ( clipName ) ;
currentClipName = clipName ;
if ( currentAction ! = null ) {
currentAction . setSpeed ( input . animPreviewSpeed ) ;
input . animPreviewCurrentClipDuration = ( float ) currentAction . getLength ( ) ;
double len = currentAction . getLength ( ) ;
if ( len > 0 . 01 & & elapsed > 0 ) ac . setTime ( elapsed % len ) ;
}
} catch ( Exception e ) {
LOG . warn ( " [AnimPreview] Direkt-Wechsel nach Blend fehlgeschlagen: {} " , e . getMessage ( ) ) ;
}
}
private static final String [ ] MODEL_SAVE_ROOTS = {
null , // ASSET_ROOT (src) – wird durch ASSET_ROOT ersetzt
" blight-assets/bin/main " ,