变换反馈
变换反馈运行顶点着色器,并将输出捕获到顶点缓冲区,而不进行光栅化。结果留在 GPU 上,模拟状态无需传回 CPU,写入的缓冲区可直接用于后续渲染通道。
它适合每个元素独立处理,且输出几何体本来就将由 GPU 绘制的模拟。
平台支持
变换反馈仅受 WebGL2 支持。配置前先检查支持情况:
if (app.graphicsDevice.isWebGL2) {
// set up transform feedback
}
WebGPU 没有对应功能,相同工作负载通过计算着色器写入存储缓冲区实现,因此面向两个 API 的代码需要分别实现两条路径。
用途
- GPU 粒子:无需 CPU 参与,为大量粒子积分位置和速度。
- 群集和人群:逐个体计算转向,将结果变换用于实例化渲染。
- 布料和软体:在顶点网格上进行 Verlet 风格积分。
- 拖尾和飘带:每帧推进位置历史。
实时示例
- 变换反馈:在单个交错缓冲区中模拟粒子云。
- 分离式变换反馈:为每个捕获的 varying 使用独立缓冲区,模拟 GPU 群集,并驱动实例化圆锥。
单个交错缓冲区
默认所有捕获的 varying 打包到一个缓冲区中。这是最简单的形式,适合每个元素用单个结构体表示状态的模拟。
第 1 步:编写模拟着色器
着色器将当前状态作为顶点属性读取,将新状态写入 varying。名称以 out_ 开头的 varying 会被自动捕获:
attribute vec4 vertex_position; // xyz = position, w = lifetime
varying vec4 out_vertex_position;
uniform float deltaTime;
void main(void) {
vec3 pos = vertex_position.xyz;
float life = vertex_position.w - deltaTime;
pos.y += deltaTime;
out_vertex_position = vec4(pos, life);
}
注意,状态被打包为单个 vec4,因为交错捕获会将全部内容写入同一缓冲区。渲染通道因此读取相同布局,并跳过不需要的部分。
第 2 步:创建着色器和辅助对象
const shader = pc.TransformFeedback.createShader(device, vertexCode, 'moveParticles');
// creates a matching output buffer internally
const tf = new pc.TransformFeedback(mesh.vertexBuffer);
使用 BUFFER_GPUDYNAMIC 创建输入缓冲区,表示 GPU 会反复写入它。
第 3 步:逐帧运行
app.on('update', (dt) => {
deltaTimeUniform.setValue(dt);
tf.process(shader);
});
process() 在禁用光栅化的情况下运行着色器,然后交换输入和输出缓冲区。交换的是底层 GPU 缓冲区,而非 VertexBuffer 对象,因此传入的引用始终持有最新数据。从该缓冲区渲染的网格无需更新,会自动获取新内容。
每个 varying 一个缓冲区
创建着色器时传入 TRANSFORM_FEEDBACK_SEPARATE,可将每个捕获的 varying 写入独立缓冲区:
const shader = pc.TransformFeedback.createShader(
device, vertexCode, 'flockSim',
['out_position', 'out_velocity', 'out_instance'], // order matches the output buffers
pc.TRANSFORM_FEEDBACK_SEPARATE
);
必须在此处明确指定模式,不能自动推导,因为模式在链接着色器程序时就固定了;三个 varying 打包进一个缓冲区或分别写入三个缓冲区都合法。
分离后,每个缓冲区可承担所需角色,无需让每一份状态共享同一布局。
缓冲区角色
用数组中的条目描述各缓冲区。每个条目可指定 input、output 或两者:
const tf = new pc.TransformFeedback([
{ input: positions, output: positionsOut }, // read and written, swapped each step
{ input: velocities, output: velocitiesOut }, // read and written, swapped each step
{ input: constants }, // read only, never modified
{ output: instances } // written only, for the render pass
]);
| 角色 | 行为 | 适用数据 |
|---|---|---|
input 和 output | 着色器读取,变换反馈写入,每步之后交换这一对缓冲区 | 随时间演变的模拟状态,例如位置和速度 |
仅 input | 着色器读取,从不写入 | 每元素常量,例如随机种子、质量或最大速度。若没有此角色,这些数据就必须每步通过变换反馈复制,并无谓地使用双缓冲 |
仅 output | 每步写入,模拟着色器从不回读 | 仅供后续通道使用的数据,例如供实例化渲染使用的紧密打包数据流 |
具有 output 的条目按出现顺序分配变换反馈缓冲区索引,跳过没有输出的条目。顺序必须与捕获 varying 列表一致。
驱动实例化渲染
只写角色有明确用途。instancing 是 VertexFormat 的属性,因此将缓冲区标记为实例化,会对该缓冲区的每次使用都应用逐实例属性除数。所以同一缓冲区不能同时作为逐顶点模拟流和逐实例渲染流。
将渲染数据写入模拟从不回读的独立缓冲区,即可彻底避免冲突:
// written by transform feedback, consumed only by the instanced draw
material.setAttribute('aInstPosition', pc.SEMANTIC_ATTR12);
meshInstance.setInstancing(instances);
模拟缓冲区不会在渲染通道中绑定,实例化缓冲区恰好保存渲染通道所需的位置和朝向。
输入属性语义
属性从着色器源码提取,并按声明顺序依次分配 ATTR0、ATTR1、ATTR2 等。各输入缓冲区的顶点格式必须使用匹配语义:
attribute vec4 aPosition; // ATTR0
attribute vec4 aVelocity; // ATTR1
attribute vec4 aConstants; // ATTR2
const format = semantic => new pc.VertexFormat(device, [
{ semantic: semantic, components: 4, type: pc.TYPE_FLOAT32 }
]);
const positions = new pc.VertexBuffer(device, format(pc.SEMANTIC_ATTR0), count, options);
const velocities = new pc.VertexBuffer(device, format(pc.SEMANTIC_ATTR1), count, options);
const constants = new pc.VertexBuffer(device, format(pc.SEMANTIC_ATTR2), count, options);
不要在同一变换反馈着色器中混用标准属性名和自定义属性名。vertex_position 等名称映射到 SEMANTIC_POSITION,位置为 0;第一个自定义名称分配为 ATTR0,也位于 0。应全部使用自定义名称。
限制
- **仅 WebGL2。**没有 WebGPU 路径;在 WebGPU 中使用计算着色器。
- 顶点格式必须交错排列,或仅包含一个元素。
- 基础 WebGL2 的分离模式将每个 varying 限制为 4 个分量(
MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS只保证为 4),因此需规划每个缓冲区的数据。更宽的状态需要更多 varying,也就需要更多缓冲区。
API 参考
TransformFeedback:管理缓冲区和通道的辅助对象。TransformFeedback.createShader:构建可捕获输出的顶点着色器。TransformFeedback#process:运行一步,可选交换缓冲区。TRANSFORM_FEEDBACK_INTERLEAVED:将所有 varying 捕获到一个缓冲区,默认模式。TRANSFORM_FEEDBACK_SEPARATE:将各 varying 捕获到独立缓冲区。