拔刀剑:迟来的礼物

拔刀剑:迟来的礼物

拔刀剑渲染重构与优化——献给每一位拔刀剑爱好者!

性能

迟来的礼物 — 拔刀剑渲染优化模组

📖 这是什么?

这是一个针对 拔刀剑:重铸版 的客户端渲染优化模组,旨在消除手持刀剑、挥刀、渲染掉落刀剑、使用光影或加载 YSM 模型时出现的严重 FPS 下降问题。

一句话总结:持刀帧率翻倍,挥刀帧率提升三倍,掉落的高模刀剑获得 30 倍性能提升。


📊 性能对比

模组版本:1.0.2

测试配置:AMD Ryzen 7 9700X + NVIDIA RTX 5070 Ti

第三人称视角,BSL 光影,测试刀剑:炼金王国

无光影

场景 原版 加速渲染 迟来的礼物 提升幅度(对比原版)
手持刀剑 359 412 810 +126%
挥刀 220 248 620 +182%
17 把高模掉落刀剑 61 425 730 +1097%

光影(BSL)下

场景 原版 加速渲染 迟来的礼物 提升幅度(对比原版)
手持刀剑 208 330 381 +83%
挥刀 115 153 365 +217%
17 把高模掉落刀剑 29 240 258 +1134%

YSM 兼容性

场景 原版 加速渲染 迟来的礼物 提升幅度(对比原版)
无光影 · 手持 374 398 724 +94%
无光影 · 挥刀 208 362 605 +191%
光影 · 手持 215 301 374 +74%
光影 · 挥刀 172 261 345 +101%

⚙️ 工作原理

不再“每帧重绘”

原版渲染在每一帧都会遍历 OBJ 模型的每个分组、面和顶点。本模组在加载时将数据“烘焙”为 GPU 友好的格式,之后每帧只需直接复用即可。

静态 VBO 缓存

刀身、刀鞘、物品图标、刀架上的刀以及掉落刀剑共享一个 96 MiB 的 LRU 缓存。同一把刀第二次出现时,直接复用已上传到显存的数据,无需重新提交顶点数据。

附魔闪光效果也拥有独立的静态缓存,不再拖累性能。

光影下的“智能节省”

  • 阴影渲染时使用简化的 32 段代理网格,而非完整的高模模型
  • 透明特效(斩击特效、次元斩等)完全跳过阴影贴图阶段
  • 发光刀身不再重复投射阴影

挥刀动画 —— “只做必要计算”

原版在每次挥刀时都会计算完整的 PMD 顶点蒙皮,但实际上只使用了两个骨骼锚点。本模组跳过了无用的顶点计算,仅保留骨骼姿态。

刀剑特效 LOD

近距离显示完整细节,中距离减少层级,远距离仅保留核心刀身——在光影下还会进一步简化。

无首次使用卡顿

模型、纹理和 RenderType 会在进入游戏之前完成预热。首次挥刀或打开物品栏时不会突然卡顿。

自动缓存清理

退出世界或重新加载资源时自动清除缓存——长时间游玩性能保持一致。

兼容性与安全回退

保留 RenderOverrideEvent。如果检测到动态 UV、动态透明度或不可缓存的网格,会自动回退到原版渲染——不会崩溃。


⚠️ 不兼容性

与 [加速渲染] 不兼容 —— 两个模组功能重叠。同时安装可能导致渲染异常或性能下降。


斩击效果渲染 API

本 API 面向需要替换斩击效果模型或纹理的拔刀剑附属模组。附属模组只需选择资源,而“迟来的礼物”继续负责基于距离的 LOD、光影兼容性、网格优化和资源预热。

采用此 API 后,附属模组不得使用 Mixin 取消 SlashEffectRenderer.render。取消该方法会导致注册表无法被访问。

仅替换斩击纹理

从排队的 FMLClientSetupEvent 任务中注册规则:

import cn.star.a_belated_gift.api.client.SlashEffectRenderRegistry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;

