Skip to content

Commit

Permalink
feat(animation): add Interpolator combination (hierarchy)
Browse files Browse the repository at this point in the history
For example, if keyframe0 has linear interpolation and keyframe1 has smooth interpolation, the interpolation used between them will be smooth

This is consistent with Blockbench's priorities, but we do not support Bézier interpolation *yet*
  • Loading branch information
yusshu committed Oct 11, 2023
1 parent 9cad6c0 commit ef605b9
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import org.jetbrains.annotations.Nullable;
import team.unnamed.creative.base.Vector3Float;

import static java.util.Objects.requireNonNull;

final class CatmullRomInterpolator implements Interpolator<Vector3Float> {

static final Interpolator<Vector3Float> INSTANCE = new CatmullRomInterpolator();
Expand All @@ -44,6 +46,15 @@ private CatmullRomInterpolator() {
return new CatmullRomInterpolation(before, from, to, after);
}

@Override
public @NotNull Interpolator<Vector3Float> combineRight(final @NotNull Interpolator<Vector3Float> right) {
requireNonNull(right, "right");
// no matter what "right" interpolator is, combining a catmull-rom
// interpolator and any other interpolator, results in a catmull-rom
// interpolation
return this;
}

// based on Blockbench and Three.js implementations
static final class CatmullRomInterpolation implements Interpolation<Vector3Float> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import team.unnamed.creative.base.Vector3Float;
import team.unnamed.hephaestus.util.Quaternion;

import static java.util.Objects.requireNonNull;

/**
* Represents an interpolation function,
* which interpolates between two values of
Expand Down Expand Up @@ -93,6 +95,16 @@ public interface Interpolator<T> {
return interpolation(from, to);
}

default @NotNull Interpolator<T> combineRight(final @NotNull Interpolator<T> right) {
requireNonNull(right, "right");
return this;
}

default @NotNull Interpolator<T> combineLeft(final @NotNull Interpolator<T> left) {
requireNonNull(left, "left");
return left.combineRight(this);
}

/**
* Returns an interpolator for lineally interpolating
* {@link Vector3Float 3d vectors}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ private LinearVectorInterpolator() {
return new LinearVectorInterpolation(from, to);
}

@Override
public @NotNull Interpolator<Vector3Float> combineRight(final @NotNull Interpolator<Vector3Float> right) {
if (right instanceof LinearVectorInterpolator || right instanceof StepVectorInterpolator) {
// only keep linear vector interpolation if the right
// interpolator is also linear, or step
return this;
} else {
return right;
}
}

static final class LinearVectorInterpolation implements Interpolation<Vector3Float> {

private final Vector3Float from;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ private StepVectorInterpolator() {
return new StepVectorInterpolation(from, to);
}

@Override
public @NotNull Interpolator<Vector3Float> combineRight(final @NotNull Interpolator<Vector3Float> right) {
requireNonNull(right, "right");
// no matter what "right" interpolator is, combining a step
// interpolator and any other interpolator, results in a step
// interpolation
return this;
}

static final class StepVectorInterpolation implements Interpolation<Vector3Float> {

private final Vector3Float from;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,18 @@ final class PlayheadImpl<T> implements Playhead<T> {
after = keyFrameIterator.hasNext() ? keyFrameIterator.next() : null;
}

interpolation = previous.interpolatorOr(timeline.defaultInterpolator())
.interpolation(null, previous.value(), next.value(), after == null ? null : after.value());
interpolation = computeInterpolator().interpolation(null, previous.value(), next.value(), after == null ? null : after.value());
}

private Interpolator<T> computeInterpolator() {
Interpolator<T> interpolator;
if (next == null) {
interpolator = Interpolator.always(previous.value());
} else {
interpolator = previous.interpolatorOr(timeline.defaultInterpolator())
.combineRight(next.interpolatorOr(timeline.defaultInterpolator()));
}
return interpolator;
}

@Override
Expand Down Expand Up @@ -113,7 +123,7 @@ final class PlayheadImpl<T> implements Playhead<T> {
after = null;
}

interpolation = previous.interpolatorOr(timeline.defaultInterpolator())
interpolation = computeInterpolator()
.interpolation(
before == null ? null : before.value(),
previous.value(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
import team.unnamed.hephaestus.animation.timeline.playhead.Playhead;
import team.unnamed.hephaestus.animation.timeline.Timeline;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static team.unnamed.hephaestus.util.StructureAssertEquals.assertVectorEquals;

class TimelineTest {

Expand All @@ -47,14 +49,14 @@ private static void testTimeline(
Playhead<Vector3Float> it = timeline.createPlayhead();
Vector3Float lastValue = null;
for (Vector3Float expectedValue : expected) {
assertEquals(expectedValue, it.next());
assertVectorEquals(expectedValue, it.next(), 0.001);
lastValue = expectedValue;
}

if (lastValue != null) {
for (int i = 0; i < 10; i++) {
// should be kept on last value
assertEquals(lastValue, it.next());
assertVectorEquals(lastValue, it.next(), 0.001);
}
}
}
Expand Down Expand Up @@ -197,4 +199,68 @@ void test_catmullrom_interpolated_timeline() {
);
}

@Test
public void test_combine_lerp_and_smooth() {
testTimeline(
timeline -> {
// linear + smooth = smooth
timeline.keyFrame(0, new Vector3Float(0, 0, 0), Interpolator.lerpVector3Float());
timeline.keyFrame(4, new Vector3Float(20, 20, 20), Interpolator.catmullRomSplineVector3Float());
},
new Vector3Float(0, 0, 0),
new Vector3Float(4.063F, 4.063F, 4.063F),
new Vector3Float(10, 10, 10),
new Vector3Float(15.938F, 15.938F, 15.938F),
new Vector3Float(20, 20, 20)
);
}

@Test
public void test_combine_step_and_any() {
List<Interpolator<Vector3Float>> interpolators = Arrays.asList(
Interpolator.stepVector3Float(),
Interpolator.lerpVector3Float(),
Interpolator.catmullRomSplineVector3Float(),
Interpolator.always(new Vector3Float(84, 84, 84))
);
for (Interpolator<Vector3Float> interpolator : interpolators) {
testTimeline(
timeline -> {
// step + any = step
timeline.keyFrame(0, new Vector3Float(0, 0, 0), Interpolator.stepVector3Float());
timeline.keyFrame(4, new Vector3Float(20, 20, 20), interpolator);
},
new Vector3Float(0, 0, 0),
new Vector3Float(0, 0, 0),
new Vector3Float(0, 0, 0),
new Vector3Float(0, 0, 0),
new Vector3Float(20, 20, 20)
);
}
}

@Test
public void test_combine_smooth_and_any() {
List<Interpolator<Vector3Float>> interpolators = Arrays.asList(
Interpolator.stepVector3Float(),
Interpolator.lerpVector3Float(),
Interpolator.catmullRomSplineVector3Float(),
Interpolator.always(new Vector3Float(84, 84, 84))
);
for (Interpolator<Vector3Float> interpolator : interpolators) {
testTimeline(
timeline -> {
// smooth + any = smooth
timeline.keyFrame(0, new Vector3Float(0, 0, 0), Interpolator.catmullRomSplineVector3Float());
timeline.keyFrame(4, new Vector3Float(20, 20, 20), interpolator);
},
new Vector3Float(0, 0, 0),
new Vector3Float(4.063F, 4.063F, 4.063F),
new Vector3Float(10, 10, 10),
new Vector3Float(15.938F, 15.938F, 15.938F),
new Vector3Float(20, 20, 20)
);
}
}

}

0 comments on commit ef605b9

Please sign in to comment.