外观
WebGPU 渲染管线
WebGPU 渲染管线是现代图形编程的核心组件,它定义了几何数据从原始顶点到最终像素的完整处理流程。与传统的 WebGL 管线相比,WebGPU 提供了更精细的控制和更高的性能,同时引入了更严格的类型安全和并行处理能力。
渲染管线概述
WebGPU 渲染管线是一个高度可配置的并行处理系统,它将 3D 场景数据转换为 2D 图像。管线设计借鉴了现代图形 API 的最佳实践,提供了明确的阶段划分和高效的数据流管理。
管线架构示意图:
应用阶段 → 输入装配 → 顶点着色 → 图元组装 → 光栅化 → 片段着色 → 输出合并
↓ ↓ ↓ ↓ ↓ ↓ ↓
CPU准备 顶点数据 顶点处理 几何构建 像素映射 颜色计算 最终输出管线创建与配置
在 WebGPU 中,渲染管线是通过管线布局和状态对象预先定义的,这种设计允许驱动程序进行深度优化。
管线创建
javascript
const renderPipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: vertexShaderModule,
entryPoint: 'vs_main',
buffers: [vertexBufferLayout]
},
fragment: {
module: fragmentShaderModule,
entryPoint: 'fs_main',
targets: [{
format: 'bgra8unorm',
blend: {
color: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
},
alpha: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
}
}
}]
},
primitive: {
topology: 'triangle-list',
cullMode: 'back'
},
depthStencil: {
format: 'depth24plus-stencil8',
depthWriteEnabled: true,
depthCompare: 'less'
},
multisample: {
count: 4
}
});管线布局
管线布局定义了着色器可以访问的资源绑定方式:
javascript
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [
bindGroupLayout // 定义统一缓冲区、纹理等资源的绑定方式
]
});输入装配阶段
输入装配阶段负责从缓冲区中读取顶点数据并组装成基本图元。这个阶段是渲染管线的起点,决定了如何解释输入的几何数据。
输入装配流程:
顶点缓冲区 → 索引缓冲区 → 顶点提取 → 图元组装
↓ ↓ ↓ ↓
位置/法线 顶点索引 属性读取 三角形/线顶点缓冲区布局
javascript
const vertexBufferLayout = {
arrayStride: 32, // 每个顶点占32字节
attributes: [
{
format: 'float32x3', // 位置:3个float32
offset: 0,
shaderLocation: 0
},
{
format: 'float32x3', // 法线:3个float32
offset: 12,
shaderLocation: 1
},
{
format: 'float32x2', // 纹理坐标:2个float32
offset: 24,
shaderLocation: 2
}
]
};图元拓扑
WebGPU 支持多种图元拓扑类型:
点列表: ● ● ● ●
线列表: ├───┼───┼───┤
线带: ├───┼───┼───┤
三角形列表: ▲ ▲ ▲ ▲
/ \ / \ / \ / \
三角形带: ▲▲▲▲▲▲▲▲▲▲▲▲▲
/ \ / \ / \ / \ /顶点着色阶段
顶点着色器是渲染管线的第一个可编程阶段,负责处理每个顶点的变换和属性计算。
顶点着色器示例
wgsl
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) texCoord: vec2<f32>,
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) worldPosition: vec3<f32>,
@location(1) worldNormal: vec3<f32>,
@location(2) texCoord: vec2<f32>,
};
@group(0) @binding(0) var<uniform> modelViewProjection: mat4x4<f32>;
@group(0) @binding(1) var<uniform> modelMatrix: mat4x4<f32>;
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
// 位置变换到裁剪空间
output.position = modelViewProjection * vec4<f32>(input.position, 1.0);
// 法线变换到世界空间
output.worldNormal = (modelMatrix * vec4<f32>(input.normal, 0.0)).xyz;
// 世界空间位置
output.worldPosition = (modelMatrix * vec4<f32>(input.position, 1.0)).xyz;
// 传递纹理坐标
output.texCoord = input.texCoord;
return output;
}内置变量
顶点着色器可以使用多种内置变量:
@vertex_index: 顶点索引
@instance_index: 实例索引
@position: 输出位置(必须)
@point_size: 点大小(可选)图元组装阶段
图元组装阶段将处理后的顶点连接成完整的几何图元,同时执行背面剔除、视锥体裁剪等操作。
图元组装流程:
处理后的顶点 → 图元连接 → 背面剔除 → 视锥裁剪 → 透视除法
↓ ↓ ↓ ↓ ↓
顶点着色器输出 三角形构建 面剔除 可见性判断 规范化坐标面剔除
javascript
primitive: {
topology: 'triangle-list',
cullMode: 'back', // 剔除背面
frontFace: 'ccw' // 逆时针为正面
}剔除模式示意图:
正面三角形 (保留): 背面三角形 (剔除):
v0 v0
/ \ / \
v1---v2 v2---v1光栅化阶段
光栅化是将几何图元转换为像素片段的过程,这个阶段确定哪些像素被图元覆盖,并为每个片段生成插值属性。
光栅化流程:
图元 → 扫描转换 → 深度计算 → 属性插值 → 片段生成
↓ ↓ ↓ ↓ ↓
几何体 像素映射 深度值 平滑过渡 片段数据多重采样抗锯齿
WebGPU 支持多重采样抗锯齿 (MSAA),通过在每个像素内采样多个位置来减少锯齿:
javascript
multisample: {
count: 4, // 4倍多重采样
mask: 0xFFFFFFFF, // 所有样本都写入
alphaToCoverageEnabled: false
}MSAA 示意图:
无抗锯齿: MSAA 4x:
像素边界锯齿 平滑边界
▓▓▓▓░░░░ ▓▓▓▓░░░░
▓▓▓░░░░░ → ▓▓▓▒░░░░
▓▓░░░░░░ ▓▓▒▒░░░░
▓░░░░░░░ ▓▒▒▒░░░░片段着色阶段
片段着色器是第二个可编程阶段,负责计算每个像素的最终颜色。这个阶段可以进行纹理采样、光照计算和材质处理。
片段着色器示例
wgsl
struct FragmentInput {
@location(0) worldPosition: vec3<f32>,
@location(1) worldNormal: vec3<f32>,
@location(2) texCoord: vec2<f32>,
};
struct FragmentOutput {
@location(0) color: vec4<f32>,
@location(1) normal: vec4<f32>, // G-Buffer 输出
};
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
@group(0) @binding(3) var textureSampler: sampler;
@fragment
fn fs_main(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
// 纹理采样
let diffuseColor = textureSample(diffuseTexture, textureSampler, input.texCoord);
// 法线归一化
let normal = normalize(input.worldNormal);
// 简单光照计算
let lightDir = normalize(vec3<f32>(1.0, 1.0, 1.0));
let intensity = max(dot(normal, lightDir), 0.1); // 10%环境光
// 最终颜色
output.color = vec4<f32>(diffuseColor.rgb * intensity, diffuseColor.a);
output.normal = vec4<f32>(normal * 0.5 + 0.5, 1.0); // 编码法线
return output;
}早期深度测试
WebGPU 支持早期深度测试,在片段着色器执行前进行深度比较,避免不必要的着色计算:
wgsl
@fragment
fn fs_main(@builtin(position) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
// 如果启用早期深度测试,深度值已经确定
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}输出合并阶段
输出合并阶段将片段着色器的输出与帧缓冲区中的现有内容结合,执行深度测试、模板测试和混合操作。
深度和模板测试
javascript
depthStencil: {
format: 'depth24plus-stencil8',
depthWriteEnabled: true,
depthCompare: 'less',
stencilFront: {
compare: 'equal',
failOp: 'keep',
depthFailOp: 'keep',
passOp: 'keep'
},
stencilBack: {
compare: 'equal',
failOp: 'keep',
depthFailOp: 'keep',
passOp: 'keep'
}
}深度测试示意图:
新片段深度: 0.3
深度缓冲区: 0.5
比较函数: 'less'
结果: 通过测试,更新深度缓冲区颜色混合
混合操作控制新颜色如何与目标颜色结合:
javascript
targets: [{
format: 'bgra8unorm',
blend: {
color: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
},
alpha: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
operation: 'add'
}
},
writeMask: GPUColorWrite.ALL
}]混合公式示意图:
源颜色: (1.0, 0.0, 0.0, 0.5)
目标颜色: (0.0, 0.0, 1.0, 1.0)
混合结果: src * src.a + dst * (1 - src.a)
= (0.5, 0.0, 0.5, 1.0)渲染通道编码
渲染通道编码器负责记录所有的渲染命令,并将其封装为可执行的命令缓冲区。
渲染通道设置
javascript
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: colorTextureView,
loadOp: 'clear',
storeOp: 'store',
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }
}
],
depthStencilAttachment: {
view: depthTextureView,
depthLoadOp: 'clear',
depthStoreOp: 'store',
depthClearValue: 1.0,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
stencilClearValue: 0
}
});绘制命令
javascript
// 设置管道
renderPass.setPipeline(renderPipeline);
// 设置顶点缓冲区
renderPass.setVertexBuffer(0, vertexBuffer);
// 设置绑定组
renderPass.setBindGroup(0, bindGroup);
// 绘制调用
renderPass.draw(vertexCount, instanceCount, firstVertex, firstInstance);
// 索引绘制
renderPass.setIndexBuffer(indexBuffer, 'uint16');
renderPass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance);多渲染目标
WebGPU 支持多渲染目标 (MRT),允许片段着色器同时输出到多个颜色附件:
wgsl
struct FragmentOutput {
@location(0) albedo: vec4<f32>, // 漫反射颜色
@location(1) normal: vec4<f32>, // 世界法线
@location(2) position: vec4<f32>, // 世界位置
@location(3) material: vec4<f32>, // 材质参数
};渲染目标配置:
javascript
targets: [
{ format: 'rgba8unorm' }, // albedo
{ format: 'rgba16float' }, // normal
{ format: 'rgba16float' }, // position
{ format: 'rgba8unorm' } // material
]性能优化特性
管线状态对象
WebGPU 的管线状态对象 (PSO) 允许驱动程序预先编译和验证管线状态:
管线创建时: 着色器编译 + 状态验证 + 资源绑定检查
渲染时: 直接使用预编译管线,无运行时开销动态状态
某些状态可以在渲染通道中动态设置,减少管线切换:
javascript
renderPass.setBlendConstant([1.0, 1.0, 1.0, 1.0]);
renderPass.setStencilReference(0xFF);
renderPass.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
renderPass.setScissorRect(0, 0, canvas.width, canvas.height);间接绘制
支持间接绘制,减少 CPU-GPU 通信:
javascript
const indirectBuffer = device.createBuffer({
size: 16, // 4个uint32: vertexCount, instanceCount, firstVertex, firstInstance
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST
});
renderPass.drawIndirect(indirectBuffer, 0);