WebGL 的概念

这篇文章不会再谈论 WebGL 教程:已经有很多优秀的教程了(我们会在最后列出一些)。

我们将只介绍 WebGL 的概念,这些概念基本上是任何通用、低级图形 API(如 OpenGL 或 Direct3D)的概念,针对的目标受众是网页开发者。

什么是 WebGL?

WebGL 是一个网页 API,允许进行低级图形编程。“低级”意味着 WebGL 命令以相对直接映射到 GPU(图形处理单元,即硬件)实际工作方式的术语来表达。这意味着 WebGL 允许你真正利用图形硬件的功能集和能力。原生游戏使用 OpenGL 或 Direct3D 做的事情,你可能也能用 WebGL 做到。

WebGL 非常低级,以至于它甚至不是一个“3D”图形 API,严格来说。就像你的图形硬件并不关心你是在做 2D 还是 3D 图形一样,WebGL 也不关心:2D 和 3D 只是两种可能的用法。1992 年 OpenGL 1.0 发布时,它是一个专门的 3D API,旨在公开那个时代 3D 图形硬件的功能。但随着图形硬件朝着更通用和可编程的方向发展,OpenGL 也随之发展。最终,OpenGL 变得如此通用,以至于 2D 和 3D 只是两种可能的用例,同时仍然提供出色的性能。那是 OpenGL 2.0,而 WebGL 是以它为蓝本的。

这就是我们说 WebGL 是一个低级图形 API 而不是专门的 3D API 的意思。这就是本文的主题;这就是即使你没有计划直接使用 WebGL,学习 WebGL 也很有价值的原因。学习 WebGL 就意味着学习一些图形硬件的工作原理。它可以帮助你直观地了解在任何图形 API 中什么是快什么是慢。

WebGL 上下文和帧缓冲区

在我们能够正确地解释有关 WebGL API 的任何内容之前,我们必须介绍一些基本概念。WebGL 是 HTML Canvas 元素的渲染上下文。你首先为你的画布获取 WebGL 上下文

var gl;
try {
  gl = canvas.getContext("experimental-webgl");
} catch(e) {}

从那里,你通过在gl上调用 WebGL API 函数来执行渲染,该函数在那里获得。WebGL 永远不会单缓冲,这意味着你当前正在渲染的图像永远不会是当前在 Canvas 元素中显示的图像。这确保了半渲染帧永远不会出现在浏览器的窗口中。正在渲染的图像称为WebGL 帧缓冲区后缓冲区。说到帧缓冲区,更复杂的是 WebGL 还允许额外的离屏帧缓冲区,但我们在这篇文章中忽略它。当前正在显示的图像称为前缓冲区当然,后缓冲区的内容最终会被复制到前缓冲区——否则 WebGL 绘制将不会有任何用户可见的效果!

但是,该操作由浏览器自动处理,实际上,WebGL 程序员根本无法显式访问前缓冲区。这里的主要规则是,浏览器可以在任何时候将后缓冲区复制到前缓冲区,除了在执行 JavaScript 时。这意味着你必须在单个 JavaScript 回调中完成一帧的整个 WebGL 渲染。只要你这样做,就可以确保渲染正确,浏览器会为你处理多缓冲合成中的复杂细节。此外,你应该让你的 WebGL 渲染回调成为一个 requestAnimationFrame 回调:如果你这样做,浏览器也会为你处理动画调度的复杂细节。

WebGL 作为一个通用、低级图形 API

我们还没有描述 WebGL 是一个低级图形 API,2D 和 3D 只是两种可能的用法。事实上,这样一个通用的图形 API 存在的可能性本身就意义重大:这个行业花了许多年才发展出这样的 API。

WebGL 允许绘制线段三角形。后者当然是最常使用的方法,所以我们将在本文的其余部分完全关注三角形。

WebGL 的三角形渲染非常通用:应用程序提供一个回调,称为像素着色器片段着色器,它将在三角形的每个像素上被调用,并确定它应该绘制的颜色。

