Bläddra i källkod

Basic stereo rendering support

David Sehnal 4 år sedan
förälder
incheckning
2f84b94227

+ 34 - 8
src/mol-canvas3d/camera.ts

@@ -10,9 +10,21 @@ import { Viewport, cameraProject, cameraUnproject } from './camera/util';
 import { CameraTransitionManager } from './camera/transition';
 import { BehaviorSubject } from 'rxjs';
 
-export { Camera };
+export { ICamera, Camera };
+
+interface ICamera {
+    readonly viewport: Viewport,
+    readonly view: Mat4,
+    readonly projection: Mat4,
+    readonly state: Readonly<Camera.Snapshot>,
+    readonly viewOffset: Camera.ViewOffset,
+    readonly far: number,
+    readonly near: number,
+    readonly fogFar: number,
+    readonly fogNear: number,
+}
 
-class Camera {
+class Camera implements ICamera {
     readonly view: Mat4 = Mat4.identity();
     readonly projection: Mat4 = Mat4.identity();
     readonly projectionView: Mat4 = Mat4.identity();
@@ -26,12 +38,7 @@ class Camera {
 
     readonly viewport: Viewport;
     readonly state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
-    readonly viewOffset: Camera.ViewOffset = {
-        enabled: false,
-        fullWidth: 1, fullHeight: 1,
-        offsetX: 0, offsetY: 0,
-        width: 1, height: 1
-    }
+    readonly viewOffset = Camera.ViewOffset();
 
     near = 1
     far = 10000
@@ -157,6 +164,15 @@ namespace Camera {
         height: number
     }
 
+    export function ViewOffset(): ViewOffset {
+        return {
+            enabled: false,
+            fullWidth: 1, fullHeight: 1,
+            offsetX: 0, offsetY: 0,
+            width: 1, height: 1
+        };
+    }
+
     export function setViewOffset(out: ViewOffset, fullWidth: number, fullHeight: number, offsetX: number, offsetY: number, width: number, height: number) {
         out.fullWidth = fullWidth;
         out.fullHeight = fullHeight;
@@ -166,6 +182,16 @@ namespace Camera {
         out.height = height;
     }
 
+    export function copyViewOffset(out: ViewOffset, view: ViewOffset) {
+        out.enabled = view.enabled;
+        out.fullWidth = view.fullWidth;
+        out.fullHeight = view.fullHeight;
+        out.offsetX = view.offsetX;
+        out.offsetY = view.offsetY;
+        out.width = view.width;
+        out.height = view.height;
+    }
+
     export function createDefaultSnapshot(): Snapshot {
         return {
             mode: 'perspective',

+ 105 - 0
src/mol-canvas3d/camera/stereo.ts

@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ *
+ * Adapted from three.js, The MIT License, Copyright © 2010-2020 three.js authors
+ */
+
+import { Mat4 } from '../../mol-math/linear-algebra';
+import { ParamDefinition } from '../../mol-util/param-definition';
+import { Camera, ICamera } from '../camera';
+import { Viewport } from './util';
+
+export class StereoCamera {
+    readonly left: ICamera = new EyeCamera();
+    readonly right: ICamera = new EyeCamera();
+
+    update(camera: Camera, params: StereoCameraParams) {
+        update(camera, params, this.left as EyeCamera, this.right as EyeCamera);
+    }
+}
+
+class EyeCamera implements ICamera {
+    viewport = Viewport.create(0, 0, 0, 0);
+    view = Mat4();
+    projection = Mat4();
+    state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
+    viewOffset: Readonly<Camera.ViewOffset> = Camera.ViewOffset();
+    far: number = 0;
+    near: number = 0;
+    fogFar: number = 0;
+    fogNear: number = 0;
+}
+
+export const StereoCameraParams = {
+    aspect: ParamDefinition.Numeric(1, { min: 0.1, max: 3, step: 0.01 }),
+    eyeSeparation: ParamDefinition.Numeric(0.064, { min: 0.01, max: 0.5, step: 0.001 }),
+    focus: ParamDefinition.Numeric(10, { min: 1, max: 100, step: 0.01 }),
+};
+export type StereoCameraParams = ParamDefinition.Values<typeof StereoCameraParams>
+
+const eyeLeft = Mat4.identity(), eyeRight = Mat4.identity();
+
+function update(camera: ICamera, params: StereoCameraParams, left: EyeCamera, right: EyeCamera) {
+    // Copy the states
+
+    Viewport.copy(left.viewport, camera.viewport);
+    Mat4.copy(left.view, camera.view);
+    Mat4.copy(left.projection, camera.projection);
+    Camera.copySnapshot(left.state, camera.state);
+    Camera.copyViewOffset(left.viewOffset, camera.viewOffset);
+    left.far = camera.far;
+    left.near = camera.near;
+    left.fogFar = camera.fogFar;
+    left.fogNear = camera.fogNear;
+
+    Viewport.copy(right.viewport, camera.viewport);
+    Mat4.copy(right.view, camera.view);
+    Mat4.copy(right.projection, camera.projection);
+    Camera.copySnapshot(right.state, camera.state);
+    Camera.copyViewOffset(right.viewOffset, camera.viewOffset);
+    right.far = camera.far;
+    right.near = camera.near;
+    right.fogFar = camera.fogFar;
+    right.fogNear = camera.fogNear;
+
+    // update the view offsets
+    let w = (camera.viewport.width / 2) | 0;
+
+    left.viewport.width = w;
+    right.viewport.x = w;
+    right.viewport.width -= w;
+
+    // update the projection and view matrices
+
+    const eyeSepHalf = params.eyeSeparation / 2;
+    const eyeSepOnProjection = eyeSepHalf * camera.near / params.focus;
+    const ymax = (camera.near * Math.tan(camera.state.fov * 0.5)) / /* cache.zoom */ 1;
+    let xmin, xmax;
+
+    // translate xOffset
+
+    eyeLeft[12] = - eyeSepHalf;
+    eyeRight[12] = eyeSepHalf;
+
+    // for left eye
+
+    xmin = - ymax * params.aspect + eyeSepOnProjection;
+    xmax = ymax * params.aspect + eyeSepOnProjection;
+
+    left.projection[0] = 2 * camera.near / (xmax - xmin);
+    left.projection[8] = (xmax + xmin) / (xmax - xmin);
+
+    Mat4.mul(left.view, left.view, eyeLeft);
+
+    // for right eye
+
+    xmin = - ymax * params.aspect - eyeSepOnProjection;
+    xmax = ymax * params.aspect - eyeSepOnProjection;
+
+    right.projection[0] = 2 * camera.near / (xmax - xmin);
+    right.projection[8] = (xmax + xmin) / (xmax - xmin);
+
+    Mat4.mul(right.view, right.view, eyeRight);
+}

+ 23 - 3
src/mol-canvas3d/canvas3d.ts

@@ -34,6 +34,7 @@ import { isDebugMode } from '../mol-util/debug';
 import { CameraHelperParams } from './helper/camera-helper';
 import { produce } from 'immer';
 import { HandleHelper, HandleHelperParams } from './helper/handle-helper';
+import { StereoCamera, StereoCameraParams } from './camera/stereo';
 
 export const Canvas3DParams = {
     camera: PD.Group({
@@ -60,6 +61,10 @@ export const Canvas3DParams = {
             height: PD.Numeric(128)
         })
     }),
+    stereo: PD.MappedStatic('off', {
+        on: PD.Group(StereoCameraParams),
+        off: PD.Group({})
+    }, { cycle: true }),
 
     cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
     transparentBackground: PD.Boolean(false),
@@ -207,6 +212,7 @@ namespace Canvas3D {
             fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
             clipFar: p.cameraClipping.far
         }, { x, y, width, height }, { pixelScale: attribs.pixelScale });
+        const stereoCamera = new StereoCamera();
 
         const controls = TrackballControls.create(input, camera, p.trackball);
         const renderer = Renderer.create(webgl, p.renderer);
@@ -214,10 +220,13 @@ namespace Canvas3D {
         const handleHelper = new HandleHelper(webgl, p.handle);
         const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
 
-        const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper, handleHelper, {
+        const drawPass = new DrawPass(webgl, renderer, scene, { standard: camera, stereo: stereoCamera }, debugHelper, handleHelper, {
             cameraHelper: p.camera.helper
         });
-        const pickPass = new PickPass(webgl, renderer, scene, camera, handleHelper, attribs.pickScale || 0.25, drawPass);
+        drawPass.isStereo = p.stereo.name === 'on';
+        const pickPass = new PickPass(webgl, renderer, scene, camera, stereoCamera, handleHelper, attribs.pickScale || 0.25, drawPass);
+        pickPass.isStereo = p.stereo.name === 'on';
+
         const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing);
         const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample);
 
@@ -275,9 +284,14 @@ namespace Canvas3D {
             const cameraChanged = camera.update();
             const multiSampleChanged = multiSample.update(force || cameraChanged);
 
+            const isStereo = p.stereo.name === 'on';
+
             if (force || cameraChanged || multiSampleChanged) {
+                if ((force || cameraChanged) && p.stereo.name === 'on') stereoCamera.update(camera, p.stereo.params);
+
                 renderer.setViewport(x, y, width, height);
-                if (multiSample.enabled) {
+                // TODO: support stereo rendering in multisampling
+                if (!isStereo && multiSample.enabled) {
                     multiSample.render(true, p.transparentBackground);
                 } else {
                     const toDrawingBuffer = !postprocessing.enabled && scene.volumes.renderables.length === 0;
@@ -490,6 +504,7 @@ namespace Canvas3D {
                 cameraResetDurationMs: p.cameraResetDurationMs,
                 transparentBackground: p.transparentBackground,
                 viewport: p.viewport,
+                stereo: p.stereo,
 
                 postprocessing: { ...postprocessing.props },
                 multiSample: { ...multiSample.props },
@@ -595,6 +610,11 @@ namespace Canvas3D {
                     p.viewport = props.viewport;
                     handleResize();
                 }
+                if (props.stereo !== undefined) {
+                    p.stereo = props.stereo;
+                    pickPass.isStereo = p.stereo.name === 'on';
+                    drawPass.isStereo = p.stereo.name === 'on';
+                }
 
                 if (props.postprocessing) postprocessing.setProps(props.postprocessing);
                 if (props.multiSample) multiSample.setProps(props.multiSample);

+ 2 - 2
src/mol-canvas3d/helper/camera-helper.ts

@@ -6,7 +6,7 @@
 
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import Scene from '../../mol-gl/scene';
-import { Camera } from '../camera';
+import { Camera, ICamera } from '../camera';
 import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
 import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
 import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
@@ -86,7 +86,7 @@ export class CameraHelper {
         return this.props.axes.name === 'on';
     }
 
-    update(camera: Camera) {
+    update(camera: ICamera) {
         if (!this.renderObject) return;
 
         updateCamera(this.camera, camera.viewport, camera.viewOffset);

+ 27 - 12
src/mol-canvas3d/passes/draw.ts

@@ -10,7 +10,7 @@ import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
 import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
 import { Texture } from '../../mol-gl/webgl/texture';
-import { Camera } from '../camera';
+import { ICamera } from '../camera';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { HandleHelper } from '../helper/handle-helper';
@@ -21,6 +21,7 @@ import { ShaderCode } from '../../mol-gl/shader-code';
 import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { ValueCell } from '../../mol-util';
 import { Vec2 } from '../../mol-math/linear-algebra';
+import { StereoCamera } from '../camera/stereo';
 
 import quad_vert from '../../mol-gl/shader/quad.vert';
 import depthMerge_frag from '../../mol-gl/shader/depth-merge.frag';
@@ -70,9 +71,11 @@ export class DrawPass {
     private depthTextureVolumes: Texture
     private depthMerge: DepthMergeRenderable
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper, private handleHelper: HandleHelper, props: Partial<DrawPassProps> = {}) {
+    isStereo = false
+
+    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: { standard: ICamera, stereo?: StereoCamera }, private debugHelper: BoundingSphereHelper, private handleHelper: HandleHelper, props: Partial<DrawPassProps> = {}) {
         const { extensions, resources } = webgl;
-        const { width, height } = camera.viewport;
+        const { width, height } = camera.standard.viewport;
 
         this.colorTarget = webgl.createRenderTarget(width, height);
         this.packedDepth = !extensions.depthTexture;
@@ -125,25 +128,35 @@ export class DrawPass {
     }
 
     render(toDrawingBuffer: boolean, transparentBackground: boolean) {
-        const { x, y, width, height } = this.camera.viewport;
+        if (this.isStereo && this.camera.stereo) {
+            this._render(this.camera.stereo.left, toDrawingBuffer, transparentBackground);
+            this._render(this.camera.stereo.right, toDrawingBuffer, transparentBackground);
+        } else {
+            this._render(this.camera.standard, toDrawingBuffer, transparentBackground);
+        }
+    }
+
+    private _render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean) {
+        const { x, y, width, height } = camera.viewport;
         if (toDrawingBuffer) {
             this.webgl.unbindFramebuffer();
             this.renderer.setViewport(x, y, width, height);
         } else {
             this.colorTarget.bind();
-            this.renderer.setViewport(0, 0, width, height);
+            this.renderer.setViewport(x, y, width, height);
             if (!this.packedDepth) {
                 this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
             }
         }
 
-        this.renderer.render(this.scene.primitives, this.camera, 'color', true, transparentBackground, null);
+        this.renderer.render(this.scene.primitives, camera, 'color', true, transparentBackground, null);
 
         // do a depth pass if not rendering to drawing buffer and
         // extensions.depthTexture is unsupported (i.e. depthTarget is set)
         if (!toDrawingBuffer && this.depthTargetPrimitives) {
             this.depthTargetPrimitives.bind();
-            this.renderer.render(this.scene.primitives, this.camera, 'depth', true, transparentBackground, null);
+            this.renderer.setViewport(x, y, width, height);
+            this.renderer.render(this.scene.primitives, camera, 'depth', true, transparentBackground, null);
             this.colorTarget.bind();
         }
 
@@ -154,12 +167,12 @@ export class DrawPass {
                 this.webgl.state.depthMask(true);
                 this.webgl.gl.clear(this.webgl.gl.DEPTH_BUFFER_BIT);
             }
-            this.renderer.render(this.scene.volumes, this.camera, 'color', false, transparentBackground, this.depthTexturePrimitives);
+            this.renderer.render(this.scene.volumes, camera, 'color', false, transparentBackground, this.depthTexturePrimitives);
 
             // do volume depth pass if extensions.depthTexture is unsupported (i.e. depthTarget is set)
             if (this.depthTargetVolumes) {
                 this.depthTargetVolumes.bind();
-                this.renderer.render(this.scene.volumes, this.camera, 'depth', true, transparentBackground, this.depthTexturePrimitives);
+                this.renderer.render(this.scene.volumes, camera, 'depth', true, transparentBackground, this.depthTexturePrimitives);
                 this.colorTarget.bind();
             }
         }
@@ -168,6 +181,7 @@ export class DrawPass {
         if (!toDrawingBuffer) {
             this.depthMerge.update();
             this.depthTarget.bind();
+            this.renderer.setViewport(x, y, width, height);
             this.webgl.state.disable(this.webgl.gl.SCISSOR_TEST);
             this.webgl.state.disable(this.webgl.gl.BLEND);
             this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
@@ -176,17 +190,18 @@ export class DrawPass {
             this.webgl.gl.clear(this.webgl.gl.COLOR_BUFFER_BIT);
             this.depthMerge.render();
             this.colorTarget.bind();
+            this.renderer.setViewport(x, y, width, height);
         }
 
         if (this.debugHelper.isEnabled) {
             this.debugHelper.syncVisibility();
-            this.renderer.render(this.debugHelper.scene, this.camera, 'color', false, transparentBackground, null);
+            this.renderer.render(this.debugHelper.scene, camera, 'color', false, transparentBackground, null);
         }
         if (this.handleHelper.isEnabled) {
-            this.renderer.render(this.handleHelper.scene, this.camera, 'color', false, transparentBackground, null);
+            this.renderer.render(this.handleHelper.scene, camera, 'color', false, transparentBackground, null);
         }
         if (this.cameraHelper.isEnabled) {
-            this.cameraHelper.update(this.camera);
+            this.cameraHelper.update(camera);
             this.renderer.render(this.cameraHelper.scene, this.cameraHelper.camera, 'color', false, transparentBackground, null);
         }
     }

+ 1 - 1
src/mol-canvas3d/passes/image.ts

@@ -47,7 +47,7 @@ export class ImagePass {
 
         this._transparentBackground = p.transparentBackground;
 
-        this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper, handleHelper, p.drawPass);
+        this.drawPass = new DrawPass(webgl, renderer, scene, { standard: this._camera }, debugHelper, handleHelper, p.drawPass);
         this.postprocessing = new PostprocessingPass(webgl, this._camera, this.drawPass, p.postprocessing);
         this.multiSample = new MultiSamplePass(webgl, this._camera, this.drawPass, this.postprocessing, p.multiSample);
 

+ 2 - 1
src/mol-canvas3d/passes/multi-sample.ts

@@ -133,7 +133,8 @@ export class MultiSamplePass {
         for (let i = 0; i < offsetList.length; ++i) {
             const offset = offsetList[i];
             Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
-            camera.update();
+            // TODO: this should not be needed
+            // camera.update();
             this.drawPass.cameraHelper.update(camera);
 
             // the theory is that equal weights for each sample lead to an accumulation of rounding

+ 38 - 18
src/mol-canvas3d/passes/pick.ts

@@ -4,13 +4,15 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLContext } from '../../mol-gl/webgl/context';
-import { RenderTarget } from '../../mol-gl/webgl/render-target';
+import { PickingId } from '../../mol-geo/geometry/picking';
 import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
-import { PickingId } from '../../mol-geo/geometry/picking';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { GraphicsRenderVariant } from '../../mol-gl/webgl/render-item';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { decodeFloatRGB } from '../../mol-util/float-packing';
-import { Camera } from '../camera';
+import { Camera, ICamera } from '../camera';
+import { StereoCamera } from '../camera/stereo';
 import { HandleHelper } from '../helper/handle-helper';
 import { DrawPass } from './draw';
 
@@ -23,6 +25,8 @@ export class PickPass {
     instancePickTarget: RenderTarget
     groupPickTarget: RenderTarget
 
+    isStereo = false
+
     private objectBuffer: Uint8Array
     private instanceBuffer: Uint8Array
     private groupBuffer: Uint8Array
@@ -31,7 +35,7 @@ export class PickPass {
     private pickWidth: number
     private pickHeight: number
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private handleHelper: HandleHelper, private pickBaseScale: number, private drawPass: DrawPass) {
+    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private stereoCamera: StereoCamera, private handleHelper: HandleHelper, private pickBaseScale: number, private drawPass: DrawPass) {
         this.pickScale = pickBaseScale / webgl.pixelRatio;
         this.pickWidth = Math.ceil(camera.viewport.width * this.pickScale);
         this.pickHeight = Math.ceil(camera.viewport.height * this.pickScale);
@@ -69,25 +73,39 @@ export class PickPass {
         }
     }
 
-    render() {
-        const { renderer, scene, camera, handleHelper: { scene: handleScene } } = this;
+    private renderVariant(variant: GraphicsRenderVariant) {
+        if (this.isStereo) {
+            const w = (this.pickWidth / 2) | 0;
+
+            this.renderer.setViewport(0, 0, w, this.pickHeight);
+            this._renderVariant(this.stereoCamera.left, variant);
+
+            this.renderer.setViewport(w, 0, this.pickWidth - w, this.pickHeight);
+            this._renderVariant(this.stereoCamera.right, variant);
+        } else {
+            this.renderer.setViewport(0, 0, this.pickWidth, this.pickHeight);
+            this._renderVariant(this.camera, variant);
+        }
+    }
+
+    private _renderVariant(camera: ICamera, variant: GraphicsRenderVariant) {
+        const { renderer, scene, handleHelper: { scene: handleScene } } = this;
         const depth = this.drawPass.depthTexturePrimitives;
-        renderer.setViewport(0, 0, this.pickWidth, this.pickHeight);
 
+        renderer.render(scene.primitives, camera, variant, true, false, null);
+        renderer.render(scene.volumes, camera, variant, false, false, depth);
+        renderer.render(handleScene, camera, variant, false, false, null);
+    }
+
+    render() {
         this.objectPickTarget.bind();
-        renderer.render(scene.primitives, camera, 'pickObject', true, false, null);
-        renderer.render(scene.volumes, camera, 'pickObject', false, false, depth);
-        renderer.render(handleScene, camera, 'pickObject', false, false, null);
+        this.renderVariant('pickObject');
 
         this.instancePickTarget.bind();
-        renderer.render(scene.primitives, camera, 'pickInstance', true, false, null);
-        renderer.render(scene.volumes, camera, 'pickInstance', false, false, depth);
-        renderer.render(handleScene, camera, 'pickInstance', false, false, null);
+        this.renderVariant('pickInstance');
 
         this.groupPickTarget.bind();
-        renderer.render(scene.primitives, camera, 'pickGroup', true, false, null);
-        renderer.render(scene.volumes, camera, 'pickGroup', false, false, depth);
-        renderer.render(handleScene, camera, 'pickGroup', false, false, null);
+        this.renderVariant('pickGroup');
 
         this.pickDirty = false;
     }
@@ -123,7 +141,9 @@ export class PickPass {
             gl.drawingBufferHeight - y < viewport.y ||
             x > viewport.x + viewport.width ||
             gl.drawingBufferHeight - y > viewport.y + viewport.height
-        ) return;
+        ) {
+            return;
+        }
 
         if (this.pickDirty) {
             this.render();

+ 1 - 0
src/mol-canvas3d/passes/postprocessing.ts

@@ -167,6 +167,7 @@ export class PostprocessingPass {
         }
 
         const { x, y, width, height } = this.camera.viewport;
+
         const { gl, state } = this.webgl;
         if (toDrawingBuffer) {
             this.webgl.unbindFramebuffer();

+ 3 - 3
src/mol-gl/renderer.ts

@@ -5,7 +5,7 @@
  */
 
 import { Viewport } from '../mol-canvas3d/camera/util';
-import { Camera } from '../mol-canvas3d/camera';
+import { ICamera } from '../mol-canvas3d/camera';
 
 import Scene from './scene';
 import { WebGLContext } from './webgl/context';
@@ -43,7 +43,7 @@ interface Renderer {
     readonly props: Readonly<RendererProps>
 
     clear: (transparentBackground: boolean) => void
-    render: (group: Scene.Group, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => void
+    render: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => void
     setProps: (props: Partial<RendererProps>) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
     dispose: () => void
@@ -300,7 +300,7 @@ namespace Renderer {
             r.render(variant);
         };
 
-        const render = (group: Scene.Group, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => {
+        const render = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => {
             ValueCell.update(globalUniforms.uModel, group.view);
             ValueCell.update(globalUniforms.uView, camera.view);
             ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view));

+ 3 - 0
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -62,6 +62,7 @@ const SimpleSettingsParams = {
         outline: Canvas3DParams.postprocessing.params.outline,
         fog: Canvas3DParams.cameraFog,
     }, { pivot: 'renderStyle' }),
+    stereo: Canvas3DParams.stereo,
     clipping: PD.Group<any>({
         ...Canvas3DParams.cameraClipping.params,
         ...(Canvas3DParams.renderer.params.clip as any).params as any
@@ -111,6 +112,7 @@ const SimpleSettingsMapping = ParamMapping({
                 outline: canvas.postprocessing.outline,
                 fog: canvas.cameraFog
             },
+            stereo: canvas.stereo,
             clipping: {
                 ...canvas.cameraClipping,
                 ...canvas.renderer.clip
@@ -136,6 +138,7 @@ const SimpleSettingsMapping = ParamMapping({
             variant: s.clipping.variant,
             objects: s.clipping.objects,
         };
+        canvas.stereo = s.stereo;
 
         props.layout = s.layout;
     },