Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the animations data-driven #195

Draft
wants to merge 4 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/java/fr/hugman/animation_api/AnimationAPI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package fr.hugman.animation_api;

import fr.hugman.animation_api.codec.AnimationCodecs;
import fr.hugman.dawn.Dawn;
import fr.hugman.dawn.registry.ReloadableResourceManager;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.render.entity.animation.Animation;
import net.minecraft.resource.ResourceType;

@Environment(value= EnvType.CLIENT)
public class AnimationAPI {
public static final ReloadableResourceManager<Animation> INSTANCE = ReloadableResourceManager.of(AnimationCodecs.ANIMATION, ResourceType.CLIENT_RESOURCES, "animations");

public static void init() {
INSTANCE.register(Dawn.id("animations"));
}
}
43 changes: 43 additions & 0 deletions src/main/java/fr/hugman/animation_api/codec/AnimationCodecs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package fr.hugman.animation_api.codec;

import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import fr.hugman.animation_api.render.entity.animation.CustomAnimation;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.render.entity.animation.Animation;
import net.minecraft.client.render.entity.animation.Transformation;

import javax.annotation.Nullable;

@Environment(value= EnvType.CLIENT)
public class AnimationCodecs {
public static final Codec<Transformation.Interpolation> TRANSFORMATION_INTERPOLATION = Codec.STRING.comapFlatMap(string -> {
Transformation.Interpolation interpolation = transformationInterpolationFromString(string);
if (interpolation instanceof Transformation.Interpolation) {
return DataResult.success(interpolation);
}
return DataResult.error(() -> "Not a compound tag: " + string);
}, AnimationCodecs::transformationInterpolationToString);

public static final Codec<Animation> ANIMATION = CustomAnimation.CODEC.xmap(CustomAnimation::toVanillaAnimation, CustomAnimation::fromVanillaAnimation);

@Nullable
private static Transformation.Interpolation transformationInterpolationFromString(String interpolation) {
return switch (interpolation) {
case "linear" -> Transformation.Interpolations.LINEAR;
case "cubic" -> Transformation.Interpolations.CUBIC;
default -> null;
};
}

private static String transformationInterpolationToString(Transformation.Interpolation interpolation) {
if(interpolation == Transformation.Interpolations.LINEAR) {
return "linear";
}
if(interpolation == Transformation.Interpolations.CUBIC) {
return "cubic";
}
throw new IllegalArgumentException("Unknown interpolation: null");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package fr.hugman.animation_api.render.entity.animation;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.hugman.animation_api.codec.AnimationCodecs;
import net.minecraft.client.render.entity.animation.Animation;
import net.minecraft.client.render.entity.animation.AnimationHelper;
import net.minecraft.client.render.entity.animation.Keyframe;
import net.minecraft.client.render.entity.animation.Transformation;
import net.minecraft.util.dynamic.Codecs;
import org.joml.Vector3f;

import java.util.*;

public record CustomAnimation(float fullLength, boolean loop, Map<String, Map<String, BoneFrame>> frames) {
public static final Codec<CustomAnimation> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.FLOAT.fieldOf("length").forGetter(CustomAnimation::fullLength),
Codec.BOOL.optionalFieldOf("loop", false).forGetter(CustomAnimation::loop),
//TODO: have a StringIdentifiable codec for float key of the first map below
Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, BoneFrame.CODEC)).fieldOf("frames").forGetter(CustomAnimation::frames)
).apply(instance, CustomAnimation::new));

