diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9ff693dc26143f04565512660bfb48933afa824..f66925d92bd3ad802b05e907c16ce05b636f3077 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Relative frame support for ``Canvas3D`` viewport.
 - Fix bug in screenshot copy UI.
 - Add ability to select residues from a list of identifiers to the Selection UI.
+- Fix SSAO bugs when used with ``Canvas3D`` viewport.
 
 ## [v2.0.4] - 2021-04-20
 
diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts
index cb5a4468f169d8b5badb7d39dccaaf6407b7ec06..628ef8252e956e4e4f10f4bdfca6b93ae7cc3940 100644
--- a/src/mol-canvas3d/camera.ts
+++ b/src/mol-canvas3d/camera.ts
@@ -234,8 +234,8 @@ namespace Camera {
             up: Vec3.create(0, 1, 0),
             target: Vec3.create(0, 0, 0),
 
-            radius: 0,
-            radiusMax: 0,
+            radius: 10,
+            radiusMax: 10,
             fog: 50,
             clipFar: true
         };
diff --git a/src/mol-canvas3d/camera/transition.ts b/src/mol-canvas3d/camera/transition.ts
index 62f3f50c5c030947329fbb637221e42d7b12893c..ad32bb076952feb14ab83939b68a02d18d69d335 100644
--- a/src/mol-canvas3d/camera/transition.ts
+++ b/src/mol-canvas3d/camera/transition.ts
@@ -39,6 +39,9 @@ class CameraTransitionManager {
             this._target.radius = this._target.radiusMax;
         }
 
