各位观众老爷,大家好!今天咱们不聊风花雪月,聊点硬核的——WebGPU!这玩意儿可是浏览器里 GPU 计算和渲染的新宠,用好了,能让你的网页像打了鸡血一样,飞起来!准备好了吗?开车啦!
第一章:WebGPU是何方神圣?
先来个灵魂拷问:你真的了解WebGL吗?WebGL虽然让浏览器能用GPU,但它本质上是对OpenGL ES 3.0的封装,API比较底层,用起来繁琐,而且性能优化空间有限。WebGPU就是来解决这些问题的!
WebGPU是W3C制定的一套新的Web API,它旨在:
- 更现代、更高效: 借鉴了Vulkan、Metal、DirectX 12等现代图形API的设计思想,提供了更低的硬件抽象层,减少了CPU的开销。
- 更强大的计算能力: 不仅仅是图形渲染,WebGPU也能进行通用计算(GPGPU),比如图像处理、机器学习等等。
- 跨平台兼容性: 目标是在不同的操作系统和浏览器上提供一致的体验。
- 安全性: 在设计上考虑了安全性,避免了WebGL的一些安全隐患。
简单来说,WebGPU就是WebGL的升级版,性能更强,功能更多,用起来也更舒服。
第二章:WebGPU的那些“零件”
WebGPU不是一个简单的API,它由一系列相互协作的对象组成。咱们来认识一下这些“零件”:
-
GPUAdapter: 代表一个GPU设备(可以是物理GPU,也可以是软件模拟的GPU)。你可以通过它来请求访问GPU。
-
GPUDevice: 代表一个逻辑上的GPU实例。所有的操作都通过这个对象来执行。它就像一个连接你代码和GPU的“桥梁”。
-
GPUShaderModule: 包含用WGSL(WebGPU Shading Language)编写的着色器代码。WGSL是一种类似于GLSL的着色器语言,但更加现代化和安全。
-
GPUBuffer: 代表GPU内存中的一块缓冲区。你可以用它来存储顶点数据、索引数据、uniform变量等等。
-
GPUTexture: 代表GPU中的一个纹理。你可以用它来存储图像数据,用于渲染或者计算。
-
GPUSampler: 定义了纹理采样的方式,比如过滤模式、寻址模式等等。
-
GPUBindGroupLayout: 定义了着色器如何访问资源(比如uniform变量、纹理、采样器)。
-
GPUBindGroup: 将实际的资源绑定到GPUBindGroupLayout定义的槽位上。
-
GPUPipelineLayout: 定义了渲染管线或者计算管线中使用的GPUBindGroupLayout的布局。
-
GPURenderPipeline: 定义了渲染管线的配置,包括顶点着色器、片元着色器、顶点格式、颜色附件等等。
-
GPUComputePipeline: 定义了计算管线的配置,包括计算着色器。
-
GPUCommandEncoder: 用于记录一系列GPU命令,比如渲染命令、计算命令、拷贝命令等等。
-
GPUCommandBuffer: 包含一系列GPU命令,可以提交给GPU执行。
-
GPUQueue: 用于提交GPUCommandBuffer,控制GPU的执行顺序。
这些“零件”之间的关系可以用一个表格来概括:
对象 | 功能 |
---|---|
GPUAdapter |
代表一个GPU设备。 |
GPUDevice |
代表一个逻辑上的GPU实例。 |
GPUShaderModule |
包含用WGSL编写的着色器代码。 |
GPUBuffer |
代表GPU内存中的一块缓冲区。 |
GPUTexture |
代表GPU中的一个纹理。 |
GPUSampler |
定义了纹理采样的方式。 |
GPUBindGroupLayout |
定义了着色器如何访问资源。 |
GPUBindGroup |
将实际的资源绑定到GPUBindGroupLayout定义的槽位上。 |
GPUPipelineLayout |
定义了管线中使用的GPUBindGroupLayout的布局。 |
GPURenderPipeline |
定义了渲染管线的配置。 |
GPUComputePipeline |
定义了计算管线的配置。 |
GPUCommandEncoder |
用于记录一系列GPU命令。 |
GPUCommandBuffer |
包含一系列GPU命令。 |
GPUQueue |
用于提交GPUCommandBuffer。 |
是不是有点晕?别怕,咱们接下来用代码来演示一下。
第三章:WebGPU实战:画一个简单的三角形
光说不练假把式,咱们来用WebGPU画一个简单的三角形。
1. 获取GPU设备
async function initWebGPU() {
if (!navigator.gpu) {
alert("WebGPU is not supported on this browser.");
return;
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
alert("No appropriate GPUAdapter found.");
return;
}
const device = await adapter.requestDevice();
return device;
}
const device = await initWebGPU();
if (!device) {
console.error("Failed to initialize WebGPU.");
return;
}
这段代码首先检查浏览器是否支持WebGPU,然后请求一个GPUAdapter,最后请求一个GPUDevice。如果任何一步失败,都会报错。
2. 创建着色器
我们需要编写两个着色器:顶点着色器和片元着色器。
- 顶点着色器: 负责将顶点坐标从模型空间转换到裁剪空间。
- 片元着色器: 负责计算每个像素的颜色。
const vertexShaderSource = `
@vertex
fn main(@location(0) pos: vec2f) -> @builtin(position) vec4f {
return vec4f(pos, 0.0, 1.0);
}
`;
const fragmentShaderSource = `
@fragment
fn main() -> @location(0) vec4f {
return vec4f(1.0, 0.0, 0.0, 1.0); // Red color
}
`;
const vertexShaderModule = device.createShaderModule({
code: vertexShaderSource,
});
const fragmentShaderModule = device.createShaderModule({
code: fragmentShaderSource,
});
这里我们用WGSL编写了两个简单的着色器。顶点着色器接收顶点坐标作为输入,并将其转换为裁剪空间坐标。片元着色器返回红色。
3. 创建缓冲区
我们需要创建一个缓冲区来存储三角形的顶点数据。
const vertices = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true, // 允许在创建时映射缓冲区
});
new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
vertexBuffer.unmap();
这段代码首先创建了一个Float32Array来存储顶点数据,然后创建了一个GPUBuffer,并将顶点数据复制到缓冲区中。GPUBufferUsage.VERTEX
表示这个缓冲区用于顶点数据,GPUBufferUsage.COPY_DST
表示可以向这个缓冲区写入数据。mappedAtCreation: true
允许我们在创建缓冲区时就映射它,方便写入数据。
4. 创建渲染管线
我们需要创建一个渲染管线来配置渲染过程。
const canvas = document.getElementById("webgpu-canvas");
const context = canvas.getContext("webgpu");
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: presentationFormat,
alphaMode: "opaque",
});
const renderPipelineDescriptor = {
layout: 'auto', // 自动推断布局
vertex: {
module: vertexShaderModule,
entryPoint: "main",
buffers: [{
arrayStride: 8, // 2 * sizeof(float)
attributes: [{
shaderLocation: 0,
offset: 0,
format: "float32x2",
}],
}],
},
fragment: {
module: fragmentShaderModule,
entryPoint: "main",
targets: [{
format: presentationFormat,
}],
},
primitive: {
topology: "triangle-list"
}
};
const renderPipeline = device.createRenderPipeline(renderPipelineDescriptor);
这段代码首先获取canvas元素和WebGPU上下文,然后配置上下文,指定设备、格式和alpha模式。接下来,我们创建了一个渲染管线描述符,指定了顶点着色器、片元着色器、顶点格式和颜色附件等等。layout: 'auto'
表示自动推断布局,WebGPU会自动根据着色器的输入输出来创建GPUBindGroupLayout和GPUPipelineLayout。vertex.buffers
定义了顶点数据的格式。primitive.topology
指定了图元类型为三角形列表。
5. 渲染
最后,我们需要编写渲染代码。
function render() {
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor = {
colorAttachments: [{
view: textureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, // Black color
loadOp: "clear",
storeOp: "store",
}],
};
const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
renderPass.setPipeline(renderPipeline);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(3, 1, 0, 0); // 3 vertices, 1 instance
renderPass.end();
const commandBuffer = commandEncoder.finish();
device.queue.submit([commandBuffer]);
}
render();
这段代码首先创建一个命令编码器,然后获取当前纹理的视图。接下来,我们创建了一个渲染通道描述符,指定了颜色附件、清除颜色、加载操作和存储操作。loadOp: "clear"
表示在渲染之前清除颜色附件,storeOp: "store"
表示在渲染之后存储颜色附件。然后,我们开始一个渲染通道,设置渲染管线、顶点缓冲区,并绘制三角形。最后,我们结束渲染通道,完成命令编码,并将命令缓冲区提交给GPU。
将上面的代码片段组合在一起,就能在canvas上看到一个红色的三角形了!
第四章:WebGPU的计算能力
WebGPU不仅仅可以用来渲染图形,还可以进行通用计算(GPGPU)。咱们来用WebGPU进行一个简单的向量加法。
1. 创建计算着色器
const computeShaderSource = `
struct Data {
a: array<f32>;
b: array<f32>;
output: array<f32>;
};
@group(0) @binding(0) var<storage, read_write> data: Data;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id: vec3u) {
let i = global_id.x;
data.output[i] = data.a[i] + data.b[i];
}
`;
const computeShaderModule = device.createShaderModule({
code: computeShaderSource,
});
这个计算着色器接收两个浮点数数组作为输入,并将它们的和存储到输出数组中。@workgroup_size(64)
表示每个工作组包含64个工作项。
2. 创建缓冲区
我们需要创建三个缓冲区:两个输入缓冲区和一个输出缓冲区。
const arrayLength = 1024;
const a = new Float32Array(arrayLength);
const b = new Float32Array(arrayLength);
for (let i = 0; i < arrayLength; ++i) {
a[i] = Math.random();
b[i] = Math.random();
}
const gpuBufferA = device.createBuffer({
size: a.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(gpuBufferA.getMappedRange()).set(a);
gpuBufferA.unmap();
const gpuBufferB = device.createBuffer({
size: b.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(gpuBufferB.getMappedRange()).set(b);
gpuBufferB.unmap();
const gpuBufferOutput = device.createBuffer({
size: a.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
这里我们创建了三个GPUBuffer,分别存储输入数组a、输入数组b和输出数组。GPUBufferUsage.STORAGE
表示这个缓冲区可以被着色器读取和写入,GPUBufferUsage.COPY_SRC
表示可以从这个缓冲区复制数据。
3. 创建计算管线
const computePipelineDescriptor = {
layout: 'auto',
compute: {
module: computeShaderModule,
entryPoint: "main",
},
};
const computePipeline = device.createComputePipeline(computePipelineDescriptor);
这段代码创建了一个计算管线,指定了计算着色器。
4. 创建绑定组
我们需要创建一个绑定组,将缓冲区绑定到着色器的输入槽位上。
const bindGroup = device.createBindGroup({
layout: computePipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: {
buffer: gpuBufferA,
},
},
{
binding: 1,
resource: {
buffer: gpuBufferB,
},
},
{
binding: 2,
resource: {
buffer: gpuBufferOutput,
},
},
],
});
这段代码创建了一个绑定组,将gpuBufferA、gpuBufferB和gpuBufferOutput绑定到计算着色器的输入槽位0、1和2上。
5. 执行计算
const commandEncoder = device.createCommandEncoder();
const pass = commandEncoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(arrayLength / 64)); // 派发工作组
pass.end();
// Copy the results to a readable buffer
const gpuReadBuffer = device.createBuffer({
size: a.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});
commandEncoder.copyBufferToBuffer(
gpuBufferOutput, // Source buffer
0, // Source offset
gpuReadBuffer, // Destination buffer
0, // Destination offset
a.byteLength // Size
);
const commandBuffer = commandEncoder.finish();
device.queue.submit([commandBuffer]);
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(gpuReadBuffer.getMappedRange());
gpuReadBuffer.unmap();
// Verify the results
for (let i = 0; i < arrayLength; ++i) {
const expected = a[i] + b[i];
if (Math.abs(result[i] - expected) > 0.0001) {
console.error(`Error: result[${i}] = ${result[i]}, expected ${expected}`);
}
}
console.log("Compute shader completed successfully!");
这段代码首先创建一个命令编码器,然后开始一个计算通道,设置计算管线、绑定组,并派发工作组。dispatchWorkgroups(Math.ceil(arrayLength / 64))
表示派发的工作组数量,我们需要确保每个数组元素都被计算到。然后,我们将计算结果从gpuBufferOutput复制到一个可读的缓冲区gpuReadBuffer,并将其映射到CPU内存中。最后,我们验证计算结果是否正确。
第五章:WGSL:WebGPU的灵魂语言
WGSL(WebGPU Shading Language)是WebGPU的着色器语言,它类似于GLSL,但更加现代化和安全。WGSL的一些关键特性:
- 基于Rust的语法: WGSL的语法受到Rust的影响,更加清晰和易于理解。
- 类型安全: WGSL是强类型语言,可以减少类型错误。
- 内存安全: WGSL在设计上考虑了内存安全,避免了内存越界等问题。
- 模块化: WGSL支持模块化编程,可以将着色器代码分割成多个模块。
WGSL的一些基本语法:
- 变量声明: 使用
var
关键字声明变量,例如var<private> x: f32;
。 - 常量声明: 使用
let
关键字声明常量,例如let PI: f32 = 3.14159;
。 - 函数声明: 使用
fn
关键字声明函数,例如fn main() -> @location(0) vec4f { ... }
。 - 结构体声明: 使用
struct
关键字声明结构体,例如struct Vertex { position: vec3f; normal: vec3f; }
。 - 属性: 使用
@
符号来声明属性,例如@vertex
、@fragment
、@location(0)
、@builtin(position)
等等。
WGSL的类型系统包括:
- 标量类型:
f32
(32位浮点数)、i32
(32位整数)、u32
(32位无符号整数)、bool
(布尔值)。 - 向量类型:
vec2f
、vec3f
、vec4f
(浮点数向量)、vec2i
、vec3i
、vec4i
(整数向量)、vec2u
、vec3u
、vec4u
(无符号整数向量)、vec2b
、vec3b
、vec4b
(布尔向量)。 - 矩阵类型:
mat2x2f
、mat3x3f
、mat4x4f
(浮点数矩阵)。 - 数组类型:
array<f32, 10>
(包含10个浮点数的数组)。 - 结构体类型: 自定义的结构体类型。
- 采样器类型:
sampler
、sampler_comparison
。 - 纹理类型:
texture_2d<f32>
、texture_cube<f32>
等等。
WGSL的内置函数:
WGSL提供了丰富的内置函数,用于进行数学运算、纹理采样等等。例如:
abs(x)
:返回x的绝对值。sin(x)
:返回x的正弦值。cos(x)
:返回x的余弦值。normalize(v)
:返回v的单位向量。dot(a, b)
:返回a和b的点积。cross(a, b)
:返回a和b的叉积。textureSample(t, s, coord)
:从纹理t中使用采样器s在坐标coord处采样颜色。
第六章:WebGPU的未来
WebGPU还在不断发展中,未来将会更加强大和完善。
- 更多的特性: WebGPU将会支持更多的特性,比如光线追踪、可变采样率等等。
- 更好的工具: 将会出现更好的WebGPU开发工具,比如调试器、性能分析器等等。
- 更广泛的应用: WebGPU将会被应用到更多的领域,比如游戏开发、虚拟现实、增强现实等等。
总结
WebGPU是浏览器端GPU计算和渲染的新一代API,它具有更现代、更高效、更强大的特点。虽然WebGPU的学习曲线比较陡峭,但只要掌握了它的基本概念和API,就能开发出令人惊艳的Web应用。
希望今天的讲座对大家有所帮助! 感谢各位的观看,下课!