所以假设你正在编写一个老式 2D 游戏。你想要的只是绘制矩形位图图像。由于 WebGL 只能绘制三角形(稍后会详细说明),你将你的矩形分解为两个三角形,如下所示,

A rectangle decomposed as two triangles.

你的片段着色器,即确定每个像素颜色的程序,非常简单:它只从位图图像中读取一个像素,并将其用作当前正在渲染的像素的颜色。

现在假设你正在编写一个 3D 游戏。你已经将你的 3D 形状细分为三角形。为什么是三角形?三角形是最流行的 3D 绘制基元,因为 3D 空间中的任意 3 个点都是一个三角形的顶点。相比之下,你不能只取 3D 空间中的任意 4 个点来定义一个四边形——它们通常无法完全位于同一个平面上。这就是为什么 WebGL 不关心除三角形之外的任何其他类型的多边形。

所以你的 3D 游戏只需要能够渲染 3D 三角形。在 3D 中,将 3D 坐标转换为实际的画布坐标有点棘手——也就是说,确定在画布中的哪个位置应该最终绘制给定的 3D 对象。这里没有一个适合所有人的公式:例如,你可能想要渲染花哨的水下或玻璃折射效果,这将不可避免地需要对每个顶点进行自定义计算。所以 WebGL 允许你提供自己的回调,称为顶点着色器,它将为每个三角形的每个顶点调用,并将确定它应该绘制的画布坐标。

人们自然会期望这些画布坐标是 2D 坐标,因为画布是一个 2D 表面;但它们实际上是 3D 坐标,其中 Z 坐标用于深度测试。仅 Z 坐标不同的两个像素对应于屏幕上的同一个像素,Z 坐标用于确定哪一个隐藏了另一个。所有三个轴都从 -1.0 到 +1.0。重要的是要理解,这是 WebGL 本地理解的唯一坐标系:任何其他坐标系只由你自己的顶点着色器理解,你在其中实现转换为画布坐标的变换。

The WebGL canvas coordinate system.

一旦知道你的 3D 三角形的画布坐标(感谢你的顶点着色器),你的三角形将被绘制,就像上面讨论的 2D 示例一样,由你的片段着色器绘制。然而,在 3D 游戏的情况下,你的片段着色器通常比 2D 游戏中的更复杂,因为 3D 游戏中的有效像素颜色并不像静态数据那样容易确定。各种效果,如灯光,可能会影响像素在屏幕上的有效颜色。在 WebGL 中,你必须自己实现所有这些效果。好消息是你能做到:如上所述,WebGL 允许你指定自己的回调,即片段着色器,它确定每个像素的有效颜色。

因此,我们看到 WebGL 是一个通用的 API,可以满足 2D 和 3D 应用程序的需求。通过让你指定任意顶点着色器,它允许实现任意的坐标变换,包括 3D 游戏需要执行的复杂变换。通过接受任意片段着色器,它允许实现任意的像素颜色计算,包括 3D 游戏中发现的微妙灯光效果。但是 WebGL API 并不特定于 3D 图形,可以用来实现几乎任何类型的实时 2D 或 3D 图形——它可以一直扩展到 1980 年代的单色位图或线框游戏,如果你想要的话。WebGL 无法实现的唯一事情是需要利用高端图形硬件最近添加的功能的最密集渲染技术。即使这样,计划也是在必要时不断改进 WebGL 的功能集,以保持可移植性和功能之间的正确平衡。

WebGL 渲染管道

到目前为止,我们已经讨论了 WebGL 工作原理的一些方面,但主要是间接地。幸运的是,用系统的方法来解释 WebGL 渲染的流程并不需要更多。

这里的关键比喻是管道。理解它很重要,因为它是所有当前图形硬件的通用功能,理解它将帮助你本能地编写更利于硬件的代码,因此运行速度更快。