public Animation toVanillaAnimation() {
Map<String, List<Transformation>> boneAnimations = new HashMap<>();

for (Map.Entry<String, Map<String, BoneFrame>> entry : frames.entrySet()) {
float timestamp = Float.parseFloat(entry.getKey());
for (Map.Entry<String, BoneFrame> boneEntry : entry.getValue().entrySet()) {
String boneName = boneEntry.getKey();
BoneFrame boneFrame = boneEntry.getValue();

boneAnimations.computeIfAbsent(boneName, k -> new ArrayList<>());
var transformations = boneAnimations.get(boneName);
boneFrame.translate().ifPresent(translate -> {
var keyframe = new Keyframe(timestamp, translationToVanilla(translate), boneFrame.interpolation());
var transformation = transformations.stream().filter(t -> t.target() == Transformation.Targets.TRANSLATE).findFirst().orElse(new Transformation(Transformation.Targets.TRANSLATE));

Keyframe[] oldKeyframes = transformation.keyframes();
Keyframe[] newKeyframes = new Keyframe[oldKeyframes.length + 1];

System.arraycopy(oldKeyframes, 0, newKeyframes, 0, oldKeyframes.length);
newKeyframes[oldKeyframes.length] = keyframe;

transformations.remove(transformation);
transformations.add(new Transformation(Transformation.Targets.TRANSLATE, newKeyframes));
});
boneFrame.rotate().ifPresent(rotate -> {
var keyframe = new Keyframe(timestamp, rotationToVanilla(rotate), boneFrame.interpolation());
var transformation = transformations.stream().filter(t -> t.target() == Transformation.Targets.ROTATE).findFirst().orElse(new Transformation(Transformation.Targets.ROTATE));

Keyframe[] oldKeyframes = transformation.keyframes();
Keyframe[] newKeyframes = new Keyframe[oldKeyframes.length + 1];

System.arraycopy(oldKeyframes, 0, newKeyframes, 0, oldKeyframes.length);
newKeyframes[oldKeyframes.length] = keyframe;

transformations.remove(transformation);
transformations.add(new Transformation(Transformation.Targets.ROTATE, newKeyframes));
});
boneFrame.scale().ifPresent(scale -> {
var keyframe = new Keyframe(timestamp, scaleToVanilla(scale), boneFrame.interpolation());
var transformation = transformations.stream().filter(t -> t.target() == Transformation.Targets.SCALE).findFirst().orElse(new Transformation(Transformation.Targets.SCALE));

Keyframe[] oldKeyframes = transformation.keyframes();
Keyframe[] newKeyframes = new Keyframe[oldKeyframes.length + 1];

System.arraycopy(oldKeyframes, 0, newKeyframes, 0, oldKeyframes.length);
newKeyframes[oldKeyframes.length] = keyframe;

transformations.remove(transformation);
transformations.add(new Transformation(Transformation.Targets.SCALE, newKeyframes));
});
}
}

return new Animation(fullLength, loop, boneAnimations);
}

public static CustomAnimation fromVanillaAnimation(Animation animation) {
Map<String, Map<String, BoneFrame>> frames = new HashMap<>();

for (Map.Entry<String, List<Transformation>> entry : animation.boneAnimations().entrySet()) {
String boneName = entry.getKey();
List<Transformation> transformations = entry.getValue();

for (Transformation transformation : transformations) {
for (Keyframe keyframe : transformation.keyframes()) {
float timestamp = keyframe.timestamp();
BoneFrame boneFrame = frames.computeIfAbsent(Float.toString(timestamp), k -> new HashMap<>()).computeIfAbsent(boneName, k -> new BoneFrame(keyframe.interpolation(), Optional.empty(), Optional.empty(), Optional.empty()));

if (transformation.target() == Transformation.Targets.TRANSLATE) {

boneFrame = boneFrame.withTranslate(translationFromVanilla(keyframe.target()));
} else if (transformation.target() == Transformation.Targets.ROTATE) {
boneFrame = boneFrame.withRotate(rotationFromVanilla(keyframe.target()));
} else if (transformation.target() == Transformation.Targets.SCALE) {
boneFrame = boneFrame.withScale(scaleFromVanilla(keyframe.target()));
}

frames.get(timestamp).put(boneName, boneFrame);
}
}
}

return new CustomAnimation(animation.lengthInSeconds(), animation.looping(), frames);
}

public static Vector3f translationToVanilla(Vector3f vector) {
return AnimationHelper.createTranslationalVector(vector.x(), vector.y(), vector.z());
}

public static Vector3f rotationToVanilla(Vector3f vector) {
return AnimationHelper.createRotationalVector(vector.x(), vector.y(), vector.z());
}

public static Vector3f scaleToVanilla(Vector3f vector) {
return AnimationHelper.createScalingVector(vector.x(), vector.y(), vector.z());
}

public static Vector3f translationFromVanilla(Vector3f vector) {
return new Vector3f(vector.x(), -vector.y(), vector.z());
}

public static Vector3f rotationFromVanilla(Vector3f vector) {
return new Vector3f(vector.x() / ((float) Math.PI / 180), vector.y() / ((float) Math.PI / 180), vector.z() / ((float) Math.PI / 180));
}

public static Vector3f scaleFromVanilla(Vector3f vector) {
return new Vector3f(vector.x() + 1.0f, vector.y() + 1.0f, vector.z() + 1.0f);
}

