在 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
在 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 } = data
const { width, height } = offCanvas
initScene()
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) {
// ...other
self.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) {
// ...other
dispatchCreateCamera({
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) {
// ...other
const $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 {
// ...other
Vector3
} from 'three'
self.onmessage = function ({ data }) {
switch (data.type) {
// ...other
case '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之后需要调用updateProjectionMatrix
camera.updateProjectionMatrix()
}
现在已经完成了在 WebWorker
中操作 Three
,在做一个绘制大量元素的场景,看一下浏览器是否还会有大时长任务阻塞
worker.js
// 初始化场景
function initScene() {
// ...other
for (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