Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Display multiple instances of three.js in a single page

I have a web app where I need to display multiple 3D objects in different containers. For the moment, I have been instantiating multiple three.js renderers, one per container. However, I get this error message: "WARNING: Too many active WebGL contexts. Oldest context will be lost" and after ~10 renderers I can't open any more. Here's an example

http://brainspell.org/article/24996404

How should I go to have multiple three.js containers in a single web page? Is it possible to have a single renderer with multiple scenes and draw each scene in a different container (a different renderer.domElement) ?

Thank you!

like image 969
roberto Avatar asked Nov 27 '15 14:11

roberto


1 Answers

This has been covered elsewhere but the easiest way is to just use one instance of three.js, make it cover the entire window, put place holder divs where you want to draw things, and then use element.getClientBoundingRect to set the scissor and viewport for each scene you want to draw in each element

There's an example here.

Here's the answer in StackOverflow from which that sample originates

https://stackoverflow.com/a/30633132/128511


Another solution would be to use another not visible canvas and pass it to all instances of three.js (which will mean they all use the same WebGL context), then copy the results to individual 2D canvas.

Example:

canvas { width: 128px; height: 128px; display: block; }
.outer { border: 1px solid black; margin: 5px; display: inline-block; }
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/build/three.module.js';
import {OrbitControls} from 'https://threejsfundamentals.org/threejs/resources/threejs/r132/examples/jsm/controls/OrbitControls.js';

function main(visibleCanvas, offscreenSharedCanvas) {
  const visibleCanvas2DContext = visibleCanvas.getContext('2d');
  const renderer = new THREE.WebGLRenderer({canvas: offscreenSharedCanvas});

  const fov = 45;
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 100;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.set(0, 10, 20);

  const controls = new OrbitControls(camera, visibleCanvas);
  controls.target.set(0, 5, 0);
  controls.update();

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('black');

  {
    const planeSize = 40;

    const loader = new THREE.TextureLoader();
    const texture = loader.load('https://threejsfundamentals.org/threejs/resources/images/checker.png');
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.magFilter = THREE.NearestFilter;
    const repeats = planeSize / 2;
    texture.repeat.set(repeats, repeats);

    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
    const planeMat = new THREE.MeshPhongMaterial({
      map: texture,
      side: THREE.DoubleSide,
    });
    const mesh = new THREE.Mesh(planeGeo, planeMat);
    mesh.rotation.x = Math.PI * -.5;
    scene.add(mesh);
  }
  {
    const cubeSize = 4;
    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
    scene.add(mesh);
  }
  {
    const sphereRadius = 3;
    const sphereWidthDivisions = 32;
    const sphereHeightDivisions = 16;
    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
    scene.add(mesh);
  }

  {
    const color = 0xFFFFFF;
    const intensity = 1;
    const light = new THREE.DirectionalLight(color, intensity);
    light.position.set(0, 10, 0);
    light.target.position.set(-5, 0, 0);
    scene.add(light);
    scene.add(light.target);
  }

  // need to resize both the visibleCanvas and the offscreenSharedCanvas
  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = visibleCanvas.clientWidth;
    const height = visibleCanvas.clientHeight;
    const needResize = offscreenSharedCanvas.width !== width || offscreenSharedCanvas.height !== height ||
                       visibleCanvas.width !== width || visibleCanvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
      visibleCanvas.width = width;
      visibleCanvas.height = height;
    }
    return needResize;
  }

  let requestId;
  let visible;
  function render() {
    requestId = undefined;

    if (resizeRendererToDisplaySize(renderer)) {
      camera.aspect = visibleCanvas.clientWidth / visibleCanvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    renderer.render(scene, camera);
    
    // copy the offscreenSharedCanvas to the visible canvas
    visibleCanvas2DContext.drawImage(offscreenSharedCanvas, 0, 0);

    if (visible) {
      startRendering();
    }
  }
  
  function startRendering() {
    if (!requestId) {
      requestId = requestAnimationFrame(render);
    }
  }

  // use an intersection observer to only render this canvas when on screen
  const intersectionObserver = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting) {
      visible = true;
      startRendering();
    } else {
      visible = false;
    }
  });
  intersectionObserver.observe(visibleCanvas);
}

const offscreenSharedCanvas = document.createElement('canvas');
for (let i = 0; i < 20; ++i) {
  const outer = document.createElement('div');
  outer.className = 'outer';
  document.body.appendChild(outer);
  const canvas = document.createElement('canvas');
  outer.appendChild(canvas);
  main(canvas, offscreenSharedCanvas);
}

</script>

The copying might make it slow. Ideally you'd use an IntersectionObserver to start/stop any canvas offscreen from updating.

like image 195
gman Avatar answered Sep 30 '22 18:09

gman