Is it possible to enable unbounded number of renderers in THREE.js?

In order to avoid the XY problem, let me explain where I'm coming from. I would like to plot a large number of waveforms stacked on top of each other using the same time axis, using THREE.js. The waveforms are simply THREE.Line's and I am implementing zoom/pan/scaling of these waveforms by modifying the view bounds of an Orthographic camera.

My initial attempt at accomplishing this lead me to create multiple canvas elements with fixed height, stacked on top of each other, and attach a THREE.WebGLRenderer to each canvas. This worked perfectly, until I tried scaling it past 15 or so waveforms, where THREE.js gave me a warning "too many active webgl contexts", and started deleting old contexts.

I feel like this is decent practice, considering it's the same technique applied here: http://threejs.org/examples/#webgl_multiple_canvases_grid

In this example, 4 WebGLRenderers are created, one for each canvas.

So, is it possible to override this warning somehow, and create an unbounded number of canvas elements, each with their own renderer?

ASIDE:

I have considered using one scene and positioning waveforms accordingly within it, and using multiple cameras with an approach similar to http://threejs.org/examples/#webgl_multiple_views.

The problems are two-fold:

(1) I lose the ability to dom-manipulate and easily attach key and mouse listeners on a per-waveform basis.

(2) This solution doesn't seem to scale either. Once the renderer's height passes somewhere around 6000px height, it starts to enter some type of corrupt state and part of the scene doesn't appear, with the rest of the content appearing stretched to compensate.

Thanks to anyone who can help!


You can use one non-scrolling full window size canvas, and place holders DIVs for your wave forms. Then with 1 renderer have 1 scene per waveform and call renderer.setViewport and renderer.setScissor with the location of each div before rendering each scene.

Effectively like this

renderer.setScissorTest( true );
scenes.forEach( function( scene ) {

  // get the element that is a place holder for where we want to
  // draw the scene
  var viewElement = scene.viewElement;

  // get its position relative to the page's viewport
  var rect = viewElement.getBoundingClientRect();

  // check if it's offscreen. If so skip it
  if ( rect.bottom < 0 || rect.top  > renderer.domElement.clientHeight ||
     rect.right  < 0 || rect.left > renderer.domElement.clientWidth ) {
    return;  // it's off screen
  }

  // set the viewport
  var width  = rect.right - rect.left;
  var height = rect.bottom - rect.top;
  var left   = rect.left;
  var top    = rect.top;

  renderer.setViewport( left, top, width, height );
  renderer.setScissor( left, top, width, height );

  camera.aspect = width / height;
  camera.updateProjectionMatrix();

  renderer.render( scene, camera );
} );
renderer.setScissorTest( false );

Example:

var canvas;

var scenes = [], camera, renderer, emptyScene;

init();
animate();

function init() {

  canvas = document.getElementById( "c" );

  camera = new THREE.PerspectiveCamera( 75, 1, 0.1, 100 );
  camera.position.z = 1.5;

  var geometries = [
    new THREE.BoxGeometry( 1, 1, 1 ),
    new THREE.SphereGeometry( 0.5, 12, 12 ),
    new THREE.DodecahedronGeometry( 0.5 ),
    new THREE.CylinderGeometry( 0.5, 0.5, 1, 12 ),
  ];

  var template = document.getElementById("template").text;
  var content = document.getElementById("content");

  var emptyScene = new THREE.Scene();

  var numScenes = 100;

  for ( var ii =  0; ii < numScenes; ++ii ) {

    var scene = new THREE.Scene();

    // make a list item.
    var element = document.createElement( "div" );
    element.innerHTML = template;
    element.className = "list-item";

    // Look up the element that represents the area
    // we want to render the scene
    scene.element = element.querySelector(".scene");
    content.appendChild(element);

    // add one random mesh to each scene
    var geometry = geometries[ geometries.length * Math.random() | 0 ];
    var material = new THREE.MeshLambertMaterial( { color: randColor() } );

    scene.add( new THREE.Mesh( geometry, material ) );

    light = new THREE.DirectionalLight( 0xffffff );
    light.position.set( 0.5, 0.8, 1 );
    scene.add( light );

    light = new THREE.DirectionalLight( 0xffffff );
    light.position.set( -0.5, -0.8, -1 );
    scene.add( light );

    scenes.push( scene );
  }


  renderer = new THREE.WebGLRenderer( { canvas: canvas, antialias: true } );
  renderer.setClearColor( 0xFFFFFF );

}

