手记

WebGL 与 WebGPU

历史

显卡要安装显卡驱动程序,通过显卡驱动程序暴露的 API 我们就可以操作 GPU 完成图形处理器的操作。问题是,显卡驱动和普通编程界的汇编一样,底层,不好写,于是各大厂就做了封装。于是 OpenGL 应运而生,负责上层接口封装并与下层显卡驱动打交道,但是,众所周知,它的设计风格已经跟不上现代 GPU 的特性了。

上个世纪 90 年代提出了 OpenGL 技术,WebGL 是基于 OpenGL ES 做出来。OpenGL 在那个显卡羸弱的年代发挥了它应有的价值。

Microsoft 为此做出来最新的图形API 是 Direct3D,Apple 为此做出来最新的图形API 是 Metal,有一个有名的组织 -- Khronos 做出来了 Vulkan。这就是现代三大图形 API。

OpenGL 在 2006 年丢给了 Khronos 管,现在各个操作系统基本都没怎么装这个很老的图形驱动了。那么,基于 OpenGL ES 的 WebGL 为什么能跑在各个操作系统的浏览器?因为 WebGL 再往下已经可以不是 OpenGL ES 了,在 Windows 上现在是通过 D3D 转译到显卡驱动的,在 macOS 则是 Metal,只不过时间越接近现在,这种非亲儿子式的实现就越发困难。苹果的 Safari 浏览器最近几年才姗姗支持 WebGL 2.0,而且已经放弃了 OpenGL ES 中 GPGPU 的特性了,或许看不到 WebGL 2.0 的 GPGPU 在 Safari 上实现了。

下一代的 Web 图形接口已经不是 GL 一脉的了,不叫 WebGL 3.0,而是采用了更贴近硬件名称的 WebGPU。WebGPU 从根源上和 WebGL 就不是一个时代的,无论是编码风格还是性能表现上。

WebGL 的编码风格

WebGL 的编程风格延续了 OpenGL 的风格。学习过 WebGL 接口的同学应该用过:gl 变量,即 WebGLRenderingContext 对象,WebGL 2.0 则是 WebGLRenderingContext2。

下面是创建顶点和片元着色器的示例代码:

const vertexShaderCode = `attribute vec4 a_position;
void main() {
    gl_Position = a_position;
}`
 const fragmentShaderCode = `precision mediump float;
 void main() {
     gl_FragColor = vec4(1, 0, 0.5, 1);
}`
 const vertexShader = gl.createShader(gl.VERTEX_SHADER)
 gl.shaderSource(vertexShader, vertexShaderCode)
 gl.compileShader(vertexShader)
 const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
 gl.shaderSource(fragmentShader, fragmentShaderCode)
 gl.compileShader(fragmentShader)
 const program = gl.createProgram()
 gl.attachShader(program, vertexShader)
 gl.attachShader(program, fragmentShader)
 gl.linkProgram(program)
 gl.useProgram(program)// ...

其中创建着色器、赋予着色器代码并编译这些顺序相对固定。

每一次调用 gl 方法时,都会完成 CPU 到 GPU 的信号传递,改变 GPU 的状态。因此该过程的效率底下。

而现代三大图形API 更倾向于先把东西准备好,最后提交给 GPU 的就是一个完整的设计图纸和缓冲数据,GPU 只需要拿着就可以专注办事。


WebGPU 的装配式编码风格

WebGPU 虽然也有一个总管家一样的对象 —— device,类型是 GPUDevice,表示可以操作 GPU 设备的一个高层级抽象,它负责创建操作图形运算的各个对象,最后装配成一个叫 “CommandBuffer(指令缓冲,GPUCommandBuffer)”的对象并提交给队列,这才完成 CPU 这边的劳动。

所以,device.createXXX 创建过程中的对象时,并不会像 WebGL 一样立即通知 GPU 完成状态的改变,而是在 CPU 端写的代码就从逻辑、类型上确保了待会传递给 GPU 的东西是准确的,并让他们按自己的坑站好位,随时等待提交给 GPU。

在这里,指令缓冲对象具备了完整的数据资料(几何、纹理、着色器、管线调度逻辑等),GPU 一拿到就知道该干什么。



更多详情可参考:developer.chrome.com/ar


WebGL 的 gl 变量依赖 HTML 的 Canvas 元素,只能在主线程调度 GPU。WebGL 中的 WebWorker 只能处理数据,无法操作 GPU。

WebGPU 中的 adapter 所依赖的 navigator.gpu 对象在 WebWorker 中也可以访问,所以在 Worker 中也可以创建 device、装配出指令缓冲,从而实现多线程提交指令缓冲,实现 CPU 端多线程调度 GPU 的能力。

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
  return
}
const device = await adapter.requestDevice()
// Get a GPU buffer in a mapped state and an arrayBuffer for writing
const buffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
})
const arrayBuffer = gpuBuffer.getMappedRange();
// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);
 
const texture = device.createTexture({
  /* 装配纹理和采样信息 */
})
 
const pipelineLayout = device.createPipelineLayout({
  /* 创建管线布局,传递绑定组布局对象 */
})
 
/* 创建着色器模块 */
const vertexShaderModule = device.createShaderModule({ /* ... */ })
const fragmentShaderModule = device.createShaderModule({ /* ... */ })
 
/*
计算着色器可能用到的着色器模块
const computeShaderModule = device.createShaderModule({ /* ... * / })
*/
 