GPU 是大规模并行处理器,由大量计算单元组成,这些单元旨在彼此并行工作,并与 CPU 并行工作。即使在移动设备中也是如此。考虑到这一点,WebGL 等图形 API 被设计成天生对这种并行架构友好。在典型工作负载中,并且在正确使用时,WebGL 允许 GPU 与任何 CPU 端工作并行执行图形命令,即 GPU 和 CPU 不必相互等待;并且 WebGL 允许 GPU 最大限度地发挥其并行处理能力。为了允许在 GPU 上运行,这些着色器是用专门的 GPU 友好语言编写的,而不是用 JavaScript 编写的。为了允许 GPU 同时运行多个着色器,着色器只是处理单个顶点或单个像素的回调——这样 GPU 就可以自由地在任何 GPU 执行单元上以任何顺序运行着色器。

以下图表总结了 WebGL 渲染管道

The WebGL rendering pipeline

应用程序设置其顶点着色器和片段着色器,并向 WebGL 提供这些着色器需要读取的任何数据:描述要绘制的三角形的顶点数据,位图数据(称为“纹理”),片段着色器将使用它。设置完成后,渲染通过为每个顶点执行顶点着色器开始,该着色器确定三角形的画布坐标;然后对生成的三角形进行光栅化,这意味着确定要绘制的像素列表;然后为每个像素执行片段着色器,确定其颜色;最后,一些帧缓冲区操作确定此计算的颜色如何影响此位置的最终帧缓冲区的像素颜色(这个最后阶段是实现深度测试和透明度等效果的地方)。

GPU 端内存与主内存

一些 GPU,尤其是在台式机上,使用它们自己的内存,该内存与主内存分离。其他 GPU 与系统的其余部分共享相同的内存。作为 WebGL 开发人员,您无法知道正在运行的系统类型。但这并不重要,因为 WebGL 强制您以专用的 GPU 内存为单位进行思考。

从实际角度来看,重要的是

  • WebGL 渲染数据必须首先被 *上传* 到特殊的 WebGL 数据结构中。上传意味着将数据从通用内存复制到 WebGL 特定的内存。这些特殊的 WebGL 数据结构称为 WebGL *纹理*(位图图像)和 WebGL *缓冲区*(通用字节数组)。
  • 一旦数据上传,渲染速度非常快。
  • 但上传数据通常很慢。

换句话说,将 GPU 想象成一台非常快的机器,但它离得很远。只要该机器能够独立运行,它就非常高效。但是从外部与它通信需要很长时间。因此您希望提前完成大多数通信,以便大多数渲染能够独立且快速地进行。

并非所有 GPU 都真正与系统的其余部分隔离——但 WebGL 强制您以这种方式思考,以便您的代码无论给定客户端使用何种特定 GPU 架构都能高效运行。WebGL 数据结构抽象了专用 GPU 内存的可能性。

一些导致图形速度变慢的原因

最后,我们可以从上面提到的内容中得出一些关于什么可能导致图形速度变慢的一般想法。这绝不是一个详尽的列表,但它确实涵盖了一些最常见的慢速原因。我们的想法是,这些知识对任何接触过图形代码的程序员来说都是有用的——无论他们是否使用 WebGL。从这个意义上说,学习 WebGL 周围的一些概念对于不仅仅是 WebGL 编程非常有用。

使用 CPU 进行图形处理速度很慢

GPU 出现在所有当前客户端系统中的原因,以及它们与 CPU 如此不同的原因是有原因的。要进行快速图形处理,您确实需要 GPU 的并行处理能力。不幸的是,在浏览器引擎中自动使用 GPU 是一项艰巨的任务。浏览器供应商尽最大努力在适当的地方使用 GPU,但这是一个难题。通过使用 WebGL,您将为您的内容承担起这个问题的责任。

让 GPU 和 CPU 相互等待速度很慢

GPU 被设计成能够与 CPU 并行运行,独立运行。无意中导致 GPU 和 CPU 相互等待是导致速度变慢的常见原因。一个典型的例子是读回 WebGL 帧缓冲区的内容(WebGL readPixels 函数)。这可能需要 CPU 等待 GPU 完成任何排队的渲染,然后也可能需要 GPU 等待 CPU 接收数据。因此,只要您能做到,将 WebGL 帧缓冲区视为只写介质。

