diff --git a/core/src/main/java/com/zigythebird/playeranimcore/PlayerAnimLib.java b/core/src/main/java/com/zigythebird/playeranimcore/PlayerAnimLib.java index 5f21407b..64513cef 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/PlayerAnimLib.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/PlayerAnimLib.java @@ -4,9 +4,11 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.zigythebird.playeranimcore.animation.Animation; +import com.zigythebird.playeranimcore.animation.CustomModelBone; import com.zigythebird.playeranimcore.loading.AnimationLoader; import com.zigythebird.playeranimcore.loading.KeyFrameLoader; import com.zigythebird.playeranimcore.loading.UniversalAnimLoader; +import com.zigythebird.playeranimcore.math.Vec3f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,6 +21,8 @@ public class PlayerAnimLib { public static final Type ANIMATIONS_MAP_TYPE = new TypeToken>() {}.getType(); public static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(CustomModelBone.class, new CustomModelBone.Deserializer()) + .registerTypeAdapter(Vec3f.class, new Vec3f.Deserializer()) .registerTypeAdapter(Animation.Keyframes.class, new KeyFrameLoader()) .registerTypeAdapter(Animation.class, new AnimationLoader()) .registerTypeAdapter(ANIMATIONS_MAP_TYPE, new UniversalAnimLoader()) diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/Animation.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/Animation.java index b93786fc..820e039f 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/animation/Animation.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/Animation.java @@ -31,7 +31,6 @@ import com.zigythebird.playeranimcore.animation.keyframe.event.data.ParticleKeyframeData; import com.zigythebird.playeranimcore.animation.keyframe.event.data.SoundKeyframeData; import com.zigythebird.playeranimcore.loading.UniversalAnimLoader; -import com.zigythebird.playeranimcore.math.Vec3f; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,7 +43,7 @@ *

* Modifications or extensions of a compiled Animation are not supported, and therefore an instance of Animation is considered final and immutable */ -public record Animation(ExtraAnimationData data, float length, LoopType loopType, Map boneAnimations, Keyframes keyFrames, Map bones, Map parents) implements Supplier { +public record Animation(ExtraAnimationData data, float length, LoopType loopType, Map boneAnimations, Keyframes keyFrames, Map bones, Map parents) implements Supplier { public record Keyframes(SoundKeyframeData[] sounds, ParticleKeyframeData[] particles, CustomInstructionKeyframeData[] customInstructions) { @Override public int hashCode() { diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/AnimationController.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/AnimationController.java index 9dce5f1c..d1abd0b2 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/animation/AnimationController.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/AnimationController.java @@ -24,6 +24,7 @@ package com.zigythebird.playeranimcore.animation; +import com.google.gson.JsonArray; import com.zigythebird.playeranimcore.PlayerAnimLib; import com.zigythebird.playeranimcore.animation.keyframe.*; import com.zigythebird.playeranimcore.animation.keyframe.event.CustomKeyFrameEvents; @@ -39,6 +40,7 @@ import com.zigythebird.playeranimcore.animation.layered.modifier.SpeedModifier; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonConfiguration; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonMode; +import com.zigythebird.playeranimcore.bindings.PlatformModel; import com.zigythebird.playeranimcore.bones.*; import com.zigythebird.playeranimcore.easing.EasingType; import com.zigythebird.playeranimcore.enums.PlayState; @@ -75,7 +77,7 @@ public abstract class AnimationController implements IAnimation { protected final Map bonePositions; protected final Map bones = new Object2ObjectOpenHashMap<>(); protected final Map activeBones = new Object2ObjectOpenHashMap<>(); - protected final Map pivotBones = new Object2ObjectOpenHashMap<>(); + protected final Map pivotBones = new Object2ObjectOpenHashMap<>(); protected Queue animationQueue = new LinkedList<>(); protected final MochaEngine molangRuntime; @@ -739,13 +741,26 @@ else if (pivotBones.containsKey(entry.getKey())) } this.pivotBones.clear(); - for (Map.Entry entry : currentAnimation.animation().bones().entrySet()) { - this.pivotBones.put(entry.getKey(), new PivotBone(entry.getKey(), entry.getValue())); + for (Map.Entry entry : currentAnimation.animation().bones().entrySet()) { + this.pivotBones.put(entry.getKey(), new CustomBone(entry.getKey(), entry.getValue().pivot(), loadCustomModel( + entry.getValue().texture(), + entry.getValue().elements() + ))); } this.postAnimationSetupConsumer.accept((name) -> bones.getOrDefault(name, null)); } + protected abstract @Nullable PlatformModel loadCustomModel(byte @Nullable [] texture, @Nullable JsonArray elements); + + @Override + public void collectModels(Consumer consumer) { + for (CustomBone customBone : this.pivotBones.values()) { + if (customBone.getModel() == null) continue; + consumer.accept(customBone); + } + } + /** * Compute animation value for the given keyframes at the specified tick */ diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/CustomModelBone.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/CustomModelBone.java new file mode 100644 index 00000000..196100d4 --- /dev/null +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/CustomModelBone.java @@ -0,0 +1,22 @@ +package com.zigythebird.playeranimcore.animation; + +import com.google.gson.*; +import com.zigythebird.playeranimcore.math.Vec3f; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; +import java.util.Base64; + +public record CustomModelBone(Vec3f pivot, byte @Nullable [] texture, @Nullable JsonArray elements) { + public static class Deserializer implements JsonDeserializer { + @Override + public CustomModelBone deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx) throws JsonParseException { + JsonObject obj = json.getAsJsonObject(); + return new CustomModelBone( + ctx.deserialize(obj.get("pivot"), Vec3f.class), + obj.has("texture") ? Base64.getDecoder().decode(obj.get("texture").getAsString()) : null, + obj.getAsJsonArray("elements") + ); + } + } +} diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/HumanoidAnimationController.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/HumanoidAnimationController.java index 52d2b92a..5771a548 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/animation/HumanoidAnimationController.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/HumanoidAnimationController.java @@ -24,9 +24,12 @@ package com.zigythebird.playeranimcore.animation; +import com.google.gson.JsonArray; +import com.zigythebird.playeranimcore.bindings.PlatformModel; import com.zigythebird.playeranimcore.bones.PlayerAnimBone; import com.zigythebird.playeranimcore.math.Vec3f; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import team.unnamed.mocha.MochaEngine; import java.util.ArrayList; @@ -129,4 +132,9 @@ public PlayerAnimBone get3DTransformRaw(@NotNull PlayerAnimBone bone) { } return bone; } + + @Override + protected @Nullable PlatformModel loadCustomModel(byte @Nullable [] texture, @Nullable JsonArray elements) { + return null; + } } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationContainer.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationContainer.java index 5433348a..d5a235c8 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationContainer.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationContainer.java @@ -27,10 +27,13 @@ import com.zigythebird.playeranimcore.animation.AnimationData; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonConfiguration; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonMode; +import com.zigythebird.playeranimcore.bones.CustomBone; import com.zigythebird.playeranimcore.bones.PlayerAnimBone; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.function.Consumer; + /** * A container to make swapping animation object easier * It will clone the behaviour of the held animation @@ -94,4 +97,9 @@ public String toString() { "anim=" + anim + '}'; } + + @Override + public void collectModels(Consumer consumer) { + if (this.anim != null) this.anim.collectModels(consumer); + } } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationStack.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationStack.java index 82b5793f..3045a3e7 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationStack.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/AnimationStack.java @@ -3,12 +3,14 @@ import com.zigythebird.playeranimcore.animation.AnimationData; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonConfiguration; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonMode; +import com.zigythebird.playeranimcore.bones.CustomBone; import com.zigythebird.playeranimcore.bones.PlayerAnimBone; import it.unimi.dsi.fastutil.Pair; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * Player animation stack, can contain multiple active or passive layers, will always be evaluated from the lowest index. @@ -131,4 +133,13 @@ public String toString() { "layers=" + layers + '}'; } + + @Override + public void collectModels(Consumer consumer) { + for (Pair layer : this.layers) { + if (layer.right().isActive()) { + layer.right().collectModels(consumer); + } + } + } } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/IAnimation.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/IAnimation.java index 87aaff0b..88272fff 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/IAnimation.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/IAnimation.java @@ -27,9 +27,12 @@ import com.zigythebird.playeranimcore.animation.AnimationData; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonConfiguration; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonMode; +import com.zigythebird.playeranimcore.bones.CustomBone; import com.zigythebird.playeranimcore.bones.PlayerAnimBone; import org.jetbrains.annotations.NotNull; +import java.util.function.Consumer; + public interface IAnimation { FirstPersonConfiguration DEFAULT_FIRST_PERSON_CONFIG = new FirstPersonConfiguration(); @@ -88,4 +91,6 @@ default FirstPersonConfiguration getFirstPersonConfiguration() { default boolean canRemove() { return false; } + + default void collectModels(Consumer consumer) {} } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/ModifierLayer.java b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/ModifierLayer.java index 407b9504..dc8f5814 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/ModifierLayer.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/animation/layered/ModifierLayer.java @@ -5,6 +5,7 @@ import com.zigythebird.playeranimcore.animation.layered.modifier.AbstractModifier; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonConfiguration; import com.zigythebird.playeranimcore.api.firstPerson.FirstPersonMode; +import com.zigythebird.playeranimcore.bones.CustomBone; import com.zigythebird.playeranimcore.bones.PlayerAnimBone; import com.zigythebird.playeranimcore.easing.EasingType; import org.jetbrains.annotations.NotNull; @@ -14,6 +15,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.function.Consumer; import java.util.function.Predicate; @@ -41,6 +43,16 @@ public ModifierLayer() { return animation; } + protected @Nullable IAnimation getTopAnimation() { + if (!this.modifiers.isEmpty()) { + return this.modifiers.getFirst(); + } else if (this.animation != null) { + return this.animation; + } else { + return null; + } + } + @Override public void tick(AnimationData state) { for (int i = 0; i < modifiers.size(); i++) { @@ -48,9 +60,8 @@ public void tick(AnimationData state) { removeModifier(i--); } } - if (modifiers.size() > 0) { - modifiers.get(0).tick(state); - } else if (animation != null) animation.tick(state); + IAnimation top = getTopAnimation(); + if (top != null) top.tick(state); } public void addModifier(@NotNull AbstractModifier modifier, int idx) { @@ -141,41 +152,34 @@ protected void linkModifiers() { @Override public boolean isActive() { - if (!modifiers.isEmpty()) { - return modifiers.get(0).isActive(); - } else if (animation != null) return animation.isActive(); + IAnimation top = getTopAnimation(); + if (top != null) return top.isActive(); return false; } @Override public void get3DTransform(@NotNull PlayerAnimBone bone) { - if (!modifiers.isEmpty()) { - modifiers.getFirst().get3DTransform(bone); - } else if (animation != null) { - animation.get3DTransform(bone); - } + IAnimation top = getTopAnimation(); + if (top != null) top.get3DTransform(bone); } @Override public void setupAnim(AnimationData state) { - if (!modifiers.isEmpty()) { - modifiers.get(0).setupAnim(state); - } else if (animation != null) animation.setupAnim(state); + IAnimation top = getTopAnimation(); + if (top != null) top.setupAnim(state); } @Override public @NotNull FirstPersonMode getFirstPersonMode() { - if (!modifiers.isEmpty()) { - return modifiers.get(0).getFirstPersonMode(); - } else if (animation != null) return animation.getFirstPersonMode(); + IAnimation top = getTopAnimation(); + if (top != null) return top.getFirstPersonMode(); return IAnimation.super.getFirstPersonMode(); } @Override public @NotNull FirstPersonConfiguration getFirstPersonConfiguration() { - if (!modifiers.isEmpty()) { - return modifiers.get(0).getFirstPersonConfiguration(); - } else if (animation != null) return animation.getFirstPersonConfiguration(); + IAnimation top = getTopAnimation(); + if (top != null) return top.getFirstPersonConfiguration(); return IAnimation.super.getFirstPersonConfiguration(); } @@ -186,4 +190,10 @@ public String toString() { ", animation=" + animation + '}'; } + + @Override + public void collectModels(Consumer consumer) { + IAnimation top = getTopAnimation(); + if (top != null) top.collectModels(consumer); + } } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/bindings/PlatformModel.java b/core/src/main/java/com/zigythebird/playeranimcore/bindings/PlatformModel.java new file mode 100644 index 00000000..85edf661 --- /dev/null +++ b/core/src/main/java/com/zigythebird/playeranimcore/bindings/PlatformModel.java @@ -0,0 +1,5 @@ +package com.zigythebird.playeranimcore.bindings; + +public interface PlatformModel { + void invalidate(); +} diff --git a/core/src/main/java/com/zigythebird/playeranimcore/bones/CustomBone.java b/core/src/main/java/com/zigythebird/playeranimcore/bones/CustomBone.java new file mode 100644 index 00000000..aa892a09 --- /dev/null +++ b/core/src/main/java/com/zigythebird/playeranimcore/bones/CustomBone.java @@ -0,0 +1,26 @@ +package com.zigythebird.playeranimcore.bones; + +import com.zigythebird.playeranimcore.bindings.PlatformModel; +import com.zigythebird.playeranimcore.math.Vec3f; +import org.jetbrains.annotations.Nullable; + +public class CustomBone extends PlayerAnimBone { + private final Vec3f pivot; + + @Nullable + private final PlatformModel model; + + public CustomBone(String name, Vec3f pivot, @Nullable PlatformModel model) { + super(name); + this.pivot = pivot; + this.model = model; + } + + public Vec3f getPivot() { + return this.pivot; + } + + public @Nullable PlatformModel getModel() { + return this.model; + } +} diff --git a/core/src/main/java/com/zigythebird/playeranimcore/bones/PivotBone.java b/core/src/main/java/com/zigythebird/playeranimcore/bones/PivotBone.java deleted file mode 100644 index 567b6949..00000000 --- a/core/src/main/java/com/zigythebird/playeranimcore/bones/PivotBone.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.zigythebird.playeranimcore.bones; - -import com.zigythebird.playeranimcore.math.Vec3f; - -public class PivotBone extends PlayerAnimBone { - private final Vec3f pivot; - - public PivotBone(String name, Vec3f pivot) { - super(name); - this.pivot = pivot; - } - - public Vec3f getPivot() { - return this.pivot; - } -} 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 f1a3dcaf..67285827 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/bones/PlayerAnimBone.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/bones/PlayerAnimBone.java @@ -8,8 +8,6 @@ import com.zigythebird.playeranimcore.easing.EasingType; import com.zigythebird.playeranimcore.enums.Axis; import com.zigythebird.playeranimcore.enums.TransformType; -import com.zigythebird.playeranimcore.math.MathHelper; -import com.zigythebird.playeranimcore.math.Vec3f; import org.jetbrains.annotations.ApiStatus; import org.joml.Vector3f; 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 9aaa8fbd..09480da1 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/loading/AnimationLoader.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/loading/AnimationLoader.java @@ -27,6 +27,7 @@ import com.google.gson.*; import com.zigythebird.playeranimcore.PlayerAnimLib; import com.zigythebird.playeranimcore.animation.Animation; +import com.zigythebird.playeranimcore.animation.CustomModelBone; import com.zigythebird.playeranimcore.animation.ExtraAnimationData; import com.zigythebird.playeranimcore.animation.keyframe.BoneAnimation; import com.zigythebird.playeranimcore.animation.keyframe.Keyframe; @@ -34,7 +35,6 @@ import com.zigythebird.playeranimcore.easing.EasingType; import com.zigythebird.playeranimcore.enums.Axis; import com.zigythebird.playeranimcore.enums.TransformType; -import com.zigythebird.playeranimcore.math.Vec3f; import com.zigythebird.playeranimcore.molang.MolangLoader; import com.zigythebird.playeranimcore.util.JsonUtil; import it.unimi.dsi.fastutil.floats.FloatObjectPair; @@ -63,7 +63,7 @@ public Animation deserialize(JsonElement json, Type typeOfT, JsonDeserialization Animation.Keyframes keyframes = context.deserialize(animationObj, Animation.Keyframes.class); Map parents = UniversalAnimLoader.getParents(JsonUtil.getAsJsonObject(animationObj, "parents", new JsonObject())); - Map bones = UniversalAnimLoader.getModel(JsonUtil.getAsJsonObject(animationObj, "model", new JsonObject())); + Map bones = UniversalAnimLoader.getModel(JsonUtil.getAsJsonObject(animationObj, "model", new JsonObject()), context); // Extra data ExtraAnimationData extraData = new ExtraAnimationData(); diff --git a/core/src/main/java/com/zigythebird/playeranimcore/loading/UniversalAnimLoader.java b/core/src/main/java/com/zigythebird/playeranimcore/loading/UniversalAnimLoader.java index 7f5b8c8c..64d7f01a 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/loading/UniversalAnimLoader.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/loading/UniversalAnimLoader.java @@ -3,11 +3,11 @@ import com.google.gson.*; import com.zigythebird.playeranimcore.PlayerAnimLib; import com.zigythebird.playeranimcore.animation.Animation; +import com.zigythebird.playeranimcore.animation.CustomModelBone; import com.zigythebird.playeranimcore.animation.ExtraAnimationData; import com.zigythebird.playeranimcore.animation.keyframe.event.data.CustomInstructionKeyframeData; import com.zigythebird.playeranimcore.animation.keyframe.event.data.ParticleKeyframeData; import com.zigythebird.playeranimcore.animation.keyframe.event.data.SoundKeyframeData; -import com.zigythebird.playeranimcore.math.Vec3f; import com.zigythebird.playeranimcore.util.JsonUtil; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.jetbrains.annotations.NotNull; @@ -39,13 +39,17 @@ public static Map loadAnimations(InputStream resource) throws public static Map<@NotNull String, Animation> loadAnimations(JsonObject json) { if (json.has("animations")) { Map animationMap = PlayerAnimLib.GSON.fromJson(json.get("animations"), PlayerAnimLib.ANIMATIONS_MAP_TYPE); - if (json.has("parents") && json.has("model")) { - Map parents = UniversalAnimLoader.getParents(JsonUtil.getAsJsonObject(json, "parents", new JsonObject())); - Map bones = UniversalAnimLoader.getModel(JsonUtil.getAsJsonObject(json, "model", new JsonObject())); + if (json.has("model")) { + Map bones = UniversalAnimLoader.getModel(JsonUtil.getAsJsonObject(json, "model", new JsonObject()), PlayerAnimLib.GSON::fromJson); for (Animation animation : animationMap.values()) { if (animation.bones().isEmpty()) { animation.bones().putAll(bones); } + } + } + if (json.has("parents")) { + Map parents = UniversalAnimLoader.getParents(JsonUtil.getAsJsonObject(json, "parents", new JsonObject())); + for (Animation animation : animationMap.values()) { if (animation.parents().isEmpty()) { animation.parents().putAll(parents); } @@ -86,13 +90,10 @@ public static Map getParents(JsonObject parentsObj) { return parents; } - public static Map getModel(JsonObject modelObj) { - Map bones = new HashMap<>(modelObj.size()); + public static Map getModel(JsonObject modelObj, JsonDeserializationContext ctx) { + Map bones = new HashMap<>(modelObj.size()); for (Map.Entry entry : modelObj.entrySet()) { - JsonObject object = entry.getValue().getAsJsonObject(); - JsonArray pivot = object.get("pivot").getAsJsonArray(); - Vec3f bone = new Vec3f(pivot.get(0).getAsFloat(), pivot.get(1).getAsFloat(), pivot.get(2).getAsFloat()); - bones.put(entry.getKey(), bone); + bones.put(entry.getKey(), ctx.deserialize(entry.getValue(), CustomModelBone.class)); } return bones; } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/math/Vec3f.java b/core/src/main/java/com/zigythebird/playeranimcore/math/Vec3f.java index 6775e6ae..26ba2a44 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/math/Vec3f.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/math/Vec3f.java @@ -1,5 +1,9 @@ package com.zigythebird.playeranimcore.math; +import com.google.gson.*; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; import java.util.Objects; public record Vec3f(float x, float y, float z) { @@ -16,6 +20,10 @@ public Vec3f mul(float scalar) { return new Vec3f(this.x * scalar, this.y * scalar, this.z * scalar); } + public Vec3f div(float scalar) { + return new Vec3f(this.x / scalar, this.y / scalar, this.z / scalar); + } + /** * Add two vectors * @@ -29,12 +37,12 @@ public Vec3f add(Vec3f other) { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof Vec3f vec)) return false; - return Objects.equals(x, vec.x) && Objects.equals(y, vec.y) && Objects.equals(z, vec.z); + if (!(o instanceof Vec3f(float x1, float y1, float z1))) return false; + return Objects.equals(x, x1) && Objects.equals(y, y1) && Objects.equals(z, z1); } @Override - public String toString() { + public @NotNull String toString() { return "Vec3f[" + this.x + "; " + this.y + "; " + this.z + "]"; } @@ -42,4 +50,12 @@ public String toString() { public int hashCode() { return Objects.hash(x, y, z); } + + public static class Deserializer implements JsonDeserializer { + @Override + public Vec3f deserialize(JsonElement json, Type type, JsonDeserializationContext ctx) throws JsonParseException { + JsonArray array = json.getAsJsonArray(); + return new Vec3f(array.get(0).getAsFloat(), array.get(1).getAsFloat(), array.get(2).getAsFloat()); + } + } } diff --git a/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinary.java b/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinary.java index 45544a35..631724f6 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinary.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinary.java @@ -1,6 +1,7 @@ package com.zigythebird.playeranimcore.network; import com.zigythebird.playeranimcore.animation.Animation; +import com.zigythebird.playeranimcore.animation.CustomModelBone; import com.zigythebird.playeranimcore.animation.ExtraAnimationData; import com.zigythebird.playeranimcore.animation.keyframe.BoneAnimation; import com.zigythebird.playeranimcore.animation.keyframe.Keyframe; @@ -12,7 +13,6 @@ import com.zigythebird.playeranimcore.enums.AnimationFormat; import com.zigythebird.playeranimcore.enums.TransformType; import com.zigythebird.playeranimcore.loading.PlayerAnimatorLoader; -import com.zigythebird.playeranimcore.math.Vec3f; import io.netty.buffer.ByteBuf; import team.unnamed.mocha.parser.ast.Expression; import team.unnamed.mocha.parser.ast.FloatExpression; @@ -81,7 +81,7 @@ public static void write(ByteBuf buf, int version, Animation animation) { } writeEventKeyframes(buf, animation.keyFrames()); - NetworkUtils.writeMap(buf, animation.bones(), ProtocolUtils::writeString, NetworkUtils::writeVec3f); + NetworkUtils.writeMap(buf, animation.bones(), ProtocolUtils::writeString, (byteBuf, bone) -> NetworkUtils.writeCustomBone(byteBuf, bone, version)); NetworkUtils.writeMap(buf, animation.parents(), ProtocolUtils::writeString, ProtocolUtils::writeString); } @@ -181,7 +181,7 @@ public static Animation read(ByteBuf buf, int version) { } Animation.Keyframes keyFrames = readEventKeyframes(buf); - Map pivotBones = NetworkUtils.readMap(buf, ProtocolUtils::readString, NetworkUtils::readVec3f); + Map pivotBones = NetworkUtils.readMap(buf, ProtocolUtils::readString, byteBuf -> NetworkUtils.readCustomBone(byteBuf, version)); Map parents = NetworkUtils.readMap(buf, ProtocolUtils::readString, ProtocolUtils::readString); return new Animation(data, length, loopType, boneAnimations, keyFrames, pivotBones, parents); diff --git a/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinaryV6.java b/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinaryV6.java index e7159fc6..4de15612 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinaryV6.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/network/AnimationBinaryV6.java @@ -1,6 +1,7 @@ package com.zigythebird.playeranimcore.network; import com.zigythebird.playeranimcore.animation.Animation; +import com.zigythebird.playeranimcore.animation.CustomModelBone; import com.zigythebird.playeranimcore.animation.ExtraAnimationData; import com.zigythebird.playeranimcore.animation.keyframe.BoneAnimation; import com.zigythebird.playeranimcore.animation.keyframe.Keyframe; @@ -8,7 +9,6 @@ import com.zigythebird.playeranimcore.easing.EasingType; import com.zigythebird.playeranimcore.enums.AnimationFormat; import com.zigythebird.playeranimcore.loading.PlayerAnimatorLoader; -import com.zigythebird.playeranimcore.math.Vec3f; import io.netty.buffer.ByteBuf; import team.unnamed.mocha.parser.ast.Expression; import team.unnamed.mocha.parser.ast.FloatExpression; @@ -37,7 +37,7 @@ static void write(ByteBuf buf, int version, Animation animation) { } AnimationBinary.writeEventKeyframes(buf, animation.keyFrames()); - NetworkUtils.writeMap(buf, animation.bones(), ProtocolUtils::writeString, NetworkUtils::writeVec3f); + NetworkUtils.writeMap(buf, animation.bones(), ProtocolUtils::writeString, (byteBuf, bone) -> NetworkUtils.writeCustomBone(byteBuf, bone, version)); NetworkUtils.writeMap(buf, animation.parents(), ProtocolUtils::writeString, ProtocolUtils::writeString); } @@ -76,7 +76,7 @@ static Animation read(ByteBuf buf, int version) { buf1 -> readBoneAnimation(buf1, isPlayerAnimator, version)); Animation.Keyframes keyFrames = AnimationBinary.readEventKeyframes(buf); - Map pivotBones = NetworkUtils.readMap(buf, ProtocolUtils::readString, NetworkUtils::readVec3f); + Map pivotBones = NetworkUtils.readMap(buf, ProtocolUtils::readString, byteBuf -> NetworkUtils.readCustomBone(byteBuf, version)); Map parents = NetworkUtils.readMap(buf, ProtocolUtils::readString, ProtocolUtils::readString); return new Animation(data, length, loopType, boneAnimations, keyFrames, pivotBones, parents); diff --git a/core/src/main/java/com/zigythebird/playeranimcore/network/CustomModelUtils.java b/core/src/main/java/com/zigythebird/playeranimcore/network/CustomModelUtils.java new file mode 100644 index 00000000..4d738cfb --- /dev/null +++ b/core/src/main/java/com/zigythebird/playeranimcore/network/CustomModelUtils.java @@ -0,0 +1,232 @@ +package com.zigythebird.playeranimcore.network; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.netty.buffer.ByteBuf; +import team.unnamed.mocha.util.network.ProtocolUtils; +import team.unnamed.mocha.util.network.VarIntUtils; + +class CustomModelUtils { + private static final String[] DIRECTION_NAMES = {"down", "up", "north", "south", "west", "east"}; + private static final String[] AXIS_NAMES = {"x", "y", "z"}; + + /** + * ── BlockElement ── + * elementFlags (varint): + * bit 0: hasFrom (almost always set → low bit) + * bit 1: hasTo (almost always set → low bit) + * bit 2: hasRotation + * bit 3: noShade (bit set = shade is false, default true → 0) + * bits 4-7: lightEmission (0-15) + */ + static void writeBlockElement(ByteBuf buf, JsonObject element, int version) { + boolean hasFrom = element.has("from"); + boolean hasTo = element.has("to"); + boolean hasRotation = element.has("rotation"); + boolean noShade = element.has("shade") && !element.get("shade").getAsBoolean(); + int lightEmission = element.has("light_emission") ? element.get("light_emission").getAsInt() : 0; + + int flags = 0; + if (hasFrom) flags |= 1; + if (hasTo) flags |= 2; + if (hasRotation) flags |= 4; + if (noShade) flags |= 8; + flags |= (lightEmission & 0xF) << 4; + VarIntUtils.writeVarInt(buf, flags); + + if (hasFrom) writeJsonVec3f(buf, element.getAsJsonArray("from")); + if (hasTo) writeJsonVec3f(buf, element.getAsJsonArray("to")); + + JsonObject faces = element.has("faces") ? element.getAsJsonObject("faces") : null; + int facesBitmask = 0; + if (faces != null) { + for (int d = 0; d < 6; d++) { + if (faces.has(DIRECTION_NAMES[d])) facesBitmask |= (1 << d); + } + } + VarIntUtils.writeVarInt(buf, facesBitmask); + + for (int d = 0; d < 6; d++) { + if ((facesBitmask & (1 << d)) == 0) continue; + writeBlockElementFace(buf, faces.getAsJsonObject(DIRECTION_NAMES[d]), version); + } + + if (hasRotation) { + writeBlockElementRotation(buf, element.getAsJsonObject("rotation"), version); + } + } + + static JsonObject readBlockElement(ByteBuf buf, int version) { + JsonObject element = new JsonObject(); + + int flags = VarIntUtils.readVarInt(buf); + boolean hasFrom = (flags & 1) != 0; + boolean hasTo = (flags & 2) != 0; + boolean hasRotation = (flags & 4) != 0; + boolean noShade = (flags & 8) != 0; + int lightEmission = (flags >> 4) & 0xF; + + if (hasFrom) element.add("from", readJsonVec3f(buf)); + if (hasTo) element.add("to", readJsonVec3f(buf)); + if (noShade) element.addProperty("shade", false); + if (lightEmission != 0) element.addProperty("light_emission", lightEmission); + + int facesBitmask = VarIntUtils.readVarInt(buf); + if (facesBitmask != 0) { + JsonObject faces = new JsonObject(); + for (int d = 0; d < 6; d++) { + if ((facesBitmask & (1 << d)) == 0) continue; + faces.add(DIRECTION_NAMES[d], readBlockElementFace(buf, version)); + } + element.add("faces", faces); + } + + if (hasRotation) { + element.add("rotation", readBlockElementRotation(buf, version)); + } + + return element; + } + + /** + * ── BlockElementFace ── + * faceFlags (varint): + * bit 0: hasTexture + * bit 1: hasUVs + * bit 2: hasTintIndex + * bits 3-5: cullForDirection (0-5=dir, 7=none) + * bit 6: hasRotation + */ + private static void writeBlockElementFace(ByteBuf buf, JsonObject face, int version) { + boolean hasTexture = face.has("texture"); + boolean hasUVs = face.has("uv"); + int tintIndex = face.has("tintindex") ? face.get("tintindex").getAsInt() : -1; + boolean hasTint = tintIndex != -1; + + int cullDir = 7; + if (face.has("cullface")) { + String cullName = face.get("cullface").getAsString(); + for (int i = 0; i < 6; i++) { + if (DIRECTION_NAMES[i].equals(cullName)) { cullDir = i; break; } + } + } + boolean hasRotation = face.has("rotation"); + + int faceFlags = (hasTexture ? 1 : 0) | (hasUVs ? (1 << 1) : 0) | (hasTint ? (1 << 2) : 0) + | ((cullDir & 0x7) << 3) | (hasRotation ? (1 << 6) : 0); + VarIntUtils.writeVarInt(buf, faceFlags); + + if (hasTexture) ProtocolUtils.writeString(buf, face.get("texture").getAsString()); + if (hasTint) VarIntUtils.writeVarInt(buf, tintIndex); + + if (hasUVs) { + JsonArray uv = face.getAsJsonArray("uv"); + buf.writeFloat(uv.get(0).getAsFloat()); + buf.writeFloat(uv.get(1).getAsFloat()); + buf.writeFloat(uv.get(2).getAsFloat()); + buf.writeFloat(uv.get(3).getAsFloat()); + } + + if (hasRotation) VarIntUtils.writeVarInt(buf, face.get("rotation").getAsInt()); + } + + private static JsonObject readBlockElementFace(ByteBuf buf, int version) { + int faceFlags = VarIntUtils.readVarInt(buf); + boolean hasTexture = (faceFlags & 1) != 0; + boolean hasUVs = (faceFlags & (1 << 1)) != 0; + boolean hasTint = (faceFlags & (1 << 2)) != 0; + int cullDir = (faceFlags >> 3) & 0x7; + boolean hasRotation = (faceFlags & (1 << 6)) != 0; + + JsonObject face = new JsonObject(); + + if (hasTexture) face.addProperty("texture", ProtocolUtils.readString(buf)); + if (cullDir < 6) face.addProperty("cullface", DIRECTION_NAMES[cullDir]); + if (hasTint) face.addProperty("tintindex", VarIntUtils.readVarInt(buf)); + + if (hasUVs) { + JsonArray uv = new JsonArray(4); + uv.add(buf.readFloat()); + uv.add(buf.readFloat()); + uv.add(buf.readFloat()); + uv.add(buf.readFloat()); + face.add("uv", uv); + } + + if (hasRotation) face.addProperty("rotation", VarIntUtils.readVarInt(buf)); + + return face; + } + + /** + * ── BlockElementRotation ── + * rotFlags (varint): bit0=isEuler, bit1=rescale + */ + private static void writeBlockElementRotation(ByteBuf buf, JsonObject rotation, int version) { + writeJsonVec3f(buf, rotation.getAsJsonArray("origin")); + + // MC logic: euler when axis/angle are absent + boolean isEuler = !rotation.has("axis") && !rotation.has("angle"); + boolean rescale = rotation.has("rescale") && rotation.get("rescale").getAsBoolean(); + + int rotFlags = 0; + if (isEuler) rotFlags |= 1; + if (rescale) rotFlags |= 2; + VarIntUtils.writeVarInt(buf, rotFlags); + + if (isEuler) { + buf.writeFloat(rotation.has("x") ? rotation.get("x").getAsFloat() : 0f); + buf.writeFloat(rotation.has("y") ? rotation.get("y").getAsFloat() : 0f); + buf.writeFloat(rotation.has("z") ? rotation.get("z").getAsFloat() : 0f); + } else { + String axisName = rotation.get("axis").getAsString(); + int axis = 0; + for (int i = 0; i < 3; i++) { + if (AXIS_NAMES[i].equals(axisName)) { axis = i; break; } + } + VarIntUtils.writeVarInt(buf, axis); + buf.writeFloat(rotation.get("angle").getAsFloat()); + } + } + + private static JsonObject readBlockElementRotation(ByteBuf buf, int version) { + JsonObject rotation = new JsonObject(); + rotation.add("origin", readJsonVec3f(buf)); + + int rotFlags = VarIntUtils.readVarInt(buf); + boolean isEuler = (rotFlags & 1) != 0; + boolean rescale = (rotFlags & 2) != 0; + + if (isEuler) { + float x = buf.readFloat(); + float y = buf.readFloat(); + float z = buf.readFloat(); + rotation.addProperty("x", x); + rotation.addProperty("y", y); + rotation.addProperty("z", z); + } else { + int axis = VarIntUtils.readVarInt(buf); + float angle = buf.readFloat(); + rotation.addProperty("axis", AXIS_NAMES[axis]); + rotation.addProperty("angle", angle); + } + + if (rescale) rotation.addProperty("rescale", true); + + return rotation; + } + + private static void writeJsonVec3f(ByteBuf buf, JsonArray arr) { + buf.writeFloat(arr.get(0).getAsFloat()); + buf.writeFloat(arr.get(1).getAsFloat()); + buf.writeFloat(arr.get(2).getAsFloat()); + } + + private static JsonArray readJsonVec3f(ByteBuf buf) { + JsonArray arr = new JsonArray(3); + arr.add(buf.readFloat()); + arr.add(buf.readFloat()); + arr.add(buf.readFloat()); + return arr; + } +} diff --git a/core/src/main/java/com/zigythebird/playeranimcore/network/NetworkUtils.java b/core/src/main/java/com/zigythebird/playeranimcore/network/NetworkUtils.java index 33f8e970..6bb1d0a7 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/network/NetworkUtils.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/network/NetworkUtils.java @@ -1,5 +1,8 @@ package com.zigythebird.playeranimcore.network; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.zigythebird.playeranimcore.animation.CustomModelBone; import com.zigythebird.playeranimcore.math.Vec3f; import io.netty.buffer.ByteBuf; import team.unnamed.mocha.util.network.VarIntUtils; @@ -30,6 +33,59 @@ public static void writeMap(ByteBuf buf, Map map, BiConsumer= 6) { + int boneFlags = VarIntUtils.readVarInt(buf); + + if ((boneFlags & 1) != 0) { + int len = VarIntUtils.readVarInt(buf); + texture = new byte[len]; + buf.readBytes(texture); + } + + if ((boneFlags & 2) != 0) { + int count = VarIntUtils.readVarInt(buf); + elements = new JsonArray(count); + for (int i = 0; i < count; i++) { + elements.add(CustomModelUtils.readBlockElement(buf, version)); + } + } + } + + return new CustomModelBone(pivot, texture, elements); + } + + public static void writeCustomBone(ByteBuf buf, CustomModelBone bone, int version) { + writeVec3f(buf, bone.pivot()); + + if (version >= 6) { + int boneFlags = 0; + if (bone.texture() != null) boneFlags |= 1; + if (bone.elements() != null) boneFlags |= 2; + VarIntUtils.writeVarInt(buf, boneFlags); + + if (bone.texture() != null) { + VarIntUtils.writeVarInt(buf, bone.texture().length); + buf.writeBytes(bone.texture()); + } + + if (bone.elements() != null) { + VarIntUtils.writeVarInt(buf, bone.elements().size()); + for (JsonElement el : bone.elements()) { + CustomModelUtils.writeBlockElement(buf, el.getAsJsonObject(), version); + } + } + } + } + public static Vec3f readVec3f(ByteBuf buf) { float x = buf.readFloat(); float y = buf.readFloat(); diff --git a/core/src/main/java/com/zigythebird/playeranimcore/util/MatrixUtil.java b/core/src/main/java/com/zigythebird/playeranimcore/util/MatrixUtil.java index fd9bff48..e17ab1db 100644 --- a/core/src/main/java/com/zigythebird/playeranimcore/util/MatrixUtil.java +++ b/core/src/main/java/com/zigythebird/playeranimcore/util/MatrixUtil.java @@ -1,6 +1,6 @@ package com.zigythebird.playeranimcore.util; -import com.zigythebird.playeranimcore.bones.PivotBone; +import com.zigythebird.playeranimcore.bones.CustomBone; import com.zigythebird.playeranimcore.bones.PlayerAnimBone; import com.zigythebird.playeranimcore.math.Vec3f; import org.joml.Matrix4f; @@ -46,7 +46,7 @@ public static void applyParentsToChild(PlayerAnimBone child, Iterable animations = UniversalAnimLoader.loadAnimations(is); + + Assertions.assertFalse(animations.isEmpty(), "no animations loaded"); + + Animation animation = animations.values().iterator().next(); + Assertions.assertFalse(animation.bones().isEmpty(), "no bones in animation"); + + for (Map.Entry entry : animation.bones().entrySet()) { + CustomModelBone bone = entry.getValue(); + + Assertions.assertNotNull(bone.pivot(), "pivot is null for bone " + entry.getKey()); + Assertions.assertNotNull(bone.texture(), "texture is null for bone " + entry.getKey()); + Assertions.assertNotNull(bone.elements(), "elements is null for bone " + entry.getKey()); + Assertions.assertFalse(bone.elements().isEmpty(), "elements empty for bone " + entry.getKey()); + + assertRoundTrip(bone, 6); + } + } + } + + @Test + @DisplayName("Buffer fully consumed after read") + void bufferFullyConsumed() { + JsonObject element = new JsonObject(); + element.add("from", vec3(1, 2, 3)); + element.add("to", vec3(4, 5, 6)); + element.addProperty("shade", false); + element.addProperty("light_emission", 7); + + JsonObject rot = new JsonObject(); + rot.add("origin", vec3(0, 0, 0)); + rot.addProperty("x", 10f); + rot.addProperty("y", 20f); + rot.addProperty("z", 30f); + rot.addProperty("rescale", true); + element.add("rotation", rot); + + JsonObject faces = new JsonObject(); + faces.add("down", makeFace("#bottom", "down", new float[]{0, 0, 16, 16}, 2, 90)); + faces.add("up", makeFace("#top", "up", new float[]{0, 0, 16, 16}, 0, 180)); + faces.add("north", makeFace("#side", "north", new float[]{0, 0, 16, 16}, -1, 0)); + element.add("faces", faces); + + JsonArray elements = new JsonArray(); + elements.add(element); + + CustomModelBone bone = new CustomModelBone(new Vec3f(4, 5, 6), new byte[]{10, 20, 30}, elements); + assertRoundTrip(bone, 6); + } +} diff --git a/core/src/test/resources/cmm_bone_test.json b/core/src/test/resources/cmm_bone_test.json new file mode 100644 index 00000000..621dd819 --- /dev/null +++ b/core/src/test/resources/cmm_bone_test.json @@ -0,0 +1,25 @@ +{ + "format_version": "1.8.0", + "model": { + "cmm": { + "pivot": [ 0, 12, 0 ], + "texture": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAfUlEQVR4Xo1QiRHAIAhzUAZgC7foAg5KjSkq1OsVr1qS8Jby18TE1PivBi/R1a4JwmtRIp1uHUYOHeIlnxEOQoyz5ZBB40MOxNNPAsL6vKEIBIxBHx5/ECzqIHBwFQg9+BT7XeOguUTYw56DL5p9LZvrxpggE00DzJOZD7sBRaHjpRD91mQAAAAASUVORK5CYII=", + "elements": [ + { + "from": [ 0, 0.25, 0 ], + "to": [ 16, 0.25, 16 ], + "faces": { + "down": { "uv": [ 0, 16, 16, 0 ], "texture": "#texture", "tintindex": 0 }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#texture", "tintindex": 0 } + } + } + ] + } + }, + "animations": { + "cmm_bone_test": { + "loop": false, + "bones": {} + } + } +} diff --git a/minecraft/src/main/java/com/zigythebird/playeranim/animation/MinecraftModel.java b/minecraft/src/main/java/com/zigythebird/playeranim/animation/MinecraftModel.java new file mode 100644 index 00000000..c9ebf1c0 --- /dev/null +++ b/minecraft/src/main/java/com/zigythebird/playeranim/animation/MinecraftModel.java @@ -0,0 +1,123 @@ +package com.zigythebird.playeranim.animation; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; +import com.zigythebird.playeranim.PlayerAnimLibMod; +import com.zigythebird.playeranimcore.bindings.PlatformModel; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.block.model.*; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.SpriteContents; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.animation.FrameSize; +import net.minecraft.client.resources.model.*; +import net.minecraft.resources.Identifier; +import org.joml.Vector3fc; +import org.jspecify.annotations.NonNull; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class MinecraftModel implements PlatformModel, MaterialBaker, ModelBaker, ModelBaker.Interner, ModelDebugName { + private static final AtomicInteger TEXTURE_COUNTER = new AtomicInteger(0); + + private final TextureAtlasSprite sprite; + private final RenderType renderType; + private final QuadCollection geometry; + + public MinecraftModel(byte @NonNull [] texture, @NonNull JsonArray elements) throws IOException { + this.sprite = loadAtlasSprite(texture); + this.renderType = RenderTypes.entityTranslucent(this.sprite.atlasLocation()); + + List blockElements = new ArrayList<>(); + for (JsonElement element : elements) { + blockElements.add(BlockModel.GSON.fromJson(element, BlockElement.class)); + } + this.geometry = SimpleUnbakedGeometry.bake(blockElements, TextureSlots.EMPTY, this, BlockModelRotation.IDENTITY, this); + } + + @NonNull + public QuadCollection getGeometry() { + return this.geometry; + } + + @NonNull + public RenderType getRenderType() { + return this.renderType; + } + + @Override + public void invalidate() { + if (this.sprite != null) this.sprite.close(); + } + + @Override + public @NonNull String debugName() { + return "playeranim:custom_bone"; + } + + @Override + public Material.@NonNull Baked get(Material material, @NonNull ModelDebugName name) { + return new Material.Baked(this.sprite, material.forceTranslucent()); + } + + @Override + public Material.@NonNull Baked reportMissingReference(@NonNull String reference, @NonNull ModelDebugName name) { + return new Material.Baked(this.sprite, false); + } + + @Override + public @NonNull ResolvedModel getModel(@NonNull Identifier location) { + throw new UnsupportedOperationException(); + } + + @Override + public @NonNull BlockModelPart missingBlockModelPart() { + throw new UnsupportedOperationException(); + } + + @Override + public @NonNull MaterialBaker materials() { + return this; + } + + @Override + public @NonNull Interner interner() { + return this; + } + + @Override + public @NonNull Vector3fc vector(@NonNull Vector3fc vector) { + return vector; + } + + @Override + public BakedQuad.@NonNull SpriteInfo spriteInfo(BakedQuad.@NonNull SpriteInfo sprite) { + return sprite; + } + + @Override + public T compute(SharedOperationKey key) { + return key.compute(this); + } + + private static TextureAtlasSprite loadAtlasSprite(byte[] texture) throws IOException { + RenderSystem.assertOnRenderThread(); + NativeImage image = NativeImage.read(texture); + + Identifier texId = PlayerAnimLibMod.id("dynamic/texture_" + TEXTURE_COUNTER.getAndIncrement()); + DynamicTexture dynTex = new DynamicTexture(texId::toString, image); + Minecraft.getInstance().getTextureManager().register(texId, dynTex); + + int w = image.getWidth(); + int h = image.getHeight(); + SpriteContents contents = new SpriteContents(texId, new FrameSize(w, h), image); + return new TextureAtlasSprite(texId, contents, w, h, 0, 0, 0); + } +} diff --git a/minecraft/src/main/java/com/zigythebird/playeranim/animation/PlayerAnimationController.java b/minecraft/src/main/java/com/zigythebird/playeranim/animation/PlayerAnimationController.java index b87abf00..3f49847b 100644 --- a/minecraft/src/main/java/com/zigythebird/playeranim/animation/PlayerAnimationController.java +++ b/minecraft/src/main/java/com/zigythebird/playeranim/animation/PlayerAnimationController.java @@ -1,12 +1,16 @@ package com.zigythebird.playeranim.animation; +import com.google.gson.JsonArray; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Axis; import com.zigythebird.playeranim.PlayerAnimLibMod; import com.zigythebird.playeranim.util.RenderUtil; +import com.zigythebird.playeranimcore.PlayerAnimLib; import com.zigythebird.playeranimcore.animation.AnimationController; import com.zigythebird.playeranimcore.animation.HumanoidAnimationController; import com.zigythebird.playeranimcore.animation.layered.modifier.AbstractFadeModifier; +import com.zigythebird.playeranimcore.bindings.PlatformModel; +import com.zigythebird.playeranimcore.bones.PlayerAnimBone; import com.zigythebird.playeranimcore.math.Vec3f; import com.zigythebird.playeranimcore.molang.MolangLoader; import net.minecraft.resources.Identifier; @@ -53,7 +57,7 @@ public boolean triggerAnimation(Identifier newAnimation, float startAnimFrom) { triggerAnimation(PlayerAnimResources.getAnimation(newAnimation), startAnimFrom); return true; } - PlayerAnimLibMod.LOGGER.error("Could not find animation with the name:" + newAnimation); + PlayerAnimLibMod.LOGGER.error("Could not find animation with the name:{}", newAnimation); return false; } @@ -78,12 +82,27 @@ public boolean replaceAnimationWithFade(@NotNull AbstractFadeModifier fadeModifi */ public @Nullable PoseStack getBoneWorldPositionPoseStack(String name, float tickDelta, Vec3 cameraPos) { if (!this.activeBones.containsKey(name)) return null; + return getBoneWorldPositionPoseStack(this.activeBones.get(name), tickDelta, cameraPos); + } + + public @NotNull PoseStack getBoneWorldPositionPoseStack(PlayerAnimBone bone, float tickDelta, Vec3 cameraPos) { PoseStack poseStack = new PoseStack(); - Vec3f pivot = getBonePosition(name); + Vec3f pivot = getBonePosition(bone.getName()); Vec3 position = avatar.getPosition(tickDelta).subtract(cameraPos).add(pivot.x(), pivot.y(), pivot.z()); poseStack.translate(position.x(), position.y(), position.z()); poseStack.mulPose(Axis.YP.rotationDegrees(180 - Mth.lerp(tickDelta, avatar.yBodyRotO, avatar.yBodyRot))); - RenderUtil.translateMatrixToBone(poseStack, this.activeBones.get(name)); + RenderUtil.translateMatrixToBone(poseStack, bone); return poseStack; } + + @Override + protected @Nullable PlatformModel loadCustomModel(byte @Nullable [] texture, @Nullable JsonArray elements) { + if (texture == null || elements == null || elements.isEmpty()) return null; + try { + return new MinecraftModel(texture, elements); + } catch (Exception e) { + PlayerAnimLib.LOGGER.error("Failed to load custom model!", e); + return null; + } + } } diff --git a/minecraft/src/main/java/com/zigythebird/playeranim/mixin/LivingEntityRendererMixin.java b/minecraft/src/main/java/com/zigythebird/playeranim/mixin/LivingEntityRendererMixin.java index 950f5cae..97cf31bb 100644 --- a/minecraft/src/main/java/com/zigythebird/playeranim/mixin/LivingEntityRendererMixin.java +++ b/minecraft/src/main/java/com/zigythebird/playeranim/mixin/LivingEntityRendererMixin.java @@ -25,20 +25,37 @@ package com.zigythebird.playeranim.mixin; import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.QuadInstance; import com.zigythebird.playeranim.accessors.IAvatarAnimationState; +import com.zigythebird.playeranim.animation.AvatarAnimManager; +import com.zigythebird.playeranim.animation.MinecraftModel; import com.zigythebird.playeranim.util.RenderUtil; +import com.zigythebird.playeranimcore.bindings.PlatformModel; import com.zigythebird.playeranimcore.bones.PlayerAnimBone; +import com.zigythebird.playeranimcore.util.MatrixUtil; +import net.minecraft.client.model.EntityModel; import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.renderer.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.resources.model.QuadCollection; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.LivingEntity; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LivingEntityRenderer.class) -public class LivingEntityRendererMixin { +public abstract class LivingEntityRendererMixin> extends EntityRenderer { + protected LivingEntityRendererMixin(EntityRendererProvider.Context context) { + super(context); + } + @Inject(method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/CameraRenderState;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/LivingEntityRenderer;scale(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;)V")) private void doTranslations(S livingEntityRenderState, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState, CallbackInfo ci) { if (livingEntityRenderState instanceof IAvatarAnimationState avatarRenderState) { @@ -62,4 +79,60 @@ private void doTranslations(S livingEntityRenderState, PoseStack poseStack, Subm } } } + + @Inject( + method = "submit(Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/CameraRenderState;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/SubmitNodeCollector;submitModel(Lnet/minecraft/client/model/Model;Ljava/lang/Object;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/rendertype/RenderType;IIILnet/minecraft/client/renderer/texture/TextureAtlasSprite;ILnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;)V", + shift = At.Shift.AFTER + ) + ) + public void pal$renderCustomModels(S state, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState camera, CallbackInfo ci) { + if (!(state instanceof IAvatarAnimationState avatarRenderState)) return; + + AvatarAnimManager animationPlayer = avatarRenderState.playerAnimLib$getAnimManager(); + if (animationPlayer == null || !animationPlayer.isActive()) return; + + int lightCoords = state.lightCoords; + + poseStack.pushPose(); + poseStack.scale(-1.0F, -1.0F, 1.0F); + poseStack.translate(-0.53F, -1.501F, -0.53F); + + animationPlayer.collectModels(bone -> { + PlatformModel platformModel = bone.getModel(); + if (!(platformModel instanceof MinecraftModel mcModel)) return; + + QuadCollection bakedPart = mcModel.getGeometry(); + + poseStack.pushPose(); + MatrixUtil.translateToPivotPoint(poseStack.last().pose(), bone.getPivot().div(16)); // idk + animationPlayer.get3DTransform(bone); + RenderUtil.translateMatrixToBone(poseStack, bone); + + submitNodeCollector.submitCustomGeometry( + poseStack, mcModel.getRenderType(), + + (pose, buffer) -> { + QuadInstance instance = new QuadInstance(); + instance.setLightCoords(lightCoords); + instance.setOverlayCoords(OverlayTexture.NO_OVERLAY); + + for (BakedQuad quad : bakedPart.getQuads(null)) { + buffer.putBakedQuad(pose, quad, instance); + } + for (Direction dir : Direction.values()) { + for (BakedQuad quad : bakedPart.getQuads(dir)) { + buffer.putBakedQuad(pose, quad, instance); + } + } + } + ); + + poseStack.popPose(); + }); + + poseStack.popPose(); + } } diff --git a/minecraft/src/main/resources/assets/player_animation_library/player_animations/cmm_test.json b/minecraft/src/main/resources/assets/player_animation_library/player_animations/cmm_test.json new file mode 100644 index 00000000..9abb53c1 --- /dev/null +++ b/minecraft/src/main/resources/assets/player_animation_library/player_animations/cmm_test.json @@ -0,0 +1,32 @@ +{ + "format_version": "1.8.0", + "model": { + "cmm": { + "pivot": [ 0, 12, 0 ], + + "texture": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAfUlEQVR4Xo1QiRHAIAhzUAZgC7foAg5KjSkq1OsVr1qS8Jby18TE1PivBi/R1a4JwmtRIp1uHUYOHeIlnxEOQoyz5ZBB40MOxNNPAsL6vKEIBIxBHx5/ECzqIHBwFQg9+BT7XeOguUTYw56DL5p9LZvrxpggE00DzJOZD7sBRaHjpRD91mQAAAAASUVORK5CYII=", + "elements": [ + { + "from": [ 0, 0.25, 0 ], + "to": [ 16, 0.25, 16 ], + "faces": { + "down": { "uv": [ 0, 16, 16, 0 ], "texture": "#texture", "tintindex": 0 }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#texture", "tintindex": 0 } + } + } + ] + } + }, + "animations": { + "cmm_test1": { + "loop": true, + "bones": { + "cmm": { + "rotation": { + "vector": [0, "q.anim_time * 20", 0] + } + } + } + } + } +} \ No newline at end of file diff --git a/minecraft/src/main/resources/player_animation_library.classtweaker b/minecraft/src/main/resources/player_animation_library.classtweaker index ecd9e71f..5f4b265f 100644 --- a/minecraft/src/main/resources/player_animation_library.classtweaker +++ b/minecraft/src/main/resources/player_animation_library.classtweaker @@ -3,5 +3,7 @@ extendable class net/minecraft/client/model/geom/ModelPart accessible field net/minecraft/client/renderer/LevelRenderer levelRenderState Lnet/minecraft/client/renderer/state/LevelRenderState; accessible field net/minecraft/client/renderer/entity/layers/RenderLayer renderer Lnet/minecraft/client/renderer/entity/RenderLayerParent; accessible field net/minecraft/client/renderer/entity/LivingEntityRenderer layers Ljava/util/List; +accessible field net/minecraft/client/renderer/block/model/BlockModel GSON Lcom/google/gson/Gson; +accessible method net/minecraft/client/renderer/texture/TextureAtlasSprite (Lnet/minecraft/resources/Identifier;Lnet/minecraft/client/renderer/texture/SpriteContents;IIIII)V transitive-inject-interface net/minecraft/world/entity/Avatar com/zigythebird/playeranim/accessors/IAnimatedAvatar transitive-inject-interface net/minecraft/client/renderer/entity/state/AvatarRenderState com/zigythebird/playeranim/accessors/IAvatarAnimationState