function updateSize() {

  var width = canvas.clientWidth;
  var height = canvas.clientHeight;

  if ( canvas.width !== width || canvas.height != height ) {

    renderer.setSize ( width, height, false );

  }

}

function animate() {

  render();

  requestAnimationFrame( animate );
}

function render() {

  updateSize();
  
  canvas.style.transform = `translateY(${window.scrollY}px`;

  renderer.setClearColor( 0xFFFFFF );
  renderer.clear( true );
  renderer.setClearColor( 0xE0E0E0 );

  renderer.setScissorTest( true );
  scenes.forEach( function( scene ) {
    // so something moves
    scene.children[0].rotation.x = Date.now() * 0.00111;
    scene.children[0].rotation.z = Date.now() * 0.001;

    // get the element that is a place holder for where we want to
    // draw the scene
    var element = scene.element;

    // get its position relative to the page's viewport
    var rect = element.getBoundingClientRect();

    // check if it's offscreen. If so skip it
    if ( rect.bottom < 0 || rect.top  > renderer.domElement.clientHeight ||
       rect.right  < 0 || rect.left > renderer.domElement.clientWidth ) {
      return;  // it's off screen
    }

    // set the viewport
    var width  = rect.right - rect.left;
    var height = rect.bottom - rect.top;
    var left   = rect.left;
    var top    = rect.top;

    renderer.setViewport( left, top, width, height );
    renderer.setScissor( left, top, width, height );

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.render( scene, camera );

  } );
  renderer.setScissorTest( false );

}

function rand( min, max ) {
  if ( max == undefined ) {
    max = min;
    min = 0;
  }

  return Math.random() * ( max - min ) + min;
}

function randColor() {
  var colors = [ rand( 256 ), rand ( 256 ), rand( 256 ) ];
  colors[ Math.random() * 3 | 0 ] = 255;
  return ( colors[0] << 16 ) |
       ( colors[1] <<  8 ) |
       ( colors[2] <<  0 ) ;
}
* {
  box-sizing: border-box;
  -moz-box-sizing: border-box;
}

body {
  color: #000;
  font-family:Monospace;
  font-size:13px;

  background-color: #fff;
  margin: 0;
}


#content {
  position: absolute;
  top: 0; width: 100%;
  z-index: 1;
  padding: 2em;
}

#c {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

.list-item {
  margin: 1em;
  padding: 2em;
  display: -webkit-flex;
  display: flex;
  flex-direction: row;
  -webkit-flex-direction: row;
}

.list-item .scene {
  width: 200px;
  height: 200px;
  flex: 0 0 auto;
  -webkit-flex: 0 0 auto;
}
.list-item .description {
  font-family: sans-serif;
  font-size: large;
  padding-left: 2em;
  flex: 1 1 auto;
  -webkit-flex: 1 1 auto;
}

@media only screen and (max-width : 600px) {
  #content {
    width: 100%;
  }
  .list-item {
    margin: 0.5em;
    padding: 0.5em;
    flex-direction: column;
    -webkit-flex-direction: column;
  }
  .list-item .description {
    padding-left: 0em;
  }
}
<canvas id="c"></canvas>
<div id="content">
</div>
<script id="template" type="notjs">
			<div class="scene"></div>
			<div class="description">some random text about this object, scene, whatever</div>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/94/three.min.js"></script>

Update:

The original solution here used a canvas with position: fixed meaning the canvas did not scroll. The new solution below changes it to position: absolute; top: 0 and then sets the canvas's transform every frame

  canvas.style.transform = `translateY(${window.scrollY}px`;

This has the advantage that even if we can't update the canvas every frame the canvas will scroll with the page until we get a chance to update it. This makes the scrolling stay in sync.

You can compare the old solution to the new solution. Both are set to only render every 4th frame to exaggerate the issue. Scroll them up and down and the difference should be clear.