diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts
index 3b09acdd91713ade71915338156e0f442b54c480..6cacc6c45dac530710816dcfd783eb154eb87139 100644
--- a/src/apps/viewer/index.ts
+++ b/src/apps/viewer/index.ts
@@ -254,6 +254,10 @@ export class Viewer {
             await repr.commit();
         });
     }
+
+    handleResize() {
+        this.plugin.layout.events.updated.next();
+    }
 }
 
 export interface VolumeIsovalueInfo {
diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts
index 2a0792fd5754ea5385e4999396e03a99fc5f78e2..3b99356dbf2dd6680ca97a7b8f55919ab4618dc1 100644
--- a/src/mol-canvas3d/camera.ts
+++ b/src/mol-canvas3d/camera.ts
@@ -16,6 +16,8 @@ interface ICamera {
     readonly viewport: Viewport,
     readonly view: Mat4,
     readonly projection: Mat4,
+    readonly projectionView: Mat4,
+    readonly inverseProjectionView: Mat4,
     readonly state: Readonly<Camera.Snapshot>,
     readonly viewOffset: Camera.ViewOffset,
     readonly far: number,
diff --git a/src/mol-canvas3d/camera/stereo.ts b/src/mol-canvas3d/camera/stereo.ts
index 8aa206b48cd63794cfd139d61a85be91b1753379..956e3762753cbedef5e957472578969178cfa83c 100644
--- a/src/mol-canvas3d/camera/stereo.ts
+++ b/src/mol-canvas3d/camera/stereo.ts
@@ -2,21 +2,56 @@
  * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  *
  * 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 { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Camera, ICamera } from '../camera';
 import { Viewport } from './util';
 
-export class StereoCamera {
+export const StereoCameraParams = {
+    eyeSeparation: PD.Numeric(0.064, { min: 0.01, max: 0.5, step: 0.001 }),
+    focus: PD.Numeric(10, { min: 1, max: 100, step: 0.01 }),
+};
+export const DefaultStereoCameraProps = PD.getDefaultValues(StereoCameraParams);
+export type StereoCameraProps = PD.Values<typeof StereoCameraParams>
+
+export { StereoCamera };
+
+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);
+    get viewport() {
+        return this.parent.viewport;
+    }
+
+    get viewOffset() {
+        return this.parent.viewOffset;
+    }
+
+    private props: StereoCameraProps
+
+    constructor(private parent: Camera, props: Partial<StereoCameraProps> = {}) {
+        this.props = { ...DefaultStereoCameraProps, ...props };
+    }
+
+    setProps(props: Partial<StereoCameraProps>) {
+        Object.assign(this.props, props);
+    }
+
+    update() {
+        this.parent.update();
+        update(this.parent, this.props, this.left as EyeCamera, this.right as EyeCamera);
+    }
+}
+
+namespace StereoCamera {
+    export function is(camera: Camera | StereoCamera): camera is StereoCamera {
+        return 'left' in camera && 'right' in camera;
     }
 }
 
@@ -24,6 +59,8 @@ class EyeCamera implements ICamera {
     viewport = Viewport.create(0, 0, 0, 0);
     view = Mat4();
     projection = Mat4();
+    projectionView = Mat4();
+    inverseProjectionView = Mat4();
     state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
     viewOffset: Readonly<Camera.ViewOffset> = Camera.ViewOffset();
     far: number = 0;
@@ -32,16 +69,9 @@ class EyeCamera implements ICamera {
     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) {
+function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right: EyeCamera) {
     // Copy the states
 
     Viewport.copy(left.viewport, camera.viewport);
@@ -65,41 +95,47 @@ function update(camera: ICamera, params: StereoCameraParams, left: EyeCamera, ri
     right.fogNear = camera.fogNear;
 
     // update the view offsets
-    let w = (camera.viewport.width / 2) | 0;
+
+    const w = Math.floor(camera.viewport.width / 2);
+    const aspect = w / camera.viewport.height;
 
     left.viewport.width = w;
-    right.viewport.x = 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;
+    const eyeSepHalf = props.eyeSeparation / 2;
+    const eyeSepOnProjection = eyeSepHalf * camera.near / props.focus;
+    const ymax = camera.near * Math.tan(camera.state.fov * 0.5);
     let xmin, xmax;
 
     // translate xOffset
 
-    eyeLeft[12] = - eyeSepHalf;
+    eyeLeft[12] = -eyeSepHalf;
     eyeRight[12] = eyeSepHalf;
 
     // for left eye
 
-    xmin = - ymax * params.aspect + eyeSepOnProjection;
-    xmax = ymax * params.aspect + eyeSepOnProjection;
+    xmin = -ymax * aspect + eyeSepOnProjection;
+    xmax = ymax * aspect + eyeSepOnProjection;
 
     left.projection[0] = 2 * camera.near / (xmax - xmin);
     left.projection[8] = (xmax + xmin) / (xmax - xmin);
 
     Mat4.mul(left.view, left.view, eyeLeft);
+    Mat4.mul(left.projectionView, left.projection, left.view);
+    Mat4.invert(left.inverseProjectionView, left.projectionView);
 
     // for right eye
 
-    xmin = - ymax * params.aspect - eyeSepOnProjection;
-    xmax = ymax * params.aspect - eyeSepOnProjection;
+    xmin = -ymax * aspect - eyeSepOnProjection;
+    xmax = ymax * aspect - eyeSepOnProjection;
 
     right.projection[0] = 2 * camera.near / (xmax - xmin);
     right.projection[8] = (xmax + xmin) / (xmax - xmin);
 
     Mat4.mul(right.view, right.view, eyeRight);
+    Mat4.mul(right.projectionView, right.projection, right.view);
+    Mat4.invert(right.inverseProjectionView, right.projectionView);
 }
\ No newline at end of file
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index f484230e73473d1dafa2845680a820225ad35696..03bb32c4bd383d003012e680bce95883ea45e752 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -21,25 +21,31 @@ import { MarkerAction } from '../mol-util/marker-action';
 import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
 import { Camera } from './camera';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
-import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
+import { DebugHelperParams } from './helper/bounding-sphere-helper';
 import { SetUtils } from '../mol-util/set';
 import { Canvas3dInteractionHelper } from './helper/interaction-events';
 import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
 import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
-import { DrawPass } from './passes/draw';
-import { PickData, PickPass } from './passes/pick';
+import { PickData } from './passes/pick';
+import { PickHelper } from './passes/pick';
 import { ImagePass, ImageProps } from './passes/image';
 import { Sphere3D } from '../mol-math/geometry';
 import { isDebugMode } from '../mol-util/debug';
 import { CameraHelperParams } from './helper/camera-helper';
 import { produce } from 'immer';
-import { HandleHelper, HandleHelperParams } from './helper/handle-helper';
+import { HandleHelperParams } from './helper/handle-helper';
 import { StereoCamera, StereoCameraParams } from './camera/stereo';
+import { Helper } from './helper/helper';
+import { Passes } from './passes/passes';
 
 export const Canvas3DParams = {
     camera: PD.Group({
-        mode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']] as const, { label: 'Camera' }),
+        mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }),
         helper: PD.Group(CameraHelperParams, { isFlat: true }),
+        stereo: PD.MappedStatic('off', {
+            on: PD.Group(StereoCameraParams),
+            off: PD.Group({})
+        }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
         manualReset: PD.Boolean(false, { isHidden: true })
     }, { pivot: 'mode' }),
     cameraFog: PD.MappedStatic('on', {
@@ -61,10 +67,6 @@ 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),
@@ -136,7 +138,7 @@ namespace Canvas3D {
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, position?: Vec3 }
 
-    export function fromCanvas(canvas: HTMLCanvasElement, props: PartialCanvas3DProps = {}, attribs: Partial<{ antialias: boolean, pixelScale: number }> = {}) {
+    export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number }> = {}) {
         const gl = getGLContext(canvas, {
             alpha: true,
             antialias: attribs.antialias ?? true,
@@ -149,6 +151,7 @@ namespace Canvas3D {
         const { pixelScale } = attribs;
         const input = InputObserver.fromElement(canvas, { pixelScale });
         const webgl = createContext(gl, { pixelScale });
+        const passes = new Passes(webgl, attribs);
 
         if (isDebugMode) {
             const loseContextExt = gl.getExtension('WEBGL_lose_context');
@@ -183,11 +186,11 @@ namespace Canvas3D {
             if (isDebugMode) console.log('context restored');
         }, false);
 
-        return create(webgl, input, props, { pixelScale });
+        return create(webgl, input, passes, props, { pixelScale });
     }
 
-    export function create(webgl: WebGLContext, input: InputObserver, props: PartialCanvas3DProps = {}, attribs: Partial<{ pickScale: number, pixelScale: number }> = {}): Canvas3D {
-        const p = { ...DefaultCanvas3DParams, ...props };
+    export function create(webgl: WebGLContext, input: InputObserver, passes: Passes, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ pixelScale: number }>): Canvas3D {
+        const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
 
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
@@ -212,23 +215,14 @@ 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 stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
 
         const controls = TrackballControls.create(input, camera, p.trackball);
         const renderer = Renderer.create(webgl, p.renderer);
-        const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
-        const handleHelper = new HandleHelper(webgl, p.handle);
-        const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
+        const helper = new Helper(webgl, scene, p);
 
-        const drawPass = new DrawPass(webgl, renderer, scene, { standard: camera, stereo: stereoCamera }, debugHelper, handleHelper, {
-            cameraHelper: p.camera.helper
-        });
-        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);
+        const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height });
+        const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
 
         let drawPending = false;
         let cameraResetRequested = false;
@@ -239,7 +233,7 @@ namespace Canvas3D {
             let loci: Loci = EmptyLoci;
             let repr: Representation.Any = Representation.Empty;
             if (pickingId) {
-                loci = handleHelper.getLoci(pickingId);
+                loci = helper.handle.getLoci(pickingId);
                 reprRenderObjects.forEach((_, _repr) => {
                     const _loci = _repr.getLoci(pickingId);
                     if (!isEmptyLoci(_loci)) {
@@ -260,15 +254,15 @@ namespace Canvas3D {
             if (repr) {
                 changed = repr.mark(loci, action);
             } else {
-                changed = handleHelper.mark(loci, action);
+                changed = helper.handle.mark(loci, action);
                 reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
             }
             if (changed) {
                 scene.update(void 0, true);
-                handleHelper.scene.update(void 0, true);
-                const prevPickDirty = pickPass.pickDirty;
+                helper.handle.scene.update(void 0, true);
+                const prevPickDirty = pickHelper.dirty;
                 draw(true);
-                pickPass.pickDirty = prevPickDirty; // marking does not change picking buffers
+                pickHelper.dirty = prevPickDirty; // marking does not change picking buffers
             }
         }
 
@@ -280,25 +274,24 @@ namespace Canvas3D {
 
             let didRender = false;
             controls.update(currentTime);
-            Viewport.set(camera.viewport, x, y, width, height);
             const cameraChanged = camera.update();
-            const multiSampleChanged = multiSample.update(force || cameraChanged);
-
-            const isStereo = p.stereo.name === 'on';
+            const multiSampleChanged = passes.multiSample.update(force || cameraChanged, p.multiSample);
 
             if (force || cameraChanged || multiSampleChanged) {
-                if ((force || cameraChanged) && p.stereo.name === 'on') stereoCamera.update(camera, p.stereo.params);
+                let cam: Camera | StereoCamera = camera;
+                if (p.camera.stereo.name === 'on') {
+                    stereoCamera.update();
+                    cam = stereoCamera;
+                }
 
-                renderer.setViewport(x, y, width, height);
-                // TODO: support stereo rendering in multisampling
-                if (!isStereo && multiSample.enabled) {
-                    multiSample.render(true, p.transparentBackground);
+                if (MultiSamplePass.isEnabled(p.multiSample)) {
+                    passes.multiSample.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
                 } else {
-                    const toDrawingBuffer = !postprocessing.enabled && scene.volumes.renderables.length === 0;
-                    drawPass.render(toDrawingBuffer, p.transparentBackground);
-                    if (!toDrawingBuffer) postprocessing.render(true);
+                    const toDrawingBuffer = !PostprocessingPass.isEnabled(p.postprocessing) && scene.volumes.renderables.length === 0;
+                    passes.draw.render(renderer, cam, scene, helper, toDrawingBuffer, p.transparentBackground);
+                    if (!toDrawingBuffer) passes.postprocessing.render(cam, true, p.postprocessing);
                 }
-                pickPass.pickDirty = true;
+                pickHelper.dirty = true;
                 didRender = true;
             }
 
@@ -347,7 +340,8 @@ namespace Canvas3D {
         }
 
         function identify(x: number, y: number): PickData | undefined {
-            return webgl.isContextLost ? undefined : pickPass.identify(x, y);
+            const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
+            return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
         }
 
         function commit(isSynchronous: boolean = false) {
@@ -356,7 +350,7 @@ namespace Canvas3D {
             if (allCommited) {
                 resolveCameraReset();
                 if (forceDrawAfterAllCommited) {
-                    if (debugHelper.isEnabled) debugHelper.update();
+                    if (helper.debug.isEnabled) helper.debug.update();
                     draw(true);
                     forceDrawAfterAllCommited = false;
                 }
@@ -414,7 +408,7 @@ namespace Canvas3D {
 
             if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
 
-            if (debugHelper.isEnabled) debugHelper.update();
+            if (helper.debug.isEnabled) helper.debug.update();
             if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
                 cameraResetRequested = true;
             }
@@ -494,7 +488,8 @@ namespace Canvas3D {
             return {
                 camera: {
                     mode: camera.state.mode,
-                    helper: { ...drawPass.props.cameraHelper },
+                    helper: { ...helper.camera.props },
+                    stereo: { ...p.camera.stereo },
                     manualReset: !!p.camera.manualReset
                 },
                 cameraFog: camera.state.fog > 0
@@ -504,21 +499,18 @@ namespace Canvas3D {
                 cameraResetDurationMs: p.cameraResetDurationMs,
                 transparentBackground: p.transparentBackground,
                 viewport: p.viewport,
-                stereo: p.stereo,
 
-                postprocessing: { ...postprocessing.props },
-                multiSample: { ...multiSample.props },
+                postprocessing: { ...p.postprocessing },
+                multiSample: { ...p.multiSample },
                 renderer: { ...renderer.props },
                 trackball: { ...controls.props },
-                debug: { ...debugHelper.props },
-                handle: { ...handleHelper.props },
+                debug: { ...helper.debug.props },
+                handle: { ...helper.handle.props },
             };
         }
 
-        handleResize();
-
         const contextRestoredSub = contextRestored.subscribe(() => {
-            pickPass.pickDirty = true;
+            pickHelper.dirty = true;
             draw(true);
         });
 
@@ -542,7 +534,7 @@ namespace Canvas3D {
                 reprUpdatedSubscriptions.clear();
                 reprRenderObjects.clear();
                 scene.clear();
-                debugHelper.clear();
+                helper.debug.clear();
                 requestDraw(true);
                 reprCount.next(reprRenderObjects.size);
             },
@@ -553,7 +545,7 @@ namespace Canvas3D {
                 }
 
                 if (scene.syncVisibility()) {
-                    if (debugHelper.isEnabled) debugHelper.update();
+                    if (helper.debug.isEnabled) helper.debug.update();
                 }
                 requestDraw(true);
             },
@@ -565,7 +557,12 @@ namespace Canvas3D {
             mark,
             getLoci,
 
-            handleResize,
+            handleResize: () => {
+                passes.updateSize();
+                updateViewport();
+                syncViewport();
+                requestDraw(true);
+            },
             requestCameraReset: options => {
                 nextCameraResetDuration = options?.durationMs;
                 nextCameraResetSnapshot = options?.snapshot;
@@ -602,31 +599,35 @@ namespace Canvas3D {
                 }
                 if (Object.keys(cameraState).length > 0) camera.setState(cameraState);
 
-                if (props.camera?.helper) drawPass.setProps({ cameraHelper: props.camera.helper });
+                if (props.camera?.helper) helper.camera.setProps(props.camera.helper);
                 if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset;
+                if (props.camera?.stereo !== undefined) Object.assign(p.camera.stereo, props.camera.stereo);
                 if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
                 if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
                 if (props.viewport !== undefined) {
+                    // clear old viewport
+                    renderer.setViewport(x, y, width, height);
+                    renderer.clear(p.transparentBackground);
                     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';
+                    updateViewport();
+                    syncViewport();
                 }
 
-                if (props.postprocessing) postprocessing.setProps(props.postprocessing);
-                if (props.multiSample) multiSample.setProps(props.multiSample);
+                if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
+                if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
                 if (props.renderer) renderer.setProps(props.renderer);
                 if (props.trackball) controls.setProps(props.trackball);
-                if (props.debug) debugHelper.setProps(props.debug);
-                if (props.handle) handleHelper.setProps(props.handle);
+                if (props.debug) helper.debug.setProps(props.debug);
+                if (props.handle) helper.handle.setProps(props.handle);
+
+                if (cameraState.mode === 'orthographic') {
+                    p.camera.stereo.name = 'off';
+                }
 
                 requestDraw(true);
             },
             getImagePass: (props: Partial<ImageProps> = {}) => {
-                return new ImagePass(webgl, renderer, scene, camera, debugHelper, handleHelper, props);
+                return new ImagePass(webgl, renderer, scene, camera, helper, props);
             },
 
             get props() {
@@ -645,7 +646,7 @@ namespace Canvas3D {
                 contextRestoredSub.unsubscribe();
 
                 scene.clear();
-                debugHelper.clear();
+                helper.debug.clear();
                 input.dispose();
                 controls.dispose();
                 renderer.dispose();
@@ -667,19 +668,11 @@ namespace Canvas3D {
             }
         }
 
-        function handleResize() {
-            updateViewport();
-
+        function syncViewport() {
+            pickHelper.setViewport(x, y, width, height);
             renderer.setViewport(x, y, width, height);
             Viewport.set(camera.viewport, x, y, width, height);
             Viewport.set(controls.viewport, x, y, width, height);
-
-            drawPass.setSize(width, height);
-            pickPass.setSize(width, height);
-            postprocessing.setSize(width, height);
-            multiSample.setSize(width, height);
-
-            requestDraw(true);
         }
     }
 }
\ No newline at end of file
diff --git a/src/mol-canvas3d/helper/helper.ts b/src/mol-canvas3d/helper/helper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5db1c9234a6d9178b64ce8271f29cb88f2d866c8
--- /dev/null
+++ b/src/mol-canvas3d/helper/helper.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Scene from '../../mol-gl/scene';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { BoundingSphereHelper, DebugHelperParams } from './bounding-sphere-helper';
+import { CameraHelper, CameraHelperParams } from './camera-helper';
+import { HandleHelper, HandleHelperParams } from './handle-helper';
+
+export const HelperParams = {
+    debug: PD.Group(DebugHelperParams),
+    camera: PD.Group({
+        helper: PD.Group(CameraHelperParams)
+    }),
+    handle: PD.Group(HandleHelperParams),
+};
+export const DefaultHelperProps = PD.getDefaultValues(HelperParams);
+export type HelperProps = PD.Values<typeof HelperParams>
+
+
+export class Helper {
+    readonly debug: BoundingSphereHelper
+    readonly camera: CameraHelper
+    readonly handle: HandleHelper
+
+    constructor(webgl: WebGLContext, scene: Scene, props: Partial<HelperProps> = {}) {
+        const p = { ...DefaultHelperProps, ...props };
+
+        this.debug = new BoundingSphereHelper(webgl, scene, p.debug);
+        this.camera = new CameraHelper(webgl, p.camera.helper);
+        this.handle = new HandleHelper(webgl, p.handle);
+    }
+}
\ No newline at end of file
diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts
index aabc493527e698714f8dc2c67408725dc3203f12..85ce63a30cd0b4c880d3e4a4479697b819cdad5a 100644
--- a/src/mol-canvas3d/passes/draw.ts
+++ b/src/mol-canvas3d/passes/draw.ts
@@ -8,12 +8,8 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 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 { 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';
+import { Camera, ICamera } from '../camera';
 import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
 import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
 import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
@@ -21,10 +17,11 @@ 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 { Helper } from '../helper/helper';
 
 import quad_vert from '../../mol-gl/shader/quad.vert';
 import depthMerge_frag from '../../mol-gl/shader/depth-merge.frag';
+import { StereoCamera } from '../camera/stereo';
 
 const DepthMergeSchema = {
     ...QuadSchema,
@@ -51,31 +48,20 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text
     return createComputeRenderable(renderItem, values);
 }
 
-export const DrawPassParams = {
-    cameraHelper: PD.Group(CameraHelperParams)
-};
-export const DefaultDrawPassProps = PD.getDefaultValues(DrawPassParams);
-export type DrawPassProps = PD.Values<typeof DrawPassParams>
-
 export class DrawPass {
     readonly colorTarget: RenderTarget
     readonly depthTexture: Texture
     readonly depthTexturePrimitives: Texture
-    readonly packedDepth: boolean
-
-    readonly cameraHelper: CameraHelper
 
+    private readonly packedDepth: boolean
     private depthTarget: RenderTarget
     private depthTargetPrimitives: RenderTarget | null
     private depthTargetVolumes: RenderTarget | null
     private depthTextureVolumes: Texture
     private depthMerge: DepthMergeRenderable
 
-    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> = {}) {
+    constructor(private webgl: WebGLContext, width: number, height: number) {
         const { extensions, resources } = webgl;
-        const { width, height } = camera.standard.viewport;
 
         this.colorTarget = webgl.createRenderTarget(width, height);
         this.packedDepth = !extensions.depthTexture;
@@ -93,70 +79,52 @@ export class DrawPass {
             this.depthTextureVolumes.define(width, height);
         }
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
-
-        const p = { ...DefaultDrawPassProps, ...props };
-        this.cameraHelper = new CameraHelper(webgl, p.cameraHelper);
     }
 
     setSize(width: number, height: number) {
-        this.colorTarget.setSize(width, height);
-        this.depthTarget.setSize(width, height);
+        const w = this.colorTarget.getWidth();
+        const h = this.colorTarget.getHeight();
 
-        if (this.depthTargetPrimitives) {
-            this.depthTargetPrimitives.setSize(width, height);
-        } else {
-            this.depthTexturePrimitives.define(width, height);
-        }
-
-        if (this.depthTargetVolumes) {
-            this.depthTargetVolumes.setSize(width, height);
-        } else {
-            this.depthTextureVolumes.define(width, height);
-        }
+        if (width !== w || height !== h) {
+            this.colorTarget.setSize(width, height);
+            this.depthTarget.setSize(width, height);
 
-        ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height));
-    }
-
-    setProps(props: Partial<DrawPassProps>) {
-        if (props.cameraHelper) this.cameraHelper.setProps(props.cameraHelper);
-    }
+            if (this.depthTargetPrimitives) {
+                this.depthTargetPrimitives.setSize(width, height);
+            } else {
+                this.depthTexturePrimitives.define(width, height);
+            }
 
-    get props(): DrawPassProps {
-        return {
-            cameraHelper: { ...this.cameraHelper.props }
-        };
-    }
+            if (this.depthTargetVolumes) {
+                this.depthTargetVolumes.setSize(width, height);
+            } else {
+                this.depthTextureVolumes.define(width, height);
+            }
 
-    render(toDrawingBuffer: boolean, transparentBackground: boolean) {
-        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);
+            ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height));
         }
     }
 
-    private _render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean) {
+    _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {
         const { x, y, width, height } = camera.viewport;
+        renderer.setViewport(x, y, width, height);
+
         if (toDrawingBuffer) {
             this.webgl.unbindFramebuffer();
-            this.renderer.setViewport(x, y, width, height);
         } else {
             this.colorTarget.bind();
-            this.renderer.setViewport(x, y, width, height);
             if (!this.packedDepth) {
                 this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
             }
         }
 
-        this.renderer.render(this.scene.primitives, camera, 'color', true, transparentBackground, null);
+        renderer.render(scene.primitives, camera, 'color', true, transparentBackground, 1, 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.setViewport(x, y, width, height);
-            this.renderer.render(this.scene.primitives, camera, 'depth', true, transparentBackground, null);
+            renderer.render(scene.primitives, camera, 'depth', true, transparentBackground, 1, null);
             this.colorTarget.bind();
         }
 
@@ -165,14 +133,16 @@ export class DrawPass {
             if (!this.packedDepth) {
                 this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
                 this.webgl.state.depthMask(true);
+                this.webgl.gl.viewport(x, y, width, height);
+                this.webgl.gl.scissor(x, y, width, height);
                 this.webgl.gl.clear(this.webgl.gl.DEPTH_BUFFER_BIT);
             }
-            this.renderer.render(this.scene.volumes, camera, 'color', false, transparentBackground, this.depthTexturePrimitives);
+            renderer.render(scene.volumes, camera, 'color', false, transparentBackground, 1, 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, camera, 'depth', true, transparentBackground, this.depthTexturePrimitives);
+                renderer.render(scene.volumes, camera, 'depth', true, transparentBackground, 1, this.depthTexturePrimitives);
                 this.colorTarget.bind();
             }
         }
@@ -181,28 +151,37 @@ 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.SCISSOR_TEST);
             this.webgl.state.disable(this.webgl.gl.BLEND);
             this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
             this.webgl.state.depthMask(false);
             this.webgl.state.clearColor(1, 1, 1, 1);
+            this.webgl.gl.viewport(x, y, width, height);
+            this.webgl.gl.scissor(x, y, width, height);
             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, camera, 'color', false, transparentBackground, null);
+        if (helper.debug.isEnabled) {
+            helper.debug.syncVisibility();
+            renderer.render(helper.debug.scene, camera, 'color', false, transparentBackground, 1, null);
+        }
+        if (helper.handle.isEnabled) {
+            renderer.render(helper.handle.scene, camera, 'color', false, transparentBackground, 1, null);
         }
-        if (this.handleHelper.isEnabled) {
-            this.renderer.render(this.handleHelper.scene, camera, 'color', false, transparentBackground, null);
+        if (helper.camera.isEnabled) {
+            helper.camera.update(camera);
+            renderer.render(helper.camera.scene, helper.camera.camera, 'color', false, transparentBackground, 1, null);
         }
-        if (this.cameraHelper.isEnabled) {
-            this.cameraHelper.update(camera);
-            this.renderer.render(this.cameraHelper.scene, this.cameraHelper.camera, 'color', false, transparentBackground, null);
+    }
+
+    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {
+        if (StereoCamera.is(camera)) {
+            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground);
+            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground);
+        } else {
+            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground);
         }
     }
 }
\ No newline at end of file
diff --git a/src/mol-canvas3d/passes/image.ts b/src/mol-canvas3d/passes/image.ts
index f3a833987ed15736c42d0bcd2759fd48d99eff6d..c07c821c311fa791a5ff36aed36290c141185ce0 100644
--- a/src/mol-canvas3d/passes/image.ts
+++ b/src/mol-canvas3d/passes/image.ts
@@ -8,21 +8,22 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
-import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { DrawPass, DrawPassParams } from './draw';
+import { DrawPass } from './draw';
 import { PostprocessingPass, PostprocessingParams } from './postprocessing';
 import { MultiSamplePass, MultiSampleParams } from './multi-sample';
 import { Camera } from '../camera';
 import { Viewport } from '../camera/util';
-import { HandleHelper } from '../helper/handle-helper';
 import { PixelData } from '../../mol-util/image';
+import { Helper } from '../helper/helper';
+import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
 
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
-    drawPass: PD.Group(DrawPassParams),
+
+    cameraHelper: PD.Group(CameraHelperParams),
 };
 export type ImageProps = PD.Values<typeof ImageParams>
 
@@ -30,26 +31,33 @@ export class ImagePass {
     private _width = 1024
     private _height = 768
     private _camera = new Camera()
-    private _transparentBackground = false
+
+    readonly props: ImageProps
 
     private _colorTarget: RenderTarget
     get colorTarget() { return this._colorTarget; }
 
     readonly drawPass: DrawPass
+
     private readonly postprocessing: PostprocessingPass
     private readonly multiSample: MultiSamplePass
+    private readonly helper: Helper
 
     get width() { return this._width; }
     get height() { return this._height; }
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, handleHelper: HandleHelper, props: Partial<ImageProps>) {
-        const p = { ...PD.getDefaultValues(ImageParams), ...props };
+    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, props: Partial<ImageProps>) {
+        this.props = { ...PD.getDefaultValues(ImageParams), ...props };
 
-        this._transparentBackground = p.transparentBackground;
+        this.drawPass = new DrawPass(webgl, 128, 128);
+        this.postprocessing = new PostprocessingPass(webgl, this.drawPass);
+        this.multiSample = new MultiSamplePass(webgl, this.drawPass, this.postprocessing);
 
-        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);
+        this.helper = {
+            camera: new CameraHelper(webgl, this.props.cameraHelper),
+            debug: helper.debug,
+            handle: helper.handle,
+        };
 
         this.setSize(this._width, this._height);
     }
@@ -61,24 +69,13 @@ export class ImagePass {
         this._height = height;
 
         this.drawPass.setSize(width, height);
-        this.postprocessing.setSize(width, height);
-        this.multiSample.setSize(width, height);
+        this.postprocessing.syncSize();
+        this.multiSample.syncSize();
     }
 
     setProps(props: Partial<ImageProps> = {}) {
-        if (props.transparentBackground !== undefined) this._transparentBackground = props.transparentBackground;
-        if (props.postprocessing) this.postprocessing.setProps(props.postprocessing);
-        if (props.multiSample) this.multiSample.setProps(props.multiSample);
-        if (props.drawPass) this.drawPass.setProps(props.drawPass);
-    }
-
-    get props(): ImageProps {
-        return {
-            transparentBackground: this._transparentBackground,
-            postprocessing: this.postprocessing.props,
-            multiSample: this.multiSample.props,
-            drawPass: this.drawPass.props
-        };
+        Object.assign(this.props, props);
+        if (props.cameraHelper) this.helper.camera.setProps(props.cameraHelper);
     }
 
     render() {
@@ -88,13 +85,13 @@ export class ImagePass {
 
         this.renderer.setViewport(0, 0, this._width, this._height);
 
-        if (this.multiSample.enabled) {
-            this.multiSample.render(false, this._transparentBackground);
+        if (MultiSamplePass.isEnabled(this.props.multiSample)) {
+            this.multiSample.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props);
             this._colorTarget = this.multiSample.colorTarget;
         } else {
-            this.drawPass.render(false, this._transparentBackground);
-            if (this.postprocessing.enabled) {
-                this.postprocessing.render(false);
+            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground);
+            if (PostprocessingPass.isEnabled(this.props.postprocessing)) {
+                this.postprocessing.render(this._camera, false, this.props.postprocessing);
                 this._colorTarget = this.postprocessing.target;
             } else {
                 this._colorTarget = this.drawPass.colorTarget;
diff --git a/src/mol-canvas3d/passes/multi-sample.ts b/src/mol-canvas3d/passes/multi-sample.ts
index d75964b4174112145211a8d011bb91cf6f4cf0aa..5f7c8e03e432fccd9c43e0c46213ce6db84e1e6a 100644
--- a/src/mol-canvas3d/passes/multi-sample.ts
+++ b/src/mol-canvas3d/passes/multi-sample.ts
@@ -16,8 +16,12 @@ import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/rendera
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { Camera } from '../../mol-canvas3d/camera';
-import { PostprocessingPass } from './postprocessing';
+import { PostprocessingPass, PostprocessingProps } from './postprocessing';
 import { DrawPass } from './draw';
+import Renderer from '../../mol-gl/renderer';
+import Scene from '../../mol-gl/scene';
+import { Helper } from '../helper/helper';
+import { StereoCamera } from '../camera/stereo';
 
 import quad_vert from '../../mol-gl/shader/quad.vert';
 import compose_frag from '../../mol-gl/shader/compose.frag';
@@ -51,8 +55,13 @@ export const MultiSampleParams = {
 };
 export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
 
+type Props = { multiSample: MultiSampleProps, postprocessing: PostprocessingProps }
+
 export class MultiSamplePass {
-    props: MultiSampleProps
+    static isEnabled(props: MultiSampleProps) {
+        return props.mode !== 'off';
+    }
+
     colorTarget: RenderTarget
 
     private composeTarget: RenderTarget
@@ -61,25 +70,25 @@ export class MultiSamplePass {
 
     private sampleIndex = -2
 
-    constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) {
-        const { gl, extensions } = webgl;
-        this.colorTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false);
-        this.composeTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false, extensions.colorBufferFloat ? 'float32' : 'uint8');
-        this.holdTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false);
+    constructor(private webgl: WebGLContext, private drawPass: DrawPass, private postprocessing: PostprocessingPass) {
+        const { extensions } = webgl;
+        const width = drawPass.colorTarget.getWidth();
+        const height = drawPass.colorTarget.getHeight();
+        this.colorTarget = webgl.createRenderTarget(width, height, false);
+        this.composeTarget = webgl.createRenderTarget(width, height, false, extensions.colorBufferFloat ? 'float32' : 'uint8');
+        this.holdTarget = webgl.createRenderTarget(width, height, false);
         this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture);
-        this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props };
     }
 
-    get enabled() {
-        return this.props.mode !== 'off';
-    }
-
-    update(changed: boolean) {
+    update(changed: boolean, props: MultiSampleProps) {
         if (changed) this.sampleIndex = -1;
-        return this.props.mode === 'temporal' ? this.sampleIndex !== -2 : false;
+        return props.mode === 'temporal' ? this.sampleIndex !== -2 : false;
     }
 
-    setSize(width: number, height: number) {
+    syncSize() {
+        const width = this.drawPass.colorTarget.getWidth();
+        const height = this.drawPass.colorTarget.getHeight();
+
         const [w, h] = this.compose.values.uTexSize.ref.value;
         if (width !== w || height !== h) {
             this.colorTarget.setSize(width, height);
@@ -89,27 +98,16 @@ export class MultiSamplePass {
         }
     }
 
-    setProps(props: Partial<MultiSampleProps>) {
-        if (props.mode !== undefined) this.props.mode = props.mode;
-        if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel;
-    }
-
-    render(toDrawingBuffer: boolean, transparentBackground: boolean) {
-        if (this.props.mode === 'temporal') {
-            this.renderTemporalMultiSample(toDrawingBuffer, transparentBackground);
+    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
+        if (props.multiSample.mode === 'temporal') {
+            this.renderTemporalMultiSample(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
         } else {
-            this.renderMultiSample(toDrawingBuffer, transparentBackground);
+            this.renderMultiSample(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
         }
     }
 
-    private setQuadShift(x: number, y: number) {
-        ValueCell.update(this.compose.values.uQuadShift, Vec2.set(
-            this.compose.values.uQuadShift.ref.value, x, y)
-        );
-    }
-
-    private renderMultiSample(toDrawingBuffer: boolean, transparentBackground: boolean) {
-        const { camera, compose, composeTarget, drawPass, postprocessing, webgl } = this;
+    private renderMultiSample(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
+        const { compose, composeTarget, drawPass, postprocessing, webgl } = this;
         const { gl, state } = webgl;
 
         // based on the Multisample Anti-Aliasing Render Pass
@@ -117,24 +115,24 @@ export class MultiSamplePass {
         //
         // This manual approach to MSAA re-renders the scene once for
         // each sample with camera jitter and accumulates the results.
-        const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ];
+        const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ];
 
+        const { x, y, width, height } = camera.viewport;
         const baseSampleWeight = 1.0 / offsetList.length;
         const roundingRange = 1 / 32;
 
+        const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
+
         camera.viewOffset.enabled = true;
-        ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+        ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
         compose.update();
 
-        const { x, y, width, height } = camera.viewport;
-
         // render the scene multiple times, each slightly jitter offset
         // from the last and accumulate the results.
         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();
-            this.drawPass.cameraHelper.update(camera);
 
             // the theory is that equal weights for each sample lead to an accumulation of rounding
             // errors. The following equation varies the sampleWeight per sample so that it is uniformly
@@ -144,8 +142,8 @@ export class MultiSamplePass {
             ValueCell.update(compose.values.uWeight, sampleWeight);
 
             // render scene and optionally postprocess
-            drawPass.render(false, transparentBackground);
-            if (postprocessing.enabled) postprocessing.render(false);
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
+            if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
 
             // compose rendered scene with compose target
             composeTarget.bind();
@@ -153,15 +151,13 @@ export class MultiSamplePass {
             state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
             state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
             state.disable(gl.DEPTH_TEST);
-            state.disable(gl.SCISSOR_TEST);
             state.depthMask(false);
+            gl.viewport(x, y, width, height);
+            gl.scissor(x, y, width, height);
             if (i === 0) {
                 state.clearColor(0, 0, 0, 0);
                 gl.clear(gl.COLOR_BUFFER_BIT);
             }
-            this.setQuadShift(0, 0);
-            gl.viewport(0, 0, width, height);
-            gl.scissor(0, 0, width, height);
             compose.render();
         }
 
@@ -174,9 +170,10 @@ export class MultiSamplePass {
         } else {
             this.colorTarget.bind();
         }
-        this.setQuadShift(x / width, y / height);
+
         gl.viewport(x, y, width, height);
         gl.scissor(x, y, width, height);
+
         state.disable(gl.BLEND);
         compose.render();
 
@@ -184,8 +181,8 @@ export class MultiSamplePass {
         camera.update();
     }
 
-    private renderTemporalMultiSample(toDrawingBuffer: boolean, transparentBackground: boolean) {
-        const { camera, compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this;
+    private renderTemporalMultiSample(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
+        const { compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this;
         const { gl, state } = webgl;
 
         // based on the Multisample Anti-Aliasing Render Pass
@@ -193,7 +190,7 @@ export class MultiSamplePass {
         //
         // This manual approach to MSAA re-renders the scene once for
         // each sample with camera jitter and accumulates the results.
-        const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ];
+        const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ];
 
         if (this.sampleIndex === -2) return;
         if (this.sampleIndex >= offsetList.length) {
@@ -203,42 +200,40 @@ export class MultiSamplePass {
 
         const { x, y, width, height } = camera.viewport;
         const sampleWeight = 1.0 / offsetList.length;
+        const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
 
         if (this.sampleIndex === -1) {
-            drawPass.render(false, transparentBackground);
-            if (postprocessing.enabled) postprocessing.render(false);
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
+            if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
             ValueCell.update(compose.values.uWeight, 1.0);
-            ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+            ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
             compose.update();
 
             holdTarget.bind();
             state.disable(gl.BLEND);
             state.disable(gl.DEPTH_TEST);
-            state.disable(gl.SCISSOR_TEST);
             state.depthMask(false);
-            this.setQuadShift(0, 0);
-            gl.viewport(0, 0, width, height);
-            gl.scissor(0, 0, width, height);
+            gl.viewport(x, y, width, height);
+            gl.scissor(x, y, width, height);
             compose.render();
             this.sampleIndex += 1;
         } else {
             camera.viewOffset.enabled = true;
-            ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+            ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
             ValueCell.update(compose.values.uWeight, sampleWeight);
             compose.update();
 
             // render the scene multiple times, each slightly jitter offset
             // from the last and accumulate the results.
-            const numSamplesPerFrame = Math.pow(2, Math.max(0, this.props.sampleLevel - 2));
+            const numSamplesPerFrame = Math.pow(2, Math.max(0, props.multiSample.sampleLevel - 2));
             for (let i = 0; i < numSamplesPerFrame; ++i) {
                 const offset = offsetList[this.sampleIndex];
                 Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
                 camera.update();
-                this.drawPass.cameraHelper.update(camera);
 
                 // render scene and optionally postprocess
-                drawPass.render(false, transparentBackground);
-                if (postprocessing.enabled) postprocessing.render(false);
+                drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
+                if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
 
                 // compose rendered scene with compose target
                 composeTarget.bind();
@@ -246,15 +241,13 @@ export class MultiSamplePass {
                 state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
                 state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
                 state.disable(gl.DEPTH_TEST);
-                state.disable(gl.SCISSOR_TEST);
                 state.depthMask(false);
+                gl.viewport(x, y, width, height);
+                gl.scissor(x, y, width, height);
                 if (this.sampleIndex === 0) {
                     state.clearColor(0, 0, 0, 0);
                     gl.clear(gl.COLOR_BUFFER_BIT);
                 }
-                this.setQuadShift(0, 0);
-                gl.viewport(0, 0, width, height);
-                gl.scissor(0, 0, width, height);
                 compose.render();
 
                 this.sampleIndex += 1;
@@ -264,16 +257,13 @@ export class MultiSamplePass {
 
         if (toDrawingBuffer) {
             webgl.unbindFramebuffer();
-            this.setQuadShift(x / width, y / height);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
         } else {
             this.colorTarget.bind();
-            this.setQuadShift(0, 0);
-            gl.viewport(0, 0, width, height);
-            gl.scissor(0, 0, width, height);
         }
 
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+
         const accumulationWeight = this.sampleIndex * sampleWeight;
         if (accumulationWeight > 0) {
             ValueCell.update(compose.values.uWeight, 1.0);
diff --git a/src/mol-canvas3d/passes/passes.ts b/src/mol-canvas3d/passes/passes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..24660ab3f49d6f7af26f914a040ac30321b857dd
--- /dev/null
+++ b/src/mol-canvas3d/passes/passes.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { DrawPass } from './draw';
+import { PickPass } from './pick';
+import { PostprocessingPass } from './postprocessing';
+import { MultiSamplePass } from './multi-sample';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+
+export class Passes {
+    readonly draw: DrawPass
+    readonly pick: PickPass
+    readonly postprocessing: PostprocessingPass
+    readonly multiSample: MultiSamplePass
+
+    constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number }> = {}) {
+        const { gl } = webgl;
+        this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight);
+        this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
+        this.postprocessing = new PostprocessingPass(webgl, this.draw);
+        this.multiSample = new MultiSamplePass(webgl, this.draw, this.postprocessing);
+    }
+
+    updateSize() {
+        const { gl } = this.webgl;
+        this.draw.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight);
+        this.pick.syncSize();
+        this.postprocessing.syncSize();
+        this.multiSample.syncSize();
+    }
+}
\ No newline at end of file
diff --git a/src/mol-canvas3d/passes/pick.ts b/src/mol-canvas3d/passes/pick.ts
index 7de7688e0ab906dd9de21d2a63fa9efa2192d698..348180eae64322f32fd96755a4e9bee70fa347d4 100644
--- a/src/mol-canvas3d/passes/pick.ts
+++ b/src/mol-canvas3d/passes/pick.ts
@@ -15,7 +15,8 @@ import { decodeFloatRGB, unpackRGBAToDepth } from '../../mol-util/float-packing'
 import { Camera, ICamera } from '../camera';
 import { StereoCamera } from '../camera/stereo';
 import { cameraUnproject } from '../camera/util';
-import { HandleHelper } from '../helper/handle-helper';
+import { Viewport } from '../camera/util';
+import { Helper } from '../helper/helper';
 import { DrawPass } from './draw';
 
 const NullId = Math.pow(2, 24) - 2;
@@ -23,51 +24,33 @@ const NullId = Math.pow(2, 24) - 2;
 export type PickData = { id: PickingId, position: Vec3 }
 
 export class PickPass {
-    pickDirty = true
+    readonly objectPickTarget: RenderTarget
+    readonly instancePickTarget: RenderTarget
+    readonly groupPickTarget: RenderTarget
+    readonly depthPickTarget: RenderTarget
 
-    objectPickTarget: RenderTarget
-    instancePickTarget: RenderTarget
-    groupPickTarget: RenderTarget
-    depthPickTarget: RenderTarget
-
-    isStereo = false
-
-    private objectBuffer: Uint8Array
-    private instanceBuffer: Uint8Array
-    private groupBuffer: Uint8Array
-    private depthBuffer: Uint8Array
-
-    private pickScale: number
     private pickWidth: number
     private pickHeight: number
 
-    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);
+    constructor(private webgl: WebGLContext, private drawPass: DrawPass, readonly pickBaseScale: number) {
+        const pickScale = pickBaseScale / webgl.pixelRatio;
+        this.pickWidth = Math.ceil(drawPass.colorTarget.getWidth() * pickScale);
+        this.pickHeight = Math.ceil(drawPass.colorTarget.getHeight() * pickScale);
 
         this.objectPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
         this.instancePickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
         this.groupPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
         this.depthPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
-
-        this.setupBuffers();
     }
 
-    private setupBuffers() {
-        const bufferSize = this.pickWidth * this.pickHeight * 4;
-        if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
-            this.objectBuffer = new Uint8Array(bufferSize);
-            this.instanceBuffer = new Uint8Array(bufferSize);
-            this.groupBuffer = new Uint8Array(bufferSize);
-            this.depthBuffer = new Uint8Array(bufferSize);
-        }
+    get drawingBufferHeight() {
+        return this.drawPass.colorTarget.getHeight();
     }
 
-    setSize(width: number, height: number) {
-        this.pickScale = this.pickBaseScale / this.webgl.pixelRatio;
-        const pickWidth = Math.ceil(width * this.pickScale);
-        const pickHeight = Math.ceil(height * this.pickScale);
+    syncSize() {
+        const pickScale = this.pickBaseScale / this.webgl.pixelRatio;
+        const pickWidth = Math.ceil(this.drawPass.colorTarget.getWidth() * pickScale);
+        const pickHeight = Math.ceil(this.drawPass.colorTarget.getHeight() * pickScale);
 
         if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
             this.pickWidth = pickWidth;
@@ -77,65 +60,92 @@ export class PickPass {
             this.instancePickTarget.setSize(this.pickWidth, this.pickHeight);
             this.groupPickTarget.setSize(this.pickWidth, this.pickHeight);
             this.depthPickTarget.setSize(this.pickWidth, this.pickHeight);
-
-            this.setupBuffers();
-        }
-    }
-
-    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;
+    private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: GraphicsRenderVariant) {
+        const pickScale = this.pickBaseScale / this.webgl.pixelRatio;
         const depth = this.drawPass.depthTexturePrimitives;
-
-        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);
+        renderer.render(scene.primitives, camera, variant, true, false, pickScale, null);
+        renderer.render(scene.volumes, camera, variant, false, false, pickScale, depth);
+        renderer.render(helper.handle.scene, camera, variant, false, false, pickScale, null);
     }
 
-    render() {
+    render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper) {
         this.objectPickTarget.bind();
-        this.renderVariant('pickObject');
+        this.renderVariant(renderer, camera, scene, helper, 'pickObject');
 
         this.instancePickTarget.bind();
-        this.renderVariant('pickInstance');
+        this.renderVariant(renderer, camera, scene, helper, 'pickInstance');
 
         this.groupPickTarget.bind();
-        this.renderVariant('pickGroup');
+        this.renderVariant(renderer, camera, scene, helper, 'pickGroup');
 
         this.depthPickTarget.bind();
-        this.renderVariant('depth');
+        this.renderVariant(renderer, camera, scene, helper, 'depth');
+    }
+}
+
+export class PickHelper {
+    dirty = true
+
+    private objectBuffer: Uint8Array
+    private instanceBuffer: Uint8Array
+    private groupBuffer: Uint8Array
+    private depthBuffer: Uint8Array
+
+    private viewport = Viewport()
+
+    private pickScale: number
+    private pickX: number
+    private pickY: number
+    private pickWidth: number
+    private pickHeight: number
+    private halfPickWidth: number
 
-        this.pickDirty = false;
+    private setupBuffers() {
+        const bufferSize = this.pickWidth * this.pickHeight * 4;
+        if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
+            this.objectBuffer = new Uint8Array(bufferSize);
+            this.instanceBuffer = new Uint8Array(bufferSize);
+            this.groupBuffer = new Uint8Array(bufferSize);
+            this.depthBuffer = new Uint8Array(bufferSize);
+        }
+    }
+
+    setViewport(x: number, y: number, width: number, height: number) {
+        Viewport.set(this.viewport, x, y, width, height);
+
+        this.pickScale = this.pickPass.pickBaseScale / this.webgl.pixelRatio;
+        this.pickX = Math.ceil(x * this.pickScale);
+        this.pickY = Math.ceil(y * this.pickScale);
+
+        const pickWidth = Math.ceil(width * this.pickScale);
+        const pickHeight = Math.ceil(height * this.pickScale);
+
+        if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
+            this.pickWidth = pickWidth;
+            this.pickHeight = pickHeight;
+            this.halfPickWidth = Math.floor(this.pickWidth / 2);
+
+            this.setupBuffers();
+        }
     }
 
     private syncBuffers() {
-        const { webgl } = this;
+        const { pickX, pickY, pickWidth, pickHeight } = this;
 
-        this.objectPickTarget.bind();
-        webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.objectBuffer);
+        this.pickPass.objectPickTarget.bind();
+        this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.objectBuffer);
 
-        this.instancePickTarget.bind();
-        webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.instanceBuffer);
+        this.pickPass.instancePickTarget.bind();
+        this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.instanceBuffer);
 
-        this.groupPickTarget.bind();
-        webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.groupBuffer);
+        this.pickPass.groupPickTarget.bind();
+        this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.groupBuffer);
 
-        this.depthPickTarget.bind();
-        webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.depthBuffer);
+        this.pickPass.depthPickTarget.bind();
+        this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.depthBuffer);
     }
 
     private getBufferIdx(x: number, y: number): number {
@@ -153,34 +163,52 @@ export class PickPass {
         return decodeFloatRGB(buffer[idx], buffer[idx + 1], buffer[idx + 2]);
     }
 
-    identify(x: number, y: number): PickData | undefined {
-        const { webgl, pickScale, camera: { viewport } } = this;
+    private render(camera: Camera | StereoCamera) {
+        const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
+
+        const { renderer, scene, helper } = this;
+
+        if (StereoCamera.is(camera)) {
+            this.renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
+            this.pickPass.render(renderer, camera.left, scene, helper);
+
+            this.renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
+            this.pickPass.render(renderer, camera.right, scene, helper);
+        } else {
+            this.renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
+            this.pickPass.render(renderer, camera, scene, helper);
+        }
+
+        this.dirty = false;
+    }
+
+    identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
+        const { webgl, pickScale } = this;
         if (webgl.isContextLost) return;
 
-        const { gl, pixelRatio } = webgl;
-        x *= pixelRatio;
-        y *= pixelRatio;
+        x *= webgl.pixelRatio;
+        y *= webgl.pixelRatio;
+        y = this.pickPass.drawingBufferHeight - y; // flip y
+
+        const { viewport } = this;
 
         // check if within viewport
         if (x < viewport.x ||
-            gl.drawingBufferHeight - y < viewport.y ||
+            y < viewport.y ||
             x > viewport.x + viewport.width ||
-            gl.drawingBufferHeight - y > viewport.y + viewport.height
-        ) {
-            return;
-        }
+            y > viewport.y + viewport.height
+        ) return;
 
-        if (this.pickDirty) {
-            this.render();
+        if (this.dirty) {
+            this.render(camera);
             this.syncBuffers();
         }
 
-        x -= viewport.x;
-        y += viewport.y; // plus because of flipped y
-        y = gl.drawingBufferHeight - y; // flip y
+        const xv = x - viewport.x;
+        const yv = y - viewport.y;
 
-        const xp = Math.floor(x * pickScale);
-        const yp = Math.floor(y * pickScale);
+        const xp = Math.floor(xv * pickScale);
+        const yp = Math.floor(yv * pickScale);
 
         const objectId = this.getId(xp, yp, this.objectBuffer);
         // console.log('objectId', objectId);
@@ -195,10 +223,25 @@ export class PickPass {
         if (groupId === -1 || groupId === NullId) return;
 
         const z = this.getDepth(xp, yp);
-        const position = Vec3.create(x, gl.drawingBufferHeight - y, z);
-        cameraUnproject(position, position, viewport, this.camera.inverseProjectionView);
+        const position = Vec3.create(x, viewport.height - y, z);
+        if (StereoCamera.is(camera)) {
+            const halfWidth = Math.floor(viewport.width / 2);
+            if (x > viewport.x + halfWidth) {
+                position[0] = viewport.x + (xv - halfWidth) * 2;
+                cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
+            } else {
+                position[0] = viewport.x + xv * 2;
+                cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
+            }
+        } else {
+            cameraUnproject(position, position, viewport, camera.inverseProjectionView);
+        }
 
         // console.log({ { objectId, instanceId, groupId }, position} );
         return { id: { objectId, instanceId, groupId }, position };
     }
+
+    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport) {
+        this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
+    }
 }
\ No newline at end of file
diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts
index 1825cb24dcb842da70fc4b19b85ca3fc58c741d6..49aa45fb12c9b9b84fff558a65aec9839d18b53c 100644
--- a/src/mol-canvas3d/passes/postprocessing.ts
+++ b/src/mol-canvas3d/passes/postprocessing.ts
@@ -16,11 +16,11 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { DrawPass } from './draw';
-import { Camera } from '../../mol-canvas3d/camera';
-import { produce } from 'immer';
+import { Camera, ICamera } from '../../mol-canvas3d/camera';
 
 import quad_vert from '../../mol-gl/shader/quad.vert';
 import postprocessing_frag from '../../mol-gl/shader/postprocessing.frag';
+import { StereoCamera } from '../camera/stereo';
 
 const PostprocessingSchema = {
     ...QuadSchema,
@@ -47,8 +47,7 @@ const PostprocessingSchema = {
 const PostprocessingShaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag);
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
 
-function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, props: Partial<PostprocessingProps>): PostprocessingRenderable {
-    const p = { ...PD.getDefaultValues(PostprocessingParams), ...props };
+function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture): PostprocessingRenderable {
     const values: Values<typeof PostprocessingSchema> = {
         ...QuadValues,
         tColor: ValueCell.create(colorTexture),
@@ -62,14 +61,14 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         uFogFar: ValueCell.create(10000),
         uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
 
-        dOcclusionEnable: ValueCell.create(p.occlusion.name === 'on'),
-        dOcclusionKernelSize: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.kernelSize : 4),
-        uOcclusionBias: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.bias : 0.5),
-        uOcclusionRadius: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.radius : 64),
+        dOcclusionEnable: ValueCell.create(false),
+        dOcclusionKernelSize: ValueCell.create(4),
+        uOcclusionBias: ValueCell.create(0.5),
+        uOcclusionRadius: ValueCell.create(64),
 
-        dOutlineEnable: ValueCell.create(p.outline.name === 'on'),
-        uOutlineScale: ValueCell.create((p.outline.name === 'on' ? p.outline.params.scale : 1) * ctx.pixelRatio),
-        uOutlineThreshold: ValueCell.create(p.outline.name === 'on' ? p.outline.params.threshold : 0.8),
+        dOutlineEnable: ValueCell.create(false),
+        uOutlineScale: ValueCell.create(1 * ctx.pixelRatio),
+        uOutlineThreshold: ValueCell.create(0.8),
     };
 
     const schema = { ...PostprocessingSchema };
@@ -98,22 +97,23 @@ export const PostprocessingParams = {
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
 export class PostprocessingPass {
+    static isEnabled(props: PostprocessingProps) {
+        return props.occlusion.name === 'on' || props.outline.name === 'on';
+    }
+
     target: RenderTarget
-    props: PostprocessingProps
     renderable: PostprocessingRenderable
 
-    constructor(private webgl: WebGLContext, private camera: Camera, drawPass: DrawPass, props: Partial<PostprocessingProps>) {
-        this.target = webgl.createRenderTarget(camera.viewport.width, camera.viewport.height, false);
-        this.props = { ...PD.getDefaultValues(PostprocessingParams), ...props };
-        const { colorTarget, depthTexture, packedDepth } = drawPass;
-        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture, packedDepth, this.props);
+    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
+        this.target = webgl.createRenderTarget(drawPass.colorTarget.getWidth(), drawPass.colorTarget.getHeight(), false);
+        const { colorTarget, depthTexture } = drawPass;
+        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture);
     }
 
-    get enabled() {
-        return this.props.occlusion.name === 'on' || this.props.outline.name === 'on';
-    }
+    syncSize() {
+        const width = this.drawPass.colorTarget.getWidth();
+        const height = this.drawPass.colorTarget.getHeight();
 
-    setSize(width: number, height: number) {
         const [w, h] = this.renderable.values.uTexSize.ref.value;
         if (width !== w || height !== h) {
             this.target.setSize(width, height);
@@ -121,69 +121,67 @@ export class PostprocessingPass {
         }
     }
 
-    setProps(props: Partial<PostprocessingProps>) {
-        this.props = produce(this.props, p => {
-            if (props.occlusion !== undefined) {
-                p.occlusion.name = props.occlusion.name;
-                ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, props.occlusion.name === 'on');
-                if (props.occlusion.name === 'on') {
-                    p.occlusion.params = { ...props.occlusion.params };
-                    ValueCell.updateIfChanged(this.renderable.values.dOcclusionKernelSize, props.occlusion.params.kernelSize);
-                    ValueCell.updateIfChanged(this.renderable.values.uOcclusionBias, props.occlusion.params.bias);
-                    ValueCell.updateIfChanged(this.renderable.values.uOcclusionRadius, props.occlusion.params.radius);
-                }
-            }
-
-            if (props.outline !== undefined) {
-                p.outline.name = props.outline.name;
-                ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, props.outline.name === 'on');
-                if (props.outline.name === 'on') {
-                    p.outline.params = { ...props.outline.params };
-                    ValueCell.updateIfChanged(this.renderable.values.uOutlineScale, props.outline.params.scale);
-                    ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, props.outline.params.threshold);
-                }
-            }
-        });
-
-        this.renderable.update();
-    }
-
-    private setQuadShift(x: number, y: number) {
-        ValueCell.update(this.renderable.values.uQuadShift, Vec2.set(
-            this.renderable.values.uQuadShift.ref.value, x, y)
-        );
-    }
+    _render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        const { values } = this.renderable;
+
+        ValueCell.updateIfChanged(values.uFar, camera.far);
+        ValueCell.updateIfChanged(values.uNear, camera.near);
+        ValueCell.updateIfChanged(values.uFogFar, camera.fogFar);
+        ValueCell.updateIfChanged(values.uFogNear, camera.fogNear);
+
+        let needsUpdate = false;
+
+        const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
+        if (values.dOrthographic.ref.value !== orthographic) needsUpdate = true;
+        ValueCell.updateIfChanged(values.dOrthographic, orthographic);
+
+        const occlusion = props.occlusion.name === 'on';
+        if (values.dOcclusionEnable.ref.value !== occlusion) needsUpdate = true;
+        ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusion);
+        if (props.occlusion.name === 'on') {
+            const { kernelSize } = props.occlusion.params;
+            if (values.dOcclusionKernelSize.ref.value !== kernelSize) needsUpdate = true;
+            ValueCell.updateIfChanged(values.dOcclusionKernelSize, kernelSize);
+            ValueCell.updateIfChanged(values.uOcclusionBias, props.occlusion.params.bias);
+            ValueCell.updateIfChanged(values.uOcclusionRadius, props.occlusion.params.radius);
+        }
 
-    render(toDrawingBuffer: boolean) {
-        ValueCell.updateIfChanged(this.renderable.values.uFar, this.camera.far);
-        ValueCell.updateIfChanged(this.renderable.values.uNear, this.camera.near);
-        ValueCell.updateIfChanged(this.renderable.values.uFogFar, this.camera.fogFar);
-        ValueCell.updateIfChanged(this.renderable.values.uFogNear, this.camera.fogNear);
+        const outline = props.outline.name === 'on';
+        if (values.dOutlineEnable.ref.value !== outline) needsUpdate = true;
+        ValueCell.updateIfChanged(values.dOutlineEnable, outline);
+        if (props.outline.name === 'on') {
+            ValueCell.updateIfChanged(values.uOutlineScale, props.outline.params.scale * this.webgl.pixelRatio);
+            ValueCell.updateIfChanged(values.uOutlineThreshold, props.outline.params.threshold);
+        }
 
-        const orthographic = this.camera.state.mode === 'orthographic' ? 1 : 0;
-        if (this.renderable.values.dOrthographic.ref.value !==  orthographic) {
-            ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
+        if (needsUpdate) {
             this.renderable.update();
         }
 
-        const { x, y, width, height } = this.camera.viewport;
-
         const { gl, state } = this.webgl;
         if (toDrawingBuffer) {
             this.webgl.unbindFramebuffer();
-            this.setQuadShift(x / width, y / height);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
         } else {
             this.target.bind();
-            this.setQuadShift(0, 0);
-            gl.viewport(0, 0, width, height);
-            gl.scissor(0, 0, width, height);
         }
         state.disable(gl.SCISSOR_TEST);
         state.disable(gl.BLEND);
         state.disable(gl.DEPTH_TEST);
         state.depthMask(false);
+
+        const { x, y, width, height } = camera.viewport;
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+
         this.renderable.render();
     }
+
+    render(camera: Camera | StereoCamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        if (StereoCamera.is(camera)) {
+            this._render(camera.left, toDrawingBuffer, props);
+            this._render(camera.right, toDrawingBuffer, props);
+        } else {
+            this._render(camera, toDrawingBuffer, props);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/mol-gl/compute/util.ts b/src/mol-gl/compute/util.ts
index 4d2cfee814dacd1f5859d6d02116914579fa4a6a..645d4b90146570e047a2a524ef4ea6039ee85132 100644
--- a/src/mol-gl/compute/util.ts
+++ b/src/mol-gl/compute/util.ts
@@ -22,7 +22,6 @@ export const QuadSchema = {
     instanceCount: ValueSpec('number'),
     aPosition: AttributeSpec('float32', 2, 0),
     uQuadScale: UniformSpec('v2'),
-    uQuadShift: UniformSpec('v2'),
 };
 
 export const QuadValues: Values<typeof QuadSchema> = {
@@ -30,7 +29,6 @@ export const QuadValues: Values<typeof QuadSchema> = {
     instanceCount: ValueCell.create(1),
     aPosition: ValueCell.create(QuadPositions),
     uQuadScale: ValueCell.create(Vec2.create(1, 1)),
-    uQuadShift: ValueCell.create(Vec2.create(0, 0)),
 };
 
 //
diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts
index b21606fb4f5d42b91c563736dbb682b14de3936f..06f65477c0393f337969b1c1cc124fe58ccc1554 100644
--- a/src/mol-gl/renderable/schema.ts
+++ b/src/mol-gl/renderable/schema.ts
@@ -120,6 +120,7 @@ export const GlobalUniformSchema = {
     uViewportHeight: UniformSpec('f'),
     uViewport: UniformSpec('v4'),
     uViewOffset: UniformSpec('v2'),
+    uDrawingBufferSize: UniformSpec('v2'),
 
     uCameraPosition: UniformSpec('v3'),
     uCameraDir: UniformSpec('v3'),
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index 32fd6aa12d11d0b14e1a38f20fd569b7364e7882..357b59fdf98c6384abc9234550daa1fcff8089c0 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -6,7 +6,6 @@
 
 import { Viewport } from '../mol-canvas3d/camera/util';
 import { ICamera } from '../mol-canvas3d/camera';
-
 import Scene from './scene';
 import { WebGLContext } from './webgl/context';
 import { Mat4, Vec3, Vec4, Vec2, Quat } from '../mol-math/linear-algebra';
@@ -43,7 +42,7 @@ interface Renderer {
     readonly props: Readonly<RendererProps>
 
     clear: (transparentBackground: boolean) => void
-    render: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => void
+    render: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, drawingBufferScale: number, depthTexture: Texture | null) => void
     setProps: (props: Partial<RendererProps>) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
     dispose: () => void
@@ -165,6 +164,7 @@ namespace Renderer {
         const clip = getClip(p.clip);
 
         const viewport = Viewport();
+        const drawingBufferSize = Vec2.create(gl.drawingBufferWidth, gl.drawingBufferHeight);
         const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor);
 
         const view = Mat4();
@@ -195,6 +195,7 @@ namespace Renderer {
             uPixelRatio: ValueCell.create(ctx.pixelRatio),
             uViewportHeight: ValueCell.create(viewport.height),
             uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
+            uDrawingBufferSize: ValueCell.create(drawingBufferSize),
 
             uCameraPosition: ValueCell.create(Vec3()),
             uCameraDir: ValueCell.create(cameraDir),
@@ -300,7 +301,7 @@ namespace Renderer {
             r.render(variant);
         };
 
-        const render = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => {
+        const render = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, drawingBufferScale: number, depthTexture: Texture | null) => {
             ValueCell.update(globalUniforms.uModel, group.view);
             ValueCell.update(globalUniforms.uView, camera.view);
             ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view));
@@ -311,18 +312,26 @@ namespace Renderer {
             ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection));
             ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection));
 
-            ValueCell.update(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0);
+            ValueCell.updateIfChanged(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0);
             ValueCell.update(globalUniforms.uViewOffset, camera.viewOffset.enabled ? Vec2.set(viewOffset, camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2.set(viewOffset, 0, 0));
 
             ValueCell.update(globalUniforms.uCameraPosition, camera.state.position);
             ValueCell.update(globalUniforms.uCameraDir, Vec3.normalize(cameraDir, Vec3.sub(cameraDir, camera.state.target, camera.state.position)));
 
-            ValueCell.update(globalUniforms.uFar, camera.far);
-            ValueCell.update(globalUniforms.uNear, camera.near);
-            ValueCell.update(globalUniforms.uFogFar, camera.fogFar);
-            ValueCell.update(globalUniforms.uFogNear, camera.fogNear);
-
-            ValueCell.update(globalUniforms.uTransparentBackground, transparentBackground);
+            ValueCell.updateIfChanged(globalUniforms.uFar, camera.far);
+            ValueCell.updateIfChanged(globalUniforms.uNear, camera.near);
+            ValueCell.updateIfChanged(globalUniforms.uFogFar, camera.fogFar);
+            ValueCell.updateIfChanged(globalUniforms.uFogNear, camera.fogNear);
+            ValueCell.updateIfChanged(globalUniforms.uTransparentBackground, transparentBackground);
+
+            if (gl.drawingBufferWidth * drawingBufferScale !== drawingBufferSize[0] ||
+                gl.drawingBufferHeight * drawingBufferScale !== drawingBufferSize[1]
+            ) {
+                ValueCell.update(globalUniforms.uDrawingBufferSize, Vec2.set(drawingBufferSize,
+                    gl.drawingBufferWidth * drawingBufferScale,
+                    gl.drawingBufferHeight * drawingBufferScale
+                ));
+            }
 
             globalUniformsNeedUpdate = true;
             state.currentRenderItemId = -1;
@@ -334,6 +343,10 @@ namespace Renderer {
             state.colorMask(true, true, true, true);
             state.enable(gl.DEPTH_TEST);
 
+            const { x, y, width, height } = viewport;
+            gl.viewport(x, y, width, height);
+            gl.scissor(x, y, width, height);
+
             if (clear) {
                 state.depthMask(true);
                 if (variant === 'color') {
diff --git a/src/mol-gl/shader/compose.frag.ts b/src/mol-gl/shader/compose.frag.ts
index 9c2abd85da9a607c4bebbaa85323c0aab9f19744..172330fded2b7030fa5bd855b6e15a36c4aeeb5e 100644
--- a/src/mol-gl/shader/compose.frag.ts
+++ b/src/mol-gl/shader/compose.frag.ts
@@ -2,14 +2,12 @@ export default `
 precision highp float;
 precision highp sampler2D;
 
-uniform vec2 uQuadShift;
-
 uniform sampler2D tColor;
 uniform vec2 uTexSize;
 uniform float uWeight;
 
 void main() {
-    vec2 coords = gl_FragCoord.xy / uTexSize - uQuadShift;
+    vec2 coords = gl_FragCoord.xy / uTexSize;
     gl_FragColor = texture2D(tColor, coords) * uWeight;
 }
 `;
\ No newline at end of file
diff --git a/src/mol-gl/shader/direct-volume.frag.ts b/src/mol-gl/shader/direct-volume.frag.ts
index a3182c6c35912e22f1ca0808276eb706b4b57b7d..4da7349b10045413091b3e79a4eebf140820041b 100644
--- a/src/mol-gl/shader/direct-volume.frag.ts
+++ b/src/mol-gl/shader/direct-volume.frag.ts
@@ -21,7 +21,7 @@ uniform mat4 uProjection, uTransform, uModelView, uView;
 uniform vec3 uCameraDir;
 
 uniform sampler2D tDepth;
-uniform vec4 uViewport;
+uniform vec2 uDrawingBufferSize;
 uniform float uNear;
 uniform float uFar;
 
@@ -179,7 +179,7 @@ vec4 raymarch(vec3 startLoc, vec3 step) {
 
                 vec4 mvPosition = uModelView * uTransform * vec4(isoPos * uGridDim, 1.0);
                 float depth = calcDepth(mvPosition.xyz);
-                if (depth > getDepth(gl_FragCoord.xy / uViewport.zw))
+                if (depth > getDepth(gl_FragCoord.xy / uDrawingBufferSize))
                     break;
 
                 #ifdef enabledFragDepth
@@ -287,7 +287,7 @@ vec4 raymarch(vec3 startLoc, vec3 step) {
         #elif defined(dRenderMode_volume)
             isoPos = toUnit(pos);
             vec4 mvPosition = uModelView * uTransform * vec4(isoPos * uGridDim, 1.0);
-            if (calcDepth(mvPosition.xyz) > getDepth(gl_FragCoord.xy / uViewport.zw))
+            if (calcDepth(mvPosition.xyz) > getDepth(gl_FragCoord.xy / uDrawingBufferSize))
                 break;
 
             vec3 vViewPosition = mvPosition.xyz;
diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts
index d1558bc0969c7ff2f188e9c15d9c9a1741f3d0f5..04d2864cfa23fb87280c970c2ca764a13db04064 100644
--- a/src/mol-gl/shader/postprocessing.frag.ts
+++ b/src/mol-gl/shader/postprocessing.frag.ts
@@ -3,8 +3,6 @@ precision highp float;
 precision highp int;
 precision highp sampler2D;
 
-uniform vec2 uQuadShift;
-
 uniform sampler2D tColor;
 uniform sampler2D tPackedDepth;
 uniform vec2 uTexSize;
@@ -96,7 +94,7 @@ vec2 calcEdgeDepth(const in vec2 coords) {
 }
 
 void main(void) {
-    vec2 coords = gl_FragCoord.xy / uTexSize - uQuadShift;
+    vec2 coords = gl_FragCoord.xy / uTexSize;
     vec4 color = texture2D(tColor, coords);
 
     #ifdef dOutlineEnable
diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts
index bc9b6e61783e59f7a5cea4a4d96e41f7e6ec9454..b2250f3a61cff551dccfba8db2bc90decadc90df 100644
--- a/src/mol-gl/webgl/context.ts
+++ b/src/mol-gl/webgl/context.ts
@@ -331,9 +331,9 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
             state.enable(gl.SCISSOR_TEST);
             state.depthMask(true);
             state.colorMask(true, true, true, true);
+            state.clearColor(red, green, blue, alpha);
             gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
             gl.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
-            state.clearColor(red, green, blue, alpha);
             gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
         },
 
diff --git a/src/mol-gl/webgl/render-target.ts b/src/mol-gl/webgl/render-target.ts
index 3209a20ff4cc4d36d27250ce997923f45ed34244..232c0d9d5388da135c61b063ad3bc0c1b396e40c 100644
--- a/src/mol-gl/webgl/render-target.ts
+++ b/src/mol-gl/webgl/render-target.ts
@@ -55,7 +55,6 @@ export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResou
         getHeight: () => _height,
         bind: () => {
             framebuffer.bind();
-            gl.viewport(0, 0, _width, _height);
         },
         setSize: (width: number, height: number) => {
             if (_width === width && _height === height) {
diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx
index fe30c31eb221a8eb4eaac82595f5789bc5c15bf5..cf72e5002ca9ab640d781007e1340666d2718a7c 100644
--- a/src/mol-plugin-ui/viewport/simple-settings.tsx
+++ b/src/mol-plugin-ui/viewport/simple-settings.tsx
@@ -62,7 +62,6 @@ 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
@@ -112,7 +111,6 @@ const SimpleSettingsMapping = ParamMapping({
                 outline: canvas.postprocessing.outline,
                 fog: canvas.cameraFog
             },
-            stereo: canvas.stereo,
             clipping: {
                 ...canvas.cameraClipping,
                 ...canvas.renderer.clip
@@ -138,7 +136,6 @@ const SimpleSettingsMapping = ParamMapping({
             variant: s.clipping.variant,
             objects: s.clipping.objects,
         };
-        canvas.stereo = s.stereo;
 
         props.layout = s.layout;
     },
diff --git a/src/mol-plugin/util/viewport-screenshot.ts b/src/mol-plugin/util/viewport-screenshot.ts
index 677ed8b75d88912dc5fa85f60907cbb176eca7fd..bd7c43f7b94eb4cb3099ebf4fe7295feeedae830 100644
--- a/src/mol-plugin/util/viewport-screenshot.ts
+++ b/src/mol-plugin/util/viewport-screenshot.ts
@@ -92,7 +92,7 @@ class ViewportScreenshotHelper {
 
         this._imagePass = this.plugin.canvas3d!.getImagePass({
             transparentBackground: this.transparent,
-            drawPass: { cameraHelper: { axes: this.axes } },
+            cameraHelper: { axes: this.axes },
             multiSample: { mode: 'on', sampleLevel: 2 },
             postprocessing: this.plugin.canvas3d!.props.postprocessing
         });
@@ -140,7 +140,7 @@ class ViewportScreenshotHelper {
 
         await ctx.update('Rendering image...');
         this.imagePass.setProps({
-            drawPass: { cameraHelper: { axes: this.axes } },
+            cameraHelper: { axes: this.axes },
             transparentBackground: this.transparent,
             postprocessing: this.plugin.canvas3d!.props.postprocessing // TODO this line should not be required, updating should work by listening to this.plugin.events.canvas3d.settingsUpdated
         });