record BoneFrame(Transformation.Interpolation interpolation, Optional<Vector3f> translate,
Optional<Vector3f> rotate, Optional<Vector3f> scale) {
public static final Codec<BoneFrame> CODEC = RecordCodecBuilder.create(instance -> instance.group(
AnimationCodecs.TRANSFORMATION_INTERPOLATION.optionalFieldOf("interpolation", Transformation.Interpolations.CUBIC).forGetter(BoneFrame::interpolation),
Codecs.VECTOR_3F.optionalFieldOf("translate").forGetter(BoneFrame::translate),
Codecs.VECTOR_3F.optionalFieldOf("rotate").forGetter(BoneFrame::rotate),
Codecs.VECTOR_3F.optionalFieldOf("scale").forGetter(BoneFrame::scale)
).apply(instance, BoneFrame::new));

public BoneFrame withTranslate(Vector3f translate) {
return new BoneFrame(interpolation, Optional.of(translate), rotate, scale);
}

public BoneFrame withRotate(Vector3f rotate) {
return new BoneFrame(interpolation, translate, Optional.of(rotate), scale);
}

public BoneFrame withScale(Vector3f scale) {
return new BoneFrame(interpolation, translate, rotate, Optional.of(scale));
}
}
}
3 changes: 3 additions & 0 deletions src/main/java/fr/hugman/promenade/Promenade.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.hugman.promenade;

import com.google.common.reflect.Reflection;
import fr.hugman.animation_api.AnimationAPI;
import fr.hugman.promenade.block.PromenadeBlocks;
import fr.hugman.promenade.boat.PromenadeBoatTypes;
import fr.hugman.promenade.config.PromenadeConfig;
Expand Down Expand Up @@ -50,6 +51,8 @@ public void onInitialize() {
PromenadeBiomes.appendWorldGen();
PromenadePlacedFeatures.appendWorldGen();
PromenadeEntityTypes.appendWorldGen();

AnimationAPI.init();
}

public static Identifier id(String path) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package fr.hugman.promenade.client.render.entity.animation;

import fr.hugman.promenade.Promenade;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.util.Identifier;

