diff --git a/README.md b/README.md
index a03c9462eb3ba0a884e85a3f5facb8de1830d000..839436bb0b9a1c8d69ce66bba6ba4bc39aec0fbb 100644
--- a/README.md
+++ b/README.md
@@ -55,19 +55,9 @@ This project builds on experience from previous solutions:
 
 ### Build automatically on file save:
     npm run watch
-    npm run watch-extra
 
-### Build/watch mol-viewer
-**Build**
-
-    npm run build
-    npm run build-viewer
-
-**Watch**
-
-    npm run watch
-    npm run watch-extra
-    npm run watch-viewer
+### With debug mode enabled:
+    DEBUG=molstar npm run watch
 
 **Run**
 
diff --git a/package-lock.json b/package-lock.json
index 8f86e5adeac89d096105360c76b4de9fcdd01ed1..f4e425fdcc74f07d5a4ffd8e8134939c45e58860 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index 430c3c900dc84aab9210303940f250a3f5431885..848f310b9d4b42ad1c37a3d5fafdf1ebbb1f5745 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
     "watch-ts": "tsc -watch",
     "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/src/ --watch",
     "build-webpack": "webpack --mode production",
-    "watch-webpack": "webpack -w --mode development",
+    "watch-webpack": "webpack -w --mode development --display minimal",
     "model-server": "node build/src/servers/model/server.js",
     "model-server-watch": "nodemon --watch build/src build/src/servers/model/server.js"
   },
@@ -79,52 +79,52 @@
     "@types/benchmark": "^1.0.31",
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.1",
-    "@types/jest": "^24.0.9",
-    "@types/node": "^11.10.4",
-    "@types/node-fetch": "^2.1.6",
-    "@types/react": "^16.8.6",
-    "@types/react-dom": "^16.8.2",
+    "@types/jest": "^24.0.11",
+    "@types/node": "^11.13.4",
+    "@types/node-fetch": "^2.3.2",
+    "@types/react": "^16.8.13",
+    "@types/react-dom": "^16.8.4",
     "@types/webgl2": "0.0.4",
     "@types/swagger-ui-dist": "3.0.0",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.0.2",
     "concurrently": "^4.1.0",
     "cpx": "^1.5.0",
-    "css-loader": "^2.1.0",
+    "css-loader": "^2.1.1",
     "extra-watch-webpack-plugin": "^1.0.3",
     "file-loader": "^3.0.1",
     "glslify": "^7.0.0",
     "glslify-import": "^3.1.0",
     "glslify-loader": "^2.0.0",
-    "graphql-code-generator": "^0.18.0",
-    "graphql-codegen-time": "^0.18.0",
-    "graphql-codegen-typescript-template": "^0.18.0",
-    "jest": "^24.1.0",
+    "graphql-code-generator": "^0.18.1",
+    "graphql-codegen-time": "^0.18.1",
+    "graphql-codegen-typescript-template": "^0.18.1",
+    "jest": "^24.7.1",
     "jest-raw-loader": "^1.0.1",
-    "mini-css-extract-plugin": "^0.5.0",
+    "mini-css-extract-plugin": "^0.6.0",
     "node-sass": "^4.11.0",
-    "raw-loader": "^1.0.0",
-    "resolve-url-loader": "^3.0.1",
+    "raw-loader": "^2.0.0",
+    "resolve-url-loader": "^3.1.0",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.1",
-    "ts-jest": "^24.0.0",
-    "tslint": "^5.13.1",
-    "typescript": "^3.3.3",
-    "uglify-js": "^3.4.9",
+    "ts-jest": "^24.0.2",
+    "tslint": "^5.15.0",
+    "typescript": "^3.4.3",
+    "uglify-js": "^3.5.4",
     "util.promisify": "^1.0.0",
-    "webpack": "^4.29.6",
-    "webpack-cli": "^3.2.3"
+    "webpack": "^4.30.0",
+    "webpack-cli": "^3.3.0"
   },
   "dependencies": {
     "argparse": "^1.0.10",
-    "compression": "^1.7.3",
+    "compression": "^1.7.4",
     "express": "^4.16.4",
-    "graphql": "^14.1.1",
+    "graphql": "^14.2.1",
     "immutable": "^3.8.2",
     "node-fetch": "^2.3.0",
-    "react": "^16.8.4",
-    "react-dom": "^16.8.4",
+    "react": "^16.8.6",
+    "react-dom": "^16.8.6",
     "rxjs": "^6.4.0",
-    "swagger-ui-dist": "^3.21.0"
+    "swagger-ui-dist": "^3.22.1"
   }
 }
diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts
index aaa87f3d9a2788661fd03c5371bb4af8124c4634..9ce98ddac0cc0b27808a39f163dc91f8df875cca 100644
--- a/src/apps/basic-wrapper/index.ts
+++ b/src/apps/basic-wrapper/index.ts
@@ -50,7 +50,7 @@ class BasicWrapper {
 
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
     }
 
@@ -98,7 +98,8 @@ class BasicWrapper {
     }
 
     setBackground(color: number) {
-        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } });
+        const renderer = this.plugin.canvas3d.props.renderer;
+        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer,  backgroundColor: Color(color) } } });
     }
 
     toggleSpin() {
diff --git a/src/apps/model-server-query/index.tsx b/src/apps/model-server-query/index.tsx
index a34f8617c8c3d70dfaf792023a9bd3a0f3a3b11d..0c143ceba2f121b03f34d0dd8151fe83e5f3ff0e 100644
--- a/src/apps/model-server-query/index.tsx
+++ b/src/apps/model-server-query/index.tsx
@@ -105,7 +105,7 @@ const state: State = {
     query: new Rx.BehaviorSubject(QueryList[1].definition),
     id: new Rx.BehaviorSubject('1cbs'),
     params: new Rx.BehaviorSubject({ }),
-    isBinary: new Rx.BehaviorSubject(false),
+    isBinary: new Rx.BehaviorSubject<boolean>(false),
     models: new Rx.BehaviorSubject<number[]>([]),
     url: new Rx.Subject()
 }
diff --git a/src/apps/viewer/extensions/jolecule.ts b/src/apps/viewer/extensions/jolecule.ts
index cdf0026b39a4201f802fe01f18bc736c2ca0c71e..1ce9c969a9a05c522b3f14044ce81b56be030abf 100644
--- a/src/apps/viewer/extensions/jolecule.ts
+++ b/src/apps/viewer/extensions/jolecule.ts
@@ -57,7 +57,7 @@ interface JoleculeSnapshot {
 
 function createTemplate(plugin: PluginContext, state: State, id: string) {
     const b = new StateBuilder.Root(state.tree);
-    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { props: { isGhost: true }});
+    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { state: { isGhost: true }});
     const model = createModelTree(data, 'cif');
     const structure = model.apply(StateTransforms.Model.StructureFromModel, {});
     complexRepresentation(plugin, structure, { hideWater: true });
diff --git a/src/examples/proteopedia-wrapper/changelog.md b/src/examples/proteopedia-wrapper/changelog.md
index 041ecacd3454949a8be9e03ad2a6284c5b9f8381..9680f701e7da996b4d28c9627f87833d9571539d 100644
--- a/src/examples/proteopedia-wrapper/changelog.md
+++ b/src/examples/proteopedia-wrapper/changelog.md
@@ -1,3 +1,11 @@
+== v3.0 ==
+
+* Fixed initial camera zoom.
+* Custom chain coloring.
+* Customize visualizations.
+* Show ligand list.
+* Show 3D-SNFG.
+
 == v2.0 ==
 
 * Changed how state saving works.
diff --git a/src/examples/proteopedia-wrapper/coloring.ts b/src/examples/proteopedia-wrapper/coloring.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a2199b6edbc72f94989016b1e97de943d467e6aa
--- /dev/null
+++ b/src/examples/proteopedia-wrapper/coloring.ts
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+
+import { Unit, StructureProperties, StructureElement, Link } from 'mol-model/structure';
+
+import { Color } from 'mol-util/color';
+import { Location } from 'mol-model/location';
+import { ColorTheme, LocationColor } from 'mol-theme/color';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { ThemeDataContext } from 'mol-theme/theme';
+import { Column } from 'mol-data/db';
+
+const Description = 'Gives every chain a color from a list based on its `asym_id` value.'
+
+export function createProteopediaCustomTheme(colors: number[]) {
+    const ProteopediaCustomColorThemeParams = {
+        colors: PD.ObjectList({ color: PD.Color(Color(0xffffff)) }, ({ color }) => Color.toHexString(color),
+            { defaultValue: colors.map(c => ({ color: Color(c) })) })
+    }
+    type ProteopediaCustomColorThemeParams = typeof ProteopediaCustomColorThemeParams
+    function getChainIdColorThemeParams(ctx: ThemeDataContext) {
+        return ProteopediaCustomColorThemeParams // TODO return copy
+    }
+
+    function getAsymId(unit: Unit): StructureElement.Property<string> {
+        switch (unit.kind) {
+            case Unit.Kind.Atomic:
+                return StructureProperties.chain.label_asym_id
+            case Unit.Kind.Spheres:
+            case Unit.Kind.Gaussians:
+                return StructureProperties.coarse.asym_id
+        }
+    }
+
+    function addAsymIds(map: Map<string, number>, data: Column<string>) {
+        let j = map.size
+        for (let o = 0, ol = data.rowCount; o < ol; ++o) {
+            const k = data.value(o)
+            if (!map.has(k)) {
+                map.set(k, j)
+                j += 1
+            }
+        }
+    }
+
+    function ProteopediaCustomColorTheme(ctx: ThemeDataContext, props: PD.Values<ProteopediaCustomColorThemeParams>): ColorTheme<ProteopediaCustomColorThemeParams> {
+        let color: LocationColor
+
+        const colors = props.colors, colorCount = colors.length, defaultColor = colors[0].color;
+
+        if (ctx.structure) {
+            const l = StructureElement.create()
+            const { models } = ctx.structure
+            const asymIdSerialMap = new Map<string, number>()
+            for (let i = 0, il = models.length; i < il; ++i) {
+                const m = models[i]
+                addAsymIds(asymIdSerialMap, m.atomicHierarchy.chains.label_asym_id)
+                if (m.coarseHierarchy.isDefined) {
+                    addAsymIds(asymIdSerialMap, m.coarseHierarchy.spheres.asym_id)
+                    addAsymIds(asymIdSerialMap, m.coarseHierarchy.gaussians.asym_id)
+                }
+            }
+
+            color = (location: Location): Color => {
+                if (StructureElement.isLocation(location)) {
+                    const asym_id = getAsymId(location.unit);
+                    const o = asymIdSerialMap.get(asym_id(location)) || 0;
+                    return colors[o % colorCount].color;
+                } else if (Link.isLocation(location)) {
+                    const asym_id = getAsymId(location.aUnit)
+                    l.unit = location.aUnit
+                    l.element = location.aUnit.elements[location.aIndex]
+                    const o = asymIdSerialMap.get(asym_id(l)) || 0;
+                    return colors[o % colorCount].color;
+                }
+                return defaultColor
+            }
+        } else {
+            color = () => defaultColor
+        }
+
+        return {
+            factory: ProteopediaCustomColorTheme,
+            granularity: 'group',
+            color,
+            props,
+            description: Description,
+            legend: undefined
+        }
+    }
+
+    const ProteopediaCustomColorThemeProvider: ColorTheme.Provider<ProteopediaCustomColorThemeParams> = {
+        label: 'Proteopedia Custom',
+        factory: ProteopediaCustomColorTheme,
+        getParams: getChainIdColorThemeParams,
+        defaultValues: PD.getDefaultValues(ProteopediaCustomColorThemeParams),
+        isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
+    }
+
+    return ProteopediaCustomColorThemeProvider;
+}
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts
index 427fb057e0b423573a13bb1ea934a977e75a8845..17082c93c22762304fcf3c7357e623bd6e3524c3 100644
--- a/src/examples/proteopedia-wrapper/helpers.ts
+++ b/src/examples/proteopedia-wrapper/helpers.ts
@@ -92,9 +92,26 @@ export interface LoadParams {
 export interface RepresentationStyle {
     sequence?: RepresentationStyle.Entry,
     hetGroups?: RepresentationStyle.Entry,
+    snfg3d?: { hide?: boolean },
     water?: RepresentationStyle.Entry
 }
 
 export namespace RepresentationStyle {
-    export type Entry = { kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName }
+    export type Entry = { hide?: boolean, kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName }
+}
+
+export enum StateElements {
+    Model = 'model',
+    ModelProps = 'model-props',
+    Assembly = 'assembly',
+
+    Sequence = 'sequence',
+    SequenceVisual = 'sequence-visual',
+    Het = 'het',
+    HetVisual = 'het-visual',
+    Het3DSNFG = 'het-3dsnfg',
+    Water = 'water',
+    WaterVisual = 'water-visual',
+
+    HetGroupFocus = 'het-group-focus'
 }
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/index.html b/src/examples/proteopedia-wrapper/index.html
index 009eb5bf8193a534a6a2c21e342e1c47cfb29e6d..288db037a5d4f2406c2a3b486fd2417dde62e592 100644
--- a/src/examples/proteopedia-wrapper/index.html
+++ b/src/examples/proteopedia-wrapper/index.html
@@ -55,11 +55,14 @@
             </select>
         </div>
         <div id="app"></div>
-        <script>  
+        <script>
+            // it might be a good idea to define these colors in a separate script file 
+            var CustomColors = [0x00ff00, 0x0000ff];
+
             // create an instance of the plugin
             var PluginWrapper = new MolStarProteopediaWrapper();
 
-            console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR);
+            console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR, MolStarProteopediaWrapper.VERSION_MINOR);
 
             function $(id) { return document.getElementById(id); }
         
@@ -78,13 +81,23 @@
             // var format = 'pdb';
             // var assemblyId = 'deposited';
 
-            PluginWrapper.init('app' /** or document.getElementById('app') */);
+            var representationStyle = {
+                sequence: { coloring: 'proteopedia-custom' }, // or just { }
+                hetGroups: { kind: 'ball-and-stick' }, // or 'spacefill
+                water: { hide: true },
+                snfg3d: { hide: false }
+            };
+
+            PluginWrapper.init('app' /** or document.getElementById('app') */, {
+                customColorList: CustomColors
+            });
             PluginWrapper.setBackground(0xffffff);
-            PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId });
+            PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId, representationStyle: representationStyle });
             PluginWrapper.toggleSpin();
 
             PluginWrapper.events.modelInfo.subscribe(function (info) {
                 console.log('Model Info', info);
+                listHetGroups(info);
             });
 
             addControl('Load Asym Unit', () => PluginWrapper.load({ url: url, format: format }));
@@ -92,6 +105,22 @@
 
             addSeparator();
 
+            addHeader('Representation');
+
+            addControl('Custom Chain Colors', () => PluginWrapper.updateStyle({ sequence: { coloring: 'proteopedia-custom' } }, true));
+            addControl('Default Chain Colors', () => PluginWrapper.updateStyle({ sequence: { } }, true));
+
+            addControl('HET Spacefill', () => PluginWrapper.updateStyle({ hetGroups: { kind: 'spacefill' } }, true));
+            addControl('HET Ball-and-stick', () => PluginWrapper.updateStyle({ hetGroups: { kind: 'ball-and-stick' } }, true));
+
+            addControl('Hide 3DSNFG', () => PluginWrapper.updateStyle({ snfg3d: { hide: true } }, true));
+            addControl('Show 3DSNFG', () => PluginWrapper.updateStyle({ snfg3d: { hide: false } }, true));
+
+            addControl('Hide Water', () => PluginWrapper.updateStyle({ water: { hide: true } }, true));
+            addControl('Show Water', () => PluginWrapper.updateStyle({ water: { hide: false } }, true));
+
+            addSeparator();
+
             addHeader('Camera');
             addControl('Toggle Spin', () => PluginWrapper.toggleSpin());
             
@@ -115,6 +144,12 @@
             addControl('Apply Evo Cons', () => PluginWrapper.coloring.evolutionaryConservation());
             addControl('Default Visuals', () => PluginWrapper.updateStyle());
 
+            addSeparator();
+            addHeader('HET Groups');
+
+            addControl('Reset', () => PluginWrapper.hetGroups.reset());
+            addHetGroupsContainer();
+
             addSeparator();
             addHeader('State');
 
@@ -133,6 +168,12 @@
 
             ////////////////////////////////////////////////////////
 
+            function addHetGroupsContainer() {
+                var div = document.createElement('div');
+                div.id = 'het-groups';
+                $('controls').appendChild(div);
+            }
+
             function addControl(label, action) {
                 var btn = document.createElement('button');
                 btn.onclick = action;
@@ -150,6 +191,19 @@
                 h.innerText = header;
                 $('controls').appendChild(h);
             }
+
+            function listHetGroups(info) {
+                var div = $('het-groups');
+                div.innerHTML = '';
+                info.hetResidues.forEach(function (r) {
+                    var l = document.createElement('button');
+                    l.innerText = r.name;
+                    l.onclick = function () {
+                        PluginWrapper.hetGroups.focusFirst(r.name);
+                    };
+                    div.appendChild(l);
+                });
+            }
         </script>
     </body>
 </html>
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts
index 91677335b5ee870e06931e007c0ca3b7857db417..c72a1adbe4107474d221a2d29694de44a00b7fe6 100644
--- a/src/examples/proteopedia-wrapper/index.ts
+++ b/src/examples/proteopedia-wrapper/index.ts
@@ -15,15 +15,25 @@ import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/ob
 import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in';
 import { StateBuilder, StateObject } from 'mol-state';
 import { EvolutionaryConservation } from './annotation';
-import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo } from './helpers';
+import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo, StateElements } from './helpers';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { ControlsWrapper } from './ui/controls';
 import { PluginState } from 'mol-plugin/state';
+import { Scheduler } from 'mol-task';
+import { createProteopediaCustomTheme } from './coloring';
+import { MolScriptBuilder as MS } from 'mol-script/language/builder';
+import { BuiltInStructureRepresentations } from 'mol-repr/structure/registry';
+import { BuiltInColorThemes } from 'mol-theme/color';
+import { BuiltInSizeThemes } from 'mol-theme/size';
+import { ColorNames } from 'mol-util/color/tables';
+// import { Vec3 } from 'mol-math/linear-algebra';
+// import { ParamDefinition } from 'mol-util/param-definition';
+// import { Text } from 'mol-geo/geometry/text/text';
 require('mol-plugin/skin/light.scss')
 
 class MolStarProteopediaWrapper {
-    static VERSION_MAJOR = 2;
-    static VERSION_MINOR = 0;
+    static VERSION_MAJOR = 3;
+    static VERSION_MINOR = 1;
 
     private _ev = RxEventHelper.create();
 
@@ -33,9 +43,14 @@ class MolStarProteopediaWrapper {
 
     plugin: PluginContext;
 
-    init(target: string | HTMLElement) {
+    init(target: string | HTMLElement, options?: {
+        customColorList?: number[]
+    }) {
         this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
             ...DefaultPluginSpec,
+            animations: [
+                AnimateModelIndex
+            ],
             layout: {
                 initial: {
                     isExpanded: false,
@@ -47,6 +62,9 @@ class MolStarProteopediaWrapper {
             }
         });
 
+        const customColoring = createProteopediaCustomTheme((options && options.customColorList) || []);
+
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add('proteopedia-custom', customColoring);
         this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.Descriptor.name, EvolutionaryConservation.colorTheme!);
         this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider);
         this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider);
@@ -66,43 +84,86 @@ class MolStarProteopediaWrapper {
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
 
         return parsed
-            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' });
+            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: StateElements.Model });
     }
 
     private structure(assemblyId: string) {
-        const model = this.state.build().to('model');
+        const model = this.state.build().to(StateElements.Model);
+
+        const s = model
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: StateElements.ModelProps, state: { isGhost: false } })
+            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly });
 
-        return model
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+        s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence });
+        s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: StateElements.Het });
+        s.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: StateElements.Water });
+
+        return s;
     }
 
-    private visual(ref: string, style?: RepresentationStyle) {
-        const structure = this.getObj<PluginStateObject.Molecule.Structure>(ref);
+    private visual(_style?: RepresentationStyle, partial?: boolean) {
+        const structure = this.getObj<PluginStateObject.Molecule.Structure>(StateElements.Assembly);
         if (!structure) return;
 
-        const root = this.state.build().to(ref);
-
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: 'sequence' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D,
-                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
-                    (style && style.sequence && style.sequence.kind) || 'cartoon',
-                    (style && style.sequence && style.sequence.coloring) || 'unit-index', structure),
-                    { ref: 'sequence-visual' });
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: 'het' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D,
-                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
-                    (style && style.hetGroups && style.hetGroups.kind) || 'ball-and-stick',
-                    (style && style.hetGroups && style.hetGroups.coloring), structure),
-                    { ref: 'het-visual' });
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: 'water' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D,
-                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
-                    (style && style.water && style.water.kind) || 'ball-and-stick',
-                    (style && style.water && style.water.coloring), structure, { alpha: 0.51 }),
-                    { ref: 'water-visual' });
-
-        return root;
+        const style = _style || { };
+
+        const update = this.state.build();
+
+        if (!partial || (partial && style.sequence)) {
+            const root = update.to(StateElements.Sequence);
+            if (style.sequence && style.sequence.hide) {
+                root.delete(StateElements.SequenceVisual);
+            } else {
+                root.applyOrUpdate(StateElements.SequenceVisual, StateTransforms.Representation.StructureRepresentation3D,
+                    StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                        (style.sequence && style.sequence.kind) || 'cartoon',
+                        (style.sequence && style.sequence.coloring) || 'unit-index', structure));
+            }
+        }
+
+        if (!partial || (partial && style.hetGroups)) {
+            const root = update.to(StateElements.Het);
+            if (style.hetGroups && style.hetGroups.hide) {
+                root.delete(StateElements.HetVisual);
+            } else {
+                if (style.hetGroups && style.hetGroups.hide) {
+                    root.delete(StateElements.HetVisual);
+                } else {
+                    root.applyOrUpdate(StateElements.HetVisual, StateTransforms.Representation.StructureRepresentation3D,
+                        StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                            (style.hetGroups && style.hetGroups.kind) || 'ball-and-stick',
+                            (style.hetGroups && style.hetGroups.coloring), structure));
+                }
+            }
+        }
+
+        if (!partial || (partial && style.snfg3d)) {
+            const root = update.to(StateElements.Het);
+            if (style.hetGroups && style.hetGroups.hide) {
+                root.delete(StateElements.HetVisual);
+            } else {
+                if (style.snfg3d && style.snfg3d.hide) {
+                    root.delete(StateElements.Het3DSNFG);
+                } else {
+                    root.applyOrUpdate(StateElements.Het3DSNFG, StateTransforms.Representation.StructureRepresentation3D,
+                        StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, 'carbohydrate', void 0, structure));
+                }
+            }
+        }
+
+        if (!partial || (partial && style.water)) {
+            const root = update.to(StateElements.Water);
+            if (style.water && style.water.hide) {
+                root.delete(StateElements.WaterVisual);
+            } else {
+                root.applyOrUpdate(StateElements.WaterVisual, StateTransforms.Representation.StructureRepresentation3D,
+                        StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                            (style.water && style.water.kind) || 'ball-and-stick',
+                            (style.water && style.water.coloring), structure, { alpha: 0.51 }));
+            }
+        }
+
+        return update;
     }
 
     private getObj<T extends StateObject>(ref: string): T['data'] {
@@ -134,7 +195,7 @@ class MolStarProteopediaWrapper {
         if (this.loadedParams.url !== url || this.loadedParams.format !== format) {
             loadType = 'full';
         } else if (this.loadedParams.url === url) {
-            if (state.select('asm').length > 0) loadType = 'update';
+            if (state.select(StateElements.Assembly).length > 0) loadType = 'update';
         }
 
         if (loadType === 'full') {
@@ -146,24 +207,25 @@ class MolStarProteopediaWrapper {
             await this.applyState(structureTree);
         } else {
             const tree = state.build();
-            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+            tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
             await this.applyState(tree);
         }
 
         await this.updateStyle(representationStyle);
 
         this.loadedParams = { url, format, assemblyId };
-        PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+        Scheduler.setImmediate(() => PluginCommands.Camera.Reset.dispatch(this.plugin, { }));
     }
 
-    async updateStyle(style?: RepresentationStyle) {
-        const tree = this.visual('asm', style);
+    async updateStyle(style?: RepresentationStyle, partial?: boolean) {
+        const tree = this.visual(style, partial);
         if (!tree) return;
         await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
     }
 
     setBackground(color: number) {
-        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } });
+        const renderer = this.plugin.canvas3d.props.renderer;
+        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer,  backgroundColor: Color(color) } } });
     }
 
     toggleSpin() {
@@ -186,7 +248,7 @@ class MolStarProteopediaWrapper {
 
     coloring = {
         evolutionaryConservation: async () => {
-            await this.updateStyle({ sequence: { kind: 'spacefill' } });
+            await this.updateStyle({ sequence: { kind: 'spacefill' } }, true);
 
             const state = this.state;
 
@@ -194,7 +256,7 @@ class MolStarProteopediaWrapper {
             const tree = state.build();
             const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues };
 
-            tree.to('sequence-visual').update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
+            tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
             // for (const v of visuals) {
             // }
 
@@ -202,6 +264,69 @@ class MolStarProteopediaWrapper {
         }
     }
 
+    hetGroups = {
+        reset: () => {
+            const update = this.state.build().delete(StateElements.HetGroupFocus);
+            PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update });
+            PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+        },
+        focusFirst: async (resn: string) => {
+            if (!this.state.transforms.has(StateElements.Assembly)) return;
+
+            // const asm = (this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure).data;
+
+            const update = this.state.build();
+
+            update.delete(StateElements.HetGroupFocus);
+
+            const surroundings = MS.struct.modifier.includeSurroundings({
+                0: MS.struct.filter.first([
+                    MS.struct.generator.atomGroups({
+                        'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), resn]),
+                        'group-by': MS.struct.atomProperty.macromolecular.residueKey()
+                    })
+                ]),
+                radius: 5,
+                'as-whole-residues': true
+            });
+
+            const sel = update.to(StateElements.Assembly)
+                .apply(StateTransforms.Model.StructureSelection, { label: resn, query: surroundings }, { ref: StateElements.HetGroupFocus });
+
+            sel.apply(StateTransforms.Representation.StructureRepresentation3D, this.createSurVisualParams());
+            // sel.apply(StateTransforms.Representation.StructureLabels3D, {
+            //     target: { name: 'residues', params: { } },
+            //     options: {
+            //         ...ParamDefinition.getDefaultValues(Text.Params),
+            //         background: true,
+            //         backgroundMargin: 0.2,
+            //         backgroundColor: ColorNames.snow,
+            //         backgroundOpacity: 0.9,
+            //     }
+            // });
+
+            await PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update });
+
+            const focus = (this.state.select(StateElements.HetGroupFocus)[0].obj as PluginStateObject.Molecule.Structure).data;
+            const sphere = focus.boundary.sphere;
+            // const asmCenter = asm.boundary.sphere.center;
+            // const position = Vec3.sub(Vec3.zero(), sphere.center, asmCenter);
+            // Vec3.normalize(position, position);
+            // Vec3.scaleAndAdd(position, sphere.center, position, sphere.radius);
+            const snapshot = this.plugin.canvas3d.camera.getFocus(sphere.center, 0.75 * sphere.radius);
+            PluginCommands.Camera.SetSnapshot.dispatch(this.plugin, { snapshot, durationMs: 250 });
+        }
+    }
+
+    private createSurVisualParams() {
+        const asm = this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure;
+        return StructureRepresentation3DHelpers.createParams(this.plugin, asm.data, {
+            repr: BuiltInStructureRepresentations['ball-and-stick'],
+            color: [BuiltInColorThemes.uniform, () => ({ value: ColorNames.gray })],
+            size: [BuiltInSizeThemes.uniform, () => ({ value: 0.33 } )]
+        });
+    }
+
     snapshot = {
         get: () => {
             return this.plugin.state.getSnapshot();
diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts
index d61da319e3d6452fddfd17ed06c439af45324766..731dc74e67c6ab1674e39d86924465ea154a5f8d 100644
--- a/src/mol-canvas3d/camera.ts
+++ b/src/mol-canvas3d/camera.ts
@@ -84,7 +84,7 @@ class Camera implements Object3D {
         return ret;
     }
 
-    focus(target: Vec3, radius: number) {
+    getFocus(target: Vec3, radius: number): Partial<Camera.Snapshot> {
         const fov = this.state.fov
         const { width, height } = this.viewport
         const aspect = width / height
@@ -98,7 +98,11 @@ class Camera implements Object3D {
         if (currentDistance < targetDistance) Vec3.negate(this.deltaDirection, this.deltaDirection)
         Vec3.add(this.newPosition, this.state.position, this.deltaDirection)
 
-        this.setState({ target, position: this.newPosition })
+        return { target, position: Vec3.clone(this.newPosition) };
+    }
+
+    focus(target: Vec3, radius: number) {
+        this.setState(this.getFocus(target, radius));
     }
 
     // lookAt(target: Vec3) {
diff --git a/src/mol-canvas3d/camera/util.ts b/src/mol-canvas3d/camera/util.ts
index 52c431833d5e93b2633d2eb08be71f6ab19735e9..488999370a4d42ab2ad196bafdbb0f9b1f11f12f 100644
--- a/src/mol-canvas3d/camera/util.ts
+++ b/src/mol-canvas3d/camera/util.ts
@@ -1,19 +1,28 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Mat4, Vec3, Vec4, EPSILON } from 'mol-math/linear-algebra'
 
-export type Viewport = {
+export { Viewport }
+
+type Viewport = {
     x: number
     y: number
     width: number
     height: number
 }
 
-export namespace Viewport {
+function Viewport() {
+    return Viewport.zero()
+}
+
+namespace Viewport {
+    export function zero(): Viewport {
+        return { x: 0, y: 0, width: 0, height: 0 }
+    }
     export function create(x: number, y: number, width: number, height: number): Viewport {
         return { x, y, width, height }
     }
@@ -38,9 +47,15 @@ export namespace Viewport {
         v4[3] = viewport.height
         return v4
     }
+
+    export function equals(a: Viewport, b: Viewport) {
+        return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
+    }
 }
 
-const tmpVec3 = Vec3.zero()
+//
+
+const tmpVec3 = Vec3()
 
 /** Modifies the direction & up vectors in place, both are normalized */
 export function cameraLookAt(position: Vec3, up: Vec3, direction: Vec3, target: Vec3) {
@@ -68,7 +83,7 @@ export function cameraLookAt(position: Vec3, up: Vec3, direction: Vec3, target:
 const NEAR_RANGE = 0
 const FAR_RANGE = 1
 
-const tmpVec4 = Vec4.zero()
+const tmpVec4 = Vec4()
 
 /** Transform point into 2D window coordinates. */
 export function cameraProject (out: Vec4, point: Vec3, viewport: Viewport, projectionView: Mat4) {
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index e4491ed0f88c6e563d3637a4bcdeb38108b25c2a..358c5d3908dbd662aebdd9c4b1379d1d4d86aca6 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -9,7 +9,7 @@ import { now } from 'mol-util/now';
 
 import { Vec3 } from 'mol-math/linear-algebra'
 import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer'
-import Renderer, { RendererStats } from 'mol-gl/renderer'
+import Renderer, { RendererStats, RendererParams } from 'mol-gl/renderer'
 import { GraphicsRenderObject } from 'mol-gl/render-object'
 
 import { TrackballControls, TrackballControlsParams } from './controls/trackball'
@@ -19,11 +19,10 @@ import { createContext, getGLContext, WebGLContext } from 'mol-gl/webgl/context'
 import { Representation } from 'mol-repr/representation';
 import { createRenderTarget } from 'mol-gl/webgl/render-target';
 import Scene from 'mol-gl/scene';
-import { RenderVariant } from 'mol-gl/webgl/render-item';
+import { GraphicsRenderVariant } from 'mol-gl/webgl/render-item';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
-import { Color } from 'mol-util/color';
 import { Camera } from './camera';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
@@ -35,11 +34,10 @@ export const Canvas3DParams = {
     // TODO: FPS cap?
     // maxFps: PD.Numeric(30),
     cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
-    backgroundColor: PD.Color(Color(0x000000)),
     cameraClipDistance: PD.Numeric(0, { min: 0.0, max: 50.0, step: 0.1 }, { description: 'The distance between camera and scene at which to clip regardless of near clipping plane.' }),
     clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }),
     fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }),
-    pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
+    renderer: PD.Group(RendererParams),
     trackball: PD.Group(TrackballControlsParams),
     debug: PD.Group(DebugHelperParams)
 }
@@ -70,11 +68,11 @@ interface Canvas3D {
     resetCamera: () => void
     readonly camera: Camera
     downloadScreenshot: () => void
-    getImageData: (variant: RenderVariant) => ImageData
+    getImageData: (variant: GraphicsRenderVariant) => ImageData
     setProps: (props: Partial<Canvas3DProps>) => void
 
     /** Returns a copy of the current Canvas3D instance props */
-    readonly props: Canvas3DProps
+    readonly props: Readonly<Canvas3DProps>
     readonly input: InputObserver
     readonly stats: RendererStats
     readonly interaction: Canvas3dInteractionHelper['events']
@@ -117,7 +115,7 @@ namespace Canvas3D {
 
         const scene = Scene.create(webgl)
         const controls = TrackballControls.create(input, camera, p.trackball)
-        const renderer = Renderer.create(webgl, camera, { clearColor: p.backgroundColor })
+        const renderer = Renderer.create(webgl, camera, p.renderer)
 
         let pickScale = 0.25 / webgl.pixelRatio
         let pickWidth = Math.round(canvas.width * pickScale)
@@ -210,19 +208,15 @@ namespace Canvas3D {
                     case 'pick':
                         renderer.setViewport(0, 0, pickWidth, pickHeight);
                         objectPickTarget.bind();
-                        renderer.clear()
                         renderer.render(scene, 'pickObject');
                         instancePickTarget.bind();
-                        renderer.clear()
                         renderer.render(scene, 'pickInstance');
                         groupPickTarget.bind();
-                        renderer.clear()
                         renderer.render(scene, 'pickGroup');
                         break;
                     case 'draw':
                         webgl.unbindFramebuffer();
                         renderer.setViewport(0, 0, canvas.width, canvas.height);
-                        renderer.clear()
                         renderer.render(scene, variant);
                         if (debugHelper.isEnabled) {
                             debugHelper.syncVisibility()
@@ -388,7 +382,7 @@ namespace Canvas3D {
             downloadScreenshot: () => {
                 // TODO
             },
-            getImageData: (variant: RenderVariant) => {
+            getImageData: (variant: GraphicsRenderVariant) => {
                 switch (variant) {
                     case 'draw': return renderer.getImageData()
                     case 'pickObject': return objectPickTarget.getImageData()
@@ -401,17 +395,11 @@ namespace Canvas3D {
                 if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
                     camera.setState({ mode: props.cameraMode })
                 }
-                if (props.backgroundColor !== undefined && props.backgroundColor !== renderer.props.clearColor) {
-                    renderer.setClearColor(props.backgroundColor)
-                }
-
                 if (props.cameraClipDistance !== undefined) p.cameraClipDistance = props.cameraClipDistance
                 if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]]
                 if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]]
 
-                if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== renderer.props.pickingAlphaThreshold) {
-                    renderer.setPickingAlphaThreshold(props.pickingAlphaThreshold)
-                }
+                if (props.renderer) renderer.setProps(props.renderer)
                 if (props.trackball) controls.setProps(props.trackball)
                 if (props.debug) debugHelper.setProps(props.debug)
                 requestDraw(true)
@@ -420,11 +408,10 @@ namespace Canvas3D {
             get props() {
                 return {
                     cameraMode: camera.state.mode,
-                    backgroundColor: renderer.props.clearColor,
                     cameraClipDistance: p.cameraClipDistance,
                     clip: p.clip,
                     fog: p.fog,
-                    pickingAlphaThreshold: renderer.props.pickingAlphaThreshold,
+                    renderer: { ...renderer.props },
                     trackball: { ...controls.props },
                     debug: { ...debugHelper.props }
                 }
diff --git a/src/mol-canvas3d/helper/bounding-sphere-helper.ts b/src/mol-canvas3d/helper/bounding-sphere-helper.ts
index c3272d840f4a26240a0ef4ae42cc8ba8821ddd75..8f5a9634784f0812431f232bf71a145724796116 100644
--- a/src/mol-canvas3d/helper/bounding-sphere-helper.ts
+++ b/src/mol-canvas3d/helper/bounding-sphere-helper.ts
@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createRenderObject, RenderObject } from 'mol-gl/render-object'
+import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object'
 import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
 import { addSphere } from 'mol-geo/geometry/mesh/builder/sphere';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
@@ -27,15 +27,15 @@ export const DebugHelperParams = {
 export type DebugHelperParams = typeof DebugHelperParams
 export type DebugHelperProps = PD.Values<DebugHelperParams>
 
-type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: RenderObject, mesh: Mesh }
+type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: GraphicsRenderObject, mesh: Mesh }
 
 export class BoundingSphereHelper {
     readonly scene: Scene
 
     private readonly parent: Scene
     private _props: DebugHelperProps
-    private objectsData = new Map<RenderObject, BoundingSphereData>()
-    private instancesData = new Map<RenderObject, BoundingSphereData>()
+    private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>()
+    private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>()
     private sceneData: BoundingSphereData | undefined
 
     constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) {
@@ -136,7 +136,8 @@ function createBoundingSphereMesh(boundingSphere: Sphere3D, mesh?: Mesh) {
     return MeshBuilder.getMesh(builderState)
 }
 
+const boundingSphereHelberMaterialId = getNextMaterialId()
 function createBoundingSphereRenderObject(mesh: Mesh, color: Color, transform?: TransformData) {
     const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false }, color, 1, transform)
-    return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false })
+    return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false }, boundingSphereHelberMaterialId)
 }
\ No newline at end of file
diff --git a/src/mol-geo/geometry/base.ts b/src/mol-geo/geometry/base.ts
index eabbbc2454694f7f9e6696f68401f1d23f3ad782..c168c5a03b3f019e950e25b6c184976dfa0b39a3 100644
--- a/src/mol-geo/geometry/base.ts
+++ b/src/mol-geo/geometry/base.ts
@@ -64,9 +64,10 @@ export namespace BaseGeometry {
             uAlpha: ValueCell.create(props.alpha),
             uHighlightColor: ValueCell.create(Color.toArrayNormalized(props.highlightColor, Vec3.zero(), 0)),
             uSelectColor: ValueCell.create(Color.toArrayNormalized(props.selectColor, Vec3.zero(), 0)),
+            dUseFog: ValueCell.create(props.useFog),
+
             uGroupCount: ValueCell.create(counts.groupCount),
             drawCount: ValueCell.create(counts.drawCount),
-            dUseFog: ValueCell.create(props.useFog),
         }
     }
 
diff --git a/src/mol-geo/geometry/color-data.ts b/src/mol-geo/geometry/color-data.ts
index 19acb4e3396212cbafad013d967fd5ac5eeb1a2c..f4991df31f0e78e79baeed21abf7b5c8c1d6b370 100644
--- a/src/mol-geo/geometry/color-data.ts
+++ b/src/mol-geo/geometry/color-data.ts
@@ -33,14 +33,14 @@ export function createColors(locationIt: LocationIterator, colorTheme: ColorThem
 
 export function createValueColor(value: Color, colorData?: ColorData): ColorData {
     if (colorData) {
-        ValueCell.update(colorData.uColor, Color.toRgbNormalized(value) as Vec3)
+        ValueCell.update(colorData.uColor, Color.toVec3Normalized(colorData.uColor.ref.value, value))
         if (colorData.dColorType.ref.value !== 'uniform') {
             ValueCell.update(colorData.dColorType, 'uniform')
         }
         return colorData
     } else {
         return {
-            uColor: ValueCell.create(Color.toRgbNormalized(value) as Vec3),
+            uColor: ValueCell.create(Color.toVec3Normalized(Vec3(), value)),
             tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
             uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
             dColorType: ValueCell.create('uniform'),
@@ -74,7 +74,7 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color
 /** Creates color texture with color for each instance/unit */
 export function createInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { instanceCount } = locationIt
-    const colors = createTextureImage(Math.max(1, instanceCount), 3, colorData && colorData.tColor.ref.value.array)
+    const colors = createTextureImage(Math.max(1, instanceCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext) {
         const { location, isSecondary, instanceIndex } = locationIt.move()
@@ -87,7 +87,7 @@ export function createInstanceColor(locationIt: LocationIterator, color: Locatio
 /** Creates color texture with color for each group (i.e. shared across instances/units) */
 export function createGroupColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount } = locationIt
-    const colors = createTextureImage(Math.max(1, groupCount), 3, colorData && colorData.tColor.ref.value.array)
+    const colors = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const { location, isSecondary, groupIndex } = locationIt.move()
@@ -100,7 +100,7 @@ export function createGroupColor(locationIt: LocationIterator, color: LocationCo
 export function createGroupInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
-    const colors = createTextureImage(Math.max(1, count), 3, colorData && colorData.tColor.ref.value.array)
+    const colors = createTextureImage(Math.max(1, count), 3, Uint8Array, colorData && colorData.tColor.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext) {
         const { location, isSecondary, index } = locationIt.move()
diff --git a/src/mol-geo/geometry/direct-volume/direct-volume.ts b/src/mol-geo/geometry/direct-volume/direct-volume.ts
index ea2c9694c19dd979d2245baba0cde0a8050eb37d..32eb6957b6d7a9bcf261962e08a33bcd106a414d 100644
--- a/src/mol-geo/geometry/direct-volume/direct-volume.ts
+++ b/src/mol-geo/geometry/direct-volume/direct-volume.ts
@@ -25,6 +25,7 @@ import { ColorListOptions, ColorListName } from 'mol-util/color/scale';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
 
 const VolumeBox = Box()
 const RenderModeOptions = [['isosurface', 'Isosurface'], ['volume', 'Volume']] as [string, string][]
@@ -103,6 +104,7 @@ export namespace DirectVolume {
         const color = createColors(locationIt, theme.color)
         const marker = createMarkers(instanceCount * groupCount)
         const overpaint = createEmptyOverpaint()
+        const transparency = createEmptyTransparency()
 
         const counts = { drawCount: VolumeBox.indices.length, groupCount, instanceCount }
 
@@ -117,6 +119,7 @@ export namespace DirectVolume {
             ...color,
             ...marker,
             ...overpaint,
+            ...transparency,
             ...transform,
             ...BaseGeometry.createValues(props, counts),
 
diff --git a/src/mol-geo/geometry/geometry.ts b/src/mol-geo/geometry/geometry.ts
index 668e118df8cb0377f01069cf1c9ffadcd0a36d51..36678a3feda45c1772dcfe01d26a271b27a6dbf3 100644
--- a/src/mol-geo/geometry/geometry.ts
+++ b/src/mol-geo/geometry/geometry.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -21,6 +21,7 @@ import { TransformData } from './transform-data';
 import { Theme } from 'mol-theme/theme';
 import { RenderObjectValuesType } from 'mol-gl/render-object';
 import { ValueOf } from 'mol-util/type-helpers';
+import { TextureMesh } from './texture-mesh/texture-mesh';
 
 export type GeometryKindType = {
     'mesh': Mesh,
@@ -29,6 +30,7 @@ export type GeometryKindType = {
     'text': Text,
     'lines': Lines,
     'direct-volume': DirectVolume,
+    'texture-mesh': TextureMesh,
 }
 export type GeometryKindParams = {
     'mesh': Mesh.Params,
@@ -37,6 +39,7 @@ export type GeometryKindParams = {
     'text': Text.Params,
     'lines': Lines.Params,
     'direct-volume': DirectVolume.Params,
+    'texture-mesh': TextureMesh.Params,
 }
 export type GeometryKind = keyof GeometryKindType
 export type Geometry = ValueOf<GeometryKindType>
@@ -63,6 +66,7 @@ export namespace Geometry {
             case 'text': return geometry.charCount * 2 * 3
             case 'lines': return geometry.lineCount * 2 * 3
             case 'direct-volume': return 12 * 3
+            case 'texture-mesh': return geometry.vertexCount.ref.value
         }
     }
 
@@ -76,6 +80,8 @@ export namespace Geometry {
                 return getDrawCount(geometry) === 0 ? 0 : (arrayMax(geometry.groupBuffer.ref.value) + 1)
             case 'direct-volume':
                 return 1
+            case 'texture-mesh':
+                return geometry.groupCount.ref.value
         }
     }
 
@@ -88,6 +94,7 @@ export namespace Geometry {
             case 'text': return Text.Utils as any
             case 'lines': return Lines.Utils as any
             case 'direct-volume': return DirectVolume.Utils as any
+            case 'texture-mesh': return TextureMesh.Utils as any
         }
         throw new Error('unknown geometry kind')
     }
diff --git a/src/mol-geo/geometry/lines/lines.ts b/src/mol-geo/geometry/lines/lines.ts
index a2795a9c8646390fd2fc0455ba9a779429ac9870..13dea32130f80e3d0c400373eca9feb74005d054 100644
--- a/src/mol-geo/geometry/lines/lines.ts
+++ b/src/mol-geo/geometry/lines/lines.ts
@@ -23,6 +23,7 @@ import { Theme } from 'mol-theme/theme';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
 
 /** Wide line */
 export interface Lines {
@@ -119,6 +120,7 @@ export namespace Lines {
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
         const overpaint = createEmptyOverpaint()
+        const transparency = createEmptyTransparency()
 
         const counts = { drawCount: lines.lineCount * 2 * 3, groupCount, instanceCount }
 
@@ -137,6 +139,7 @@ export namespace Lines {
             ...size,
             ...marker,
             ...overpaint,
+            ...transparency,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
@@ -177,7 +180,7 @@ function getBoundingSphere(lineStart: Float32Array, lineEnd: Float32Array, lineC
     const start = calculateBoundingSphere(lineStart, lineCount * 4, transform, transformCount)
     const end = calculateBoundingSphere(lineEnd, lineCount * 4, transform, transformCount)
     return {
-        boundingSphere: Sphere3D.addSphere(start.boundingSphere, end.boundingSphere),
-        invariantBoundingSphere: Sphere3D.addSphere(start.invariantBoundingSphere, end.invariantBoundingSphere)
+        boundingSphere: Sphere3D.expandBySphere(start.boundingSphere, end.boundingSphere),
+        invariantBoundingSphere: Sphere3D.expandBySphere(start.invariantBoundingSphere, end.invariantBoundingSphere)
     }
 }
\ No newline at end of file
diff --git a/src/mol-geo/geometry/marker-data.ts b/src/mol-geo/geometry/marker-data.ts
index 2c49bd1b233b05d234831506791289b26de0ecd6..65d8e59816a2c018f250227b051f72e44c95fa18 100644
--- a/src/mol-geo/geometry/marker-data.ts
+++ b/src/mol-geo/geometry/marker-data.ts
@@ -65,7 +65,7 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number,
 }
 
 export function createMarkers(count: number, markerData?: MarkerData): MarkerData {
-    const markers = createTextureImage(Math.max(1, count), 1, markerData && markerData.tMarker.ref.value.array)
+    const markers = createTextureImage(Math.max(1, count), 1, Uint8Array, markerData && markerData.tMarker.ref.value.array)
     if (markerData) {
         ValueCell.update(markerData.tMarker, markers)
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height))
diff --git a/src/mol-geo/geometry/mesh/mesh-builder.ts b/src/mol-geo/geometry/mesh/mesh-builder.ts
index 2bcaf085159f4597b6ee0c0d915b754c6aeac46c..cfe57234db1d2b4fd8ce2648158cf4d4369e5656 100644
--- a/src/mol-geo/geometry/mesh/mesh-builder.ts
+++ b/src/mol-geo/geometry/mesh/mesh-builder.ts
@@ -45,7 +45,7 @@ export namespace MeshBuilder {
     export function addTriangle(state: State, a: Vec3, b: Vec3, c: Vec3) {
         const { vertices, normals, indices, groups, currentGroup } = state
         const offset = vertices.elementCount
-        
+
         // positions
         ChunkedArray.add3(vertices, a[0], a[1], a[2]);
         ChunkedArray.add3(vertices, b[0], b[1], b[2]);
diff --git a/src/mol-geo/geometry/mesh/mesh.ts b/src/mol-geo/geometry/mesh/mesh.ts
index fd594d952c6ad0aa3f886828537e0f833651b53c..1095cd65484827f2a2b72916adea018f47bef388 100644
--- a/src/mol-geo/geometry/mesh/mesh.ts
+++ b/src/mol-geo/geometry/mesh/mesh.ts
@@ -22,6 +22,7 @@ import { MeshValues } from 'mol-gl/renderable/mesh';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
 
 export interface Mesh {
     readonly kind: 'mesh',
@@ -383,6 +384,7 @@ export namespace Mesh {
         const color = createColors(locationIt, theme.color)
         const marker = createMarkers(instanceCount * groupCount)
         const overpaint = createEmptyOverpaint()
+        const transparency = createEmptyTransparency()
 
         const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount }
 
@@ -401,6 +403,7 @@ export namespace Mesh {
             ...color,
             ...marker,
             ...overpaint,
+            ...transparency,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
diff --git a/src/mol-geo/geometry/overpaint-data.ts b/src/mol-geo/geometry/overpaint-data.ts
index 875bef81b528dd5989f394dbfd4e0cfdf53870bb..a1c74f1273bb7f2b854357062a0bb1390ca45802 100644
--- a/src/mol-geo/geometry/overpaint-data.ts
+++ b/src/mol-geo/geometry/overpaint-data.ts
@@ -28,7 +28,7 @@ export function clearOverpaint(array: Uint8Array, start: number, end: number) {
 }
 
 export function createOverpaint(count: number, overpaintData?: OverpaintData): OverpaintData {
-    const overpaint = createTextureImage(Math.max(1, count), 4, overpaintData && overpaintData.tOverpaint.ref.value.array)
+    const overpaint = createTextureImage(Math.max(1, count), 4, Uint8Array, overpaintData && overpaintData.tOverpaint.ref.value.array)
     if (overpaintData) {
         ValueCell.update(overpaintData.tOverpaint, overpaint)
         ValueCell.update(overpaintData.uOverpaintTexDim, Vec2.create(overpaint.width, overpaint.height))
diff --git a/src/mol-geo/geometry/points/points.ts b/src/mol-geo/geometry/points/points.ts
index 7cda2e36ef2d8241561cdbcb1a341cfbf99d1446..cb5f9c648f38006387d7d9604bcc3704f5153ccf 100644
--- a/src/mol-geo/geometry/points/points.ts
+++ b/src/mol-geo/geometry/points/points.ts
@@ -22,6 +22,7 @@ import { RenderableState } from 'mol-gl/renderable';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
 
 /** Point cloud */
 export interface Points {
@@ -84,6 +85,7 @@ export namespace Points {
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
         const overpaint = createEmptyOverpaint()
+        const transparency = createEmptyTransparency()
 
         const counts = { drawCount: points.pointCount, groupCount, instanceCount }
 
@@ -101,6 +103,7 @@ export namespace Points {
             ...size,
             ...marker,
             ...overpaint,
+            ...transparency,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
diff --git a/src/mol-geo/geometry/size-data.ts b/src/mol-geo/geometry/size-data.ts
index fb4b94b851027ba336703e18d8502f2b34c6338b..9fdc122f25e83585c983a3c0552bb5af6012d111 100644
--- a/src/mol-geo/geometry/size-data.ts
+++ b/src/mol-geo/geometry/size-data.ts
@@ -101,7 +101,7 @@ export function createTextureSize(sizes: TextureImage<Uint8Array>, type: SizeTyp
 /** Creates size texture with size for each instance/unit */
 export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
     const { instanceCount} = locationIt
-    const sizes = createTextureImage(Math.max(1, instanceCount), 1, sizeData && sizeData.tSize.ref.value.array)
+    const sizes = createTextureImage(Math.max(1, instanceCount), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
@@ -114,7 +114,7 @@ export function createInstanceSize(locationIt: LocationIterator, sizeFn: Locatio
 /** Creates size texture with size for each group (i.e. shared across instances/units) */
 export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
     const { groupCount } = locationIt
-    const sizes = createTextureImage(Math.max(1, groupCount), 1, sizeData && sizeData.tSize.ref.value.array)
+    const sizes = createTextureImage(Math.max(1, groupCount), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
@@ -127,7 +127,7 @@ export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSi
 export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
-    const sizes = createTextureImage(Math.max(1, count), 1, sizeData && sizeData.tSize.ref.value.array)
+    const sizes = createTextureImage(Math.max(1, count), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
diff --git a/src/mol-geo/geometry/spheres/spheres.ts b/src/mol-geo/geometry/spheres/spheres.ts
index eb22f55a918043f512f28824d857b3fb61d00db1..9d38ea9a2bfcb28cd64b5119e62f6caccad21303 100644
--- a/src/mol-geo/geometry/spheres/spheres.ts
+++ b/src/mol-geo/geometry/spheres/spheres.ts
@@ -19,6 +19,7 @@ import { createSizes, getMaxSize } from '../size-data';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
 
 /** Spheres */
 export interface Spheres {
@@ -81,6 +82,7 @@ export namespace Spheres {
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
         const overpaint = createEmptyOverpaint()
+        const transparency = createEmptyTransparency()
 
         const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount }
 
@@ -101,6 +103,7 @@ export namespace Spheres {
             ...size,
             ...marker,
             ...overpaint,
+            ...transparency,
             ...transform,
 
             padding: ValueCell.create(padding),
diff --git a/src/mol-geo/geometry/text/font-atlas.ts b/src/mol-geo/geometry/text/font-atlas.ts
index 0ad781f2bd77edd0ee34462da0aa2199c9546ec4..46f11f633197e941f93173e9aba398bb5a100562 100644
--- a/src/mol-geo/geometry/text/font-atlas.ts
+++ b/src/mol-geo/geometry/text/font-atlas.ts
@@ -81,7 +81,7 @@ export class FontAtlas {
         this.maxWidth = Math.round(this.lineHeight * 0.75)
 
         // create texture (for ~350 characters)
-        this.texture = createTextureImage(350 * this.lineHeight * this.maxWidth, 1)
+        this.texture = createTextureImage(350 * this.lineHeight * this.maxWidth, 1, Uint8Array)
 
         // prepare scratch canvas
         this.scratchCanvas = document.createElement('canvas')
diff --git a/src/mol-geo/geometry/text/text.ts b/src/mol-geo/geometry/text/text.ts
index 4238f2432cc9b24f9cb21e2d3dbf2841c70179af..0323d397ded64e55b7fb0c7ffd4880f93252bd4d 100644
--- a/src/mol-geo/geometry/text/text.ts
+++ b/src/mol-geo/geometry/text/text.ts
@@ -25,6 +25,7 @@ import { clamp } from 'mol-math/interpolate';
 import { createRenderObject as _createRenderObject } from 'mol-gl/render-object';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
 
 type TextAttachment = (
     'bottom-left' | 'bottom-center' | 'bottom-right' |
@@ -57,7 +58,7 @@ export interface Text {
 
 export namespace Text {
     export function createEmpty(text?: Text): Text {
-        const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1)
+        const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1, Uint8Array)
         const cb = text ? text.centerBuffer.ref.value : new Float32Array(0)
         const mb = text ? text.mappingBuffer.ref.value : new Float32Array(0)
         const db = text ? text.depthBuffer.ref.value : new Float32Array(0)
@@ -124,6 +125,7 @@ export namespace Text {
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
         const overpaint = createEmptyOverpaint()
+        const transparency = createEmptyTransparency()
 
         const counts = { drawCount: text.charCount * 2 * 3, groupCount, instanceCount }
 
@@ -145,6 +147,7 @@ export namespace Text {
             ...size,
             ...marker,
             ...overpaint,
+            ...transparency,
             ...transform,
 
             aTexCoord: text.tcoordBuffer,
diff --git a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e235f03ca3bbe469448c6f9188a1a8bee9e5266
--- /dev/null
+++ b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util'
+import { Sphere3D } from 'mol-math/geometry'
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { LocationIterator } from 'mol-geo/util/location-iterator';
+import { TransformData } from '../transform-data';
+import { createColors } from '../color-data';
+import { createMarkers } from '../marker-data';
+import { GeometryUtils } from '../geometry';
+import { Theme } from 'mol-theme/theme';
+import { Color } from 'mol-util/color';
+import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
+import { TextureMeshValues } from 'mol-gl/renderable/texture-mesh';
+import { calculateTransformBoundingSphere } from 'mol-gl/renderable/util';
+import { Texture } from 'mol-gl/webgl/texture';
+import { Vec2 } from 'mol-math/linear-algebra';
+import { fillSerial } from 'mol-util/array';
+
+export interface TextureMesh {
+    readonly kind: 'texture-mesh',
+
+    /** Number of vertices in the texture-mesh */
+    readonly vertexCount: ValueCell<number>,
+    /** Number of groups in the texture-mesh */
+    readonly groupCount: ValueCell<number>,
+
+    readonly geoTextureDim: ValueCell<Vec2>,
+    /** texture has vertex positions in XYZ and group id in W */
+    readonly vertexGroupTexture: ValueCell<Texture>,
+    readonly normalTexture: ValueCell<Texture>,
+
+    readonly boundingSphere: ValueCell<Sphere3D>,
+}
+
+export namespace TextureMesh {
+    export function create(vertexCount: number, groupCount: number, vertexGroupTexture: Texture, normalTexture: Texture, boundingSphere: Sphere3D, textureMesh?: TextureMesh): TextureMesh {
+        const { width, height } = vertexGroupTexture
+        if (textureMesh) {
+            ValueCell.update(textureMesh.vertexCount, vertexCount)
+            ValueCell.update(textureMesh.groupCount, groupCount)
+            ValueCell.update(textureMesh.geoTextureDim, Vec2.set(textureMesh.geoTextureDim.ref.value, width, height))
+            ValueCell.update(textureMesh.vertexGroupTexture, vertexGroupTexture)
+            ValueCell.update(textureMesh.normalTexture, normalTexture)
+            ValueCell.update(textureMesh.boundingSphere, boundingSphere)
+            return textureMesh
+        } else {
+            return {
+                kind: 'texture-mesh',
+                vertexCount: ValueCell.create(vertexCount),
+                groupCount: ValueCell.create(groupCount),
+                geoTextureDim: ValueCell.create(Vec2.create(width, height)),
+                vertexGroupTexture: ValueCell.create(vertexGroupTexture),
+                normalTexture: ValueCell.create(normalTexture),
+                boundingSphere: ValueCell.create(boundingSphere),
+            }
+        }
+    }
+
+    export function createEmpty(textureMesh?: TextureMesh): TextureMesh {
+        return {} as TextureMesh // TODO
+    }
+
+    export const Params = {
+        ...BaseGeometry.Params,
+        doubleSided: PD.Boolean(false),
+        flipSided: PD.Boolean(false),
+        flatShaded: PD.Boolean(false),
+    }
+    export type Params = typeof Params
+
+    export const Utils: GeometryUtils<TextureMesh, Params> = {
+        Params,
+        createEmpty,
+        createValues,
+        createValuesSimple,
+        updateValues,
+        updateBoundingSphere,
+        createRenderableState: BaseGeometry.createRenderableState,
+        updateRenderableState: BaseGeometry.updateRenderableState
+    }
+
+    function createValues(textureMesh: TextureMesh, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextureMeshValues {
+        const { instanceCount, groupCount } = locationIt
+        const color = createColors(locationIt, theme.color)
+        const marker = createMarkers(instanceCount * groupCount)
+        const overpaint = createEmptyOverpaint()
+        const transparency = createEmptyTransparency()
+
+        const counts = { drawCount: textureMesh.vertexCount.ref.value, groupCount, instanceCount }
+
+        const transformBoundingSphere = calculateTransformBoundingSphere(textureMesh.boundingSphere.ref.value, transform.aTransform.ref.value, transform.instanceCount.ref.value)
+
+        return {
+            uGeoTexDim: textureMesh.geoTextureDim,
+            tPositionGroup: textureMesh.vertexGroupTexture,
+            tNormal: textureMesh.normalTexture,
+
+            // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup
+            aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount.ref.value))),
+            boundingSphere: ValueCell.create(transformBoundingSphere),
+            invariantBoundingSphere: textureMesh.boundingSphere,
+
+            ...color,
+            ...marker,
+            ...overpaint,
+            ...transparency,
+            ...transform,
+
+            ...BaseGeometry.createValues(props, counts),
+            dDoubleSided: ValueCell.create(props.doubleSided),
+            dFlatShaded: ValueCell.create(props.flatShaded),
+            dFlipSided: ValueCell.create(props.flipSided),
+            dGeoTexture: ValueCell.create(true),
+        }
+    }
+
+    function createValuesSimple(textureMesh: TextureMesh, props: Partial<PD.Values<Params>>, colorValue: Color, sizeValue: number, transform?: TransformData) {
+        const s = BaseGeometry.createSimple(colorValue, sizeValue, transform)
+        const p = { ...PD.getDefaultValues(Params), ...props }
+        return createValues(textureMesh, s.transform, s.locationIterator, s.theme, p)
+    }
+
+    function updateValues(values: TextureMeshValues, props: PD.Values<Params>) {
+        if (Color.fromNormalizedArray(values.uHighlightColor.ref.value, 0) !== props.highlightColor) {
+            ValueCell.update(values.uHighlightColor, Color.toArrayNormalized(props.highlightColor, values.uHighlightColor.ref.value, 0))
+        }
+        if (Color.fromNormalizedArray(values.uSelectColor.ref.value, 0) !== props.selectColor) {
+            ValueCell.update(values.uSelectColor, Color.toArrayNormalized(props.selectColor, values.uSelectColor.ref.value, 0))
+        }
+        ValueCell.updateIfChanged(values.alpha, props.alpha) // `uAlpha` is set in renderable.render
+        ValueCell.updateIfChanged(values.dUseFog, props.useFog)
+
+        ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided)
+        ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded)
+        ValueCell.updateIfChanged(values.dFlipSided, props.flipSided)
+
+        if (values.drawCount.ref.value > values.aGroup.ref.value.length) {
+            // console.log('updating vertex ids in aGroup to handle larger drawCount')
+            ValueCell.update(values.aGroup, fillSerial(new Float32Array(values.drawCount.ref.value)))
+        }
+    }
+
+    function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {
+        const invariantBoundingSphere = textureMesh.boundingSphere.ref.value
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
+        if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
+            ValueCell.update(values.boundingSphere, boundingSphere)
+        }
+        if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
+            ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-geo/geometry/transparency-data.ts b/src/mol-geo/geometry/transparency-data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3b19b6130f9b6dff272c9dd7617c5ae8cb3ad98b
--- /dev/null
+++ b/src/mol-geo/geometry/transparency-data.ts
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+import { Vec2 } from 'mol-math/linear-algebra'
+import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
+import { Transparency } from 'mol-theme/transparency';
+
+export type TransparencyData = {
+    tTransparency: ValueCell<TextureImage<Uint8Array>>
+    uTransparencyTexDim: ValueCell<Vec2>
+    dTransparency: ValueCell<boolean>,
+    dTransparencyVariant: ValueCell<string>,
+}
+
+export function applyTransparencyValue(array: Uint8Array, start: number, end: number, value: number) {
+    for (let i = start; i < end; ++i) {
+        array[i] = value * 255
+    }
+    return true
+}
+
+export function clearTransparency(array: Uint8Array, start: number, end: number) {
+    array.fill(0, start, end)
+}
+
+export function createTransparency(count: number, variant: Transparency.Variant, transparencyData?: TransparencyData): TransparencyData {
+    const transparency = createTextureImage(Math.max(1, count), 1, Uint8Array, transparencyData && transparencyData.tTransparency.ref.value.array)
+    if (transparencyData) {
+        ValueCell.update(transparencyData.tTransparency, transparency)
+        ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height))
+        ValueCell.update(transparencyData.dTransparency, count > 0)
+        ValueCell.update(transparencyData.dTransparencyVariant, variant)
+        return transparencyData
+    } else {
+        return {
+            tTransparency: ValueCell.create(transparency),
+            uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)),
+            dTransparency: ValueCell.create(count > 0),
+            dTransparencyVariant: ValueCell.create(variant),
+        }
+    }
+}
+
+const emptyTransparencyTexture = { array: new Uint8Array(1), width: 1, height: 1 }
+export function createEmptyTransparency(transparencyData?: TransparencyData): TransparencyData {
+    if (transparencyData) {
+        ValueCell.update(transparencyData.tTransparency, emptyTransparencyTexture)
+        ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(1, 1))
+        return transparencyData
+    } else {
+        return {
+            tTransparency: ValueCell.create(emptyTransparencyTexture),
+            uTransparencyTexDim: ValueCell.create(Vec2.create(1, 1)),
+            dTransparency: ValueCell.create(false),
+            dTransparencyVariant: ValueCell.create('single'),
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-geo/util/marching-cubes/algorithm.ts b/src/mol-geo/util/marching-cubes/algorithm.ts
index e9d9e4ad46ee87691bc4c9d34b01dfe8a476b071..e446ad2576f1d9a3ffc2b8afda4c0ba2f87b8c2a 100644
--- a/src/mol-geo/util/marching-cubes/algorithm.ts
+++ b/src/mol-geo/util/marching-cubes/algorithm.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -12,7 +12,6 @@ import { Index, EdgeIdInfo, CubeEdges, EdgeTable, TriTable } from './tables'
 import { defaults } from 'mol-util'
 import { MarchinCubesBuilder, MarchinCubesMeshBuilder, MarchinCubesLinesBuilder } from './builder';
 import { Lines } from '../../geometry/lines/lines';
-// import { Lines } from '../../geometry/lines/lines';
 
 /**
  * The parameters required by the algorithm.
@@ -156,17 +155,40 @@ class MarchingCubesState {
         const ret = this.verticesOnEdges[edgeId];
         if (ret > 0) return ret - 1;
 
+        const sf = this.scalarField
+        const sfg = this.scalarFieldGet
+
         const edge = CubeEdges[edgeNum];
         const a = edge.a, b = edge.b;
         const li = a.i + this.i, lj = a.j + this.j, lk = a.k + this.k;
         const hi = b.i + this.i, hj = b.j + this.j, hk = b.k + this.k;
-        const v0 = this.scalarFieldGet(this.scalarField, li, lj, lk);
-        const v1 = this.scalarFieldGet(this.scalarField, hi, hj, hk);
+        const v0 = sfg(sf, li, lj, lk);
+        const v1 = sfg(sf, hi, hj, hk);
         const t = (this.isoLevel - v0) / (v0 - v1);
 
-        const id = this.builder.addVertex(li + t * (li - hi), lj + t * (lj - hj), lk + t * (lk - hk));
+        const id = this.builder.addVertex(
+            li + t * (li - hi),
+            lj + t * (lj - hj),
+            lk + t * (lk - hk)
+        );
         this.verticesOnEdges[edgeId] = id + 1;
 
+        // TODO cache scalarField differences for slices
+        // TODO make calculation optional
+        const n0x = sfg(sf, Math.max(0, li - 1), lj, lk) - sfg(sf, Math.min(this.nX - 1, li + 1), lj, lk)
+        const n0y = sfg(sf, li, Math.max(0, lj - 1), lk) - sfg(sf, li, Math.min(this.nY - 1, lj + 1), lk)
+        const n0z = sfg(sf, li, lj, Math.max(0, lk - 1)) - sfg(sf, li, lj, Math.min(this.nZ, lk + 1))
+
+        const n1x = sfg(sf, Math.max(0, hi - 1), hj, hk) - sfg(sf, Math.min(this.nX - 1, hi + 1), hj, hk)
+        const n1y = sfg(sf, hi, Math.max(0, hj - 1), hk) - sfg(sf, hi, Math.min(this.nY - 1, hj + 1), hk)
+        const n1z = sfg(sf, hi, hj, Math.max(0, hk - 1)) - sfg(sf, hi, hj, Math.min(this.nZ - 1, hk + 1))
+
+        this.builder.addNormal(
+            n0x + t * (n0x - n1x),
+            n0y + t * (n0y - n1y),
+            n0z + t * (n0z - n1z)
+        )
+
         if (this.idField) {
             const u = this.idFieldGet!(this.idField, li, lj, lk);
             const v = this.idFieldGet!(this.idField, hi, hj, hk)
diff --git a/src/mol-geo/util/marching-cubes/builder.ts b/src/mol-geo/util/marching-cubes/builder.ts
index be7abebb915efd13d1d86f11e3ce866633b09263..8d78d9f32614947573d734bc390fee08b05e9b9d 100644
--- a/src/mol-geo/util/marching-cubes/builder.ts
+++ b/src/mol-geo/util/marching-cubes/builder.ts
@@ -7,7 +7,7 @@
  */
 
 import { ChunkedArray } from '../../../mol-data/util';
-import { ValueCell } from 'mol-util';
+import { ValueCell, noop } from 'mol-util';
 import { Mesh } from '../../geometry/mesh/mesh';
 import { AllowedContours } from './tables';
 import { LinesBuilder } from '../../geometry/lines/lines-builder';
@@ -15,6 +15,7 @@ import { Lines } from '../../geometry/lines/lines';
 
  export interface MarchinCubesBuilder<T> {
     addVertex(x: number, y: number, z: number): number
+    addNormal(x: number, y: number, z: number): void
     addGroup(group: number): void
     addTriangle(vertList: number[], a: number, b: number, c: number, edgeFilter: number): void
     get(): T
@@ -24,6 +25,7 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M
     const triangleChunkSize = Math.min(1 << 16, vertexChunkSize * 4)
 
     const vertices = ChunkedArray.create(Float32Array, 3, vertexChunkSize, mesh && mesh.vertexBuffer.ref.value);
+    const normals = ChunkedArray.create(Float32Array, 3, vertexChunkSize, mesh && mesh.normalBuffer.ref.value);
     const groups = ChunkedArray.create(Float32Array, 1, vertexChunkSize, mesh && mesh.groupBuffer.ref.value);
     const indices = ChunkedArray.create(Uint32Array, 3, triangleChunkSize, mesh && mesh.indexBuffer.ref.value);
 
@@ -35,6 +37,9 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M
             ++vertexCount
             return ChunkedArray.add3(vertices, x, y, z );
         },
+        addNormal: (x: number, y: number, z: number) => {
+            ChunkedArray.add3(normals, x, y, z );
+        },
         addGroup: (group: number) => {
             ChunkedArray.add(groups, group);
         },
@@ -44,6 +49,7 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M
         },
         get: () => {
             const vb = ChunkedArray.compact(vertices, true) as Float32Array;
+            const nb = ChunkedArray.compact(normals, true) as Float32Array;
             const ib = ChunkedArray.compact(indices, true) as Uint32Array;
             const gb = ChunkedArray.compact(groups, true) as Float32Array;
 
@@ -54,8 +60,8 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M
                 vertexBuffer: mesh ? ValueCell.update(mesh.vertexBuffer, vb) : ValueCell.create(vb),
                 groupBuffer: mesh ? ValueCell.update(mesh.groupBuffer, gb) : ValueCell.create(gb),
                 indexBuffer: mesh ? ValueCell.update(mesh.indexBuffer, ib) : ValueCell.create(ib),
-                normalBuffer: mesh ? mesh.normalBuffer : ValueCell.create(new Float32Array(0)),
-                normalsComputed: false
+                normalBuffer: mesh ? ValueCell.update(mesh.normalBuffer, nb) : ValueCell.create(nb),
+                normalsComputed: true
             }
         }
     }
@@ -72,6 +78,7 @@ export function MarchinCubesLinesBuilder(vertexChunkSize: number, lines?: Lines)
         addVertex: (x: number, y: number, z: number) => {
             return ChunkedArray.add3(vertices, x, y, z);
         },
+        addNormal: () => noop,
         addGroup: (group: number) => {
             ChunkedArray.add(groups, group);
         },
diff --git a/src/mol-gl/_spec/gl.shim.ts b/src/mol-gl/_spec/gl.shim.ts
index d4242f5a1628383c9dcb93083eb7608ad56df978..b593beef409268de777313028b3ecdfc9c069de1 100644
--- a/src/mol-gl/_spec/gl.shim.ts
+++ b/src/mol-gl/_spec/gl.shim.ts
@@ -1,5 +1,3 @@
-
-
 /**
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
@@ -679,14 +677,14 @@ export function createGl(width: number, height: number, contextAttributes: WebGL
             return {
                 size: 1,
                 type: gl.INT_VEC3,
-                name: `activeUniform${index}`,
+                name: `__activeUniform${index}`,
             };
         },
         getActiveAttrib: function (program: WebGLProgram, index: number) {
             return {
                 size: 1,
                 type: gl.FLOAT,
-                name: `activeAttrib${index}`
+                name: `__activeAttrib${index}`
             };
         },
         clear: function () { },
diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts
index 314d75f0bd3dc31c67d860fd296c56e969a8c4b1..62cbb35e491404bd1e029c06fa1017eb4e8d24f2 100644
--- a/src/mol-gl/_spec/renderer.spec.ts
+++ b/src/mol-gl/_spec/renderer.spec.ts
@@ -23,6 +23,7 @@ import { fillSerial } from 'mol-util/array';
 import { Color } from 'mol-util/color';
 import { Sphere3D } from 'mol-math/geometry';
 import { createEmptyOverpaint } from 'mol-geo/geometry/overpaint-data';
+import { createEmptyTransparency } from 'mol-geo/geometry/transparency-data';
 
 // function writeImage(gl: WebGLRenderingContext, width: number, height: number) {
 //     const pixels = new Uint8Array(width * height * 4)
@@ -54,6 +55,7 @@ function createPoints() {
     const size = createValueSize(1)
     const marker = createEmptyMarkers()
     const overpaint = createEmptyOverpaint()
+    const transparency = createEmptyTransparency()
 
     const aTransform = ValueCell.create(new Float32Array(16))
     const m4 = Mat4.identity()
@@ -73,6 +75,7 @@ function createPoints() {
         ...marker,
         ...size,
         ...overpaint,
+        ...transparency,
 
         uAlpha: ValueCell.create(1.0),
         uHighlightColor: ValueCell.create(Vec3.create(1.0, 0.4, 0.6)),
@@ -102,7 +105,7 @@ function createPoints() {
         opaque: true
     }
 
-    return createRenderObject('points', values, state)
+    return createRenderObject('points', values, state, -1)
 }
 
 describe('renderer', () => {
@@ -114,9 +117,9 @@ describe('renderer', () => {
         expect(ctx.gl.canvas.width).toBe(32)
         expect(ctx.gl.canvas.height).toBe(32)
 
-        expect(ctx.bufferCount).toBe(0);
-        expect(ctx.textureCount).toBe(0);
-        expect(ctx.vaoCount).toBe(0);
+        expect(ctx.stats.bufferCount).toBe(0);
+        expect(ctx.stats.textureCount).toBe(0);
+        expect(ctx.stats.vaoCount).toBe(0);
         expect(ctx.programCache.count).toBe(0);
         expect(ctx.shaderCache.count).toBe(0);
 
@@ -134,16 +137,16 @@ describe('renderer', () => {
         const points = createPoints()
 
         scene.add(points)
-        expect(ctx.bufferCount).toBe(4);
-        expect(ctx.textureCount).toBe(4);
-        expect(ctx.vaoCount).toBe(4);
+        expect(ctx.stats.bufferCount).toBe(4);
+        expect(ctx.stats.textureCount).toBe(5);
+        expect(ctx.stats.vaoCount).toBe(4);
         expect(ctx.programCache.count).toBe(4);
         expect(ctx.shaderCache.count).toBe(8);
 
         scene.remove(points)
-        expect(ctx.bufferCount).toBe(0);
-        expect(ctx.textureCount).toBe(0);
-        expect(ctx.vaoCount).toBe(0);
+        expect(ctx.stats.bufferCount).toBe(0);
+        expect(ctx.stats.textureCount).toBe(0);
+        expect(ctx.stats.vaoCount).toBe(0);
         expect(ctx.programCache.count).toBe(4);
         expect(ctx.shaderCache.count).toBe(8);
 
diff --git a/src/mol-gl/compute/histogram-pyramid/reduction.ts b/src/mol-gl/compute/histogram-pyramid/reduction.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9fda346fea9ced4abab09d230e824ef0d57be62e
--- /dev/null
+++ b/src/mol-gl/compute/histogram-pyramid/reduction.ts
@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createComputeRenderable, ComputeRenderable } from '../../renderable'
+import { WebGLContext } from '../../webgl/context';
+import { createComputeRenderItem } from '../../webgl/render-item';
+import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
+import { Texture, createTexture } from 'mol-gl/webgl/texture';
+import { ShaderCode } from 'mol-gl/shader-code';
+import { ValueCell } from 'mol-util';
+import { QuadSchema, QuadValues } from '../util';
+import { Vec2 } from 'mol-math/linear-algebra';
+import { getHistopyramidSum } from './sum';
+import { Framebuffer, createFramebuffer } from 'mol-gl/webgl/framebuffer';
+import { isPowerOfTwo } from 'mol-math/misc';
+
+const HistopyramidReductionSchema = {
+    ...QuadSchema,
+    tPreviousLevel: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    uSize: UniformSpec('f'),
+    uTexSize: UniformSpec('f'),
+}
+
+let HistopyramidReductionRenderable: ComputeRenderable<Values<typeof HistopyramidReductionSchema>>
+function getHistopyramidReductionRenderable(ctx: WebGLContext, initialTexture: Texture) {
+    if (HistopyramidReductionRenderable) {
+        ValueCell.update(HistopyramidReductionRenderable.values.tPreviousLevel, initialTexture)
+        HistopyramidReductionRenderable.update()
+        return HistopyramidReductionRenderable
+    } else {
+        const values: Values<typeof HistopyramidReductionSchema> = {
+            ...QuadValues,
+            tPreviousLevel: ValueCell.create(initialTexture),
+            uSize: ValueCell.create(0),
+            uTexSize: ValueCell.create(0),
+        }
+
+        const schema = { ...HistopyramidReductionSchema }
+        const shaderCode = ShaderCode(
+            require('mol-gl/shader/quad.vert').default,
+            require('mol-gl/shader/histogram-pyramid/reduction.frag').default
+        )
+        const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values)
+
+        HistopyramidReductionRenderable = createComputeRenderable(renderItem, values);
+        return HistopyramidReductionRenderable
+    }
+}
+
+type TextureFramebuffer = { texture: Texture, framebuffer: Framebuffer }
+const LevelTexturesFramebuffers: TextureFramebuffer[] = []
+function getLevelTextureFramebuffer(ctx: WebGLContext, level: number) {
+    let textureFramebuffer  = LevelTexturesFramebuffers[level]
+    const size = Math.pow(2, level)
+    if (textureFramebuffer === undefined) {
+        const texture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+        const framebuffer = createFramebuffer(ctx.gl, ctx.stats)
+        texture.attachFramebuffer(framebuffer, 0)
+        textureFramebuffer = { texture, framebuffer }
+        textureFramebuffer.texture.define(size, size)
+        LevelTexturesFramebuffers[level] = textureFramebuffer
+    }
+    return textureFramebuffer
+}
+
+function setRenderingDefaults(ctx: WebGLContext) {
+    const { gl, state } = ctx
+    state.disable(gl.CULL_FACE)
+    state.disable(gl.BLEND)
+    state.disable(gl.DEPTH_TEST)
+    state.disable(gl.SCISSOR_TEST)
+    state.depthMask(false)
+    state.colorMask(true, true, true, true)
+    state.clearColor(0, 0, 0, 0)
+}
+
+export interface HistogramPyramid {
+    pyramidTex: Texture
+    count: number
+    height: number
+    levels: number
+    scale: Vec2
+}
+
+export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2): HistogramPyramid {
+    const { gl, framebufferCache } = ctx
+
+    // printTexture(ctx, inputTexture, 2)
+    if (inputTexture.width !== inputTexture.height || !isPowerOfTwo(inputTexture.width)) {
+        throw new Error('inputTexture must be of square power-of-two size')
+    }
+
+    // This part set the levels
+    const levels = Math.ceil(Math.log(inputTexture.width) / Math.log(2))
+    const maxSize = Math.pow(2, levels)
+    // console.log('levels', levels, 'maxSize', maxSize)
+
+    const pyramidTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    pyramidTexture.define(maxSize, maxSize)
+
+    const framebuffer = framebufferCache.get('reduction').value
+    pyramidTexture.attachFramebuffer(framebuffer, 0)
+    gl.clear(gl.COLOR_BUFFER_BIT)
+
+    const levelTexturesFramebuffers: TextureFramebuffer[] = []
+    for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i))
+
+    const renderable = getHistopyramidReductionRenderable(ctx, inputTexture)
+    ctx.state.currentRenderItemId = -1
+    setRenderingDefaults(ctx)
+
+    let offset = 0;
+    for (let i = 0; i < levels; i++) {
+        const currLevel = levels - 1 - i
+        const tf = levelTexturesFramebuffers[currLevel]
+        tf.framebuffer.bind()
+        // levelTextures[currLevel].attachFramebuffer(framebuffer, 0)
+
+        const size = Math.pow(2, currLevel)
+        // console.log('size', size, 'draw-level', currLevel, 'read-level', levels - i)
+        gl.clear(gl.COLOR_BUFFER_BIT)
+        gl.viewport(0, 0, size, size)
+
+        ValueCell.update(renderable.values.uSize, Math.pow(2, i + 1) / maxSize)
+        ValueCell.update(renderable.values.uTexSize, size)
+        if (i > 0) {
+            ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture)
+            renderable.update()
+        }
+        ctx.state.currentRenderItemId = -1
+        renderable.render()
+
+        pyramidTexture.bind(0)
+        gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, offset, 0, 0, 0, size, size);
+        pyramidTexture.unbind(0)
+
+        offset += size;
+    }
+
+    gl.finish()
+
+    // printTexture(ctx, pyramidTexture, 2)
+
+    //
+
+    const finalCount = getHistopyramidSum(ctx, levelTexturesFramebuffers[0].texture)
+    const height = Math.ceil(finalCount / Math.pow(2, levels))
+    // const scale = Vec2.create(maxSize / inputTexture.width, maxSize / inputTexture.height)
+    // console.log('height', height, 'finalCount', finalCount, 'scale', scale)
+
+
+    return {
+        pyramidTex: pyramidTexture,
+        count: finalCount,
+        height,
+        levels,
+        scale
+    }
+}
\ No newline at end of file
diff --git a/src/mol-gl/compute/histogram-pyramid/sum.ts b/src/mol-gl/compute/histogram-pyramid/sum.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cc360e859463268776a95bde4d95e128f5024099
--- /dev/null
+++ b/src/mol-gl/compute/histogram-pyramid/sum.ts
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createComputeRenderable, ComputeRenderable } from '../../renderable'
+import { WebGLContext } from '../../webgl/context';
+import { createComputeRenderItem } from '../../webgl/render-item';
+import { Values, TextureSpec } from '../../renderable/schema';
+import { Texture, createTexture } from 'mol-gl/webgl/texture';
+import { ShaderCode } from 'mol-gl/shader-code';
+import { ValueCell } from 'mol-util';
+import { decodeFloatRGB } from 'mol-util/float-packing';
+import { QuadSchema, QuadValues } from '../util';
+
+const HistopyramidSumSchema = {
+    ...QuadSchema,
+    tTexture: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+}
+
+let HistopyramidSumRenderable: ComputeRenderable<Values<typeof HistopyramidSumSchema>>
+function getHistopyramidSumRenderable(ctx: WebGLContext, texture: Texture) {
+    if (HistopyramidSumRenderable) {
+        ValueCell.update(HistopyramidSumRenderable.values.tTexture, texture)
+        HistopyramidSumRenderable.update()
+        return HistopyramidSumRenderable
+    } else {
+        const values: Values<typeof HistopyramidSumSchema> = {
+            ...QuadValues,
+            tTexture: ValueCell.create(texture),
+        }
+
+        const schema = { ...HistopyramidSumSchema }
+        const shaderCode = ShaderCode(
+            require('mol-gl/shader/quad.vert').default,
+            require('mol-gl/shader/histogram-pyramid/sum.frag').default
+        )
+        const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values)
+
+        HistopyramidSumRenderable = createComputeRenderable(renderItem, values)
+        return HistopyramidSumRenderable
+    }
+}
+
+let SumTexture: Texture
+function getSumTexture(ctx: WebGLContext) {
+    if (SumTexture) return SumTexture
+    SumTexture = createTexture(ctx, 'image-uint8', 'rgba', 'ubyte', 'nearest')
+    SumTexture.define(1, 1)
+    return SumTexture
+}
+
+/** name for shared framebuffer used for histogram-pyramid operations */
+const FramebufferName = 'histogram-pyramid-sum'
+
+function setRenderingDefaults(ctx: WebGLContext) {
+    const { gl, state } = ctx
+    state.disable(gl.CULL_FACE)
+    state.disable(gl.BLEND)
+    state.disable(gl.DEPTH_TEST)
+    state.disable(gl.SCISSOR_TEST)
+    state.depthMask(false)
+    state.colorMask(true, true, true, true)
+    state.clearColor(0, 0, 0, 0)
+}
+
+const sumArray = new Uint8Array(4)
+export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
+    const { gl, framebufferCache } = ctx
+
+    const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture)
+    ctx.state.currentRenderItemId = -1
+    
+    const framebuffer = framebufferCache.get(FramebufferName).value
+    const sumTexture = getSumTexture(ctx)
+    sumTexture.attachFramebuffer(framebuffer, 0)
+
+    setRenderingDefaults(ctx)
+
+    gl.viewport(0, 0, 1, 1)
+    renderable.render()
+    gl.finish()
+    ctx.readPixels(0, 0, 1, 1, sumArray)
+    ctx.unbindFramebuffer()
+
+    return decodeFloatRGB(sumArray[0], sumArray[1], sumArray[2])
+}
\ No newline at end of file
diff --git a/src/mol-gl/compute/marching-cubes/active-voxels.ts b/src/mol-gl/compute/marching-cubes/active-voxels.ts
new file mode 100644
index 0000000000000000000000000000000000000000..04a9f3f8770382b51b566882125dd716df65b3c8
--- /dev/null
+++ b/src/mol-gl/compute/marching-cubes/active-voxels.ts
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createComputeRenderable } from '../../renderable'
+import { WebGLContext } from '../../webgl/context';
+import { createComputeRenderItem } from '../../webgl/render-item';
+import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
+import { Texture, createTexture } from 'mol-gl/webgl/texture';
+import { ShaderCode } from 'mol-gl/shader-code';
+import { ValueCell } from 'mol-util';
+import { Vec3, Vec2 } from 'mol-math/linear-algebra';
+import { QuadSchema, QuadValues } from '../util';
+import { getTriCount } from './tables';
+
+/** name for shared framebuffer used for gpu marching cubes operations */
+const FramebufferName = 'marching-cubes-active-voxels'
+
+const ActiveVoxelsSchema = {
+    ...QuadSchema,
+
+    tTriCount: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uIsoValue: UniformSpec('f'),
+
+    uGridDim: UniformSpec('v3'),
+    uGridTexDim: UniformSpec('v3'),
+
+    uScale: UniformSpec('v2'),
+}
+
+function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2) {
+    const values: Values<typeof ActiveVoxelsSchema> = {
+        ...QuadValues,
+        uQuadScale: ValueCell.create(scale),
+
+        tTriCount: ValueCell.create(getTriCount()),
+        tVolumeData: ValueCell.create(volumeData),
+        uIsoValue: ValueCell.create(isoValue),
+
+        uGridDim: ValueCell.create(gridDim),
+        uGridTexDim: ValueCell.create(gridTexDim),
+
+        uScale: ValueCell.create(scale),
+    }
+
+    const schema = { ...ActiveVoxelsSchema }
+    const shaderCode = ShaderCode(
+        require('mol-gl/shader/quad.vert').default,
+        require('mol-gl/shader/marching-cubes/active-voxels.frag').default
+    )
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values)
+
+    return createComputeRenderable(renderItem, values);
+}
+
+function setRenderingDefaults(ctx: WebGLContext) {
+    const { gl, state } = ctx
+    state.disable(gl.CULL_FACE)
+    state.disable(gl.BLEND)
+    state.disable(gl.DEPTH_TEST)
+    state.disable(gl.SCISSOR_TEST)
+    state.depthMask(false)
+    state.colorMask(true, true, true, true)
+    state.clearColor(0, 0, 0, 0)
+}
+
+export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) {
+    const { gl, framebufferCache } = ctx
+    const { width, height } = volumeData
+
+    const framebuffer = framebufferCache.get(FramebufferName).value
+    framebuffer.bind()
+
+    const activeVoxelsTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    activeVoxelsTex.define(width, height)
+
+    const renderable = getActiveVoxelsRenderable(ctx, volumeData, gridDim, gridTexDim, isoValue, gridScale)
+    ctx.state.currentRenderItemId = -1
+
+    activeVoxelsTex.attachFramebuffer(framebuffer, 0)
+    setRenderingDefaults(ctx)
+    gl.viewport(0, 0, width, height)
+    renderable.render()
+
+    // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim)
+    // console.log('volumeData', volumeData)
+    // console.log('at', readTexture(ctx, activeVoxelsTex))
+
+    gl.finish()
+
+    return activeVoxelsTex
+}
\ No newline at end of file
diff --git a/src/mol-gl/compute/marching-cubes/isosurface.ts b/src/mol-gl/compute/marching-cubes/isosurface.ts
new file mode 100644
index 0000000000000000000000000000000000000000..053e5d0a382d841b4dc151ce5b41abb0852fce7f
--- /dev/null
+++ b/src/mol-gl/compute/marching-cubes/isosurface.ts
@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createComputeRenderable } from '../../renderable'
+import { WebGLContext } from '../../webgl/context';
+import { createComputeRenderItem } from '../../webgl/render-item';
+import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
+import { Texture, createTexture } from 'mol-gl/webgl/texture';
+import { ShaderCode } from 'mol-gl/shader-code';
+import { ValueCell } from 'mol-util';
+import { Vec3, Vec2, Mat4 } from 'mol-math/linear-algebra';
+import { QuadSchema, QuadValues } from '../util';
+import { HistogramPyramid } from '../histogram-pyramid/reduction';
+import { getTriIndices } from './tables';
+
+/** name for shared framebuffer used for gpu marching cubes operations */
+const FramebufferName = 'marching-cubes-isosurface'
+
+const IsosurfaceSchema = {
+    ...QuadSchema,
+
+    tTriIndices: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    tActiveVoxelsPyramid: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tActiveVoxelsBase: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uIsoValue: UniformSpec('f'),
+
+    uSize: UniformSpec('f'),
+    uLevels: UniformSpec('f'),
+    uCount: UniformSpec('f'),
+
+    uGridDim: UniformSpec('v3'),
+    uGridTexDim: UniformSpec('v3'),
+    uGridTransform: UniformSpec('m4'),
+
+    uScale: UniformSpec('v2'),
+}
+
+function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, height: number) {
+    // console.log('uSize', Math.pow(2, levels))
+    const values: Values<typeof IsosurfaceSchema> = {
+        ...QuadValues,
+        uQuadScale: ValueCell.create(Vec2.create(1, height / Math.pow(2, levels))),
+
+        tTriIndices: ValueCell.create(getTriIndices()),
+        tActiveVoxelsPyramid: ValueCell.create(activeVoxelsPyramid),
+        tActiveVoxelsBase: ValueCell.create(activeVoxelsBase),
+        tVolumeData: ValueCell.create(volumeData),
+        uIsoValue: ValueCell.create(isoValue),
+
+        uSize: ValueCell.create(Math.pow(2, levels)),
+        uLevels: ValueCell.create(levels),
+        uCount: ValueCell.create(count),
+
+        uGridDim: ValueCell.create(gridDim),
+        uGridTexDim: ValueCell.create(gridTexDim),
+        uGridTransform: ValueCell.create(transform),
+
+        uScale: ValueCell.create(scale),
+    }
+
+    const schema = { ...IsosurfaceSchema }
+    const shaderCode = ShaderCode(
+        require('mol-gl/shader/quad.vert').default,
+        require('mol-gl/shader/marching-cubes/isosurface.frag').default,
+        { drawBuffers: true }
+    )
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values)
+
+    return createComputeRenderable(renderItem, values);
+}
+
+function setRenderingDefaults(ctx: WebGLContext) {
+    const { gl, state } = ctx
+    state.disable(gl.CULL_FACE)
+    state.disable(gl.BLEND)
+    state.disable(gl.DEPTH_TEST)
+    state.disable(gl.SCISSOR_TEST)
+    state.depthMask(false)
+    state.colorMask(true, true, true, true)
+    state.clearColor(0, 0, 0, 0)
+}
+
+export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, vertexGroupTexture?: Texture, normalTexture?: Texture) {
+    const { gl, framebufferCache } = ctx
+    const { pyramidTex, height, levels, scale, count } = histogramPyramid
+
+    // console.log('iso', 'gridDim', gridDim, 'scale', scale, 'gridTexDim', gridTexDim)
+    // console.log('iso volumeData', volumeData)
+
+    const framebuffer = framebufferCache.get(FramebufferName).value
+
+    let needsClear = false
+
+    if (!vertexGroupTexture) {
+        vertexGroupTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+        vertexGroupTexture.define(pyramidTex.width, pyramidTex.height)
+    } else if (vertexGroupTexture.width !== pyramidTex.width || vertexGroupTexture.height !== pyramidTex.height) {
+        vertexGroupTexture.define(pyramidTex.width, pyramidTex.height)
+    } else {
+        needsClear = true
+    }
+
+    if (!normalTexture) {
+        normalTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+        normalTexture.define(pyramidTex.width, pyramidTex.height)
+    } else if (normalTexture.width !== pyramidTex.width || normalTexture.height !== pyramidTex.height) {
+        normalTexture.define(pyramidTex.width, pyramidTex.height)
+    } else {
+        needsClear = true
+    }
+
+    // const infoTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    // infoTex.define(pyramidTex.width, pyramidTex.height)
+
+    // const pointTexA = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    // pointTexA.define(pyramidTex.width, pyramidTex.height)
+
+    // const pointTexB = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    // pointTexB.define(pyramidTex.width, pyramidTex.height)
+
+    // const coordTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    // coordTex.define(pyramidTex.width, pyramidTex.height)
+
+    // const indexTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    // indexTex.define(pyramidTex.width, pyramidTex.height)
+
+    const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, height)
+    ctx.state.currentRenderItemId = -1
+
+    vertexGroupTexture.attachFramebuffer(framebuffer, 0)
+    normalTexture.attachFramebuffer(framebuffer, 1)
+    // infoTex.attachFramebuffer(framebuffer, 1)
+    // pointTexA.attachFramebuffer(framebuffer, 2)
+    // pointTexB.attachFramebuffer(framebuffer, 3)
+    // coordTex.attachFramebuffer(framebuffer, 4)
+    // indexTex.attachFramebuffer(framebuffer, 5)
+
+    const { drawBuffers } = ctx.extensions
+    if (!drawBuffers) throw new Error('need WebGL draw buffers')
+
+    drawBuffers.drawBuffers([
+        drawBuffers.COLOR_ATTACHMENT0,
+        drawBuffers.COLOR_ATTACHMENT1,
+        // drawBuffers.COLOR_ATTACHMENT2,
+        // drawBuffers.COLOR_ATTACHMENT3,
+        // drawBuffers.COLOR_ATTACHMENT4,
+        // drawBuffers.COLOR_ATTACHMENT5
+    ])
+
+    setRenderingDefaults(ctx)
+    gl.viewport(0, 0, pyramidTex.width, pyramidTex.height)
+    if (needsClear) gl.clear(gl.COLOR_BUFFER_BIT)
+    renderable.render()
+
+    gl.finish()
+
+    // const vgt = readTexture(ctx, vertexGroupTexture, pyramidTex.width, height)
+    // console.log('vertexGroupTexture', vgt.array.subarray(0, 4 * count))
+
+    // const vt = readTexture(ctx, verticesTex, pyramidTex.width, height)
+    // console.log('vt', vt)
+    // const vertices = new Float32Array(3 * compacted.count)
+    // for (let i = 0; i < compacted.count; ++i) {
+    //     vertices[i * 3] = vt.array[i * 4]
+    //     vertices[i * 3 + 1] = vt.array[i * 4 + 1]
+    //     vertices[i * 3 + 2] = vt.array[i * 4 + 2]
+    // }
+    // console.log('vertices', vertices)
+
+    // const it = readTexture(ctx, infoTex, pyramidTex.width, height)
+    // console.log('info', it.array.subarray(0, 4 * compacted.count))
+
+    // const pat = readTexture(ctx, pointTexA, pyramidTex.width, height)
+    // console.log('point a', pat.array.subarray(0, 4 * compacted.count))
+
+    // const pbt = readTexture(ctx, pointTexB, pyramidTex.width, height)
+    // console.log('point b', pbt.array.subarray(0, 4 * compacted.count))
+
+    // const ct = readTexture(ctx, coordTex, pyramidTex.width, height)
+    // console.log('coord', ct.array.subarray(0, 4 * compacted.count))
+
+    // const idxt = readTexture(ctx, indexTex, pyramidTex.width, height)
+    // console.log('index', idxt.array.subarray(0, 4 * compacted.count))
+
+    // const { field, idField } = await fieldFromTexture2d(ctx, volumeData, gridDimensions)
+    // console.log({ field, idField })
+
+    // const valuesA = new Float32Array(compacted.count)
+    // const valuesB = new Float32Array(compacted.count)
+    // for (let i = 0; i < compacted.count; ++i) {
+    //     valuesA[i] = field.space.get(field.data, pat.array[i * 4], pat.array[i * 4 + 1], pat.array[i * 4 + 2])
+    //     valuesB[i] = field.space.get(field.data, pbt.array[i * 4], pbt.array[i * 4 + 1], pbt.array[i * 4 + 2])
+    // }
+    // console.log('valuesA', valuesA)
+    // console.log('valuesB', valuesB)
+
+    return { vertexGroupTexture, normalTexture, vertexCount: count }
+}
\ No newline at end of file
diff --git a/src/mol-gl/compute/marching-cubes/tables.ts b/src/mol-gl/compute/marching-cubes/tables.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3fafe4de31e5b1d5c7d6120b468b19ba483210e6
--- /dev/null
+++ b/src/mol-gl/compute/marching-cubes/tables.ts
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { TriTable,  } from 'mol-geo/util/marching-cubes/tables';
+import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
+
+let TriCount: TextureImage<Uint8Array> | undefined
+export function getTriCount(): TextureImage<Uint8Array> {
+    if (TriCount !== undefined) return TriCount
+    TriCount = createTextureImage(16 * 16, 1, Uint8Array)
+    const { array } = TriCount
+    for (let i = 0, il = TriTable.length; i < il; ++i) {
+        array[i] = TriTable[i].length / 3
+    }
+    return TriCount
+}
+
+let TriIndices: TextureImage<Uint8Array> | undefined
+export function getTriIndices(): TextureImage<Uint8Array> {
+    if (TriIndices !== undefined) return TriIndices
+    TriIndices = createTextureImage(64 * 64, 1, Uint8Array)
+    const { array } = TriIndices
+    for (let i = 0, il = TriTable.length; i < il; ++i) {
+        for (let j = 0; j < 16; ++j) {
+            if (j < TriTable[i].length) {
+                array[i * 16 + j] = TriTable[i][j]
+            } else {
+                array[i * 16 + j] = 255
+            }
+        }
+    }
+    return TriIndices
+}
\ No newline at end of file
diff --git a/src/mol-gl/compute/util.ts b/src/mol-gl/compute/util.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fb7f9947e7f493b1d268c72fd9af38605c333135
--- /dev/null
+++ b/src/mol-gl/compute/util.ts
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { WebGLContext } from 'mol-gl/webgl/context';
+import { Texture } from 'mol-gl/webgl/texture';
+import { printTextureImage } from 'mol-gl/renderable/util';
+import { defaults, ValueCell } from 'mol-util';
+import { ValueSpec, AttributeSpec, UniformSpec, Values } from 'mol-gl/renderable/schema';
+import { Vec2 } from 'mol-math/linear-algebra';
+
+export const QuadPositions = new Float32Array([
+     1.0,  1.0,  -1.0,  1.0,  -1.0, -1.0, // First triangle
+    -1.0, -1.0,   1.0, -1.0,   1.0,  1.0  // Second triangle
+])
+
+export const QuadSchema = {
+    drawCount: ValueSpec('number'),
+    instanceCount: ValueSpec('number'),
+    aPosition: AttributeSpec('float32', 2, 0),
+    uQuadScale: UniformSpec('v2'),
+}
+
+export const QuadValues: Values<typeof QuadSchema> = {
+    drawCount: ValueCell.create(6),
+    instanceCount: ValueCell.create(1),
+    aPosition: ValueCell.create(QuadPositions),
+    uQuadScale: ValueCell.create(Vec2.create(1, 1)),
+}
+
+//
+
+export function readTexture(ctx: WebGLContext, texture: Texture, width?: number, height?: number) {
+    const { gl, framebufferCache } = ctx
+    width = defaults(width, texture.width)
+    height = defaults(height, texture.height)
+    const framebuffer = framebufferCache.get('read-texture').value
+    const array = texture.type === gl.UNSIGNED_BYTE ? new Uint8Array(width * height * 4) : new Float32Array(width * height * 4)
+    framebuffer.bind()
+    texture.attachFramebuffer(framebuffer, 0)
+    ctx.readPixels(0, 0, width, height, array)
+
+    return { array, width, height }
+}
+
+export function printTexture(ctx: WebGLContext, texture: Texture, scale: number) {
+    printTextureImage(readTexture(ctx, texture), scale)
+}
\ No newline at end of file
diff --git a/src/mol-gl/render-object.ts b/src/mol-gl/render-object.ts
index 57ad487070e0ec547dc07fe982808eecab3690ae..ec7536ff335d4b2405125dd1088a109cc7d889ca 100644
--- a/src/mol-gl/render-object.ts
+++ b/src/mol-gl/render-object.ts
@@ -8,33 +8,30 @@ import { RenderableState, Renderable } from './renderable'
 import { RenderableValues } from './renderable/schema';
 import { idFactory } from 'mol-util/id-factory';
 import { WebGLContext } from './webgl/context';
-import { GaussianDensityValues, GaussianDensityRenderable } from './renderable/gaussian-density';
 import { DirectVolumeValues, DirectVolumeRenderable } from './renderable/direct-volume';
 import { MeshValues, MeshRenderable } from './renderable/mesh';
 import { PointsValues, PointsRenderable } from './renderable/points';
 import { LinesValues, LinesRenderable } from './renderable/lines';
 import { SpheresValues, SpheresRenderable } from './renderable/spheres';
 import { TextValues, TextRenderable } from './renderable/text';
+import { TextureMeshValues, TextureMeshRenderable } from './renderable/texture-mesh';
 
 const getNextId = idFactory(0, 0x7FFFFFFF)
 
-export interface BaseRenderObject { id: number, type: string, values: RenderableValues, state: RenderableState }
+export const getNextMaterialId = idFactory(0, 0x7FFFFFFF)
+
+export interface BaseRenderObject { id: number, type: string, values: RenderableValues, state: RenderableState, materialId: number }
 export interface MeshRenderObject extends BaseRenderObject { type: 'mesh', values: MeshValues }
 export interface PointsRenderObject extends BaseRenderObject { type: 'points', values: PointsValues }
 export interface SpheresRenderObject extends BaseRenderObject { type: 'spheres', values: SpheresValues }
 export interface TextRenderObject extends BaseRenderObject { type: 'text', values: TextValues }
 export interface LinesRenderObject extends BaseRenderObject { type: 'lines', values: LinesValues }
 export interface DirectVolumeRenderObject extends BaseRenderObject { type: 'direct-volume', values: DirectVolumeValues }
-
-export interface GaussianDensityRenderObject extends BaseRenderObject { type: 'gaussian-density', values: GaussianDensityValues }
+export interface TextureMeshRenderObject extends BaseRenderObject { type: 'texture-mesh', values: TextureMeshValues }
 
 //
 
-export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | TextRenderObject | LinesRenderObject | DirectVolumeRenderObject
-
-export type ComputeRenderObject = GaussianDensityRenderObject
-
-export type RenderObject = GraphicsRenderObject | ComputeRenderObject
+export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | TextRenderObject | LinesRenderObject | DirectVolumeRenderObject | TextureMeshRenderObject
 
 export type RenderObjectKindType = {
     'mesh': MeshRenderObject
@@ -43,8 +40,7 @@ export type RenderObjectKindType = {
     'text': TextRenderObject
     'lines': LinesRenderObject
     'direct-volume': DirectVolumeRenderObject
-
-    'gaussian-density': GaussianDensityRenderObject
+    'texture-mesh': TextureMeshRenderObject
 }
 export type RenderObjectValuesType = {
     'mesh': MeshValues
@@ -53,26 +49,24 @@ export type RenderObjectValuesType = {
     'text': TextValues
     'lines': LinesValues
     'direct-volume': DirectVolumeValues
-
-    'gaussian-density': GaussianDensityValues
+    'texture-mesh': TextureMeshValues
 }
 export type RenderObjectType = keyof RenderObjectKindType
 
 //
 
-export function createRenderObject<T extends RenderObjectType>(type: T, values: RenderObjectValuesType[T], state: RenderableState): RenderObjectKindType[T] {
-    return { id: getNextId(), type, values, state } as RenderObjectKindType[T]
+export function createRenderObject<T extends RenderObjectType>(type: T, values: RenderObjectValuesType[T], state: RenderableState, materialId: number): RenderObjectKindType[T] {
+    return { id: getNextId(), type, values, state, materialId } as RenderObjectKindType[T]
 }
 
-export function createRenderable(ctx: WebGLContext, o: RenderObject): Renderable<any> {
+export function createRenderable(ctx: WebGLContext, o: GraphicsRenderObject): Renderable<any> {
     switch (o.type) {
-        case 'mesh': return MeshRenderable(ctx, o.id, o.values, o.state)
-        case 'points': return PointsRenderable(ctx, o.id, o.values, o.state)
-        case 'spheres': return SpheresRenderable(ctx, o.id, o.values, o.state)
-        case 'text': return TextRenderable(ctx, o.id, o.values, o.state)
-        case 'lines': return LinesRenderable(ctx, o.id, o.values, o.state)
-        case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values, o.state)
-
-        case 'gaussian-density': return GaussianDensityRenderable(ctx, o.id, o.values, o.state)
+        case 'mesh': return MeshRenderable(ctx, o.id, o.values, o.state, o.materialId)
+        case 'points': return PointsRenderable(ctx, o.id, o.values, o.state, o.materialId)
+        case 'spheres': return SpheresRenderable(ctx, o.id, o.values, o.state, o.materialId)
+        case 'text': return TextRenderable(ctx, o.id, o.values, o.state, o.materialId)
+        case 'lines': return LinesRenderable(ctx, o.id, o.values, o.state, o.materialId)
+        case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values, o.state, o.materialId)
+        case 'texture-mesh': return TextureMeshRenderable(ctx, o.id, o.values, o.state, o.materialId)
     }
 }
\ No newline at end of file
diff --git a/src/mol-gl/renderable.ts b/src/mol-gl/renderable.ts
index 34f0eee4159c8750f94c7eabc9669119e7b4a6b8..19bb43892275aae908decac6710fa30a9b1ba7ce 100644
--- a/src/mol-gl/renderable.ts
+++ b/src/mol-gl/renderable.ts
@@ -6,7 +6,7 @@
 
 import { Program } from './webgl/program';
 import { RenderableValues, Values, RenderableSchema } from './renderable/schema';
-import { RenderVariant, RenderItem } from './webgl/render-item';
+import { GraphicsRenderItem, ComputeRenderItem, GraphicsRenderVariant } from './webgl/render-item';
 import { ValueCell } from 'mol-util';
 import { idFactory } from 'mol-util/id-factory';
 import { clamp } from 'mol-math/interpolate';
@@ -22,22 +22,24 @@ export type RenderableState = {
 
 export interface Renderable<T extends RenderableValues> {
     readonly id: number
+    readonly materialId: number
     readonly values: T
     readonly state: RenderableState
 
-    render: (variant: RenderVariant) => void
-    getProgram: (variant: RenderVariant) => Program
+    render: (variant: GraphicsRenderVariant) => void
+    getProgram: (variant: GraphicsRenderVariant) => Program
     update: () => void
     dispose: () => void
 }
 
-export function createRenderable<T extends Values<RenderableSchema>>(renderItem: RenderItem, values: T, state: RenderableState): Renderable<T> {
+export function createRenderable<T extends Values<RenderableSchema>>(renderItem: GraphicsRenderItem, values: T, state: RenderableState): Renderable<T> {
     return {
         id: getNextRenderableId(),
+        materialId: renderItem.materialId,
         values,
         state,
 
-        render: (variant: RenderVariant) => {
+        render: (variant: GraphicsRenderVariant) => {
             if (values.uAlpha && values.alpha) {
                 ValueCell.updateIfChanged(values.uAlpha, clamp(values.alpha.ref.value * state.alphaFactor, 0, 1))
             }
@@ -46,7 +48,29 @@ export function createRenderable<T extends Values<RenderableSchema>>(renderItem:
             }
             renderItem.render(variant)
         },
-        getProgram: (variant: RenderVariant) => renderItem.getProgram(variant),
+        getProgram: (variant: GraphicsRenderVariant) => renderItem.getProgram(variant),
+        update: () => renderItem.update(),
+        dispose: () => renderItem.destroy()
+    }
+}
+
+//
+
+export interface ComputeRenderable<T extends RenderableValues> {
+    readonly id: number
+    readonly values: T
+
+    render: () => void
+    update: () => void
+    dispose: () => void
+}
+
+export function createComputeRenderable<T extends Values<RenderableSchema>>(renderItem: ComputeRenderItem, values: T): ComputeRenderable<T> {
+    return {
+        id: getNextRenderableId(),
+        values,
+
+        render: () => renderItem.render('compute'),
         update: () => renderItem.update(),
         dispose: () => renderItem.destroy()
     }
diff --git a/src/mol-gl/renderable/direct-volume.ts b/src/mol-gl/renderable/direct-volume.ts
index eb94c44996943bdd529c67c93898f25375b9009c..8b8d2dfe47ab5c0366156b739bbfcba32debbb2c 100644
--- a/src/mol-gl/renderable/direct-volume.ts
+++ b/src/mol-gl/renderable/direct-volume.ts
@@ -6,7 +6,7 @@
 
 import { Renderable, RenderableState, createRenderable } from '../renderable'
 import { WebGLContext } from '../webgl/context';
-import { createRenderItem } from '../webgl/render-item';
+import { createGraphicsRenderItem } from '../webgl/render-item';
 import { AttributeSpec, Values, UniformSpec, GlobalUniformSchema, InternalSchema, TextureSpec, ValueSpec, ElementsSpec, DefineSpec, InternalValues } from './schema';
 import { DirectVolumeShaderCode } from '../shader-code';
 import { ValueCell } from 'mol-util';
@@ -24,6 +24,11 @@ export const DirectVolumeSchema = {
     tOverpaint: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
     dOverpaint: DefineSpec('boolean'),
 
+    uTransparencyTexDim: UniformSpec('v2'),
+    tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    dTransparency: DefineSpec('boolean'),
+    dTransparencyVariant: DefineSpec('string', ['single', 'multi']),
+
     uInstanceCount: UniformSpec('i'),
     uGroupCount: UniformSpec('i'),
 
@@ -62,18 +67,18 @@ export const DirectVolumeSchema = {
 
     dGridTexType: DefineSpec('string', ['2d', '3d']),
     uGridTexDim: UniformSpec('v3'),
-    tGridTex: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    tGridTex: TextureSpec('texture', 'rgba', 'float', 'nearest'),
 }
 export type DirectVolumeSchema = typeof DirectVolumeSchema
 export type DirectVolumeValues = Values<DirectVolumeSchema>
 
-export function DirectVolumeRenderable(ctx: WebGLContext, id: number, values: DirectVolumeValues, state: RenderableState): Renderable<DirectVolumeValues> {
+export function DirectVolumeRenderable(ctx: WebGLContext, id: number, values: DirectVolumeValues, state: RenderableState, materialId: number): Renderable<DirectVolumeValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...DirectVolumeSchema }
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
-        uPickable: ValueCell.create(state.pickable ? 1 : 0)
+        uPickable: ValueCell.create(state.pickable ? 1 : 0),
     }
     const shaderCode = DirectVolumeShaderCode
-    const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId)
     return createRenderable(renderItem, values, state);
 }
\ No newline at end of file
diff --git a/src/mol-gl/renderable/gaussian-density.ts b/src/mol-gl/renderable/gaussian-density.ts
deleted file mode 100644
index a8b4f1518a862487568da811962de281eb30bc9e..0000000000000000000000000000000000000000
--- a/src/mol-gl/renderable/gaussian-density.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Renderable, RenderableState, createRenderable } from '../renderable'
-import { WebGLContext } from '../webgl/context';
-import { createRenderItem } from '../webgl/render-item';
-import { AttributeSpec, Values, UniformSpec, ValueSpec, DefineSpec, TextureSpec } from './schema';
-import { GaussianDensityShaderCode } from '../shader-code';
-
-export const GaussianDensitySchema = {
-    drawCount: ValueSpec('number'),
-    instanceCount: ValueSpec('number'),
-
-    aRadius: AttributeSpec('float32', 1, 0),
-    aPosition: AttributeSpec('float32', 3, 0),
-    aGroup: AttributeSpec('float32', 1, 0),
-
-    uCurrentSlice: UniformSpec('f'),
-    uCurrentX: UniformSpec('f'),
-    uCurrentY: UniformSpec('f'),
-    uBboxMin: UniformSpec('v3'),
-    uBboxMax: UniformSpec('v3'),
-    uBboxSize: UniformSpec('v3'),
-    uGridDim: UniformSpec('v3'),
-    uGridTexDim: UniformSpec('v3'),
-    uAlpha: UniformSpec('f'),
-    tMinDistanceTex: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
-
-    dGridTexType: DefineSpec('string', ['2d', '3d']),
-    dCalcType: DefineSpec('string', ['density', 'minDistance', 'groupId']),
-}
-export type GaussianDensitySchema = typeof GaussianDensitySchema
-export type GaussianDensityValues = Values<GaussianDensitySchema>
-
-export function GaussianDensityRenderable(ctx: WebGLContext, id: number, values: GaussianDensityValues, state: RenderableState): Renderable<GaussianDensityValues> {
-    const schema = { ...GaussianDensitySchema }
-    const shaderCode = GaussianDensityShaderCode
-    const renderItem = createRenderItem(ctx, 'points', shaderCode, schema, values)
-
-    return createRenderable(renderItem, values, state);
-}
\ No newline at end of file
diff --git a/src/mol-gl/renderable/lines.ts b/src/mol-gl/renderable/lines.ts
index 54e62c5b448bf5b4bf8bcf88037092564fcf52ce..e44a62f727944a953bf33ada6fcc43307fd2b3dc 100644
--- a/src/mol-gl/renderable/lines.ts
+++ b/src/mol-gl/renderable/lines.ts
@@ -6,7 +6,7 @@
 
 import { Renderable, RenderableState, createRenderable } from '../renderable'
 import { WebGLContext } from '../webgl/context';
-import { createRenderItem } from '../webgl/render-item';
+import { createGraphicsRenderItem } from '../webgl/render-item';
 import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues } from './schema';
 import { ValueCell } from 'mol-util';
 import { LinesShaderCode } from '../shader-code';
@@ -25,14 +25,14 @@ export const LinesSchema = {
 export type LinesSchema = typeof LinesSchema
 export type LinesValues = Values<LinesSchema>
 
-export function LinesRenderable(ctx: WebGLContext, id: number, values: LinesValues, state: RenderableState): Renderable<LinesValues> {
+export function LinesRenderable(ctx: WebGLContext, id: number, values: LinesValues, state: RenderableState, materialId: number): Renderable<LinesValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...LinesSchema }
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
         uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = LinesShaderCode
-    const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId)
 
     return createRenderable(renderItem, values, state);
 }
\ No newline at end of file
diff --git a/src/mol-gl/renderable/mesh.ts b/src/mol-gl/renderable/mesh.ts
index a3d70909409dda202f1649ab5397ae0cc47e8d29..bcf65b9f0901227dc3407c7e64832fdb434ecc14 100644
--- a/src/mol-gl/renderable/mesh.ts
+++ b/src/mol-gl/renderable/mesh.ts
@@ -6,7 +6,7 @@
 
 import { Renderable, RenderableState, createRenderable } from '../renderable'
 import { WebGLContext } from '../webgl/context';
-import { createRenderItem } from '../webgl/render-item';
+import { createGraphicsRenderItem } from '../webgl/render-item';
 import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues } from './schema';
 import { MeshShaderCode } from '../shader-code';
 import { ValueCell } from 'mol-util';
@@ -23,14 +23,14 @@ export const MeshSchema = {
 export type MeshSchema = typeof MeshSchema
 export type MeshValues = Values<MeshSchema>
 
-export function MeshRenderable(ctx: WebGLContext, id: number, values: MeshValues, state: RenderableState): Renderable<MeshValues> {
+export function MeshRenderable(ctx: WebGLContext, id: number, values: MeshValues, state: RenderableState, materialId: number): Renderable<MeshValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...MeshSchema }
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
         uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = MeshShaderCode
-    const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId)
 
     return createRenderable(renderItem, values, state)
 }
\ No newline at end of file
diff --git a/src/mol-gl/renderable/points.ts b/src/mol-gl/renderable/points.ts
index 60f9f243622fcda3422b43bc1ce5b028f8dd1be2..6b2cf5e1a3e69891055c76e22fd39d28baae3c00 100644
--- a/src/mol-gl/renderable/points.ts
+++ b/src/mol-gl/renderable/points.ts
@@ -6,7 +6,7 @@
 
 import { Renderable, RenderableState, createRenderable } from '../renderable'
 import { WebGLContext } from '../webgl/context';
-import { createRenderItem } from '../webgl/render-item';
+import { createGraphicsRenderItem } from '../webgl/render-item';
 import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues } from './schema';
 import { PointsShaderCode } from '../shader-code';
 import { ValueCell } from 'mol-util';
@@ -22,13 +22,13 @@ export const PointsSchema = {
 export type PointsSchema = typeof PointsSchema
 export type PointsValues = Values<PointsSchema>
 
-export function PointsRenderable(ctx: WebGLContext, id: number, values: PointsValues, state: RenderableState): Renderable<PointsValues> {
+export function PointsRenderable(ctx: WebGLContext, id: number, values: PointsValues, state: RenderableState, materialId: number): Renderable<PointsValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...PointsSchema }
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
         uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = PointsShaderCode
-    const renderItem = createRenderItem(ctx, 'points', shaderCode, schema, { ...values, ...internalValues })
+    const renderItem = createGraphicsRenderItem(ctx, 'points', shaderCode, schema, { ...values, ...internalValues }, materialId)
     return createRenderable(renderItem, values, state);
 }
\ No newline at end of file
diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts
index 28e36306fe14d52b691e0da71cbc5781e3702da1..4c9a9e453dcd536a60efccbc3c7ac8a06be8411c 100644
--- a/src/mol-gl/renderable/schema.ts
+++ b/src/mol-gl/renderable/schema.ts
@@ -1,11 +1,11 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ValueCell } from 'mol-util';
-import { ArrayKind, BufferItemSize, ElementsKind, AttributeValues } from '../webgl/buffer';
+import { AttributeItemSize, ElementsKind, AttributeValues, AttributeKind } from '../webgl/buffer';
 import { UniformKind, UniformValues } from '../webgl/uniform';
 import { DefineKind, DefineValues } from '../shader-code';
 import { Vec2, Vec3, Vec4, Mat3, Mat4 } from 'mol-math/linear-algebra';
@@ -68,27 +68,19 @@ export function splitValues(schema: RenderableSchema, values: RenderableValues)
     const defineValues: DefineValues = {}
     const textureValues: TextureValues = {}
     const uniformValues: UniformValues = {}
+    const materialUniformValues: UniformValues = {}
     Object.keys(schema).forEach(k => {
-        if (schema[k].type === 'attribute') attributeValues[k] = values[k]
-        if (schema[k].type === 'define') defineValues[k] = values[k]
-        if (schema[k].type === 'texture') textureValues[k] = values[k]
-        if (schema[k].type === 'uniform') uniformValues[k] = values[k]
+        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
+        if (spec.type === 'uniform' && values[k] !== undefined) {
+            if (spec.isMaterial) materialUniformValues[k] = values[k]
+            else uniformValues[k] = values[k]
+        }
     })
-    return { attributeValues, defineValues, textureValues, uniformValues }
-}
-
-export function splitKeys(schema: RenderableSchema) {
-    const attributeKeys: string[] = []
-    const defineKeys: string[] = []
-    const textureKeys: string[] = []
-    const uniformKeys: string[] = []
-    Object.keys(schema).forEach(k => {
-        if (schema[k].type === 'attribute') attributeKeys.push(k)
-        if (schema[k].type === 'define') defineKeys.push(k)
-        if (schema[k].type === 'texture') textureKeys.push(k)
-        if (schema[k].type === 'uniform') uniformKeys.push(k)
-    })
-    return { attributeKeys, defineKeys, textureKeys, uniformKeys }
+    return { attributeValues, defineValues, textureValues, uniformValues, materialUniformValues }
 }
 
 export type Versions<T extends RenderableValues> = { [k in keyof T]: number }
@@ -102,14 +94,14 @@ export function getValueVersions<T extends RenderableValues>(values: T) {
 
 //
 
-export type AttributeSpec<K extends ArrayKind> = { type: 'attribute', kind: K, itemSize: BufferItemSize, divisor: number }
-export function AttributeSpec<K extends ArrayKind>(kind: K, itemSize: BufferItemSize, divisor: number): AttributeSpec<K> {
+export type AttributeSpec<K extends AttributeKind> = { type: 'attribute', kind: K, itemSize: AttributeItemSize, divisor: number }
+export function AttributeSpec<K extends AttributeKind>(kind: K, itemSize: AttributeItemSize, divisor: number): AttributeSpec<K> {
     return { type: 'attribute', kind, itemSize, divisor }
 }
 
-export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K }
-export function UniformSpec<K extends UniformKind>(kind: K): UniformSpec<K> {
-    return { type: 'uniform', kind }
+export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K, isMaterial: boolean }
+export function UniformSpec<K extends UniformKind>(kind: K, isMaterial = false): UniformSpec<K> {
+    return { type: 'uniform', kind, isMaterial }
 }
 
 export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter }
@@ -136,7 +128,7 @@ export function ValueSpec<K extends ValueKind>(kind: K): ValueSpec<K> {
 
 export type RenderableSchema = {
     [k: string]: (
-        AttributeSpec<ArrayKind> | UniformSpec<UniformKind> | TextureSpec<TextureKind> |
+        AttributeSpec<AttributeKind> | UniformSpec<UniformKind> | TextureSpec<TextureKind> |
         ValueSpec<ValueKind> | DefineSpec<DefineKind> | ElementsSpec<ElementsKind>
     )
 }
@@ -154,9 +146,13 @@ export const GlobalUniformSchema = {
     uInvProjection: UniformSpec('m4'),
     uModelViewProjection: UniformSpec('m4'),
     uInvModelViewProjection: UniformSpec('m4'),
-    // uLightPosition: Uniform('v3'),
-    uLightColor: UniformSpec('v3'),
-    uLightAmbient: UniformSpec('v3'),
+
+    uLightIntensity: UniformSpec('f'),
+    uAmbientIntensity: UniformSpec('f'),
+
+    uMetalness: UniformSpec('f'),
+    uRoughness: UniformSpec('f'),
+    uReflectivity: UniformSpec('f'),
 
     uPixelRatio: UniformSpec('f'),
     uViewportHeight: UniformSpec('f'),
@@ -170,18 +166,18 @@ export const GlobalUniformSchema = {
     uPickingAlphaThreshold: UniformSpec('f'),
 }
 export type GlobalUniformSchema = typeof GlobalUniformSchema
-export type GlobalUniformValues = { [k in keyof GlobalUniformSchema]: ValueCell<any> }
+export type GlobalUniformValues = Values<GlobalUniformSchema> // { [k in keyof GlobalUniformSchema]: ValueCell<any> }
 
 export const InternalSchema = {
     uObjectId: UniformSpec('i'),
-    uPickable: UniformSpec('i'),
+    uPickable: UniformSpec('i', true),
 }
 export type InternalSchema = typeof InternalSchema
 export type InternalValues = { [k in keyof InternalSchema]: ValueCell<any> }
 
 export const ColorSchema = {
     // aColor: AttributeSpec('float32', 3, 0), // TODO
-    uColor: UniformSpec('v3'),
+    uColor: UniformSpec('v3', true),
     uColorTexDim: UniformSpec('v2'),
     tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
     dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']),
@@ -191,7 +187,7 @@ export type ColorValues = Values<ColorSchema>
 
 export const SizeSchema = {
     // aSize: AttributeSpec('float32', 1, 0), // TODO
-    uSize: UniformSpec('f'),
+    uSize: UniformSpec('f', true),
     uSizeTexDim: UniformSpec('v2'),
     tSize: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     dSizeType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']),
@@ -215,10 +211,22 @@ export const OverpaintSchema = {
 export type OverpaintSchema = typeof OverpaintSchema
 export type OverpaintValues = Values<OverpaintSchema>
 
+export const TransparencySchema = {
+    // aTransparency: AttributeSpec('float32', 1, 0), // TODO
+    uTransparencyTexDim: UniformSpec('v2'),
+    tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    dTransparency: DefineSpec('boolean'),
+    // dTransparencyType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']), // TODO
+    dTransparencyVariant: DefineSpec('string', ['single', 'multi']),
+}
+export type TransparencySchema = typeof TransparencySchema
+export type TransparencyValues = Values<TransparencySchema>
+
 export const BaseSchema = {
     ...ColorSchema,
     ...MarkerSchema,
     ...OverpaintSchema,
+    ...TransparencySchema,
 
     aInstance: AttributeSpec('float32', 1, 1),
     aGroup: AttributeSpec('float32', 1, 0),
@@ -231,12 +239,12 @@ export const BaseSchema = {
     /**
      * final alpha, calculated as `values.alpha * state.alpha`
      */
-    uAlpha: UniformSpec('f'),
+    uAlpha: UniformSpec('f', true),
     uInstanceCount: UniformSpec('i'),
     uGroupCount: UniformSpec('i'),
 
-    uHighlightColor: UniformSpec('v3'),
-    uSelectColor: UniformSpec('v3'),
+    uHighlightColor: UniformSpec('v3', true),
+    uSelectColor: UniformSpec('v3', true),
 
     drawCount: ValueSpec('number'),
     instanceCount: ValueSpec('number'),
diff --git a/src/mol-gl/renderable/spheres.ts b/src/mol-gl/renderable/spheres.ts
index 0136658ab4f5451c1610e301a89837a6c3bb759c..1af5348ec7029a76e0dd93dc790eaeac20967cc0 100644
--- a/src/mol-gl/renderable/spheres.ts
+++ b/src/mol-gl/renderable/spheres.ts
@@ -6,7 +6,7 @@
 
 import { Renderable, RenderableState, createRenderable } from '../renderable'
 import { WebGLContext } from '../webgl/context';
-import { createRenderItem } from '../webgl/render-item';
+import { createGraphicsRenderItem } from '../webgl/render-item';
 import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec } from './schema';
 import { SpheresShaderCode } from '../shader-code';
 import { ValueCell } from 'mol-util';
@@ -24,13 +24,13 @@ export const SpheresSchema = {
 export type SpheresSchema = typeof SpheresSchema
 export type SpheresValues = Values<SpheresSchema>
 
-export function SpheresRenderable(ctx: WebGLContext, id: number, values: SpheresValues, state: RenderableState): Renderable<SpheresValues> {
+export function SpheresRenderable(ctx: WebGLContext, id: number, values: SpheresValues, state: RenderableState, materialId: number): Renderable<SpheresValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...SpheresSchema }
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
         uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = SpheresShaderCode
-    const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId)
     return createRenderable(renderItem, values, state);
 }
\ No newline at end of file
diff --git a/src/mol-gl/renderable/text.ts b/src/mol-gl/renderable/text.ts
index 6f10a8fad2fc94589cfe215d39f0bbd8466b5c0e..1bf43a8569bf25c8cb22efa3bff6730b7f99b61e 100644
--- a/src/mol-gl/renderable/text.ts
+++ b/src/mol-gl/renderable/text.ts
@@ -6,7 +6,7 @@
 
 import { Renderable, RenderableState, createRenderable } from '../renderable'
 import { WebGLContext } from '../webgl/context';
-import { createRenderItem } from '../webgl/render-item';
+import { createGraphicsRenderItem } from '../webgl/render-item';
 import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, Values, InternalSchema, SizeSchema, InternalValues, TextureSpec, ElementsSpec, ValueSpec } from './schema';
 import { TextShaderCode } from '../shader-code';
 import { ValueCell } from 'mol-util';
@@ -34,13 +34,13 @@ export const TextSchema = {
 export type TextSchema = typeof TextSchema
 export type TextValues = Values<TextSchema>
 
-export function TextRenderable(ctx: WebGLContext, id: number, values: TextValues, state: RenderableState): Renderable<TextValues> {
+export function TextRenderable(ctx: WebGLContext, id: number, values: TextValues, state: RenderableState, materialId: number): Renderable<TextValues> {
     const schema = { ...GlobalUniformSchema, ...InternalSchema, ...TextSchema }
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
         uPickable: ValueCell.create(state.pickable ? 1 : 0)
     }
     const shaderCode = TextShaderCode
-    const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues })
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId)
     return createRenderable(renderItem, values, state);
 }
\ No newline at end of file
diff --git a/src/mol-gl/renderable/texture-mesh.ts b/src/mol-gl/renderable/texture-mesh.ts
new file mode 100644
index 0000000000000000000000000000000000000000..75f6f08b1a66bea15c48cce11e5b24532c1c992d
--- /dev/null
+++ b/src/mol-gl/renderable/texture-mesh.ts
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Renderable, RenderableState, createRenderable } from '../renderable'
+import { WebGLContext } from '../webgl/context';
+import { createGraphicsRenderItem } from '../webgl/render-item';
+import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec } from './schema';
+import { MeshShaderCode } from '../shader-code';
+import { ValueCell } from 'mol-util';
+
+export const TextureMeshSchema = {
+    ...BaseSchema,
+
+    uGeoTexDim: UniformSpec('v2'),
+    /** texture has vertex positions in XYZ and group id in W */
+    tPositionGroup: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tNormal: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+
+    dFlatShaded: DefineSpec('boolean'),
+    dDoubleSided: DefineSpec('boolean'),
+    dFlipSided: DefineSpec('boolean'),
+    dGeoTexture: DefineSpec('boolean'),
+}
+export type TextureMeshSchema = typeof TextureMeshSchema
+export type TextureMeshValues = Values<TextureMeshSchema>
+
+export function TextureMeshRenderable(ctx: WebGLContext, id: number, values: TextureMeshValues, state: RenderableState, materialId: number): Renderable<TextureMeshValues> {
+    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...TextureMeshSchema }
+    const internalValues: InternalValues = {
+        uObjectId: ValueCell.create(id),
+        uPickable: ValueCell.create(state.pickable ? 1 : 0)
+    }
+    const shaderCode = MeshShaderCode
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId)
+
+    return createRenderable(renderItem, values, state)
+}
\ No newline at end of file
diff --git a/src/mol-gl/renderable/util.ts b/src/mol-gl/renderable/util.ts
index a8d4d0c8b0e74f52f05b3d70395380cde3f75040..5c768690c85d276809d48647916c7166a511ead2 100644
--- a/src/mol-gl/renderable/util.ts
+++ b/src/mol-gl/renderable/util.ts
@@ -29,9 +29,9 @@ export interface TextureVolume<T extends Uint8Array | Float32Array> {
     readonly depth: number
 }
 
-export function createTextureImage(n: number, itemSize: number, array?: Uint8Array): TextureImage<Uint8Array> {
+export function createTextureImage<T extends Uint8Array | Float32Array>(n: number, itemSize: number, arrayCtor: new (length: number) => T, array?: T): TextureImage<T> {
     const { length, width, height } = calculateTextureInfo(n, itemSize)
-    array = array && array.length >= length ? array : new Uint8Array(length)
+    array = array && array.length >= length ? array : new arrayCtor(length)
     return { array, width, height }
 }
 
@@ -65,7 +65,8 @@ export function printImageData(imageData: ImageData, scale = 1) {
         const img = document.createElement('img')
         img.src = objectURL
         img.style.width = imageData.width * scale + 'px'
-        img.style.height = imageData.height * scale + 'px'
+        img.style.height = imageData.height * scale + 'px';
+        (img.style as any).imageRendering = 'pixelated' // works in Chrome
         img.style.position = 'absolute'
         img.style.top = '0px'
         img.style.left = '0px'
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index a85a8f3a0278bb2b0e7e1bd73782f7f155866450..2c427b728c71764ce5e0c934276058a7ff70b410 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -1,10 +1,9 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-// import { Vec3, Mat4 } from 'mol-math/linear-algebra'
 import { Viewport } from 'mol-canvas3d/camera/util';
 import { Camera } from 'mol-canvas3d/camera';
 
@@ -15,7 +14,9 @@ import { Renderable } from './renderable';
 import { Color } from 'mol-util/color';
 import { ValueCell } from 'mol-util';
 import { RenderableValues, GlobalUniformValues, BaseValues } from './renderable/schema';
-import { RenderVariant } from './webgl/render-item';
+import { GraphicsRenderVariant } from './webgl/render-item';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { deepClone } from 'mol-util/object';
 
 export interface RendererStats {
     programCount: number
@@ -34,44 +35,36 @@ export interface RendererStats {
 
 interface Renderer {
     readonly stats: RendererStats
-    readonly props: RendererProps
+    readonly props: Readonly<RendererProps>
 
     clear: () => void
-    render: (scene: Scene, variant: RenderVariant) => void
+    render: (scene: Scene, variant: GraphicsRenderVariant) => void
+    setProps: (props: Partial<RendererProps>) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
-    setClearColor: (color: Color) => void
-    setPickingAlphaThreshold: (value: number) => void
     getImageData: () => ImageData
     dispose: () => void
 }
 
-export const DefaultRendererProps = {
-    clearColor: Color(0x000000),
-    viewport: Viewport.create(0, 0, 0, 0),
-    pickingAlphaThreshold: 0.5,
+export const RendererParams = {
+    backgroundColor: PD.Color(Color(0x000000)),
+    pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
+
+    lightIntensity: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
+    ambientIntensity: PD.Numeric(0.4, { min: 0.0, max: 1.0, step: 0.01 }),
+
+    metalness: PD.Numeric(0.0, { min: 0.0, max: 1.0, step: 0.01 }),
+    roughness: PD.Numeric(0.4, { min: 0.0, max: 1.0, step: 0.01 }),
+    reflectivity: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
 }
-export type RendererProps = typeof DefaultRendererProps
+export type RendererProps = PD.Values<typeof RendererParams>
 
 namespace Renderer {
     export function create(ctx: WebGLContext, camera: Camera, props: Partial<RendererProps> = {}): Renderer {
-        const { gl } = ctx
-        let { clearColor, viewport: _viewport, pickingAlphaThreshold } = { ...DefaultRendererProps, ...props }
-
-        const viewport = Viewport.clone(_viewport)
-        const viewportVec4 = Viewport.toVec4(Vec4.zero(), viewport)
-
-        // const lightPosition = Vec3.create(0, 0, -100)
-        const lightColor = Vec3.create(1.0, 1.0, 1.0)
-        const lightAmbient = Vec3.create(0.5, 0.5, 0.5)
-        const fogColor = Vec3.create(0.0, 0.0, 0.0)
-
-        function setClearColor(color: Color) {
-            clearColor = color
-            const [ r, g, b ] = Color.toRgbNormalized(color)
-            gl.clearColor(r, g, b, 1.0)
-            Vec3.set(fogColor, r, g, b)
-        }
-        setClearColor(clearColor)
+        const { gl, state, stats } = ctx
+        const p = deepClone({ ...PD.getDefaultValues(RendererParams), ...props })
+
+        const viewport = Viewport()
+        const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor)
 
         const view = Mat4.clone(camera.view)
         const invView = Mat4.invert(Mat4.identity(), view)
@@ -94,63 +87,71 @@ namespace Renderer {
 
             uPixelRatio: ValueCell.create(ctx.pixelRatio),
             uViewportHeight: ValueCell.create(viewport.height),
-            uViewport: ValueCell.create(viewportVec4),
+            uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
+
+            uLightIntensity: ValueCell.create(p.lightIntensity),
+            uAmbientIntensity: ValueCell.create(p.ambientIntensity),
 
-            uLightColor: ValueCell.create(lightColor),
-            uLightAmbient: ValueCell.create(lightAmbient),
+            uMetalness: ValueCell.create(p.metalness),
+            uRoughness: ValueCell.create(p.roughness),
+            uReflectivity: ValueCell.create(p.reflectivity),
 
             uCameraPosition: ValueCell.create(Vec3.clone(camera.state.position)),
             uFogNear: ValueCell.create(camera.state.fogNear),
             uFogFar: ValueCell.create(camera.state.fogFar),
-            uFogColor: ValueCell.create(fogColor),
+            uFogColor: ValueCell.create(bgColor),
 
-            uPickingAlphaThreshold: ValueCell.create(pickingAlphaThreshold),
+            uPickingAlphaThreshold: ValueCell.create(p.pickingAlphaThreshold),
         }
+        const globalUniformList = Object.entries(globalUniforms)
 
         let globalUniformsNeedUpdate = true
-        const renderObject = (r: Renderable<RenderableValues & BaseValues>, variant: RenderVariant) => {
+
+        const renderObject = (r: Renderable<RenderableValues & BaseValues>, variant: GraphicsRenderVariant) => {
             const program = r.getProgram(variant)
             if (r.state.visible) {
-                if (ctx.currentProgramId !== program.id) {
+                if (state.currentProgramId !== program.id) {
+                    // console.log('new program')
                     globalUniformsNeedUpdate = true
+                    program.use()
                 }
 
-                program.use()
                 if (globalUniformsNeedUpdate) {
-                    program.setUniforms(globalUniforms)
+                    // console.log('globalUniformsNeedUpdate')
+                    program.setUniforms(globalUniformList)
                     globalUniformsNeedUpdate = false
                 }
 
                 if (r.values.dDoubleSided) {
                     if (r.values.dDoubleSided.ref.value) {
-                        gl.disable(gl.CULL_FACE)
+                        state.disable(gl.CULL_FACE)
                     } else {
-                        gl.enable(gl.CULL_FACE)
+                        state.enable(gl.CULL_FACE)
                     }
                 } else {
                     // webgl default
-                    gl.disable(gl.CULL_FACE)
+                    state.disable(gl.CULL_FACE)
                 }
 
                 if (r.values.dFlipSided) {
                     if (r.values.dFlipSided.ref.value) {
-                        gl.frontFace(gl.CW)
-                        gl.cullFace(gl.FRONT)
+                        state.frontFace(gl.CW)
+                        state.cullFace(gl.FRONT)
                     } else {
-                        gl.frontFace(gl.CCW)
-                        gl.cullFace(gl.BACK)
+                        state.frontFace(gl.CCW)
+                        state.cullFace(gl.BACK)
                     }
                 } else {
                     // webgl default
-                    gl.frontFace(gl.CCW)
-                    gl.cullFace(gl.BACK)
+                    state.frontFace(gl.CCW)
+                    state.cullFace(gl.BACK)
                 }
 
                 r.render(variant)
             }
         }
 
-        const render = (scene: Scene, variant: RenderVariant) => {
+        const render = (scene: Scene, variant: GraphicsRenderVariant) => {
             ValueCell.update(globalUniforms.uModel, scene.view)
             ValueCell.update(globalUniforms.uView, camera.view)
             ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view))
@@ -166,30 +167,33 @@ namespace Renderer {
             ValueCell.update(globalUniforms.uFogNear, camera.state.fogNear)
 
             globalUniformsNeedUpdate = true
+            state.currentRenderItemId = -1
 
             const { renderables } = scene
 
+            state.disable(gl.SCISSOR_TEST)
+            state.disable(gl.BLEND)
+            state.depthMask(true)
+            state.colorMask(true, true, true, true)
+            state.enable(gl.DEPTH_TEST)
+            state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0)
+            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
+
             if (variant === 'draw') {
-                gl.disable(gl.BLEND)
-                gl.enable(gl.DEPTH_TEST)
-                gl.depthMask(true)
                 for (let i = 0, il = renderables.length; i < il; ++i) {
                     const r = renderables[i]
                     if (r.state.opaque) renderObject(r, variant)
                 }
 
-                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
-                gl.enable(gl.BLEND)
+                state.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
+                state.enable(gl.BLEND)
                 for (let i = 0, il = renderables.length; i < il; ++i) {
                     const r = renderables[i]
-                    gl.depthMask(r.values.uAlpha.ref.value === 1.0)
+                    state.depthMask(r.values.uAlpha.ref.value === 1.0)
                     if (!r.state.opaque) renderObject(r, variant)
                 }
             } else {
                 // picking
-                gl.disable(gl.BLEND)
-                gl.enable(gl.DEPTH_TEST)
-                gl.depthMask(true)
                 for (let i = 0, il = renderables.length; i < il; ++i) {
                     renderObject(renderables[i], variant)
                 }
@@ -200,51 +204,79 @@ namespace Renderer {
 
         return {
             clear: () => {
-                gl.depthMask(true)
+                state.depthMask(true)
+                state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0)
                 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
             },
             render,
 
-            setClearColor,
-            setPickingAlphaThreshold: (value: number) => {
-                pickingAlphaThreshold = value
-                ValueCell.update(globalUniforms.uPickingAlphaThreshold, pickingAlphaThreshold)
+            setProps: (props: Partial<RendererProps>) => {
+                if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== p.pickingAlphaThreshold) {
+                    p.pickingAlphaThreshold = props.pickingAlphaThreshold
+                    ValueCell.update(globalUniforms.uPickingAlphaThreshold, p.pickingAlphaThreshold)
+                }
+                if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) {
+                    p.backgroundColor = props.backgroundColor
+                    Color.toVec3Normalized(bgColor, p.backgroundColor)
+                    ValueCell.update(globalUniforms.uFogColor, Vec3.copy(globalUniforms.uFogColor.ref.value, bgColor))
+                }
+                if (props.lightIntensity !== undefined && props.lightIntensity !== p.lightIntensity) {
+                    p.lightIntensity = props.lightIntensity
+                    ValueCell.update(globalUniforms.uLightIntensity, p.lightIntensity)
+                }
+                if (props.ambientIntensity !== undefined && props.ambientIntensity !== p.ambientIntensity) {
+                    p.ambientIntensity = props.ambientIntensity
+                    ValueCell.update(globalUniforms.uAmbientIntensity, p.ambientIntensity)
+                }
+
+                if (props.metalness !== undefined && props.metalness !== p.metalness) {
+                    p.metalness = props.metalness
+                    ValueCell.update(globalUniforms.uMetalness, p.metalness)
+                }
+                if (props.roughness !== undefined && props.roughness !== p.roughness) {
+                    p.roughness = props.roughness
+                    ValueCell.update(globalUniforms.uRoughness, p.roughness)
+                }
+                if (props.reflectivity !== undefined && props.reflectivity !== p.reflectivity) {
+                    p.reflectivity = props.reflectivity
+                    ValueCell.update(globalUniforms.uReflectivity, p.reflectivity)
+                }
             },
             setViewport: (x: number, y: number, width: number, height: number) => {
-                Viewport.set(viewport, x, y, width, height)
                 gl.viewport(x, y, width, height)
-                ValueCell.update(globalUniforms.uViewportHeight, height)
-                ValueCell.update(globalUniforms.uViewport, Vec4.set(viewportVec4, x, y, width, height))
+                if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) {
+                    Viewport.set(viewport, x, y, width, height)
+                    ValueCell.update(globalUniforms.uViewportHeight, height)
+                    ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height))
+                }
             },
             getImageData: () => {
-                const { width, height } = viewport
-                const buffer = new Uint8Array(width * height * 4)
+                const { x, y, width, height } = viewport
+                const dw = width - x
+                const dh = height - y
+                const buffer = new Uint8Array(dw * dh * 4)
                 ctx.unbindFramebuffer()
-                ctx.readPixels(0, 0, width, height, buffer)
-                return createImageData(buffer, width, height)
+                ctx.readPixels(x, y, width, height, buffer)
+                return createImageData(buffer, dw, dh)
             },
 
             get props() {
-                return {
-                    clearColor,
-                    pickingAlphaThreshold,
-                    viewport
-                }
+                return p
             },
             get stats(): RendererStats {
                 return {
                     programCount: ctx.programCache.count,
                     shaderCount: ctx.shaderCache.count,
 
-                    bufferCount: ctx.bufferCount,
-                    framebufferCount: ctx.framebufferCount,
-                    renderbufferCount: ctx.renderbufferCount,
-                    textureCount: ctx.textureCount,
-                    vaoCount: ctx.vaoCount,
+                    bufferCount: stats.bufferCount,
+                    framebufferCount: stats.framebufferCount,
+                    renderbufferCount: stats.renderbufferCount,
+                    textureCount: stats.textureCount,
+                    vaoCount: stats.vaoCount,
 
-                    drawCount: ctx.drawCount,
-                    instanceCount: ctx.instanceCount,
-                    instancedDrawCount: ctx.instancedDrawCount,
+                    drawCount: stats.drawCount,
+                    instanceCount: stats.instanceCount,
+                    instancedDrawCount: stats.instancedDrawCount,
                 }
             },
             dispose: () => {
diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts
index e8e079ceb5d01b3aeb5e0a07442bc510e0d481a4..749836fe396ff5d1b56cdf87179c9e316300cda4 100644
--- a/src/mol-gl/scene.ts
+++ b/src/mol-gl/scene.ts
@@ -7,7 +7,7 @@
 import { Renderable } from './renderable'
 import { WebGLContext } from './webgl/context';
 import { RenderableValues, BaseValues } from './renderable/schema';
-import { RenderObject, createRenderable, GraphicsRenderObject } from './render-object';
+import { GraphicsRenderObject, createRenderable } from './render-object';
 import { Object3D } from './object3d';
 import { Sphere3D } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
@@ -38,15 +38,19 @@ function calculateBoundingSphere(renderables: Renderable<RenderableValues & Base
 function renderableSort(a: Renderable<RenderableValues & BaseValues>, b: Renderable<RenderableValues & BaseValues>) {
     const drawProgramIdA = a.getProgram('draw').id
     const drawProgramIdB = b.getProgram('draw').id
+    const materialIdA = a.materialId
+    const materialIdB = b.materialId
     const zA = a.values.boundingSphere.ref.value.center[2]
-    const zB = a.values.boundingSphere.ref.value.center[2]
+    const zB = b.values.boundingSphere.ref.value.center[2]
 
     if (drawProgramIdA !== drawProgramIdB) {
-        return drawProgramIdA - drawProgramIdB; // sort by program id to minimize gl state changes
+        return drawProgramIdA - drawProgramIdB // sort by program id to minimize gl state changes
+    } else if (materialIdA !== materialIdB) {
+        return materialIdA - materialIdB // sort by material id to minimize gl state changes
     } else if (zA !== zB) {
         return a.state.opaque
-            ? zA - zB // when opaque draw closer elements first to minimize overdraw
-            : zB - zA // when transparent draw elements last to maximize partial visibility
+            ? zA - zB // when opaque, draw closer elements first to minimize overdraw
+            : zB - zA // when transparent, draw elements last to maximize partial visibility
     } else {
         return a.id - b.id;
     }
@@ -58,16 +62,16 @@ interface Scene extends Object3D {
     readonly boundingSphere: Sphere3D
 
     update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean) => void
-    add: (o: RenderObject) => Renderable<any>
-    remove: (o: RenderObject) => void
-    has: (o: RenderObject) => boolean
+    add: (o: GraphicsRenderObject) => Renderable<any>
+    remove: (o: GraphicsRenderObject) => void
+    has: (o: GraphicsRenderObject) => boolean
     clear: () => void
-    forEach: (callbackFn: (value: Renderable<RenderableValues & BaseValues>, key: RenderObject) => void) => void
+    forEach: (callbackFn: (value: Renderable<RenderableValues & BaseValues>, key: GraphicsRenderObject) => void) => void
 }
 
 namespace Scene {
     export function create(ctx: WebGLContext): Scene {
-        const renderableMap = new Map<RenderObject, Renderable<RenderableValues & BaseValues>>()
+        const renderableMap = new Map<GraphicsRenderObject, Renderable<RenderableValues & BaseValues>>()
         const renderables: Renderable<RenderableValues & BaseValues>[] = []
         const boundingSphere = Sphere3D.zero()
         let boundingSphereDirty = true
@@ -95,7 +99,7 @@ namespace Scene {
                 }
                 if (!keepBoundingSphere) boundingSphereDirty = true
             },
-            add: (o: RenderObject) => {
+            add: (o: GraphicsRenderObject) => {
                 if (!renderableMap.has(o)) {
                     const renderable = createRenderable(ctx, o)
                     renderables.push(renderable)
@@ -108,7 +112,7 @@ namespace Scene {
                     return renderableMap.get(o)!
                 }
             },
-            remove: (o: RenderObject) => {
+            remove: (o: GraphicsRenderObject) => {
                 const renderable = renderableMap.get(o)
                 if (renderable) {
                     renderable.dispose()
@@ -118,7 +122,7 @@ namespace Scene {
                     boundingSphereDirty = true
                 }
             },
-            has: (o: RenderObject) => {
+            has: (o: GraphicsRenderObject) => {
                 return renderableMap.has(o)
             },
             clear: () => {
@@ -129,7 +133,7 @@ namespace Scene {
                 renderableMap.clear()
                 boundingSphereDirty = true
             },
-            forEach: (callbackFn: (value: Renderable<any>, key: RenderObject) => void) => {
+            forEach: (callbackFn: (value: Renderable<any>, key: GraphicsRenderObject) => void) => {
                 renderableMap.forEach(callbackFn)
             },
             get count() {
diff --git a/src/mol-gl/shader-code.ts b/src/mol-gl/shader-code.ts
index 48e635d1271d17a18261167443f74c16b6e26195..3778ba8ff85859b8e29edc6842d3b1d04d4187c9 100644
--- a/src/mol-gl/shader-code.ts
+++ b/src/mol-gl/shader-code.ts
@@ -1,12 +1,13 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ValueCell } from 'mol-util';
 import { idFactory } from 'mol-util/id-factory';
-import { WebGLContext } from './webgl/context';
+import { WebGLExtensions } from './webgl/context';
+import { isWebGL2, GLRenderingContext } from './webgl/compat';
 
 export type DefineKind = 'boolean' | 'string' | 'number'
 export type DefineType = boolean | string
@@ -15,8 +16,10 @@ export type DefineValues = { [k: string]: ValueCell<DefineType> }
 const shaderCodeId = idFactory()
 
 export interface ShaderExtensions {
-    readonly standardDerivatives: boolean
-    readonly fragDepth: boolean
+    readonly standardDerivatives?: boolean
+    readonly fragDepth?: boolean
+    readonly drawBuffers?: boolean
+    readonly shaderTextureLod?: boolean
 }
 
 export interface ShaderCode {
@@ -26,52 +29,46 @@ export interface ShaderCode {
     readonly extensions: ShaderExtensions
 }
 
-export function ShaderCode(vert: string, frag: string, extensions: ShaderExtensions): ShaderCode {
+export function ShaderCode(vert: string, frag: string, extensions: ShaderExtensions = {}): ShaderCode {
     return { id: shaderCodeId(), vert, frag, extensions }
 }
 
 export const PointsShaderCode = ShaderCode(
-    require('mol-gl/shader/points.vert'),
-    require('mol-gl/shader/points.frag'),
-    { standardDerivatives: false, fragDepth: false }
+    require('mol-gl/shader/points.vert').default,
+    require('mol-gl/shader/points.frag').default
 )
 
 export const SpheresShaderCode = ShaderCode(
-    require('mol-gl/shader/spheres.vert'),
-    require('mol-gl/shader/spheres.frag'),
-    { standardDerivatives: false, fragDepth: true }
+    require('mol-gl/shader/spheres.vert').default,
+    require('mol-gl/shader/spheres.frag').default,
+    { fragDepth: true }
 )
 
 export const TextShaderCode = ShaderCode(
-    require('mol-gl/shader/text.vert'),
-    require('mol-gl/shader/text.frag'),
-    { standardDerivatives: true, fragDepth: false }
+    require('mol-gl/shader/text.vert').default,
+    require('mol-gl/shader/text.frag').default,
+    { standardDerivatives: true }
 )
 
 export const LinesShaderCode = ShaderCode(
-    require('mol-gl/shader/lines.vert'),
-    require('mol-gl/shader/lines.frag'),
-    { standardDerivatives: false, fragDepth: false }
+    require('mol-gl/shader/lines.vert').default,
+    require('mol-gl/shader/lines.frag').default
 )
 
 export const MeshShaderCode = ShaderCode(
-    require('mol-gl/shader/mesh.vert'),
-    require('mol-gl/shader/mesh.frag'),
-    { standardDerivatives: true, fragDepth: false }
-)
-
-export const GaussianDensityShaderCode = ShaderCode(
-    require('mol-gl/shader/gaussian-density.vert'),
-    require('mol-gl/shader/gaussian-density.frag'),
-    { standardDerivatives: false, fragDepth: false }
+    require('mol-gl/shader/mesh.vert').default,
+    require('mol-gl/shader/mesh.frag').default,
+    { standardDerivatives: true }
 )
 
 export const DirectVolumeShaderCode = ShaderCode(
-    require('mol-gl/shader/direct-volume.vert'),
-    require('mol-gl/shader/direct-volume.frag'),
-    { standardDerivatives: false, fragDepth: true }
+    require('mol-gl/shader/direct-volume.vert').default,
+    require('mol-gl/shader/direct-volume.frag').default,
+    { fragDepth: true }
 )
 
+
+
 export type ShaderDefines = {
     [k: string]: ValueCell<DefineType>
 }
@@ -97,16 +94,34 @@ function getDefinesCode (defines: ShaderDefines) {
     return lines.join('\n') + '\n'
 }
 
-function getGlsl100FragPrefix(ctx: WebGLContext, extensions: ShaderExtensions) {
+function getGlsl100FragPrefix(extensions: WebGLExtensions, shaderExtensions: ShaderExtensions) {
     const prefix: string[] = []
-    if (extensions.standardDerivatives) {
+    if (shaderExtensions.standardDerivatives) {
         prefix.push('#extension GL_OES_standard_derivatives : enable')
         prefix.push('#define enabledStandardDerivatives')
     }
-    if (extensions.fragDepth) {
-        if (ctx.extensions.fragDepth) {
+    if (shaderExtensions.fragDepth) {
+        if (extensions.fragDepth) {
             prefix.push('#extension GL_EXT_frag_depth : enable')
             prefix.push('#define enabledFragDepth')
+        } else {
+            throw new Error(`requested 'GL_EXT_frag_depth' extension is unavailable`)
+        }
+    }
+    if (shaderExtensions.drawBuffers) {
+        if (extensions.drawBuffers) {
+            prefix.push('#extension GL_EXT_draw_buffers : require')
+            prefix.push('#define requiredDrawBuffers')
+        } else {
+            throw new Error(`requested 'GL_EXT_draw_buffers' extension is unavailable`)
+        }
+    }
+    if (shaderExtensions.shaderTextureLod) {
+        if (extensions.shaderTextureLod) {
+            prefix.push('#extension GL_EXT_shader_texture_lod : enable')
+            prefix.push('#define enabledShaderTextureLod')
+        } else {
+            throw new Error(`requested 'GL_EXT_shader_texture_lod' extension is unavailable`)
         }
     }
     return prefix.join('\n') + '\n'
@@ -119,25 +134,41 @@ const glsl300VertPrefix = `#version 300 es
 `
 
 const glsl300FragPrefix = `#version 300 es
+layout(location = 0) out highp vec4 out_FragData0;
+layout(location = 1) out highp vec4 out_FragData1;
+layout(location = 2) out highp vec4 out_FragData2;
+layout(location = 3) out highp vec4 out_FragData3;
+layout(location = 4) out highp vec4 out_FragData4;
+layout(location = 5) out highp vec4 out_FragData5;
+layout(location = 6) out highp vec4 out_FragData6;
+layout(location = 7) out highp vec4 out_FragData7;
+
 #define varying in
-layout(location = 0) out highp vec4 out_FragColor;
-#define gl_FragColor out_FragColor
-#define gl_FragDepthEXT gl_FragDepth
 #define texture2D texture
+#define texture2DLodEXT textureLod
+
+#define gl_FragColor out_FragData0
+#define gl_FragDepthEXT gl_FragDepth
 
 #define enabledStandardDerivatives
 #define enabledFragDepth
+#define requiredDrawBuffers
 `
 
-export function addShaderDefines(ctx: WebGLContext, defines: ShaderDefines, shaders: ShaderCode): ShaderCode {
-    const { isWebGL2 } = ctx
+function transformGlsl300Frag(frag: string) {
+    return frag.replace(/gl_FragData\[([0-7])\]/g, 'out_FragData$1')
+}
+
+export function addShaderDefines(gl: GLRenderingContext, extensions: WebGLExtensions, defines: ShaderDefines, shaders: ShaderCode): ShaderCode {
+    const webgl2 = isWebGL2(gl)
     const header = getDefinesCode(defines)
-    const vertPrefix = isWebGL2 ? glsl300VertPrefix : ''
-    const fragPrefix = isWebGL2 ? glsl300FragPrefix : getGlsl100FragPrefix(ctx, shaders.extensions)
+    const vertPrefix = webgl2 ? glsl300VertPrefix : ''
+    const fragPrefix = webgl2 ? glsl300FragPrefix : getGlsl100FragPrefix(extensions, shaders.extensions)
+    const frag = webgl2 ? transformGlsl300Frag(shaders.frag) : shaders.frag
     return {
         id: shaderCodeId(),
         vert: `${vertPrefix}${header}${shaders.vert}`,
-        frag: `${fragPrefix}${header}${shaders.frag}`,
+        frag: `${fragPrefix}${header}${frag}`,
         extensions: shaders.extensions
     }
 }
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/apply-light-color.glsl b/src/mol-gl/shader/chunks/apply-light-color.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..d71afe83dc4f4459587ae31ae52a04503a581819
--- /dev/null
+++ b/src/mol-gl/shader/chunks/apply-light-color.glsl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * adapted from three.js (https://github.com/mrdoob/three.js/)
+ * which under the MIT License, Copyright © 2010-2019 three.js authors
+ */
+
+// inputs
+// - vec4 material
+// - vec3 vViewPosition
+// - vec3 normal
+// - float uMetalness
+// - float uRoughness
+// - float uReflectivity
+// - float uLightIntensity
+// - float uAmbientIntensity
+
+// outputs
+// - sets gl_FragColor
+
+vec4 color = material;
+
+ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0));
+
+PhysicalMaterial physicalMaterial;
+physicalMaterial.diffuseColor = color.rgb * (1.0 - uMetalness);
+physicalMaterial.specularRoughness = clamp(uRoughness, 0.04, 1.0);
+physicalMaterial.specularColor = mix(vec3(0.16 * pow2(uReflectivity)), color.rgb, uMetalness);
+
+GeometricContext geometry;
+geometry.position = -vViewPosition;
+geometry.normal = normal;
+geometry.viewDir = normalize(vViewPosition);
+
+IncidentLight directLight;
+directLight.direction = geometry.viewDir;
+directLight.color = vec3(uLightIntensity);
+
+RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
+
+vec3 irradiance = vec3(uAmbientIntensity) * PI;
+RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
+
+vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular;
+
+gl_FragColor = vec4(outgoingLight, color.a);
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/apply-marker-color.glsl b/src/mol-gl/shader/chunks/apply-marker-color.glsl
index 327ba74bb881a097df42ef173ec4bc1db168dc3c..59067c0074d3335034a90d7d2fbd5342977700c1 100644
--- a/src/mol-gl/shader/chunks/apply-marker-color.glsl
+++ b/src/mol-gl/shader/chunks/apply-marker-color.glsl
@@ -1,5 +1,5 @@
 // only mark elements with an alpha above the picking threshold
-if (uAlpha >= uPickingAlphaThreshold) {
+if (gl_FragColor.a >= uPickingAlphaThreshold) {
     float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win
     if (marker > 0.1) {
         if (intMod(marker, 2.0) > 0.1) {
diff --git a/src/mol-gl/shader/chunks/assign-color-varying.glsl b/src/mol-gl/shader/chunks/assign-color-varying.glsl
index 7175df041fa3cf59c66964a3a0598d49ac20c05f..4dfd59bfd884bcab975c2f03fda8f2df9bb8113a 100644
--- a/src/mol-gl/shader/chunks/assign-color-varying.glsl
+++ b/src/mol-gl/shader/chunks/assign-color-varying.glsl
@@ -3,17 +3,22 @@
 #elif defined(dColorType_instance)
     vColor.rgb = readFromTexture(tColor, aInstance, uColorTexDim).rgb;
 #elif defined(dColorType_group)
-    vColor.rgb = readFromTexture(tColor, aGroup, uColorTexDim).rgb;
+    vColor.rgb = readFromTexture(tColor, group, uColorTexDim).rgb;
 #elif defined(dColorType_groupInstance)
-    vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + aGroup, uColorTexDim).rgb;
+    vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + group, uColorTexDim).rgb;
 #elif defined(dColorType_objectPicking)
     vColor = vec4(encodeFloatRGB(float(uObjectId)), 1.0);
 #elif defined(dColorType_instancePicking)
     vColor = vec4(encodeFloatRGB(aInstance), 1.0);
 #elif defined(dColorType_groupPicking)
-    vColor = vec4(encodeFloatRGB(aGroup), 1.0);
+    vColor = vec4(encodeFloatRGB(group), 1.0);
 #endif
 
 #ifdef dOverpaint
-    vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + aGroup, uOverpaintTexDim);
+    vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + group, uOverpaintTexDim);
+#endif
+
+#ifdef dTransparency
+    vGroup = group;
+    vTransparency = readFromTexture(tTransparency, aInstance * float(uGroupCount) + group, uTransparencyTexDim).a;
 #endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-group.glsl b/src/mol-gl/shader/chunks/assign-group.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..9a797bc402bcbf9ade19590f03382a57cf1d88c3
--- /dev/null
+++ b/src/mol-gl/shader/chunks/assign-group.glsl
@@ -0,0 +1,6 @@
+#ifdef dGeoTexture
+    // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup
+    float group = readFromTexture(tPositionGroup, aGroup, uGeoTexDim).w;
+#else
+    float group = aGroup;
+#endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-marker-varying.glsl b/src/mol-gl/shader/chunks/assign-marker-varying.glsl
index 38c3e0a474debc92226378e42368c7b42362f693..69259033f037ba52806451f51e03d19931dd19a1 100644
--- a/src/mol-gl/shader/chunks/assign-marker-varying.glsl
+++ b/src/mol-gl/shader/chunks/assign-marker-varying.glsl
@@ -1 +1 @@
-vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + aGroup, uMarkerTexDim).a;
\ No newline at end of file
+vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + group, uMarkerTexDim).a;
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-material-color.glsl b/src/mol-gl/shader/chunks/assign-material-color.glsl
index d79b00d7da917237b2e5c1b657ccf1683adbd14b..68fba305e63861689d69d403e15ca2eea82517a9 100644
--- a/src/mol-gl/shader/chunks/assign-material-color.glsl
+++ b/src/mol-gl/shader/chunks/assign-material-color.glsl
@@ -9,4 +9,24 @@
 // mix material with overpaint
 #if defined(dOverpaint) && (defined(dColorType_uniform) || defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance))
     material.rgb = mix(material.rgb, vOverpaint.rgb, vOverpaint.a);
+#endif
+
+// apply screendoor transparency
+#if defined(dTransparency) && (defined(dColorType_uniform) || defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance))
+    float ta = 1.0 - vTransparency;
+    float at = 0.0;
+
+    #if defined(dTransparencyVariant_single)
+        const mat4 thresholdMatrix = mat4(
+            1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
+            13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
+            4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
+            16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
+        );
+        at = thresholdMatrix[int(intMod(gl_FragCoord.x, 4.0))][int(intMod(gl_FragCoord.y, 4.0))];
+    #elif defined(dTransparencyVariant_multi)
+        at = fract(dot(vec3(gl_FragCoord.xy, vGroup + 0.5), vec3(2.0, 7.0, 23.0) / 17.0f));
+    #endif
+
+    if (ta < 0.99 && (ta < 0.01 || ta < at)) discard;
 #endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-normal.glsl b/src/mol-gl/shader/chunks/assign-normal.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..56caada40a5324b4ecd8bb9b07bfd57683aa8a89
--- /dev/null
+++ b/src/mol-gl/shader/chunks/assign-normal.glsl
@@ -0,0 +1,18 @@
+// inputs
+// - vViewPosition (if dFlatShaded)
+// - vNormal (if NOT dFlatShaded)
+
+// outputs
+// - normal
+
+// surface normal
+#if defined(dFlatShaded) && defined(enabledStandardDerivatives)
+    vec3 fdx = dFdx(vViewPosition);
+    vec3 fdy = dFdy(vViewPosition);
+    vec3 normal = -normalize(cross(fdx, fdy));
+#else
+    vec3 normal = -normalize(vNormal);
+    #ifdef dDoubleSided
+        normal = normal * (float(gl_FrontFacing) * 2.0 - 1.0);
+    #endif
+#endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-position.glsl b/src/mol-gl/shader/chunks/assign-position.glsl
index 06231cf7ff806afbd988c00961471fe9cb3b81dd..50440d786ecc1685bd37313a9e96f015c27873b0 100644
--- a/src/mol-gl/shader/chunks/assign-position.glsl
+++ b/src/mol-gl/shader/chunks/assign-position.glsl
@@ -1,4 +1,9 @@
 mat4 modelView = uView * uModel * aTransform;
-vec4 mvPosition = modelView * vec4(aPosition, 1.0);
+#ifdef dGeoTexture
+    vec3 position = readFromTexture(tPositionGroup, aGroup, uGeoTexDim).xyz;
+#else
+    vec3 position = aPosition;
+#endif
+vec4 mvPosition = modelView * vec4(position, 1.0);
 vViewPosition = mvPosition.xyz;
 gl_Position = uProjection * mvPosition;
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-size.glsl b/src/mol-gl/shader/chunks/assign-size.glsl
index be66b9bfff1c10383baaf03dd571066a66772364..3f1fe2582213eb0e551cce472e547d1788ed7920 100644
--- a/src/mol-gl/shader/chunks/assign-size.glsl
+++ b/src/mol-gl/shader/chunks/assign-size.glsl
@@ -5,9 +5,9 @@
 #elif defined(dSizeType_instance)
     float size = readFromTexture(tSize, aInstance, uSizeTexDim).a;
 #elif defined(dSizeType_group)
-    float size = readFromTexture(tSize, aGroup, uSizeTexDim).a;
+    float size = readFromTexture(tSize, group, uSizeTexDim).a;
 #elif defined(dSizeType_groupInstance)
-    float size = readFromTexture(tSize, aInstance * float(uGroupCount) + aGroup, uSizeTexDim).a;
+    float size = readFromTexture(tSize, aInstance * float(uGroupCount) + group, uSizeTexDim).a;
 #endif
 
 #if defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance)
diff --git a/src/mol-gl/shader/chunks/color-frag-params.glsl b/src/mol-gl/shader/chunks/color-frag-params.glsl
index 871e2dee3e9551de259c1273ac869154228df38e..8b3e905b4c5134ede7f3e9d69b5b2aae658b4f71 100644
--- a/src/mol-gl/shader/chunks/color-frag-params.glsl
+++ b/src/mol-gl/shader/chunks/color-frag-params.glsl
@@ -12,4 +12,9 @@
 
 #ifdef dOverpaint
     varying vec4 vOverpaint;
+#endif
+
+#ifdef dTransparency
+    varying float vGroup;
+    varying float vTransparency;
 #endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/color-vert-params.glsl b/src/mol-gl/shader/chunks/color-vert-params.glsl
index b3156a22ee6f0cabb1cf1505e09b82570ce865da..3daf743fe42366c229372b9767c532e18f4390e8 100644
--- a/src/mol-gl/shader/chunks/color-vert-params.glsl
+++ b/src/mol-gl/shader/chunks/color-vert-params.glsl
@@ -20,4 +20,11 @@
     varying vec4 vOverpaint;
     uniform vec2 uOverpaintTexDim;
     uniform sampler2D tOverpaint;
+#endif
+
+#ifdef dTransparency
+    varying float vGroup;
+    varying float vTransparency;
+    uniform vec2 uTransparencyTexDim;
+    uniform sampler2D tTransparency;
 #endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/common.glsl b/src/mol-gl/shader/chunks/common.glsl
index c26150ec86632fe48034fa8e6265a42d375ed4eb..25d0d9b9c52522b3c70f7909bf8958cbc1472767 100644
--- a/src/mol-gl/shader/chunks/common.glsl
+++ b/src/mol-gl/shader/chunks/common.glsl
@@ -1,5 +1,13 @@
-float intDiv(float a, float b) { return float(int(a) / int(b)); }
-float intMod(float a, float b) { return a - b * float(int(a) / int(b)); }
+#define PI 3.14159265
+#define RECIPROCAL_PI 0.31830988618
+#define EPSILON 1e-6
+
+#define saturate(a) clamp(a, 0.0, 1.0)
+
+float intDiv(const in float a, const in float b) { return float(int(a) / int(b)); }
+float intMod(const in float a, const in float b) { return a - b * float(int(a) / int(b)); }
+
+float pow2(const in float x) { return x*x; }
 
 #if __VERSION__ != 300
     // transpose
diff --git a/src/mol-gl/shader/chunks/light-frag-params.glsl b/src/mol-gl/shader/chunks/light-frag-params.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..8a7eae37e202880a108212ca6d4b623babbd4c17
--- /dev/null
+++ b/src/mol-gl/shader/chunks/light-frag-params.glsl
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * adapted from three.js (https://github.com/mrdoob/three.js/)
+ * which under the MIT License, Copyright © 2010-2019 three.js authors
+ */
+
+uniform float uLightIntensity;
+uniform float uAmbientIntensity;
+uniform float uReflectivity;
+uniform float uMetalness;
+uniform float uRoughness;
+
+struct PhysicalMaterial {
+	vec3 diffuseColor;
+	float specularRoughness;
+	vec3 specularColor;
+};
+
+struct IncidentLight {
+	vec3 color;
+	vec3 direction;
+};
+
+struct ReflectedLight {
+	vec3 directDiffuse;
+	vec3 directSpecular;
+	vec3 indirectDiffuse;
+};
+
+struct GeometricContext {
+	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;
+}
+
+// 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);
+}
+
+// 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);
+}
+
+vec3 BRDF_Diffuse_Lambert(const in vec3 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);
+}
+
+// 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;
+}
+
+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));
+    vec3 irradiance = dotNL * directLight.color;
+	irradiance *= PI; // punctual light
+
+	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);
+}
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/normal-frag-params.glsl b/src/mol-gl/shader/chunks/normal-frag-params.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..81ce20d5df0e2db35bbf769137bb73102bfcc17f
--- /dev/null
+++ b/src/mol-gl/shader/chunks/normal-frag-params.glsl
@@ -0,0 +1,3 @@
+#if !defined(dFlatShaded) || !defined(enabledStandardDerivatives)
+    varying vec3 vNormal;
+#endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/direct-volume.frag b/src/mol-gl/shader/direct-volume.frag
index 5728c6c41b3e1a0239a305f38024d3034775e604..42ce46af5477c52be865333100b75f29dd4a3d50 100644
--- a/src/mol-gl/shader/direct-volume.frag
+++ b/src/mol-gl/shader/direct-volume.frag
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 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>
@@ -30,11 +30,11 @@ uniform float uPickingAlphaThreshold;
 uniform int uPickable;
 
 #if defined(dGridTexType_2d)
-    precision mediump sampler2D;
+    precision highp sampler2D;
     uniform sampler2D tGridTex;
     uniform vec3 uGridTexDim;
 #elif defined(dGridTexType_3d)
-    precision mediump sampler3D;
+    precision highp sampler3D;
     uniform sampler3D tGridTex;
 #endif
 
@@ -46,26 +46,14 @@ uniform int uPickable;
 #endif
 
 #pragma glslify: import('./chunks/common.glsl')
+#pragma glslify: import('./chunks/light-frag-params.glsl')
+
 #pragma glslify: readFromTexture = require(./utils/read-from-texture.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify
 #pragma glslify: encodeFloatRGB = require(./utils/encode-float-rgb.glsl)
 #pragma glslify: decodeFloatRGB = require(./utils/decode-float-rgb.glsl)
 #pragma glslify: texture3dFrom2dNearest = require(./utils/texture3d-from-2d-nearest.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify
 #pragma glslify: texture3dFrom2dLinear = require(./utils/texture3d-from-2d-linear.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify
 
-// uniform vec3 uLightPosition;
-uniform vec3 uLightColor;
-uniform vec3 uLightAmbient;
-uniform mat4 uView;
-
-#pragma glslify: attenuation = require(./utils/attenuation.glsl)
-#pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl)
-#pragma glslify: calculateDiffuse = require(./utils/oren-nayar-diffuse.glsl)
-
-const float specularScale = 0.15;
-const float shininess = 200.0;
-const float roughness = 100.0;
-const float albedo = 0.95;
-
 #if defined(dGridTexType_2d)
     vec4 textureVal(vec3 pos) {
         return texture3dFrom2dLinear(tGridTex, pos, uGridDim, uGridTexDim.xy);
@@ -160,29 +148,16 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir) {
                         color = readFromTexture(tColor, instance * float(uGroupCount) + group, uColorTexDim).rgb;
                     #endif
 
-                    vec3 L = normalize(viewDir); // light direction
-                    vec3 V = normalize(viewDir); // eye direction
-                    vec3 N = normalize(gradient); // surface normal
+                    vec3 normal = normalize(gradient);
+                    vec3 vViewPosition = normalize(viewDir);
+                    vec4 material = vec4(color, uAlpha);
+                    #pragma glslify: import('./chunks/apply-light-color.glsl')
 
-                    // compute our diffuse & specular terms
-                    float specular = calculateSpecular(L, V, N, shininess) * specularScale;
-                    vec3 diffuse = uLightColor * calculateDiffuse(L, V, N, roughness, albedo);
-                    vec3 ambient = uLightAmbient;
+                    float vMarker = readFromTexture(tMarker, instance * float(uGroupCount) + group, uMarkerTexDim).a;
+                    #pragma glslify: import('./chunks/apply-marker-color.glsl')
 
-                    // add the lighting
-                    vec3 finalColor = color.rgb * (diffuse + ambient) + specular;
-
-                    src.rgb = finalColor;
-                    src.a = uAlpha;
-
-                    float marker = readFromTexture(tMarker, instance * float(uGroupCount) + group, uMarkerTexDim).a * 255.0;
-                    if (marker > 0.1) {
-                        if (mod(marker, 2.0) > 0.1) {
-                            src.rgb = mix(uHighlightColor, src.rgb, 0.3);
-                        } else {
-                            src.rgb = mix(uSelectColor, src.rgb, 0.3);
-                        }
-                    }
+                    src.rgb = gl_FragColor.rgb;
+                    src.a = gl_FragColor.a;
 
                     // draw interior darker
                     if( (prevValue - uIsoValue) > 0.0 ) {
diff --git a/src/mol-gl/shader/gaussian-density.frag b/src/mol-gl/shader/gaussian-density.frag
index 652e87891b45aa5e4a4bbafb8a5d654926acfe57..74f6539de55cee5c4d85da388a084aab3cf16d9b 100644
--- a/src/mol-gl/shader/gaussian-density.frag
+++ b/src/mol-gl/shader/gaussian-density.frag
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 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>
@@ -8,62 +8,41 @@
 precision highp float;
 
 varying vec3 vPosition;
-varying float vRadius;
+varying float vRadiusSqInv;
 #if defined(dCalcType_groupId)
-    #if defined(dGridTexType_2d)
-        precision mediump sampler2D;
-        uniform sampler2D tMinDistanceTex;
-        uniform vec3 uGridTexDim;
-    #elif defined(dGridTexType_3d)
-        precision highp sampler3D;
-        uniform sampler3D tMinDistanceTex;
-    #endif
+    precision highp sampler2D;
+    uniform sampler2D tMinDistanceTex;
+    uniform vec3 uGridTexDim;
     varying float vGroup;
 #endif
 
 #pragma glslify: import('./chunks/common.glsl')
-#pragma glslify: encodeFloatLog = require(./utils/encode-float-log.glsl)
-#pragma glslify: decodeFloatLog = require(./utils/decode-float-log.glsl)
+// #pragma glslify: encodeFloatLog = require(./utils/encode-float-log.glsl)
+// #pragma glslify: decodeFloatLog = require(./utils/decode-float-log.glsl)
 #pragma glslify: encodeFloatRGB = require(./utils/encode-float-rgb.glsl)
-#pragma glslify: texture3dFrom2dNearest = require(./utils/texture3d-from-2d-nearest.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify
 
-uniform vec3 uBboxSize;
-uniform vec3 uBboxMin;
-uniform vec3 uBboxMax;
 uniform vec3 uGridDim;
+uniform vec2 uGridTexScale;
 uniform float uCurrentSlice;
 uniform float uCurrentX;
 uniform float uCurrentY;
 uniform float uAlpha;
-
-#if defined(dCalcType_groupId)
-    #if defined(dGridTexType_2d)
-        vec4 textureMinDist(vec3 pos) {
-            return texture3dFrom2dNearest(tMinDistanceTex, pos, uGridDim, uGridTexDim.xy);
-        }
-    #elif defined(dGridTexType_3d)
-        vec4 textureMinDist(vec3 pos) {
-            return texture(tMinDistanceTex, pos);
-        }
-    #endif
-#endif
+uniform float uResolution;
 
 void main() {
     vec2 v = gl_FragCoord.xy - vec2(uCurrentX, uCurrentY) - 0.5;
-    vec3 fragPos = vec3(v.x, v.y, uCurrentSlice) / uGridDim;
-    float dist = distance(fragPos * uBboxSize, vPosition * uBboxSize);
+    vec3 fragPos = vec3(v.x, v.y, uCurrentSlice);
+    float dist = distance(fragPos, vPosition) * uResolution;
 
     #if defined(dCalcType_density)
-        float radiusSq = vRadius * vRadius;
-        float density = exp(-uAlpha * ((dist * dist) / radiusSq));
-        gl_FragColor = vec4(density);
+        float density = exp(-uAlpha * ((dist * dist) * vRadiusSqInv));
+        gl_FragColor.a = density;
     #elif defined(dCalcType_minDistance)
-        gl_FragColor.a = 1.0 - encodeFloatLog(dist);
+        gl_FragColor.a = 10000.0 - dist;
+        // gl_FragColor.a = 1.0 - encodeFloatLog(dist);
     #elif defined(dCalcType_groupId)
-        float minDistance = decodeFloatLog(1.0 - textureMinDist(fragPos).a);
-        // TODO verify `length(uBboxSize / uGridDim) * 2.0`
-        //      on some machines `* 2.0` is needed while on others `* 0.5` works
-        if (dist > minDistance + length(uBboxSize / uGridDim) * 0.5)
+        float minDistance = 10000.0 - texture2D(tMinDistanceTex, (gl_FragCoord.xy) / (uGridTexDim.xy / uGridTexScale)).a;
+        if (dist > minDistance + uResolution * 0.05)
             discard;
         gl_FragColor.rgb = encodeFloatRGB(vGroup);
     #endif
diff --git a/src/mol-gl/shader/gaussian-density.vert b/src/mol-gl/shader/gaussian-density.vert
index f7557182942ce69b7861146dd3da32ffc3b5c52d..1048285027c3f595e9a50728bafc7ec66e1fee3e 100644
--- a/src/mol-gl/shader/gaussian-density.vert
+++ b/src/mol-gl/shader/gaussian-density.vert
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 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>
@@ -11,7 +11,7 @@ attribute vec3 aPosition;
 attribute float aRadius;
 
 varying vec3 vPosition;
-varying float vRadius;
+varying float vRadiusSqInv;
 
 #if defined(dCalcType_groupId)
     attribute float aGroup;
@@ -20,17 +20,15 @@ varying float vRadius;
 
 uniform vec3 uBboxSize;
 uniform vec3 uBboxMin;
-uniform vec3 uBboxMax;
-uniform vec3 uGridDim;
 uniform float uCurrentSlice;
+uniform float uResolution;
 
 void main() {
-    vRadius = aRadius;
+    vRadiusSqInv = 1.0 / (aRadius * aRadius);
     #if defined(dCalcType_groupId)
         vGroup = aGroup;
     #endif
-    float scale = max(uBboxSize.z, max(uBboxSize.x, uBboxSize.y));
-    gl_PointSize = (vRadius / scale) * max(uGridDim.x, uGridDim.y) * 6.0;
-    vPosition = (aPosition - uBboxMin) / uBboxSize;
-    gl_Position = vec4(vPosition * 2.0 - 1.0, 1.0);
+    gl_PointSize = floor(((aRadius * 4.0) / uResolution) + 0.5);
+    vPosition = (aPosition - uBboxMin) / uResolution;
+    gl_Position = vec4(((aPosition - uBboxMin) / uBboxSize) * 2.0 - 1.0, 1.0);
 }
\ No newline at end of file
diff --git a/src/mol-gl/shader/histogram-pyramid/reduction.frag b/src/mol-gl/shader/histogram-pyramid/reduction.frag
new file mode 100644
index 0000000000000000000000000000000000000000..ce3b9bb302c5143c10e17bf8a84b71f254448978
--- /dev/null
+++ b/src/mol-gl/shader/histogram-pyramid/reduction.frag
@@ -0,0 +1,22 @@
+precision highp float;
+precision highp sampler2D;
+
+// input texture (previous level used to evaluate the new level)
+uniform sampler2D tPreviousLevel;
+
+// 1/size of the previous level texture.
+uniform float uSize;
+uniform float uTexSize;
+
+void main(void) {
+    float k = 0.5 * uSize;
+    vec2 position = floor((gl_FragCoord.xy / uTexSize) / uSize) * uSize;
+    float a = texture2D(tPreviousLevel, position).r;
+    float b = texture2D(tPreviousLevel, position + vec2(k, 0.)).r;
+    float c = texture2D(tPreviousLevel, position + vec2(0., k)).r;
+    float d = texture2D(tPreviousLevel, position + vec2(k, k)).r;
+    gl_FragColor.a = a;
+    gl_FragColor.b = a + b;
+    gl_FragColor.g = gl_FragColor.b + c;
+    gl_FragColor.r = gl_FragColor.g + d;
+}
\ No newline at end of file
diff --git a/src/mol-gl/shader/histogram-pyramid/sum.frag b/src/mol-gl/shader/histogram-pyramid/sum.frag
new file mode 100644
index 0000000000000000000000000000000000000000..4f56dd7c1264bd4a2bcc56b949dc711aeecf680f
--- /dev/null
+++ b/src/mol-gl/shader/histogram-pyramid/sum.frag
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tTexture;
+
+#pragma glslify: encodeFloatRGB = require(../utils/encode-float-rgb.glsl)
+
+void main(void) {
+    gl_FragColor = vec4(encodeFloatRGB(texture2D(tTexture, vec2(0.5)).r), 1.0);
+}
\ No newline at end of file
diff --git a/src/mol-gl/shader/lines.vert b/src/mol-gl/shader/lines.vert
index 91f0ca8d676f43a197f7acd087a88026a5fb46e3..e97b8e20ee41674d326c46cb01091bd9ef808bea 100644
--- a/src/mol-gl/shader/lines.vert
+++ b/src/mol-gl/shader/lines.vert
@@ -36,6 +36,7 @@ void trimSegment(const in vec4 start, inout vec4 end) {
 }
 
 void main(){
+    #pragma glslify: import('./chunks/assign-group.glsl')
     #pragma glslify: import('./chunks/assign-color-varying.glsl')
     #pragma glslify: import('./chunks/assign-marker-varying.glsl')
     #pragma glslify: import('./chunks/assign-size.glsl')
diff --git a/src/mol-gl/shader/marching-cubes/active-voxels.frag b/src/mol-gl/shader/marching-cubes/active-voxels.frag
new file mode 100644
index 0000000000000000000000000000000000000000..1d3e94a8f9366c8cf5377fb43f56d5e7f33967af
--- /dev/null
+++ b/src/mol-gl/shader/marching-cubes/active-voxels.frag
@@ -0,0 +1,75 @@
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tTriCount;
+uniform sampler2D tVolumeData;
+
+uniform float uIsoValue;
+uniform vec3 uGridDim;
+uniform vec3 uGridTexDim;
+uniform vec2 uScale;
+
+// cube corners
+const vec3 c0 = vec3(0., 0., 0.);
+const vec3 c1 = vec3(1., 0., 0.);
+const vec3 c2 = vec3(1., 1., 0.);
+const vec3 c3 = vec3(0., 1., 0.);
+const vec3 c4 = vec3(0., 0., 1.);
+const vec3 c5 = vec3(1., 0., 1.);
+const vec3 c6 = vec3(1., 1., 1.);
+const vec3 c7 = vec3(0., 1., 1.);
+
+vec3 index3dFrom2d(vec2 coord) {
+    vec2 gridTexPos = coord * uGridTexDim.xy;
+    vec2 columnRow = floor(gridTexPos / uGridDim.xy);
+    vec2 posXY = gridTexPos - columnRow * uGridDim.xy;
+    float posZ = columnRow.y * floor(uGridTexDim.x / uGridDim.x) + columnRow.x;
+    vec3 posXYZ = vec3(posXY, posZ) / uGridDim;
+    return posXYZ;
+}
+
+float intDiv(float a, float b) { return float(int(a) / int(b)); }
+float intMod(float a, float b) { return a - b * float(int(a) / int(b)); }
+
+vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim) {
+    float zSlice = floor(pos.z * gridDim.z + 0.5); // round to nearest z-slice
+    float column = intMod(zSlice * gridDim.x, texDim.x) / gridDim.x;
+    float row = floor(intDiv(zSlice * gridDim.x, texDim.x));
+    vec2 coord = (vec2(column * gridDim.x, row * gridDim.y) + (pos.xy * gridDim.xy)) / (texDim / uScale);
+    // return texture2D(tex, coord + 0.5 / texDim);
+    return texture2D(tex, coord);
+}
+
+vec4 voxel(vec3 pos) {
+    return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy);
+}
+
+void main(void) {
+    vec2 uv = gl_FragCoord.xy / uGridTexDim.xy;
+    vec3 posXYZ = index3dFrom2d(uv);
+
+    // get MC case as the sum of corners that are below the given iso level
+    float c = step(voxel(posXYZ).a, uIsoValue)
+        + 2. * step(voxel(posXYZ + c1 / uGridDim).a, uIsoValue)
+        + 4. * step(voxel(posXYZ + c2 / uGridDim).a, uIsoValue)
+        + 8. * step(voxel(posXYZ + c3 / uGridDim).a, uIsoValue)
+        + 16. * step(voxel(posXYZ + c4 / uGridDim).a, uIsoValue)
+        + 32. * step(voxel(posXYZ + c5 / uGridDim).a, uIsoValue)
+        + 64. * step(voxel(posXYZ + c6 / uGridDim).a, uIsoValue)
+        + 128. * step(voxel(posXYZ + c7 / uGridDim).a, uIsoValue);
+    c *= step(c, 254.);
+
+    // get total triangles to generate for calculated MC case from triCount texture
+    float totalTrianglesToGenerate = texture2D(tTriCount, vec2(intMod(c, 16.), floor(c / 16.)) / 16.).a;
+    gl_FragColor = vec4(vec3(floor(totalTrianglesToGenerate * 255.0 + 0.5) * 3.0), c);
+
+    // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ + c4 / uGridDim).a * 255.0);
+    // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ).a * 255.0);
+
+    // vec2 uv = vCoordinate;
+    // uv = gl_FragCoord.xy / uGridTexDim.xy;
+
+    // if (uv.y < 0.91) discard;
+    // gl_FragColor = vec4(vCoordinate * 255.0, 0.0, 255.0);
+    // gl_FragColor = vec4(250.0, 0.0, 0.0, 255.0);
+}
\ No newline at end of file
diff --git a/src/mol-gl/shader/marching-cubes/isosurface.frag b/src/mol-gl/shader/marching-cubes/isosurface.frag
new file mode 100644
index 0000000000000000000000000000000000000000..2499bbf7f43221dd94d659646a1050849ab2f1a4
--- /dev/null
+++ b/src/mol-gl/shader/marching-cubes/isosurface.frag
@@ -0,0 +1,209 @@
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tActiveVoxelsPyramid;
+uniform sampler2D tActiveVoxelsBase;
+uniform sampler2D tVolumeData;
+uniform sampler2D tTriIndices;
+
+uniform float uIsoValue;
+uniform float uLevels;
+uniform float uSize;
+uniform float uCount;
+
+uniform vec3 uGridDim;
+uniform vec3 uGridTexDim;
+uniform mat4 uGridTransform;
+
+// scale to volume data coord
+uniform vec2 uScale;
+
+// varying vec2 vCoordinate;
+
+#pragma glslify: import('../chunks/common.glsl')
+#pragma glslify: decodeFloatRGB = require(../utils/decode-float-rgb.glsl)
+
+// cube corners
+const vec3 c0 = vec3(0., 0., 0.);
+const vec3 c1 = vec3(1., 0., 0.);
+const vec3 c2 = vec3(1., 1., 0.);
+const vec3 c3 = vec3(0., 1., 0.);
+const vec3 c4 = vec3(0., 0., 1.);
+const vec3 c5 = vec3(1., 0., 1.);
+const vec3 c6 = vec3(1., 1., 1.);
+const vec3 c7 = vec3(0., 1., 1.);
+
+const float EPS = 0.00001;
+
+vec3 index3dFrom2d(vec2 coord) {
+    vec2 gridTexPos = coord * uGridTexDim.xy;
+    vec2 columnRow = floor(gridTexPos / uGridDim.xy);
+    vec2 posXY = gridTexPos - columnRow * uGridDim.xy;
+    float posZ = columnRow.y * floor(uGridTexDim.x / uGridDim.x) + columnRow.x;
+    vec3 posXYZ = vec3(posXY, posZ);
+    return posXYZ;
+}
+
+vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim) {
+    float zSlice = floor(pos.z * gridDim.z + 0.5); // round to nearest z-slice
+    float column = intMod(zSlice * gridDim.x, texDim.x) / gridDim.x;
+    float row = floor(intDiv(zSlice * gridDim.x, texDim.x));
+    vec2 coord = (vec2(column * gridDim.x, row * gridDim.y) + (pos.xy * gridDim.xy)) / (texDim / uScale);
+    return texture2D(tex, coord + 0.5 / (texDim / uScale));
+    // return texture2D(tex, coord);
+}
+
+vec4 voxel(vec3 pos) {
+    return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy);
+}
+
+void main(void) {
+    // get 1D index
+    float vI = dot(floor(uSize * (gl_FragCoord.xy / uSize)), vec2(1.0, uSize));
+
+    // ignore 1D indices outside of the grid
+    if(vI >= uCount) discard;
+
+    float offset = uSize - 2.;
+    float k = 1. / uSize;
+
+    vec2 relativePosition = k * vec2(offset, 0.);
+    vec4 partialSums = texture2D(tActiveVoxelsPyramid, relativePosition);
+    float start = 0.;
+    vec4 starts = vec4(0.);
+    vec4 ends = vec4(0.);
+    float diff = 2.;
+    vec4 m = vec4(0.);
+    vec2 position = vec2(0.);
+    vec4 vI4 = vec4(vI);
+
+    // traverse the different levels of the pyramid
+    for(int i = 1; i < 12; i++) {
+        if(float(i) >= uLevels) break;
+
+        offset -= diff;
+        diff *= 2.;
+        relativePosition = position + k * vec2(offset, 0.);
+
+        ends = partialSums.wzyx + vec4(start);
+        starts = vec4(start, ends.xyz);
+        m = vec4(greaterThanEqual(vI4, starts)) * vec4(lessThan(vI4, ends));
+        relativePosition += m.y * vec2(k, 0.) + m.z * vec2(0., k) + m.w * vec2(k, k);
+
+        start = dot(m, starts);
+        position = 2. * (relativePosition - k * vec2(offset, 0.));
+        partialSums = texture2D(tActiveVoxelsPyramid, relativePosition);
+    }
+
+    ends = partialSums.wzyx + vec4(start);
+    starts = vec4(start, ends.xyz);
+    m = vec4(greaterThanEqual(vI4, starts)) * vec4(lessThan(vI4, ends));
+    position += m.y * vec2(k, 0.) + m.z * vec2(0., k) + m.w * vec2(k, k);
+
+    vec2 coord2d = position / uScale;
+    vec3 coord3d = floor(index3dFrom2d(coord2d) + 0.5);
+
+    float edgeIndex = floor(texture2D(tActiveVoxelsBase, position).a + 0.5);
+
+    // current vertex for the up to 15 MC cases
+    float currentVertex = vI - dot(m, starts);
+
+    // get index into triIndices table
+    float mcIndex = 16. * edgeIndex + currentVertex;
+    vec4 mcData = texture2D(tTriIndices, vec2(intMod(mcIndex, 64.), floor(mcIndex / 64.)) / 64.);
+
+    // bit mask to avoid conditionals (see comment below) for getting MC case corner
+    vec4 m0 = vec4(floor(mcData.a * 255.0 + 0.5));
+
+    // get edge value masks
+    vec4 m1 = vec4(equal(m0, vec4(0., 1., 2., 3.)));
+    vec4 m2 = vec4(equal(m0, vec4(4., 5., 6., 7.)));
+    vec4 m3 = vec4(equal(m0, vec4(8., 9., 10., 11.)));
+
+    // apply bit masks
+    vec3 b0 = coord3d +
+                m1.y * c1 +
+                m1.z * c2 +
+                m1.w * c3 +
+                m2.x * c4 +
+                m2.y * c5 +
+                m2.z * c6 +
+                m2.w * c7 +
+                m3.y * c1 +
+                m3.z * c2 +
+                m3.w * c3;
+    vec3 b1 = coord3d +
+                m1.x * c1 +
+                m1.y * c2 +
+                m1.z * c3 +
+                m2.x * c5 +
+                m2.y * c6 +
+                m2.z * c7 +
+                m2.w * c4 +
+                m3.x * c4 +
+                m3.y * c5 +
+                m3.z * c6 +
+                m3.w * c7;
+
+    // the conditionals that are avoided by above bitmasks
+    // vec3 b0 = coord3d;
+    // vec3 b1 = coord3d;
+    // if (mcIndex == 0.0) {
+    //     b0 += c0; b1 += c1;
+    // } else if (mcIndex == 1.0) {
+    //     b0 += c1; b1 += c2;
+    // } else if (mcIndex == 2.0) {
+    //     b0 += c2; b1 += c3;
+    // } else if (mcIndex == 3.0) {
+    //     b0 += c3; b1 += c0;
+    // } else if (mcIndex == 4.0) {
+    //     b0 += c4; b1 += c5;
+    // } else if (mcIndex == 5.0) {
+    //     b0 += c5; b1 += c6;
+    // } else if (mcIndex == 6.0) {
+    //     b0 += c6; b1 += c7;
+    // } else if (mcIndex == 7.0) {
+    //     b0 += c7; b1 += c4;
+    // } else if (mcIndex == 8.0) {
+    //     b0 += c0; b1 += c4;
+    // } else if (mcIndex == 9.0) {
+    //     b0 += c1; b1 += c5;
+    // } else if (mcIndex == 10.0) {
+    //     b0 += c2; b1 += c6;
+    // } else if (mcIndex == 11.0) {
+    //     b0 += c3; b1 += c7;
+    // }
+    // b0 = floor(b0 + 0.5);
+    // b1 = floor(b1 + 0.5);
+
+    vec4 d0 = voxel(b0 / uGridDim);
+    vec4 d1 = voxel(b1 / uGridDim);
+
+    float v0 = d0.a;
+    float v1 = d1.a;
+
+    float t = (uIsoValue - v0) / (v0 - v1);
+    // t = -0.5;
+    gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz;
+    gl_FragData[0].w = decodeFloatRGB(d0.rgb); // group id
+
+    // normals from gradients
+    vec3 n0 = -normalize(vec3(
+        voxel((b0 - c1) / uGridDim).a - voxel((b0 + c1) / uGridDim).a,
+        voxel((b0 - c3) / uGridDim).a - voxel((b0 + c3) / uGridDim).a,
+        voxel((b0 - c4) / uGridDim).a - voxel((b0 + c4) / uGridDim).a
+    ));
+    vec3 n1 = -normalize(vec3(
+        voxel((b1 - c1) / uGridDim).a - voxel((b1 + c1) / uGridDim).a,
+        voxel((b1 - c3) / uGridDim).a - voxel((b1 + c3) / uGridDim).a,
+        voxel((b1 - c4) / uGridDim).a - voxel((b1 + c4) / uGridDim).a
+    ));
+    gl_FragData[1].xyz = -vec3(
+        n0.x + t * (n0.x - n1.x),
+        n0.y + t * (n0.y - n1.y),
+        n0.z + t * (n0.z - n1.z)
+    );
+
+    mat3 normalMatrix = transpose(inverse(mat3(uGridTransform)));
+    gl_FragData[1].xyz = normalMatrix * gl_FragData[1].xyz;
+}
\ No newline at end of file
diff --git a/src/mol-gl/shader/mesh.frag b/src/mol-gl/shader/mesh.frag
index 93858961510496217a7121ee8182aee085218313..5198d5e30ef434aacb170ab68b0dbff3e643425d 100644
--- a/src/mol-gl/shader/mesh.frag
+++ b/src/mol-gl/shader/mesh.frag
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,24 +9,8 @@ precision highp int;
 
 #pragma glslify: import('./chunks/common-frag-params.glsl')
 #pragma glslify: import('./chunks/color-frag-params.glsl')
-
-// uniform vec3 uLightPosition;
-uniform vec3 uLightColor;
-uniform vec3 uLightAmbient;
-uniform mat4 uView;
-
-#if !defined(dFlatShaded) || !defined(enabledStandardDerivatives)
-    varying vec3 vNormal;
-#endif
-
-#pragma glslify: attenuation = require(./utils/attenuation.glsl)
-#pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl)
-#pragma glslify: calculateDiffuse = require(./utils/oren-nayar-diffuse.glsl)
-
-const float specularScale = 0.15;
-const float shininess = 200.0;
-const float roughness = 100.0;
-const float albedo = 0.95;
+#pragma glslify: import('./chunks/light-frag-params.glsl')
+#pragma glslify: import('./chunks/normal-frag-params.glsl')
 
 void main() {
     // material color
@@ -37,40 +21,8 @@ void main() {
             discard; // ignore so the element below can be picked
         gl_FragColor = material;
     #else
-        // determine surface to light direction
-        // vec4 viewLightPosition = view * vec4(lightPosition, 1.0);
-        // vec3 lightVector = viewLightPosition.xyz - vViewPosition;
-        vec3 lightVector = vViewPosition;
-
-        vec3 L = normalize(lightVector); // light direction
-        vec3 V = normalize(vViewPosition); // eye direction
-
-        // surface normal
-        #if defined(dFlatShaded) && defined(enabledStandardDerivatives)
-            vec3 fdx = dFdx(vViewPosition);
-            vec3 fdy = dFdy(vViewPosition);
-            vec3 N = -normalize(cross(fdx, fdy));
-        #else
-            vec3 N = -normalize(vNormal);
-            #ifdef dDoubleSided
-                N = N * (float(gl_FrontFacing) * 2.0 - 1.0);
-            #endif
-        #endif
-
-        // compute our diffuse & specular terms
-        float specular = calculateSpecular(L, V, N, shininess) * specularScale;
-        vec3 diffuse = uLightColor * calculateDiffuse(L, V, N, roughness, albedo);
-        vec3 ambient = uLightAmbient;
-
-        // add the lighting
-        vec3 finalColor = material.rgb * (diffuse + ambient) + specular;
-
-        // gl_FragColor.rgb = N;
-        // gl_FragColor.a = 1.0;
-        // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
-        gl_FragColor.rgb = finalColor;
-        gl_FragColor.a = material.a;
-
+        #pragma glslify: import('./chunks/assign-normal.glsl')
+        #pragma glslify: import('./chunks/apply-light-color.glsl')
         #pragma glslify: import('./chunks/apply-marker-color.glsl')
         #pragma glslify: import('./chunks/apply-fog.glsl')
     #endif
diff --git a/src/mol-gl/shader/mesh.vert b/src/mol-gl/shader/mesh.vert
index 99cf417396901d1ec6a3d61b1b5978550a8586d9..d49bba6012ca5deb366c5a648a4b8906bce4221b 100644
--- a/src/mol-gl/shader/mesh.vert
+++ b/src/mol-gl/shader/mesh.vert
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,24 +10,39 @@ precision highp int;
 #pragma glslify: import('./chunks/common-vert-params.glsl')
 #pragma glslify: import('./chunks/color-vert-params.glsl')
 
-attribute vec3 aPosition;
+#ifdef dGeoTexture
+    uniform vec2 uGeoTexDim;
+    uniform sampler2D tPositionGroup;
+#else
+    attribute vec3 aPosition;
+#endif
 attribute mat4 aTransform;
 attribute float aInstance;
 attribute float aGroup;
 
 #ifndef dFlatShaded
-    attribute vec3 aNormal;
+    #ifdef dGeoTexture
+        uniform sampler2D tNormal;
+    #else
+        attribute vec3 aNormal;
+    #endif
     varying vec3 vNormal;
 #endif
 
 void main(){
+    #pragma glslify: import('./chunks/assign-group.glsl')
     #pragma glslify: import('./chunks/assign-color-varying.glsl')
     #pragma glslify: import('./chunks/assign-marker-varying.glsl')
     #pragma glslify: import('./chunks/assign-position.glsl')
 
     #ifndef dFlatShaded
+        #ifdef dGeoTexture
+            vec3 normal = readFromTexture(tNormal, aGroup, uGeoTexDim).xyz;
+        #else
+            vec3 normal = aNormal;
+        #endif
         mat3 normalMatrix = transpose(inverse(mat3(modelView)));
-        vec3 transformedNormal = normalize(normalMatrix * normalize(aNormal));
+        vec3 transformedNormal = normalize(normalMatrix * normalize(normal));
         #if defined(dFlipSided) && !defined(dDoubleSided) // TODO checking dDoubleSided should not be required, ASR
             transformedNormal = -transformedNormal;
         #endif
diff --git a/src/mol-gl/shader/points.vert b/src/mol-gl/shader/points.vert
index 27fe6a387b81597f33ab360098a3217068bff2ec..7a87072bff2dd8831f62fd8a708cd26dffff6681 100644
--- a/src/mol-gl/shader/points.vert
+++ b/src/mol-gl/shader/points.vert
@@ -20,6 +20,7 @@ attribute float aInstance;
 attribute float aGroup;
 
 void main(){
+    #pragma glslify: import('./chunks/assign-group.glsl')
     #pragma glslify: import('./chunks/assign-color-varying.glsl')
     #pragma glslify: import('./chunks/assign-marker-varying.glsl')
     #pragma glslify: import('./chunks/assign-position.glsl')
diff --git a/src/mol-gl/shader/quad.vert b/src/mol-gl/shader/quad.vert
new file mode 100644
index 0000000000000000000000000000000000000000..4c3b888e5b2552c9a5709f843d64c8fef66f05ce
--- /dev/null
+++ b/src/mol-gl/shader/quad.vert
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+precision highp float;
+
+attribute vec2 aPosition;
+uniform vec2 uQuadScale;
+
+void main(void) {
+    vec2 position = aPosition * uQuadScale - vec2(1.0, 1.0) + uQuadScale;
+    gl_Position = vec4(position, 0.0, 1.0);
+}
\ No newline at end of file
diff --git a/src/mol-gl/shader/spheres.frag b/src/mol-gl/shader/spheres.frag
index fc215b5ad16a0e2969feb038fefa47ad7963e507..0f0488b4f66fee13a14590d01dbaa28ce5bc7d6c 100644
--- a/src/mol-gl/shader/spheres.frag
+++ b/src/mol-gl/shader/spheres.frag
@@ -9,11 +9,7 @@ precision highp int;
 
 #pragma glslify: import('./chunks/common-frag-params.glsl')
 #pragma glslify: import('./chunks/color-frag-params.glsl')
-
-// uniform vec3 uLightPosition;
-uniform vec3 uLightColor;
-uniform vec3 uLightAmbient;
-uniform mat4 uView;
+#pragma glslify: import('./chunks/light-frag-params.glsl')
 
 uniform mat4 uProjection;
 // uniform vec3 interiorColor;
@@ -30,27 +26,18 @@ varying float vRadiusSq;
 varying vec3 vPoint;
 varying vec3 vPointViewPosition;
 
-#pragma glslify: attenuation = require(./utils/attenuation.glsl)
-#pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl)
-#pragma glslify: calculateDiffuse = require(./utils/oren-nayar-diffuse.glsl)
-
-const float specularScale = 0.15;
-const float shininess = 200.0;
-const float roughness = 100.0;
-const float albedo = 0.95;
-
 bool flag2 = false;
 bool interior = false;
 vec3 cameraPos;
 vec3 cameraNormal;
 
 // Calculate depth based on the given camera position.
-float calcDepth(in vec3 cameraPos){
+float calcDepth(const in vec3 cameraPos){
     vec2 clipZW = cameraPos.z * uProjection[2].zw + uProjection[3].zw;
     return 0.5 + 0.5 * clipZW.x / clipZW.y;
 }
 
-float calcClip(in vec3 cameraPos) {
+float calcClip(const in vec3 cameraPos) {
     return dot(vec4(cameraPos, 1.0), vec4(0.0, 0.0, 1.0, clipNear - 0.5));
 }
 
@@ -143,36 +130,9 @@ void main(void){
             discard; // ignore so the element below can be picked
         gl_FragColor = material;
     #else
-
-        vec3 vNormal = cameraNormal;
+        vec3 normal = cameraNormal;
         vec3 vViewPosition = -cameraPos;
-
-        // determine surface to light direction
-        // vec4 viewLightPosition = view * vec4(lightPosition, 1.0);
-        // vec3 lightVector = viewLightPosition.xyz - vViewPosition;
-        vec3 lightVector = vViewPosition;
-
-        vec3 L = normalize(lightVector); // light direction
-        vec3 V = normalize(vViewPosition); // eye direction
-
-        vec3 N = normalize(vNormal);
-        #ifdef dDoubleSided
-            N = N * (float(gl_FrontFacing) * 2.0 - 1.0);
-        #endif
-
-        // compute our diffuse & specular terms
-        float specular = calculateSpecular(L, V, N, shininess) * specularScale;
-        vec3 diffuse = uLightColor * calculateDiffuse(L, V, N, roughness, albedo);
-        vec3 ambient = uLightAmbient;
-
-        // add the lighting
-        vec3 finalColor = material.rgb * (diffuse + ambient) + specular;
-
-        // gl_FragColor.rgb = N;
-        // gl_FragColor.a = 1.0;
-        // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
-        gl_FragColor.rgb = finalColor;
-        gl_FragColor.a = material.a;
+        #pragma glslify: import('./chunks/apply-light-color.glsl')
 
         if(interior){
             #ifdef USE_INTERIOR_COLOR
diff --git a/src/mol-gl/shader/spheres.vert b/src/mol-gl/shader/spheres.vert
index 6203d8a3b2b8d24ca6229eb771ea15570a5e365d..65656354334a22dcaa33ba90cb0cb511209a2c22 100644
--- a/src/mol-gl/shader/spheres.vert
+++ b/src/mol-gl/shader/spheres.vert
@@ -86,6 +86,7 @@ void quadraticProjection(const in float radius, const in vec3 position){
 
 
 void main(void){
+    #pragma glslify: import('./chunks/assign-group.glsl')
     #pragma glslify: import('./chunks/assign-color-varying.glsl')
     #pragma glslify: import('./chunks/assign-marker-varying.glsl')
     #pragma glslify: import('./chunks/assign-size.glsl')
diff --git a/src/mol-gl/shader/text.vert b/src/mol-gl/shader/text.vert
index 4bed4c542e26fa3ea27d83f08a5db597e87c5e6d..3e5309bbf83aeacfd4e9a6927be4c5e8b6b26f4f 100644
--- a/src/mol-gl/shader/text.vert
+++ b/src/mol-gl/shader/text.vert
@@ -34,6 +34,7 @@ varying vec2 vTexCoord;
 #pragma glslify: matrixScale = require(./utils/matrix-scale.glsl)
 
 void main(void){
+    #pragma glslify: import('./chunks/assign-group.glsl')
     #pragma glslify: import('./chunks/assign-color-varying.glsl')
     #pragma glslify: import('./chunks/assign-marker-varying.glsl')
     #pragma glslify: import('./chunks/assign-size.glsl')
diff --git a/src/mol-gl/shader/utils/attenuation.glsl b/src/mol-gl/shader/utils/attenuation.glsl
deleted file mode 100644
index 833423b85b9fcfc3dfd334b1714bfccfec9d78ee..0000000000000000000000000000000000000000
--- a/src/mol-gl/shader/utils/attenuation.glsl
+++ /dev/null
@@ -1,14 +0,0 @@
-// by Tom Madams
-// Simple:
-// https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/
-//
-// Improved
-// https://imdoingitwrong.wordpress.com/2011/02/10/improved-light-attenuation/
-float attenuation(const in float r, const in float f, const in float d) {
-    float denom = d / r + 1.0;
-    float attenuation = 1.0 / (denom*denom);
-    float t = (attenuation - f) / (1.0 - f);
-    return max(t, 0.0);
-}
-
-#pragma glslify: export(attenuation)
\ No newline at end of file
diff --git a/src/mol-gl/shader/utils/oren-nayar-diffuse.glsl b/src/mol-gl/shader/utils/oren-nayar-diffuse.glsl
deleted file mode 100644
index 59a3078b831a51003f990693a0da9ff0ed06a743..0000000000000000000000000000000000000000
--- a/src/mol-gl/shader/utils/oren-nayar-diffuse.glsl
+++ /dev/null
@@ -1,21 +0,0 @@
-// (c) 2014 Mikola Lysenko. MIT License
-// https://github.com/glslify/glsl-diffuse-oren-nayar
-
-#define PI 3.14159265
-
-float orenNayarDiffuse(const in vec3 lightDirection, const in vec3 viewDirection, const in vec3 surfaceNormal, const in float roughness, const in float albedo) {
-    float LdotV = dot(lightDirection, viewDirection);
-    float NdotL = dot(lightDirection, surfaceNormal);
-    float NdotV = dot(surfaceNormal, viewDirection);
-
-    float s = LdotV - NdotL * NdotV;
-    float t = mix(1.0, max(NdotL, NdotV), step(0.0, s));
-
-    float sigma2 = roughness * roughness;
-    float A = 1.0 + sigma2 * (albedo / (sigma2 + 0.13) + 0.5 / (sigma2 + 0.33));
-    float B = 0.45 * sigma2 / (sigma2 + 0.09);
-
-    return albedo * max(0.0, NdotL) * (A + B * s / t) / PI;
-}
-
-#pragma glslify: export(orenNayarDiffuse)
\ No newline at end of file
diff --git a/src/mol-gl/shader/utils/phong-specular.glsl b/src/mol-gl/shader/utils/phong-specular.glsl
deleted file mode 100644
index 6d42305acf8e23d6204d7b0a13f91a69d15f9cf3..0000000000000000000000000000000000000000
--- a/src/mol-gl/shader/utils/phong-specular.glsl
+++ /dev/null
@@ -1,10 +0,0 @@
-// (c) 2014 Mikola Lysenko. MIT License
-// https://github.com/glslify/glsl-specular-phong
-
-float phongSpecular(const in vec3 lightDirection, const in vec3 viewDirection, const in vec3 surfaceNormal, const in float shininess) {
-    //Calculate Phong power
-    vec3 R = -reflect(lightDirection, surfaceNormal);
-    return pow(max(0.0, dot(viewDirection, R)), shininess);
-}
-
-#pragma glslify: export(phongSpecular)
\ No newline at end of file
diff --git a/src/mol-gl/webgl/buffer.ts b/src/mol-gl/webgl/buffer.ts
index 80cd13401bfe84604f844b89b75d5c42d816812d..eb2c4f43553aea7e69c117c5d3e2169b800d01c2 100644
--- a/src/mol-gl/webgl/buffer.ts
+++ b/src/mol-gl/webgl/buffer.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,12 +9,13 @@ import { ValueCell } from 'mol-util';
 import { RenderableSchema } from '../renderable/schema';
 import { idFactory } from 'mol-util/id-factory';
 import { ValueOf } from 'mol-util/type-helpers';
+import { GLRenderingContext } from './compat';
 
 const getNextBufferId = idFactory()
 
 export type UsageHint = 'static' | 'dynamic' | 'stream'
 export type DataType = 'uint8' | 'int8' | 'uint16' | 'int16' | 'uint32' | 'int32' | 'float32'
-export type BufferType = 'attribute' | 'elements'
+export type BufferType = 'attribute' | 'elements' | 'uniform'
 
 export type DataTypeArrayType = {
     'uint8': Uint8Array
@@ -28,8 +29,6 @@ export type DataTypeArrayType = {
 export type ArrayType = ValueOf<DataTypeArrayType>
 export type ArrayKind = keyof DataTypeArrayType
 
-export type BufferItemSize = 1 | 2 | 3 | 4 | 16
-
 export function getUsageHint(ctx: WebGLContext, usageHint: UsageHint) {
     const { gl } = ctx
     switch (usageHint) {
@@ -78,6 +77,7 @@ export function getBufferType(ctx: WebGLContext, bufferType: BufferType) {
     switch (bufferType) {
         case 'attribute': return gl.ARRAY_BUFFER
         case 'elements': return gl.ELEMENT_ARRAY_BUFFER
+        case 'uniform': return (gl as WebGL2RenderingContext).UNIFORM_BUFFER
     }
 }
 
@@ -90,8 +90,6 @@ export interface Buffer {
     readonly _dataType: number
     readonly _bpe: number
 
-    readonly itemSize: number
-    readonly itemCount: number
     readonly length: number
 
     updateData: (array: ArrayType) => void
@@ -99,8 +97,8 @@ export interface Buffer {
     destroy: () => void
 }
 
-export function createBuffer(ctx: WebGLContext, array: ArrayType, itemSize: BufferItemSize, usageHint: UsageHint, bufferType: BufferType): Buffer {
-    const { gl } = ctx
+export function createBuffer(ctx: WebGLContext, array: ArrayType, usageHint: UsageHint, bufferType: BufferType): Buffer {
+    const { gl, stats } = ctx
     const _buffer = gl.createBuffer()
     if (_buffer === null) {
         throw new Error('Could not create WebGL buffer')
@@ -111,16 +109,15 @@ export function createBuffer(ctx: WebGLContext, array: ArrayType, itemSize: Buff
     const _dataType = dataTypeFromArray(ctx, array)
     const _bpe = array.BYTES_PER_ELEMENT
     const _length = array.length
-    const _itemCount = Math.floor(_length / itemSize)
 
     function updateData(array: ArrayType) {
         gl.bindBuffer(_bufferType, _buffer);
-        (gl as WebGLRenderingContext).bufferData(_bufferType, array, _usageHint) // TODO remove cast when webgl2 types are fixed
+        gl.bufferData(_bufferType, array, _usageHint)
     }
     updateData(array)
 
     let destroyed = false
-    ctx.bufferCount += 1
+    stats.bufferCount += 1
 
     return {
         id: getNextBufferId(),
@@ -131,40 +128,66 @@ export function createBuffer(ctx: WebGLContext, array: ArrayType, itemSize: Buff
         _dataType,
         _bpe,
 
-        get itemSize () { return itemSize },
-        get itemCount () { return _itemCount },
-        get length () { return _length },
+        length: _length,
 
         updateData,
         updateSubData: (array: ArrayType, offset: number, count: number) => {
             gl.bindBuffer(_bufferType, _buffer);
-            (gl as WebGLRenderingContext).bufferSubData(_bufferType, offset * _bpe, array.subarray(offset, offset + count)) // TODO remove cast when webgl2 types are fixed
+            gl.bufferSubData(_bufferType, offset * _bpe, array.subarray(offset, offset + count))
         },
 
         destroy: () => {
             if (destroyed) return
             gl.deleteBuffer(_buffer)
             destroyed = true
-            ctx.bufferCount -= 1
+            stats.bufferCount -= 1
         }
     }
 }
 
+//
+
+export type AttributeItemSize = 1 | 2 | 3 | 4 | 16
+export type AttributeKind = 'float32' | 'int32'
+
+export function getAttribType(gl: GLRenderingContext, kind: AttributeKind, itemSize: AttributeItemSize) {
+    switch (kind) {
+        case 'int32':
+            switch (itemSize) {
+                case 1: return gl.INT
+                case 2: return gl.INT_VEC2
+                case 3: return gl.INT_VEC3
+                case 4: return gl.INT_VEC4
+            }
+            break
+        case 'float32':
+            switch (itemSize) {
+                case 1: return gl.FLOAT
+                case 2: return gl.FLOAT_VEC2
+                case 3: return gl.FLOAT_VEC3
+                case 4: return gl.FLOAT_VEC4
+                case 16: return gl.FLOAT_MAT4
+            }
+            break
+    }
+    throw new Error(`unknown attribute type for kind '${kind}' and itemSize '${itemSize}'`)
+}
+
 export type AttributeDefs = {
-    [k: string]: { kind: ArrayKind, itemSize: BufferItemSize, divisor: number }
+    [k: string]: { kind: AttributeKind, itemSize: AttributeItemSize, divisor: number }
 }
 export type AttributeValues = { [k: string]: ValueCell<ArrayType> }
-export type AttributeBuffers = { [k: string]: AttributeBuffer }
+export type AttributeBuffers = [string, AttributeBuffer][]
 
 export interface AttributeBuffer extends Buffer {
     bind: (location: number) => void
 }
 
-export function createAttributeBuffer<T extends ArrayType, S extends BufferItemSize>(ctx: WebGLContext, array: ArrayType, itemSize: S, divisor: number, usageHint: UsageHint = 'dynamic'): AttributeBuffer {
+export function createAttributeBuffer<T extends ArrayType, S extends AttributeItemSize>(ctx: WebGLContext, array: T, itemSize: S, divisor: number, usageHint: UsageHint = 'dynamic'): AttributeBuffer {
     const { gl } = ctx
     const { instancedArrays } = ctx.extensions
 
-    const buffer = createBuffer(ctx, array, itemSize, usageHint, 'attribute')
+    const buffer = createBuffer(ctx, array, usageHint, 'attribute')
     const { _buffer, _bufferType, _dataType, _bpe } = buffer
 
     return {
@@ -187,16 +210,18 @@ export function createAttributeBuffer<T extends ArrayType, S extends BufferItemS
 }
 
 export function createAttributeBuffers(ctx: WebGLContext, schema: RenderableSchema, values: AttributeValues) {
-    const buffers: AttributeBuffers = {}
+    const buffers: AttributeBuffers = []
     Object.keys(schema).forEach(k => {
         const spec = schema[k]
         if (spec.type === 'attribute') {
-            buffers[k] = createAttributeBuffer(ctx, values[k].ref.value, spec.itemSize, spec.divisor)
+            buffers[buffers.length] = [k, createAttributeBuffer(ctx, values[k].ref.value, spec.itemSize, spec.divisor)]
         }
     })
-    return buffers as AttributeBuffers
+    return buffers
 }
 
+//
+
 export type ElementsType = Uint16Array | Uint32Array
 export type ElementsKind = 'uint16' | 'uint32'
 
@@ -206,7 +231,7 @@ export interface ElementsBuffer extends Buffer {
 
 export function createElementsBuffer(ctx: WebGLContext, array: ElementsType, usageHint: UsageHint = 'static'): ElementsBuffer {
     const { gl } = ctx
-    const buffer = createBuffer(ctx, array, 1, usageHint, 'elements')
+    const buffer = createBuffer(ctx, array, usageHint, 'elements')
     const { _buffer } = buffer
 
     return {
diff --git a/src/mol-gl/webgl/compat.ts b/src/mol-gl/webgl/compat.ts
index 7d763614ea33e22cf2c1c5ec32b8d5b85fb64ce2..9021af77e7f829228e8755fbc84d73463651c949 100644
--- a/src/mol-gl/webgl/compat.ts
+++ b/src/mol-gl/webgl/compat.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -50,9 +50,7 @@ export function getStandardDerivatives(gl: GLRenderingContext): COMPAT_standard_
         return { FRAGMENT_SHADER_DERIVATIVE_HINT: gl.FRAGMENT_SHADER_DERIVATIVE_HINT }
     } else {
         const ext = gl.getExtension('OES_standard_derivatives')
-        if (ext === null) {
-            throw new Error('Could not get "OES_standard_derivatives" extension')
-        }
+        if (ext === null) return null
         return { FRAGMENT_SHADER_DERIVATIVE_HINT: ext.FRAGMENT_SHADER_DERIVATIVE_HINT_OES }
     }
 }
@@ -79,7 +77,7 @@ export function getVertexArrayObject(gl: GLRenderingContext): COMPAT_vertex_arra
             bindVertexArray: gl.bindVertexArray.bind(gl),
             createVertexArray: gl.createVertexArray.bind(gl),
             deleteVertexArray: gl.deleteVertexArray.bind(gl),
-            isVertexArray: gl.isVertexArray.bind(gl) as (value: any) => value is WebGLVertexArrayObject // TODO change when webgl2 types are fixed
+            isVertexArray: gl.isVertexArray.bind(gl)
         }
     } else {
         const ext = gl.getExtension('OES_vertex_array_object')
@@ -128,4 +126,98 @@ export interface COMPAT_frag_depth {
 
 export function getFragDepth(gl: GLRenderingContext): COMPAT_frag_depth | null {
     return isWebGL2(gl) ? {} : gl.getExtension('EXT_frag_depth')
+}
+
+export interface COMPAT_color_buffer_float {
+    readonly RGBA32F: number;
+}
+
+export function getColorBufferFloat(gl: GLRenderingContext): COMPAT_color_buffer_float | null {
+    if (isWebGL2(gl)) {
+        if (gl.getExtension('EXT_color_buffer_float') === null) return null
+        return { RGBA32F: gl.RGBA32F }
+    } else {
+        const ext = gl.getExtension('WEBGL_color_buffer_float')
+        if (ext === null) return null
+        return { RGBA32F: ext.RGBA32F_EXT }
+    }
+}
+
+export interface COMPAT_draw_buffers {
+    drawBuffers(buffers: number[]): void;
+    readonly COLOR_ATTACHMENT0: number;
+    readonly COLOR_ATTACHMENT1: number;
+    readonly COLOR_ATTACHMENT2: number;
+    readonly COLOR_ATTACHMENT3: number;
+    readonly COLOR_ATTACHMENT4: number;
+    readonly COLOR_ATTACHMENT5: number;
+    readonly COLOR_ATTACHMENT6: number;
+    readonly COLOR_ATTACHMENT7: number;
+    readonly DRAW_BUFFER0: number;
+    readonly DRAW_BUFFER1: number;
+    readonly DRAW_BUFFER2: number;
+    readonly DRAW_BUFFER3: number;
+    readonly DRAW_BUFFER4: number;
+    readonly DRAW_BUFFER5: number;
+    readonly DRAW_BUFFER6: number;
+    readonly DRAW_BUFFER7: number;
+    readonly MAX_COLOR_ATTACHMENTS: number;
+    readonly MAX_DRAW_BUFFERS: number;
+}
+
+export function getDrawBuffers(gl: GLRenderingContext): COMPAT_draw_buffers | null {
+    if (isWebGL2(gl)) {
+        return {
+            drawBuffers: gl.drawBuffers.bind(gl),
+            COLOR_ATTACHMENT0: gl.COLOR_ATTACHMENT0,
+            COLOR_ATTACHMENT1: gl.COLOR_ATTACHMENT1,
+            COLOR_ATTACHMENT2: gl.COLOR_ATTACHMENT2,
+            COLOR_ATTACHMENT3: gl.COLOR_ATTACHMENT3,
+            COLOR_ATTACHMENT4: gl.COLOR_ATTACHMENT4,
+            COLOR_ATTACHMENT5: gl.COLOR_ATTACHMENT5,
+            COLOR_ATTACHMENT6: gl.COLOR_ATTACHMENT6,
+            COLOR_ATTACHMENT7: gl.COLOR_ATTACHMENT7,
+            DRAW_BUFFER0: gl.DRAW_BUFFER0,
+            DRAW_BUFFER1: gl.DRAW_BUFFER1,
+            DRAW_BUFFER2: gl.DRAW_BUFFER2,
+            DRAW_BUFFER3: gl.DRAW_BUFFER3,
+            DRAW_BUFFER4: gl.DRAW_BUFFER4,
+            DRAW_BUFFER5: gl.DRAW_BUFFER5,
+            DRAW_BUFFER6: gl.DRAW_BUFFER6,
+            DRAW_BUFFER7: gl.DRAW_BUFFER7,
+            MAX_COLOR_ATTACHMENTS: gl.MAX_COLOR_ATTACHMENTS,
+            MAX_DRAW_BUFFERS: gl.MAX_DRAW_BUFFERS,
+        }
+    } else {
+        const ext = gl.getExtension('WEBGL_draw_buffers')
+        if (ext === null) return null
+        return {
+            drawBuffers: ext.drawBuffersWEBGL.bind(ext),
+            COLOR_ATTACHMENT0: ext.COLOR_ATTACHMENT0_WEBGL,
+            COLOR_ATTACHMENT1: ext.COLOR_ATTACHMENT1_WEBGL,
+            COLOR_ATTACHMENT2: ext.COLOR_ATTACHMENT2_WEBGL,
+            COLOR_ATTACHMENT3: ext.COLOR_ATTACHMENT3_WEBGL,
+            COLOR_ATTACHMENT4: ext.COLOR_ATTACHMENT4_WEBGL,
+            COLOR_ATTACHMENT5: ext.COLOR_ATTACHMENT5_WEBGL,
+            COLOR_ATTACHMENT6: ext.COLOR_ATTACHMENT6_WEBGL,
+            COLOR_ATTACHMENT7: ext.COLOR_ATTACHMENT7_WEBGL,
+            DRAW_BUFFER0: ext.DRAW_BUFFER0_WEBGL,
+            DRAW_BUFFER1: ext.DRAW_BUFFER1_WEBGL,
+            DRAW_BUFFER2: ext.DRAW_BUFFER2_WEBGL,
+            DRAW_BUFFER3: ext.DRAW_BUFFER3_WEBGL,
+            DRAW_BUFFER4: ext.DRAW_BUFFER4_WEBGL,
+            DRAW_BUFFER5: ext.DRAW_BUFFER5_WEBGL,
+            DRAW_BUFFER6: ext.DRAW_BUFFER6_WEBGL,
+            DRAW_BUFFER7: ext.DRAW_BUFFER7_WEBGL,
+            MAX_COLOR_ATTACHMENTS: ext.MAX_COLOR_ATTACHMENTS_WEBGL,
+            MAX_DRAW_BUFFERS: ext.MAX_DRAW_BUFFERS_WEBGL,
+        }
+    }
+}
+
+export interface COMPAT_shader_texture_lod {
+}
+
+export function getShaderTextureLod(gl: GLRenderingContext): COMPAT_shader_texture_lod | null {
+    return isWebGL2(gl) ? {} : gl.getExtension('EXT_shader_texture_lod')
 }
\ No newline at end of file
diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts
index 9b231fdfb20ffafb28829c4bfcd83b75af089a03..aca8e579dcc80a93d21a5edc09af0362c5c6b3c6 100644
--- a/src/mol-gl/webgl/context.ts
+++ b/src/mol-gl/webgl/context.ts
@@ -1,14 +1,15 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { createProgramCache, ProgramCache } from './program'
 import { createShaderCache, ShaderCache } from './shader'
-import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, isWebGL2, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth } from './compat';
-import { createFramebufferCache, FramebufferCache } from './framebuffer';
+import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, isWebGL2, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod } from './compat';
+import { createFramebufferCache, FramebufferCache, checkFramebufferStatus } from './framebuffer';
 import { Scheduler } from 'mol-task';
+import { isDebugMode } from 'mol-util/debug';
 
 export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null {
     function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') {
@@ -25,6 +26,24 @@ function getPixelRatio() {
     return (typeof window !== 'undefined') ? window.devicePixelRatio : 1
 }
 
+function getErrorDescription(gl: GLRenderingContext, error: number) {
+    switch (error) {
+        case gl.NO_ERROR: return 'no error'
+        case gl.INVALID_ENUM: return 'invalid enum'
+        case gl.INVALID_VALUE: return 'invalid value'
+        case gl.INVALID_OPERATION: return 'invalid operation'
+        case gl.INVALID_FRAMEBUFFER_OPERATION: return 'invalid framebuffer operation'
+        case gl.OUT_OF_MEMORY: return 'out of memory'
+        case gl.CONTEXT_LOST_WEBGL: return 'context lost'
+    }
+    return 'unknown error'
+}
+
+export function checkError(gl: GLRenderingContext) {
+    const error = gl.getError()
+    if (error) throw new Error(`WebGL error: '${getErrorDescription(gl, error)}'`)
+}
+
 function unbindResources (gl: GLRenderingContext) {
     // bind null to all texture units
     const maxTextureImageUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)
@@ -32,6 +51,10 @@ function unbindResources (gl: GLRenderingContext) {
         gl.activeTexture(gl.TEXTURE0 + i)
         gl.bindTexture(gl.TEXTURE_2D, null)
         gl.bindTexture(gl.TEXTURE_CUBE_MAP, null)
+        if (isWebGL2(gl)) {
+            gl.bindTexture(gl.TEXTURE_2D_ARRAY, null)
+            gl.bindTexture(gl.TEXTURE_3D, null)
+        }
     }
 
     // assign the smallest possible buffer to all attributes
@@ -93,9 +116,20 @@ function waitForGpuCommandsComplete(gl: GLRenderingContext): Promise<void> {
 }
 
 function waitForGpuCommandsCompleteSync(gl: GLRenderingContext): void {
+    gl.bindFramebuffer(gl.FRAMEBUFFER, null)
     gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
 }
 
+function readPixels(gl: GLRenderingContext, x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) {
+    if (isDebugMode) checkFramebufferStatus(gl)
+    if (buffer instanceof Uint8Array) {
+        gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)
+    } else {
+        gl.readPixels(x, y, width, height, gl.RGBA, gl.FLOAT, buffer)
+    }
+    if (isDebugMode) checkError(gl)
+}
+
 export function createImageData(buffer: ArrayLike<number>, width: number, height: number) {
     const w = width * 4
     const h = height
@@ -113,7 +147,7 @@ export function createImageData(buffer: ArrayLike<number>, width: number, height
 
 //
 
-type Extensions = {
+export type WebGLExtensions = {
     instancedArrays: COMPAT_instanced_arrays
     standardDerivatives: COMPAT_standard_derivatives
     blendMinMax: COMPAT_blend_minmax
@@ -122,43 +156,12 @@ type Extensions = {
     elementIndexUint: COMPAT_element_index_uint | null
     vertexArrayObject: COMPAT_vertex_array_object | null
     fragDepth: COMPAT_frag_depth | null
+    colorBufferFloat: COMPAT_color_buffer_float | null
+    drawBuffers: COMPAT_draw_buffers | null
+    shaderTextureLod: COMPAT_shader_texture_lod | null
 }
 
-/** A WebGL context object, including the rendering context, resource caches and counts */
-export interface WebGLContext {
-    readonly gl: GLRenderingContext
-    readonly isWebGL2: boolean
-    readonly extensions: Extensions
-    readonly pixelRatio: number
-
-    readonly shaderCache: ShaderCache
-    readonly programCache: ProgramCache
-    readonly framebufferCache: FramebufferCache
-
-    currentProgramId: number
-
-    bufferCount: number
-    framebufferCount: number
-    renderbufferCount: number
-    textureCount: number
-    vaoCount: number
-
-    drawCount: number
-    instanceCount: number
-    instancedDrawCount: number
-
-    readonly maxTextureSize: number
-    readonly maxDrawBuffers: number
-
-    unbindFramebuffer: () => void
-    readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => void
-    readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
-    waitForGpuCommandsComplete: () => Promise<void>
-    waitForGpuCommandsCompleteSync: () => void
-    destroy: () => void
-}
-
-export function createContext(gl: GLRenderingContext): WebGLContext {
+function createExtensions(gl: GLRenderingContext): WebGLExtensions {
     const instancedArrays = getInstancedArrays(gl)
     if (instancedArrays === null) {
         throw new Error('Could not find support for "instanced_arrays"')
@@ -191,19 +194,230 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
     if (fragDepth === null) {
         console.log('Could not find support for "frag_depth"')
     }
+    const colorBufferFloat = getColorBufferFloat(gl)
+    if (colorBufferFloat === null) {
+        console.log('Could not find support for "color_buffer_float"')
+    }
+    const drawBuffers = getDrawBuffers(gl)
+    if (drawBuffers === null) {
+        console.log('Could not find support for "draw_buffers"')
+    }
+    const shaderTextureLod = getShaderTextureLod(gl)
+    if (shaderTextureLod === null) {
+        console.log('Could not find support for "shader_texture_lod"')
+    }
+    
+
+    return {
+        instancedArrays,
+        standardDerivatives,
+        blendMinMax,
+        textureFloat,
+        textureFloatLinear,
+        elementIndexUint,
+        vertexArrayObject,
+        fragDepth,
+        colorBufferFloat,
+        drawBuffers,
+        shaderTextureLod
+    }
+}
+
+export type WebGLStats = {
+    bufferCount: number
+    framebufferCount: number
+    renderbufferCount: number
+    textureCount: number
+    vaoCount: number
+
+    drawCount: number
+    instanceCount: number
+    instancedDrawCount: number
+}
+
+function createStats(): WebGLStats {
+    return {
+        bufferCount: 0,
+        framebufferCount: 0,
+        renderbufferCount: 0,
+        textureCount: 0,
+        vaoCount: 0,
+
+        drawCount: 0,
+        instanceCount: 0,
+        instancedDrawCount: 0,
+    }
+}
+
+export type WebGLState = {
+    currentProgramId: number
+    currentMaterialId: number
+    currentRenderItemId: number
+
+    enable: (cap: number) => void
+    disable: (cap: number) => void
+
+    frontFace: (mode: number) => void
+    cullFace: (mode: number) => void
+    depthMask: (flag: boolean) => void
+    colorMask: (red: boolean, green: boolean, blue: boolean, alpha: boolean) => void
+    clearColor: (red: number, green: number, blue: number, alpha: number) => void
+
+    blendFunc: (src: number, dst: number) => void
+    blendFuncSeparate: (srcRGB: number, dstRGB: number, srcAlpha: number, dstAlpha: number) => void
+
+    blendEquation: (mode: number) => void
+    blendEquationSeparate: (modeRGB: number, modeAlpha: number) => void
+}
+
+function createState(gl: GLRenderingContext): WebGLState {
+    const enabledCapabilities: { [k: number]: boolean } = {}
+
+    let currentFrontFace = gl.getParameter(gl.FRONT_FACE)
+    let currentCullFace = gl.getParameter(gl.CULL_FACE_MODE)
+    let currentDepthMask = gl.getParameter(gl.DEPTH_WRITEMASK)
+    let currentColorMask = gl.getParameter(gl.COLOR_WRITEMASK)
+    let currentClearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE)
+
+    let currentBlendSrcRGB = gl.getParameter(gl.BLEND_SRC_RGB)
+    let currentBlendDstRGB = gl.getParameter(gl.BLEND_DST_RGB)
+    let currentBlendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA)
+    let currentBlendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA)
+
+    let currentBlendEqRGB = gl.getParameter(gl.BLEND_EQUATION_RGB)
+    let currentBlendEqAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA)
+
+    return {
+        currentProgramId: -1,
+        currentMaterialId: -1,
+        currentRenderItemId: -1,
+
+        enable: (cap: number) => {
+            if (enabledCapabilities[cap] !== true ) {
+                gl.enable(cap)
+                enabledCapabilities[cap] = true
+            }
+        },
+        disable: (cap: number) => {
+            if (enabledCapabilities[cap] !== false) {
+                gl.disable(cap)
+                enabledCapabilities[cap] = false
+            }
+        },
+
+        frontFace: (mode: number) => {
+            if (mode !== currentFrontFace) {
+                gl.frontFace(mode)
+                currentFrontFace = mode
+            }
+        },
+        cullFace: (mode: number) => {
+            if (mode !== currentCullFace) {
+                gl.cullFace(mode)
+                currentCullFace = mode
+            }
+        },
+        depthMask: (flag: boolean) => {
+            if (flag !== currentDepthMask) {
+                gl.depthMask(flag)
+                currentDepthMask = flag
+            }
+        },
+        colorMask: (red: boolean, green: boolean, blue: boolean, alpha: boolean) => {
+            if (red !== currentColorMask[0] || green !== currentColorMask[1] || blue !== currentColorMask[2] || alpha !== currentColorMask[3])
+            gl.colorMask(red, green, blue, alpha)
+            currentColorMask[0] = red
+            currentColorMask[1] = green
+            currentColorMask[2] = blue
+            currentColorMask[3] = alpha
+        },
+        clearColor: (red: number, green: number, blue: number, alpha: number) => {
+            if (red !== currentClearColor[0] || green !== currentClearColor[1] || blue !== currentClearColor[2] || alpha !== currentClearColor[3])
+            gl.clearColor(red, green, blue, alpha)
+            currentClearColor[0] = red
+            currentClearColor[1] = green
+            currentClearColor[2] = blue
+            currentClearColor[3] = alpha
+        },
+
+        blendFunc: (src: number, dst: number) => {
+            if (src !== currentBlendSrcRGB || dst !== currentBlendDstRGB || src !== currentBlendSrcAlpha || dst !== currentBlendDstAlpha) {
+                gl.blendFunc(src, dst)
+                currentBlendSrcRGB = src
+                currentBlendDstRGB = dst
+                currentBlendSrcAlpha = src
+                currentBlendDstAlpha = dst
+            }
+        },
+        blendFuncSeparate: (srcRGB: number, dstRGB: number, srcAlpha: number, dstAlpha: number) => {
+            if (srcRGB !== currentBlendSrcRGB || dstRGB !== currentBlendDstRGB || srcAlpha !== currentBlendSrcAlpha || dstAlpha !== currentBlendDstAlpha) {
+                gl.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha)
+                currentBlendSrcRGB = srcRGB
+                currentBlendDstRGB = dstRGB
+                currentBlendSrcAlpha = srcAlpha
+                currentBlendDstAlpha = dstAlpha
+            }
+        },
+
+        blendEquation: (mode: number) => {
+            if (mode !== currentBlendEqRGB || mode !== currentBlendEqAlpha) {
+                gl.blendEquation(mode)
+                currentBlendEqRGB = mode
+                currentBlendEqAlpha = mode
+            }
+        },
+        blendEquationSeparate: (modeRGB: number, modeAlpha: number) => {
+            if (modeRGB !== currentBlendEqRGB || modeAlpha !== currentBlendEqAlpha) {
+                gl.blendEquationSeparate(modeRGB, modeAlpha)
+                currentBlendEqRGB = modeRGB
+                currentBlendEqAlpha = modeAlpha
+            }
+        }
+    }
+}
+
+/** A WebGL context object, including the rendering context, resource caches and counts */
+export interface WebGLContext {
+    readonly gl: GLRenderingContext
+    readonly isWebGL2: boolean
+    readonly pixelRatio: number
+
+    readonly extensions: WebGLExtensions
+    readonly state: WebGLState
+    readonly stats: WebGLStats
+
+    readonly shaderCache: ShaderCache
+    readonly programCache: ProgramCache
+    readonly framebufferCache: FramebufferCache
+
+    readonly maxTextureSize: number
+    readonly maxDrawBuffers: number
+
+    unbindFramebuffer: () => void
+    readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => void
+    readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
+    waitForGpuCommandsComplete: () => Promise<void>
+    waitForGpuCommandsCompleteSync: () => void
+    destroy: () => void
+}
+
+export function createContext(gl: GLRenderingContext): WebGLContext {
+    const extensions = createExtensions(gl)
+    const state = createState(gl)
+    const stats = createStats()
 
-    const shaderCache = createShaderCache()
-    const programCache = createProgramCache()
-    const framebufferCache = createFramebufferCache()
+    const shaderCache: ShaderCache = createShaderCache(gl)
+    const programCache: ProgramCache = createProgramCache(gl, state, extensions, shaderCache)
+    const framebufferCache: FramebufferCache = createFramebufferCache(gl, stats)
 
     const parameters = {
-        maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
-        maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) : 0,
-        maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS),
+        maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
+        maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) as number : 0,
+        maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number,
     }
 
-    if (parameters.maxVertexTextureImageUnits < 4) {
-        throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 4')
+    if (parameters.maxVertexTextureImageUnits < 8) {
+        throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 8')
     }
 
     let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
@@ -239,53 +453,32 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
         })
     } else {
         readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
-            gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)
+            readPixels(gl, x, y, width, height, buffer)
         }
     }
 
     return {
         gl,
         isWebGL2: isWebGL2(gl),
-        extensions: {
-            instancedArrays,
-            standardDerivatives,
-            blendMinMax,
-            textureFloat,
-            textureFloatLinear,
-            elementIndexUint,
-            vertexArrayObject,
-            fragDepth
+        get pixelRatio () {
+            // this can change during the lifetime of a rendering context, so need to re-obtain on access
+            return getPixelRatio()
         },
-        get pixelRatio () { return getPixelRatio() },
+
+        extensions,
+        state,
+        stats,
 
         shaderCache,
         programCache,
         framebufferCache,
 
-        currentProgramId: -1,
-
-        bufferCount: 0,
-        framebufferCount: 0,
-        renderbufferCount: 0,
-        textureCount: 0,
-        vaoCount: 0,
-
-        drawCount: 0,
-        instanceCount: 0,
-        instancedDrawCount: 0,
-
         get maxTextureSize () { return parameters.maxTextureSize },
         get maxDrawBuffers () { return parameters.maxDrawBuffers },
 
         unbindFramebuffer: () => unbindFramebuffer(gl),
-        readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
-            gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)
-            // TODO check is very expensive
-            // if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
-            //     gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)
-            // } else {
-            //     console.error('Reading pixels failed. Framebuffer not complete.')
-            // }
+        readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => {
+            readPixels(gl, x, y, width, height, buffer)
         },
         readPixelsAsync,
         waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
diff --git a/src/mol-gl/webgl/framebuffer.ts b/src/mol-gl/webgl/framebuffer.ts
index ef3439b723e5eb243281da27e83d78d0d74241b6..997c7412e50ec1d42822a9236507110ceee725c3 100644
--- a/src/mol-gl/webgl/framebuffer.ts
+++ b/src/mol-gl/webgl/framebuffer.ts
@@ -1,15 +1,41 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLContext } from './context'
+import { WebGLStats } from './context'
 import { idFactory } from 'mol-util/id-factory';
 import { ReferenceCache, createReferenceCache } from 'mol-util/reference-cache';
+import { GLRenderingContext, isWebGL2 } from './compat';
 
 const getNextFramebufferId = idFactory()
 
+function getFramebufferStatusDescription(gl: GLRenderingContext, status: number) {
+    switch (status) {
+        case gl.FRAMEBUFFER_COMPLETE: return 'complete'
+        case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: return 'incomplete attachment'
+        case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: return 'incomplete missing attachment'
+        case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: return 'incomplete dimensions'
+        case gl.FRAMEBUFFER_UNSUPPORTED: return 'unsupported'
+    }
+    if (isWebGL2(gl)) {
+        switch (status) {
+            case gl.FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: return 'incomplete multisample'
+            case gl.RENDERBUFFER_SAMPLES: return 'renderbuffer samples'
+        }
+    }
+    return 'unknown error'
+}
+
+export function checkFramebufferStatus(gl: GLRenderingContext) {
+    const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER)
+    if (status !== gl.FRAMEBUFFER_COMPLETE) {
+        const description = getFramebufferStatusDescription(gl, status)
+        throw new Error(`Framebuffer status: ${description}`)
+    }
+}
+
 export interface Framebuffer {
     readonly id: number
 
@@ -17,15 +43,14 @@ export interface Framebuffer {
     destroy: () => void
 }
 
-export function createFramebuffer (ctx: WebGLContext): Framebuffer {
-    const { gl } = ctx
+export function createFramebuffer (gl: GLRenderingContext, stats: WebGLStats): Framebuffer {
     const _framebuffer = gl.createFramebuffer()
     if (_framebuffer === null) {
         throw new Error('Could not create WebGL framebuffer')
     }
 
     let destroyed = false
-    ctx.framebufferCount += 1
+    stats.framebufferCount += 1
 
     return {
         id: getNextFramebufferId(),
@@ -35,17 +60,17 @@ export function createFramebuffer (ctx: WebGLContext): Framebuffer {
             if (destroyed) return
             gl.deleteFramebuffer(_framebuffer)
             destroyed = true
-            ctx.framebufferCount -= 1
+            stats.framebufferCount -= 1
         }
     }
 }
 
-export type FramebufferCache = ReferenceCache<Framebuffer, string, WebGLContext>
+export type FramebufferCache = ReferenceCache<Framebuffer, string>
 
-export function createFramebufferCache(): FramebufferCache {
+export function createFramebufferCache(gl: GLRenderingContext, stats: WebGLStats): FramebufferCache {
     return createReferenceCache(
         (name: string) => name,
-        (ctx: WebGLContext) => createFramebuffer(ctx),
+        () => createFramebuffer(gl, stats),
         (framebuffer: Framebuffer) => { framebuffer.destroy() }
     )
 }
\ No newline at end of file
diff --git a/src/mol-gl/webgl/program.ts b/src/mol-gl/webgl/program.ts
index 6cc476d5056cc0f0c76cd7de7c682cb362af0884..5997cc71165cb322693132650ee8412add4fe475 100644
--- a/src/mol-gl/webgl/program.ts
+++ b/src/mol-gl/webgl/program.ts
@@ -1,18 +1,21 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ShaderCode, DefineValues, addShaderDefines } from '../shader-code'
-import { WebGLContext } from './context';
-import { UniformValues, getUniformSetters } from './uniform';
-import { AttributeBuffers } from './buffer';
-import { Textures, TextureId } from './texture';
+import { WebGLExtensions, WebGLState } from './context';
+import { getUniformSetters, UniformsList, getUniformType } from './uniform';
+import { AttributeBuffers, getAttribType } from './buffer';
+import { TextureId, Textures } from './texture';
 import { createReferenceCache, ReferenceCache } from 'mol-util/reference-cache';
 import { idFactory } from 'mol-util/id-factory';
 import { RenderableSchema } from '../renderable/schema';
 import { hashFnv32a, hashString } from 'mol-data/util';
+import { isDebugMode } from 'mol-util/debug';
+import { GLRenderingContext } from './compat';
+import { ShaderCache } from './shader';
 
 const getNextProgramId = idFactory()
 
@@ -20,7 +23,7 @@ export interface Program {
     readonly id: number
 
     use: () => void
-    setUniforms: (uniformValues: UniformValues) => void
+    setUniforms: (uniformValues: UniformsList) => void
     bindAttributes: (attribueBuffers: AttributeBuffers) => void
     bindTextures: (textures: Textures) => void
 
@@ -29,8 +32,7 @@ export interface Program {
 
 type Locations = { [k: string]: number }
 
-function getLocations(ctx: WebGLContext, program: WebGLProgram, schema: RenderableSchema) {
-    const { gl } = ctx
+function getLocations(gl: GLRenderingContext, program: WebGLProgram, schema: RenderableSchema) {
     const locations: Locations = {}
     Object.keys(schema).forEach(k => {
         const spec = schema[k]
@@ -47,14 +49,76 @@ function getLocations(ctx: WebGLContext, program: WebGLProgram, schema: Renderab
     return locations
 }
 
+function checkActiveAttributes(gl: GLRenderingContext, program: WebGLProgram, schema: RenderableSchema) {
+    const attribCount = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
+    for (let i = 0; i < attribCount; ++i) {
+        const info = gl.getActiveAttrib(program, i);
+        if (info) {
+            const { name, type } = info
+            if (name.startsWith('__activeAttribute')) {
+                // name assigned by `gl.shim.ts`, ignore for checks
+                continue
+            }
+            const spec = schema[name]
+            if (spec === undefined) {
+                throw new Error(`missing 'uniform' or 'texture' with name '${name}' in schema`)
+            }
+            if (spec.type !== 'attribute') {
+                throw new Error(`'${name}' must be of type 'attribute' but is '${spec.type}'`)
+            }
+            const attribType = getAttribType(gl, spec.kind, spec.itemSize)
+            if (attribType !== type) {
+                throw new Error(`unexpected attribute type for ${name}`)
+            }
+        }
+    }
+}
+
+function checkActiveUniforms(gl: GLRenderingContext, program: WebGLProgram, schema: RenderableSchema) {
+    const attribCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
+    for (let i = 0; i < attribCount; ++i) {
+        const info = gl.getActiveUniform(program, i);
+        if (info) {
+            const { name, type } = info
+            if (name.startsWith('__activeUniform')) {
+                // name assigned by `gl.shim.ts`, ignore for checks
+                continue
+            }
+            const spec = schema[name]
+            if (spec === undefined) {
+                throw new Error(`missing 'uniform' or 'texture' with name '${name}' in schema`)
+            }
+            if (spec.type === 'uniform') {
+                const uniformType = getUniformType(gl, spec.kind)
+                if (uniformType !== type) {
+                    throw new Error(`unexpected uniform type for ${name}`)
+                }
+            } else if (spec.type === 'texture') {
+                if (spec.kind === 'image-float32' || spec.kind === 'image-uint8') {
+                    if (type !== gl.SAMPLER_2D) {
+                        throw new Error(`unexpected sampler type for '${name}'`)
+                    }
+                } else if (spec.kind === 'volume-float32' || spec.kind === 'volume-uint8') {
+                    if (type !== (gl as WebGL2RenderingContext).SAMPLER_3D) {
+                        throw new Error(`unexpected sampler type for '${name}'`)
+                    }
+                } else {
+                    // TODO
+                }
+            } else {
+                throw new Error(`'${name}' must be of type 'uniform' or 'texture' but is '${spec.type}'`)
+            }
+        }
+    }
+}
+
 export interface ProgramProps {
     defineValues: DefineValues,
     shaderCode: ShaderCode,
     schema: RenderableSchema
 }
 
-export function createProgram(ctx: WebGLContext, props: ProgramProps): Program {
-    const { gl, shaderCache } = ctx
+export function createProgram(gl: GLRenderingContext, state: WebGLState, extensions: WebGLExtensions, shaderCache: ShaderCache, props: ProgramProps): Program {
     const { defineValues, shaderCode: _shaderCode, schema } = props
 
     const program = gl.createProgram()
@@ -63,20 +127,29 @@ export function createProgram(ctx: WebGLContext, props: ProgramProps): Program {
     }
     const programId = getNextProgramId()
 
-    const shaderCode = addShaderDefines(ctx, defineValues, _shaderCode)
-    const vertShaderRef = shaderCache.get(ctx, { type: 'vert', source: shaderCode.vert })
-    const fragShaderRef = shaderCache.get(ctx, { type: 'frag', source: shaderCode.frag })
+    const shaderCode = addShaderDefines(gl, extensions, defineValues, _shaderCode)
+    const vertShaderRef = shaderCache.get({ type: 'vert', source: shaderCode.vert })
+    const fragShaderRef = shaderCache.get({ type: 'frag', source: shaderCode.frag })
 
     vertShaderRef.value.attach(program)
     fragShaderRef.value.attach(program)
     gl.linkProgram(program)
-    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
-        throw new Error(`Could not compile WebGL program. \n\n${gl.getProgramInfoLog(program)}`);
+    if (isDebugMode) {
+        // no-op in FF on Mac, see https://bugzilla.mozilla.org/show_bug.cgi?id=1284425
+        // gl.validateProgram(program)
+        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+            throw new Error(`Could not compile WebGL program. \n\n${gl.getProgramInfoLog(program)}`);
+        }
     }
 
-    const locations = getLocations(ctx, program, schema)
+    const locations = getLocations(gl, program, schema)
     const uniformSetters = getUniformSetters(schema)
 
+    if (isDebugMode) {
+        checkActiveAttributes(gl, program, schema)
+        checkActiveUniforms(gl, program, schema)
+    }
+
     let destroyed = false
 
     return {
@@ -84,33 +157,35 @@ export function createProgram(ctx: WebGLContext, props: ProgramProps): Program {
 
         use: () => {
             // console.log('use', programId)
-            ctx.currentProgramId = programId
+            state.currentProgramId = programId
             gl.useProgram(program)
         },
-        setUniforms: (uniformValues: UniformValues) => {
-            const uniformKeys = Object.keys(uniformValues)
-            for (let i = 0, il = uniformKeys.length; i < il; ++i) {
-                const k = uniformKeys[i]
-                const l = locations[k]
-                const v = uniformValues[k]
-                if (v) uniformSetters[k](gl, l, v.ref.value)
+        setUniforms: (uniformValues: UniformsList) => {
+            for (let i = 0, il = uniformValues.length; i < il; ++i) {
+                const [k, v] = uniformValues[i]
+                if (v) {
+                    const l = locations[k]
+                    if (l !== null) uniformSetters[k](gl, l, v.ref.value)
+                }
             }
         },
         bindAttributes: (attribueBuffers: AttributeBuffers) => {
-            const attributeKeys = Object.keys(attribueBuffers)
-            for (let i = 0, il = attributeKeys.length; i < il; ++i) {
-                const k = attributeKeys[i]
+            for (let i = 0, il = attribueBuffers.length; i < il; ++i) {
+                const [k, buffer] = attribueBuffers[i]
                 const l = locations[k]
-                if (l !== -1) attribueBuffers[k].bind(l)
+                if (l !== -1) buffer.bind(l)
             }
         },
         bindTextures: (textures: Textures) => {
-            const textureKeys = Object.keys(textures)
-            for (let i = 0, il = textureKeys.length; i < il; ++i) {
-                const k = textureKeys[i]
+            for (let i = 0, il = textures.length; i < il; ++i) {
+                const [k, texture] = textures[i]
                 const l = locations[k]
-                textures[k].bind(i as TextureId)
-                uniformSetters[k](gl, l, i as TextureId)
+                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)
+                }
             }
         },
 
@@ -124,14 +199,14 @@ export function createProgram(ctx: WebGLContext, props: ProgramProps): Program {
     }
 }
 
-export type ProgramCache = ReferenceCache<Program, ProgramProps, WebGLContext>
+export type ProgramCache = ReferenceCache<Program, ProgramProps>
 
 function defineValueHash(v: boolean | number | string): number {
     return typeof v === 'boolean' ? (v ? 1 : 0) :
         typeof v === 'number' ? v : hashString(v)
 }
 
-export function createProgramCache(): ProgramCache {
+export function createProgramCache(gl: GLRenderingContext, state: WebGLState, extensions: WebGLExtensions, shaderCache: ShaderCache): ProgramCache {
     return createReferenceCache(
         (props: ProgramProps) => {
             const array = [ props.shaderCode.id ]
@@ -141,7 +216,7 @@ export function createProgramCache(): ProgramCache {
             })
             return hashFnv32a(array).toString()
         },
-        (ctx: WebGLContext, props: ProgramProps) => createProgram(ctx, props),
+        (props: ProgramProps) => createProgram(gl, state, extensions, shaderCache, props),
         (program: Program) => { program.destroy() }
     )
 }
\ No newline at end of file
diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts
index fad8f866e25789f402f9a8ef880d0d6fbeb0441a..b516fb8099172ed50c21646934ab016174f4a947 100644
--- a/src/mol-gl/webgl/render-item.ts
+++ b/src/mol-gl/webgl/render-item.ts
@@ -1,20 +1,22 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, ArrayKind } from './buffer';
-import { createTextures } from './texture';
-import { WebGLContext } from './context';
+import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, AttributeKind } from './buffer';
+import { createTextures, Texture } from './texture';
+import { WebGLContext, checkError } from './context';
 import { ShaderCode } from '../shader-code';
 import { Program } from './program';
-import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values, splitKeys } from '../renderable/schema';
+import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values } from '../renderable/schema';
 import { idFactory } from 'mol-util/id-factory';
 import { deleteVertexArray, createVertexArray } from './vertex-array';
 import { ValueCell } from 'mol-util';
 import { ReferenceItem } from 'mol-util/reference-cache';
 import { TextureImage, TextureVolume } from 'mol-gl/renderable/util';
+import { checkFramebufferStatus } from './framebuffer';
+import { isDebugMode } from 'mol-util/debug';
 
 const getNextRenderItemId = idFactory()
 
@@ -33,22 +35,34 @@ export function getDrawMode(ctx: WebGLContext, drawMode: DrawMode) {
     }
 }
 
-export interface RenderItem {
+export interface RenderItem<T extends string> {
     readonly id: number
-    getProgram: (variant: RenderVariant) => Program
+    readonly materialId: number
+    getProgram: (variant: T) => Program
 
-    render: (variant: RenderVariant) => void
+    render: (variant: T) => void
     update: () => Readonly<ValueChanges>
     destroy: () => void
 }
 
-const RenderVariantDefines = {
+//
+
+const GraphicsRenderVariantDefines = {
     'draw': {},
     'pickObject': { dColorType: ValueCell.create('objectPicking') },
     'pickInstance': { dColorType: ValueCell.create('instancePicking') },
     'pickGroup': { dColorType: ValueCell.create('groupPicking') }
 }
-export type RenderVariant = keyof typeof RenderVariantDefines
+export type GraphicsRenderVariant = keyof typeof GraphicsRenderVariantDefines
+
+const ComputeRenderVariantDefines = {
+    'compute': {},
+}
+export type ComputeRenderVariant = keyof typeof ComputeRenderVariantDefines
+
+type RenderVariantDefines = typeof GraphicsRenderVariantDefines | typeof ComputeRenderVariantDefines
+
+//
 
 type ProgramVariants = { [k: string]: ReferenceItem<Program> }
 type VertexArrayVariants = { [k: string]: WebGLVertexArrayObjectOES | null }
@@ -58,7 +72,6 @@ interface ValueChanges {
     defines: boolean
     elements: boolean
     textures: boolean
-    uniforms: boolean
 }
 function createValueChanges() {
     return {
@@ -66,7 +79,6 @@ function createValueChanges() {
         defines: false,
         elements: false,
         textures: false,
-        uniforms: false,
     }
 }
 function resetValueChanges(valueChanges: ValueChanges) {
@@ -74,31 +86,44 @@ function resetValueChanges(valueChanges: ValueChanges) {
     valueChanges.defines = false
     valueChanges.elements = false
     valueChanges.textures = false
-    valueChanges.uniforms = false
 }
 
-// TODO make `RenderVariantDefines` a parameter for `createRenderItem`
+//
+
+export type GraphicsRenderItem = RenderItem<keyof typeof GraphicsRenderVariantDefines & string>
+export function createGraphicsRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId: number) {
+    return createRenderItem(ctx, drawMode, shaderCode, schema, values, materialId, GraphicsRenderVariantDefines)
+}
+
+export type ComputeRenderItem = RenderItem<keyof typeof ComputeRenderVariantDefines & string>
+export function createComputeRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId = -1) {
+    return createRenderItem(ctx, drawMode, shaderCode, schema, values, materialId, ComputeRenderVariantDefines)
+}
 
 /**
  * Creates a render item
  *
  * - assumes that `values.drawCount` and `values.instanceCount` exist
  */
-export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues): RenderItem {
+export function createRenderItem<T extends RenderVariantDefines, S extends keyof T & string>(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId: number, renderVariantDefines: T): RenderItem<S> {
     const id = getNextRenderItemId()
-    const { programCache } = ctx
+    const { stats, state, programCache } = ctx
     const { instancedArrays, vertexArrayObject } = ctx.extensions
 
-    const { attributeValues, defineValues, textureValues, uniformValues } = splitValues(schema, values)
-    const { attributeKeys, defineKeys, textureKeys } = splitKeys(schema)
+    const { attributeValues, defineValues, textureValues, uniformValues, materialUniformValues } = splitValues(schema, values)
+
+    const uniformValueEntries = Object.entries(uniformValues)
+    const materialUniformValueEntries = Object.entries(materialUniformValues)
+    const defineValueEntries = Object.entries(defineValues)
+
     const versions = getValueVersions(values)
 
     const glDrawMode = getDrawMode(ctx, drawMode)
 
     const programs: ProgramVariants = {}
-    Object.keys(RenderVariantDefines).forEach(k => {
-        const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k]
-        programs[k] = programCache.get(ctx, {
+    Object.keys(renderVariantDefines).forEach(k => {
+        const variantDefineValues: Values<RenderableSchema> = (renderVariantDefines as any)[k]
+        programs[k] = programCache.get({
             defineValues: { ...defineValues, ...variantDefineValues },
             shaderCode,
             schema
@@ -115,51 +140,80 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
     }
 
     const vertexArrays: VertexArrayVariants = {}
-    Object.keys(RenderVariantDefines).forEach(k => {
+    Object.keys(renderVariantDefines).forEach(k => {
         vertexArrays[k] = createVertexArray(ctx, programs[k].value, attributeBuffers, elementsBuffer)
     })
 
     let drawCount = values.drawCount.ref.value
     let instanceCount = values.instanceCount.ref.value
 
-    ctx.drawCount += drawCount
-    ctx.instanceCount += instanceCount
-    ctx.instancedDrawCount += instanceCount * drawCount
+    stats.drawCount += drawCount
+    stats.instanceCount += instanceCount
+    stats.instancedDrawCount += instanceCount * drawCount
 
     const valueChanges = createValueChanges()
 
     let destroyed = false
+    let currentProgramId = -1
 
     return {
         id,
-        getProgram: (variant: RenderVariant) => programs[variant].value,
+        materialId,
+        getProgram: (variant: S) => programs[variant].value,
 
-        render: (variant: RenderVariant) => {
+        render: (variant: S) => {
             if (drawCount === 0 || instanceCount === 0) return
             const program = programs[variant].value
-            const vertexArray = vertexArrays[variant]
-            program.setUniforms(uniformValues)
-            if (vertexArrayObject && vertexArray) {
-                vertexArrayObject.bindVertexArray(vertexArray)
-                // need to bind elements buffer explicitly since it is not always recorded in the VAO
-                if (elementsBuffer) elementsBuffer.bind()
+            if (program.id === currentProgramId && state.currentRenderItemId === id) {
+                program.setUniforms(uniformValueEntries)
+                program.bindTextures(textures)
             } else {
-                if (elementsBuffer) elementsBuffer.bind()
-                program.bindAttributes(attributeBuffers)
+                const vertexArray = vertexArrays[variant]
+                if (program.id !== state.currentProgramId || program.id !== currentProgramId ||
+                    materialId === -1 || materialId !== state.currentMaterialId
+                ) {
+                    // console.log('program.id changed or materialId changed/-1', materialId)
+                    if (program.id !== state.currentProgramId) program.use()
+                    program.setUniforms(materialUniformValueEntries)
+                    state.currentMaterialId = materialId
+                    currentProgramId = program.id
+                }
+                program.setUniforms(uniformValueEntries)
+                program.bindTextures(textures)
+                if (vertexArrayObject && vertexArray) {
+                    vertexArrayObject.bindVertexArray(vertexArray)
+                    // need to bind elements buffer explicitly since it is not always recorded in the VAO
+                    if (elementsBuffer) elementsBuffer.bind()
+                } else {
+                    if (elementsBuffer) elementsBuffer.bind()
+                    program.bindAttributes(attributeBuffers)
+                }
+                state.currentRenderItemId = id
+            }
+            if (isDebugMode) {
+                checkFramebufferStatus(ctx.gl)
             }
-            program.bindTextures(textures)
             if (elementsBuffer) {
                 instancedArrays.drawElementsInstanced(glDrawMode, drawCount, elementsBuffer._dataType, 0, instanceCount);
             } else {
                 instancedArrays.drawArraysInstanced(glDrawMode, 0, drawCount, instanceCount)
             }
+            if (isDebugMode) {
+                try {
+                    checkError(ctx.gl)
+                } catch (e) {
+                    // console.log('shaderCode', shaderCode)
+                    // console.log('schema', schema)
+                    // console.log('attributeBuffers', attributeBuffers)
+                    throw new Error(`Error rendering item id ${id}: '${e}'`)
+                }
+            }
         },
         update: () => {
             resetValueChanges(valueChanges)
 
-            for (let i = 0, il = defineKeys.length; i < il; ++i) {
-                const k = defineKeys[i]
-                const value = defineValues[k]
+            for (let i = 0, il = defineValueEntries.length; i < il; ++i) {
+                const [k, value] = defineValueEntries[i]
                 if (value.ref.version !== versions[k]) {
                     // console.log('define version changed', k)
                     valueChanges.defines = true
@@ -169,10 +223,10 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
 
             if (valueChanges.defines) {
                 // console.log('some defines changed, need to rebuild programs')
-                Object.keys(RenderVariantDefines).forEach(k => {
-                    const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k]
+                Object.keys(renderVariantDefines).forEach(k => {
+                    const variantDefineValues: Values<RenderableSchema> = (renderVariantDefines as any)[k]
                     programs[k].free()
-                    programs[k] = programCache.get(ctx, {
+                    programs[k] = programCache.get({
                         defineValues: { ...defineValues, ...variantDefineValues },
                         shaderCode,
                         schema
@@ -182,32 +236,31 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
 
             if (values.drawCount.ref.version !== versions.drawCount) {
                 // console.log('drawCount version changed')
-                ctx.drawCount += values.drawCount.ref.value - drawCount
-                ctx.instancedDrawCount += instanceCount * values.drawCount.ref.value - instanceCount * drawCount
+                stats.drawCount += values.drawCount.ref.value - drawCount
+                stats.instancedDrawCount += instanceCount * values.drawCount.ref.value - instanceCount * drawCount
                 drawCount = values.drawCount.ref.value
                 versions.drawCount = values.drawCount.ref.version
             }
             if (values.instanceCount.ref.version !== versions.instanceCount) {
                 // console.log('instanceCount version changed')
-                ctx.instanceCount += values.instanceCount.ref.value - instanceCount
-                ctx.instancedDrawCount += values.instanceCount.ref.value * drawCount - instanceCount * drawCount
+                stats.instanceCount += values.instanceCount.ref.value - instanceCount
+                stats.instancedDrawCount += values.instanceCount.ref.value * drawCount - instanceCount * drawCount
                 instanceCount = values.instanceCount.ref.value
                 versions.instanceCount = values.instanceCount.ref.version
             }
 
-            for (let i = 0, il = attributeKeys.length; i < il; ++i) {
-                const k = attributeKeys[i]
+            for (let i = 0, il = attributeBuffers.length; i < il; ++i) {
+                const [k, buffer] = attributeBuffers[i]
                 const value = attributeValues[k]
                 if (value.ref.version !== versions[k]) {
-                    const buffer = attributeBuffers[k]
                     if (buffer.length >= value.ref.value.length) {
                         // console.log('attribute array large enough to update', k, value.ref.id, value.ref.version)
                         buffer.updateData(value.ref.value)
                     } else {
                         // console.log('attribute array to small, need to create new attribute', k, value.ref.id, value.ref.version)
                         buffer.destroy()
-                        const { itemSize, divisor } = schema[k] as AttributeSpec<ArrayKind>
-                        attributeBuffers[k] = createAttributeBuffer(ctx, value.ref.value, itemSize, divisor)
+                        const { itemSize, divisor } = schema[k] as AttributeSpec<AttributeKind>
+                        attributeBuffers[i][1] = createAttributeBuffer(ctx, value.ref.value, itemSize, divisor)
                         valueChanges.attributes = true
                     }
                     versions[k] = value.ref.version
@@ -231,7 +284,7 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
                 // console.log('program/defines or buffers changed, update vaos')
                 const { vertexArrayObject } = ctx.extensions
                 if (vertexArrayObject) {
-                    Object.keys(RenderVariantDefines).forEach(k => {
+                    Object.keys(renderVariantDefines).forEach(k => {
                         vertexArrayObject.bindVertexArray(vertexArrays[k])
                         if (elementsBuffer && (valueChanges.defines || valueChanges.elements)) {
                             elementsBuffer.bind()
@@ -244,16 +297,18 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
                 }
             }
 
-            for (let i = 0, il = textureKeys.length; i < il; ++i) {
-                const k = textureKeys[i]
+            for (let i = 0, il = textures.length; i < il; ++i) {
+                const [k, texture] = textures[i]
                 const value = textureValues[k]
                 if (value.ref.version !== versions[k]) {
                     // update of textures with kind 'texture' is done externally
                     if (schema[k].kind !== 'texture') {
                         // console.log('texture version changed, uploading image', k)
-                        textures[k].load(value.ref.value as TextureImage<any> | TextureVolume<any>)
+                        texture.load(value.ref.value as TextureImage<any> | TextureVolume<any>)
                         versions[k] = value.ref.version
                         valueChanges.textures = true
+                    } else {
+                        textures[i][1] = value.ref.value as Texture
                     }
                 }
             }
@@ -262,17 +317,17 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
         },
         destroy: () => {
             if (!destroyed) {
-                Object.keys(RenderVariantDefines).forEach(k => {
+                Object.keys(renderVariantDefines).forEach(k => {
                     programs[k].free()
                     deleteVertexArray(ctx, vertexArrays[k])
                 })
-                Object.keys(textures).forEach(k => {
+                textures.forEach(([k, texture]) => {
                     // lifetime of textures with kind 'texture' is defined externally
                     if (schema[k].kind !== 'texture') {
-                        textures[k].destroy()
+                        texture.destroy()
                     }
                 })
-                Object.keys(attributeBuffers).forEach(k => attributeBuffers[k].destroy())
+                attributeBuffers.forEach(([_, buffer]) => buffer.destroy())
                 if (elementsBuffer) elementsBuffer.destroy()
                 destroyed = true
             }
diff --git a/src/mol-gl/webgl/render-target.ts b/src/mol-gl/webgl/render-target.ts
index d26ec40b136d87c9669b32d2ddfca8ee55c7285c..01f623ecc8658f02fbf14b913a23a8332197929a 100644
--- a/src/mol-gl/webgl/render-target.ts
+++ b/src/mol-gl/webgl/render-target.ts
@@ -31,7 +31,7 @@ export interface RenderTarget {
 }
 
 export function createRenderTarget (ctx: WebGLContext, _width: number, _height: number): RenderTarget {
-    const { gl } = ctx
+    const { gl, stats } = ctx
 
     const image: Mutable<TextureImage<Uint8Array>> = {
         array: new Uint8Array(_width * _height * 4),
@@ -42,7 +42,7 @@ export function createRenderTarget (ctx: WebGLContext, _width: number, _height:
     const targetTexture = createTexture(ctx, 'image-uint8', 'rgba', 'ubyte', 'linear')
     targetTexture.load(image)
 
-    const framebuffer = createFramebuffer(ctx)
+    const framebuffer = createFramebuffer(gl, stats)
 
     // attach the texture as the first color attachment
     targetTexture.attachFramebuffer(framebuffer, 'color0')
diff --git a/src/mol-gl/webgl/renderbuffer.ts b/src/mol-gl/webgl/renderbuffer.ts
index 8cc41d487b06dcdcfc1adc8f5bea866f5e960a32..86eea102485f89f7b01a6910dc572d42b1b99223 100644
--- a/src/mol-gl/webgl/renderbuffer.ts
+++ b/src/mol-gl/webgl/renderbuffer.ts
@@ -42,7 +42,7 @@ export interface Renderbuffer {
 }
 
 export function createRenderbuffer (ctx: WebGLContext, format: RenderbufferFormat, attachment: RenderbufferAttachment, _width: number, _height: number): Renderbuffer {
-    const { gl } = ctx
+    const { gl, stats } = ctx
     const _renderbuffer = gl.createRenderbuffer()
     if (_renderbuffer === null) {
         throw new Error('Could not create WebGL renderbuffer')
@@ -57,7 +57,7 @@ export function createRenderbuffer (ctx: WebGLContext, format: RenderbufferForma
     gl.framebufferRenderbuffer(gl.FRAMEBUFFER, _attachment, gl.RENDERBUFFER, _renderbuffer)
 
     let destroyed = false
-    ctx.renderbufferCount += 1
+    stats.renderbufferCount += 1
 
     return {
         id: getNextRenderbufferId(),
@@ -72,7 +72,7 @@ export function createRenderbuffer (ctx: WebGLContext, format: RenderbufferForma
             if (destroyed) return
             gl.deleteRenderbuffer(_renderbuffer)
             destroyed = true
-            ctx.framebufferCount -= 1
+            stats.framebufferCount -= 1
         }
     }
 }
\ No newline at end of file
diff --git a/src/mol-gl/webgl/shader.ts b/src/mol-gl/webgl/shader.ts
index a209cd017546234b711b4b3069275ed5c015417a..546ba0328b185ddbf452181fadea06887112e9ed 100644
--- a/src/mol-gl/webgl/shader.ts
+++ b/src/mol-gl/webgl/shader.ts
@@ -1,12 +1,13 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { createReferenceCache, ReferenceCache } from 'mol-util/reference-cache';
-import { WebGLContext } from './context';
 import { idFactory } from 'mol-util/id-factory';
+import { GLRenderingContext } from './compat';
+import { isDebugMode } from 'mol-util/debug';
 
 const getNextShaderId = idFactory()
 
@@ -26,8 +27,7 @@ export interface Shader {
     destroy: () => void
 }
 
-function createShader(ctx: WebGLContext, props: ShaderProps): Shader {
-    const { gl } = ctx
+function createShader(gl: GLRenderingContext, props: ShaderProps): Shader {
     const { type, source } = props
 
     const shader = gl.createShader(type === 'vert' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER)
@@ -38,7 +38,7 @@ function createShader(ctx: WebGLContext, props: ShaderProps): Shader {
     gl.shaderSource(shader, source)
     gl.compileShader(shader)
 
-    if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) === false) {
+    if (isDebugMode && gl.getShaderParameter(shader, gl.COMPILE_STATUS) === false) {
         console.warn(`'${type}' shader info log '${gl.getShaderInfoLog(shader)}'\n${addLineNumbers(source)}`)
         throw new Error(`Error compiling ${type} shader`)
     }
@@ -54,12 +54,12 @@ function createShader(ctx: WebGLContext, props: ShaderProps): Shader {
     }
 }
 
-export type ShaderCache = ReferenceCache<Shader, ShaderProps, WebGLContext>
+export type ShaderCache = ReferenceCache<Shader, ShaderProps>
 
-export function createShaderCache(): ShaderCache {
+export function createShaderCache(gl: GLRenderingContext): ShaderCache {
     return createReferenceCache(
         (props: ShaderProps) => JSON.stringify(props),
-        (ctx: WebGLContext, props: ShaderProps) => createShader(ctx, props),
+        (props: ShaderProps) => createShader(gl, props),
         (shader: Shader) => { shader.destroy() }
     )
 }
\ No newline at end of file
diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts
index c5128cc4ad2ed9eb3faee3ba3e2bf887ba858adc..20cc12853ad6a7475e1359899da9d6757d7ad9a3 100644
--- a/src/mol-gl/webgl/texture.ts
+++ b/src/mol-gl/webgl/texture.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -42,40 +42,42 @@ export function getTarget(ctx: WebGLContext, kind: TextureKind): number {
             case 'volume-float32': return gl.TEXTURE_3D
         }
     }
-    throw new Error('unknown texture kind')
+    throw new Error(`unknown texture kind '${kind}'`)
 }
 
-export function getFormat(ctx: WebGLContext, format: TextureFormat): number {
+export function getFormat(ctx: WebGLContext, format: TextureFormat, type: TextureType): number {
     const { gl } = ctx
     switch (format) {
-        case 'alpha': return gl.ALPHA
+        case 'alpha':
+            if (isWebGL2 && type === 'float') return (gl as WebGL2RenderingContext).RED
+            else return gl.ALPHA
         case 'rgb': return gl.RGB
         case 'rgba': return gl.RGBA
     }
 }
 
 export function getInternalFormat(ctx: WebGLContext, format: TextureFormat, type: TextureType): number {
-    const { gl, isWebGL2 } = ctx
-    if (isWebGL2) {
+    const { gl } = ctx
+    if (isWebGL2(gl)) {
         switch (format) {
             case 'alpha':
                 switch (type) {
                     case 'ubyte': return gl.ALPHA
-                    case 'float': throw new Error('invalid format/type combination alpha/float')
+                    case 'float': return gl.R32F
                 }
             case 'rgb':
                 switch (type) {
                     case 'ubyte': return gl.RGB
-                    case 'float': return (gl as WebGL2RenderingContext).RGB32F
+                    case 'float': return gl.RGB32F
                 }
             case 'rgba':
                 switch (type) {
                     case 'ubyte': return gl.RGBA
-                    case 'float': return (gl as WebGL2RenderingContext).RGBA32F
+                    case 'float': return gl.RGBA32F
                 }
         }
     }
-    return getFormat(ctx, format)
+    return getFormat(ctx, format, type)
 }
 
 export function getType(ctx: WebGLContext, type: TextureType): number {
@@ -95,21 +97,21 @@ export function getFilter(ctx: WebGLContext, type: TextureFilter): number {
 }
 
 export function getAttachment(ctx: WebGLContext, attachment: TextureAttachment): number {
-    const { gl } = ctx
+    const { gl, extensions } = ctx
     switch (attachment) {
         case 'depth': return gl.DEPTH_ATTACHMENT
         case 'stencil': return gl.STENCIL_ATTACHMENT
         case 'color0': case 0: return gl.COLOR_ATTACHMENT0
     }
-    if (isWebGL2(gl)) {
+    if (extensions.drawBuffers) {
         switch (attachment) {
-            case 'color1': case 1: return gl.COLOR_ATTACHMENT1
-            case 'color2': case 2: return gl.COLOR_ATTACHMENT2
-            case 'color3': case 3: return gl.COLOR_ATTACHMENT3
-            case 'color4': case 4: return gl.COLOR_ATTACHMENT4
-            case 'color5': case 5: return gl.COLOR_ATTACHMENT5
-            case 'color6': case 6: return gl.COLOR_ATTACHMENT6
-            case 'color7': case 7: return gl.COLOR_ATTACHMENT7
+            case 'color1': case 1: return extensions.drawBuffers.COLOR_ATTACHMENT1
+            case 'color2': case 2: return extensions.drawBuffers.COLOR_ATTACHMENT2
+            case 'color3': case 3: return extensions.drawBuffers.COLOR_ATTACHMENT3
+            case 'color4': case 4: return extensions.drawBuffers.COLOR_ATTACHMENT4
+            case 'color5': case 5: return extensions.drawBuffers.COLOR_ATTACHMENT5
+            case 'color6': case 6: return extensions.drawBuffers.COLOR_ATTACHMENT6
+            case 'color7': case 7: return extensions.drawBuffers.COLOR_ATTACHMENT7
         }
     }
     throw new Error('unknown texture attachment')
@@ -139,19 +141,24 @@ export interface Texture {
 export type TextureId = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
 
 export type TextureValues = { [k: string]: ValueCell<TextureValueType> }
-export type Textures = { [k: string]: Texture }
+export type Textures = [string, Texture][]
 
 export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: TextureFormat, _type: TextureType, _filter: TextureFilter): Texture {
     const id = getNextTextureId()
-    const { gl } = ctx
+    const { gl, stats } = ctx
     const texture = gl.createTexture()
     if (texture === null) {
         throw new Error('Could not create WebGL texture')
     }
 
+    // check texture kind and type compatability
+    if ((kind.endsWith('float32') && _type !== 'float') || kind.endsWith('uint8') && _type !== 'ubyte') {
+        throw new Error(`texture kind '${kind}' and type '${_type}' are incompatible`)
+    }
+
     const target = getTarget(ctx, kind)
     const filter = getFilter(ctx, _filter)
-    const format = getFormat(ctx, _format)
+    const format = getFormat(ctx, _format, _type)
     const internalFormat = getInternalFormat(ctx, _format, _type)
     const type = getType(ctx, _type)
 
@@ -166,7 +173,7 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
     let width = 0, height = 0, depth = 0
 
     let destroyed = false
-    ctx.textureCount += 1
+    stats.textureCount += 1
 
     return {
         id,
@@ -184,8 +191,8 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
             gl.bindTexture(target, texture)
             if (target === gl.TEXTURE_2D) {
                 gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, null)
-            } else if (target === (gl as WebGL2RenderingContext).TEXTURE_3D && depth !== undefined) {
-                (gl as WebGL2RenderingContext).texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null)
+            } else if (isWebGL2(gl) && target === gl.TEXTURE_3D && depth !== undefined) {
+                gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null)
             } else {
                 throw new Error('unknown texture target')
             }
@@ -200,10 +207,10 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
                 const { array, width: _width, height: _height } = data as TextureImage<any>
                 width = _width, height = _height;
                 gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, array)
-            } else if (target === (gl as WebGL2RenderingContext).TEXTURE_3D) {
+            } else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
                 const { array, width: _width, height: _height, depth: _depth } = data as TextureVolume<any>
-                width = _width, height = _height, depth = _depth;
-                (gl as WebGL2RenderingContext).texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, array)
+                width = _width, height = _height, depth = _depth
+                gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, array)
             } else {
                 throw new Error('unknown texture target')
             }
@@ -238,22 +245,22 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
             if (destroyed) return
             gl.deleteTexture(texture)
             destroyed = true
-            ctx.textureCount -= 1
+            stats.textureCount -= 1
         }
     }
 }
 
 export function createTextures(ctx: WebGLContext, schema: RenderableSchema, values: TextureValues) {
-    const textures: Textures = {}
-    Object.keys(schema).forEach((k, i) => {
+    const textures: Textures = []
+    Object.keys(schema).forEach(k => {
         const spec = schema[k]
         if (spec.type === 'texture') {
             if (spec.kind === 'texture') {
-                textures[k] = values[k].ref.value as Texture
+                textures[textures.length] = [k, values[k].ref.value as Texture]
             } else {
                 const texture = createTexture(ctx, spec.kind, spec.format, spec.dataType, spec.filter)
                 texture.load(values[k].ref.value as TextureImage<any> | TextureVolume<any>)
-                textures[k] = texture
+                textures[textures.length] = [k, texture]
             }
         }
     })
diff --git a/src/mol-gl/webgl/uniform.ts b/src/mol-gl/webgl/uniform.ts
index a979a64fee9228f01f052d39aa94f067a2289807..bdc7bccc1c2ed3dd7e070ceca149d7e75e780134 100644
--- a/src/mol-gl/webgl/uniform.ts
+++ b/src/mol-gl/webgl/uniform.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -23,6 +23,20 @@ export type UniformKind = keyof UniformKindValue
 export type UniformType = number | Vec2 | Vec3 | Vec4 | Mat3 | Mat4
 
 export type UniformValues = { [k: string]: ValueCell<UniformType> }
+export type UniformsList = [string, ValueCell<UniformType>][]
+
+export function getUniformType(gl: GLRenderingContext, kind: UniformKind) {
+    switch (kind) {
+        case 'f': return gl.FLOAT
+        case 'i': return gl.INT
+        case 'v2': return gl.FLOAT_VEC2
+        case 'v3': return gl.FLOAT_VEC3
+        case 'v4': return gl.FLOAT_VEC4
+        case 'm3': return gl.FLOAT_MAT3
+        case 'm4': return gl.FLOAT_MAT4
+        default: console.error(`unknown uniform kind '${kind}'`)
+    }
+}
 
 export function setUniform(gl: GLRenderingContext, location: WebGLUniformLocation | null, kind: UniformKind, value: any) {
     switch (kind) {
diff --git a/src/mol-gl/webgl/vertex-array.ts b/src/mol-gl/webgl/vertex-array.ts
index f6a2315180e2c1cac18bb253fffe65abbd622599..cbb630d2582f9c3b2579bc92785f36932c9c6229 100644
--- a/src/mol-gl/webgl/vertex-array.ts
+++ b/src/mol-gl/webgl/vertex-array.ts
@@ -6,18 +6,19 @@
 
 import { WebGLContext } from './context';
 import { Program } from './program';
-import { AttributeBuffers, ElementsBuffer } from './buffer';
+import { ElementsBuffer, AttributeBuffers } from './buffer';
 
 export function createVertexArray(ctx: WebGLContext, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) {
     const { vertexArrayObject } = ctx.extensions
     let vertexArray: WebGLVertexArrayObject | null = null
     if (vertexArrayObject) {
         vertexArray = vertexArrayObject.createVertexArray()
-        vertexArrayObject.bindVertexArray(vertexArray)
-        if (elementsBuffer) elementsBuffer.bind()
-        program.bindAttributes(attributeBuffers)
-        ctx.vaoCount += 1
-        vertexArrayObject.bindVertexArray(null)
+        if (vertexArray) {
+            updateVertexArray(ctx, vertexArray, program, attributeBuffers, elementsBuffer)
+            ctx.stats.vaoCount += 1
+        } else {
+            console.warn('Could not create WebGL vertex array')
+        }
     }
     return vertexArray
 }
@@ -36,6 +37,6 @@ export function deleteVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArr
     const { vertexArrayObject } = ctx.extensions
     if (vertexArrayObject && vertexArray) {
         vertexArrayObject.deleteVertexArray(vertexArray)
-        ctx.vaoCount -= 1
+        ctx.stats.vaoCount -= 1
     }
 }
\ No newline at end of file
diff --git a/src/mol-io/reader/_spec/mol2.spec.ts b/src/mol-io/reader/_spec/mol2.spec.ts
index dc88b16043caf6777ca6571656f2c3cfab6eb38f..04570831700fc0db83cae496f660bbf50ef654ef 100644
--- a/src/mol-io/reader/_spec/mol2.spec.ts
+++ b/src/mol-io/reader/_spec/mol2.spec.ts
@@ -265,10 +265,10 @@ describe('mol2 reader', () => {
         expect(molecule.num_subst).toBe(0);
         expect(molecule.num_feat).toBe(0);
         expect(molecule.num_sets).toBe(0);
-        expect(molecule.mol_type).toBe("SMALL")
-        expect(molecule.charge_type).toBe("GASTEIGER");
-        expect(molecule.status_bits).toBe("");
-        expect(molecule.mol_comment).toBe("");
+        expect(molecule.mol_type).toBe('SMALL')
+        expect(molecule.charge_type).toBe('GASTEIGER');
+        expect(molecule.status_bits).toBe('');
+        expect(molecule.mol_comment).toBe('');
 
         // required atom fields
         expect(atoms.count).toBe(26);
@@ -277,7 +277,7 @@ describe('mol2 reader', () => {
         expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001);
         expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001);
         expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001);
-        expect(atoms.atom_type.value(0)).toBe("O.3");
+        expect(atoms.atom_type.value(0)).toBe('O.3');
 
         // optional atom fields
         expect(atoms.subst_id.value(0)).toBe(1);
@@ -316,10 +316,10 @@ describe('mol2 reader', () => {
         expect(molecule.num_subst).toBe(0);
         expect(molecule.num_feat).toBe(0);
         expect(molecule.num_sets).toBe(0);
-        expect(molecule.mol_type).toBe("SMALL")
-        expect(molecule.charge_type).toBe("GASTEIGER");
-        expect(molecule.status_bits).toBe("");
-        expect(molecule.mol_comment).toBe("");
+        expect(molecule.mol_type).toBe('SMALL')
+        expect(molecule.charge_type).toBe('GASTEIGER');
+        expect(molecule.status_bits).toBe('');
+        expect(molecule.mol_comment).toBe('');
 
         // required atom fields
         expect(atoms.count).toBe(26);
@@ -328,7 +328,7 @@ describe('mol2 reader', () => {
         expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001);
         expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001);
         expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001);
-        expect(atoms.atom_type.value(0)).toBe("O.3");
+        expect(atoms.atom_type.value(0)).toBe('O.3');
 
         // optional atom fields
         expect(atoms.subst_id.value(0)).toBe(1);
@@ -367,10 +367,10 @@ describe('mol2 reader', () => {
         expect(molecule.num_subst).toBe(0);
         expect(molecule.num_feat).toBe(0);
         expect(molecule.num_sets).toBe(0);
-        expect(molecule.mol_type).toBe("SMALL")
-        expect(molecule.charge_type).toBe("GASTEIGER");
-        expect(molecule.status_bits).toBe("");
-        expect(molecule.mol_comment).toBe("");
+        expect(molecule.mol_type).toBe('SMALL')
+        expect(molecule.charge_type).toBe('GASTEIGER');
+        expect(molecule.status_bits).toBe('');
+        expect(molecule.mol_comment).toBe('');
 
         // required atom fields
         expect(atoms.count).toBe(26);
@@ -379,7 +379,7 @@ describe('mol2 reader', () => {
         expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001);
         expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001);
         expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001);
-        expect(atoms.atom_type.value(0)).toBe("O.3");
+        expect(atoms.atom_type.value(0)).toBe('O.3');
 
         // optional atom fields
         expect(atoms.subst_id.value(0)).toBe(0);
diff --git a/src/mol-io/reader/_spec/ply.spec.ts b/src/mol-io/reader/_spec/ply.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..75325914641932f2495716166566fbdc9d084521
--- /dev/null
+++ b/src/mol-io/reader/_spec/ply.spec.ts
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Ply from '../ply/parser'
+import { PlyTable, PlyList } from '../ply/schema';
+
+const plyString = `ply
+format ascii 1.0
+comment file created by MegaMol
+element vertex 6
+property float x
+property float y
+property float z
+property uchar red
+property uchar green
+property uchar blue
+property uchar alpha
+property float nx
+property float ny
+property float nz
+property int atomid
+property uchar contactcount_r
+property uchar contactcount_g
+property uchar contactcount_b
+property uchar contactsteps_r
+property uchar contactsteps_g
+property uchar contactsteps_b
+property uchar hbonds_r
+property uchar hbonds_g
+property uchar hbonds_b
+property uchar hbondsteps_r
+property uchar hbondsteps_g
+property uchar hbondsteps_b
+property uchar molcount_r
+property uchar molcount_g
+property uchar molcount_b
+property uchar spots_r
+property uchar spots_g
+property uchar spots_b
+property uchar rmsf_r
+property uchar rmsf_g
+property uchar rmsf_b
+element face 2
+property list uchar int vertex_index
+end_header
+130.901 160.016 163.033 90 159 210 255 -0.382 -0.895 -0.231 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212
+131.372 159.778 162.83 90 159 210 255 -0.618 -0.776 -0.129 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199
+131.682 159.385 163.089 90 159 210 255 -0.773 -0.579 -0.259 180 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 172 196 212
+131.233 160.386 162.11 90 159 210 255 -0.708 -0.383 -0.594 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199
+130.782 160.539 162.415 90 159 210 255 -0.482 -0.459 -0.746 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212
+131.482 160.483 161.621 90 159 210 255 -0.832 -0.431 -0.349 179 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212
+3 0 2 1
+3 3 5 4
+`
+
+const plyCubeString = `ply
+format ascii 1.0
+comment test cube
+element vertex 24
+property float32 x
+property float32 y
+property float32 z
+property uint32 material_index
+element face 6
+property list uint8 int32 vertex_indices
+element material 6
+property uint8 red
+property uint8 green
+property uint8 blue
+end_header
+-1 -1 -1 0
+1 -1 -1 0
+1 1 -1 0
+-1 1 -1 0
+1 -1 1 1
+-1 -1 1 1
+-1 1 1 1
+1 1 1 1
+1 1 1 2
+1 1 -1 2
+1 -1 -1 2
+1 -1 1 2
+-1 1 -1 3
+-1 1 1 3
+-1 -1 1 3
+-1 -1 -1 3
+-1 1 1 4
+-1 1 -1 4
+1 1 -1 4
+1 1 1 4
+1 -1 1 5
+1 -1 -1 5
+-1 -1 -1 5
+-1 -1 1 5
+4 0 1 2 3
+4 4 5 6 7
+4 8 9 10 11
+4 12 13 14 15
+4 16 17 18 19
+4 20 21 22 23
+255 0 0
+0 255 0
+0 0 255
+255 255 0
+0 255 255
+255 0 255
+`
+
+
+describe('ply reader', () => {
+    it('basic', async () => {
+        const parsed = await Ply(plyString).run();
+        if (parsed.isError) return;
+        const plyFile = parsed.result;
+
+        const vertex = plyFile.getElement('vertex') as PlyTable
+        if (!vertex) return
+        const x = vertex.getProperty('x')
+        if (!x) return
+        expect(x.value(0)).toEqual(130.901)
+
+        const face = plyFile.getElement('face') as PlyList
+        if (!face) return
+        expect(face.value(0)).toEqual({ count: 3, entries: [0, 2, 1]})
+        expect(face.value(1)).toEqual({ count: 3, entries: [3, 5, 4]})
+
+        expect.assertions(3)
+    });
+
+    it('material', async () => {
+        const parsed = await Ply(plyCubeString).run();
+        if (parsed.isError) return;
+        const plyFile = parsed.result;
+
+        const vertex = plyFile.getElement('vertex') as PlyTable
+        if (!vertex) return
+        expect(vertex.rowCount).toBe(24)
+
+        const face = plyFile.getElement('face') as PlyList
+        if (!face) return
+        expect(face.rowCount).toBe(6)
+
+        const material = plyFile.getElement('face') as PlyTable
+        if (!material) return
+        expect(face.rowCount).toBe(6)
+
+        expect.assertions(3)
+    });
+});
\ No newline at end of file
diff --git a/src/mol-io/reader/cif/data-model.ts b/src/mol-io/reader/cif/data-model.ts
index 2800437dc930bd57af8dafe14913f9d5e15fc105..c5778c7b55a847616c5c36ff57dcf300f9af1926 100644
--- a/src/mol-io/reader/cif/data-model.ts
+++ b/src/mol-io/reader/cif/data-model.ts
@@ -199,7 +199,7 @@ export namespace CifField {
 
     export function ofColumn(column: Column<any>): CifField {
         const { rowCount, valueKind, areValuesEqual } = column;
-        
+
         let str: CifField['str']
         let int: CifField['int']
         let float: CifField['float']
@@ -219,7 +219,6 @@ export namespace CifField {
             default:
                 throw new Error('unsupported')
         }
-                
 
         return {
             __array: void 0,
diff --git a/src/mol-io/reader/csv/data-model.ts b/src/mol-io/reader/csv/data-model.ts
index 401c7aa2d5855cd530131ac22345bc0052823f9f..7c538467e25bf5eb2143ba569af1af41fe02e3e1 100644
--- a/src/mol-io/reader/csv/data-model.ts
+++ b/src/mol-io/reader/csv/data-model.ts
@@ -9,12 +9,11 @@ import { CifField as CsvColumn } from '../cif/data-model'
 export { CsvColumn }
 
 export interface CsvFile {
-    readonly name?: string,
     readonly table: CsvTable
 }
 
-export function CsvFile(table: CsvTable, name?: string): CsvFile {
-    return { name, table };
+export function CsvFile(table: CsvTable): CsvFile {
+    return { table };
 }
 
 export interface CsvTable {
@@ -27,10 +26,4 @@ export function CsvTable(rowCount: number, columnNames: string[], columns: CsvCo
     return { rowCount, columnNames: [...columnNames], getColumn(name) { return columns[name]; } };
 }
 
-export type CsvColumns = { [name: string]: CsvColumn }
-
-// export namespace CsvTable {
-//     export function empty(name: string): Table {
-//         return { rowCount: 0, name, fieldNames: [], getColumn(name: string) { return void 0; } };
-//     };
-// }
\ No newline at end of file
+export type CsvColumns = { [name: string]: CsvColumn }
\ No newline at end of file
diff --git a/src/mol-io/reader/ply/parser.ts b/src/mol-io/reader/ply/parser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bd9ce4f2d044a9e0cf62efe7c5f06c87d4258658
--- /dev/null
+++ b/src/mol-io/reader/ply/parser.ts
@@ -0,0 +1,263 @@
+/**
+ * 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, RuntimeContext } from 'mol-task'
+import { PlyFile, PlyType, PlyElement } from './schema';
+import { Tokenizer, TokenBuilder, Tokens } from '../common/text/tokenizer';
+import { Column } from 'mol-data/db';
+import { TokenColumn } from '../common/text/column/token';
+
+interface State {
+    data: string
+    tokenizer: Tokenizer
+    runtimeCtx: RuntimeContext
+
+    comments: string[]
+    elementSpecs: ElementSpec[]
+    elements: PlyElement[]
+}
+
+function State(data: string, runtimeCtx: RuntimeContext): State {
+    const tokenizer = Tokenizer(data)
+    return {
+        data,
+        tokenizer,
+        runtimeCtx,
+
+        comments: [],
+        elementSpecs: [],
+        elements: []
+    }
+}
+
+type ColumnProperty = { kind: 'column', type: PlyType, name: string }
+type ListProperty = { kind: 'list', countType: PlyType, dataType: PlyType, name: string }
+type Property = ColumnProperty | ListProperty
+
+type TableElementSpec = { kind: 'table', name: string, count: number, properties: ColumnProperty[] }
+type ListElementSpec = { kind: 'list', name: string, count: number, property: ListProperty }
+type ElementSpec = TableElementSpec | ListElementSpec
+
+function markHeader(tokenizer: Tokenizer) {
+    const endHeaderIndex = tokenizer.data.indexOf('end_header', tokenizer.position)
+    if (endHeaderIndex === -1) throw new Error(`no 'end_header' record found`)
+    // TODO set `tokenizer.lineNumber` correctly
+    tokenizer.tokenStart = tokenizer.position
+    tokenizer.tokenEnd = endHeaderIndex
+    tokenizer.position = endHeaderIndex
+    Tokenizer.eatLine(tokenizer)
+}
+
+function parseHeader(state: State) {
+    const { tokenizer, comments, elementSpecs } = state
+
+    markHeader(tokenizer)
+    const headerLines = Tokenizer.getTokenString(tokenizer).split(/\r?\n/)
+
+    if (headerLines[0] !== 'ply') throw new Error(`data not starting with 'ply'`)
+    if (headerLines[1] !== 'format ascii 1.0') throw new Error(`format not 'ascii 1.0'`)
+
+    let currentName: string | undefined
+    let currentCount: number | undefined
+    let currentProperties: Property[] | undefined
+
+
+    function addCurrentElementSchema() {
+        if (currentName !== undefined && currentCount !== undefined && currentProperties !== undefined) {
+            let isList = false
+            for (let i = 0, il = currentProperties.length; i < il; ++i) {
+                const p = currentProperties[i]
+                if (p.kind === 'list') {
+                    isList = true
+                    break
+                }
+            }
+            if (isList && currentProperties.length !== 1) throw new Error('expected single list property')
+            if (isList) {
+                elementSpecs.push({
+                    kind: 'list',
+                    name: currentName,
+                    count: currentCount,
+                    property: currentProperties[0] as ListProperty
+                })
+            } else {
+                elementSpecs.push({
+                    kind: 'table',
+                    name: currentName,
+                    count: currentCount,
+                    properties: currentProperties as ColumnProperty[]
+                })
+            }
+        }
+    }
+
+    for (let i = 2, il = headerLines.length; i < il; ++i) {
+        const l = headerLines[i]
+        const ls = l.split(' ')
+        if (l.startsWith('comment')) {
+            comments.push(l.substr(8))
+        } else if (l.startsWith('element')) {
+            addCurrentElementSchema()
+            currentProperties = []
+            currentName = ls[1]
+            currentCount = parseInt(ls[2])
+        } else if (l.startsWith('property')) {
+            if (currentProperties === undefined) throw new Error(`properties outside of element`)
+            if (ls[1] === 'list') {
+                currentProperties.push({
+                    kind: 'list',
+                    countType: PlyType(ls[2]),
+                    dataType: PlyType(ls[3]),
+                    name: ls[4]
+                })
+            } else {
+                currentProperties.push({
+                    kind: 'column',
+                    type: PlyType(ls[1]),
+                    name: ls[2]
+                })
+            }
+        } else if (l.startsWith('end_header')) {
+            addCurrentElementSchema()
+        } else {
+            console.warn('unknown header line')
+        }
+    }
+}
+
+function parseElements(state: State) {
+    const { elementSpecs } = state
+    for (let i = 0, il = elementSpecs.length; i < il; ++i) {
+        const spec = elementSpecs[i]
+        if (spec.kind === 'table') parseTableElement(state, spec)
+        else if (spec.kind === 'list') parseListElement(state, spec)
+    }
+}
+
+function getColumnSchema(type: PlyType): Column.Schema {
+    switch (type) {
+        case 'char': case 'uchar': case 'int8': case 'uint8':
+        case 'short': case 'ushort': case 'int16': case 'uint16':
+        case 'int': case 'uint': case 'int32': case 'uint32':
+            return Column.Schema.int
+        case 'float': case 'double': case 'float32': case 'float64':
+            return Column.Schema.float
+    }
+}
+
+function parseTableElement(state: State, spec: TableElementSpec) {
+    const { elements, tokenizer } = state
+    const { count, properties } = spec
+    const propertyCount = properties.length
+    const propertyNames: string[] = []
+    const propertyTypes: PlyType[] = []
+    const propertyTokens: Tokens[] = []
+    const propertyColumns = new Map<string, Column<number>>()
+
+    for (let i = 0, il = propertyCount; i < il; ++i) {
+        const tokens = TokenBuilder.create(tokenizer.data, count * 2)
+        propertyTokens.push(tokens)
+    }
+
+    for (let i = 0, il = count; i < il; ++i) {
+        for (let j = 0, jl = propertyCount; j < jl; ++j) {
+            Tokenizer.skipWhitespace(tokenizer)
+            Tokenizer.markStart(tokenizer)
+            Tokenizer.eatValue(tokenizer)
+            TokenBuilder.addUnchecked(propertyTokens[j], tokenizer.tokenStart, tokenizer.tokenEnd)
+        }
+    }
+
+    for (let i = 0, il = propertyCount; i < il; ++i) {
+        const { type, name } = properties[i]
+        const column = TokenColumn(propertyTokens[i], getColumnSchema(type))
+        propertyNames.push(name)
+        propertyTypes.push(type)
+        propertyColumns.set(name, column)
+    }
+
+    elements.push({
+        kind: 'table',
+        rowCount: count,
+        propertyNames,
+        propertyTypes,
+        getProperty: (name: string) => propertyColumns.get(name)
+    })
+}
+
+function parseListElement(state: State, spec: ListElementSpec) {
+    const { elements, tokenizer } = state
+    const { count, property } = spec
+
+    // initial tokens size assumes triangle index data
+    const tokens = TokenBuilder.create(tokenizer.data, count * 2 * 3)
+
+    const offsets = new Uint32Array(count + 1)
+    let entryCount = 0
+
+    for (let i = 0, il = count; i < il; ++i) {
+        // skip over row entry count as it is determined by line break
+        Tokenizer.skipWhitespace(tokenizer)
+        Tokenizer.eatValue(tokenizer)
+
+        while (Tokenizer.skipWhitespace(tokenizer) !== 10) {
+            ++entryCount
+            Tokenizer.markStart(tokenizer)
+            Tokenizer.eatValue(tokenizer)
+            TokenBuilder.addToken(tokens, tokenizer)
+        }
+        offsets[i + 1] = entryCount
+    }
+
+    // console.log(tokens.indices)
+    // console.log(offsets)
+
+    /** holds row value entries transiently */
+    const listValue = {
+        entries: [] as number[],
+        count: 0
+    }
+
+    const column = TokenColumn(tokens, getColumnSchema(property.dataType))
+
+    elements.push({
+        kind: 'list',
+        rowCount: count,
+        name: property.name,
+        type: property.dataType,
+        value: (row: number) => {
+            const start = offsets[row]
+            const end = offsets[row + 1]
+            for (let i = start; i < end; ++i) {
+                listValue.entries[i - start] = column.value(i)
+            }
+            listValue.count = end - start
+            return listValue
+        }
+    })
+}
+
+async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<PlyFile>> {
+    const state = State(data, ctx);
+    ctx.update({ message: 'Parsing...', current: 0, max: data.length });
+    parseHeader(state)
+    // console.log(state.comments)
+    // console.log(JSON.stringify(state.elementSpecs, undefined, 4))
+    parseElements(state)
+    const { elements, elementSpecs, comments } = state
+    const elementNames = elementSpecs.map(s => s.name)
+    const result = PlyFile(elements, elementNames, comments)
+    return Result.success(result);
+}
+
+export function parse(data: string) {
+    return Task.create<Result<PlyFile>>('Parse PLY', async ctx => {
+        return await parseInternal(data, ctx)
+    })
+}
+
+export default parse;
\ No newline at end of file
diff --git a/src/mol-io/reader/ply/schema.ts b/src/mol-io/reader/ply/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c5fbcb995ae825d0ea7f829ffd3ea96bca73f715
--- /dev/null
+++ b/src/mol-io/reader/ply/schema.ts
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Column } from 'mol-data/db';
+
+// http://paulbourke.net/dataformats/ply/
+// https://en.wikipedia.org/wiki/PLY_(file_format)
+
+export const PlyTypeByteLength = {
+    'char': 1,
+    'uchar': 1,
+    'short': 2,
+    'ushort': 2,
+    'int': 4,
+    'uint': 4,
+    'float': 4,
+    'double': 8,
+
+    'int8': 1,
+    'uint8': 1,
+    'int16': 2,
+    'uint16': 2,
+    'int32': 4,
+    'uint32': 4,
+    'float32': 4,
+    'float64': 8
+}
+export type PlyType = keyof typeof PlyTypeByteLength
+export const PlyTypes = new Set(Object.keys(PlyTypeByteLength))
+export function PlyType(str: string) {
+    if (!PlyTypes.has(str)) throw new Error(`unknown ply type '${str}'`)
+    return str as PlyType
+}
+
+export interface PlyFile {
+    readonly comments: ReadonlyArray<string>
+    readonly elementNames: ReadonlyArray<string>
+    getElement(name: string): PlyElement | undefined
+}
+
+export function PlyFile(elements: PlyElement[], elementNames: string[], comments: string[]): PlyFile {
+    const elementMap = new Map<string, PlyElement>()
+    for (let i = 0, il = elementNames.length; i < il; ++i) {
+        elementMap.set(elementNames[i], elements[i])
+    }
+    return {
+        comments,
+        elementNames,
+        getElement: (name: string) => {
+            return elementMap.get(name)
+        }
+    };
+}
+
+export type PlyElement = PlyTable | PlyList
+
+export interface PlyTable {
+    readonly kind: 'table'
+    readonly rowCount: number
+    readonly propertyNames: ReadonlyArray<string>
+    readonly propertyTypes: ReadonlyArray<PlyType>
+    getProperty(name: string): Column<number> | undefined
+}
+
+export interface PlyListValue {
+    readonly entries: ArrayLike<number>
+    readonly count: number
+}
+
+export interface PlyList {
+    readonly kind: 'list'
+    readonly rowCount: number,
+    readonly name: string,
+    readonly type: PlyType,
+    value: (row: number) => PlyListValue
+}
\ No newline at end of file
diff --git a/src/mol-math/geometry/common.ts b/src/mol-math/geometry/common.ts
index 674e3ede99369615562c0a8056b8735e43390c05..61d34da38fe51f482b96d122c0cb02843e1e8ebe 100644
--- a/src/mol-math/geometry/common.ts
+++ b/src/mol-math/geometry/common.ts
@@ -1,12 +1,12 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { OrderedSet } from 'mol-data/int'
-import { Mat4, Tensor, Vec3 } from '../linear-algebra';
+import { Mat4, Tensor, Vec3, Vec2 } from '../linear-algebra';
 import { Box3D } from '../geometry';
 import { Texture } from 'mol-gl/webgl/texture';
 
@@ -30,5 +30,15 @@ export type DensityTextureData = {
     transform: Mat4,
     texture: Texture,
     bbox: Box3D,
-    gridDimension: Vec3
+    gridDim: Vec3,
+    gridTexDim: Vec3
+    gridTexScale: Vec2
+}
+
+export function fillGridDim(length: number, start: number, step: number) {
+    const a = new Float32Array(length)
+    for (let i = 0; i < a.length; i++) {
+        a[i] = start + (step * i)
+    }
+    return a
 }
\ No newline at end of file
diff --git a/src/mol-math/geometry/gaussian-density.ts b/src/mol-math/geometry/gaussian-density.ts
index 434ba158c1dbc59d5161c7e4d3fe07a12919c448..e152830cc6117c7b52ca146a3c836241ecaafbb2 100644
--- a/src/mol-math/geometry/gaussian-density.ts
+++ b/src/mol-math/geometry/gaussian-density.ts
@@ -1,20 +1,24 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Box3D } from '../geometry';
-import { Vec3 } from '../linear-algebra';
 import { RuntimeContext, Task } from 'mol-task';
 import { PositionData, DensityData } from './common';
 import { GaussianDensityCPU } from './gaussian-density/cpu';
 import { WebGLContext } from 'mol-gl/webgl/context';
+import { Texture } from 'mol-gl/webgl/texture';
+import { GaussianDensityTexture2d, GaussianDensityTexture3d } from './gaussian-density/gpu';
 
-// import { GaussianDensityGPU } from './gaussian-density/gpu';
+// import { GaussianDensityGPU, GaussianDensityTexture } from './gaussian-density/gpu';
 const GaussianDensityGPU = typeof document !== 'undefined'
     ? (require('./gaussian-density/gpu') as typeof import('./gaussian-density/gpu')).GaussianDensityGPU
     : void 0;
+const GaussianDensityTexture = typeof document !== 'undefined'
+    ? (require('./gaussian-density/gpu') as typeof import('./gaussian-density/gpu')).GaussianDensityTexture
+    : void 0;
 
 export const DefaultGaussianDensityGPUProps = {
     resolution: 1,
@@ -29,17 +33,9 @@ export const DefaultGaussianDensityProps = {
 }
 export type GaussianDensityProps = typeof DefaultGaussianDensityProps
 
-export function getDelta(box: Box3D, resolution: number) {
-    const extent = Vec3.sub(Vec3.zero(), box.max, box.min)
-    const size = Vec3.zero()
-    Vec3.ceil(size, Vec3.scale(size, extent, resolution))
-    const delta = Vec3.div(Vec3.zero(), extent, size)
-    return delta
-}
-
-export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps) {
+export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps, webgl?: WebGLContext) {
     return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensity(ctx, position, box, radius, props)
+        return await GaussianDensity(ctx, position, box, radius, props, webgl)
     });
 }
 
@@ -47,8 +43,29 @@ export async function GaussianDensity(ctx: RuntimeContext, position: PositionDat
     if (props.useGpu) {
         if (!GaussianDensityGPU) throw 'GPU computation not supported on this platform';
         if (!webgl) throw 'No WebGL context provided';
-        return await GaussianDensityGPU(ctx, position, box, radius, props, webgl)
+        return GaussianDensityGPU(position, box, radius, props, webgl)
     } else {
         return await GaussianDensityCPU(ctx, position, box, radius, props)
     }
+}
+
+export function computeGaussianDensityTexture(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
+    return _computeGaussianDensityTexture(webgl.isWebGL2 ? '3d' : '2d', position, box, radius, props, webgl, texture)
+}
+
+export function computeGaussianDensityTexture2d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
+    return _computeGaussianDensityTexture('2d', position, box, radius, props, webgl, texture)
+}
+
+export function computeGaussianDensityTexture3d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
+    return _computeGaussianDensityTexture('2d', position, box, radius, props, webgl, texture)
+}
+
+function _computeGaussianDensityTexture(type: '2d' | '3d', position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
+    if (!GaussianDensityTexture) throw 'GPU computation not supported on this platform';
+    return Task.create('Gaussian Density', async ctx => {
+        return type === '2d' ?
+            GaussianDensityTexture2d(webgl, position, box, radius, props, texture) :
+            GaussianDensityTexture3d(webgl, position, box, radius, props, texture);
+    });
 }
\ No newline at end of file
diff --git a/src/mol-math/geometry/gaussian-density/cpu.ts b/src/mol-math/geometry/gaussian-density/cpu.ts
index a52619139e28ff2ba9b39989ea9468b32c326133..3dedac18f884b9a1acc9891f4739bff7428201da 100644
--- a/src/mol-math/geometry/gaussian-density/cpu.ts
+++ b/src/mol-math/geometry/gaussian-density/cpu.ts
@@ -1,44 +1,37 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Box3D } from '../../geometry';
+import { Box3D, fillGridDim } from '../../geometry';
 import { Vec3, Mat4, Tensor } from '../../linear-algebra';
 import { RuntimeContext } from 'mol-task';
 import { PositionData, DensityData } from '../common';
 import { OrderedSet } from 'mol-data/int';
-import { GaussianDensityProps, getDelta } from '../gaussian-density';
+import { GaussianDensityProps } from '../gaussian-density';
 
 export async function GaussianDensityCPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps): Promise<DensityData> {
     const { resolution, radiusOffset, smoothness } = props
+    const scaleFactor = 1 / resolution
 
     const { indices, x, y, z } = position
     const n = OrderedSet.size(indices)
-
-    const v = Vec3.zero()
-    const p = Vec3.zero()
+    const radii = new Float32Array(n)
 
     let maxRadius = 0
     for (let i = 0; i < n; ++i) {
         const r = radius(OrderedSet.getAt(indices, i)) + radiusOffset
         if (maxRadius < r) maxRadius = r
-
-        if (i % 10000 === 0 && ctx.shouldUpdate) {
-            await ctx.update({ message: 'calculating max radius', current: i, max: n })
-        }
+        radii[i] = r
     }
 
     const pad = maxRadius * 2 + resolution
-    const expandedBox = Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad))
-    const extent = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min)
+    const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
     const min = expandedBox.min
-
-    const delta = getDelta(Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad)), resolution)
-    const dim = Vec3.zero()
-    Vec3.ceil(dim, Vec3.mul(dim, extent, delta))
-    // console.log('grid dim cpu', dim)
+    const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor)
+    const dim = Box3D.size(Vec3(), scaledBox)
+    Vec3.ceil(dim, dim)
 
     const space = Tensor.Space(dim, [0, 1, 2], Float32Array)
     const data = space.create()
@@ -47,68 +40,91 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position
     const idData = space.create()
     const idField = Tensor.create(space, idData)
 
-    const densData = space.create()
-
-    const c = Vec3.zero()
-
-    const alpha = smoothness
-
-    const _r2 = maxRadius * 2
-    const _radius2 = Vec3.create(_r2, _r2, _r2)
-    Vec3.mul(_radius2, _radius2, delta)
-    const updateChunk = Math.ceil(10000 / (_radius2[0] * _radius2[1] * _radius2[2]))
+    const [ dimX, dimY, dimZ ] = dim
+    const iu = dimZ, iv = dimY, iuv = iu * iv
 
-    const beg = Vec3.zero()
-    const end = Vec3.zero()
+    const gridx = fillGridDim(dim[0], min[0], resolution)
+    const gridy = fillGridDim(dim[1], min[1], resolution)
+    const gridz = fillGridDim(dim[2], min[2], resolution)
 
-    const gridPad = 1 / Math.max(...delta)
+    const densData = space.create()
 
-    // console.time('gaussian density cpu')
-    for (let i = 0; i < n; ++i) {
-        const j = OrderedSet.getAt(indices, i)
-
-        Vec3.set(v, x[j], y[j], z[j])
-
-        Vec3.sub(v, v, min)
-        Vec3.mul(c, v, delta)
-
-        const rad = radius(j) + radiusOffset
-        const rSq = rad * rad
-
-        const r2 = radiusOffset + rad * 2 + gridPad
-        const rad2 = Vec3.create(r2, r2, r2)
-        Vec3.mul(rad2, rad2, delta)
-        const r2sq = r2 * r2
-
-        const [ begX, begY, begZ ] = Vec3.floor(beg, Vec3.sub(beg, c, rad2))
-        const [ endX, endY, endZ ] = Vec3.ceil(end, Vec3.add(end, c, rad2))
-
-        for (let xi = begX; xi < endX; ++xi) {
-            for (let yi = begY; yi < endY; ++yi) {
-                for (let zi = begZ; zi < endZ; ++zi) {
-                    Vec3.set(p, xi, yi, zi)
-                    Vec3.div(p, p, delta)
-                    const distSq = Vec3.squaredDistance(p, v)
-                    if (distSq <= r2sq) {
-                        const dens = Math.exp(-alpha * (distSq / rSq))
-                        space.add(data, xi, yi, zi, dens)
-                        if (dens > space.get(densData, xi, yi, zi)) {
-                            space.set(densData, xi, yi, zi, dens)
-                            space.set(idData, xi, yi, zi, i)
+    const alpha = smoothness
+    const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor)))
+
+    function accumulateRange(begI: number, endI: number) {
+        for (let i = begI; i < endI; ++i) {
+            const j = OrderedSet.getAt(indices, i)
+            const vx = x[j], vy = y[j], vz = z[j]
+
+            const rad = radii[i]
+            const rSq = rad * rad
+            const rSqInv = 1 / rSq
+
+            const r2 = rad * 2
+            const r2sq = r2 * r2
+
+            // Number of grid points, round this up...
+            const ng = Math.ceil(r2 * scaleFactor)
+
+            // Center of the atom, mapped to grid points (take floor)
+            const iax = Math.floor(scaleFactor * (vx - min[0]))
+            const iay = Math.floor(scaleFactor * (vy - min[1]))
+            const iaz = Math.floor(scaleFactor * (vz - min[2]))
+
+            // Extents of grid to consider for this atom
+            const begX = Math.max(0, iax - ng)
+            const begY = Math.max(0, iay - ng)
+            const begZ = Math.max(0, iaz - ng)
+
+            // Add two to these points:
+            // - iax are floor'd values so this ensures coverage
+            // - these are loop limits (exclusive)
+            const endX = Math.min(dimX, iax + ng + 2)
+            const endY = Math.min(dimY, iay + ng + 2)
+            const endZ = Math.min(dimZ, iaz + ng + 2)
+
+            for (let xi = begX; xi < endX; ++xi) {
+                const dx = gridx[xi] - vx
+                const xIdx = xi * iuv
+                for (let yi = begY; yi < endY; ++yi) {
+                    const dy = gridy[yi] - vy
+                    const dxySq = dx * dx + dy * dy
+                    const xyIdx = yi * iu + xIdx
+                    for (let zi = begZ; zi < endZ; ++zi) {
+                        const dz = gridz[zi] - vz
+                        const dSq = dxySq + dz * dz
+                        if (dSq <= r2sq) {
+                            const dens = Math.exp(-alpha * (dSq * rSqInv))
+                            const idx = zi + xyIdx
+                            data[idx] += dens
+                            if (dens > densData[idx]) {
+                                densData[idx] = dens
+                                idData[idx] = i
+                            }
                         }
                     }
                 }
             }
         }
+    }
 
-        if (i % updateChunk === 0 && ctx.shouldUpdate) {
-            await ctx.update({ message: 'filling density grid', current: i, max: n })
+    async function accumulate() {
+        for (let i = 0; i < n; i += updateChunk) {
+            accumulateRange(i, Math.min(i + updateChunk, n))
+
+            if (ctx.shouldUpdate) {
+                await ctx.update({ message: 'filling density grid', current: i, max: n })
+            }
         }
     }
+
+    // console.time('gaussian density cpu')
+    await accumulate()
     // console.timeEnd('gaussian density cpu')
 
     const transform = Mat4.identity()
-    Mat4.fromScaling(transform, Vec3.inverse(Vec3.zero(), delta))
+    Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution))
     Mat4.setTranslation(transform, expandedBox.min)
 
     return { field, idField, transform }
diff --git a/src/mol-math/geometry/gaussian-density/gpu.ts b/src/mol-math/geometry/gaussian-density/gpu.ts
index 2f91bcd1af1f8228fc22c7979abb0a1493b63a13..80f6314a527711c92b14ad04a847b9a558b355d2 100644
--- a/src/mol-math/geometry/gaussian-density/gpu.ts
+++ b/src/mol-math/geometry/gaussian-density/gpu.ts
@@ -1,86 +1,142 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 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>
  */
 
-import { RuntimeContext } from 'mol-task'
 import { PositionData, DensityData, DensityTextureData } from '../common'
 import { Box3D } from '../../geometry'
-import { GaussianDensityGPUProps, getDelta } from '../gaussian-density'
+import { GaussianDensityGPUProps } from '../gaussian-density'
 import { OrderedSet } from 'mol-data/int'
-import { Vec3, Tensor, Mat4 } from '../../linear-algebra'
-import { GaussianDensityValues } from 'mol-gl/renderable/gaussian-density'
+import { Vec3, Tensor, Mat4, Vec2 } from '../../linear-algebra'
 import { ValueCell } from 'mol-util'
-import { RenderableState, Renderable } from 'mol-gl/renderable'
-import { createRenderable, createRenderObject } from 'mol-gl/render-object'
+import { createComputeRenderable, ComputeRenderable } from 'mol-gl/renderable'
 import { WebGLContext } from 'mol-gl/webgl/context';
 import { createTexture, Texture } from 'mol-gl/webgl/texture';
-import { GLRenderingContext } from 'mol-gl/webgl/compat';
 import { decodeFloatRGB } from 'mol-util/float-packing';
+import { ShaderCode } from 'mol-gl/shader-code';
+import { createComputeRenderItem } from 'mol-gl/webgl/render-item';
+import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, DefineSpec, Values } from 'mol-gl/renderable/schema';
+
+export const GaussianDensitySchema = {
+    drawCount: ValueSpec('number'),
+    instanceCount: ValueSpec('number'),
+
+    aRadius: AttributeSpec('float32', 1, 0),
+    aPosition: AttributeSpec('float32', 3, 0),
+    aGroup: AttributeSpec('float32', 1, 0),
+
+    uCurrentSlice: UniformSpec('f'),
+    uCurrentX: UniformSpec('f'),
+    uCurrentY: UniformSpec('f'),
+    uBboxMin: UniformSpec('v3', true),
+    uBboxSize: UniformSpec('v3', true),
+    uGridDim: UniformSpec('v3', true),
+    uGridTexDim: UniformSpec('v3', true),
+    uGridTexScale: UniformSpec('v2', true),
+    uAlpha: UniformSpec('f', true),
+    uResolution: UniformSpec('f', true),
+    tMinDistanceTex: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+
+    dGridTexType: DefineSpec('string', ['2d', '3d']),
+    dCalcType: DefineSpec('string', ['density', 'minDistance', 'groupId']),
+}
+
+export const GaussianDensityShaderCode = ShaderCode(
+    require('mol-gl/shader/gaussian-density.vert').default,
+    require('mol-gl/shader/gaussian-density.frag').default,
+    { standardDerivatives: false, fragDepth: false }
+)
 
 /** name for shared framebuffer used for gpu gaussian surface operations */
-const FramebufferName = 'gaussian-density-gpu'
+const FramebufferName = 'gaussian-density'
 
-export async function GaussianDensityGPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext): Promise<DensityData> {
+export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext): DensityData {
     // always use texture2d when the gaussian density needs to be downloaded from the GPU,
     // it's faster than texture3d
     // console.time('GaussianDensityTexture2d')
-    const { scale, bbox, texture, dim } = await GaussianDensityTexture2d(ctx, webgl, position, box, radius, props)
+    const { scale, bbox, texture, gridDim, gridTexDim } = calcGaussianDensityTexture2d(webgl, position, box, radius, props)
+    // webgl.waitForGpuCommandsCompleteSync()
     // console.timeEnd('GaussianDensityTexture2d')
-    const { field, idField } = await fieldFromTexture2d(webgl, texture, dim)
+    const { field, idField } = fieldFromTexture2d(webgl, texture, gridDim, gridTexDim)
 
-    const transform = Mat4.identity()
-    Mat4.fromScaling(transform, scale)
-    Mat4.setTranslation(transform, bbox.min)
+    return { field, idField, transform: getTransform(scale, bbox) }
+}
+
+export function GaussianDensityTexture(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): DensityTextureData {
+    return webgl.isWebGL2 ?
+        GaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture) :
+        GaussianDensityTexture2d(webgl, position, box, radius, props, oldTexture)
+}
+
+export function GaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): DensityTextureData {
+    return finalizeGaussianDensityTexture(calcGaussianDensityTexture2d(webgl, position, box, radius, props, oldTexture))
+}
 
-    return { field, idField, transform }
+export function GaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): DensityTextureData {
+    return finalizeGaussianDensityTexture(calcGaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture))
 }
 
-export async function GaussianDensityTexture(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): Promise<DensityTextureData> {
-    // console.time(`GaussianDensityTexture, ${webgl.isWebGL2 ? '3d' : '2d'}`)
-    const { texture, scale, bbox, dim } = webgl.isWebGL2 ?
-        await GaussianDensityTexture3d(ctx, webgl, position, box, radius, props, oldTexture) :
-        await GaussianDensityTexture2d(ctx, webgl, position, box, radius, props, oldTexture)
-    // console.timeEnd(`GaussianDensityTexture, ${webgl.isWebGL2 ? '3d' : '2d'}`)
+function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale }: GaussianDensityTextureData): DensityTextureData {
+    return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridTexScale }
+}
 
+function getTransform(scale: Vec3, bbox: Box3D) {
     const transform = Mat4.identity()
     Mat4.fromScaling(transform, scale)
     Mat4.setTranslation(transform, bbox.min)
-
-    return { transform, texture, bbox, gridDimension: dim }
+    return transform
 }
 
 //
 
-async function GaussianDensityTexture2d(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture) {
+type GaussianDensityTextureData = {
+    texture: Texture,
+    scale: Vec3,
+    bbox: Box3D,
+    gridDim: Vec3,
+    gridTexDim: Vec3
+    gridTexScale: Vec2
+}
+
+function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): GaussianDensityTextureData {
     const { smoothness } = props
 
-    const { drawCount, positions, radii, groups, delta, expandedBox, dim } = await prepareGaussianDensityData(ctx, position, box, radius, props)
+    const { drawCount, positions, radii, groups, scale, expandedBox, dim } = prepareGaussianDensityData(position, box, radius, props)
     const [ dx, dy, dz ] = dim
-    const { texDimX, texDimY, texCols } = getTexture2dSize(webgl.maxTextureSize, dim)
+    const { texDimX, texDimY, texCols, powerOfTwoSize } = getTexture2dSize(dim)
+    // console.log({ texDimX, texDimY, texCols, powerOfTwoSize, dim })
+    const gridTexDim = Vec3.create(texDimX, texDimY, 0)
+    const gridTexScale = Vec2.create(texDimX / powerOfTwoSize, texDimY / powerOfTwoSize)
 
-    const minDistanceTexture = createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'nearest')
-    minDistanceTexture.define(texDimX, texDimY)
+    const minDistanceTexture = createTexture(webgl, 'image-float32', 'rgba', 'float', 'nearest')
+    minDistanceTexture.define(powerOfTwoSize, powerOfTwoSize)
 
-    const renderObject = getGaussianDensityRenderObject(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, smoothness)
-    const renderable = createRenderable(webgl, renderObject)
+    const renderable = getGaussianDensityRenderable(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, gridTexDim, gridTexScale, smoothness, props.resolution)
 
     //
 
-    const { gl, framebufferCache } = webgl
-    const { uCurrentSlice, uCurrentX, uCurrentY } = renderObject.values
+    const { gl, framebufferCache, state } = webgl
+    const { uCurrentSlice, uCurrentX, uCurrentY } = renderable.values
 
-    const framebuffer = framebufferCache.get(webgl, FramebufferName).value
+    const framebuffer = framebufferCache.get(FramebufferName).value
     framebuffer.bind()
-    setRenderingDefaults(gl)
+    setRenderingDefaults(webgl)
+
+    if (!texture) {
+        texture = createTexture(webgl, 'image-float32', 'rgba', 'float', 'nearest')
+        texture.define(powerOfTwoSize, powerOfTwoSize)
+    } else if (texture.width !== powerOfTwoSize || texture.height !== powerOfTwoSize) {
+        texture.define(powerOfTwoSize, powerOfTwoSize)
+    }
 
-    if (!texture) texture = createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'linear')
-    texture.define(texDimX, texDimY)
+    // console.log(renderable)
 
-    function render(fbTex: Texture) {
+    function render(fbTex: Texture, clear: boolean) {
+        state.currentRenderItemId = -1
         fbTex.attachFramebuffer(framebuffer, 0)
+        if (clear) gl.clear(gl.COLOR_BUFFER_BIT)
         let currCol = 0
         let currY = 0
         let currX = 0
@@ -89,61 +145,63 @@ async function GaussianDensityTexture2d(ctx: RuntimeContext, webgl: WebGLContext
                 currCol -= texCols
                 currY += dy
                 currX = 0
+                ValueCell.update(uCurrentY, currY)
             }
-            gl.viewport(currX, currY, dx, dy)
-            ValueCell.update(uCurrentSlice, i)
+            // console.log({ i, currX, currY })
             ValueCell.update(uCurrentX, currX)
-            ValueCell.update(uCurrentY, currY)
-            renderable.render('draw')
+            ValueCell.update(uCurrentSlice, i)
+            gl.viewport(currX, currY, dx, dy)
+            renderable.render()
             ++currCol
             currX += dx
         }
+        gl.finish()
     }
 
-    setupMinDistanceRendering(webgl, renderable)
-    render(minDistanceTexture)
-
     setupDensityRendering(webgl, renderable)
-    render(texture)
+    render(texture, true)
+
+    setupMinDistanceRendering(webgl, renderable)
+    render(minDistanceTexture, true)
 
     setupGroupIdRendering(webgl, renderable)
-    render(texture)
+    render(texture, false)
 
-    if (ctx.shouldUpdate) await ctx.update({ message: 'gpu gaussian density calculation' })
-    await webgl.waitForGpuCommandsCompleteSync()
+    // printTexture(webgl, texture, 1)
 
-    return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim }
+    return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale }
 }
 
-async function GaussianDensityTexture3d(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture) {
+function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): GaussianDensityTextureData {
     const { smoothness } = props
 
-    const { drawCount, positions, radii, groups, delta, expandedBox, dim } = await prepareGaussianDensityData(ctx, position, box, radius, props)
+    const { drawCount, positions, radii, groups, scale, expandedBox, dim } = prepareGaussianDensityData(position, box, radius, props)
     const [ dx, dy, dz ] = dim
-    const minDistanceTexture = createTexture(webgl, 'volume-uint8', 'rgba', 'ubyte', 'nearest')
+    const minDistanceTexture = createTexture(webgl, 'volume-float32', 'rgba', 'float', 'nearest')
     minDistanceTexture.define(dx, dy, dz)
 
-    const renderObject = getGaussianDensityRenderObject(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, smoothness)
-    const renderable = createRenderable(webgl, renderObject)
+    const gridTexScale = Vec2.create(1, 1)
+
+    const renderable = getGaussianDensityRenderable(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, dim, gridTexScale, smoothness, props.resolution)
 
     //
 
     const { gl, framebufferCache } = webgl
-    const { uCurrentSlice } = renderObject.values
+    const { uCurrentSlice } = renderable.values
 
-    const framebuffer = framebufferCache.get(webgl, FramebufferName).value
+    const framebuffer = framebufferCache.get(FramebufferName).value
     framebuffer.bind()
-    setRenderingDefaults(gl)
+    setRenderingDefaults(webgl)
     gl.viewport(0, 0, dx, dy)
 
-    if (!texture) texture = createTexture(webgl, 'volume-uint8', 'rgba', 'ubyte', 'linear')
+    if (!texture) texture = createTexture(webgl, 'volume-float32', 'rgba', 'float', 'nearest')
     texture.define(dx, dy, dz)
 
     function render(fbTex: Texture) {
         for (let i = 0; i < dz; ++i) {
             ValueCell.update(uCurrentSlice, i)
             fbTex.attachFramebuffer(framebuffer, 0, i)
-            renderable.render('draw')
+            renderable.render()
         }
     }
 
@@ -156,16 +214,14 @@ async function GaussianDensityTexture3d(ctx: RuntimeContext, webgl: WebGLContext
     setupGroupIdRendering(webgl, renderable)
     render(texture)
 
-    await ctx.update({ message: 'gpu gaussian density calculation' });
-    await webgl.waitForGpuCommandsCompleteSync()
-
-    return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim }
+    return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridTexScale }
 }
 
 //
 
-async function prepareGaussianDensityData(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps) {
+function prepareGaussianDensityData(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps) {
     const { resolution, radiusOffset } = props
+    const scaleFactor = 1 / resolution
 
     const { indices, x, y, z } = position
     const n = OrderedSet.size(indices)
@@ -186,29 +242,23 @@ async function prepareGaussianDensityData(ctx: RuntimeContext, position: Positio
         if (maxRadius < r) maxRadius = r
         radii[i] = r
         groups[i] = i
-
-        if (i % 10000 === 0 && ctx.shouldUpdate) {
-            await ctx.update({ message: 'preparing density data', current: i, max: n })
-        }
     }
 
-    const pad = maxRadius * 2 + resolution
-    const expandedBox = Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad));
-    const extent = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min)
+    const pad = maxRadius * 2 + resolution * 4
+    const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
+    const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor)
+    const dim = Box3D.size(Vec3(), scaledBox)
+    Vec3.ceil(dim, dim)
 
-    const delta = getDelta(expandedBox, resolution)
-    const dim = Vec3.zero()
-    Vec3.ceil(dim, Vec3.mul(dim, extent, delta))
-    // console.log('grid dim gpu', dim)
+    const scale = Vec3.create(resolution, resolution, resolution)
 
-    return { drawCount: n, positions, radii, groups, delta, expandedBox, dim }
+    return { drawCount: n, positions, radii, groups, scale, expandedBox, dim }
 }
 
-function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number, positions: Float32Array, radii: Float32Array, groups: Float32Array, minDistanceTexture: Texture, box: Box3D, dimensions: Vec3, smoothness: number) {
+function getGaussianDensityRenderable(webgl: WebGLContext, drawCount: number, positions: Float32Array, radii: Float32Array, groups: Float32Array, minDistanceTexture: Texture, box: Box3D, gridDim: Vec3, gridTexDim: Vec3, gridTexScale: Vec2, smoothness: number, resolution: number) {
     const extent = Vec3.sub(Vec3.zero(), box.max, box.min)
-    const { texDimX, texDimY } = getTexture2dSize(webgl.maxTextureSize, dimensions)
 
-    const values: GaussianDensityValues = {
+    const values: Values<typeof GaussianDensitySchema> = {
         drawCount: ValueCell.create(drawCount),
         instanceCount: ValueCell.create(1),
 
@@ -220,85 +270,92 @@ function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number,
         uCurrentX: ValueCell.create(0),
         uCurrentY: ValueCell.create(0),
         uBboxMin: ValueCell.create(box.min),
-        uBboxMax: ValueCell.create(box.max),
         uBboxSize: ValueCell.create(extent),
-        uGridDim: ValueCell.create(dimensions),
-        uGridTexDim: ValueCell.create(Vec3.create(texDimX, texDimY, 0)),
+        uGridDim: ValueCell.create(gridDim),
+        uGridTexDim: ValueCell.create(gridTexDim),
+        uGridTexScale: ValueCell.create(gridTexScale),
         uAlpha: ValueCell.create(smoothness),
+        uResolution: ValueCell.create(resolution),
         tMinDistanceTex: ValueCell.create(minDistanceTexture),
 
         dGridTexType: ValueCell.create(minDistanceTexture.depth > 0 ? '3d' : '2d'),
-        dCalcType: ValueCell.create('density'),
-    }
-    const state: RenderableState = {
-        visible: true,
-        alphaFactor: 1,
-        pickable: false,
-        opaque: true
+        dCalcType: ValueCell.create('minDistance'),
     }
 
-    const renderObject = createRenderObject('gaussian-density', values, state)
+    const schema = { ...GaussianDensitySchema }
+    const shaderCode = GaussianDensityShaderCode
+    const renderItem =  createComputeRenderItem(webgl, 'points', shaderCode, schema, values)
 
-    return renderObject
+    return createComputeRenderable(renderItem, values)
 }
 
-function setRenderingDefaults(gl: GLRenderingContext) {
-    gl.disable(gl.CULL_FACE)
-    gl.frontFace(gl.CCW)
-    gl.cullFace(gl.BACK)
-    gl.enable(gl.BLEND)
+function setRenderingDefaults(ctx: WebGLContext) {
+    const { gl, state } = ctx
+    state.disable(gl.CULL_FACE)
+    state.enable(gl.BLEND)
+    state.disable(gl.DEPTH_TEST)
+    state.disable(gl.SCISSOR_TEST)
+    state.depthMask(false)
+    state.clearColor(0, 0, 0, 0)
 }
 
-function setupMinDistanceRendering(webgl: WebGLContext, renderable: Renderable<any>) {
-    const { gl } = webgl
+function setupMinDistanceRendering(webgl: WebGLContext, renderable: ComputeRenderable<any>) {
+    const { gl, state } = webgl
     ValueCell.update(renderable.values.dCalcType, 'minDistance')
     renderable.update()
-    renderable.getProgram('draw').use()
-    gl.blendFunc(gl.ONE, gl.ONE)
+    state.colorMask(false, false, false, true)
+    state.blendFunc(gl.ONE, gl.ONE)
     // the shader writes 1 - dist so we set blending to MAX
-    gl.blendEquation(webgl.extensions.blendMinMax.MAX)
+    state.blendEquation(webgl.extensions.blendMinMax.MAX)
 }
 
-function setupDensityRendering(webgl: WebGLContext, renderable: Renderable<any>) {
-    const { gl } = webgl
+function setupDensityRendering(webgl: WebGLContext, renderable: ComputeRenderable<any>) {
+    const { gl, state } = webgl
     ValueCell.update(renderable.values.dCalcType, 'density')
     renderable.update()
-    renderable.getProgram('draw').use()
-    gl.blendFunc(gl.ONE, gl.ONE)
-    gl.blendEquation(gl.FUNC_ADD)
+    state.colorMask(false, false, false, true)
+    state.blendFunc(gl.ONE, gl.ONE)
+    // state.colorMask(true, true, true, true)
+    // state.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ONE)
+    state.blendEquation(gl.FUNC_ADD)
 }
 
-function setupGroupIdRendering(webgl: WebGLContext, renderable: Renderable<any>) {
-    const { gl } = webgl
+function setupGroupIdRendering(webgl: WebGLContext, renderable: ComputeRenderable<any>) {
+    const { gl, state } = webgl
     ValueCell.update(renderable.values.dCalcType, 'groupId')
     renderable.update()
-    renderable.getProgram('draw').use()
     // overwrite color, don't change alpha
-    gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ZERO, gl.ONE)
-    gl.blendEquation(gl.FUNC_ADD)
+    state.colorMask(true, true, true, false)
+    state.blendFunc(gl.ONE, gl.ZERO)
+    state.blendEquation(gl.FUNC_ADD)
 }
 
-function getTexture2dSize(maxTexSize: number, gridDim: Vec3) {
+function getTexture2dSize(gridDim: Vec3) {
+    const area = gridDim[0] * gridDim[1] * gridDim[2]
+    const squareDim = Math.sqrt(area)
+    const powerOfTwoSize = Math.pow(2, Math.ceil(Math.log(squareDim) / Math.log(2)))
+
     let texDimX = 0
     let texDimY = gridDim[1]
     let texRows = 1
     let texCols = gridDim[2]
-    if (maxTexSize < gridDim[0] * gridDim[2]) {
-        texCols = Math.floor(maxTexSize / gridDim[0])
+    if (powerOfTwoSize < gridDim[0] * gridDim[2]) {
+        texCols = Math.floor(powerOfTwoSize / gridDim[0])
         texRows = Math.ceil(gridDim[2] / texCols)
         texDimX = texCols * gridDim[0]
         texDimY *= texRows
     } else {
         texDimX = gridDim[0] * gridDim[2]
     }
-    return { texDimX, texDimY, texRows, texCols }
+    return { texDimX, texDimY, texRows, texCols, powerOfTwoSize: texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 }
 }
 
-async function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3) {
+export function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3, texDim: Vec3) {
     // console.time('fieldFromTexture2d')
     const { framebufferCache } = ctx
     const [ dx, dy, dz ] = dim
-    const { width, height } = texture
+    // const { width, height } = texture
+    const [ width, height ] = texDim
     const fboTexCols = Math.floor(width / dx)
 
     const space = Tensor.Space(dim, [2, 1, 0], Float32Array)
@@ -307,16 +364,15 @@ async function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3
     const idData = space.create()
     const idField = Tensor.create(space, idData)
 
-    const image = new Uint8Array(width * height * 4)
+    // const image = new Uint8Array(width * height * 4)
+    const image = new Float32Array(width * height * 4)
 
-    const framebuffer = framebufferCache.get(ctx, FramebufferName).value
+    const framebuffer = framebufferCache.get(FramebufferName).value
     framebuffer.bind()
     texture.attachFramebuffer(framebuffer, 0)
-    // TODO too slow, why? Too many checks if gpu ready???
-    // await ctx.readPixelsAsync(0, 0, width, height, image)
     ctx.readPixels(0, 0, width, height, image)
 
-    // debugTexture(createImageData(image, width, height), 1/3)
+    // printImageData(createImageData(image, width, height), 1/3)
 
     let j = 0
     let tmpCol = 0
@@ -329,8 +385,8 @@ async function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3
         for (let iy = 0; iy < dy; ++iy) {
             for (let ix = 0; ix < dx; ++ix) {
                 const idx = 4 * (tmpCol * dx + (iy + tmpRow) * width + ix)
-                data[j] = image[idx + 3] / 255
-                idData[j] = decodeFloatRGB(image[idx], image[idx + 1], image[idx + 2])
+                data[j] = image[idx + 3] // / 255
+                idData[j] = decodeFloatRGB(image[idx] * 255, image[idx + 1] * 255, image[idx + 2] * 255)
                 j++
             }
         }
diff --git a/src/mol-math/geometry/lookup3d/common.ts b/src/mol-math/geometry/lookup3d/common.ts
index 874735b9ca2659be52324834350cc595f5bc5024..924274b8b0f520a3e1bfb5e6aeab36b7889670bd 100644
--- a/src/mol-math/geometry/lookup3d/common.ts
+++ b/src/mol-math/geometry/lookup3d/common.ts
@@ -33,4 +33,6 @@ export interface Lookup3D<T = number> {
     find(x: number, y: number, z: number, radius: number): Result<T>,
     check(x: number, y: number, z: number, radius: number): boolean,
     readonly boundary: { readonly box: Box3D, readonly sphere: Sphere3D }
+    /** transient result */
+    readonly result: Result<T>
 }
\ No newline at end of file
diff --git a/src/mol-math/geometry/lookup3d/grid.ts b/src/mol-math/geometry/lookup3d/grid.ts
index 5270a27600e482edf597e63ec2b821116a16ab47..328f2d00aafa2f6c4e2a2edf0280a08ac2bb8473 100644
--- a/src/mol-math/geometry/lookup3d/grid.ts
+++ b/src/mol-math/geometry/lookup3d/grid.ts
@@ -27,6 +27,7 @@ class GridLookup3DImpl implements GridLookup3D<number> {
     private ctx: QueryContext;
     boundary: Lookup3D['boundary'];
     buckets: GridLookup3D['buckets'];
+    result: Result<number>
 
     find(x: number, y: number, z: number, radius: number): Result<number> {
         this.ctx.x = x;
@@ -52,6 +53,7 @@ class GridLookup3DImpl implements GridLookup3D<number> {
         this.ctx = createContext(structure);
         this.boundary = { box: structure.boundingBox, sphere: structure.boundingSphere };
         this.buckets = { offset: structure.bucketOffset, count: structure.bucketCounts, array: structure.bucketArray };
+        this.result = this.ctx.result
     }
 }
 
diff --git a/src/mol-math/geometry/molecular-surface.ts b/src/mol-math/geometry/molecular-surface.ts
new file mode 100644
index 0000000000000000000000000000000000000000..326e863c4e0fbc1ab9626862427eba9e087ea4a3
--- /dev/null
+++ b/src/mol-math/geometry/molecular-surface.ts
@@ -0,0 +1,366 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Fred Ludlow <fred.ludlow@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * ported from NGL (https://github.com/arose/ngl), licensed under MIT
+ */
+
+import { fillUniform } from 'mol-util/array';
+import { Vec3, Tensor } from 'mol-math/linear-algebra';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { RuntimeContext } from 'mol-task';
+import { OrderedSet } from 'mol-data/int';
+import { PositionData } from './common';
+import { Mat4 } from 'mol-math/linear-algebra/3d';
+import { Box3D, GridLookup3D, fillGridDim } from 'mol-math/geometry';
+
+function normalToLine (out: Vec3, p: Vec3) {
+    out[0] = out[1] = out[2] = 1.0
+    if (p[0] !== 0) {
+        out[0] = (p[1] + p[2]) / -p[0]
+    } else if (p[1] !== 0) {
+        out[1] = (p[0] + p[2]) / -p[1]
+    } else if (p[2] !== 0) {
+        out[2] = (p[0] + p[1]) / -p[2]
+    }
+    return out
+}
+
+type AnglesTables = { cosTable: Float32Array, sinTable: Float32Array }
+function getAngleTables (probePositions: number): AnglesTables {
+    let theta = 0.0
+    const step = 2 * Math.PI / probePositions
+
+    const cosTable = new Float32Array(probePositions)
+    const sinTable = new Float32Array(probePositions)
+    for (let i = 0; i < probePositions; i++) {
+        cosTable[i] = Math.cos(theta)
+        sinTable[i] = Math.sin(theta)
+        theta += step
+    }
+    return { cosTable, sinTable}
+}
+
+//
+
+export const MolecularSurfaceCalculationParams = {
+    resolution: PD.Numeric(0.5, { min: 0.01, max: 20, step: 0.01 }),
+    probeRadius: PD.Numeric(1.4, { min: 0, max: 10, step: 0.1 }),
+    probePositions: PD.Numeric(30, { min: 12, max: 90, step: 1 }),
+}
+export const DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(MolecularSurfaceCalculationParams)
+export type MolecularSurfaceCalculationProps = typeof DefaultMolecularSurfaceCalculationProps
+
+
+export async function calcMolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number,  props: MolecularSurfaceCalculationProps) {
+    // Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
+    // Other parts based heavily on NGL (Alexander Rose) EDT Surface class
+
+    let lastClip = -1
+
+    /**
+     * Is the point at x,y,z obscured by any of the atoms specifeid by indices in neighbours.
+     * Ignore indices a and b (these are the relevant atoms in projectPoints/Torii)
+     *
+     * Cache the last clipped atom (as very often the same one in subsequent calls)
+     *
+     * `a` and `b` must be resolved indices
+     */
+    function obscured(x: number, y: number, z: number, a: number, b: number) {
+        if (lastClip !== -1) {
+            const ai = lastClip
+            if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
+                return ai
+            } else {
+                lastClip = -1
+            }
+        }
+
+        for (let j = 0, jl = neighbours.count; j < jl; ++j) {
+            const ai = OrderedSet.getAt(position.indices, neighbours.indices[j])
+            if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
+                lastClip = ai
+                return ai
+            }
+        }
+
+        return -1
+    }
+
+    /**
+     * `ai` must be a resolved index
+     */
+    function singleAtomObscures(ai: number, x: number, y: number, z: number) {
+        const r = radius[ai]
+        const dx = px[ai] - x
+        const dy = py[ai] - y
+        const dz = pz[ai] - z
+        const dSq = dx * dx + dy * dy + dz * dz
+        return dSq < (r * r)
+    }
+
+    /**
+     * For each atom:
+     *     Iterate over a subsection of the grid, for each point:
+     *         If current value < 0.0, unvisited, set positive
+     *
+     *         In any case: Project this point onto surface of the atomic sphere
+     *         If this projected point is not obscured by any other atom
+     *             Calculate delta distance and set grid value to minimum of
+     *             itself and delta
+     */
+    function projectPointsRange (begI: number, endI: number) {
+        for (let i = begI; i < endI; ++i) {
+            const j = OrderedSet.getAt(indices, i)
+            const vx = px[j], vy = py[j], vz = pz[j]
+            const rad = radius[j]
+            const rSq = rad * rad
+
+            lookup3d.find(vx, vy, vz, rad)
+
+            // Number of grid points, round this up...
+            const ng = Math.ceil(rad * scaleFactor)
+
+            // Center of the atom, mapped to grid points (take floor)
+            const iax = Math.floor(scaleFactor * (vx - minX))
+            const iay = Math.floor(scaleFactor * (vy - minY))
+            const iaz = Math.floor(scaleFactor * (vz - minZ))
+
+            // Extents of grid to consider for this atom
+            const begX = Math.max(0, iax - ng)
+            const begY = Math.max(0, iay - ng)
+            const begZ = Math.max(0, iaz - ng)
+
+            // Add two to these points:
+            // - iax are floor'd values so this ensures coverage
+            // - these are loop limits (exclusive)
+            const endX = Math.min(dimX, iax + ng + 2)
+            const endY = Math.min(dimY, iay + ng + 2)
+            const endZ = Math.min(dimZ, iaz + ng + 2)
+
+            for (let xi = begX; xi < endX; ++xi) {
+                const dx = gridx[xi] - vx
+                const xIdx = xi * iuv
+                for (let yi = begY; yi < endY; ++yi) {
+                    const dy = gridy[yi] - vy
+                    const dxySq = dx * dx + dy * dy
+                    const xyIdx = yi * iu + xIdx
+                    for (let zi = begZ; zi < endZ; ++zi) {
+                        const dz = gridz[zi] - vz
+                        const dSq = dxySq + dz * dz
+
+                        if (dSq < rSq) {
+                            const idx = zi + xyIdx
+
+                            // if unvisited, make positive
+                            if (data[idx] < 0.0) data[idx] *= -1
+
+                            // Project on to the surface of the sphere
+                            // sp is the projected point ( dx, dy, dz ) * ( ra / d )
+                            const d = Math.sqrt(dSq)
+                            const ap = rad / d
+                            const spx = dx * ap + vx
+                            const spy = dy * ap + vy
+                            const spz = dz * ap + vz
+
+                            if (obscured(spx, spy, spz, j, -1) === -1) {
+                                const dd = rad - d
+                                if (dd < data[idx]) {
+                                    data[idx] = dd
+                                    idData[idx] = i
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    async function projectPoints() {
+        for (let i = 0; i < n; i += updateChunk) {
+            projectPointsRange(i, Math.min(i + updateChunk, n))
+
+            if (ctx.shouldUpdate) {
+                await ctx.update({ message: 'projecting points', current: i, max: n })
+            }
+        }
+    }
+
+    // Vectors for Torus Projection
+    const atob = Vec3()
+    const mid = Vec3()
+    const n1 = Vec3()
+    const n2 = Vec3()
+    /**
+     * `a` and `b` must be resolved indices
+     */
+    function projectTorus(a: number, b: number) {
+        const rA = radius[a]
+        const rB = radius[b]
+        const dx = atob[0] = px[b] - px[a]
+        const dy = atob[1] = py[b] - py[a]
+        const dz = atob[2] = pz[b] - pz[a]
+        const dSq = dx * dx + dy * dy + dz * dz
+
+        // This check now redundant as already done in AVHash.withinRadii
+        // if (dSq > ((rA + rB) * (rA + rB))) { return }
+
+        const d = Math.sqrt(dSq)
+
+        // Find angle between a->b vector and the circle
+        // of their intersection by cosine rule
+        const cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d)
+
+        // distance along a->b at intersection
+        const dmp = rA * cosA
+
+        Vec3.normalize(atob, atob)
+
+        // Create normal to line
+        normalToLine(n1, atob)
+        Vec3.normalize(n1, n1)
+
+        // Cross together for second normal vector
+        Vec3.cross(n2, atob, n1)
+        Vec3.normalize(n2, n2)
+
+        // r is radius of circle of intersection
+        const rInt = Math.sqrt(rA * rA - dmp * dmp)
+
+        Vec3.scale(n1, n1, rInt)
+        Vec3.scale(n2, n2, rInt)
+        Vec3.scale(atob, atob, dmp)
+
+        mid[0] = atob[0] + px[a]
+        mid[1] = atob[1] + py[a]
+        mid[2] = atob[2] + pz[a]
+
+        lastClip = -1
+
+        for (let i = 0; i < probePositions; ++i) {
+            const cost = cosTable[i]
+            const sint = sinTable[i]
+
+            const px = mid[0] + cost * n1[0] + sint * n2[0]
+            const py = mid[1] + cost * n1[1] + sint * n2[1]
+            const pz = mid[2] + cost * n1[2] + sint * n2[2]
+
+            if (obscured(px, py, pz, a, b) === -1) {
+                const iax = Math.floor(scaleFactor * (px - minX))
+                const iay = Math.floor(scaleFactor * (py - minY))
+                const iaz = Math.floor(scaleFactor * (pz - minZ))
+
+                const begX = Math.max(0, iax - ngTorus)
+                const begY = Math.max(0, iay - ngTorus)
+                const begZ = Math.max(0, iaz - ngTorus)
+
+                const endX = Math.min(dimX, iax + ngTorus + 2)
+                const endY = Math.min(dimY, iay + ngTorus + 2)
+                const endZ = Math.min(dimZ, iaz + ngTorus + 2)
+
+                for (let xi = begX; xi < endX; ++xi) {
+                    const dx = px - gridx[xi]
+                    const xIdx = xi * iuv
+
+                    for (let yi = begY; yi < endY; ++yi) {
+                        const dy = py - gridy[yi]
+                        const dxySq = dx * dx + dy * dy
+                        const xyIdx = yi * iu + xIdx
+
+                        for (let zi = begZ; zi < endZ; ++zi) {
+                            const dz = pz - gridz[zi]
+                            const dSq = dxySq + dz * dz
+
+                            const idx = zi + xyIdx
+                            const current = data[idx]
+
+                            if (current > 0.0 && dSq < (current * current)) {
+                                data[idx] = Math.sqrt(dSq)
+                                // Is this grid point closer to a or b?
+                                // Take dot product of atob and gridpoint->p (dx, dy, dz)
+                                const dp = dx * atob[0] + dy * atob[1] + dz * atob[2]
+                                idData[idx] = OrderedSet.indexOf(position.indices, dp < 0.0 ? b : a)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    async function projectTorii () {
+        for (let i = 0; i < n; ++i) {
+            const k = OrderedSet.getAt(indices, i)
+            lookup3d.find(px[k], py[k], pz[k], radius[k])
+            for (let j = 0, jl = neighbours.count; j < jl; ++j) {
+                const l = OrderedSet.getAt(indices, neighbours.indices[j])
+                if (k < l) projectTorus(k, l)
+            }
+
+            if (i % updateChunk === 0 && ctx.shouldUpdate) {
+                await ctx.update({ message: 'projecting torii', current: i, max: n })
+            }
+        }
+    }
+
+    // console.time('MolecularSurface')
+    // console.time('MolecularSurface createState')
+    const { resolution, probeRadius, probePositions } = props
+    const scaleFactor = 1 / resolution
+    const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor))
+
+    const cellSize =  Vec3.create(maxRadius, maxRadius, maxRadius)
+    Vec3.scale(cellSize, cellSize, 2)
+    const lookup3d = GridLookup3D(position, cellSize)
+    const neighbours = lookup3d.result
+    const box = lookup3d.boundary.box
+    const { indices, x: px, y: py, z: pz, radius } = position
+    const n = OrderedSet.size(indices)
+
+    const pad = maxRadius * 2 + resolution
+    const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad));
+    const [ minX, minY, minZ ] = expandedBox.min
+    const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor)
+    const dim = Box3D.size(Vec3(), scaledBox)
+    Vec3.ceil(dim, dim)
+
+    const [ dimX, dimY, dimZ ] = dim
+    const iu = dimZ, iv = dimY, iuv = iu * iv
+
+    const { cosTable, sinTable } = getAngleTables(probePositions)
+
+    const space = Tensor.Space(dim, [0, 1, 2], Float32Array)
+    const data = space.create()
+    const idData = space.create()
+
+    fillUniform(data, -1001.0)
+    fillUniform(idData, -1)
+
+    const gridx = fillGridDim(dimX, minX, resolution)
+    const gridy = fillGridDim(dimY, minY, resolution)
+    const gridz = fillGridDim(dimZ, minZ, resolution)
+
+    const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor)))
+    // console.timeEnd('MolecularSurface createState')
+
+    // console.time('MolecularSurface projectPoints')
+    await projectPoints()
+    // console.timeEnd('MolecularSurface projectPoints')
+
+    // console.time('MolecularSurface projectTorii')
+    await projectTorii()
+    // console.timeEnd('MolecularSurface projectTorii')
+    // console.timeEnd('MolecularSurface')
+
+    const field = Tensor.create(space, data)
+    const idField = Tensor.create(space, idData)
+
+    const transform = Mat4.identity()
+    Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution))
+    Mat4.setTranslation(transform, expandedBox.min)
+    // console.log({ field, idField, transform, updateChunk })
+    return { field, idField, transform }
+}
\ No newline at end of file
diff --git a/src/mol-math/geometry/primitives/box3d.ts b/src/mol-math/geometry/primitives/box3d.ts
index 83c352cb5c64a908ef2b98997b9e28946e73c9ca..90fd7e49e5fa527eb21499cf4a7569104ef90329 100644
--- a/src/mol-math/geometry/primitives/box3d.ts
+++ b/src/mol-math/geometry/primitives/box3d.ts
@@ -11,9 +11,13 @@ import { OrderedSet } from 'mol-data/int';
 
 interface Box3D { min: Vec3, max: Vec3 }
 
+function Box3D() {
+    return Box3D.empty();
+}
+
 namespace Box3D {
     export function create(min: Vec3, max: Vec3): Box3D { return { min, max }; }
-    export function empty(): Box3D { return { min: Vec3.zero(), max: Vec3.zero() }; }
+    export function empty(): Box3D { return { min: Vec3(), max: Vec3() }; }
 
     export function clone(a: Box3D): Box3D {
         const out = empty();
@@ -39,12 +43,12 @@ namespace Box3D {
         return { min, max }
     }
 
-    /** Get size of the box */
+    /** Get size/extent of the box */
     export function size(size: Vec3, box: Box3D): Vec3 {
         return Vec3.sub(size, box.max, box.min);
     }
 
-    const tmpSizeV = Vec3.zero()
+    const tmpSizeV = Vec3()
     /** Get size of the box */
     export function volume(box: Box3D): number {
         size(tmpSizeV, box)
@@ -71,7 +75,13 @@ namespace Box3D {
         return out
     }
 
-    const tmpTransformV = Vec3.zero()
+    export function scale(out: Box3D, box: Box3D, scale: number) {
+        Vec3.scale(out.min, box.min, scale)
+        Vec3.scale(out.max, box.max, scale)
+        return out;
+    }
+
+    const tmpTransformV = Vec3()
     /** Transform box with a Mat4 */
     export function transform(out: Box3D, box: Box3D, m: Mat4): Box3D {
         const [ minX, minY, minZ ] = box.min
diff --git a/src/mol-math/geometry/primitives/sphere3d.ts b/src/mol-math/geometry/primitives/sphere3d.ts
index 94c9593f9889b9a68b694cb71968a2cfc221d762..9df2560a973962dc4aa8d74c8ec994b041102260 100644
--- a/src/mol-math/geometry/primitives/sphere3d.ts
+++ b/src/mol-math/geometry/primitives/sphere3d.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -9,9 +9,14 @@ import { Vec3, Mat4, EPSILON } from '../../linear-algebra'
 import { PositionData } from '../common'
 import { OrderedSet } from 'mol-data/int';
 import { NumberArray } from 'mol-util/type-helpers';
+import { Box3D } from './box3d';
 
 interface Sphere3D { center: Vec3, radius: number }
 
+function Sphere3D() {
+    return Sphere3D.zero();
+}
+
 namespace Sphere3D {
     export function create(center: Vec3, radius: number): Sphere3D { return { center, radius }; }
     export function zero(): Sphere3D { return { center: Vec3.zero(), radius: 0 }; }
@@ -76,12 +81,31 @@ namespace Sphere3D {
         return out
     }
 
-    export function addSphere(out: Sphere3D, sphere: Sphere3D) {
+    export function fromBox3D(out: Sphere3D, box: Box3D) {
+        Vec3.scale(out.center, Vec3.add(out.center, box.max, box.min), 0.5)
+        out.radius = Vec3.distance(out.center, box.max)
+        return out
+    }
+
+    const tmpAddVec3 = Vec3()
+    export function addVec3(out: Sphere3D, s: Sphere3D, v: Vec3) {
+        const d = Vec3.distance(s.center, v)
+        if (d < s.radius) return Sphere3D.copy(out, s)
+        Vec3.sub(tmpAddVec3, s.center, v)
+        Vec3.sub(tmpAddVec3, s.center, tmpAddVec3)
+        Vec3.setMagnitude(tmpAddVec3, tmpAddVec3, s.radius)
+        Vec3.scale(out.center, Vec3.add(tmpAddVec3, tmpAddVec3, v), 0.5)
+        out.radius = Vec3.distance(out.center, v)
+        return out
+    }
+
+    /** Expand sphere radius by another sphere */
+    export function expandBySphere(out: Sphere3D, sphere: Sphere3D) {
         out.radius = Math.max(out.radius, Vec3.distance(out.center, sphere.center) + sphere.radius)
         return out
     }
 
-    /** Expand sphere by delta */
+    /** Expand sphere radius by delta */
     export function expand(out: Sphere3D, sphere: Sphere3D, delta: number): Sphere3D {
         out.radius = sphere.radius + delta
         return out
diff --git a/src/mol-math/geometry/symmetry-operator.ts b/src/mol-math/geometry/symmetry-operator.ts
index 44ba2975516385dcf35260e7eddc971dd48c80b6..6f3a71fe52e0802fe89cc25d1014ed6d2e707567 100644
--- a/src/mol-math/geometry/symmetry-operator.ts
+++ b/src/mol-math/geometry/symmetry-operator.ts
@@ -5,6 +5,7 @@
  */
 
 import { Vec3, Mat4, Mat3, Quat } from '../linear-algebra/3d'
+import { lerp as scalar_lerp } from 'mol-math/interpolate';
 
 interface SymmetryOperator {
     readonly name: string,
@@ -64,7 +65,7 @@ namespace SymmetryOperator {
         return create(name, t, { id: '', operList: [] }, ncsId);
     }
 
-    const _q1 = Quat.identity(), _q2 = Quat.zero(), _axis = Vec3.zero();
+    const _q1 = Quat.identity(), _q2 = Quat.zero(), _q3 = Quat.zero(), _axis = Vec3.zero();
     export function lerpFromIdentity(out: Mat4, op: SymmetryOperator, t: number): Mat4 {
         const m = op.inverse;
         if (op.isIdentity) return Mat4.copy(out, m);
@@ -84,6 +85,25 @@ namespace SymmetryOperator {
         return out;
     }
 
+    export function slerp(out: Mat4, src: Mat4, tar: Mat4, t: number): Mat4 {
+        if (Math.abs(t) <= 0.00001) return Mat4.copy(out, src);
+        if (Math.abs(t - 1) <= 0.00001) return Mat4.copy(out, tar);
+
+        // interpolate rotation
+        Mat4.getRotation(_q2, src);
+        Mat4.getRotation(_q3, tar);
+        Quat.slerp(_q3, _q2, _q3, t);
+        const angle = Quat.getAxisAngle(_axis, _q3);
+        Mat4.fromRotation(out, angle, _axis);
+
+        // interpolate translation
+        Mat4.setValue(out, 0, 3, scalar_lerp(Mat4.getValue(src, 0, 3), Mat4.getValue(tar, 0, 3), t));
+        Mat4.setValue(out, 1, 3, scalar_lerp(Mat4.getValue(src, 1, 3), Mat4.getValue(tar, 1, 3), t));
+        Mat4.setValue(out, 2, 3, scalar_lerp(Mat4.getValue(src, 2, 3), Mat4.getValue(tar, 2, 3), t));
+
+        return out;
+    }
+
     /**
      * Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix).
      * Keep `name`, `assembly`, `ncsId` and `hkl` properties from second.
diff --git a/src/mol-math/linear-algebra/3d/mat3.ts b/src/mol-math/linear-algebra/3d/mat3.ts
index a646193d7d6ecc7403bde52b2ec097610169e964..a7bb2cfdb84a55208b2292e2751a11d03484f297 100644
--- a/src/mol-math/linear-algebra/3d/mat3.ts
+++ b/src/mol-math/linear-algebra/3d/mat3.ts
@@ -22,6 +22,10 @@ import { NumberArray } from 'mol-util/type-helpers';
 
 interface Mat3 extends Array<number> { [d: number]: number, '@type': 'mat3', length: 9 }
 
+function Mat3() {
+    return Mat3.zero();
+}
+
 namespace Mat3 {
     export function zero(): Mat3 {
         // force double backing array by 0.1.
diff --git a/src/mol-math/linear-algebra/3d/mat4.ts b/src/mol-math/linear-algebra/3d/mat4.ts
index c8a307e8de9a57ab893dab678ce1bff9d1203076..2bc177fab268d0ba36614a7811ef140ef8a4cdc3 100644
--- a/src/mol-math/linear-algebra/3d/mat4.ts
+++ b/src/mol-math/linear-algebra/3d/mat4.ts
@@ -1009,28 +1009,32 @@ namespace Mat4 {
         return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq))
     }
 
+    const xAxis = Vec3.create(1, 0, 0)
+    const yAxis = Vec3.create(1, 0, 0)
+    const zAxis = Vec3.create(1, 0, 0)
+
     /** Rotation matrix for 90deg around x-axis */
-    export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(1, 0, 0))
+    export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), xAxis)
     /** Rotation matrix for 180deg around x-axis */
-    export const rotX180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(1, 0, 0))
+    export const rotX180: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(180), xAxis)
     /** Rotation matrix for 90deg around y-axis */
-    export const rotY90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(0, 1, 0))
+    export const rotY90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), yAxis)
     /** Rotation matrix for 180deg around y-axis */
-    export const rotY180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(0, 1, 0))
+    export const rotY180: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(180), yAxis)
     /** Rotation matrix for 90deg around z-axis */
-    export const rotZ90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(0, 0, 1))
+    export const rotZ90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), zAxis)
     /** Rotation matrix for 180deg around z-axis */
-    export const rotZ180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(0, 0, 1))
+    export const rotZ180: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(180), zAxis)
     /** Rotation matrix for 90deg around first x-axis and then y-axis */
-    export const rotXY90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotX90, rotY90)
+    export const rotXY90: ReadonlyMat4 = Mat4.mul(Mat4(), rotX90, rotY90)
     /** Rotation matrix for 90deg around first z-axis and then y-axis */
-    export const rotZY90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZ90, rotY90)
+    export const rotZY90: ReadonlyMat4 = Mat4.mul(Mat4(), rotZ90, rotY90)
     /** Rotation matrix for 90deg around first z-axis and then y-axis and then z-axis */
-    export const rotZYZ90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZY90, rotZ90)
+    export const rotZYZ90: ReadonlyMat4 = Mat4.mul(Mat4(), rotZY90, rotZ90)
     /** Rotation matrix for 90deg around first z-axis and then 180deg around x-axis */
-    export const rotZ90X180: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZ90, rotX180)
+    export const rotZ90X180: ReadonlyMat4 = Mat4.mul(Mat4(), rotZ90, rotX180)
     /** Rotation matrix for 90deg around first y-axis and then 180deg around z-axis */
-    export const rotY90Z180: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotY90, rotZ180)
+    export const rotY90Z180: ReadonlyMat4 = Mat4.mul(Mat4(), rotY90, rotZ180)
 }
 
 export default Mat4
\ No newline at end of file
diff --git a/src/mol-math/linear-algebra/3d/quat.ts b/src/mol-math/linear-algebra/3d/quat.ts
index 59e68c88bd5fe70b02d7e19bbc4cac22ed614956..abbe498e2a93943273156d6f2142143783553921 100644
--- a/src/mol-math/linear-algebra/3d/quat.ts
+++ b/src/mol-math/linear-algebra/3d/quat.ts
@@ -276,7 +276,7 @@ namespace Quat {
         return out;
     }
 
-    const fromUnitVec3Temp = Vec3.zero()
+    const fromUnitVec3Temp = Vec3()
     /** Quaternion from two normalized unit vectors. */
     export function fromUnitVec3 (out: Quat, a: Vec3, b: Vec3) {
         // assumes a and b are normalized
@@ -374,7 +374,7 @@ namespace Quat {
      *
      * Both vectors are assumed to be unit length.
      */
-    const rotTmpVec3 = Vec3.zero();
+    const rotTmpVec3 = Vec3();
     const rotTmpVec3UnitX = Vec3.create(1, 0, 0);
     const rotTmpVec3UnitY = Vec3.create(0, 1, 0);
     export function rotationTo(out: Quat, a: Vec3, b: Vec3) {
@@ -405,8 +405,8 @@ namespace Quat {
     /**
      * Performs a spherical linear interpolation with two control points
      */
-    let sqlerpTemp1 = Quat.zero();
-    let sqlerpTemp2 = Quat.zero();
+    let sqlerpTemp1 = Quat();
+    let sqlerpTemp2 = Quat();
     export function sqlerp(out: Quat, a: Quat, b: Quat, c: Quat, d: Quat, t: number) {
         slerp(sqlerpTemp1, a, d, t);
         slerp(sqlerpTemp2, b, c, t);
@@ -419,7 +419,7 @@ namespace Quat {
      * axes. Each axis is a vec3 and is expected to be unit length and
      * perpendicular to all other specified axes.
      */
-    const axesTmpMat = Mat3.zero();
+    const axesTmpMat = Mat3();
     export function setAxes(out: Quat, view: Vec3, right: Vec3, up: Vec3) {
         axesTmpMat[0] = right[0];
         axesTmpMat[3] = right[1];
diff --git a/src/mol-math/linear-algebra/3d/vec2.ts b/src/mol-math/linear-algebra/3d/vec2.ts
index be7b7cdf93b52dac9b90fca247f42f75485fd383..65a32c3ff61149bd8a9c3e606ee67ab3c3e1b5b3 100644
--- a/src/mol-math/linear-algebra/3d/vec2.ts
+++ b/src/mol-math/linear-algebra/3d/vec2.ts
@@ -20,6 +20,10 @@ import { NumberArray } from 'mol-util/type-helpers';
 
 interface Vec2 extends Array<number> { [d: number]: number, '@type': 'vec2', length: 2 }
 
+function Vec2() {
+    return Vec2.zero();
+}
+
 namespace Vec2 {
     export function zero(): Vec2 {
         // force double backing array by 0.1.
diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts
index 3e8fd5d5ee7765fcb88215a1455655b75357b10e..982d323f1933cb5e9ebf17fc1b50c452dfc6581e 100644
--- a/src/mol-math/linear-algebra/3d/vec3.ts
+++ b/src/mol-math/linear-algebra/3d/vec3.ts
@@ -24,6 +24,10 @@ import { NumberArray } from 'mol-util/type-helpers';
 
 interface Vec3 extends Array<number> { [d: number]: number, '@type': 'vec3', length: 3 }
 
+function Vec3() {
+    return Vec3.zero();
+}
+
 namespace Vec3 {
     export function zero(): Vec3 {
         const out = [0.1, 0.0, 0.0];
@@ -43,6 +47,13 @@ namespace Vec3 {
         return isNaN(a[0]) || isNaN(a[1]) || isNaN(a[2])
     }
 
+    export function setNaN(out: Vec3) {
+        out[0] = NaN;
+        out[1] = NaN;
+        out[2] = NaN;
+        return out
+    }
+
     export function fromObj(v: { x: number, y: number, z: number }): Vec3 {
         return create(v.x, v.y, v.z);
     }
@@ -414,6 +425,7 @@ namespace Vec3 {
     }
 
     const angleTempA = zero(), angleTempB = zero();
+    /** Computes the angle between 2 vectors, reports in rad. */
     export function angle(a: Vec3, b: Vec3) {
         copy(angleTempA, a);
         copy(angleTempB, b);
@@ -433,6 +445,30 @@ namespace Vec3 {
         }
     }
 
+    const tmp_dh_ab = zero();
+    const tmp_dh_cb = zero();
+    const tmp_dh_bc = zero();
+    const tmp_dh_dc = zero();
+    const tmp_dh_abc = zero();
+    const tmp_dh_bcd = zero();
+    const tmp_dh_cross = zero();
+    /**
+     * Computes the dihedral angles of 4 points.
+     */
+    export function dihedralAngle(a: Vec3, b: Vec3, c: Vec3, d: Vec3): number {
+        sub(tmp_dh_ab, a, b);
+        sub(tmp_dh_cb, c, b);
+        sub(tmp_dh_bc, b, c);
+        sub(tmp_dh_dc, d, c);
+
+        cross(tmp_dh_abc, tmp_dh_ab, tmp_dh_cb);
+        cross(tmp_dh_bcd, tmp_dh_bc, tmp_dh_dc);
+
+        const _angle = angle(tmp_dh_abc, tmp_dh_bcd) * 360.0 / (2 * Math.PI);
+        cross(tmp_dh_cross, tmp_dh_abc, tmp_dh_bcd);
+        return dot(tmp_dh_cb, tmp_dh_cross) > 0 ? _angle : -_angle;
+    }
+
     /**
      * Returns whether or not the vectors have exactly the same elements in the same position (when compared with ===)
      */
diff --git a/src/mol-math/linear-algebra/3d/vec4.ts b/src/mol-math/linear-algebra/3d/vec4.ts
index c7c4e3380250572a684ce87ce4bdbd90ee8d19df..35346760ee95d740d18a346211f7a2d1920919d7 100644
--- a/src/mol-math/linear-algebra/3d/vec4.ts
+++ b/src/mol-math/linear-algebra/3d/vec4.ts
@@ -23,6 +23,10 @@ import { NumberArray } from 'mol-util/type-helpers';
 
 interface Vec4 extends Array<number> { [d: number]: number, '@type': 'vec4', length: 4 }
 
+function Vec4() {
+    return Vec4.zero();
+}
+
 namespace Vec4 {
     export function zero(): Vec4 {
         // force double backing array by 0.1.
diff --git a/src/mol-math/linear-algebra/_spec/vec3.spec.ts b/src/mol-math/linear-algebra/_spec/vec3.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb98228acaf7b051186c8dd54cc985d81159ce52
--- /dev/null
+++ b/src/mol-math/linear-algebra/_spec/vec3.spec.ts
@@ -0,0 +1,14 @@
+import { Vec3 } from '../3d'
+
+describe('vec3', () => {
+    const vec1 = [ 1, 2, 3 ] as Vec3;
+    const vec2 = [ 2, 3, 1 ] as Vec3;
+    const orthVec1 = [ 0, 1, 0 ] as Vec3;
+    const orthVec2 = [ 1, 0, 0 ] as Vec3;
+
+    it('angle calculation', () => {
+        expect(Vec3.angle(vec1, vec1) * 360 / (2 * Math.PI)).toBe(0.0);
+        expect(Vec3.angle(orthVec1, orthVec2) * 360 / (2 * Math.PI)).toBe(90.0);
+        expect(Vec3.angle(vec1, vec2)).toBeCloseTo(0.666946);
+    });
+})
\ No newline at end of file
diff --git a/src/mol-math/misc.ts b/src/mol-math/misc.ts
index 98dd209ea168bb3b4e25e897d6e19b424fa6719f..cfcb0d2355350d79adbd424a1b06947fa094caa9 100644
--- a/src/mol-math/misc.ts
+++ b/src/mol-math/misc.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,4 +10,8 @@ export function degToRad (deg: number) {
 
 export function radToDeg (rad: number) {
     return rad * 57.29578  // rad * 180 / Math.PI
+}
+
+export function isPowerOfTwo (x: number) {
+    return (x !== 0) && (x & (x - 1)) === 0
 }
\ No newline at end of file
diff --git a/src/mol-model-formats/shape/ply.ts b/src/mol-model-formats/shape/ply.ts
new file mode 100644
index 0000000000000000000000000000000000000000..edeceded4abdafa8e59e8370fef32b4c0b7f9f56
--- /dev/null
+++ b/src/mol-model-formats/shape/ply.ts
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Schäfer, Marco <marco.schaefer@uni-tuebingen.de>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { RuntimeContext, Task } from 'mol-task';
+import { ShapeProvider } from 'mol-model/shape/provider';
+import { Color } from 'mol-util/color';
+import { PlyFile, PlyTable, PlyList } from 'mol-io/reader/ply/schema';
+import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { Shape } from 'mol-model/shape';
+import { ChunkedArray } from 'mol-data/util';
+import { arrayMax, fillSerial } from 'mol-util/array';
+import { Column } from 'mol-data/db';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { ColorNames } from 'mol-util/color/tables';
+import { deepClone } from 'mol-util/object';
+
+// TODO support 'edge' element, see https://www.mathworks.com/help/vision/ug/the-ply-format.html
+// TODO support missing face element
+
+function createPlyShapeParams(plyFile?: PlyFile) {
+    const vertex = plyFile && plyFile.getElement('vertex') as PlyTable
+    const material = plyFile && plyFile.getElement('material') as PlyTable
+
+    const defaultValues = { group: '', vRed: '', vGreen: '', vBlue: '', mRed: '', mGreen: '', mBlue: '' }
+
+    const groupOptions: [string, string][] = [['', '']]
+    const colorOptions: [string, string][] = [['', '']]
+    if (vertex) {
+        for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) {
+            const name = vertex.propertyNames[i]
+            const type = vertex.propertyTypes[i]
+            if (
+                type === 'uchar' || type === 'uint8' ||
+                type === 'ushort' || type === 'uint16' ||
+                type === 'uint' || type === 'uint32'
+            ) groupOptions.push([ name, name ])
+            if (type === 'uchar' || type === 'uint8') colorOptions.push([ name, name ])
+        }
+
+        // TODO hardcoded as convenience for data provided by MegaMol
+        if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid'
+        else if (vertex.propertyNames.includes('material_index')) defaultValues.group = 'material_index'
+
+        if (vertex.propertyNames.includes('red')) defaultValues.vRed = 'red'
+        if (vertex.propertyNames.includes('green')) defaultValues.vGreen = 'green'
+        if (vertex.propertyNames.includes('blue')) defaultValues.vBlue = 'blue'
+    }
+
+    const materialOptions: [string, string][] = [['', '']]
+    if (material) {
+        for (let i = 0, il = material.propertyNames.length; i < il; ++i) {
+            const name = material.propertyNames[i]
+            const type = material.propertyTypes[i]
+            if (type === 'uchar' || type === 'uint8') materialOptions.push([ name, name ])
+        }
+
+        if (material.propertyNames.includes('red')) defaultValues.mRed = 'red'
+        if (material.propertyNames.includes('green')) defaultValues.mGreen = 'green'
+        if (material.propertyNames.includes('blue')) defaultValues.mBlue = 'blue'
+    }
+
+    const defaultColoring = defaultValues.vRed && defaultValues.vGreen && defaultValues.vBlue ? 'vertex' :
+        defaultValues.mRed && defaultValues.mGreen && defaultValues.mBlue ? 'material' : 'uniform'
+
+    return {
+        ...Mesh.Params,
+
+        coloring: PD.MappedStatic(defaultColoring, {
+            vertex: PD.Group({
+                red: PD.Select(defaultValues.vRed, colorOptions, { label: 'Red Property' }),
+                green: PD.Select(defaultValues.vGreen, colorOptions, { label: 'Green Property' }),
+                blue: PD.Select(defaultValues.vBlue, colorOptions, { label: 'Blue Property' }),
+            }, { isFlat: true }),
+            material: PD.Group({
+                red: PD.Select(defaultValues.mRed, materialOptions, { label: 'Red Property' }),
+                green: PD.Select(defaultValues.mGreen, materialOptions, { label: 'Green Property' }),
+                blue: PD.Select(defaultValues.mBlue, materialOptions, { label: 'Blue Property' }),
+            }, { isFlat: true }),
+            uniform: PD.Group({
+                color: PD.Color(ColorNames.grey)
+            }, { isFlat: true })
+        }),
+        grouping: PD.MappedStatic(defaultValues.group ? 'vertex' : 'none', {
+            vertex: PD.Group({
+                group: PD.Select(defaultValues.group, groupOptions, { label: 'Group Property' }),
+            }, { isFlat: true }),
+            none: PD.Group({ })
+        }),
+    }
+}
+
+export const PlyShapeParams = createPlyShapeParams()
+export type PlyShapeParams = typeof PlyShapeParams
+
+async function getMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, groupIds: ArrayLike<number>, mesh?: Mesh) {
+    const builderState = MeshBuilder.createState(vertex.rowCount, vertex.rowCount / 4, mesh)
+    const { vertices, normals, indices, groups } = builderState
+
+    const x = vertex.getProperty('x')
+    const y = vertex.getProperty('y')
+    const z = vertex.getProperty('z')
+    if (!x || !y || !z) throw new Error('missing coordinate properties')
+
+    const nx = vertex.getProperty('nx')
+    const ny = vertex.getProperty('ny')
+    const nz = vertex.getProperty('nz')
+
+    const hasNormals = !!nx && !!ny && !!nz
+
+    for (let i = 0, il = vertex.rowCount; i < il; ++i) {
+        if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding vertex ${i}` })
+
+        ChunkedArray.add3(vertices, x.value(i), y.value(i), z.value(i))
+        if (hasNormals) ChunkedArray.add3(normals, nx!.value(i), ny!.value(i), nz!.value(i));
+        ChunkedArray.add(groups, groupIds[i])
+    }
+
+    for (let i = 0, il = face.rowCount; i < il; ++i) {
+        if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding face ${i}` })
+
+        const { entries, count } = face.value(i)
+        if (count === 3) {
+            // triangle
+            ChunkedArray.add3(indices, entries[0], entries[1], entries[2])
+        } else if (count === 4) {
+            // quadrilateral
+            ChunkedArray.add3(indices, entries[2], entries[1], entries[0])
+            ChunkedArray.add3(indices, entries[2], entries[0], entries[3])
+        }
+    }
+
+    const m = MeshBuilder.getMesh(builderState);
+    m.normalsComputed = hasNormals
+    await Mesh.computeNormals(m).runInContext(ctx)
+
+    return m
+}
+
+const int = Column.Schema.int
+
+type Grouping = { ids: ArrayLike<number>, map: ArrayLike<number> }
+function getGrouping(vertex: PlyTable, props: PD.Values<PlyShapeParams>): Grouping {
+    const { grouping } = props
+    const { rowCount } = vertex
+    const column = grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined
+
+    const ids = column ? column.toArray({ array: Uint32Array }) : fillSerial(new Uint32Array(rowCount))
+    const maxId = arrayMax(ids) // assumes uint ids
+    const map = new Uint32Array(maxId + 1)
+    for (let i = 0, il = ids.length; i < il; ++i) map[ids[i]] = i
+    return { ids, map }
+}
+
+type Coloring = { kind: 'vertex' | 'material' | 'uniform', red: Column<number>, green: Column<number>, blue: Column<number> }
+function getColoring(vertex: PlyTable, material: PlyTable | undefined, props: PD.Values<PlyShapeParams>): Coloring {
+    const { coloring } = props
+    const { rowCount } = vertex
+
+    let red: Column<number>, green: Column<number>, blue: Column<number>
+    if (coloring.name === 'vertex') {
+        red = vertex.getProperty(coloring.params.red) || Column.ofConst(127, rowCount, int)
+        green = vertex.getProperty(coloring.params.green) || Column.ofConst(127, rowCount, int)
+        blue = vertex.getProperty(coloring.params.blue) || Column.ofConst(127, rowCount, int)
+    } else if (coloring.name === 'material') {
+        red = (material && material.getProperty(coloring.params.red)) || Column.ofConst(127, rowCount, int)
+        green = (material && material.getProperty(coloring.params.green)) || Column.ofConst(127, rowCount, int)
+        blue = (material && material.getProperty(coloring.params.blue)) || Column.ofConst(127, rowCount, int)
+    } else {
+        const [r, g, b] = Color.toRgb(coloring.params.color)
+        red = Column.ofConst(r, rowCount, int)
+        green = Column.ofConst(g, rowCount, int)
+        blue = Column.ofConst(b, rowCount, int)
+    }
+    return { kind: coloring.name, red, green, blue }
+}
+
+function createShape(plyFile: PlyFile, mesh: Mesh, coloring: Coloring, grouping: Grouping) {
+    const { kind, red, green, blue } = coloring
+    const { ids, map } = grouping
+    return Shape.create(
+        'ply-mesh', plyFile, mesh,
+        (groupId: number) => {
+            const idx = kind === 'material' ? groupId : map[groupId]
+            return Color.fromRgb(red.value(idx), green.value(idx), blue.value(idx))
+        },
+        () => 1, // size: constant
+        (groupId: number) => {
+            return ids[groupId].toString()
+        }
+    )
+}
+
+function makeShapeGetter() {
+    let _plyFile: PlyFile | undefined
+    let _props: PD.Values<PlyShapeParams> | undefined
+
+    let _shape: Shape<Mesh>
+    let _mesh: Mesh
+    let _coloring: Coloring
+    let _grouping: Grouping
+
+    const getShape = async (ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) => {
+
+        const vertex = plyFile.getElement('vertex') as PlyTable
+        if (!vertex) throw new Error('missing vertex element')
+
+        const face = plyFile.getElement('face') as PlyList
+        if (!face) throw new Error('missing face element')
+
+        const material = plyFile.getElement('material') as PlyTable
+
+        let newMesh = false
+        let newColor = false
+
+        if (!_plyFile || _plyFile !== plyFile) {
+            newMesh = true
+        }
+
+        if (!_props || !PD.isParamEqual(PlyShapeParams.grouping, _props.grouping, props.grouping)) {
+            newMesh = true
+        }
+
+        if (!_props || !PD.isParamEqual(PlyShapeParams.coloring, _props.coloring, props.coloring)) {
+            newColor = true
+        }
+
+        if (newMesh) {
+            _coloring = getColoring(vertex, material, props)
+            _grouping = getGrouping(vertex, props)
+            _mesh = await getMesh(ctx, vertex, face, _grouping.ids, shape && shape.geometry)
+            _shape = createShape(plyFile, _mesh, _coloring, _grouping)
+        } else if (newColor) {
+            _coloring = getColoring(vertex, material, props)
+            _shape = createShape(plyFile, _mesh, _coloring, _grouping)
+        }
+
+        _plyFile = plyFile
+        _props = deepClone(props)
+
+        return _shape
+    }
+    return getShape
+}
+
+export function shapeFromPly(source: PlyFile, params?: {}) {
+    return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Shape Provider', async ctx => {
+        return {
+            label: 'Mesh',
+            data: source,
+            params: createPlyShapeParams(source),
+            getShape: makeShapeGetter(),
+            geometryUtils: Mesh.Utils
+        }
+    })
+}
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/_spec/pdb.spec.ts b/src/mol-model-formats/structure/_spec/pdb.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8e365ed4c9c8f2fc1b01e447e32e5314eeb6bd45
--- /dev/null
+++ b/src/mol-model-formats/structure/_spec/pdb.spec.ts
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { guessElementSymbol } from '../pdb/to-cif';
+import { TokenBuilder } from 'mol-io/reader/common/text/tokenizer';
+
+const records = [
+    ['ATOM     19 HD23 LEU A   1     151.940 143.340 155.670  0.00  0.00', 'H'],
+    ['ATOM     38  CA  SER A   3     146.430 138.150 162.270  0.00  0.00', 'C'],
+    ['ATOM     38 NA   SER A   3     146.430 138.150 162.270  0.00  0.00', 'NA'],
+    ['ATOM     38  NAA SER A   3     146.430 138.150 162.270  0.00  0.00', 'N'],
+]
+
+describe('PDB to-cif', () => {
+    it('guess-element-symbol', () => {
+        for (let i = 0, il = records.length; i < il; ++i) {
+            const [ data, element ] = records[i]
+            const tokens = TokenBuilder.create(data, 2)
+            guessElementSymbol(tokens, data, 12, 16)
+            expect(data.substring(tokens.indices[0], tokens.indices[1])).toBe(element)
+        }
+    });
+});
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/mmcif/bonds/comp.ts b/src/mol-model-formats/structure/mmcif/bonds/comp.ts
index eceb64760607d51d544a8a9a587a46471bd9334d..9ba0b69eb645f03e1fc9f10946eba6bcb09eeff4 100644
--- a/src/mol-model-formats/structure/mmcif/bonds/comp.ts
+++ b/src/mol-model-formats/structure/mmcif/bonds/comp.ts
@@ -7,7 +7,7 @@
 
 import { Model } from 'mol-model/structure/model/model'
 import { LinkType } from 'mol-model/structure/model/types'
-import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
+import { CustomPropertyDescriptor } from 'mol-model/structure';
 import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
 import { Structure, Unit, StructureProperties, StructureElement } from 'mol-model/structure';
 import { Segmentation } from 'mol-data/int';
@@ -18,7 +18,7 @@ export interface ComponentBond {
 }
 
 export namespace ComponentBond {
-    export const Descriptor: ModelPropertyDescriptor = {
+    export const Descriptor: CustomPropertyDescriptor = {
         isStatic: true,
         name: 'chem_comp_bond',
         cifExport: {
diff --git a/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts b/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
index 91c819e5a3b918e854029231df48cea4e3619fad..69689c51dbae68518e2d4ad14edc55303e6b5ade 100644
--- a/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
+++ b/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
@@ -10,7 +10,7 @@ import { Structure } from 'mol-model/structure'
 import { LinkType } from 'mol-model/structure/model/types'
 import { findEntityIdByAsymId, findAtomIndexByLabelName } from '../util'
 import { Column } from 'mol-data/db'
-import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
+import { CustomPropertyDescriptor } from 'mol-model/structure';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { SortedArray } from 'mol-data/int';
 import { CifWriter } from 'mol-io/writer/cif'
@@ -23,7 +23,7 @@ export interface StructConn {
 }
 
 export namespace StructConn {
-    export const Descriptor: ModelPropertyDescriptor = {
+    export const Descriptor: CustomPropertyDescriptor = {
         isStatic: true,
         name: 'struct_conn',
         cifExport: {
diff --git a/src/mol-model-formats/structure/mmcif/parser.ts b/src/mol-model-formats/structure/mmcif/parser.ts
index d31b63d1a8a6d0eaa4368ee6660178a237f0f59b..71063f94d82d6df46bdcafa3bc5bb73da9102ba7 100644
--- a/src/mol-model-formats/structure/mmcif/parser.ts
+++ b/src/mol-model-formats/structure/mmcif/parser.ts
@@ -13,7 +13,7 @@ import { RuntimeContext } from 'mol-task';
 import UUID from 'mol-util/uuid';
 import { Model } from 'mol-model/structure/model/model';
 import { Entities } from 'mol-model/structure/model/properties/common';
-import { CustomProperties } from 'mol-model/structure/model/properties/custom';
+import { CustomProperties } from 'mol-model/structure';
 import { ModelSymmetry } from 'mol-model/structure/model/properties/symmetry';
 import { createAssemblies } from './assembly';
 import { getAtomicHierarchyAndConformation } from './atomic';
diff --git a/src/mol-model-formats/structure/pdb/to-cif.ts b/src/mol-model-formats/structure/pdb/to-cif.ts
index 853a1b9319eb97121a5394a55e772dde2621608a..0f699f297a4c3f03095cd1514bfb8d7db7c129b8 100644
--- a/src/mol-model-formats/structure/pdb/to-cif.ts
+++ b/src/mol-model-formats/structure/pdb/to-cif.ts
@@ -8,7 +8,7 @@
 import { substringStartsWith } from 'mol-util/string';
 import { CifField, CifCategory, CifFrame } from 'mol-io/reader/cif';
 import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
-import { TokenBuilder, Tokenizer } from 'mol-io/reader/common/text/tokenizer';
+import { TokenBuilder, Tokenizer, Tokens } from 'mol-io/reader/common/text/tokenizer';
 import { PdbFile } from 'mol-io/reader/pdb/schema';
 import { parseCryst1, parseRemark350, parseMtrix } from './assembly';
 import { WaterNames } from 'mol-model/structure/model/types';
@@ -89,6 +89,43 @@ function getEntityId(residueName: string, isHet: boolean) {
     return '1';
 }
 
+export function guessElementSymbol(tokens: Tokens, str: string, start: number, end: number) {
+    let s = start, e = end - 1
+
+    // trim spaces and numbers
+    let c = str.charCodeAt(s)
+    while ((c === 32 || (c >= 48 && c <= 57)) && s <= e) c = str.charCodeAt(++s)
+    c = str.charCodeAt(e)
+    while ((c === 32 || (c >= 48 && c <= 57)) && e >= s) c = str.charCodeAt(--e)
+
+    ++e
+
+    if (s === e) return TokenBuilder.add(tokens, s, e) // empty
+    if (s + 1 === e) return TokenBuilder.add(tokens, s, e) // one char
+
+    c = str.charCodeAt(s)
+
+    if (s + 2 === e) { // two chars
+        const c2 = str.charCodeAt(s + 1)
+        if (
+            ((c === 78 || c === 110) && (c2 === 65 || c2 ===  97)) || // NA na Na nA
+            ((c === 67 || c ===  99) && (c2 === 76 || c2 === 108)) || // CL
+            ((c === 70 || c === 102) && (c2 === 69 || c2 === 101))    // FE
+        ) return TokenBuilder.add(tokens, s, s + 2)
+    }
+
+    if (
+        c === 67 || c ===  99 || // C c
+        c === 72 || c === 104 || // H h
+        c === 78 || c === 110 || // N n
+        c === 79 || c === 111 || // O o
+        c === 80 || c === 112 || // P p
+        c === 83 || c === 115    // S s
+    ) return TokenBuilder.add(tokens, s, s + 1)
+
+    TokenBuilder.add(tokens, s, s) // no reasonable guess, add empty token
+}
+
 function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: number, e: number, isHet: boolean) {
     const { data: str } = data;
     const length = e - s;
@@ -162,11 +199,10 @@ function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: num
         if (data.tokenStart < data.tokenEnd) {
             TokenBuilder.addToken(sites.type_symbol, data);
         } else {
-            // "guess" the symbol
-            TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+            guessElementSymbol(sites.type_symbol, str, s + 12, s + 16)
         }
     } else {
-        TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+        guessElementSymbol(sites.type_symbol, str, s + 12, s + 16)
     }
 
     sites.label_entity_id[sites.index] = getEntityId(residueName, isHet);
diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts
index 85f9306753784f838dbc00dce60b168394c5b6be..f5e2a6ff7e86a9a6a1845acb5d186cc851fe4529 100644
--- a/src/mol-model-props/common/custom-element-property.ts
+++ b/src/mol-model-props/common/custom-element-property.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ElementIndex, Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { ElementIndex, Model, CustomPropertyDescriptor } from 'mol-model/structure';
 import { StructureElement } from 'mol-model/structure/structure';
 import { Location } from 'mol-model/location';
 import { CustomPropertyRegistry } from './custom-property-registry';
@@ -36,7 +36,7 @@ namespace CustomElementProperty {
     export function create<T>(params: CreateParams<T>) {
         const name = params.name;
 
-        const Descriptor = ModelPropertyDescriptor({
+        const Descriptor = CustomPropertyDescriptor({
             isStatic: params.isStatic,
             name: params.name,
         });
diff --git a/src/mol-model-props/common/custom-property-registry.ts b/src/mol-model-props/common/custom-property-registry.ts
index b537d0df3752caf9a4679d8deae0b0909c568bb6..977cade54990a71dd9b3f821cc89bd02a91c78c6 100644
--- a/src/mol-model-props/common/custom-property-registry.ts
+++ b/src/mol-model-props/common/custom-property-registry.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ModelPropertyDescriptor, Model } from 'mol-model/structure';
+import { CustomPropertyDescriptor, Model } from 'mol-model/structure';
 import { OrderedMap } from 'immutable';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { Task } from 'mol-task';
@@ -58,7 +58,7 @@ namespace CustomPropertyRegistry {
     export interface Provider {
         option: [string, string],
         defaultSelected: boolean,
-        descriptor: ModelPropertyDescriptor<any, any>,
+        descriptor: CustomPropertyDescriptor<any, any>,
         attachableTo: (model: Model) => boolean,
         attach: (model: Model) => Task<boolean>
     }
diff --git a/src/mol-model-props/pdbe/preferred-assembly.ts b/src/mol-model-props/pdbe/preferred-assembly.ts
index fb5f5dc69d30b0ee3f12611aa9890086bed3fd8c..a5c540566b2cbd850c36a5f91ad90ce7447cfad8 100644
--- a/src/mol-model-props/pdbe/preferred-assembly.ts
+++ b/src/mol-model-props/pdbe/preferred-assembly.ts
@@ -7,7 +7,7 @@
 import { Column, Table } from 'mol-data/db';
 import { toTable } from 'mol-io/reader/cif/schema';
 import { CifWriter } from 'mol-io/writer/cif';
-import { Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor } from 'mol-model/structure';
 
 export namespace PDBePreferredAssembly {
     export type Property = string
@@ -31,7 +31,7 @@ export namespace PDBePreferredAssembly {
     };
     export type Schema = typeof Schema
 
-    export const Descriptor = ModelPropertyDescriptor({
+    export const Descriptor = CustomPropertyDescriptor({
         isStatic: true,
         name: 'pdbe_preferred_assembly',
         cifExport: {
diff --git a/src/mol-model-props/pdbe/struct-ref-domain.ts b/src/mol-model-props/pdbe/struct-ref-domain.ts
index 3872d5a4d470fe0056024f961385846925f79bfe..8c3a8a7f34ef9762832395db1c6819a7627ad361 100644
--- a/src/mol-model-props/pdbe/struct-ref-domain.ts
+++ b/src/mol-model-props/pdbe/struct-ref-domain.ts
@@ -7,7 +7,7 @@
 import { Column, Table } from 'mol-data/db';
 import { toTable } from 'mol-io/reader/cif/schema';
 import { CifWriter } from 'mol-io/writer/cif';
-import { Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor } from 'mol-model/structure';
 import { PropertyWrapper } from '../common/wrapper';
 
 export namespace PDBeStructRefDomain {
@@ -39,7 +39,7 @@ export namespace PDBeStructRefDomain {
     };
     export type Schema = typeof Schema
 
-    export const Descriptor = ModelPropertyDescriptor({
+    export const Descriptor = CustomPropertyDescriptor({
         isStatic: true,
         name: 'pdbe_struct_ref_domain',
         cifExport: {
diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts
index b534c465ff624e5793f4de87e7bc437c099073cd..483b957047120c0118d58b966805bc51ef92ac22 100644
--- a/src/mol-model-props/pdbe/structure-quality-report.ts
+++ b/src/mol-model-props/pdbe/structure-quality-report.ts
@@ -8,7 +8,7 @@ import { Column, Table } from 'mol-data/db';
 import { toTable } from 'mol-io/reader/cif/schema';
 import { mmCIF_residueId_schema } from 'mol-io/reader/cif/schema/mmcif-extras';
 import { CifWriter } from 'mol-io/writer/cif';
-import { Model, ModelPropertyDescriptor, ResidueIndex, Unit, IndexedCustomProperty } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor, ResidueIndex, Unit, IndexedCustomProperty } from 'mol-model/structure';
 import { residueIdFields } from 'mol-model/structure/export/categories/atom_site';
 import { StructureElement, CifExportContext } from 'mol-model/structure/structure';
 import { CustomPropSymbol } from 'mol-script/language/symbol';
@@ -43,7 +43,7 @@ export namespace StructureQualityReport {
     };
     export type Schema = typeof Schema
 
-    export const Descriptor = ModelPropertyDescriptor({
+    export const Descriptor = CustomPropertyDescriptor({
         isStatic: false,
         name: 'pdbe_structure_quality_report',
         cifExport: {
diff --git a/src/mol-model-props/rcsb/assembly-symmetry.ts b/src/mol-model-props/rcsb/assembly-symmetry.ts
index c67ed0aca0b78727b2d073b60563f7b1f128d76b..4eab862518f6da97b1adf445a6d24ec16ab4d2ad 100644
--- a/src/mol-model-props/rcsb/assembly-symmetry.ts
+++ b/src/mol-model-props/rcsb/assembly-symmetry.ts
@@ -7,7 +7,7 @@
 import { AssemblySymmetry as AssemblySymmetryGraphQL } from './graphql/types';
 import query from './graphql/symmetry.gql';
 
-import { Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { Model, CustomPropertyDescriptor } from 'mol-model/structure';
 import { CifWriter } from 'mol-io/writer/cif';
 import { Database as _Database, Column, Table } from 'mol-data/db'
 import { Category } from 'mol-io/writer/cif/encoder';
@@ -140,7 +140,7 @@ function createDatabaseFromCif(model: Model): AssemblySymmetry.Database {
     })
 }
 
-const _Descriptor: ModelPropertyDescriptor = {
+const _Descriptor: CustomPropertyDescriptor = {
     isStatic: true,
     name: 'rcsb_assembly_symmetry',
     cifExport: {
diff --git a/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts b/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts
index 09abfebeb49ec7b35be09744fa09ef1c202cbc30..1ba6eb9512c43ac603fcbe19d9679a079a684979 100644
--- a/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts
+++ b/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts
@@ -59,7 +59,7 @@ export const AssemblySymmetryAxesRepresentationProvider: StructureRepresentation
 
 //
 
-export function AssemblySymmetryAxesVisual(): ComplexVisual<AssemblySymmetryAxesParams> {
+export function AssemblySymmetryAxesVisual(materialId: number): ComplexVisual<AssemblySymmetryAxesParams> {
     return ComplexMeshVisual<AssemblySymmetryAxesParams>({
         defaultProps: PD.getDefaultValues(AssemblySymmetryAxesParams),
         createGeometry: createAssemblySymmetryAxesMesh,
@@ -73,7 +73,7 @@ export function AssemblySymmetryAxesVisual(): ComplexVisual<AssemblySymmetryAxes
                 newProps.symmetryId !== currentProps.symmetryId
             )
         }
-    })
+    }, materialId)
 }
 
 function createLocationIterator(structure: Structure) {
diff --git a/src/mol-model/location.ts b/src/mol-model/location.ts
index 39546ddac69d0642e20e7f5d232b824d798e2c90..d5006c61710d8684b26c982e1529703a3cfcceda 100644
--- a/src/mol-model/location.ts
+++ b/src/mol-model/location.ts
@@ -1,12 +1,12 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { StructureElement } from './structure'
 import { Link } from './structure/structure/unit/links'
-import { Shape } from './shape/shape';
+import { ShapeGroup } from './shape/shape';
 
 /** A null value Location */
 export const NullLocation = { kind: 'null-location' as 'null-location' }
@@ -15,4 +15,4 @@ export function isNullLocation(x: any): x is NullLocation {
     return !!x && x.kind === 'null-location';
 }
 
-export type Location = StructureElement | Link.Location | Shape.Location | NullLocation
\ No newline at end of file
+export type Location = StructureElement | Link.Location | ShapeGroup.Location | NullLocation
\ No newline at end of file
diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts
index 35cad50ed5ecb596bf791d532455964f921db7af..517b0631ac0456006bc7fe62ec7a54690587980b 100644
--- a/src/mol-model/loci.ts
+++ b/src/mol-model/loci.ts
@@ -1,12 +1,12 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { StructureElement } from './structure'
 import { Link } from './structure/structure/unit/links'
-import { Shape } from './shape';
+import { Shape, ShapeGroup } from './shape';
 import { Sphere3D } from 'mol-math/geometry';
 import { CentroidHelper } from 'mol-math/geometry/centroid-helper';
 import { Vec3 } from 'mol-math/linear-algebra';
@@ -46,7 +46,7 @@ export function createDataLoci(data: any, tag: string, indices: OrderedSet<numbe
 
 export { Loci }
 
-type Loci = StructureElement.Loci | Structure.Loci | Link.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci
+type Loci = StructureElement.Loci | Structure.Loci | Link.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci | ShapeGroup.Loci
 
 namespace Loci {
     export function areEqual(lociA: Loci, lociB: Loci) {
@@ -67,6 +67,9 @@ namespace Loci {
         if (Shape.isLoci(lociA) && Shape.isLoci(lociB)) {
             return Shape.areLociEqual(lociA, lociB)
         }
+        if (ShapeGroup.isLoci(lociA) && ShapeGroup.isLoci(lociB)) {
+            return ShapeGroup.areLociEqual(lociA, lociB)
+        }
         return false
     }
 
@@ -96,6 +99,9 @@ namespace Loci {
                 e.aUnit.conformation.position(e.bUnit.elements[e.bIndex], tempPos);
                 sphereHelper.radiusStep(tempPos);
             }
+        } else if (loci.kind === 'shape-loci') {
+            // TODO
+            return void 0;
         } else if (loci.kind === 'group-loci') {
             // TODO
             return void 0;
diff --git a/src/mol-model/shape/provider.ts b/src/mol-model/shape/provider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dea45e8ba202b2a09da762a1b9a84926e836b64b
--- /dev/null
+++ b/src/mol-model/shape/provider.ts
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ShapeGetter } from 'mol-repr/shape/representation';
+import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry';
+
+export interface ShapeProvider<D, G extends Geometry, P extends Geometry.Params<G>> {
+    label: string
+    data: D
+    params: P
+    getShape: ShapeGetter<D, G, P>
+    geometryUtils: GeometryUtils<G>
+}
\ No newline at end of file
diff --git a/src/mol-model/shape/shape.ts b/src/mol-model/shape/shape.ts
index 314af70fbfec3c4df1a7379c9359ead45f4fcc05..0d740ef14a47a24fab8dbb37f0a5c994c466645e 100644
--- a/src/mol-model/shape/shape.ts
+++ b/src/mol-model/shape/shape.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -15,6 +15,8 @@ export interface Shape<G extends Geometry = Geometry> {
     readonly id: UUID
     /** A name to describe the shape */
     readonly name: string
+    /** The data used to create the shape */
+    readonly sourceData: unknown
     /** The geometry of the shape, e.g. `Mesh` or `Lines` */
     readonly geometry: G
     /** An array of transformation matrices to describe multiple instances of the geometry */
@@ -30,10 +32,11 @@ export interface Shape<G extends Geometry = Geometry> {
 }
 
 export namespace Shape {
-    export function create<G extends Geometry>(name: string, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> {
+    export function create<G extends Geometry>(name: string, sourceData: unknown, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> {
         return {
             id: UUID.create22(),
             name,
+            sourceData,
             geometry,
             transforms: transforms || [Mat4.identity()],
             get groupCount() { return Geometry.getGroupCount(geometry) },
@@ -43,6 +46,13 @@ export namespace Shape {
         }
     }
 
+    export interface Loci { readonly kind: 'shape-loci', readonly shape: Shape }
+    export function Loci(shape: Shape): Loci { return { kind: 'shape-loci', shape } }
+    export function isLoci(x: any): x is Loci { return !!x && x.kind === 'shape-loci' }
+    export function areLociEqual(a: Loci, b: Loci) { return a.shape === b.shape }
+}
+
+export namespace ShapeGroup {
     export interface Location {
         readonly kind: 'group-location'
         shape: Shape
diff --git a/src/mol-model/structure.ts b/src/mol-model/structure.ts
index 38d5890d5f0fada2ea72a32c23e6e9d3f2e0261b..3dff04a4684f0c1ad2062fb255eb10755ffa7ea8 100644
--- a/src/mol-model/structure.ts
+++ b/src/mol-model/structure.ts
@@ -6,4 +6,5 @@
 
 export * from './structure/model'
 export * from './structure/structure'
-export * from './structure/query'
\ No newline at end of file
+export * from './structure/query'
+export * from './structure/common/custom-property'
\ No newline at end of file
diff --git a/src/mol-model/structure/common/custom-property.ts b/src/mol-model/structure/common/custom-property.ts
new file mode 100644
index 0000000000000000000000000000000000000000..12f6f557c4aa5da2e91bd2cb6600b74dd2a08376
--- /dev/null
+++ b/src/mol-model/structure/common/custom-property.ts
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CifWriter } from 'mol-io/writer/cif'
+import { CifExportContext } from '../export/mmcif';
+import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
+import { UUID } from 'mol-util';
+
+export { CustomPropertyDescriptor, CustomProperties }
+
+interface CustomPropertyDescriptor<ExportCtx = CifExportContext, Symbols extends { [name: string]: QuerySymbolRuntime } = { }> {
+    readonly isStatic: boolean,
+    readonly name: string,
+
+    cifExport?: {
+        // Prefix enforced during export.
+        prefix: string,
+        context?: (ctx: CifExportContext) => ExportCtx | undefined,
+        categories: CifWriter.Category<ExportCtx>[]
+    },
+
+    // TODO: add aliases when lisp-like mol-script is done
+    symbols?: Symbols
+}
+
+function CustomPropertyDescriptor<Ctx, Desc extends CustomPropertyDescriptor<Ctx>>(desc: Desc) {
+    return desc;
+}
+
+namespace CustomPropertyDescriptor {
+    export function getUUID(prop: CustomPropertyDescriptor): UUID {
+        if (!(prop as any).__key) {
+            (prop as any).__key = UUID.create22();
+        }
+        return (prop as any).__key;
+    }
+}
+
+class CustomProperties {
+    private _list: CustomPropertyDescriptor[] = [];
+    private _set = new Set<CustomPropertyDescriptor>();
+
+    get all(): ReadonlyArray<CustomPropertyDescriptor> {
+        return this._list;
+    }
+
+    add(desc: CustomPropertyDescriptor<any>) {
+        if (this._set.has(desc)) return;
+
+        this._list.push(desc);
+        this._set.add(desc);
+    }
+
+    has(desc: CustomPropertyDescriptor<any>): boolean {
+        return this._set.has(desc);
+    }
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/export/mmcif.ts b/src/mol-model/structure/export/mmcif.ts
index e0c189ab4afa7088656e3f0312e2539f7335e4de..b94604b104dae5a250080db2bfc3c94c0c9c036a 100644
--- a/src/mol-model/structure/export/mmcif.ts
+++ b/src/mol-model/structure/export/mmcif.ts
@@ -16,7 +16,7 @@ import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './
 import { Model } from '../model';
 import { getUniqueEntityIndicesFromStructures, copy_mmCif_category } from './categories/utils';
 import { _struct_asym, _entity_poly, _entity_poly_seq } from './categories/sequence';
-import { ModelPropertyDescriptor } from '../model/properties/custom';
+import { CustomPropertyDescriptor } from '../common/custom-property';
 
 export interface CifExportContext {
     structures: Structure[],
@@ -100,8 +100,32 @@ export const mmCIF_Export_Filters = {
     }
 }
 
+function encodeCustomProp(customProp: CustomPropertyDescriptor, ctx: CifExportContext, encoder: CifWriter.Encoder, params: encode_mmCIF_categories_Params) {
+    if (!customProp.cifExport || customProp.cifExport.categories.length === 0) return;
+
+    const prefix = customProp.cifExport.prefix;
+    const cats = customProp.cifExport.categories;
+
+    let propCtx = ctx;
+    if (customProp.cifExport.context) {
+        const propId = CustomPropertyDescriptor.getUUID(customProp);
+        if (ctx.cache[propId + '__ctx']) propCtx = ctx.cache[propId + '__ctx'];
+        else {
+            propCtx = customProp.cifExport.context(ctx) || ctx;
+            ctx.cache[propId + '__ctx'] = propCtx;
+        }
+    }
+    for (const cat of cats) {
+        if (params.skipCategoryNames && params.skipCategoryNames.has(cat.name)) continue;
+        if (cat.name.indexOf(prefix) !== 0) throw new Error(`Custom category '${cat.name}' name must start with prefix '${prefix}.'`);
+        encoder.writeCategory(cat, propCtx);
+    }
+}
+
+type encode_mmCIF_categories_Params = { skipCategoryNames?: Set<string>, exportCtx?: CifExportContext }
+
 /** Doesn't start a data block */
-export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures: Structure | Structure[], params?: { skipCategoryNames?: Set<string>, exportCtx?: CifExportContext }) {
+export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures: Structure | Structure[], params?: encode_mmCIF_categories_Params) {
     const first = Array.isArray(structures) ? structures[0] : (structures as Structure);
     const models = first.models;
     if (models.length !== 1) throw 'Can\'t export stucture composed from multiple models.';
@@ -115,26 +139,15 @@ export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures:
     }
 
     for (const customProp of models[0].customProperties.all) {
-        if (!customProp.cifExport || customProp.cifExport.categories.length === 0) continue;
-
-        const prefix = customProp.cifExport.prefix;
-        const cats = customProp.cifExport.categories;
-
-        let propCtx = ctx;
-        if (customProp.cifExport.context) {
-            const propId = ModelPropertyDescriptor.getUUID(customProp);
-            if (ctx.cache[propId + '__ctx']) propCtx = ctx.cache[propId + '__ctx'];
-            else {
-                propCtx = customProp.cifExport.context(ctx) || ctx;
-                ctx.cache[propId + '__ctx'] = propCtx;
-            }
-        }
-        for (const cat of cats) {
-            if (_params.skipCategoryNames && _params.skipCategoryNames.has(cat.name)) continue;
-            if (cat.name.indexOf(prefix) !== 0) throw new Error(`Custom category '${cat.name}' name must start with prefix '${prefix}.'`);
-            encoder.writeCategory(cat, propCtx);
-        }
+        encodeCustomProp(customProp, ctx, encoder, _params);
+    }
+
+    const structureCustomProps = new Set<CustomPropertyDescriptor>();
+    for (const s of ctx.structures) {
+        if (!s.hasCustomProperties) continue;
+        for (const p of s.customPropertyDescriptors.all) structureCustomProps.add(p);
     }
+    structureCustomProps.forEach(customProp => encodeCustomProp(customProp, ctx, encoder, _params));
 }
 
 function to_mmCIF(name: string, structure: Structure, asBinary = false) {
diff --git a/src/mol-model/structure/model.ts b/src/mol-model/structure/model.ts
index df9e98d18fa289780ace42f1326250fd232ce92e..8a1dccfdebe0bda698574a261203ab9741156036 100644
--- a/src/mol-model/structure/model.ts
+++ b/src/mol-model/structure/model.ts
@@ -9,6 +9,6 @@ import * as Types from './model/types'
 import { ModelSymmetry } from './model/properties/symmetry'
 import StructureSequence from './model/properties/sequence'
 
-export * from './model/properties/custom'
+export * from './model/properties/custom/indexed'
 export * from './model/indexing'
 export { Model, Types, ModelSymmetry, StructureSequence }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts
index c777d8d55fe61d75afc5b24f07e58dbfe0b4748e..303048fdde6740ec47502a9c3baee05a0d927dc7 100644
--- a/src/mol-model/structure/model/model.ts
+++ b/src/mol-model/structure/model/model.ts
@@ -10,7 +10,7 @@ import { AtomicHierarchy, AtomicConformation } from './properties/atomic';
 import { ModelSymmetry } from './properties/symmetry';
 import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
 import { Entities } from './properties/common';
-import { CustomProperties } from './properties/custom';
+import { CustomProperties } from '../common/custom-property';
 import { SecondaryStructure } from './properties/seconday-structure';
 import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
 import { ModelFormat } from 'mol-model-formats/structure/format';
diff --git a/src/mol-model/structure/model/properties/custom.ts b/src/mol-model/structure/model/properties/custom.ts
deleted file mode 100644
index 9b9ab7c3dc7430388fe7b5bf4a6977c6936454a4..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/model/properties/custom.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-export * from './custom/descriptor'
-export * from './custom/collection'
-export * from './custom/indexed'
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/custom/chain.ts b/src/mol-model/structure/model/properties/custom/chain.ts
deleted file mode 100644
index 4e711c32e7ad450c3d409de7b38169f79b7dbab8..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/model/properties/custom/chain.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-// /**
-//  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
-//  *
-//  * @author David Sehnal <david.sehnal@gmail.com>
-//  */
-
-// import { ChainIndex } from '../../indexing';
-// import { Unit, Structure, StructureElement } from '../../../structure';
-// import { Segmentation } from 'mol-data/int';
-// import { UUID } from 'mol-util';
-// import { CifWriter } from 'mol-io/writer/cif';
-
-// export interface ChainCustomProperty<T = any> {
-//     readonly id: UUID,
-//     readonly kind: Unit.Kind,
-//     has(idx: ChainIndex): boolean
-//     get(idx: ChainIndex): T | undefined
-// }
-
-// export namespace ChainCustomProperty {
-//     export interface ExportCtx<T> {
-//         elements: StructureElement[],
-//         property(index: number): T
-//     };
-
-//     function getExportCtx<T>(prop: ChainCustomProperty<T>, structure: Structure): ExportCtx<T> {
-//         const chainIndex = structure.model.atomicHierarchy.chainAtomSegments.index;
-//         const elements = getStructureElements(structure, prop);
-//         return { elements, property: i => prop.get(chainIndex[elements[i].element])! };
-//     }
-
-//     export function getCifDataSource<T>(structure: Structure, prop: ChainCustomProperty<T> | undefined, cache: any): CifWriter.Category.Instance['source'][0] {
-//         if (!prop) return { rowCount: 0 };
-//         if (cache && cache[prop.id]) return cache[prop.id];
-//         const data = getExportCtx(prop, structure);
-//         const ret = { data, rowCount: data.elements.length };
-//         if (cache) cache[prop.id] = ret;
-//         return ret;
-//     }
-
-//     class FromMap<T> implements ChainCustomProperty<T> {
-//         readonly id = UUID.create();
-
-//         has(idx: ChainIndex): boolean {
-//             return this.map.has(idx);
-//         }
-
-//         get(idx: ChainIndex) {
-//             return this.map.get(idx);
-//         }
-
-//         constructor(private map: Map<ChainIndex, T>, public kind: Unit.Kind) {
-//         }
-//     }
-
-//     export function fromMap<T>(map: Map<ChainIndex, T>, kind: Unit.Kind) {
-//         return new FromMap(map, kind);
-//     }
-
-//     /**
-//      * Gets all StructureElements that correspond to 1st atoms of residues that have an property assigned.
-//      * Only works correctly for structures with a single model.
-//      */
-//     export function getStructureElements(structure: Structure, property: ChainCustomProperty) {
-//         const models = structure.models;
-//         if (models.length !== 1) throw new Error(`Only works on structures with a single model.`);
-
-//         const seenChains = new Set<ChainIndex>();
-//         const unitGroups = structure.unitSymmetryGroups;
-//         const loci: StructureElement[] = [];
-
-//         for (const unitGroup of unitGroups) {
-//             const unit = unitGroup.units[0];
-//             if (unit.kind !== property.kind) {
-//                 continue;
-//             }
-
-//             const chains = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
-//             while (chains.hasNext) {
-//                 const seg = chains.move();
-//                 if (!property.has(seg.index) || seenChains.has(seg.index)) continue;
-
-//                 seenChains.add(seg.index);
-//                 loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
-//             }
-//         }
-
-//         loci.sort((x, y) => x.element - y.element);
-//         return loci;
-//     }
-// }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/custom/collection.ts b/src/mol-model/structure/model/properties/custom/collection.ts
deleted file mode 100644
index e6602fe192108b654abfaaf90667d51a4e8a8482..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/model/properties/custom/collection.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { ModelPropertyDescriptor } from './descriptor'
-
-export class CustomProperties {
-    private _list: ModelPropertyDescriptor[] = [];
-    private _set = new Set<ModelPropertyDescriptor>();
-
-    get all(): ReadonlyArray<ModelPropertyDescriptor> {
-        return this._list;
-    }
-
-    add(desc: ModelPropertyDescriptor<any>) {
-        if (this._set.has(desc)) return;
-
-        this._list.push(desc);
-        this._set.add(desc);
-    }
-
-    has(desc: ModelPropertyDescriptor<any>): boolean {
-        return this._set.has(desc);
-    }
-}
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/custom/descriptor.ts b/src/mol-model/structure/model/properties/custom/descriptor.ts
deleted file mode 100644
index 1d2abb21c712dc17dcfd8129e3cf6faf7abc31c7..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/model/properties/custom/descriptor.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { CifWriter } from 'mol-io/writer/cif'
-import { CifExportContext } from '../../../export/mmcif';
-import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
-import { UUID } from 'mol-util';
-
-interface ModelPropertyDescriptor<ExportCtx = CifExportContext, Symbols extends { [name: string]: QuerySymbolRuntime } = { }> {
-    readonly isStatic: boolean,
-    readonly name: string,
-
-    cifExport?: {
-        // Prefix enforced during export.
-        prefix: string,
-        context?: (ctx: CifExportContext) => ExportCtx | undefined,
-        categories: CifWriter.Category<ExportCtx>[]
-    },
-
-    // TODO: add aliases when lisp-like mol-script is done
-    symbols?: Symbols
-}
-
-function ModelPropertyDescriptor<Ctx, Desc extends ModelPropertyDescriptor<Ctx>>(desc: Desc) {
-    return desc;
-}
-
-namespace ModelPropertyDescriptor {
-    export function getUUID(prop: ModelPropertyDescriptor): UUID {
-        if (!(prop as any).__key) {
-            (prop as any).__key = UUID.create22();
-        }
-        return (prop as any).__key;
-    }
-}
-
-export { ModelPropertyDescriptor }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/custom/residue.ts b/src/mol-model/structure/model/properties/custom/residue.ts
deleted file mode 100644
index 4dd7cb130e9e06d94e2337d1928405faec51422a..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/model/properties/custom/residue.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-// /**
-//  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
-//  *
-//  * @author David Sehnal <david.sehnal@gmail.com>
-//  */
-
-// import { ResidueIndex } from '../../indexing';
-// import { Unit, Structure, StructureElement } from '../../../structure';
-// import { Segmentation } from 'mol-data/int';
-// import { UUID } from 'mol-util';
-// import { CifWriter } from 'mol-io/writer/cif';
-
-// export interface ResidueCustomProperty<T = any> {
-//     readonly id: UUID,
-//     readonly kind: Unit.Kind,
-//     has(idx: ResidueIndex): boolean
-//     get(idx: ResidueIndex): T | undefined
-// }
-
-// export namespace ResidueCustomProperty {
-//     export interface ExportCtx<T> {
-//         elements: StructureElement[],
-//         property(index: number): T
-//     };
-
-//     function getExportCtx<T>(prop: ResidueCustomProperty<T>, structure: Structure): ExportCtx<T> {
-//         const residueIndex = structure.model.atomicHierarchy.residueAtomSegments.index;
-//         const elements = getStructureElements(structure, prop);
-//         return { elements, property: i => prop.get(residueIndex[elements[i].element])! };
-//     }
-
-//     export function getCifDataSource<T>(structure: Structure, prop: ResidueCustomProperty<T> | undefined, cache: any): CifWriter.Category.Instance['source'][0] {
-//         if (!prop) return { rowCount: 0 };
-//         if (cache && cache[prop.id]) return cache[prop.id];
-//         const data = getExportCtx(prop, structure);
-//         const ret = { data, rowCount: data.elements.length };
-//         if (cache) cache[prop.id] = ret;
-//         return ret;
-//     }
-
-//     class FromMap<T> implements ResidueCustomProperty<T> {
-//         readonly id = UUID.create();
-
-//         has(idx: ResidueIndex): boolean {
-//             return this.map.has(idx);
-//         }
-
-//         get(idx: ResidueIndex) {
-//             return this.map.get(idx);
-//         }
-
-//         constructor(private map: Map<ResidueIndex, T>, public kind: Unit.Kind) {
-//         }
-//     }
-
-//     export function fromMap<T>(map: Map<ResidueIndex, T>, kind: Unit.Kind) {
-//         return new FromMap(map, kind);
-//     }
-
-//     /**
-//      * Gets all StructureElements that correspond to 1st atoms of residues that have an property assigned.
-//      * Only works correctly for structures with a single model.
-//      */
-//     export function getStructureElements(structure: Structure, property: ResidueCustomProperty) {
-//         const models = structure.models;
-//         if (models.length !== 1) throw new Error(`Only works on structures with a single model.`);
-
-//         const seenResidues = new Set<ResidueIndex>();
-//         const unitGroups = structure.unitSymmetryGroups;
-//         const loci: StructureElement[] = [];
-
-//         for (const unitGroup of unitGroups) {
-//             const unit = unitGroup.units[0];
-//             if (unit.kind !== property.kind) {
-//                 continue;
-//             }
-
-//             const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
-//             while (residues.hasNext) {
-//                 const seg = residues.move();
-//                 if (!property.has(seg.index) || seenResidues.has(seg.index)) continue;
-
-//                 seenResidues.add(seg.index);
-//                 loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
-//             }
-//         }
-
-//         loci.sort((x, y) => x.element - y.element);
-//         return loci;
-//     }
-// }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/seconday-structure.ts b/src/mol-model/structure/model/properties/seconday-structure.ts
index 49618f0071fc1974657ffd2f01cd1ffaf5c18d31..9434507ebc9ae1a11b2fc02f4cb9e00d0d4c4d0c 100644
--- a/src/mol-model/structure/model/properties/seconday-structure.ts
+++ b/src/mol-model/structure/model/properties/seconday-structure.ts
@@ -17,12 +17,17 @@ interface SecondaryStructure {
 }
 
 namespace SecondaryStructure {
-    export type Element = None | Helix | Sheet
+    export type Element = None | Turn | Helix | Sheet
 
     export interface None {
         kind: 'none'
     }
 
+    export interface Turn {
+        kind: 'turn',
+        flags: SecondaryStructureType
+    }
+
     export interface Helix {
         kind: 'helix',
         flags: SecondaryStructureType,
diff --git a/src/mol-model/structure/model/properties/utils/guess-element.ts b/src/mol-model/structure/model/properties/utils/guess-element.ts
index 05658249f5aed93f771b66cae471b9a74be22955..54a66a8f99537da61f09ebcdab5c4a30e33237fc 100644
--- a/src/mol-model/structure/model/properties/utils/guess-element.ts
+++ b/src/mol-model/structure/model/properties/utils/guess-element.ts
@@ -12,7 +12,7 @@ function charAtIsNumber(str: string, index: number) {
     return code >= 48 && code <= 57
 }
 
-export function guessElement (str: string) {
+export function guessElement(str: string) {
     let at = str.trim().toUpperCase()
 
     if (charAtIsNumber(at, 0)) at = at.substr(1)
diff --git a/src/mol-model/structure/model/properties/utils/secondary-structure.ts b/src/mol-model/structure/model/properties/utils/secondary-structure.ts
index afa8199475f42a616da01e9c8ec056a416c0fd53..e61dd42415969b91a9e065e2a5ba6e18522d1c16 100644
--- a/src/mol-model/structure/model/properties/utils/secondary-structure.ts
+++ b/src/mol-model/structure/model/properties/utils/secondary-structure.ts
@@ -2,6 +2,7 @@
  * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  */
 
 import { SecondaryStructure } from 'mol-model/structure/model/properties/seconday-structure';
@@ -14,13 +15,120 @@ import { IntAdjacencyGraph } from 'mol-math/graph';
 import { BitFlags } from 'mol-util';
 import { ElementIndex } from 'mol-model/structure/model/indexing';
 import { AtomicHierarchy, AtomicConformation } from '../atomic';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
 
-export function computeSecondaryStructure(hierarchy: AtomicHierarchy, conformation: AtomicConformation): SecondaryStructure {
+/**
+ * TODO bugs to fix:
+ * - some turns are not detected correctly: see e.g. pdb:1acj - maybe more than 2 hbonds require some residue to donate electrons
+ * - some sheets are not extended correctly: see e.g. pdb:1acj
+ * - validate new helix definition
+ * - validate new ordering of secondary structure elements
+ */
+
+ /** max distance between two C-alpha atoms to check for hbond */
+const caMaxDist = 9.0;
+
+/**
+ * Constant for electrostatic energy in kcal/mol
+ *      f  *  q1 *   q2
+ * Q = -332 * 0.42 * 0.20
+ *
+ * f is the dimensional factor
+ *
+ * q1 and q2 are partial charges which are placed on the C,O
+ * (+q1,-q1) and N,H (-q2,+q2)
+ */
+const Q = -27.888
+
+/** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */
+const hbondEnergyCutoff = -0.5
+/** prevent extremely low hbond energies */
+const hbondEnergyMinimal = -9.9
+
+interface DSSPContext {
+    params: Partial<PD.Values<SecondaryStructureComputationParams>>,
+    getResidueFlag: (f: DSSPType) => SecondaryStructureType,
+    getFlagName: (f: DSSPType) => String,
+
+    hierarchy: AtomicHierarchy
+    proteinResidues: SortedArray<ResidueIndex>
+    /** flags for each residue */
+    flags: Uint32Array
+    hbonds: DsspHbonds,
+
+    torsionAngles: { phi: Float32Array, psi: Float32Array },
+    backboneIndices: BackboneAtomIndices,
+    conformation: AtomicConformation,
+    ladders: Ladder[],
+    bridges: Bridge[]
+}
+
+interface Ladder {
+    previousLadder: number,
+    nextLadder: number,
+    firstStart: number,
+    secondStart: number,
+    secondEnd: number,
+    firstEnd: number,
+    type: BridgeType
+}
+
+const enum BridgeType {
+    PARALLEL = 0x0,
+    ANTI_PARALLEL = 0x1
+}
+
+class Bridge {
+    partner1: number;
+    partner2: number;
+    type: BridgeType;
+
+    constructor(p1: number, p2: number, type: BridgeType) {
+        this.partner1 = Math.min(p1, p2)
+        this.partner2 = Math.max(p1, p2)
+        this.type = type
+    }
+}
+
+type DSSPType = BitFlags<DSSPType.Flag>
+namespace DSSPType {
+    export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has
+    export const create: (f: Flag) => DSSPType = BitFlags.create
+    export const enum Flag {
+        _ = 0x0,
+        H = 0x1,
+        B = 0x2,
+        E = 0x4,
+        G = 0x8,
+        I = 0x10,
+        S = 0x20,
+        T = 0x40,
+        T3 = 0x80,
+        T4 = 0x100,
+        T5 = 0x200,
+        T3S = 0x400, // marks 3-turn start
+        T4S = 0x800,
+        T5S = 0x1000
+    }
+}
+
+export const SecondaryStructureComputationParams = {
+    oldDefinition: PD.Boolean(true, { description: 'Whether to use the old DSSP convention for the annotation of turns and helices, causes them to be two residues shorter' }),
+    oldOrdering: PD.Boolean(true, { description: 'Alpha-helices are preferred over 3-10 helices' })
+}
+export type SecondaryStructureComputationParams = typeof SecondaryStructureComputationParams
+
+export function computeSecondaryStructure(hierarchy: AtomicHierarchy,
+    conformation: AtomicConformation) {
     // TODO use Zhang-Skolnik for CA alpha only parts or for coarse parts with per-residue elements
     return computeModelDSSP(hierarchy, conformation)
 }
 
-export function computeModelDSSP(hierarchy: AtomicHierarchy, conformation: AtomicConformation) {
+export function computeModelDSSP(hierarchy: AtomicHierarchy,
+    conformation: AtomicConformation,
+    params: Partial<PD.Values<SecondaryStructureComputationParams>> = {}): SecondaryStructure {
+    params = { ...PD.getDefaultValues(SecondaryStructureComputationParams), ...params };
+
     const { lookup3d, proteinResidues } = calcAtomicTraceLookup3D(hierarchy, conformation)
     const backboneIndices = calcBackboneAtomIndices(hierarchy, proteinResidues)
     const hbonds = calcBackboneHbonds(hierarchy, conformation, proteinResidues, backboneIndices, lookup3d)
@@ -28,69 +136,103 @@ export function computeModelDSSP(hierarchy: AtomicHierarchy, conformation: Atomi
     const residueCount = proteinResidues.length
     const flags = new Uint32Array(residueCount)
 
+    // console.log(`calculating secondary structure elements using ${ params.oldDefinition ? 'old' : 'revised'} definition and ${ params.oldOrdering ? 'old' : 'revised'} ordering of secondary structure elements`)
+
+    const torsionAngles = calculateDihedralAngles(hierarchy, conformation, proteinResidues, backboneIndices)
+
+    const ladders: Ladder[] = []
+    const bridges: Bridge[] = []
+
+    const getResidueFlag = params.oldDefinition ? getOriginalResidueFlag : getUpdatedResidueFlag
+    const getFlagName = params.oldOrdering ? getOriginalFlagName : getUpdatedFlagName
+
     const ctx: DSSPContext = {
+        params,
+        getResidueFlag,
+        getFlagName,
+
         hierarchy,
         proteinResidues,
         flags,
-        hbonds
+        hbonds,
+
+        torsionAngles,
+        backboneIndices,
+        conformation,
+        ladders,
+        bridges
     }
 
-    assignBends(ctx)
     assignTurns(ctx)
     assignHelices(ctx)
+    assignBends(ctx)
     assignBridges(ctx)
     assignLadders(ctx)
     assignSheets(ctx)
 
-    const assignment = getDSSPAssignment(flags)
-
+    const assignment = getDSSPAssignment(flags, getResidueFlag)
     const type = new Uint32Array(hierarchy.residues._rowCount) as unknown as SecondaryStructureType[]
+    const keys: number[] = []
+    const elements: SecondaryStructure.Element[] = []
+
     for (let i = 0, il = proteinResidues.length; i < il; ++i) {
-        type[proteinResidues[i]] = assignment[i]
+        const assign = assignment[i]
+        type[proteinResidues[i]] = assign
+        const flag = getResidueFlag(flags[i])
+        // TODO is this expected behavior? elements will be strictly split depending on 'winning' flag
+        if (elements.length === 0 /* would fail at very start */ || flag !== (elements[elements.length - 1] as SecondaryStructure.Helix | SecondaryStructure.Sheet | SecondaryStructure.Turn).flags /* flag changed */) {
+            elements[elements.length] = createElement(mapToKind(assign), flags[i], getResidueFlag)
+        }
+        keys[i] = elements.length - 1
     }
 
     const secondaryStructure: SecondaryStructure = {
         type,
-        key: [], // TODO
-        elements: [] // TODO
+        key: keys,
+        elements: elements
     }
+
     return secondaryStructure
 }
 
-interface DSSPContext {
-    hierarchy: AtomicHierarchy
-    proteinResidues: SortedArray<ResidueIndex>
-    /** flags for each residue */
-    flags: Uint32Array
-
-    hbonds: DsspHbonds
+function createElement(kind: string, flag: DSSPType.Flag, getResidueFlag: (f: DSSPType) => SecondaryStructureType): SecondaryStructure.Element {
+    // TODO would be nice to add more detailed information
+    if (kind === 'helix') {
+        return {
+            kind: 'helix',
+            flags: getResidueFlag(flag)
+        } as SecondaryStructure.Helix
+    } else if (kind === 'sheet') {
+        return {
+            kind: 'sheet',
+            flags: getResidueFlag(flag)
+        } as SecondaryStructure.Sheet
+    } else if (kind === 'turn' || kind === 'bend') {
+        return {
+            kind: 'turn',
+            flags: getResidueFlag(flag)
+        }
+    } else {
+        return {
+            kind: 'none'
+        }
+    }
 }
 
-type DSSPType = BitFlags<DSSPType.Flag>
-namespace DSSPType {
-    export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has
-    export const create: (f: Flag) => DSSPType = BitFlags.create
-    export const enum Flag {
-        _ = 0x0,
-        H = 0x1,
-        B = 0x2,
-        E = 0x4,
-        G = 0x8,
-        I = 0x10,
-        S = 0x20,
-        T = 0x40,
-        T3 = 0x80,
-        T4 = 0x100,
-        T5 = 0x200,
+function mapToKind(assignment: SecondaryStructureType.Flag) {
+    if (assignment === SecondaryStructureType.SecondaryStructureDssp.H || assignment === SecondaryStructureType.SecondaryStructureDssp.G || assignment === SecondaryStructureType.SecondaryStructureDssp.I) {
+        return 'helix'
+    } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.B || assignment === SecondaryStructureType.SecondaryStructureDssp.E) {
+        return 'sheet'
+    } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.T) {
+        return 'turn'
+    } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.S) {
+        return 'bend'
+    } else {
+        return 'none'
     }
 }
 
-/** max distance between two C-alpha atoms to check for hbond */
-const caMaxDist = 7.0;
-
-/** min distance between two C-alpha atoms to check for hbond */
-const caMinDist = 4.0;
-
 function calcAtomicTraceLookup3D(hierarchy: AtomicHierarchy, conformation: AtomicConformation) {
     const { x, y, z } = conformation;
     const { moleculeType, traceElementIndex } = hierarchy.derived.residue
@@ -141,6 +283,114 @@ function calcBackboneAtomIndices(hierarchy: AtomicHierarchy, proteinResidues: So
 
 type DsspHbonds = IntAdjacencyGraph<{ readonly energies: ArrayLike<number> }>
 
+/**
+ * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"]
+ *
+ * Type: S
+ */
+function assignBends(ctx: DSSPContext) {
+    const flags = ctx.flags
+    const { x, y, z } = ctx.conformation
+    const { traceElementIndex } = ctx.hierarchy.derived.residue
+
+    const proteinResidues = ctx.proteinResidues
+    const residueCount = proteinResidues.length
+
+    const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i])
+
+    const caPosPrev2 = Vec3()
+    const caPos = Vec3()
+    const caPosNext2 = Vec3()
+
+    const nIndices = ctx.backboneIndices.nIndices
+    const cPos = Vec3()
+    const nPosNext = Vec3()
+
+    const caMinus2 = Vec3()
+    const caPlus2 = Vec3()
+
+    f1: for (let i = 2; i < residueCount - 2; i++) {
+        // check for peptide bond
+        for (let k = 0; k < 4; k++) {
+            let index = i + k - 2
+            position(traceElementIndex[index], cPos)
+            position(nIndices[index + 1], nPosNext)
+            if (Vec3.squaredDistance(cPos, nPosNext) > 6.25 /* max squared peptide bond distance allowed */) {
+                continue f1
+            }
+        }
+
+        const oRIprev2 = proteinResidues[i - 2]
+        const oRI = proteinResidues[i]
+        const oRInext2 = proteinResidues[i + 2]
+
+        const caAtomPrev2 = traceElementIndex[oRIprev2]
+        const caAtom = traceElementIndex[oRI]
+        const caAtomNext2 = traceElementIndex[oRInext2]
+
+        position(caAtomPrev2, caPosPrev2)
+        position(caAtom, caPos)
+        position(caAtomNext2, caPosNext2)
+
+        Vec3.sub(caMinus2, caPosPrev2, caPos)
+        Vec3.sub(caPlus2, caPos, caPosNext2)
+
+        const angle = Vec3.angle(caMinus2, caPlus2) * 360 / (2 * Math.PI)
+        if (angle && angle > 70.00) {
+            flags[i] |= DSSPType.Flag.S
+        }
+    }
+}
+
+function calculateDihedralAngles(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices): { phi: Float32Array, psi: Float32Array } {
+    const { cIndices, nIndices } = backboneIndices
+    const { index } = hierarchy
+    const { x, y, z } = conformation
+    const { traceElementIndex } = hierarchy.derived.residue
+
+    const residueCount = proteinResidues.length
+    const position = (i: number, v: Vec3) => i === -1 ? Vec3.setNaN(v) : Vec3.set(v, x[i], y[i], z[i])
+
+    let cPosPrev = Vec3(), caPosPrev = Vec3(), nPosPrev = Vec3()
+    let cPos = Vec3(), caPos = Vec3(), nPos = Vec3()
+    let cPosNext = Vec3(), caPosNext = Vec3(), nPosNext = Vec3()
+
+    if (residueCount === 0) return { phi: new Float32Array(0), psi: new Float32Array(0) }
+
+    const phi: Float32Array = new Float32Array(residueCount - 1)
+    const psi: Float32Array = new Float32Array(residueCount - 1)
+
+    position(-1, cPosPrev)
+    position(-1, caPosPrev)
+    position(-1, nPosPrev)
+
+    position(cIndices[0], cPos)
+    position(traceElementIndex[proteinResidues[0]], caPos)
+    position(nIndices[0], nPos)
+
+    position(cIndices[1], cPosNext)
+    position(traceElementIndex[proteinResidues[1]], caPosNext)
+    position(nIndices[1], nPosNext)
+
+    for (let i = 0; i < residueCount - 1; ++i) {
+        // ignore C-terminal residue as acceptor
+        if (index.findAtomOnResidue(proteinResidues[i], 'OXT') !== -1) continue
+
+        // returns NaN for missing atoms
+        phi[i] = Vec3.dihedralAngle(cPosPrev, nPos, caPos, cPos)
+        psi[i] = Vec3.dihedralAngle(nPos, caPos, cPos, nPosNext)
+
+        cPosPrev = cPos, caPosPrev = caPos, nPosPrev = nPos
+        cPos = cPosNext, caPos = caPosNext, nPos = nPosNext
+
+        position(cIndices[i + 1], cPosNext)
+        position(traceElementIndex[proteinResidues[i + 1]], caPosNext)
+        position(nIndices[i + 1], nPosNext)
+    }
+
+    return { phi, psi };
+}
+
 function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices, lookup3d: GridLookup3D): DsspHbonds {
     const { cIndices, hIndices, nIndices, oIndices } = backboneIndices
     const { index } = hierarchy
@@ -154,16 +404,14 @@ function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConf
     const nAtomResidues: number[] = [];
     const energies: number[] = [];
 
-    const oPos = Vec3.zero()
-    const cPos = Vec3.zero()
-    const caPos = Vec3.zero()
-    const nPos = Vec3.zero()
-    const hPos = Vec3.zero()
-
-    const cPosPrev = Vec3.zero()
-    const oPosPrev = Vec3.zero()
+    const oPos = Vec3()
+    const cPos = Vec3()
+    const caPos = Vec3()
+    const nPos = Vec3()
+    const hPos = Vec3()
 
-    const caMinDistSq = caMinDist * caMinDist
+    const cPosPrev = Vec3()
+    const oPosPrev = Vec3()
 
     for (let i = 0, il = proteinResidues.length; i < il; ++i) {
         const oPI = i
@@ -183,11 +431,9 @@ function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConf
         position(cAtom, cPos)
         position(caAtom, caPos)
 
-        const { indices, count, squaredDistances } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist)
+        const { indices, count } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist)
 
         for (let j = 0; j < count; ++j) {
-            if (squaredDistances[j] < caMinDistSq) continue
-
             const nPI = indices[j]
 
             // ignore bonds within a residue or to prev or next residue, TODO take chain border into account
@@ -245,8 +491,8 @@ function buildHbondGraph(residueCount: number, oAtomResidues: number[], nAtomRes
 /** Original priority: H,B,E,G,I,T,S */
 function getOriginalResidueFlag(f: DSSPType) {
     if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
-    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
+    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
     if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
     if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
@@ -254,55 +500,50 @@ function getOriginalResidueFlag(f: DSSPType) {
     return SecondaryStructureType.Flag.None
 }
 
+function getOriginalFlagName(f: DSSPType) {
+    if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
+    if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
+    if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
+    if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
+    if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
+    if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
+    if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
+    return '-'
+}
+
 /** Version 2.1.0 priority: I,H,B,E,G,T,S */
 function getUpdatedResidueFlag(f: DSSPType) {
     if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
     if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
-    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
+    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
     if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
     if (DSSPType.is(f, DSSPType.Flag.S)) return SecondaryStructureType.SecondaryStructureDssp.S
     return SecondaryStructureType.Flag.None
 }
 
-// function geFlagName(f: DSSPType) {
-//     if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
-//     if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
-//     if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
-//     if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
-//     if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
-//     if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
-//     if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
-//     return '-'
-// }
-
-function getDSSPAssignment(flags: Uint32Array, useOriginal = false) {
-    const getResidueFlag = useOriginal ? getOriginalResidueFlag : getUpdatedResidueFlag
+function getUpdatedFlagName(f: DSSPType) {
+    if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
+    if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
+    if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
+    if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
+    if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
+    if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
+    if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
+    return '-'
+}
+
+function getDSSPAssignment(flags: Uint32Array, getResidueFlag: (f: DSSPType) => SecondaryStructureType) {
     const type = new Uint32Array(flags.length)
     for (let i = 0, il = flags.length; i < il; ++i) {
         const f = DSSPType.create(flags[i])
-        // console.log(i, geFlagName(f))
         type[i] = getResidueFlag(f)
     }
+
     return type as unknown as ArrayLike<SecondaryStructureType>
 }
 
-/**
- * Constant for electrostatic energy in kcal/mol
- *      f  *  q1 *   q2
- * Q = -332 * 0.42 * 0.20
- *
- * f is the dimensional factor
- *
- * q1 and q2 are partial charges which are placed on the C,O
- * (+q1,-q1) and N,H (-q2,+q2)
- */
-const Q = -27.888
-
-/** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */
-const hbondEnergyCutoff = -0.5
-
 /**
  * E = Q * (1/r(ON) + l/r(CH) - l/r(OH) - l/r(CN))
  */
@@ -314,7 +555,13 @@ function calcHbondEnergy(oPos: Vec3, cPos: Vec3, nPos: Vec3, hPos: Vec3) {
 
     const e1 = Q / distOH - Q / distCH
     const e2 = Q / distCN - Q / distON
-    return e1 + e2
+    const e = e1 + e2
+
+    // cap lowest possible energy
+    if (e < hbondEnergyMinimal)
+        return hbondEnergyMinimal
+
+    return e
 }
 
 /**
@@ -329,24 +576,31 @@ function assignTurns(ctx: DSSPContext) {
     const { chains, residueAtomSegments, chainAtomSegments } = hierarchy
     const { label_asym_id } = chains
 
-    const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
+    const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
 
-    for (let i = 0, il = proteinResidues.length; i < il; ++i) {
-        const rI = proteinResidues[i]
-        const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
-
-        // TODO should take sequence gaps into account
-        for (let k = 3; k <= 5; ++k) {
-            if (i + k >= proteinResidues.length) continue
+    for (let idx = 0; idx < 3; idx++) {
+        for (let i = 0, il = proteinResidues.length - 1; i < il; ++i) {
+            const rI = proteinResidues[i]
+            const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
 
-            const rN = proteinResidues[i + k]
+            // TODO should take sequence gaps into account
+            const rN = proteinResidues[i + idx + 3]
             const cN = chainAtomSegments.index[residueAtomSegments.offsets[rN]]
             // check if on same chain
             if (!label_asym_id.areValuesEqual(cI, cN)) continue
 
             // check if hbond exists
-            if (hbonds.getDirectedEdgeIndex(i, i + k) !== -1) {
-                flags[i] |= turnFlag[k] | DSSPType.Flag.T
+            if (hbonds.getDirectedEdgeIndex(i, i + idx + 3) !== -1) {
+                flags[i] |= turnFlag[idx + 3] | turnFlag[idx]
+                if (ctx.params.oldDefinition) {
+                    for (let k = 1; k < idx + 3; ++k) {
+                        flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T
+                    }
+                } else {
+                    for (let k = 0; k <= idx + 3; ++k) {
+                        flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T
+                    }
+                }
             }
         }
     }
@@ -369,7 +623,7 @@ function assignTurns(ctx: DSSPContext) {
  * Type: B
  */
 function assignBridges(ctx: DSSPContext) {
-    const { proteinResidues, hbonds, flags } = ctx
+    const { proteinResidues, hbonds, flags, bridges } = ctx
 
     const { offset, b } = hbonds
     let i: number, j: number
@@ -385,6 +639,8 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j, i + 1) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                // TODO move to constructor, actually omit object all together
+                bridges[bridges.length] = new Bridge(i, j, BridgeType.PARALLEL)
             }
 
             // Parallel Bridge(i, j) =: [Hbond(j - 1, i) and Hbond(i, j + 1)]
@@ -393,6 +649,7 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                bridges[bridges.length] = new Bridge(j, i, BridgeType.PARALLEL)
             }
 
             // Antiparallel Bridge(i, j) =: [Hbond(i, j) and Hbond(j, i)]
@@ -401,6 +658,7 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j, i) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                bridges[bridges.length] = new Bridge(j, i, BridgeType.ANTI_PARALLEL)
             }
 
             // Antiparallel Bridge(i, j) =: [Hbond(i - 1, j + 1) and Hbond(j - 1, i + l)]
@@ -409,9 +667,12 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i + 1) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                bridges[bridges.length] = new Bridge(j, i, BridgeType.ANTI_PARALLEL)
             }
         }
     }
+
+    bridges.sort((a, b) => a.partner1 > b.partner1 ? 1 : a.partner1 < b.partner1 ? -1 : 0)
 }
 
 /**
@@ -428,17 +689,41 @@ function assignBridges(ctx: DSSPContext) {
 function assignHelices(ctx: DSSPContext) {
     const { proteinResidues, flags } = ctx
 
-    const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
+    const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
     const helixFlag = [0, 0, 0, DSSPType.Flag.G, DSSPType.Flag.H, DSSPType.Flag.I]
 
-    for (let i = 1, il = proteinResidues.length; i < il; ++i) {
-        const fI = DSSPType.create(flags[i])
-        const fI1 = DSSPType.create(flags[i - 1])
+    const helixCheckOrder = ctx.params.oldOrdering ? [4, 3, 5] : [3, 4, 5]
+    for (let ni = 0; ni < helixCheckOrder.length; ni++) {
+        const n = helixCheckOrder[ni]
+
+        for (let i = 1, il = proteinResidues.length - n; i < il; i++) {
+            const fI = DSSPType.create(flags[i])
+            const fI1 = DSSPType.create(flags[i - 1])
+            const fI2 = DSSPType.create(flags[i + 1])
+
+            // TODO rework to elegant solution which will not break instantly
+            if (ctx.params.oldOrdering) {
+                if ((n === 3 && (DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.H)) || // for 3-10 yield to alpha helix
+                    (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices
+                    continue
+                }
+            } else {
+                if ((n === 4 && (DSSPType.is(fI, DSSPType.Flag.G) || DSSPType.is(fI2, DSSPType.Flag.G)) || // for alpha helix yield to 3-10
+                    (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices
+                    continue
+                }
+            }
 
-        for (let k = 3; k <= 5; ++k) {
-            if (DSSPType.is(fI, turnFlag[k]) && DSSPType.is(fI1, turnFlag[k])) {
-                for (let l = 0; l < k; ++l) {
-                    flags[i + l] |= helixFlag[k]
+            if (DSSPType.is(fI, turnFlag[n]) && DSSPType.is(fI, turnFlag[n - 3]) && // check fI for turn start of proper type
+                DSSPType.is(fI1, turnFlag[n]) && DSSPType.is(fI1, turnFlag[n - 3])) { // check fI1 accordingly
+                if (ctx.params.oldDefinition) {
+                    for (let k = 0; k < n; k++) {
+                        flags[i + k] |= helixFlag[n]
+                    }
+                } else {
+                    for (let k = -1; k <= n; k++) {
+                        flags[i + k] |= helixFlag[n]
+                    }
                 }
             }
         }
@@ -451,23 +736,137 @@ function assignHelices(ctx: DSSPContext) {
  * Type: E
  */
 function assignLadders(ctx: DSSPContext) {
-    // TODO
+    const { bridges, ladders } = ctx
+
+    // create ladders
+    for (let bridgeIndex = 0; bridgeIndex < bridges.length; bridgeIndex++) {
+        const bridge = bridges[bridgeIndex]
+        let found = false
+        for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) {
+            const ladder = ladders[ladderIndex]
+            if (shouldExtendLadder(ladder, bridge)) {
+                found = true
+                ladder.firstEnd++
+                if (bridge.type === BridgeType.PARALLEL) {
+                    ladder.secondEnd++
+                } else {
+                    ladder.secondStart--
+                }
+            }
+        }
+
+        // no suitable assignment: create new ladder with single bridge as content
+        if (!found) {
+            ladders[ladders.length] = {
+                previousLadder: 0,
+                nextLadder: 0,
+                firstStart: bridge.partner1,
+                firstEnd: bridge.partner1,
+                secondStart: bridge.partner2,
+                secondEnd: bridge.partner2,
+                type: bridge.type
+            }
+        }
+    }
+
+    // connect ladders
+    for (let ladderIndex1 = 0; ladderIndex1 < ladders.length; ladderIndex1++) {
+        const ladder1 = ladders[ladderIndex1]
+        for (let ladderIndex2 = ladderIndex1; ladderIndex2 < ladders.length; ladderIndex2++) {
+            const ladder2 = ladders[ladderIndex2]
+            if (resemblesBulge(ladder1, ladder2)) {
+                ladder1.nextLadder = ladderIndex2
+                ladder2.previousLadder = ladderIndex1
+            }
+        }
+    }
 }
 
 /**
- * sheet=: set of one or more ladders connected by shared residues
- *
- * Type: E
+ * For beta structures, we define: a bulge-linked ladder consists of two ladders or bridges of the same type
+ * connected by at most one extra residue of one strand and at most four extra residues  on the other strand,
+ * all residues in bulge-linked ladders are marked E, including any extra residues.
  */
-function assignSheets(ctx: DSSPContext) {
-    // TODO
+function resemblesBulge(ladder1: Ladder, ladder2: Ladder) {
+    if (!(ladder1.type === ladder2.type && ladder2.firstStart - ladder1.firstEnd < 6 &&
+        ladder1.firstStart < ladder2.firstStart && ladder2.nextLadder === 0)) return false
+
+    if (ladder1.type === BridgeType.PARALLEL) {
+        return bulgeCriterion2(ladder1, ladder2)
+    } else {
+        return bulgeCriterion2(ladder2, ladder1)
+    }
+}
+
+function bulgeCriterion2(ladder1: Ladder, ladder2: Ladder) {
+    return ladder2.secondStart - ladder1.secondEnd > 0 && ((ladder2.secondStart - ladder1.secondEnd < 6 &&
+        ladder2.firstStart - ladder1.firstEnd < 3) || ladder2.secondStart - ladder1.secondEnd < 3)
+}
+
+function shouldExtendLadder(ladder: Ladder, bridge: Bridge): boolean {
+    // in order to extend ladders, same type must be present
+    if (bridge.type !== ladder.type) return false
+
+    // only extend if residue 1 is sequence neighbor with regard to ladder
+    if (bridge.partner1 !== ladder.firstEnd + 1) return false
+
+    if (bridge.type === BridgeType.PARALLEL) {
+        if (bridge.partner2 === ladder.secondEnd + 1) {
+            return true
+        }
+    } else {
+        if (bridge.partner2 === ladder.secondStart - 1) {
+            return true
+        }
+    }
+
+    return false
+}
+
+function isHelixType(f: DSSPType) {
+    return DSSPType.is(f, DSSPType.Flag.G) || DSSPType.is(f, DSSPType.Flag.H) || DSSPType.is(f, DSSPType.Flag.I)
 }
 
 /**
- * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"]
+ * sheet=: set of one or more ladders connected by shared residues
  *
- * Type: S
+ * Type: E
  */
-function assignBends(ctx: DSSPContext) {
-    // TODO
+function assignSheets(ctx: DSSPContext) {
+    const { ladders, flags } = ctx
+    for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) {
+        const ladder = ladders[ladderIndex]
+        for (let lcount = ladder.firstStart; lcount <= ladder.firstEnd; lcount++) {
+            const diff = ladder.firstStart - lcount
+            const l2count = ladder.secondStart - diff
+
+            if (ladder.firstStart !== ladder.firstEnd) {
+                flags[lcount] |= DSSPType.Flag.E
+                flags[l2count] |= DSSPType.Flag.E
+            } else {
+                if (!isHelixType(flags[lcount]) && DSSPType.is(flags[lcount], DSSPType.Flag.E)) {
+                    flags[lcount] |= DSSPType.Flag.B
+                }
+                if (!isHelixType(flags[l2count]) && DSSPType.is(flags[l2count], DSSPType.Flag.E)) {
+                    flags[l2count] |= DSSPType.Flag.B
+                }
+            }
+        }
+
+        if (ladder.nextLadder === 0) continue
+
+        const conladder = ladders[ladder.nextLadder]
+        for (let lcount = ladder.firstStart; lcount <= conladder.firstEnd; lcount++) {
+            flags[lcount] |= DSSPType.Flag.E
+        }
+        if (ladder.type === BridgeType.PARALLEL) {
+            for (let lcount = ladder.secondStart; lcount <= conladder.secondEnd; lcount++) {
+                flags[lcount] |= DSSPType.Flag.E
+            }
+        } else {
+            for (let lcount = conladder.secondEnd; lcount <= ladder.secondStart; lcount++) {
+                flags[lcount] |= DSSPType.Flag.E
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/utils/secondary-structure.validation b/src/mol-model/structure/model/properties/utils/secondary-structure.validation
new file mode 100644
index 0000000000000000000000000000000000000000..4f1a1f1bba2be14065663ee39ef36e21fb2d575a
--- /dev/null
+++ b/src/mol-model/structure/model/properties/utils/secondary-structure.validation
@@ -0,0 +1,75 @@
+compares Mol* port of DSSP (with default parameters) to the BioJava implementation
+
+### pdb:1pga ###
+# turns #
+Mol*: ----------------------TTTTTTTTTTTTTTTT--------TTTT------
+53 turns, 18 openings
+DSSP: ----------------------TTTTTTTTTTTTTTTT--------TTTT------
+53 turns, 18 openings
+
+# bends #
+Mol*: ---------SS---------SSSSSSSSSSSSSSSSSS--------SSS-------
+23 bends
+DSSP: ---------SS---------SSSSSSSSSSSSSSSSSS--------SSS-------
+23 bends
+
+# helices #
+Mol*: ----------------------HHHHHHHHHHHHHHTT--------TTTT------
+44 helix elements - 0 3-10, 44 alpha, 0 pi
+DSSP: ----------------------HHHHHHHHHHHHHHTT--------TTTT------
+44 helix elements - 0 3-10, 44 alpha, 0 pi
+
+# all #
+Mol*: -EEEEEEE-SS-EEEEEEE-SSHHHHHHHHHHHHHHTT---EEEEETTTTEEEEE-
+DSSP: -EEEEEEE-SS-EEEEEEE-SSHHHHHHHHHHHHHHTT---EEEEETTTTEEEEE-
+
+
+### pdb:1bta ###
+# turns #
+Mol*: ------TTT---TTTTTTTTTTTTT--TT----TTTTTTTTTTT-----------TTTTTTTTT--TTTTTTTTTTTTTTT--------
+127 turns, 44 openings
+DSSP: ------TTT---TTTTTTTTTTTTT--TT----TTTTTTTTTTT-----------TTTTTTTTT--TTTTTTTTTTTTTTT--------
+127 turns, 44 openings
+
+# bends #
+Mol*: ------SSS--SSSSSSSSSSSSS---SS---SSSSSSSSSSSSS-SS------SSSSSSSSSSSSSSSSSSSSSSSSSSS--------
+60 bends
+DSSP: ------SSS--SSSSSSSSSSSSS---SS---SSSSSSSSSSSSS-SS------SSSSSSSSSSSSSSSSSSSSSSSSSSS--------
+60 bends
+
+# helices #
+Mol*: ------TTT---HHHHHHHHHHHHT--TT----HHHHHHHHTTT-----------TTHHHHTTT--HHHHHHHHHHHHHTT--------
+100 helix elements - 0 3-10, 100 alpha, 0 pi
+DSSP: ------TTT---HHHHHHHHHHHHT--TT----HHHHHHHHTTT-----------TTHHHHTTT--HHHHHHHHHHHHHTT--------
+100 helix elements - 0 3-10, 100 alpha, 0 pi
+
+# all #
+Mol*: -EEEEETTT--SHHHHHHHHHHHHT--TT---SHHHHHHHHTTTS-SSEEEEEESTTHHHHTTTSSHHHHHHHHHHHHHTT--EEEEE-
+DSSP: -EEEEETTT--SHHHHHHHHHHHHT--TT---SHHHHHHHHTTTS-SSEEEEEESTTHHHHTTTSSHHHHHHHHHHHHHTT--EEEEE-
+
+
+### pdb:1acj ###
+# turns #
+Mol*: -------TT----------TT----------------TTTTT----------------------------TTTT-TTTTTT----------------------------------TTT------TTT-TTTTTTTTT-----------TTTT---TT-------TTTTTTTTTTTTTTTTTTTTT--TT-------TTTTTTTTTTTT-TTTTTT----------TT-TT----TTTTTTTTTTTTTTTT-----TTTTTTTTTT--TTTTTTTTTTT-----------------------TTTTTTTT----------------TTTTTTT-TT--TT------TTTTTTTTTTTTTT--TTTTTTTTTTT--TTTTT-TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT-------------TT----TTT---TTTTTTTTTTTTT-TTT---TTTTTTTTTTTTTTTTTTTT--------------------------------TTTTTTTTTTTTTTTTTTT-
+614 turns, 223 openings
+DSSP: -------TT----------TT----------------TTTTT------------------------------TT-TTTTTT----------------------------------TTT------TTT-TTTTTTTTT-----------TTTT---TT-------TTTTTTTTTTTTTTTTTTTTT--TT-------TTTTTTTTTTTT-TTTTTT----------TT-TT----TTTTTTTTTTTTTTTT-----TTTTTTTTTT--TTTTTTTTTTT-----------------------TTTTTTTT----------------TTTTTTT-TT--TT------TTTTTTTTTTT-TT--TTTTTTTTTTT--TTTTT-TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT-------------TT----TTT---TTTTTTTTTTTTT-TTT---TTTTTTTTTTTTTTTTTTTT--------------------------------TTTTTTTTTTTTTTTTTTT-
+606 turns, 220 openings
+
+# bends #
+Mol*: --S----SS----------SSS----------S----SS-SSS--------SS-----S-----------SSSS-SSSSSSS--S---S----------SS--SS---------SSSS---S-SSSS--SSSSSSS-----------SSSS----SS-SSS-S-SSSSSSSSSSSSSSSSSSSSS--SSS------SSSSSSSSSSSS-SSSSSS-S----SS--SSSSSS---SSSSSSSSSSSSSSS----S-SSSSSSSSSSS-SSSSSSSSSS--SS--SS--S------SSSSSS-SSSSSSS--S--S-------S--SSSSSSSSSSS--SSS-----SSSSSSSSSSS-SS--SSSSSSSSSSS--SSSSS-SSSSSSSSSSSSSSSSS----SSSSSSSSSSSS-----------SS--S-SSS-S-SS-SSSSSS--SS-SSS---SSSSSSSSSSSSSSSSSSSSSSSS---------SSS------SSSS----SSSS-S----SSSSSSSSSS--
+305 bends
+DSSP: --S----SS----------SSS----------S----SS-SSS--------SS-----S-----------SSSS-SSSSSSS--S---S----------SS--SS---------SSSS---S-SSSS--SSSSSSS-----------SSSS----SS-SSS-S-SSSSSSSSSSSSSSSSSSSSS--SSS------SSSSSSSSSSSS-SSSSSS-S----SS--SSSSSS---SSSSSSSSSSSSSSS----S-SSSSSSSSSSS-SSSSSSSSSS--SS--SS--S------SSSSSS-SSSSSSS--S--S-------S--SSSSSSSSSSS--SSS-----SSSSSSSSSSS-SS--SSSSSSSSSSS--SSSSS-SSSSSSSSSSSSSSSSS----SSSSSSSSSSSS-----------SS--S-SSS-S-SS-SSSSSS--SS-SSS---SSSSSSSSSSSSSSSSSSSSSSSS---------SSS------SSSS----SSSS-S----SSSSSSSSSS--
+305 bends
+
+# helices #
+Mol*: -------TT----------TT----------------GGGTT----------------------------TTTT-HHHHTT----------------------------------TTT------GGG-THHHHHHHT-----------HHHH---TT-------HHHHHHHHHHHHHHHHGGGGT--TT-------THHHHHHHHHHH-HHHHTT----------TT-TT----HHHHHHHHHHHHHHTT-----HHHHHHHHHH--HHHHHHHHGGG-----------------------HHHHHHHT----------------HHHHHHH-TT--TT------HHHHHHHHHHHTTT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTT-------------TT----GGG---TTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHT--------------------------------TTHHHHHHHHTHHHHHHHH-
+523 helix elements - 27 3-10, 496 alpha, 0 pi
+DSSP: -------TT----------TT----------------GGGTT------------------------------TT-HHHHTT----------------------------------TTT------GGG-THHHHHHHT-----------HHHH---TT-------HHHHHHHHHHHHHHHHGGGGT--TT-------THHHHHHHHHHH-HHHHTT----------TT-TT----HHHHHHHHHHHHHHTT-----HHHHHHHHHH--HHHHHHHHGGG-----------------------HHHHHHHT----------------HHHHHHH-TT--TT------HHHHHHHHHHH-TT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTT-------------TT----GGG---TTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHT--------------------------------TTHHHHHHHHTHHHHHHHH-
+523 helix elements - 27 3-10, 496 alpha, 0 pi
+
+# all #
+Mol*: --SEEEETTEEEE-EEEEETTEEEEEEEEEE-EE---GGGTTS--EE----SSEEE--S---B-------TTTT-HHHHTTS--S-B-S---EEEEEE-SS--SSEEEEEEE--STTT---S-SGGG-THHHHHHHT-EEEE-----SHHHH---TT-SSS-S-HHHHHHHHHHHHHHHHGGGGTEEEEEEEEEEETHHHHHHHHHHH-HHHHTT-SEEEEES--TTSTTSEEEHHHHHHHHHHHHHHTT---S-HHHHHHHHHHS-HHHHHHHHGGG-SS--SS--S--EEE-SSSSSS-HHHHHHHT-S--S-EEEEEESB-SHHHHHHHSTT--TTS-----HHHHHHHHHHHTTT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTTSS-EEEEEE----TT--S-GGG-SBTTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHTSSSS---------SSS-EEEEESSSS--EEESTTHHHHHHHHTHHHHHHHH-
+DSSP: --SEEEETTEEEE-EEEEETTEEEEEEEEEE-EE---GGGTTS--EE----SSEEE--S---B-------SSTT-HHHHTTS--S-B-S---EEEEEE-SS--SSEEEEEEE--STTT---S-SGGG-THHHHHHHT-EEEE-----SHHHH---TT-SSS-S-HHHHHHHHHHHHHHHHGGGGTEEEEEEEEEEETHHHHHHHHHHH-HHHHTT-SEEEEES--TTSTTS-EEHHHHHHHHHHHHHHTT---S-HHHHHHHHHHS-HHHHHHHHGGG-SS--SS--S---EE-SSSSSS-HHHHHHHT-S--S-EEEEEESB-SHHHHHHHSTT--TTS-----HHHHHHHHHHH-TT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTTSS-EEEEEE----TT--S-GGG-SBTTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHTSSSS---------SSS-EEEEESSSS--EEESTTHHHHHHHHTHHHHHHHH-
+
+TODO fix mismatches                                                   e.g. here
+TODO move to spec.ts once tests are running
\ No newline at end of file
diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts
index 1361395d7064bf4607f18930dfbdb7e8b193dcdc..8b5e1b947e970cc7bf8fcb6533801849423e972e 100644
--- a/src/mol-model/structure/model/types.ts
+++ b/src/mol-model/structure/model/types.ts
@@ -275,41 +275,42 @@ export namespace SecondaryStructureType {
         DoubleHelix = 0x1,
         Helix = 0x2,
         Beta = 0x4,
-        Turn = 0x8,
+        Bend = 0x8,
+        Turn = 0x10,
 
         // category variant
-        LeftHanded = 0x10,  // helix
-        RightHanded = 0x20,
+        LeftHanded = 0x20,  // helix
+        RightHanded = 0x40,
 
-        ClassicTurn = 0x40,  // turn
-        InverseTurn = 0x80,
+        ClassicTurn = 0x80,  // turn
+        InverseTurn = 0x100,
 
         // sub-category
-        HelixOther = 0x100,  // protein
-        Helix27 = 0x200,
-        Helix3Ten = 0x400,
-        HelixAlpha = 0x800,
-        HelixGamma = 0x1000,
-        HelixOmega = 0x2000,
-        HelixPi = 0x4000,
-        HelixPolyproline = 0x8000,
-
-        DoubleHelixOther = 0x10000,  // nucleic
-        DoubleHelixZ = 0x20000,
-        DoubleHelixA = 0x40000,
-        DoubleHelixB = 0x80000,
-
-        BetaOther = 0x100000,  // protein
-        BetaStrand = 0x200000,  // single strand
-        BetaSheet = 0x400000,  // multiple hydrogen bonded strands
-        BetaBarell = 0x800000,  // closed series of sheets
-
-        TurnOther = 0x1000000,  // protein
-        Turn1 = 0x2000000,
-        Turn2 = 0x4000000,
-        Turn3 = 0x8000000,
-
-        NA = 0x10000000,  // not applicable/available
+        HelixOther = 0x200,  // protein
+        Helix27 = 0x400,
+        Helix3Ten = 0x800,
+        HelixAlpha = 0x1000,
+        HelixGamma = 0x2000,
+        HelixOmega = 0x4000,
+        HelixPi = 0x8000,
+        HelixPolyproline = 0x10000,
+
+        DoubleHelixOther = 0x20000,  // nucleic
+        DoubleHelixZ = 0x40000,
+        DoubleHelixA = 0x80000,
+        DoubleHelixB = 0x100000,
+
+        BetaOther = 0x200000,  // protein
+        BetaStrand = 0x400000,  // single strand
+        BetaSheet = 0x800000,  // multiple hydrogen bonded strands
+        BetaBarell = 0x1000000,  // closed series of sheets
+
+        TurnOther = 0x2000000,  // protein
+        Turn1 = 0x4000000,
+        Turn2 = 0x8000000,
+        Turn3 = 0x10000000,
+
+        NA = 0x20000000,  // not applicable/available
     }
 
     export const SecondaryStructureMmcif: { [value: string]: number } = {
@@ -386,7 +387,7 @@ export namespace SecondaryStructureType {
         G: Flag.Helix | Flag.Helix3Ten,  // 3-helix (310 helix)
         I: Flag.Helix | Flag.HelixPi,  // 5 helix (pi-helix)
         T: Flag.Turn,  // hydrogen bonded turn
-        S: Flag.Turn,  // bend
+        S: Flag.Bend,  // bend
     }
 }
 
diff --git a/src/mol-model/structure/query.ts b/src/mol-model/structure/query.ts
index df09d47cb60acb44696a3ebe3ea7d7bb26480c79..8538f0bc795a9043518a5109bf5df6f86609586e 100644
--- a/src/mol-model/structure/query.ts
+++ b/src/mol-model/structure/query.ts
@@ -9,12 +9,14 @@ import { StructureQuery } from './query/query'
 export * from './query/context'
 import * as generators from './query/queries/generators'
 import * as modifiers from './query/queries/modifiers'
+import * as filters from './query/queries/filters'
 import * as combinators from './query/queries/combinators'
 import * as internal from './query/queries/internal'
 import pred from './query/predicates'
 
 export const Queries = {
     generators,
+    filters,
     modifiers,
     combinators,
     pred,
diff --git a/src/mol-model/structure/query/queries/filters.ts b/src/mol-model/structure/query/queries/filters.ts
index a64cffc38bf15eae0a037b80153a2ac03b8d6fd4..4b08350f68967e22fa5002cc6babafb37121756a 100644
--- a/src/mol-model/structure/query/queries/filters.ts
+++ b/src/mol-model/structure/query/queries/filters.ts
@@ -31,6 +31,25 @@ export function pick(query: StructureQuery, pred: QueryPredicate): StructureQuer
     };
 }
 
+export function first(query: StructureQuery): StructureQuery {
+    return ctx => {
+        const sel = query(ctx);
+        const ret = StructureSelection.LinearBuilder(ctx.inputStructure);
+        if (sel.kind === 'singletons') {
+            if (sel.structure.elementCount > 0) {
+                const u = sel.structure.units[0];
+                const s = Structure.create([u.getChild(SortedArray.ofSingleton(u.elements[0]))], ctx.inputStructure);
+                ret.add(s);
+            }
+        } else {
+            if (sel.structures.length > 0) {
+                ret.add(sel.structures[0]);
+            }
+        }
+        return ret.getSelection();
+    };
+}
+
 export interface UnitTypeProperties { atomic?: QueryFn, coarse?: QueryFn }
 
 export function getCurrentStructureProperties(ctx: QueryContext, props: UnitTypeProperties, set: Set<any>) {
diff --git a/src/mol-model/structure/query/queries/generators.ts b/src/mol-model/structure/query/queries/generators.ts
index 34ce3fd620119edf4ce852b80bd19e146f205489..24f49db0f080c80869b55d31efdfa8c481b46ef2 100644
--- a/src/mol-model/structure/query/queries/generators.ts
+++ b/src/mol-model/structure/query/queries/generators.ts
@@ -168,10 +168,10 @@ function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, group
     };
 }
 
-function getRingStructure(unit: Unit.Atomic, ring: UnitRing) {
+function getRingStructure(unit: Unit.Atomic, ring: UnitRing, inputStructure: Structure) {
     const elements = new Int32Array(ring.length) as any as ElementIndex[];
     for (let i = 0, _i = ring.length; i < _i; i++) elements[i] = unit.elements[ring[i]];
-    return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))])
+    return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))], inputStructure);
 }
 
 export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): StructureQuery {
@@ -184,7 +184,7 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure
                 if (!Unit.isAtomic(u)) continue;
 
                 for (const r of u.rings.all) {
-                    ret.add(getRingStructure(u, r));
+                    ret.add(getRingStructure(u, r, ctx.inputStructure));
                 }
             }
         } else {
@@ -198,7 +198,7 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure
                 for (const fp of uniqueFps.array) {
                     if (!rings.byFingerprint.has(fp)) continue;
                     for (const r of rings.byFingerprint.get(fp)!) {
-                        ret.add(getRingStructure(u, rings.all[r]));
+                        ret.add(getRingStructure(u, rings.all[r], ctx.inputStructure));
                     }
                 }
             }
diff --git a/src/mol-model/structure/query/queries/internal.ts b/src/mol-model/structure/query/queries/internal.ts
index 8cf1078110a6c3c929f56bada9d17a1e2ea8d8bf..d451ca422df1647be96601af99d03351f986556c 100644
--- a/src/mol-model/structure/query/queries/internal.ts
+++ b/src/mol-model/structure/query/queries/internal.ts
@@ -35,7 +35,7 @@ export function atomicSequence(): StructureQuery {
 
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure, ));
     };
 }
 
@@ -54,7 +54,7 @@ export function water(): StructureQuery {
             if (P.entity.type(l) !== 'water') continue;
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure));
     };
 }
 
@@ -84,7 +84,7 @@ export function atomicHet(): StructureQuery {
 
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure));
     };
 }
 
@@ -97,6 +97,6 @@ export function spheres(): StructureQuery {
             if (unit.kind !== Unit.Kind.Spheres) continue;
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure));
     };
 }
diff --git a/src/mol-model/structure/query/selection.ts b/src/mol-model/structure/query/selection.ts
index c13c370daaa5ea8783845a10d5303611cefd2c99..9fe6201be03c59e39f096f9650b9c08127988e35 100644
--- a/src/mol-model/structure/query/selection.ts
+++ b/src/mol-model/structure/query/selection.ts
@@ -135,7 +135,7 @@ namespace StructureSelection {
                 const { elements } = unit;
                 for (let i = 0, _i = elements.length; i < _i; i++) {
                     // TODO: optimize this somehow???
-                    const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))]);
+                    const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))], sel.source);
                     fn(s, idx++);
                 }
             }
diff --git a/src/mol-model/structure/query/utils/structure-set.ts b/src/mol-model/structure/query/utils/structure-set.ts
index 0ab6a9bbfafe296799320aa3f5d22e7f5d6b28b8..4f61dbc78f7223a01597dce742942cf657044e57 100644
--- a/src/mol-model/structure/query/utils/structure-set.ts
+++ b/src/mol-model/structure/query/utils/structure-set.ts
@@ -80,7 +80,7 @@ export function structureIntersect(sA: Structure, sB: Structure): Structure {
         }
     }
 
-    return Structure.create(units);
+    return Structure.create(units, sA.parent || sB.parent);
 }
 
 export function structureSubtract(a: Structure, b: Structure): Structure {
@@ -100,5 +100,5 @@ export function structureSubtract(a: Structure, b: Structure): Structure {
         }
     }
 
-    return Structure.create(units);
+    return Structure.create(units, a.parent || b.parent);
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index 8d45b51467ac1f67d2d734e7fd12c70efddbd444..5243b670ff63ffe4c5d13052525010479372ef96 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -26,6 +26,7 @@ import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { idFactory } from 'mol-util/id-factory';
 import { GridLookup3D } from 'mol-math/geometry';
 import { UUID } from 'mol-util';
+import { CustomProperties } from '../common/custom-property';
 
 class Structure {
     /** Maps unit.id to unit */
@@ -34,6 +35,7 @@ class Structure {
     readonly units: ReadonlyArray<Unit>;
 
     private _props: {
+        parent?: Structure,
         lookup3d?: StructureLookup3D,
         links?: InterUnitBonds,
         crossLinkRestraints?: PairRestraints<CrossLinkRestraint>,
@@ -49,7 +51,16 @@ class Structure {
         transformHash: number,
         elementCount: number,
         polymerResidueCount: number,
-    } = { hashCode: -1, transformHash: -1, elementCount: 0, polymerResidueCount: 0 };
+        coordinateSystem: SymmetryOperator,
+        propertyData?: any,
+        customProps?: CustomProperties
+    } = {
+        hashCode: -1,
+        transformHash: -1,
+        elementCount: 0,
+        polymerResidueCount: 0,
+        coordinateSystem: SymmetryOperator.Default
+    };
 
     subsetBuilder(isSorted: boolean) {
         return new StructureSubsetBuilder(this, isSorted);
@@ -60,6 +71,30 @@ class Structure {
         return this._props.elementCount;
     }
 
+    get hasCustomProperties() {
+        return !!this._props.customProps && this._props.customProps.all.length > 0;
+    }
+
+    get customPropertyDescriptors() {
+        if (!this._props.customProps) this._props.customProps = new CustomProperties();
+        return this._props.customProps;
+    }
+
+    /**
+     * Property data unique to this instance of the structure.
+     */
+    get currentPropertyData() {
+        if (!this._props.propertyData) this._props.propertyData = Object.create(null);
+        return this._props.propertyData;
+    }
+
+    /**
+     * Property data of the parent structure if it exists, currentPropertyData otherwise.
+     */
+    get inheritedPropertyData() {
+        return this.parent ? this.parent.currentPropertyData : this.currentPropertyData;
+    }
+
     /** Count of all polymer residues in the structure */
     get polymerResidueCount() {
         return this._props.polymerResidueCount;
@@ -106,6 +141,14 @@ class Structure {
         return new Structure.ElementLocationIterator(this);
     }
 
+    get parent() {
+        return this._props.parent;
+    }
+
+    get coordinateSystem() {
+        return this._props.coordinateSystem;
+    }
+
     get boundary() {
         return this.lookup3d.boundary;
     }
@@ -174,7 +217,7 @@ class Structure {
         return SortedArray.has(this.unitMap.get(e.unit.id).elements, e.element);
     }
 
-    constructor(units: ArrayLike<Unit>) {
+    private initUnits(units: ArrayLike<Unit>) {
         const map = IntMap.Mutable<Unit>();
         let elementCount = 0;
         let polymerResidueCount = 0;
@@ -188,11 +231,18 @@ class Structure {
             if (u.id < lastId) isSorted = false;
             lastId = u.id;
         }
-        if (!isSorted) sort(units, 0, units.length, cmpUnits, arraySwap)
-        this.unitMap = map;
-        this.units = units as ReadonlyArray<Unit>;
+        if (!isSorted) sort(units, 0, units.length, cmpUnits, arraySwap);
         this._props.elementCount = elementCount;
         this._props.polymerResidueCount = polymerResidueCount;
+        return map;
+    }
+
+    constructor(units: ArrayLike<Unit>, parent: Structure | undefined, coordinateSystem?: SymmetryOperator) {
+        this.unitMap = this.initUnits(units);
+        this.units = units as ReadonlyArray<Unit>;
+        if (parent) this._props.parent = parent;
+        if (coordinateSystem) this._props.coordinateSystem = coordinateSystem;
+        else if (parent) this._props.coordinateSystem = parent.coordinateSystem;
     }
 }
 
@@ -283,7 +333,7 @@ function getUniqueAtomicResidueIndices(structure: Structure): ReadonlyMap<UUID,
 }
 
 namespace Structure {
-    export const Empty = new Structure([]);
+    export const Empty = new Structure([], void 0, void 0);
 
     /** Represents a single structure */
     export interface Loci {
@@ -302,7 +352,9 @@ namespace Structure {
         return a.structure === b.structure
     }
 
-    export function create(units: ReadonlyArray<Unit>): Structure { return new Structure(units); }
+    export function create(units: ReadonlyArray<Unit>, parent: Structure | undefined, coordinateSystem?: SymmetryOperator): Structure {
+        return new Structure(units, parent, coordinateSystem);
+    }
 
     /**
      * Construct a Structure from a model.
@@ -312,7 +364,7 @@ namespace Structure {
      */
     export function ofModel(model: Model): Structure {
         const chains = model.atomicHierarchy.chainAtomSegments;
-        const builder = new StructureBuilder();
+        const builder = new StructureBuilder(void 0, void 0);
 
         for (let c = 0; c < chains.count; c++) {
             const start = chains.offsets[c];
@@ -381,11 +433,13 @@ namespace Structure {
         const units: Unit[] = [];
         for (const u of s.units) {
             const old = u.conformation.operator;
-            const op = SymmetryOperator.create(old.name, transform, { id: '', operList: [] }, old.ncsId, old.hkl);
+            const op = SymmetryOperator.create(old.name, transform, old.assembly, old.ncsId, old.hkl);
             units.push(u.applyOperator(u.id, op));
         }
 
-        return new Structure(units);
+        const cs = s.coordinateSystem;
+        const newCS = SymmetryOperator.compose(SymmetryOperator.create(cs.name, transform, cs.assembly, cs.ncsId, cs.hkl), cs);
+        return new Structure(units, s, newCS);
     }
 
     export class StructureBuilder {
@@ -405,15 +459,21 @@ namespace Structure {
         }
 
         getStructure(): Structure {
-            return create(this.units);
+            return create(this.units, this.parent, this.coordinateSystem);
         }
 
         get isEmpty() {
             return this.units.length === 0;
         }
+
+        constructor(private parent: Structure | undefined, private coordinateSystem: SymmetryOperator | undefined) {
+
+        }
     }
 
-    export function Builder() { return new StructureBuilder(); }
+    export function Builder(parent: Structure | undefined, coordinateSystem: SymmetryOperator | undefined) {
+        return new StructureBuilder(parent, coordinateSystem);
+    }
 
     export function hashCode(s: Structure) {
         return s.hashCode;
diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts
index 1b917041444704a539124a1d2800354e8fdebb6e..eee13f1b61ee70727fc562a6cc38b9490b674ad3 100644
--- a/src/mol-model/structure/structure/symmetry.ts
+++ b/src/mol-model/structure/structure/symmetry.ts
@@ -24,7 +24,7 @@ namespace StructureSymmetry {
             const assembly = ModelSymmetry.findAssembly(models[0], asmName);
             if (!assembly) throw new Error(`Assembly '${asmName}' is not defined.`);
 
-            const assembler = Structure.Builder();
+            const assembler = Structure.Builder(void 0, SymmetryOperator.create(assembly.id, Mat4.identity(), { id: assembly.id, operList: [] }));
 
             const queryCtx = new QueryContext(structure);
 
@@ -83,7 +83,12 @@ namespace StructureSymmetry {
     export function areTransformGroupsEquivalent(a: ReadonlyArray<Unit.SymmetryGroup>, b: ReadonlyArray<Unit.SymmetryGroup>) {
         if (a.length !== b.length) return false
         for (let i = 0, il = a.length; i < il; ++i) {
+            const au = a[i].units, bu = b[i].units;
+            if (au.length !== bu.length) return false;
             if (a[i].hashCode !== b[i].hashCode) return false
+            for (let j = 0, _j = au.length; j < _j; j++) {
+                if (au[j].conformation !== bu[j].conformation) return false;
+            }
         }
         return true
     }
@@ -132,7 +137,7 @@ function getOperatorsCached333(symmetry: ModelSymmetry) {
 }
 
 function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) {
-    const assembler = Structure.Builder();
+    const assembler = Structure.Builder(void 0, void 0);
     const { units } = structure;
     for (const oper of operators) {
         for (const unit of units) {
@@ -174,7 +179,7 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
     const operators = getOperatorsCached333(symmetry);
     const lookup = structure.lookup3d;
 
-    const assembler = Structure.Builder();
+    const assembler = Structure.Builder(void 0, void 0);
 
     const { units } = structure;
     const center = Vec3.zero();
diff --git a/src/mol-model/structure/structure/util/subset-builder.ts b/src/mol-model/structure/structure/util/subset-builder.ts
index 77c228902fc14c27a75e070df1868498ca68057a..dbdf0cf774c647e4380a1c3a81f0b4ebff1fc8d0 100644
--- a/src/mol-model/structure/structure/util/subset-builder.ts
+++ b/src/mol-model/structure/structure/util/subset-builder.ts
@@ -90,7 +90,7 @@ export class StructureSubsetBuilder {
             newUnits[newUnits.length] = child;
         }
 
-        return Structure.create(newUnits);
+        return Structure.create(newUnits, this.parent);
     }
 
     getStructure() {
diff --git a/src/mol-model/structure/structure/util/unique-subset-builder.ts b/src/mol-model/structure/structure/util/unique-subset-builder.ts
index bc18e6519217570f2bb6e6b8097e5baa3e2d6f96..8b808e2466b9b768288de4fc22e6ba783f99f0ab 100644
--- a/src/mol-model/structure/structure/util/unique-subset-builder.ts
+++ b/src/mol-model/structure/structure/util/unique-subset-builder.ts
@@ -85,7 +85,7 @@ export class StructureUniqueSubsetBuilder {
             newUnits[newUnits.length] = child;
         }
 
-        return Structure.create(newUnits);
+        return Structure.create(newUnits, this.parent, this.parent.coordinateSystem);
     }
 
     get isEmpty() {
diff --git a/src/mol-model/volume/data.ts b/src/mol-model/volume/data.ts
index df2964b686d68ff7fc6e7f7b8cd9ec425856d099..4f414675ee8b6c5bd6ba57dcfc59a674a3aa95bc 100644
--- a/src/mol-model/volume/data.ts
+++ b/src/mol-model/volume/data.ts
@@ -1,11 +1,13 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { SpacegroupCell, Box3D } from 'mol-math/geometry'
 import { Tensor, Mat4, Vec3 } from 'mol-math/linear-algebra'
+import { equalEps } from 'mol-math/linear-algebra/3d/common';
 
 /** The basic unit cell that contains the data. */
 interface VolumeData {
@@ -48,7 +50,7 @@ namespace VolumeIsoValue {
     export type Absolute = Readonly<{ kind: 'absolute', absoluteValue: number }>
 
     export function areSame(a: VolumeIsoValue, b: VolumeIsoValue, stats: VolumeData['dataStats']) {
-        return toAbsolute(a, stats).absoluteValue === toAbsolute(b, stats).absoluteValue
+        return equalEps(toAbsolute(a, stats).absoluteValue, toAbsolute(b, stats).absoluteValue, stats.sigma / 100)
     }
 
     export function absolute(value: number): Absolute { return { kind: 'absolute', absoluteValue: value }; }
diff --git a/src/mol-plugin/behavior/dynamic/labels.ts b/src/mol-plugin/behavior/dynamic/labels.ts
index be54651502d57168464b9a568c7a69eb7043cec6..b3597932ce0e3f68dbfcb74d00c6fc9c8e27a1bf 100644
--- a/src/mol-plugin/behavior/dynamic/labels.ts
+++ b/src/mol-plugin/behavior/dynamic/labels.ts
@@ -107,7 +107,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
 
         private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => {
             this.geo = getLabelsText(data, props, this.geo)
-            return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
+            return Shape.create('Scene Labels', data, this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
         }
 
         /** Update structures to be labeled, returns true if changed */
@@ -118,7 +118,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
             for (const s of structures) {
                 const rootStructure = getRootStructure(s, state)
                 if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
-                if (!state.cellStates.get(s.transform.ref).isHidden) {
+                if (!s.state.isHidden) {
                     rootStructures.add(rootStructure.obj)
                 }
             }
diff --git a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
index 497d1420536422a93cff6f13aded50981b145507..7fc4487aa03f3610b41120f4ce75eb9bbf4d3609 100644
--- a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
+++ b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
@@ -56,30 +56,30 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
 
         if (!refs['structure-interaction-group']) {
             refs['structure-interaction-group'] = builder.to(cell).group(StateTransforms.Misc.CreateGroup,
-                { label: 'Current Interaction' }, { props: { tag: Tags.Group } }).ref;
+                { label: 'Current Interaction' }, { tags: Tags.Group }).ref;
         }
 
         // Selections
         if (!refs[Tags.ResidueSel]) {
             refs[Tags.ResidueSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
-                { query: { } as any, label: 'Residue' }, { props: { tag: Tags.ResidueSel } }).ref;
+                { query: { } as any, label: 'Residue' }, { tags: Tags.ResidueSel }).ref;
         }
 
         if (!refs[Tags.SurrSel]) {
             refs[Tags.SurrSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
-                { query: { } as any, label: 'Surroundings' }, { props: { tag: Tags.SurrSel } }).ref;
+                { query: { } as any, label: 'Surroundings' }, { tags: Tags.SurrSel }).ref;
         }
 
         // Representations
         // TODO: ability to customize how it looks in the behavior params
         if (!refs[Tags.ResidueRepr]) {
             refs[Tags.ResidueRepr] = builder.to(refs['structure-interaction-residue-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
-                this.createResVisualParams(cell.obj!.data), { props: { tag: Tags.ResidueRepr } }).ref;
+                this.createResVisualParams(cell.obj!.data), { tags: Tags.ResidueRepr }).ref;
         }
 
         if (!refs[Tags.SurrRepr]) {
             refs[Tags.SurrRepr] = builder.to(refs['structure-interaction-surr-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
-                this.createSurVisualParams(cell.obj!.data), { props: { tag: Tags.SurrRepr } }).ref;
+                this.createSurVisualParams(cell.obj!.data), { tags: Tags.SurrRepr }).ref;
         }
 
         return { state, builder, refs };
@@ -87,7 +87,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
 
     private clear(root: StateTransform.Ref) {
         const state = this.plugin.state.dataState;
-        const groups = state.select(StateSelection.Generators.byRef(root).subtree().filter(o => o.transform.props.tag === Tags.Group));
+        const groups = state.select(StateSelection.Generators.byRef(root).subtree().withTag(Tags.Group));
         if (groups.length === 0) return;
 
         const update = state.build();
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
index 2ad87f7ff3b7a676dcd14d4d2298f06a267a7ecb..e7b357d9b7e3987edd704fcdd98559c4eed7bd73 100644
--- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
@@ -22,6 +22,7 @@ import { VolumeServerHeader, VolumeServerInfo } from './model';
 import { ButtonsType } from 'mol-util/input/input-observer';
 import { PluginCommands } from 'mol-plugin/command';
 import { StateSelection } from 'mol-state';
+import { Representation } from 'mol-repr/representation';
 
 export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
 
@@ -172,7 +173,21 @@ export namespace VolumeStreaming {
         }
 
         register(ref: string): void {
-            // this.ref = ref;
+            let lastLoci: Representation.Loci = Representation.Loci.Empty;
+
+            this.subscribeObservable(this.plugin.events.state.object.removed, o => {
+                if (!PluginStateObject.Molecule.Structure.is(o.obj) || lastLoci.loci.kind !== 'element-loci') return;
+                if (lastLoci.loci.structure === o.obj.data) {
+                    lastLoci = Representation.Loci.Empty;
+                }
+            });
+
+            this.subscribeObservable(this.plugin.events.state.object.updated, o => {
+                if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || lastLoci.loci.kind !== 'element-loci') return;
+                if (lastLoci.loci.structure === o.oldObj.data) {
+                    lastLoci = Representation.Loci.Empty;
+                }
+            });
 
             this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
                 if (buttons !== ButtonsType.Flag.Secondary || this.params.view.name !== 'selection-box') return;
@@ -193,6 +208,13 @@ export namespace VolumeStreaming {
                 const root = this.getStructureRoot(ref);
                 if (!root || !root.obj || root.obj !== parent.obj) return;
 
+                if (Representation.Loci.areEqual(lastLoci, current)) {
+                    lastLoci = Representation.Loci.Empty;
+                    this.updateDynamicBox(ref, Box3D.empty());
+                    return;
+                }
+                lastLoci = current;
+
                 const loci = StructureElement.Loci.extendToWholeResidues(current.loci);
                 const box = StructureElement.Loci.getBoundary(loci).box;
                 this.updateDynamicBox(ref, box);
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
index 42e01f59695ca66d0bcf036babc2364823cd0036..aa98aeff147ac90f77973a1dab5f16b2bbb7d56a 100644
--- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
@@ -59,11 +59,11 @@ export const InitVolumeStreaming = StateAction.build({
         PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data)));
 
     if (params.method === 'em') {
-        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true } });
     } else {
-        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { props: { isGhost: true } });
-        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { props: { isGhost: true } });
-        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true } });
     }
     await state.updateTree(behTree).runInContext(taskCtx);
 }));
diff --git a/src/mol-plugin/behavior/static/representation.ts b/src/mol-plugin/behavior/static/representation.ts
index b84697c9d7237d9ec824fba879929dfdc19f7eca..df0310e777b3f0af68d50952f13a2f4c1279a616 100644
--- a/src/mol-plugin/behavior/static/representation.ts
+++ b/src/mol-plugin/behavior/static/representation.ts
@@ -7,7 +7,7 @@
 import { PluginStateObject as SO } from '../../state/objects';
 import { PluginContext } from 'mol-plugin/context';
 import { Representation } from 'mol-repr/representation';
-import { State } from 'mol-state';
+import { StateObjectCell } from 'mol-state';
 
 export function registerDefault(ctx: PluginContext) {
     SyncRepresentationToCanvas(ctx);
@@ -21,7 +21,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
     const events = ctx.state.dataState.events;
     events.object.created.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
-        updateVisibility(e, e.obj.data.repr);
+        updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr);
         e.obj.data.repr.setState({ syncManually: true });
         ctx.canvas3d.add(e.obj.data.repr);
 
@@ -39,7 +39,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
             return;
         }
 
-        updateVisibility(e, e.obj.data.repr);
+        updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr);
         if (e.action === 'recreate') {
             e.obj.data.repr.setState({ syncManually: true });
         }
@@ -86,11 +86,11 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) {
     ctx.state.dataState.events.cell.stateUpdated.subscribe(e => {
         const cell = e.state.cells.get(e.ref)!;
         if (!SO.isRepresentation3D(cell.obj)) return;
-        updateVisibility(e, cell.obj.data.repr);
+        updateVisibility(cell, cell.obj.data.repr);
         ctx.canvas3d.requestDraw(true);
     })
 }
 
-function updateVisibility(e: State.ObjectEvent, r: Representation<any>) {
-    r.setState({ visible: !e.state.cellStates.get(e.ref).isHidden });
+function updateVisibility(cell: StateObjectCell, r: Representation<any>) {
+    r.setState({ visible: !cell.state.isHidden });
 }
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts
index 818a14252d4b8f0a553c8c73ed13f30b17bd71c1..c4111b37104383e19db7b1f1d602892404d82b3d 100644
--- a/src/mol-plugin/behavior/static/state.ts
+++ b/src/mol-plugin/behavior/static/state.ts
@@ -72,7 +72,8 @@ export function RemoveObject(ctx: PluginContext) {
                 const children = tree.children.get(curr.parent);
                 if (curr.parent === curr.ref || children.size > 1) return remove(state, curr.ref);
                 const parent = tree.transforms.get(curr.parent);
-                if (!parent.props || !parent.props.isGhost) return remove(state, curr.ref);
+                // TODO: should this use "cell state" instead?
+                if (!parent.state.isGhost) return remove(state, curr.ref);
                 curr = parent;
             }
         } else {
@@ -86,7 +87,7 @@ export function ToggleExpanded(ctx: PluginContext) {
 }
 
 export function ToggleVisibility(ctx: PluginContext) {
-    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden));
+    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cells.get(ref)!.state.isHidden));
 }
 
 function setVisibility(state: State, root: StateTransform.Ref, value: boolean) {
diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts
index 05a967dccdd398e0e8815993742a515da3a2a221..01e3375f9552d19827ba5c8c1efeb610f5b9e2b6 100644
--- a/src/mol-plugin/command.ts
+++ b/src/mol-plugin/command.ts
@@ -53,7 +53,7 @@ export const PluginCommands = {
     },
     Camera: {
         Reset: PluginCommand<{}>(),
-        SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>(),
+        SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(),
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string }>(),
             Remove: PluginCommand<{ id: string }>(),
diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index 6b2e8e6e44db466294b3d80b08593d3ff86472b9..85da6b7514b8073897fb16a3c4f7cdf1eb5237ea 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -34,6 +34,7 @@ import { StructureElementSelectionManager } from './util/structure-element-selec
 import { SubstructureParentHelper } from './util/substructure-parent-helper';
 import { Representation } from 'mol-repr/representation';
 import { ModifiersKeys } from 'mol-util/input/input-observer';
+import { isProductionMode, isDebugMode } from 'mol-util/debug';
 
 export class PluginContext {
     private disposed = false;
@@ -111,7 +112,8 @@ export class PluginContext {
             this.layout.setRoot(container);
             if (this.spec.layout && this.spec.layout.initial) this.layout.setProps(this.spec.layout.initial);
             (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
-            PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
+            const renderer = this.canvas3d.props.renderer;
+            PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { renderer: { ...renderer, backgroundColor: Color(0xFCFBF9) } } });
             this.canvas3d.animate();
             return true;
         } catch (e) {
@@ -180,7 +182,7 @@ export class PluginContext {
         const tree = this.state.behaviorState.build();
 
         for (const cat of Object.keys(PluginBehavior.Categories)) {
-            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, props: { isLocked: true } });
+            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, state: { isLocked: true } });
         }
 
         for (const b of this.spec.behaviors) {
@@ -225,5 +227,7 @@ export class PluginContext {
         this.lociLabels = new LociLabelManager(this);
 
         this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`);
+        if (!isProductionMode) this.log.message(`Development mode enabled`);
+        if (isDebugMode) this.log.message(`Debug mode enabled`);
     }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts
index 3fce9342c9409b2ce783bb4559a0f3b8de0aa507..8b9610bba7b1695ba377016a29bc5a396fc25eec 100644
--- a/src/mol-plugin/index.ts
+++ b/src/mol-plugin/index.ts
@@ -12,10 +12,11 @@ import * as ReactDOM from 'react-dom';
 import { PluginSpec } from './spec';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
-import { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode } from './state/animation/built-in';
+import { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode, AnimateStateInterpolation } from './state/animation/built-in';
 import { StateActions } from './state/actions';
 import { InitVolumeStreaming, BoxifyVolumeStreaming, CreateVolumeStreamingBehavior } from './behavior/dynamic/volume-streaming/transformers';
 import { StructureRepresentationInteraction } from './behavior/dynamic/selection/structure-representation-interaction';
+import { TransformStructureConformation } from './state/actions/structure';
 
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
@@ -38,6 +39,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Model.TrajectoryFromPDB),
         PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
+        PluginSpec.Action(TransformStructureConformation),
         PluginSpec.Action(StateTransforms.Model.StructureFromModel),
         PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory),
         PluginSpec.Action(StateTransforms.Model.UserStructureSelection),
@@ -46,7 +48,8 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Representation.StructureLabels3D),
         PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),
-        PluginSpec.Action(StateTransforms.Representation.ColorStructureRepresentation3D),
+        PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3D),
+        PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D),
 
         PluginSpec.Action(StateActions.Structure.StructureFromSelection),
@@ -65,6 +68,7 @@ export const DefaultPluginSpec: PluginSpec = {
         AnimateModelIndex,
         AnimateAssemblyUnwind,
         AnimateUnitsExplode,
+        AnimateStateInterpolation
     ]
 }
 
diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss
index 8913fd5d31ccf465be9388fe3aa63820d001eac7..9d20d320fa4b0ca6cc344b6decb9b8b65a1ef6db 100644
--- a/src/mol-plugin/skin/base/components/temp.scss
+++ b/src/mol-plugin/skin/base/components/temp.scss
@@ -180,11 +180,15 @@
         line-height: $row-height;
         float: left;
         margin-right: $control-spacing;
-        background-color: $msp-form-control-background;
+
+        > button {
+            background-color: $msp-form-control-background;
+        }
 
         > select {
             display: inline-block;
             width: 200px;
+            margin-right: $control-spacing;
         }
     }
 
diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts
index 0e2a82007cb13fd299de8f7e56cf8fdd302ee28d..25f6c1035fe071c91ca4c1f06bc0577e2717a863 100644
--- a/src/mol-plugin/state.ts
+++ b/src/mol-plugin/state.ts
@@ -99,7 +99,7 @@ class PluginState {
     constructor(private plugin: import('./context').PluginContext) {
         this.snapshots = new PluginStateSnapshotManager(plugin);
         this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
-        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } });
+        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootState: { isLocked: true } });
 
         this.dataState.behaviors.currentObject.subscribe(o => {
             if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o);
diff --git a/src/mol-plugin/state/actions/data-format.ts b/src/mol-plugin/state/actions/data-format.ts
index 7e4bbd656f0e3d47097f3c7d73f0960571a448b3..3cb8a05da5a2fae2722df766e895f691050b2d53 100644
--- a/src/mol-plugin/state/actions/data-format.ts
+++ b/src/mol-plugin/state/actions/data-format.ts
@@ -13,6 +13,8 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Ccp4Provider, Dsn6Provider, DscifProvider } from './volume';
 import { StateTransforms } from '../transforms';
 import { MmcifProvider, PdbProvider, GroProvider } from './structure';
+import msgpackDecode from 'mol-io/common/msgpack/decode'
+import { PlyProvider } from './shape';
 
 export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
     private _list: { name: string, provider: DataFormatProvider<D> }[] = []
@@ -59,6 +61,7 @@ export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | Plugin
         this.add('gro', GroProvider)
         this.add('mmcif', MmcifProvider)
         this.add('pdb', PdbProvider)
+        this.add('ply', PlyProvider)
     };
 
     private _clear() {
@@ -139,4 +142,18 @@ export const OpenFile = StateAction.build({
     const b = state.build().to(data.ref);
     // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
     await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
-}));
\ No newline at end of file
+}));
+
+//
+
+type cifVariants = 'dscif' | -1
+export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
+    if (info.ext === 'bcif') {
+        try {
+            if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif'
+        } catch { }
+    } else if (info.ext === 'cif') {
+        if ((data as string).startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
+    }
+    return -1
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/actions/shape.ts b/src/mol-plugin/state/actions/shape.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7a1311149ed894a5112b189a1ab0f92f323b79d8
--- /dev/null
+++ b/src/mol-plugin/state/actions/shape.ts
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { State, StateBuilder } from 'mol-state';
+import { Task } from 'mol-task';
+import { FileInfo } from 'mol-util/file-info';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { DataFormatProvider } from './data-format';
+
+export const PlyProvider: DataFormatProvider<any> = {
+    label: 'PLY',
+    description: 'PLY',
+    stringExtensions: ['ply'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === 'ply'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, state: State) => {
+        return Task.create('PLY default builder', async taskCtx => {
+            const tree = data.apply(StateTransforms.Data.ParsePly)
+                .apply(StateTransforms.Model.ShapeFromPly)
+                .apply(StateTransforms.Representation.ShapeRepresentation3D)
+            await state.updateTree(tree).runInContext(taskCtx)
+        })
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/actions/structure.ts b/src/mol-plugin/state/actions/structure.ts
index 714adecf217632e6c000e5e2344ff76866148005..c32e25c47e360c1e12bb71c36e849c9b50458df4 100644
--- a/src/mol-plugin/state/actions/structure.ts
+++ b/src/mol-plugin/state/actions/structure.ts
@@ -13,7 +13,7 @@ import { StateTransforms } from '../transforms';
 import { Download } from '../transforms/data';
 import { StructureRepresentation3DHelpers } from '../transforms/representation';
 import { CustomModelProperties, StructureSelection } from '../transforms/model';
-import { DataFormatProvider } from './data-format';
+import { DataFormatProvider, guessCifVariant } from './data-format';
 import { FileInfo } from 'mol-util/file-info';
 import { Task } from 'mol-task';
 import { StructureElement } from 'mol-model/structure';
@@ -24,7 +24,10 @@ export const MmcifProvider: DataFormatProvider<any> = {
     stringExtensions: ['cif', 'mmcif', 'mcif'],
     binaryExtensions: ['bcif'],
     isApplicable: (info: FileInfo, data: Uint8Array | string) => {
-        return info.ext === 'cif' || info.ext === 'mmcif' || info.ext === 'mcif' || info.ext === 'bcif'
+        if (info.ext === 'mmcif' || info.ext === 'mcif') return true
+        // assume cif/bcif files that are not DensityServer CIF are mmCIF
+        if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) !== 'dscif'
+        return false
     },
     getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, state: State) => {
         return Task.create('mmCIF default builder', async taskCtx => {
@@ -143,7 +146,7 @@ const DownloadStructure = StateAction.build({
         createStructureTree(ctx, traj, supportProps);
     } else {
         for (const download of downloadParams) {
-            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true } });
+            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { state: { isGhost: true } });
             const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
             createStructureTree(ctx, traj, supportProps)
         }
@@ -176,14 +179,14 @@ export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary
     let parsed: StateBuilder.To<PluginStateObject.Molecule.Trajectory>
     switch (format) {
         case 'cif':
-            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true } })
-                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true } })
+            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
+                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { state: { isGhost: true } })
             break
         case 'pdb':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true } });
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { state: { isGhost: true } });
             break
         case 'gro':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true } });
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { state: { isGhost: true } });
             break
         default:
             throw new Error('unsupported format')
@@ -284,6 +287,15 @@ export const EnableModelCustomProps = StateAction.build({
     return state.updateTree(root);
 });
 
+export const TransformStructureConformation = StateAction.build({
+    display: { name: 'Transform Conformation' },
+    from: PluginStateObject.Molecule.Structure,
+    params: StateTransforms.Model.TransformStructureConformation.definition.params,
+})(({ ref, params, state }) => {
+    const root = state.build().to(ref).insert(StateTransforms.Model.TransformStructureConformation, params as any);
+    return state.updateTree(root);
+});
+
 export const StructureFromSelection = StateAction.build({
     display: { name: 'Selection Structure', description: 'Create a new Structure from the current selection.' },
     from: PluginStateObject.Molecule.Structure,
diff --git a/src/mol-plugin/state/actions/volume.ts b/src/mol-plugin/state/actions/volume.ts
index baf6e0ded64d08c380cf907bd4a671302cbeac5a..727114bf9f8b56ec9b7c619130b28e208d79ab06 100644
--- a/src/mol-plugin/state/actions/volume.ts
+++ b/src/mol-plugin/state/actions/volume.ts
@@ -16,7 +16,7 @@ import { PluginStateObject } from '../objects';
 import { StateTransforms } from '../transforms';
 import { Download } from '../transforms/data';
 import { VolumeRepresentation3DHelpers } from '../transforms/representation';
-import { DataFormatProvider } from './data-format';
+import { DataFormatProvider, guessCifVariant } from './data-format';
 
 export const Ccp4Provider: DataFormatProvider<any> = {
     label: 'CCP4/MRC/BRIX',
@@ -59,10 +59,10 @@ export const DscifProvider: DataFormatProvider<any> = {
     description: 'DensityServer CIF',
     stringExtensions: ['cif'],
     binaryExtensions: ['bcif'],
-    isApplicable: (info: FileInfo, data: Uint8Array) => {
-        return info.ext === 'cif' || info.ext === 'bcif'
+    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
+        return guessCifVariant(info, data) === 'dscif' ? true : false
     },
-    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, state: State) => {
         return Task.create('DensityServer CIF default builder', async taskCtx => {
             const cifBuilder = data.apply(StateTransforms.Data.ParseCif)
             const cifStateObject = await state.updateTree(cifBuilder).runInContext(taskCtx)
diff --git a/src/mol-plugin/state/animation/built-in.ts b/src/mol-plugin/state/animation/built-in.ts
index 308bc1bd6d07f909c3977287cf1a825719cc4b2f..4bbab98fe88601f239b20f6a5b45812123ce7aec 100644
--- a/src/mol-plugin/state/animation/built-in.ts
+++ b/src/mol-plugin/state/animation/built-in.ts
@@ -121,7 +121,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
 
             changed = true;
             update.to(r)
-                .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { props: { tag: 'animate-assembly-unwind' } });
+                .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { tags: 'animate-assembly-unwind' });
         }
 
         if (!changed) return;
@@ -131,7 +131,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
     async teardown(_, plugin) {
         const state = plugin.state.dataState;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.props.tag === 'animate-assembly-unwind'));
+            .withTag('animate-assembly-unwind'));
         if (reprs.length === 0) return;
 
         const update = state.build();
@@ -191,7 +191,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
             changed = true;
             update.to(r.transform.ref)
-                .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { props: { tag: 'animate-units-explode' } });
+                .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { tags: 'animate-units-explode' });
         }
 
         if (!changed) return;
@@ -201,7 +201,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
     async teardown(_, plugin) {
         const state = plugin.state.dataState;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.props.tag === 'animate-units-explode'));
+            .withTag('animate-units-explode'));
         if (reprs.length === 0) return;
 
         const update = state.build();
@@ -229,4 +229,54 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
         return { kind: 'next', state: { t: newTime } };
     }
+})
+
+export const AnimateStateInterpolation = PluginStateAnimation.create({
+    name: 'built-in.animate-state-interpolation',
+    display: { name: 'Animate State Interpolation' },
+    params: () => ({
+        transtionDurationInMs: PD.Numeric(2000, { min: 100, max: 30000, step: 10 })
+    }),
+    initialState: () => ({ }),
+    async apply(animState, t, ctx) {
+
+        const snapshots = ctx.plugin.state.snapshots.state.entries;
+        if (snapshots.size < 2) return { kind: 'finished' };
+
+        // const totalTime = (snapshots.size - 1) * ctx.params.transtionDurationInMs;
+        const currentT = (t.current % ctx.params.transtionDurationInMs) / ctx.params.transtionDurationInMs;
+
+        let srcIndex = Math.floor(t.current / ctx.params.transtionDurationInMs) % snapshots.size;
+        let tarIndex = Math.ceil(t.current / ctx.params.transtionDurationInMs);
+        if (tarIndex === srcIndex) tarIndex++;
+        tarIndex = tarIndex % snapshots.size;
+
+        const _src = snapshots.get(srcIndex)!.snapshot, _tar = snapshots.get(tarIndex)!.snapshot;
+
+        if (!_src.data || !_tar.data) return { kind: 'skip' };
+
+        const src = _src.data.tree.transforms, tar = _tar.data.tree.transforms;
+
+        const state = ctx.plugin.state.dataState;
+        const update = state.build();
+
+        for (const s of src) {
+            for (const t of tar) {
+                if (t.ref !== s.ref) continue;
+                if (t.version === s.version) continue;
+
+                const e = StateTransform.fromJSON(s), f = StateTransform.fromJSON(t);
+
+                if (!e.transformer.definition.interpolate) {
+                    update.to(s.ref).update(currentT <= 0.5 ? e.params : f.params);
+                } else {
+                    update.to(s.ref).update(e.transformer.definition.interpolate(e.params, f.params, currentT, ctx.plugin));
+                }
+            }
+        }
+
+        await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+
+        return { kind: 'next', state: { } };
+    }
 })
\ No newline at end of file
diff --git a/src/mol-plugin/state/animation/helpers.ts b/src/mol-plugin/state/animation/helpers.ts
index df6bfb962bbc35bfe78088aed47fc39df1d854f2..d86448d5fedb44a6d39f2e87c4c3a895e112d238 100644
--- a/src/mol-plugin/state/animation/helpers.ts
+++ b/src/mol-plugin/state/animation/helpers.ts
@@ -4,16 +4,10 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-
 import { SymmetryOperator } from 'mol-math/geometry';
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
-import { Structure, StructureSelection, QueryContext } from 'mol-model/structure';
+import { Structure } from 'mol-model/structure';
 import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
-import { Color } from 'mol-util/color';
-import { Overpaint } from 'mol-theme/overpaint';
-import { parseMolScript } from 'mol-script/language/parser';
-import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
-import { compile } from 'mol-script/runtime/query/compiler';
 
 const _unwindMatrix = Mat4.zero();
 export function unwindStructureAssembly(structure: Structure, unitTransforms: StructureUnitTransforms, t: number) {
@@ -38,22 +32,4 @@ export function explodeStructure(structure: Structure, unitTransforms: Structure
 
         unitTransforms.setTransform(_transMat, u);
     }
-}
-
-type ScriptLayers = { script: { language: string, expression: string }, color: Color }[]
-export function getStructureOverpaint(structure: Structure, scriptLayers: ScriptLayers, alpha: number): Overpaint {
-    const layers: Overpaint.Layer[] = []
-    for (let i = 0, il = scriptLayers.length; i < il; ++i) {
-        const { script, color } = scriptLayers[i]
-        const parsed = parseMolScript(script.expression)
-        if (parsed.length === 0) throw new Error('No query')
-        const query = transpileMolScript(parsed[0])
-
-        const compiled = compile<StructureSelection>(query)
-        const result = compiled(new QueryContext(structure))
-        const loci = StructureSelection.toLoci2(result)
-
-        layers.push({ loci, color })
-    }
-    return { layers, alpha }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts
index c73a2d03df02236ad1204e229ae0bf8a65816ba5..c3a0a8c979c81d6c9fc04c2fc5c3830c571774dd 100644
--- a/src/mol-plugin/state/objects.ts
+++ b/src/mol-plugin/state/objects.ts
@@ -6,6 +6,7 @@
  */
 
 import { CifFile } from 'mol-io/reader/cif';
+import { PlyFile } from 'mol-io/reader/ply/schema';
 import { Model as _Model, Structure as _Structure } from 'mol-model/structure';
 import { VolumeData } from 'mol-model/volume';
 import { PluginBehavior } from 'mol-plugin/behavior/behavior';
@@ -16,6 +17,8 @@ import { StateObject, StateTransformer } from 'mol-state';
 import { Ccp4File } from 'mol-io/reader/ccp4/schema';
 import { Dsn6File } from 'mol-io/reader/dsn6/schema';
 import { ShapeRepresentation } from 'mol-repr/shape/representation';
+import { Shape as _Shape } from 'mol-model/shape';
+import { ShapeProvider } from 'mol-model/shape/provider';
 
 export type TypeClass = 'root' | 'data' | 'prop'
 
@@ -61,6 +64,7 @@ export namespace PluginStateObject {
     export namespace Format {
         export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { }
         export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { }
+        export class Ply extends Create<PlyFile>({ name: 'PLY File', typeClass: 'Data' }) { }
         export class Ccp4 extends Create<Ccp4File>({ name: 'CCP4/MRC/MAP File', typeClass: 'Data' }) { }
         export class Dsn6 extends Create<Dsn6File>({ name: 'DSN6/BRIX File', typeClass: 'Data' }) { }
 
@@ -71,6 +75,7 @@ export namespace PluginStateObject {
             | { kind: 'cif', data: CifFile }
             | { kind: 'ccp4', data: Ccp4File }
             | { kind: 'dsn6', data: Dsn6File }
+            | { kind: 'ply', data: PlyFile }
             // For non-build in extensions
             | { kind: 'custom', data: unknown, tag: string })
         export type BlobData = BlobEntry[]
@@ -100,6 +105,11 @@ export namespace PluginStateObject {
         export class Data extends Create<VolumeData>({ name: 'Volume Data', typeClass: 'Object' }) { }
         export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>>({ name: 'Volume 3D' }) { }
     }
+
+    export namespace Shape {
+        export class Provider extends Create<ShapeProvider<any, any, any>>({ name: 'Shape Provider', typeClass: 'Object' }) { }
+        export class Representation3D extends CreateRepresentation3D<ShapeRepresentation<any, any, any>>({ name: 'Shape 3D' }) { }
+    }
 }
 
 export namespace PluginStateTransform {
diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts
index aa0554a568085ad5ac2cb6a93bc0259e33e39b4b..43c82b639de8388404961518f571027401a37003 100644
--- a/src/mol-plugin/state/transforms/data.ts
+++ b/src/mol-plugin/state/transforms/data.ts
@@ -15,6 +15,7 @@ import { StateTransformer } from 'mol-state';
 import { readFromFile, ajaxGetMany } from 'mol-util/data-source';
 import * as CCP4 from 'mol-io/reader/ccp4/parser'
 import * as DSN6 from 'mol-io/reader/dsn6/parser'
+import * as PLY from 'mol-io/reader/ply/parser'
 
 export { Download }
 type Download = typeof Download
@@ -185,6 +186,23 @@ const ParseCif = PluginStateTransform.BuiltIn({
     }
 });
 
+export { ParsePly }
+type ParsePly = typeof ParsePly
+const ParsePly = PluginStateTransform.BuiltIn({
+    name: 'parse-ply',
+    display: { name: 'Parse PLY', description: 'Parse PLY from String data' },
+    from: [SO.Data.String],
+    to: SO.Format.Ply
+})({
+    apply({ a }) {
+        return Task.create('Parse PLY', async ctx => {
+            const parsed = await PLY.parse(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            return new SO.Format.Ply(parsed.result, { label: parsed.result.comments[0] || 'PLY Data' });
+        });
+    }
+});
+
 export { ParseCcp4 }
 type ParseCcp4 = typeof ParseCcp4
 const ParseCcp4 = PluginStateTransform.BuiltIn({
diff --git a/src/mol-plugin/state/transforms/helpers.ts b/src/mol-plugin/state/transforms/helpers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6a58d384a376b62865174483050c237e715b82fe
--- /dev/null
+++ b/src/mol-plugin/state/transforms/helpers.ts
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure, StructureSelection, QueryContext } from 'mol-model/structure';
+import { Color } from 'mol-util/color';
+import { Overpaint } from 'mol-theme/overpaint';
+import { parseMolScript } from 'mol-script/language/parser';
+import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
+import { compile } from 'mol-script/runtime/query/compiler';
+import { Transparency } from 'mol-theme/transparency';
+
+type Script = { language: string, expression: string }
+
+function scriptToLoci(structure: Structure, script: Script) {
+    const parsed = parseMolScript(script.expression)
+    if (parsed.length === 0) throw new Error('No query')
+    const query = transpileMolScript(parsed[0])
+
+    const compiled = compile<StructureSelection>(query)
+    const result = compiled(new QueryContext(structure))
+    return StructureSelection.toLoci2(result)
+}
+
+export function getStructureOverpaint(structure: Structure, scriptLayers: { script: Script, color: Color }[], alpha: number): Overpaint {
+    const layers: Overpaint.Layer[] = []
+    for (let i = 0, il = scriptLayers.length; i < il; ++i) {
+        const { script, color } = scriptLayers[i]
+        layers.push({ loci: scriptToLoci(structure, script), color })
+    }
+    return { layers, alpha }
+}
+
+export function getStructureTransparency(structure: Structure, script: Script, value: number, variant: Transparency.Variant): Transparency {
+    return { loci: scriptToLoci(structure, script), value, variant }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts
index dd54e9dc49b54ddf352dd5f4e6888dfe33615734..01ef8d7ddeb870f743cf4a733fe4efccc1ab3a63 100644
--- a/src/mol-plugin/state/transforms/model.ts
+++ b/src/mol-plugin/state/transforms/model.ts
@@ -6,7 +6,7 @@
  */
 
 import { parsePDB } from 'mol-io/reader/pdb/parser';
-import { Vec3 } from 'mol-math/linear-algebra';
+import { Vec3, Mat4, Quat } from 'mol-math/linear-algebra';
 import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
 import { Model, ModelSymmetry, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureSymmetry, QueryFn } from 'mol-model/structure';
@@ -24,6 +24,8 @@ import { trajectoryFromGRO } from 'mol-model-formats/structure/gro';
 import { parseGRO } from 'mol-io/reader/gro/parser';
 import { parseMolScript } from 'mol-script/language/parser';
 import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
+import { shapeFromPly } from 'mol-model-formats/shape/ply';
+import { SymmetryOperator } from 'mol-math/geometry';
 
 export { TrajectoryFromBlob };
 export { TrajectoryFromMmCif };
@@ -33,6 +35,7 @@ export { ModelFromTrajectory };
 export { StructureFromModel };
 export { StructureAssemblyFromModel };
 export { StructureSymmetryFromModel };
+export { TransformStructureConformation }
 export { StructureSelection };
 export { UserStructureSelection };
 export { StructureComplexElement };
@@ -250,6 +253,52 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
+const _translation = Vec3.zero(), _m = Mat4.zero(), _n = Mat4.zero();
+type TransformStructureConformation = typeof TransformStructureConformation
+const TransformStructureConformation = PluginStateTransform.BuiltIn({
+    name: 'transform-structure-conformation',
+    display: { name: 'Transform Conformation' },
+    from: SO.Molecule.Structure,
+    to: SO.Molecule.Structure,
+    params: {
+        axis: PD.Vec3(Vec3.create(1, 0, 0)),
+        angle: PD.Numeric(0, { min: -180, max: 180, step: 0.1 }),
+        translation: PD.Vec3(Vec3.create(0, 0, 0)),
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        // TODO: optimze
+
+        const center = a.data.boundary.sphere.center;
+        Mat4.fromTranslation(_m, Vec3.negate(_translation, center));
+        Mat4.fromTranslation(_n, Vec3.add(_translation, center, params.translation));
+        const rot = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * params.angle, Vec3.normalize(Vec3.zero(), params.axis));
+
+        const m = Mat4.zero();
+        Mat4.mul3(m, _n, rot, _m);
+
+        const s = Structure.transform(a.data, m);
+        const props = { label: `${a.label}`, description: `Transformed` };
+        return new SO.Molecule.Structure(s, props);
+    },
+    interpolate(src, tar, t) {
+        // TODO: optimize
+        const u = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * src.angle, Vec3.normalize(Vec3.zero(), src.axis));
+        Mat4.setTranslation(u, src.translation);
+        const v = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * tar.angle, Vec3.normalize(Vec3.zero(), tar.axis));
+        Mat4.setTranslation(v, tar.translation);
+        const m = SymmetryOperator.slerp(Mat4.zero(), u, v, t);
+        const rot = Mat4.getRotation(Quat.zero(), m);
+        const axis = Vec3.zero();
+        const angle = Quat.getAxisAngle(axis, rot);
+        const translation = Mat4.getTranslation(Vec3.zero(), m);
+        return { axis, angle, translation };
+    }
+});
+
 type StructureSelection = typeof StructureSelection
 const StructureSelection = PluginStateTransform.BuiltIn({
     name: 'structure-selection',
@@ -338,7 +387,6 @@ function updateStructureFromQuery(query: QueryFn<Sel>, src: Structure, obj: SO.M
     return true;
 }
 
-
 namespace StructureComplexElement {
     export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
 }
@@ -394,4 +442,24 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon
         const p = ctx.customModelProperties.get(name);
         await p.attach(model).runInContext(taskCtx);
     }
-}
\ No newline at end of file
+}
+
+export { ShapeFromPly }
+type ShapeFromPly = typeof ShapeFromPly
+const ShapeFromPly = PluginStateTransform.BuiltIn({
+    name: 'shape-from-ply',
+    display: { name: 'Shape from PLY', description: 'Create Shape from PLY data' },
+    from: SO.Format.Ply,
+    to: SO.Shape.Provider,
+    params(a) {
+        return { };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create shape from PLY', async ctx => {
+            const shape = await shapeFromPly(a.data, params).runInContext(ctx)
+            const props = { label: 'Shape' };
+            return new SO.Shape.Provider(shape, props);
+        });
+    }
+});
\ No newline at end of file
diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts
index b5c9e1a8257281547faf30b096c49b42558d1816..f0873a69e5a3589938802a013ea1faaf89d58cba 100644
--- a/src/mol-plugin/state/transforms/representation.ts
+++ b/src/mol-plugin/state/transforms/representation.ts
@@ -15,7 +15,7 @@ import { BuiltInVolumeRepresentationsName } from 'mol-repr/volume/registry';
 import { VolumeParams } from 'mol-repr/volume/representation';
 import { StateTransformer } from 'mol-state';
 import { Task } from 'mol-task';
-import { BuiltInColorThemeName, ColorTheme } from 'mol-theme/color';
+import { BuiltInColorThemeName, ColorTheme, BuiltInColorThemes } from 'mol-theme/color';
 import { BuiltInSizeThemeName, SizeTheme } from 'mol-theme/size';
 import { createTheme, ThemeRegistryContext } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -25,16 +25,20 @@ import { ColorNames } from 'mol-util/color/tables';
 import { getLabelRepresentation } from 'mol-plugin/util/structure-labels';
 import { ShapeRepresentation } from 'mol-repr/shape/representation';
 import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
-import { unwindStructureAssembly, explodeStructure, getStructureOverpaint } from '../animation/helpers';
+import { unwindStructureAssembly, explodeStructure } from '../animation/helpers';
 import { Color } from 'mol-util/color';
 import { Overpaint } from 'mol-theme/overpaint';
+import { Transparency } from 'mol-theme/transparency';
+import { getStructureOverpaint, getStructureTransparency } from './helpers';
+import { BaseGeometry } from 'mol-geo/geometry/base';
 
 export { StructureRepresentation3D }
 export { StructureRepresentation3DHelpers }
 export { StructureLabels3D}
 export { ExplodeStructureRepresentation3D }
 export { UnwindStructureAssemblyRepresentation3D }
-export { OverpaintStructureRepresentation3D as ColorStructureRepresentation3D }
+export { OverpaintStructureRepresentation3D }
+export { TransparencyStructureRepresentation3D }
 export { VolumeRepresentation3D }
 
 namespace StructureRepresentation3DHelpers {
@@ -187,10 +191,22 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
             return StateTransformer.UpdateResult.Updated;
         });
+    },
+    interpolate(src, tar, t) {
+        if (src.colorTheme.name !== 'uniform' || tar.colorTheme.name !== 'uniform') {
+            return t <= 0.5 ? src : tar;
+        }
+        BuiltInColorThemes
+        const from = src.colorTheme.params.value as Color, to = tar.colorTheme.params.value as Color;
+        const value = Color.interpolate(from, to, t);
+        return {
+            type: t <= 0.5 ? src.type : tar.type,
+            colorTheme: { name: 'uniform', params: { value } },
+            sizeTheme: t <= 0.5 ? src.sizeTheme : tar.sizeTheme,
+        };
     }
 });
 
-
 type StructureLabels3D = typeof StructureLabels3D
 const StructureLabels3D = PluginStateTransform.BuiltIn({
     name: 'structure-labels-3d',
@@ -366,6 +382,46 @@ const OverpaintStructureRepresentation3D = PluginStateTransform.BuiltIn({
     }
 });
 
+type TransparencyStructureRepresentation3D = typeof TransparencyStructureRepresentation3D
+const TransparencyStructureRepresentation3D = PluginStateTransform.BuiltIn({
+    name: 'transparency-structure-representation-3d',
+    display: 'Transparency 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: {
+        script: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :chain-test (= atom.label_asym_id A))' }),
+        value: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
+        variant: PD.Select('single', [['single', 'Single-layer'], ['multi', 'Multi-layer']])
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        const structure = a.data.source.data
+        const transparency = getStructureTransparency(structure, params.script, params.value, params.variant)
+
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { transparency },
+            initialState: { transparency: Transparency.Empty },
+            info: structure,
+            source: a
+        }, { label: `Transparency (${transparency.value})` })
+    },
+    update({ a, b, newParams, oldParams }) {
+        const structure = b.data.info as Structure
+        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate
+        const oldTransparency = b.data.state.transparency!
+        const newTransparency = getStructureTransparency(structure, newParams.script, newParams.value, newParams.variant)
+        if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged
+
+        b.data.state.transparency = newTransparency
+        b.data.source = a
+        b.label = `Transparency (${newTransparency.value})`
+        return StateTransformer.UpdateResult.Updated
+    }
+});
+
 //
 
 export namespace VolumeRepresentation3DHelpers {
@@ -471,4 +527,38 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
             return StateTransformer.UpdateResult.Updated;
         });
     }
+});
+
+//
+
+export { ShapeRepresentation3D }
+type ShapeRepresentation3D = typeof ShapeRepresentation3D
+const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
+    name: 'shape-representation-3d',
+    display: '3D Representation',
+    from: SO.Shape.Provider,
+    to: SO.Shape.Representation3D,
+    params: (a, ctx: PluginContext) => {
+        return a ? a.data.params : BaseGeometry.Params
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Shape Representation', async ctx => {
+            const props = { ...PD.getDefaultValues(a.data.params), params }
+            const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils)
+            // TODO set initial state, repr.setState({})
+            await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: a.data.label });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Shape Representation', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams }
+            await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx);
+            return StateTransformer.UpdateResult.Updated;
+        });
+    }
 });
\ No newline at end of file
diff --git a/src/mol-plugin/ui/base.tsx b/src/mol-plugin/ui/base.tsx
index d4ae0171d84882a5f0c9fb6e2152b4fbfd5e2d83..afa47c47d7cf31e9b27ba82b054fbbcb94ecebcb 100644
--- a/src/mol-plugin/ui/base.tsx
+++ b/src/mol-plugin/ui/base.tsx
@@ -58,4 +58,7 @@ export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends Rea
         this.plugin = context;
         if (this.init) this.init();
     }
-}
\ No newline at end of file
+}
+
+export type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
+export type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never
\ No newline at end of file
diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx
index 72135f24d2a4cca1b01a47a69b98830547f9e292..e179f8cfabfef097d13bee5106eaad2819ee8e15 100644
--- a/src/mol-plugin/ui/controls/parameters.tsx
+++ b/src/mol-plugin/ui/controls/parameters.tsx
@@ -16,6 +16,7 @@ import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
 import { NumericInput, IconButton } from './common';
+import { _Props, _State } from '../base';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
@@ -513,9 +514,6 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
     }
 }
 
-type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
-type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never
-
 class ObjectListEditor extends React.PureComponent<{ params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }, { params: PD.Params, value: object, current: object }> {
     state = { params: {}, value: void 0 as any, current: void 0 as any };
 
diff --git a/src/mol-plugin/ui/state/tree.tsx b/src/mol-plugin/ui/state/tree.tsx
index f2c4ac4f805505441a4f14a1b9e387b64cdf1a5a..16293756d3cb72c941337869271fc52a0a629e0c 100644
--- a/src/mol-plugin/ui/state/tree.tsx
+++ b/src/mol-plugin/ui/state/tree.tsx
@@ -6,9 +6,9 @@
 
 import * as React from 'react';
 import { PluginStateObject } from 'mol-plugin/state/objects';
-import { State, StateObject, StateTransform } from 'mol-state'
+import { State, StateObject, StateTransform, StateObjectCell } from 'mol-state'
 import { PluginCommands } from 'mol-plugin/command';
-import { PluginUIComponent } from '../base';
+import { PluginUIComponent, _Props, _State } from '../base';
 import { StateObjectActions } from './actions';
 
 export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
@@ -37,74 +37,75 @@ export class StateTree extends PluginUIComponent<{ state: State }, { showActions
         if (this.state.showActions) {
             return <StateObjectActions state={this.props.state} nodeRef={ref} hideHeader={true} />
         }
-        return <StateTreeNode state={this.props.state} nodeRef={ref} depth={0} />;
+        return <StateTreeNode cell={this.props.state.cells.get(ref)!} depth={0} />;
     }
 }
 
-class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, depth: number }, { state: State, isCollapsed: boolean }> {
+class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: number }, { isCollapsed: boolean }> {
     is(e: State.ObjectEvent) {
-        return e.ref === this.props.nodeRef && e.state === this.props.state;
+        return e.ref === this.ref && e.state === this.props.cell.parent;
     }
 
-    get cellState() {
-        return this.props.state.cellStates.get(this.props.nodeRef);
+    get ref() {
+        return this.props.cell.transform.ref;
     }
 
     componentDidMount() {
         this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
-            if (this.is(e) && e.state.transforms.has(this.props.nodeRef)) {
-                this.setState({ isCollapsed: e.cellState.isCollapsed });
+            if (this.props.cell === e.cell && this.is(e) && e.state.cells.has(this.ref)) {
+                this.forceUpdate();
+                // if (!!this.props.cell.state.isCollapsed !== this.state.isCollapsed) {
+                //     this.setState({ isCollapsed: !!e.cell.state.isCollapsed });
+                // }
             }
         });
 
         this.subscribe(this.plugin.events.state.cell.created, e => {
-            if (this.props.state === e.state && this.props.nodeRef === e.cell.transform.parent) {
+            if (this.props.cell.parent === e.state && this.ref === e.cell.transform.parent) {
                 this.forceUpdate();
             }
         });
 
         this.subscribe(this.plugin.events.state.cell.removed, e => {
-            if (this.props.state === e.state && this.props.nodeRef === e.parent) {
+            if (this.props.cell.parent === e.state && this.ref === e.parent) {
                 this.forceUpdate();
             }
         });
     }
 
     state = {
-        isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state
+        isCollapsed: !!this.props.cell.state.isCollapsed
     }
 
-    static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCollapsed: boolean }) {
-        if (props.state === state.state) return null;
-        return {
-            isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state
-        };
+    static getDerivedStateFromProps(props: _Props<StateTreeNode>, state: _State<StateTreeNode>): _State<StateTreeNode> | null {
+        if (!!props.cell.state.isCollapsed === state.isCollapsed) return null;
+        return { isCollapsed: !!props.cell.state.isCollapsed };
     }
 
     render() {
-        const cell = this.props.state.cells.get(this.props.nodeRef);
-        if (!cell || cell.obj === StateObject.Null) return null;
+        const cell = this.props.cell;
+        if (!cell || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) {
+            return null;
+        }
 
-        const cellState = this.cellState;
-        const showLabel = cell.status !== 'ok' || !cell.transform.props || !cell.transform.props.isGhost;
-        const children = this.props.state.tree.children.get(this.props.nodeRef);
+        const cellState = cell.state;
+        const showLabel = cell.status !== 'ok' || !cell.state.isGhost;
+        const children = cell.parent.tree.children.get(this.ref);
         const newDepth = showLabel ? this.props.depth + 1 : this.props.depth;
 
         if (!showLabel) {
             if (children.size === 0) return null;
             return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+                {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
             </div>;
         }
 
         return <>
-            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} depth={this.props.depth} />
+            <StateTreeNodeLabel cell={cell} depth={this.props.depth} />
             {children.size === 0
                 ? void 0
                 : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                    {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+                    {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
                 </div>
             }
         </>;
@@ -112,11 +113,15 @@ class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, d
 }
 
 class StateTreeNodeLabel extends PluginUIComponent<
-    { nodeRef: string, state: State, depth: number },
-    { state: State, isCurrent: boolean, isCollapsed: boolean /*, updaterCollapsed: boolean */ }> {
+    { cell: StateObjectCell, depth: number },
+    { isCurrent: boolean, isCollapsed: boolean }> {
 
     is(e: State.ObjectEvent) {
-        return e.ref === this.props.nodeRef && e.state === this.props.state;
+        return e.ref === this.ref && e.state === this.props.cell.parent;
+    }
+
+    get ref() {
+        return this.props.cell.transform.ref;
     }
 
     componentDidMount() {
@@ -126,70 +131,66 @@ class StateTreeNodeLabel extends PluginUIComponent<
 
         this.subscribe(this.plugin.state.behavior.currentObject, e => {
             if (!this.is(e)) {
-                if (this.state.isCurrent && e.state.transforms.has(this.props.nodeRef)) {
-                    this.setState({ isCurrent: this.props.state.current === this.props.nodeRef });
+                if (this.state.isCurrent && e.state.transforms.has(this.ref)) {
+                    this.setState({ isCurrent: this.props.cell.parent.current === this.ref });
                 }
                 return;
             }
 
-            if (e.state.transforms.has(this.props.nodeRef)) {
+            if (e.state.transforms.has(this.ref)) {
                 this.setState({
-                    isCurrent: this.props.state.current === this.props.nodeRef,
-                    isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed
+                    isCurrent: this.props.cell.parent.current === this.ref,
+                    isCollapsed: !!this.props.cell.state.isCollapsed
                 });
             }
         });
     }
 
     state = {
-        isCurrent: this.props.state.current === this.props.nodeRef,
-        isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state,
-        // updaterCollapsed: true
+        isCurrent: this.props.cell.parent.current === this.ref,
+        isCollapsed: !!this.props.cell.state.isCollapsed
     }
 
-    static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCurrent: boolean, isCollapsed: boolean }) {
-        if (props.state === state.state) return null;
-        return {
-            isCurrent: props.state.current === props.nodeRef,
-            isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state,
-            updaterCollapsed: true
-        };
+    static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null {
+        const isCurrent = props.cell.parent.current === props.cell.transform.ref;
+        const isCollapsed = !!props.cell.state.isCollapsed;
+
+        if (state.isCollapsed === isCollapsed && state.isCurrent === isCurrent) return null;
+        return { isCurrent, isCollapsed };
     }
 
     setCurrent = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         e.currentTarget.blur();
-        PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
     }
 
     remove = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef, removeParentGhosts: true });
+        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref, removeParentGhosts: true });
     }
 
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
@@ -201,12 +202,11 @@ class StateTreeNodeLabel extends PluginUIComponent<
     // }
 
     render() {
-        const n = this.props.state.transforms.get(this.props.nodeRef)!;
-        const cell = this.props.state.cells.get(this.props.nodeRef);
+        const cell = this.props.cell;
+        const n = cell.transform;
         if (!cell) return null;
 
-        const isCurrent = this.is(this.props.state.behaviors.currentObject.value);
-
+        const isCurrent = this.state.isCurrent; // this.is(cell.parent.behaviors.currentObject.value);
 
         let label: any;
         if (cell.status === 'pending' || cell.status === 'processing') {
@@ -226,8 +226,8 @@ class StateTreeNodeLabel extends PluginUIComponent<
             }
         }
 
-        const children = this.props.state.tree.children.get(this.props.nodeRef);
-        const cellState = this.props.state.cellStates.get(this.props.nodeRef);
+        const children = cell.parent.tree.children.get(this.ref);
+        const cellState = cell.state;
 
         const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}>
             <span className='msp-icon msp-icon-visual-visibility' />
@@ -244,7 +244,7 @@ class StateTreeNodeLabel extends PluginUIComponent<
             {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
                 <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />
             </button>}
-            {!cell.transform.props.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
+            {!cell.state.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
                 <span className='msp-icon msp-icon-remove' />
             </button>}{visibility}
         </div>;
diff --git a/src/mol-plugin/util/structure-labels.ts b/src/mol-plugin/util/structure-labels.ts
index c6e944b29219ac96fed56f47556ad3885780ea36..932569da9046774aca3952c380c7174f7d9f31ca 100644
--- a/src/mol-plugin/util/structure-labels.ts
+++ b/src/mol-plugin/util/structure-labels.ts
@@ -44,7 +44,7 @@ export async function getLabelRepresentation(ctx: RuntimeContext, structure: Str
 
 function getLabelsShape(ctx: RuntimeContext, data: LabelsData, props: PD.Values<Text.Params>, shape?: Shape<Text>) {
     const geo = getLabelsText(data, props, shape && shape.geometry);
-    return Shape.create('Scene Labels', geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '')
+    return Shape.create('Scene Labels', data, geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '')
 }
 
 const boundaryHelper = new BoundaryHelper();
diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts
index 5bc851e2fdbac8163d79b00a4fdd8a5c18b319b9..812bc71d0ecbc3d35f9d0dc03200f76f2cfcd8d1 100644
--- a/src/mol-repr/representation.ts
+++ b/src/mol-repr/representation.ts
@@ -20,6 +20,7 @@ import { Mat4 } from 'mol-math/linear-algebra';
 import { BaseGeometry } from 'mol-geo/geometry/base';
 import { Visual } from './visual';
 import { Overpaint } from 'mol-theme/overpaint';
+import { Transparency } from 'mol-theme/transparency';
 
 // export interface RepresentationProps {
 //     visuals?: string[]
@@ -146,19 +147,22 @@ namespace Representation {
         pickable: boolean
         /** Overpaint applied to the representation's renderobjects */
         overpaint: Overpaint
+        /** Per group transparency applied to the representation's renderobjects */
+        transparency: Transparency
         /** Controls if the representation's renderobjects are synced automatically with GPU or not */
         syncManually: boolean
         /** A transformation applied to the representation's renderobjects */
         transform: Mat4
     }
     export function createState(): State {
-        return { visible: false, alphaFactor: 0, pickable: false, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty }
+        return { visible: false, alphaFactor: 0, pickable: false, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty, transparency: Transparency.Empty }
     }
     export function updateState(state: State, update: Partial<State>) {
         if (update.visible !== undefined) state.visible = update.visible
         if (update.alphaFactor !== undefined) state.alphaFactor = update.alphaFactor
         if (update.pickable !== undefined) state.pickable = update.pickable
         if (update.overpaint !== undefined) state.overpaint = update.overpaint
+        if (update.transparency !== undefined) state.transparency = update.transparency
         if (update.syncManually !== undefined) state.syncManually = update.syncManually
         if (update.transform !== undefined) Mat4.copy(state.transform, update.transform)
     }
@@ -322,6 +326,9 @@ namespace Representation {
                 if (state.overpaint !== undefined) {
                     // TODO
                 }
+                if (state.transparency !== undefined) {
+                    // TODO
+                }
                 if (state.transform !== undefined) Visual.setTransform(renderObject, state.transform)
 
                 Representation.updateState(currentState, state)
diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts
index 83f1edb96e25a5ed817d4cba918fb1d2a9ec803b..b84a0bffc03e96a677e7fb103f10f52de4b011c3 100644
--- a/src/mol-repr/shape/representation.ts
+++ b/src/mol-repr/shape/representation.ts
@@ -5,11 +5,11 @@
  */
 
 import { Task, RuntimeContext } from 'mol-task'
-import { createRenderObject, GraphicsRenderObject } from 'mol-gl/render-object';
+import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object';
 import { Representation } from '../representation';
 import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci';
 import { ValueCell } from 'mol-util';
-import { Shape } from 'mol-model/shape';
+import { Shape, ShapeGroup } from 'mol-model/shape';
 import { OrderedSet, Interval } from 'mol-data/int';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { createTransform, TransformData } from 'mol-geo/geometry/transform-data';
@@ -35,6 +35,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
     let version = 0
     const updated = new Subject<number>()
     const _state = Representation.createState()
+    const materialId = getNextMaterialId()
     const renderObjects: GraphicsRenderObject[] = []
     let _renderObject: GraphicsRenderObject | undefined
     let _shape: Shape<G>
@@ -56,9 +57,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
             updateState.createNew = true
         } else if (shape && _shape && shape.id === _shape.id) {
             // console.log('same shape')
-            // trigger color update when shape has not changed
-            updateState.updateColor = true
-            updateState.updateTransform = true
+            // nothing to set
         } else if (shape && _shape && shape.id !== _shape.id) {
             // console.log('new shape')
             updateState.updateTransform = true
@@ -101,7 +100,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
                 const values = geometryUtils.createValues(_shape.geometry, transform, locationIt, _theme, newProps)
                 const state = geometryUtils.createRenderableState(newProps)
 
-                _renderObject = createRenderObject(_shape.geometry.kind, values, state)
+                _renderObject = createRenderObject(_shape.geometry.kind, values, state, materialId)
                 if (_renderObject) renderObjects.push(_renderObject) // add new renderObject to list
             } else {
                 if (!_renderObject) {
@@ -170,7 +169,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
         getLoci(pickingId: PickingId) {
             const { objectId, groupId, instanceId } = pickingId
             if (_renderObject && _renderObject.id === objectId) {
-                return Shape.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId) }], instanceId)
+                return ShapeGroup.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId) }], instanceId)
             }
             return EmptyLoci
         },
@@ -185,6 +184,9 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
                 if (state.overpaint !== undefined) {
                     Visual.setOverpaint(_renderObject, state.overpaint, lociApply, true)
                 }
+                if (state.transparency !== undefined) {
+                    Visual.setTransparency(_renderObject, state.transparency, lociApply, true)
+                }
                 if (state.transform !== undefined) Visual.setTransform(_renderObject, state.transform)
             }
 
@@ -210,7 +212,7 @@ function createShapeTransform(transforms: Mat4[], transformData?: TransformData)
 }
 
 function eachShapeGroup(loci: Loci, shape: Shape, apply: (interval: Interval) => boolean) {
-    if (!Shape.isLoci(loci)) return false
+    if (!ShapeGroup.isLoci(loci)) return false
     if (loci.shape !== shape) return false
     let changed = false
     const { groupCount } = shape
@@ -233,7 +235,7 @@ function eachShapeGroup(loci: Loci, shape: Shape, apply: (interval: Interval) =>
 export namespace ShapeGroupIterator {
     export function fromShape(shape: Shape): LocationIterator {
         const instanceCount = shape.transforms.length
-        const location = Shape.Location(shape)
+        const location = ShapeGroup.Location(shape)
         const getLocation = (groupIndex: number, instanceIndex: number) => {
             location.group = groupIndex
             location.instance = instanceIndex
diff --git a/src/mol-repr/structure/complex-representation.ts b/src/mol-repr/structure/complex-representation.ts
index bbb8325859e481f190099c5e212bb79588ce0b95..e8ed4daf473e96be54a04d54cd67e357f1c6a329 100644
--- a/src/mol-repr/structure/complex-representation.ts
+++ b/src/mol-repr/structure/complex-representation.ts
@@ -16,11 +16,12 @@ import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/repr
 import { Theme, createEmptyTheme } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
-import { GraphicsRenderObject } from 'mol-gl/render-object';
+import { GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object';
 
-export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
+export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const materialId = getNextMaterialId()
     const renderObjects: GraphicsRenderObject[] = []
     const _state = StructureRepresentationStateBuilder.create()
     let visual: ComplexVisual<P> | undefined
@@ -39,7 +40,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         _props = Object.assign({}, _props, props)
 
         return Task.create('Creating or updating ComplexRepresentation', async runtime => {
-            if (!visual) visual = visualCtor()
+            if (!visual) visual = visualCtor(materialId)
             const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, structure)
             if (promise) await promise
             // update list of renderObjects
@@ -68,6 +69,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         if (state.alphaFactor !== undefined && visual) visual.setAlphaFactor(state.alphaFactor)
         if (state.pickable !== undefined && visual) visual.setPickable(state.pickable)
         if (state.overpaint !== undefined && visual) visual.setOverpaint(state.overpaint)
+        if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency)
         if (state.transform !== undefined && visual) visual.setTransform(state.transform)
         if (state.unitTransforms !== undefined && visual) {
             // Since ComplexVisuals always renders geometries between units the application of `unitTransforms`
diff --git a/src/mol-repr/structure/complex-visual.ts b/src/mol-repr/structure/complex-visual.ts
index 7f5407789c126d221f45642205eab9576b0150df..0cc0bf9274c6ba4484e871bcc87300770119b993 100644
--- a/src/mol-repr/structure/complex-visual.ts
+++ b/src/mol-repr/structure/complex-visual.ts
@@ -29,15 +29,16 @@ import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
 import { Mat4 } from 'mol-math/linear-algebra';
 import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
 import { Overpaint } from 'mol-theme/overpaint';
+import { Transparency } from 'mol-theme/transparency';
 
 export interface  ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
 
-function createComplexRenderObject<G extends Geometry>(structure: Structure, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) {
+function createComplexRenderObject<G extends Geometry>(structure: Structure, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) {
     const { createValues, createRenderableState } = Geometry.getUtils(geometry)
     const transform = createIdentityTransform()
     const values = createValues(geometry, transform, locationIt, theme, props)
     const state = createRenderableState(props)
-    return createRenderObject(geometry.kind, values, state)
+    return createRenderObject(geometry.kind, values, state, materialId)
 }
 
 const ComplexParams = {
@@ -59,7 +60,7 @@ interface ComplexVisualGeometryBuilder<P extends UnitsParams, G extends Geometry
     geometryUtils: GeometryUtils<G>
 }
 
-export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geometry.Params<G>>(builder: ComplexVisualGeometryBuilder<P, G>): ComplexVisual<P> {
+export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geometry.Params<G>>(builder: ComplexVisualGeometryBuilder<P, G>, materialId: number): ComplexVisual<P> {
     const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder
     const { updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils
     const updateState = VisualUpdateState.create()
@@ -117,7 +118,7 @@ export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geom
         if (updateState.createNew) {
             locationIt = createLocationIterator(newStructure)
             if (newGeometry) {
-                renderObject = createComplexRenderObject(newStructure, newGeometry, locationIt, newTheme, newProps)
+                renderObject = createComplexRenderObject(newStructure, newGeometry, locationIt, newTheme, newProps, materialId)
             } else {
                 throw new Error('expected geometry to be given')
             }
@@ -196,9 +197,12 @@ export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geom
         setTransform(matrix?: Mat4, instanceMatrices?: Float32Array | null) {
             Visual.setTransform(renderObject, matrix, instanceMatrices)
         },
-        setOverpaint(overpaint: Overpaint, clear = false) {
+        setOverpaint(overpaint: Overpaint) {
             return Visual.setOverpaint(renderObject, overpaint, lociApply, true)
         },
+        setTransparency(transparency: Transparency) {
+            return Visual.setTransparency(renderObject, transparency, lociApply, true)
+        },
         destroy() {
             // TODO
             renderObject = undefined
@@ -216,7 +220,7 @@ export type ComplexMeshParams = typeof ComplexMeshParams
 
 export interface ComplexMeshVisualBuilder<P extends ComplexMeshParams> extends ComplexVisualBuilder<P, Mesh> { }
 
-export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexMeshVisualBuilder<P>): ComplexVisual<P> {
+export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexMeshVisualBuilder<P>, materialId: number): ComplexVisual<P> {
     return ComplexVisual<Mesh, StructureMeshParams & UnitsParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
@@ -224,7 +228,7 @@ export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexM
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
         geometryUtils: Mesh.Utils
-    })
+    }, materialId)
 }
 
 // direct-volume
@@ -237,7 +241,7 @@ export type ComplexDirectVolumeParams = typeof ComplexDirectVolumeParams
 
 export interface ComplexDirectVolumeVisualBuilder<P extends ComplexDirectVolumeParams> extends ComplexVisualBuilder<P, DirectVolume> { }
 
-export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(builder: ComplexDirectVolumeVisualBuilder<P>): ComplexVisual<P> {
+export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(builder: ComplexDirectVolumeVisualBuilder<P>, materialId: number): ComplexVisual<P> {
     return ComplexVisual<DirectVolume, StructureDirectVolumeParams & UnitsParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
@@ -245,5 +249,5 @@ export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(b
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
         geometryUtils: DirectVolume.Utils
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/registry.ts b/src/mol-repr/structure/registry.ts
index 5cadc109a7933a576c8e291c75c97d052765a476..c83be7e241276e7a6b96e5f9b216f4e0d48b8731 100644
--- a/src/mol-repr/structure/registry.ts
+++ b/src/mol-repr/structure/registry.ts
@@ -8,14 +8,14 @@ import { Structure } from 'mol-model/structure';
 import { RepresentationProvider, RepresentationRegistry } from '../representation';
 import { CartoonRepresentationProvider } from './representation/cartoon';
 import { BallAndStickRepresentationProvider } from './representation/ball-and-stick';
-import { MolecularSurfaceRepresentationProvider } from './representation/molecular-surface';
-import { MolecularVolumeRepresentationProvider } from './representation/molecular-volume';
+import { GaussianSurfaceRepresentationProvider } from './representation/gaussian-surface';
 import { CarbohydrateRepresentationProvider } from './representation/carbohydrate';
 import { SpacefillRepresentationProvider } from './representation/spacefill';
 import { DistanceRestraintRepresentationProvider } from './representation/distance-restraint';
 import { PointRepresentationProvider } from './representation/point';
 import { StructureRepresentationState } from './representation';
 import { PuttyRepresentationProvider } from './representation/putty';
+import { MolecularSurfaceRepresentationProvider } from './representation/molecular-surface';
 
 export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> {
     constructor() {
@@ -32,8 +32,9 @@ export const BuiltInStructureRepresentations = {
     'ball-and-stick': BallAndStickRepresentationProvider,
     'carbohydrate': CarbohydrateRepresentationProvider,
     'distance-restraint': DistanceRestraintRepresentationProvider,
+    'gaussian-surface': GaussianSurfaceRepresentationProvider,
+    // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work
     'molecular-surface': MolecularSurfaceRepresentationProvider,
-    'molecular-volume': MolecularVolumeRepresentationProvider,
     'point': PointRepresentationProvider,
     'putty': PuttyRepresentationProvider,
     'spacefill': SpacefillRepresentationProvider,
diff --git a/src/mol-repr/structure/representation.ts b/src/mol-repr/structure/representation.ts
index 183cd297c381b744e1abfe7a073d28b9322fefb4..427ab0c0fc33c18e1ed27a24710423c2b30003f1 100644
--- a/src/mol-repr/structure/representation.ts
+++ b/src/mol-repr/structure/representation.ts
@@ -15,6 +15,7 @@ import { Lines } from 'mol-geo/geometry/lines/lines';
 import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
 import { Spheres } from 'mol-geo/geometry/spheres/spheres';
 import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
+import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh';
 
 export interface StructureRepresentationState extends Representation.State {
     unitTransforms: StructureUnitTransforms | null
@@ -56,6 +57,9 @@ export type StructureLinesParams = typeof StructureLinesParams
 export const StructureDirectVolumeParams = { ...DirectVolume.Params, ...StructureParams }
 export type StructureDirectVolumeParams = typeof StructureDirectVolumeParams
 
+export const StructureTextureMeshParams = { ...TextureMesh.Params, ...StructureParams }
+export type StructureTextureMeshParams = typeof StructureTextureMeshParams
+
 export { ComplexRepresentation } from './complex-representation'
 export { UnitsRepresentation } from './units-representation'
 export { ComplexVisual } from './complex-visual'
diff --git a/src/mol-repr/structure/representation/gaussian-surface.ts b/src/mol-repr/structure/representation/gaussian-surface.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e1e748f9fb67134062c9ac9200f9000f7f77662
--- /dev/null
+++ b/src/mol-repr/structure/representation/gaussian-surface.ts
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { GaussianSurfaceMeshVisual, GaussianSurfaceTextureMeshVisual, GaussianSurfaceMeshParams } from '../visual/gaussian-surface-mesh';
+import { UnitsRepresentation } from '../units-representation';
+import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaussian-surface-wireframe';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
+import { ThemeRegistryContext } from 'mol-theme/theme';
+import { Structure } from 'mol-model/structure';
+
+const GaussianSurfaceVisuals = {
+    'gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceMeshVisual),
+    'gaussian-surface-texture-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceTextureMeshVisual),
+    'gaussian-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', ctx, getParams, GaussianWireframeVisual),
+}
+type GaussianSurfaceVisualName = keyof typeof GaussianSurfaceVisuals
+const GaussianSurfaceVisualOptions = Object.keys(GaussianSurfaceVisuals).map(name => [name, name] as [GaussianSurfaceVisualName, string])
+
+export const GaussianSurfaceParams = {
+    ...GaussianSurfaceMeshParams,
+    ...GaussianWireframeParams,
+    visuals: PD.MultiSelect<GaussianSurfaceVisualName>(['gaussian-surface-mesh'], GaussianSurfaceVisualOptions),
+}
+export type GaussianSurfaceParams = typeof GaussianSurfaceParams
+export function getGaussianSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(GaussianSurfaceParams)
+}
+
+export type GaussianSurfaceRepresentation = StructureRepresentation<GaussianSurfaceParams>
+export function GaussianSurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>): GaussianSurfaceRepresentation {
+    return Representation.createMulti('Gaussian Surface', ctx, getParams, StructureRepresentationStateBuilder, GaussianSurfaceVisuals as unknown as Representation.Def<Structure, GaussianSurfaceParams>)
+}
+
+export const GaussianSurfaceRepresentationProvider: StructureRepresentationProvider<GaussianSurfaceParams> = {
+    label: 'Gaussian Surface',
+    description: 'Displays a gaussian molecular surface.',
+    factory: GaussianSurfaceRepresentation,
+    getParams: getGaussianSurfaceParams,
+    defaultValues: PD.getDefaultValues(GaussianSurfaceParams),
+    defaultColorTheme: 'polymer-id',
+    defaultSizeTheme: 'uniform'
+}
\ No newline at end of file
diff --git a/src/mol-repr/structure/representation/molecular-volume.ts b/src/mol-repr/structure/representation/gaussian-volume.ts
similarity index 52%
rename from src/mol-repr/structure/representation/molecular-volume.ts
rename to src/mol-repr/structure/representation/gaussian-volume.ts
index 4bf0a0141629d5912bd6cf5de50e001a4ed3a0da..df1e99b19484849f29fd78ae690f0600f17ce96c 100644
--- a/src/mol-repr/structure/representation/molecular-volume.ts
+++ b/src/mol-repr/structure/representation/gaussian-volume.ts
@@ -11,29 +11,29 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
 
-const MolecularVolumeVisuals = {
+const GaussianVolumeVisuals = {
     'gaussian-volume': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianDensityVolumeParams>) => ComplexRepresentation('Gaussian volume', ctx, getParams, GaussianDensityVolumeVisual)
 }
 
-export const MolecularVolumeParams = {
+export const GaussianVolumeParams = {
     ...GaussianDensityVolumeParams,
 }
-export type MolecularVolumeParams = typeof MolecularVolumeParams
-export function getMolecularVolumeParams(ctx: ThemeRegistryContext, structure: Structure) {
-    return PD.clone(MolecularVolumeParams)
+export type GaussianVolumeParams = typeof GaussianVolumeParams
+export function getGaussianVolumeParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(GaussianVolumeParams)
 }
 
-export type MolecularVolumeRepresentation = StructureRepresentation<MolecularVolumeParams>
-export function MolecularVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularVolumeParams>): MolecularVolumeRepresentation {
-    return Representation.createMulti('Molecular Volume', ctx, getParams, StructureRepresentationStateBuilder, MolecularVolumeVisuals as unknown as Representation.Def<Structure, MolecularVolumeParams>)
+export type GaussianVolumeRepresentation = StructureRepresentation<GaussianVolumeParams>
+export function GaussianVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianVolumeParams>): GaussianVolumeRepresentation {
+    return Representation.createMulti('Gaussian Volume', ctx, getParams, StructureRepresentationStateBuilder, GaussianVolumeVisuals as unknown as Representation.Def<Structure, GaussianVolumeParams>)
 }
 
-export const MolecularVolumeRepresentationProvider: StructureRepresentationProvider<MolecularVolumeParams> = {
-    label: 'Molecular Volume',
+export const GaussianVolumeRepresentationProvider: StructureRepresentationProvider<GaussianVolumeParams> = {
+    label: 'Gaussian Volume',
     description: 'Displays a gaussian molecular density using direct volume rendering.',
-    factory: MolecularVolumeRepresentation,
-    getParams: getMolecularVolumeParams,
-    defaultValues: PD.getDefaultValues(MolecularVolumeParams),
+    factory: GaussianVolumeRepresentation,
+    getParams: getGaussianVolumeParams,
+    defaultValues: PD.getDefaultValues(GaussianVolumeParams),
     defaultColorTheme: 'polymer-id',
     defaultSizeTheme: 'uniform'
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/representation/molecular-surface.ts b/src/mol-repr/structure/representation/molecular-surface.ts
index 4cef6716dca74fcef9b2ec284b41b1b41eaee9f3..3a5f0c0510c8d7d3302c5c458d8f3d94a35d4b40 100644
--- a/src/mol-repr/structure/representation/molecular-surface.ts
+++ b/src/mol-repr/structure/representation/molecular-surface.ts
@@ -1,12 +1,11 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { GaussianSurfaceVisual, GaussianSurfaceParams } from '../visual/gaussian-surface-mesh';
+import { MolecularSurfaceMeshVisual, MolecularSurfaceMeshParams } from '../visual/molecular-surface-mesh';
 import { UnitsRepresentation } from '../units-representation';
-import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaussian-surface-wireframe';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
@@ -14,16 +13,14 @@ import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
 
 const MolecularSurfaceVisuals = {
-    'gaussian-surface': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceVisual),
-    'gaussian-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', ctx, getParams, GaussianWireframeVisual),
+    'molecular-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularSurfaceMeshParams>) => UnitsRepresentation('Molecular surface', ctx, getParams, MolecularSurfaceMeshVisual),
 }
 type MolecularSurfaceVisualName = keyof typeof MolecularSurfaceVisuals
 const MolecularSurfaceVisualOptions = Object.keys(MolecularSurfaceVisuals).map(name => [name, name] as [MolecularSurfaceVisualName, string])
 
 export const MolecularSurfaceParams = {
-    ...GaussianSurfaceParams,
-    ...GaussianWireframeParams,
-    visuals: PD.MultiSelect<MolecularSurfaceVisualName>(['gaussian-surface'], MolecularSurfaceVisualOptions),
+    ...MolecularSurfaceMeshParams,
+    visuals: PD.MultiSelect<MolecularSurfaceVisualName>(['molecular-surface-mesh'], MolecularSurfaceVisualOptions),
 }
 export type MolecularSurfaceParams = typeof MolecularSurfaceParams
 export function getMolecularSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) {
@@ -37,7 +34,7 @@ export function MolecularSurfaceRepresentation(ctx: RepresentationContext, getPa
 
 export const MolecularSurfaceRepresentationProvider: StructureRepresentationProvider<MolecularSurfaceParams> = {
     label: 'Molecular Surface',
-    description: 'Displays a gaussian molecular surface.',
+    description: 'Displays a molecular surface.',
     factory: MolecularSurfaceRepresentation,
     getParams: getMolecularSurfaceParams,
     defaultValues: PD.getDefaultValues(MolecularSurfaceParams),
diff --git a/src/mol-repr/structure/units-representation.ts b/src/mol-repr/structure/units-representation.ts
index 92e0c6e28cb1581a2aa9843a2eb2921898ebaa19..89e7adc7b8f1fecb5075cc92a5e578e3957c5ee4 100644
--- a/src/mol-repr/structure/units-representation.ts
+++ b/src/mol-repr/structure/units-representation.ts
@@ -7,7 +7,7 @@
 
 import { Structure, Unit } from 'mol-model/structure';
 import { Task } from 'mol-task'
-import { GraphicsRenderObject } from 'mol-gl/render-object';
+import { GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object';
 import { RepresentationContext, RepresentationParamsGetter } from '../representation';
 import { Visual } from '../visual';
 import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
@@ -28,9 +28,10 @@ export type UnitsParams = typeof UnitsParams
 
 export interface UnitsVisual<P extends UnitsParams> extends Visual<StructureGroup, P> { }
 
-export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
+export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => UnitsVisual<P>): StructureRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const materialId = getNextMaterialId()
     const renderObjects: GraphicsRenderObject[] = []
     const _state = StructureRepresentationStateBuilder.create()
     let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
@@ -57,14 +58,13 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
                 _groups = structure.unitSymmetryGroups;
                 for (let i = 0; i < _groups.length; i++) {
                     const group = _groups[i];
-                    const visual = visualCtor()
+                    const visual = visualCtor(materialId)
                     const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure })
                     if (promise) await promise
                     visuals.set(group.hashCode, { visual, group })
                     if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length })
                 }
             } else if (structure && !Structure.areEquivalent(structure, _structure)) {
-                // console.log(label, 'structure not equivalent')
                 // Tries to re-use existing visuals for the groups of the new structure.
                 // Creates additional visuals if needed, destroys left-over visuals.
                 _groups = structure.unitSymmetryGroups;
@@ -86,7 +86,7 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
                     } else {
                         // console.log(label, 'not found visualGroup to reuse, creating new')
                         // newGroups.push(group)
-                        const visual = visualCtor()
+                        const visual = visualCtor(materialId)
                         const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure })
                         if (promise) await promise
                         visuals.set(group.hashCode, { visual, group })
@@ -171,11 +171,12 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
     }
 
     function setState(state: Partial<StructureRepresentationState>) {
-        const { visible, alphaFactor, pickable, overpaint, transform, unitTransforms } = state
+        const { visible, alphaFactor, pickable, overpaint, transparency, transform, unitTransforms } = state
         if (visible !== undefined) visuals.forEach(({ visual }) => visual.setVisibility(visible))
         if (alphaFactor !== undefined) visuals.forEach(({ visual }) => visual.setAlphaFactor(alphaFactor))
         if (pickable !== undefined) visuals.forEach(({ visual }) => visual.setPickable(pickable))
         if (overpaint !== undefined) visuals.forEach(({ visual }) => visual.setOverpaint(overpaint))
+        if (transparency !== undefined) visuals.forEach(({ visual }) => visual.setTransparency(transparency))
         if (transform !== undefined) visuals.forEach(({ visual }) => visual.setTransform(transform))
         if (unitTransforms !== undefined) {
             visuals.forEach(({ visual, group }) => {
diff --git a/src/mol-repr/structure/units-visual.ts b/src/mol-repr/structure/units-visual.ts
index 4f3c5bf22635d6cd2a7c1a1f09d8d7e5059241f9..bad9e645c3e32c8137c6ef0438b9bdc2219b06ae 100644
--- a/src/mol-repr/structure/units-visual.ts
+++ b/src/mol-repr/structure/units-visual.ts
@@ -7,7 +7,7 @@
 import { Unit, Structure } from 'mol-model/structure';
 import { RepresentationProps } from '../representation';
 import { Visual, VisualContext } from '../visual';
-import { StructureMeshParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureSpheresParams } from './representation';
+import { StructureMeshParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureSpheresParams, StructureTextureMeshParams } from './representation';
 import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
 import { GraphicsRenderObject, createRenderObject } from 'mol-gl/render-object';
 import { deepEqual, ValueCell } from 'mol-util';
@@ -32,17 +32,19 @@ import { Mat4 } from 'mol-math/linear-algebra';
 import { Spheres } from 'mol-geo/geometry/spheres/spheres';
 import { createUnitsTransform, includesUnitKind } from './visual/util/common';
 import { Overpaint } from 'mol-theme/overpaint';
+import { Transparency } from 'mol-theme/transparency';
+import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh';
 
 export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
 
 export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
 
-function createUnitsRenderObject<G extends Geometry>(group: Unit.SymmetryGroup, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) {
+function createUnitsRenderObject<G extends Geometry>(group: Unit.SymmetryGroup, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) {
     const { createValues, createRenderableState } = Geometry.getUtils(geometry)
     const transform = createUnitsTransform(group)
     const values = createValues(geometry, transform, locationIt, theme, props)
     const state = createRenderableState(props)
-    return createRenderObject(geometry.kind, values, state)
+    return createRenderObject(geometry.kind, values, state, materialId)
 }
 
 interface UnitsVisualBuilder<P extends UnitsParams, G extends Geometry> {
@@ -58,7 +60,7 @@ interface UnitsVisualGeometryBuilder<P extends UnitsParams, G extends Geometry>
     geometryUtils: GeometryUtils<G>
 }
 
-export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry.Params<G>>(builder: UnitsVisualGeometryBuilder<P, G>): UnitsVisual<P> {
+export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry.Params<G>>(builder: UnitsVisualGeometryBuilder<P, G>, materialId: number): UnitsVisual<P> {
     const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder
     const { createEmpty: createEmptyGeometry, updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils
     const updateState = VisualUpdateState.create()
@@ -119,8 +121,12 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
         }
 
         // check if the conformation of unit.model has changed
-        if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])) {
+        // if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])) {
+        if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])
+            // TODO: this needs more attention
+            || newStructureGroup.group.units[0].conformation !== currentStructureGroup.group.units[0].conformation) {
             // console.log('new conformation')
+            updateState.updateTransform = true;
             updateState.createGeometry = true
         }
 
@@ -140,7 +146,7 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
         if (updateState.createNew) {
             locationIt = createLocationIterator(newStructureGroup.group)
             if (newGeometry) {
-                renderObject = createUnitsRenderObject(newStructureGroup.group, newGeometry, locationIt, newTheme, newProps)
+                renderObject = createUnitsRenderObject(newStructureGroup.group, newGeometry, locationIt, newTheme, newProps, materialId)
             } else {
                 throw new Error('expected geometry to be given')
             }
@@ -245,6 +251,9 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
         setOverpaint(overpaint: Overpaint) {
             return Visual.setOverpaint(renderObject, overpaint, lociApply, true)
         },
+        setTransparency(transparency: Transparency) {
+            return Visual.setTransparency(renderObject, transparency, lociApply, true)
+        },
         destroy() {
             // TODO
             renderObject = undefined
@@ -258,7 +267,7 @@ export const UnitsMeshParams = { ...StructureMeshParams, ...UnitsParams }
 export type UnitsMeshParams = typeof UnitsMeshParams
 export interface UnitsMeshVisualBuilder<P extends UnitsMeshParams> extends UnitsVisualBuilder<P, Mesh> { }
 
-export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVisualBuilder<P>): UnitsVisual<P> {
+export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVisualBuilder<P>, materialId: number): UnitsVisual<P> {
     return UnitsVisual<Mesh, StructureMeshParams & UnitsParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
@@ -266,7 +275,7 @@ export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVis
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
         geometryUtils: Mesh.Utils
-    })
+    }, materialId)
 }
 
 // spheres
@@ -275,7 +284,7 @@ export const UnitsSpheresParams = { ...StructureSpheresParams, ...UnitsParams }
 export type UnitsSpheresParams = typeof UnitsSpheresParams
 export interface UnitsSpheresVisualBuilder<P extends UnitsSpheresParams> extends UnitsVisualBuilder<P, Spheres> { }
 
-export function UnitsSpheresVisual<P extends UnitsSpheresParams>(builder: UnitsSpheresVisualBuilder<P>): UnitsVisual<P> {
+export function UnitsSpheresVisual<P extends UnitsSpheresParams>(builder: UnitsSpheresVisualBuilder<P>, materialId: number): UnitsVisual<P> {
     return UnitsVisual<Spheres, StructureSpheresParams & UnitsParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
@@ -283,7 +292,7 @@ export function UnitsSpheresVisual<P extends UnitsSpheresParams>(builder: UnitsS
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
         geometryUtils: Spheres.Utils
-    })
+    }, materialId)
 }
 
 // points
@@ -292,7 +301,7 @@ export const UnitsPointsParams = { ...StructurePointsParams, ...UnitsParams }
 export type UnitsPointsParams = typeof UnitsPointsParams
 export interface UnitsPointVisualBuilder<P extends UnitsPointsParams> extends UnitsVisualBuilder<P, Points> { }
 
-export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPointVisualBuilder<P>): UnitsVisual<P> {
+export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPointVisualBuilder<P>, materialId: number): UnitsVisual<P> {
     return UnitsVisual<Points, StructurePointsParams & UnitsParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
@@ -300,7 +309,7 @@ export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPoi
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
         geometryUtils: Points.Utils
-    })
+    }, materialId)
 }
 
 // lines
@@ -309,7 +318,7 @@ export const UnitsLinesParams = { ...StructureLinesParams, ...UnitsParams }
 export type UnitsLinesParams = typeof UnitsLinesParams
 export interface UnitsLinesVisualBuilder<P extends UnitsLinesParams> extends UnitsVisualBuilder<P, Lines> { }
 
-export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLinesVisualBuilder<P>): UnitsVisual<P> {
+export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLinesVisualBuilder<P>, materialId: number): UnitsVisual<P> {
     return UnitsVisual<Lines, StructureLinesParams & UnitsParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
@@ -317,16 +326,16 @@ export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLines
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
         geometryUtils: Lines.Utils
-    })
+    }, materialId)
 }
 
 // direct-volume
 
 export const UnitsDirectVolumeParams = { ...StructureDirectVolumeParams, ...UnitsParams }
 export type UnitsDirectVolumeParams = typeof UnitsDirectVolumeParams
-export interface UnitsDirectVolumeVisualBuilder<P extends UnitsDirectVolumeParams> extends UnitsVisualGeometryBuilder<P, DirectVolume> { }
+export interface UnitsDirectVolumeVisualBuilder<P extends UnitsDirectVolumeParams> extends UnitsVisualBuilder<P, DirectVolume> { }
 
-export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(builder: UnitsDirectVolumeVisualBuilder<P>): UnitsVisual<P> {
+export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(builder: UnitsDirectVolumeVisualBuilder<P>, materialId: number): UnitsVisual<P> {
     return UnitsVisual<DirectVolume, StructureDirectVolumeParams & UnitsParams>({
         ...builder,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
@@ -334,5 +343,22 @@ export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(build
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
         geometryUtils: DirectVolume.Utils
-    })
+    }, materialId)
+}
+
+// texture-mesh
+
+export const UnitsTextureMeshParams = { ...StructureTextureMeshParams, ...UnitsParams }
+export type UnitsTextureMeshParams = typeof UnitsTextureMeshParams
+export interface UnitsTextureMeshVisualBuilder<P extends UnitsTextureMeshParams> extends UnitsVisualBuilder<P, TextureMesh> { }
+
+export function UnitsTextureMeshVisual<P extends UnitsTextureMeshParams>(builder: UnitsTextureMeshVisualBuilder<P>, materialId: number): UnitsVisual<P> {
+    return UnitsVisual<TextureMesh, StructureTextureMeshParams & UnitsParams>({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => {
+            builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
+        },
+        geometryUtils: TextureMesh.Utils
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
index c699d7464aecdc28c8c48e764db74c516e48d872..73cd6fe643016d4532dae2938660a55d23956009 100644
--- a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
@@ -55,7 +55,7 @@ export const CarbohydrateLinkParams = {
 }
 export type CarbohydrateLinkParams = typeof CarbohydrateLinkParams
 
-export function CarbohydrateLinkVisual(): ComplexVisual<CarbohydrateLinkParams> {
+export function CarbohydrateLinkVisual(materialId: number): ComplexVisual<CarbohydrateLinkParams> {
     return ComplexMeshVisual<CarbohydrateLinkParams>({
         defaultProps: PD.getDefaultValues(CarbohydrateLinkParams),
         createGeometry: createCarbohydrateLinkCylinderMesh,
@@ -68,7 +68,7 @@ export function CarbohydrateLinkVisual(): ComplexVisual<CarbohydrateLinkParams>
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
 
 function CarbohydrateLinkIterator(structure: Structure): LocationIterator {
diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
index bc4e4ad6f71267d9e7599562be026c6324863535..c1c2b046f1d908e78c76dd2a27e102733a026ecd 100644
--- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
@@ -148,7 +148,7 @@ export const CarbohydrateSymbolParams = {
 }
 export type CarbohydrateSymbolParams = typeof CarbohydrateSymbolParams
 
-export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolParams> {
+export function CarbohydrateSymbolVisual(materialId: number): ComplexVisual<CarbohydrateSymbolParams> {
     return ComplexMeshVisual<CarbohydrateSymbolParams>({
         defaultProps: PD.getDefaultValues(CarbohydrateSymbolParams),
         createGeometry: createCarbohydrateSymbolMesh,
@@ -161,7 +161,7 @@ export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolPara
                 newProps.detail !== currentProps.detail
             )
         }
-    })
+    }, materialId)
 }
 
 function CarbohydrateElementIterator(structure: Structure): LocationIterator {
diff --git a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
index d78b03bd16a475cbfc7aa617f9d77a27104ef1d9..7a3d4e35a5274199d73aa36877da4172a214b866 100644
--- a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
@@ -65,7 +65,7 @@ export const CarbohydrateTerminalLinkParams = {
 }
 export type CarbohydrateTerminalLinkParams = typeof CarbohydrateTerminalLinkParams
 
-export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerminalLinkParams> {
+export function CarbohydrateTerminalLinkVisual(materialId: number): ComplexVisual<CarbohydrateTerminalLinkParams> {
     return ComplexMeshVisual<CarbohydrateTerminalLinkParams>({
         defaultProps: PD.getDefaultValues(CarbohydrateTerminalLinkParams),
         createGeometry: createCarbohydrateTerminalLinkCylinderMesh,
@@ -78,7 +78,7 @@ export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerm
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
 
 function CarbohydrateTerminalLinkIterator(structure: Structure): LocationIterator {
diff --git a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
index 26de0839cfa9744b7ecd4e5e980ec98afc4da141..ccfb6c46ec9e170f9f4867156d1e017e3b7416aa 100644
--- a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
+++ b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
@@ -58,7 +58,7 @@ export const CrossLinkRestraintParams = {
 }
 export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams
 
-export function CrossLinkRestraintVisual(): ComplexVisual<CrossLinkRestraintParams> {
+export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintParams> {
     return ComplexMeshVisual<CrossLinkRestraintParams>({
         defaultProps: PD.getDefaultValues(CrossLinkRestraintParams),
         createGeometry: createCrossLinkRestraintCylinderMesh,
@@ -71,7 +71,7 @@ export function CrossLinkRestraintVisual(): ComplexVisual<CrossLinkRestraintPara
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
 
 function CrossLinkRestraintIterator(structure: Structure): LocationIterator {
diff --git a/src/mol-repr/structure/visual/element-point.ts b/src/mol-repr/structure/visual/element-point.ts
index 64db953cbb4402571c10e3db99a2cf44d8109da8..5292dfa8fb9fd51f34b0cab92788c401fd0a778a 100644
--- a/src/mol-repr/structure/visual/element-point.ts
+++ b/src/mol-repr/structure/visual/element-point.ts
@@ -42,7 +42,7 @@ export function createElementPoint(ctx: VisualContext, unit: Unit, structure: St
     return builder.getPoints()
 }
 
-export function ElementPointVisual(): UnitsVisual<ElementPointParams> {
+export function ElementPointVisual(materialId: number): UnitsVisual<ElementPointParams> {
     return UnitsPointsVisual<ElementPointParams>({
         defaultProps: PD.getDefaultValues(ElementPointParams),
         createGeometry: createElementPoint,
@@ -52,5 +52,5 @@ export function ElementPointVisual(): UnitsVisual<ElementPointParams> {
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementPointParams>, currentProps: PD.Values<ElementPointParams>) => {
 
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/element-sphere.ts b/src/mol-repr/structure/visual/element-sphere.ts
index afbe90fbb144fe2a3dad2aff1817781fa19df137..6bff3a354f331036e868e6112026cc0b0cd4b616 100644
--- a/src/mol-repr/structure/visual/element-sphere.ts
+++ b/src/mol-repr/structure/visual/element-sphere.ts
@@ -24,7 +24,7 @@ export function getElementSphereVisual(webgl?: WebGLContext) {
     return webgl && webgl.extensions.fragDepth ? ElementSphereImpostorVisual : ElementSphereMeshVisual
 }
 
-export function ElementSphereImpostorVisual(): UnitsVisual<ElementSphereParams> {
+export function ElementSphereImpostorVisual(materialId: number): UnitsVisual<ElementSphereParams> {
     return UnitsSpheresVisual<ElementSphereParams>({
         defaultProps: PD.getDefaultValues(ElementSphereParams),
         createGeometry: createElementSphereImpostor,
@@ -34,10 +34,10 @@ export function ElementSphereImpostorVisual(): UnitsVisual<ElementSphereParams>
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementSphereParams>, currentProps: PD.Values<ElementSphereParams>) => {
 
         }
-    })
+    }, materialId)
 }
 
-export function ElementSphereMeshVisual(): UnitsVisual<ElementSphereParams> {
+export function ElementSphereMeshVisual(materialId: number): UnitsVisual<ElementSphereParams> {
     return UnitsMeshVisual<ElementSphereParams>({
         defaultProps: PD.getDefaultValues(ElementSphereParams),
         createGeometry: createElementSphereMesh,
@@ -50,5 +50,5 @@ export function ElementSphereMeshVisual(): UnitsVisual<ElementSphereParams> {
                 newProps.detail !== currentProps.detail
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/gaussian-density-point.ts b/src/mol-repr/structure/visual/gaussian-density-point.ts
deleted file mode 100644
index beef6aae62dbbf963c6d2b9460309e24664d48d4..0000000000000000000000000000000000000000
--- a/src/mol-repr/structure/visual/gaussian-density-point.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Unit, Structure } from 'mol-model/structure';
-import { UnitsVisual } from '../representation';
-import { VisualUpdateState } from '../../util';
-import { StructureElementIterator } from './util/element';
-import { EmptyLoci } from 'mol-model/loci';
-import { Vec3 } from 'mol-math/linear-algebra';
-import { UnitsPointsVisual, UnitsPointsParams } from '../units-visual';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { Points } from 'mol-geo/geometry/points/points';
-import { PointsBuilder } from 'mol-geo/geometry/points/points-builder';
-import { VisualContext } from 'mol-repr/visual';
-import { Theme } from 'mol-theme/theme';
-import { computeUnitGaussianDensity, GaussianDensityParams, GaussianDensityProps } from './util/gaussian';
-
-export const GaussianDensityPointParams = {
-    ...UnitsPointsParams,
-    ...GaussianDensityParams,
-    pointSizeAttenuation: PD.Boolean(false),
-}
-export type GaussianDensityPointParams = typeof GaussianDensityPointParams
-
-export async function createGaussianDensityPoint(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, points?: Points) {
-    const { transform, field: { space, data } } = await computeUnitGaussianDensity(unit, props, ctx.webgl).runInContext(ctx.runtime)
-
-    const { dimensions, get } = space
-    const [ xn, yn, zn ] = dimensions
-
-    const n = xn * yn * zn * 3
-    const builder = PointsBuilder.create(n, n / 10, points)
-
-    const p = Vec3.zero()
-    let i = 0
-
-    for (let x = 0; x < xn; ++x) {
-        for (let y = 0; y < yn; ++y) {
-            for (let z = 0; z < zn; ++z) {
-                if (get(data, x, y, z) > 0.001) {
-                    Vec3.set(p, x, y, z)
-                    Vec3.transformMat4(p, p, transform)
-                    builder.add(p[0], p[1], p[2], i)
-                }
-                if (i % 100000 === 0 && ctx.runtime.shouldUpdate) {
-                    await ctx.runtime.update({ message: 'Creating density points', current: i, max: n });
-                }
-                ++i
-            }
-        }
-    }
-    return builder.getPoints()
-}
-
-export function GaussianDensityPointVisual(): UnitsVisual<GaussianDensityPointParams> {
-    return UnitsPointsVisual<GaussianDensityPointParams>({
-        defaultProps: PD.getDefaultValues(GaussianDensityPointParams),
-        createGeometry: createGaussianDensityPoint,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: () => EmptyLoci,
-        eachLocation: () => false,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianDensityPointParams>, currentProps: PD.Values<GaussianDensityPointParams>) => {
-            if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
-            if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
-            if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
-            if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
-        }
-    })
-}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/gaussian-density-volume.ts b/src/mol-repr/structure/visual/gaussian-density-volume.ts
index 7aac5a7b9aafbbe775f4769c0051f77c36e00152..40e2592a13516bdb707327dbb59381f7ae21b3bb 100644
--- a/src/mol-repr/structure/visual/gaussian-density-volume.ts
+++ b/src/mol-repr/structure/visual/gaussian-density-volume.ts
@@ -23,9 +23,9 @@ async function createGaussianDensityVolume(ctx: VisualContext, structure: Struct
     const p = { ...props, useGpu: true }
     const oldTexture = directVolume ? directVolume.gridTexture.ref.value : undefined
     const densityTextureData = await computeStructureGaussianDensityTexture(structure, p, webgl, oldTexture).runInContext(runtime)
-    const { transform, texture, bbox, gridDimension } = densityTextureData
+    const { transform, texture, bbox, gridDim } = densityTextureData
 
-    return DirectVolume.create(bbox, gridDimension, transform, texture, directVolume)
+    return DirectVolume.create(bbox, gridDim, transform, texture, directVolume)
 }
 
 export const GaussianDensityVolumeParams = {
@@ -34,7 +34,7 @@ export const GaussianDensityVolumeParams = {
 }
 export type GaussianDensityVolumeParams = typeof GaussianDensityVolumeParams
 
-export function GaussianDensityVolumeVisual(): ComplexVisual<GaussianDensityVolumeParams> {
+export function GaussianDensityVolumeVisual(materialId: number): ComplexVisual<GaussianDensityVolumeParams> {
     return ComplexDirectVolumeVisual<GaussianDensityVolumeParams>({
         defaultProps: PD.getDefaultValues(GaussianDensityVolumeParams),
         createGeometry: createGaussianDensityVolume,
@@ -49,5 +49,5 @@ export function GaussianDensityVolumeVisual(): ComplexVisual<GaussianDensityVolu
                 newProps.isoValueNorm = Math.exp(-newProps.smoothness)
             }
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
index 166252a81a076d7e1ccd496890c2c63496264b8b..ae26f57dd3fdb3fbada4a32de7d1b48f8778c371 100644
--- a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
+++ b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,14 +7,33 @@
 import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
+import { UnitsMeshVisual, UnitsMeshParams, UnitsTextureMeshParams, UnitsTextureMeshVisual } from '../units-visual';
 import { StructureElementIterator, getElementLoci, eachElement } from './util/element';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm';
 import { VisualContext } from 'mol-repr/visual';
 import { Theme } from 'mol-theme/theme';
-import { GaussianDensityProps, computeUnitGaussianDensity, GaussianDensityParams } from './util/gaussian';
+import { GaussianDensityProps, computeUnitGaussianDensity, GaussianDensityParams, computeUnitGaussianDensityTexture2d, GaussianDensityTextureProps } from './util/gaussian';
+import { WebGLContext } from 'mol-gl/webgl/context';
+import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh';
+import { calcActiveVoxels } from 'mol-gl/compute/marching-cubes/active-voxels';
+import { createHistogramPyramid } from 'mol-gl/compute/histogram-pyramid/reduction';
+import { createIsosurfaceBuffers } from 'mol-gl/compute/marching-cubes/isosurface';
+import { Sphere3D } from 'mol-math/geometry';
+
+export const GaussianSurfaceMeshParams = {
+    ...UnitsMeshParams,
+    ...UnitsTextureMeshParams,
+    ...GaussianDensityParams,
+}
+export type GaussianSurfaceMeshParams = typeof GaussianSurfaceMeshParams
+
+export function getGaussianSurfaceVisual(webgl?: WebGLContext) {
+    return webgl && webgl.extensions.drawBuffers ? GaussianSurfaceTextureMeshVisual : GaussianSurfaceMeshVisual
+}
+
+//
 
 async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, mesh?: Mesh): Promise<Mesh> {
     const { smoothness } = props
@@ -28,30 +47,67 @@ async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structu
     const surface = await computeMarchingCubesMesh(params, mesh).runAsChild(ctx.runtime)
 
     Mesh.transformImmediate(surface, transform)
-    Mesh.computeNormalsImmediate(surface)
     Mesh.uniformTriangleGroup(surface)
 
-    return surface;
+    return surface
 }
 
-export const GaussianSurfaceParams = {
-    ...UnitsMeshParams,
-    ...GaussianDensityParams,
-}
-export type GaussianSurfaceParams = typeof GaussianSurfaceParams
-
-export function GaussianSurfaceVisual(): UnitsVisual<GaussianSurfaceParams> {
-    return UnitsMeshVisual<GaussianSurfaceParams>({
-        defaultProps: PD.getDefaultValues(GaussianSurfaceParams),
+export function GaussianSurfaceMeshVisual(materialId: number): UnitsVisual<GaussianSurfaceMeshParams> {
+    return UnitsMeshVisual<GaussianSurfaceMeshParams>({
+        defaultProps: PD.getDefaultValues(GaussianSurfaceMeshParams),
         createGeometry: createGaussianSurfaceMesh,
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
         eachLocation: eachElement,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianSurfaceParams>, currentProps: PD.Values<GaussianSurfaceParams>) => {
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianSurfaceMeshParams>, currentProps: PD.Values<GaussianSurfaceMeshParams>) => {
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
             if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
         }
-    })
+    }, materialId)
+}
+
+//
+
+async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
+    if (!ctx.webgl) throw new Error('webgl context required to create gaussian surface texture-mesh')
+    const isoLevel = Math.exp(-props.smoothness)
+
+    const densityTextureData = await computeUnitGaussianDensityTexture2d(unit, props, ctx.webgl).runInContext(ctx.runtime)
+    // console.log(densityTextureData)
+    // console.log('vertexGroupTexture', readTexture(ctx.webgl, densityTextureData.texture))
+    // ctx.webgl.waitForGpuCommandsCompleteSync()
+
+    const activeVoxelsTex = calcActiveVoxels(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoLevel, densityTextureData.gridTexScale)
+    // ctx.webgl.waitForGpuCommandsCompleteSync()
+
+    const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, densityTextureData.gridTexScale)
+    // ctx.webgl.waitForGpuCommandsCompleteSync()
+
+    const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined)
+    // ctx.webgl.waitForGpuCommandsCompleteSync()
+
+    // const boundingSphere = Sphere3D.zero()
+    // Sphere3D.addVec3(boundingSphere, boundingSphere, densityTextureData.gridDimension)
+    const boundingSphere = Sphere3D.fromBox3D(Sphere3D(), densityTextureData.bbox)
+    const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, boundingSphere, textureMesh)
+
+    // ctx.webgl.waitForGpuCommandsCompleteSync()
+    return surface
+}
+
+export function GaussianSurfaceTextureMeshVisual(materialId: number): UnitsVisual<GaussianSurfaceMeshParams> {
+    return UnitsTextureMeshVisual<GaussianSurfaceMeshParams>({
+        defaultProps: PD.getDefaultValues(GaussianSurfaceMeshParams),
+        createGeometry: createGaussianSurfaceTextureMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        eachLocation: eachElement,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianSurfaceMeshParams>, currentProps: PD.Values<GaussianSurfaceMeshParams>) => {
+            if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
+            if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
+            if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
+        }
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
index 5efe3f47bf6395431987a0717b27e6f7d4558b38..834b6fd41612761c1665a620955f1f224d038bca 100644
--- a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
+++ b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
@@ -39,7 +39,7 @@ export const GaussianWireframeParams = {
 }
 export type GaussianWireframeParams = typeof GaussianWireframeParams
 
-export function GaussianWireframeVisual(): UnitsVisual<GaussianWireframeParams> {
+export function GaussianWireframeVisual(materialId: number): UnitsVisual<GaussianWireframeParams> {
     return UnitsLinesVisual<GaussianWireframeParams>({
         defaultProps: PD.getDefaultValues(GaussianWireframeParams),
         createGeometry: createGaussianWireframe,
@@ -52,5 +52,5 @@ export function GaussianWireframeVisual(): UnitsVisual<GaussianWireframeParams>
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
             if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
index fa55745425b88e8b15149d9d2e83ee4af92e5377..90999a7f28ddfe8e4ff180e5f234ef69b30ee866 100644
--- a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
@@ -58,7 +58,7 @@ export const InterUnitLinkParams = {
 }
 export type InterUnitLinkParams = typeof InterUnitLinkParams
 
-export function InterUnitLinkVisual(): ComplexVisual<InterUnitLinkParams> {
+export function InterUnitLinkVisual(materialId: number): ComplexVisual<InterUnitLinkParams> {
     return ComplexMeshVisual<InterUnitLinkParams>({
         defaultProps: PD.getDefaultValues(InterUnitLinkParams),
         createGeometry: createInterUnitLinkCylinderMesh,
@@ -74,7 +74,7 @@ export function InterUnitLinkVisual(): ComplexVisual<InterUnitLinkParams> {
                 newProps.linkSpacing !== currentProps.linkSpacing
             )
         }
-    })
+    }, materialId)
 }
 
 function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
diff --git a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
index 9f86c6700696e789bcfd4b4b4895b981facc26bc..7cbe60c189d4455ae3e134abbd682a02a1dd3adc 100644
--- a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
@@ -78,7 +78,7 @@ export const IntraUnitLinkParams = {
 }
 export type IntraUnitLinkParams = typeof IntraUnitLinkParams
 
-export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkParams> {
+export function IntraUnitLinkVisual(materialId: number): UnitsVisual<IntraUnitLinkParams> {
     return UnitsMeshVisual<IntraUnitLinkParams>({
         defaultProps: PD.getDefaultValues(IntraUnitLinkParams),
         createGeometry: createIntraUnitLinkCylinderMesh,
@@ -94,7 +94,7 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkParams> {
                 newProps.linkSpacing !== currentProps.linkSpacing
             )
         }
-    })
+    }, materialId)
 }
 
 function getLinkLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
diff --git a/src/mol-repr/structure/visual/molecular-surface-mesh.ts b/src/mol-repr/structure/visual/molecular-surface-mesh.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aea6b6de911d54e3b67996e1cb028cf1c9881db8
--- /dev/null
+++ b/src/mol-repr/structure/visual/molecular-surface-mesh.ts
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, Structure } from 'mol-model/structure';
+import { UnitsVisual } from '../representation';
+import { VisualUpdateState } from '../../util';
+import { UnitsMeshVisual, UnitsMeshParams, UnitsTextureMeshParams } from '../units-visual';
+import { StructureElementIterator, getElementLoci, eachElement } from './util/element';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm';
+import { VisualContext } from 'mol-repr/visual';
+import { Theme } from 'mol-theme/theme';
+import { computeUnitMolecularSurface } from './util/molecular-surface';
+import { MolecularSurfaceCalculationParams, MolecularSurfaceCalculationProps } from 'mol-math/geometry/molecular-surface';
+
+export const MolecularSurfaceMeshParams = {
+    ...UnitsMeshParams,
+    ...UnitsTextureMeshParams,
+    ...MolecularSurfaceCalculationParams,
+}
+export type MolecularSurfaceMeshParams = typeof MolecularSurfaceMeshParams
+
+//
+
+async function createMolecularSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: MolecularSurfaceCalculationProps, mesh?: Mesh): Promise<Mesh> {
+
+    const { transform, field, idField } = await computeUnitMolecularSurface(unit, props).runInContext(ctx.runtime)
+    const params = {
+        isoLevel: props.probeRadius,
+        scalarField: field,
+        idField
+    }
+    const surface = await computeMarchingCubesMesh(params, mesh).runAsChild(ctx.runtime)
+
+    Mesh.transformImmediate(surface, transform)
+    Mesh.uniformTriangleGroup(surface)
+
+    return surface
+}
+
+export function MolecularSurfaceMeshVisual(materialId: number): UnitsVisual<MolecularSurfaceMeshParams> {
+    return UnitsMeshVisual<MolecularSurfaceMeshParams>({
+        defaultProps: PD.getDefaultValues(MolecularSurfaceMeshParams),
+        createGeometry: createMolecularSurfaceMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        eachLocation: eachElement,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<MolecularSurfaceMeshParams>, currentProps: PD.Values<MolecularSurfaceMeshParams>) => {
+            if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
+            if (newProps.probeRadius !== currentProps.probeRadius) state.createGeometry = true
+            if (newProps.probePositions !== currentProps.probePositions) state.createGeometry = true
+        }
+    }, materialId)
+}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
index 78fa5aa2f974b63e88cac56235da5aa3a09a66d4..f23f1424ef64516424b4be5826a2a8f8f0df7232 100644
--- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
+++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
@@ -128,7 +128,7 @@ export const NucleotideBlockParams = {
 }
 export type NucleotideBlockParams = typeof NucleotideBlockParams
 
-export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockParams> {
+export function NucleotideBlockVisual(materialId: number): UnitsVisual<NucleotideBlockParams> {
     return UnitsMeshVisual<NucleotideBlockParams>({
         defaultProps: PD.getDefaultValues(NucleotideBlockParams),
         createGeometry: createNucleotideBlockMesh,
@@ -141,5 +141,5 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockParams> {
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
index 6baef02237f105ec1392ceb42465fa160525e571..d3b246d5c68bea5bd3751d12a255f4e225a2616d 100644
--- a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
+++ b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
@@ -175,7 +175,7 @@ export const NucleotideRingParams = {
 }
 export type NucleotideRingParams = typeof NucleotideRingParams
 
-export function NucleotideRingVisual(): UnitsVisual<NucleotideRingParams> {
+export function NucleotideRingVisual(materialId: number): UnitsVisual<NucleotideRingParams> {
     return UnitsMeshVisual<NucleotideRingParams>({
         defaultProps: PD.getDefaultValues(NucleotideRingParams),
         createGeometry: createNucleotideRingMesh,
@@ -188,5 +188,5 @@ export function NucleotideRingVisual(): UnitsVisual<NucleotideRingParams> {
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
index 8584245222c4ebd57889f01f13c254ab3790bf40..ce98d3d40174b412a73abff94212c3789c9c9634 100644
--- a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
+++ b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
@@ -67,7 +67,7 @@ export const PolymerBackboneParams = {
 }
 export type PolymerBackboneParams = typeof PolymerBackboneParams
 
-export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneParams> {
+export function PolymerBackboneVisual(materialId: number): UnitsVisual<PolymerBackboneParams> {
     return UnitsMeshVisual<PolymerBackboneParams>({
         defaultProps: PD.getDefaultValues(PolymerBackboneParams),
         createGeometry: createPolymerBackboneCylinderMesh,
@@ -81,5 +81,5 @@ export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneParams> {
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/polymer-direction-wedge.ts b/src/mol-repr/structure/visual/polymer-direction-wedge.ts
index ab3e037548ced5b766f2c8b630fd14b14697310d..43712a89d45b50cd00ffd9e810b4d11220c31d45 100644
--- a/src/mol-repr/structure/visual/polymer-direction-wedge.ts
+++ b/src/mol-repr/structure/visual/polymer-direction-wedge.ts
@@ -92,7 +92,7 @@ export const PolymerDirectionParams = {
 }
 export type PolymerDirectionParams = typeof PolymerDirectionParams
 
-export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionParams> {
+export function PolymerDirectionVisual(materialId: number): UnitsVisual<PolymerDirectionParams> {
     return UnitsMeshVisual<PolymerDirectionParams>({
         defaultProps: PD.getDefaultValues(PolymerDirectionParams),
         createGeometry: createPolymerDirectionWedgeMesh,
@@ -104,5 +104,5 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionParams> {
                 newProps.sizeFactor !== currentProps.sizeFactor
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
index dc80fba747b4d4de619b47257899b0efc3ec3a14..28dddc9f1eefcede5512fae57dc399d14de9d660 100644
--- a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
+++ b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
@@ -85,7 +85,7 @@ export const PolymerGapParams = {
 }
 export type PolymerGapParams = typeof PolymerGapParams
 
-export function PolymerGapVisual(): UnitsVisual<PolymerGapParams> {
+export function PolymerGapVisual(materialId: number): UnitsVisual<PolymerGapParams> {
     return UnitsMeshVisual<PolymerGapParams>({
         defaultProps: PD.getDefaultValues(PolymerGapParams),
         createGeometry: createPolymerGapCylinderMesh,
@@ -98,5 +98,5 @@ export function PolymerGapVisual(): UnitsVisual<PolymerGapParams> {
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/polymer-trace-mesh.ts b/src/mol-repr/structure/visual/polymer-trace-mesh.ts
index dfbd3b6a363862434493f0977d83a5a362ab0b09..9fafb82b81d350f00d38afd0c6a80304d3608d19 100644
--- a/src/mol-repr/structure/visual/polymer-trace-mesh.ts
+++ b/src/mol-repr/structure/visual/polymer-trace-mesh.ts
@@ -104,7 +104,7 @@ export const PolymerTraceParams = {
 }
 export type PolymerTraceParams = typeof PolymerTraceParams
 
-export function PolymerTraceVisual(): UnitsVisual<PolymerTraceParams> {
+export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTraceParams> {
     return UnitsMeshVisual<PolymerTraceParams>({
         defaultProps: PD.getDefaultValues(PolymerTraceParams),
         createGeometry: createPolymerTraceMesh,
@@ -120,5 +120,5 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceParams> {
                 newProps.arrowFactor !== currentProps.arrowFactor
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/polymer-tube-mesh.ts b/src/mol-repr/structure/visual/polymer-tube-mesh.ts
index 94accbff631fddd68ac6de7a8b8640d53f64a7d2..8f433bf01a8c4ba16704f1e13601db57148fd33e 100644
--- a/src/mol-repr/structure/visual/polymer-tube-mesh.ts
+++ b/src/mol-repr/structure/visual/polymer-tube-mesh.ts
@@ -70,7 +70,7 @@ export const PolymerTubeParams = {
 }
 export type PolymerTubeParams = typeof PolymerTubeParams
 
-export function PolymerTubeVisual(): UnitsVisual<PolymerTubeParams> {
+export function PolymerTubeVisual(materialId: number): UnitsVisual<PolymerTubeParams> {
     return UnitsMeshVisual<PolymerTubeParams>({
         defaultProps: PD.getDefaultValues(PolymerTubeParams),
         createGeometry: createPolymerTubeMesh,
@@ -84,5 +84,5 @@ export function PolymerTubeVisual(): UnitsVisual<PolymerTubeParams> {
                 newProps.radialSegments !== currentProps.radialSegments
             )
         }
-    })
+    }, materialId)
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/util/common.ts b/src/mol-repr/structure/visual/util/common.ts
index 1e2f479d22e546a668aab603854caf7841db563f..f2f8faa6da167c06383549f84056fc1af8ee163a 100644
--- a/src/mol-repr/structure/visual/util/common.ts
+++ b/src/mol-repr/structure/visual/util/common.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,9 +9,10 @@ import { Mat4 } from 'mol-math/linear-algebra';
 import { TransformData, createTransform } from 'mol-geo/geometry/transform-data';
 import { OrderedSet, SortedArray } from 'mol-data/int';
 import { EmptyLoci, Loci } from 'mol-model/loci';
+import { PhysicalSizeTheme } from 'mol-theme/size/physical';
 
 /** Return a Loci for the elements of a whole residue the elementIndex belongs to. */
-export function getResidueLoci(structure: Structure, unit: Unit, elementIndex: ElementIndex): Loci {
+export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci {
     const { elements, model } = unit
     if (OrderedSet.indexOf(elements, elementIndex) !== -1) {
         const { index, offsets } = model.atomicHierarchy.residueAtomSegments
@@ -55,4 +56,68 @@ export function includesUnitKind(unitKinds: UnitKind[], unit: Unit) {
         if (Unit.isGaussians(unit) && unitKinds[i] === 'gaussians') return true
     }
     return false
+}
+
+//
+
+export function getConformation(unit: Unit) {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic: return unit.model.atomicConformation
+        case Unit.Kind.Spheres: return unit.model.coarseConformation.spheres
+        case Unit.Kind.Gaussians: return unit.model.coarseConformation.gaussians
+    }
+}
+
+export function getUnitConformationAndRadius(unit: Unit) {
+    const conformation = getConformation(unit)
+    const { elements } = unit
+    const position = {
+        indices: elements,
+        x: conformation.x,
+        y: conformation.y,
+        z: conformation.z
+    }
+
+    const l = StructureElement.create(unit)
+    const sizeTheme = PhysicalSizeTheme({}, {})
+    const radius = (index: number) => {
+        l.element = index as ElementIndex
+        return sizeTheme.size(l)
+    }
+
+    return { position, radius }
+}
+
+export function getStructureConformationAndRadius(structure: Structure) {
+    const n = structure.elementCount
+
+    const xs = new Float32Array(n)
+    const ys = new Float32Array(n)
+    const zs = new Float32Array(n)
+    const rs = new Float32Array(n)
+
+    const l = StructureElement.create()
+    const sizeTheme = PhysicalSizeTheme({}, {})
+
+    let m = 0
+    for (let i = 0, il = structure.units.length; i < il; ++i) {
+        const unit = structure.units[i]
+        const { elements } = unit
+        const { x, y, z } = unit.conformation
+        l.unit = unit
+        for (let j = 0, jl = elements.length; j < jl; ++j) {
+            const eI = elements[j]
+            xs[m + j] = x(eI)
+            ys[m + j] = y(eI)
+            zs[m + j] = z(eI)
+            l.element = eI
+            rs[m + j] = sizeTheme.size(l)
+        }
+        m += elements.length
+    }
+
+    const position = { indices: OrderedSet.ofRange(0, n), x: xs, y: ys, z: zs }
+    const radius = (index: number) => rs[index]
+
+    return { position, radius }
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/util/gaussian.ts b/src/mol-repr/structure/visual/util/gaussian.ts
index 9cee5dcdddacc77754afb886ad4829538ea46779..2011ee1e1e7716c89e56ce08435cda4310b1d802 100644
--- a/src/mol-repr/structure/visual/util/gaussian.ts
+++ b/src/mol-repr/structure/visual/util/gaussian.ts
@@ -4,18 +4,17 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, StructureElement, ElementIndex, Structure } from 'mol-model/structure';
+import { Unit, Structure } from 'mol-model/structure';
 import { GaussianDensity } from 'mol-math/geometry/gaussian-density';
 import { Task } from 'mol-task';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { GaussianDensityTexture } from 'mol-math/geometry/gaussian-density/gpu';
+import { GaussianDensityTexture, GaussianDensityTexture2d } from 'mol-math/geometry/gaussian-density/gpu';
 import { Texture } from 'mol-gl/webgl/texture';
 import { WebGLContext } from 'mol-gl/webgl/context';
-import { PhysicalSizeTheme } from 'mol-theme/size/physical';
-import { OrderedSet } from 'mol-data/int';
+import { getUnitConformationAndRadius, getStructureConformationAndRadius } from './common';
 
 export const GaussianDensityParams = {
-    resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
+    resolution: PD.Numeric(1, { min: 0.1, max: 20, step: 0.1 }),
     radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
     smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
     useGpu: PD.Boolean(false),
@@ -24,7 +23,7 @@ export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityPa
 export type GaussianDensityProps = typeof DefaultGaussianDensityProps
 
 export const GaussianDensityTextureParams = {
-    resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
+    resolution: PD.Numeric(1, { min: 0.1, max: 20, step: 0.1 }),
     radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
     smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
 }
@@ -33,34 +32,6 @@ export type GaussianDensityTextureProps = typeof DefaultGaussianDensityTexturePr
 
 //
 
-function getConformation(unit: Unit) {
-    switch (unit.kind) {
-        case Unit.Kind.Atomic: return unit.model.atomicConformation
-        case Unit.Kind.Spheres: return unit.model.coarseConformation.spheres
-        case Unit.Kind.Gaussians: return unit.model.coarseConformation.gaussians
-    }
-}
-
-function getUnitConformationAndRadius(unit: Unit) {
-    const conformation = getConformation(unit)
-    const { elements } = unit
-    const position = {
-        indices: elements,
-        x: conformation.x,
-        y: conformation.y,
-        z: conformation.z
-    }
-
-    const l = StructureElement.create(unit)
-    const sizeTheme = PhysicalSizeTheme({}, {})
-    const radius = (index: number) => {
-        l.element = index as ElementIndex
-        return sizeTheme.size(l)
-    }
-
-    return { position, radius }
-}
-
 export function computeUnitGaussianDensity(unit: Unit, props: GaussianDensityProps, webgl?: WebGLContext) {
     const { position, radius } = getUnitConformationAndRadius(unit)
     return Task.create('Gaussian Density', async ctx => {
@@ -68,49 +39,22 @@ export function computeUnitGaussianDensity(unit: Unit, props: GaussianDensityPro
     });
 }
 
-export function computeUnitGaussianDensityTexture(unit: Unit, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
+export function computeUnitGaussianDensityTexture(unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
     const { position, radius } = getUnitConformationAndRadius(unit)
     return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensityTexture(ctx, webgl, position, unit.lookup3d.boundary.box, radius, props, texture);
+        return GaussianDensityTexture(webgl, position, unit.lookup3d.boundary.box, radius, props, texture);
     });
 }
 
-//
-
-function getStructureConformationAndRadius(structure: Structure) {
-    const n = structure.elementCount
-
-    const xs = new Float32Array(n)
-    const ys = new Float32Array(n)
-    const zs = new Float32Array(n)
-    const rs = new Float32Array(n)
-
-    const l = StructureElement.create()
-    const sizeTheme = PhysicalSizeTheme({}, {})
-
-    let m = 0
-    for (let i = 0, il = structure.units.length; i < il; ++i) {
-        const unit = structure.units[i]
-        const { elements } = unit
-        const { x, y, z } = unit.conformation
-        l.unit = unit
-        for (let j = 0, jl = elements.length; j < jl; ++j) {
-            const eI = elements[j]
-            xs[m + j] = x(eI)
-            ys[m + j] = y(eI)
-            zs[m + j] = z(eI)
-            l.element = eI
-            rs[m + j] = sizeTheme.size(l)
-        }
-        m += elements.length
-    }
-
-    const position = { indices: OrderedSet.ofRange(0, n), x: xs, y: ys, z: zs }
-    const radius = (index: number) => rs[index]
-
-    return { position, radius }
+export function computeUnitGaussianDensityTexture2d(unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+    const { position, radius } = getUnitConformationAndRadius(unit)
+    return Task.create('Gaussian Density', async ctx => {
+        return GaussianDensityTexture2d(webgl, position, unit.lookup3d.boundary.box, radius, props, texture);
+    });
 }
 
+//
+
 export function computeStructureGaussianDensity(structure: Structure, props: GaussianDensityProps, webgl?: WebGLContext) {
     const { position, radius } = getStructureConformationAndRadius(structure)
     return Task.create('Gaussian Density', async ctx => {
@@ -121,6 +65,6 @@ export function computeStructureGaussianDensity(structure: Structure, props: Gau
 export function computeStructureGaussianDensityTexture(structure: Structure, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
     const { position, radius } = getStructureConformationAndRadius(structure)
     return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensityTexture(ctx, webgl, position, structure.lookup3d.boundary.box, radius, props, texture);
+        return GaussianDensityTexture(webgl, position, structure.lookup3d.boundary.box, radius, props, texture);
     });
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/util/molecular-surface.ts b/src/mol-repr/structure/visual/util/molecular-surface.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a1b54e9ff690de393d97b2ed9524861d04b427cb
--- /dev/null
+++ b/src/mol-repr/structure/visual/util/molecular-surface.ts
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit } from 'mol-model/structure';
+import { Task, RuntimeContext } from 'mol-task';
+import { getUnitConformationAndRadius } from './common';
+import { PositionData, DensityData } from 'mol-math/geometry';
+import { MolecularSurfaceCalculationProps, calcMolecularSurface } from 'mol-math/geometry/molecular-surface';
+import { OrderedSet } from 'mol-data/int';
+
+function getPositionDataAndMaxRadius(unit: Unit, props: MolecularSurfaceCalculationProps) {
+    const { position, radius } = getUnitConformationAndRadius(unit)
+    const { indices } = position
+    const n = OrderedSet.size(indices)
+    const radii = new Float32Array(OrderedSet.end(indices))
+
+    let maxRadius = 0
+    for (let i = 0; i < n; ++i) {
+        const j = OrderedSet.getAt(indices, i)
+        const r = radius(j)
+        if (maxRadius < r) maxRadius = r
+        radii[j] = r + props.probeRadius
+    }
+
+    return { position: { ...position, radius: radii }, maxRadius }
+}
+
+export function computeUnitMolecularSurface(unit: Unit, props: MolecularSurfaceCalculationProps) {
+    const { position, maxRadius } = getPositionDataAndMaxRadius(unit, props)
+    return Task.create('Molecular Surface', async ctx => {
+        return await MolecularSurface(ctx, position, maxRadius, props);
+    });
+}
+
+//
+
+async function MolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number,  props: MolecularSurfaceCalculationProps): Promise<DensityData> {
+    return calcMolecularSurface(ctx, position, maxRadius, props)
+}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/util/nucleotide.ts b/src/mol-repr/structure/visual/util/nucleotide.ts
index 22fe970f1692264dadea9354d0b454aa44fea7bc..8e8ed71d1e3ad7de88cede0d4e6d5000bb6e77ef 100644
--- a/src/mol-repr/structure/visual/util/nucleotide.ts
+++ b/src/mol-repr/structure/visual/util/nucleotide.ts
@@ -35,7 +35,9 @@ export function getNucleotideElementLoci(pickingId: PickingId, structureGroup: S
     if (id === objectId) {
         const { structure, group } = structureGroup
         const unit = group.units[instanceId]
-        return getResidueLoci(structure, unit, unit.polymerElements[groupId])
+        if (Unit.isAtomic(unit)) {
+            return getResidueLoci(structure, unit, unit.polymerElements[groupId])
+        }
     }
     return EmptyLoci
 }
diff --git a/src/mol-repr/structure/visual/util/polymer.ts b/src/mol-repr/structure/visual/util/polymer.ts
index 71b987a3ea8b5ac3954524d393e59875b55b5733..5734c8dee6464fd4c6ffe60c26f38129f139f9be 100644
--- a/src/mol-repr/structure/visual/util/polymer.ts
+++ b/src/mol-repr/structure/visual/util/polymer.ts
@@ -72,12 +72,25 @@ export function getPolymerElementLoci(pickingId: PickingId, structureGroup: Stru
     if (id === objectId) {
         const { structure, group } = structureGroup
         const unit = group.units[instanceId]
-        return getResidueLoci(structure, unit, unit.polymerElements[groupId])
+        if (Unit.isAtomic(unit)) {
+            return getResidueLoci(structure, unit, unit.polymerElements[groupId])
+        } else {
+            const { elements } = unit
+            const elementIndex = unit.polymerElements[groupId]
+            const unitIndex = OrderedSet.indexOf(elements, elementIndex) as StructureElement.UnitIndex | -1
+            if (unitIndex !== -1) {
+                const indices = OrderedSet.ofSingleton(unitIndex)
+                return StructureElement.Loci(structure, [{ unit, indices }])
+            }
+        }
     }
     return EmptyLoci
 }
 
-/** Mark a polymer element (e.g. part of a cartoon trace) when all its residue's elements are in a loci. */
+/**
+ * Mark a polymer element (e.g. part of a cartoon trace)
+ * - for atomic units mark only when all its residue's elements are in a loci
+ */
 export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
     if (!StructureElement.isLoci(loci)) return false
@@ -90,19 +103,32 @@ export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, a
     for (const e of loci.elements) {
         const unitIdx = group.unitIndexMap.get(e.unit.id)
         if (unitIdx !== undefined) {
-            // TODO optimized implementation for intervals
-            OrderedSet.forEach(e.indices, v => {
-                const rI = index[elements[v]]
-                const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
-                const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
-                const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-                if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
-                const eI = traceElementIndex[rI]
-                const idx = OrderedSet.indexOf(e.unit.polymerElements, eI)
-                if (idx !== -1) {
-                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+            if (Unit.isAtomic(e.unit)) {
+                // TODO optimized implementation for intervals
+                OrderedSet.forEach(e.indices, v => {
+                    const rI = index[elements[v]]
+                    const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
+                    const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
+                    const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
+                    if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
+                    const eI = traceElementIndex[rI]
+                    const idx = OrderedSet.indexOf(e.unit.polymerElements, eI)
+                    if (idx !== -1) {
+                        if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                    }
+                })
+            } else {
+                if (Interval.is(e.indices)) {
+                    const start = unitIdx * groupCount + Interval.start(e.indices);
+                    const end = unitIdx * groupCount + Interval.end(e.indices);
+                    if (apply(Interval.ofBounds(start, end))) changed = true
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = unitIdx * groupCount + e.indices[i];
+                        if (apply(Interval.ofSingleton(idx))) changed = true
+                    }
                 }
-            })
+            }
         }
     }
     return changed
@@ -128,17 +154,33 @@ export function getPolymerGapElementLoci(pickingId: PickingId, structureGroup: S
 
 export function eachPolymerGapElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (!Link.isLoci(loci)) return false
-    const { structure, group } = structureGroup
-    if (!Structure.areEquivalent(loci.structure, structure)) return false
-    const groupCount = group.units[0].gapElements.length
-    for (const b of loci.links) {
-        const unitIdx = group.unitIndexMap.get(b.aUnit.id)
-        if (unitIdx !== undefined) {
-            const idxA = OrderedSet.indexOf(b.aUnit.gapElements, b.aUnit.elements[b.aIndex])
-            const idxB = OrderedSet.indexOf(b.bUnit.gapElements, b.bUnit.elements[b.bIndex])
-            if (idxA !== -1 && idxB !== -1) {
-                if (apply(Interval.ofSingleton(unitIdx * groupCount + idxA))) changed = true
+    if (Link.isLoci(loci)) {
+        const { structure, group } = structureGroup
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
+        const groupCount = group.units[0].gapElements.length
+        for (const b of loci.links) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+            if (unitIdx !== undefined) {
+                const idxA = OrderedSet.indexOf(b.aUnit.gapElements, b.aUnit.elements[b.aIndex])
+                const idxB = OrderedSet.indexOf(b.bUnit.gapElements, b.bUnit.elements[b.bIndex])
+                if (idxA !== -1 && idxB !== -1) {
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idxA))) changed = true
+                }
+            }
+        }
+    } else if (StructureElement.isLoci(loci)) {
+        const { structure, group } = structureGroup
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
+        const groupCount = group.units[0].gapElements.length
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined) {
+                OrderedSet.forEach(e.indices, v => {
+                    const idx = OrderedSet.indexOf(e.unit.gapElements, e.unit.elements[v])
+                    if (idx !== -1) {
+                        if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                    }
+                })
             }
         }
     }
diff --git a/src/mol-repr/visual.ts b/src/mol-repr/visual.ts
index 92072569ebe9dae974c23f7e6966aebefdafabfe..f0e6f182ab17fcd3423ff4b2211e5bca664ddfb8 100644
--- a/src/mol-repr/visual.ts
+++ b/src/mol-repr/visual.ts
@@ -7,7 +7,7 @@
 import { RuntimeContext } from 'mol-task'
 import { GraphicsRenderObject } from 'mol-gl/render-object'
 import { PickingId } from '../mol-geo/geometry/picking';
-import { Loci } from 'mol-model/loci';
+import { Loci, isEmptyLoci } from 'mol-model/loci';
 import { MarkerAction, applyMarkerAction } from '../mol-geo/geometry/marker-data';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { WebGLContext } from 'mol-gl/webgl/context';
@@ -19,6 +19,8 @@ import { ValueCell } from 'mol-util';
 import { Overpaint } from 'mol-theme/overpaint';
 import { createOverpaint, clearOverpaint, applyOverpaintColor } from 'mol-geo/geometry/overpaint-data';
 import { Interval } from 'mol-data/int';
+import { Transparency } from 'mol-theme/transparency';
+import { createTransparency, clearTransparency, applyTransparencyValue } from 'mol-geo/geometry/transparency-data';
 
 export interface VisualContext {
     readonly runtime: RuntimeContext
@@ -39,6 +41,7 @@ interface Visual<D, P extends PD.Params> {
     setPickable: (pickable: boolean) => void
     setTransform: (matrix?: Mat4, instanceMatrices?: Float32Array | null) => void
     setOverpaint: (overpaint: Overpaint) => void
+    setTransparency: (transparency: Transparency) => void
     destroy: () => void
 }
 namespace Visual {
@@ -96,6 +99,30 @@ namespace Visual {
         ValueCell.update(tOverpaint, tOverpaint.ref.value)
     }
 
+    export function setTransparency(renderObject: GraphicsRenderObject | undefined, transparency: Transparency, lociApply: LociApply, clear: boolean) {
+        if (!renderObject) return
+
+        const { tTransparency, uGroupCount, instanceCount } = renderObject.values
+        const count = uGroupCount.ref.value * instanceCount.ref.value
+
+        const { loci, value, variant } = transparency
+
+        // ensure texture has right size and variant
+        createTransparency(value && !isEmptyLoci(loci) ? count : 0, variant, renderObject.values)
+
+        // clear if requested
+        if (clear) clearTransparency(tTransparency.ref.value.array, 0, count)
+
+        const apply = (interval: Interval) => {
+            const start = Interval.start(interval)
+            const end = Interval.end(interval)
+            return applyTransparencyValue(tTransparency.ref.value.array, start, end, value)
+        }
+        lociApply(loci, apply)
+
+        ValueCell.update(tTransparency, tTransparency.ref.value)
+    }
+
     export function setTransform(renderObject: GraphicsRenderObject | undefined, transform?: Mat4, instanceTransforms?: Float32Array | null) {
         if (!renderObject || (!transform && !instanceTransforms)) return
 
diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts
index 805045ae744120de65151dbd556ee97ad3d20f04..813a030f9899169011e9fa674f953800afa93abb 100644
--- a/src/mol-repr/volume/direct-volume.ts
+++ b/src/mol-repr/volume/direct-volume.ts
@@ -163,7 +163,7 @@ export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: VolumeD
     return PD.clone(DirectVolumeParams)
 }
 
-export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> {
+export function DirectVolumeVisual(materialId: number): VolumeVisual<DirectVolumeParams> {
     return VolumeVisual<DirectVolume, DirectVolumeParams>({
         defaultProps: PD.getDefaultValues(DirectVolumeParams),
         createGeometry: createDirectVolume,
@@ -173,7 +173,7 @@ export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> {
         setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
         },
         geometryUtils: DirectVolume.Utils
-    })
+    }, materialId)
 }
 
 export function DirectVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, DirectVolumeParams>): VolumeRepresentation<DirectVolumeParams> {
diff --git a/src/mol-repr/volume/isosurface.ts b/src/mol-repr/volume/isosurface.ts
index b29c7afe5970b0898958696bda5c23590a15f924..224afe6779457f11d03b73c9a9e4806d41d0e752 100644
--- a/src/mol-repr/volume/isosurface.ts
+++ b/src/mol-repr/volume/isosurface.ts
@@ -78,7 +78,6 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
     const transform = VolumeData.getGridToCartesianTransform(volume);
     ctx.runtime.update({ message: 'Transforming mesh...' });
     Mesh.transformImmediate(surface, transform);
-    Mesh.computeNormalsImmediate(surface)
 
     return surface;
 }
@@ -89,7 +88,7 @@ export const IsosurfaceMeshParams = {
 }
 export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
 
-export function IsosurfaceMeshVisual(): VolumeVisual<IsosurfaceMeshParams> {
+export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> {
     return VolumeVisual<Mesh, IsosurfaceMeshParams>({
         defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
         createGeometry: createVolumeIsosurfaceMesh,
@@ -100,7 +99,7 @@ export function IsosurfaceMeshVisual(): VolumeVisual<IsosurfaceMeshParams> {
             if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true
         },
         geometryUtils: Mesh.Utils
-    })
+    }, materialId)
 }
 
 //
@@ -125,7 +124,7 @@ export const IsosurfaceWireframeParams = {
 }
 export type IsosurfaceWireframeParams = typeof IsosurfaceWireframeParams
 
-export function IsosurfaceWireframeVisual(): VolumeVisual<IsosurfaceWireframeParams> {
+export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<IsosurfaceWireframeParams> {
     return VolumeVisual<Lines, IsosurfaceWireframeParams>({
         defaultProps: PD.getDefaultValues(IsosurfaceWireframeParams),
         createGeometry: createVolumeIsosurfaceWireframe,
@@ -136,7 +135,7 @@ export function IsosurfaceWireframeVisual(): VolumeVisual<IsosurfaceWireframePar
             if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true
         },
         geometryUtils: Lines.Utils
-    })
+    }, materialId)
 }
 
 //
diff --git a/src/mol-repr/volume/registry.ts b/src/mol-repr/volume/registry.ts
index 51507b46e7188e41f305255fcf422d6e4a780d3d..fde44525b8afd8496cb737c17c03cacfae3163dc 100644
--- a/src/mol-repr/volume/registry.ts
+++ b/src/mol-repr/volume/registry.ts
@@ -7,7 +7,6 @@
 import { RepresentationProvider, RepresentationRegistry, Representation } from '../representation';
 import { VolumeData } from 'mol-model/volume';
 import { IsosurfaceRepresentationProvider } from './isosurface';
-import { DirectVolumeRepresentationProvider } from './direct-volume';
 
 export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeData, Representation.State> {
     constructor() {
@@ -21,7 +20,7 @@ export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeD
 
 export const BuiltInVolumeRepresentations = {
     'isosurface': IsosurfaceRepresentationProvider,
-    'direct-volume': DirectVolumeRepresentationProvider,
+    // 'direct-volume': DirectVolumeRepresentationProvider, // TODO disabled for now, needs more work
 }
 export type BuiltInVolumeRepresentationsName = keyof typeof BuiltInVolumeRepresentations
 export const BuiltInVolumeRepresentationsNames = Object.keys(BuiltInVolumeRepresentations)
diff --git a/src/mol-repr/volume/representation.ts b/src/mol-repr/volume/representation.ts
index 9b2472a4cd1a29540f927f8127ba97ffac0db454..621c17ebcf8fb4e07924573f8b544e7e2fa28291 100644
--- a/src/mol-repr/volume/representation.ts
+++ b/src/mol-repr/volume/representation.ts
@@ -13,7 +13,7 @@ import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
-import { GraphicsRenderObject, createRenderObject } from 'mol-gl/render-object';
+import { GraphicsRenderObject, createRenderObject, getNextMaterialId } from 'mol-gl/render-object';
 import { Interval } from 'mol-data/int';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { VisualUpdateState } from 'mol-repr/util';
@@ -27,15 +27,16 @@ import { ColorTheme } from 'mol-theme/color';
 import { createColors } from 'mol-geo/geometry/color-data';
 import { createSizes } from 'mol-geo/geometry/size-data';
 import { Overpaint } from 'mol-theme/overpaint';
+import { Transparency } from 'mol-theme/transparency';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeData, P> { }
 
-function createVolumeRenderObject<G extends Geometry>(volume: VolumeData, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) {
+function createVolumeRenderObject<G extends Geometry>(volume: VolumeData, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) {
     const { createValues, createRenderableState } = Geometry.getUtils(geometry)
     const transform = createIdentityTransform()
     const values = createValues(geometry, transform, locationIt, theme, props)
     const state = createRenderableState(props)
-    return createRenderObject(geometry.kind, values, state)
+    return createRenderObject(geometry.kind, values, state, materialId)
 }
 
 interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
@@ -51,7 +52,7 @@ interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry
     geometryUtils: GeometryUtils<G>
 }
 
-export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geometry.Params<G>>(builder: VolumeVisualGeometryBuilder<P, G>): VolumeVisual<P> {
+export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geometry.Params<G>>(builder: VolumeVisualGeometryBuilder<P, G>, materialId: number): VolumeVisual<P> {
     const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder
     const { updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils
     const updateState = VisualUpdateState.create()
@@ -104,7 +105,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
         if (updateState.createNew) {
             locationIt = createLocationIterator(newVolume)
             if (newGeometry) {
-                renderObject = createVolumeRenderObject(newVolume, newGeometry, locationIt, newTheme, newProps)
+                renderObject = createVolumeRenderObject(newVolume, newGeometry, locationIt, newTheme, newProps, materialId)
             } else {
                 throw new Error('expected geometry to be given')
             }
@@ -186,6 +187,9 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
         setOverpaint(overpaint: Overpaint) {
             return Visual.setOverpaint(renderObject, overpaint, lociApply, true)
         },
+        setTransparency(transparency: Transparency) {
+            return Visual.setTransparency(renderObject, transparency, lociApply, true)
+        },
         destroy() {
             // TODO
             renderObject = undefined
@@ -204,9 +208,10 @@ export const VolumeParams = {
 }
 export type VolumeParams = typeof VolumeParams
 
-export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: () => VolumeVisual<P>): VolumeRepresentation<P> {
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (materialId: number) => VolumeVisual<P>): VolumeRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const materialId = getNextMaterialId()
     const renderObjects: GraphicsRenderObject[] = []
     const _state = Representation.createState()
     let visual: VolumeVisual<P> | undefined
@@ -225,7 +230,7 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         _props = Object.assign({}, _props, props)
 
         return Task.create('Creating or updating VolumeRepresentation', async runtime => {
-            if (!visual) visual = visualCtor()
+            if (!visual) visual = visualCtor(materialId)
             const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
             if (promise) await promise
             // update list of renderObjects
@@ -249,6 +254,7 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         if (state.alphaFactor !== undefined && visual) visual.setAlphaFactor(state.alphaFactor)
         if (state.pickable !== undefined && visual) visual.setPickable(state.pickable)
         if (state.overpaint !== undefined && visual) visual.setOverpaint(state.overpaint)
+        if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency)
         if (state.transform !== undefined && visual) visual.setTransform(state.transform)
 
         Representation.updateState(_state, state)
diff --git a/src/mol-script/language/symbol-table/structure-query.ts b/src/mol-script/language/symbol-table/structure-query.ts
index 4f9f7377efb220f0dbb65717677675540095a9d1..f7d81aa44419493271ac210e149e5b3fb28f2e0e 100644
--- a/src/mol-script/language/symbol-table/structure-query.ts
+++ b/src/mol-script/language/symbol-table/structure-query.ts
@@ -167,6 +167,10 @@ const filter = {
         test: Argument(Type.Bool)
     }), Types.ElementSelectionQuery, 'Pick all atom sets that satisfy the test.'),
 
+    first: symbol(Arguments.Dictionary({
+        0: Argument(Types.ElementSelectionQuery)
+    }), Types.ElementSelectionQuery, 'Take the 1st atom set in the sequence.'),
+
     withSameAtomProperties: symbol(Arguments.Dictionary({
         0: Argument(Types.ElementSelectionQuery),
         source: Argument(Types.ElementSelectionQuery),
diff --git a/src/mol-script/runtime/query/compiler.ts b/src/mol-script/runtime/query/compiler.ts
index ce49a29c3a640db42c317b06f302f52d339dbd34..28b96cb5275dda8bb12fa12f8b03eaf49d09af5e 100644
--- a/src/mol-script/runtime/query/compiler.ts
+++ b/src/mol-script/runtime/query/compiler.ts
@@ -5,7 +5,7 @@
  */
 
 import Expression from '../../language/expression';
-import { QueryContext, QueryFn, Structure, ModelPropertyDescriptor } from 'mol-model/structure';
+import { QueryContext, QueryFn, Structure, CustomPropertyDescriptor } from 'mol-model/structure';
 import { MSymbol } from '../../language/symbol';
 
 export class QueryRuntimeTable {
@@ -18,7 +18,7 @@ export class QueryRuntimeTable {
         this.map.set(runtime.symbol.id, runtime);
     }
 
-    addCustomProp(desc: ModelPropertyDescriptor<any>) {
+    addCustomProp(desc: CustomPropertyDescriptor<any>) {
         if (!desc.symbols) return;
 
         for (const k of Object.keys(desc.symbols)) {
diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts
index ba1072a587beb9c0c8ed2bf3b83dac130894b7f8..4ddbdb8b1945619c723c187001858a952a489ad8 100644
--- a/src/mol-script/runtime/query/table.ts
+++ b/src/mol-script/runtime/query/table.ts
@@ -186,6 +186,9 @@ const symbols = [
     C(MolScript.structureQuery.slot.element, (ctx, _) => ctx.element),
     // C(MolScript.structureQuery.slot.elementSetReduce, (ctx, _) => ctx.element),
 
+    // ============= FILTERS ================
+    D(MolScript.structureQuery.filter.first, (ctx, xs) => Queries.filters.first(xs[0] as any)(ctx)),
+
     // ============= GENERATORS ================
     D(MolScript.structureQuery.generator.atomGroups, (ctx, xs) => Queries.generators.atoms({
         entityTest: xs['entity-test'],
diff --git a/src/mol-script/script/mol-script/symbols.ts b/src/mol-script/script/mol-script/symbols.ts
index 0c4826f7ffc685e85a84b9617b83188def6a6e06..9bd1119533c764200a1ab49b9e0f2e13faed0738 100644
--- a/src/mol-script/script/mol-script/symbols.ts
+++ b/src/mol-script/script/mol-script/symbols.ts
@@ -148,6 +148,7 @@ export const SymbolTable = [
         [
             'Filters',
             Alias(MolScript.structureQuery.filter.pick, 'sel.atom.pick'),
+            Alias(MolScript.structureQuery.filter.first, 'sel.atom.first'),
             Alias(MolScript.structureQuery.filter.withSameAtomProperties, 'sel.atom.with-same-atom-properties'),
             Alias(MolScript.structureQuery.filter.intersectedBy, 'sel.atom.intersected-by'),
             Alias(MolScript.structureQuery.filter.within, 'sel.atom.within'),
diff --git a/src/mol-state/manager.ts b/src/mol-state/manager.ts
deleted file mode 100644
index 0042b15a93f4184628d575672d2570f63ac64dc6..0000000000000000000000000000000000000000
--- a/src/mol-state/manager.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-// TODO manage snapshots etc
\ No newline at end of file
diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts
index b74efcf5ee78f98572acf08c549929d32282932d..dd4c5c1d34261e53c36614f9ffca1c8c86543c44 100644
--- a/src/mol-state/object.ts
+++ b/src/mol-state/object.ts
@@ -19,7 +19,7 @@ interface StateObject<D = any, T extends StateObject.Type = StateObject.Type<any
     readonly label: string,
     readonly description?: string,
     // assigned by reconciler to be StateTransform.props.tag
-    readonly tag?: string
+    readonly tags?: string[]
 }
 
 namespace StateObject {
@@ -56,12 +56,15 @@ namespace StateObject {
 }
 
 interface StateObjectCell<T extends StateObject = StateObject, F extends StateTransform<StateTransformer<any, T, any>> = StateTransform<StateTransformer<any, T, any>>> {
+    parent: State,
+
     transform: F,
 
     // Which object was used as a parent to create data in this cell
     sourceRef: StateTransform.Ref | undefined,
 
     status: StateObjectCell.Status,
+    state: StateTransform.State,
 
     params: {
         definition: ParamDefinition.Params,
@@ -79,24 +82,6 @@ namespace StateObjectCell {
 
     export type Obj<C extends StateObjectCell> = C extends StateObjectCell<infer T> ? T : never
     export type Transform<C extends StateObjectCell> = C extends StateObjectCell<any, infer T> ? T : never
-
-    export interface State {
-        isHidden: boolean,
-        isCollapsed: boolean
-    }
-
-    export const DefaultState: State = { isHidden: false, isCollapsed: false };
-
-    export function areStatesEqual(a: State, b: State) {
-        return a.isHidden !== b.isHidden || a.isCollapsed !== b.isCollapsed;
-    }
-
-    export function isStateChange(a: State, b?: Partial<State>) {
-        if (!b) return false;
-        if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true;
-        if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true;
-        return false;
-    }
 }
 
 // TODO: improve the API?
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index 66563660ade1dd87bd95a428fd867f2d20618baa..6ffb8ff6480353dec56a2d1a6692e573a3314f71 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -33,7 +33,7 @@ class State {
     readonly globalContext: unknown = void 0;
     readonly events = {
         cell: {
-            stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
+            stateUpdated: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
         },
@@ -55,7 +55,6 @@ class State {
 
     get tree(): StateTree { return this._tree; }
     get transforms() { return (this._tree as StateTree).transforms; }
-    get cellStates() { return (this._tree as StateTree).cellStates; }
     get current() { return this.behaviors.currentObject.value.ref; }
 
     build() { return new StateBuilder.Root(this.tree, this); }
@@ -76,13 +75,15 @@ class State {
         this.behaviors.currentObject.next({ state: this, ref });
     }
 
-    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
-        const update = typeof stateOrProvider === 'function'
-            ? stateOrProvider(this.tree.cellStates.get(ref))
-            : stateOrProvider;
+    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateTransform.State) => Partial<StateTransform.State>) | Partial<StateTransform.State>) {
+        const cell = this.cells.get(ref);
+        if (!cell) return;
 
-        if (this._tree.updateCellState(ref, update)) {
-            this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
+        const update = typeof stateOrProvider === 'function' ? stateOrProvider(cell.state) : stateOrProvider;
+
+        if (StateTransform.assignState(cell.state, update)) {
+            cell.transform = this._tree.assignState(cell.transform.ref, update);
+            this.events.cell.stateUpdated.next({ state: this, ref, cell });
         }
     }
 
@@ -165,10 +166,6 @@ class State {
 
             if (updated) this.events.changed.next();
             this.events.isUpdating.next(false);
-
-            for (const ref of ctx.stateChanges) {
-                this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
-            }
         }
     }
 
@@ -189,7 +186,6 @@ class State {
             spine: this.spine,
 
             results: [],
-            stateChanges: [],
 
             options: { ...StateUpdateDefaultOptions, ...options },
 
@@ -203,16 +199,18 @@ class State {
         return ctx;
     }
 
-    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
-        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootProps)).asTransient();
+    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
+        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootState)).asTransient();
         const tree = this._tree;
         const root = tree.root;
 
         (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, {
+            parent: this,
             transform: root,
             sourceRef: void 0,
             obj: rootObject,
             status: 'ok',
+            state: { ...root.state },
             errorText: void 0,
             params: {
                 definition: {},
@@ -245,7 +243,7 @@ namespace State {
         doNotUpdateCurrent: boolean
     }
 
-    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
+    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
         return new State(rootObject, params);
     }
 }
@@ -271,7 +269,6 @@ interface UpdateContext {
     spine: StateTreeSpine.Impl,
 
     results: UpdateNodeResult[],
-    stateChanges: StateTransform.Ref[],
 
     // suppress timing messages
     options: State.UpdateOptions,
@@ -319,12 +316,6 @@ async function update(ctx: UpdateContext) {
         roots = findUpdateRoots(ctx.cells, ctx.tree);
     }
 
-    let newCellStates: StateTree.CellStates;
-    if (!ctx.editInfo) {
-        newCellStates = ctx.tree.cellStatesSnapshot();
-        syncOldStates(ctx);
-    }
-
     // Init empty cells where not present
     // this is done in "pre order", meaning that "parents" will be created 1st.
     const addedCells = initCells(ctx, roots);
@@ -353,7 +344,7 @@ async function update(ctx: UpdateContext) {
 
     // Sync cell states
     if (!ctx.editInfo) {
-        syncNewStates(ctx, newCellStates!);
+        syncNewStates(ctx);
     }
 
     let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent;
@@ -363,7 +354,7 @@ async function update(ctx: UpdateContext) {
             ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! });
             if (!ctx.newCurrent) {
                 const transform = ctx.tree.transforms.get(update.ref);
-                if (!(transform.props && transform.props.isGhost) && update.obj !== StateObject.Null) newCurrent = update.ref;
+                if (!transform.state.isGhost && update.obj !== StateObject.Null) newCurrent = update.ref;
             }
         } else if (update.action === 'updated') {
             ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj });
@@ -415,25 +406,14 @@ function findDeletes(ctx: UpdateContext): Ref[] {
     return deleteCtx.deletes;
 }
 
-function syncOldStatesVisitor(n: StateTransform, tree: StateTree, oldState: StateTree.CellStates) {
-    if (oldState.has(n.ref)) {
-        (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
-    }
-}
-function syncOldStates(ctx: UpdateContext) {
-    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncOldStatesVisitor);
+function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: UpdateContext) {
+    const cell = ctx.cells.get(n.ref);
+    if (!cell || !StateTransform.syncState(cell.state, n.state)) return;
+    ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref: n.ref, cell });
 }
 
-function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: { newState: StateTree.CellStates, changes: StateTransform.Ref[] }) {
-    if (ctx.newState.has(n.ref)) {
-        const changed = (tree as TransientTree).updateCellState(n.ref, ctx.newState.get(n.ref));
-        if (changed) {
-            ctx.changes.push(n.ref);
-        }
-    }
-}
-function syncNewStates(ctx: UpdateContext, newState: StateTree.CellStates) {
-    StateTree.doPreOrder(ctx.tree, ctx.tree.root, { newState, changes: ctx.stateChanges }, syncNewStatesVisitor);
+function syncNewStates(ctx: UpdateContext) {
+    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx, syncNewStatesVisitor);
 }
 
 function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
@@ -441,7 +421,7 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta
     const changed = cell.status !== status;
     cell.status = status;
     cell.errorText = errorText;
-    if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) });
+    if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cell });
 }
 
 function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) {
@@ -462,9 +442,11 @@ function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: Ini
     }
 
     const cell: StateObjectCell = {
+        parent: ctx.parent,
         transform,
         sourceRef: void 0,
         status: 'pending',
+        state: { ...transform.state },
         errorText: void 0,
         params: void 0,
         cache: void 0
@@ -505,7 +487,7 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Ma
         }
 
         const t = tree.transforms.get(s.value);
-        if (t.props && t.props.isGhost) continue;
+        if (t.state.isGhost) continue;
         if (s.value === ref) {
             seenRef = true;
             if (!deletes.has(ref)) prevCandidate = ref;
@@ -671,7 +653,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
 
 function updateTag(obj: StateObject | undefined, transform: StateTransform) {
     if (!obj || obj === StateObject.Null) return;
-    (obj.tag as string | undefined) = transform.props.tag;
+    (obj.tags as string[] | undefined) = transform.tags;
 }
 
 function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
diff --git a/src/mol-state/state/builder.ts b/src/mol-state/state/builder.ts
index 6e9e48a10a680457d27f4efa86e602a0450f032f..ec9fd4d9217a875edaeb6fc9c2cfab05a998152e 100644
--- a/src/mol-state/state/builder.ts
+++ b/src/mol-state/state/builder.ts
@@ -36,7 +36,7 @@ namespace StateBuilder {
         | { kind: 'add', transform: StateTransform }
         | { kind: 'update', ref: string, params: any }
         | { kind: 'delete', ref: string }
-        | { kind: 'insert', ref: string, transform: StateTransform, initialCellState?: Partial<StateObjectCell.State> }
+        | { kind: 'insert', ref: string, transform: StateTransform }
 
     function buildTree(state: BuildState) {
         if (!state.state || state.state.tree === state.editInfo.sourceTree) {
@@ -52,7 +52,7 @@ namespace StateBuilder {
                 case 'delete': tree.remove(a.ref); break;
                 case 'insert': {
                     const children = tree.children.get(a.ref).toArray();
-                    tree.add(a.transform, a.initialCellState);
+                    tree.add(a.transform);
                     for (const c of children) {
                         tree.changeParent(c, a.transform.ref);
                     }
@@ -84,12 +84,13 @@ namespace StateBuilder {
         }
         toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
         delete(ref: StateTransform.Ref) {
+            if (!this.state.tree.transforms.has(ref)) return this;
             this.editInfo.count++;
             this.state.tree.remove(ref);
             this.state.actions.push({ kind: 'delete', ref });
             return this;
         }
-        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+        getTree(): StateTree { return buildTree(this.state); }
         constructor(tree: StateTree, state?: State) { this.state = { state, tree: tree.asTransient(), actions: [], editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
     }
 
@@ -102,9 +103,9 @@ namespace StateBuilder {
          * Apply the transformed to the parent node
          * If no params are specified (params <- undefined), default params are lazily resolved.
          */
-        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
             const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
+            this.state.tree.add(t);
             this.editInfo.count++;
             this.editInfo.lastUpdate = t.ref;
 
@@ -113,23 +114,36 @@ namespace StateBuilder {
             return new To(this.state, t.ref, this.root);
         }
 
+        /**
+         * If the ref is present, the transform is applied.
+         * Otherwise a transform with the specifed ref is created.
+         */
+        applyOrUpdate<T extends StateTransformer<A, any, any>>(ref: StateTransform.Ref, tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
+            if (this.state.tree.transforms.has(ref)) {
+                this.to(ref).update(params);
+                return this.to(ref) as To<StateTransformer.To<T>>;
+            } else {
+                return this.apply(tr, params, { ...options, ref });
+            }
+        }
+
         /**
          * A helper to greate a group-like state object and keep the current type.
          */
-        group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<A> {
-            return this.apply(tr, params, options, initialCellState) as To<A>;
+        group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<A> {
+            return this.apply(tr, params, options) as To<A>;
         }
 
         /**
          * Inserts a new transform that does not change the object type and move the original children to it.
          */
-        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> {
             // cache the children
             const children = this.state.tree.children.get(this.ref).toArray();
 
             // add the new node
             const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
+            this.state.tree.add(t);
 
             // move the original children to the new node
             for (const c of children) {
@@ -139,7 +153,7 @@ namespace StateBuilder {
             this.editInfo.count++;
             this.editInfo.lastUpdate = t.ref;
 
-            this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t, initialCellState });
+            this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t });
 
             return new To(this.state, t.ref, this.root);
         }
@@ -193,7 +207,7 @@ namespace StateBuilder {
         toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
         delete(ref: StateTransform.Ref) { return this.root.delete(ref); }
 
-        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+        getTree(): StateTree { return buildTree(this.state); }
 
         constructor(private state: BuildState, ref: StateTransform.Ref, private root: Root) {
             this.ref = ref;
diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts
index 12a31907342c71fbfcdb7dc27ac750e255034e7f..2a47745f5c84e51b9ec4116a91a10b88e6033461 100644
--- a/src/mol-state/state/selection.ts
+++ b/src/mol-state/state/selection.ts
@@ -49,6 +49,7 @@ namespace StateSelection {
         parent(): Builder<C>;
         first(): Builder<C>;
         filter(p: (n: C) => boolean): Builder<C>;
+        withTag(tag: string): Builder<C>;
         withTransformer<T extends StateTransformer<any, StateObjectCell.Obj<C>, any>>(t: T): Builder<StateObjectCell<StateObjectCell.Obj<C>, StateTransform<T>>>;
         withStatus(s: StateObjectCell.Status): Builder<C>;
         subtree(): Builder;
@@ -200,6 +201,9 @@ namespace StateSelection {
     registerModifier('withStatus', withStatus);
     export function withStatus(b: Selector, s: StateObjectCell.Status) { return filter(b, n => n.status === s); }
 
+    registerModifier('withTag', withTag);
+    export function withTag(b: Selector, tag: string) { return filter(b, n => !!n.transform.tags && n.transform.tags.indexOf(tag) >= 0); }
+
     registerModifier('subtree', subtree);
     export function subtree(b: Selector) {
         return flatMap(b, (n, s) => {
@@ -268,8 +272,12 @@ namespace StateSelection {
     }
 
     function _findUniqueTagsInSubtree(n: StateTransform, _: any, s: { refs: { [name: string]: StateTransform.Ref }, tags: Set<string> }) {
-        if (n.props.tag && s.tags.has(n.props.tag)) {
-            s.refs[n.props.tag] = n.ref;
+        if (n.tags) {
+            for (const t of n.tags) {
+                if (!s.tags.has(t)) continue;
+                s.refs[t] = n.ref;
+                break;
+            }
         }
         return true;
     }
@@ -279,7 +287,7 @@ namespace StateSelection {
     }
 
     function _findTagInSubtree(n: StateTransform, _: any, s: { ref: string | undefined, tag: string }) {
-        if (n.props.tag === s.tag) {
+        if (n.tags && n.tags.indexOf(s.tag) >= 0) {
             s.ref = n.ref;
             return false;
         }
diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts
index 9703a2f53254c6dbfbcf2e5449b8a0775679cc05..fe4b251bf76b60ec6e6cea23d6b903f99b6586cf 100644
--- a/src/mol-state/transform.ts
+++ b/src/mol-state/transform.ts
@@ -12,7 +12,8 @@ export { Transform as StateTransform }
 interface Transform<T extends StateTransformer = StateTransformer> {
     readonly parent: Transform.Ref,
     readonly transformer: T,
-    readonly props: Transform.Props,
+    readonly state: Transform.State,
+    readonly tags?: string[],
     readonly ref: Transform.Ref,
     readonly params?: StateTransformer.Params<T>,
     readonly version: string
@@ -24,24 +25,80 @@ namespace Transform {
 
     export const RootRef = '-=root=-' as Ref;
 
-    export interface Props {
-        tag?: string
+    export interface State {
+        // is the cell shown in the UI
         isGhost?: boolean,
-        // determine if the corresponding cell can be deleted by the user.
-        isLocked?: boolean
+        // can the corresponding be deleted by the user.
+        isLocked?: boolean,
+        // is the representation associated with the cell hidden
+        isHidden?: boolean,
+        // is the tree node collapsed?
+        isCollapsed?: boolean
+    }
+
+    export function areStatesEqual(a: State, b: State) {
+        return !!a.isHidden !== !!b.isHidden || !!a.isCollapsed !== !!b.isCollapsed
+            || !!a.isGhost !== !!b.isGhost || !!a.isLocked !== !!b.isLocked;
+    }
+
+    export function isStateChange(a: State, b?: Partial<State>) {
+        if (!b) return false;
+        if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true;
+        if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true;
+        if (typeof b.isGhost !== 'undefined' && a.isGhost !== b.isGhost) return true;
+        if (typeof b.isLocked !== 'undefined' && a.isLocked !== b.isLocked) return true;
+        return false;
+    }
+
+    export function assignState(a: State, b?: Partial<State>): boolean {
+        if (!b) return false;
+
+        let changed = false;
+        for (const k of Object.keys(b)) {
+            const s = (b as any)[k], t = (a as any)[k];
+            if (!!s === !!t) continue;
+            changed = true;
+            (a as any)[k] = s;
+        }
+        return changed;
+    }
+
+    export function syncState(a: State, b?: Partial<State>): boolean {
+        if (!b) return false;
+
+        let changed = false;
+        for (const k of Object.keys(b)) {
+            const s = (b as any)[k], t = (a as any)[k];
+            if (!!s === !!t) continue;
+            changed = true;
+            (a as any)[k] = s;
+        }
+        for (const k of Object.keys(a)) {
+            const s = (b as any)[k], t = (a as any)[k];
+            if (!!s === !!t) continue;
+            changed = true;
+            (a as any)[k] = s;
+        }
+        return changed;
     }
 
     export interface Options {
         ref?: string,
-        props?: Props
+        tags?: string | string[],
+        state?: State
     }
 
     export function create<T extends StateTransformer>(parent: Ref, transformer: T, params?: StateTransformer.Params<T>, options?: Options): Transform<T> {
         const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref;
+        let tags: string[] | undefined = void 0;
+        if (options && options.tags) {
+            tags = typeof options.tags === 'string' ? [options.tags] : options.tags;
+        }
         return {
             parent,
             transformer,
-            props: (options && options.props) || { },
+            state: (options && options.state) || { },
+            tags,
             ref,
             params,
             version: UUID.create22()
@@ -52,23 +109,30 @@ namespace Transform {
         return { ...t, params, version: UUID.create22() };
     }
 
+    export function withState(t: Transform, state?: Partial<State>): Transform {
+        if (!state) return t;
+        return { ...t, state: { ...t.state, ...state } };
+    }
+
     export function withParent(t: Transform, parent: Ref): Transform {
         return { ...t, parent, version: UUID.create22() };
     }
 
-    export function withNewVersion(t: Transform): Transform {
-        return { ...t, version: UUID.create22() };
+    export function createRoot(state?: State): Transform {
+        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, state });
     }
 
-    export function createRoot(props?: Props): Transform {
-        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, props });
+    export function hasTag(t: Transform, tag: string) {
+        if (!t.tags) return false;
+        return t.tags.indexOf(tag) >= 0;
     }
 
     export interface Serialized {
         parent: string,
         transformer: string,
         params: any,
-        props: Props,
+        state?: State,
+        tags?: string[],
         ref: string,
         version: string
     }
@@ -78,11 +142,19 @@ namespace Transform {
         const pToJson = t.transformer.definition.customSerialization
             ? t.transformer.definition.customSerialization.toJSON
             : _id;
+        let state: any = void 0;
+        for (const k of Object.keys(t.state)) {
+            const s = (t.state as any)[k];
+            if (!s) continue;
+            if (!state) state = { };
+            state[k] = true;
+        }
         return {
             parent: t.parent,
             transformer: t.transformer.id,
             params: t.params ? pToJson(t.params) : void 0,
-            props: t.props,
+            state,
+            tags: t.tags,
             ref: t.ref,
             version: t.version
         };
@@ -97,7 +169,8 @@ namespace Transform {
             parent: t.parent as Ref,
             transformer,
             params: t.params ? pFromJson(t.params) : void 0,
-            props: t.props,
+            state: t.state || { },
+            tags: t.tags,
             ref: t.ref as Ref,
             version: t.version
         };
diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts
index 01759e7382efe88f25f3311f1509300c06290ced..4e60189b745c7bbd21e844f0a28f230c736cd1cc 100644
--- a/src/mol-state/transformer.ts
+++ b/src/mol-state/transformer.ts
@@ -86,6 +86,9 @@ namespace Transformer {
         /** By default, returns true */
         isSerializable?(params: P): { isSerializable: true } | { isSerializable: false; reason: string },
 
+        /** Parameter interpolation */
+        interpolate?(src: P, target: P, t: number, globalCtx: unknown): P
+
         /** Custom conversion to and from JSON */
         readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P }
     }
@@ -95,7 +98,7 @@ namespace Transformer {
         readonly from: StateObject.Ctor[],
         readonly to: StateObject.Ctor[],
         readonly display: { readonly name: string, readonly description?: string },
-        params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any },
+        params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any }
     }
 
     const registry = new Map<Id, Transformer<any, any>>();
diff --git a/src/mol-state/tree/immutable.ts b/src/mol-state/tree/immutable.ts
index ab3d8f60301414dc4deb67d41348a393eea6e2f5..fba1811e7e3108decbb0b36023e791ec8de11a51 100644
--- a/src/mol-state/tree/immutable.ts
+++ b/src/mol-state/tree/immutable.ts
@@ -7,7 +7,6 @@
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
 import { StateTransform } from '../transform';
 import { TransientTree } from './transient';
-import { StateObjectCell } from 'mol-state/object';
 
 export { StateTree }
 
@@ -19,7 +18,6 @@ interface StateTree {
     readonly root: StateTransform,
     readonly transforms: StateTree.Transforms,
     readonly children: StateTree.Children,
-    readonly cellStates: StateTree.CellStates,
 
     asTransient(): TransientTree
 }
@@ -43,7 +41,6 @@ namespace StateTree {
 
     export interface Transforms extends _Map<StateTransform> {}
     export interface Children extends _Map<ChildSet> { }
-    export interface CellStates extends _Map<StateObjectCell.State> { }
 
     class Impl implements StateTree {
         get root() { return this.transforms.get(StateTransform.RootRef)! }
@@ -52,7 +49,7 @@ namespace StateTree {
             return new TransientTree(this);
         }
 
-        constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) {
+        constructor(public transforms: StateTree.Transforms, public children: Children) {
         }
     }
 
@@ -61,11 +58,11 @@ namespace StateTree {
      */
     export function createEmpty(customRoot?: StateTransform): StateTree {
         const root = customRoot || StateTransform.createRoot();
-        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap([[root.ref, StateObjectCell.DefaultState]]));
+        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]));
     }
 
-    export function create(nodes: Transforms, children: Children, cellStates: CellStates): StateTree {
-        return new Impl(nodes, children, cellStates);
+    export function create(nodes: Transforms, children: Children): StateTree {
+        return new Impl(nodes, children);
     }
 
     type VisitorCtx = { tree: StateTree, state: any, f: (node: StateTransform, tree: StateTree, state: any) => boolean | undefined | void };
@@ -116,19 +113,19 @@ namespace StateTree {
         return doPostOrder<StateTransform[]>(tree, root, [], _subtree);
     }
 
-    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: [StateTransform.Serialized, StateObjectCell.State][]) {
+    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: StateTransform.Serialized[]) {
         // const children: Ref[] = [];
         // tree.children.get(node.ref).forEach(_visitChildToJson as any, children);
-        ctx.push([StateTransform.toJSON(node), tree.cellStates.get(node.ref)]);
+        ctx.push(StateTransform.toJSON(node));
     }
 
     export interface Serialized {
         /** Transforms serialized in pre-order */
-        transforms: [StateTransform.Serialized, StateObjectCell.State][]
+        transforms: StateTransform.Serialized[]
     }
 
     export function toJSON(tree: StateTree): Serialized {
-        const transforms: [StateTransform.Serialized, StateObjectCell.State][] = [];
+        const transforms: StateTransform.Serialized[] = [];
         doPreOrder(tree, tree.root, transforms, _visitNodeToJson);
         return { transforms };
     }
@@ -136,12 +133,10 @@ namespace StateTree {
     export function fromJSON(data: Serialized): StateTree {
         const nodes = ImmutableMap<Ref, StateTransform>().asMutable();
         const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable();
-        const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable();
 
         for (const t of data.transforms) {
-            const transform = StateTransform.fromJSON(t[0]);
+            const transform = StateTransform.fromJSON(t);
             nodes.set(transform.ref, transform);
-            cellStates.set(transform.ref, t[1]);
 
             if (!children.has(transform.ref)) {
                 children.set(transform.ref, OrderedSet<Ref>().asMutable());
@@ -151,19 +146,18 @@ namespace StateTree {
         }
 
         for (const t of data.transforms) {
-            const ref = t[0].ref;
+            const ref = t.ref;
             children.set(ref, children.get(ref).asImmutable());
         }
 
-        return create(nodes.asImmutable(), children.asImmutable(), cellStates.asImmutable());
+        return create(nodes.asImmutable(), children.asImmutable());
     }
 
     export function dump(tree: StateTree) {
         console.log({
             tr: (tree.transforms as ImmutableMap<any, any>).keySeq().toArray(),
             tr1: (tree.transforms as ImmutableMap<any, any>).valueSeq().toArray().map(t => t.ref),
-            ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray(),
-            cs: (tree.cellStates as ImmutableMap<any, any>).keySeq().toArray()
+            ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray()
         });
     }
 }
\ No newline at end of file
diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts
index a48e8e6635778e3037298890dedf69f5f892f1b8..c9a5e8d8402a08a0c2115b2caca26af4bb424105 100644
--- a/src/mol-state/tree/transient.ts
+++ b/src/mol-state/tree/transient.ts
@@ -7,7 +7,6 @@
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
 import { StateTransform } from '../transform';
 import { StateTree } from './immutable';
-import { StateObjectCell } from 'mol-state/object';
 import { shallowEqual } from 'mol-util/object';
 
 export { TransientTree }
@@ -15,13 +14,12 @@ export { TransientTree }
 class TransientTree implements StateTree {
     transforms = this.tree.transforms as ImmutableMap<StateTransform.Ref, StateTransform>;
     children = this.tree.children as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>;
-    cellStates = this.tree.cellStates as ImmutableMap<StateTransform.Ref, StateObjectCell.State>;
 
     private changedNodes = false;
     private changedChildren = false;
-    private changedStates = false;
 
     private _childMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0;
+    private _stateUpdates: Set<StateTransform.Ref> | undefined = void 0;
 
     private get childMutations() {
         if (this._childMutations) return this._childMutations;
@@ -29,12 +27,6 @@ class TransientTree implements StateTree {
         return this._childMutations;
     }
 
-    private changeStates() {
-        if (this.changedStates) return;
-        this.changedStates = true;
-        this.cellStates = this.cellStates.asMutable();
-    }
-
     private changeNodes() {
         if (this.changedNodes) return;
         this.changedNodes = true;
@@ -49,10 +41,6 @@ class TransientTree implements StateTree {
 
     get root() { return this.transforms.get(StateTransform.RootRef)! }
 
-    cellStatesSnapshot() {
-        return this.cellStates.asImmutable();
-    }
-
     asTransient() {
         return this.asImmutable().asTransient();
     }
@@ -104,15 +92,7 @@ class TransientTree implements StateTree {
         this.transforms.set(ref, StateTransform.withParent(old, newParent));
     }
 
-    updateVersion(ref: StateTransform.Ref) {
-        ensurePresent(this.transforms, ref);
-
-        const t = this.transforms.get(ref);
-        this.changeNodes();
-        this.transforms.set(ref, StateTransform.withNewVersion(t));
-    }
-
-    add(transform: StateTransform, initialState?: Partial<StateObjectCell.State>) {
+    add(transform: StateTransform) {
         const ref = transform.ref;
 
         if (this.transforms.has(transform.ref)) {
@@ -138,15 +118,6 @@ class TransientTree implements StateTree {
         this.changeNodes();
         this.transforms.set(ref, transform);
 
-        if (!this.cellStates.has(ref)) {
-            this.changeStates();
-            if (StateObjectCell.isStateChange(StateObjectCell.DefaultState, initialState)) {
-                this.cellStates.set(ref, { ...StateObjectCell.DefaultState, ...initialState });
-            } else {
-                this.cellStates.set(ref, StateObjectCell.DefaultState);
-            }
-        }
-
         return this;
     }
 
@@ -169,16 +140,21 @@ class TransientTree implements StateTree {
         return true;
     }
 
-    updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) {
+    assignState(ref: StateTransform.Ref, state?: Partial<StateTransform.State>) {
         ensurePresent(this.transforms, ref);
 
-        const old = this.cellStates.get(ref);
-        if (!StateObjectCell.isStateChange(old, state)) return false;
-
-        this.changeStates();
-        this.cellStates.set(ref, { ...old, ...state });
-
-        return true;
+        const old = this.transforms.get(ref);
+        if (this._stateUpdates && this._stateUpdates.has(ref)) {
+            StateTransform.assignState(old.state, state);
+            return old;
+        } else {
+            if (!this._stateUpdates) this._stateUpdates = new Set();
+            this._stateUpdates.add(old.ref);
+            this.changeNodes();
+            const updated = StateTransform.withState(old, state);
+            this.transforms.set(ref, updated);
+            return updated;
+        }
     }
 
     remove(ref: StateTransform.Ref): StateTransform[] {
@@ -197,12 +173,10 @@ class TransientTree implements StateTree {
 
         this.changeNodes();
         this.changeChildren();
-        this.changeStates();
 
         for (const n of st) {
             this.transforms.delete(n.ref);
             this.children.delete(n.ref);
-            this.cellStates.delete(n.ref);
             if (this._childMutations) this._childMutations.delete(n.ref);
         }
 
@@ -210,12 +184,11 @@ class TransientTree implements StateTree {
     }
 
     asImmutable() {
-        if (!this.changedNodes && !this.changedChildren && !this.changedStates && !this._childMutations) return this.tree;
+        if (!this.changedNodes && !this.changedChildren && !this._childMutations) return this.tree;
         if (this._childMutations) this._childMutations.forEach(fixChildMutations, this.children);
         return StateTree.create(
             this.changedNodes ? this.transforms.asImmutable() : this.transforms,
-            this.changedChildren ? this.children.asImmutable() : this.children,
-            this.changedStates ? this.cellStates.asImmutable() : this.cellStates);
+            this.changedChildren ? this.children.asImmutable() : this.children);
     }
 
     constructor(private tree: StateTree) {
diff --git a/src/mol-task/util/user-timing.ts b/src/mol-task/util/user-timing.ts
index a4e3b7e33808f2dad86b1c59c929a85dda08a550..54456d65fa3b1cecce102d16b8ac492f297d15b7 100644
--- a/src/mol-task/util/user-timing.ts
+++ b/src/mol-task/util/user-timing.ts
@@ -5,14 +5,10 @@
  */
 
 import { Task } from '../task'
+import { isProductionMode } from 'mol-util/debug';
 
 const hasPerformance = typeof performance !== 'undefined'
-/**
- * on node `process.env.NODE_ENV` is available, in webpack build it is automatically set
- * by the DefinePlugin to the webpack `mode` value
- */
-const productionMode = process.env.NODE_ENV === 'production'
-const timingEnabled = hasPerformance && !productionMode
+const timingEnabled = hasPerformance && !isProductionMode
 
 export namespace UserTiming {
     function startMarkName(task: Task<any>) { return `startTask${task.id}` }
diff --git a/src/mol-theme/color/secondary-structure.ts b/src/mol-theme/color/secondary-structure.ts
index 384c5fb0b7d3c37031ef670b0d17a9e7b9f79ade..cb8878f28b77889d830573b5609a3b1c32feed6e 100644
--- a/src/mol-theme/color/secondary-structure.ts
+++ b/src/mol-theme/color/secondary-structure.ts
@@ -22,6 +22,8 @@ const SecondaryStructureColors = ColorMap({
     'betaTurn': 0x6080FF,
     'betaStrand': 0xFFC800,
     'coil': 0xFFFFFF,
+    'bend': 0x66D8C9 /* biting original color used 0x00FF00 */,
+    'turn': 0x00B266,
 
     'dna': 0xAE00FE,
     'rna': 0xFD0162,
@@ -53,8 +55,10 @@ export function secondaryStructureColor(unit: Unit, element: ElementIndex): Colo
         return SecondaryStructureColors.alphaHelix
     } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Beta)) {
         return SecondaryStructureColors.betaStrand
+    } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Bend)) {
+        return SecondaryStructureColors.bend
     } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Turn)) {
-        return SecondaryStructureColors.coil
+        return SecondaryStructureColors.turn
     } else {
         const moleculeType = getElementMoleculeType(unit, element)
         if (moleculeType === MoleculeType.DNA) {
diff --git a/src/mol-theme/color/shape-group.ts b/src/mol-theme/color/shape-group.ts
index b352ecd50e0581296b9cd59a3ac9669214a670ac..905cce02c84062540b7465a00d98868e5ffb946a 100644
--- a/src/mol-theme/color/shape-group.ts
+++ b/src/mol-theme/color/shape-group.ts
@@ -7,7 +7,7 @@
 import { ColorTheme } from '../color';
 import { Color } from 'mol-util/color';
 import { Location } from 'mol-model/location';
-import { Shape } from 'mol-model/shape';
+import { ShapeGroup } from 'mol-model/shape';
 import { ParamDefinition as PD } from 'mol-util/param-definition'
 import { ThemeDataContext } from 'mol-theme/theme';
 
@@ -25,7 +25,7 @@ export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: PD.Values<Sha
         factory: ShapeGroupColorTheme,
         granularity: 'groupInstance',
         color: (location: Location): Color => {
-            if (Shape.isLocation(location)) {
+            if (ShapeGroup.isLocation(location)) {
                 return location.shape.getColor(location.group, location.instance)
             }
             return DefaultColor
diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts
index 8aac229a59d8db1ca68eeab84eb03b921ec55f0c..69b67a6b9b4559b4e12c9e1bc5bd8c52313d6bb8 100644
--- a/src/mol-theme/label.ts
+++ b/src/mol-theme/label.ts
@@ -33,6 +33,8 @@ export function labelFirst(loci: Loci): string {
         case 'link-loci':
             const link = loci.links[0]
             return link ? linkLabel(link) : 'Unknown'
+        case 'shape-loci':
+            return loci.shape.name
         case 'group-loci':
             const g = loci.groups[0]
             if (g) {
diff --git a/src/mol-theme/size/shape-group.ts b/src/mol-theme/size/shape-group.ts
index 60fa329f3aacd2ee175aedbc8122e9d6398dc505..8bfcdf4f715519e1e91b88c2914795671857c955 100644
--- a/src/mol-theme/size/shape-group.ts
+++ b/src/mol-theme/size/shape-group.ts
@@ -5,7 +5,7 @@
  */
 
 import { Location } from 'mol-model/location';
-import { Shape } from 'mol-model/shape';
+import { ShapeGroup } from 'mol-model/shape';
 import { ParamDefinition as PD } from 'mol-util/param-definition'
 import { ThemeDataContext } from 'mol-theme/theme';
 import { SizeTheme } from 'mol-theme/size';
@@ -24,7 +24,7 @@ export function ShapeGroupSizeTheme(ctx: ThemeDataContext, props: PD.Values<Shap
         factory: ShapeGroupSizeTheme,
         granularity: 'groupInstance',
         size: (location: Location): number => {
-            if (Shape.isLocation(location)) {
+            if (ShapeGroup.isLocation(location)) {
                 return location.shape.getSize(location.group, location.instance)
             }
             return DefaultSize
diff --git a/src/mol-theme/transparency.ts b/src/mol-theme/transparency.ts
new file mode 100644
index 0000000000000000000000000000000000000000..404a55b220aa6992dc2282fb9dfd16b408476a1a
--- /dev/null
+++ b/src/mol-theme/transparency.ts
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci, EmptyLoci } from 'mol-model/loci';
+
+export { Transparency }
+
+interface Transparency {
+    readonly loci: Loci
+    readonly value: number
+    readonly variant: Transparency.Variant
+}
+
+namespace Transparency {
+    export type Variant = 'single' | 'multi'
+    export const Empty: Transparency = { loci: EmptyLoci, value: 0, variant: 'single' }
+
+    export function areEqual(tA: Transparency, tB: Transparency) {
+        if (tA.value !== tB.value) return false
+        if (tA.variant !== tB.variant) return false
+        if (!Loci.areEqual(tA.loci, tB.loci)) return false
+        return true
+    }
+}
\ No newline at end of file
diff --git a/src/mol-util/_spec/reference-cache.spec.ts b/src/mol-util/_spec/reference-cache.spec.ts
index 544b642ecfb686d638c8f718f890f5271a552007..0dc3bf4b03d870ca162a5773a12e5ce438efdbd5 100644
--- a/src/mol-util/_spec/reference-cache.spec.ts
+++ b/src/mol-util/_spec/reference-cache.spec.ts
@@ -10,20 +10,19 @@ describe('reference-cache', () => {
     it('basic', () => {
         const refCache = createReferenceCache(
             (x: number) => x.toString(),
-            (ctx: {}, x) => x,
+            (x) => x,
             () => {}
         )
         expect(refCache.count).toBe(0)
 
-        const ctx = {}
-        const ref2a = refCache.get(ctx, 2)
+        const ref2a = refCache.get(2)
         expect(refCache.count).toBe(1)
 
-        const ref2b = refCache.get(ctx, 2)
+        const ref2b = refCache.get(2)
         expect(refCache.count).toBe(1)
         expect(ref2b.value).toBe(2)
 
-        const ref3 = refCache.get(ctx, 3)
+        const ref3 = refCache.get(3)
         expect(refCache.count).toBe(2)
         expect(ref3.value).toBe(3)
 
diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts
index 078ab5319b4db6bdd612c7655390a8e4a0e2225f..fae588111095541e1614aca6a6b40b0010602c7a 100644
--- a/src/mol-util/array.ts
+++ b/src/mol-util/array.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,7 +9,7 @@ import { NumberArray } from './type-helpers';
 // TODO move to mol-math as Vector???
 
 /** Get the maximum value in an array */
-export function arrayMax(array: NumberArray) {
+export function arrayMax(array: ArrayLike<number>) {
     let max = -Infinity
     for (let i = 0, il = array.length; i < il; ++i) {
         if (array[i] > max) max = array[i]
@@ -18,7 +18,7 @@ export function arrayMax(array: NumberArray) {
 }
 
 /** Get the minimum value in an array */
-export function arrayMin(array: NumberArray) {
+export function arrayMin(array: ArrayLike<number>) {
     let min = Infinity
     for (let i = 0, il = array.length; i < il; ++i) {
         if (array[i] < min) min = array[i]
@@ -27,7 +27,7 @@ export function arrayMin(array: NumberArray) {
 }
 
 /** Get the sum of values in an array */
-export function arraySum(array: NumberArray, stride = 1, offset = 0) {
+export function arraySum(array: ArrayLike<number>, stride = 1, offset = 0) {
     const n = array.length
     let sum = 0
     for (let i = offset; i < n; i += stride) {
@@ -37,12 +37,12 @@ export function arraySum(array: NumberArray, stride = 1, offset = 0) {
 }
 
 /** Get the mean of values in an array */
-export function arrayMean(array: NumberArray, stride = 1, offset = 0) {
+export function arrayMean(array: ArrayLike<number>, stride = 1, offset = 0) {
     return arraySum(array, stride, offset) / (array.length / stride)
 }
 
 /** Get the root mean square of values in an array */
-export function arrayRms(array: NumberArray) {
+export function arrayRms(array: ArrayLike<number>) {
     const n = array.length
     let sumSq = 0
     for (let i = 0; i < n; ++i) {
@@ -54,7 +54,13 @@ export function arrayRms(array: NumberArray) {
 
 /** Fill an array with serial numbers starting from 0 until n - 1 (defaults to array.length) */
 export function fillSerial<T extends NumberArray> (array: T, n?: number) {
-    for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i
+    for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[i] = i
+    return array
+}
+
+/** Fill an array with value starting from index 0 until n - 1 (defaults to array.length) */
+export function fillUniform<T extends NumberArray> (array: T, v: number, n?: number) {
+    for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[i] = v
     return array
 }
 
diff --git a/src/mol-util/color/color.ts b/src/mol-util/color/color.ts
index 150417a6820ebfb44ac7fe6ff46884b0c1e926fa..60260c745788673d11c3e6fcd8f588321b542584 100644
--- a/src/mol-util/color/color.ts
+++ b/src/mol-util/color/color.ts
@@ -5,6 +5,7 @@
  */
 
 import { NumberArray } from 'mol-util/type-helpers';
+import { Vec3 } from 'mol-math/linear-algebra';
 
 /** RGB color triplet expressed as a single number */
 export type Color = { readonly '@type': 'color' } & number
@@ -24,11 +25,11 @@ export namespace Color {
         return `RGB: ${Color.toRgb(hexColor).join(', ')}`
     }
 
-    export function toRgb(hexColor: Color) {
+    export function toRgb(hexColor: Color): [number, number, number] {
         return [ hexColor >> 16 & 255, hexColor >> 8 & 255, hexColor & 255 ]
     }
 
-    export function toRgbNormalized(hexColor: Color) {
+    export function toRgbNormalized(hexColor: Color): [number, number, number] {
         return [ (hexColor >> 16 & 255) / 255, (hexColor >> 8 & 255) / 255, (hexColor & 255) / 255 ]
     }
 
@@ -64,6 +65,22 @@ export namespace Color {
         return array
     }
 
+    /** Copies hex color to rgb vec3 */
+    export function toVec3(out: Vec3, hexColor: Color) {
+        out[0] = (hexColor >> 16 & 255)
+        out[1] = (hexColor >> 8 & 255)
+        out[2] = (hexColor & 255)
+        return out
+    }
+
+    /** Copies normalized (0 to 1) hex color to rgb vec3 */
+    export function toVec3Normalized(out: Vec3, hexColor: Color) {
+        out[0] = (hexColor >> 16 & 255) / 255
+        out[1] = (hexColor >> 8 & 255) / 255
+        out[2] = (hexColor & 255) / 255
+        return out
+    }
+
     /** Linear interpolation between two colors */
     export function interpolate(c1: Color, c2: Color, t: number): Color {
         const r1 = c1 >> 16 & 255
diff --git a/src/mol-util/debug.ts b/src/mol-util/debug.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5dc93664ac37fbb9825811dc1bd6e38e421d3d56
--- /dev/null
+++ b/src/mol-util/debug.ts
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/**
+ * on node `process.env.NODE_ENV` is available, in webpack build it is automatically set
+ * by the DefinePlugin to the webpack `mode` value
+ */
+const isProductionMode = process.env.NODE_ENV === 'production'
+
+/**
+ * set to true to enable more comprehensive checks and assertions,
+ * mostly used in `mol-gl`
+ */
+const isDebugMode = process.env.DEBUG === '*' || process.env.DEBUG === 'molstar'
+
+export { isProductionMode, isDebugMode }
\ No newline at end of file
diff --git a/src/mol-util/float-packing.ts b/src/mol-util/float-packing.ts
index 42087b10738d71adb4e1913c187cf5fd460b38cf..e75d31a6bc815612f561069aae7bffbf1c9998fb 100644
--- a/src/mol-util/float-packing.ts
+++ b/src/mol-util/float-packing.ts
@@ -15,7 +15,7 @@ export function encodeFloatLog(value: number) { return Math.log(value + 1.0) / f
 /** decode logarithmically encoded float */
 export function decodeFloatLog(value: number) { return Math.exp(value * floatLogFactor) - 1.0 }
 
-/** encode float as rgb triplet */
+/** encode float as normalized rgb triplet */
 export function encodeFloatRGB(value: number) {
     value = clamp(value, 0.0, 16777216.0 - 1.0) + 1.0
     const b = (value % 256) / 255.0
diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts
index a7f08f3cf1a3d8227703292d4c4078aeb862e499..67c6b8d06879da6e2dbd7dbe67e52ce27bc89e12 100644
--- a/src/mol-util/param-definition.ts
+++ b/src/mol-util/param-definition.ts
@@ -168,7 +168,7 @@ export namespace ParamDefinition {
     }
 
     export interface NamedParams<T = any, K = string> { name: K, params: T }
-    export type NamedParamUnion<P extends Params, K = keyof P> = K extends any ? NamedParams<P[K]['defaultValue'], K> : never
+    export type NamedParamUnion<P extends Params, K extends keyof P = keyof P> = K extends any ? NamedParams<P[K]['defaultValue'], K> : never
     export interface Mapped<T extends NamedParams<any, any>> extends Base<T> {
         type: 'mapped',
         select: Select<string>,
@@ -284,7 +284,7 @@ export namespace ParamDefinition {
         return true;
     }
 
-    function isParamEqual(p: Any, a: any, b: any): boolean {
+    export function isParamEqual(p: Any, a: any, b: any): boolean {
         if (a === b) return true;
         if (!a) return !b;
         if (!b) return !a;
diff --git a/src/mol-util/reference-cache.ts b/src/mol-util/reference-cache.ts
index aa5f341c1349a1e656b5b11ffdcdbbc389604cbd..e383416cf487b5fdfd493418c1f76515984161e7 100644
--- a/src/mol-util/reference-cache.ts
+++ b/src/mol-util/reference-cache.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -24,22 +24,22 @@ export function createReferenceItem<T>(ref: Reference<T>) {
     }
 }
 
-export interface ReferenceCache<T, P, C> {
-    get: (ctx: C, props: P) => ReferenceItem<T>
+export interface ReferenceCache<T, P> {
+    get: (props: P) => ReferenceItem<T>
     clear: () => void
     readonly count: number
     dispose: () => void
 }
 
-export function createReferenceCache<T, P, C>(hashFn: (props: P) => string, ctor: (ctx: C, props: P) => T, deleteFn: (v: T) => void): ReferenceCache<T, P, C> {
+export function createReferenceCache<T, P, C>(hashFn: (props: P) => string, ctor: (props: P) => T, deleteFn: (v: T) => void): ReferenceCache<T, P> {
     const map: Map<string, Reference<T>> = new Map()
 
     return {
-        get: (ctx: C, props: P) => {
+        get: (props: P) => {
             const id = hashFn(props)
             let ref = map.get(id)
             if (!ref) {
-                ref = createReference<T>(ctor(ctx, props))
+                ref = createReference<T>(ctor(props))
                 map.set(id, ref)
             }
             ref.usageCount += 1
diff --git a/src/perf-tests/mol-script.ts b/src/perf-tests/mol-script.ts
index 3895e0f8269a018c9bd9dae8ceba58744bb842ce..f459e3a203a5c48a0df98d490066240b8c9dcab3 100644
--- a/src/perf-tests/mol-script.ts
+++ b/src/perf-tests/mol-script.ts
@@ -1,6 +1,6 @@
 import { MolScriptBuilder } from 'mol-script/language/builder';
 import { compile, QuerySymbolRuntime, DefaultQueryRuntimeTable } from 'mol-script/runtime/query/compiler';
-import { QueryContext, Structure, StructureQuery, ModelPropertyDescriptor } from 'mol-model/structure';
+import { QueryContext, Structure, StructureQuery, CustomPropertyDescriptor } from 'mol-model/structure';
 import { readCifFile, getModelsAndStructure } from '../apps/structure-info/model';
 import { CustomPropSymbol } from 'mol-script/language/symbol';
 import Type from 'mol-script/language/type';
@@ -46,7 +46,7 @@ const compiled = compile<number>(expr);
 const result = compiled(new QueryContext(Structure.Empty));
 console.log(result);
 
-const CustomProp = ModelPropertyDescriptor({
+const CustomProp = CustomPropertyDescriptor({
     name: 'test_prop',
     isStatic: true,
     cifExport: { prefix: '', categories: [ ]},
diff --git a/src/tests/browser/index.html b/src/tests/browser/index.html
index f28af95b2858709af424bf4ff1cbf49396329846..62d647dc8a15e3801ee7119a313c2b86927d1cc7 100644
--- a/src/tests/browser/index.html
+++ b/src/tests/browser/index.html
@@ -1,38 +1,37 @@
 <!DOCTYPE html>
 <html lang="en">
-    <head>
-        <meta charset="utf-8" />
-        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
-        <title>Mol* Browser Test</title>
-        <style>
-            * {
-                margin: 0;
-                padding: 0;
-                box-sizing: border-box;
-            }
-            html, body {
-                width: 100%;
-                height: 100%;
-                overflow: hidden;
-            }
-        </style>
-    </head>
-    <body>
-        <div id="app"></div>
-        <script type="text/javascript">
-            function urlQueryParameter (id) {
-                if (typeof window === 'undefined') return undefined
-                const a = new RegExp(`${id}=([^&#=]*)`)
-                const m = a.exec(window.location.search)
-                return m ? decodeURIComponent(m[1]) : undefined
-            }
-
-            const name = urlQueryParameter('name')
-            if (name) {
-                const script = document.createElement('script')
-                script.src = name + '.js'
-                document.body.appendChild(script)
-            }
-        </script>
-    </body>
+	<head>
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<title>Mol* Browser Test</title>
+		<style>
+			* {
+				margin: 0;
+				padding: 0;
+				box-sizing: border-box;
+			}
+			html, body {
+				width: 100%;
+				height: 100%;
+				overflow: hidden;
+			}
+		</style>
+	</head>
+	<body>
+		<div id="app"></div>
+		<script type="text/javascript">
+			function urlQueryParameter (id) {
+				if (typeof window === 'undefined') return undefined
+				const a = new RegExp(`${id}=([^&#=]*)`)
+				const m = a.exec(window.location.search)
+				return m ? decodeURIComponent(m[1]) : undefined
+			}
+			const name = urlQueryParameter('name')
+			if (name) {
+				const script = document.createElement('script')
+				script.src = name + '.js'
+				document.body.appendChild(script)
+			}
+		</script>
+	</body>
 </html>
\ No newline at end of file
diff --git a/src/tests/browser/marching-cubes.ts b/src/tests/browser/marching-cubes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2aa507cc08ade1e0745f08befbfd7fa8dd1103e9
--- /dev/null
+++ b/src/tests/browser/marching-cubes.ts
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import './index.html'
+import { Canvas3D } from 'mol-canvas3d/canvas3d';
+import { Representation } from 'mol-repr/representation';
+import { Color } from 'mol-util/color';
+import { createRenderObject } from 'mol-gl/render-object';
+import { computeGaussianDensity, computeGaussianDensityTexture2d } from 'mol-math/geometry/gaussian-density';
+import { PositionData, Box3D, Sphere3D } from 'mol-math/geometry';
+import { OrderedSet } from 'mol-data/int';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { ColorNames } from 'mol-util/color/tables';
+import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh';
+import { calcActiveVoxels } from 'mol-gl/compute/marching-cubes/active-voxels';
+import { createHistogramPyramid } from 'mol-gl/compute/histogram-pyramid/reduction';
+import { createIsosurfaceBuffers } from 'mol-gl/compute/marching-cubes/isosurface';
+import { RendererParams } from 'mol-gl/renderer';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+
+const parent = document.getElementById('app')!
+parent.style.width = '100%'
+parent.style.height = '100%'
+
+const canvas = document.createElement('canvas')
+canvas.style.width = '100%'
+canvas.style.height = '100%'
+parent.appendChild(canvas)
+
+const canvas3d = Canvas3D.create(canvas, parent, {
+    renderer: { ...PD.getDefaultValues(RendererParams), backgroundColor: ColorNames.white },
+    cameraMode: 'orthographic'
+})
+canvas3d.animate()
+
+async function init() {
+    const { webgl } = canvas3d
+
+    const position: PositionData = {
+        x: [0, 2],
+        y: [0, 2],
+        z: [0, 2],
+        indices: OrderedSet.ofSortedArray([0, 1]),
+    }
+    const box = Box3D.create(Vec3.create(0, 0, 0), Vec3.create(2, 2, 2))
+    const radius = () => 1.8
+    const props = {
+        resolution: 0.1,
+        radiusOffset: 0,
+        smoothness: 1.5
+    }
+    const isoValue = Math.exp(-props.smoothness)
+
+    if (true) {
+        console.time('gpu gaussian2')
+        const densityTextureData2 = await computeGaussianDensityTexture2d(position, box, radius, props, webgl).run()
+        webgl.waitForGpuCommandsCompleteSync()
+        console.timeEnd('gpu gaussian2')
+
+        console.time('gpu mc2')
+        console.time('gpu mc active2')
+        const activeVoxelsTex2 = calcActiveVoxels(webgl, densityTextureData2.texture, densityTextureData2.gridDim, densityTextureData2.gridTexDim, isoValue, densityTextureData2.gridTexScale)
+        webgl.waitForGpuCommandsCompleteSync()
+        console.timeEnd('gpu mc active2')
+
+        console.time('gpu mc pyramid2')
+        const compacted2 = createHistogramPyramid(webgl, activeVoxelsTex2, densityTextureData2.gridTexScale)
+        webgl.waitForGpuCommandsCompleteSync()
+        console.timeEnd('gpu mc pyramid2')
+
+        console.time('gpu mc vert2')
+        createIsosurfaceBuffers(webgl, activeVoxelsTex2, densityTextureData2.texture, compacted2, densityTextureData2.gridDim, densityTextureData2.gridTexDim, densityTextureData2.transform, isoValue)
+        webgl.waitForGpuCommandsCompleteSync()
+        console.timeEnd('gpu mc vert2')
+        console.timeEnd('gpu mc2')
+    }
+
+    console.time('gpu gaussian')
+    const densityTextureData = await computeGaussianDensityTexture2d(position, box, radius, props, webgl).run()
+    webgl.waitForGpuCommandsCompleteSync()
+    console.timeEnd('gpu gaussian')
+
+    console.time('gpu mc')
+    console.time('gpu mc active')
+    const activeVoxelsTex = calcActiveVoxels(webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoValue, densityTextureData.gridTexScale)
+    webgl.waitForGpuCommandsCompleteSync()
+    console.timeEnd('gpu mc active')
+
+    console.time('gpu mc pyramid')
+    const compacted = createHistogramPyramid(webgl, activeVoxelsTex, densityTextureData.gridTexScale)
+    webgl.waitForGpuCommandsCompleteSync()
+    console.timeEnd('gpu mc pyramid')
+
+    console.time('gpu mc vert')
+    const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoValue)
+    webgl.waitForGpuCommandsCompleteSync()
+    console.timeEnd('gpu mc vert')
+    console.timeEnd('gpu mc')
+
+    console.log({ ...webgl.stats, programCount: webgl.programCache.count, shaderCount: webgl.shaderCache.count })
+
+    const mcBoundingSphere = Sphere3D.fromBox3D(Sphere3D(), densityTextureData.bbox)
+    const mcIsosurface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, mcBoundingSphere)
+    const mcIsoSurfaceProps = { doubleSided: true, flatShaded: true, alpha: 1.0 }
+    const mcIsoSurfaceValues = TextureMesh.Utils.createValuesSimple(mcIsosurface, mcIsoSurfaceProps, Color(0x112299), 1)
+    // console.log('mcIsoSurfaceValues', mcIsoSurfaceValues)
+    const mcIsoSurfaceState = TextureMesh.Utils.createRenderableState(mcIsoSurfaceProps)
+    const mcIsoSurfaceRenderObject = createRenderObject('texture-mesh', mcIsoSurfaceValues, mcIsoSurfaceState, -1)
+    const mcIsoSurfaceRepr = Representation.fromRenderObject('texture-mesh', mcIsoSurfaceRenderObject)
+
+    canvas3d.add(mcIsoSurfaceRepr)
+    canvas3d.resetCamera()
+
+    //
+
+    console.time('cpu gaussian')
+    const densityData = await computeGaussianDensity(position, box, radius, { ...props, useGpu: false }, webgl).run()
+    console.timeEnd('cpu gaussian')
+    console.log({ densityData })
+
+    const params = {
+        isoLevel: isoValue,
+        scalarField: densityData.field,
+        idField: densityData.idField
+    }
+
+    console.time('cpu mc')
+    const surface = await computeMarchingCubesMesh(params).run()
+    console.timeEnd('cpu mc')
+    console.log('surface', surface)
+    Mesh.transformImmediate(surface, densityData.transform)
+    const meshProps = { doubleSided: true, flatShaded: false, alpha: 1.0 }
+    const meshValues = Mesh.Utils.createValuesSimple(surface, meshProps, Color(0x995511), 1)
+    const meshState = Mesh.Utils.createRenderableState(meshProps)
+    const meshRenderObject = createRenderObject('mesh', meshValues, meshState, -1)
+    const meshRepr = Representation.fromRenderObject('mesh', meshRenderObject)
+
+    canvas3d.add(meshRepr)
+    canvas3d.resetCamera()
+}
+
+init()
\ No newline at end of file
diff --git a/src/tests/browser/render-lines.ts b/src/tests/browser/render-lines.ts
index 9eeecd9a090d762b12fb75c563488aad41d40da6..40962cea9511bd2940f28dbf733a856028cd1f81 100644
--- a/src/tests/browser/render-lines.ts
+++ b/src/tests/browser/render-lines.ts
@@ -35,7 +35,7 @@ function linesRepr() {
 
     const values = Lines.Utils.createValuesSimple(lines, {}, Color(0xFF0000), 3)
     const state = Lines.Utils.createRenderableState({})
-    const renderObject = createRenderObject('lines', values, state)
+    const renderObject = createRenderObject('lines', values, state, -1)
     const repr = Representation.fromRenderObject('cage-lines', renderObject)
     return repr
 }
diff --git a/src/tests/browser/render-mesh.ts b/src/tests/browser/render-mesh.ts
index 4c63a2eb90fbef2500adcdcfea766fc5d045e153..13d752b7d66c0c6fd45fa42cba6be670c3c57646 100644
--- a/src/tests/browser/render-mesh.ts
+++ b/src/tests/browser/render-mesh.ts
@@ -29,7 +29,7 @@ canvas3d.animate()
 
 function meshRepr() {
     const builderState = MeshBuilder.createState()
-    
+
     const t = Mat4.identity()
     MeshBuilder.addCage(builderState, t, HexagonalPrismCage(), 0.005, 2)
 
@@ -41,7 +41,7 @@ function meshRepr() {
 
     const values = Mesh.Utils.createValuesSimple(mesh, {}, Color(0xFF0000), 1)
     const state = Mesh.Utils.createRenderableState({})
-    const renderObject = createRenderObject('mesh', values, state)
+    const renderObject = createRenderObject('mesh', values, state, -1)
     const repr = Representation.fromRenderObject('mesh', renderObject)
     return repr
 }
diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts
index 943a50476785bf19aedb3bd7b959cc210ce54c75..adc6c9eae42b7c5b3c54ff6a68db6007d7ca8871 100644
--- a/src/tests/browser/render-shape.ts
+++ b/src/tests/browser/render-shape.ts
@@ -15,6 +15,9 @@ import { ColorNames } from 'mol-util/color/tables';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 import { labelFirst } from 'mol-theme/label';
 import { RuntimeContext, Progress } from 'mol-task';
+import { Representation } from 'mol-repr/representation';
+import { MarkerAction } from 'mol-geo/geometry/marker-data';
+import { EveryLoci } from 'mol-model/loci';
 
 const parent = document.getElementById('app')!
 parent.style.width = '100%'
@@ -34,14 +37,23 @@ info.style.right = '20px'
 info.style.color = 'white'
 parent.appendChild(info)
 
+let prevReprLoci = Representation.Loci.Empty
 const canvas3d = Canvas3D.create(canvas, parent)
 canvas3d.animate()
 canvas3d.input.move.subscribe(async ({x, y}) => {
     const pickingId = await canvas3d.identify(x, y)
     let label = ''
     if (pickingId) {
-        const { loci } = canvas3d.getLoci(pickingId)
-        label = labelFirst(loci)
+        const reprLoci = canvas3d.getLoci(pickingId)
+        label = labelFirst(reprLoci.loci)
+        if (!Representation.Loci.areEqual(prevReprLoci, reprLoci)) {
+            canvas3d.mark(prevReprLoci, MarkerAction.RemoveHighlight)
+            canvas3d.mark(reprLoci, MarkerAction.Highlight)
+            prevReprLoci = reprLoci
+        }
+    } else {
+        canvas3d.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight)
+        prevReprLoci = Representation.Loci.Empty
     }
     info.innerText = label
 })
@@ -56,7 +68,7 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh
     const builderState = MeshBuilder.createState(centers.length * 128, centers.length * 128 / 2, mesh)
     const t = Mat4.identity()
     const v = Vec3.zero()
-    const sphere = Sphere(2)
+    const sphere = Sphere(3)
     builderState.currentGroup = 0
     for (let i = 0, il = centers.length / 3; i < il; ++i) {
         // for production, calls to update should be guarded by `if (ctx.shouldUpdate)`
@@ -69,8 +81,8 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh
 }
 
 const myData = {
-    centers: [0, 0, 0, 0, 3, 0],
-    colors: [ColorNames.tomato, ColorNames.springgreen],
+    centers: [0, 0, 0, 0, 3, 0, 1, 0 , 4],
+    colors: [ColorNames.tomato, ColorNames.springgreen, ColorNames.springgreen],
     labels: ['Sphere 0, Instance A', 'Sphere 1, Instance A', 'Sphere 0, Instance B', 'Sphere 1, Instance B'],
     transforms: [Mat4.identity(), Mat4.fromTranslation(Mat4.zero(), Vec3.create(3, 0, 0))]
 }
@@ -84,8 +96,8 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh
     const { centers, colors, labels, transforms } = data
     const mesh = await getSphereMesh(ctx, centers, shape && shape.geometry)
     const groupCount = centers.length / 3
-    return shape || Shape.create(
-        'test', mesh,
+    return Shape.create(
+        'test', data, mesh,
         (groupId: number) => colors[groupId], // color: per group, same for instances
         () => 1, // size: constant
         (groupId: number, instanceId: number) => labels[instanceId * groupCount + groupId], // label: per group and instance
@@ -96,10 +108,9 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh
 // Init ShapeRepresentation container
 const repr = ShapeRepresentation(getShape, Mesh.Utils)
 
-async function init() {
+export async function init() {
     // Create shape from myData and add to canvas3d
     await repr.createOrUpdate({}, myData).run((p: Progress) => console.log(Progress.format(p)))
-    console.log(repr)
     canvas3d.add(repr)
     canvas3d.resetCamera()
 
diff --git a/src/tests/browser/render-spheres.ts b/src/tests/browser/render-spheres.ts
index d17e32eb5250a57ee4376f759b57c7d22a96af77..c933d27819eb94058201acaee3b175be2f4f1a84 100644
--- a/src/tests/browser/render-spheres.ts
+++ b/src/tests/browser/render-spheres.ts
@@ -33,7 +33,7 @@ function spheresRepr() {
 
     const values = Spheres.Utils.createValuesSimple(spheres, {}, Color(0xFF0000), 1)
     const state = Spheres.Utils.createRenderableState({})
-    const renderObject = createRenderObject('spheres', values, state)
+    const renderObject = createRenderObject('spheres', values, state, -1)
     console.log(renderObject)
     const repr = Representation.fromRenderObject('spheres', renderObject)
     return repr
diff --git a/src/tests/browser/render-structure.ts b/src/tests/browser/render-structure.ts
index 9dae8a78c7380888fce1cf4db1a64259745cba21..290afa43d6b56c3b607aafcfdf98f8a7ac46724d 100644
--- a/src/tests/browser/render-structure.ts
+++ b/src/tests/browser/render-structure.ts
@@ -13,6 +13,9 @@ import { SizeTheme } from 'mol-theme/size';
 import { CartoonRepresentationProvider } from 'mol-repr/structure/representation/cartoon';
 import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 import { computeModelDSSP } from 'mol-model/structure/model/properties/utils/secondary-structure';
+import { MolecularSurfaceRepresentationProvider } from 'mol-repr/structure/representation/molecular-surface';
+import { BallAndStickRepresentationProvider } from 'mol-repr/structure/representation/ball-and-stick';
+import { GaussianSurfaceRepresentationProvider } from 'mol-repr/structure/representation/gaussian-surface';
 
 const parent = document.getElementById('app')!
 parent.style.width = '100%'
@@ -61,24 +64,81 @@ function getCartoonRepr() {
     return CartoonRepresentationProvider.factory(reprCtx, CartoonRepresentationProvider.getParams)
 }
 
+function getBallAndStickRepr() {
+    return BallAndStickRepresentationProvider.factory(reprCtx, BallAndStickRepresentationProvider.getParams)
+}
+
+function getMolecularSurfaceRepr() {
+    return MolecularSurfaceRepresentationProvider.factory(reprCtx, MolecularSurfaceRepresentationProvider.getParams)
+}
+
+function getGaussianSurfaceRepr() {
+    return GaussianSurfaceRepresentationProvider.factory(reprCtx, GaussianSurfaceRepresentationProvider.getParams)
+}
+
 async function init() {
-    const cif = await downloadFromPdb('3j3q')
+    const cif = await downloadFromPdb('1crn')
     const models = await getModels(cif)
     console.time('computeModelDSSP')
     const secondaryStructure = computeModelDSSP(models[0].atomicHierarchy, models[0].atomicConformation)
-    console.timeEnd('computeModelDSSP')
-    ;(models[0].properties as any).secondaryStructure = secondaryStructure
+    console.timeEnd('computeModelDSSP');
+    (models[0].properties as any).secondaryStructure = secondaryStructure
     const structure = await getStructure(models[0])
+
+    const show = {
+        cartoon: false,
+        ballAndStick: true,
+        molecularSurface: true,
+        gaussianSurface: false,
+    }
+
     const cartoonRepr = getCartoonRepr()
+    const ballAndStickRepr = getBallAndStickRepr()
+    const molecularSurfaceRepr = getMolecularSurfaceRepr()
+    const gaussianSurfaceRepr = getGaussianSurfaceRepr()
+
+    if (show.cartoon) {
+        cartoonRepr.setTheme({
+            color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }),
+            size: reprCtx.sizeThemeRegistry.create('uniform', { structure })
+        })
+        await cartoonRepr.createOrUpdate({ ...CartoonRepresentationProvider.defaultValues, quality: 'auto' }, structure).run()
+    }
+
+    if (show.ballAndStick) {
+        ballAndStickRepr.setTheme({
+            color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }),
+            size: reprCtx.sizeThemeRegistry.create('uniform', { structure })
+        })
+        await ballAndStickRepr.createOrUpdate({ ...BallAndStickRepresentationProvider.defaultValues, quality: 'auto' }, structure).run()
+    }
+
+    if (show.molecularSurface) {
+        molecularSurfaceRepr.setTheme({
+            color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }),
+            size: reprCtx.sizeThemeRegistry.create('physical', { structure })
+        })
+        console.time('molecular surface')
+        await molecularSurfaceRepr.createOrUpdate({ ...MolecularSurfaceRepresentationProvider.defaultValues, quality: 'custom', alpha: 0.5, flatShaded: true, doubleSided: true, resolution: 0.3 }, structure).run()
+        console.timeEnd('molecular surface')
+    }
+
+    if (show.gaussianSurface) {
+        gaussianSurfaceRepr.setTheme({
+            color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }),
+            size: reprCtx.sizeThemeRegistry.create('physical', { structure })
+        })
+        console.time('gaussian surface')
+        await gaussianSurfaceRepr.createOrUpdate({ ...GaussianSurfaceRepresentationProvider.defaultValues, quality: 'custom', alpha: 1.0, flatShaded: true, doubleSided: true, resolution: 0.3 }, structure).run()
+        console.timeEnd('gaussian surface')
+    }
 
-    cartoonRepr.setTheme({
-        color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }),
-        size: reprCtx.sizeThemeRegistry.create('uniform', { structure })
-    })
-    await cartoonRepr.createOrUpdate({ ...CartoonRepresentationProvider.defaultValues, quality: 'auto' }, structure).run()
-    
-    canvas3d.add(cartoonRepr)
+    if (show.cartoon) canvas3d.add(cartoonRepr)
+    if (show.ballAndStick) canvas3d.add(ballAndStickRepr)
+    if (show.molecularSurface) canvas3d.add(molecularSurfaceRepr)
+    if (show.gaussianSurface) canvas3d.add(gaussianSurfaceRepr)
     canvas3d.resetCamera()
+    // canvas3d.setProps({ trackball: { ...canvas3d.props.trackball, spin: true } })
 }
 
 init()
\ No newline at end of file
diff --git a/src/tests/browser/render-text.ts b/src/tests/browser/render-text.ts
index 4c453479b34dcd3041d72219f577b1b7e143bbb8..c8f55c2fb9659ecf1883bac6a07b129ac3e148f5 100644
--- a/src/tests/browser/render-text.ts
+++ b/src/tests/browser/render-text.ts
@@ -51,7 +51,7 @@ function textRepr() {
 
     const values = Text.Utils.createValuesSimple(text, props, Color(0xFFDD00), 1)
     const state = Text.Utils.createRenderableState(props)
-    const renderObject = createRenderObject('text', values, state)
+    const renderObject = createRenderObject('text', values, state, -1)
     console.log('text', renderObject, props)
     const repr = Representation.fromRenderObject('text', renderObject)
     return repr
@@ -66,7 +66,7 @@ function spheresRepr() {
 
     const values = Spheres.Utils.createValuesSimple(spheres, {}, Color(0xFF0000), 0.2)
     const state = Spheres.Utils.createRenderableState({})
-    const renderObject = createRenderObject('spheres', values, state)
+    const renderObject = createRenderObject('spheres', values, state, -1)
     console.log('spheres', renderObject)
     const repr = Representation.fromRenderObject('spheres', renderObject)
     return repr
diff --git a/webpack.config.js b/webpack.config.js
index 59557a3d688f91d2d87b2543bdf469f2495ffc0e..28c3be13aab7da7eb0e18b7b6dfeacd9edfa8b97 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -49,6 +49,8 @@ const sharedConfig = {
         }),
         new webpack.DefinePlugin({
             __PLUGIN_VERSION_TIMESTAMP__: webpack.DefinePlugin.runtimeValue(() => `${new Date().valueOf()}`, true),
+            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
+            'process.env.DEBUG': JSON.stringify(process.env.DEBUG)
         }),
         new MiniCssExtractPlugin({ filename: 'app.css' })
     ],
@@ -103,6 +105,7 @@ module.exports = [
 
     createBrowserTest('font-atlas'),
     createBrowserTest('render-asa'),
+    createBrowserTest('marching-cubes'),
     createBrowserTest('render-lines'),
     createBrowserTest('render-mesh'),
     createBrowserTest('render-shape'),