diff --git a/src/apps/image-generator/index.ts b/src/apps/image-generator/index.ts
index a4cba746ca064f6eb2fdf7beb79649554e90fcec..6224bc0317f07ccd82cbf1401bdce4b92d242c0c 100644
--- a/src/apps/image-generator/index.ts
+++ b/src/apps/image-generator/index.ts
@@ -30,8 +30,10 @@ const gl = createContext(width, height, {
 
 const input = InputObserver.create()
 const canvas3d = Canvas3D.create(gl, input, {
-    multiSample: 'on',
-    sampleLevel: 3,
+    multiSample: {
+        mode: 'on',
+        sampleLevel: 3
+    },
     renderer: {
         ...Canvas3DParams.renderer.defaultValue,
         lightIntensity: 0,
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index 83bd67d0a4813e6a73592e7100d1f9df02efb4a6..0d9d56c3220f7ce4292e4d6472aa7c16f3a571fb 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -6,12 +6,10 @@
 
 import { BehaviorSubject, Subscription } from 'rxjs';
 import { now } from 'mol-util/now';
-
-import { Vec3, Vec2 } from 'mol-math/linear-algebra'
+import { Vec3 } from 'mol-math/linear-algebra'
 import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer'
 import Renderer, { RendererStats, RendererParams } from 'mol-gl/renderer'
 import { GraphicsRenderObject } from 'mol-gl/render-object'
-
 import { TrackballControls, TrackballControlsParams } from './controls/trackball'
 import { Viewport } from './camera/util'
 import { createContext, WebGLContext, getGLContext } from 'mol-gl/webgl/context';
@@ -29,9 +27,8 @@ import { decodeFloatRGB } from 'mol-util/float-packing';
 import { SetUtils } from 'mol-util/set';
 import { Canvas3dInteractionHelper } from './helper/interaction-events';
 import { createTexture } from 'mol-gl/webgl/texture';
-import { ValueCell } from 'mol-util';
 import { PostprocessingParams, PostprocessingPass } from './helper/postprocessing';
-import { JitterVectors, getComposeRenderable } from './helper/multi-sample';
+import { MultiSampleParams, MultiSamplePass } from './helper/multi-sample';
 import { GLRenderingContext } from 'mol-gl/webgl/compat';
 import { PixelData } from 'mol-util/image';
 import { readTexture } from 'mol-gl/compute/util';
@@ -44,9 +41,7 @@ export const Canvas3DParams = {
     clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }),
     fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }),
 
-    multiSample: PD.Select('off', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
-    sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }),
-
+    multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
     renderer: PD.Group(RendererParams),
     trackball: PD.Group(TrackballControlsParams),
@@ -127,7 +122,6 @@ namespace Canvas3D {
         })
 
         const webgl = createContext(gl)
-        const { state } = webgl
 
         let width = gl.drawingBufferWidth
         let height = gl.drawingBufferHeight
@@ -145,10 +139,7 @@ namespace Canvas3D {
         }
 
         const postprocessing = new PostprocessingPass(webgl, drawTarget.texture, depthTexture, !!depthTarget, p.postprocessing)
-
-        const composeTarget = createRenderTarget(webgl, width, height)
-        const holdTarget = createRenderTarget(webgl, width, height)
-        const compose = getComposeRenderable(webgl, drawTarget.texture)
+        const multiSample = new MultiSamplePass(webgl, camera, drawTarget, postprocessing, renderDraw, p.multiSample)
 
         const pickBaseScale = 0.5
         let pickScale = pickBaseScale / webgl.pixelRatio
@@ -163,9 +154,6 @@ namespace Canvas3D {
         let isUpdating = false
         let drawPending = false
 
-        let multiSampleIndex = -1
-        let multiSample = false
-
         const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
         const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
 
@@ -249,166 +237,6 @@ namespace Canvas3D {
             }
         }
 