@Environment(EnvType.CLIENT)
public class AnimationKeys {
public static final Identifier CAPYBARA_EAR_WIGGLE = Promenade.id("capybara_ear_wiggle");
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@

@Environment(EnvType.CLIENT)
public class CapybaraAnimations {
public static final Animation EAR_WIGGLE = Animation.Builder.create(0.2f)
.addBoneAnimation(EntityModelPartNames.RIGHT_EAR, new Transformation(Transformation.Targets.ROTATE,
new Keyframe(0f, AnimationHelper.createRotationalVector(0f, 0f, 0f), Transformation.Interpolations.CUBIC),
new Keyframe(0.1f, AnimationHelper.createRotationalVector(-13.5f, -15f, 13f), Transformation.Interpolations.CUBIC),
new Keyframe(0.2f, AnimationHelper.createRotationalVector(0f, 0f, 0f), Transformation.Interpolations.CUBIC)))
.addBoneAnimation(EntityModelPartNames.LEFT_EAR, new Transformation(Transformation.Targets.ROTATE,
new Keyframe(0f, AnimationHelper.createRotationalVector(0f, 0f, 0f), Transformation.Interpolations.CUBIC),
new Keyframe(0.1f, AnimationHelper.createRotationalVector(-13.5f, 15f, -13f), Transformation.Interpolations.CUBIC),
new Keyframe(0.2f, AnimationHelper.createRotationalVector(0f, 0f, 0f), Transformation.Interpolations.CUBIC)))
.build();

public static final Animation WALKING = Animation.Builder.create(1.5f).looping()
.addBoneAnimation(EntityModelPartNames.BODY, new Transformation(Transformation.Targets.ROTATE,
new Keyframe(0f, AnimationHelper.createRotationalVector(0f, 0f, 2.5f), Transformation.Interpolations.CUBIC),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fr.hugman.promenade.client.render.entity.model;

import fr.hugman.animation_api.AnimationAPI;
import fr.hugman.promenade.client.render.entity.animation.AnimationKeys;
import fr.hugman.promenade.client.render.entity.animation.CapybaraAnimations;
import fr.hugman.promenade.entity.CapybaraEntity;
import net.fabricmc.api.EnvType;
Expand Down Expand Up @@ -57,7 +59,12 @@ public void setAngles(CapybaraEntity capybara, float limbAngle, float limbDistan
if (capybara.canAngleHead()) {
this.setHeadAngles(headYaw, headPitch);
}
this.updateAnimations(capybara, animationProgress);
this.animateMovement(CapybaraAnimations.WALKING, limbAngle, limbDistance, 5.0f, 3.5f);
this.updateAnimation(capybara.earWiggleAnimState, AnimationAPI.INSTANCE.get(AnimationKeys.CAPYBARA_EAR_WIGGLE), animationProgress, capybara.getEarWiggleSpeed());
this.updateAnimation(capybara.fallToSleepAnimState, CapybaraAnimations.FALL_TO_SLEEP, animationProgress, 1.0F);
this.updateAnimation(capybara.sleepingAnimState, CapybaraAnimations.SLEEP, animationProgress, 1.0F);
this.updateAnimation(capybara.wakeUpAnimState, CapybaraAnimations.WAKE_UP, animationProgress, 1.0F);
this.updateAnimation(capybara.fartAnimState, CapybaraAnimations.FART, animationProgress, 1.0F);
}

public void setHeadAngles(float headYaw, float headPitch) {
Expand All @@ -73,18 +80,6 @@ public ModelPart getPart() {
return this.root;
}

private void updateAnimations(CapybaraEntity capybara, float progress) {
float v = (float) capybara.getVelocity().horizontalLengthSquared();
float speed = MathHelper.clamp(v * 400.0F, 0.3F, 2.0F);

this.updateAnimation(capybara.walkingAnimationState, CapybaraAnimations.WALKING, progress, speed * 2);
this.updateAnimation(capybara.earWiggleAnimState, CapybaraAnimations.EAR_WIGGLE, progress, capybara.getEarWiggleSpeed());
this.updateAnimation(capybara.fallToSleepAnimState, CapybaraAnimations.FALL_TO_SLEEP, progress, 1.0F);
this.updateAnimation(capybara.sleepingAnimState, CapybaraAnimations.SLEEP, progress, 1.0F);
this.updateAnimation(capybara.wakeUpAnimState, CapybaraAnimations.WAKE_UP, progress, 1.0F);
this.updateAnimation(capybara.fartAnimState, CapybaraAnimations.FART, progress, 1.0F);
}

@Override
public void render(MatrixStack matrices, VertexConsumer vertices, int light, int overlay, float red, float green, float blue, float alpha) {
if (this.child) {
Expand Down
6 changes: 0 additions & 6 deletions src/main/java/fr/hugman/promenade/entity/CapybaraEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ public void fart() {

private static final int EAR_WIGGLE_LENGHT = (int) (0.2f * SharedConstants.TICKS_PER_SECOND);
private static final IntProvider EAR_WIGGLE_COOLDOWN_PROVIDER = BiasedToBottomIntProvider.create(EAR_WIGGLE_LENGHT, 64); // Minimum MUST be the length of the anim
public final AnimationState walkingAnimationState = new AnimationState();
public final AnimationState earWiggleAnimState = new AnimationState();
public final AnimationState fallToSleepAnimState = new AnimationState();
public final AnimationState sleepingAnimState = new AnimationState();
Expand All @@ -292,35 +291,30 @@ private void updateAnimations() {

switch (this.getState()) {
case STANDING -> {
this.walkingAnimationState.setRunning((this.isOnGround() || this.hasControllingPassenger()) && this.getVelocity().horizontalLengthSquared() > 1.0E-6, this.age);
this.fallToSleepAnimState.stop();
this.sleepingAnimState.stop();
this.wakeUpAnimState.stop();
this.fartAnimState.stop();
}
case FALL_TO_SLEEP -> {
this.walkingAnimationState.stop();
this.fallToSleepAnimState.startIfNotRunning(this.age);
this.sleepingAnimState.stop();
this.wakeUpAnimState.stop();
this.fartAnimState.stop();
}
case SLEEPING -> {
this.walkingAnimationState.stop();
this.fallToSleepAnimState.stop();
this.sleepingAnimState.startIfNotRunning(this.age);
this.wakeUpAnimState.stop();
this.fartAnimState.stop();
}
case WAKE_UP -> {
this.walkingAnimationState.stop();
this.fallToSleepAnimState.stop();
this.sleepingAnimState.stop();
this.wakeUpAnimState.startIfNotRunning(this.age);
this.fartAnimState.stop();
}
case FARTING -> {
this.walkingAnimationState.stop();
this.fallToSleepAnimState.stop();
this.sleepingAnimState.stop();
this.wakeUpAnimState.stop();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"length": 0.2,
"frames": {
"0.0": {
"right_ear": {
"rotate": [0,0,0]
},
"left_ear": {
"rotate": [0,0,0]
}
} ,
"0.1": {
"right_ear": {
"rotate": [-13.5,-15,13]
},
"left_ear": {
"rotate": [-13.5,15,-13]
}
},
"0.2": {
"right_ear": {
"rotate": [0,0,0]
},
"left_ear": {
"rotate": [0,0,0]
}
}
}
}