feat(告示牌甲渲染重构): 更新计划

This commit is contained in:
叁玖领域 2026-06-24 08:09:29 +08:00
parent c4ea7ae46c
commit 70666c8cc6
8 changed files with 465 additions and 192 deletions

View File

@ -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<LivingEntity, EntityModel<LivingEntity>> {
// ==================== 可配置的旋转限制 ====================
/** 垂直速度缩放 */
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<WoodType, SignArmorRenderer> RENDERERS = new HashMap<>();
private float smoothPitch, smoothRoll, smoothYaw;
public SignArmorLayer(RenderLayerParent<LivingEntity, EntityModel<LivingEntity>> 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<LivingEntity> 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<SignArmorItem> 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);
});
}
}

View File

@ -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<SignArmorItem> {
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<SignArmorItem> {
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<GeoBone> 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<FormattedCharSequence> 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<FormattedCharSequence> 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;
}
}

View File

@ -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<SignArmorItem> {
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<FormattedCharSequence> 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);
}
}
}
}

View File

@ -213,16 +213,12 @@ public class SignArmorItem extends ArmorItem implements GeoItem {
@Override
public void initializeClient(@NotNull Consumer<IClientItemExtensions> 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

View File

@ -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 = "<init>", at = @At("RETURN"))
private void edg$addSignArmorLayer(CallbackInfo ci) {
LivingEntityRenderer.class.cast(this) .addLayer(
new SignArmorLayer(
(RenderLayerParent<LivingEntity, EntityModel<LivingEntity>>) this)
);
}
}

View File

@ -25,6 +25,7 @@
"minecraft.MixinGameRender",
"minecraft.MixinIntegratedServer",
"minecraft.MixinItemInHandRenderer",
"minecraft.MixinLivingEntityRenderer",
"minecraft.MixinMinecraft",
"minecraft.MixinPlayerRenderer"
]