498 lines
22 KiB
Java
498 lines
22 KiB
Java
package de.blight.game.animation;
|
||
|
||
import com.jme3.anim.*;
|
||
import com.jme3.app.Application;
|
||
import com.jme3.app.SimpleApplication;
|
||
import com.jme3.app.state.BaseAppState;
|
||
import com.jme3.asset.AssetManager;
|
||
import com.jme3.scene.Spatial;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
|
||
import com.jme3.math.Quaternion;
|
||
import com.jme3.math.Vector3f;
|
||
|
||
import java.io.IOException;
|
||
import java.nio.file.Files;
|
||
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).
|
||
*/
|
||
public class AnimationLibrary extends BaseAppState {
|
||
|
||
private static final Logger log = LoggerFactory.getLogger(AnimationLibrary.class);
|
||
|
||
private static final String[] ASSET_BASES = {
|
||
"blight-assets/src/main/resources", // CWD = Projekt-Root (Editor-Subprocess)
|
||
"../blight-assets/src/main/resources", // CWD = blight-game/ (Direktstart)
|
||
"assets",
|
||
".",
|
||
};
|
||
|
||
private AssetManager assetManager;
|
||
|
||
/** clip name → clip (an Quell-Armatur gebunden; wird bei Bedarf retargeted) */
|
||
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<>();
|
||
|
||
|
||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||
|
||
@Override
|
||
protected void initialize(Application app) {
|
||
assetManager = ((SimpleApplication) app).getAssetManager();
|
||
Path assetRoot = findAssetRoot();
|
||
try {
|
||
assetManager.registerLocator(
|
||
assetRoot.toAbsolutePath().toString(),
|
||
com.jme3.asset.plugins.FileLocator.class);
|
||
log.info("[AnimLib] Asset-Root registriert: {}", assetRoot.toAbsolutePath());
|
||
} catch (Exception e) {
|
||
log.warn("[AnimLib] Asset-Root konnte nicht registriert werden: {}", e.getMessage());
|
||
}
|
||
loadAll();
|
||
}
|
||
|
||
@Override protected void cleanup(Application app) { clips.clear(); armatures.clear(); }
|
||
@Override protected void onEnable() {}
|
||
@Override protected void onDisable() {}
|
||
|
||
// ── Public API ────────────────────────────────────────────────────────────
|
||
|
||
/** Alle geladenen Clip-Namen. */
|
||
public Collection<String> getClipKeys() {
|
||
return Collections.unmodifiableSet(clips.keySet());
|
||
}
|
||
|
||
/**
|
||
* Retargeted den Clip auf das Skeleton von {@code model} und registriert
|
||
* ihn im AnimComposer des Modells (idempotent).
|
||
*
|
||
* @return true wenn der Clip erfolgreich angewendet wurde
|
||
*/
|
||
public boolean applyTo(String clipName, Spatial model) {
|
||
AnimClip src = clips.get(clipName);
|
||
Armature srcArm = armatures.get(clipName);
|
||
if (src == null) {
|
||
log.warn("[AnimLib] applyTo: Clip '{}' nicht in Bibliothek (verfügbar: {})", clipName, clips.keySet());
|
||
return false;
|
||
}
|
||
|
||
AnimComposer ac = RetargetingSystem.findAnimComposer(model);
|
||
SkinningControl sc = RetargetingSystem.findSkinningControl(model);
|
||
if (ac == null) {
|
||
log.warn("[AnimLib] applyTo: Kein AnimComposer in '{}' für Clip '{}'", model != null ? model.getName() : "null", clipName);
|
||
return false;
|
||
}
|
||
if (sc == null) {
|
||
log.warn("[AnimLib] applyTo: Kein SkinningControl in '{}' für Clip '{}'", model != null ? model.getName() : "null", clipName);
|
||
return false;
|
||
}
|
||
|
||
if (ac.getAnimClip(clipName) != null) return true; // bereits vorhanden
|
||
|
||
{
|
||
com.jme3.anim.AnimTrack<?>[] st = src.getTracks();
|
||
int sn0 = 0;
|
||
if (st != null && st.length > 0 && st[0] instanceof TransformTrack stt)
|
||
sn0 = stt.getTimes() != null ? stt.getTimes().length : 0;
|
||
log.info("[AnimLib] applyTo pre-retarget '{}': srcLen={}s srcTracks={} srcT0Frames={}",
|
||
clipName, src.getLength(), st != null ? st.length : 0, sn0);
|
||
}
|
||
AnimClip target;
|
||
if (srcArm != null && srcArm != sc.getArmature()) {
|
||
// Immer retarget() aufrufen – auch bei gleichen Knochen-Namen.
|
||
// retarget() nutzt intern den "redirect"-Schnellpfad für gleiche Rigs,
|
||
// erstellt aber korrekte TransformTrack-Referenzen auf die Ziel-Armatur.
|
||
// Direkte Nutzung des Quell-Clips würde Transforms auf die falschen
|
||
// (entkoppelten) Joint-Objekte der Quell-j3o anwenden.
|
||
target = RetargetingSystem.retarget(src, srcArm, sc.getArmature());
|
||
} else {
|
||
target = src;
|
||
}
|
||
// Der interne Clip-Name kann vom Library-Schlüssel abweichen (z. B. Blender-Default
|
||
// "Action" statt "sit_down_new"). AnimComposer.setCurrentAction() sucht per Name,
|
||
// daher muss der Name des gespeicherten Clips dem clipName entsprechen.
|
||
if (target != null && !clipName.equals(target.getName())) {
|
||
AnimClip renamed = new AnimClip(clipName);
|
||
renamed.setTracks(target.getTracks());
|
||
target = renamed;
|
||
}
|
||
if (target == null) {
|
||
log.warn("[AnimLib] applyTo: Retargeting für '{}' schlug fehl", clipName);
|
||
return false;
|
||
}
|
||
if (snapClipNames.contains(clipName)) {
|
||
target = snapRootBoneXZ(target, sc.getArmature());
|
||
}
|
||
|
||
ac.addAnimClip(target);
|
||
int trackCount = target.getTracks() != null ? target.getTracks().length : 0;
|
||
log.info("[AnimLib] Clip '{}' → AnimComposer '{}': length={}s tracks={}",
|
||
clipName, model.getName(), target.getLength(), trackCount);
|
||
if (clipName.equals("sit_down") || target.getLength() == 0) {
|
||
dumpClipTracks(target);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/** Wendet alle geladenen Clips auf {@code model} an (nur wenn es ein Rig hat). */
|
||
public void applyAllTo(Spatial model) {
|
||
if (RetargetingSystem.findSkinningControl(model) == null) return;
|
||
AnimComposer ac = RetargetingSystem.findAnimComposer(model);
|
||
if (ac != null) {
|
||
for (AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
||
ac.removeAnimClip(c);
|
||
}
|
||
}
|
||
int applied = 0;
|
||
for (String key : clips.keySet()) {
|
||
if (applyTo(key, model)) applied++;
|
||
}
|
||
if (applied > 0)
|
||
log.info("[AnimLib] {} Clips auf '{}' angewendet", applied, model.getName());
|
||
}
|
||
|
||
/**
|
||
* Stellt sicher dass der Clip auf das Modell angewendet ist und SkinningControl aktiv ist,
|
||
* startet ihn aber NICHT (das übernimmt der Aufrufer via AnimComposer.setCurrentAction).
|
||
*
|
||
* @return true wenn der Clip bereit ist
|
||
*/
|
||
public boolean ensureApplied(String clipName, Spatial model) {
|
||
if (!applyTo(clipName, model)) return false;
|
||
enableSkinningControls(model);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Wendet den Clip an und startet ihn sofort.
|
||
*
|
||
* @return true bei Erfolg
|
||
*/
|
||
public boolean playOn(String clipName, Spatial model) {
|
||
if (!applyTo(clipName, model)) return false;
|
||
AnimComposer ac = RetargetingSystem.findAnimComposer(model);
|
||
if (ac == null) return false;
|
||
enableSkinningControls(model);
|
||
log.info("[AnimLib] setCurrentAction('{}') auf '{}'", clipName, model.getName());
|
||
com.jme3.anim.tween.action.Action action = ac.setCurrentAction(clipName);
|
||
log.info("[AnimLib] Action: {} length={}", action, action != null ? action.getLength() : "N/A");
|
||
return action != null;
|
||
}
|
||
|
||
private static void enableSkinningControls(Spatial s) {
|
||
SkinningControl sc = s.getControl(SkinningControl.class);
|
||
if (sc != null && !sc.isEnabled()) {
|
||
sc.setEnabled(true);
|
||
log.info("[AnimLib] SkinningControl aktiviert auf '{}'", s.getName());
|
||
}
|
||
if (s instanceof com.jme3.scene.Node n) {
|
||
for (Spatial child : n.getChildren()) enableSkinningControls(child);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gibt den Clip-Namen zurück, der einer semantischen Aktion in einem Set zugeordnet ist.
|
||
*
|
||
* @param assetRoot absoluter Pfad zum Assets-Wurzelverzeichnis
|
||
* @param setName Name des Animations-Sets (ohne Pfad/Extension, z. B. {@code "human"})
|
||
* @param action semantische Aktion
|
||
* @return Clip-Name oder {@code null}
|
||
*/
|
||
public static String getClipForAction(Path assetRoot, String setName, AnimationAction action) {
|
||
Path setDir = assetRoot.resolve("Characters").resolve("sets");
|
||
try {
|
||
AnimSet set = AnimSet.load(setDir, setName);
|
||
return set.getActionMap().get(action.name());
|
||
} catch (Exception e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
snapClipNames.addAll(set.getSnapClips());
|
||
log.info("[AnimLib] snapRootBoneXZ für Clips: {}", snapClipNames);
|
||
} catch (Exception e) {
|
||
log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage());
|
||
}
|
||
}
|
||
|
||
// ── Loading ───────────────────────────────────────────────────────────────
|
||
|
||
private void loadAll() {
|
||
Path clipsDir = findClipsDir();
|
||
log.info("[AnimLib] Asset-Root: {}", findAssetRoot().toAbsolutePath());
|
||
if (clipsDir == null) {
|
||
log.warn("[AnimLib] Kein clips-Verzeichnis gefunden – Bibliothek leer.");
|
||
return;
|
||
}
|
||
log.info("[AnimLib] Scanne clips-Verzeichnis: {}", clipsDir.toAbsolutePath());
|
||
try (var walk = Files.walk(clipsDir, 1)) {
|
||
walk.filter(p -> p.toString().endsWith(".j3o"))
|
||
.forEach(this::loadClipFromFile);
|
||
} catch (IOException e) {
|
||
log.warn("[AnimLib] Fehler beim Scannen: {}", e.getMessage());
|
||
}
|
||
if (clips.isEmpty()) {
|
||
log.warn("[AnimLib] KEINE Clips geladen! Prüfe ob der Asset-Root korrekt ist.");
|
||
} else {
|
||
log.info("[AnimLib] {} Clips geladen: {}", clips.size(), clips.keySet());
|
||
}
|
||
|
||
Path setDir = findAssetRoot().resolve("Characters").resolve("sets");
|
||
try {
|
||
AnimSet animSet = AnimSet.load(setDir, "human");
|
||
createSubClips(animSet);
|
||
} catch (Exception e) {
|
||
log.warn("[AnimLib] Sub-Clips konnten nicht erstellt werden: {}", e.getMessage());
|
||
}
|
||
}
|
||
|
||
private void createSubClips(AnimSet animSet) {
|
||
for (Map.Entry<String, AnimSet.SubClipDef> entry : animSet.getSubClips().entrySet()) {
|
||
String subName = entry.getKey();
|
||
AnimSet.SubClipDef def = entry.getValue();
|
||
AnimClip source = clips.get(def.source);
|
||
Armature arm = armatures.get(def.source);
|
||
if (source == null) {
|
||
log.warn("[AnimLib] SubClip '{}': Quelle '{}' nicht gefunden – übersprungen", subName, def.source);
|
||
continue;
|
||
}
|
||
AnimClip sub = extractSubClip(source, subName, def.start, def.end);
|
||
clips.put(subName, sub);
|
||
if (arm != null) {
|
||
armatures.put(subName, arm);
|
||
}
|
||
log.info("[AnimLib] SubClip '{}' extrahiert [{}-{}s] aus '{}'",
|
||
subName, def.start, def.end, def.source);
|
||
}
|
||
}
|
||
|
||
@SuppressWarnings("rawtypes")
|
||
private static AnimClip extractSubClip(AnimClip source, String name, float startSec, float endSec) {
|
||
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||
for (AnimTrack<?> track : source.getTracks()) {
|
||
if (!(track instanceof TransformTrack tt)) {
|
||
newTracks.add(track);
|
||
continue;
|
||
}
|
||
float[] times = tt.getTimes();
|
||
int first = 0;
|
||
while (first < times.length - 1 && times[first] < startSec - 1e-5f) {
|
||
first++;
|
||
}
|
||
int last = times.length - 1;
|
||
while (last > 0 && times[last] > endSec + 1e-5f) {
|
||
last--;
|
||
}
|
||
int count = last - first + 1;
|
||
if (count <= 0) {
|
||
continue;
|
||
}
|
||
float[] newTimes = new float[count];
|
||
for (int i = 0; i < count; i++) {
|
||
newTimes[i] = times[first + i] - startSec;
|
||
}
|
||
Vector3f[] trans = tt.getTranslations();
|
||
Quaternion[] rots = tt.getRotations();
|
||
Vector3f[] scales = tt.getScales();
|
||
Vector3f[] newTrans = trans != null ? Arrays.copyOfRange(trans, first, last + 1) : null;
|
||
Quaternion[] newRots = rots != null ? Arrays.copyOfRange(rots, first, last + 1) : null;
|
||
Vector3f[] newScales = scales != null ? Arrays.copyOfRange(scales, first, last + 1) : null;
|
||
newTracks.add(new TransformTrack(tt.getTarget(), newTimes, newTrans, newRots, newScales));
|
||
}
|
||
AnimClip sub = new AnimClip(name);
|
||
sub.setTracks(newTracks.toArray(new AnimTrack[0]));
|
||
return sub;
|
||
}
|
||
|
||
private void loadClipFromFile(Path file) {
|
||
String fileName = file.getFileName().toString();
|
||
String fileClipKey = fileName.replaceFirst("\\.j3o$", "");
|
||
String assetKey = "Characters/clips/" + fileName;
|
||
|
||
try {
|
||
Spatial loaded = assetManager.loadModel(assetKey);
|
||
AnimComposer ac = RetargetingSystem.findAnimComposer(loaded);
|
||
SkinningControl sc = RetargetingSystem.findSkinningControl(loaded);
|
||
if (ac == null) {
|
||
log.warn("[AnimLib] Kein AnimComposer in {} – übersprungen", assetKey);
|
||
return;
|
||
}
|
||
Armature armature = sc != null ? sc.getArmature() : null;
|
||
|
||
AnimClip bestClip = null;
|
||
for (String name : ac.getAnimClipsNames()) {
|
||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
||
clips.put(name, animClip);
|
||
if (armature != null) armatures.put(name, armature);
|
||
com.jme3.anim.AnimTrack<?>[] rawTracks = animClip.getTracks();
|
||
int n0 = 0;
|
||
if (rawTracks != null && rawTracks.length > 0 && rawTracks[0] instanceof TransformTrack tt0) {
|
||
n0 = tt0.getTimes() != null ? tt0.getTimes().length : 0;
|
||
}
|
||
log.info("[AnimLib] Clip geladen: '{}' aus {} | clipLen={}s tracks={} t0Frames={}",
|
||
name, assetKey, animClip.getLength(),
|
||
rawTracks != null ? rawTracks.length : 0, n0);
|
||
if (bestClip == null || animClip.getLength() > bestClip.getLength()) {
|
||
bestClip = animClip;
|
||
}
|
||
}
|
||
// Kanonischen Dateiname-Key auf den längsten Clip mappen.
|
||
// Verhindert dass ein leerer Null-Frame-Clip einen guten Clip überschreibt,
|
||
// wenn die j3o mehrere Clips mit leicht unterschiedlichen Namen enthält
|
||
// (z.B. "clip_Sword And Shield Slash" vs. "Sword And Shield Slash").
|
||
if (bestClip != null) {
|
||
AnimClip current = clips.get(fileClipKey);
|
||
if (current == null || bestClip.getLength() > current.getLength()) {
|
||
clips.put(fileClipKey, bestClip);
|
||
if (armature != null) armatures.put(fileClipKey, armature);
|
||
if (!fileClipKey.equals(bestClip.getName())) {
|
||
log.info("[AnimLib] Key '{}' → '{}' (clipLen={}s, besser)",
|
||
fileClipKey, bestClip.getName(), bestClip.getLength());
|
||
}
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage());
|
||
}
|
||
}
|
||
|
||
/** Gibt den Asset-Wurzelpfad zurück (erstes Verzeichnis mit {@code Characters/}-Unterordner). */
|
||
public static Path findAssetRoot() {
|
||
for (String base : ASSET_BASES) {
|
||
Path p = Paths.get(base);
|
||
if (Files.isDirectory(p.resolve("Characters"))) return p;
|
||
}
|
||
return Paths.get(ASSET_BASES[0]);
|
||
}
|
||
|
||
private static Path findClipsDir() {
|
||
for (String base : ASSET_BASES) {
|
||
Path p = Paths.get(base, "Characters", "clips");
|
||
if (Files.isDirectory(p)) return p;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Entfernt Root-Motion aus dem flachsten Bone mit Translation-Track (typischerweise Hips).
|
||
* Nur dieser eine Bone wird modifiziert — alle anderen Bones bleiben unverändert.
|
||
*
|
||
* Achsenbelegung dieses Mixamo-Exports im Hips-Bone-Local-Space:
|
||
* Local X → JME3-X (seitlich) → wird auf 0 eingefroren
|
||
* Local Y → JME3-Y (Höhe oben) → IMMER frei lassen (Charakter-Höhe erhalten)
|
||
* Local Z → JME3-Z (vorwärts) → wird auf 0 eingefroren (alle Clips auf der Stelle)
|
||
*/
|
||
@SuppressWarnings("rawtypes")
|
||
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
|
||
if (clip == null || armature == null) return clip;
|
||
|
||
// Tiefe des flachsten Joints mit Translation-Track bestimmen
|
||
int minDepth = Integer.MAX_VALUE;
|
||
for (AnimTrack<?> track : clip.getTracks()) {
|
||
if (!(track instanceof TransformTrack tt) || !(tt.getTarget() instanceof Joint j)) continue;
|
||
Vector3f[] tr = tt.getTranslations();
|
||
if (tr != null && tr.length > 0) {
|
||
minDepth = Math.min(minDepth, jointDepth(j));
|
||
}
|
||
}
|
||
if (minDepth == Integer.MAX_VALUE) return clip;
|
||
|
||
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||
boolean modified = false;
|
||
for (AnimTrack<?> track : clip.getTracks()) {
|
||
if (!(track instanceof TransformTrack tt) || !(tt.getTarget() instanceof Joint j)) {
|
||
newTracks.add(track);
|
||
continue;
|
||
}
|
||
Vector3f[] translations = tt.getTranslations();
|
||
// Nur den flachsten Bone anfassen – alle anderen unverändert lassen
|
||
if (translations == null || translations.length == 0 || jointDepth(j) != minDepth) {
|
||
newTracks.add(track);
|
||
continue;
|
||
}
|
||
|
||
// Diagnostik
|
||
float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE;
|
||
float xRange = 0, zRange = 0;
|
||
for (Vector3f t : translations) {
|
||
if (t.y < yMin) yMin = t.y;
|
||
if (t.y > yMax) yMax = t.y;
|
||
xRange = Math.max(xRange, Math.abs(t.x - translations[0].x));
|
||
zRange = Math.max(zRange, Math.abs(t.z - translations[0].z));
|
||
}
|
||
log.info("[AnimLib] snap '{}' root='{}' xRange={} yRange={} zRange={}",
|
||
clip.getName(), j.getName(),
|
||
String.format("%.3f", xRange),
|
||
String.format("%.3f", yMax - yMin),
|
||
String.format("%.3f", zRange));
|
||
|
||
Vector3f[] snapped = new Vector3f[translations.length];
|
||
for (int i = 0; i < translations.length; i++) {
|
||
snapped[i] = new Vector3f(
|
||
0f, // X = 0 (kein seitlicher Drift)
|
||
translations[i].y, // Y frei (Höhe; Y=oben in bone-local / JME3-Y)
|
||
0f // Z = 0 (kein Vorwärts-Drift; Z=vorwärts in bone-local)
|
||
);
|
||
}
|
||
newTracks.add(new TransformTrack(j, tt.getTimes(), snapped, tt.getRotations(), tt.getScales()));
|
||
modified = true;
|
||
}
|
||
if (!modified) return clip;
|
||
AnimClip result = new AnimClip(clip.getName());
|
||
result.setTracks(newTracks.toArray(new AnimTrack[0]));
|
||
return result;
|
||
}
|
||
|
||
private static int jointDepth(Joint j) {
|
||
int d = 0;
|
||
Joint p = j.getParent();
|
||
while (p != null) { d++; p = p.getParent(); }
|
||
return d;
|
||
}
|
||
|
||
/** Loggt alle Tracks eines Clips: Bone-Name, hat Translation (T), Rotation (R), Scale (S). */
|
||
private void dumpClipTracks(com.jme3.anim.AnimClip clip) {
|
||
log.info("[ClipDump] '{}' length={}s tracks={}",
|
||
clip.getName(), clip.getLength(), clip.getTracks().length);
|
||
int tracksWithTranslation = 0;
|
||
for (com.jme3.anim.AnimTrack<?> t : clip.getTracks()) {
|
||
if (!(t instanceof com.jme3.anim.TransformTrack tt)) continue;
|
||
boolean hasT = tt.getTranslations() != null && tt.getTranslations().length > 0;
|
||
boolean hasR = tt.getRotations() != null && tt.getRotations().length > 0;
|
||
boolean hasS = tt.getScales() != null && tt.getScales().length > 0;
|
||
String target = tt.getTarget() instanceof com.jme3.anim.Joint j ? j.getName() : "?";
|
||
if (hasT) {
|
||
tracksWithTranslation++;
|
||
com.jme3.math.Vector3f t0 = tt.getTranslations()[0];
|
||
com.jme3.math.Vector3f tN = tt.getTranslations()[tt.getTranslations().length - 1];
|
||
log.info("[ClipDump] TRANSLATE '{}' frames={} start=({},{},{}) end=({},{},{}) deltaY={}",
|
||
target, tt.getTranslations().length,
|
||
t0.x, t0.y, t0.z, tN.x, tN.y, tN.z, tN.y - t0.y);
|
||
} else {
|
||
log.info("[ClipDump] rot-only '{}' T={} R={} S={}", target, hasT, hasR, hasS);
|
||
}
|
||
}
|
||
log.info("[ClipDump] Gesamt: {} Tracks mit Translation (von {})", tracksWithTranslation, clip.getTracks().length);
|
||
}
|
||
|
||
}
|