Web 端的下一代三维图形

今天,苹果 WebKit 团队提议在 W3C 成立一个新的社区群组(Community Group)来讨论 Web 端三维图形的将来和开发一款支持现代 GPU 特性(包括底层图像处理和通用计算)的标准 API。W3C 社区容许你们自由参与进来,并且咱们也诚邀浏览器开发商、GPU 硬件提供商、软件开发者和 Web 社区加入咱们html

权当抛砖引玉,咱们分享了一个 API 提案和一个针对 WebKit 开源项目的 API 原型。咱们但愿这是一个有益的开始,并期待随着社区讨论的进行 API 会不断发展进化。前端

更新:如今有一个实现和演示 WebGPU 的 demoreact

让咱们来看看咱们成立这个社区群组的来龙去脉,以及这个新组与现有 Web 图形 API(如 WebGL)的关系。android

首先谈点历史问题

有一段时间,基于 Web 标准的技术能够生成具备静态内容的页面,而其中惟一的图形则是嵌入的图片。不久以后,Web 开始增长更多开发人员能够经过 JavaScript 访问的功能。最终,咱们须要一个彻底可编程的图形 API,以使脚本能够实时建立图像。所以,“canvas” 元素及其相关的 2D 渲染 API 诞生于 WebKit,随后迅速普及到其余浏览器引擎中,而且很快标准化了。ios

随着时间的推移,Web 应用程序和内容渐趋丰富和复杂,并开始触及平台的瓶颈。以游戏为例,其性能和视觉质量相当重要。在浏览器中开发游戏的需求是有的,但大多数游戏使用的是 GPU 提供的 3D 图形 API。Mozilla 和 Opera 公布了一些从 “canvas” 元素中暴露出 3D 渲染上下文的实验,其结果很是具备吸引力,所以社区决定一块儿将你们均可以实现的内容进行标准化。git

全部的浏览器引擎协做建立了 WebGL,这是在 Web 上渲染 3D 图形的标准。它基于 OpenGL ES —— 一种面向嵌入式系统的跨平台图形 API。这个起点是正确的,由于它能够轻松地在全部浏览器中实现相同的 API,并且大多数浏览器引擎都在支持 OpenGL 的系统上运行。即便系统没有直接支持 OpenGL,像 ANGLE 这样的项目也能够在其余技术之上进行仿真,毕竟这种 API 的抽象级别是很高的。随着 OpenGL 的发展,WebGL 也能够跟着发展。github

WebGL 已经在开放平台上赋予了开发人员图形处理器的功能,全部主流浏览器都支持 WebGL 1,使得能够在 Web 上开发出高质量的游戏(console-quality games),也促进了 three.js 等第三方库的蓬勃发展。此后,该标准发展成为 WebGL 2,包括 WebKit 在内的全部主流浏览器引擎都承诺对它提供支持。web

接下来呢?

在 WebGL 发展的同时,GPU 技术也在发展进步,并且已经建立了新的软件 API,可以更好地反映现代 GPU 的设计特性。这些新 API 的抽象级别比较低,而且因为其下降了开销,一般来讲比 OpenGL 的性能更好。该领域的主要技术平台有微软的 Direct3D 十二、苹果的 Metal 和 Khronos Group 的 Vulkan。虽然这些技术的设计理念都是类似的,但惋惜的是没有一项技术是跨平台可用的。编程

那么这对 Web 意味着什么呢?从充分利用 GPU 的角度来说,这些新技术无疑是将来的发展方向。Web 平台想要成功必须定义一种容许多个系统上实现的通用标准,而如今已经有几个在架构上稍有差异的图形 API 了。要开发一款能够加速图形和计算的现代化底层技术,必须设计一个能够在多种系统(包括上面提到的那些系统)上实现的 API。随着图形技术的蓬勃发展,继续遵循像 OpenGL 这样的某个特定 API 标准显然是不可行的。canvas

相反,咱们须要评估和设计一个新的 Web 标准:它可以提供一组核心功能,以及一个支持多种系统图形技术和平台的 API,此外还要保障 Web 所要求的保密性和安全性。

再者,咱们还须要考虑如何在图形处理以外使用 GPU,以及新标准如何与其余 Web 技术协同工做。该标准应该暴露现代 GPU 的通用计算功能。其设计架构应符合 Web 的既定模式以便开发和使用。它须要可以与其余重要的新兴 Web 标准(如 WebAssembly 和 WebVR)协同工做。最重要的是,这个标准的制定应该是一个开放的过程,容许行业专家和更普遍的网络社区参与。

W3C 为这种状况提供了社区群组平台。“Web 端的 GPU” 社区群组现已开放会员注册。

