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

JSON Entity Animations #1476

Merged
merged 27 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e6bc36a
Add JSON entity animation support
Gaming32 Aug 20, 2024
5b2ba99
Remove "spline" as an alias for "catmullrom"
Gaming32 Aug 20, 2024
f144016
Allow custom targets and interpolations to be registered
Gaming32 Aug 21, 2024
724c024
Remove JsonAnimator, add an AnimationHolder, and expose AnimationHold…
Gaming32 Aug 21, 2024
a02ae3f
Remove reference to JsonEA in javadoc
Gaming32 Aug 21, 2024
21d883d
Only hold a weak reference to unbound AnimationHolders
Gaming32 Aug 22, 2024
1f7c333
Use codecs for animations
Gaming32 Aug 22, 2024
1facc03
Fix build
Gaming32 Aug 22, 2024
7f7ff8d
Add javadoc and make slight improvements
Gaming32 Aug 22, 2024
0f5ac47
Add serialization round trip test
Gaming32 Aug 22, 2024
734e6c1
Run applyAllFormatting
Gaming32 Aug 22, 2024
796f358
Add some more documentation
Gaming32 Aug 23, 2024
198cc8b
Change missing animation warnings to be more robust
Gaming32 Aug 23, 2024
a230458
Error if a target could not be found from a channel target
Gaming32 Aug 23, 2024
17e1418
Register custom animation types in datagen
Gaming32 Aug 23, 2024
b0c51b9
strongHolderList -> strongHolderReferences
Gaming32 Aug 26, 2024
8f484e3
Insert javadocs indicating serialized representations of AnimationPar…
Gaming32 Aug 26, 2024
397062b
Include example of looping
Gaming32 Aug 26, 2024
27e176f
Apply formatting
Gaming32 Aug 26, 2024
6d44bee
Use KeyDispatchCodec directly
Gaming32 Aug 26, 2024
cbb6182
Move json animation type loading into ClientHooks.initClientHooks
Gaming32 Sep 4, 2024
8ece2a1
Merge branch '1.21.x' into json-entity-animations
Gaming32 Sep 4, 2024
a563035
Precompute keyframe codecs
Gaming32 Sep 4, 2024
9a3e45d
Check against duplicate registration for RegisterJsonAnimationTypesEvent
Gaming32 Sep 5, 2024
01b3b8b
Fix formatting
Gaming32 Sep 5, 2024
48892da
Don't use ModLoadingContext
Gaming32 Sep 5, 2024
f3924aa
Merge branch '1.21.x' into json-entity-animations
XFactHD Sep 5, 2024
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
48 changes: 48 additions & 0 deletions patches/net/minecraft/client/model/HierarchicalModel.java.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--- a/net/minecraft/client/model/HierarchicalModel.java
+++ b/net/minecraft/client/model/HierarchicalModel.java
@@ -27,6 +_,10 @@
super(p_170623_);
}

+ protected static net.neoforged.neoforge.client.entity.animation.json.AnimationHolder getAnimation(ResourceLocation key) {
+ return net.neoforged.neoforge.client.entity.animation.json.AnimationLoader.INSTANCE.getAnimationHolder(key);
+ }
+
@Override
public void renderToBuffer(PoseStack p_170625_, VertexConsumer p_170626_, int p_170627_, int p_170628_, int p_350603_) {
this.root().render(p_170625_, p_170626_, p_170627_, p_170628_, p_350603_);
@@ -44,18 +_,34 @@
this.animate(p_233382_, p_233383_, p_233384_, 1.0F);
}

+ protected void animate(AnimationState animationState, net.neoforged.neoforge.client.entity.animation.json.AnimationHolder animation, float ageInTicks) {
+ this.animate(animationState, animation.get(), ageInTicks);
+ }
+
protected void animateWalk(AnimationDefinition p_268159_, float p_268057_, float p_268347_, float p_268138_, float p_268165_) {
long i = (long)(p_268057_ * 50.0F * p_268138_);
float f = Math.min(p_268347_ * p_268165_, 1.0F);
KeyframeAnimations.animate(this, p_268159_, i, f, ANIMATION_VECTOR_CACHE);
}

