着色器 API
shader 节点用于把多个输入通道交给一个 shader pass 统一处理。它既可以承载内建转场效果,也可以直接运行自定义 WGSL 片元着色器。shader-slot 则负责为它提供各个输入 channel。
<shader shader={{ type: 'builtin', name: 'crossfade' }} timeControl="transition" displayChannel={1}> <shader-slot channel={0}> <sprite src="bg/day.png" /> </shader-slot> <shader-slot channel={1}> <sprite src="bg/night.png" /> </shader-slot></shader>当前固定支持 0..3 四个 channel。未占用的 channel 会自动填充 dummy texture。
<shader> 属性
Section titled “<shader> 属性”| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
width |
number |
自动 | 显式布局宽度;未设置时按 normal、非空 slot 的内容宽度计算 |
height |
number |
自动 | 显式布局高度;未设置时按 normal、非空 slot 的内容高度计算 |
shader |
ShaderSource |
{ type: 'builtin', name: 'crossfade' } |
着色器来源:内建效果、raw WGSL 或 WGSL 资源文件 |
timeControl |
"auto" | "manual" | "transition" |
"auto" |
时间推进方式 |
displayChannel |
number | null |
null |
稳定状态下直接显示的 channel;做转场时通常设为目标 channel |
onPrepared |
() => void |
— | transition 模式下,prepare 完成捕获并进入可执行状态时触发 |
onFinished |
() => void |
— | transition 模式完成时触发 |
timeControl 的含义如下:
auto:节点挂载后自动推进时间,适合持续播放的普通 shader。manual:初始暂停,通过命令手动开始、停止和重置时间。transition:进入转场状态机,由prepare/perform驱动progress,并在完成后触发finished事件。
shader 默认汇总直属 shader-slot 的内容尺寸:只计算 space="normal" 且 empty={false} 的 slot,并分别取宽、高的最大值。space="shader" 和 empty slot 不贡献自动尺寸。
设置 width 或 height 后,对应轴改为显式尺寸,另一个未设置的轴仍按内容自动计算。显式尺寸适合没有 normal 内容的程序化 shader,或需要在布局容器中预留固定空间的场景。
<shader-slot> 属性
Section titled “<shader-slot> 属性”| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
channel |
number |
0 |
输入 channel 编号,范围 0..3 |
empty |
boolean |
false |
是否声明为空输入;为空时不会渲染子节点 |
static |
boolean |
false |
将输入视为静态内容,作为减少重复 render-to-texture 的优化手段 |
space |
ShaderSlotSpace |
'normal' |
控制输入的内容原点;normal 使用 shader 效果区域左上角,shader 使用 slot 自身的局部原点 |
width |
number |
0 |
empty={true} 时的纹理宽度,必须为非零值 |
height |
number |
0 |
empty={true} 时的纹理高度,必须为非零值 |
shader-slot 通常直接作为 <shader> 的子节点使用。对于 mask 这类需要额外规则图的效果,常见做法是把规则图放在 channel={2},并配合 space="shader"。
shader-slot 的布局尺寸固定为 0 × 0,不参与普通父级测量,也不能作为 <vbox> 或 <hbox> 的直接布局子节点。这个尺寸不影响它的离屏输入纹理:normal、非空 slot 会单独测量其直属子内容,供父 <shader> 自动计算尺寸;empty slot 的 width / height 只用于分配空输入纹理。
由于 slot 本身为 0 × 0,其直接子节点的 anchor 以 0 × 0 为基准。更深层子节点仍根据各自父节点的实际布局尺寸计算 anchor。
ShaderSource
Section titled “ShaderSource”内建效果写法如下:
<shader shader={{ type: 'builtin', name: 'wipe', direction: 'left', softness: 0.08 }} timeControl="transition" displayChannel={1}/>当前内建 name 包括 crossfade、wipe、fade、push、slideaway、zoom、pixellate 和 mask。它们的参数结构与 @transPerform、@bgTransEffect 和 @charTransEffect 完全一致。
当使用内建转场约定时,通常会使用以下 channel:
channel 0:旧画面(from)channel 1:新画面(to)channel 2:mask效果的规则图
自定义 raw WGSL
Section titled “自定义 raw WGSL”raw 模式要求传入完整的片元 WGSL 模块,而不是单个函数片段:
<shader shader={{ type: 'raw', content: fragmentWgsl, params: [ { name: 'strength', type: 'float', value: 0.35 }, { name: 'speed', type: 'float', value: 1.2 }, ], }} timeControl="auto" displayChannel={0}> <shader-slot channel={0}> <sprite src="bg/classroom.png" /> </shader-slot></shader>自定义 WGSL 文件
Section titled “自定义 WGSL 文件”file 模式从 assets/ 下异步加载 UTF-8 WGSL 文件,路径相对于资源目录:
<shader shader={{ type: 'file', src: 'shaders/wave.wgsl', params: [ { name: 'strength', type: 'float', value: 0.35 }, { name: 'speed', type: 'float', value: 1.2 }, ], }} timeControl="auto" displayChannel={0}> <shader-slot channel={0}> <sprite src="bg/classroom.png" /> </shader-slot></shader>文件内容与 raw.content 遵循相同的 WGSL 约定。
params 可用于 raw 和 file。每一项都是一个 4 字节槽位,类型只支持 float 和 int。当前总共提供 32 个槽位,也就是 128 字节。
自定义 WGSL 约定
Section titled “自定义 WGSL 约定”引擎会提供顶点着色器,因此你只需要编写片元模块,并导出 fs_main。raw 和 file shader 使用相同的运行时符号。
引擎自动注入的内容
Section titled “引擎自动注入的内容”以下类型、全局变量和 helper 会在运行时自动注入到你的 WGSL 模块前面:
| 名称 | 等价声明 | 用途 |
|---|---|---|
RenderUniform |
struct RenderUniform { position: vec2<f32>, size: vec2<f32> } |
当前 shader rect 在舞台逻辑坐标中的位置和尺寸 |
BuiltinsUniform |
struct BuiltinsUniform { time: f32, time_delta: f32, progress: f32, effect_id: i32, frame: u32, channel_count: u32, stage_size: vec2<f32> } |
引擎内建时间、进度和舞台信息 |
HiddenUniform |
内部辅助结构 | 引擎内部使用的采样修正信息,不建议依赖 |
render_uniform |
@group(1) @binding(0) var<uniform> render_uniform: RenderUniform; |
读取当前 shader rect 的逻辑坐标与尺寸 |
moyu_hidden_uniform_internal |
@group(1) @binding(1) var<uniform> moyu_hidden_uniform_internal: HiddenUniform; |
采样范围修正的内部 uniform,不属于稳定 API |
builtins |
@group(1) @binding(2) var<uniform> builtins: BuiltinsUniform; |
读取内建时间、progress、舞台尺寸等 |
texture_sampler |
@group(1) @binding(4) var texture_sampler: sampler; |
通用采样器 |
channel0..3 |
@group(1) @binding(5..8) var channelN: texture_2d<f32>; |
四个输入通道纹理 |
sampleChannel0..3(uv) |
fn sampleChannelN(uv: vec2<f32>) -> vec4<f32> |
对对应通道进行带采样范围修正的常规采样 |
这意味着在自定义 shader 中,你不需要也不应该重复声明 render_uniform、builtins、texture_sampler、channel0..3 以及 sampleChannel0..3()。重复声明会与引擎注入版本重名。
对于 channel0..3 的常规采样,应当优先使用 sampleChannel0..3(uv),而不是直接写 textureSample(channelN, texture_sampler, input.uv)。helper 会自动处理 render-to-texture 带来的采样范围修正,同时保持 input.uv 继续表示当前 shader rect 内的逻辑归一化坐标 0..1。
可以把两者的职责简单理解为:
input.uv:当前 shader rect 内的逻辑归一化坐标,始终是0..1sampleChannel0..3(uv):在保持uv语义不变的前提下,采样对应输入 channel 的正确内容区域
当你需要 textureDimensions(channel0)、textureLoad(...) 这类更底层的纹理操作时,仍然可以直接访问 channel0..3 和 texture_sampler。helper 只是统一提供这些符号,不会限制自定义 shader 的能力。
你需要自己声明的内容
Section titled “你需要自己声明的内容”| 项目 | 是否必须 | 说明 |
|---|---|---|
VertexOutput |
是 | 片元入口参数类型,需要与引擎顶点着色器输出匹配 |
fs_main |
是 | 片元入口函数 |
ParamsUniform |
仅在读取 params 时需要 |
由你自己定义内存布局 |
params 变量 |
仅在读取 params 时需要 |
固定使用 @group(1) @binding(3) |
一个最小的自定义 WGSL 模板通常如下:
struct ParamsUniform { slots: array<vec4<u32>, 8>,}
struct VertexOutput { @builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(1) @binding(3)var<uniform> params: ParamsUniform;BuiltinsUniform 中各字段的含义如下:
| 字段 | 说明 |
|---|---|
time |
已播放时间(秒)。auto / manual 模式下来自普通时间轴;transition 模式下来自当前转场时间轴 |
time_delta |
与上一帧的时间差(秒) |
progress |
转场进度,范围 0~1;非 transition 模式下恒为 0 |
effect_id |
内建效果编号;raw 和 file shader 下固定为 -1 |
frame |
当前节点的局部帧计数 |
channel_count |
当前已声明的 channel 数量 |
stage_size |
逻辑舞台尺寸 |
关于 params,有两点需要注意:
- 引擎只是把
32个 4 字节槽位原样写入这块 uniform 内存,不会按名字做查找或重排。 name仅用于用户侧语义和报错信息,真正的布局完全由数组顺序决定。
如果你不想自己处理 WGSL uniform 对齐,最稳妥的方式就是像内建 shader 一样使用 slots: array<vec4<u32>, 8>,然后在 shader 里自行解包:
fn read_param_u32(index: u32) -> u32 { let lane = params.slots[index / 4u]; switch (index % 4u) { case 0u: { return lane.x; } case 1u: { return lane.y; } case 2u: { return lane.z; } default: { return lane.w; } }}
fn read_param_f32(index: u32) -> f32 { return bitcast<f32>(read_param_u32(index));}shader 节点通过 ref.current?.executeCommand(...) 接收命令。
prepare
Section titled “prepare”为一次转场准备 from/to 输入,但不会立刻开始播放。
shaderRef.current?.executeCommand({ subCommand: 'prepare', fromChannel: 0, toChannel: 1, mode: 'static',});| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fromChannel |
number |
— | 旧画面 channel,范围 0..3 |
toChannel |
number |
— | 新画面 channel,范围 0..3 |
mode |
"static" | "live" |
沿用当前保留模式 | 旧画面保留方式 |
fromChannel 和 toChannel 必须不同,且都必须是已声明、非空的 slot。
perform
Section titled “perform”开始播放一次已准备好的转场。
shaderRef.current?.executeCommand({ subCommand: 'perform', duration: 800,});| 参数 | 类型 | 说明 |
|---|---|---|
duration |
number |
转场时长(毫秒) |
start / stop / reset
Section titled “start / stop / reset”这三个命令用于控制 auto / manual 模式下的普通时间轴:
shaderRef.current?.executeCommand({ subCommand: 'start' });shaderRef.current?.executeCommand({ subCommand: 'stop' });shaderRef.current?.executeCommand({ subCommand: 'reset' });start:开始或恢复时间推进。stop:暂停时间推进。reset:把时间轴重置到0。
当前 shader 节点暴露两个事件:
prepared
Section titled “prepared”仅在 transition 模式下触发,表示这次 prepare 已经完成,from 输入已经捕获完毕,可以安全执行 perform。React 层通常直接使用 onPrepared:
<shader timeControl="transition" onPrepared={() => { console.log('prepared'); }}/>finished
Section titled “finished”仅在 transition 模式下触发,表示这次转场已经结束并回到稳定状态。React 层通常直接使用 onFinished:
<shader timeControl="transition" onFinished={() => { console.log('transition done'); }}/>1. 内建 transition
Section titled “1. 内建 transition”下面的例子使用内建 wipe 效果,在两张背景图之间执行一次转场:
import { useLayoutEffect, useRef } from 'react';import type { Node } from '@momoyu-ink/kit';
function WipeTransition({ trigger }: { trigger: number }) { const shaderRef = useRef<Node>(null);
useLayoutEffect(() => { shaderRef.current?.executeCommand({ subCommand: 'prepare', fromChannel: 0, toChannel: 1, mode: 'static', }); shaderRef.current?.executeCommand({ subCommand: 'perform', duration: 900, }); }, [trigger]);
return ( <shader ref={shaderRef} shader={{ type: 'builtin', name: 'wipe', direction: 'left', softness: 0.08 }} timeControl="transition" displayChannel={1} onFinished={() => console.log('finished')} > <shader-slot channel={0}> <sprite src="bg/day.png" /> </shader-slot> <shader-slot channel={1}> <sprite src="bg/night.png" /> </shader-slot> </shader> );}2. 普通 raw shader
Section titled “2. 普通 raw shader”下面的例子展示一个持续播放的波纹色偏效果。它使用 channel0 作为输入,并通过 params 传入两个浮点参数:
const fragmentWgsl = `struct ParamsUniform { slots: array<vec4<u32>, 8>,}
struct VertexOutput { @builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(1) @binding(3)var<uniform> params: ParamsUniform;
fn read_param_u32(index: u32) -> u32 { let lane = params.slots[index / 4u]; switch (index % 4u) { case 0u: { return lane.x; } case 1u: { return lane.y; } case 2u: { return lane.z; } default: { return lane.w; } }}
fn read_param_f32(index: u32) -> f32 { return bitcast<f32>(read_param_u32(index));}
@fragmentfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { let color = sampleChannel0(input.uv); let strength = read_param_f32(0u); let speed = read_param_f32(1u); let wave = 0.5 + 0.5 * sin((input.uv.y * 8.0 + builtins.time * speed) * 6.28318); let rgb = mix(color.rgb, color.rgb * wave, strength); return vec4<f32>(rgb, color.a);}`;
<shader shader={{ type: 'raw', content: fragmentWgsl, params: [ { name: 'strength', type: 'float', value: 0.35 }, { name: 'speed', type: 'float', value: 1.2 }, ], }} timeControl="auto" displayChannel={0}> <shader-slot channel={0}> <sprite src="bg/classroom.png" /> </shader-slot></shader>
