diff --git a/package-lock.json b/package-lock.json
index 1a944196c1c6e3f06e68667429b103248fe14d46..ced5f348fdac4b2587a95d97acdd7f7c672f166f 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index 8e9186ad2159a76e30a5a46a972d345ec4c32edc..7283779d1b43c0d1cc42699743ce8a5f66206e8f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "1.2.4",
+  "version": "1.2.6",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -86,64 +86,64 @@
   ],
   "license": "MIT",
   "devDependencies": {
-    "@graphql-codegen/add": "^1.17.7",
-    "@graphql-codegen/cli": "^1.17.8",
-    "@graphql-codegen/time": "^1.17.10",
-    "@graphql-codegen/typescript": "^1.17.9",
-    "@graphql-codegen/typescript-graphql-files-modules": "^1.17.8",
-    "@graphql-codegen/typescript-graphql-request": "^1.17.7",
-    "@graphql-codegen/typescript-operations": "^1.17.8",
-    "@types/cors": "^2.8.7",
-    "@typescript-eslint/eslint-plugin": "^3.10.1",
-    "@typescript-eslint/parser": "^3.10.1",
+    "@graphql-codegen/add": "^2.0.2",
+    "@graphql-codegen/cli": "^1.19.4",
+    "@graphql-codegen/time": "^2.0.2",
+    "@graphql-codegen/typescript": "^1.19.0",
+    "@graphql-codegen/typescript-graphql-files-modules": "^1.18.1",
+    "@graphql-codegen/typescript-graphql-request": "^2.0.3",
+    "@graphql-codegen/typescript-operations": "^1.17.12",
+    "@types/cors": "^2.8.8",
+    "@typescript-eslint/eslint-plugin": "^4.9.1",
+    "@typescript-eslint/parser": "^4.9.1",
     "benchmark": "^2.1.4",
     "concurrently": "^5.3.0",
-    "cpx2": "^2.0.0",
-    "css-loader": "^3.6.0",
-    "eslint": "^7.8.1",
+    "cpx2": "^3.0.0",
+    "css-loader": "^5.0.1",
+    "eslint": "^7.15.0",
     "extra-watch-webpack-plugin": "^1.0.3",
-    "file-loader": "^6.1.0",
+    "file-loader": "^6.2.0",
     "fs-extra": "^9.0.1",
-    "graphql": "^15.3.0",
+    "graphql": "^15.4.0",
     "http-server": "^0.12.3",
-    "jest": "^26.4.2",
-    "mini-css-extract-plugin": "^0.9.0",
-    "node-sass": "^4.14.1",
-    "raw-loader": "^4.0.1",
-    "sass-loader": "^8.0.2",
-    "simple-git": "^2.20.1",
-    "style-loader": "^1.2.1",
-    "ts-jest": "^26.3.0",
-    "typescript": "^4.0.2",
+    "jest": "^26.6.3",
+    "mini-css-extract-plugin": "^1.3.2",
+    "node-sass": "^5.0.0",
+    "raw-loader": "^4.0.2",
+    "sass-loader": "^10.1.0",
+    "simple-git": "^2.25.0",
+    "style-loader": "^2.0.0",
+    "ts-jest": "^26.4.4",
+    "typescript": "^4.1.2",
     "webpack": "^4.44.1",
     "webpack-cli": "^3.3.12",
     "webpack-version-file-plugin": "^0.4.0"
   },
   "dependencies": {
     "@types/argparse": "^1.0.38",
-    "@types/benchmark": "^1.0.33",
+    "@types/benchmark": "^2.1.0",
     "@types/compression": "1.7.0",
-    "@types/express": "^4.17.8",
-    "@types/jest": "^25.2.3",
-    "@types/node": "^14.10.1",
+    "@types/express": "^4.17.9",
+    "@types/jest": "^26.0.18",
+    "@types/node": "^14.14.11",
     "@types/node-fetch": "^2.5.7",
-    "@types/react": "^16.9.49",
-    "@types/react-dom": "^16.9.8",
-    "@types/swagger-ui-dist": "3.0.5",
+    "@types/react": "^17.0.0",
+    "@types/react-dom": "^17.0.0",
+    "@types/swagger-ui-dist": "3.30.0",
     "argparse": "^1.0.10",
     "body-parser": "^1.19.0",
     "compression": "^1.7.4",
     "cors": "^2.8.5",
     "express": "^4.17.1",
     "h264-mp4-encoder": "^1.0.12",
-    "immer": "^7.0.9",
+    "immer": "^8.0.0",
     "immutable": "^3.8.2",
-    "node-fetch": "^2.6.0",
-    "react": "^16.13.1",
-    "react-dom": "^16.13.1",
+    "node-fetch": "^2.6.1",
+    "react": "^17.0.1",
+    "react-dom": "^17.0.1",
     "rxjs": "^6.6.3",
-    "swagger-ui-dist": "^3.33.0",
-    "tslib": "^2.0.1",
+    "swagger-ui-dist": "^3.37.2",
+    "tslib": "^2.0.3",
     "util.promisify": "^1.0.1",
     "xhr2": "^0.2.0"
   }
diff --git a/src/apps/docking-viewer/viewport.tsx b/src/apps/docking-viewer/viewport.tsx
index ed41fd7542c9cfae40414619c591060151ad052b..5ead151ff5ff4f28ee12d2e0d4033706704d7877 100644
--- a/src/apps/docking-viewer/viewport.tsx
+++ b/src/apps/docking-viewer/viewport.tsx
@@ -46,13 +46,14 @@ function occlusionStyle(plugin: PluginContext) {
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
             occlusion: { name: 'on', params: {
-                kernelSize: 8,
-                bias: 0.8,
-                radius: 64
+                samples: 64,
+                radius: 8,
+                bias: 1.0,
+                blurKernelSize: 13
             } },
             outline: { name: 'on', params: {
                 scale: 1.0,
-                threshold: 0.8
+                threshold: 0.33
             } }
         }
     } });
diff --git a/src/examples/lighting/index.ts b/src/examples/lighting/index.ts
index f8abba5482a52eafbf57226ef2008cc802860223..95757a6215baf0d3737948d2cdf3065bb7f6a1f6 100644
--- a/src/examples/lighting/index.ts
+++ b/src/examples/lighting/index.ts
@@ -24,8 +24,8 @@ const Canvas3DPresets = {
             mode: 'temporal' as Canvas3DProps['multiSample']['mode']
         },
         postprocessing: {
-            occlusion: { name: 'on', params: { bias: 0.8, kernelSize: 6, radius: 64 } },
-            outline: { name: 'on', params: { scale: 1, threshold: 0.8 } }
+            occlusion: { name: 'on', params: { samples: 64, radius: 8, bias: 1.0, blurKernelSize: 13 } },
+            outline: { name: 'on', params: { scale: 1, threshold: 0.33 } }
         },
         renderer: {
             ambientIntensity: 1,
@@ -37,7 +37,7 @@ const Canvas3DPresets = {
             mode: 'temporal' as Canvas3DProps['multiSample']['mode']
         },
         postprocessing: {
-            occlusion: { name: 'on', params: { bias: 0.8, kernelSize: 6, radius: 64 } },
+            occlusion: { name: 'on', params: { samples: 64, radius: 8, bias: 1.0, blurKernelSize: 13 } },
             outline: { name: 'off', params: { } }
         },
         renderer: {
diff --git a/src/extensions/anvil/representation.ts b/src/extensions/anvil/representation.ts
index b4bd6b112c33907a56b3e6fa96ff46525f1ab7fc..157de0a84eb5cc0fd837dc1a981c329c0e7f29cf 100644
--- a/src/extensions/anvil/representation.ts
+++ b/src/extensions/anvil/representation.ts
@@ -10,7 +10,6 @@ import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
 import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../mol-repr/representation';
 import { Structure } from '../../mol-model/structure';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
-import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
 import { StructureRepresentationProvider, StructureRepresentation, StructureRepresentationStateBuilder } from '../../mol-repr/structure/representation';
 import { MembraneOrientation } from './prop';
 import { ThemeRegistryContext } from '../../mol-theme/theme';
@@ -27,6 +26,7 @@ import { MembraneOrientationProvider } from './prop';
 import { MarkerActions } from '../../mol-util/marker-action';
 import { lociLabel } from '../../mol-theme/label';
 import { ColorNames } from '../../mol-util/color/names';
+import { CustomProperty } from '../../mol-model-props/common/custom-property';
 
 const SharedParams = {
     color: PD.Color(ColorNames.lightgrey),
@@ -61,7 +61,6 @@ export type BilayerRimsParams = typeof BilayerRimsParams
 export type BilayerRimsProps = PD.Values<BilayerRimsParams>
 
 const MembraneOrientationVisuals = {
-    'bilayer-spheres': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneOrientation, BilayerSpheresParams>) => ShapeRepresentation(getBilayerSpheres, Spheres.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) }),
     'bilayer-planes': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneOrientation, BilayerPlanesParams>) => ShapeRepresentation(getBilayerPlanes, Mesh.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }), modifyProps: p => ({ ...p, alpha: p.sectorOpacity, ignoreLight: true, doubleSided: false }) }),
     'bilayer-rims': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneOrientation, BilayerRimsParams>) => ShapeRepresentation(getBilayerRims, Lines.Utils, { modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) })
 };
@@ -91,9 +90,13 @@ export const MembraneOrientationRepresentationProvider = StructureRepresentation
     factory: MembraneOrientationRepresentation,
     getParams: getMembraneOrientationParams,
     defaultValues: PD.getDefaultValues(MembraneOrientationParams),
