Make held items follow bent arms and straighten the first-person arm
- PlayerModelMixin: after translateToHand, move the item from the hand of the straight arm to the hand of the bent one (BendTransforms.attachAt, the same rigid move as the ring at the hand). PlayerModel overrides translateToHand for slim arms without calling HumanoidModel's, so the hook has to be on PlayerModel. - FirstPersonHandMixin: the first-person arm is drawn from the shared model without setupAnim, so clear any bend left by the last third-person player. - LimbBend.start()/end() getters. - testmod: a hold animation, and gametest shots holding a sword and in first person. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -96,6 +96,16 @@ public final class LimbBend {
|
||||
return segments;
|
||||
}
|
||||
|
||||
/** Top of the limb along Y (its pivot end), as given to the constructor. */
|
||||
public float start() {
|
||||
return start;
|
||||
}
|
||||
|
||||
/** Bottom of the limb along Y (the hand or foot end), as given to the constructor. */
|
||||
public float end() {
|
||||
return start + length;
|
||||
}
|
||||
|
||||
/** Y positions where the limb is cut into pieces, not counting its ends. */
|
||||
public float[] cuts() {
|
||||
float[] cuts = new float[segments - 1];
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.saturnclient.emotes.fabric;
|
||||
|
||||
import org.joml.Quaternionf;
|
||||
import org.saturnclient.emotes.core.bend.LimbBend;
|
||||
import org.saturnclient.emotes.core.bend.Vec3;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
|
||||
/** Pose stack helpers for attaching things to bent limbs. */
|
||||
public final class BendTransforms {
|
||||
private BendTransforms() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves whatever is drawn next from the straight limb onto the bent one, as if it were fixed to the
|
||||
* limb at {@code y} (model pixels, in the limb's space). This is the same rigid move the ring at
|
||||
* {@code y} makes, so anything attached there stays attached the way the mesh does. The pose stack
|
||||
* must already be in the limb's space, i.e. after {@code ModelPart.translateAndRotate}.
|
||||
*/
|
||||
public static void attachAt(PoseStack poseStack, LimbBend bend, float y) {
|
||||
float[] center = new float[3];
|
||||
bend.bendPoint(0, y, 0, center);
|
||||
Vec3 axis = bend.axis();
|
||||
poseStack.translate(center[0] / 16, center[1] / 16, center[2] / 16);
|
||||
poseStack.mulPose(new Quaternionf().rotationAxis(bend.angleAt(y), axis.x(), axis.y(), axis.z()));
|
||||
poseStack.translate(0, -y / 16, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.saturnclient.emotes.fabric.mixin;
|
||||
|
||||
import org.saturnclient.emotes.fabric.access.BendablePart;
|
||||
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;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
|
||||
//?} else
|
||||
/*import net.minecraft.client.renderer.entity.player.PlayerRenderer;*/
|
||||
|
||||
/**
|
||||
* The first-person arm is drawn from the same shared model without calling setupAnim, so it would
|
||||
* keep whatever bend the last third-person player drawn left on it. It should never be bent.
|
||||
*/
|
||||
//? if >=1.21.9 {
|
||||
@Mixin(AvatarRenderer.class)
|
||||
//?} else
|
||||
/*@Mixin(PlayerRenderer.class)*/
|
||||
public class FirstPersonHandMixin {
|
||||
@Inject(method = "renderHand", at = @At("HEAD"))
|
||||
private void saturnEmotes$straightenArm(CallbackInfo ci, @Local(argsOnly = true) ModelPart arm) {
|
||||
((BendablePart) (Object) arm).saturnEmotes$setBend(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.saturnclient.emotes.fabric.mixin;
|
||||
|
||||
import org.saturnclient.emotes.core.bend.LimbBend;
|
||||
import org.saturnclient.emotes.fabric.BendTransforms;
|
||||
import org.saturnclient.emotes.fabric.access.BendablePart;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
|
||||
import net.minecraft.client.model.HumanoidModel;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
//? if >=1.21.11 {
|
||||
import net.minecraft.client.model.player.PlayerModel;
|
||||
//?} else
|
||||
/*import net.minecraft.client.model.PlayerModel;*/
|
||||
|
||||
/**
|
||||
* Held items are placed at the hand of the straight arm. PlayerModel overrides translateToHand (for
|
||||
* slim arms) without calling HumanoidModel's, so this has to hook PlayerModel's own.
|
||||
*/
|
||||
@Mixin(PlayerModel.class)
|
||||
public class PlayerModelMixin {
|
||||
//? if >=1.21.9 {
|
||||
@Unique
|
||||
private static final String TRANSLATE_TO_HAND = "translateToHand(Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;Lnet/minecraft/world/entity/HumanoidArm;Lcom/mojang/blaze3d/vertex/PoseStack;)V";
|
||||
//?} else
|
||||
/*@Unique private static final String TRANSLATE_TO_HAND = "translateToHand(Lnet/minecraft/world/entity/HumanoidArm;Lcom/mojang/blaze3d/vertex/PoseStack;)V";*/
|
||||
|
||||
/** Moves the item from the hand of the straight arm to the hand of the bent one. */
|
||||
@Inject(method = TRANSLATE_TO_HAND, at = @At("TAIL"))
|
||||
private void saturnEmotes$followBentArm(CallbackInfo ci, @Local(argsOnly = true) HumanoidArm arm,
|
||||
@Local(argsOnly = true) PoseStack poseStack) {
|
||||
HumanoidModel<?> model = (HumanoidModel<?>) (Object) this;
|
||||
ModelPart part = arm == HumanoidArm.LEFT ? model.leftArm : model.rightArm;
|
||||
LimbBend bend = ((BendablePart) (Object) part).saturnEmotes$getBend();
|
||||
if (bend != null && !bend.isStraight()) {
|
||||
BendTransforms.attachAt(poseStack, bend, bend.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@
|
||||
"package": "org.saturnclient.emotes.fabric.mixin",
|
||||
"compatibilityLevel": "${java}",
|
||||
"client": [
|
||||
"FirstPersonHandMixin",
|
||||
"HumanoidModelMixin",
|
||||
"LivingEntityRenderStateMixin",
|
||||
"LivingEntityRendererMixin",
|
||||
"ModelPartMixin"
|
||||
"ModelPartMixin",
|
||||
"PlayerModelMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
@@ -49,6 +49,29 @@ public final class EmotesGameTest implements FabricClientGameTest {
|
||||
}
|
||||
context.runOnClient(client -> Emotes.stop(client.player.getUUID()));
|
||||
}
|
||||
|
||||
// A held item should come out of the fist on the bent forearm.
|
||||
singleplayer.getServer().runCommand("give @a diamond_sword");
|
||||
context.runOnClient(client -> Emotes.play(client.player.getUUID(), TestAnimations.ALL.get("hold")));
|
||||
context.waitTicks(10);
|
||||
screenshot(context, "hold-0", 0);
|
||||
screenshot(context, "hold-90", 90);
|
||||
|
||||
// The first-person arm shares the model, so it must not keep the bend from the frame above.
|
||||
// Vanilla only draws the arm with an empty hand, and hiding the GUI hides it too.
|
||||
singleplayer.getServer().runCommand("clear @a");
|
||||
context.waitTicks(2);
|
||||
context.runOnClient(client -> {
|
||||
client.options.hideGui = false;
|
||||
client.options.setCameraType(CameraType.FIRST_PERSON);
|
||||
});
|
||||
// Give the hand time to come back up after the item change.
|
||||
context.waitTicks(20);
|
||||
context.takeScreenshot(MINECRAFT + "-hold-firstperson");
|
||||
context.runOnClient(client -> {
|
||||
Emotes.stop(client.player.getUUID());
|
||||
client.options.hideGui = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,10 @@ final class TestAnimations {
|
||||
.set(Bone.BODY, BoneTransform.IDENTITY.withBend(lean, 0))
|
||||
.build();
|
||||
}),
|
||||
// Right arm forward with the forearm folded up, as if showing off a sword. Tests held items.
|
||||
"hold", of(20, true, t -> Pose.builder()
|
||||
.set(Bone.RIGHT_ARM, BoneTransform.rotation(-0.9f, 0, 0).withBend(1.4f, 0))
|
||||
.build()),
|
||||
"twerk", of(8, true, TestAnimations::twerk));
|
||||
|
||||
private TestAnimations() {
|
||||
|
||||
Reference in New Issue
Block a user