diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index b9b20e81a7f7d871928a326a381ead3dbd73437b..1751c4c77ac72572360ff4af1cbe6c041509f532 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -255,8 +255,9 @@ namespace Canvas3D {
                 if (multiSample.enabled) {
                     multiSample.render(true, p.transparentBackground);
                 } else {
-                    drawPass.render(!postprocessing.enabled, p.transparentBackground);
-                    if (postprocessing.enabled) postprocessing.render(true);
+                    const toDrawingBuffer = !postprocessing.enabled && scene.volumes.renderables.length === 0;
+                    drawPass.render(toDrawingBuffer, p.transparentBackground);
+                    if (!toDrawingBuffer) postprocessing.render(true);
                 }
                 pickPass.pickDirty = true;
                 didRender = true;
diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts
index 33dc0073d7f90d841e34e6001f4647dfceb80b99..67fec49ec519035d2ebf8538c092f85b24dac68f 100644
--- a/src/mol-canvas3d/passes/draw.ts
+++ b/src/mol-canvas3d/passes/draw.ts
@@ -14,6 +14,42 @@ import { Camera } from '../camera';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { HandleHelper } from '../helper/handle-helper';
+import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
+import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { ShaderCode } from '../../mol-gl/shader-code';
+import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
+import { ValueCell } from '../../mol-util';
+import { Vec2 } from '../../mol-math/linear-algebra';
+
+import quad_vert from '../../mol-gl/shader/quad.vert';
+import depthMerge_frag from '../../mol-gl/shader/depth-merge.frag';
+
+const DepthMergeSchema = {
+    ...QuadSchema,
+    tDepthPrimitives: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
+    tDepthVolumes: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+    dPackedDepth: DefineSpec('boolean'),
+};
+
+type DepthMergeRenderable = ComputeRenderable<Values<typeof DepthMergeSchema>>
+
+function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Texture, depthTextureVolumes: Texture, packedDepth: boolean): DepthMergeRenderable {
+    const values: Values<typeof DepthMergeSchema> = {
+        ...QuadValues,
+        tDepthPrimitives: ValueCell.create(depthTexturePrimitives),
+        tDepthVolumes: ValueCell.create(depthTextureVolumes),
+        uTexSize: ValueCell.create(Vec2.create(depthTexturePrimitives.getWidth(), depthTexturePrimitives.getHeight())),
+        dPackedDepth: ValueCell.create(packedDepth),
+    };
+
+    const schema = { ...DepthMergeSchema };
+    const shaderCode = ShaderCode('depth-merge', quad_vert, depthMerge_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
 
 export const DrawPassParams = {
     cameraHelper: PD.Group(CameraHelperParams)
@@ -28,7 +64,12 @@ export class DrawPass {
 
     cameraHelper: CameraHelper
 
-    private depthTarget: RenderTarget | null
+    private depthTarget: RenderTarget
+    private depthTargetPrimitives: RenderTarget | null
+    private depthTargetVolumes: RenderTarget | null
+    private depthTexturePrimitives: Texture
+    private depthTextureVolumes: Texture
+    private depthMerge: DepthMergeRenderable
 
     constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper, private handleHelper: HandleHelper, props: Partial<DrawPassProps> = {}) {
         const { gl, extensions, resources } = webgl;
@@ -36,12 +77,20 @@ export class DrawPass {
         const height = gl.drawingBufferHeight;
         this.colorTarget = webgl.createRenderTarget(width, height);
         this.packedDepth = !extensions.depthTexture;
-        this.depthTarget = this.packedDepth ? webgl.createRenderTarget(width, height) : null;
-        this.depthTexture = this.depthTarget ? this.depthTarget.texture : resources.texture('image-depth', 'depth', 'ushort', 'nearest');
+
+        this.depthTarget = webgl.createRenderTarget(width, height);
+        this.depthTexture = this.depthTarget.texture;
+
+        this.depthTargetPrimitives = this.packedDepth ? webgl.createRenderTarget(width, height) : null;
+        this.depthTargetVolumes = this.packedDepth ? webgl.createRenderTarget(width, height) : null;
+
+        this.depthTexturePrimitives = this.depthTargetPrimitives ? this.depthTargetPrimitives.texture : resources.texture('image-depth', 'depth', 'ushort', 'nearest');
+        this.depthTextureVolumes = this.depthTargetVolumes ? this.depthTargetVolumes.texture : resources.texture('image-depth', 'depth', 'ushort', 'nearest');
         if (!this.packedDepth) {
-            this.depthTexture.define(width, height);
-            this.depthTexture.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+            this.depthTexturePrimitives.define(width, height);
+            this.depthTextureVolumes.define(width, height);
         }
+        this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
 
         const p = { ...DefaultDrawPassProps, ...props };
         this.cameraHelper = new CameraHelper(webgl, p.cameraHelper);
@@ -49,11 +98,21 @@ export class DrawPass {
 
     setSize(width: number, height: number) {
         this.colorTarget.setSize(width, height);
-        if (this.depthTarget) {
-            this.depthTarget.setSize(width, height);
+        this.depthTarget.setSize(width, height);
+
+        if (this.depthTargetPrimitives) {
+            this.depthTargetPrimitives.setSize(width, height);
+        } else {
+            this.depthTexturePrimitives.define(width, height);
+        }
+
+        if (this.depthTargetVolumes) {
+            this.depthTargetVolumes.setSize(width, height);
         } else {
-            this.depthTexture.define(width, height);
+            this.depthTextureVolumes.define(width, height);
         }
+
+        ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height));
     }
 
     setProps(props: Partial<DrawPassProps>) {
@@ -67,41 +126,67 @@ export class DrawPass {
     }
 
     render(toDrawingBuffer: boolean, transparentBackground: boolean) {
-        const { webgl, renderer, colorTarget, depthTarget } = this;
         if (toDrawingBuffer) {
-            webgl.unbindFramebuffer();
+            this.webgl.unbindFramebuffer();
         } else {
-            colorTarget.bind();
+            this.colorTarget.bind();
             if (!this.packedDepth) {
-                // TODO unlcear why it is not enough to call `attachFramebuffer` in `Texture.reset`
-                this.depthTexture.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+                this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
             }
         }
 
-        renderer.setViewport(0, 0, colorTarget.getWidth(), colorTarget.getHeight());
-        this.renderInternal('color', transparentBackground);
+        this.renderer.setViewport(0, 0, this.colorTarget.getWidth(), this.colorTarget.getHeight());
+        this.renderer.render(this.scene.primitives, this.camera, 'color', true, transparentBackground, null);
 
         // do a depth pass if not rendering to drawing buffer and
         // extensions.depthTexture is unsupported (i.e. depthTarget is set)
-        if (!toDrawingBuffer && depthTarget) {
-            depthTarget.bind();
-            this.renderInternal('depth', transparentBackground);
+        if (!toDrawingBuffer && this.depthTargetPrimitives) {
+            this.depthTargetPrimitives.bind();
+            this.renderer.render(this.scene.primitives, this.camera, 'depth', true, transparentBackground, null);
+            this.colorTarget.bind();
+        }
+
+        // do direct-volume rendering
+        if (!toDrawingBuffer && this.scene.volumes.renderables.length > 0) {
+            if (!this.packedDepth) {
+                this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+                this.webgl.state.depthMask(true);
+                this.webgl.gl.clear(this.webgl.gl.DEPTH_BUFFER_BIT);
+            }
+            this.renderer.render(this.scene.volumes, this.camera, 'color', false, transparentBackground, this.depthTexturePrimitives);
+
+            // do volume depth pass if extensions.depthTexture is unsupported (i.e. depthTarget is set)
+            if (this.depthTargetVolumes) {
+                this.depthTargetVolumes.bind();
+                this.renderer.render(this.scene.volumes, this.camera, 'depth', false, transparentBackground, this.depthTexturePrimitives);
+                this.colorTarget.bind();
+            }
+        }
+
+        // merge depths from primitive and volume rendering
+        if (!toDrawingBuffer) {
+            this.depthMerge.update();
+            this.depthTarget.bind();
+            this.webgl.state.disable(this.webgl.gl.SCISSOR_TEST);
+            this.webgl.state.disable(this.webgl.gl.BLEND);
+            this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
+            this.webgl.state.depthMask(false);
+            this.webgl.state.clearColor(1, 1, 1, 1);
+            this.webgl.gl.clear(this.webgl.gl.COLOR_BUFFER_BIT);
+            this.depthMerge.render();
+            this.colorTarget.bind();
         }
-    }
 
-    private renderInternal(variant: 'color' | 'depth', transparentBackground: boolean) {
-        const { renderer, scene, camera, debugHelper, cameraHelper, handleHelper } = this;
-        renderer.render(scene, camera, variant, true, transparentBackground);
-        if (debugHelper.isEnabled) {
-            debugHelper.syncVisibility();
-            renderer.render(debugHelper.scene, camera, variant, false, transparentBackground);
+        if (this.debugHelper.isEnabled) {
+            this.debugHelper.syncVisibility();
+            this.renderer.render(this.debugHelper.scene, this.camera, 'color', false, transparentBackground, null);
         }
-        if (handleHelper.isEnabled) {
-            renderer.render(handleHelper.scene, camera, variant, false, transparentBackground);
+        if (this.handleHelper.isEnabled) {
+            this.renderer.render(this.handleHelper.scene, this.camera, 'color', false, transparentBackground, null);
         }
-        if (cameraHelper.isEnabled) {
-            cameraHelper.update(camera);
-            renderer.render(cameraHelper.scene, cameraHelper.camera, variant, false, transparentBackground);
+        if (this.cameraHelper.isEnabled) {
+            this.cameraHelper.update(this.camera);
+            this.renderer.render(this.cameraHelper.scene, this.cameraHelper.camera, 'color', false, transparentBackground, null);
         }
     }
 }
\ No newline at end of file
diff --git a/src/mol-canvas3d/passes/pick.ts b/src/mol-canvas3d/passes/pick.ts
index c64d20dfbafe6b5c5f9ee03da16fdab28c4722ce..425c07544ce563ce360a4107ad36c86ea709b31c 100644
--- a/src/mol-canvas3d/passes/pick.ts
+++ b/src/mol-canvas3d/passes/pick.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -72,14 +72,14 @@ export class PickPass {
         renderer.setViewport(0, 0, this.pickWidth, this.pickHeight);
 
         this.objectPickTarget.bind();
-        renderer.render(scene, camera, 'pickObject', true, false);
-        renderer.render(handleScene, camera, 'pickObject', false, false);
+        renderer.render(scene, camera, 'pickObject', true, false, null);
+        renderer.render(handleScene, camera, 'pickObject', false, false, null);
         this.instancePickTarget.bind();
-        renderer.render(scene, camera, 'pickInstance', true, false);
-        renderer.render(handleScene, camera, 'pickInstance', false, false);
+        renderer.render(scene, camera, 'pickInstance', true, false, null);
+        renderer.render(handleScene, camera, 'pickInstance', false, false, null);
         this.groupPickTarget.bind();
-        renderer.render(scene, camera, 'pickGroup', true, false);
-        renderer.render(handleScene, camera, 'pickGroup', false, false);
+        renderer.render(scene, camera, 'pickGroup', true, false, null);
+        renderer.render(handleScene, camera, 'pickGroup', false, false, null);
 
         this.pickDirty = false;
     }
diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts
index 86be9580946dcca972d9a70c3424a19c2368cfdd..fb4a34378ac2a1db78c2964ae720a52c9ddcf122 100644
--- a/src/mol-canvas3d/passes/postprocessing.ts
+++ b/src/mol-canvas3d/passes/postprocessing.ts
@@ -25,7 +25,7 @@ import postprocessing_frag from '../../mol-gl/shader/postprocessing.frag';
 const PostprocessingSchema = {
     ...QuadSchema,
     tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
-    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    tPackedDepth: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
     uTexSize: UniformSpec('v2'),
 
     dOrthographic: DefineSpec('number'),
@@ -43,8 +43,6 @@ const PostprocessingSchema = {
     dOutlineEnable: DefineSpec('boolean'),
     uOutlineScale: UniformSpec('f'),
     uOutlineThreshold: UniformSpec('f'),
-
-    dPackedDepth: DefineSpec('boolean'),
 };
 
 export const PostprocessingParams = {
@@ -73,7 +71,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
     const values: Values<typeof PostprocessingSchema> = {
         ...QuadValues,
         tColor: ValueCell.create(colorTexture),
-        tDepth: ValueCell.create(depthTexture),
+        tPackedDepth: ValueCell.create(depthTexture),
         uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
 
         dOrthographic: ValueCell.create(0),
@@ -91,8 +89,6 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         dOutlineEnable: ValueCell.create(p.outline.name === 'on'),
         uOutlineScale: ValueCell.create((p.outline.name === 'on' ? p.outline.params.scale : 1) * ctx.pixelRatio),
         uOutlineThreshold: ValueCell.create(p.outline.name === 'on' ? p.outline.params.threshold : 0.8),
-
-        dPackedDepth: ValueCell.create(packedDepth),
     };
 
     const schema = { ...PostprocessingSchema };
diff --git a/src/mol-geo/geometry/direct-volume/direct-volume.ts b/src/mol-geo/geometry/direct-volume/direct-volume.ts
index fe16fdd357702f9e853dce42f823e417f01dfc11..aa18b974b36a80e7962e514b2db55feb8fe7c534 100644
--- a/src/mol-geo/geometry/direct-volume/direct-volume.ts
+++ b/src/mol-geo/geometry/direct-volume/direct-volume.ts
@@ -42,15 +42,20 @@ export interface DirectVolume {
     readonly bboxMax: ValueCell<Vec3>
     readonly transform: ValueCell<Mat4>
 
+    readonly cellDim: ValueCell<Vec3>
+    readonly unitToCartn: ValueCell<Mat4>
+    readonly cartnToUnit: ValueCell<Mat4>
+    readonly packedGroup: ValueCell<boolean>
+
     /** Bounding sphere of the volume */
-    boundingSphere: Sphere3D
+    readonly boundingSphere: Sphere3D
 }
 
 export namespace DirectVolume {
-    export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture, stats: Grid['stats'], directVolume?: DirectVolume): DirectVolume {
+    export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, directVolume?: DirectVolume): DirectVolume {
         return directVolume ?
-            update(bbox, gridDimension, transform, texture, stats, directVolume) :
-            fromData(bbox, gridDimension, transform, texture, stats);
+            update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, directVolume) :
+            fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup);
     }
 
     function hashCode(directVolume: DirectVolume) {
@@ -61,7 +66,7 @@ export namespace DirectVolume {
         ]);
     }
 
-    function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture, stats: Grid['stats']): DirectVolume {
+    function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean): DirectVolume {
         const boundingSphere = Sphere3D();
         let currentHash = -1;
 
@@ -77,8 +82,11 @@ export namespace DirectVolume {
             gridStats: ValueCell.create(Vec4.create(stats.min, stats.max, stats.mean, stats.sigma)),
             bboxMin: ValueCell.create(bbox.min),
             bboxMax: ValueCell.create(bbox.max),
-            bboxSize: ValueCell.create(Vec3.sub(Vec3.zero(), bbox.max, bbox.min)),
+            bboxSize: ValueCell.create(Vec3.sub(Vec3(), bbox.max, bbox.min)),
             transform: ValueCell.create(transform),
+            cellDim: ValueCell.create(cellDim),
+            unitToCartn: ValueCell.create(unitToCartn),
+            cartnToUnit: ValueCell.create(Mat4.invert(Mat4(), unitToCartn)),
             get boundingSphere() {
                 const newHash = hashCode(directVolume);
                 if (newHash !== currentHash) {
@@ -88,11 +96,12 @@ export namespace DirectVolume {
                 }
                 return boundingSphere;
             },
+            packedGroup: ValueCell.create(packedGroup)
         };
         return directVolume;
     }
 
-    function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture, stats: Grid['stats'], directVolume: DirectVolume): DirectVolume {
+    function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, directVolume: DirectVolume): DirectVolume {
         const width = texture.getWidth();
         const height = texture.getHeight();
         const depth = texture.getDepth();
@@ -105,6 +114,10 @@ export namespace DirectVolume {
         ValueCell.update(directVolume.bboxMax, bbox.max);
         ValueCell.update(directVolume.bboxSize, Vec3.sub(directVolume.bboxSize.ref.value, bbox.max, bbox.min));
         ValueCell.update(directVolume.transform, transform);
+        ValueCell.update(directVolume.cellDim, cellDim);
+        ValueCell.update(directVolume.unitToCartn, unitToCartn);
+        ValueCell.update(directVolume.cartnToUnit, Mat4.invert(Mat4(), unitToCartn));
+        ValueCell.updateIfChanged(directVolume.packedGroup, packedGroup);
         return directVolume;
     }
 
@@ -138,6 +151,7 @@ export namespace DirectVolume {
         flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
         ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
         renderMode: createRenderModeParam(),
+        stepsPerCell: PD.Numeric(5, { min: 1, max: 20, step: 1 }),
     };
     export type Params = typeof Params
 
@@ -182,7 +196,7 @@ export namespace DirectVolume {
             ? props.renderMode.params.isoValue
             : Volume.IsoValue.relative(2);
 
-        const maxSteps = Math.ceil(Vec3.magnitude(gridDimension.ref.value) * 5);
+        const maxSteps = Math.ceil(Vec3.magnitude(gridDimension.ref.value) * props.stepsPerCell);
 
         return {
             ...color,
@@ -204,6 +218,7 @@ export namespace DirectVolume {
             uBboxMax: bboxMax,
             uBboxSize: bboxSize,
             uMaxSteps: ValueCell.create(maxSteps),
+            uStepFactor: ValueCell.create(1 / props.stepsPerCell),
             uTransform: gridTransform,
             uGridDim: gridDimension,
             dRenderMode: ValueCell.create(props.renderMode.name),
@@ -214,6 +229,11 @@ export namespace DirectVolume {
             tGridTex: gridTexture,
             uGridStats: gridStats,
 
+            uCellDim: directVolume.cellDim,
+            uCartnToUnit: directVolume.cartnToUnit,
+            uUnitToCartn: directVolume.unitToCartn,
+            dPackedGroup: directVolume.packedGroup,
+
             dDoubleSided: ValueCell.create(false),
             dFlatShaded: ValueCell.create(props.flatShaded),
             dFlipSided: ValueCell.create(true),
@@ -242,6 +262,10 @@ export namespace DirectVolume {
             const controlPoints = getControlPointsFromVec2Array(props.renderMode.params.controlPoints);
             createTransferFunctionTexture(controlPoints, props.renderMode.params.list.colors, values.tTransferTex);
         }
+
+        const maxSteps = Math.ceil(Vec3.magnitude(values.uGridDim.ref.value) * props.stepsPerCell);
+        ValueCell.updateIfChanged(values.uMaxSteps, maxSteps);
+        ValueCell.updateIfChanged(values.uStepFactor, 1 / props.stepsPerCell);
     }
 
     function updateBoundingSphere(values: DirectVolumeValues, directVolume: DirectVolume) {
@@ -260,6 +284,7 @@ export namespace DirectVolume {
     function createRenderableState(props: PD.Values<Params>): RenderableState {
         const state = BaseGeometry.createRenderableState(props);
         state.opaque = false;
+        state.writeDepth = props.renderMode.name === 'isosurface';
         return state;
     }
 
diff --git a/src/mol-gl/renderable/direct-volume.ts b/src/mol-gl/renderable/direct-volume.ts
index 7e5e34af040738177698447a49256313bd95572d..729e4f0e582938b54fbf76e09dba442d702af4e3 100644
--- a/src/mol-gl/renderable/direct-volume.ts
+++ b/src/mol-gl/renderable/direct-volume.ts
@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { AttributeSpec, Values, UniformSpec, GlobalUniformSchema, InternalSchema, TextureSpec, ValueSpec, ElementsSpec, DefineSpec, InternalValues } from './schema';
+import { AttributeSpec, Values, UniformSpec, GlobalUniformSchema, InternalSchema, TextureSpec, ValueSpec, ElementsSpec, DefineSpec, InternalValues, GlobalTextureSchema } from './schema';
 import { DirectVolumeShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 
@@ -65,6 +65,7 @@ export const DirectVolumeSchema = {
     uBboxMax: UniformSpec('v3'),
     uBboxSize: UniformSpec('v3'),
     uMaxSteps: UniformSpec('i'),
+    uStepFactor: UniformSpec('f'),
     uTransform: UniformSpec('m4'),
     uGridDim: UniformSpec('v3'),
     dRenderMode: DefineSpec('string', ['isosurface', 'volume']),
@@ -75,6 +76,11 @@ export const DirectVolumeSchema = {
     tGridTex: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
     uGridStats: UniformSpec('v4'), // [min, max, mean, sigma]
 
+    uCellDim: UniformSpec('v3'),
+    uCartnToUnit: UniformSpec('m4'),
+    uUnitToCartn: UniformSpec('m4'),
+    dPackedGroup: DefineSpec('boolean'),
+
     dDoubleSided: DefineSpec('boolean'),
     dFlipSided: DefineSpec('boolean'),
     dFlatShaded: DefineSpec('boolean'),
@@ -84,7 +90,7 @@ export type DirectVolumeSchema = typeof DirectVolumeSchema
 export type DirectVolumeValues = Values<DirectVolumeSchema>
 
 export function DirectVolumeRenderable(ctx: WebGLContext, id: number, values: DirectVolumeValues, state: RenderableState, materialId: number): Renderable<DirectVolumeValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...DirectVolumeSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...DirectVolumeSchema };
     if (!ctx.isWebGL2) {
         // workaround for webgl1 limitation that loop counters need to be `const`
         (schema.uMaxSteps as any) = DefineSpec('number');
diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts
index eaadd52e73772c32c88c3abee32b9b7820c0a287..c35193cf00e83e23fa4e6909f697a2122da75579 100644
--- a/src/mol-gl/renderable/schema.ts
+++ b/src/mol-gl/renderable/schema.ts
@@ -40,8 +40,9 @@ export function splitValues(schema: RenderableSchema, values: RenderableValues)
         const spec = schema[k];
         if (spec.type === 'attribute') attributeValues[k] = values[k];
         if (spec.type === 'define') defineValues[k] = values[k];
-        if (spec.type === 'texture') textureValues[k] = values[k];
-        // check if k exists in values so that global uniforms are excluded here
+        // check if k exists in values to exclude global textures
+        if (spec.type === 'texture' && values[k] !== undefined) textureValues[k] = values[k];
+        // check if k exists in values to exclude global uniforms
         if (spec.type === 'uniform' && values[k] !== undefined) {
             if (spec.isMaterial) materialUniformValues[k] = values[k];
             else uniformValues[k] = values[k];
@@ -121,6 +122,7 @@ export const GlobalUniformSchema = {
     uViewOffset: UniformSpec('v2'),
 
     uCameraPosition: UniformSpec('v3'),
+    uCameraDir: UniformSpec('v3'),
     uNear: UniformSpec('f'),
     uFar: UniformSpec('f'),
     uFogNear: UniformSpec('f'),
@@ -154,13 +156,19 @@ export const GlobalUniformSchema = {
     uSelectColor: UniformSpec('v3'),
 } as const;
 export type GlobalUniformSchema = typeof GlobalUniformSchema
-export type GlobalUniformValues = Values<GlobalUniformSchema> // { [k in keyof GlobalUniformSchema]: ValueCell<any> }
+export type GlobalUniformValues = Values<GlobalUniformSchema>
+
+export const GlobalTextureSchema = {
+    tDepth: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
+} as const;
+export type GlobalTextureSchema = typeof GlobalTextureSchema
+export type GlobalTextureValues = Values<GlobalTextureSchema>
 
 export const InternalSchema = {
     uObjectId: UniformSpec('i'),
 } as const;
 export type InternalSchema = typeof InternalSchema
-export type InternalValues = { [k in keyof InternalSchema]: ValueCell<any> }
+export type InternalValues = Values<InternalSchema>
 
 export const ColorSchema = {
     // aColor: AttributeSpec('float32', 3, 0), // TODO
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index 5359479a545b3ab0fdd921ba1244862dea0283d6..c563f2eb2d1b29789c5763a07b9004b6722d1afe 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -20,6 +20,7 @@ import { Clipping } from '../mol-theme/clipping';
 import { stringToWords } from '../mol-util/string';
 import { Transparency } from '../mol-theme/transparency';
 import { degToRad } from '../mol-math/misc';
+import { Texture } from './webgl/texture';
 
 export interface RendererStats {
     programCount: number
@@ -42,7 +43,7 @@ interface Renderer {
     readonly props: Readonly<RendererProps>
 
     clear: (transparentBackground: boolean) => void
-    render: (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean) => void
+    render: (group: Scene.Group, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => void
     setProps: (props: Partial<RendererProps>) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
     dispose: () => void
@@ -174,6 +175,7 @@ namespace Renderer {
         const modelViewProjection = Mat4();
         const invModelViewProjection = Mat4();
 
+        const cameraDir = Vec3();
         const viewOffset = Vec2();
 
         const globalUniforms: GlobalUniformValues = {
@@ -195,6 +197,7 @@ namespace Renderer {
             uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
 
             uCameraPosition: ValueCell.create(Vec3()),
+            uCameraDir: ValueCell.create(cameraDir),
             uNear: ValueCell.create(1),
             uFar: ValueCell.create(10000),
             uFogNear: ValueCell.create(1),
@@ -228,7 +231,7 @@ namespace Renderer {
 
         let globalUniformsNeedUpdate = true;
 
-        const renderObject = (r: Renderable<RenderableValues & BaseValues>, variant: GraphicsRenderVariant) => {
+        const renderObject = (r: Renderable<RenderableValues & BaseValues>, variant: GraphicsRenderVariant, depthTexture: Texture | null) => {
             if (!r.state.visible || (!r.state.pickable && variant[0] === 'p')) {
                 return;
             }
@@ -261,6 +264,8 @@ namespace Renderer {
                 globalUniformsNeedUpdate = false;
             }
 
+            if (depthTexture) program.bindTextures([['tDepth', depthTexture]]);
+
             if (r.values.dDoubleSided) {
                 if (r.values.dDoubleSided.ref.value || r.values.hasReflection.ref.value) {
                     state.disable(gl.CULL_FACE);
@@ -293,11 +298,11 @@ namespace Renderer {
             r.render(variant);
         };
 
-        const render = (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean) => {
-            ValueCell.update(globalUniforms.uModel, scene.view);
+        const render = (group: Scene.Group, camera: Camera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => {
+            ValueCell.update(globalUniforms.uModel, group.view);
             ValueCell.update(globalUniforms.uView, camera.view);
             ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view));
-            ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, scene.view, camera.view));
+            ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, group.view, camera.view));
             ValueCell.update(globalUniforms.uInvModelView, Mat4.invert(invModelView, modelView));
             ValueCell.update(globalUniforms.uProjection, camera.projection);
             ValueCell.update(globalUniforms.uInvProjection, Mat4.invert(invProjection, camera.projection));
@@ -308,6 +313,8 @@ namespace Renderer {
             ValueCell.update(globalUniforms.uViewOffset, camera.viewOffset.enabled ? Vec2.set(viewOffset, camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2.set(viewOffset, 0, 0));
 
             ValueCell.update(globalUniforms.uCameraPosition, camera.state.position);
+            ValueCell.update(globalUniforms.uCameraDir, Vec3.normalize(cameraDir, Vec3.sub(cameraDir, camera.state.target, camera.state.position)));
+
             ValueCell.update(globalUniforms.uFar, camera.far);
             ValueCell.update(globalUniforms.uNear, camera.near);
             ValueCell.update(globalUniforms.uFogFar, camera.fogFar);
@@ -318,7 +325,7 @@ namespace Renderer {
             globalUniformsNeedUpdate = true;
             state.currentRenderItemId = -1;
 
-            const { renderables } = scene;
+            const { renderables } = group;
 
             state.disable(gl.SCISSOR_TEST);
             state.disable(gl.BLEND);
@@ -338,22 +345,28 @@ namespace Renderer {
             if (variant === 'color') {
                 for (let i = 0, il = renderables.length; i < il; ++i) {
                     const r = renderables[i];
-                    if (r.state.opaque) renderObject(r, variant);
+                    if (r.state.opaque) {
+                        renderObject(r, variant, depthTexture);
+                    }
                 }
 
                 state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
                 state.enable(gl.BLEND);
                 for (let i = 0, il = renderables.length; i < il; ++i) {
                     const r = renderables[i];
-                    if (!r.state.opaque && r.state.writeDepth) renderObject(r, variant);
+                    if (!r.state.opaque && r.state.writeDepth) {
+                        renderObject(r, variant, depthTexture);
+                    }
                 }
                 for (let i = 0, il = renderables.length; i < il; ++i) {
                     const r = renderables[i];
-                    if (!r.state.opaque && !r.state.writeDepth) renderObject(r, variant);
+                    if (!r.state.opaque && !r.state.writeDepth) {
+                        renderObject(r, variant, depthTexture);
+                    }
                 }
             } else { // picking & depth
                 for (let i = 0, il = renderables.length; i < il; ++i) {
-                    renderObject(renderables[i], variant);
+                    renderObject(renderables[i], variant, depthTexture);
                 }
             }
 
diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts
index 980e90618a21be7efa8cd7a0754cbbaf6164f2a0..4838b4724878237b5f8ae91421e57d8ff8d3b0df 100644
--- a/src/mol-gl/scene.ts
+++ b/src/mol-gl/scene.ts
@@ -66,6 +66,9 @@ interface Scene extends Object3D {
     readonly boundingSphere: Sphere3D
     readonly boundingSphereVisible: Sphere3D
 
+    readonly primitives: Scene.Group
+    readonly volumes: Scene.Group
+
     /** Returns `true` if some visibility has changed, `false` otherwise. */
     syncVisibility: () => boolean
     update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean) => void
@@ -79,21 +82,34 @@ interface Scene extends Object3D {
 }
 
 namespace Scene {
+    export interface Group extends Object3D {
+        readonly renderables: ReadonlyArray<Renderable<RenderableValues & BaseValues>>
+    }
+
     export function create(ctx: WebGLContext): Scene {
         const renderableMap = new Map<GraphicsRenderObject, Renderable<RenderableValues & BaseValues>>();
         const renderables: Renderable<RenderableValues & BaseValues>[] = [];
         const boundingSphere = Sphere3D();
         const boundingSphereVisible = Sphere3D();
 
+        const primitives: Renderable<RenderableValues & BaseValues>[] = [];
+        const volumes: Renderable<RenderableValues & BaseValues>[] = [];
+
         let boundingSphereDirty = true;
         let boundingSphereVisibleDirty = true;
 
         const object3d = Object3D.create();
+        const { view, position, direction, up } = object3d;
 
         function add(o: GraphicsRenderObject) {
             if (!renderableMap.has(o)) {
                 const renderable = createRenderable(ctx, o);
                 renderables.push(renderable);
+                if (o.type === 'direct-volume') {
+                    volumes.push(renderable);
+                } else {
+                    primitives.push(renderable);
+                }
                 renderableMap.set(o, renderable);
                 boundingSphereDirty = true;
                 boundingSphereVisibleDirty = true;
@@ -109,6 +125,8 @@ namespace Scene {
             if (renderable) {
                 renderable.dispose();
                 arraySetRemove(renderables, renderable);
+                arraySetRemove(primitives, renderable);
+                arraySetRemove(volumes, renderable);
                 renderableMap.delete(o);
                 boundingSphereDirty = true;
                 boundingSphereVisibleDirty = true;
@@ -164,10 +182,11 @@ namespace Scene {
         }
 
         return {
-            get view () { return object3d.view; },
-            get position () { return object3d.position; },
-            get direction () { return object3d.direction; },
-            get up () { return object3d.up; },
+            view, position, direction, up,
+
+            renderables,
+            primitives: { view, position, direction, up, renderables: primitives },
+            volumes: { view, position, direction, up, renderables: volumes },
 
             syncVisibility,
             update(objects, keepBoundingSphere) {
@@ -212,7 +231,6 @@ namespace Scene {
             get count() {
                 return renderables.length;
             },
-            renderables,
             get boundingSphere() {
                 if (boundingSphereDirty) {
                     calculateBoundingSphere(renderables, boundingSphere, false);
diff --git a/src/mol-gl/shader/chunks/apply-fog.glsl.ts b/src/mol-gl/shader/chunks/apply-fog.glsl.ts
index f77354be3e2c4cf8bade029d16e6fd9659e28b92..5045fcf0ac739b137df13888ffc0f4f6a95a9585 100644
--- a/src/mol-gl/shader/chunks/apply-fog.glsl.ts
+++ b/src/mol-gl/shader/chunks/apply-fog.glsl.ts
@@ -1,6 +1,6 @@
 export default `
-float depth = length(vViewPosition);
-float fogFactor = smoothstep(uFogNear, uFogFar, depth);
+float fogDepth = length(vViewPosition);
+float fogFactor = smoothstep(uFogNear, uFogFar, fogDepth);
 float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
 if (uTransparentBackground == 0) {
     gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
diff --git a/src/mol-gl/shader/chunks/common.glsl.ts b/src/mol-gl/shader/chunks/common.glsl.ts
index 4156d6230df0f882125dd93c087e182a96af0732..965f1a1d1a09ca47f51c7543e3bb8e40e2c97d1b 100644
--- a/src/mol-gl/shader/chunks/common.glsl.ts
+++ b/src/mol-gl/shader/chunks/common.glsl.ts
@@ -67,6 +67,10 @@ vec4 linearTosRGB(const in vec4 c) {
     return vec4(mix(pow(c.rgb, vec3(0.41666)) * 1.055 - vec3(0.055), c.rgb * 12.92, vec3(lessThanEqual(c.rgb, vec3(0.0031308)))), c.a);
 }
 
+float linearizeDepth(in float depth, in float near, in float far) {
+    return (2.0 * near) / (far + near - depth * (far - near));
+}
+
 #if __VERSION__ != 300
     // transpose
 
diff --git a/src/mol-gl/shader/depth-merge.frag.ts b/src/mol-gl/shader/depth-merge.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2eb9ee381e2080a2ff83766a8115bb6b51c3c412
--- /dev/null
+++ b/src/mol-gl/shader/depth-merge.frag.ts
@@ -0,0 +1,24 @@
+export default `
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tDepthPrimitives;
+uniform sampler2D tDepthVolumes;
+uniform vec2 uTexSize;
+
+#include common
+
+float getDepth(const in vec2 coords, sampler2D tDepth) {
+    #ifdef dPackedDepth
+        return unpackRGBAToDepth(texture2D(tDepth, coords));
+    #else
+        return texture2D(tDepth, coords).r;
+    #endif
+}
+
+void main() {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    float depth = min(getDepth(coords, tDepthPrimitives), getDepth(coords, tDepthVolumes));
+    gl_FragColor = packDepthToRGBA(depth);
+}
+`;
\ No newline at end of file
diff --git a/src/mol-gl/shader/direct-volume.frag.ts b/src/mol-gl/shader/direct-volume.frag.ts
index e2571db4e623736dd75c0c5c44a9a9bb91d7d7b5..7540037cdc96181a447f1e7525b1a9b4507cb3a9 100644
--- a/src/mol-gl/shader/direct-volume.frag.ts
+++ b/src/mol-gl/shader/direct-volume.frag.ts
@@ -17,6 +17,12 @@ precision highp int;
 #include texture3d_from_2d_linear
 
 uniform mat4 uProjection, uTransform, uModelView, uView;
+uniform vec3 uCameraDir;
+
+uniform sampler2D tDepth;
+uniform vec4 uViewport;
+uniform float uNear;
+uniform float uFar;
 
 varying vec3 unitCoord;
 varying vec3 origPos;
@@ -26,6 +32,7 @@ uniform mat4 uInvView;
 uniform vec2 uIsoValue;
 uniform vec3 uGridDim;
 uniform sampler2D tTransferTex;
+uniform float uStepFactor;
 
 uniform int uObjectId;
 uniform int uInstanceCount;
@@ -51,6 +58,11 @@ bool interior;
 
 uniform float uIsOrtho;
 
+uniform vec3 uCellDim;
+uniform vec3 uCameraPosition;
+uniform mat4 uCartnToUnit;
+uniform mat4 uUnitToCartn;
+
 #if __VERSION__ == 300
     // for webgl1 this is given as a 'define'
     uniform int uMaxSteps;
@@ -107,61 +119,71 @@ float calcDepth(const in vec3 cameraPos){
     return 0.5 + 0.5 * clipZW.x / clipZW.y;
 }
 
+float getDepth(const in vec2 coords) {
+    #ifdef dPackedDepth
+        return unpackRGBAToDepth(texture2D(tDepth, coords));
+    #else
+        return texture2D(tDepth, coords).r;
+    #endif
+}
+
 const float gradOffset = 0.5;
 
-vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir, vec3 rayDir) {
+vec3 toUnit(vec3 p) {
+    return (uCartnToUnit * vec4(p, 1.0)).xyz;
+}
+
+vec4 raymarch(vec3 startLoc, vec3 step) {
     vec3 scaleVol = vec3(1.0) / uGridDim;
     vec3 pos = startLoc;
+    vec4 cell;
+    vec4 prevCell = vec4(-1);
     float prevValue = -1.0;
     float value = 0.0;
     vec4 src = vec4(0.0);
     vec4 dst = vec4(0.0);
     bool hit = false;
-    // float count = 0.0;
 
     vec3 posMin = vec3(0.0);
     vec3 posMax = vec3(1.0) - vec3(1.0) / uGridDim;
 
-    #if defined(dRenderMode_isosurface)
-        vec3 isoPos;
-        float tmp;
+    vec3 unitPos;
+    vec3 isoPos;
 
-        vec3 color = vec3(0.45, 0.55, 0.8);
-        vec3 gradient = vec3(1.0);
-        vec3 dx = vec3(gradOffset * scaleVol.x, 0.0, 0.0);
-        vec3 dy = vec3(0.0, gradOffset * scaleVol.y, 0.0);
-        vec3 dz = vec3(0.0, 0.0, gradOffset * scaleVol.z);
-    #endif
+    vec3 color = vec3(0.45, 0.55, 0.8);
+    vec3 gradient = vec3(1.0);
+    vec3 dx = vec3(gradOffset * scaleVol.x, 0.0, 0.0);
+    vec3 dy = vec3(0.0, gradOffset * scaleVol.y, 0.0);
+    vec3 dz = vec3(0.0, 0.0, gradOffset * scaleVol.z);
 
     for(int i = 0; i < uMaxSteps; ++i){
-        value = textureVal(pos).a; // current voxel value
-        // if(pos.x > 1.01 || pos.y > 1.01 || pos.z > 1.01 || pos.x < -0.01 || pos.y < -0.01 || pos.z < -0.01)
-        //     break;
-
-        if(pos.x > posMax.x || pos.y > posMax.y || pos.z > posMax.z || pos.x < posMin.x || pos.y < posMin.y || pos.z < posMin.z) {
+        unitPos = toUnit(pos);
+        if(unitPos.x > posMax.x || unitPos.y > posMax.y || unitPos.z > posMax.z || unitPos.x < posMin.x || unitPos.y < posMin.y || unitPos.z < posMin.z) {
+            if (hit) break;
             prevValue = value;
+            prevCell = cell;
             pos += step;
             continue;
         }
 
-        #if defined(dRenderMode_volume)
-            src = transferFunction(value);
-            src.rgb *= src.a;
-            dst = (1.0 - dst.a) * src + dst; // standard blending
-        #endif
+        cell = textureVal(unitPos);
+        value = cell.a; // current voxel value
 
         #if defined(dRenderMode_isosurface)
             if(prevValue > 0.0 && ( // there was a prev Value
                 (prevValue < uIsoValue.x && value > uIsoValue.x) || // entering isosurface
                 (prevValue > uIsoValue.x && value < uIsoValue.x) // leaving isosurface
             )) {
-                tmp = ((prevValue - uIsoValue.x) / ((prevValue - uIsoValue.x) - (value - uIsoValue.x)));
-                isoPos = mix(pos - step, pos, tmp);
+                isoPos = toUnit(mix(pos - step, pos, ((prevValue - uIsoValue.x) / ((prevValue - uIsoValue.x) - (value - uIsoValue.x)))));
 
                 vec4 mvPosition = uModelView * uTransform * vec4(isoPos * uGridDim, 1.0);
+                float depth = calcDepth(mvPosition.xyz);
+                if (depth > getDepth(gl_FragCoord.xy / uViewport.zw))
+                    break;
+
                 #ifdef enabledFragDepth
                     if (!hit) {
-                        gl_FragDepthEXT = calcDepth(mvPosition.xyz);
+                        gl_FragDepthEXT = depth;
                         hit = true;
                     }
                 #endif
@@ -171,7 +193,12 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir, vec3 rayDir) {
                 #elif defined(dRenderVariant_pickInstance)
                     return vec4(encodeFloatRGB(instance), 1.0);
                 #elif defined(dRenderVariant_pickGroup)
-                    return vec4(textureGroup(isoPos).rgb, 1.0);
+                    #ifdef dPackedGroup
+                        return vec4(textureGroup(floor(isoPos * uGridDim + 0.5) / uGridDim).rgb, 1.0);
+                    #else
+                        vec3 g = floor(isoPos * uGridDim + 0.5);
+                        return vec4(encodeFloatRGB(g.z + g.y * uGridDim.z + g.x * uGridDim.z * uGridDim.y), 1.0);
+                    #endif
                 #elif defined(dRenderVariant_depth)
                     #ifdef enabledFragDepth
                         return packDepthToRGBA(gl_FragDepthEXT);
@@ -179,7 +206,12 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir, vec3 rayDir) {
                         return packDepthToRGBA(gl_FragCoord.z);
                     #endif
                 #elif defined(dRenderVariant_color)
-                    float group = floor(decodeFloatRGB(textureGroup(isoPos).rgb) + 0.5);
+                    #ifdef dPackedGroup
+                        float group = decodeFloatRGB(textureGroup(floor(isoPos * uGridDim + 0.5) / uGridDim).rgb);
+                    #else
+                        vec3 g = floor(isoPos * uGridDim + 0.5);
+                        float group = g.z + g.y * uGridDim.z + g.x * uGridDim.z * uGridDim.y;
+                    #endif
 
                     #if defined(dColorType_instance)
                         color = readFromTexture(tColor, instance, uColorTexDim).rgb;
@@ -203,10 +235,14 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir, vec3 rayDir) {
                             // nearest grid point
                             isoPos = floor(isoPos * uGridDim + 0.5) / uGridDim;
                         #endif
-                        // compute gradient by central differences
-                        gradient.x = textureVal(isoPos - dx).a - textureVal(isoPos + dx).a;
-                        gradient.y = textureVal(isoPos - dy).a - textureVal(isoPos + dy).a;
-                        gradient.z = textureVal(isoPos - dz).a - textureVal(isoPos + dz).a;
+                        #ifdef dPackedGroup
+                            // compute gradient by central differences
+                            gradient.x = textureVal(isoPos - dx).a - textureVal(isoPos + dx).a;
+                            gradient.y = textureVal(isoPos - dy).a - textureVal(isoPos + dy).a;
+                            gradient.z = textureVal(isoPos - dz).a - textureVal(isoPos + dz).a;
+                        #else
+                            gradient = textureVal(isoPos).xyz * 2.0 - 1.0;
+                        #endif
                         mat3 normalMatrix = transpose3(inverse3(mat3(uModelView)));
                         vec3 normal = -normalize(normalMatrix * normalize(gradient));
                         normal = normal * (float(flipped) * 2.0 - 1.0);
@@ -222,30 +258,60 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir, vec3 rayDir) {
                     src.rgb = gl_FragColor.rgb;
                     src.a =  gl_FragColor.a;
 
-                    // count += 1.0;
                     src.rgb *= src.a;
                     dst = (1.0 - dst.a) * src + dst; // standard blending
-                    // dst.rgb = vec3(1.0, 0.0, 0.0) * (count / 20.0);
-                    dst.a = min(1.0, dst.a);
-                    if(dst.a >= 1.0) {
-                        // dst.rgb = vec3(1.0, 0.0, 0.0);
-                        break;
-                    }
                 #endif
             }
             prevValue = value;
         #endif
 
+        #if defined(dRenderMode_volume)
+            isoPos = toUnit(pos);
+            vec4 mvPosition = uModelView * uTransform * vec4(isoPos * uGridDim, 1.0);
+            if (calcDepth(mvPosition.xyz) > getDepth(gl_FragCoord.xy / uViewport.zw))
+                break;
+
+            // bool flipped = value > uIsoValue.y; // negative isosurfaces
+            // interior = value < uIsoValue.x && flipped;
+            vec3 vViewPosition = mvPosition.xyz;
+            vec4 material = transferFunction(value);
+            src.a = material.a * uAlpha;
+
+            if (material.a >= 0.01) {
+                #ifdef dPackedGroup
+                    // compute gradient by central differences
+                    gradient.x = textureVal(isoPos - dx).a - textureVal(isoPos + dx).a;
+                    gradient.y = textureVal(isoPos - dy).a - textureVal(isoPos + dy).a;
+                    gradient.z = textureVal(isoPos - dz).a - textureVal(isoPos + dz).a;
+                #else
+                    gradient = cell.xyz * 2.0 - 1.0;
+                #endif
+                mat3 normalMatrix = transpose3(inverse3(mat3(uModelView * uUnitToCartn)));
+                vec3 normal = -normalize(normalMatrix * normalize(gradient));
+                // normal = normal * (float(flipped) * 2.0 - 1.0);
+                // normal = normal * -(float(interior) * 2.0 - 1.0);
+                #include apply_light_color
+                src.rgb = gl_FragColor.rgb;
+            } else {
+                src.rgb = material.rgb;
+            }
+
+            src.rgb *= src.a;
+            dst = (1.0 - dst.a) * src + dst; // standard blending
+        #endif
+
+        // break if the color is opaque enough
+        if (dst.a > 0.95)
+            break;
+
         pos += step;
     }
     return dst;
 }
 
 // TODO calculate normalMatrix on CPU
-// TODO fix orthographic projection
 // TODO fix near/far clipping
 // TODO support clip objects
-// TODO check and combine with pre-rendererd opaque texture
 // TODO support float texture for higher precision values
 
 void main () {
@@ -258,18 +324,16 @@ void main () {
                 discard; // ignore so the element below can be picked
         #endif
     #endif
-    // gl_FragColor = vec4(1.0, 0.0, 0.0, uAlpha);
 
     vec3 cameraPos = uInvView[3].xyz / uInvView[3].w;
+    vec3 rayDir = mix(normalize(origPos - uCameraPosition), uCameraDir, uIsOrtho);;
 
-    vec3 rayDir = normalize(origPos - cameraPos);
-    vec3 step = rayDir * (1.0 / uGridDim) * 0.2;
-    vec3 startLoc = unitCoord; // - step * float(uMaxSteps);
+    // TODO: set the scale as uniform?
+    float stepScale = min(uCellDim.x, min(uCellDim.y, uCellDim.z)) * uStepFactor;
+    vec3 step = rayDir * stepScale;
 
-    gl_FragColor = raymarch(startLoc, step, normalize(cameraPos), rayDir);
-    if (length(gl_FragColor.rgb) < 0.00001) discard;
-    #if defined(dRenderMode_volume)
-        gl_FragColor.a *= uAlpha;
-    #endif
+    gl_FragColor = raymarch(origPos, step);
+    if (length(gl_FragColor.rgb) < 0.00001)
+        discard;
 }
 `;
\ No newline at end of file
diff --git a/src/mol-gl/shader/direct-volume.vert.ts b/src/mol-gl/shader/direct-volume.vert.ts
index bf2361a6b99fbc7a7d3be8310f86de3f82b4d9b5..370e2cb053f873a165a39af9202b5ef2c04a45a0 100644
--- a/src/mol-gl/shader/direct-volume.vert.ts
+++ b/src/mol-gl/shader/direct-volume.vert.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -22,13 +22,16 @@ uniform vec3 uBboxMax;
 uniform vec3 uGridDim;
 uniform mat4 uTransform;
 
+uniform mat4 uUnitToCartn;
+uniform mat4 uCartnToUnit;
+
 uniform mat4 uModelView;
 uniform mat4 uProjection;
 
 void main() {
     unitCoord = aPosition + vec3(0.5);
-    vec4 mvPosition = uModelView * uTransform * vec4(unitCoord * uGridDim, 1.0);
-    origPos = unitCoord * uBboxSize + uBboxMin;
+    vec4 mvPosition = uModelView * uUnitToCartn * vec4(unitCoord, 1.0);
+    origPos = (uUnitToCartn * vec4(unitCoord, 1.0)).xyz;
     instance = aInstance;
     gl_Position = uProjection * mvPosition;
 
diff --git a/src/mol-gl/shader/lines.vert.ts b/src/mol-gl/shader/lines.vert.ts
index 5bbe0c42485706dd20872a260edacf3bcea898d7..f2fd212fe07b2d4ab9fac6b4b65ad10eaf2c5b75 100644
--- a/src/mol-gl/shader/lines.vert.ts
+++ b/src/mol-gl/shader/lines.vert.ts
@@ -32,8 +32,8 @@ attribute vec3 aEnd;
 void trimSegment(const in vec4 start, inout vec4 end) {
     // trim end segment so it terminates between the camera plane and the near plane
     // conservative estimate of the near plane
-    float a = uProjection[2][2];  // 3nd entry in 3th column
-    float b = uProjection[3][2];  // 3nd entry in 4th column
+    float a = uProjection[2][2];  // 3rd entry in 3rd column
+    float b = uProjection[3][2];  // 3rd entry in 4th column
     float nearEstimate = -0.5 * b / a;
     float alpha = (nearEstimate - start.z) / (end.z - start.z);
     end.xyz = mix(start.xyz, end.xyz, alpha);
diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts
index a75180878859fe7f1a9ef153b530cd96191b5b37..04d2864cfa23fb87280c970c2ca764a13db04064 100644
--- a/src/mol-gl/shader/postprocessing.frag.ts
+++ b/src/mol-gl/shader/postprocessing.frag.ts
@@ -4,7 +4,7 @@ precision highp int;
 precision highp sampler2D;
 
 uniform sampler2D tColor;
-uniform sampler2D tDepth;
+uniform sampler2D tPackedDepth;
 uniform vec2 uTexSize;
 
 uniform float uNear;
@@ -25,52 +25,48 @@ 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);
+    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);
+    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);
+    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;
+    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
+    #if dOrthographic == 1
+        return orthographicDepthToViewZ(depth, uNear, uFar);
+    #else
+        return perspectiveDepthToViewZ(depth, uNear, uFar);
+    #endif
 }
 
 float getDepth(const in vec2 coords) {
-	#ifdef dPackedDepth
-		return unpackRGBAToDepth(texture2D(tDepth, coords));
-	#else
-		return texture2D(tDepth, coords).r;
-	#endif
+    return unpackRGBAToDepth(texture2D(tPackedDepth, coords));
 }
 
 float calcSSAO(const in vec2 coords, const in float depth) {
-	float occlusionFactor = 0.0;
+    float occlusionFactor = 0.0;
 
-	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);
+    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;
-		}
-	}
+            if (getDepth(coordsDelta) < depth) occlusionFactor += 1.0;
+        }
+    }
 
-	return occlusionFactor / float((2 * dOcclusionKernelSize + 1) * (2 * dOcclusionKernelSize + 1));
+    return occlusionFactor / float((2 * dOcclusionKernelSize + 1) * (2 * dOcclusionKernelSize + 1));
 }
 
 vec2 calcEdgeDepth(const in vec2 coords) {
@@ -92,34 +88,34 @@ vec2 calcEdgeDepth(const in vec2 coords) {
     float depthFiniteDifference1 = depth3 - depth2;
 
     return vec2(
-		sqrt(pow(depthFiniteDifference0, 2.0) + pow(depthFiniteDifference1, 2.0)) * 100.0,
-		min(depth0, min(depth1, min(depth2, depth3)))
-	);
+        sqrt(pow(depthFiniteDifference0, 2.0) + pow(depthFiniteDifference1, 2.0)) * 100.0,
+        min(depth0, min(depth1, min(depth2, depth3)))
+    );
 }
 
 void main(void) {
-	vec2 coords = gl_FragCoord.xy / uTexSize;
-	vec4 color = texture2D(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);
-	#endif
-
-	// occlusion needs to be handled after outline to darken them properly
-	#ifdef dOcclusionEnable
-		float depth = getDepth(coords);
-		if (depth != 1.0) {
-			float occlusionFactor = calcSSAO(coords, depth);
-			color = mix(color, occlusionColor, uOcclusionBias * occlusionFactor);
-		}
-	#endif
-
-	gl_FragColor = color;
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    vec4 color = texture2D(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);
+    #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);
+        }
+    #endif
+
+    gl_FragColor = color;
 }
 `;
\ No newline at end of file
diff --git a/src/mol-gl/webgl/program.ts b/src/mol-gl/webgl/program.ts
index 8f67d8ce062f3a1d8a1231cf4095467619c742b4..b968e5dd69c530108ca6788a24befbdc4cc30592 100644
--- a/src/mol-gl/webgl/program.ts
+++ b/src/mol-gl/webgl/program.ts
@@ -200,11 +200,17 @@ export function createProgram(gl: GLRenderingContext, state: WebGLState, extensi
             for (let i = 0, il = textures.length; i < il; ++i) {
                 const [k, texture] = textures[i];
                 const l = locations[k];
-                if (l !== null) {
-                    // TODO if the order and count of textures in a material can be made invariant
-                    //      bind needs to be called only when the material changes
-                    texture.bind(i as TextureId);
-                    uniformSetters[k](gl, l, i as TextureId);
+                if (l !== null && l !== undefined) {
+                    if (k === 'tDepth') {
+                        // TODO find more explicit way?
+                        texture.bind(15 as TextureId);
+                        uniformSetters[k](gl, l, 15 as TextureId);
+                    } else {
+                        // TODO if the order and count of textures in a material can be made invariant
+                        //      bind needs to be called only when the material changes
+                        texture.bind(i as TextureId);
+                        uniformSetters[k](gl, l, i as TextureId);
+                    }
                 }
             }
         },
diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts
index 3552738dd0278ea54f9550d53954a6b9d4937f78..f0f8205a37ec7f1f9e11a40dead7812942784eef 100644
--- a/src/mol-gl/webgl/texture.ts
+++ b/src/mol-gl/webgl/texture.ts
@@ -154,12 +154,6 @@ export type TextureId = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 1
 export type TextureValues = { [k: string]: ValueCell<TextureValueType> }
 export type Textures = [string, Texture][]
 
-type FramebufferAttachment = {
-    framebuffer: Framebuffer
-    attachment: TextureAttachment
-    layer?: number
-}
-
 function getTexture(gl: GLRenderingContext) {
     const texture = gl.createTexture();
     if (texture === null) {
@@ -167,7 +161,7 @@ function getTexture(gl: GLRenderingContext) {
     }
     return texture;
 }
-// export type TextureProps = { kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter }
+
 export function createTexture(gl: GLRenderingContext, extensions: WebGLExtensions, kind: TextureKind, _format: TextureFormat, _type: TextureType, _filter: TextureFilter): Texture {
     const id = getNextTextureId();
     let texture = getTexture(gl);
@@ -198,8 +192,6 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
     }
     init();
 
-    let fba: undefined | FramebufferAttachment = undefined;
-
     let width = 0, height = 0, depth = 0;
     let loadedData: undefined | TextureImage<any> | TextureVolume<any>;
     let destroyed = false;
@@ -237,9 +229,6 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
     }
 
     function attachFramebuffer(framebuffer: Framebuffer, attachment: TextureAttachment, layer?: number) {
-        if (fba && fba.framebuffer === framebuffer && fba.attachment === attachment && fba.layer === layer) {
-            return;
-        }
         framebuffer.bind();
         if (target === gl.TEXTURE_2D) {
             gl.framebufferTexture2D(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), gl.TEXTURE_2D, texture, 0);
@@ -249,7 +238,6 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
         } else {
             throw new Error('unknown texture target');
         }
-        fba = { framebuffer, attachment, layer };
     }
 
     return {
@@ -283,7 +271,6 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
             } else {
                 throw new Error('unknown texture target');
             }
-            fba = undefined;
         },
         reset: () => {
             texture = getTexture(gl);
@@ -294,12 +281,6 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
             } else {
                 define(width, height, depth);
             }
-
-            if (fba) {
-                // TODO unclear why calling `attachFramebuffer` here does not work reliably after context loss
-                // e.g. it still needs to be called in `DrawPass` to work
-                fba = undefined;
-            }
         },
         destroy: () => {
             if (destroyed) return;
@@ -315,12 +296,15 @@ export function createTextures(ctx: WebGLContext, schema: RenderableSchema, valu
     Object.keys(schema).forEach(k => {
         const spec = schema[k];
         if (spec.type === 'texture') {
-            if (spec.kind === 'texture') {
-                textures[textures.length] = [k, values[k].ref.value as Texture];
-            } else {
-                const texture = resources.texture(spec.kind, spec.format, spec.dataType, spec.filter);
-                texture.load(values[k].ref.value as TextureImage<any> | TextureVolume<any>);
-                textures[textures.length] = [k, texture];
+            const value = values[k];
+            if (value) {
+                if (spec.kind === 'texture') {
+                    textures[textures.length] = [k, value.ref.value as Texture];
+                } else {
+                    const texture = resources.texture(spec.kind, spec.format, spec.dataType, spec.filter);
+                    texture.load(value.ref.value as TextureImage<any> | TextureVolume<any>);
+                    textures[textures.length] = [k, texture];
+                }
             }
         }
     });
diff --git a/src/mol-gl/webgl/uniform.ts b/src/mol-gl/webgl/uniform.ts
index f3e6783e37d365c1ea0096455acebde9276b24c8..abc415c5923c7d2635099253a890a0218a57d882 100644
--- a/src/mol-gl/webgl/uniform.ts
+++ b/src/mol-gl/webgl/uniform.ts
@@ -71,7 +71,7 @@ export function getUniformSetters(schema: RenderableSchema) {
     Object.keys(schema).forEach(k => {
         const spec = schema[k];
         if (spec.type === 'uniform') {
-            setters[k] = getUniformSetter(spec.kind as UniformKind);
+            setters[k] = getUniformSetter(spec.kind);
         } else if (spec.type === 'texture') {
             setters[k] = getUniformSetter('t');
         }
diff --git a/src/mol-repr/structure/visual/gaussian-density-volume.ts b/src/mol-repr/structure/visual/gaussian-density-volume.ts
index 1344fbeff25493c42094caaca0199b99712df493..cdb7e5e66f129a86a4dc75d6a2838466a6f06446 100644
--- a/src/mol-repr/structure/visual/gaussian-density-volume.ts
+++ b/src/mol-repr/structure/visual/gaussian-density-volume.ts
@@ -11,10 +11,9 @@ import { Theme } from '../../../mol-theme/theme';
 import { GaussianDensityTextureProps, computeStructureGaussianDensityTexture, GaussianDensityTextureParams } from './util/gaussian';
 import { DirectVolume } from '../../../mol-geo/geometry/direct-volume/direct-volume';
 import { ComplexDirectVolumeParams, ComplexVisual, ComplexDirectVolumeVisual } from '../complex-visual';
-import { LocationIterator } from '../../../mol-geo/util/location-iterator';
-import { NullLocation } from '../../../mol-model/location';
-import { EmptyLoci } from '../../../mol-model/loci';
 import { VisualUpdateState } from '../../util';
+import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
+import { eachSerialElement, ElementIterator, getSerialElementLoci } from './util/element';
 
 async function createGaussianDensityVolume(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, directVolume?: DirectVolume): Promise<DirectVolume> {
     const { runtime, webgl } = ctx;
@@ -26,7 +25,10 @@ async function createGaussianDensityVolume(ctx: VisualContext, structure: Struct
     const { transform, texture, bbox, gridDim } = densityTextureData;
     const stats = { min: 0, max: 1, mean: 0.5, sigma: 0.1 };
 
-    return DirectVolume.create(bbox, gridDim, transform, texture, stats, directVolume);
+    const unitToCartn = Mat4.mul(Mat4(), transform, Mat4.fromScaling(Mat4(), gridDim));
+    const cellDim = Vec3.create(1, 1, 1);
+
+    return DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, directVolume);
 }
 
 export const GaussianDensityVolumeParams = {
@@ -40,9 +42,9 @@ export function GaussianDensityVolumeVisual(materialId: number): ComplexVisual<G
     return ComplexDirectVolumeVisual<GaussianDensityVolumeParams>({
         defaultProps: PD.getDefaultValues(GaussianDensityVolumeParams),
         createGeometry: createGaussianDensityVolume,
-        createLocationIterator: (structure: Structure) => LocationIterator(structure.elementCount, 1, () => NullLocation),
-        getLoci: () => EmptyLoci, // TODO
-        eachLocation: () => false, // TODO
+        createLocationIterator: ElementIterator.fromStructure,
+        getLoci: getSerialElementLoci,
+        eachLocation: eachSerialElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianDensityVolumeParams>, currentProps: PD.Values<GaussianDensityVolumeParams>) => {
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true;
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true;
diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts
index 736fbe203bcbd3c2afaeb86875db1ec82b9c71fd..33073f7e5dfc9fdd06701943dedcaf716ff7d020 100644
--- a/src/mol-repr/volume/direct-volume.ts
+++ b/src/mol-repr/volume/direct-volume.ts
@@ -22,10 +22,9 @@ import { Interval } from '../../mol-data/int';
 import { Loci, EmptyLoci } from '../../mol-model/loci';
 import { PickingId } from '../../mol-geo/geometry/picking';
 import { eachVolumeLoci } from './util';
-import { encodeFloatRGBtoArray } from '../../mol-util/float-packing';
 
 function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
-    const bbox = Box3D.setEmpty(Box3D());
+    const bbox = Box3D();
     Box3D.add(bbox, gridDimension);
     Box3D.transform(bbox, bbox, transform);
     return bbox;
@@ -54,7 +53,7 @@ function getVolumeTexture2dLayout(dim: Vec3, maxTextureSize: number) {
 function createVolumeTexture2d(volume: Volume, maxTextureSize: number) {
     const { cells: { space, data }, stats: { max, min } } = volume.grid;
     const dim = space.dimensions as Vec3;
-    const { dataOffset } = space;
+    const { dataOffset: o } = space;
     const { width, height } = getVolumeTexture2dLayout(dim, maxTextureSize);
 
     const array = new Uint8Array(width * height * 4);
@@ -65,6 +64,10 @@ function createVolumeTexture2d(volume: Volume, maxTextureSize: number) {
     const xlp = xl + 1; // horizontal padding
     const ylp = yl + 1; // vertical padding
 
+    const n0 = Vec3();
+    const n1 = Vec3();
+
+    let i = 0;
     for (let z = 0; z < zl; ++z) {
         for (let y = 0; y < yl; ++y) {
             for (let x = 0; x < xl; ++x) {
@@ -72,9 +75,16 @@ function createVolumeTexture2d(volume: Volume, maxTextureSize: number) {
                 const row = Math.floor((z * xlp) / width);
                 const px = column * xlp + x;
                 const index = 4 * ((row * ylp * width) + (y * width) + px);
-                const offset = dataOffset(x, y, z);
-                encodeFloatRGBtoArray(offset, array, index);
+                const offset = o(x, y, z);
+
+                Vec3.set(n0, data[o(x - 1, y, z)], data[o(x, y - 1, z)], data[o(x, y, z - 1)]);
+                Vec3.set(n1, data[o(x + 1, y, z)], data[o(x, y + 1, z)], data[o(x, y, z + 1)]);
+                Vec3.normalize(n0, Vec3.sub(n0, n0, n1));
+                Vec3.addScalar(n0, Vec3.scale(n0, n0, 0.5), 0.5);
+                Vec3.toArray(Vec3.scale(n0, n0, 255), array, i);
+
                 array[index + 3] = ((data[offset] - min) / diff) * 255;
+                i += 4;
             }
         }
     }
@@ -95,7 +105,8 @@ export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, v
     const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
     texture.load(textureImage);
 
-    return DirectVolume.create(bbox, dim, transform, texture, volume.grid.stats, directVolume);
+    const { unitToCartn, cellDim } = getUnitToCartn(volume.grid);
+    return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, directVolume);
 }
 
 // 3d volume texture
@@ -103,18 +114,27 @@ export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, v
 function createVolumeTexture3d(volume: Volume) {
     const { cells: { space, data }, stats: { max, min } } = volume.grid;
     const [ width, height, depth ] = space.dimensions as Vec3;
-    const { dataOffset } = space;
+    const { dataOffset: o } = space;
 
     const array = new Uint8Array(width * height * depth * 4);
     const textureVolume = { array, width, height, depth };
     const diff = max - min;
 
+    const n0 = Vec3();
+    const n1 = Vec3();
+
     let i = 0;
     for (let z = 0; z < depth; ++z) {
         for (let y = 0; y < height; ++y) {
             for (let x = 0; x < width; ++x) {
-                const offset = dataOffset(x, y, z);
-                encodeFloatRGBtoArray(offset, array, i);
+                const offset = o(x, y, z);
+
+                Vec3.set(n0, data[o(x - 1, y, z)], data[o(x, y - 1, z)], data[o(x, y, z - 1)]);
+                Vec3.set(n1, data[o(x + 1, y, z)], data[o(x, y + 1, z)], data[o(x, y, z + 1)]);
+                Vec3.normalize(n0, Vec3.sub(n0, n0, n1));
+                Vec3.addScalar(n0, Vec3.scale(n0, n0, 0.5), 0.5);
+                Vec3.toArray(Vec3.scale(n0, n0, 255), array, i);
+
                 array[i + 3] = ((data[offset] - min) / diff) * 255;
                 i += 4;
             }
@@ -124,6 +144,27 @@ function createVolumeTexture3d(volume: Volume) {
     return textureVolume;
 }
 
+function getUnitToCartn(grid: Grid) {
+    if (grid.transform.kind === 'matrix') {
+        // TODO:
+        return {
+            unitToCartn: Mat4.mul(Mat4(),
+                Grid.getGridToCartesianTransform(grid),
+                Mat4.fromScaling(Mat4(), grid.cells.space.dimensions as Vec3)),
+            cellDim: Vec3.create(1, 1, 1)
+        };
+    }
+    const box = grid.transform.fractionalBox;
+    const size = Box3D.size(Vec3(), box);
+    return {
+        unitToCartn: Mat4.mul3(Mat4(),
+            grid.transform.cell.fromFractional,
+            Mat4.fromTranslation(Mat4(), box.min),
+            Mat4.fromScaling(Mat4(), size)),
+        cellDim: Vec3.div(Vec3(), grid.transform.cell.size, grid.cells.space.dimensions as Vec3)
+    };
+}
+
 export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
     const gridDimension = volume.grid.cells.space.dimensions as Vec3;
     const textureVolume = createVolumeTexture3d(volume);
@@ -133,7 +174,8 @@ export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, v
     const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear');
     texture.load(textureVolume);
 
-    return DirectVolume.create(bbox, gridDimension, transform, texture, volume.grid.stats, directVolume);
+    const { unitToCartn, cellDim } = getUnitToCartn(volume.grid);
+    return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, directVolume);
 }
 
 //