diff --git a/core/src/main/java/com/zigythebird/playeranimcore/bones/PlayerAnimBone.java b/core/src/main/java/com/zigythebird/playeranimcore/bones/PlayerAnimBone.java index 042e21c7..3a28627b 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/bones/PlayerAnimBone.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/bones/PlayerAnimBone.java @@ -432,7 +432,7 @@ private float beginOrEndTickLerp(float startValue, float endValue, Float transit if (!keyFrames.isEmpty()) easingType = keyFrames.getLast().easingType(); } } - if (easingType == EasingType.BEZIER || easingType == EasingType.BEZIER_AFTER || easingType == EasingType.CATMULLROM) + if (easingType == EasingType.BEZIER || easingType == EasingType.CATMULLROM) easingType = EasingType.EASE_IN_OUT_SINE; } if (transitionLength == null) return endValue; diff --git a/core/src/main/java/com/zigythebird/playeranimcore/easing/BezierEasing.java b/core/src/main/java/com/zigythebird/playeranimcore/easing/BezierEasing.java index 4ccdb5d7..c692b7d8 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/easing/BezierEasing.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/easing/BezierEasing.java @@ -11,125 +11,131 @@ import java.util.ArrayList; import java.util.List; -abstract class BezierEasing implements EasingTypeTransformer { +public class BezierEasing implements EasingTypeTransformer { @Override public Float2FloatFunction buildTransformer(@Nullable Float value) { return EasingType.easeIn(EasingType::linear); } - abstract boolean isEasingBefore(); - @Override public float apply(MochaEngine env, AnimationPoint animationPoint, @Nullable Float easingValue, float lerpValue) { List> easingArgs = animationPoint.easingArgs(); if (easingArgs.isEmpty()) return MochaMath.lerp(animationPoint.animationStartValue(), animationPoint.animationEndValue(), buildTransformer(easingValue).apply(lerpValue)); - float rightValue = isEasingBefore() ? 0 : env.eval(easingArgs.getFirst()); - float rightTime = isEasingBefore() ? 0.1f : env.eval(easingArgs.get(1)); - float leftValue = isEasingBefore() ? env.eval(easingArgs.getFirst()) : 0; - float leftTime = isEasingBefore() ? env.eval(easingArgs.get(1)) : -0.1f; + float rightValue; + float rightTime; + float leftValue = env.eval(easingArgs.getFirst()); + float leftTime = env.eval(easingArgs.get(1)); if (easingArgs.size() > 3) { rightValue = env.eval(easingArgs.get(2)); rightTime = env.eval(easingArgs.get(3)); } - - leftValue = (float) Math.toRadians(leftValue); - rightValue = (float) Math.toRadians(rightValue); - - float gapTime = animationPoint.transitionLength()/20; - - float time_handle_before = Math.clamp(rightTime, 0, gapTime); - float time_handle_after = Math.clamp(leftTime, -gapTime, 0); - - CubicBezierCurve curve = new CubicBezierCurve( - new ModVector2d(0, animationPoint.animationStartValue()), - new ModVector2d(time_handle_before, animationPoint.animationStartValue() + rightValue), - new ModVector2d(time_handle_after + gapTime, animationPoint.animationEndValue() + leftValue), - new ModVector2d(gapTime, animationPoint.animationEndValue())); - float time = gapTime * lerpValue; - - List points = curve.getPoints(200); - ModVector2d closest = new ModVector2d(); - float closest_diff = Float.POSITIVE_INFINITY; - for (ModVector2d point : points) { - float diff = Math.abs(point.x - time); - if (diff < closest_diff) { - closest_diff = diff; - closest.set(point); - } + else { + rightValue = 0; + rightTime = 0.1f; } - ModVector2d second_closest = new ModVector2d(); - closest_diff = Float.POSITIVE_INFINITY; - for (ModVector2d point : points) { - if (point == closest) continue; - float diff = Math.abs(point.x - time); - if (diff < closest_diff) { - closest_diff = diff; - second_closest.set(closest); - second_closest.set(point); - } + + float transitionLength = animationPoint.transitionLength()/20f; + + float time_handle_before = Math.clamp(rightTime/transitionLength, 0, 1); + float time_handle_after = Math.clamp(leftTime/transitionLength, -1, 0); + + ModVector2d P0 = new ModVector2d(0, animationPoint.animationStartValue()); + ModVector2d P1 = new ModVector2d(time_handle_before, animationPoint.animationStartValue() + rightValue); + ModVector2d P2 = new ModVector2d(time_handle_after + 1, animationPoint.animationEndValue() + leftValue); + ModVector2d P3 = new ModVector2d(1, animationPoint.animationEndValue()); + + // Determine t + float t; + if (lerpValue == P0.x) { + // Handle corner cases explicitly to prevent rounding errors + t = 0; + } else if (lerpValue == P3.x) { + t = 1; + } else { + // Calculate t + float a = -P0.x + 3 * P1.x - 3 * P2.x + P3.x; + float b = 3 * P0.x - 6 * P1.x + 3 * P2.x; + float c = -3 * P0.x + 3 * P1.x; + float d = P0.x - lerpValue; + Float tTemp = SolveCubic(a, b, c, d); + if (tTemp == null) return animationPoint.animationEndValue(); + t = tTemp; } - return MochaMath.lerp(closest.y, second_closest.y, Math.clamp(MochaMath.lerp(closest.x, second_closest.x, time), 0, 1)); - } -} -class BezierEasingBefore extends BezierEasing { - @Override - boolean isEasingBefore() { - return true; + // Calculate y from t + return Cubed(1 - t) * P0.y + + 3 * t * Squared(1 - t) * P1.y + + 3 * Squared(t) * (1 - t) * P2.y + + Cubed(t) * P3.y; } -} -class BezierEasingAfter extends BezierEasing { - @Override - boolean isEasingBefore() { - return false; - } -} - -class CubicBezierCurve { - private ModVector2d v0; - private ModVector2d v1; - private ModVector2d v2; - private ModVector2d v3; - - public CubicBezierCurve(ModVector2d v0, ModVector2d v1, ModVector2d v2, ModVector2d v3) { - this.v0 = v0; - this.v1 = v1; - this.v2 = v2; - this.v3 = v3; - } + // Solves the equation ax³+bx²+cx+d = 0 for x ϵ ℝ + // and returns the first result in [0, 1] or null. + private static Float SolveCubic(float a, float b, float c, float d) { + if (a == 0) return SolveQuadratic(b, c, d); + if (d == 0) return 0f; + + b /= a; + c /= a; + d /= a; + float q = (3 * c - Squared(b)) / 9; + float r = (-27 * d + b * (9 * c - 2 * Squared(b))) / 54; + float disc = Cubed(q) + Squared(r); + float term1 = b / 3; + + if (disc > 0) { + float s = (float) (r + Math.sqrt(disc)); + s = (s < 0) ? -CubicRoot(-s) : CubicRoot(s); + float t = (float) (r - Math.sqrt(disc)); + t = (t < 0) ? -CubicRoot(-t) : CubicRoot(t); + + float result = -term1 + s + t; + if (result >= 0 && result <= 1) return result; + } else if (disc == 0) { + float r13 = (r < 0) ? -CubicRoot(-r) : CubicRoot(r); + + float result = -term1 + 2 * r13; + if (result >= 0 && result <= 1) return result; + + result = -(r13 + term1); + if (result >= 0 && result <= 1) return result; + } else { + q = -q; + float dum1 = q * q * q; + dum1 = (float) Math.acos(r / Math.sqrt(dum1)); + float r13 = (float) (2 * Math.sqrt(q)); + + float result = (float) (-term1 + r13 * Math.cos(dum1 / 3)); + if (result >= 0 && result <= 1) return result; + + result = (float) (-term1 + r13 * Math.cos((dum1 + 2 * Math.PI) / 3)); + if (result >= 0 && result <= 1) return result; + + result = (float) (-term1 + r13 * Math.cos((dum1 + 4 * Math.PI) / 3)); + if (result >= 0 && result <= 1) return result; + } - public ModVector2d getPoint(float t) { - return getPoint(t, new ModVector2d()); + return null; } - public ModVector2d getPoint(float t, ModVector2d target) { - if (target == null) { - target = new ModVector2d(); - } - - float u = 1 - t; - float tt = t * t; - float uu = u * u; - float uuu = uu * u; - float ttt = tt * t; + // Solves the equation ax² + bx + c = 0 for x ϵ ℝ + // and returns the first result in [0, 1] or null. + private static Float SolveQuadratic(float a, float b, float c) { + float result = (float) ((-b + Math.sqrt(Squared(b) - 4 * a * c)) / (2 * a)); + if (result >= 0 && result <= 1) return result; - target.x = uuu * v0.x + 3 * uu * t * v1.x + 3 * u * tt * v2.x + ttt * v3.x; - target.y = uuu * v0.y + 3 * uu * t * v1.y + 3 * u * tt * v2.y + ttt * v3.y; + result = (float) ((-b - Math.sqrt(Squared(b) - 4 * a * c)) / (2 * a)); + if (result >= 0 && result <= 1) return result; - return target; + return null; } - public List getPoints(int divisions) { - List points = new ArrayList<>(); + private static float Squared(float f) { return f * f; } - for (int i = 0; i <= divisions; i++) { - points.add(getPoint((float) i / divisions)); - } + private static float Cubed(float f) { return f * f * f; } - return points; - } + private static float CubicRoot(float f) { return (float) Math.pow(f, 1.0 / 3.0); } } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/easing/EasingType.java b/core/src/main/java/com/zigythebird/playeranimcore/easing/EasingType.java index 0998ad52..b0ed38ca 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/easing/EasingType.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/easing/EasingType.java @@ -70,8 +70,7 @@ public enum EasingType implements EasingTypeTransformer { CATMULLROM(36, "catmullrom", new CatmullRomEasing()), // 37 - STEP - BEZIER(38, "bezier", new BezierEasingBefore()), - BEZIER_AFTER(39, "bezier_after", new BezierEasingAfter()); + BEZIER(38, "bezier", new BezierEasing()); public final byte id; public final String name; diff --git a/core/src/main/java/com/zigythebird/playeranimcore/loading/AnimationLoader.java b/core/src/main/java/com/zigythebird/playeranimcore/loading/AnimationLoader.java index cdaa0c6e..263f57b7 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/loading/AnimationLoader.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/loading/AnimationLoader.java @@ -43,6 +43,7 @@ import team.unnamed.mocha.parser.ast.Expression; import team.unnamed.mocha.parser.ast.FloatExpression; import team.unnamed.mocha.parser.ast.IdentifierExpression; +import team.unnamed.mocha.runtime.IsConstantExpression; import java.lang.reflect.Type; import java.util.Collections; @@ -50,6 +51,8 @@ import java.util.List; import java.util.Map; +import static com.zigythebird.playeranimcore.molang.MolangLoader.MOCHA_ENGINE; + public class AnimationLoader implements JsonDeserializer { @Override public Animation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { @@ -274,7 +277,7 @@ private static KeyframeStack buildKeyframeStack(List> getEasingArgsForAxis(JsonObject entryObj, easingArg; } - private static List addArgsForKeyframes(List frames) { + private static List addArgsForKeyframes(List frames, TransformType type) { if (frames.getFirst().startValue().getFirst() instanceof AccessExpression accessExpression && "disabled".equals(accessExpression.property()) && accessExpression.object() instanceof IdentifierExpression identifierExpression && "pal".equals(identifierExpression.name())) @@ -317,17 +320,29 @@ private static List addArgsForKeyframes(List frames) { ))); } else if (frame.easingType() == EasingType.BEZIER) { + List leftValue = frame.easingArgs().getFirst(); List rightValue = frame.easingArgs().get(2); List rightTime = frame.easingArgs().get(3); - frame.easingArgs().remove(2); - frame.easingArgs().remove(2); + if (type == TransformType.ROTATION) { + rightValue = toRadiansForBezier(rightValue); + leftValue = toRadiansForBezier(leftValue); + } + frames.set(i, new Keyframe(frame.length(), frame.startValue(), frame.endValue(), frame.easingType(), + ObjectArrayList.of(leftValue, frame.easingArgs().get(1)))); + if (frame.easingArgs().size() > 4) { + frames.get(i).easingArgs().add(frame.easingArgs().get(4)); + frames.get(i).easingArgs().add(frame.easingArgs().get(5)); + } if (frames.size() > i + 1) { Keyframe nextKeyframe = frames.get(i + 1); - if (nextKeyframe.easingType() == EasingType.BEZIER) { + if (nextKeyframe.easingType() != EasingType.BEZIER) { + frames.set(i + 1, new Keyframe(nextKeyframe.length(), nextKeyframe.startValue(), nextKeyframe.endValue(), + EasingType.BEZIER, ObjectArrayList.of(PlayerAnimatorLoader.ZERO, PlayerAnimatorLoader.ZERO, rightValue, rightTime))); //TODO Maybe move the ZERO field to UniversalAnimLoader + } + else { nextKeyframe.easingArgs().add(rightValue); nextKeyframe.easingArgs().add(rightTime); } - else frames.set(i + 1, new Keyframe(nextKeyframe.length(), nextKeyframe.startValue(), nextKeyframe.endValue(), EasingType.BEZIER_AFTER, ObjectArrayList.of(rightValue, rightTime))); } } } @@ -335,6 +350,14 @@ else if (frame.easingType() == EasingType.BEZIER) { return frames; } + private static List toRadiansForBezier(List expressions) { + if (expressions.size() == 1 && IsConstantExpression.test(expressions.getFirst())) { + return Collections.singletonList(FloatExpression.of(Math.toRadians(MOCHA_ENGINE.eval(expressions)))); + } + PlayerAnimLib.LOGGER.warn("Invalid easing arguments for bezier: {}\nFor rotations bezier args can only be floats.", expressions); + return expressions; + } + public static float calculateAnimationLength(Map boneAnimations) { float length = 0; diff --git a/minecraft/common/src/main/resources/assets/player_animation_library/player_animations/bezier_test.json b/minecraft/common/src/main/resources/assets/player_animation_library/player_animations/bezier_test.json new file mode 100644 index 00000000..593399e1 --- /dev/null +++ b/minecraft/common/src/main/resources/assets/player_animation_library/player_animations/bezier_test.json @@ -0,0 +1,53 @@ +{ + "format_version": "1.8.0", + "geckolib_format_version": 2, + "model": {}, + "parents": {}, + "animations": { + "bezier_test": { + "loopTick": 0.0, + "loop": true, + "animation_length": 2.0, + "player_animation_library": { + "name": "Тест экспорта бедрока", + "author": "MineEmotes", + "description": "", + "bages": [] + }, + "bones": { + "leftArm": { + "rotation": { + "0.0": { + "vector": [ + 0.0, + "pal.disabled", + "pal.disabled" + ], + "easingX": "bezier", + "easingArgsX": [ + -88.22536, + -0.66596, + -260.45306, + 0.98345 + ] + }, + "2.0": { + "vector": [ + 0.0, + "pal.disabled", + "pal.disabled" + ], + "easingX": "bezier", + "easingArgsX": [ + 246.38388, + -1.02578, + 80.31529, + 0.66608 + ] + } + } + } + } + } + } +} \ No newline at end of file