历史
显卡要安装显卡驱动程序,通过显卡驱动程序暴露的 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 一拿到就知道该干什么。
更多详情可参考:https://developer.chrome.com/articles/gpu-compute/
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具有以下优势:
更高效:WebGPU使用GPU来加速图形处理,从而提供比传统的Web图形API更高的性能。
更灵活:WebGPU提供了更灵活的编程模型,使开发人员可以更轻松地控制GPU操作,以及更好地利用GPU并行性。
更现代:WebGPU利用了现代GPU的功能,包括异步计算、纹理压缩和着色器模块化等特性,这使得它成为开发更现代Web应用程序的理想选择。
跨平台:WebGPU是一个跨平台的标准,可以在支持WebGPU的任何设备上运行,包括桌面浏览器、移动设备和VR/AR头戴式显示器。
与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是两个不同的技术,其主要差异如下:
WebGPU是一种用于Web浏览器中进行高性能图形渲染的新API,而WebAssembly是一种用于在Web浏览器中运行高性能计算密集型应用程序的字节码格式。
WebGPU旨在提供比现有Web图形API更好的性能和效率,而WebAssembly旨在提供比JavaScript更快的执行速度以及更好的可移植性和安全性。
WebGPU需要硬件支持,并且只能在支持WebGPU的浏览器中使用,而WebAssembly可以在任何支持它的Web浏览器中使用。
尽管WebGPU和WebAssembly都提供了一些新的功能和优势,但它们并不相互排斥,实际上,它们可以一起使用来实现更高效和灵活的Web应用程序。