WebKit 的初始 API 提案

几年前咱们就预估了下一代图形 API 的发展状况,并着手在 WebKit 中设计原型以验证咱们能够将很是低级别的 GPU API 暴露给 Web 同时还能够得到有价值的性能提高。咱们获得了一些很是鼓舞人心的实验结果,因此咱们将原型分享给了 W3C 社区群组。咱们也准备将代码部署到 WebKit 中,因此你很快就能够本身去尝试了。咱们并不奢望这一 API 自己能成为最后的标准,社区也有可能根本就不会从它入手,可是咱们认为编写代码的工做自己是颇有价值的。其余浏览器引擎也已经开发了相似的原型。与社区合做并为计算机图形提出一个伟大的新技术想必是一件十分使人激动的事情。

下文将详细阐述咱们的实验,咱们将它称为 “WebGPU”。

获取渲染上下文(Rendering Context)和渲染管道(Rendering Pipeline)

不出意料,WebGPU 的接口是经过 “canvas” 元素来访问的。

let canvas = document.querySelector("canvas");
let gpu = canvas.getContext("webgpu");复制代码

WebGPU 比 WebGL 要更加面向对象化,事实上这也是性能提高的原因之一。WebGPU 容许你建立和存储表示状态的对象和能够处理一组命令的对象,而无需在每次绘制操做以前设置状态。这样,咱们能够在状态建立时就执行一些验证工做,从而减小绘图时的工做量。

WebGPU 上下文暴露了图形命令和并行计算命令。假设须要绘制一些图形,这须要用到图形管道。图形管道中最重要的元素是着色器(shaders),它们是在 GPU 上运行用以处理几何数据并为每一个像素的绘制提供颜色的程序。着色器一般用专门用于图形的编程语言进行编写。

决定 Web API 使用何种着色语言是件有趣的事情,由于有不少因素须要考虑。咱们须要一种功能强大的语言,要求编程尽可能简单、能序列化为可高效传输的格式,并要求能够由浏览器进行验证以确保着色器的安全性。业内有部分人倾向于使用能够从许多源格式生成的着色器表示,这有点相似于汇编语言。同时,在“查看源代码”方面 Web 可谓发展迅速,对人而言代码的可读性仍是很重要的。咱们指望关于着色语言的讨论成为标准化过程当中最有趣的部分之一,咱们也十分愿意听取社区的意见。

就 WebGPU 原型而言,咱们决定暂不考虑着色语言的问题,而是直接采用一种现存的语言。由于咱们当时的工做是创建在苹果的平台上的,因此咱们选择了Metal Shading Language。那接下来的问题就是如何将着色器加载到 WebGPU 了。

let library = gpu.createLibrary( /* 源代码 */ );

let vertexFunction = library.functionWithName("vertex_main");
let fragmentFunction = library.functionWithName("fragment_main");复制代码

咱们使用 gpu 对象从源代码加载并编译着色器,生成一个 WebGPULibrary。着色器代码自己并不重要 —— 其实就是一个很是简单的顶点(vertex)和片断(fragment)的组合。一个 WebGPULibrary 能够容纳多个着色器函数,所以咱们经过函数名称取出将要在管道中用到的相应函数。

如今咱们就能够建立管道了。

// 管道的一些细节。
let pipelineDescriptor = new WebGPURenderPipelineDescriptor();
pipelineDescriptor.vertexFunction = vertexFunction;
pipelineDescriptor.fragmentFunction = fragmentFunction;
pipelineDescriptor.colorAttachments[0].pixelFormat = "BGRA8Unorm";

let pipelineState = gpu.createRenderPipelineState(pipelineDescriptor);复制代码

传入所需描述信息(包括使用的顶点、片断着色器以及图像格式)便可从上下文中获得一个新的 WebGPURenderPipelineState 对象。

缓冲区(Buffers)

绘图操做要求使用缓冲区向渲染管道提供数据,例如几何坐标、颜色、法向量等等,而 WebGPUBuffer 则是容纳这些数据的对象。

let vertexData = new Float32Array([ /* some data */ ]);
let vertexBuffer = gpu.createBuffer(vertexData);复制代码

此例中,咱们有一个 Float32Array,它包含了须要在几何图形中绘制的每一个顶点的数据。咱们从 Float32Array 建立一个 WebGPUBuffer,该缓冲区会在以后的绘图操做中用到。

诸如此类的顶点数据不多发生变化,但也有些数据是几乎每次绘制时都会发生变化的。像这种不变的数据被称为 uniforms。表示相机位置的当前变换矩阵便是 uniform 的一个很常见的例子。WebGPUBuffer 也可用于 uniform,但此处咱们但愿在建立以后将其写入缓冲区。

