WebGL 脱离主线程

我们很高兴宣布 Firefox 44+ 中的 Web Workers 中的 WebGL!使用新的 OffscreenCanvas API ,您现在可以创建脱离主线程的 WebGL 上下文。

要继续操作,您需要一个 Firefox 44 或更高版本的副本(目前是 Firefox 开发者版Firefox Nightly)。您需要通过在 Firefox 中导航到 about:config,搜索 gfx.offscreencanvas.enabled 并将其设置为 true 来启用此 API。您可以从 GitHub 获取代码示例 或在 Firefox 44+ 中预览它 这里,将 gfx.offscreencanvas.enabled 设置为 true。此功能在 Windows 上尚不可用,等待 ANGLE 支持。 AlteredQualia 指出 这些功能在 Windows/FF Nightly 46 上运行良好。我应该为没有验证而感到羞愧!

用例

此 API 是第一个允许除主线程之外的线程更改显示给用户的内容的 API。这使得无论主线程正在进行什么操作,渲染都能继续进行。您可以在 工作组的正在进行的规范 中看到更多用例。

代码更改

让我们看一下 来自 我的 Raw WebGL 演讲 的 WebGL 动画基本示例。我们将移植此代码以在工作线程中运行,而不是在主线程中运行。

WebGL in Workers

第一步是将所有代码从 WebGL 上下文创建移动到绘制调用到一个单独的文件中。


<script src="gl-matrix.js"></script>
<script>
  // main thread
  var canvas = document.getElementById('myCanvas');
  ...
  gl.useProgram(program);
  ...

变成