private static final ResourceLocation RULE_ID =
        new ResourceLocation("example", "special_blade_slash");
private static final ResourceLocation SLASH_TEXTURE =
        new ResourceLocation("example", "model/util/special_slash.png");

private static void onClientSetup(FMLClientSetupEvent event) {
    event.enqueueWork(() -> SlashEffectRenderRegistry.registerTexture(
            RULE_ID,
            100,
            effect -> effect.getOwner() instanceof Player player
                    && player.getMainHandItem().is(EXAMPLE_BLADE.get()),
            SLASH_TEXTURE
    ));
}

registerTexture 继续使用拔刀剑默认的 model/util/slash.obj 模型。

同时替换 OBJ 模型和纹理

import cn.star.a_belated_gift.api.client.SlashEffectRenderDefinition;
import cn.star.a_belated_gift.api.client.SlashEffectRenderRegistry;

private static final SlashEffectRenderDefinition SPECIAL_SLASH =
        new SlashEffectRenderDefinition(
                new ResourceLocation("example", "model/util/special_slash.obj"),
                new ResourceLocation("example", "model/util/special_slash.png")
        );

private static void onClientSetup(FMLClientSetupEvent event) {
    event.enqueueWork(() -> SlashEffectRenderRegistry.register(
            new ResourceLocation("example", "special_blade_slash"),
            100,
            effect -> effect.getOwner() instanceof Player player
                    && player.getMainHandItem().is(EXAMPLE_BLADE.get()),
            SPECIAL_SLASH
    ));
}

此 register 重载会自动将定义的 OBJ 和纹理添加到预热列表中。

动态提供器

当定义需要根据特效状态动态选择时,实现 SlashEffectRenderProvider:

import cn.star.a_belated_gift.api.client.SlashEffectRenderDefinition;
import cn.star.a_belated_gift.api.client.SlashEffectRenderProvider;
import mods.flammpfeil.slashblade.entity.EntitySlashEffect;

import java.util.Collection;
import java.util.List;

public final class ExampleSlashProvider implements SlashEffectRenderProvider {
    private static final SlashEffectRenderDefinition BLUE =
            SlashEffectRenderDefinition.withTexture(
                    new ResourceLocation("example", "model/util/blue_slash.png"));
    private static final SlashEffectRenderDefinition RED =
            SlashEffectRenderDefinition.withTexture(
                    new ResourceLocation("example", "model/util/red_slash.png"));

    @Override
    public SlashEffectRenderDefinition resolve(EntitySlashEffect effect) {
        if (!(effect.getOwner() instanceof Player player)
                || !player.getMainHandItem().is(EXAMPLE_BLADE.get())) {
            return null;
        }
        return effect.getIsCritical() ? RED : BLUE;
    }

    @Override
    public Collection<SlashEffectRenderDefinition> warmupDefinitions() {
        return List.of(BLUE, RED);
    }
}
event.enqueueWork(() -> SlashEffectRenderRegistry.register(
        new ResourceLocation("example", "dynamic_slash"),
        200,
        new ExampleSlashProvider()
));

resolve 返回 null 表示当前规则不匹配。注册表随后会检查下一个提供器。可能返回的所有资源都应通过 warmupDefinitions 声明。

匹配顺序

  • 数值优先级更高的规则优先被评估。
  • 相同优先级的规则按注册 ID 排序,确保结果不受模组加载顺序影响。
  • 第一个返回非 null 定义的提供器胜出。
  • 当没有规则匹配时,使用拔刀剑默认的模型和纹理。
  • 重复注册相同 ID 会替换之前的规则。
  • 提供器异常会被隔离并记录日志,允许其余规则继续匹配。

注销规则

boolean removed = SlashEffectRenderRegistry.unregister(
        new ResourceLocation("example", "special_blade_slash")
);

注销仅影响后续的斩击效果选择。已预热的资源由 Minecraft 的资源生命周期管理。