Search code examples
javascriptthree.jsvirtual-realitystereoscopy

Can anyone explain what is going on in this code for Three.js StereoEffect


Can someone who understands stereo rendering give an explanation of what each of these functions is doing to create the VR stereo effect. There's little to no documentation on functions like StereoCamera(), setScissor(), setViewPort() in the three.js library.

I would greatly appreciate any sort of high/low-level explanation.

Also, a bug I'm having is when I try to change the eyeSep value it has no effect on the final render.

/**
 * @author alteredq / http://alteredqualia.com/
 * @authod mrdoob / http://mrdoob.com/
 * @authod arodic / http://aleksandarrodic.com/
 * @authod fonserbc / http://fonserbc.github.io/
*/

THREE.StereoEffect = function ( renderer ) {

    var _stereo = new THREE.StereoCamera();
    _stereo.aspect = 0.5;
    var size = new THREE.Vector2();

    this.setEyeSeparation = function ( eyeSep ) {

        _stereo.eyeSep = eyeSep;

    };

    this.setSize = function ( width, height ) {

        renderer.setSize( width, height );

    };

    this.render = function ( scene, camera ) {

        scene.updateMatrixWorld();

        if ( camera.parent === null ) camera.updateMatrixWorld();

        _stereo.update( camera );

        renderer.getSize( size );

        if ( renderer.autoClear ) renderer.clear();
        renderer.setScissorTest( true );

        renderer.setScissor( 0, 0, size.width / 2, size.height );
        renderer.setViewport( 0, 0, size.width / 2, size.height );
        renderer.render( scene, _stereo.cameraL );

        renderer.setScissor( size.width / 2, 0, size.width / 2, size.height );
        renderer.setViewport( size.width / 2, 0, size.width / 2, size.height );
        renderer.render( scene, _stereo.cameraR );

        renderer.setScissorTest( false );

    };

};

module.exports = THREE.StereoEffect;

Solution

  • setScissor and setViewport

    set the area of the canvas to render to. More specifically setViewport sets how to convert from a shader's clip space to some portion of the canvas's pixel space and setScissor sets a rectangle outside of which nothing can be rendered.

    See this.

    Otherwise StereoCamera just provides 2 cameras that are eyeSep apart so you only have to manipulate one camera, the PerspectiveCamera then you update the StereoCamera and it will automatically update the 2 eye cameras you can use for rendering.

    body { margin: 0; }
    #c { width: 100vw; height: 100vh; display: block; }
    #ui { position: absolute; left: 1em; top: 1em; }
    <canvas id="c"></canvas>
    <script type="module">
    import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';
    import {GUI} from 'https://threejsfundamentals.org/threejs/../3rdparty/dat.gui.module.js';
    
    function main() {
      const canvas = document.querySelector('#c');
      const renderer = new THREE.WebGLRenderer({canvas});
    
      const fov = 75;
      const aspect = 2;  // the canvas default
      const near = 0.1;
      const far = 5;
      const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
      camera.position.z = 2;
      
      const stereo = new THREE.StereoCamera();
    
      const gui = new GUI();
      gui.add(stereo, 'eyeSep', 0, 2, 0.001);
    
      const scene = new THREE.Scene();
    
      {
        const color = 0xFFFFFF;
        const intensity = 1;
        const light = new THREE.DirectionalLight(color, intensity);
        light.position.set(-1, 2, 4);
        scene.add(light);
      }
    
      const geometry = new THREE.SphereBufferGeometry(0.5, 6, 3);
    
      function makeInstance(geometry, color, x) {
        const material = new THREE.MeshPhongMaterial({color, flatShading: true});
    
        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);
    
        cube.position.x = x;
    
        return cube;
      }
    
      const cubes = [
        makeInstance(geometry, 0x44aa88,  0),
        makeInstance(geometry, 0x8844aa, -1),
        makeInstance(geometry, 0xaa8844,  1),
      ];
    
      function resizeRendererToDisplaySize(renderer) {
        const canvas = renderer.domElement;
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;
        const needResize = canvas.width !== width || canvas.height !== height;
        if (needResize) {
          renderer.setSize(width, height, false);
        }
        return needResize;
      }
    
      function render(time) {
        time *= 0.001;
    
        if (resizeRendererToDisplaySize(renderer)) {
          const canvas = renderer.domElement;
          camera.aspect = canvas.clientWidth / canvas.clientHeight / 2;  
          camera.updateProjectionMatrix();
        }
    
        cubes.forEach((cube, ndx) => {
          const speed = 1 + ndx * .1;
          const rot = time * speed;
          cube.rotation.x = rot;
          cube.rotation.y = rot;
        });
    
        {
            // we need to manually update camera matrix
            // because it will not be passed directly to
            // renderer.render were it would normally be
            // updated
    
            camera.updateWorldMatrix();
            stereo.update(camera);
    
            const size = new THREE.Vector2();
            renderer.getSize(size);
    
            renderer.setScissorTest(true);
    
            renderer.setScissor(0, 0, size.width / 2, size.height);
            renderer.setViewport(0, 0, size.width / 2, size.height);
            renderer.render(scene, stereo.cameraL);
    
            renderer.setScissor(size.width / 2, 0, size.width / 2, size.height);
            renderer.setViewport(size.width / 2, 0, size.width / 2, size.height);
            renderer.render(scene, stereo.cameraR);
    
            renderer.setScissorTest(false);
        }
    
        requestAnimationFrame(render);
      }
    
      requestAnimationFrame(render);
    }
    
    main();
    </script>