6

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!

gman
  • 100,619
  • 31
  • 269
  • 393
hercuptruk
  • 83
  • 2
  • 4
  • "4 WebGLRenderers are created" To clarify: they are not. There are 4 canvases, each of them gets renderer only a part of the view, using its own camera, but only one renderer is created and used for this (and I have check it was done the same way at the time the question was asked). – Suma Jan 08 '23 at 16:58

1 Answers1

17

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.

Update 2:

Yet another solution is to virtualize the WebGL context in which you create one offscreen WebGL context and then patch things so other other uses of WebGL get a virtual WebGL context that is simulated on top of a single shared actual WebGL context.

gman
  • 100,619
  • 31
  • 269
  • 393
  • amazing! I am truly impressed. This is exactly the type of ingenious solution I was hoping for. As a noob in these domains, I thank you very much. – hercuptruk Jun 04 '15 at 20:44
  • Great answer! I'm trying to do something similar, but I'm working with typescript. The compiler has an issue with `scene.element`, saying that the property `element` does not exist for the type `scene`. Any idea how to fix this? – Eben Kadile Feb 05 '19 at 04:40
  • How you associate which scene is associated with which element is entirely up to you. It's not part of three.js. Make your own class with scene property and an element property – gman Feb 05 '19 at 05:12