WebWorker、ThreeJs的渲染和控制

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

WebWorker、ThreeJs的渲染和控制

背景介绍

在之前的项目经验中,如果使用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 } = 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()
}

WebWorker、ThreeJs的渲染和控制

写完 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;
    }
}

控制台出现报错,不可行

WebWorker、ThreeJs的渲染和控制

原因是因为 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 的修改

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

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

现在已经完成了在 WebWorker 中操作 Three ,在做一个绘制大量元素的场景,看一下浏览器是否还会有大时长任务阻塞

worker.js

// 初始化场景
function initScene() {
    // ...other

    for (let i = 0; i < 10000; i ++) {
        createMesh(i)
    }
}

WebWorker、ThreeJs的渲染和控制

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

WebWorker、ThreeJs的渲染和控制

问题

  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

原文链接:https://juejin.cn/post/7221113101591904317 作者:武康

(0)
上一篇 2023年4月12日 上午11:14
下一篇 2023年4月13日 上午10:05

相关推荐

发表评论

登录后才能评论