【译】在屏幕外渲染 3d:使用画布工作者获得最大性能

118 阅读10分钟

在屏幕外进行3D渲染。使用画布工作者获得最大性能

Tobias Uhlig

Tobias Uhlig

关注

8月6日- 9分钟阅读

OffscreenCanvas Web API使我们能够将Canvas DOM元素的所有权转移到worker中。

由于工作者在独立的线程中运行(如果可能的话,使用自己的 CPU),这意味着昂贵的应用程序相关的 JS 逻辑不会降低 Canvas 的渲染性能,反之亦然:Canvas 的昂贵逻辑不会影响应用程序的其他部分。

良好的用例是图像处理以及渲染互动图表或地图。由于 Canvas 也支持 WebGL,因此使用屏外概念创建一个游戏引擎也是有意义的。

内容

  1. 简介
  2. 浏览器支持
  3. 框架相关的增强功能
  4. 夸奖
  5. 创建基于neo.mjs的演示应用程序
  6. 创建自定义的WebGlComponent
  7. 研究canvas.Helper类
  8. 在浏览器中直接使用d3
  9. 演示视频
  10. 在线演示
  11. 最后的思考

1.简介

工作者不能访问DOM →window 以及window.document 都没有定义。

在没有这个API的情况下,实现多线程是有点困难的。neo.mjs项目以一种好的方式解决了这个问题。

使用一个应用工作者作为主要角色,让主线程尽可能地闲置。虚拟DOM是实现这一目标的必要条件,因为你的应用程序(包括你的组件)生活在App worker里面。

为了最大限度地利用OffscreenCanvas Web API,我们需要以下列方式增强工作者的设置。

"结合离屏画布和应用程序工作者 "也是本文的合适标题,但我们还将介绍在此设置中创建一个漂亮的基于WebGL的演示应用程序。

2.浏览器支持

Chromium(Chrome,Edge)已经很好地支持新的API。

虽然兼容性表一开始看起来很吓人。

重要的是要知道,Mozilla(火狐)和Webkit(Safari)团队正在积极推动这个话题。

[

1390089 - (offscreen-canvas) [meta] OffscreenCanvas

Core中的新(无人)--Canvas。WebGL。最后更新于2021-07-29。

bugzilla.mozilla.org

](bugzilla.mozilla.org/show_bug.cg…)

估计。"最好是在今年"

[

183720 - 完成OffscreenCanvas的实现

Bug 183720:完成OffscreenCanvas的实现

bugs.webkit.org

](bugs.webkit.org/show_bug.cg…)

"现在bug 224178已经落地,我相信所有主要的OffscreenCanvas功能都已实现,并在Linux平台上启用。还有相当多的工作要做,以巩固和优化这个实现,当然还有在所有平台上启用它,但基础是存在的。好哇!"

我们将需要时间来创造惊人的实现,所以现在开始是有意义的。

3.框架相关的增强

我们首先需要的是新的画布工作者:
src/worker/Canvas.mjs

这里的关键部分是使用afterConnect() 方法来创建一个新的MessageChannel ,它在App和Canvas工作者之间建立了直接连接(postMessages不需要通过主线程传递)。

我们将Canvas节点存储在一个map 配置中,使用
DOM(===组件id)作为关键。

我们还需要创建一个新的组件类:
src/component/Canvas.mjs

我们使用的是offscreen 配置,默认为true

