> 文章列表 > WebWorker、ThreeJs的渲染和控制

WebWorker、ThreeJs的渲染和控制

WebWorker、ThreeJs的渲染和控制

在这里插入图片描述

ios16.4 版本中已经开始支持了 OffscreenCanvas ,那看样子,是时候再把Three做一波优化了

背景介绍

在之前的项目经验中,如果使用threejs加载比较大的3d场景,那么在创建 threejs对象和绘制的时候,会占用浏览器线程执行一个大时长的任务,导致页面卡住,不能交互。

那有什么即可以绘制 canvas 又不占用主线程的方法吗?

今天它来了(其实已经来了很久了)

使用WebWorker + OffscreenCanvas 就可以实现在另外的线程中绘制canvas ,从而做到不影响主线程。

本文不会主要介绍 WebWorkerThreejs 基础知识,只是一篇实操(辛酸史),但是在必要的时候会提供相关的链接

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 的通信通过 postMessageonmessage

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 的修改

OrbitControlsupdate 方法中,除了 对 Cameraposition 的修改外,还调用了 CameralookAt 方法,所以这里我们做一个投机取巧的操作。

Camera 做代理,每次调用 lookAt 的时候,就把 CamerapositionzoomlookAt 的参数,传递给 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的后台渲染

在这里插入图片描述

问题

  1. ios16.4支持了 OffscreenCanvas 但是并没有支持 3d 的应用,OffscreenCanvas 获取 webgl 的上下文返回的是 null

  2. 现在的主流设备并没有完全支持 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