向 GPU 发送数据可能很慢

如上所述,GPU 内存由 WebGL 数据结构(如纹理)抽象。这些数据最好只上传一次到 WebGL,然后使用多次。过于频繁地上传新数据是导致速度变慢的典型原因:上传本身很慢,如果您在使用数据渲染之前上传数据,GPU 必须等待数据才能继续渲染——因此您实际上将渲染速度限制在缓慢的内存传输上。

小的渲染操作速度很慢

GPU 旨在用于一次绘制大量三角形。如果您有 10,000 个三角形要绘制,则一次性进行(如 WebGL 允许的)将比进行 10,000 次单独的单三角形绘制操作快得多。将 GPU 想象成一台预热时间很长的非常快的机器。最好预热一次并进行大量工作,而不是多次支付预热成本。将渲染组织成大批量确实需要一些思考,但这是值得的。

在哪里学习 WebGL

我们故意没有在这里编写教程,因为已经有许多优秀的教程了

我还想提一下 我做的那个演讲,因为它有一些特别 简洁 示例

关于 Robert Nyman [荣誉编辑]

Mozilla 技术布道师和 Mozilla Hacks 编辑。进行有关 HTML5、JavaScript 和开放网络的演讲和博客。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直在从事网络前端开发工作——在瑞典和纽约市。他还定期在 http://robertnyman.com 上发表博客,并且喜欢旅行和结识新朋友。

更多 Robert Nyman [荣誉编辑] 的文章…


11 条评论

  1. Gervase Markham

    这篇文章非常有用。

    2013 年 4 月 22 日 下午 3:21

    1. Benoit Jacob

      谢谢!

      2013 年 4 月 22 日 下午 4:33

  2. Ondřej Žára

    非常棒且实用的文章。谢谢!

    2013 年 4 月 22 日 上午 5:01

  3. 法国读者

    @ Benoit Jacob:我希望我们能用您的母语阅读它:法语。也许在 https://linuxfr.org/users/bjacob 中?

    2013 年 4 月 22 日 上午 8:07

    1. Benoit Jacob

      哈哈,也许如果我有时间。其他人也可以这样做。

      2013 年 4 月 22 日 上午 9:13

  4. andbas

    对于任何开始使用 WebGL 进行开发的人来说,这篇文章绝对是必读的。谢谢!

    2013 年 4 月 22 日 下午 12:44

  5. Owen Densmore

    是的,同意。

    但还有另一篇文章在等着!我一直在和 Ed Angel 谈论 webgl,以及他如何将他的畅销教科书改用 webgl。我们一直在研究 Three.js 和其他库。但在教科书中,您需要使用核心技术。这提出了一个问题

    一个大问题是如何管理程序、着色器、缓冲区、GPU 纹理、通过帧缓冲区对象进行双缓冲等的细节。与其说它们的功能,不如说当您有多个程序对象及其自己的或共享的着色器时,如何管理它们。如何激活/停用对象。还要按什么顺序执行操作才能避免出现黑屏!

    我们不确定是否可能存在一个比 Three.js 低得多的 API,它只以清晰、有效和安全的方式管理原始 webgl。

    2013 年 4 月 24 日 上午 9:33

  6. devendran

    真的对我很有帮助

    2013 年 4 月 25 日 上午 6:02

  7. Mathew Porter

    嗯,这帮我解释了 WebGL,很高兴知道为了提高性能,应该避免小批量渲染,而应使用大批量渲染。

    2013 年 4 月 25 日 下午 12:42

  8. Igorian

    谢谢,解释得很好。

    2013 年 4 月 30 日 上午 5:13

  9. vsvankhede

    谢谢分享这篇文章!

    2013 年 5 月 8 日 下午 11:38

本文的评论已关闭。