-    defaultColorTheme: { name: 'uniform' },
-    defaultSizeTheme: { name: 'uniform' },
-    isApplicable: (structure: Structure) => structure.elementCount > 0
+    defaultColorTheme: { name: 'shape-group' },
+    defaultSizeTheme: { name: 'shape-group' },
+    isApplicable: (structure: Structure) => structure.elementCount > 0,
+    ensureCustomProperties: {
+        attach: (ctx: CustomProperty.Context, structure: Structure) => MembraneOrientationProvider.attach(ctx, structure, void 0, true),
+        detach: (data) => MembraneOrientationProvider.ref(data, false)
+    }
 });
 
 function membraneLabel(data: Structure) {
@@ -151,28 +154,3 @@ function getLayerPlane(state: MeshBuilder.State, p: Vec3, centroid: Vec3, normal
     MeshBuilder.addPrimitive(state, Mat4.id, circle);
     MeshBuilder.addPrimitiveFlipped(state, Mat4.id, circle);
 }
-
-function getBilayerSpheres(ctx: RuntimeContext, data: Structure, props: BilayerSpheresProps, shape?: Shape<Spheres>): Shape<Spheres> {
-    const { density } = props;
-    const { radius, planePoint1, planePoint2, normalVector } = MembraneOrientationProvider.get(data).value!;
-    const scaledRadius = (props.radiusFactor * radius) * (props.radiusFactor * radius);
-
-    const spheresBuilder = SpheresBuilder.create(256, 128, shape?.geometry);
-    getLayerSpheres(spheresBuilder, planePoint1, normalVector, density, scaledRadius);
-    getLayerSpheres(spheresBuilder, planePoint2, normalVector, density, scaledRadius);
-    return Shape.create('Bilayer spheres', data, spheresBuilder.getSpheres(), () => props.color, () => props.sphereSize, () => membraneLabel(data));
-}
-
-function getLayerSpheres(spheresBuilder: SpheresBuilder, point: Vec3, normalVector: Vec3, density: number, sqRadius: number) {
-    Vec3.normalize(normalVector, normalVector);
-    const d = -Vec3.dot(normalVector, point);
-    const rep = Vec3();
-    for (let i = -1000, il = 1000; i < il; i += density) {
-        for (let j = -1000, jl = 1000; j < jl; j += density) {
-            Vec3.set(rep, i, j, normalVector[2] === 0 ? 0 : -(d + i * normalVector[0] + j * normalVector[1]) / normalVector[2]);
-            if (Vec3.squaredDistance(rep, point) < sqRadius) {
-                spheresBuilder.add(rep[0], rep[1], rep[2], 0);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index cb6992f2ddc6182bf968784a940cbc7a58c42001..b9860ebc942af1c99668a39b3c80ca1c4519e620 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -24,7 +24,7 @@ import { ParamDefinition as PD } from '../mol-util/param-definition';
 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 { PostprocessingParams } from './passes/postprocessing';
 import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
 import { PickData } from './passes/pick';
 import { PickHelper } from './passes/pick';
@@ -310,9 +310,7 @@ namespace Canvas3D {
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
                     multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
                 } else {
-                    const toDrawingBuffer = !PostprocessingPass.isEnabled(p.postprocessing) && scene.volumes.renderables.length === 0 && !passes.draw.wboitEnabled;
-                    passes.draw.render(renderer, cam, scene, helper, toDrawingBuffer, p.transparentBackground);
-                    if (!toDrawingBuffer) passes.postprocessing.render(cam, true, p.postprocessing);
+                    passes.draw.render(renderer, cam, scene, helper, true, p.renderer.backgroundColor, p.transparentBackground, p.postprocessing);
                 }
                 pickHelper.dirty = true;
                 didRender = true;
diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts
index d6c821168b7ccbf57322c2432c505c81b5aa7bad..4224ea482f425f2631bc1bd4fd8942c74522fd51 100644
--- a/src/mol-canvas3d/passes/draw.ts
+++ b/src/mol-canvas3d/passes/draw.ts
@@ -22,8 +22,11 @@ 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 copyFbo_frag from '../../mol-gl/shader/copy-fbo.frag';
 import { StereoCamera } from '../camera/stereo';
 import { WboitPass } from './wboit';
+import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
+import { Color } from '../../mol-util/color';
 
 const DepthMergeSchema = {
     ...QuadSchema,
@@ -50,6 +53,29 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text
     return createComputeRenderable(renderItem, values);
 }
 
+const CopyFboSchema = {
+    ...QuadSchema,
+    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    tDepth: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+};
+const  CopyFboShaderCode = ShaderCode('copy-fbo', quad_vert, copyFbo_frag);
+type  CopyFboRenderable = ComputeRenderable<Values<typeof CopyFboSchema>>
+
+function getCopyFboRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture): CopyFboRenderable {
+    const values: Values<typeof CopyFboSchema> = {
+        ...QuadValues,
+        tColor: ValueCell.create(colorTexture),
+        tDepth: ValueCell.create(depthTexture),
+        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
+    };
+
+    const schema = { ...CopyFboSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', CopyFboShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
 export class DrawPass {
     private readonly drawTarget: RenderTarget
 
@@ -57,14 +83,20 @@ export class DrawPass {
     readonly depthTexture: Texture
     readonly depthTexturePrimitives: Texture
 
-    private readonly packedDepth: boolean
+    readonly packedDepth: boolean
+
     private depthTarget: RenderTarget
     private depthTargetPrimitives: RenderTarget | null
     private depthTargetVolumes: RenderTarget | null
     private depthTextureVolumes: Texture
     private depthMerge: DepthMergeRenderable
 
+    private copyFboTarget: CopyFboRenderable
+    private copyFboPostprocessing: CopyFboRenderable
+
     private wboit: WboitPass | undefined
+    readonly postprocessing: PostprocessingPass
+    private readonly fxaa: AntialiasingPass
 
     get wboitEnabled() {
         return !!this.wboit?.supported;
@@ -93,6 +125,11 @@ export class DrawPass {
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
 
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
+        this.postprocessing = new PostprocessingPass(webgl, this);
+        this.fxaa = new AntialiasingPass(webgl, this);
+
+        this.copyFboTarget = getCopyFboRenderable(webgl, this.colorTarget.texture, this.depthTarget.texture);
+        this.copyFboPostprocessing = getCopyFboRenderable(webgl, this.postprocessing.target.texture, this.depthTarget.texture);
     }
 
     setSize(width: number, height: number) {
@@ -117,9 +154,15 @@ export class DrawPass {
 
             ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height));
 
+            ValueCell.update(this.copyFboTarget.values.uTexSize, Vec2.set(this.copyFboTarget.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.copyFboPostprocessing.values.uTexSize, Vec2.set(this.copyFboPostprocessing.values.uTexSize.ref.value, width, height));
+
             if (this.wboit?.supported) {
                 this.wboit.setSize(width, height);
             }
+
+            this.postprocessing.setSize(width, height);
+            this.fxaa.setSize(width, height);
         }
     }
 
@@ -137,41 +180,50 @@ export class DrawPass {
         this.depthMerge.render();
     }
 
-    private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean) {
-        if (!this.wboit?.supported) throw new Error('expected wboit to be enabled');
+    private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, backgroundColor: Color, postprocessingProps: PostprocessingProps) {
+        if (!this.wboit?.supported) throw new Error('expected wboit to be supported');
 
-        const renderTarget = toDrawingBuffer ? this.drawTarget : this.colorTarget;
-        renderTarget.bind();
+        this.colorTarget.bind();
         renderer.clear(true);
 
         // render opaque primitives
-        this.depthTexturePrimitives.attachFramebuffer(renderTarget.framebuffer, 'depth');
-        renderTarget.bind();
+        this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+        this.colorTarget.bind();
+        renderer.clearDepth();
         renderer.renderWboitOpaque(scene.primitives, camera, null);
 
         // render opaque volumes
-        this.depthTextureVolumes.attachFramebuffer(renderTarget.framebuffer, 'depth');
-        renderTarget.bind();
+        this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+        this.colorTarget.bind();
         renderer.clearDepth();
         renderer.renderWboitOpaque(scene.volumes, camera, this.depthTexturePrimitives);
 
         // merge depth of opaque primitives and volumes
         this._depthMerge();
 
+        if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            this.postprocessing.render(camera, false, backgroundColor, postprocessingProps);
+        }
+
         // render transparent primitives and volumes
         this.wboit.bind();
         renderer.renderWboitTransparent(scene.primitives, camera, this.depthTexture);
         renderer.renderWboitTransparent(scene.volumes, camera, this.depthTexture);
 
         // evaluate wboit
-        this.depthTexturePrimitives.attachFramebuffer(renderTarget.framebuffer, 'depth');
-        renderTarget.bind();
+        if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            this.depthTexturePrimitives.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
+            this.postprocessing.target.bind();
+        } else {
+            this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+            this.colorTarget.bind();
+        }
         this.wboit.render();
     }
 
-    private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean) {
+    private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, backgroundColor: Color, toDrawingBuffer: boolean, postprocessingProps: PostprocessingProps) {
         if (toDrawingBuffer) {
-            this.webgl.unbindFramebuffer();
+            this.drawTarget.bind();
         } else {
             this.colorTarget.bind();
             if (!this.packedDepth) {
@@ -182,22 +234,22 @@ export class DrawPass {
         renderer.clear(true);
         renderer.renderBlendedOpaque(scene.primitives, camera, 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();
-            renderer.clear(false);
-            renderer.renderDepth(scene.primitives, camera, null);
-            this.colorTarget.bind();
-        }
-
-        // do direct-volume rendering
         if (!toDrawingBuffer) {
+            // do a depth pass if not rendering to drawing buffer and
+            // extensions.depthTexture is unsupported (i.e. depthTarget is set)
+            if (this.depthTargetPrimitives) {
+                this.depthTargetPrimitives.bind();
+                renderer.clear(false);
+                renderer.renderDepth(scene.primitives, camera, null);
+                this.colorTarget.bind();
+            }
+
+            // do direct-volume rendering
             if (!this.packedDepth) {
                 this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
                 renderer.clearDepth(); // from previous frame
             }
-            renderer.renderBlendedVolume(scene.volumes, camera, this.depthTexturePrimitives);
+            renderer.renderBlendedVolumeOpaque(scene.volumes, camera, this.depthTexturePrimitives);
 
             // do volume depth pass if extensions.depthTexture is unsupported (i.e. depthTarget is set)
             if (this.depthTargetVolumes) {
@@ -207,29 +259,52 @@ export class DrawPass {
                 this.colorTarget.bind();
             }
 
-            if (!this.packedDepth) {
-                this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+            // merge depths from primitive and volume rendering
+            this._depthMerge();
+            this.colorTarget.bind();
+
+            if (PostprocessingPass.isEnabled(postprocessingProps)) {
+                this.postprocessing.render(camera, false, backgroundColor, postprocessingProps);
+            }
+            renderer.renderBlendedVolumeTransparent(scene.volumes, camera, this.depthTexturePrimitives);
+
+            if (PostprocessingPass.isEnabled(postprocessingProps)) {
+                if (!this.packedDepth) {
+                    this.depthTexturePrimitives.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
+                }
+                this.postprocessing.target.bind();
+            } else {
+                if (!this.packedDepth) {
+                    this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+                }
+                this.colorTarget.bind();
             }
         }
 
         renderer.renderBlendedTransparent(scene.primitives, camera, null);
-
-        // merge depths from primitive and volume rendering
-        if (!toDrawingBuffer) {
-            this._depthMerge();
-            this.colorTarget.bind();
-        }
     }
 
-    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean) {
+    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, backgroundColor: Color, postprocessingProps: PostprocessingProps) {
+        const volumeRendering = scene.volumes.renderables.length > 0;
+        const postprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
+        const antialiasingEnabled = AntialiasingPass.isEnabled(postprocessingProps);
+
         const { x, y, width, height } = camera.viewport;
         renderer.setViewport(x, y, width, height);
         renderer.update(camera);
 
         if (this.wboitEnabled) {
-            this._renderWboit(renderer, camera, scene, toDrawingBuffer);
+            this._renderWboit(renderer, camera, scene, backgroundColor, postprocessingProps);
+        } else {
+            this._renderBlended(renderer, camera, scene, backgroundColor, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, postprocessingProps);
+        }
+
+        if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            this.postprocessing.target.bind();
+        } else if (!toDrawingBuffer || volumeRendering || this.wboitEnabled) {
+            this.colorTarget.bind();
         } else {
-            this._renderBlended(renderer, camera, scene, toDrawingBuffer);
+            this.drawTarget.bind();
         }
 
         if (helper.debug.isEnabled) {
@@ -245,18 +320,40 @@ export class DrawPass {
             renderer.renderBlended(helper.camera.scene, helper.camera.camera, null);
         }
 
+        if (antialiasingEnabled) {
+            this.fxaa.render(camera, toDrawingBuffer, postprocessingProps);
+        } else if (toDrawingBuffer) {
+            this.drawTarget.bind();
+
+            this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
+            if (PostprocessingPass.isEnabled(postprocessingProps)) {
+                this.copyFboPostprocessing.render();
+            } else if (volumeRendering || this.wboitEnabled) {
+                this.copyFboTarget.render();
+            }
+        }
+
         this.webgl.gl.flush();
     }
 
-    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {
+    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, backgroundColor: Color, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
         renderer.setTransparentBackground(transparentBackground);
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
 
         if (StereoCamera.is(camera)) {
-            this._render(renderer, camera.left, scene, helper, toDrawingBuffer);
-            this._render(renderer, camera.right, scene, helper, toDrawingBuffer);
+            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, backgroundColor, postprocessingProps);
+            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, backgroundColor, postprocessingProps);
         } else {
-            this._render(renderer, camera, scene, helper, toDrawingBuffer);
+            this._render(renderer, camera, scene, helper, toDrawingBuffer, backgroundColor, postprocessingProps);
+        }
+    }
+
+    getColorTarget(postprocessingProps: PostprocessingProps): RenderTarget {
+        if (AntialiasingPass.isEnabled(postprocessingProps)) {
+            return this.fxaa.target;
+        } else if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            return this.postprocessing.target;
         }
+        return this.colorTarget;
     }
 }
\ No newline at end of file
diff --git a/src/mol-canvas3d/passes/image.ts b/src/mol-canvas3d/passes/image.ts
index 8c96084efc3f5400aef9a0d4ce152f7dcbb18068..89e0aa1ef0f70967d88e66937a9fd4de28d79783 100644
--- a/src/mol-canvas3d/passes/image.ts
+++ b/src/mol-canvas3d/passes/image.ts
@@ -10,13 +10,14 @@ import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { DrawPass } from './draw';
-import { PostprocessingPass, PostprocessingParams } from './postprocessing';
+import { PostprocessingParams } from './postprocessing';
 import { MultiSamplePass, MultiSampleParams, MultiSampleHelper } from './multi-sample';
 import { Camera } from '../camera';
 import { Viewport } from '../camera/util';
 import { PixelData } from '../../mol-util/image';
 import { Helper } from '../helper/helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
+import { Color } from '../../mol-util/color';
 
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
@@ -38,7 +39,6 @@ export class ImagePass {
     get colorTarget() { return this._colorTarget; }
 
     private readonly drawPass: DrawPass
-    private readonly postprocessingPass: PostprocessingPass
     private readonly multiSamplePass: MultiSamplePass
     private readonly multiSampleHelper: MultiSampleHelper
     private readonly helper: Helper
@@ -50,8 +50,7 @@ export class ImagePass {
         this.props = { ...PD.getDefaultValues(ImageParams), ...props };
 
         this.drawPass = new DrawPass(webgl, 128, 128, enableWboit);
-        this.postprocessingPass = new PostprocessingPass(webgl, this.drawPass);
-        this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass, this.postprocessingPass);
+        this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
         this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
 
         this.helper = {
@@ -70,7 +69,6 @@ export class ImagePass {
         this._height = height;
 
         this.drawPass.setSize(width, height);
-        this.postprocessingPass.syncSize();
         this.multiSamplePass.syncSize();
     }
 
@@ -88,13 +86,8 @@ export class ImagePass {
             this.multiSampleHelper.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props);
             this._colorTarget = this.multiSamplePass.colorTarget;
         } else {
-            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground);
-            if (PostprocessingPass.isEnabled(this.props.postprocessing)) {
-                this.postprocessingPass.render(this._camera, false, this.props.postprocessing);
-                this._colorTarget = this.postprocessingPass.target;
-            } else {
-                this._colorTarget = this.drawPass.colorTarget;
-            }
+            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, Color(0xffffff), this.props.transparentBackground, this.props.postprocessing);
+            this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
         }
     }
 
diff --git a/src/mol-canvas3d/passes/multi-sample.ts b/src/mol-canvas3d/passes/multi-sample.ts
index 6db628cd96cc1cec14066676dabc18eea85913d6..c3b96eb642806191e58a253ecc4fde3028994d24 100644
--- a/src/mol-canvas3d/passes/multi-sample.ts
+++ b/src/mol-canvas3d/passes/multi-sample.ts
@@ -16,7 +16,7 @@ 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, PostprocessingProps } from './postprocessing';
+import { PostprocessingProps } from './postprocessing';
 import { DrawPass } from './draw';
 import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
@@ -25,6 +25,7 @@ import { StereoCamera } from '../camera/stereo';
 
 import quad_vert from '../../mol-gl/shader/quad.vert';
 import compose_frag from '../../mol-gl/shader/compose.frag';
+import { Color } from '../../mol-util/color';
 
 const ComposeSchema = {
     ...QuadSchema,
@@ -68,7 +69,7 @@ export class MultiSamplePass {
     private holdTarget: RenderTarget
     private compose: ComposeRenderable
 
-    constructor(private webgl: WebGLContext, private drawPass: DrawPass, private postprocessing: PostprocessingPass) {
+    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
         const { colorBufferFloat, textureFloat } = webgl.extensions;
         const width = drawPass.colorTarget.getWidth();
         const height = drawPass.colorTarget.getHeight();
@@ -109,7 +110,7 @@ export class MultiSamplePass {
     }
 
     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 { compose, composeTarget, drawPass, webgl } = this;
         const { gl, state } = webgl;
 
         // based on the Multisample Anti-Aliasing Render Pass
@@ -123,10 +124,8 @@ export class MultiSamplePass {
         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, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+        ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
         compose.update();
 
         // render the scene multiple times, each slightly jitter offset
@@ -143,9 +142,8 @@ export class MultiSamplePass {
             const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution;
             ValueCell.update(compose.values.uWeight, sampleWeight);
 
-            // render scene and optionally postprocess
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
-            if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
+            // render scene
+            drawPass.render(renderer, camera, scene, helper, false, Color(0xffffff), transparentBackground, props.postprocessing);
 
             // compose rendered scene with compose target
             composeTarget.bind();
@@ -179,7 +177,7 @@ export class MultiSamplePass {
     }
 
     private renderTemporalMultiSample(sampleIndex: number, renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
-        const { compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this;
+        const { compose, composeTarget, holdTarget, drawPass, webgl } = this;
         const { gl, state } = webgl;
 
         // based on the Multisample Anti-Aliasing Render Pass
@@ -193,13 +191,11 @@ export class MultiSamplePass {
 
         const { x, y, width, height } = camera.viewport;
         const sampleWeight = 1.0 / offsetList.length;
-        const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
 
         if (sampleIndex === -1) {
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
-            if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
+            drawPass.render(renderer, camera, scene, helper, false, Color(0xffffff), transparentBackground, props.postprocessing);
             ValueCell.update(compose.values.uWeight, 1.0);
-            ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+            ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             compose.update();
 
             holdTarget.bind();
@@ -212,7 +208,7 @@ export class MultiSamplePass {
             sampleIndex += 1;
         } else {
             camera.viewOffset.enabled = true;
-            ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+            ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             ValueCell.update(compose.values.uWeight, sampleWeight);
             compose.update();
 
@@ -224,9 +220,8 @@ export class MultiSamplePass {
                 Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
                 camera.update();
 
-                // render scene and optionally postprocess
-                drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
-                if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
+                // render scene
+                drawPass.render(renderer, camera, scene, helper, false, Color(0xffffff), transparentBackground, props.postprocessing);
 
                 // compose rendered scene with compose target
                 composeTarget.bind();
diff --git a/src/mol-canvas3d/passes/passes.ts b/src/mol-canvas3d/passes/passes.ts
index 8e06be24334680d4d5f97a4d5115355e2e746d1b..cc61b18b7048192fa04948787356dbad44ee6b08 100644
--- a/src/mol-canvas3d/passes/passes.ts
+++ b/src/mol-canvas3d/passes/passes.ts
@@ -6,29 +6,25 @@
 
 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, enableWboit: boolean }> = {}) {
         const { gl } = webgl;
         this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false);
         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);
+        this.multiSample = new MultiSamplePass(webgl, this.draw);
     }
 
     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/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts
index 068a8b0a8a4dbf93ebd4fc1ba6433ce7ecea7275..5747c8d294d5f8ab7095f5ecaa7204ad74d008e4 100644
--- a/src/mol-canvas3d/passes/postprocessing.ts
+++ b/src/mol-canvas3d/passes/postprocessing.ts
@@ -2,6 +2,7 @@
  * Copyright (c) 2019-2020 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>
  */
 
 import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
@@ -12,71 +13,227 @@ 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 { Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { Mat4, 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, ICamera } from '../../mol-canvas3d/camera';
+import { ICamera } from '../../mol-canvas3d/camera';
 import quad_vert from '../../mol-gl/shader/quad.vert';
+import outlines_frag from '../../mol-gl/shader/outlines.frag';
+import ssao_frag from '../../mol-gl/shader/ssao.frag';
+import ssao_blur_frag from '../../mol-gl/shader/ssao-blur.frag';
 import postprocessing_frag from '../../mol-gl/shader/postprocessing.frag';
-import { StereoCamera } from '../camera/stereo';
+import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
+import { Color } from '../../mol-util/color';
 import { FxaaParams, FxaaPass } from './fxaa';
 import { SmaaParams, SmaaPass } from './smaa';
 
+const OutlinesSchema = {
+    ...QuadSchema,
+    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+
+    dOrthographic: DefineSpec('number'),
+    uNear: UniformSpec('f'),
+    uFar: UniformSpec('f'),
+
+    uMaxPossibleViewZDiff: UniformSpec('f'),
+};
+type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
+
+function getOutlinesRenderable(ctx: WebGLContext, depthTexture: Texture): OutlinesRenderable {
+    const values: Values<typeof OutlinesSchema> = {
+        ...QuadValues,
+        tDepth: ValueCell.create(depthTexture),
+        uTexSize: ValueCell.create(Vec2.create(depthTexture.getWidth(), depthTexture.getHeight())),
+
+        dOrthographic: ValueCell.create(0),
+        uNear: ValueCell.create(1),
+        uFar: ValueCell.create(10000),
+
+        uMaxPossibleViewZDiff: ValueCell.create(0.5),
+    };
+
+    const schema = { ...OutlinesSchema };
+    const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+const SsaoSchema = {
+    ...QuadSchema,
+    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+
+    uSamples: UniformSpec('v3[]'),
+    dNSamples: DefineSpec('number'),
+
+    uProjection: UniformSpec('m4'),
+    uInvProjection: UniformSpec('m4'),
+
+    uTexSize: UniformSpec('v2'),
+
+    uRadius: UniformSpec('f'),
+    uBias: UniformSpec('f'),
+};
+
+type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
+
+function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRenderable {
+    const values: Values<typeof SsaoSchema> = {
+        ...QuadValues,
+        tDepth: ValueCell.create(depthTexture),
+
+        uSamples: ValueCell.create([0.0, 0.0, 1.0]),
+        dNSamples: ValueCell.create(1),
+
+        uProjection: ValueCell.create(Mat4.identity()),
+        uInvProjection: ValueCell.create(Mat4.identity()),
+
+        uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
+
+        uRadius: ValueCell.create(8.0),
+        uBias: ValueCell.create(0.025),
+    };
+
+    const schema = { ...SsaoSchema };
+    const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+const SsaoBlurSchema = {
+    ...QuadSchema,
+    tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+
+    uKernel: UniformSpec('f[]'),
+    dOcclusionKernelSize: DefineSpec('number'),
+
+    uBlurDirectionX: UniformSpec('f'),
+    uBlurDirectionY: UniformSpec('f'),
+
+    uMaxPossibleViewZDiff: UniformSpec('f'),
+
+    uNear: UniformSpec('f'),
+    uFar: UniformSpec('f'),
+    dOrthographic: DefineSpec('number'),
+};
+
+type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
+
+function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
+    const values: Values<typeof SsaoBlurSchema> = {
+        ...QuadValues,
+        tSsaoDepth: ValueCell.create(ssaoDepthTexture),
+        uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
+
+        uKernel: ValueCell.create([0.0]),
+        dOcclusionKernelSize: ValueCell.create(1),
+
+        uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
+        uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
+
+        uMaxPossibleViewZDiff: ValueCell.create(0.5),
+
+        uNear: ValueCell.create(0.0),
+        uFar: ValueCell.create(10000.0),
+        dOrthographic: ValueCell.create(0),
+    };
+
+    const schema = { ...SsaoBlurSchema };
+    const shaderCode = ShaderCode('ssao_blur', quad_vert, ssao_blur_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+function getBlurKernel(kernelSize: number): number[] {
+    let sigma = kernelSize / 3.0;
+    let halfKernelSize = Math.floor((kernelSize + 1) / 2);
+
+    let kernel = [];
+    for (let x = 0; x < halfKernelSize; x++) {
+        kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
+    }
+
+    return kernel;
+}
+
+function getSamples(vectorSamples: Vec3[], nSamples: number): number[] {
+    let samples = [];
+    for (let i = 0; i < nSamples; i++) {
+        let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
+        scale = 0.1 + scale * (1.0 - 0.1);
+
+        samples.push(vectorSamples[i][0] * scale);
+        samples.push(vectorSamples[i][1] * scale);
+        samples.push(vectorSamples[i][2] * scale);
+    }
+
+    return samples;
+}
+
 const PostprocessingSchema = {
     ...QuadSchema,
+    tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
-    tPackedDepth: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
+    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     uTexSize: UniformSpec('v2'),
 
     dOrthographic: DefineSpec('number'),
+    uInvProjection: UniformSpec('m4'),
     uNear: UniformSpec('f'),
     uFar: UniformSpec('f'),
     uFogNear: UniformSpec('f'),
     uFogFar: UniformSpec('f'),
     uFogColor: UniformSpec('v3'),
 
+    uMaxPossibleViewZDiff: UniformSpec('f'),
+
     dOcclusionEnable: DefineSpec('boolean'),
-    dOcclusionKernelSize: DefineSpec('number'),
-    uOcclusionBias: UniformSpec('f'),
-    uOcclusionRadius: UniformSpec('f'),
 
     dOutlineEnable: DefineSpec('boolean'),
     uOutlineScale: UniformSpec('f'),
     uOutlineThreshold: UniformSpec('f'),
+
+    dPackedDepth: DefineSpec('boolean'),
 };
-const PostprocessingShaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag);
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
 
-function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture): PostprocessingRenderable {
-    const width = colorTexture.getWidth();
-    const height = colorTexture.getHeight();
-
+function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, outlinesTexture: Texture, ssaoDepthTexture: Texture): PostprocessingRenderable {
     const values: Values<typeof PostprocessingSchema> = {
         ...QuadValues,
+        tSsaoDepth: ValueCell.create(ssaoDepthTexture),
         tColor: ValueCell.create(colorTexture),
-        tPackedDepth: ValueCell.create(depthTexture),
-        uTexSize: ValueCell.create(Vec2.create(width, height)),
+        tDepth: ValueCell.create(depthTexture),
+        tOutlines: ValueCell.create(outlinesTexture),
+        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
 
         dOrthographic: ValueCell.create(0),
+        uInvProjection: ValueCell.create(Mat4.identity()),
         uNear: ValueCell.create(1),
         uFar: ValueCell.create(10000),
         uFogNear: ValueCell.create(10000),
         uFogFar: ValueCell.create(10000),
         uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
 
+        uMaxPossibleViewZDiff: ValueCell.create(0.5),
+
         dOcclusionEnable: ValueCell.create(false),
-        dOcclusionKernelSize: ValueCell.create(4),
-        uOcclusionBias: ValueCell.create(0.5),
-        uOcclusionRadius: ValueCell.create(64),
 
         dOutlineEnable: ValueCell.create(false),
-        uOutlineScale: ValueCell.create(1 * ctx.pixelRatio),
+        uOutlineScale: ValueCell.create(ctx.pixelRatio),
         uOutlineThreshold: ValueCell.create(0.8),
+
+        dPackedDepth: ValueCell.create(packedDepth),
     };
 
     const schema = { ...PostprocessingSchema };
-    const renderItem = createComputeRenderItem(ctx, 'triangles', PostprocessingShaderCode, schema, values);
+    const shaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
 
     return createComputeRenderable(renderItem, values);
 }
@@ -84,16 +241,17 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
 export const PostprocessingParams = {
     occlusion: PD.MappedStatic('off', {
         on: PD.Group({
-            kernelSize: PD.Numeric(4, { min: 1, max: 32, step: 1 }),
-            bias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
-            radius: PD.Numeric(64, { min: 0, max: 256, step: 1 }),
+            samples: PD.Numeric(64, {min: 1, max: 256, step: 1}),
+            radius: PD.Numeric(8.0, { min: 1, max: 64, step: 1 }),
+            bias: PD.Numeric(1.0, { min: 0, max: 1, step: 0.001 }),
+            blurKernelSize: PD.Numeric(13, { min: 1, max: 25, step: 2 }),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
     outline: PD.MappedStatic('off', {
         on: PD.Group({
-            scale: PD.Numeric(1, { min: 0, max: 10, step: 1 }),
-            threshold: PD.Numeric(0.8, { min: 0, max: 5, step: 0.01 }),
+            scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
+            threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Draw outline around 3D objects' }),
@@ -107,44 +265,181 @@ export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
 export class PostprocessingPass {
     static isEnabled(props: PostprocessingProps) {
-        return props.occlusion.name === 'on' || props.outline.name === 'on' || props.antialiasing.name !== 'off';
+        return props.occlusion.name === 'on' || props.outline.name === 'on';
     }
 
     readonly target: RenderTarget
 
-    private readonly tmpTarget: RenderTarget
+    private readonly outlinesTarget: RenderTarget
+    private readonly outlinesRenderable: OutlinesRenderable
+
+    private readonly randomHemisphereVector: Vec3[]
+    private readonly ssaoFramebuffer: Framebuffer
+    private readonly ssaoBlurFirstPassFramebuffer: Framebuffer
+    private readonly ssaoBlurSecondPassFramebuffer: Framebuffer
+
+    private readonly ssaoDepthTexture: Texture
+    private readonly ssaoDepthBlurProxyTexture: Texture
+
+    private readonly ssaoRenderable: SsaoRenderable
+    private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable
+    private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable
+
+    private nSamples: number
+    private blurKernelSize: number
+
     private readonly renderable: PostprocessingRenderable
-    private readonly fxaa: FxaaPass
-    private readonly smaa: SmaaPass
 
-    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
-        const { colorTarget, depthTexture } = drawPass;
+    constructor(private webgl: WebGLContext, drawPass: DrawPass) {
+        const { colorTarget, depthTexture, packedDepth } = drawPass;
         const width = colorTarget.getWidth();
         const height = colorTarget.getHeight();
 
-        this.target = webgl.createRenderTarget(width, height, false);
-        this.tmpTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
-        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture);
-        this.fxaa = new FxaaPass(webgl, this.tmpTarget.texture);
-        this.smaa = new SmaaPass(webgl, this.tmpTarget.texture);
-    }
+        this.nSamples = 1;
+        this.blurKernelSize = 1;
+
+        this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
+
+        this.outlinesTarget = webgl.createRenderTarget(width, height, false);
+        this.outlinesRenderable = getOutlinesRenderable(webgl, depthTexture);
+
+        this.randomHemisphereVector = [];
+        for (let i = 0; i < 256; i++) {
+            let v = Vec3();
+            v[0] = Math.random() * 2.0 - 1.0;
+            v[1] = Math.random() * 2.0 - 1.0;
+            v[2] = Math.random();
+            Vec3.normalize(v, v);
+            Vec3.scale(v, v, Math.random());
+            this.randomHemisphereVector.push(v);
+        }
+        this.ssaoFramebuffer = webgl.resources.framebuffer();
+        this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
+        this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
 
-    syncSize() {
-        const width = this.drawPass.colorTarget.getWidth();
-        const height = this.drawPass.colorTarget.getHeight();
+        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        this.ssaoDepthTexture.define(width, height);
+        this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
 
+        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        this.ssaoDepthBlurProxyTexture.define(width, height);
+        this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
+
+        this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
+
+        this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture);
+        this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
+        this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
+        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture,  depthTexture, packedDepth, this.outlinesTarget.texture, this.ssaoDepthTexture);
+    }
+
+    setSize(width: number, height: number) {
         const [w, h] = this.renderable.values.uTexSize.ref.value;
         if (width !== w || height !== h) {
             this.target.setSize(width, height);
-            this.tmpTarget.setSize(width, height);
-            ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
-            this.fxaa.setSize(width, height);
+            this.outlinesTarget.setSize(width, height);
+            this.ssaoDepthTexture.define(width, height);
+            this.ssaoDepthBlurProxyTexture.define(width, height);
 
-            if (this.smaa.supported) this.smaa.setSize(width, height);
+            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, width, height));
+            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, width, height));
         }
     }
 
-    private updateState(camera: ICamera) {
+    private updateState(camera: ICamera, backgroundColor: Color, props: PostprocessingProps) {
+        let needsUpdateMain = false;
+        let needsUpdateSsao = false;
+        let needsUpdateSsaoBlur = false;
+
+        let orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
+        let outlinesEnabled = props.outline.name === 'on';
+        let occlusionEnabled = props.occlusion.name === 'on';
+
+        let invProjection = Mat4.identity();
+        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.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
+            ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
+
+            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; }
+            ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
+            ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
+
+            if (this.nSamples !== props.occlusion.params.samples) {
+                needsUpdateSsao = true;
+
+                this.nSamples = props.occlusion.params.samples;
+                ValueCell.updateIfChanged(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
+                ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
+            }
+            ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, props.occlusion.params.radius);
+            ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
+
+            if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
+                needsUpdateSsaoBlur = true;
+
+                this.blurKernelSize = props.occlusion.params.blurKernelSize;
+                let kernel = getBlurKernel(this.blurKernelSize);
+
+                ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
+                ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
+                ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
+                ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
+            }
+
+        }
+
+        if (props.outline.name === 'on') {
+            let factor = Math.pow(1000, props.outline.params.threshold) / 1000;
+            let maxPossibleViewZDiff = factor * (camera.far - camera.near);
+
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
+
+            ValueCell.updateIfChanged(this.renderable.values.uInvProjection, invProjection);
+            ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
+            let fogColor = Vec3();
+            Color.toVec3Normalized(fogColor, backgroundColor);
+            ValueCell.updateIfChanged(this.renderable.values.uFogColor, fogColor);
+            ValueCell.updateIfChanged(this.renderable.values.uOutlineScale, props.outline.params.scale - 1);
+            ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, props.outline.params.threshold);
+        }
+
+        ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
+        ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
+        ValueCell.updateIfChanged(this.renderable.values.uFogFar, camera.fogFar);
+        ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
+        if (this.renderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateMain = true; }
+        ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
+        if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) { needsUpdateMain = true; }
+        ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, outlinesEnabled);
+        if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
+        ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
+
+        if (needsUpdateSsao) {
+            this.ssaoRenderable.update();
+        }
+
+        if (needsUpdateSsaoBlur) {
+            this.ssaoBlurFirstPassRenderable.update();
+            this.ssaoBlurSecondPassRenderable.update();
+        }
+
+        if (needsUpdateMain) {
+            this.renderable.update();
+        }
+
         const { gl, state } = this.webgl;
 
         state.enable(gl.SCISSOR_TEST);
@@ -155,65 +450,91 @@ export class PostprocessingPass {
         const { x, y, width, height } = camera.viewport;
         gl.viewport(x, y, width, height);
         gl.scissor(x, y, width, height);
-
-        state.clearColor(0, 0, 0, 1);
-        gl.clear(gl.COLOR_BUFFER_BIT);
     }
 
-    private _renderPostprocessing(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
-        const { values } = this.renderable;
+    render(camera: ICamera, toDrawingBuffer: boolean, backgroundColor: Color, props: PostprocessingProps) {
+        this.updateState(camera, backgroundColor, props);
 
-        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);
+        if (props.outline.name === 'on') {
+            this.outlinesTarget.bind();
+            this.outlinesRenderable.render();
+        }
 
-        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);
-        }
+            this.ssaoFramebuffer.bind();
+            this.ssaoRenderable.render();
 
-        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);
-        }
+            this.ssaoBlurFirstPassFramebuffer.bind();
+            this.ssaoBlurFirstPassRenderable.render();
 
-        if (needsUpdate) {
-            this.renderable.update();
+            this.ssaoBlurSecondPassFramebuffer.bind();
+            this.ssaoBlurSecondPassRenderable.render();
         }
 
-        if (props.antialiasing.name !== 'off') {
-            this.tmpTarget.bind();
-        } else if (toDrawingBuffer) {
+        if (toDrawingBuffer) {
             this.webgl.unbindFramebuffer();
         } else {
             this.target.bind();
         }
 
-        this.updateState(camera);
+        const { gl, state } = this.webgl;
+        state.clearColor(0, 0, 0, 1);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
         this.renderable.render();
     }
+}
+
+export class AntialiasingPass {
+    static isEnabled(props: PostprocessingProps) {
+        return props.antialiasing.name !== 'off';
+    }
+
+    readonly target: RenderTarget
+    private readonly fxaa: FxaaPass
+    private readonly smaa: SmaaPass
+
+    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
+        const { colorTarget } = drawPass;
+        const width = colorTarget.getWidth();
+        const height = colorTarget.getHeight();
+
+        this.target = webgl.createRenderTarget(width, height, false);
+        this.fxaa = new FxaaPass(webgl, this.target.texture);
+        this.smaa = new SmaaPass(webgl, this.target.texture);
+    }
+
+    setSize(width: number, height: number) {
+        const [w, h] = [this.target.texture.getWidth(), this.target.texture.getHeight()];
+        if (width !== w || height !== h) {
+            this.target.setSize(width, height);
+            this.fxaa.setSize(width, height);
+            if (this.smaa.supported) this.smaa.setSize(width, height);
+        }
+    }
+
+    private updateState(camera: ICamera) {
+        const { gl, state } = this.webgl;
+
+        state.enable(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);
+
+        state.clearColor(0, 0, 0, 1);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+    }
 
     private _renderFxaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
         if (props.antialiasing.name !== 'fxaa') return;
 
-        const input = (props.occlusion.name === 'on' || props.outline.name === 'on')
-            ? this.tmpTarget.texture : this.drawPass.colorTarget.texture;
+        const input = PostprocessingPass.isEnabled(props)
+            ? this.drawPass.postprocessing.target.texture
+            : this.drawPass.colorTarget.texture;
         this.fxaa.update(input, props.antialiasing.params);
 
         if (toDrawingBuffer) {
@@ -229,17 +550,16 @@ export class PostprocessingPass {
     private _renderSmaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
         if (props.antialiasing.name !== 'smaa') return;
 
-        const input = (props.occlusion.name === 'on' || props.outline.name === 'on')
-            ? this.tmpTarget.texture : this.drawPass.colorTarget.texture;
+        const input = PostprocessingPass.isEnabled(props)
+            ? this.drawPass.postprocessing.target.texture
+            : this.drawPass.colorTarget.texture;
         this.smaa.update(input, props.antialiasing.params);
 
         this.smaa.render(camera.viewport, toDrawingBuffer ? undefined : this.target);
     }
 
-    private _render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
-        if (props.occlusion.name === 'on' || props.outline.name === 'on' || props.antialiasing.name === 'off') {
-            this._renderPostprocessing(camera, toDrawingBuffer, props);
-        }
+    render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        if (props.antialiasing.name === 'off') return;
 
         if (props.antialiasing.name === 'fxaa') {
             this._renderFxaa(camera, toDrawingBuffer, props);
@@ -250,14 +570,5 @@ export class PostprocessingPass {
             this._renderSmaa(camera, toDrawingBuffer, props);
         }
     }
-
-    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);
-        }
-    }
 }
 
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index d5fc2ef5297417682d8aee12d8a996d7c3669df8..88da0af16e68f83d3987883cb989b857c6631332 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -20,6 +20,7 @@ import { stringToWords } from '../mol-util/string';
 import { degToRad } from '../mol-math/misc';
 import { createNullTexture, Texture, Textures } from './webgl/texture';
 import { arrayMapUpsert } from '../mol-util/array';
+import { clamp } from '../mol-math/interpolate';
 
 export interface RendererStats {
     programCount: number
@@ -50,7 +51,8 @@ interface Renderer {
     renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
-    renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlendedVolumeOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlendedVolumeTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderWboitOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderWboitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
 
@@ -451,7 +453,7 @@ namespace Renderer {
             }
         };
 
-        const renderBlendedVolume = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+        const renderBlendedVolumeOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
             state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
             state.enable(gl.BLEND);
 
@@ -460,7 +462,32 @@ namespace Renderer {
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
-                renderObject(r, 'colorBlended');
+
+                // TODO: simplify, handle on renderable.state???
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && !r.values.dXrayShaded?.ref.value) {
+                    renderObject(r, 'colorBlended');
+                }
+            }
+        };
+
+        const renderBlendedVolumeTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+            state.enable(gl.BLEND);
+
+            updateInternal(group, camera, depthTexture, false);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+
+                // TODO: simplify, handle on renderable.state???
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dXrayShaded?.ref.value) {
+                    renderObject(r, 'colorBlended');
+                }
             }
         };
 