const bindGroupLayout = device.createBindGroupLayout({
  /* 创建绑定组的布局对象 */
})
 
const pipelineLayout = device.createPipelineLayout({
  /* 传递绑定组布局对象 */
})
 
/*
上面两个布局对象其实可以偷懒不创建,绑定组虽然需要绑定组布局以
通知对应管线阶段绑定组的资源长啥样,但是绑定组布局是可以由
管线对象通过可编程阶段的代码自己推断出来绑定组布局对象的
本示例代码保存了完整的过程
*/
 
const pipeline = device.createRenderPipeline({
  /*
  创建管线
  指定管线各个阶段所需的素材
  其中有三个阶段可以传递着色器以实现可编程,即顶点、片段、计算
  每个阶段还可以指定其所需要的数据、信息,例如 buffer 等
   
  除此之外,管线还需要一个管线的布局对象,其内置的绑定组布局对象可以
  让着色器知晓之后在通道中使用的绑定组资源是啥样子的
  */
})
 
const bindGroup_0 = deivce.createBindGroup({
  /*
  资源打组,将 buffer 和 texture 归到逻辑上的分组中,
  方便各个过程调用,过程即管线,
  此处必须传递绑定组布局对象,可以从管线中推断获取,也可以直接传递绑定组布局对象本身
  */
})
 
const commandEncoder = device.createCommandEncoder() // 创建指令缓冲编码器对象
const renderPassEncoder = commandEncoder.beginRenderPass() // 启动一个渲染通道编码器
// 也可以启动一个计算通道
// const computePassEncoder = commandEncoder.beginComputePass({ /* ... */ })
 
/*
以渲染通道为例,使用 renderPassEncoder 完成这个通道内要做什么的顺序设置,例如
*/
 
// 第一道绘制,设置管线0、绑定组0、绑定组1、vbo,并触发绘制
renderPassEncoder.setPipeline(renderPipeline_0)
renderPassEncoder.setBindGroup(0, bindGroup_0)
renderPassEncoder.setBindGroup(1, bindGroup_1)
renderPassEncoder.setVertexBuffer(0, vbo, 0, size)
renderPassEncoder.draw(vertexCount)
 
// 第二道绘制,设置管线1、另一个绑定组并触发绘制
renderPassEncoder.setPipeline(renderPipeline_1)
renderPassEncoder.setBindGroup(1, another_bindGroup)
renderPassEncoder.draw(vertexCount)
 
// 结束通道编码
renderPassEncoder.endPass()
 
// 最后提交至 queue,也即 commandEncoder 调用 finish 完成编码,返回一个指令缓冲navigator.gpu.requestAdapter device.queue.submit([
  commandEncoder.finish()
])
跟

WebGPU 优势总结

WebGPU是一种新的Web标准,旨在为Web开发人员提供更高效、更灵活和更现代的图形编程接口。WebGPU具有以下优势:

  1. 更高效:WebGPU使用GPU来加速图形处理,从而提供比传统的Web图形API更高的性能。

  2. 更灵活:WebGPU提供了更灵活的编程模型,使开发人员可以更轻松地控制GPU操作,以及更好地利用GPU并行性。

  3. 更现代:WebGPU利用了现代GPU的功能,包括异步计算、纹理压缩和着色器模块化等特性,这使得它成为开发更现代Web应用程序的理想选择。

  4. 跨平台:WebGPU是一个跨平台的标准,可以在支持WebGPU的任何设备上运行,包括桌面浏览器、移动设备和VR/AR头戴式显示器。

  5. 与Web集成:WebGPU是为Web开发设计的,因此它可以无缝地与其他Web技术集成,包括WebGL、WebVR和WebXR等。

WebGPU 比 WebGL 的优势有哪些

WebGPU 和 WebGL 是两种不同的 Web 图形 API,它们有以下优势对比:

  • 性能:WebGPU 可以更好地利用硬件加速,因此可以提供更高的图形性能,特别是在处理大量数据时。相比之下,WebGL 的性能较低。

  • 功能:WebGPU 提供了更多的功能和灵活性,例如支持计算着色器,反走样等高级渲染技术。而 WebGL 的功能相对较少,并且受到旧的OpenGL ES标准的限制。

  • 设计:WebGPU 基于现代图形API设计,更具可扩展性和可维护性,而 WebGL 则是基于较早的 OpenGL ES 标准设计的。

总体来说,WebGPU 更适合需要高性能和先进渲染技术的场景,而WebGL 则更适合一些简单的 2D 或 3D 渲染需求。

WebGPU和WebAssembly有哪些差异

WebGPU和WebAssembly是两个不同的技术,其主要差异如下:

  1. WebGPU是一种用于Web浏览器中进行高性能图形渲染的新API,而WebAssembly是一种用于在Web浏览器中运行高性能计算密集型应用程序的字节码格式。

  2. WebGPU旨在提供比现有Web图形API更好的性能和效率,而WebAssembly旨在提供比JavaScript更快的执行速度以及更好的可移植性和安全性。

  3. WebGPU需要硬件支持,并且只能在支持WebGPU的浏览器中使用,而WebAssembly可以在任何支持它的Web浏览器中使用。

  4. 尽管WebGPU和WebAssembly都提供了一些新的功能和优势,但它们并不相互排斥,实际上,它们可以一起使用来实现更高效和灵活的Web应用程序。


2人推荐
随时随地看视频
慕课网APP