Search code examples
web-workerreact-three-fiberoffscreen-canvas

Can't use offscreencanvas with react-three-fiber


I am using offscreencanvas (https://threejs.org/manual/#en/offscreencanvas) to render react-three-fiber elements on a separate thread. There are not a lot of resources about this on the internet (however I found this issue: https://github.com/pmndrs/react-three-fiber/issues/280). I tried reproducing it but I get an error.

Main thread create canvas:

// main.jsx (main thread)

    import React from "react";

    export const Main = () => {
      const canvasRef = React.useRef();

      React.useEffect(() => {
        const offscreen = canvasRef.current.transferControlToOffscreen();

        const worker = new Worker('worker.js');

        worker.postMessage({
          canvas: offscreen,
        }, [offscreen])
      }, []);

      return (
        <canvas ref={canvasRef} />
      );
    }

Separate thread:

// worker.jsx (worker thread)
import { render } from 'react-three-fiber';
import { WebGLRenderer, Scene, PerspectiveCamera } from 'three';

self.onmessage = (event) => {
  const { canvas } = event.data;

  const scene = new Scene();
  const renderer = new WebGLRenderer({ canvas });
  const camera = new PerspectiveCamera( 75, canvas.width / canvas.height, 0.1, 1000 );

  render(<gridHelper scale={[10, 10, 10]}/>, scene);

  renderer.render(scene, camera);
}

When I render this, I get an error saying "Cannot read property of undefined". Could anyone help me identify why I get this error? Or provide a demo using offscreencanvas with react-three-fiber?


Solution

  • Demo: https://f-loat.github.io/offscreen-canvas-demo

    GitHub: https://github.com/F-loat/offscreen-canvas-demo

    // main.js
    import React, { useEffect } from 'react';
    
    const App = () => {
      const canvasRef = React.useRef();
    
      useEffect(() => {
        const canvas = canvasRef.current;
        const offscreen = canvasRef.current.transferControlToOffscreen();
    
        const worker = new Worker(new URL('./worker.js', import.meta.url));
    
        worker.postMessage( {
          drawingSurface: offscreen,
          width: canvas.clientWidth,
          height: canvas.clientHeight,
          pixelRatio: window.devicePixelRatio,
        }, [ offscreen ] );
      }, []);
    
      return (
        <canvas ref={canvasRef} />
      );
    }
    
    export default App
    
    // worker.js
    import React, { useRef, useState } from 'react'
    import * as THREE from 'three'
    import { extend, useFrame, createRoot, events } from '@react-three/fiber'
    
    extend(THREE)
    
    function Cube(props) {
      // This reference will give us direct access to the mesh
      const mesh = useRef()
      // Set up state for the hovered and active state
      const [hovered, setHover] = useState(false)
      const [active, setActive] = useState(false)
      // Subscribe this component to the render-loop, rotate the mesh every frame
      useFrame((state, delta) => {
        mesh.current.rotation.x += 0.01
        mesh.current.rotation.y += 0.01
      })
      // Return view, these are regular three.js elements expressed in JSX
      return (
        <mesh
          {...props}
          ref={mesh}
          scale={active ? 1.5 : 1}
          onClick={() => setActive(!active)}
          onPointerOver={() => setHover(true)}
          onPointerOut={() => setHover(false)}>
          <boxGeometry args={[1, 1, 1]} />
          <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
        </mesh>
      )
    }
    
    const App = () => {
      return (
        <>
          <ambientLight />
          <pointLight position={[10, 10, 10]} />
          <Cube position={[0, 0, 0]} />
        </>
      );
    }
    
    self.onmessage = (event) => {
      const { drawingSurface: canvas, width, height, pixelRatio } = event.data;
    
      const root = createRoot(canvas)
    
      root.configure({
        events,
        size: {
          width,
          height,
        },
        dpr: pixelRatio, // important
      })
    
      root.render(<App />)
    }
    
    // @react-three/fiber/dist/index-1e2a4313.esm.js
    
    + var window = self || window
    
    - gl.setSize(size.width, size.height);
    + gl.setSize(size.width, size.height, false);