◆ 星遥社区 上传日志,AI协助分析!还有专人解答!快来试试吧! 立即体验
头图

拔刀剑:迟来的礼物

简介

📖 这是什么?

这是一个面向 SlashBlade: Resharped 的客户端渲染优化模组,专门解决拔刀剑在持刀、挥刀、落地刀、光影环境和 YSM 模型下掉帧严重的问题。

一句话总结:持刀翻倍,挥刀翻三倍,落地刀翻十几倍。


📊 帧数对比

模组版本 1.0.2

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

第二人称视角,BSL 光影,炼金王国

无光影

场景原版加速渲染迟来的礼物提升(vs 原版)
持刀359412810+126%
挥刀220248620+182%
十七把高模刀落地61425730+1097%

开启光影

场景原版加速渲染迟来的礼物提升(vs 原版)
持刀208330381+83%
挥刀115153365+217%
十七把高模刀落地29240358+1134%

兼容 YSM 模型

场景原版加速渲染迟来的礼物提升(vs 原版)
无光影 · 持刀374398724+94%
无光影 · 挥刀208362605+191%
光影 · 持刀215301374+74%
光影 · 挥刀172261345+101%

⚙️ 怎么做到的?

刀模不再"每帧重画"

原版每次渲染都要遍历 OBJ 模型的所有分组、面和顶点。本模组在模型加载时就把数据"烘焙"成 GPU 可直接读取的格式,之后每帧直接复用。

静态 VBO 缓存

刀身、刀鞘、物品图标、刀架上的刀、落地刀共用 96 MiB LRU 缓存。同一把刀第二次出现时,直接复用上次上传到显存的数据,不再重新提交顶点。

附魔闪光也有独立的静态缓存,不再额外拖慢帧率。

光影下"聪明地省"

  • 阴影阶段用 32 段的简化刀模代替完整高模
  • 刀光、次元斩等透明特效不进入阴影贴图阶段
  • 发光刀身不重复投射阴影

挥刀动画"只算必要的"

原版挥刀会计算完整 PMD 模型的逐顶点蒙皮,但实际只用了两个骨骼挂点的位置。本模组跳过无用的顶点计算,只保留骨骼姿态。

刀光特效按距离降级

近距离全细节,中距离减少层数,远距离只保留主体,光影模式下进一步简化。

首次不卡顿

在进入游戏前就预热好模型、纹理和 RenderType,第一次挥刀或打开物品栏不再突发卡顿。

资源自动清理

退出世界或重载资源包时自动清理缓存,不会越玩越卡。

保留兼容与安全回退

保留 RenderOverrideEvent,遇到动态 UV、动态透明度或无法缓存的网格时自动回退原版渲染路径,不会崩溃。


⚠️ 不兼容

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


下载方式

CurseForge: https://www.curseforge.com/minecraft/mc-mods/slashblade-a-belated-gift


刀光渲染接口

该优化模组会导致其他附属通过 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 会继续使用 SlashBlade 默认的 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
    ));
}

这个注册重载会自动把定义中的 OBJ 和纹理加入预热列表。

动态 Provider

需要根据实体状态动态选择多个刀光定义时,实现 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 定义的 Provider 生效。
  • 没有规则匹配时使用 SlashBlade 默认模型和纹理。
  • 使用相同注册 ID 再次注册会替换旧规则。
  • Provider 抛出的异常会被隔离并记录,其他规则仍可继续匹配。

注销

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

注销仅影响后续刀光选择,已经预热的资源会由 Minecraft 的资源生命周期管理。