翻译《threejsfundamentals》离屏渲染+web-worker一篇
Three.js OffscreenCanvas
OffscreenCanvas是一种相对较新的浏览器功能,目前仅在Chrome中可用,但显然也即将适用于其他浏览器。
OffscreenCanvas使web worker能够渲染canvas。这是一种办法把繁重的工作(如渲染复杂的3D场景)交给web worker,为了避免降低浏览器的响应速度。这还意味着数据是在worker中加载和解析的,因此在页面加载时可能会减少卡顿(jank)
初步的使用非常简单,让我们移植3旋转立方体示例从the article on responsiveness。worker通常将他们的代码分离到另一个脚本文件中,而这个站点上的大多数示例都将他们的脚本嵌入到他们所在页面的HTML文件中。 在本例中,我们将创建一个名为offscreencanvas-cubes.js的文件,并将响应式example中的所有JavaScript复制到该文件中。然后,我们将进行必要的更改,使其在辅助进程中运行。
我们仍然需要写一些JavaScript代码在HTML页面中。我们需要做的第一件事是找到canvas,然后通过调用 canvas.transferControlToOffscreen 将该画布的控制权转移到屏幕外。
function main() {
const canvas = document.querySelector('#c');
const offscreen = canvas.transferControlToOffscreen();
...
然后,我们可以用新的 worker(pathToScript,{type:'module'} 启动我们的worker。并将 offscreen 对象传递给它。
function main() {
const canvas = document.querySelector('#c');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
}
main();
需要注意的是,worker不能访问DOM。他们不能查看HTML元素,也不能接收鼠标事件或键盘事件。通常他们能做的唯一一件事就是响应发送给他们的消息,并将消息发送回页面。
要向worker发送消息,我们调用 worker.postMessage 并向其传递1或2个参数。第一个参数是一个JavaScript对象,它将被 clone 并发送给worker。第二个参数是一个可选的对象数组,它是第一个对象的一部分,第一个对象是我们想要传递给worker的对象。这些对象将不会被克隆。相反,它们将被转移,并将不再存在于主页中。“不存在”可能是错误的描述,相反,它们是中性的。只能传输某些类型的对象,而不能克隆。它们包括 OffscreenCanvas ,因此一旦将 OffscreenCanvas 传输回主页,就没有用了。
worker从他们的 onmessage 处理程序接收消息。我们传递给 postMessage 的对象在 event.data 传递给worker上的 onmessage 处理程序时到达。上面的代码在传递给worker的对象中声明了一个类型:“main”。此对象对浏览器没有意义。这完全是我们自己用的。我们将创建一个处理程序,他基于类型调用worker中不同函数。然后,我们可以根据需要添加函数,并从主页面轻松调用它们。
const handlers = {
main,
};
self.onmessage = function(e) {
const fn = handlers[e.data.type];
if (!fn) {
throw new Error('no handler for type: ' + e.data.type);
}
fn(e.data);
};
调用一个不同的函数,您可以在上面看到,我们只是根据类型查找处理程序,并将从主页发送的数据传递给它,因此,现在我们只需要开始更改我们从the article on responsiveness 粘贴到 offscreencanvas-cubes.js 中的 main 。
我们将从事件数据接收canvas,而不是从DOM中查找canvas。(canvas可以传递吗)
//function main() {
// const canvas = document.querySelector('#c');
function main(data) {
const {canvas} = data;
const renderer = new THREE.WebGLRenderer({canvas});
...
记住worker根本看不到DOM,我们遇到的第一个问题是 ResizeRenderToDisplaySize 不能查看 canvas.clientWidth 和 canvas.ClientHight ,因为它们是DOM值。这是原始代码
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
相反,我们需要在 size 变化时将其发送给worker。所以,让我们添加一些全局状态,并将宽度和高度放置在那里。
const state = {
width: 300, // canvas default
height: 150, // canvas default
};
然后,让我们添加一个 “size” 处理程序来更新这些值。
function size(data) {
state.width = data.width;
state.height = data.height;
}
const handlers = {
main,
size,
};
现在我们可以将 ResizeRenderToDisplaySize 更改为使用 state.width 和 state.height 。
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
// const width = canvas.clientWidth;
// const height = canvas.clientHeight;
const width = state.width;
const height = state.height;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
在我们计算aspect 的地方,我们需要类似的变化
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
// camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.aspect = state.width / state.height;
camera.updateProjectionMatrix();
}
...
回到主页,我们将在页面大小更改时发送大小事件。
const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
function sendSize() {
worker.postMessage({
type: 'size',
width: canvas.clientWidth,
height: canvas.clientHeight,
});
}
window.addEventListener('resize', sendSize);
sendSize();
我们还可以调用它一次,以发送初始大小。
仅做了这些改动,假设您的浏览器完全支持Offscreencanvas,它应该可以正常工作。在我们运行它之前,让我们检查一下浏览器是否真的支持OffscreenCanvas,如果不支持,则显示一个错误。首先,让我们添加一些HTML来显示错误。
<body>
<canvas id="c"></canvas>
<div id="noOffscreenCanvas" style="display:none;">
<div>no OffscreenCanvas support</div>
</div>
</body>
和一些css:
#noOffscreenCanvas {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
background: red;
color: white;
}
然后我们可以检查 transferControlToOffscreen 是否存在,以查看浏览器是否支持 OffscreenCanvas
function main() {
const canvas = document.querySelector('#c');
if (!canvas.transferControlToOffscreen) {
canvas.style.display = 'none';
document.querySelector('#noOffscreenCanvas').style.display = '';
return;
}
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('offscreencanvas-picking.js', {type: 'module});
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
...
如果你的浏览器支持Offscreencanv,那么这个例子就可以了(官方页面中有,我没粘过来,在博客园弄js不太方便)
click here to open in a separate window
这个例子很不错,但因为目前不是每个浏览器都支持OffscreenCanvas,所以让我们将代码更改为同时使用 OffscreenCanvas 和 canvas ,如果不支持 OffscreenCanvas ,则退回到主页中使用 canvas ,就像正常情况一样
顺便说一句,如果你需要OffscreenCanvas来让你的页面更具响应性,那么现在还不清楚回退的意义何在。可能基于您是否最终在主页面上运行或在worker中运行,您可能会调整完成的工作量,以便在worker中运行时可以比在主页面上运行时做得更多。你做什么真的取决于你自己。
我们应该做的第一件事可能是将three.js代码与特定于worker的代码分开。这样我们就可以在主页和工作页面上使用相同的代码。换句话说,我们现在将有3个文件
-
- our html file: threejs-offscreencanvas-w-fallback.html
- a JavaScript that contains our three.js code: shared-cubes.js
- our worker support code: offscreencanvas-worker-cubes.js
shared-cubes.js 和 offscreencanvas-worker-cubes.js 基本上是我们之前 offscreencanvas-cubes.js 文件的拆分。首先,我们将 offscreencanvas-cubes.js 复制到 shared-cube.js 。然后我们将 main 重命名为 init ,因为我们的HTML文件中已经有一个 main ,我们需要导出 init 和 state
import * as THREE from './resources/threejs/r132/build/three.module.js';
const state = {
export const state = {
width: 300, // canvas default
height: 150, // canvas default
};
function main(data) {
export function init(data) {
const {canvas} = data;
const renderer = new THREE.WebGLRenderer({canvas});
(课业太忙了,暂时没时间写了,未完待续。。。)