diff --git a/src/main/java/fr/hugman/animation_api/AnimationAPI.java b/src/main/java/fr/hugman/animation_api/AnimationAPI.java new file mode 100644 index 00000000..06b6ef9e --- /dev/null +++ b/src/main/java/fr/hugman/animation_api/AnimationAPI.java @@ -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 INSTANCE = ReloadableResourceManager.of(AnimationCodecs.ANIMATION, ResourceType.CLIENT_RESOURCES, "animations"); + + public static void init() { + INSTANCE.register(Dawn.id("animations")); + } +} diff --git a/src/main/java/fr/hugman/animation_api/codec/AnimationCodecs.java b/src/main/java/fr/hugman/animation_api/codec/AnimationCodecs.java new file mode 100644 index 00000000..eb7cfe63 --- /dev/null +++ b/src/main/java/fr/hugman/animation_api/codec/AnimationCodecs.java @@ -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 = 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 = 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"); + } +} diff --git a/src/main/java/fr/hugman/animation_api/render/entity/animation/CustomAnimation.java b/src/main/java/fr/hugman/animation_api/render/entity/animation/CustomAnimation.java new file mode 100644 index 00000000..004164a3 --- /dev/null +++ b/src/main/java/fr/hugman/animation_api/render/entity/animation/CustomAnimation.java @@ -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> frames) { + public static final Codec 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> boneAnimations = new HashMap<>(); + + for (Map.Entry> entry : frames.entrySet()) { + float timestamp = Float.parseFloat(entry.getKey()); + for (Map.Entry 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> frames = new HashMap<>(); + + for (Map.Entry> entry : animation.boneAnimations().entrySet()) { + String boneName = entry.getKey(); + List 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 translate, + Optional rotate, Optional scale) { + public static final Codec 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)); + } + } +} diff --git a/src/main/java/fr/hugman/promenade/Promenade.java b/src/main/java/fr/hugman/promenade/Promenade.java index c1f16403..53c086c3 100644 --- a/src/main/java/fr/hugman/promenade/Promenade.java +++ b/src/main/java/fr/hugman/promenade/Promenade.java @@ -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; @@ -50,6 +51,8 @@ public void onInitialize() { PromenadeBiomes.appendWorldGen(); PromenadePlacedFeatures.appendWorldGen(); PromenadeEntityTypes.appendWorldGen(); + + AnimationAPI.init(); } public static Identifier id(String path) { diff --git a/src/main/java/fr/hugman/promenade/client/render/entity/animation/AnimationKeys.java b/src/main/java/fr/hugman/promenade/client/render/entity/animation/AnimationKeys.java new file mode 100644 index 00000000..6e2b8c96 --- /dev/null +++ b/src/main/java/fr/hugman/promenade/client/render/entity/animation/AnimationKeys.java @@ -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"); +} diff --git a/src/main/java/fr/hugman/promenade/client/render/entity/animation/CapybaraAnimations.java b/src/main/java/fr/hugman/promenade/client/render/entity/animation/CapybaraAnimations.java index ba4fba0b..b24479a5 100644 --- a/src/main/java/fr/hugman/promenade/client/render/entity/animation/CapybaraAnimations.java +++ b/src/main/java/fr/hugman/promenade/client/render/entity/animation/CapybaraAnimations.java @@ -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), diff --git a/src/main/java/fr/hugman/promenade/client/render/entity/model/CapybaraModel.java b/src/main/java/fr/hugman/promenade/client/render/entity/model/CapybaraModel.java index 2809577c..5e979a44 100644 --- a/src/main/java/fr/hugman/promenade/client/render/entity/model/CapybaraModel.java +++ b/src/main/java/fr/hugman/promenade/client/render/entity/model/CapybaraModel.java @@ -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; @@ -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) { @@ -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) { diff --git a/src/main/java/fr/hugman/promenade/entity/CapybaraEntity.java b/src/main/java/fr/hugman/promenade/entity/CapybaraEntity.java index 7e56d0a6..623a967d 100644 --- a/src/main/java/fr/hugman/promenade/entity/CapybaraEntity.java +++ b/src/main/java/fr/hugman/promenade/entity/CapybaraEntity.java @@ -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(); @@ -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(); diff --git a/src/main/resources/assets/promenade/animations/capybara_ear_wiggle.json b/src/main/resources/assets/promenade/animations/capybara_ear_wiggle.json new file mode 100644 index 00000000..dbadb7cc --- /dev/null +++ b/src/main/resources/assets/promenade/animations/capybara_ear_wiggle.json @@ -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] + } + } + } +} \ No newline at end of file