WebGPU 透明度与混合
原文标题:WebGPU Transparency and Blending
原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-transparency.html
以下内容基于原文编译,保留核心代码,并对关键术语进行规范化翻译。
很难全面覆盖透明度和混合,因为不同场景下的需求往往不同。因此,本文将主要对 WebGPU 相关特性进行导览,以便后续在涉及具体技术时回溯参考。

Canvas alphaMode
首先需要明确:WebGPU 内部的透明与混合,与 WebGPU canvas 和 HTML 页面之间的透明与混合,是两回事。
默认情况下,WebGPU canvas 是不透明的,其 alpha 通道被忽略。若要让 alpha 生效,必须在调用 configure 时将其 alphaMode 设为 'premultiplied'。默认值是 'opaque'。
context.configure({
device,
format: presentationFormat,
+ alphaMode: 'premultiplied',
});
理解 alphaMode: 'premultiplied' 的含义很重要:它表示放入 canvas 的颜色值必须已经乘以了 alpha 值。
下面做一个最小示例,只创建渲染通道并设置清除颜色。
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
// 获取 canvas 的 WebGPU 上下文并配置
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
+ alphaMode: 'premultiplied',
});
const clearValue = [1, 0, 0, 0.01];
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- 渲染时填充
clearValue,
loadOp: 'clear',
storeOp: 'store',
},
],
};
function render() {
const encoder = device.createCommandEncoder({ label: 'clear encoder' });
const canvasTexture = context.getCurrentTexture();
renderPassDescriptor.colorAttachments[0].view =
canvasTexture.createView();
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const canvas = entry.target;
const width = entry.contentBoxSize[0].inlineSize;
const height = entry.contentBoxSize[0].blockSize;
canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
render();
}
});
observer.observe(canvas);
}
再给 canvas 添加灰色棋盘格 CSS 背景:
canvas {
background-color: #404040;
background-image:
linear-gradient(45deg, #808080 25%, transparent 25%),
linear-gradient(-45deg, #808080 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #808080 75%),
linear-gradient(-45deg, transparent 75%, #808080 75%);
background-size: 32px 32px;
background-position: 0 0, 0 16px, 16px -16px, -16px 0px;
}
加入 UI 以便设置清除颜色的 alpha、颜色以及是否预乘:
+import GUI from '../3rdparty/muigui-0.x.module.js';
...
+ const color = [1, 0, 0];
+ const settings = {
+ premultiply: false,
+ color,
+ alpha: 0.01,
+ };
+
+ const gui = new GUI().onChange(render);
+ gui.add(settings, 'premultiply');
+ gui.add(settings, 'alpha', 0, 1);
+ gui.addColor(settings, 'color');
function render() {
const encoder = device.createCommandEncoder({ label: 'clear encoder' });
const canvasTexture = context.getCurrentTexture();
renderPassDescriptor.colorAttachments[0].view =
canvasTexture.createView();
+ const { alpha } = settings;
+ clearValue[3] = alpha;
+ if (settings.premultiply) {
+ // 将颜色乘以 alpha 进行预乘
+ clearValue[0] = color[0] * alpha;
+ clearValue[1] = color[1] * alpha;
+ clearValue[2] = color[2] * alpha;
+ } else {
+ // 使用未预乘的颜色
+ clearValue[0] = color[0];
+ clearValue[1] = color[1];
+ clearValue[2] = color[2];
+ }
const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
运行后会发现有问题:实际出现的颜色是未定义的!
在作者机器上出现了两种亮粉色。问题在于:alpha 设为 0.01,背景为中灰/深灰棋盘格,而清除颜色是红色 (1,0,0)。在灰色背景上叠 0.01 的红色几乎不可察觉,为何出现亮粉?
原因是:这是一个非法颜色!canvas 中的颜色为 1, 0, 0, 0.01,但这不是预乘颜色。“预乘”意味着放入 canvas 的颜色必须已经乘以 alpha 值。当 alpha 为 0.01 时,其他任何分量都不应大于 0.01。
勾选“premultiplied”复选框后,代码将对颜色进行预乘,实际写入 canvas 的值为 0.01, 0, 0, 0.01,就正确了,几乎不可见。调整 alpha 可以看到逐渐变红。
注意:由于
1, 0, 0, 0.01是非法颜色,其显示方式由浏览器决定,可能在不同设备上不同。不要使用非法颜色。
假设我们希望颜色为橙色 (1, 0.5, 0.25) 且透明度为 33%(alpha=0.33),则预乘后的颜色为:
premultiplied
---------------------------------
r = 1 * 0.33 = 0.33
g = 0.5 * 0.33 = 0.165
b = 0.25 * 0.33 = 0.0825
a = 0.33 = 0.33
如何获得预乘颜色取决于你。如果只有未预乘颜色,可以在着色器中执行类似:
return vec4f(color.rgb * color.a, color.a);
函数 copyExternalImageToTexture(在“导入纹理”一文介绍过)接受 premultipliedAlpha: true 选项。这意味着加载图像时可以让 WebGPU 在复制到纹理时自动预乘颜色,这样调用 textureSample 得到的值就已经是预乘的。
本节要点:
- 解释
alphaMode: 'premultiplied'canvas 配置选项,让 WebGPU canvas 支持透明。 - 引入预乘 Alpha 颜色概念。如何获得预乘颜色由你决定:可以在 JavaScript 中创建预乘
clearValue,也可以在片元着色器返回时预乘,或通过后处理预乘。关键是使用alphaMode: 'premultiplied'时,canvas 中的颜色最终必须是预乘的。推荐阅读 GPUs prefer premultiplication。
Discard
discard 是 WGSL 中的语句,用于在片元着色器中丢弃当前片元,即不绘制该像素。
沿用之前基于 @builtin(position) 绘制棋盘格的示例,我们改为其中一种情况丢弃。
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
- let red = vec4f(1, 0, 0, 1);
let cyan = vec4f(0, 1, 1, 1);
let grid = vec2u(fsInput.position.xy) / 8;
let checker = (grid.x + grid.y) % 2 == 1;
+ if (checker) {
+ discard;
+ }
+
+ return cyan;
- return select(red, cyan, checker);
}
同时在 JS 中设置 alphaMode: 'premultiplied' 并将 clearValue 设为透明 [0,0,0,0]。
context.configure({
device,
format: presentationFormat,
+ alphaMode: 'premultiplied',
});
...
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
- clearValue: [0.3, 0.3, 0.3, 1],
+ clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
],
};
...
效果是隔一个方格透明。
在透明着色器中,通常根据 alpha 值进行丢弃:
@fragment fn fs(fsInput: OurVertexShaderOutput) -> @location(0) vec4f {
let color = ... compute a color ....
if (color.a < threshold) {
discard;
}
return color;
}
threshold 可以来自 uniform 或常量。这种技术常用于精灵和草叶等植被。因为如果启用了深度纹理(见正交投影一文),即使 alpha 为 0,绘制时也会更新深度缓冲,导致后面的精灵/叶片被遮挡。因此直接 discard 不写入深度。后续文章会详述。
Blend Settings(混合设置)
最后来到混合设置。创建渲染管线时,可以针对片元着色器的每个 target 设置混合状态。常规管线如下:
const pipeline = device.createRenderPipeline({
label: 'hardcoded textured quad pipeline',
layout: pipelineLayout,
vertex: {
module,
},
fragment: {
module,
targets: [
{
format: presentationFormat,
},
],
},
});
为 target[0] 添加混合:
const pipeline = device.createRenderPipeline({
label: 'hardcoded textured quad pipeline',
layout: pipelineLayout,
vertex: {
module,
},
fragment: {
module,
targets: [
{
format: presentationFormat,
+ blend: {
+ color: {
+ srcFactor: 'one',
+ dstFactor: 'one-minus-src-alpha'
+ },
+ alpha: {
+ srcFactor: 'one',
+ dstFactor: 'one-minus-src-alpha'
+ },
+ },
},
],
},
});
完整默认设置为:
blend: {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'zero',
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'zero',
},
}
其中 color 控制 RGB 部分,alpha 控制 alpha 部分。
operation 可选值:'add'、'subtract'、'reverse-subtract'、'min'、'max'。
srcFactor 和 dstFactor 可选值:
'zero'、'one'、'src'、'one-minus-src'、'src-alpha'、'one-minus-src-alpha'、'dst'、'one-minus-dst'、'dst-alpha'、'one-minus-dst-alpha'、'src-alpha-saturated'、'constant'、'one-minus-constant'。
理解公式:
result = operation((src * srcFactor), (dst * dstFactor))
src 是片元着色器返回值,dst 是目标纹理中已有的值。默认 'add', 'one', 'zero' 意味着 result = src。
提到 'constant' 和 'one-minus-constant' 时,常量通过渲染通道 setBlendConstant 命令设置,默认 [0,0,0,0],可在绘制间改变。
最常见的混合设置是:
{
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
}
这种模式通常与“预乘 Alpha”一起使用,即期望 src 的 RGB 已被 alpha 预乘。
接下来构造示例,用两个 Canvas 2D 图像(具有 alpha)加载为 WebGPU 纹理。创建目标图像和源图像的函数略(原代码较长,核心逻辑保留)。关键部分是创建纹理时传递 premultipliedAlpha 选项,以及使用两组纹理(预乘/未预乘)。随后创建 bindGroupLayout、pipeline、uniform buffer,并允许 UI 调整混合因子、操作和清除颜色。完整示例包含大量交互代码,此处仅提取核心混合管线创建部分:
const srcPipeline = device.createRenderPipeline({
label: 'hardcoded textured quad pipeline',
layout: pipelineLayout,
vertex: { module },
fragment: {
module,
targets: [
{
format: presentationFormat,
blend: {
color,
alpha,
},
},
],
},
});
渲染时先绘制目标纹理(无混合),再绘制源纹理(带混合)。还提供了预设列表对应 Canvas 2D 的 globalCompositeOperation 模式,如 source-over、destination-over、additive 等。
可直接运行完整示例:https://webgpufundamentals.org/webgpu/lessons/webgpu-blend.html
从源图像和目标图像出发,一些实用的混合设置:
- 预乘混合(source-over):
color: {operation:'add', srcFactor:'one', dstFactor:'one-minus-src-alpha'} - destination-over:
color: {operation:'add', srcFactor:'one-minus-dst-alpha', dstFactor:'one'} - 加色(lighten):
color: {operation:'add', srcFactor:'one', dstFactor:'one'} - source-in / destination-in / source-out / destination-out / source-atop / destination-atop 等。
这些名称来自 Canvas 2D 的 globalCompositeOperation,但某些模式需要超出基础混合设置的额外数学,需自定义解决方案。
掌握了这些 WebGPU 混合基础,后续技术文章便可引用。
核心优化要点
-
优先采用预乘 Alpha(Premultiplied Alpha)
GPU 硬件更偏好预乘颜色,使用alphaMode: 'premultiplied'并确保所有颜色值分量不超过 alpha 值,可以避免非法颜色未定义行为,同时混合公式可简化为src * 1 + dst * (1 - src.a),减少着色器中额外的乘法操作,提高混合效率。 -
透明片元使用
discard而非输出 Alpha=0
对于精灵、草叶等需要透明镂空的对象,在片元着色器中用if (alpha < threshold) discard;直接丢弃片元,可避免写入深度缓冲,从而防止后面的透明物体被错误遮挡。但需注意过度discard可能影响 Early-Z 优化,应只对真正需要的地方使用。 -
合理配置 Blend State 实现高效混合
根据需求选择合适的srcFactor、dstFactor和operation。常用预乘混合srcFactor: 'one',dstFactor: 'one-minus-src-alpha'最简洁高效。对于加法混合(如光晕、粒子)使用srcFactor: 'one',dstFactor: 'one'。避免在着色器内手动实现混合,利用硬件混合单元减少带宽和计算开销。
专业术语检查(GLSL/WebGL 相关)
- Premultiplied Alpha → 预乘 Alpha:颜色分量预先乘以 alpha 值。
- Blending → 混合:片元输出与目标缓冲中已有颜色的合并过程。
- Discard → 丢弃:WGSL 语句,与 GLSL 中的
discard语义相同。 - Fragment Shader → 片元着色器(WebGPU/WGSL 中与 GLSL fragment shader 对应)。
- Render Pipeline → 渲染管线。
- Render Pass → 渲染通道。
- Bind Group → 绑定组(WebGPU 特有,类似 WebGL 中 uniform/texture 绑定集合)。
- Depth Texture → 深度纹理。
- Mipmap → 多级渐远纹理。
- SrcFactor/DstFactor → 源因子/目标因子。
- Blend Operation → 混合操作(add/subtract/reverse-subtract/min/max)。
以上术语在编译中均已保持统一,确保技术含义准确。
