diff --git a/model_source/blender/kneel_display_rack.blend b/model_source/blender/kneel_display_rack.blend index 15d19cb5..603465a2 100644 Binary files a/model_source/blender/kneel_display_rack.blend and b/model_source/blender/kneel_display_rack.blend differ diff --git a/model_source/blender/kneel_display_rack.blend1 b/model_source/blender/kneel_display_rack.blend1 index 75cf99b5..15d19cb5 100644 Binary files a/model_source/blender/kneel_display_rack.blend1 and b/model_source/blender/kneel_display_rack.blend1 differ diff --git a/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/armor/SignArmorLayer.java b/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/armor/SignArmorLayer.java new file mode 100644 index 00000000..5f971a2b --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/armor/SignArmorLayer.java @@ -0,0 +1,194 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.client.renderer.armor; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.RenderLayerParent; +import net.minecraft.client.renderer.entity.layers.RenderLayer; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.properties.WoodType; +import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.renderer.GeoRenderer; +import top.r3944realms.eroticdungeongame.content.item.SignArmorItem; + +import java.util.HashMap; +import java.util.Map; + +public class SignArmorLayer extends RenderLayer> { + + // ==================== 可配置的旋转限制 ==================== + + /** 垂直速度缩放 */ + private static final float VERTICAL_SCALE = 10.0F; + /** 垂直旋转钳制 [min, max] 度 */ + private static final float VERTICAL_CLAMP_MIN = -6.0F; + private static final float VERTICAL_CLAMP_MAX = 32.0F; + + /** 前进速度缩放 */ + private static final float FORWARD_SCALE = 100.0F; + /** 前进旋转钳制 [min, max] 度 */ + private static final float FORWARD_CLAMP_MIN = 0.0F; + private static final float FORWARD_CLAMP_MAX = 150.0F; + + /** 侧向速度缩放 */ + private static final float SIDEWAYS_SCALE = 100.0F; + /** 侧向旋转钳制 [min, max] 度 */ + private static final float SIDEWAYS_CLAMP_MIN = -20.0F; + private static final float SIDEWAYS_CLAMP_MAX = 20.0F; + + /** 基准俯仰角(负值=后仰远离身体) */ + private static final float BASE_PITCH_DEG = -7.5F; + + /** 旋转除数 */ + private static final float FORWARD_DIVISOR = 2.0F; + private static final float SIDEWAYS_DIVISOR = 2.0F; + private static final float YAW_BASE_DEG = 180.0F; + + /** 走路弹跳幅度 */ + private static final float WALK_BOB_SCALE = 32.0F; + /** 蹲下附加上仰 */ + private static final float CROUCH_BONUS = 25.0F; + + /** 旋转平滑系数 (0=无平滑, 1=完全不更新) */ + private static final float SMOOTHING = 0.5F; + + private static final float BASE_YP_ROTATION = 180F; + + private static final double BASE_X_TRANS = 0; + private static final double BASE_Y_TRANS = -1.5D; + private static final double BASE_Z_TRANS = 0; + + + + // ==================== 实现 ==================== + + private static final Map RENDERERS = new HashMap<>(); + private float smoothPitch, smoothRoll, smoothYaw; + + public SignArmorLayer(RenderLayerParent> parent) { + super(parent); + } + + @Override + public void render(@NotNull PoseStack poseStack, @NotNull MultiBufferSource buffer, int packedLight, + LivingEntity entity, float limbSwing, float limbSwingAmount, + float partialTick, float ageInTicks, float netHeadYaw, float headPitch) { + ItemStack chest = entity.getItemBySlot(EquipmentSlot.CHEST); + if (!(chest.getItem() instanceof SignArmorItem signItem)) return; + + // 模型必须有 body 部分才渲染(即 HumanoidModel 子类) + EntityModel parentModel = getParentModel(); + if (!(parentModel instanceof HumanoidModel humanoidModel)) return; + + SignArmorRenderer renderer = RENDERERS.computeIfAbsent( + signItem.getWoodType(), SignArmorRenderer::new); + + // CapeLayer 式物理 → bipedBody 骨骼旋转 + applyCapeLayerPhysics(entity, partialTick, limbSwing, limbSwingAmount, renderer); + + renderer.prepForRender(entity, chest, EquipmentSlot.CHEST, humanoidModel); + // 手动触发文字准备 (renderRecursively 绕过 preRender) + renderer.prepareSignText(signItem); + + poseStack.pushPose(); + humanoidModel.body.translateAndRotate(poseStack); + // GeckoLib armor 内部坐标系矫正 (GeoArmorRenderer.render 默认应用此变换) + poseStack.scale(-1, -1, 1); + poseStack.mulPose(Axis.YP.rotationDegrees(BASE_YP_ROTATION)); + poseStack.translate(BASE_X_TRANS, BASE_Y_TRANS, BASE_Z_TRANS); + + + GeoRenderer gr = renderer; + ResourceLocation modelRes = renderer.getGeoModel().getModelResource(signItem); + BakedGeoModel baked = renderer.getGeoModel().getBakedModel(modelRes); + RenderType rt = RenderType.entityCutoutNoCull( + renderer.getGeoModel().getTextureResource(signItem)); + VertexConsumer vc = buffer.getBuffer(rt); + + for (GeoBone topBone : baked.topLevelBones()) { + gr.renderRecursively(poseStack, signItem, topBone, rt, buffer, vc, + false, partialTick, packedLight, OverlayTexture.NO_OVERLAY, + 1, 1, 1, 1); + } + poseStack.popPose(); + } + + /** + * 基于 CapeLayer 的速度分解物理。 + * 对 LivingEntity 使用通用字段 (yBodyRot, walkDist, xo/yo/zo), + * 走路弹跳使用传入的 limbSwing 参数代替 Player 专属的 bob。 + */ + private void applyCapeLayerPhysics(@NotNull LivingEntity entity, float partialTick, + float limbSwing, float limbSwingAmount, + SignArmorRenderer renderer) { + float bodyYaw = Mth.rotLerp(partialTick, entity.yBodyRotO, entity.yBodyRot); + float yawRad = bodyYaw * Mth.DEG_TO_RAD; + double sin = Mth.sin(yawRad); + double cos = -Mth.cos(yawRad); + + double dx = entity.getX() - entity.xo; + double dy = entity.getY() - entity.yo; + double dz = entity.getZ() - entity.zo; + + float forward = (float) (dx * sin + dz * cos) * FORWARD_SCALE; + forward = Mth.clamp(forward, FORWARD_CLAMP_MIN, FORWARD_CLAMP_MAX); + if (forward < 0.0F) forward = 0.0F; + + float sideways = (float) (dx * cos - dz * sin) * SIDEWAYS_SCALE; + sideways = Mth.clamp(sideways, SIDEWAYS_CLAMP_MIN, SIDEWAYS_CLAMP_MAX); + + float vertical = (float) dy * VERTICAL_SCALE; + vertical = Mth.clamp(vertical, VERTICAL_CLAMP_MIN, VERTICAL_CLAMP_MAX); + + // 走路弹跳:使用 limbSwing 和 walkDist + float walkDist = Mth.lerp(partialTick, entity.walkDistO, entity.walkDist); + float bobAmount = Math.min(limbSwingAmount * 1.5F, 1.0F); + vertical += Mth.sin(walkDist * 6.0F) * WALK_BOB_SCALE * bobAmount; + + if (entity.isCrouching()) { + vertical += CROUCH_BONUS; + } + + float targetPitch = BASE_PITCH_DEG + forward / FORWARD_DIVISOR + vertical; + float targetRoll = sideways / SIDEWAYS_DIVISOR; + float targetYaw = YAW_BASE_DEG - sideways / SIDEWAYS_DIVISOR; + + float a = SMOOTHING; + smoothPitch = Mth.lerp(a, smoothPitch, targetPitch); + smoothRoll = Mth.lerp(a, smoothRoll, targetRoll); + smoothYaw = Mth.lerp(a, smoothYaw, targetYaw); + + renderer.getGeoModel().getBone("bipedBody").ifPresent(b -> { + b.setRotX(smoothPitch * Mth.DEG_TO_RAD); + b.setRotY(smoothYaw * Mth.DEG_TO_RAD); + b.setRotZ(smoothRoll * Mth.DEG_TO_RAD); + }); + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/armor/SignArmorRenderer.java b/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/armor/SignArmorRenderer.java index d6f269b7..51276787 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/armor/SignArmorRenderer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/armor/SignArmorRenderer.java @@ -16,6 +16,7 @@ package top.r3944realms.eroticdungeongame.client.renderer.armor; +import com.github.tartaricacid.touhoulittlemaid.geckolib3.geo.raw.pojo.Bone; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.Axis; @@ -24,8 +25,11 @@ import net.minecraft.client.gui.Font; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.block.entity.SignText; import net.minecraft.world.level.block.state.properties.WoodType; +import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import software.bernie.geckolib.cache.object.BakedGeoModel; @@ -36,16 +40,38 @@ import top.r3944realms.eroticdungeongame.content.item.SignArmorItem; import top.r3944realms.eroticdungeongame.util.ColorUtil; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.WeakHashMap; +import java.util.function.Consumer; public class SignArmorRenderer extends GeoArmorRenderer { private static final float TEXT_SCALE = 0.015625F * 0.9f; - private static final float TEXT_Y_OFFSET = 51; - private static final float TEXT_Z_OFFSET = -14.2f; private static final float LINE_HEIGHT = 9.5F; - private float crouchingYOffset = 0; - private boolean isCrouching; - private boolean init; + + // 文字在 bone 局部空间的偏移(手调值,对应牌面在 bone pivot 的相对位置) + // bone pivot = [0, 24, 0], board face center ≈ [0, 21, -2.025] + // 相对 pivot = [0, -3, -2.025] BB单位 → /16 → /TEXT_SCALE → 手调修正 + private static final float BONE_LOCAL_TEXT_Y = (-3f / 16f) / TEXT_SCALE - 43; + private static final float BONE_LOCAL_TEXT_Z = (-2.025f / 16f) / TEXT_SCALE + 30; + private static final float BONE_ROTATE_XP = 7.5f; + + private float crouchingYOffset; + + private FormattedCharSequence[] signMessages; + private boolean signHasGlowingText; + private int signTextColor; + private int signDarkColor; + + private static class CapePhysics { + Vec3 prevPos = Vec3.ZERO; + float prevYRot; + float accYaw, accPitch, accRoll; + float targetYaw, targetPitch, targetRoll; + float walkTime; + boolean wasOnGround; + } + public SignArmorRenderer(WoodType woodType) { super(new SignArmorModel(woodType)); } @@ -57,114 +83,109 @@ public class SignArmorRenderer extends GeoArmorRenderer { float red, float green, float blue, float alpha) { super.preRender(poseStack, animatable, model, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha); + if (currentEntity != null && currentEntity.isCrouching()) { crouchingYOffset = -4.5f; - isCrouching = true; - Optional bipedBody = this.model.getBone("bipedBody"); - bipedBody.ifPresent(geoBone -> geoBone.setPosY(crouchingYOffset)); + this.model.getBone("bipedBody") + .ifPresent(geoBone -> geoBone.setPosY(crouchingYOffset)); } else { crouchingYOffset = 0; - isCrouching = false; } + + prepareSignText(animatable); } @Override - public void actuallyRender(PoseStack poseStack, SignArmorItem animatable, BakedGeoModel model, RenderType renderType, MultiBufferSource bufferSource, VertexConsumer buffer, boolean isReRender, float partialTick, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) { - super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, buffer, isReRender, partialTick, packedLight, packedOverlay, red, green, blue, alpha); - // 后渲染文字 - renderSignText(animatable, poseStack, bufferSource, packedLight, animatable.getTextLineHeight(), animatable.getMaxTextLineWidth()); + public void renderRecursively(PoseStack poseStack, SignArmorItem animatable, GeoBone bone, + RenderType renderType, MultiBufferSource bufferSource, + VertexConsumer buffer, boolean isReRender, float partialTick, + int packedLight, int packedOverlay, + float red, float green, float blue, float alpha) { + super.renderRecursively(poseStack, animatable, bone, renderType, bufferSource, + buffer, isReRender, partialTick, packedLight, packedOverlay, + red, green, blue, alpha); + + if ("bone".equals(bone.getName()) && signMessages != null) { + renderSignTextInBoneSpace(poseStack, bufferSource, packedLight); + } } - private void renderSignText(@NotNull SignArmorItem armorItem, PoseStack poseStack, - MultiBufferSource bufferSource, int packedLight, int lineHeight, int maxWidth) { - SignText signText = armorItem.getSignText(currentStack); - if (signText == null) { - return; - } + // ==================== 文字渲染 ==================== - // 检查是否有任何文字 - boolean hasText = false; + void prepareSignText(SignArmorItem armorItem) { + SignText signText = armorItem.getSignText(currentStack); + if (signText == null) { signMessages = null; return; } + boolean any = false; for (int i = 0; i < 4; i++) { - if (!signText.getMessage(i, false).getString().isEmpty()) { - hasText = true; - break; - } + if (!signText.getMessage(i, false).getString().isEmpty()) { any = true; break; } } - if (!hasText) return; + if (!any) { signMessages = null; return; } + + signHasGlowingText = signText.hasGlowingText(); + signDarkColor = ColorUtil.getDarkColor(signText); + signTextColor = signHasGlowingText ? signText.getColor().getTextColor() : signDarkColor; Minecraft mc = Minecraft.getInstance(); Font font = mc.font; + int maxWidth = armorItem.getMaxTextLineWidth(); + FormattedCharSequence[] msgs = signText.getRenderMessages(mc.isTextFilteringEnabled(), split -> { + List l = font.split(split, maxWidth); + return l.isEmpty() ? FormattedCharSequence.EMPTY : l.get(0); + }); + signMessages = new FormattedCharSequence[4]; + for (int i = 0; i < 4; i++) { + signMessages[i] = msgs[i] != null ? msgs[i] : null; + } + } + + /** + * 在 bone 骨骼局部空间中渲染文字。 + * PoseStack 已累积了 bipedBody → bone 的完整层级变换(含任意角度骨骼旋转), + * 此处的 translate 在已旋转空间中位移至牌面位置,无需手动 mulPose。 + */ + private void renderSignTextInBoneSpace(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) { + if (signMessages == null) return; + + Minecraft mc = Minecraft.getInstance(); + Font font = mc.font; + int light = signHasGlowingText ? 15728880 : packedLight; poseStack.pushPose(); - poseStack.scale(TEXT_SCALE, TEXT_SCALE, TEXT_SCALE); - poseStack.mulPose(Axis.XP.rotationDegrees(-7.5f)); - poseStack.translate(0, TEXT_Y_OFFSET, TEXT_Z_OFFSET); - if(isCrouching) { - poseStack.translate(0, - (crouchingYOffset*4.5), 3.2); - } - - // 发光文字使用高亮度 - int light = signText.hasGlowingText() ? 15728880 : packedLight; + poseStack.scale(-TEXT_SCALE, -TEXT_SCALE, -TEXT_SCALE); + poseStack.translate(0, BONE_LOCAL_TEXT_Y, BONE_LOCAL_TEXT_Z); + poseStack.mulPose(Axis.XP.rotationDegrees(BONE_ROTATE_XP)); for (int i = 0; i < 4; i++) { - if (!signText.getMessage(i, false).getString().isEmpty()) { - break; + FormattedCharSequence message = signMessages[i]; + if (message == null) continue; + + float x = -font.width(message) / 2f; + float y = i * LINE_HEIGHT - 4 * LINE_HEIGHT / 2f; + + if (signHasGlowingText) { + font.drawInBatch8xOutline( + message, x, y, signTextColor, signDarkColor, + poseStack.last().pose(), bufferSource, light); + } else { + font.drawInBatch( + message, x, y, signTextColor, + false, poseStack.last().pose(), bufferSource, + Font.DisplayMode.POLYGON_OFFSET, 0, light); } } - - // 获取文字颜色(原版使用 getDarkColor) - int darkColor = ColorUtil.getDarkColor(signText); - - // 获取渲染用的文字行(原版使用 getRenderMessages) - FormattedCharSequence[] renderMessages = signText.getRenderMessages(Minecraft.getInstance().isTextFilteringEnabled(), (split) -> { - List list = font.split(split, maxWidth); - return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0); - }); - - // 确定是否使用发光文字 - boolean hasGlowingText = signText.hasGlowingText(); - int glowLight = hasGlowingText ? 15728880 : packedLight; - int textColor = hasGlowingText ? signText.getColor().getTextColor() : darkColor; - - // 渲染4行文字(原版循环) - for (int line = 0; line < 4; line++) { - FormattedCharSequence formattedText = renderMessages[line]; - if (formattedText != null) { - // 计算X轴偏移(居中) - float xOffset = (float)(-font.width(formattedText) / 2); - // 计算Y轴偏移(原版公式) - float yOffset = line * LINE_HEIGHT - 4 * LINE_HEIGHT / 2; - - if (hasGlowingText) { - // 发光文字 - font.drawInBatch8xOutline( - formattedText, - xOffset, yOffset, - textColor, // 内部颜色 - darkColor, // 轮廓颜色 - poseStack.last().pose(), - bufferSource, - glowLight - ); - } else { - // 普通文字 - font.drawInBatch( - formattedText, - xOffset, yOffset, - textColor, - false, - poseStack.last().pose(), - bufferSource, - Font.DisplayMode.POLYGON_OFFSET, - 0, - packedLight - ); - } - } - } poseStack.popPose(); } - + @Override + public void actuallyRender(PoseStack poseStack, SignArmorItem animatable, BakedGeoModel model, + RenderType renderType, MultiBufferSource bufferSource, + VertexConsumer buffer, boolean isReRender, float partialTick, + int packedLight, int packedOverlay, + float red, float green, float blue, float alpha) { + super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, + buffer, isReRender, partialTick, packedLight, packedOverlay, + red, green, blue, alpha); + signMessages = null; + } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/item/SignArmorItemRenderer.java b/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/item/SignArmorItemRenderer.java index ec31ebc1..69c5b5ae 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/item/SignArmorItemRenderer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/client/renderer/item/SignArmorItemRenderer.java @@ -17,28 +17,44 @@ package top.r3944realms.eroticdungeongame.client.renderer.item; import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.math.Axis; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; -import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.entity.ItemRenderer; -import net.minecraft.client.resources.model.BakedModel; -import net.minecraft.network.chat.Style; +import net.minecraft.client.renderer.RenderType; import net.minecraft.util.FormattedCharSequence; import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.entity.SignText; import org.jetbrains.annotations.NotNull; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.renderer.GeoItemRenderer; +import top.r3944realms.eroticdungeongame.client.model.item.SignArmorModel; import top.r3944realms.eroticdungeongame.content.item.SignArmorItem; import top.r3944realms.eroticdungeongame.util.ColorUtil; -public class SignArmorItemRenderer extends BlockEntityWithoutLevelRenderer { +import java.util.List; +import java.util.Optional; + +public class SignArmorItemRenderer extends GeoItemRenderer { + private static final float TEXT_SCALE = 0.015625F * 0.9f; + private static final float LINE_HEIGHT = 9.5F; + private static final float BONE_ROTATE_XP = 7.5f; + + // 文字在 bone 局部空间的偏移(bone pivot [0,24,0], face cube [0,21,-2.025]) + private static final float BONE_LOCAL_TEXT_Y = (-3f / 16f) / TEXT_SCALE - 43; + private static final float BONE_LOCAL_TEXT_Z = (-2.025f / 16f) / TEXT_SCALE + 30; + private final SignArmorItem item; + private FormattedCharSequence[] signMessages; + private boolean signHasGlowingText; + private int signTextColor; + private int signDarkColor; public SignArmorItemRenderer(SignArmorItem item) { - super(Minecraft.getInstance().getBlockEntityRenderDispatcher(), - Minecraft.getInstance().getEntityModels()); + super(new SignArmorModel(item.getWoodType())); this.item = item; } @@ -46,114 +62,119 @@ public class SignArmorItemRenderer extends BlockEntityWithoutLevelRenderer { public void renderByItem(@NotNull ItemStack stack, @NotNull ItemDisplayContext transformType, @NotNull PoseStack poseStack, @NotNull MultiBufferSource bufferSource, int packedLight, int packedOverlay) { - - // 1. 渲染原版 JSON 模型 - Minecraft mc = Minecraft.getInstance(); - ItemRenderer itemRenderer = mc.getItemRenderer(); - BakedModel bakedModel = itemRenderer.getModel(stack, null, null, 0); - - // 使用原版渲染器渲染模型 - itemRenderer.render(stack, transformType, false, poseStack, bufferSource, packedLight, packedOverlay, bakedModel); - - // 2. 在模型之上渲染文字 - renderSignText(stack, transformType, poseStack, bufferSource, packedLight); + prepareSignText(stack); + super.renderByItem(stack, transformType, poseStack, bufferSource, packedLight, packedOverlay); + signMessages = null; } - private void renderSignText(ItemStack stack, ItemDisplayContext transformType, - PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) { - SignText signText = item.getSignText(stack); - if (signText == null) return; + @Override + public void actuallyRender(PoseStack poseStack, SignArmorItem animatable, BakedGeoModel model, + RenderType renderType, MultiBufferSource bufferSource, + VertexConsumer buffer, boolean isReRender, float partialTick, + int packedLight, int packedOverlay, + float red, float green, float blue, float alpha) { + super.actuallyRender(poseStack, animatable, model, renderType, bufferSource, + buffer, isReRender, partialTick, packedLight, packedOverlay, + red, green, blue, alpha); - // 检查是否有文字内容 - boolean hasText = false; - for (int i = 0; i < 4; i++) { - if (!signText.getMessage(i, false).getString().isEmpty()) { - hasText = true; - break; - } + // 直接在模型渲染完成后渲染文字 + if (signMessages != null) { + renderSignTextOnBone(poseStack, model, bufferSource, packedLight); } - if (!hasText) return; + } + + private void prepareSignText(ItemStack stack) { + SignText signText = item.getSignText(stack); + if (signText == null) { signMessages = null; return; } + boolean any = false; + for (int i = 0; i < 4; i++) { + if (!signText.getMessage(i, false).getString().isEmpty()) { any = true; break; } + } + if (!any) { signMessages = null; return; } + + signHasGlowingText = signText.hasGlowingText(); + signDarkColor = ColorUtil.getDarkColor(signText); + signTextColor = signHasGlowingText ? signText.getColor().getTextColor() : signDarkColor; Minecraft mc = Minecraft.getInstance(); Font font = mc.font; + int maxWidth = item.getMaxTextLineWidth(); + FormattedCharSequence[] msgs = signText.getRenderMessages(mc.isTextFilteringEnabled(), split -> { + List l = font.split(split, maxWidth); + return l.isEmpty() ? FormattedCharSequence.EMPTY : l.get(0); + }); + signMessages = new FormattedCharSequence[4]; + for (int i = 0; i < 4; i++) { + signMessages[i] = msgs[i] != null ? msgs[i] : null; + } + } + + /** + * 从 BakedGeoModel 找到 "bone" 骨骼,在其局部空间渲染文字。 + * 手动遍历骨骼层级累积变换矩阵,不依赖 renderRecursively 回调。 + */ + private void renderSignTextOnBone(PoseStack poseStack, BakedGeoModel model, + MultiBufferSource bufferSource, int packedLight) { + for (GeoBone topBone : model.topLevelBones()) { + findAndRenderOnBone(poseStack, topBone, bufferSource, packedLight); + } + } + + private boolean findAndRenderOnBone(PoseStack poseStack, GeoBone bone, + MultiBufferSource bufferSource, int packedLight) { + poseStack.pushPose(); + // 应用骨骼变换 + poseStack.translate(bone.getPivotX() / 16f, -bone.getPivotY() / 16f, bone.getPivotZ() / 16f); + poseStack.mulPose(Axis.XP.rotation(bone.getRotX())); + poseStack.mulPose(Axis.YP.rotation(bone.getRotY())); + poseStack.mulPose(Axis.ZP.rotation(bone.getRotZ())); + + boolean found = false; + if ("bone".equals(bone.getName())) { + renderSignTextInBoneSpace(poseStack, bufferSource, packedLight); + found = true; + } + + for (GeoBone child : bone.getChildBones()) { + if (findAndRenderOnBone(poseStack, child, bufferSource, packedLight)) { + found = true; + } + } + poseStack.popPose(); + return found; + } + + private void renderSignTextInBoneSpace(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) { + if (signMessages == null) return; + + Minecraft mc = Minecraft.getInstance(); + Font font = mc.font; + int light = signHasGlowingText ? 15728880 : packedLight; poseStack.pushPose(); + poseStack.scale(-TEXT_SCALE, -TEXT_SCALE, -TEXT_SCALE); + poseStack.translate(0, BONE_LOCAL_TEXT_Y, BONE_LOCAL_TEXT_Z); + poseStack.mulPose(Axis.XP.rotationDegrees(BONE_ROTATE_XP)); - // 根据不同显示模式调整文字位置和缩放 - applyTransform(transformType, poseStack); + for (int i = 0; i < 4; i++) { + FormattedCharSequence message = signMessages[i]; + if (message == null) continue; - boolean hasGlowingText = signText.hasGlowingText(); - int textColor = signText.getColor().getTextColor(); - int darkColor = ColorUtil.getDarkColor(signText); - int glowLight = hasGlowingText ? 15728880 : packedLight; + float x = -font.width(message) / 2f; + float y = i * LINE_HEIGHT - 4 * LINE_HEIGHT / 2f; - // 渲染4行文字 - for (int line = 0; line < 4; line++) { - String message = signText.getMessage(line, false).getString(); - if (message.isEmpty()) continue; - - float xOffset = -font.width(message) / 2.0f; - float yOffset = line * item.getTextLineHeight() - ((float) (4 * item.getTextLineHeight()) / 2); - - if (hasGlowingText) { + if (signHasGlowingText) { font.drawInBatch8xOutline( - FormattedCharSequence.forward(message, Style.EMPTY), - xOffset, yOffset, - textColor, darkColor, - poseStack.last().pose(), - bufferSource, - glowLight - ); + message, x, y, signTextColor, signDarkColor, + poseStack.last().pose(), bufferSource, light); } else { font.drawInBatch( - message, - xOffset, yOffset, - textColor, - false, - poseStack.last().pose(), - bufferSource, - Font.DisplayMode.NORMAL, - 0, - packedLight - ); + message, x, y, signTextColor, + false, poseStack.last().pose(), bufferSource, + Font.DisplayMode.POLYGON_OFFSET, 0, light); } } poseStack.popPose(); } - - private void applyTransform(@NotNull ItemDisplayContext transformType, PoseStack poseStack) { - switch (transformType) { - case GUI -> { - // 物品栏显示 - poseStack.translate(8, 4, 0); - poseStack.scale(0.025f, 0.025f, 0.025f); - poseStack.mulPose(Axis.ZP.rotationDegrees(180)); - } - case GROUND -> { - // 掉落物实体 - poseStack.translate(8, 2, 0); - poseStack.scale(0.03f, 0.03f, 0.03f); - } - case FIRST_PERSON_RIGHT_HAND, FIRST_PERSON_LEFT_HAND -> { - // 第一人称手持 - poseStack.translate(6, 4, 5); - poseStack.scale(0.02f, 0.02f, 0.02f); - } - case THIRD_PERSON_RIGHT_HAND, THIRD_PERSON_LEFT_HAND -> { - // 第三人称手持 - poseStack.translate(4, 2, 3); - poseStack.scale(0.02f, 0.02f, 0.02f); - } - case FIXED -> { - // 展示框 - poseStack.translate(8, 4, 0); - poseStack.scale(0.025f, 0.025f, 0.025f); - } - default -> { - poseStack.translate(8, 4, 0); - poseStack.scale(0.025f, 0.025f, 0.025f); - } - } - } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/item/SignArmorItem.java b/src/main/java/top/r3944realms/eroticdungeongame/content/item/SignArmorItem.java index e64a9da2..f89ce731 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/content/item/SignArmorItem.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/item/SignArmorItem.java @@ -213,16 +213,12 @@ public class SignArmorItem extends ArmorItem implements GeoItem { @Override public void initializeClient(@NotNull Consumer consumer) { consumer.accept(new IClientItemExtensions() { - private GeoArmorRenderer renderer; private SignArmorItemRenderer itemRenderer; - @Override public @NotNull HumanoidModel getHumanoidArmorModel(LivingEntity livingEntity, ItemStack itemStack, EquipmentSlot equipmentSlot, HumanoidModel original) { - if (this.renderer == null) - this.renderer = new SignArmorRenderer(woodType); - this.renderer.prepForRender(livingEntity, itemStack, equipmentSlot, original); - return this.renderer; + // 告示牌甲的模型渲染由 SignArmorLayer 统一处理,此处返回原始模型避免双重渲染 + return original; } @Override diff --git a/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinLivingEntityRenderer.java b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinLivingEntityRenderer.java new file mode 100644 index 00000000..3cbbd09c --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/mixin/minecraft/MixinLivingEntityRenderer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.mixin.minecraft; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.client.renderer.entity.RenderLayerParent; +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; +import top.r3944realms.eroticdungeongame.client.renderer.armor.SignArmorLayer; + +@Mixin(LivingEntityRenderer.class) +public class MixinLivingEntityRenderer { + + @SuppressWarnings({"unchecked"}) + @Inject(method = "", at = @At("RETURN")) + private void edg$addSignArmorLayer(CallbackInfo ci) { + LivingEntityRenderer.class.cast(this) .addLayer( + new SignArmorLayer( + (RenderLayerParent>) this) + ); + } +} diff --git a/src/main/resources/eroticdungeongame.mixins.json b/src/main/resources/eroticdungeongame.mixins.json index 9caac11a..acef1fce 100644 --- a/src/main/resources/eroticdungeongame.mixins.json +++ b/src/main/resources/eroticdungeongame.mixins.json @@ -25,6 +25,7 @@ "minecraft.MixinGameRender", "minecraft.MixinIntegratedServer", "minecraft.MixinItemInHandRenderer", + "minecraft.MixinLivingEntityRenderer", "minecraft.MixinMinecraft", "minecraft.MixinPlayerRenderer" ]