Theseus 模组文档
开发指南

渲染与视觉扩展


模组的所有自绘入口都是中立的:只面对 RenderCanvas 与 RenderContext,不接触任何平台图形 API(宿主内部以具体实现接入,颜色表示与转换由宿主处理)。

6.1 渲染扩展点的注册方式

三类渲染扩展点全部走 ctx.registries()(ModRegistries),统一使用 Registry#register(RegistryKey, 内容实例):

不再有 per-模组的 registerXxx(registry) 回调(GameplayMod 上已无这类方法)。注册必须写在注册窗口内(onPreInit):宿主在全部模组的 onPreInit 结束后立即冻结注册表,此后再注册抛 RegistryFrozenException;同一 ResourceLocation 重复注册抛 IllegalArgumentException。

扩展点注册方式执行顺序规则返回 null 的语义
渲染滤镜ctx.registries().renderFilters()按 zOrder() 升序执行(数字小的先执行、越靠下),层号相同按注册顺序不适用(apply(RenderCanvas, FilterFrame) 无返回值)
背景渲染器ctx.registries().backgroundRenderers()按 zOrder() 升序执行(数字小的先绘制、越靠下),层号相同按注册顺序不适用(render(RenderCanvas, FilterFrame) 无返回值)
打击特效样式ctx.registries().hitEffectStyles()按注册顺序遍历 provider,所有返回非 null 的样式同时生效、依次绘制表示该 provider 不参与本次打击;全部 provider 均返回 null 时使用游戏默认文字特效

补充说明:

6.2 绘制入口 RenderCanvas

java
void fillRect(double x, double y, double w, double h);
void drawRect(double x, double y, double w, double h);
void drawLine(double x1, double y1, double x2, double y2);
void fillTriangle(double x1, double y1, double x2, double y2, double x3, double y3);
void setColor(Color color);
void setAlpha(double alpha);
void clip(double x, double y, double w, double h);
void save();
void restore();
void translate(double dx, double dy);
void scale(double sx, double sy);
void rotate(double angle, double pivotX, double pivotY);
void drawText(String text, double x, double y, TextStyle style);
TextMetrics measureText(String text, TextStyle style);
void drawImage(RenderImage image, double x, double y, double w, double h);

关键约定:

6.3 RenderContext 提供什么

一次渲染回调的上下文,模组从渲染事件里拿到它(event.context()):

java
int width();
int height();
double partialTick();
RenderLayer layer();
MatrixStack matrices();
RenderCanvas canvas();
void drawTexture(RenderImage image, float x, float y, float w, float h,
                 float u0, float , float u1, float );
AdvancedRenderAccess advanced();

6.4 七个 RenderLayer

RenderLayer 是引擎内置渲染阶段枚举,自底向上共 7 层,每层对应固定数字编号(index())。编号越大越靠上。宿主在每一层的前后各投递一次 RenderEvent.Pre / RenderEvent.Post(共 7 层,每帧 14 次),模组据此把自绘内容插到引擎各层之间。

枚举常量index()用途
BACKGROUND0背景层——模组背景渲染器(曲绘 / 纯色等),位于游玩内容之下
WORLD1轨道容器层——模组轨道样式、Z 分层轨道、HOLD 长条装饰;模组在此绘制“轨道之下、背景之上”的内容(如自定义轨道底板)
NOTES2轨道与音符层——真实轨道 / 音符、假轨与假音符、屏障指示
JUDGE_FX3判定特效层——打击特效(含模组的 HitEffectStyle)
HUD4原版 HUD 层——combo、快捷键框、进度条、分数 / 准度 / 血条、模组 HUD 覆盖
OVERLAY5覆盖渲染层——altHud、暂停对话框与花屏、模组 renderOverlay、入场黑幕 / 读秒遮罩
FULLSCREEN6全屏覆盖层(最顶层)——后处理滤镜(模组的 RenderFilter)

6.5 示例:注册一个简单渲染滤镜(描边 / 边框)

java
package com.example.bordermod;

import com.seecmd.mod.api.GameplayMod;
import com.seecmd.mod.api.ModContext;
import com.seecmd.mod.api.ModInfo;
import com.seecmd.mod.api.registry.RegistryKey;
import com.seecmd.mod.api.render.FilterFrame;
import com.seecmd.mod.api.render.RenderCanvas;
import com.seecmd.mod.api.render.RenderFilter;

import java.awt.Color;
import java.util.List;

/**
 * 渲染滤镜示例:在所有游玩内容之上叠加一圈边框。
 *
 * <p>滤镜固定落在 {@link com.seecmd.mod.api.render.RenderLayer#FULLSCREEN}(第 6 层,最顶层),
 * zOrder 只决定同一类回调(其它滤镜)之间的相对叠放。
 */
public final class BorderExampleMod implements GameplayMod {

    private static final String MOD_ID = "example_border";

    @Override
    public ModInfo info() {
        return ModInfo.of(MOD_ID, "Border Example", "1.0.0", List.of("YourName"),
                "演示注册一个在游玩内容之上叠加边框的渲染滤镜");
    }