+        if (this._target.radius < 0.01) this._target.radius = 0.01;
+        if (this._target.radiusMax < 0.01) this._target.radiusMax = 0.01;
+
         if (!this.inTransition && durationMs <= 0 || (typeof to.mode !== 'undefined' && to.mode !== this.camera.state.mode)) {
             this.finish(this._target);
             return;
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index 85e55a0f8b212118986c7b908a7a110a890feb8e..9887d61e372e278d71d77b9f5156887083a9f1f0 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -106,7 +106,7 @@ interface Canvas3DContext {
 }
 
 namespace Canvas3DContext {
-    const DefaultAttribs = {
+    export const DefaultAttribs = {
         /** true by default to avoid issues with Safari (Jan 2021) */
         antialias: true,
         /** true to support multiple Canvas3D objects with a single context */
diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts
index 1f671d251495688f459f0f6502e710f95b284b06..c851c8acc5d320357d56172b1978f16f5eec2268 100644
--- a/src/mol-canvas3d/passes/postprocessing.ts
+++ b/src/mol-canvas3d/passes/postprocessing.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -13,7 +13,7 @@ import { Texture } from '../../mol-gl/webgl/texture';
 import { ValueCell } from '../../mol-util';
 import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
-import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { Mat4, Vec2, Vec3, Vec4 } 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';
@@ -70,6 +70,7 @@ const SsaoSchema = {
 
     uProjection: UniformSpec('m4'),
     uInvProjection: UniformSpec('m4'),
+    uBounds: UniformSpec('v4'),
 
     uTexSize: UniformSpec('v2'),
 
@@ -89,6 +90,7 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
 
         uProjection: ValueCell.create(Mat4.identity()),
         uInvProjection: ValueCell.create(Mat4.identity()),
+        uBounds: ValueCell.create(Vec4()),
 
         uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
 
@@ -118,6 +120,7 @@ const SsaoBlurSchema = {
 
     uNear: UniformSpec('f'),
     uFar: UniformSpec('f'),
+    uBounds: UniformSpec('v4'),
     dOrthographic: DefineSpec('number'),
 };
 
@@ -139,6 +142,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
 
         uNear: ValueCell.create(0.0),
         uFar: ValueCell.create(10000.0),
+        uBounds: ValueCell.create(Vec4()),
         dOrthographic: ValueCell.create(0),
     };
 
@@ -286,10 +290,14 @@ export class PostprocessingPass {
 
     private readonly renderable: PostprocessingRenderable
 
-    private scale: number
+    private ssaoScale: number
+    private calcSsaoScale() {
+        // downscale ssao for high pixel-ratios
+        return Math.min(1, 1 / this.webgl.pixelRatio);
+    }
 
     constructor(private webgl: WebGLContext, drawPass: DrawPass) {
-        this.scale = 1 / this.webgl.pixelRatio;
+        this.ssaoScale = this.calcSsaoScale();
 
         const { colorTarget, depthTexture } = drawPass;
         const width = colorTarget.getWidth();
@@ -298,7 +306,7 @@ export class PostprocessingPass {
         this.nSamples = 1;
         this.blurKernelSize = 1;
 
-        this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
+        this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest');
 
         this.outlinesTarget = webgl.createRenderTarget(width, height, false);
         this.outlinesRenderable = getOutlinesRenderable(webgl, depthTexture);
@@ -317,14 +325,14 @@ export class PostprocessingPass {
         this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
 
-        const sw = Math.floor(width * this.scale);
-        const sh = Math.floor(height * this.scale);
+        const sw = Math.floor(width * this.ssaoScale);
+        const sh = Math.floor(height * this.ssaoScale);
 
-        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
         this.ssaoDepthTexture.define(sw, sh);
         this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
 
-        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
         this.ssaoDepthBlurProxyTexture.define(sw, sh);
         this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
 
@@ -338,9 +346,13 @@ export class PostprocessingPass {
 
     setSize(width: number, height: number) {
         const [w, h] = this.renderable.values.uTexSize.ref.value;
-        if (width !== w || height !== h) {
-            const sw = Math.floor(width * this.scale);
-            const sh = Math.floor(height * this.scale);
+        const ssaoScale = this.calcSsaoScale();
+
+        if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
+            this.ssaoScale = ssaoScale;
+
+            const sw = Math.floor(width * this.ssaoScale);
+            const sh = Math.floor(height * this.ssaoScale);
             this.target.setSize(width, height);
             this.outlinesTarget.setSize(width, height);
             this.ssaoDepthTexture.define(sw, sh);
@@ -349,8 +361,8 @@ export class PostprocessingPass {
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
-            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
-            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
+            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
+            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
         }
     }
 
@@ -367,8 +379,22 @@ export class PostprocessingPass {
         Mat4.invert(invProjection, camera.projection);
 
         if (props.occlusion.name === 'on') {
-            ValueCell.updateIfChanged(this.ssaoRenderable.values.uProjection, camera.projection);
-            ValueCell.updateIfChanged(this.ssaoRenderable.values.uInvProjection, invProjection);
+            ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
+            ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
+
+            const [w, h] = this.renderable.values.uTexSize.ref.value;
+            const b = this.ssaoRenderable.values.uBounds;
+            const v = camera.viewport;
+            const s = this.ssaoScale;
+            Vec4.set(b.ref.value,
+                Math.floor(v.x * s) / (w * s),
+                Math.floor(v.y * s) / (h * s),
+                Math.ceil((v.x + v.width) * s) / (w * s),
+                Math.ceil((v.y + v.height) * s) / (h * s)
+            );
+            ValueCell.update(b, b.ref.value);
+            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
+            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
 
             ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
             ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
@@ -376,7 +402,9 @@ export class PostprocessingPass {
             ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
             ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
 
-            if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateSsaoBlur = true; }
+            if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
+                needsUpdateSsaoBlur = true;
+            }
             ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
             ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
 
@@ -384,7 +412,7 @@ export class PostprocessingPass {
                 needsUpdateSsao = true;
 
                 this.nSamples = props.occlusion.params.samples;
-                ValueCell.updateIfChanged(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
+                ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
                 ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
             }
             ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
@@ -394,10 +422,10 @@ export class PostprocessingPass {
                 needsUpdateSsaoBlur = true;
 
                 this.blurKernelSize = props.occlusion.params.blurKernelSize;
-                let kernel = getBlurKernel(this.blurKernelSize);
+                const kernel = getBlurKernel(this.blurKernelSize);
 
-                ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
-                ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
+                ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
+                ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
                 ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
                 ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
             }
@@ -467,10 +495,10 @@ export class PostprocessingPass {
 
         if (props.occlusion.name === 'on') {
             const { x, y, width, height } = camera.viewport;
-            const sx = Math.floor(x * this.scale);
-            const sy = Math.floor(y * this.scale);
-            const sw = Math.floor(width * this.scale);
-            const sh = Math.floor(height * this.scale);
+            const sx = Math.floor(x * this.ssaoScale);
+            const sy = Math.floor(y * this.ssaoScale);
+            const sw = Math.ceil(width * this.ssaoScale);
+            const sh = Math.ceil(height * this.ssaoScale);
             this.webgl.gl.viewport(sx, sy, sw, sh);
             this.webgl.gl.scissor(sx, sy, sw, sh);
 
diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts
index b7872bc3ecc644daf819e19ab9a99c4eeb652596..55cdf3062610ad8c22c2af544991c84fc7172eeb 100644
--- a/src/mol-gl/shader/postprocessing.frag.ts
+++ b/src/mol-gl/shader/postprocessing.frag.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -88,7 +88,8 @@ float getSsao(vec2 coords) {
     } else if (rawSsao > 0.001) {
         return rawSsao;
     }
-    return 0.0;
+    // treat values close to 0.0 as errors and return no occlusion
+    return 1.0;
 }
 
 void main(void) {
diff --git a/src/mol-gl/shader/ssao-blur.frag.ts b/src/mol-gl/shader/ssao-blur.frag.ts
index 34d16aa7e5719a8a536480321f8acd32137b1bdf..efa894dc21f8d9e4f490424f1bb5afd6e9864a57 100644
--- a/src/mol-gl/shader/ssao-blur.frag.ts
+++ b/src/mol-gl/shader/ssao-blur.frag.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 export const ssaoBlur_frag = `
@@ -11,6 +12,7 @@ precision highp sampler2D;
 
 uniform sampler2D tSsaoDepth;
 uniform vec2 uTexSize;
+uniform vec4 uBounds;
 
 uniform float uKernel[dOcclusionKernelSize];
 
@@ -36,16 +38,25 @@ bool isBackground(const in float depth) {
     return depth == 1.0;
 }
 
+bool outsideBounds(const in vec2 p) {
+    return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
+}
+
 void main(void) {
     vec2 coords = gl_FragCoord.xy / uTexSize;
 
     vec2 packedDepth = texture2D(tSsaoDepth, coords).zw;
 
+    if (outsideBounds(coords)) {
+        gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
+        return;
+    }
+
     float selfDepth = unpackRGToUnitInterval(packedDepth);
     // if background and if second pass
     if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
-       gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
-       return;
+        gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
+        return;
     }
 
     float selfViewZ = getViewZ(selfDepth);
@@ -57,6 +68,9 @@ void main(void) {
     // only if kernelSize is odd
     for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
         vec2 sampleCoords = coords + float(i) * offset;
+        if (outsideBounds(sampleCoords)) {
+            continue;
+        }
 
         vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords);
 
diff --git a/src/mol-gl/shader/ssao.frag.ts b/src/mol-gl/shader/ssao.frag.ts
index 5b36d9116b6e6566a0d9a60ed2ab6005d6992da6..029661de7af06a9a8c4d5599dfb599b737efdf7a 100644
--- a/src/mol-gl/shader/ssao.frag.ts
+++ b/src/mol-gl/shader/ssao.frag.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -13,14 +13,14 @@ precision highp sampler2D;
 #include common
 
 uniform sampler2D tDepth;
+uniform vec2 uTexSize;
+uniform vec4 uBounds;
 
 uniform vec3 uSamples[dNSamples];
 
 uniform mat4 uProjection;
 uniform mat4 uInvProjection;
 
-uniform vec2 uTexSize;
-
 uniform float uRadius;
 uniform float uBias;
 
@@ -46,8 +46,12 @@ bool isBackground(const in float depth) {
     return depth == 1.0;
 }
 
+bool outsideBounds(const in vec2 p) {
+    return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
+}
+
 float getDepth(const in vec2 coords) {
-    return unpackRGBAToDepth(texture2D(tDepth, coords));
+    return outsideBounds(coords) ? 1.0 : unpackRGBAToDepth(texture2D(tDepth, coords));
 }
 
 vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {