From 53fbc1705c5757aa3750d4a12f9b0b1ae30e89d9 Mon Sep 17 00:00:00 2001 From: Julian Lechner Date: Tue, 14 Jul 2026 15:27:01 +0200 Subject: [PATCH] feat: render GeckoLib cosmetics on player bodies --- .../client/cosmetic/PlayerCosmeticModel.java | 29 ++++++++++ .../cosmetic/PlayerCosmeticRenderLayer.java | 57 +++++++++++++++++++ .../cosmetic/PlayerCosmeticRenderer.java | 26 +++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticModel.java create mode 100644 src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderLayer.java create mode 100644 src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderer.java diff --git a/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticModel.java b/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticModel.java new file mode 100644 index 0000000..25d999a --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticModel.java @@ -0,0 +1,29 @@ +package de.fullrisk.citrusDev.client.cosmetic; + +import com.geckolib.model.GeoModel; +import com.geckolib.renderer.base.GeoRenderState; +import net.minecraft.resources.Identifier; + +public final class PlayerCosmeticModel extends GeoModel { + private static final Identifier MODEL = Identifier.fromNamespaceAndPath( + "citrus-dev", "geckolib/models/player/backpack.geo.json"); + private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath( + "citrus-dev", "textures/cosmetic/backpack.png"); + private static final Identifier ANIMATION = Identifier.fromNamespaceAndPath( + "citrus-dev", "geckolib/animations/backpack.animation.json"); + + @Override + public Identifier getModelResource(GeoRenderState renderState) { + return MODEL; + } + + @Override + public Identifier getTextureResource(GeoRenderState renderState) { + return TEXTURE; + } + + @Override + public Identifier getAnimationResource(PlayerCosmetic animatable) { + return ANIMATION; + } +} diff --git a/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderLayer.java b/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderLayer.java new file mode 100644 index 0000000..13023bc --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderLayer.java @@ -0,0 +1,57 @@ +package de.fullrisk.citrusDev.client.cosmetic; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.model.player.PlayerModel; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.RenderLayerParent; +import net.minecraft.client.renderer.entity.layers.RenderLayer; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; + +public final class PlayerCosmeticRenderLayer extends RenderLayer { + // Blockbench uses Minecraft model units around the vanilla body origin; no corrective transform is needed. + private static final float MODEL_SCALE = 1.0F; + + private final PlayerCosmeticRenderer renderer = new PlayerCosmeticRenderer(); + + public PlayerCosmeticRenderLayer(RenderLayerParent parent) { + super(parent); + } + + @Override + public void submit( + PoseStack poseStack, + SubmitNodeCollector nodeCollector, + int packedLight, + AvatarRenderState state, + float limbAngle, + float limbDistance + ) { + CosmeticRenderData renderData = state.getData(CosmeticRenderStateKeys.COSMETIC); + CameraRenderState cameraRenderState = state.getData(CosmeticRenderStateKeys.CAMERA); + + if (renderData == null || cameraRenderState == null || state.isInvisible || state.isSpectator + || !renderData.cosmetic().visible()) { + return; + } + + PlayerCosmetic animatable = PlayerCosmeticManager.getOrCreateAnimatable(renderData.playerId()); + poseStack.pushPose(); + + try { + getParentModel().body.translateAndRotate(poseStack); + poseStack.scale(MODEL_SCALE, MODEL_SCALE, MODEL_SCALE); + renderer.performRenderPass( + animatable, + renderData, + poseStack, + nodeCollector, + cameraRenderState, + packedLight, + renderData.partialTick() + ); + } finally { + poseStack.popPose(); + } + } +} diff --git a/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderer.java b/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderer.java new file mode 100644 index 0000000..10d8225 --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/client/cosmetic/PlayerCosmeticRenderer.java @@ -0,0 +1,26 @@ +package de.fullrisk.citrusDev.client.cosmetic; + +import com.geckolib.renderer.GeoObjectRenderer; +import com.geckolib.renderer.base.GeoRenderState; +import com.geckolib.renderer.base.RenderPassInfo; + +import java.util.UUID; + +public final class PlayerCosmeticRenderer extends GeoObjectRenderer< + PlayerCosmetic, CosmeticRenderData, GeoRenderState> { + + public PlayerCosmeticRenderer() { + super(new PlayerCosmeticModel()); + } + + @Override + public long getInstanceId(PlayerCosmetic animatable, CosmeticRenderData data) { + UUID uuid = data.playerId(); + return uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits(); + } + + @Override + public void adjustRenderPose(RenderPassInfo renderPassInfo) { + // The layer already supplies a body-local pose; omit GeoObjectRenderer's block-centering offset. + } +}