跳到主要内容

变换反馈

变换反馈运行顶点着色器,并将输出捕获到顶点缓冲区,而不进行光栅化。结果留在 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 参考​