@@ -474,8 +501,11 @@ namespace Renderer {
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
+
                 // TODO: simplify, handle on renderable.state???
-                if (r.values.uAlpha.ref.value === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dRenderMode?.ref.value !== 'volume' && !r.values.dPointFilledCircle?.ref.value && !r.values.dXrayShaded?.ref.value) {
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dRenderMode?.ref.value !== 'volume' && !r.values.dPointFilledCircle?.ref.value && !r.values.dXrayShaded?.ref.value) {
                     renderObject(r, 'colorWboit');
                 }
             }
@@ -487,8 +517,11 @@ namespace Renderer {
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
+
                 // TODO: simplify, handle on renderable.state???
-                if (r.values.uAlpha.ref.value < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dRenderMode?.ref.value === 'volume' || r.values.dPointFilledCircle?.ref.value || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dRenderMode?.ref.value === 'volume' || r.values.dPointFilledCircle?.ref.value || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
                     renderObject(r, 'colorWboit');
                 }
             }
@@ -523,7 +556,8 @@ namespace Renderer {
             renderBlended,
             renderBlendedOpaque,
             renderBlendedTransparent,
-            renderBlendedVolume,
+            renderBlendedVolumeOpaque,
+            renderBlendedVolumeTransparent,
             renderWboitOpaque,
             renderWboitTransparent,
 
diff --git a/src/mol-gl/shader/chunks/common.glsl.ts b/src/mol-gl/shader/chunks/common.glsl.ts
index 710e8779fb0938206849f7be11191c8e067b31e6..077baa036bd8bf81b3b565c9a187a5b4f09e7580 100644
--- a/src/mol-gl/shader/chunks/common.glsl.ts
+++ b/src/mol-gl/shader/chunks/common.glsl.ts
@@ -49,6 +49,25 @@ float decodeFloatRGB(const in vec3 rgb) {
     return (rgb.r * 256.0 * 256.0 * 255.0 + rgb.g * 256.0 * 255.0 + rgb.b * 255.0) - 1.0;
 }
 
+vec2 packUnitIntervalToRG(const in float v) {
+    vec2 enc;
+    enc.xy = vec2(fract(v * 256.0), v);
+    enc.y -= enc.x * (1.0 / 256.0);
+    enc.xy *=  256.0 / 255.0;
+
+    return enc;
+}
+
+float unpackRGToUnitInterval(const in vec2 enc) {
+    return dot(enc, vec2(255.0 / (256.0 * 256.0), 255.0 / 256.0));
+}
+
+vec3 screenSpaceToViewSpace(const in vec3 ssPos, const in mat4 invProjection) {
+    vec4 p = vec4(ssPos * 2.0 - 1.0, 1.0);
+    p = invProjection * p;
+    return p.xyz / p.w;
+}
+
 const float PackUpscale = 256.0 / 255.0; // fraction -> 0..1 (including 1)
 const float UnpackDownscale = 255.0 / 256.0; // 0..1 -> fraction (excluding 1)
 const vec3 PackFactors = vec3(256.0 * 256.0 * 256.0, 256.0 * 256.0,  256.0);
diff --git a/src/mol-gl/shader/chunks/light-frag-params.glsl.ts b/src/mol-gl/shader/chunks/light-frag-params.glsl.ts
index 7c0a731c744a75d766e7d717b1533b19febeb3fb..7f6f25493f26153be4515feba58d6cc22efab6b3 100644
--- a/src/mol-gl/shader/chunks/light-frag-params.glsl.ts
+++ b/src/mol-gl/shader/chunks/light-frag-params.glsl.ts
@@ -15,97 +15,97 @@ uniform float uMetalness;
 uniform float uRoughness;
 
 struct PhysicalMaterial {
-	vec3 diffuseColor;
-	float specularRoughness;
-	vec3 specularColor;
+    vec3 diffuseColor;
+    float specularRoughness;
+    vec3 specularColor;
 };
 
 struct IncidentLight {
-	vec3 color;
-	vec3 direction;
+    vec3 color;
+    vec3 direction;
 };
 
 struct ReflectedLight {
-	vec3 directDiffuse;
-	vec3 directSpecular;
-	vec3 indirectDiffuse;
+    vec3 directDiffuse;
+    vec3 directSpecular;
+    vec3 indirectDiffuse;
 };
 
 struct GeometricContext {
-	vec3 position;
-	vec3 normal;
-	vec3 viewDir;
+    vec3 position;
+    vec3 normal;
+    vec3 viewDir;
 };
 
 vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {
-	// Original approximation by Christophe Schlick '94
-	// float fresnel = pow( 1.0 - dotLH, 5.0 );
-	// Optimized variant (presented by Epic at SIGGRAPH '13)
-	// https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
-	float fresnel = exp2((-5.55473 * dotLH - 6.98316) * dotLH);
-	return (1.0 - specularColor) * fresnel + specularColor;
+    // Original approximation by Christophe Schlick '94
+    // float fresnel = pow( 1.0 - dotLH, 5.0 );
+    // Optimized variant (presented by Epic at SIGGRAPH '13)
+    // https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
+    float fresnel = exp2((-5.55473 * dotLH - 6.98316) * dotLH);
+    return (1.0 - specularColor) * fresnel + specularColor;
 }
 
 // Moving Frostbite to Physically Based Rendering 3.0 - page 12, listing 2
 // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
 float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {
-	float a2 = pow2(alpha);
-	// dotNL and dotNV are explicitly swapped. This is not a mistake.
-	float gv = dotNL * sqrt(a2 + (1.0 - a2) * pow2(dotNV));
-	float gl = dotNV * sqrt(a2 + (1.0 - a2) * pow2(dotNL));
-	return 0.5 / max(gv + gl, EPSILON);
+    float a2 = pow2(alpha);
+    // dotNL and dotNV are explicitly swapped. This is not a mistake.
+    float gv = dotNL * sqrt(a2 + (1.0 - a2) * pow2(dotNV));
+    float gl = dotNV * sqrt(a2 + (1.0 - a2) * pow2(dotNL));
+    return 0.5 / max(gv + gl, EPSILON);
 }
 
 // Microfacet Models for Refraction through Rough Surfaces - equation (33)
 // http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
 // alpha is "roughness squared" in Disney’s reparameterization
 float D_GGX(const in float alpha, const in float dotNH) {
-	float a2 = pow2(alpha);
-	float denom = pow2(dotNH) * (a2 - 1.0) + 1.0; // avoid alpha = 0 with dotNH = 1
-	return RECIPROCAL_PI * a2 / pow2(denom);
+    float a2 = pow2(alpha);
+    float denom = pow2(dotNH) * (a2 - 1.0) + 1.0; // avoid alpha = 0 with dotNH = 1
+    return RECIPROCAL_PI * a2 / pow2(denom);
 }
 
 vec3 BRDF_Diffuse_Lambert(const in vec3 diffuseColor) {
-	return RECIPROCAL_PI * diffuseColor;
+    return RECIPROCAL_PI * diffuseColor;
 }
 
 // GGX Distribution, Schlick Fresnel, GGX-Smith Visibility
 vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float roughness) {
-	float alpha = pow2(roughness); // UE4's roughness
-	vec3 halfDir = normalize(incidentLight.direction + geometry.viewDir);
-
-	float dotNL = saturate(dot(geometry.normal, incidentLight.direction));
-	float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
-	float dotNH = saturate(dot(geometry.normal, halfDir));
-	float dotLH = saturate(dot(incidentLight.direction, halfDir));
-
-	vec3 F = F_Schlick(specularColor, dotLH);
-	float G = G_GGX_SmithCorrelated(alpha, dotNL, dotNV);
-	float D = D_GGX(alpha, dotNH);
-	return F * (G * D);
+    float alpha = pow2(roughness); // UE4's roughness
+    vec3 halfDir = normalize(incidentLight.direction + geometry.viewDir);
+
+    float dotNL = saturate(dot(geometry.normal, incidentLight.direction));
+    float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
+    float dotNH = saturate(dot(geometry.normal, halfDir));
+    float dotLH = saturate(dot(incidentLight.direction, halfDir));
+
+    vec3 F = F_Schlick(specularColor, dotLH);
+    float G = G_GGX_SmithCorrelated(alpha, dotNL, dotNV);
+    float D = D_GGX(alpha, dotNH);
+    return F * (G * D);
 }
 
 // ref: https://www.unrealengine.com/blog/physically-based-shading-on-mobile - environmentBRDF for GGX on mobile
 vec3 BRDF_Specular_GGX_Environment(const in GeometricContext geometry, const in vec3 specularColor, const in float roughness) {
-	float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
-	const vec4 c0 = vec4(-1, -0.0275, -0.572, 0.022);
-	const vec4 c1 = vec4(1, 0.0425, 1.04, -0.04);
-	vec4 r = roughness * c0 + c1;
-	float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;
-	vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
-	return specularColor * AB.x + AB.y;
+    float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
+    const vec4 c0 = vec4(-1, -0.0275, -0.572, 0.022);
+    const vec4 c1 = vec4(1, 0.0425, 1.04, -0.04);
+    vec4 r = roughness * c0 + c1;
+    float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;
+    vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
+    return specularColor * AB.x + AB.y;
 }
 
 void RE_Direct_Physical(const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {
-	float dotNL = saturate(dot(geometry.normal, directLight.direction));
+    float dotNL = saturate(dot(geometry.normal, directLight.direction));
     vec3 irradiance = dotNL * directLight.color;
-	irradiance *= PI; // punctual light
+    irradiance *= PI; // punctual light
 
-	reflectedLight.directSpecular += irradiance * BRDF_Specular_GGX(directLight, geometry, material.specularColor, material.specularRoughness);
-	reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
+    reflectedLight.directSpecular += irradiance * BRDF_Specular_GGX(directLight, geometry, material.specularColor, material.specularRoughness);
+    reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
 }
 
 void RE_IndirectDiffuse_Physical(const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {
-	reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
+    reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
 }
 `;
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/wboit-write.glsl.ts b/src/mol-gl/shader/chunks/wboit-write.glsl.ts
index 644fd8665ab3514f539a6e323781d3176491d220..92c5d4ddffe13fccfebf58ff8e9d3cd1091c6090 100644
--- a/src/mol-gl/shader/chunks/wboit-write.glsl.ts
+++ b/src/mol-gl/shader/chunks/wboit-write.glsl.ts
@@ -1,3 +1,10 @@
+/**
+ * Copyright (c) 2019-2020 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>
+ */
+
 export default `
 #if defined(dRenderVariant_colorWboit)
     if (!uRenderWboit) {
diff --git a/src/mol-gl/shader/copy-fbo.frag.ts b/src/mol-gl/shader/copy-fbo.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e0200bb182e8e17771baee888c83f5a644bc65ee
--- /dev/null
+++ b/src/mol-gl/shader/copy-fbo.frag.ts
@@ -0,0 +1,20 @@
+export default `
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tColor;
+uniform sampler2D tDepth;
+uniform vec2 uTexSize;
+
+#include common
+
+float getDepth(const in vec2 coords) {
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
+}
+
+void main() {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    gl_FragColor = texture2D(tColor, coords);
+    gl_FragDepthEXT = getDepth(coords);
+}
+`;
\ No newline at end of file
diff --git a/src/mol-gl/shader/outlines.frag.ts b/src/mol-gl/shader/outlines.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19f91fc381dedd8c9da325303f81f43b1bc2d048
--- /dev/null
+++ b/src/mol-gl/shader/outlines.frag.ts
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+uniform sampler2D tDepth;
+uniform vec2 uTexSize;
+
+uniform float uNear;
+uniform float uFar;
+
+uniform float uMaxPossibleViewZDiff;
+
+#include common
+
+float perspectiveDepthToViewZ(const in float invClipZ, const in float near, const in float far) {
+    return (near * far) / ((far - near) * invClipZ - far);
+}
+
+float orthographicDepthToViewZ(const in float linearClipZ, const in float near, const in float far) {
+    return linearClipZ * (near - far) - near;
+}
+
+float getViewZ(const in float depth) {
+    #if dOrthographic == 1
+        return orthographicDepthToViewZ(depth, uNear, uFar);
+    #else
+        return perspectiveDepthToViewZ(depth, uNear, uFar);
+    #endif
+}
+
+float getDepth(const in vec2 coords) {
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
+}
+
+bool isBackground(const in float depth) {
+    return depth >= 0.99;
+}
+
+void main(void) {
+    float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
+    
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    vec2 invTexSize = 1.0 / uTexSize;
+
+    float selfDepth = getDepth(coords);
+    float selfViewZ = isBackground(selfDepth) ? backgroundViewZ : getViewZ(getDepth(coords));
+
+    float outline = 1.0;
+    float bestDepth = 1.0;
+
+    for (int y = -1; y <= 1; y++) {
+        for (int x = -1; x <= 1; x++) {
+            vec2 sampleCoords = coords + vec2(float(x), float(y)) * invTexSize;
+            float sampleDepth = getDepth(sampleCoords);
+            float sampleViewZ = isBackground(sampleDepth) ? backgroundViewZ : getViewZ(sampleDepth);
+
+            if (abs(selfViewZ - sampleViewZ) > uMaxPossibleViewZDiff && selfDepth > sampleDepth && sampleDepth <= bestDepth) {
+                outline = 0.0;
+                bestDepth = sampleDepth;
+            }
+        }
+    }
+
+    gl_FragColor = vec4(outline, packUnitIntervalToRG(bestDepth), 0.0);
+}
+`;
\ No newline at end of file
diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts
index 04d2864cfa23fb87280c970c2ca764a13db04064..ecc9e5153c782e992ac7b5f3406ec09f69b7db50 100644
--- a/src/mol-gl/shader/postprocessing.frag.ts
+++ b/src/mol-gl/shader/postprocessing.frag.ts
@@ -1,12 +1,22 @@
+/**
+ * Copyright (c) 2019-2020 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>
+ */
+
 export default `
 precision highp float;
 precision highp int;
 precision highp sampler2D;
 
+uniform sampler2D tSsaoDepth;
 uniform sampler2D tColor;
-uniform sampler2D tPackedDepth;
+uniform sampler2D tDepth;
+uniform sampler2D tOutlines;
 uniform vec2 uTexSize;
 
+uniform mat4 uInvProjection;
 uniform float uNear;
 uniform float uFar;
 uniform float uFogNear;
@@ -19,21 +29,12 @@ uniform float uOcclusionRadius;
 uniform float uOutlineScale;
 uniform float uOutlineThreshold;
 
-const float noiseAmount = 0.0002;
+uniform float uMaxPossibleViewZDiff;
+
 const vec4 occlusionColor = vec4(0.0, 0.0, 0.0, 1.0);
 
 #include common
 
-float noise(const in vec2 coords) {
-    float a = 12.9898;
-    float b = 78.233;
-    float c = 43758.5453;
-    float dt = dot(coords, vec2(a,b));
-    float sn = mod(dt, 3.14159);
-
-    return fract(sin(sn) * c);
-}
-
 float perspectiveDepthToViewZ(const in float invClipZ, const in float near, const in float far) {
     return (near * far) / ((far - near) * invClipZ - far);
 }
@@ -51,68 +52,80 @@ float getViewZ(const in float depth) {
 }
 
 float getDepth(const in vec2 coords) {
-    return unpackRGBAToDepth(texture2D(tPackedDepth, coords));
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
 }
 
-float calcSSAO(const in vec2 coords, const in float depth) {
-    float occlusionFactor = 0.0;
+bool isBackground(const in float depth) {
+    return depth >= 0.99;
+}
+
+float getOutline(const in vec2 coords, out float closestTexel) {
+    float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
+    vec2 invTexSize = 1.0 / uTexSize;
+
+    float selfDepth = getDepth(coords);
+    float selfViewZ = isBackground(selfDepth) ? backgroundViewZ : getViewZ(getDepth(coords));
 
-    for (int i = -dOcclusionKernelSize; i <= dOcclusionKernelSize; i++) {
-        for (int j = -dOcclusionKernelSize; j <= dOcclusionKernelSize; j++) {
-            vec2 coordsDelta = coords + uOcclusionRadius / float(dOcclusionKernelSize) * vec2(float(i) / uTexSize.x, float(j) / uTexSize.y);
-            coordsDelta += noiseAmount * (noise(coordsDelta) - 0.5) / uTexSize;
-            coordsDelta = clamp(coordsDelta, 0.5 / uTexSize, 1.0 - 1.0 / uTexSize);
-            if (getDepth(coordsDelta) < depth) occlusionFactor += 1.0;
+    float outline = 1.0;
+    closestTexel = backgroundViewZ;
+    for (float y = -uOutlineScale; y <= uOutlineScale; y++) {
+        for (float x = -uOutlineScale; x <= uOutlineScale; x++) {
+            if (x * x + y * y > uOutlineScale * uOutlineScale) {
+                continue;
+            }
+
+            vec2 sampleCoords = coords + vec2(x, y) * invTexSize;
+
+            vec4 sampleOutlineCombined = texture2D(tOutlines, sampleCoords);
+            float sampleOutline = sampleOutlineCombined.r;
+            float sampleOutlineDepth = unpackRGToUnitInterval(sampleOutlineCombined.gb);
+
+            float sampleOutlineViewDirLength = length(screenSpaceToViewSpace(vec3(sampleCoords, sampleOutlineDepth), uInvProjection));
+
+            if (sampleOutline == 0.0 && sampleOutlineViewDirLength < closestTexel && abs(selfViewZ - sampleOutlineDepth) > uMaxPossibleViewZDiff) {
+                outline = 0.0;
+                closestTexel = sampleOutlineDepth;
+            }
         }
     }
-
-    return occlusionFactor / float((2 * dOcclusionKernelSize + 1) * (2 * dOcclusionKernelSize + 1));
+    return outline;
 }
 
-vec2 calcEdgeDepth(const in vec2 coords) {
-    vec2 invTexSize = 1.0 / uTexSize;
-    float halfScaleFloor = floor(uOutlineScale * 0.5);
-    float halfScaleCeil = ceil(uOutlineScale * 0.5);
-
-    vec2 bottomLeftUV = coords - invTexSize * halfScaleFloor;
-    vec2 topRightUV = coords + invTexSize * halfScaleCeil;
-    vec2 bottomRightUV = coords + vec2(invTexSize.x * halfScaleCeil, -invTexSize.y * halfScaleFloor);
-    vec2 topLeftUV = coords + vec2(-invTexSize.x * halfScaleFloor, invTexSize.y * halfScaleCeil);
-
-    float depth0 = getDepth(bottomLeftUV);
-    float depth1 = getDepth(topRightUV);
-    float depth2 = getDepth(bottomRightUV);
-    float depth3 = getDepth(topLeftUV);
-
-    float depthFiniteDifference0 = depth1 - depth0;
-    float depthFiniteDifference1 = depth3 - depth2;
-
-    return vec2(
-        sqrt(pow(depthFiniteDifference0, 2.0) + pow(depthFiniteDifference1, 2.0)) * 100.0,
-        min(depth0, min(depth1, min(depth2, depth3)))
-    );
+float getSsao(vec2 coords) {
+    float rawSsao = unpackRGToUnitInterval(texture(tSsaoDepth, coords).xy);
+    if (rawSsao > 0.999) {
+        return 1.0;
+    } else if (rawSsao > 0.001) {
+        return rawSsao;
+    }
+    return 0.0;
 }
 
 void main(void) {
     vec2 coords = gl_FragCoord.xy / uTexSize;
-    vec4 color = texture2D(tColor, coords);
+    vec4 color = texture(tColor, coords);
 
     #ifdef dOutlineEnable
-        vec2 edgeDepth = calcEdgeDepth(coords);
-        float edgeFlag = step(edgeDepth.x, uOutlineThreshold);
-        color.rgb *= edgeFlag;
-
-        float viewDist = abs(getViewZ(edgeDepth.y));
-        float fogFactor = smoothstep(uFogNear, uFogFar, viewDist) * (1.0 - edgeFlag);
-        color.rgb = mix(color.rgb, uFogColor, fogFactor);
+        float closestTexel;
+        float outline = getOutline(coords, closestTexel);
+
+        if (outline == 0.0) {
+            color.rgb *= outline;
+            float viewDist = abs(getViewZ(closestTexel));
+            float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
+            if (color.a != 1.0) {
+                color.a = 1.0 - fogFactor;
+            }
+            color.rgb = mix(color.rgb, uFogColor, fogFactor);
+        }
     #endif
 
     // occlusion needs to be handled after outline to darken them properly
     #ifdef dOcclusionEnable
         float depth = getDepth(coords);
-        if (depth <= 0.99) {
-            float occlusionFactor = calcSSAO(coords, depth);
-            color = mix(color, occlusionColor, uOcclusionBias * occlusionFactor);
+        if (!isBackground(depth)) {
+            float occlusionFactor = getSsao(coords);
+            color = mix(occlusionColor, color, occlusionFactor);
         }
     #endif
 
diff --git a/src/mol-gl/shader/ssao-blur.frag.ts b/src/mol-gl/shader/ssao-blur.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ace3593d5f54e0036c3b8735ed6fa63edbae18b7
--- /dev/null
+++ b/src/mol-gl/shader/ssao-blur.frag.ts
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+uniform sampler2D tSsaoDepth;
+uniform vec2 uTexSize;
+
+uniform float uKernel[dOcclusionKernelSize];
+
+uniform float uBlurDirectionX;
+uniform float uBlurDirectionY;
+
+uniform float uMaxPossibleViewZDiff;
+
+uniform float uNear;
+uniform float uFar;
+
+#include common
+
+float perspectiveDepthToViewZ(const in float invClipZ, const in float near, const in float far) {
+    return (near * far) / ((far - near) * invClipZ - far);
+}
+
+float orthographicDepthToViewZ(const in float linearClipZ, const in float near, const in float far) {
+    return linearClipZ * (near - far) - near;
+}
+
+float getViewZ(const in float depth) {
+    #if dOrthographic == 1
+        return orthographicDepthToViewZ(depth, uNear, uFar);
+    #else
+        return perspectiveDepthToViewZ(depth, uNear, uFar);
+    #endif
+}
+
+bool isBackground(const in float depth) {
+    return depth >= 0.99;
+}
+
+void main(void) {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+
+    vec2 packedDepth = texture(tSsaoDepth, coords).zw;
+
+    float selfDepth = unpackRGToUnitInterval(packedDepth);
+    // if background and if second pass
+    if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
+       gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
+       return;
+    }
+
+    float selfViewZ = getViewZ(selfDepth);
+
+    vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
+
+    float sum = 0.0;
+    float kernelSum = 0.0;
+    // only if kernelSize is odd
+    for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
+        vec2 sampleCoords = coords + float(i) * offset;
+
+        vec4 sampleSsaoDepth = texture(tSsaoDepth, sampleCoords);
+
+        float sampleDepth = unpackRGToUnitInterval(sampleSsaoDepth.zw);
+        if (isBackground(sampleDepth)) {
+            continue;
+        }
+
+        if (abs(i) > 1) {
+            float sampleViewZ = getViewZ(sampleDepth);
+            if (abs(selfViewZ - sampleViewZ) > uMaxPossibleViewZDiff) {
+                continue;
+            }
+        }
+
+        float kernel = uKernel[abs(i)];
+        float sampleValue = unpackRGToUnitInterval(sampleSsaoDepth.xy);
+
+        sum += kernel * sampleValue;
+        kernelSum += kernel;
+    }
+    
+    gl_FragColor = vec4(packUnitIntervalToRG(sum / kernelSum), packedDepth);
+}
+`;
\ No newline at end of file
diff --git a/src/mol-gl/shader/ssao.frag.ts b/src/mol-gl/shader/ssao.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ef74f9d5d7c8b87fbc10a739a9eaaed488de4ac5
--- /dev/null
+++ b/src/mol-gl/shader/ssao.frag.ts
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2019-2020 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>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+#include common
+
+uniform sampler2D tDepth;
+
+uniform vec3 uSamples[dNSamples];
+
+uniform mat4 uProjection;
+uniform mat4 uInvProjection;
+
+uniform vec2 uTexSize;
+
+uniform float uRadius;
+uniform float uBias;
+
+float smootherstep(float edge0, float edge1, float x) {
+    x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+    return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+float noise(const in vec2 coords) {
+    float a = 12.9898;
+    float b = 78.233;
+    float c = 43758.5453;
+    float dt = dot(coords, vec2(a,b));
+    float sn = mod(dt, PI);
+    return abs(fract(sin(sn) * c)); // is abs necessary?
+}
+
+vec2 getNoiseVec2(const in vec2 coords) {
+    return vec2(noise(coords), noise(coords + vec2(PI, 2.71828)));
+}
+
+bool isBackground(const in float depth) {
+    return depth >= 0.99;
+}
+
+float getDepth(const in vec2 coords) {
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
+}
+
+vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {
+    vec3 p1 = vec3(offset1, depth1 - depth);
+    vec3 p2 = vec3(offset2, depth2 - depth);
+    
+    vec3 normal = cross(p1, p2);
+    normal.z = -normal.z;
+    
+    return normalize(normal);
+}
+
+// StarCraft II Ambient Occlusion by [Filion and McNaughton 2008]
+void main(void) {
+    vec2 invTexSize = 1.0 / uTexSize;
+    vec2 selfCoords = gl_FragCoord.xy * invTexSize;
+
+    float selfDepth = getDepth(selfCoords);
+    vec2 selfPackedDepth = packUnitIntervalToRG(selfDepth);
+
+    if (isBackground(selfDepth)) {
+        gl_FragColor = vec4(packUnitIntervalToRG(0.0), selfPackedDepth);
+        return;
+    }
+    
+    vec2 offset1 = vec2(0.0, invTexSize.y);
+    vec2 offset2 = vec2(invTexSize.x, 0.0);
+
+    float selfDepth1 = getDepth(selfCoords + offset1);
+    float selfDepth2 = getDepth(selfCoords + offset2);
+
+    vec3 selfViewNormal = normalFromDepth(selfDepth, selfDepth1, selfDepth2, offset1, offset2);
+    vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
+
+    vec3 randomVec = normalize(vec3(getNoiseVec2(selfCoords) * 2.0 - 1.0, 0.0));
+
+    vec3 tangent = normalize(randomVec - selfViewNormal * dot(randomVec, selfViewNormal));
+    vec3 bitangent = cross(selfViewNormal, tangent);
+    mat3 TBN = mat3(tangent, bitangent, selfViewNormal);
+
+    float occlusion = 0.0;
+    for(int i = 0; i < dNSamples; i++){
+        vec3 sampleViewPos = TBN * uSamples[i];
+        sampleViewPos = selfViewPos + sampleViewPos * uRadius; 
+
+        vec4 offset = vec4(sampleViewPos, 1.0);
+        offset = uProjection * offset;
+        offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
+
+        float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, getDepth(offset.xy)), uInvProjection).z;
+
+        occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));         
+    }
+    occlusion = 1.0 - (uBias * occlusion / float(dNSamples));
+
+    vec2 packedOcclusion = packUnitIntervalToRG(occlusion);
+    
+    gl_FragColor = vec4(packedOcclusion, selfPackedDepth);
+}
+`;
\ No newline at end of file
diff --git a/src/mol-io/reader/3dg/parser.ts b/src/mol-io/reader/3dg/parser.ts
deleted file mode 100644
index 1fc7b6a36b2aa1eb428a38015fcab3f48ad723ce..0000000000000000000000000000000000000000
--- a/src/mol-io/reader/3dg/parser.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { ReaderResult as Result } from '../result';
-import { Task } from '../../../mol-task';
-import { parseCsv } from '../csv/parser';
-import { Column, Table } from '../../../mol-data/db';
-import { toTable } from '../cif/schema';
-
-import Schema = Column.Schema
-import { CsvTable } from '../csv/data-model';
-
-
-export const Schema3DG = {
-    /** Chromosome name */
-    chromosome: Schema.str,
-    /** Base position */
-    position: Schema.int,
-    /** X coordinate */
-    x: Schema.float,
-    /** Y coordinate */
-    y: Schema.float,
-    /** Z coordinate */
-    z: Schema.float,
-};
-export type Schema3DG = typeof Schema3DG
-
-export interface File3DG {
-    table: Table<Schema3DG>
-}
-
-const FieldNames = [ 'chromosome', 'position', 'x', 'y', 'z' ];
-
-function categoryFromTable(name: string, table: CsvTable) {
-    return {
-        name,
-        rowCount: table.rowCount,
-        fieldNames: FieldNames,
-        getField: (name: string) => {
-            return table.getColumn(FieldNames.indexOf(name).toString());
-        }
-    };
-}
-
-export function parse3DG(data: string) {
-    return Task.create<Result<File3DG>>('Parse 3DG', async ctx => {
-        const opts = { quote: '', comment: '#', delimiter: '\t', noColumnNames: true };
-        const csvFile = await parseCsv(data, opts).runInContext(ctx);
-        if (csvFile.isError) return Result.error(csvFile.message, csvFile.line);
-        const category = categoryFromTable('3dg', csvFile.result.table);
-        const table = toTable(Schema3DG, category);
-        return Result.success({ table });
-    });
-}
\ No newline at end of file
diff --git a/src/mol-io/reader/_spec/3dg.spec.ts b/src/mol-io/reader/_spec/3dg.spec.ts
deleted file mode 100644
index 25f4dd4d6bd72261d34f018bdf53c8de8ff9773a..0000000000000000000000000000000000000000
--- a/src/mol-io/reader/_spec/3dg.spec.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { parse3DG } from '../3dg/parser';
-
-const basic3dgString = `1(mat)	1420000	0.791377837067	10.9947291355	-13.1882897693
-1(mat)	1440000	-0.268241283699	10.5200875887	-13.0896257278
-1(mat)	1460000	-1.3853075236	10.5513787498	-13.1440142173
-1(mat)	1480000	-1.55984101733	11.4340829129	-13.6026301209
-1(mat)	1500000	-0.770991778399	11.4758488546	-14.5881137222
-1(mat)	1520000	-0.0848245107875	12.2624690808	-14.354289628
-1(mat)	1540000	-0.458643807046	12.5985791771	-13.4701149287
-1(mat)	1560000	-0.810322906201	12.2461643989	-12.3172933413
-1(mat)	1580000	-2.08211172035	12.8886838656	-12.8742007778
-1(mat)	1600000	-3.52093948201	13.1850935438	-12.4118684428`;
-
-describe('3dg reader', () => {
-    it('basic', async () => {
-        const parsed = await parse3DG(basic3dgString).run();
-        expect(parsed.isError).toBe(false);
-
-        if (parsed.isError) return;
-        const { chromosome, position, x, y, z } = parsed.result.table;
-        expect(chromosome.value(0)).toBe('1(mat)');
-        expect(position.value(1)).toBe(1440000);
-        expect(x.value(5)).toBe(-0.0848245107875);
-        expect(y.value(5)).toBe(12.2624690808);
-        expect(z.value(5)).toBe(-14.354289628);
-    });
-});
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/3dg.ts b/src/mol-model-formats/structure/3dg.ts
deleted file mode 100644
index 18aeab4730f8c4897a90049795fc5136a81784f7..0000000000000000000000000000000000000000
--- a/src/mol-model-formats/structure/3dg.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Task } from '../../mol-task';
-import { ModelFormat } from '../format';
-import { Column, Table } from '../../mol-data/db';
-import { EntityBuilder } from './common/entity';
-import { File3DG } from '../../mol-io/reader/3dg/parser';
-import { fillSerial } from '../../mol-util/array';
-import { MoleculeType } from '../../mol-model/structure/model/types';
-import { BasicSchema, createBasic } from './basic/schema';
-import { createModels } from './basic/parser';
-import { Trajectory } from '../../mol-model/structure';
-
-function getBasic(table: File3DG['table']) {
-    const entityIds = new Array<string>(table._rowCount);
-    const entityBuilder = new EntityBuilder();
-
-    const seqIdStarts = table.position.toArray({ array: Uint32Array });
-    const seqIdEnds = new Uint32Array(table._rowCount);
-    const stride = seqIdStarts[1] - seqIdStarts[0];
-
-    const objectRadius = stride / 3500;
-
-    for (let i = 0, il = table._rowCount; i < il; ++i) {
-        const chr = table.chromosome.value(i);
-        const entityId = entityBuilder.getEntityId(chr, MoleculeType.DNA, chr);
-        entityIds[i] = entityId;
-        seqIdEnds[i] = seqIdStarts[i] + stride - 1;
-    }
-
-    const ihm_sphere_obj_site = Table.ofPartialColumns(BasicSchema.ihm_sphere_obj_site, {
-        id: Column.ofIntArray(fillSerial(new Uint32Array(table._rowCount))),
-        entity_id: Column.ofStringArray(entityIds),
-        seq_id_begin: Column.ofIntArray(seqIdStarts),
-        seq_id_end: Column.ofIntArray(seqIdEnds),
-        asym_id: table.chromosome,
-
-        Cartn_x: Column.ofFloatArray(Column.mapToArray(table.x, x => x * 10, Float32Array)),
-        Cartn_y: Column.ofFloatArray(Column.mapToArray(table.y, y => y * 10, Float32Array)),
-        Cartn_z: Column.ofFloatArray(Column.mapToArray(table.z, z => z * 10, Float32Array)),
-
-        object_radius: Column.ofConst(objectRadius, table._rowCount, Column.Schema.float),
-        rmsf: Column.ofConst(0, table._rowCount, Column.Schema.float),
-        model_id: Column.ofConst(1, table._rowCount, Column.Schema.int),
-    }, table._rowCount);
-
-    return createBasic({
-        entity: entityBuilder.getEntityTable(),
-        ihm_model_list: Table.ofPartialColumns(BasicSchema.ihm_model_list, {
-            model_id: Column.ofIntArray([1]),
-            model_name: Column.ofStringArray(['3DG Model']),
-        }, 1),
-        ihm_sphere_obj_site
-    });
-}
-
-//
-
-export { Format3dg };
-
-type Format3dg = ModelFormat<File3DG>
-
-namespace Format3dg {
-    export function is(x: ModelFormat): x is Format3dg {
-        return x.kind === '3dg';
-    }
-
-    export function from3dg(file3dg: File3DG): Format3dg {
-        return { kind: '3dg', name: '3DG', data: file3dg };
-    }
-}
-
-export function trajectoryFrom3DG(file3dg: File3DG): Task<Trajectory> {
-    return Task.create('Parse 3DG', async ctx => {
-        const format = Format3dg.from3dg(file3dg);
-        const basic = getBasic(file3dg.table);
-        return createModels(basic, format, ctx);
-    });
-}
diff --git a/src/mol-model/structure/export/categories/atom_site.ts b/src/mol-model/structure/export/categories/atom_site.ts
index 3f8354fb354388e6922eafa9cb432b2b93384708..98ab02958a7658f08c13d58f823cb96cbef72911 100644
--- a/src/mol-model/structure/export/categories/atom_site.ts
+++ b/src/mol-model/structure/export/categories/atom_site.ts
@@ -83,13 +83,16 @@ export const _atom_site: CifCategory<CifExportContext> = {
     }
 };
 
-function prepostfixed(prefix: string | undefined, postfix: string | undefined, name: string) {
-    if (prefix && postfix) return `${prefix}_${name}_${postfix}`;
+function prepostfixed(prefix: string | undefined, name: string) {
     if (prefix) return `${prefix}_${name}`;
-    if (postfix) return `${name}_${postfix}`;
     return name;
 }
 
+function prefixedInsCode(prefix: string | undefined) {
+    if (!prefix) return 'pdbx_PDB_ins_code';
+    return `pdbx_${prefix}_PDB_ins_code`;
+}
+
 function mappedProp<K, D>(loc: (key: K, data: D) => StructureElement.Location, prop: (e: StructureElement.Location) => any) {
     return (k: K, d: D) => prop(loc(k, d));
 }
@@ -102,15 +105,14 @@ function addModelNum<K, D>(fields: CifWriter.Field.Builder<K, D>, getLocation: (
 
 export interface IdFieldsOptions {
     prefix?: string,
-    postfix?: string,
     includeModelNum?: boolean
 }
 
 export function residueIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
-    const prefix = options && options.prefix, postfix = options && options.postfix;
+    const prefix = options && options.prefix;
     const ret = CifWriter.fields<K, D>()
-        .str(prepostfixed(prefix, postfix, `label_comp_id`), mappedProp(getLocation, P.atom.label_comp_id))
-        .int(prepostfixed(prefix, postfix, `label_seq_id`), mappedProp(getLocation, P.residue.label_seq_id), {
+        .str(prepostfixed(prefix, `label_comp_id`), mappedProp(getLocation, P.atom.label_comp_id))
+        .int(prepostfixed(prefix, `label_seq_id`), mappedProp(getLocation, P.residue.label_seq_id), {
             encoder: E.deltaRLE,
             valueKind: (k, d) => {
                 const e = getLocation(k, d);
@@ -118,45 +120,45 @@ export function residueIdFields<K, D>(getLocation: (key: K, data: D) => Structur
                 return m.atomicHierarchy.residues.label_seq_id.valueKind(m.atomicHierarchy.residueAtomSegments.index[e.element]);
             }
         })
-        .str(prepostfixed(prefix, postfix, `pdbx_PDB_ins_code`), mappedProp(getLocation, P.residue.pdbx_PDB_ins_code))
+        .str(prefixedInsCode(prefix), mappedProp(getLocation, P.residue.pdbx_PDB_ins_code))
 
-        .str(prepostfixed(prefix, postfix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id))
-        .str(prepostfixed(prefix, postfix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id))
+        .str(prepostfixed(prefix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id))
+        .str(prepostfixed(prefix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id))
 
-        .str(prepostfixed(prefix, postfix, `auth_comp_id`), mappedProp(getLocation, P.atom.auth_comp_id))
-        .int(prepostfixed(prefix, postfix, `auth_seq_id`), mappedProp(getLocation, P.residue.auth_seq_id), { encoder: E.deltaRLE })
-        .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id));
+        .str(prepostfixed(prefix, `auth_comp_id`), mappedProp(getLocation, P.atom.auth_comp_id))
+        .int(prepostfixed(prefix, `auth_seq_id`), mappedProp(getLocation, P.residue.auth_seq_id), { encoder: E.deltaRLE })
+        .str(prepostfixed(prefix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id));
 
     addModelNum(ret, getLocation, options);
     return ret.getFields();
 }
 
 export function chainIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
-    const prefix = options && options.prefix, postfix = options && options.postfix;
+    const prefix = options && options.prefix;
     const ret = CifField.build<K, D>()
-        .str(prepostfixed(prefix, postfix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id))
-        .str(prepostfixed(prefix, postfix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id))
-        .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id));
+        .str(prepostfixed(prefix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id))
+        .str(prepostfixed(prefix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id))
+        .str(prepostfixed(prefix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id));
 
     addModelNum(ret, getLocation, options);
     return ret.getFields();
 }
 
 export function entityIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
-    const prefix = options && options.prefix, postfix = options && options.postfix;
+    const prefix = options && options.prefix;
     const ret = CifField.build<K, D>()
-        .str(prepostfixed(prefix, postfix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id));
+        .str(prepostfixed(prefix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id));
 
     addModelNum(ret, getLocation, options);
     return ret.getFields();
 }
 
 export function atomIdFields<K, D>(getLocation: (key: K, data: D) => StructureElement.Location, options?: IdFieldsOptions): CifField<K, D>[] {
-    const prefix = options && options.prefix, postfix = options && options.postfix;
+    const prefix = options && options.prefix;
     const ret = CifWriter.fields<K, D>()
-        .str(prepostfixed(prefix, postfix, `label_atom_id`), mappedProp(getLocation, P.atom.label_atom_id))
-        .str(prepostfixed(prefix, postfix, `label_comp_id`), mappedProp(getLocation, P.atom.label_comp_id))
-        .int(prepostfixed(prefix, postfix, `label_seq_id`), mappedProp(getLocation, P.residue.label_seq_id), {
+        .str(prepostfixed(prefix, `label_atom_id`), mappedProp(getLocation, P.atom.label_atom_id))
+        .str(prepostfixed(prefix, `label_comp_id`), mappedProp(getLocation, P.atom.label_comp_id))
+        .int(prepostfixed(prefix, `label_seq_id`), mappedProp(getLocation, P.residue.label_seq_id), {
             encoder: E.deltaRLE,
             valueKind: (k, d) => {
                 const e = getLocation(k, d);
@@ -164,16 +166,16 @@ export function atomIdFields<K, D>(getLocation: (key: K, data: D) => StructureEl
                 return m.atomicHierarchy.residues.label_seq_id.valueKind(m.atomicHierarchy.residueAtomSegments.index[e.element]);
             }
         })
-        .str(prepostfixed(prefix, postfix, `label_alt_id`), mappedProp(getLocation, P.atom.label_alt_id))
-        .str(prepostfixed(prefix, postfix, `pdbx_PDB_ins_code`), mappedProp(getLocation, P.residue.pdbx_PDB_ins_code))
+        .str(prepostfixed(prefix, `label_alt_id`), mappedProp(getLocation, P.atom.label_alt_id))
+        .str(prefixedInsCode(prefix), mappedProp(getLocation, P.residue.pdbx_PDB_ins_code))
 
-        .str(prepostfixed(prefix, postfix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id))
-        .str(prepostfixed(prefix, postfix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id))
+        .str(prepostfixed(prefix, `label_asym_id`), mappedProp(getLocation, P.chain.label_asym_id))
+        .str(prepostfixed(prefix, `label_entity_id`), mappedProp(getLocation, P.chain.label_entity_id))
 
-        .str(prepostfixed(prefix, postfix, `auth_atom_id`), mappedProp(getLocation, P.atom.auth_atom_id))
-        .str(prepostfixed(prefix, postfix, `auth_comp_id`), mappedProp(getLocation, P.atom.auth_comp_id))
-        .int(prepostfixed(prefix, postfix, `auth_seq_id`), mappedProp(getLocation, P.residue.auth_seq_id), { encoder: E.deltaRLE })
-        .str(prepostfixed(prefix, postfix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id));
+        .str(prepostfixed(prefix, `auth_atom_id`), mappedProp(getLocation, P.atom.auth_atom_id))
+        .str(prepostfixed(prefix, `auth_comp_id`), mappedProp(getLocation, P.atom.auth_comp_id))
+        .int(prepostfixed(prefix, `auth_seq_id`), mappedProp(getLocation, P.residue.auth_seq_id), { encoder: E.deltaRLE })
+        .str(prepostfixed(prefix, `auth_asym_id`), mappedProp(getLocation, P.chain.auth_asym_id));
 
     addModelNum(ret, getLocation, options);
     return ret.getFields();
diff --git a/src/mol-plugin-state/formats/trajectory.ts b/src/mol-plugin-state/formats/trajectory.ts
index bfeddf182f650e060944be6eefb68a6c3e066716..b0494f01b5c7eeafd0c9c7620c4c58bbad4b332a 100644
--- a/src/mol-plugin-state/formats/trajectory.ts
+++ b/src/mol-plugin-state/formats/trajectory.ts
@@ -113,15 +113,6 @@ export const GroProvider: TrajectoryFormatProvider = {
     visuals: defaultVisuals
 };
 
-export const Provider3dg: TrajectoryFormatProvider = {
-    label: '3DG',
-    description: '3DG',
-    category: TrajectoryFormatCategory,
-    stringExtensions: ['3dg'],
-    parse: directTrajectory(StateTransforms.Model.TrajectoryFrom3DG),
-    visuals: defaultVisuals
-};
-
 export const MolProvider: TrajectoryFormatProvider = {
     label: 'MOL/SDF',
     description: 'MOL/SDF',
@@ -146,7 +137,6 @@ export const BuiltInTrajectoryFormats = [
     ['pdb', PdbProvider] as const,
     ['pdbqt', PdbqtProvider] as const,
     ['gro', GroProvider] as const,
-    ['3dg', Provider3dg] as const,
     ['mol', MolProvider] as const,
     ['mol2', Mol2Provider] as const,
 ] as const;
diff --git a/src/mol-plugin-state/objects.ts b/src/mol-plugin-state/objects.ts
index 9c3fade63ea32e43f608e5554635389ba94f0f99..403fa9669755c33dd187aa44dc32de8f8f64e82d 100644
--- a/src/mol-plugin-state/objects.ts
+++ b/src/mol-plugin-state/objects.ts
@@ -5,7 +5,6 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { File3DG } from '../mol-io/reader/3dg/parser';
 import { Ccp4File } from '../mol-io/reader/ccp4/schema';
 import { CifFile } from '../mol-io/reader/cif';
 import { DcdFile } from '../mol-io/reader/dcd/parser';
@@ -83,7 +82,6 @@ export namespace PluginStateObject {
             { kind: 'cif', data: CifFile } |
             { kind: 'pdb', data: CifFile } |
             { kind: 'gro', data: CifFile } |
-            { kind: '3dg', data: File3DG } |
             { kind: 'dcd', data: DcdFile } |
             { kind: 'ccp4', data: Ccp4File } |
             { kind: 'dsn6', data: Dsn6File } |
diff --git a/src/mol-plugin-state/transforms/model.ts b/src/mol-plugin-state/transforms/model.ts
index 31bcef917f3a7fa3d432a131323e0411669d4085..a40cd78e5f676953cf9169d2cf2abecb213d153b 100644
--- a/src/mol-plugin-state/transforms/model.ts
+++ b/src/mol-plugin-state/transforms/model.ts
@@ -5,13 +5,11 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { parse3DG } from '../../mol-io/reader/3dg/parser';
 import { parseDcd } from '../../mol-io/reader/dcd/parser';
 import { parseGRO } from '../../mol-io/reader/gro/parser';
 import { parsePDB } from '../../mol-io/reader/pdb/parser';
 import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
 import { shapeFromPly } from '../../mol-model-formats/shape/ply';
-import { trajectoryFrom3DG } from '../../mol-model-formats/structure/3dg';
 import { coordinatesFromDcd } from '../../mol-model-formats/structure/dcd';
 import { trajectoryFromGRO } from '../../mol-model-formats/structure/gro';
 import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
@@ -52,7 +50,6 @@ export { TrajectoryFromMOL };
 export { TrajectoryFromMOL2 };
 export { TrajectoryFromCube };
 export { TrajectoryFromCifCore };
-export { TrajectoryFrom3DG };
 export { ModelFromTrajectory };
 export { StructureFromTrajectory };
 export { StructureFromModel };
@@ -339,24 +336,6 @@ const TrajectoryFromCifCore = PluginStateTransform.BuiltIn({
     }
 });
 
-type TrajectoryFrom3DG = typeof TrajectoryFrom3DG
-const TrajectoryFrom3DG = PluginStateTransform.BuiltIn({
-    name: 'trajectory-from-3dg',
-    display: { name: 'Parse 3DG', description: 'Parse 3DG string and create trajectory.' },
-    from: [SO.Data.String],
-    to: SO.Molecule.Trajectory
-})({
-    apply({ a }) {
-        return Task.create('Parse 3DG', async ctx => {
-            const parsed = await parse3DG(a.data).runInContext(ctx);
-            if (parsed.isError) throw new Error(parsed.message);
-            const models = await trajectoryFrom3DG(parsed.result).runInContext(ctx);
-            const props = trajectoryProps(models);
-            return new SO.Molecule.Trajectory(models, props);
-        });
-    }
-});
-
 const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
 type ModelFromTrajectory = typeof ModelFromTrajectory
 const ModelFromTrajectory = PluginStateTransform.BuiltIn({
diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts
index 3ee72b35acb74a0a582d94046d495a46d916e904..50b9b53f249680d1d3b96ff03272a5de4519e401 100644
--- a/src/mol-repr/shape/representation.ts
+++ b/src/mol-repr/shape/representation.ts
@@ -23,6 +23,7 @@ import { PickingId } from '../../mol-geo/geometry/picking';
 import { Visual } from '../visual';
 import { RuntimeContext, Task } from '../../mol-task';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { isDebugMode } from '../../mol-util/debug';
 
 export interface ShapeRepresentation<D, G extends Geometry, P extends Geometry.Params<G>> extends Representation<D, P> { }
 
@@ -216,7 +217,9 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
             Representation.updateState(_state, state);
         },
         setTheme(theme: Theme) {
-            console.warn('The `ShapeRepresentation` theme is fixed to `ShapeGroupColorTheme` and `ShapeGroupSizeTheme`. Colors are taken from `Shape.getColor` and sizes from `Shape.getSize`');
+            if(isDebugMode) {
+                console.warn('The `ShapeRepresentation` theme is fixed to `ShapeGroupColorTheme` and `ShapeGroupSizeTheme`. Colors are taken from `Shape.getColor` and sizes from `Shape.getSize`');
+            }
         },
         destroy() {
             // TODO