跳转到内容

Uniform 缓冲区

Uniform 缓冲适合放每帧都变、但体积不大的参数:颜色、时间、变换矩阵。着色器只能读它,不能写。大数组和计算结果应该用存储缓冲,见计算着色器。

struct Uniforms {
color: vec4f,
offset: vec2f,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4f {
var positions = array<vec2f, 3>(
vec2f(0.0, 0.5),
vec2f(-0.5, -0.5),
vec2f(0.5, -0.5),
);
let position = positions[vertexIndex] + uniforms.offset;
return vec4f(position, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4f {
return uniforms.color;
}

var<uniform> 是只读的。JavaScript 侧的缓冲必须带 GPUBufferUsage.UNIFORM。用 writeBuffer() 更新时再加上 COPY_DST。

WGSL 的 uniform 地址空间不是按紧凑的 C 结构体来排的。标量和向量的对齐是:

类型对齐大小
f3244
vec2f88
vec3f1612
vec4f1616

结构体的对齐取成员对齐的最大值,再向上取整到 16。结构体的大小再向上取整到这个对齐。上面的 Uniforms 因此是 32 字节,不是 24:

字节内容
0–15color(4 个 f32)
16–23offset(2 个 f32)
24–31补齐
const uniformData = new Float32Array(8); // 32 字节
uniformData[0] = 0.9; // r
uniformData[1] = 0.4; // g
uniformData[2] = 0.2; // b
uniformData[3] = 1; // a
uniformData[4] = 0.2; // offset.x
uniformData[5] = -0.1; // offset.y
const uniformBuffer = device.createBuffer({
size: uniformData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformData);

缓冲至少要有结构体 SizeOf 这么大。只分配 24 字节时,绘制阶段的最小绑定大小检查会失败。

只有一个 f32 或一个 vec4f 时,结构体大小是 16 字节。vec3f 容易少算:它的大小是 12,对齐却是 16,后面的成员不能紧贴着放。

绑定组指向缓冲,不复制当时的内容。动画里保留同一块缓冲和同一个绑定组,每帧 writeBuffer(),再录制新的命令缓冲:

function frame(time) {
uniformData[4] = Math.sin(time) * 0.35;
uniformData[5] = Math.cos(time * 0.8) * 0.25;
device.queue.writeBuffer(uniformBuffer, 0, uniformData);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0.3, g: 0.3, b: 0.3, a: 1 },
loadOp: "clear",
storeOp: "store",
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame((now) => frame(now / 1000));
}

writeBuffer() 在同一条队列上先于后面的 submit() 生效。不要在映射状态下写入,也不要复用上一帧的 GPUCommandEncoder。

颜色和位移都放在同一个 uniform 结构体里,三角形会移动并变色。

每帧写入 uniform

图像数据不适合塞进 uniform。下一章用纹理与采样器把一张棋盘格贴到四边形上。