/* * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight) * by the Epic Fight Team, licensed under GPLv3. * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3. */ package com.tiedup.remake.rig.anim; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.mutable.MutableInt; import net.minecraft.util.Mth; public class AnimationClip { public static final AnimationClip EMPTY_CLIP = new AnimationClip(); protected Map jointTransforms = new HashMap<> (); protected float clipTime; protected float[] bakedTimes; /// To modify existing keyframes in runtime and keep the baked state, call [#setBaked] again /// after finishing clip modification. (Frequent calls of this method will cause a performance issue) public void addJointTransform(String jointName, TransformSheet sheet) { this.jointTransforms.put(jointName, sheet); this.bakedTimes = null; } public boolean hasJointTransform(String jointName) { return this.jointTransforms.containsKey(jointName); } /// Bakes all keyframes to optimize calculating current pose, public void bakeKeyframes() { Set timestamps = new HashSet<> (); this.jointTransforms.values().forEach(transformSheet -> { transformSheet.forEach((i, keyframe) -> { timestamps.add(keyframe.time()); }); }); float[] bakedTimestamps = new float[timestamps.size()]; MutableInt mi = new MutableInt(0); timestamps.stream().sorted().toList().forEach(f -> { bakedTimestamps[mi.getAndAdd(1)] = f; }); Map bakedJointTransforms = new HashMap<> (); this.jointTransforms.forEach((jointName, transformSheet) -> { bakedJointTransforms.put(jointName, transformSheet.createInterpolated(bakedTimestamps)); }); this.jointTransforms = bakedJointTransforms; this.bakedTimes = bakedTimestamps; } /// Bake keyframes supposing all keyframes are aligned (mainly used when creating link animations) public void setBaked() { TransformSheet transformSheet = this.jointTransforms.get("Root"); if (transformSheet != null) { this.bakedTimes = new float[transformSheet.getKeyframes().length]; for (int i = 0; i < transformSheet.getKeyframes().length; i++) { this.bakedTimes[i] = transformSheet.getKeyframes()[i].time(); } } } public TransformSheet getJointTransform(String jointName) { return this.jointTransforms.get(jointName); } public final Pose getPoseInTime(float time) { Pose pose = new Pose(); if (time < 0.0F) { time = this.clipTime + time; } if (this.bakedTimes != null && this.bakedTimes.length > 0) { // Binary search int begin = 0, end = this.bakedTimes.length - 1; while (end - begin > 1) { int i = begin + (end - begin) / 2; if (this.bakedTimes[i] <= time && this.bakedTimes[i+1] > time) { begin = i; end = i+1; break; } else { if (this.bakedTimes[i] > time) { end = i; } else if (this.bakedTimes[i+1] <= time) { begin = i; } } } float delta = Mth.clamp((time - this.bakedTimes[begin]) / (this.bakedTimes[end] - this.bakedTimes[begin]), 0.0F, 1.0F); TransformSheet.InterpolationInfo iInfo = new TransformSheet.InterpolationInfo(begin, end, delta); for (String jointName : this.jointTransforms.keySet()) { pose.putJointData(jointName, this.jointTransforms.get(jointName).getInterpolatedTransform(iInfo)); } } else { for (String jointName : this.jointTransforms.keySet()) { pose.putJointData(jointName, this.jointTransforms.get(jointName).getInterpolatedTransform(time)); } } return pose; } /// @return returns protected keyframes of each joint to keep the baked state of keyframes. public Map getJointTransforms() { return Collections.unmodifiableMap(this.jointTransforms); } public void reset() { this.jointTransforms.clear(); this.bakedTimes = null; } public void setClipTime(float clipTime) { this.clipTime = clipTime; } public float getClipTime() { return this.clipTime; } }