// 将 "buffer" 看做是一个以前分配好的 WebGPUBuffer。
// buffer.contents 暴露一个 ArrayBufferView,咱们将其
// 解析为一个 32 位的浮点数数组。
let uniforms = new Float32Array(buffer.contents);

// 设置所需 uniform。
uniforms[42] = Math.PI;复制代码

这样作的好处之一是 JavaScript 开发人员能够将 ArrayBufferView 封装在带有自定义 getter 和 setter 的类或代理对象(Proxy object)中,这样外部接口看起来像典型的 JavasScript 对象同样。而后,包装器对象会更新缓冲区正在使用的底层数组中的相应部分。

绘图(Drawing)

在通知 WebGPU 上下文绘图以前还须要设置一些状态,这包括渲染的目标位置(最终将在 canvas 中显示的 WebGPUTexture)以及纹理(texture)初始化和使用状况的描述信息。这些状态存储在 WebGPURenderPassDescriptor 中。

// 从上下文获取下一帧所指望的纹理信息。
let drawable = gpu.nextDrawable();

let passDescriptor = new WebGPURenderPassDescriptor();
passDescriptor.colorAttachments[0].loadAction = "clear";
passDescriptor.colorAttachments[0].storeAction = "store";
passDescriptor.colorAttachments[0].clearColor = [0.8, 0.8, 0.8, 1.0];
passDescriptor.colorAttachments[0].texture = drawable.texture;复制代码

首先,咱们向 WebGPU 上下文请求一个表示下一可绘帧的对象,此对象最终会被复制到 canvas 元素中去。完成绘图代码后,咱们要通知 WebGPU 以便其显示绘图结果并准备下一个可绘帧。

从初始化 WebGPURenderPassDescriptor 的代码中能够看出,咱们不会在绘图操做正在进行的时候从纹理中读取信息(由于 loadAction 的值是 clear),而是在绘图操做完成以后才使用该纹理(由于 storeAction 的值是 store),此外代码还指定了纹理的填充颜色。

接下来,咱们建立用于保存实际绘制操做的对象。一个 WebGPUCommandQueue 有一组 WebGPUCommandBuffers。咱们使用 WebGPUCommandEncoder 将操做推送到 WebGPUCommandBuffer 中去。

let commandQueue = gpu.createCommandQueue();
let commandBuffer = commandQueue.createCommandBuffer();

// 使用以前建立的描述符。
let commandEncoder = commandBuffer.createRenderCommandEncoderWithDescriptor(
                        passDescriptor);

// 告知编码器使用何种状态(例如:着色器)。
commandEncoder.setRenderPipelineState(pipelineState);

// 最后,编码器还须要知道使用哪一个缓冲区。
commandEncoder.setVertexBuffer(vertexBuffer, 0, 0);复制代码

至此,咱们已经设置好了一个渲染管道,其中包含若干着色器、一个用于保存几何信息的缓冲区、一个用于保存绘制操做的队列以及一个能够提交到该队列的编码器。如今只需将实际绘图命令推入编码器便可。

// 咱们知道咱们的缓冲区有 3 个顶点,
// 咱们但愿绘制出一个填充的三角形。
commandEncoder.drawPrimitives("triangle", 0, 3);
commandEncoder.endEncoding();

// 全部绘图命令已经提交。通知 WebGPU
// 一旦队列处理完毕即刻显示 canvas 中的绘图结果。
commandBuffer.presentDrawable(drawable);
commandBuffer.commit();复制代码

像大多数 3D 图形的示例代码同样,绘制一个简单的形状看起来要写不少代码,但其实并不是如此。这些现代 API 有一个优势 —— 其大部分代码都是在建立能够重用以绘制其余内容的对象。例如,通常渲染上下文只须要一个 WebGPUCommandQueue 实例,又者能够为不一样的着色器提早建立多个 WebGPURenderPipelineState 对象。此外,浏览器还能够在前期进行不少验证工做,从而减小绘图操做过程当中的开销。

但愿本文可让你对 WebGPU 提案有一个大体了解。尽管由 W3C 社区群组最终肯定的 API 可能同此提案有很大不一样,但咱们相信不少通常的设计原则都是通用的。

公开邀请

苹果的 WebKit 团队已经建议为 Web 端 GPU 创建一个 W3C 社区群组做为工做论坛,同时也请你加入咱们一块儿定义 GPU 的下一代标准。咱们的建议获得了其余浏览器引擎开发商、GPU 供应商、框架开发人员等业内同仁的积极回应。在行业的支持下,咱们诚邀全部对 Web GPU 感兴趣或有专长的人加入社区群组。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索