feat(告示牌甲渲染重构): 更新计划
This commit is contained in:
parent
c4ea7ae46c
commit
70666c8cc6
Binary file not shown.
Binary file not shown.
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
"minecraft.MixinGameRender",
|
||||
"minecraft.MixinIntegratedServer",
|
||||
"minecraft.MixinItemInHandRenderer",
|
||||
"minecraft.MixinLivingEntityRenderer",
|
||||
"minecraft.MixinMinecraft",
|
||||
"minecraft.MixinPlayerRenderer"
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user