WebWorker、ThreeJs的渲染和控制


在 ios16.4 版本中已经开始支持了 OffscreenCanvas ,那看样子,是时候再把Three做一波优化了
背景介绍
在之前的项目经验中,如果使用threejs加载比较大的3d场景,那么在创建 threejs 的对象和绘制的时候,会占用浏览器线程执行一个大时长的任务,导致页面卡住,不能交互。
那有什么即可以绘制 canvas 又不占用主线程的方法吗?
今天它来了(其实已经来了很久了)
使用WebWorker + OffscreenCanvas 就可以实现在另外的线程中绘制canvas ,从而做到不影响主线程。
本文不会主要介绍 WebWorker 和 Threejs 基础知识,只是一篇实操(辛酸史),但是在必要的时候会提供相关的链接
WebWorker 可以在后台启动一个线程执行js脚本,并且不会影响到主线程。
关于Webworker: 使用 Web Workers - Web API 接口参考 | MDN
OffscreenCanvas 是一个可以脱离屏幕渲染的canvas 对象,在串口环境和 WebWorker 环境都可以使用
关于OffscreenCanvas: OffscreenCanvas - Web API 接口参考 | MDN
项目开始
接下来就实践一下WebWorker + Threejs 渲染3d场景,并使用 OrbitControls 实现人机交互
Demo使用
vue3开发
app.vue
<template> <canvas ref="canvas"></canvas>
</template>
<script setup>
import { ref } from 'vue'
canvas = ref()
</script>
在 WebWorker 中只能使用 OffscreenCanvas,不能直接操作 DOM,所以需要把 canvas 元素转成 OffscreenCanvas 对象,在传递给 WebWorker 中使用
在 vue 的组件 onMounted 生命周期中,可以访问 DOM 元素
App.vue
import {// ...other onMounted
} from 'vue'
import Worker from './worker?worker'
const worker = new Worker()
onMounted(() =>{const offCanvas = canvas.value.transferControlToOffscreen()
})
和 WebWorker 的通信通过 postMessage 和 onmessage
worker.postMessage({type: 'init',data: {offCanvas}
}, [offCanvas])
在 webworker 中接收传入的 OffscreenCanvas 对象
worker.js
self.onmessage = function ({ data }) {switch (data.type) {case 'init':init(data.data)break;}
}
使用传入的 OffscreenCanvas 对象做threejs的渲染
import {OrthographicCamera,Scene,WebGLRenderer,BoxGeometry,MeshLambertMaterial,Mesh,AmbientLight
} from 'three'self.onmessage = function ({ data }) {switch (data.type) {case 'init':init(data.data)break;}
}let scene;
let camera;
let renderer;function init(data) {const { offCanvas } = dataconst { width, height } = offCanvasinitScene()initLight()initCamera(width, height)initRenderer(offCanvas, width, height)render()
}
// 初始化相机
function initCamera(w, h) {const k = w / h;const s = 300;camera = new OrthographicCamera(-s * k, s * k, s, -s, 1, 1000)camera.position.set(550, 600, 100);camera.lookAt(scene.position);
}// 初始化渲染器
function initRenderer(canvas, w, h) {renderer = new WebGLRenderer({canvas})renderer.setSize(w, h, false);renderer.setClearColor(0xb9d3ff, 1)
}// 初始化场景
function initScene() {scene = new Scene();function createMesh(i) {// 立方体网格模型const geometry1 = new BoxGeometry(10, 10, 10);const material1 = new MeshLambertMaterial({color: 0x0000ff})const mesh1 = new Mesh(geometry1, material1);mesh1.translateZ(i * 10)scene.add(mesh1);}for (let i = 0; i < 10; i ++) {createMesh(i)}
}// 初始化环境光
function initLight() {const ambient = new AmbientLight(0x444444)scene.add(ambient)
}// 开始渲染
function render() {function _render() {renderer.render(scene, camera);self.requestAnimationFrame(_render)}_render()
}

写完 worker.js之后,3d场景已经可以渲染但是不能交互,threejs 中有提供OrbitControls 轨道控制器做交互
OrbitControls需要在页面上绑定DOM事件实现人机交互
因为 WebWorker 中不能操作 DOM 所以 OrbitControls 不能直接在 WebWorker 中使用,要在主线程中使用
用法很简单,只需要传入一个 Camera 对象,和一个绑定事件用的 DOM元素 就可以了
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
new OrbitControls(camera, canvas)
主线程中已经有了一个canvas 元素,现在还需要一个 camera 对象,第一考虑直接把 WebWorker 中创建的 Camera 传到主线程中
worker.js
// 初始化相机
function initCamera(w, h) {// ...otherself.postMessage({type: 'create-camera',data: { camera }})
}
App.vue
let camera;
function createCamera(data) {camera = data.camera;
}
worker.onmessage = function ({data}) {switch (data.type) {case "create-camera":createCamera(data.data);break;}
}
控制台出现报错,不可行

原因是因为 postMessage 方法传递的参数,必须是可以被结构化克隆算法处理的JavaScript对象。
Function 对象和 Dom 对象是不能被结构化克隆算法复制的
关于WebWorker.postMessage:Worker.postMessage() - Web API 接口参考 | MDN
关于结构化克隆算法:结构化克隆算法 - Web API 接口参考 | MDN
既然不能直接传 Camera 对象,那就传递创建 Camera 使用的参数,在主线程中创建一个一样的 Camera 对象
worker.js
// 通知主线程camera创建成功
function dispatchCreateCamera(data) {self.postMessage({type: 'create-camera',data: data})
}
// 初始化相机
function initCamera(w, h) {// ...otherdispatchCreateCamera({args: [-s * k, s * k, s, -s, 1, 1000],position: [550, 600, 100],lookAt: [scene.position.x, scene.position.y, scene.position.z]})
}
App.vue
import { OrthographicCamera, Vector3 } from 'three'
function createCamera(data) {const { args, position, lookAt} = data;camera = new OrthographicCamera(...args)camera.position.set(...position);camera.lookAt(new Vector3(...lookAt));
}
new OrbitControls(camera, canvas.value)
这样就创建了控制器了,但是现在控制器还是没有实现交互,因为现在修改的是主线程的Camera ,而canvas绘制是用的 WebWorker 中的 Camera,所以还需要把控制器对 Camera 的修改同步到 WebWorker
看 OrbitControls 的代码(这里就不展开看了)发现在事件处理中,通过调用 scope.update 完成对 Camera 的修改
在 OrbitControls 的 update 方法中,除了 对 Camera 的 position 的修改外,还调用了 Camera 的 lookAt 方法,所以这里我们做一个投机取巧的操作。
对 Camera 做代理,每次调用 lookAt 的时候,就把 Camera 的 position 、zoom 和 lookAt 的参数,传递给 WebWorker ,对 WebWorker 中的 Camera 做一样的操作,完成交互
App.vue
function dispatchCameraUpdate(data) {worker.postMessage({type: 'update-camera',data})
}
function createCamera(data) {// ...otherconst $camera = new Proxy(camera, {get(target, key, receiver) {const value = Reflect.get(target, key, receiver)if (key === 'lookAt') {return function ($target) {value.call(target, $target)dispatchCameraUpdate({position: [camera.position.x, camera.position.y, camera.position.z],lookAt: [$target.x, $target.y, $target.z],zoom: camera.zoom})}}return value}})new OrbitControls($camera, canvas.value)
}
worker.js
import {// ...otherVector3
} from 'three'
self.onmessage = function ({ data }) {switch (data.type) {// ...othercase 'update-camera':updateCamera(data.data)break;}
}
function updateCamera(data) {const { position, lookAt, zoom } = data;camera.zoom = zoom;camera.position.set(...position)camera.lookAt(new Vector3(...lookAt))// 修改了zoom之后需要调用updateProjectionMatrixcamera.updateProjectionMatrix()
}

现在已经完成了在 WebWorker 中操作 Three ,在做一个绘制大量元素的场景,看一下浏览器是否还会有大时长任务阻塞
worker.js
// 初始化场景
function initScene() {// ...otherfor (let i = 0; i < 10000; i ++) {createMesh(i)}
}

可以看到在threejs绘制期间,浏览器的渲染并没有被阻塞,在WebWorker 中有一个 2.43s的长任务,这个任务的执行,并不会阻塞浏览器的渲染,这就是 WebWorker的后台渲染

问题
-
ios16.4支持了
OffscreenCanvas但是并没有支持 3d 的应用,OffscreenCanvas获取webgl的上下文返回的是null -
现在的主流设备并没有完全支持
OffscreenCanvas,所以开发中还需要考虑好兼容性
参考链接
使用 Web Workers - Web API 接口参考 | MDN
OffscreenCanvas - Web API 接口参考 | MDN
Worker.postMessage() - Web API 接口参考 | MDN
结构化克隆算法 - Web API 接口参考 | MDN
代码地址
GitHub - wukang0718/webworker-three: 在webworker中渲染three的Demo


