WebGPU Scale(缩放)
原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-scale.html
本文是 WebGPU 3D 数学系列教程的第三篇,基于前两篇《平移》和《旋转》的内容继续展开。缩放与平移一样简单:只需将顶点位置乘以期望的缩放系数即可。下面我们来看对着色器所做的改动。
3 个核心优化要点
在编译原文的基础上,提炼出以下 3 个与 Uniform 缓冲区、变换顺序及内存布局相关的核心要点:
-
精确计算 Uniform 缓冲区大小,移除 padding 冗余
原代码从(4 + 2 + 2 + 2) * 4 + 8(包含手动 padding 字节)改为(4 + 2 + 2 + 2 + 2) * 4,利用Float32Array的subarray视图直接映射每个 uniform 的偏移量,避免因手动字节对齐错误而浪费空间。 -
在着色器内保持变换顺序:先缩放、再旋转、最后平移
WGSL 代码中先计算scaledPosition,再计算rotatedPosition,最后加上translation。这种固定顺序能保证几何体先围绕自身缩放/旋转,再整体平移到目标位置,是后续矩阵组合的基础。 -
通过偏移量常量管理 Uniform 布局,提升可维护性
使用kColorOffset、kResolutionOffset、kTranslationOffset、kRotationOffset、kScaleOffset等常量在 JavaScript 侧管理Float32Array索引,避免在多个地方硬编码数字,降低更新 Uniform 时出错的风险。
着色器改动
首先在 Uniform 结构体中添加 scale: vec2f,并在顶点着色器中先缩放、再旋转、最后平移:
struct Uniforms {
color: vec4f,
resolution: vec2f,
translation: vec2f,
rotation: vec2f,
scale: vec2f,
};
struct Vertex {
@location(0) position: vec2f,
};
struct VSOutput {
@builtin(position) position: vec4f,
};
@group(0) @binding(0) `var<uniform>` uni: Uniforms;
@vertex fn vs(vert: Vertex) -> VSOutput {
var vsOut: VSOutput;
// Scale the position
let scaledPosition = vert.position * uni.scale;
// Rotate the position
let rotatedPosition = vec2f(
scaledPosition.x * uni.rotation.y - scaledPosition.y * uni.rotation.x,
scaledPosition.x * uni.rotation.x + scaledPosition.y * uni.rotation.y
);
// Add in the translation
let position = rotatedPosition + uni.translation;
// convert the position from pixels to a 0.0 to 1.0 value
let zeroToOne = position / uni.resolution;
// convert from 0 <-> 1 to 0 <-> 2
let zeroToTwo = zeroToOne * 2.0;
// covert from 0 <-> 2 to -1 <-> +1 (clip space)
let flippedClipSpace = zeroToTwo - 1.0;
// flip Y
let clipSpace = flippedClipSpace * vec2f(1, -1);
vsOut.position = vec4f(clipSpace, 0.0, 1.0);
return vsOut;
}
Uniform 缓冲区更新
接下来调整 Uniform 缓冲区的大小,移除原先的 padding 字节,并添加 scale 的偏移量:
- // color, resolution, translation, rotation, padding
- const uniformBufferSize = (4 + 2 + 2 + 2) * 4 + 8;
+ // color, resolution, translation, rotation, scale
+ const uniformBufferSize = (4 + 2 + 2 + 2 + 2) * 4;
const uniformBuffer = device.createBuffer({
label: 'uniforms',
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformValues = new Float32Array(uniformBufferSize / 4);
// offsets to the various uniform values in float32 indices
const kColorOffset = 0;
const kResolutionOffset = 4;
const kTranslationOffset = 6;
const kRotationOffset = 8;
const kScaleOffset = 10;
const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
const resolutionValue = uniformValues.subarray(kResolutionOffset, kResolutionOffset + 2);
const translationValue = uniformValues.subarray(kTranslationOffset, kTranslationOffset + 2);
const rotationValue = uniformValues.subarray(kRotationOffset, kRotationOffset + 2);
const scaleValue = uniformValues.subarray(kScaleOffset, kScaleOffset + 2);
渲染时,需要将缩放值写入 Uniform 缓冲区:
const settings = {
translation: [150, 100],
rotation: degToRad(30),
scale: [1, 1],
};
const radToDegOptions = { min: -360, max: 360, step: 1, converters: GUI.converters.radToDeg };
const gui = new GUI();
gui.onChange(render);
gui.add(settings.translation, '0', 0, 1000).name('translation.x');
gui.add(settings.translation, '1', 0, 1000).name('translation.y');
gui.add(settings, 'rotation', radToDegOptions);
gui.add(settings.scale, '0', -5, 5).name('scale.x');
gui.add(settings.scale, '1', -5, 5).name('scale.y');
function render() {
...
// Set the uniform values in our JavaScript side Float32Array
resolutionValue.set([canvas.width, canvas.height]);
translationValue.set(settings.translation);
rotationValue.set([
Math.cos(settings.rotation),
Math.sin(settings.rotation),
]);
scaleValue.set(settings.scale);
// upload the uniform values to the uniform buffer
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
现在可以拖动滑块来观察缩放效果。注意两个现象:
- 使用负值缩放会翻转几何体。
- 缩放是相对于原点
(0, 0)进行的,对于示例中的字母 F,原点位于左上角。因为顶点位置乘以缩放系数后会远离原点,这符合预期。如果想围绕其他点缩放,可以在缩放前添加一个“预缩放平移”,或者直接修改 F 的顶点数据。稍后我们会介绍另一种更通用的方法。
GLSL / WebGL 专业术语检查
原文使用 WebGPU + WGSL,并非 GLSL / WebGL,但两者在着色器编程中有许多对应概念。以下列出关键术语及其准确翻译,避免混淆:
| 原文术语 | 中文翻译 | 备注 |
|---|---|---|
| Uniform buffer | 统一缓冲区 | WebGPU 中通过 GPUBufferUsage.UNIFORM 创建,对应 WebGL 的 uniform 变量集合 |
| Vertex shader | 顶点着色器 | WGSL 中用 @vertex 标记 |
| Clip space | 裁剪空间 | 顶点着色器输出的坐标系,范围 -1 到 +1 |
@builtin(position) | 内建位置输出 | 对应 GLSL 的 gl_Position |
@group(0) @binding(0) | 绑定组 0,绑定点 0 | 对应 WebGL 中 uniform 的绑定位置,但基于绑定组模型 |
vec2f / vec4f | 2 分量 / 4 分量浮点向量 | WGSL 类型,类比 GLSL 的 vec2 / vec4 |
Float32Array | 32 位浮点类型数组 | JavaScript 侧存储 uniform 数据 |
device.queue.writeBuffer | 命令队列写入缓冲区 | 将数据上传到 GPU 缓冲区 |
需要特别注意的是,WebGPU 的 Uniform 缓冲区布局遵循 std140 对齐规则,但本示例通过 Float32Array 的紧凑排列避开了对齐填充,这在 WebGL 中可能需要手动计算 UNPACK_ALIGNMENT 或使用 gl.uniform* 逐个设置。WebGPU 的显式缓冲管理让内存布局更可控,但也要求开发者精确计算偏移量。