    @Override
    public void onPreInit(ModContext ctx) {
        // 注册窗口内注册:重复注册同一 location 会立刻失败,
        // 注册表冻结后(onInit 及之后)再注册会抛 RegistryFrozenException。
        ctx.registries().renderFilters().register(
                RegistryKey.of(MOD_ID, "border", RenderFilter.class),
                new BorderFilter());
    }

    /** 4px 彩色边框滤镜。 */
    private static final class BorderFilter implements RenderFilter {

        private static final int BORDER = 4;

        @Override
        public int zOrder() {
            return 10; // 数字越大越靠上
        }

        @Override
        public void apply(RenderCanvas canvas, FilterFrame frame) {
            double w = frame.width();
            double h = frame.height();

            // 自绘前先 save():宿主会在回调结束后强制恢复状态,
            // 但这不影响插槽内部的状态污染,配对自己收尾更稳妥。
            canvas.save();

            canvas.setAlpha(1.0);

            canvas.setColor(new Color(255, 0, 0, 180));   // 上
            canvas.fillRect(0, 0, w, BORDER);
            canvas.setColor(new Color(0, 0, 255, 180));   // 下
            canvas.fillRect(0, h - BORDER, w, BORDER);
            canvas.setColor(new Color(0, 255, 0, 180));   // 左
            canvas.fillRect(0, 0, BORDER, h);
            canvas.setColor(new Color(255, 255, 0, 180)); // 右
            canvas.fillRect(w - BORDER, 0, BORDER, h);

            // 内层半透明描边:裁剪必须包在一对 save/restore 之间,结束后裁剪自动失效。
            canvas.save();
            canvas.clip(20, 20, w - 40, h - 40);
            canvas.setAlpha(0.5);
            canvas.setColor(Color.WHITE);
            canvas.drawRect(20, 20, w - 40, h - 40);
            canvas.restore();

            canvas.restore();
        }
    }
}

6.6 HUD 自绘的位置

模组若要隐藏并自绘原版 HUD 组件(分数 / 连击 / 准度 / HP 条),应提供 GameplayHud 能力(经 GameplayMod#createGameplayHud() 返回,或经 ModContext#capabilities().register(GameplayHud.class, ...) 注册):

GameplayHud、HudContext、HudValues 的完整签名与语义见「能力接口」与「只读视图」相关章节。

6.7 场景实体:挂到世界层级 + 查询世界矩阵自绘(查询面)

模组可以把自有实体挂进宿主的世界层级(轨道区 / 某条轨道 / 自己的另一个实体之下), 每帧写入本地位姿,再查询世界矩阵来摆放自绘内容——这就是「模组可见层级 API」 (2026-10-16,统一世界二期,查询面)。入口全部在 GameStateAccess: createEntity / mountToTrackArea / mountToTrack / mountToEntity / entityPose / entityWorldMatrix / entityVisible / setEntityVisible / entityAlpha / setEntityAlpha / destroyEntity;类型为 render.SceneEntity(不透明句柄)与 render.EntityPose (本 tick 位姿写入面)。

最小流程(三步):

java
// ① 创建 + 挂载(挂载窗口 = 非 tick 上下文,如 GameStartEvent 回调)
ctx.eventBus().subscribe(GameStartEvent.class, e -> {
    GameStateAccess game = ctx.capabilities().get(GameStateAccess.class);
    if (game == null) return;                       // 能力仅在游玩中存在,取用前判空
    this.marker = game.createEntity("marker");      // 实体归本模组所有(未挂载)
    game.mountToTrack(this.marker, 1);              // 挂到轨道 1(也可 mountToTrackArea)
});

// ② 每 tick 写本地位姿(GameplayTickEvent 回调内——唯一合法窗口;未写 = 恒等、帧间不累积)
ctx.eventBus().subscribe(GameplayTickEvent.Post.class, e -> {
    GameStateAccess game = ctx.capabilities().get(GameStateAccess.class);
    if (game == null || this.marker == null) return;
    com.seecmd.mod.api.render.EntityPose pose = game.entityPose(this.marker);
    if (pose == null) return;                       // 窗口外 / 句柄失效 → null(限频告警)
    pose.setOffsetX(120);                           // 标量平移(实体本地坐标,可写)
    pose.translate(0, 40);                          // 标量叠加(等价 offsetY += 40)
    pose.rotateZ(e.musicTimeMs() * 0.09, 0, 0);     // op 追加(保序;禁随机/禁墙钟——只用 τ)
});

// ③ 查询世界矩阵 → 经既有渲染扩展点自绘(查询面:宿主不替你渲染实体,画什么、怎么画由你决定)
//    在你的 RenderEvent / RenderCanvas 回调里:
double[] m = game.entityWorldMatrix(this.marker);   // 列主序 double[16](防御性拷贝,可留存)
//    世界坐标 = m 作用于本地点:wx = m[0]*x + m[4]*y + m[12],wy = m[1]*x + m[5]*y + m[13]

必须遵守的契约:

完整签名与契约见 API 参考 的「SceneEntity 与 EntityPose(render 包)」与 GameStateAccess「模组可见层级」段。