// main thread
var canvas = document.getElementById('myCanvas');
if (!('transferControlToOffscreen' in canvas)) {
  throw new Error('webgl in worker unsupported');
}
var offscreen = canvas.transferControlToOffscreen();
var worker = new Worker('worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
...

认识到我们正在调用 HTMLCanvasElement.prototype.transferControlToOffscreen,然后将其传输到新构造的工作线程。transferControlToOffscreen 返回一个新对象,该对象是 OffscreenCanvas 的实例,而不是 HTMLCanvasElement。虽然相似,但您无法访问诸如 offscreen.clientWidthoffscreen.clientHeight 之类的属性,但您可以访问 offscreen.widthoffscreen.height。通过将其作为第二个参数传递给 postMessage,我们将该变量的所有权转移到第二个线程。

现在在工作线程中,我们将等待接收来自主线程的带有画布元素的消息,然后再尝试获取 WebGL 上下文。获取 WebGL 上下文、创建和填充缓冲区、获取和设置属性和统一变量以及绘制的代码不会改变。


// worker thread
importScripts('gl-matrix.js');

onmessage = function (e) {
  if (e.data.canvas) {
    createContext(e.data.canvas);
  }
};

function createContext (canvas) {
  var gl = canvas.getContext('webgl');
  ...

OffScreenCanvas 只向 WebGLRenderingContext.prototype 添加了一个新方法,名为 commit。commit 方法将渲染的图像推送到创建用于 WebGL 上下文的 OffscreenCanvas 的画布元素。

动画同步

现在要使代码动画化,我们可以使用 postMessage 将主线程的 requestAnimationFrame 定时代理到工作线程。


// main thread
(function tick (t) {
  worker.postMessage({ rAF: t });
  requestAnimationFrame(tick);
})(performance.now());

和工作线程中的 onmessage 变成


// worker thread
onmessage = function (e) {
  if (e.data.rAF && render) {
    render(e.data.rAF);
  } else if (e.data.canvas) {
    createContext(e.data.canvas);
  }
};

我们的渲染函数现在有一个最终的 gl.commit(); 语句,而不是设置另一个 requestAnimationFrame 循环。


// main thread
function render (dt) {
  // update
  ...
  // render
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
  requestAnimationFrame(render);
};

变成


// worker thread
function render (dt) {
  // update
  ...
  // render
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
  gl.commit(); // new for webgl in workers
};

这种方法的局限性

虽然在示例代码中,我没有进行正确的基于速度的动画(通过不使用从 requestAnimationFrame 传递的值,我正在进行帧速率依赖的动画,而不是更正确的基于速度的动画,它是帧速率独立的),但我们仍然在这个方法中遇到了问题。

假设我们将渲染逻辑移出了主线程,以避免 JavaScript 虚拟机 (JVM) 垃圾收集器 (GC) 暂停导致的暂停。主线程上的 GC 暂停会减慢 requestAnimationFrame 的调用速度。由于对 gl.drawArraysgl.commit 的调用是由主线程上的 requestAnimationFrame 循环中的 postMessages 在工作线程中异步触发的,因此主线程上的 GC 暂停会阻塞工作线程上的渲染。注意:主线程上的 GC 暂停不应阻塞工作线程的进度(至少在 Firefox 的 SpiderMonkey 虚拟机中不会)。在 SpiderMonkey 中,GC 暂停是每个工作线程的。

虽然我们可以尝试在工作线程中做一些巧妙的事情来解决这个问题,但解决方案是使 requestAnimationFrame 在工作线程上下文中可用。跟踪此工作的错误可以在这里找到 这里

总结

由于新的 OffscreenCanvas API,开发人员现在能够在不阻塞主线程的情况下渲染到屏幕上。获取工作线程上的 requestAnimationFrame 还有更多工作要做。我能够在几分钟内将现有的 WebGL 代码移植到工作线程中运行。作为比较,请参阅 animation.htmlanimation-worker.htmlworker.js

关于 Nick Desaulniers

更多由 Nick Desaulniers 撰写的文章…


6 评论

  1. Brian Gavin

    这可能是一个有用的功能。其他 Web 浏览器是否也在考虑这样做?

    2016 年 1 月 24 日 上午 05:02

  2. Zimondai

    这就是我一直在等待的功能!!!!!
    希望它很快就能得到其他浏览器的支持。

    目前我使用一个类来提供所有 Canvas 方法和属性来模拟 Web 工作线程中的 `Canvas`,以生成一个命令列表数组,该数组将被发送到主线程以进行实际绘制。这很脏,但效果很好。

    2016 年 1 月 24 日 下午 17:45

  3. Morris Tseng

    当有多个工作线程同时使用 OffscreenCanvas 时,ANGLE 会出现问题(程序会崩溃)。如果只有一个工作线程,则没有问题。

    2016 年 1 月 27 日 下午 19:44

  4. Gordon Rankin

    这太棒了,我非常兴奋地开始玩它。但我仍然想知道我们应该如何管理交互性?目前,我们在主线程中使用一个由许多 javascript 对象组成的场景图。鼠标/触摸事件直接操作对象的定位和大小,以及更改各种着色器统一变量等。我们的渲染在每一帧中都通过它们,尽可能地对它们进行批处理,然后绘制它们,无论是精灵、图元还是其他任何东西。

    使用 Offscreen 方法,我们是否应该继续在主线程中管理我们所有可渲染的对象(定位、缩放、大小等),按照我们现在通过鼠标/触摸事件的方式操作它们,然后简单地将所有 webgl 命令代理并发送到工作线程?

    还是我们应该将整个 javascript 场景和对象类保留在工作线程本身中,并将鼠标和触摸事件代理到工作线程中,以操作场景及其对象?

    我非常喜欢这个想法,但我对如何利用它来构建需要交互性的大型复杂游戏/应用程序,以及大量使用场景图的应用程序感到困惑。

    2016 年 1 月 28 日 上午 08:27

  5. Niels Rood

    @Gordon Rankin 我还没有尝试过,但我认为最好将尽可能多的渲染代码移到工作线程中,以减少主线程的负载。因此,我会将鼠标和触摸事件所需的信息直接通过 postMessage 传递给工作线程。使用可转移对象可能可以减少 postMessage 开销。

    非常棒的技术!期待它在所有浏览器中都可用的时候 :)

    2016 年 1 月 29 日 上午 03:10

  6. Alex Bell

    这里是一个非常强大的功能,谢谢。是否有对此的规范?是否有 Chromium 错误/表达的兴趣?是否有 FF 关于默认公开的发布时间表?

    2016 年 2 月 8 日 下午 12:49

本文的评论已关闭。