+ protected void animateWalk(net.neoforged.neoforge.client.entity.animation.json.AnimationHolder animation, float limbSwing, float limbSwingAmount, float maxAnimationSpeed, float animationScaleFactor) {
+ this.animateWalk(animation.get(), limbSwing, limbSwingAmount, maxAnimationSpeed, animationScaleFactor);
+ }
+
protected void animate(AnimationState p_233386_, AnimationDefinition p_233387_, float p_233388_, float p_233389_) {
p_233386_.updateTime(p_233388_, p_233389_);
p_233386_.ifStarted(p_233392_ -> KeyframeAnimations.animate(this, p_233387_, p_233392_.getAccumulatedTime(), 1.0F, ANIMATION_VECTOR_CACHE));
}

+ protected void animate(AnimationState animationState, net.neoforged.neoforge.client.entity.animation.json.AnimationHolder animation, float ageInTicks, float speed) {
+ this.animate(animationState, animation.get(), ageInTicks, speed);
+ }
+
protected void applyStatic(AnimationDefinition p_288996_) {
KeyframeAnimations.animate(this, p_288996_, 0L, 1.0F, ANIMATION_VECTOR_CACHE);
+ }
+
+ protected void applyStatic(net.neoforged.neoforge.client.entity.animation.json.AnimationHolder animation) {
+ this.applyStatic(animation.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfigs;
import net.neoforged.neoforge.client.entity.animation.json.AnimationLoader;
import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent;
import net.neoforged.neoforge.client.event.ModelEvent;
import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent;
Expand Down Expand Up @@ -74,6 +75,7 @@ static void onRegisterGeometryLoaders(ModelEvent.RegisterGeometryLoaders event)
@SubscribeEvent
static void onRegisterReloadListeners(RegisterClientReloadListenersEvent event) {
event.registerReloadListener(ObjLoader.INSTANCE);
event.registerReloadListener(AnimationLoader.INSTANCE);
}

@SubscribeEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.entity.animation;

import org.joml.Vector3f;

@FunctionalInterface
public interface AnimationKeyframeTarget {
Vector3f apply(float x, float y, float z);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.entity.animation;

import net.minecraft.client.animation.AnimationChannel;
import net.minecraft.client.animation.KeyframeAnimations;

public record AnimationTarget(AnimationChannel.Target channelTarget, AnimationKeyframeTarget keyframeTarget) {
public static final AnimationTarget POSITION = new AnimationTarget(AnimationChannel.Targets.POSITION, KeyframeAnimations::posVec);
public static final AnimationTarget ROTATION = new AnimationTarget(AnimationChannel.Targets.ROTATION, KeyframeAnimations::degreeVec);
public static final AnimationTarget SCALE = new AnimationTarget(AnimationChannel.Targets.SCALE, KeyframeAnimations::scaleVec);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.entity.animation.json;

import net.minecraft.client.animation.AnimationDefinition;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Nullable;

public final class AnimationHolder {
private final ResourceLocation key;
@Nullable
private AnimationDefinition value;

AnimationHolder(ResourceLocation key) {
this.key = key;
}

void unbind() {
this.value = null;
}

void bind(AnimationDefinition value) {
this.value = value;
}

public ResourceLocation key() {
return key;
}

public AnimationDefinition get() {
final var result = value;
if (result == null) {
throw new IllegalStateException("Unknown entity animation " + key);
}
return result;
}

@Nullable
public AnimationDefinition getOrNull() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.entity.animation.json;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.mojang.logging.LogUtils;
import java.util.HashMap;
import java.util.Map;
import net.minecraft.client.animation.AnimationDefinition;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.profiling.ProfilerFiller;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

/**
* A loader for entity animations written in JSON. You can also get parsed animations from this class.
*/
public final class AnimationLoader extends SimpleJsonResourceReloadListener {
private static final Logger LOGGER = LogUtils.getLogger();

public static final AnimationLoader INSTANCE = new AnimationLoader();

private final Map<ResourceLocation, AnimationHolder> animations = new HashMap<>();
Gaming32 marked this conversation as resolved.
Show resolved Hide resolved

private AnimationLoader() {
super(new Gson(), "animations/entity");
}

@Nullable
public AnimationDefinition getAnimation(ResourceLocation key) {
final var holder = animations.get(key);
return holder != null ? holder.getOrNull() : null;
}

public AnimationHolder getAnimationHolder(ResourceLocation key) {
return animations.computeIfAbsent(key, AnimationHolder::new);
}

@Override
protected void apply(Map<ResourceLocation, JsonElement> animationJsons, ResourceManager resourceManager, ProfilerFiller profiler) {
AnimationTypeManager.init();
Gaming32 marked this conversation as resolved.
Show resolved Hide resolved
animations.values().forEach(AnimationHolder::unbind);
int loaded = 0;
for (final var entry : animationJsons.entrySet()) {
try {
final var animation = AnimationParser.parseDefinition(
GsonHelper.convertToJsonObject(entry.getValue(), "animation"));
getAnimationHolder(entry.getKey()).bind(animation);
loaded++;
} catch (Exception e) {
LOGGER.error("Failed to load animation {}", entry.getKey(), e);
}
}
LOGGER.info("Loaded {} entity animations", loaded);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.entity.animation.json;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.util.Locale;
import net.minecraft.client.animation.AnimationChannel;
import net.minecraft.client.animation.AnimationDefinition;
import net.minecraft.client.animation.Keyframe;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import net.neoforged.neoforge.client.entity.animation.AnimationKeyframeTarget;
import org.joml.Vector3f;

/**
* A parser for parsing JSON-based entity animation files.
*/
public final class AnimationParser {
private AnimationParser() {}

/**
* Parses the specified {@link JsonObject} into an animation
*
* @param json The {@link JsonObject} to parse
* @return The parsed animation
* @throws IllegalArgumentException If the specified {@link JsonObject} does not represent a valid entity animation
*/
public static AnimationDefinition parseDefinition(JsonObject json) {
final AnimationDefinition.Builder builder = AnimationDefinition.Builder.withLength(
GsonHelper.getAsFloat(json, "length"));
if (GsonHelper.getAsBoolean(json, "loop", false)) {
builder.looping();
}
for (final JsonElement element : GsonHelper.getAsJsonArray(json, "animations")) {
final JsonObject object = GsonHelper.convertToJsonObject(element, "animation");
builder.addAnimation(GsonHelper.getAsString(object, "bone"), parseChannel(object));
}
return builder.build();
}

private static AnimationChannel parseChannel(JsonObject json) {
final var targetName = ResourceLocation.parse(GsonHelper.getAsString(json, "target"));
final var target = AnimationTypeManager.getTarget(targetName);
if (target == null) {
throw new JsonParseException(String.format(
Locale.ENGLISH, "Animation target '%s' not found. Registered targets: %s",
targetName, AnimationTypeManager.getTargetList()));
}
final JsonArray keyframesJson = GsonHelper.getAsJsonArray(json, "keyframes");
final Keyframe[] keyframes = new Keyframe[keyframesJson.size()];
for (int i = 0; i < keyframes.length; i++) {
keyframes[i] = parseKeyframe(
GsonHelper.convertToJsonObject(keyframesJson.get(i), "keyframe"),
target.keyframeTarget());
}
return new AnimationChannel(target.channelTarget(), keyframes);
}

private static Keyframe parseKeyframe(JsonObject json, AnimationKeyframeTarget target) {
final var interpolationName = ResourceLocation.parse(GsonHelper.getAsString(json, "interpolation"));
final var interpolation = AnimationTypeManager.getInterpolation(interpolationName);
if (interpolation == null) {
throw new JsonParseException(String.format(
Locale.ENGLISH, "Animation interpolation '%s' not found. Registered interpolations: %s",
interpolationName, AnimationTypeManager.getInterpolationList()));
}
return new Keyframe(
GsonHelper.getAsFloat(json, "timestamp"),
parseVector(GsonHelper.getAsJsonArray(json, "target"), target),
interpolation);
}

private static Vector3f parseVector(JsonArray array, AnimationKeyframeTarget target) {
return target.apply(
GsonHelper.convertToFloat(array.get(0), "x"),
GsonHelper.convertToFloat(array.get(1), "y"),
GsonHelper.convertToFloat(array.get(2), "z"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.neoforge.client.entity.animation.json;

import com.google.common.collect.ImmutableMap;
import java.util.stream.Collectors;
import net.minecraft.client.animation.AnimationChannel;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.fml.ModLoader;
import net.neoforged.neoforge.client.entity.animation.AnimationTarget;
import net.neoforged.neoforge.client.event.RegisterJsonAnimationTypesEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

public final class AnimationTypeManager {
private static ImmutableMap<ResourceLocation, AnimationTarget> TARGETS = ImmutableMap.of();
private static ImmutableMap<ResourceLocation, AnimationChannel.Interpolation> INTERPOLATIONS = ImmutableMap.of();
private static String TARGET_LIST = "";
private static String INTERPOLATION_LIST = "";

private AnimationTypeManager() {}

@Nullable
public static AnimationTarget getTarget(ResourceLocation name) {
return TARGETS.get(name);
}

@Nullable
public static AnimationChannel.Interpolation getInterpolation(ResourceLocation name) {
return INTERPOLATIONS.get(name);
}

public static String getTargetList() {
return TARGET_LIST;
}

public static String getInterpolationList() {
return INTERPOLATION_LIST;
}

@ApiStatus.Internal
public static void init() {
final var targets = ImmutableMap.<ResourceLocation, AnimationTarget>builder()
.put(ResourceLocation.withDefaultNamespace("position"), AnimationTarget.POSITION)
.put(ResourceLocation.withDefaultNamespace("rotation"), AnimationTarget.ROTATION)
.put(ResourceLocation.withDefaultNamespace("scale"), AnimationTarget.SCALE);
final var interpolations = ImmutableMap.<ResourceLocation, AnimationChannel.Interpolation>builder()
.put(ResourceLocation.withDefaultNamespace("linear"), AnimationChannel.Interpolations.LINEAR)
.put(ResourceLocation.withDefaultNamespace("catmullrom"), AnimationChannel.Interpolations.CATMULLROM);
final var event = new RegisterJsonAnimationTypesEvent(targets, interpolations);
ModLoader.postEventWrapContainerInModOrder(event);
TARGETS = targets.buildOrThrow();
INTERPOLATIONS = interpolations.buildOrThrow();
TARGET_LIST = TARGETS.keySet()
.stream()
.map(ResourceLocation::toString)
.collect(Collectors.joining(", "));
INTERPOLATION_LIST = INTERPOLATIONS.keySet()
.stream()
.map(ResourceLocation::toString)
.collect(Collectors.joining(", "));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

@FieldsAreNonnullByDefault
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
package net.neoforged.neoforge.client.entity.animation.json;

import javax.annotation.ParametersAreNonnullByDefault;
import net.minecraft.FieldsAreNonnullByDefault;
import net.minecraft.MethodsReturnNonnullByDefault;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

@FieldsAreNonnullByDefault
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
package net.neoforged.neoforge.client.entity.animation;

import javax.annotation.ParametersAreNonnullByDefault;
import net.minecraft.FieldsAreNonnullByDefault;
import net.minecraft.MethodsReturnNonnullByDefault;
Loading
Loading