在一个新的canvas组件被安装后,afterSetMounted() 方法将被触发。如果offscreen 被设置为 "true",它将请求画布节点的所有权,并将其传递给画布工作者(它将把它存储在刚刚提到的地图中。

之后,offscreenRegistered 配置将被设置为真,这样我们就可以使用afterSetOffscreenRegistered() 作为我们自己的组件实现的入口。

任务完成了。

4.嘉奖

由于我不想涵盖所有关于如何使用WebGL的逻辑(偏离主题),我一直在寻找一个很好的、有记录的演示。

我很幸运。

在此向Chris Pierce致敬,感谢他的出色作品

[

用OffscreenCanvas渲染图表

对于浏览器来说,渲染图表是一个非常密集的操作,尤其是在上下文中的多个图表小部件...

blog.scottlogic.com

](blog.scottlogic.com/2020/03/19/…)

虽然这篇文章已经是一年前的了,但它在很多方面仍然是相关的。

我强烈反对的一点是使用多个MessageChannels来覆盖不同的消息类别。在postMessages的基础上使用自己的API才是正确的做法,并在neo.mjs项目中实现。

你可以在这里找到Chris的演示版本。

[

GitHub - chrisprice/offscreen-canvas。使用offscreen canvas渲染图表的例子

对于浏览器来说,渲染图表是一个非常密集的操作,尤其是在上下文中的多个图表小工具...

github.com

](github.com/chrisprice/…)

5.创建基于neo.mjs的演示应用程序

我从使用CLI开始,使用:
,生成了一个新的工作区。npx neo-app

你可以在这里找到最终的结果。

[

GitHub - neomjs/offscreen-canvas。使用 worker.Canvas 的演示应用程序

使用worker.Canvas的演示应用程序。通过在GitHub上创建一个账户,为neomjs/offscreen-canvas的开发做出贡献。

github.com

](github.com/neomjs/offs…)

由于新的画布工作者是可选的,我们需要做的第一件事是在我们的neo-config.json 文件中激活它。

该框架使用app.mjs 文件作为您的应用程序工作者的入口点。

因此,使用一个名为canvas.mjs 的文件作为新的画布工作者的起点是有意义的。

这个也是使用一个onStart() 方法,允许你在工作者准备好后触发逻辑。

让我们从MainContainer.mjs 视图开始。

我们使用的是一个具有垂直框(vbox)布局的视口。

由于我们使用的是d3,我们将自定义WebGlComponent 映射到一个容器中(自定义d3-fc 标签),该容器负责根据需要调整画布节点的大小。

我们在底部放了一个带有几个按钮的工具条。

你会注意到一些MyApp.canvas.Helper.* 方法被触发了。这是基于远程方法的访问(remotes API),我们将在后面更深入地介绍它。

onStopMainButtonClick() 将触发Neo.Main.alert() ,因为工作者不能自己调用alert() 。这个也是一个远程方法。显示警报将停止主线程相关的JS执行。然而,它并没有停止与UI相关的渲染线程,所以我们的canvas worker仍然可以继续为我们的canvas节点制作动画。

6.创建自定义的WebGlComponent

只需要几行代码就可以了。

再一次,d3要求我们创建一个自定义的包装节点
(d3fc-canvas tag)。

我们使用这个节点来应用自定义的d3measure domListener,所以我们可以将我们的vdom根保持在最高级别。

然而,我们仍然需要为我们的内部canvas节点提供一个唯一的ID,因为我们正在使用它来请求所有权的转移。我在component.Canvas ,实现了getCanvasId() 方法,所以我们可以直接指向我们的第一个子节点。

7.查看canvas.Helper类

希望你还记得,我们的canvas.mjs 入口点导入了canvas.Helper 。所以这个文件(单子)将在canvas worker里面运行。

这里最重要的一点是第55行:我们正在将我们的一些类方法名称添加到remote 配置中。

这将在目标工作者内部注册名称空间,然后允许我们在新范围内调用相同的名称空间和函数名称作为承诺。

如果你想知道什么。

MyApp.canvas.Helper.enableAnimation(enableAnimation);

里面的MainContainer.mjs文件,这就是它的答案。

在引擎盖下,它将从app worker向canvas worker发送一个postMessage ,触发相关的方法并发送一个包含返回值的回复postMessage

由于我们的方法是一个Promise,我们可以使用then() 作为回调,或者与asyncawait

一旦d3和d3fc文件被导入,我们将遵循Chris Scott的演示应用程序的逻辑。生成数据、系列,一旦画布节点的所有权到达,我们就可以渲染系列。

与原来的演示不同的是,改变项目(点)的数量不需要重新加载整个应用程序(页面),但会动态地生成一个新的画布系列。我还添加了一个停止/重新启用动画的功能。

这很方便,工人有自己的requestAnimationFrame() 实现。我认为OffscreenCanvas是它的第一个也是唯一一个用例。

8.在浏览器中直接使用d3

这可能是我花了最多时间处理的项目。

看一下Chris的演示。

[旁注]d3-collection在这一点上已经被废弃了→不再需要了。

当使用基于JS模块的工作者时,使用importScripts() 是不可能/不允许的。

new Worker('App.mjs', {type: 'module'})

我尝试的第一件事是。

这在开发模式下运行得非常好(意味着直接在浏览器中运行,没有任何构建或转译)。

然而,我也想让它在dist/developmentdist/production 环境中运行,这些环境是基于 webpack 的。

使用静态导入会产生一个巨大的分割块,对我来说,这并不奏效(JS运行时间错误)。

为了解决分块的问题,我们需要切换到动态导入。

d3是相当刻薄的,因为每个文件基本上都是一个嵌套的函数,它将立即触发一个内部函数。每个内层函数都需要前面的导入已经存在了。

作为一个解决方案,我们确实需要动态导入,它可以作为一个序列被处理。这就是为什么我在我们的canvas.Helper文件中加入了以下逻辑。

[题外话]我主要坚持使用 "let "和逗号,而不是 "const",以实现更小的文件大小。

将每个导入文件包裹到一个函数中是超级重要的,例如:() => import() ,以确保导入文件不会被立即执行。然后,我们可以对每个单一的导入使用await 。肯定会慢一点,但我们在这里没有选择。

虽然d3能够将其函数添加到全局d3命名空间中,但对于d3fc库来说,实际上并不一样。在dist envs里面,它只是将函数添加到模块中,所以我们需要手动修复它。我将很快创建一个错误报告。

9.演示视频

这里有一个好东西是在最后。

我们正在将canvas worker Helper类记录到控制台。

我们可以直接改变这里的配置,用户界面会自动调整。

10.在线演示

请记住,这些演示目前只能在基于Chromium的浏览器中运行。

开发模式(按原样运行代码):
neomjs.github.io/pages2/work…

开发/生产(基于webpack):
neomjs.github.io/pages2/work…

我注意到,在开发模式下,画布动画的运行更加流畅,特别是在上升到100万点时。

但我并不100%确定原因。我的猜测是,webpack创建了封装函数来访问每个模块→总共有更多的函数调用,而这些调用加起来确实是很多。

点击 "stop main "按钮会创建一个alert(),暂停计时器,而我们的画布则继续制作动画。

11.最后的思考

如果你还没有找到时间深入研究neo.mjs项目,你真的应该。

[

GitHub - neomjs/neo: The application worker driven frontend framework

neo.mjs让你能够使用不止一个CPU来创建可扩展和高性能的应用程序。不需要照顾到每一个人。

淘宝网

](github.com/neomjs/neo)

新的canvas worker标志着v2.3版本的发布(我将很快调整版本号)。

完成日历将被推入v2.4版本(已经很接近了)。

我们现在已经有了基本的设置,可以在neo项目中创建一个图像、图表或游戏库。

如果有人想在这一部分做贡献,我们将非常感激

我期待着看到用新的OffscreenCanvas工作者构建的演示或实际应用

最好的问候和快乐的编码,
Tobias