-        function renderTemporalMultiSample() {
-            // based on the Multisample Anti-Aliasing Render Pass
-            // contributed to three.js by bhouston / http://clara.io/
-            //
-            // 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(p.sampleLevel, 5)) ]
-
-            if (multiSampleIndex === -1) return
-            if (multiSampleIndex >= offsetList.length) {
-                multiSampleIndex = -1
-                return
-            }
-
-            const i = multiSampleIndex
-
-            if (i === 0) {
-                drawTarget.bind()
-                renderDraw()
-                if (postprocessing.enabled) postprocessing.render(false)
-                ValueCell.update(compose.values.uWeight, 1.0)
-                ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture)
-                compose.update()
-
-                holdTarget.bind()
-                state.disable(gl.BLEND)
-                compose.render()
-            }
-
-            const sampleWeight = 1.0 / offsetList.length
-
-            camera.viewOffset.enabled = true
-            ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture)
-            ValueCell.update(compose.values.uWeight, sampleWeight)
-            compose.update()
-
-            const { width, height } = drawTarget
-
-            // render the scene multiple times, each slightly jitter offset
-            // from the last and accumulate the results.
-            const numSamplesPerFrame = Math.pow(2, p.sampleLevel)
-            for (let i = 0; i < numSamplesPerFrame; ++i) {
-                const offset = offsetList[multiSampleIndex]
-                Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height)
-                camera.updateMatrices()
-
-                // render scene and optionally postprocess
-                drawTarget.bind()
-                renderDraw()
-                if (postprocessing.enabled) postprocessing.render(false)
-
-                // compose rendered scene with compose target
-                composeTarget.bind()
-                state.enable(gl.BLEND)
-                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)
-                if (multiSampleIndex === 0) {
-                    webgl.state.clearColor(0, 0, 0, 0)
-                    gl.clear(gl.COLOR_BUFFER_BIT)
-                }
-                compose.render()
-
-                multiSampleIndex += 1
-                if (multiSampleIndex >= offsetList.length ) break
-            }
-
-            const accumulationWeight = multiSampleIndex * sampleWeight
-            if (accumulationWeight > 0) {
-                ValueCell.update(compose.values.uWeight, 1.0)
-                ValueCell.update(compose.values.tColor, composeTarget.texture)
-                compose.update()
-                webgl.unbindFramebuffer()
-                gl.viewport(0, 0, width, height)
-                state.disable(gl.BLEND)
-                compose.render()
-            }
-            if (accumulationWeight < 1.0) {
-                ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight)
-                ValueCell.update(compose.values.tColor, holdTarget.texture)
-                compose.update()
-                webgl.unbindFramebuffer()
-                gl.viewport(0, 0, width, height)
-                if (accumulationWeight === 0) state.disable(gl.BLEND)
-                else state.enable(gl.BLEND)
-                compose.render()
-            }
-
-            camera.viewOffset.enabled = false
-            camera.updateMatrices()
-            if (multiSampleIndex >= offsetList.length) multiSampleIndex = -1
-        }
-
-        function renderMultiSample() {
-            // based on the Multisample Anti-Aliasing Render Pass
-            // contributed to three.js by bhouston / http://clara.io/
-            //
-            // 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(p.sampleLevel, 5)) ]
-
-            const baseSampleWeight = 1.0 / offsetList.length
-            const roundingRange = 1 / 32
-
-            camera.viewOffset.enabled = true
-            ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture)
-            compose.update()
-
-            const { width, height } = drawTarget
-
-            // 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.updateMatrices()
-
-                // 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
-                // distributed across a range of values whose rounding errors cancel each other out.
-                const uniformCenteredDistribution = -0.5 + (i + 0.5) / offsetList.length
-                const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution
-                ValueCell.update(compose.values.uWeight, sampleWeight)
-
-                // render scene and optionally postprocess
-                drawTarget.bind()
-                renderDraw()
-                if (postprocessing.enabled) postprocessing.render(false)
-
-                // compose rendered scene with compose target
-                composeTarget.bind()
-                gl.viewport(0, 0, width, height)
-                state.enable(gl.BLEND)
-                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)
-                if (i === 0) {
-                    webgl.state.clearColor(0, 0, 0, 0)
-                    gl.clear(gl.COLOR_BUFFER_BIT)
-                }
-                compose.render()
-            }
-
-            ValueCell.update(compose.values.uWeight, 1.0)
-            ValueCell.update(compose.values.tColor, composeTarget.texture)
-            compose.update()
-
-            webgl.unbindFramebuffer()
-            gl.viewport(0, 0, width, height)
-            state.disable(gl.BLEND)
-            compose.render()
-
-            camera.viewOffset.enabled = false
-            camera.updateMatrices()
-        }
-
         function render(variant: 'pick' | 'draw', force: boolean) {
             if (isIdentifying || isUpdating) return false
 
@@ -417,21 +245,9 @@ namespace Canvas3D {
             // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane.
             if (!camera.transition.inTransition) setClipping();
             const cameraChanged = camera.updateMatrices();
-            if (force || cameraChanged) lastRenderTime = now()
-            if (p.multiSample === 'temporal') {
-                if (currentTime - lastRenderTime > 200) {
-                    multiSample = multiSampleIndex !== -1
-                } else {
-                    multiSampleIndex = 0
-                    multiSample = false
-                }
-            } else if (p.multiSample === 'on') {
-                multiSample = true
-            } else {
-                multiSample = false
-            }
+            multiSample.update(force || cameraChanged, currentTime)
 
-            if (force || cameraChanged || multiSample) {
+            if (force || cameraChanged || multiSample.enabled) {
                 switch (variant) {
                     case 'pick':
                         renderer.setViewport(0, 0, pickWidth, pickHeight);
@@ -444,12 +260,8 @@ namespace Canvas3D {
                         break;
                     case 'draw':
                         renderer.setViewport(0, 0, width, height);
-                        if (multiSample) {
-                            if (p.multiSample === 'temporal') {
-                                renderTemporalMultiSample()
-                            } else {
-                                renderMultiSample()
-                            }
+                        if (multiSample.enabled) {
+                            multiSample.render()
                         } else {
                             if (postprocessing.enabled) drawTarget.bind()
                             else webgl.unbindFramebuffer()
@@ -467,7 +279,6 @@ namespace Canvas3D {
 
         let forceNextDraw = false;
         let currentTime = 0;
-        let lastRenderTime = 0;
 
         function draw(force?: boolean) {
             if (render('draw', !!force || forceNextDraw)) {
@@ -636,11 +447,8 @@ namespace Canvas3D {
                 if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]]
                 if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]]
 
-                if (props.multiSample !== undefined) p.multiSample = props.multiSample
-                if (props.sampleLevel !== undefined) p.sampleLevel = props.sampleLevel
-
                 if (props.postprocessing) postprocessing.setProps(props.postprocessing)
-
+                if (props.multiSample) multiSample.setProps(props.multiSample)
                 if (props.renderer) renderer.setProps(props.renderer)
                 if (props.trackball) controls.setProps(props.trackball)
                 if (props.debug) debugHelper.setProps(props.debug)
@@ -654,10 +462,8 @@ namespace Canvas3D {
                     clip: p.clip,
                     fog: p.fog,
 
-                    multiSample: p.multiSample,
-                    sampleLevel: p.sampleLevel,
-
                     postprocessing: { ...postprocessing.props },
+                    multiSample: { ...multiSample.props },
                     renderer: { ...renderer.props },
                     trackball: { ...controls.props },
                     debug: { ...debugHelper.props }
@@ -693,14 +499,13 @@ namespace Canvas3D {
 
             drawTarget.setSize(width, height)
             postprocessing.setSize(width, height)
-            composeTarget.setSize(width, height)
-            holdTarget.setSize(width, height)
+            multiSample.setSize(width, height)
+
             if (depthTarget) {
                 depthTarget.setSize(width, height)
             } else {
                 depthTexture.define(width, height)
             }
-            ValueCell.update(compose.values.uTexSize, Vec2.set(compose.values.uTexSize.ref.value, width, height))
 
             pickScale = pickBaseScale / webgl.pixelRatio
             pickWidth = Math.round(width * pickScale)
diff --git a/src/mol-canvas3d/helper/multi-sample.ts b/src/mol-canvas3d/helper/multi-sample.ts
index 3cec89814b6e94162525b7cbbd8168a8ba361b06..d2d9647dc5226df4010cc6bdd714a0d1386ce126 100644
--- a/src/mol-canvas3d/helper/multi-sample.ts
+++ b/src/mol-canvas3d/helper/multi-sample.ts
@@ -12,7 +12,11 @@ import { ValueCell } from 'mol-util';
 import { Vec2 } from 'mol-math/linear-algebra';
 import { ShaderCode } from 'mol-gl/shader-code';
 import { createComputeRenderItem } from 'mol-gl/webgl/render-item';
-import { createComputeRenderable } from 'mol-gl/renderable';
+import { createComputeRenderable, ComputeRenderable } from 'mol-gl/renderable';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { RenderTarget, createRenderTarget } from 'mol-gl/webgl/render-target';
+import { Camera } from 'mol-canvas3d/camera';
+import { PostprocessingPass } from './postprocessing';
 
 const ComposeSchema = {
     ...QuadSchema,
@@ -21,7 +25,9 @@ const ComposeSchema = {
     uWeight: UniformSpec('f'),
 }
 
-export function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture) {
+type ComposeRenderable = ComputeRenderable<Values<typeof ComposeSchema>>
+
+function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): ComposeRenderable {
     const values: Values<typeof ComposeSchema> = {
         ...QuadValues,
         tColor: ValueCell.create(colorTexture),
@@ -39,7 +45,238 @@ export function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture) {
     return createComputeRenderable(renderItem, values)
 }
 
-export const JitterVectors = [
+export const MultiSampleParams = {
+    mode: PD.Select('off', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
+    sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }),
+}
+export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
+
+export class MultiSamplePass {
+    props: MultiSampleProps
+
+    private composeTarget: RenderTarget
+    private holdTarget: RenderTarget
+    private compose: ComposeRenderable
+
+    private sampleIndex = -1
+    private currentTime = 0
+    private lastRenderTime = 0
+
+    constructor(private webgl: WebGLContext, private camera: Camera, private drawTarget: RenderTarget, private postprocessing: PostprocessingPass, private renderDraw: () => void, props: Partial<MultiSampleProps>) {
+        const { gl } = webgl
+        this.composeTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
+        this.holdTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
+        this.compose = getComposeRenderable(webgl, drawTarget.texture)
+        this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props }
+    }
+
+    get enabled() {
+        if (this.props.mode === 'temporal') {
+            if (this.currentTime - this.lastRenderTime > 200) {
+                return this.sampleIndex !== -1
+            } else {
+                this.sampleIndex = 0
+                return false
+            }
+        } else if (this.props.mode === 'on') {
+            return true
+        } else {
+            return false
+        }
+    }
+
+    update(changed: boolean, currentTime: number) {
+        if (changed) this.lastRenderTime = currentTime
+        this.currentTime = currentTime
+    }
+
+    setSize(width: number, height: number) {
+        this.composeTarget.setSize(width, height)
+        this.holdTarget.setSize(width, height)
+        ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height))
+    }
+
+    setProps(props: Partial<MultiSampleProps>) {
+        if (props.mode !== undefined) this.props.mode = props.mode
+        if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel
+    }
+
+    render() {
+        if (this.props.mode === 'temporal') {
+            this.renderTemporalMultiSample()
+        } else {
+            this.renderMultiSample()
+        }
+    }
+
+    private renderMultiSample() {
+        const { camera, compose, drawTarget, composeTarget, postprocessing, renderDraw, webgl } = this
+        const { gl, state } = webgl
+
+        // based on the Multisample Anti-Aliasing Render Pass
+        // contributed to three.js by bhouston / http://clara.io/
+        //
+        // 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 baseSampleWeight = 1.0 / offsetList.length
+        const roundingRange = 1 / 32
+
+        camera.viewOffset.enabled = true
+        ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture)
+        compose.update()
+
+        const { width, height } = drawTarget
+
+        // 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.updateMatrices()
+
+            // 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
+            // distributed across a range of values whose rounding errors cancel each other out.
+            const uniformCenteredDistribution = -0.5 + (i + 0.5) / offsetList.length
+            const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution
+            ValueCell.update(compose.values.uWeight, sampleWeight)
+
+            // render scene and optionally postprocess
+            drawTarget.bind()
+            renderDraw()
+            if (postprocessing.enabled) postprocessing.render(false)
+
+            // compose rendered scene with compose target
+            composeTarget.bind()
+            gl.viewport(0, 0, width, height)
+            state.enable(gl.BLEND)
+            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)
+            if (i === 0) {
+                state.clearColor(0, 0, 0, 0)
+                gl.clear(gl.COLOR_BUFFER_BIT)
+            }
+            compose.render()
+        }
+
+        ValueCell.update(compose.values.uWeight, 1.0)
+        ValueCell.update(compose.values.tColor, composeTarget.texture)
+        compose.update()
+
+        webgl.unbindFramebuffer()
+        gl.viewport(0, 0, width, height)
+        state.disable(gl.BLEND)
+        compose.render()
+
+        camera.viewOffset.enabled = false
+        camera.updateMatrices()
+    }
+
+    private renderTemporalMultiSample() {
+        const { camera, compose, drawTarget, composeTarget, holdTarget, postprocessing, renderDraw, webgl } = this
+        const { gl, state } = webgl
+
+        // based on the Multisample Anti-Aliasing Render Pass
+        // contributed to three.js by bhouston / http://clara.io/
+        //
+        // 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)) ]
+
+        if (this.sampleIndex === -1) return
+        if (this.sampleIndex >= offsetList.length) {
+            this.sampleIndex = -1
+            return
+        }
+
+        const i = this.sampleIndex
+
+        if (i === 0) {
+            drawTarget.bind()
+            renderDraw()
+            if (postprocessing.enabled) postprocessing.render(false)
+            ValueCell.update(compose.values.uWeight, 1.0)
+            ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture)
+            compose.update()
+
+            holdTarget.bind()
+            state.disable(gl.BLEND)
+            compose.render()
+        }
+
+        const sampleWeight = 1.0 / offsetList.length
+
+        camera.viewOffset.enabled = true
+        ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture)
+        ValueCell.update(compose.values.uWeight, sampleWeight)
+        compose.update()
+
+        const { width, height } = drawTarget
+
+        // render the scene multiple times, each slightly jitter offset
+        // from the last and accumulate the results.
+        const numSamplesPerFrame = Math.pow(2, this.props.sampleLevel)
+        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.updateMatrices()
+
+            // render scene and optionally postprocess
+            drawTarget.bind()
+            renderDraw()
+            if (postprocessing.enabled) postprocessing.render(false)
+
+            // compose rendered scene with compose target
+            composeTarget.bind()
+            state.enable(gl.BLEND)
+            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)
+            if (this.sampleIndex === 0) {
+                state.clearColor(0, 0, 0, 0)
+                gl.clear(gl.COLOR_BUFFER_BIT)
+            }
+            compose.render()
+
+            this.sampleIndex += 1
+            if (this.sampleIndex >= offsetList.length ) break
+        }
+
+        const accumulationWeight = this.sampleIndex * sampleWeight
+        if (accumulationWeight > 0) {
+            ValueCell.update(compose.values.uWeight, 1.0)
+            ValueCell.update(compose.values.tColor, composeTarget.texture)
+            compose.update()
+            webgl.unbindFramebuffer()
+            gl.viewport(0, 0, width, height)
+            state.disable(gl.BLEND)
+            compose.render()
+        }
+        if (accumulationWeight < 1.0) {
+            ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight)
+            ValueCell.update(compose.values.tColor, holdTarget.texture)
+            compose.update()
+            webgl.unbindFramebuffer()
+            gl.viewport(0, 0, width, height)
+            if (accumulationWeight === 0) state.disable(gl.BLEND)
+            else state.enable(gl.BLEND)
+            compose.render()
+        }
+
+        camera.viewOffset.enabled = false
+        camera.updateMatrices()
+        if (this.sampleIndex >= offsetList.length) this.sampleIndex = -1
+    }
+}
+
+const JitterVectors = [
     [
         [ 0, 0 ]
     ],
diff --git a/src/mol-canvas3d/helper/postprocessing.ts b/src/mol-canvas3d/helper/postprocessing.ts
index 39fc3965988e334f2e17d7716e4303202e14220f..eb54c3a424bf155d04c4ec611139d098f1a8232a 100644
--- a/src/mol-canvas3d/helper/postprocessing.ts
+++ b/src/mol-canvas3d/helper/postprocessing.ts
@@ -48,7 +48,7 @@ export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
 
-export function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, props: Partial<PostprocessingProps>): PostprocessingRenderable {
+function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, props: Partial<PostprocessingProps>): PostprocessingRenderable {
     const p = { ...PD.getDefaultValues(PostprocessingParams), ...props }
     const values: Values<typeof PostprocessingSchema> = {
         ...QuadValues,