JS `WebGPU`:浏览器端的 GPU 计算与渲染引擎深度

各位观众老爷,大家好!今天咱们不聊风花雪月,聊点硬核的——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(布尔值)。
  • 向量类型: vec2fvec3fvec4f(浮点数向量)、vec2ivec3ivec4i(整数向量)、vec2uvec3uvec4u(无符号整数向量)、vec2bvec3bvec4b(布尔向量)。
  • 矩阵类型: mat2x2fmat3x3fmat4x4f(浮点数矩阵)。
  • 数组类型: array<f32, 10>(包含10个浮点数的数组)。
  • 结构体类型: 自定义的结构体类型。
  • 采样器类型: samplersampler_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应用。

希望今天的讲座对大家有所帮助! 感谢各位的观看,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注