Animationen ingame und im editor angepasst
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
"Sitting Idle": {
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"tz": -0.55,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
@@ -48,7 +48,7 @@
|
||||
"Sit To Stand": {
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"tz": -0.55,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
|
||||
@@ -34,10 +34,10 @@ vec4 triplanarArray(vec2 uvX, vec2 uvY, vec2 uvZ, vec3 bw, int idx) {
|
||||
#endif
|
||||
|
||||
void main() {
|
||||
// World-space splatmap UV (identisch zur Painting-Logik in TerrainEditorState)
|
||||
// World-space splatmap UV (identisch zur Painting-Logik in TerrainEditorState, WORLD_HALF=1024)
|
||||
vec2 splatUV = vec2(
|
||||
(vWorldPos.x + 2048.0) / 4096.0,
|
||||
1.0 - (vWorldPos.z + 2048.0) / 4096.0
|
||||
(vWorldPos.x + 1024.0) / 2048.0,
|
||||
1.0 - (vWorldPos.z + 1024.0) / 2048.0
|
||||
);
|
||||
|
||||
vec4 a0 = texture(m_AlphaMap, splatUV);
|
||||
|
||||
@@ -10579,6 +10579,12 @@ public class EditorApp extends Application {
|
||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
||||
}
|
||||
// Locomotion-Clips für snapRootBoneXZ ermitteln
|
||||
java.util.Set<String> locomotionActions = java.util.Set.of("WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
||||
input.animPreviewLocomotionClips.clear();
|
||||
animSet.getActionMap().forEach((action, clip) -> {
|
||||
if (locomotionActions.contains(action)) input.animPreviewLocomotionClips.add(clip);
|
||||
});
|
||||
|
||||
Path animRootDir = ASSET_ROOT.resolve("Characters");
|
||||
|
||||
@@ -11460,6 +11466,12 @@ public class EditorApp extends Application {
|
||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
||||
}
|
||||
// Locomotion-Clips für snapRootBoneXZ synchronisieren
|
||||
java.util.Set<String> locoActions = java.util.Set.of("WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
||||
input.animPreviewLocomotionClips.clear();
|
||||
animSet.getActionMap().forEach((action, clip) -> {
|
||||
if (locoActions.contains(action)) input.animPreviewLocomotionClips.add(clip);
|
||||
});
|
||||
// Vorschau-Modell-Pfad beibehalten
|
||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||
|
||||
@@ -656,6 +656,9 @@ public class SharedInput {
|
||||
/** JavaFX → JME3: Clip-Name → [tx, ty, tz] für alle Clips mit gesetztem Offset. */
|
||||
public final java.util.concurrent.ConcurrentHashMap<String, float[]> animPreviewClipOffsets =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
/** JavaFX → JME3: Clip-Namen die snapRootBoneXZ erhalten sollen (Locomotion). */
|
||||
public final java.util.concurrent.ConcurrentSkipListSet<String> animPreviewLocomotionClips =
|
||||
new java.util.concurrent.ConcurrentSkipListSet<>();
|
||||
|
||||
/**
|
||||
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
||||
|
||||
@@ -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.setValue(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.getCurrentAction();
|
||||
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",
|
||||
|
||||
@@ -395,7 +395,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
if (loadedMapData != null) {
|
||||
float[] src = loadedMapData.terrainHeight;
|
||||
heights = new float[TOTAL_SIZE * TOTAL_SIZE];
|
||||
int srcVerts = MapData.TERRAIN_VERTS; // 16385
|
||||
int srcVerts = MapData.TERRAIN_VERTS; // 8193
|
||||
if (srcVerts == TOTAL_SIZE) {
|
||||
System.arraycopy(src, 0, heights, 0, heights.length);
|
||||
} else {
|
||||
|
||||
@@ -168,7 +168,7 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
// ── Voxel-Splatmap ───────────────────────────────────────────────────────
|
||||
|
||||
private static final int VOX_SPLAT_SIZE = MapData.SPLAT_SIZE; // 2049
|
||||
private static final int VOX_SPLAT_SIZE = MapData.SPLAT_SIZE; // 1025
|
||||
private static final float VOX_WORLD_HALF = 2048f;
|
||||
private static final float VOX_SPLAT_WE_PER_PX =
|
||||
(VOX_WORLD_HALF * 2f) / (VOX_SPLAT_SIZE - 1); // ~2 WE/px
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package de.blight.game.animation;
|
||||
|
||||
import com.jme3.anim.AnimClip;
|
||||
import com.jme3.anim.AnimComposer;
|
||||
import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.anim.TransformTrack;
|
||||
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.math.Quaternion;
|
||||
import com.jme3.math.Transform;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Kapselt das Abspielen von AnimClips: BlendAction+FreezeClip für nahtlose
|
||||
* Übergänge. Gemeinsame Codebasis für Editor (AnimPreviewState) und Spiel
|
||||
* (PlayerInputControl).
|
||||
*
|
||||
* snapRootBoneXZ für Locomotion-Clips bleibt Aufgabe des Aufrufers (AnimationLibrary).
|
||||
*/
|
||||
public class AnimPlayer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AnimPlayer.class);
|
||||
|
||||
/** Standard-Übergangsdauer in Sekunden. */
|
||||
public static final float BLEND_DURATION = 0.15f;
|
||||
|
||||
private final AnimComposer composer;
|
||||
private final SkinningControl skinning;
|
||||
|
||||
private String currentClip;
|
||||
private Action currentAction;
|
||||
|
||||
private LinearBlendSpace blendSpace;
|
||||
private String pendingClip;
|
||||
private float blendElapsed;
|
||||
|
||||
public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
|
||||
this.composer = composer;
|
||||
this.skinning = skinning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt einen Clip ab.
|
||||
* Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang
|
||||
* via BlendAction+FreezeClip (kein interner JME3-Cross-Fade).
|
||||
*
|
||||
* @param clipName Ziel-Clip (muss im AnimComposer bekannt sein)
|
||||
* @param transition Übergangsdauer in Sekunden; 0 = harter Schnitt
|
||||
* @return true wenn Playback erfolgreich gestartet
|
||||
*/
|
||||
public boolean play(String clipName, float transition) {
|
||||
if (composer == null) return false;
|
||||
if (!composer.getAnimClipsNames().contains(clipName)) {
|
||||
log.warn("[AnimPlayer] Clip '{}' nicht im AnimComposer", clipName);
|
||||
return false;
|
||||
}
|
||||
if (skinning != null && !skinning.isEnabled()) skinning.setEnabled(true);
|
||||
|
||||
if ((currentClip != null || pendingClip != null) && transition > 0f) {
|
||||
com.jme3.anim.Armature arm = skinning != null ? skinning.getArmature() : null;
|
||||
AnimClip freeze = buildFreezeClip(arm);
|
||||
if (freeze != null) {
|
||||
try {
|
||||
if (composer.hasAnimClip("__freeze__"))
|
||||
composer.removeAnimClip(composer.getAnimClip("__freeze__"));
|
||||
composer.addAnimClip(freeze);
|
||||
if (composer.hasAction("__blend__"))
|
||||
composer.removeAction("__blend__");
|
||||
LinearBlendSpace space = new LinearBlendSpace(0f, 1f);
|
||||
space.setValue(0f);
|
||||
BlendAction blend = composer.actionBlended("__blend__", space, "__freeze__", clipName);
|
||||
blend.clearSpeedFactors();
|
||||
// setTransitionLength(0) verhindert internen JME3-Cross-Fade,
|
||||
// der sonst parallel zu unserem blendSpace.setValue läuft.
|
||||
blend.setTransitionLength(0.0);
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setCurrentAction(blend);
|
||||
blendSpace = space;
|
||||
pendingClip = clipName;
|
||||
blendElapsed = 0f;
|
||||
currentAction = blend;
|
||||
currentClip = null;
|
||||
log.debug("[AnimPlayer] Blend → '{}'", clipName);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimPlayer] BlendAction fehlgeschlagen, direktes Wechseln: {}", e.getMessage());
|
||||
blendSpace = null;
|
||||
pendingClip = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return directPlay(clipName);
|
||||
}
|
||||
|
||||
/** Muss jeden Frame aufgerufen werden (Blend-Fortschritt). */
|
||||
public void update(float tpf) {
|
||||
if (pendingClip == null || blendSpace == null) return;
|
||||
blendElapsed += tpf;
|
||||
float bt = Math.min(1f, blendElapsed / BLEND_DURATION);
|
||||
blendSpace.setValue(bt);
|
||||
if (bt >= 1f) finishBlend();
|
||||
}
|
||||
|
||||
/** Setzt Playback-Zustand zurück (kein AnimComposer-Stop). */
|
||||
public void stop() {
|
||||
pendingClip = null;
|
||||
blendSpace = null;
|
||||
currentClip = null;
|
||||
currentAction = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name des aktiven Clips: Target-Clip während Blend, stabiler Clip danach.
|
||||
* Gibt null zurück wenn nichts spielt.
|
||||
*/
|
||||
public String getCurrentClip() {
|
||||
return pendingClip != null ? pendingClip : currentClip;
|
||||
}
|
||||
|
||||
/** Aktuelle JME3-Action (BlendAction während Blend, ClipAction danach). */
|
||||
public Action getCurrentAction() { return currentAction; }
|
||||
|
||||
// ── intern ───────────────────────────────────────────────────────────────
|
||||
|
||||
private boolean directPlay(String clipName) {
|
||||
try {
|
||||
Action act = composer.action(clipName);
|
||||
if (act instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
||||
currentAction = composer.setCurrentAction(clipName);
|
||||
currentClip = clipName;
|
||||
pendingClip = null;
|
||||
blendSpace = null;
|
||||
log.debug("[AnimPlayer] direktPlay → '{}'", clipName);
|
||||
return currentAction != null;
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishBlend() {
|
||||
String clip = pendingClip;
|
||||
float elapsed = blendElapsed;
|
||||
pendingClip = null;
|
||||
blendSpace = null;
|
||||
if (clip == null) return;
|
||||
try {
|
||||
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
Action next = composer.action(clip);
|
||||
if (next instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
||||
currentAction = composer.setCurrentAction(clip);
|
||||
currentClip = clip;
|
||||
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
||||
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt.
|
||||
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
||||
if (len > 0.01 && elapsed > 0) {
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(elapsed % len);
|
||||
}
|
||||
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static AnimClip buildFreezeClip(com.jme3.anim.Armature arm) {
|
||||
if (arm == null || arm.getJointCount() == 0) return null;
|
||||
int n = arm.getJointCount();
|
||||
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);
|
||||
Transform lt = j.getLocalTransform();
|
||||
tracks[i] = new TransformTrack(
|
||||
j, new float[]{0f},
|
||||
new Vector3f[]{lt.getTranslation().clone()},
|
||||
new Quaternion[]{lt.getRotation().clone()},
|
||||
new Vector3f[]{lt.getScale().clone()});
|
||||
}
|
||||
clip.setTracks(tracks);
|
||||
return clip;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
* Lädt alle Clip-Dateien aus {@code animations/clips/} beim Start.
|
||||
* Clip-Schlüssel entsprechen dem Dateinamen ohne Extension (= Clip-Name).
|
||||
@@ -39,6 +40,13 @@ public class AnimationLibrary extends BaseAppState {
|
||||
private final Map<String, AnimClip> clips = new LinkedHashMap<>();
|
||||
/** clip name → Armatur der Quell-Datei */
|
||||
private final Map<String, Armature> armatures = new LinkedHashMap<>();
|
||||
/** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */
|
||||
private final Set<String> snapClipNames = new LinkedHashSet<>();
|
||||
|
||||
/** Standard-Locomotion-Aktionen, deren gemappte Clips snapRootBoneXZ erhalten. */
|
||||
private static final Set<String> LOCOMOTION_ACTIONS = Set.of(
|
||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"
|
||||
);
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -118,6 +126,9 @@ public class AnimationLibrary extends BaseAppState {
|
||||
log.warn("[AnimLib] applyTo: Retargeting für '{}' schlug fehl", clipName);
|
||||
return false;
|
||||
}
|
||||
if (snapClipNames.contains(clipName)) {
|
||||
target = snapRootBoneXZ(target, sc.getArmature());
|
||||
}
|
||||
|
||||
ac.addAnimClip(target);
|
||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
||||
@@ -201,6 +212,24 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt anhand des AnimSets welche Clips snapRootBoneXZ erhalten sollen
|
||||
* (Locomotion-Aktionen: Walk, Run, Sprint, Jump, Idle, Default).
|
||||
* Muss vor dem ersten {@link #applyTo}-Aufruf für das Player-Modell gesetzt werden.
|
||||
*/
|
||||
public void configureLocomotionSnap(Path assetRoot, String animSetName) {
|
||||
snapClipNames.clear();
|
||||
try {
|
||||
AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
|
||||
set.getActionMap().forEach((action, clip) -> {
|
||||
if (LOCOMOTION_ACTIONS.contains(action)) snapClipNames.add(clip);
|
||||
});
|
||||
log.info("[AnimLib] snapRootBoneXZ für Locomotion-Clips: {}", snapClipNames);
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Loading ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadAll() {
|
||||
@@ -307,9 +336,6 @@ public class AnimationLibrary extends BaseAppState {
|
||||
|
||||
for (String name : ac.getAnimClipsNames()) {
|
||||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
||||
if (armature != null) {
|
||||
animClip = snapRootBoneXZ(animClip, armature);
|
||||
}
|
||||
clips.put(name, animClip);
|
||||
if (armature != null) armatures.put(name, armature);
|
||||
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Spatial;
|
||||
import de.blight.game.animation.AnimOffset;
|
||||
import de.blight.game.animation.AnimPlayer;
|
||||
import de.blight.game.animation.AnimSet;
|
||||
import de.blight.game.animation.AnimationAction;
|
||||
import de.blight.game.animation.AnimationLibrary;
|
||||
@@ -53,6 +54,7 @@ public class PlayerInputControl {
|
||||
private boolean animCtxLogged = false;
|
||||
|
||||
private AnimComposer animComposer;
|
||||
private AnimPlayer animPlayer;
|
||||
private String runningClip;
|
||||
|
||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||
@@ -91,6 +93,7 @@ public class PlayerInputControl {
|
||||
private int jumpFrames = 0;
|
||||
private int groundGraceFrames = 0;
|
||||
private double nextTransitionLength = 0.0;
|
||||
|
||||
private boolean pickupActive = false;
|
||||
private float pickupRemaining = 0f;
|
||||
|
||||
@@ -156,6 +159,7 @@ public class PlayerInputControl {
|
||||
this.assetRoot = assetRoot;
|
||||
this.currentAnim = null;
|
||||
this.runningClip = null;
|
||||
animLib.configureLocomotionSnap(assetRoot, animSetName);
|
||||
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
|
||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||
skinningControl = findSkinningControl(visual);
|
||||
@@ -164,6 +168,9 @@ public class PlayerInputControl {
|
||||
armature = skinningControl.getArmature();
|
||||
logJointNames(armature);
|
||||
}
|
||||
animPlayer = (animComposer != null && skinningControl != null)
|
||||
? new AnimPlayer(animComposer, skinningControl)
|
||||
: null;
|
||||
if (visual != null) {
|
||||
visualBaseTranslation = visual.getLocalTranslation().clone();
|
||||
}
|
||||
@@ -336,18 +343,16 @@ public class PlayerInputControl {
|
||||
inputsBlocked = true;
|
||||
if (animComposer != null) {
|
||||
if (animLib != null && visual != null) animLib.ensureApplied(reviveClip, visual);
|
||||
// transitionLength=0: verhindert den 0.4 s Überblend-Effekt bei speed=0.
|
||||
// Ohne diesen Fix wäre transitionWeight=time/0.4=0 → Charakter zeigt Idle-Pose.
|
||||
com.jme3.anim.tween.action.Action reviveAction = animComposer.action(reviveClip);
|
||||
if (reviveAction instanceof com.jme3.anim.tween.action.BlendableAction ba) {
|
||||
ba.setTransitionLength(0.0);
|
||||
}
|
||||
frozenReviveAction = animComposer.setCurrentAction(reviveClip);
|
||||
if (animPlayer == null) animPlayer = new AnimPlayer(animComposer, skinningControl);
|
||||
// transition=0: harter Schnitt, kein BlendAction → Charakter startet sofort in frame 0
|
||||
animPlayer.play(reviveClip, 0f);
|
||||
frozenReviveAction = animPlayer.getCurrentAction();
|
||||
if (frozenReviveAction != null) {
|
||||
frozenReviveAction.setSpeed(0f);
|
||||
runningClip = animPlayer.getCurrentClip();
|
||||
log.info("[Intro] REVIVE '{}' eingefroren (frame 0)", reviveClip);
|
||||
} else {
|
||||
log.warn("[Intro] setCurrentAction('{}') → null – REVIVE nicht eingefroren", reviveClip);
|
||||
log.warn("[Intro] REVIVE '{}' → action null", reviveClip);
|
||||
}
|
||||
} else {
|
||||
log.warn("[Intro] animComposer null – REVIVE kann nicht eingefroren werden");
|
||||
@@ -456,6 +461,11 @@ public class PlayerInputControl {
|
||||
public void update(float tpf) {
|
||||
if (physicsChar == null) return;
|
||||
|
||||
if (animPlayer != null) {
|
||||
animPlayer.update(tpf);
|
||||
runningClip = animPlayer.getCurrentClip();
|
||||
}
|
||||
|
||||
if (visual != null && animOffsetSpeed > 0f) {
|
||||
Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent);
|
||||
float dist = delta.length();
|
||||
@@ -764,7 +774,28 @@ public class PlayerInputControl {
|
||||
if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
|
||||
animOffsetSpeed = 5.0f;
|
||||
}
|
||||
log.info("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
|
||||
log.debug("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Anim-Offset für eine AnimationAction sofort (kein Lerp).
|
||||
* Verwenden bei Übergängen, bei denen der Lerp die Skelett-Animation kompensieren würde
|
||||
* (z.B. sit-down: Offset-Lerp 0→-0.5m hebt Hip-Bone-Vorwärtsbewegung auf → Hüfte wirkt statisch).
|
||||
*/
|
||||
public void applyActionOffsetInstant(AnimationAction action) {
|
||||
if (visual == null || animLib == null || animSetName == null) return;
|
||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||
if (clip == null) return;
|
||||
AnimOffset off = animOffsets.get(clip);
|
||||
if (off == null) { clearAnimOffsetInstant(); return; }
|
||||
Quaternion facing = visual.getLocalRotation().clone();
|
||||
Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz));
|
||||
animOffsetTarget.set(worldOffset);
|
||||
animOffsetCurrent.set(worldOffset);
|
||||
animOffsetSpeed = 0f;
|
||||
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
|
||||
log.info("[AnimOffset] Sofort: action={} clip='{}' → ({},{},{})", action, clip,
|
||||
worldOffset.x, worldOffset.y, worldOffset.z);
|
||||
}
|
||||
|
||||
public void clearAnimOffset() {
|
||||
@@ -821,29 +852,17 @@ public class PlayerInputControl {
|
||||
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||
return false;
|
||||
}
|
||||
// Optionale Überblend-Zeit (einmalig, wird nach Verwendung auf 0 zurückgesetzt)
|
||||
double transLen = nextTransitionLength;
|
||||
if (animPlayer == null) {
|
||||
animPlayer = new AnimPlayer(animComposer, skinningControl);
|
||||
}
|
||||
float transLen = (float) nextTransitionLength;
|
||||
nextTransitionLength = 0.0;
|
||||
if (transLen > 0.0) {
|
||||
com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip);
|
||||
if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction) {
|
||||
((com.jme3.anim.tween.action.BlendableAction) nextAction).setTransitionLength(transLen);
|
||||
log.info("[Anim] Transition {} → '{}' über {:.2f}s", runningClip, clip, transLen);
|
||||
}
|
||||
boolean ok = animPlayer.play(clip, transLen);
|
||||
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
|
||||
if (ok) {
|
||||
runningClip = animPlayer.getCurrentClip();
|
||||
applyAnimOffset(clip);
|
||||
}
|
||||
// Erst Action setzen, DANN SkinningControl aktivieren –
|
||||
// vermeidet 1 Frame in Bind-Pose × Armature-Rx90° = liegender Charakter.
|
||||
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
|
||||
log.info("[Anim] setCurrentAction('{}') → {}", clip, action != null ? "OK" : "FAILED");
|
||||
if (action == null) {
|
||||
return false;
|
||||
}
|
||||
if (skinningControl != null && !skinningControl.isEnabled()) {
|
||||
skinningControl.setEnabled(true);
|
||||
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
|
||||
}
|
||||
runningClip = clip;
|
||||
applyAnimOffset(clip);
|
||||
return true;
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +130,9 @@ public class WorldScene extends BaseAppState {
|
||||
if (loadedMapData == null) return result;
|
||||
|
||||
int size = de.blight.common.MapData.SPLAT_SIZE;
|
||||
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 2048f) / 2f)));
|
||||
// Z ist im Splatmap gespiegelt (Editor: pz = (size-1) - round((z+2048)/2))
|
||||
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 2048f) / 2f)));
|
||||
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 1024f) / 2f)));
|
||||
// Z ist im Splatmap gespiegelt (Editor: pz = (size-1) - round((z+1024)/2))
|
||||
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 1024f) / 2f)));
|
||||
int idx = pz * size + px;
|
||||
|
||||
// Upper/Third-Splatmap unterdrücken die Basistextur visuell (Overlay-Modell).
|
||||
@@ -540,9 +540,12 @@ public class WorldScene extends BaseAppState {
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
private void setupAnimationContext() {
|
||||
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
|
||||
MainCharacter mc = findMainCharacter();
|
||||
String setName = (mc != null) ? mc.getAnimSetPath() : null;
|
||||
if (setName != null) {
|
||||
animLib.configureLocomotionSnap(AnimationLibrary.findAssetRoot(), setName);
|
||||
}
|
||||
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
|
||||
log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}",
|
||||
mc != null ? mc.getCharacterId() : "null",
|
||||
setName,
|
||||
|
||||
@@ -58,7 +58,6 @@ public class WorldInteractableState extends BaseAppState {
|
||||
private static final float BENCH_RANGE = 5f;
|
||||
private static final float BED_RANGE = 6f;
|
||||
private static final float WALK_TIMEOUT = 12f;
|
||||
private static final float BENCH_SIT_MOVE_DIST = 0.5f;
|
||||
|
||||
// Nach dem Aufstehen: Bank-Physik erst re-enablen wenn Charakter weit genug weg
|
||||
private static final float BENCH_REENABLE_DIST_SQ = 0.36f; // 0.6m Radius
|
||||
@@ -67,8 +66,6 @@ public class WorldInteractableState extends BaseAppState {
|
||||
private float benchPendingX = 0f;
|
||||
private float benchPendingZ = 0f;
|
||||
private float benchPendingTimer = 0f;
|
||||
/** Blickrichtung des Charakters beim Hinsetzen (weg von der Bank); für Positions-Versatz nach Anim. */
|
||||
private Vector3f currentBenchSitDir = null;
|
||||
|
||||
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -333,9 +330,6 @@ public class WorldInteractableState extends BaseAppState {
|
||||
InteractableEntry entry = entries.get(targetIdx);
|
||||
float rotY = getSitFacingRotY(entry);
|
||||
Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY));
|
||||
if (isBench(entry)) {
|
||||
currentBenchSitDir = sitDir.clone();
|
||||
}
|
||||
playerInput.requestTurn(sitDir, 0.35f, () -> startSitAnim(entry));
|
||||
}
|
||||
|
||||
@@ -360,18 +354,11 @@ public class WorldInteractableState extends BaseAppState {
|
||||
AnimationAction downAction = isBench(entry) ? AnimationAction.SIT_DOWN : AnimationAction.LIE_DOWN;
|
||||
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
||||
|
||||
if (isBench(entry)) {
|
||||
playerInput.applyActionOffsetInstant(downAction);
|
||||
}
|
||||
|
||||
playerInput.requestAnimation(downAction, 0f, () -> {
|
||||
if (isBench(entry) && currentBenchSitDir != null) {
|
||||
Vector3f cur = physicsChar.getPhysicsLocation();
|
||||
Vector3f delta = currentBenchSitDir.mult(-BENCH_SIT_MOVE_DIST);
|
||||
physicsChar.setPhysicsLocation(cur.add(delta));
|
||||
// Sofortiges Spatial-Update verhindert 1-Frame-Kamerazuckeln
|
||||
Spatial phySpatial = physicsChar.getSpatial();
|
||||
if (phySpatial != null) {
|
||||
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
|
||||
}
|
||||
log.info("[WorldInteractable] Charakter 50cm zur Bank verschoben.");
|
||||
}
|
||||
if (!isBench(entry)) snapToSitPos(entry);
|
||||
playerInput.lockInPlace();
|
||||
if (isBench(entry)) playerInput.setNextAnimTransition(0.2);
|
||||
@@ -412,20 +399,8 @@ public class WorldInteractableState extends BaseAppState {
|
||||
|
||||
playerInput.requestAnimation(upAction, 0f, () -> {
|
||||
if (isBench(entry)) {
|
||||
// Nach stand_up_bench: Charakter 50cm von Bank wegbewegen
|
||||
if (currentBenchSitDir != null) {
|
||||
Vector3f cur = physicsChar.getPhysicsLocation();
|
||||
Vector3f delta = currentBenchSitDir.mult(BENCH_SIT_MOVE_DIST);
|
||||
physicsChar.setPhysicsLocation(cur.add(delta));
|
||||
Spatial phySpatial = physicsChar.getSpatial();
|
||||
if (phySpatial != null) {
|
||||
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
|
||||
}
|
||||
currentBenchSitDir = null;
|
||||
playerInput.setGroundGrace(4);
|
||||
playerInput.setNextAnimTransition(0.2);
|
||||
log.info("[WorldInteractable] Charakter 50cm von Bank wegbewegt.");
|
||||
}
|
||||
playerInput.setGroundGrace(4);
|
||||
playerInput.setNextAnimTransition(0.2);
|
||||
playerInput.clearAnimOffset();
|
||||
benchPendingId = entry.interactableId();
|
||||
benchPendingX = entry.worldX();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "19f0ce17-838d-4938-8739-fc17de491657",
|
||||
"benchType": "Simple",
|
||||
"sitzX": 237.17992,
|
||||
"sitzY": 6.99606,
|
||||
"sitzZ": -891.853,
|
||||
"sitzRotY": -0.8307737,
|
||||
"sitzSet": true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "5a91d7c8-244c-488d-b4c9-6429a63b2afe",
|
||||
"benchType": "Simple",
|
||||
"sitzX": 238.92255,
|
||||
"sitzY": 6.996063,
|
||||
"sitzZ": -892.69714,
|
||||
"sitzRotY": 1.9757149,
|
||||
"sitzSet": true
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,3 +2,10 @@
|
||||
Models/northcoast/wrack1.j3o 286.06750 -3.50554 -947.28595 -1.76657 1.00000 -0.00000 -0.50184 false true true 30.00000 80.00000 120.00000
|
||||
Models/trees/palm/palm_20260816_213338.j3o 277.36694 2.74803 -956.39905 0.43292 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/trees/palm/palm_20260816_213338.j3o 280.27863 2.66144 -928.60248 -1.51992 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/imported/bank1.j3o 237.17992 6.49606 -891.85303 -2.40157 1.00000 -0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 19f0ce17-838d-4938-8739-fc17de491657
|
||||
Models/trees/palm/palm_20260816_213341.j3o 249.12691 6.49606 -895.80035 -0.96942 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/trees/palm/palm_20260816_213341.j3o 243.51527 6.49606 -894.84039 -2.48591 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/trees/palm/palm_20260816_213338.j3o 233.49467 4.51015 -892.32526 2.66339 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/trees/palm/palm_20260816_213341.j3o 247.05431 6.44288 -888.47949 1.23606 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/trees/palm/palm_20260816_213341.j3o 239.43253 6.49157 -886.23218 -2.49448 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user