Skip to content

着色器 API

This content is not available in your language yet.

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。

属性类型默认值说明
shaderShaderSource{ type: 'builtin', name: 'crossfade' }着色器来源,可以是内建效果,也可以是自定义 raw WGSL
timeControl"auto" | "manual" | "transition""auto"时间推进方式
displayChannelnumber | nullnull稳定状态下直接显示的 channel;做转场时通常设为目标 channel
onFinished() => voidtransition 模式完成时触发

timeControl 的含义如下:

  • auto:节点挂载后自动推进时间,适合持续播放的普通 shader。
  • manual:初始暂停,通过命令手动开始、停止和重置时间。
  • transition:进入转场状态机,由 prepare / perform 驱动 progress,并在完成后触发 finished 事件。
属性类型默认值说明
channelnumber0输入 channel 编号,范围 0..3
emptybooleanfalse是否声明为空输入;为空时不会渲染子节点
staticbooleanfalse将输入视为静态内容,作为减少重复 render-to-texture 的优化手段
spaceShaderSlotSpace'normal'控制子内容使用普通舞台坐标还是 shader 局部坐标;设为 shader 时,slot 左上角为 (0, 0)
widthnumber0empty={true} 时的纹理宽度,必须为非零值
heightnumber0empty={true} 时的纹理高度,必须为非零值

shader-slot 通常直接作为 <shader> 的子节点使用。对于 mask 这类需要额外规则图的效果,常见做法是把规则图放在 channel={2},并配合 space="shader"

内建效果写法如下:

<shader
shader={{ type: 'builtin', name: 'wipe', direction: 'left', softness: 0.08 }}
timeControl="transition"
displayChannel={1}
/>

当前内建 name 包括 crossfadewipefadepushslideawayzoompixellatemask。它们的参数结构与 @transPerform@bgTransEffect@charTransEffect 完全一致。

当使用内建转场约定时,通常会使用以下 channel:

  • channel 0:旧画面(from)
  • channel 1:新画面(to)
  • channel 2mask 效果的规则图

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>

params 仅在 type: 'raw' 时可用。每一项都是一个 4 字节槽位,类型只支持 floatint。当前总共提供 32 个槽位,也就是 128 字节。

引擎会提供顶点着色器,因此你只需要编写片元模块,并导出 fs_main

binding内容
@group(1) @binding(0)render_uniform
@group(1) @binding(1)builtins
@group(1) @binding(2)params
@group(1) @binding(3)texture_sampler
@group(1) @binding(4)channel0
@group(1) @binding(5)channel1
@group(1) @binding(6)channel2
@group(1) @binding(7)channel3

推荐直接沿用下面这组声明:

struct RenderUniform {
position: vec2<f32>,
size: vec2<f32>,
}
struct BuiltinsUniform {
time: f32,
time_delta: f32,
progress: f32,
effect_id: i32,
frame: u32,
channel_count: u32,
stage_size: vec2<f32>,
}
/// 可以使用内存对齐的任意结构
struct ParamsUniform {
slots: array<vec4<u32>, 8>,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(1) @binding(0)
var<uniform> render_uniform: RenderUniform;
@group(1) @binding(1)
var<uniform> builtins: BuiltinsUniform;
@group(1) @binding(2)
var<uniform> params: ParamsUniform;
@group(1) @binding(3)
var texture_sampler: sampler;
@group(1) @binding(4)
var channel0: texture_2d<f32>;
@group(1) @binding(5)
var channel1: texture_2d<f32>;
@group(1) @binding(6)
var channel2: texture_2d<f32>;
@group(1) @binding(7)
var channel3: texture_2d<f32>;

BuiltinsUniform 中各字段的含义如下:

字段说明
time已播放时间(秒)。auto / manual 模式下来自普通时间轴;transition 模式下来自当前转场时间轴
time_delta与上一帧的时间差(秒)
progress转场进度,范围 0~1;非 transition 模式下恒为 0
effect_id内建效果编号;raw 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(...) 接收命令。

为一次转场准备 from/to 输入,但不会立刻开始播放。

shaderRef.current?.executeCommand({
subCommand: 'prepare',
fromChannel: 0,
toChannel: 1,
mode: 'static',
});
参数类型默认值说明
fromChannelnumber旧画面 channel,范围 0..3
toChannelnumber新画面 channel,范围 0..3
mode"static" | "live"沿用当前保留模式旧画面保留方式

fromChanneltoChannel 必须不同,且都必须是已声明、非空的 slot。

开始播放一次已准备好的转场。

shaderRef.current?.executeCommand({
subCommand: 'perform',
duration: 800,
});
参数类型说明
durationnumber转场时长(毫秒)

这三个命令用于控制 auto / manual 模式下的普通时间轴:

shaderRef.current?.executeCommand({ subCommand: 'start' });
shaderRef.current?.executeCommand({ subCommand: 'stop' });
shaderRef.current?.executeCommand({ subCommand: 'reset' });
  • start:开始或恢复时间推进。
  • stop:暂停时间推进。
  • reset:把时间轴重置到 0

当前 shader 节点暴露一个事件:

仅在 transition 模式下触发,表示这次转场已经结束并回到稳定状态。React 层通常直接使用 onFinished

<shader
timeControl="transition"
onFinished={() => {
console.log('transition done');
}}
/>

下面的例子使用内建 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>
);
}

下面的例子展示一个持续播放的波纹色偏效果。它使用 channel0 作为输入,并通过 params 传入两个浮点参数:

const fragmentWgsl = `
struct RenderUniform {
position: vec2<f32>,
size: vec2<f32>,
}
struct BuiltinsUniform {
time: f32,
time_delta: f32,
progress: f32,
effect_id: i32,
frame: u32,
channel_count: u32,
stage_size: vec2<f32>,
}
struct ParamsUniform {
slots: array<vec4<u32>, 8>,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(1) @binding(0)
var<uniform> render_uniform: RenderUniform;
@group(1) @binding(1)
var<uniform> builtins: BuiltinsUniform;
@group(1) @binding(2)
var<uniform> params: ParamsUniform;
@group(1) @binding(3)
var texture_sampler: sampler;
@group(1) @binding(4)
var channel0: texture_2d<f32>;
@group(1) @binding(5)
var channel1: texture_2d<f32>;
@group(1) @binding(6)
var channel2: texture_2d<f32>;
@group(1) @binding(7)
var channel3: texture_2d<f32>;
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));
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
let color = textureSample(channel0, texture_sampler, 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>