Prechádzať zdrojové kódy

Merge branch 'master' into cylinders

Alexander Rose 4 rokov pred
rodič
commit
0597a1ef24
51 zmenil súbory, kde vykonal 824 pridanie a 565 odobranie
  1. 2 1
      docs/interesting-pdb-entries.md
  2. 2 2
      package-lock.json
  3. 1 1
      package.json
  4. 2 3
      src/extensions/cellpack/color/generate.ts
  5. 129 68
      src/mol-canvas3d/canvas3d.ts
  6. 4 0
      src/mol-canvas3d/passes/draw.ts
  7. 29 13
      src/mol-canvas3d/passes/wboit.ts
  8. 10 0
      src/mol-geo/geometry/lines/lines-builder.ts
  9. 8 7
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  10. 10 15
      src/mol-gl/compute/histogram-pyramid/reduction.ts
  11. 3 3
      src/mol-gl/compute/marching-cubes/active-voxels.ts
  12. 5 1
      src/mol-gl/shader-code.ts
  13. 1 0
      src/mol-gl/shader/chunks/common.glsl.ts
  14. 46 0
      src/mol-gl/shader/chunks/float-to-rgba.glsl.ts
  15. 91 0
      src/mol-gl/shader/chunks/rgba-to-float.glsl.ts
  16. 6 5
      src/mol-gl/shader/direct-volume.frag.ts
  17. 3 3
      src/mol-gl/shader/direct-volume.vert.ts
  18. 24 29
      src/mol-gl/shader/marching-cubes/active-voxels.frag.ts
  19. 34 24
      src/mol-gl/shader/marching-cubes/isosurface.frag.ts
  20. 5 133
      src/mol-gl/shader/util/grid3d-template.frag.ts
  21. 9 5
      src/mol-gl/webgl/context.ts
  22. 2 4
      src/mol-gl/webgl/program.ts
  23. 2 2
      src/mol-gl/webgl/render-item.ts
  24. 22 11
      src/mol-gl/webgl/texture.ts
  25. 19 11
      src/mol-plugin-state/manager/structure/measurement.ts
  26. 2 2
      src/mol-plugin-ui/skin/base/components/controls.scss
  27. 5 0
      src/mol-plugin-ui/skin/base/components/misc.scss
  28. 2 33
      src/mol-plugin-ui/viewport/canvas.tsx
  29. 4 2
      src/mol-plugin/config.ts
  30. 46 13
      src/mol-plugin/context.ts
  31. 1 1
      src/mol-plugin/layout.ts
  32. 30 4
      src/mol-repr/shape/loci/dihedral.ts
  33. 1 1
      src/mol-repr/structure/visual/gaussian-surface-mesh.ts
  34. 2 121
      src/mol-repr/volume/direct-volume.ts
  35. 83 3
      src/mol-repr/volume/isosurface.ts
  36. 134 1
      src/mol-repr/volume/util.ts
  37. 2 2
      src/mol-theme/color/chain-id.ts
  38. 2 2
      src/mol-theme/color/entity-source.ts
  39. 6 4
      src/mol-theme/color/operator-hkl.ts
  40. 2 2
      src/mol-theme/color/operator-name.ts
  41. 2 2
      src/mol-theme/color/polymer-id.ts
  42. 12 12
      src/mol-util/color/palette.ts
  43. 4 4
      src/mol-util/param-definition.ts
  44. 2 2
      src/tests/browser/marching-cubes.ts
  45. 2 2
      src/tests/browser/render-lines.ts
  46. 2 2
      src/tests/browser/render-mesh.ts
  47. 2 2
      src/tests/browser/render-shape.ts
  48. 2 2
      src/tests/browser/render-spheres.ts
  49. 2 2
      src/tests/browser/render-structure.ts
  50. 2 2
      src/tests/browser/render-text.ts
  51. 1 1
      webpack.config.common.js

+ 2 - 1
docs/interesting-pdb-entries.md

@@ -24,4 +24,5 @@
 * Close backbone atoms but not linked (e.g. 4HIV)
 * Non-standard residues
     * Protein (1BRR, 5Z6Y)
-    * DNA (5D3G)
+    * DNA (5D3G)
+* Multiple models with different sets of ligands or missing ligands (1J6T, 1VRC, 2ICY, 1O2F)

+ 2 - 2
package-lock.json

@@ -1,11 +1,11 @@
 {
   "name": "molstar",
-  "version": "1.2.9",
+  "version": "1.2.12",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "version": "1.2.9",
+      "version": "1.2.12",
       "license": "MIT",
       "dependencies": {
         "@types/argparse": "^1.0.38",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "1.2.9",
+  "version": "1.2.12",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {

+ 2 - 3
src/extensions/cellpack/color/generate.ts

@@ -46,10 +46,9 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
             name: 'generate',
             params: {
                 hue, chroma: [30, 80], luminance: [15, 85],
-                clusteringStepCount: 50, minSampleCount: 800, maxCount: 75,
-                minLabel: 'Min', maxLabel: 'Max', valueLabel: (i: number) => `${i + 1}`,
+                clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
             }
-        }});
+        }}, { minLabel: 'Min', maxLabel: 'Max' });
         legend = palette.legend;
         const modelColor = new Map<number, Color>();
         for (let i = 0, il = models.length; i < il; ++i) {

+ 129 - 68
src/mol-canvas3d/canvas3d.ts

@@ -85,6 +85,110 @@ export type PartialCanvas3DProps = {
     [K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
 }
 
+export { Canvas3DContext };
+
+/** Can be used to create multiple Canvas3D objects */
+interface Canvas3DContext {
+    readonly canvas: HTMLCanvasElement
+    readonly webgl: WebGLContext
+    readonly input: InputObserver
+    readonly passes: Passes
+    readonly attribs: Readonly<Canvas3DContext.Attribs>
+    readonly contextLost: BehaviorSubject<now.Timestamp>
+    readonly contextRestored: BehaviorSubject<now.Timestamp>
+    dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
+}
+
+namespace Canvas3DContext {
+    const DefaultAttribs = {
+        /** true by default to avoid issues with Safari (Jan 2021) */
+        antialias: true,
+        /** true to support multiple Canvas3D objects with a single context */
+        preserveDrawingBuffer: true,
+        pixelScale: 1,
+        pickScale: 0.25,
+        enableWboit: true
+    };
+    export type Attribs = typeof DefaultAttribs
+
+    export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
+        const a = { ...DefaultAttribs, ...attribs };
+        const { antialias, preserveDrawingBuffer, pixelScale } = a;
+        const gl = getGLContext(canvas, {
+            antialias,
+            preserveDrawingBuffer,
+            alpha: true, // the renderer requires an alpha channel
+            depth: true, // the renderer requires a depth buffer
+            premultipliedAlpha: true, // the renderer outputs PMA
+        });
+        if (gl === null) throw new Error('Could not create a WebGL rendering context');
+
+        const input = InputObserver.fromElement(canvas, { pixelScale });
+        const webgl = createContext(gl, { pixelScale });
+        const passes = new Passes(webgl, attribs);
+
+        if (isDebugMode) {
+            const loseContextExt = gl.getExtension('WEBGL_lose_context');
+            if (loseContextExt) {
+                // Hold down shift+ctrl+alt and press any mouse button to call `loseContext`.
+                // After 1 second `restoreContext` will be called.
+                canvas.addEventListener('mousedown', e => {
+                    if (webgl.isContextLost) return;
+                    if (!e.shiftKey || !e.ctrlKey || !e.altKey) return;
+
+                    if (isDebugMode) console.log('lose context');
+                    loseContextExt.loseContext();
+
+                    setTimeout(() => {
+                        if (!webgl.isContextLost) return;
+                        if (isDebugMode) console.log('restore context');
+                        loseContextExt.restoreContext();
+                    }, 1000);
+                }, false);
+            }
+        }
+
+        // https://www.khronos.org/webgl/wiki/HandlingContextLost
+
+        const contextLost = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
+
+        const handleWebglContextLost = (e: Event) => {
+            webgl.setContextLost();
+            e.preventDefault();
+            if (isDebugMode) console.log('context lost');
+            contextLost.next(now());
+        };
+
+        const handlewWebglContextRestored = () => {
+            if (!webgl.isContextLost) return;
+            webgl.handleContextRestored(() => {
+                passes.draw.reset();
+            });
+            if (isDebugMode) console.log('context restored');
+        };
+
+        canvas.addEventListener('webglcontextlost', handleWebglContextLost, false);
+        canvas.addEventListener('webglcontextrestored', handlewWebglContextRestored, false);
+
+        return {
+            canvas,
+            webgl,
+            input,
+            passes,
+            attribs: a,
+            contextLost,
+            contextRestored: webgl.contextRestored,
+            dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
+                input.dispose();
+
+                canvas.removeEventListener('webglcontextlost', handleWebglContextLost, false);
+                canvas.removeEventListener('webglcontextrestored', handlewWebglContextRestored, false);
+                webgl.destroy(options);
+            }
+        };
+    }
+}
+
 export { Canvas3D };
 
 interface Canvas3D {
@@ -122,6 +226,8 @@ interface Canvas3D {
     readonly resized: BehaviorSubject<any>
 
     handleResize(): void
+    /** performs handleResize on the next animation frame */
+    requestResize(): void
     /** Focuses camera on scene's bounding sphere, centered and zoomed. */
     requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
     readonly camera: Camera
@@ -150,67 +256,7 @@ namespace Canvas3D {
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
 
-    export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number, enableWboit: boolean }> = {}) {
-        const antialias = (attribs.antialias ?? true) && !attribs.enableWboit;
-        const gl = getGLContext(canvas, {
-            alpha: true,
-            antialias,
-            depth: true,
-            preserveDrawingBuffer: true,
-            premultipliedAlpha: true,
-        });
-        if (gl === null) throw new Error('Could not create a WebGL rendering context');
-
-        const { pixelScale } = attribs;
-        const input = InputObserver.fromElement(canvas, { pixelScale });
-        const webgl = createContext(gl, { pixelScale });
-        const passes = new Passes(webgl, attribs);
-
-        if (isDebugMode) {
-            const loseContextExt = gl.getExtension('WEBGL_lose_context');
-            if (loseContextExt) {
-                canvas.addEventListener('mousedown', e => {
-                    if (webgl.isContextLost) return;
-                    if (!e.shiftKey || !e.ctrlKey || !e.altKey) return;
-
-                    if (isDebugMode) console.log('lose context');
-                    loseContextExt.loseContext();
-
-                    setTimeout(() => {
-                        if (!webgl.isContextLost) return;
-                        if (isDebugMode) console.log('restore context');
-                        loseContextExt.restoreContext();
-                    }, 1000);
-                }, false);
-            }
-        }
-
-        // https://www.khronos.org/webgl/wiki/HandlingContextLost
-
-        canvas.addEventListener('webglcontextlost', e => {
-            webgl.setContextLost();
-            e.preventDefault();
-            if (isDebugMode) console.log('context lost');
-        }, false);
-
-        canvas.addEventListener('webglcontextrestored', () => {
-            if (!webgl.isContextLost) return;
-            webgl.handleContextRestored();
-            if (isDebugMode) console.log('context restored');
-        }, false);
-
-        // disable postprocessing anti-aliasing if canvas anti-aliasing is enabled
-        if (antialias && !props.postprocessing?.antialiasing) {
-            props.postprocessing = {
-                ...DefaultCanvas3DParams.postprocessing,
-                antialiasing: { name: 'off', params: {} }
-            };
-        }
-
-        return create(webgl, input, passes, props, { pixelScale });
-    }
-
-    export function create(webgl: WebGLContext, input: InputObserver, passes: Passes, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ pixelScale: number }>): Canvas3D {
+    export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
         const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
 
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
@@ -251,6 +297,7 @@ namespace Canvas3D {
         let cameraResetRequested = false;
         let nextCameraResetDuration: number | undefined = void 0;
         let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0;
+        let resizeRequested = false;
 
         let notifyDidDraw = true;
 
@@ -293,6 +340,12 @@ namespace Canvas3D {
 
         function render(force: boolean) {
             if (webgl.isContextLost) return false;
+
+            if (resizeRequested) {
+                handleResize(false);
+                resizeRequested = false;
+            }
+
             if (x > gl.drawingBufferWidth || x + width < 0 ||
                 y > gl.drawingBufferHeight || y + height < 0
             ) return false;
@@ -558,10 +611,22 @@ namespace Canvas3D {
         const contextRestoredSub = contextRestored.subscribe(() => {
             pickHelper.dirty = true;
             draw(true);
+            // Unclear why, but in Chrome with wboit enabled the first `draw` only clears
+            // the drawingBuffer. Note that in Firefox the drawingBuffer is preserved after
+            // context loss so it is unclear if it behaves the same.
+            draw(true);
         });
 
         const resized = new BehaviorSubject<any>(0);
 
+        function handleResize(draw = true) {
+            passes.updateSize();
+            updateViewport();
+            syncViewport();
+            if (draw) requestDraw(true);
+            resized.next(+new Date());
+        }
+
         return {
             webgl,
 
@@ -607,12 +672,9 @@ namespace Canvas3D {
             mark,
             getLoci,
 
-            handleResize: () => {
-                passes.updateSize();
-                updateViewport();
-                syncViewport();
-                requestDraw(true);
-                resized.next(+new Date());
+            handleResize,
+            requestResize: () => {
+                resizeRequested = true;
             },
             requestCameraReset: options => {
                 nextCameraResetDuration = options?.durationMs;
@@ -708,7 +770,6 @@ namespace Canvas3D {
 
                 scene.clear();
                 helper.debug.clear();
-                input.dispose();
                 controls.dispose();
                 renderer.dispose();
                 interactionHelper.dispose();

+ 4 - 0
src/mol-canvas3d/passes/draw.ts

@@ -129,6 +129,10 @@ export class DrawPass {
         this.copyFboPostprocessing = getCopyRenderable(webgl, this.postprocessing.target.texture);
     }
 
+    reset() {
+        this.wboit?.reset();
+    }
+
     setSize(width: number, height: number) {
         const w = this.colorTarget.getWidth();
         const h = this.colorTarget.getHeight();

+ 29 - 13
src/mol-canvas3d/passes/wboit.ts

@@ -89,9 +89,25 @@ export class WboitPass {
         }
     }
 
-    constructor(private webgl: WebGLContext, width: number, height: number) {
-        const { resources, extensions } = webgl;
-        const { drawBuffers, textureFloat, colorBufferFloat, depthTexture } = extensions;
+    reset() {
+        if (this._supported) this._init();
+    }
+
+    private _init() {
+        const { extensions: { drawBuffers } } = this.webgl;
+
+        this.framebuffer.bind();
+        drawBuffers!.drawBuffers([
+            drawBuffers!.COLOR_ATTACHMENT0,
+            drawBuffers!.COLOR_ATTACHMENT1,
+        ]);
+
+        this.textureA.attachFramebuffer(this.framebuffer, 'color0');
+        this.textureB.attachFramebuffer(this.framebuffer, 'color1');
+    }
+
+    static isSupported(webgl: WebGLContext) {
+        const { extensions: { drawBuffers, textureFloat, colorBufferFloat, depthTexture } } = webgl;
         if (!textureFloat || !colorBufferFloat || !depthTexture || !drawBuffers) {
             if (isDebugMode) {
                 const missing: string[] = [];
@@ -101,8 +117,16 @@ export class WboitPass {
                 if (!drawBuffers) missing.push('drawBuffers');
                 console.log(`Missing "${missing.join('", "')}" extensions required for "wboit"`);
             }
-            return;
+            return false;
+        } else {
+            return true;
         }
+    }
+
+    constructor(private webgl: WebGLContext, width: number, height: number) {
+        if (!WboitPass.isSupported(webgl)) return;
+
+        const { resources } = webgl;
 
         this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
         this.textureA.define(width, height);
@@ -111,17 +135,9 @@ export class WboitPass {
         this.textureB.define(width, height);
 
         this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
-
         this.framebuffer = resources.framebuffer();
-        this.framebuffer.bind();
-        drawBuffers.drawBuffers([
-            drawBuffers.COLOR_ATTACHMENT0,
-            drawBuffers.COLOR_ATTACHMENT1,
-        ]);
-
-        this.textureA.attachFramebuffer(this.framebuffer, 'color0');
-        this.textureB.attachFramebuffer(this.framebuffer, 'color1');
 
         this._supported = true;
+        this._init();
     }
 }

+ 10 - 0
src/mol-geo/geometry/lines/lines-builder.ts

@@ -11,6 +11,7 @@ import { Cage } from '../../../mol-geo/primitive/cage';
 
 export interface LinesBuilder {
     add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number): void
+    addVec(start: Vec3, end: Vec3, group: number): void
     addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, group: number): void
     addFixedLengthDashes(start: Vec3, end: Vec3, segmentLength: number, group: number): void
     addCage(t: Mat4, cage: Cage, group: number): void
@@ -39,6 +40,14 @@ export namespace LinesBuilder {
             }
         };
 
+        const addVec = (start: Vec3, end: Vec3, group: number) => {
+            for (let i = 0; i < 4; ++i) {
+                caAdd3(starts, start[0], start[1], start[2]);
+                caAdd3(ends, end[0], end[1], end[2]);
+                caAdd(groups, group);
+            }
+        };
+
         const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, group: number) => {
             const d = Vec3.distance(start, end);
             const s = Math.floor(segmentCount / 2);
@@ -57,6 +66,7 @@ export namespace LinesBuilder {
 
         return {
             add,
+            addVec,
             addFixedCountDashes,
             addFixedLengthDashes: (start: Vec3, end: Vec3, segmentLength: number, group: number) => {
                 const d = Vec3.distance(start, end);

+ 8 - 7
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -104,7 +104,8 @@ export namespace TextureMesh {
 
         const counts = { drawCount: textureMesh.vertexCount, vertexCount: textureMesh.vertexCount / 3, groupCount, instanceCount };
 
-        const transformBoundingSphere = calculateTransformBoundingSphere(textureMesh.boundingSphere, transform.aTransform.ref.value, transform.instanceCount.ref.value);
+        const invariantBoundingSphere = Sphere3D.clone(textureMesh.boundingSphere);
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
 
         return {
             uGeoTexDim: textureMesh.geoTextureDim,
@@ -113,9 +114,9 @@ export namespace TextureMesh {
 
             // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup
             aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount))),
-            boundingSphere: ValueCell.create(transformBoundingSphere),
-            invariantBoundingSphere: ValueCell.create(Sphere3D.clone(textureMesh.boundingSphere)),
-            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(textureMesh.boundingSphere)),
+            boundingSphere: ValueCell.create(boundingSphere),
+            invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
 
             ...color,
             ...marker,
@@ -141,8 +142,7 @@ export namespace TextureMesh {
     }
 
     function updateValues(values: TextureMeshValues, props: PD.Values<Params>) {
-        ValueCell.updateIfChanged(values.alpha, props.alpha); // `uAlpha` is set in renderable.render
-
+        BaseGeometry.updateValues(values, props);
         ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
         ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
         ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
@@ -156,8 +156,9 @@ export namespace TextureMesh {
     }
 
     function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {
-        const invariantBoundingSphere = textureMesh.boundingSphere;
+        const invariantBoundingSphere = Sphere3D.clone(textureMesh.boundingSphere);
         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);
         }

+ 10 - 15
src/mol-gl/compute/histogram-pyramid/reduction.ts

@@ -124,11 +124,11 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     const maxSize = Math.pow(2, levels);
     // console.log('levels', levels, 'maxSize', maxSize, 'input', w);
 
-    const pyramidTexture = getTexture('pyramid', ctx, 'image-float32', 'rgba', 'float', 'nearest');
-    pyramidTexture.define(maxSize, maxSize);
+    const pyramidTex = getTexture('pyramid', ctx, 'image-float32', 'rgba', 'float', 'nearest');
+    pyramidTex.define(maxSize, maxSize);
 
     const framebuffer = getFramebuffer('pyramid', ctx);
-    pyramidTexture.attachFramebuffer(framebuffer, 0);
+    pyramidTex.attachFramebuffer(framebuffer, 0);
     gl.viewport(0, 0, maxSize, maxSize);
     gl.clear(gl.COLOR_BUFFER_BIT);
 
@@ -162,29 +162,24 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
         gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
         renderable.render();
 
-        pyramidTexture.bind(0);
+        pyramidTex.bind(0);
         gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, offset, 0, 0, 0, size, size);
-        pyramidTexture.unbind(0);
+        pyramidTex.unbind(0);
 
         offset += size;
     }
 
     gl.finish();
 
-    // printTexture(ctx, pyramidTexture, 2)
+    // printTexture(ctx, pyramidTex, 2)
 
     //
 
-    const finalCount = getHistopyramidSum(ctx, levelTexturesFramebuffers[0].texture);
-    const height = Math.ceil(finalCount / Math.pow(2, levels));
+    // return at least a count of one to avoid issues downstram
+    const count = Math.max(1, getHistopyramidSum(ctx, levelTexturesFramebuffers[0].texture));
+    const height = Math.ceil(count / 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
-    };
+    return { pyramidTex, count, height, levels, scale };
 }

+ 3 - 3
src/mol-gl/compute/marching-cubes/active-voxels.ts

@@ -109,9 +109,9 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim
     gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
     renderable.render();
 
-    // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim)
-    // console.log('volumeData', volumeData)
-    // console.log('at', readTexture(ctx, activeVoxelsTex))
+    // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim);
+    // console.log('volumeData', volumeData);
+    // console.log('at', readTexture(ctx, activeVoxelsTex));
 
     gl.finish();
 

+ 5 - 1
src/mol-gl/shader-code.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -51,10 +51,12 @@ import common_clip from './shader/chunks/common-clip.glsl';
 import common_frag_params from './shader/chunks/common-frag-params.glsl';
 import common_vert_params from './shader/chunks/common-vert-params.glsl';
 import common from './shader/chunks/common.glsl';
+import float_to_rgba from './shader/chunks/float-to-rgba.glsl';
 import light_frag_params from './shader/chunks/light-frag-params.glsl';
 import matrix_scale from './shader/chunks/matrix-scale.glsl';
 import normal_frag_params from './shader/chunks/normal-frag-params.glsl';
 import read_from_texture from './shader/chunks/read-from-texture.glsl';
+import rgba_to_float from './shader/chunks/rgba-to-float.glsl';
 import size_vert_params from './shader/chunks/size-vert-params.glsl';
 import texture3d_from_1d_trilinear from './shader/chunks/texture3d-from-1d-trilinear.glsl';
 import texture3d_from_2d_linear from './shader/chunks/texture3d-from-2d-linear.glsl';
@@ -83,10 +85,12 @@ const ShaderChunks: { [k: string]: string } = {
     common_frag_params,
     common_vert_params,
     common,
+    float_to_rgba,
     light_frag_params,
     matrix_scale,
     normal_frag_params,
     read_from_texture,
+    rgba_to_float,
     size_vert_params,
     texture3d_from_1d_trilinear,
     texture3d_from_2d_linear,

+ 1 - 0
src/mol-gl/shader/chunks/common.glsl.ts

@@ -26,6 +26,7 @@ export default `
 #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)); }
+vec2 ivec2Div(const in vec2 a, const in vec2 b) { return vec2(ivec2(a) / ivec2(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; }

+ 46 - 0
src/mol-gl/shader/chunks/float-to-rgba.glsl.ts

@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export default `
+    // floatToRgba adapted from https://github.com/equinor/glsl-float-to-rgba
+    // MIT License, Copyright (c) 2020 Equinor
+
+    float shiftRight (float v, float amt) {
+    v = floor(v) + 0.5;
+    return floor(v / exp2(amt));
+    }
+    float shiftLeft (float v, float amt) {
+        return floor(v * exp2(amt) + 0.5);
+    }
+    float maskLast (float v, float bits) {
+        return mod(v, shiftLeft(1.0, bits));
+    }
+    float extractBits (float num, float from, float to) {
+        from = floor(from + 0.5); to = floor(to + 0.5);
+        return maskLast(shiftRight(num, from), to - from);
+    }
+
+    vec4 floatToRgba(float texelFloat, bool littleEndian) {
+        if (texelFloat == 0.0) return vec4(0.0, 0.0, 0.0, 0.0);
+        float sign = texelFloat > 0.0 ? 0.0 : 1.0;
+        texelFloat = abs(texelFloat);
+        float exponent = floor(log2(texelFloat));
+        float biased_exponent = exponent + 127.0;
+        float fraction = ((texelFloat / exp2(exponent)) - 1.0) * 8388608.0;
+        float t = biased_exponent / 2.0;
+        float last_bit_of_biased_exponent = fract(t) * 2.0;
+        float remaining_bits_of_biased_exponent = floor(t);
+        float byte4 = extractBits(fraction, 0.0, 8.0) / 255.0;
+        float byte3 = extractBits(fraction, 8.0, 16.0) / 255.0;
+        float byte2 = (last_bit_of_biased_exponent * 128.0 + extractBits(fraction, 16.0, 23.0)) / 255.0;
+        float byte1 = (sign * 128.0 + remaining_bits_of_biased_exponent) / 255.0;
+        return (
+            littleEndian
+                ? vec4(byte4, byte3, byte2, byte1)
+                : vec4(byte1, byte2, byte3, byte4)
+        );
+    }
+`;

+ 91 - 0
src/mol-gl/shader/chunks/rgba-to-float.glsl.ts

@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export default `
+    // rgbaToFloat adapted from https://github.com/ihmeuw/glsl-rgba-to-float
+    // BSD 3-Clause License
+    //
+    // Copyright (c) 2019, Institute for Health Metrics and Evaluation All rights reserved.
+    // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+    //  - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+    //  - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+    //  - Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+    //
+    // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+    // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+    // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+    // OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+    // OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+    // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+    // OF THE POSSIBILITY OF SUCH DAMAGE.
+
+    ivec4 floatsToBytes(vec4 inputFloats, bool littleEndian) {
+        ivec4 bytes = ivec4(inputFloats * 255.0);
+        return (
+            littleEndian
+                ? bytes.abgr
+                : bytes
+        );
+    }
+
+    // Break the four bytes down into an array of 32 bits.
+    void bytesToBits(const in ivec4 bytes, out bool bits[32]) {
+        for (int channelIndex = 0; channelIndex < 4; ++channelIndex) {
+            float acc = float(bytes[channelIndex]);
+            for (int indexInByte = 7; indexInByte >= 0; --indexInByte) {
+                float powerOfTwo = exp2(float(indexInByte));
+                bool bit = acc >= powerOfTwo;
+                bits[channelIndex * 8 + (7 - indexInByte)] = bit;
+                acc = mod(acc, powerOfTwo);
+            }
+        }
+    }
+
+    // Compute the exponent of the 32-bit float.
+    float getExponent(bool bits[32]) {
+        const int startIndex = 1;
+        const int bitStringLength = 8;
+        const int endBeforeIndex = startIndex + bitStringLength;
+        float acc = 0.0;
+        int pow2 = bitStringLength - 1;
+        for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {
+            acc += float(bits[bitIndex]) * exp2(float(pow2--));
+        }
+        return acc;
+    }
+
+    // Compute the mantissa of the 32-bit float.
+    float getMantissa(bool bits[32], bool subnormal) {
+        const int startIndex = 9;
+        const int bitStringLength = 23;
+        const int endBeforeIndex = startIndex + bitStringLength;
+        // Leading/implicit/hidden bit convention:
+        // If the number is not subnormal (with exponent 0), we add a leading 1 digit.
+        float acc = float(!subnormal) * exp2(float(bitStringLength));
+        int pow2 = bitStringLength - 1;
+        for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {
+            acc += float(bits[bitIndex]) * exp2(float(pow2--));
+        }
+        return acc;
+    }
+
+    // Parse the float from its 32 bits.
+    float bitsToFloat(bool bits[32]) {
+        float signBit = float(bits[0]) * -2.0 + 1.0;
+        float exponent = getExponent(bits);
+        bool subnormal = abs(exponent - 0.0) < 0.01;
+        float mantissa = getMantissa(bits, subnormal);
+        float exponentBias = 127.0;
+        return signBit * mantissa * exp2(exponent - exponentBias - 23.0);
+    }
+
+    float rgbaToFloat(vec4 texelRGBA, bool littleEndian) {
+        ivec4 rgbaBytes = floatsToBytes(texelRGBA, littleEndian);
+        bool bits[32];
+        bytesToBits(rgbaBytes, bits);
+        return bitsToFloat(bits);
+    }
+`;

+ 6 - 5
src/mol-gl/shader/direct-volume.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 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>
@@ -260,7 +260,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
                     #ifdef enabledFragDepth
                         return packDepthToRGBA(gl_FragDepthEXT);
                     #else
-                        return packDepthToRGBA(gl_FragCoord.z);
+                        return packDepthToRGBA(depth);
                     #endif
                 #elif defined(dRenderVariant_color)
                     #ifdef dPackedGroup
@@ -446,8 +446,9 @@ void main() {
     vec3 step = rayDir * uStepScale;
 
     float boundingSphereNear = distance(vBoundingSphere.xyz, uCameraPosition) - vBoundingSphere.w;
-    float d = max(uNear, boundingSphereNear);
-    gl_FragColor = raymarch(uCameraPosition + (d * rayDir), step, rayDir);
+    float d = max(uNear, boundingSphereNear) - mix(0.0, distance(vOrigPos, uCameraPosition), uIsOrtho);
+    vec3 start = mix(uCameraPosition, vOrigPos, uIsOrtho) + (d * rayDir);
+    gl_FragColor = raymarch(start, step, rayDir);
 
     #if defined(dRenderVariant_pick) || defined(dRenderVariant_depth)
         // discard when nothing was hit
@@ -459,7 +460,7 @@ void main() {
         #if defined(dRenderMode_isosurface) && defined(enabledFragDepth)
             float fragmentDepth = gl_FragDepthEXT;
         #else
-            float fragmentDepth = calcDepth((uView * vec4(uCameraPosition + (d * rayDir), 1.0)).xyz);
+            float fragmentDepth = calcDepth((uModelView * vec4(start, 1.0)).xyz);
         #endif
         float preFogAlpha = clamp(preFogAlphaBlended, 0.0, 1.0);
         interior = false;

+ 3 - 3
src/mol-gl/shader/direct-volume.vert.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 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>
@@ -43,7 +43,7 @@ void main() {
 
     gl_Position = uProjection * mvPosition;
 
-    // move z position to near clip plane
-    gl_Position.z = gl_Position.w - 0.0001;
+    // move z position to near clip plane (but not too close to get precision issues)
+    gl_Position.z = gl_Position.w - 0.01;
 }
 `;

+ 24 - 29
src/mol-gl/shader/marching-cubes/active-voxels.frag.ts

@@ -1,5 +1,6 @@
 export default `
 precision highp float;
+precision highp int;
 precision highp sampler2D;
 
 uniform sampler2D tTriCount;
@@ -10,8 +11,9 @@ uniform vec3 uGridDim;
 uniform vec3 uGridTexDim;
 uniform vec2 uScale;
 
-// cube corners
-const vec3 c0 = vec3(0., 0., 0.);
+#include common
+
+// cube corners (excluding origin)
 const vec3 c1 = vec3(1., 0., 0.);
 const vec3 c2 = vec3(1., 1., 0.);
 const vec3 c3 = vec3(0., 1., 0.);
@@ -22,26 +24,23 @@ const vec3 c7 = vec3(0., 1., 1.);
 
 vec3 index3dFrom2d(vec2 coord) {
     vec2 gridTexPos = coord * uGridTexDim.xy;
-    vec2 columnRow = floor(gridTexPos / uGridDim.xy);
+    vec2 columnRow = ivec2Div(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 posZ = columnRow.y * intDiv(uGridTexDim.x, uGridDim.x) + columnRow.x;
+    return vec3(posXY, posZ);
 }
 
-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));
+    float column = intDiv(intMod(zSlice * gridDim.x, texDim.x), gridDim.x);
+    float row = 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);
 }
 
 vec4 voxel(vec3 pos) {
-    return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy);
+    pos = min(max(vec3(0.0), pos), uGridDim - vec3(1.0));
+    return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
 }
 
 void main(void) {
@@ -50,27 +49,23 @@ void main(void) {
 
     // 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);
+        + 2. * step(voxel(posXYZ + c1).a, uIsoValue)
+        + 4. * step(voxel(posXYZ + c2).a, uIsoValue)
+        + 8. * step(voxel(posXYZ + c3).a, uIsoValue)
+        + 16. * step(voxel(posXYZ + c4).a, uIsoValue)
+        + 32. * step(voxel(posXYZ + c5).a, uIsoValue)
+        + 64. * step(voxel(posXYZ + c6).a, uIsoValue)
+        + 128. * step(voxel(posXYZ + c7).a, uIsoValue);
     c *= step(c, 254.);
 
+    // handle out of bounds positions
+    posXYZ += 1.0;
+    posXYZ.xy += 1.0; // pixel padding (usually ok even if the texture has no padding)
+    if (posXYZ.x >= uGridDim.x || posXYZ.y >= uGridDim.y || posXYZ.z >= uGridDim.z)
+        c = 0.0;
+
     // 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(totalTrianglesToGenerate * 3.0), c / 255.0);
-
-    // 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);
 }
 `;

+ 34 - 24
src/mol-gl/shader/marching-cubes/isosurface.frag.ts

@@ -1,5 +1,6 @@
 export default `
 precision highp float;
+precision highp int;
 precision highp sampler2D;
 
 uniform sampler2D tActiveVoxelsPyramid;
@@ -21,8 +22,7 @@ uniform vec2 uScale;
 
 #include common
 
-// cube corners
-const vec3 c0 = vec3(0., 0., 0.);
+// cube corners (excluding origin)
 const vec3 c1 = vec3(1., 0., 0.);
 const vec3 c2 = vec3(1., 1., 0.);
 const vec3 c3 = vec3(0., 1., 0.);
@@ -31,27 +31,30 @@ 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 columnRow = ivec2Div(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;
+    float posZ = columnRow.y * intDiv(uGridTexDim.x, uGridDim.x) + columnRow.x;
+    return vec3(posXY, posZ);
 }
 
 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));
+    float column = intDiv(intMod(zSlice * gridDim.x, texDim.x), gridDim.x);
+    float row = 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));
 }
 
 vec4 voxel(vec3 pos) {
-    return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy);
+    pos = min(max(vec3(0.0), pos), uGridDim - vec3(1.0));
+    return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
+}
+
+vec4 voxel2(vec3 pos) {
+    pos = min(max(vec3(0.0), pos), uGridDim - vec3(vec2(2.0), 1.0));
+    return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
 }
 
 void main(void) {
@@ -146,13 +149,13 @@ void main(void) {
     // vec3 b0 = coord3d;
     // vec3 b1 = coord3d;
     // if (mcIndex == 0.0) {
-    //     b0 += c0; b1 += c1;
+    //     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;
+    //     b0 += c3;
     // } else if (mcIndex == 4.0) {
     //     b0 += c4; b1 += c5;
     // } else if (mcIndex == 5.0) {
@@ -162,7 +165,7 @@ void main(void) {
     // } else if (mcIndex == 7.0) {
     //     b0 += c7; b1 += c4;
     // } else if (mcIndex == 8.0) {
-    //     b0 += c0; b1 += c4;
+    //     b1 += c4;
     // } else if (mcIndex == 9.0) {
     //     b0 += c1; b1 += c5;
     // } else if (mcIndex == 10.0) {
@@ -173,27 +176,34 @@ void main(void) {
     // b0 = floor(b0 + 0.5);
     // b1 = floor(b1 + 0.5);
 
-    vec4 d0 = voxel(b0 / uGridDim);
-    vec4 d1 = voxel(b1 / uGridDim);
+    vec4 d0 = voxel(b0);
+    vec4 d1 = voxel(b1);
 
     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
+
+    // group id
+    #if __VERSION__ == 100
+        // webgl1 does not support 'flat' interpolation (i.e. no interpolation)
+        // so we ensure a constant group id per triangle
+        gl_FragData[0].w = decodeFloatRGB(voxel(coord3d).rgb);
+    #else
+        gl_FragData[0].w = t < 0.5 ? decodeFloatRGB(d0.rgb) : decodeFloatRGB(d1.rgb);
+    #endif
 
     // 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
+        voxel2(b0 - c1).a - voxel2(b0 + c1).a,
+        voxel2(b0 - c3).a - voxel2(b0 + c3).a,
+        voxel2(b0 - c4).a - voxel2(b0 + c4).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
+        voxel2(b1 - c1).a - voxel2(b1 + c1).a,
+        voxel2(b1 - c3).a - voxel2(b1 + c3).a,
+        voxel2(b1 - c4).a - voxel2(b1 + c4).a
     ));
     gl_FragData[1].xyz = -vec3(
         n0.x + t * (n0.x - n1.x),

+ 5 - 133
src/mol-gl/shader/util/grid3d-template.frag.ts

@@ -17,146 +17,18 @@ uniform bool uLittleEndian;
 uniform float uWidth;
 
 #ifdef CUMULATIVE
-uniform sampler2D tCumulativeSum;
+    uniform sampler2D tCumulativeSum;
 #endif
 
 {UNIFORMS}
 
 {UTILS}
 
-//////////////////////////////////////////////////////////
-
-// floatToRgba adapted from https://github.com/equinor/glsl-float-to-rgba
-// MIT License, Copyright (c) 2020 Equinor
-
-float shiftRight (float v, float amt) {
-  v = floor(v) + 0.5;
-  return floor(v / exp2(amt));
-}
-float shiftLeft (float v, float amt) {
-    return floor(v * exp2(amt) + 0.5);
-}
-float maskLast (float v, float bits) {
-    return mod(v, shiftLeft(1.0, bits));
-}
-float extractBits (float num, float from, float to) {
-    from = floor(from + 0.5); to = floor(to + 0.5);
-    return maskLast(shiftRight(num, from), to - from);
-}
-
-vec4 floatToRgba(float texelFloat) {
-    if (texelFloat == 0.0) return vec4(0, 0, 0, 0);
-    float sign = texelFloat > 0.0 ? 0.0 : 1.0;
-    texelFloat = abs(texelFloat);
-    float exponent = floor(log2(texelFloat));
-    float biased_exponent = exponent + 127.0;
-    float fraction = ((texelFloat / exp2(exponent)) - 1.0) * 8388608.0;
-    float t = biased_exponent / 2.0;
-    float last_bit_of_biased_exponent = fract(t) * 2.0;
-    float remaining_bits_of_biased_exponent = floor(t);
-    float byte4 = extractBits(fraction, 0.0, 8.0) / 255.0;
-    float byte3 = extractBits(fraction, 8.0, 16.0) / 255.0;
-    float byte2 = (last_bit_of_biased_exponent * 128.0 + extractBits(fraction, 16.0, 23.0)) / 255.0;
-    float byte1 = (sign * 128.0 + remaining_bits_of_biased_exponent) / 255.0;
-    return (
-        uLittleEndian
-            ? vec4(byte4, byte3, byte2, byte1)
-            : vec4(byte1, byte2, byte3, byte4)
-    );
-}
-
-///////////////////////////////////////////////////////
-
-// rgbaToFloat adapted from https://github.com/ihmeuw/glsl-rgba-to-float
-// BSD 3-Clause License
-//
-// Copyright (c) 2019, Institute for Health Metrics and Evaluation All rights reserved.
-// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-//  - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-//  - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
-//  - Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
-// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
-// IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
-// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-// OF THE POSSIBILITY OF SUCH DAMAGE.
-
+#include float_to_rgba
 #ifdef CUMULATIVE
-
-ivec4 floatsToBytes(vec4 inputFloats) {
-  ivec4 bytes = ivec4(inputFloats * 255.0);
-  return (
-    uLittleEndian
-    ? bytes.abgr
-    : bytes
-  );
-}
-
-// Break the four bytes down into an array of 32 bits.
-void bytesToBits(const in ivec4 bytes, out bool bits[32]) {
-  for (int channelIndex = 0; channelIndex < 4; ++channelIndex) {
-    float acc = float(bytes[channelIndex]);
-    for (int indexInByte = 7; indexInByte >= 0; --indexInByte) {
-      float powerOfTwo = exp2(float(indexInByte));
-      bool bit = acc >= powerOfTwo;
-      bits[channelIndex * 8 + (7 - indexInByte)] = bit;
-      acc = mod(acc, powerOfTwo);
-    }
-  }
-}
-
-// Compute the exponent of the 32-bit float.
-float getExponent(bool bits[32]) {
-  const int startIndex = 1;
-  const int bitStringLength = 8;
-  const int endBeforeIndex = startIndex + bitStringLength;
-  float acc = 0.0;
-  int pow2 = bitStringLength - 1;
-  for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {
-    acc += float(bits[bitIndex]) * exp2(float(pow2--));
-  }
-  return acc;
-}
-
-// Compute the mantissa of the 32-bit float.
-float getMantissa(bool bits[32], bool subnormal) {
-  const int startIndex = 9;
-  const int bitStringLength = 23;
-  const int endBeforeIndex = startIndex + bitStringLength;
-  // Leading/implicit/hidden bit convention:
-  // If the number is not subnormal (with exponent 0), we add a leading 1 digit.
-  float acc = float(!subnormal) * exp2(float(bitStringLength));
-  int pow2 = bitStringLength - 1;
-  for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {
-    acc += float(bits[bitIndex]) * exp2(float(pow2--));
-  }
-  return acc;
-}
-
-// Parse the float from its 32 bits.
-float bitsToFloat(bool bits[32]) {
-  float signBit = float(bits[0]) * -2.0 + 1.0;
-  float exponent = getExponent(bits);
-  bool subnormal = abs(exponent - 0.0) < 0.01;
-  float mantissa = getMantissa(bits, subnormal);
-  float exponentBias = 127.0;
-  return signBit * mantissa * exp2(exponent - exponentBias - 23.0);
-}
-
-float rgbaToFloat(vec4 texelRGBA) {
-  ivec4 rgbaBytes = floatsToBytes(texelRGBA);
-  bool bits[32];
-  bytesToBits(rgbaBytes, bits);
-  return bitsToFloat(bits);
-}
-
+    #include rgba_to_float
 #endif
 
-///////////////////////////////////////////////////////
-
 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)); }
 
@@ -174,8 +46,8 @@ void main(void) {
     {MAIN}
 
     #ifdef CUMULATIVE
-        float current = rgbaToFloat(texture2D(tCumulativeSum, gl_FragCoord.xy / vec2(uWidth, uWidth)));
+        float current = rgbaToFloat(texture2D(tCumulativeSum, gl_FragCoord.xy / vec2(uWidth, uWidth)), uLittleEndian);
     #endif
-    gl_FragColor = floatToRgba({RETURN});
+    gl_FragColor = floatToRgba({RETURN}, uLittleEndian);
 }
 `;

+ 9 - 5
src/mol-gl/webgl/context.ts

@@ -193,7 +193,7 @@ export interface WebGLContext {
     readonly isContextLost: boolean
     readonly contextRestored: BehaviorSubject<now.Timestamp>
     setContextLost: () => void
-    handleContextRestored: () => void
+    handleContextRestored: (extraResets?: () => void) => void
 
     /** Cache for compute renderables, managed by consumers */
     readonly namedComputeRenderables: { [name: string]: ComputeRenderable<any> }
@@ -210,7 +210,7 @@ export interface WebGLContext {
     waitForGpuCommandsCompleteSync: () => void
     getDrawingBufferPixelData: () => PixelData
     clear: (red: number, green: number, blue: number, alpha: number) => void
-    destroy: () => void
+    destroy: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
 }
 
 export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScale: number }> = {}): WebGLContext {
@@ -232,7 +232,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
     }
 
     let isContextLost = false;
-    let contextRestored = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
+    const contextRestored = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
 
     let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>;
     if (isWebGL2(gl)) {
@@ -302,7 +302,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         setContextLost: () => {
             isContextLost = true;
         },
-        handleContextRestored: () => {
+        handleContextRestored: (extraResets?: () => void) => {
             Object.assign(extensions, createExtensions(gl));
 
             state.reset();
@@ -312,6 +312,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
 
             resources.reset();
             renderTargets.forEach(rt => rt.reset());
+            extraResets?.();
 
             isContextLost = false;
             contextRestored.next(now());
@@ -347,9 +348,12 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
             gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
         },
 
-        destroy: () => {
+        destroy: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
             resources.destroy();
             unbindResources(gl);
+
+            // to aid GC
+            if (!options?.doNotForceWebGLContextLoss) gl.getExtension('WEBGL_lose_context')?.loseContext();
         }
     };
 }

+ 2 - 4
src/mol-gl/webgl/program.ts

@@ -24,7 +24,7 @@ export interface Program {
     use: () => void
     setUniforms: (uniformValues: UniformsList) => void
     bindAttributes: (attribueBuffers: AttributeBuffers) => void
-    bindTextures: (textures: Textures, startingTargetUnit?: number) => void
+    bindTextures: (textures: Textures, startingTargetUnit: number) => void
 
     reset: () => void
     destroy: () => void
@@ -198,9 +198,7 @@ export function createProgram(gl: GLRenderingContext, state: WebGLState, extensi
                 if (l !== -1) buffer.bind(l);
             }
         },
-        bindTextures: (textures: Textures, startingTargetUnit?: number) => {
-            startingTargetUnit = startingTargetUnit ?? 0;
-
+        bindTextures: (textures: Textures, startingTargetUnit: number) => {
             for (let i = 0, il = textures.length; i < il; ++i) {
                 const [k, texture] = textures[i];
                 const l = locations[k];

+ 2 - 2
src/mol-gl/webgl/render-item.ts

@@ -173,7 +173,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                     program.bindTextures(sharedTexturesList, 0);
                     program.bindTextures(textures, sharedTexturesList.length);
                 } else {
-                    program.bindTextures(textures);
+                    program.bindTextures(textures, 0);
                 }
             } else {
                 const vertexArray = vertexArrays[variant];
@@ -191,7 +191,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                     program.bindTextures(sharedTexturesList, 0);
                     program.bindTextures(textures, sharedTexturesList.length);
                 } else {
-                    program.bindTextures(textures);
+                    program.bindTextures(textures, 0);
                 }
                 if (vertexArray) {
                     vertexArray.bind();

+ 22 - 11
src/mol-gl/webgl/texture.ts

@@ -177,7 +177,11 @@ export interface Texture {
     getByteCount: () => number
 
     define: (width: number, height: number, depth?: number) => void
-    load: (image: TextureImage<any> | TextureVolume<any> | HTMLImageElement) => void
+    /**
+     * The `sub` option requires an existing allocation on the GPU, that is, either
+     * `define` or `load` without `sub` must have been called before.
+     */
+    load: (image: TextureImage<any> | TextureVolume<any> | HTMLImageElement, sub?: boolean) => void
     bind: (id: TextureId) => void
     unbind: (id: TextureId) => void
     /** Use `layer` to attach a z-slice of a 3D texture */
@@ -250,7 +254,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
         }
     }
 
-    function load(data: TextureImage<any> | TextureVolume<any> | HTMLImageElement) {
+    function load(data: TextureImage<any> | TextureVolume<any> | HTMLImageElement, sub = false) {
         gl.bindTexture(target, texture);
         // unpack alignment of 1 since we use textures only for data
         gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
@@ -262,12 +266,20 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
             gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, format, type, data);
         } else if (isTexture2d(data, target, gl)) {
             gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!data.flipY);
-            width = data.width, height = data.height;
-            gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, data.array);
+            if (sub) {
+                gl.texSubImage2D(target, 0, 0, 0, data.width, data.height, format, type, data.array);
+            } else {
+                width = data.width, height = data.height;
+                gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, data.array);
+            }
         } else if (isWebGL2(gl) && isTexture3d(data, target, gl)) {
             gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
-            width = data.width, height = data.height, depth = data.depth;
-            gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, data.array);
+            if (sub) {
+                gl.texSubImage3D(target, 0, 0, 0, 0, data.width, data.height, data.depth, format, type, data.array);
+            } else {
+                width = data.width, height = data.height, depth = data.depth;
+                gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, data.array);
+            }
         } else {
             throw new Error('unknown texture target');
         }
@@ -325,11 +337,10 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
             texture = getTexture(gl);
             init();
 
-            if (loadedData) {
-                load(loadedData);
-            } else {
-                define(width, height, depth);
-            }
+            const [_width, _height, _depth] = [width, height, depth];
+            width = 0, height = 0, depth = 0; // set to zero to trigger resize
+            define(_width, _height, _depth);
+            if (loadedData) load(loadedData);
         },
         destroy: () => {
             if (destroyed) return;

+ 19 - 11
src/mol-plugin-state/manager/structure/measurement.ts

@@ -89,7 +89,8 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
         await PluginCommands.State.Update(this.plugin, { state: this.plugin.state.data, tree: update, options: { doNotLogTiming: true } });
     }
 
-    async addDistance(a: StructureElement.Loci, b: StructureElement.Loci, options?: StructureMeasurementManagerAddOptions) {
+    async addDistance(a: StructureElement.Loci, b: StructureElement.Loci,
+        options?: StructureMeasurementManagerAddOptions & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsDistance3D>> }) {
         const cellA = this.plugin.helpers.substructureParent.get(a.structure);
         const cellB = this.plugin.helpers.substructureParent.get(b.structure);
 
@@ -112,15 +113,17 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
                 customText: options?.customText || '',
                 unitLabel: this.state.options.distanceUnitLabel,
                 textColor: this.state.options.textColor,
-                ...options?.lineParams,
-                ...options?.labelParams
+                ...(options?.lineParams as any),
+                ...options?.labelParams,
+                ...options?.visualParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
     }
 
-    async addAngle(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci, options?: StructureMeasurementManagerAddOptions) {
+    async addAngle(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci,
+        options?: StructureMeasurementManagerAddOptions & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsAngle3D>> }) {
         const cellA = this.plugin.helpers.substructureParent.get(a.structure);
         const cellB = this.plugin.helpers.substructureParent.get(b.structure);
         const cellC = this.plugin.helpers.substructureParent.get(c.structure);
@@ -145,15 +148,17 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
             .apply(StateTransforms.Representation.StructureSelectionsAngle3D, {
                 customText: options?.customText || '',
                 textColor: this.state.options.textColor,
-                ...options?.lineParams,
-                ...options?.labelParams
+                ...(options?.lineParams as any),
+                ...options?.labelParams,
+                ...options?.visualParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
     }
 
-    async addDihedral(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci, d: StructureElement.Loci, options?: StructureMeasurementManagerAddOptions) {
+    async addDihedral(a: StructureElement.Loci, b: StructureElement.Loci, c: StructureElement.Loci, d: StructureElement.Loci,
+        options?: StructureMeasurementManagerAddOptions & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsDihedral3D>> }) {
         const cellA = this.plugin.helpers.substructureParent.get(a.structure);
         const cellB = this.plugin.helpers.substructureParent.get(b.structure);
         const cellC = this.plugin.helpers.substructureParent.get(c.structure);
@@ -181,15 +186,17 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
             .apply(StateTransforms.Representation.StructureSelectionsDihedral3D, {
                 customText: options?.customText || '',
                 textColor: this.state.options.textColor,
-                ...options?.lineParams,
-                ...options?.labelParams
+                ...(options?.lineParams as any),
+                ...options?.labelParams,
+                ...options?.visualParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
     }
 
-    async addLabel(a: StructureElement.Loci, options?: Omit<StructureMeasurementManagerAddOptions, 'customText' | 'lineParams'>) {
+    async addLabel(a: StructureElement.Loci,
+        options?: Omit<StructureMeasurementManagerAddOptions, 'customText' | 'lineParams'> & { visualParams?: Partial<StateTransformer.Params<typeof StateTransforms.Representation.StructureSelectionsLabel3D>> }) {
         const cellA = this.plugin.helpers.substructureParent.get(a.structure);
 
         if (!cellA) return;
@@ -207,7 +214,8 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
             }, { dependsOn, tags: options?.selectionTags })
             .apply(StateTransforms.Representation.StructureSelectionsLabel3D, {
                 textColor: this.state.options.textColor,
-                ...options?.labelParams
+                ...options?.labelParams,
+                ...options?.visualParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;

+ 2 - 2
src/mol-plugin-ui/skin/base/components/controls.scss

@@ -94,7 +94,7 @@
         left: 18px;
         bottom: 0;
         right: 62px;
-        display: grid;
+        display: flex;
     }
     > div:last-child {
         position: absolute;
@@ -137,7 +137,7 @@
         left: 35px;
         bottom: 0;
         right: 37px;
-        display: grid;
+        display: flex;
     }
     > div:last-child {
         position: absolute;

+ 5 - 0
src/mol-plugin-ui/skin/base/components/misc.scss

@@ -609,4 +609,9 @@
             margin-right: 0;
         }
     }
+}
+
+.msp-list-unstyled {
+    padding-left: 0;
+    list-style: none;
 }

+ 2 - 33
src/mol-plugin-ui/viewport/canvas.tsx

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 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>
@@ -7,11 +7,6 @@
 
 import * as React from 'react';
 import { PluginUIComponent } from '../base';
-import { resizeCanvas } from '../../mol-canvas3d/util';
-import { Subject } from 'rxjs';
-import { debounceTime } from 'rxjs/internal/operators/debounceTime';
-import { PluginConfig } from '../../mol-plugin/config';
-import { Color } from '../../mol-util/color';
 
 interface ViewportCanvasState {
     noWebGl: boolean
@@ -41,39 +36,13 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View
         this.setState({ showLogo: !this.plugin.canvas3d?.reprCount.value });
     }
 
-    private handleResize = () => {
-        const container = this.container.current;
-        const canvas = this.canvas.current;
-        if (container && canvas) {
-            const pixelScale = this.plugin.config.get(PluginConfig.General.PixelScale) || 1;
-            resizeCanvas(canvas, container, pixelScale);
-            const [r, g, b] = Color.toRgbNormalized(this.plugin.canvas3d!.props.renderer.backgroundColor);
-            const a = this.plugin.canvas3d!.props.transparentBackground ? 0 : 1;
-            this.plugin.canvas3d!.webgl.clear(r, g, b, a);
-            this.plugin.canvas3d!.handleResize();
-        }
-    }
-
     componentDidMount() {
         if (!this.canvas.current || !this.container.current || !this.plugin.initViewer(this.canvas.current!, this.container.current!)) {
             this.setState({ noWebGl: true });
             return;
         }
         this.handleLogo();
-        this.handleResize();
-
-        const canvas3d = this.plugin.canvas3d!;
-        this.subscribe(canvas3d.reprCount, this.handleLogo);
-
-        const resized = new Subject();
-        const resize = () => resized.next();
-
-        this.subscribe(resized.pipe(debounceTime(1000 / 24)), () => this.handleResize());
-        this.subscribe(canvas3d.input.resize, resize);
-        this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.interaction.click.next(e));
-        this.subscribe(canvas3d.interaction.drag, e => this.plugin.behaviors.interaction.drag.next(e));
-        this.subscribe(canvas3d.interaction.hover, e => this.plugin.behaviors.interaction.hover.next(e));
-        this.subscribe(this.plugin.layout.events.updated, resize);
+        this.subscribe(this.plugin.canvas3d!.reprCount, this.handleLogo);
     }
 
     componentWillUnmount() {

+ 4 - 2
src/mol-plugin/config.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 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>
@@ -24,8 +24,10 @@ export const PluginConfig = {
     General: {
         IsBusyTimeoutMs: item('plugin-config.is-busy-timeout', 750),
         DisableAntialiasing: item('plugin-config.disable-antialiasing', false),
+        DisablePreserveDrawingBuffer: item('plugin-config.disable-preserve-drawing-buffer', false),
         PixelScale: item('plugin-config.pixel-scale', 1),
-        EnableWboit: item('plugin-config.enable-wboit', true)
+        PickScale: item('plugin-config.pick-scale', 0.25),
+        EnableWboit: item('plugin-config.enable-wboit', true),
     },
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),

+ 46 - 13
src/mol-plugin/context.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -7,8 +7,8 @@
 
 import produce, { setAutoFreeze } from 'immer';
 import { List } from 'immutable';
-import { merge } from 'rxjs';
-import { Canvas3D, DefaultCanvas3DParams } from '../mol-canvas3d/canvas3d';
+import { merge, Subscription } from 'rxjs';
+import { Canvas3D, Canvas3DContext, DefaultCanvas3DParams } from '../mol-canvas3d/canvas3d';
 import { CustomProperty } from '../mol-model-props/common/custom-property';
 import { Model, Structure } from '../mol-model/structure';
 import { DataBuilder } from '../mol-plugin-state/builder/data';
@@ -61,6 +61,7 @@ import { VolumeHierarchyManager } from '../mol-plugin-state/manager/volume/hiera
 import { filter, take } from 'rxjs/operators';
 import { Vec2 } from '../mol-math/linear-algebra';
 import { PluginAnimationLoop } from './animation-loop';
+import { resizeCanvas } from '../mol-canvas3d/util';
 
 export class PluginContext {
     runTask = <T>(task: Task<T>, params?: { useOverlay?: boolean }) => this.managers.task.run(task, params);
@@ -70,6 +71,8 @@ export class PluginContext {
         return object;
     }
 
+    private subs: Subscription[] = [];
+
     private disposed = false;
     private ev = RxEventHelper.create();
 
@@ -104,6 +107,7 @@ export class PluginContext {
         }
     } as const;
 
+    readonly canvas3dContext: Canvas3DContext | undefined;
     readonly canvas3d: Canvas3D | undefined;
     readonly animationLoop = new PluginAnimationLoop(this);
     readonly layout = new PluginLayout(this);
@@ -189,9 +193,12 @@ export class PluginContext {
             if (this.spec.layout && this.spec.layout.initial) this.layout.setProps(this.spec.layout.initial);
 
             const antialias = !(this.config.get(PluginConfig.General.DisableAntialiasing) ?? false);
+            const preserveDrawingBuffer = !(this.config.get(PluginConfig.General.DisablePreserveDrawingBuffer) ?? false);
             const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1;
+            const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25;
             const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false;
-            (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas, {}, { antialias, pixelScale, enableWboit });
+            (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit });
+            (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!);
             this.canvas3dInit.next(true);
             let props = this.spec.components?.viewport?.canvas3d;
 
@@ -209,6 +216,15 @@ export class PluginContext {
             }
             this.animationLoop.start();
             (this.helpers.viewportScreenshot as ViewportScreenshotHelper) = new ViewportScreenshotHelper(this);
+
+            this.subs.push(this.canvas3d!.interaction.click.subscribe(e => this.behaviors.interaction.click.next(e)));
+            this.subs.push(this.canvas3d!.interaction.drag.subscribe(e => this.behaviors.interaction.drag.next(e)));
+            this.subs.push(this.canvas3d!.interaction.hover.subscribe(e => this.behaviors.interaction.hover.next(e)));
+            this.subs.push(this.canvas3d!.input.resize.subscribe(() => this.handleResize()));
+            this.subs.push(this.layout.events.updated.subscribe(() => requestAnimationFrame(() => this.handleResize())));
+
+            this.handleResize();
+
             return true;
         } catch (e) {
             this.log.error('' + e);
@@ -217,6 +233,16 @@ export class PluginContext {
         }
     }
 
+    handleResize() {
+        const canvas = this.canvas3dContext?.canvas;
+        const container = this.layout.root;
+        if (container && canvas) {
+            const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1;
+            resizeCanvas(canvas, container, pixelScale);
+            this.canvas3d?.requestResize();
+        }
+    }
+
     readonly log = {
         entries: List<LogEntry>(),
         entry: (e: LogEntry) => this.events.log.next(e),
@@ -254,10 +280,17 @@ export class PluginContext {
         return PluginCommands.State.RemoveObject(this, { state: this.state.data, ref: StateTransform.RootRef });
     }
 
-    dispose() {
+    dispose(options?: { doNotForceWebGLContextLoss?: boolean }) {
         if (this.disposed) return;
+
+        for (const s of this.subs) {
+            s.unsubscribe();
+        }
+        this.subs = [];
+
         this.commands.dispose();
         this.canvas3d?.dispose();
+        this.canvas3dContext?.dispose(options);
         this.ev.dispose();
         this.state.dispose();
         this.managers.task.dispose();
@@ -271,9 +304,9 @@ export class PluginContext {
     }
 
     private initBehaviorEvents() {
-        merge(this.state.data.behaviors.isUpdating, this.state.behaviors.behaviors.isUpdating).subscribe(u => {
+        this.subs.push(merge(this.state.data.behaviors.isUpdating, this.state.behaviors.behaviors.isUpdating).subscribe(u => {
             if (this.behaviors.state.isUpdating.value !== u) this.behaviors.state.isUpdating.next(u);
-        });
+        }));
 
         const timeoutMs = this.config.get(PluginConfig.General.IsBusyTimeoutMs) || 750;
         const isBusy = this.behaviors.state.isBusy;
@@ -287,7 +320,7 @@ export class PluginContext {
             timeout = void 0;
         };
 
-        merge(this.behaviors.state.isUpdating, this.behaviors.state.isAnimating).subscribe(v => {
+        this.subs.push(merge(this.behaviors.state.isUpdating, this.behaviors.state.isAnimating).subscribe(v => {
             const isUpdating = this.behaviors.state.isUpdating.value;
             const isAnimating = this.behaviors.state.isAnimating.value;
 
@@ -300,13 +333,13 @@ export class PluginContext {
                 reset();
                 isBusy.next(false);
             }
-        });
+        }));
 
-        this.behaviors.interaction.selectionMode.subscribe(v => {
+        this.subs.push(this.behaviors.interaction.selectionMode.subscribe(v => {
             if (!v) {
                 this.managers.interactivity?.lociSelects.deselectAll();
             }
-        });
+        }));
     }
 
     private initBuiltInBehavior() {
@@ -315,7 +348,7 @@ export class PluginContext {
         BuiltInPluginBehaviors.Camera.registerDefault(this);
         BuiltInPluginBehaviors.Misc.registerDefault(this);
 
-        merge(this.state.data.events.log, this.state.behaviors.events.log).subscribe(e => this.events.log.next(e));
+        this.subs.push(merge(this.state.data.events.log, this.state.behaviors.events.log).subscribe(e => this.events.log.next(e)));
     }
 
     private async initBehaviors() {
@@ -374,7 +407,7 @@ export class PluginContext {
     }
 
     async init() {
-        this.events.log.subscribe(e => this.log.entries = this.log.entries.push(e));
+        this.subs.push(this.events.log.subscribe(e => this.log.entries = this.log.entries.push(e)));
 
         this.initCustomFormats();
         this.initBehaviorEvents();

+ 1 - 1
src/mol-plugin/layout.ts

@@ -72,7 +72,7 @@ export class PluginLayout extends StatefulPluginComponent<PluginLayoutStateProps
         this.events.updated.next();
     }
 
-    private root: HTMLElement | undefined;
+    root: HTMLElement | undefined;
     private rootState: RootState | undefined = void 0;
     private expandedViewport: HTMLMetaElement;
 

+ 30 - 4
src/mol-repr/shape/loci/dihedral.ts

@@ -53,6 +53,11 @@ const ExtendersParams = {
 };
 type ExtendersParams = typeof ExtendersParams
 
+const ArmsParams = {
+    ...LinesParams
+};
+type ArmsParams = typeof ArmsParams
+
 const ArcParams = {
     ...LinesParams
 };
@@ -70,6 +75,7 @@ const DihedralVisuals = {
     'vectors': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, VectorsParams>) => ShapeRepresentation(getVectorsShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'extenders': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ExtendersParams>) => ShapeRepresentation(getExtendersShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'connector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ExtendersParams>) => ShapeRepresentation(getConnectorShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
+    'arms': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ArmsParams>) => ShapeRepresentation(getArmsShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'arc': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ArcParams>) => ShapeRepresentation(getArcShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'sector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, SectorParams>) => ShapeRepresentation(getSectorShape, Mesh.Utils, { modifyProps: p => ({ ...p, alpha: p.sectorOpacity }), modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) }),
     'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, LociLabelTextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, markerActions: MarkerAction.None }) }),
@@ -78,10 +84,11 @@ const DihedralVisuals = {
 export const DihedralParams = {
     ...VectorsParams,
     ...ExtendersParams,
+    ...ArmsParams,
     ...ArcParams,
     ...SectorParams,
     ...LociLabelTextParams,
-    visuals: PD.MultiSelect(['extenders', 'sector', 'text'], PD.objectToOptions(DihedralVisuals)),
+    visuals: PD.MultiSelect(['extenders', 'arms', 'sector', 'text'], PD.objectToOptions(DihedralVisuals)),
 };
 export type DihedralParams = typeof DihedralParams
 export type DihedralProps = PD.Values<DihedralParams>
@@ -155,12 +162,12 @@ function setDihedralState(quad: Loci.Bundle<4>, state: DihedralState, arcScale:
 
     Vec3.matchDirection(tmpVec, arcNormal, Vec3.sub(tmpVec, arcPointA, sphereA.center));
     const angleA = Vec3.angle(dirBA, tmpVec);
-    const lenA = radius / Math.cos(angleA > halfPI ? angleA - halfPI : angleA);
+    const lenA = radius / Math.cos(angleA - halfPI);
     Vec3.add(projA, sphereB.center, Vec3.setMagnitude(tmpVec, dirBA, lenA));
 
     Vec3.matchDirection(tmpVec, arcNormal, Vec3.sub(tmpVec, arcPointD, sphereD.center));
     const angleD = Vec3.angle(dirCD, tmpVec);
-    const lenD = radius / Math.cos(angleD > halfPI ? angleD - halfPI : angleD);
+    const lenD = radius / Math.cos(angleD - halfPI);
     Vec3.add(projD, sphereC.center, Vec3.setMagnitude(tmpVec, dirCD, lenD));
 
     return state;
@@ -221,6 +228,24 @@ function getConnectorShape(ctx: RuntimeContext, data: DihedralData, props: Dihed
 
 //
 
+function buildArmsLines(data: DihedralData, props: DihedralProps, lines?: Lines): Lines {
+    const builder = LinesBuilder.create(128, 64, lines);
+    for (let i = 0, il = data.quads.length; i < il; ++i) {
+        setDihedralState(data.quads[i], tmpState, props.arcScale);
+        builder.addFixedLengthDashes(tmpState.sphereB.center, tmpState.sphereA.center, props.dashLength, i);
+        builder.addFixedLengthDashes(tmpState.sphereC.center, tmpState.sphereD.center, props.dashLength, i);
+    }
+    return builder.getLines();
+}
+
+function getArmsShape(ctx: RuntimeContext, data: DihedralData, props: DihedralProps, shape?: Shape<Lines>) {
+    const lines = buildArmsLines(data, props, shape && shape.geometry);
+    const name = getDihedralName(data);
+    return Shape.create(name, data, lines, () => props.color, () => props.linesSize, () => '');
+}
+
+//
+
 function buildExtendersLines(data: DihedralData, props: DihedralProps, lines?: Lines): Lines {
     const builder = LinesBuilder.create(128, 64, lines);
     for (let i = 0, il = data.quads.length; i < il; ++i) {
@@ -299,7 +324,8 @@ function buildText(data: DihedralData, props: DihedralProps, text?: Text): Text
         Vec3.setMagnitude(tmpVec, tmpVec, tmpState.radius);
         Vec3.add(tmpVec, tmpState.arcCenter, tmpVec);
 
-        const angle = radToDeg(tmpState.angle).toFixed(2);
+        let angle = radToDeg(tmpState.angle).toFixed(2);
+        if (angle === '-0.00') angle = '0.00';
         const label =  props.customText || `${angle}\u00B0`;
         const radius = Math.max(2, tmpState.sphereA.radius, tmpState.sphereB.radius, tmpState.sphereC.radius, tmpState.sphereD.radius);
         const scale = radius / 2;

+ 1 - 1
src/mol-repr/structure/visual/gaussian-surface-mesh.ts

@@ -157,7 +157,7 @@ async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit,
     // ctx.webgl.waitForGpuCommandsCompleteSync();
     // console.timeEnd('createIsosurfaceBuffers');
 
-    const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure));
+    const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure));
     const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, boundingSphere, textureMesh);
     // console.log({
     //     renderables: ctx.webgl.namedComputeRenderables,

+ 2 - 121
src/mol-repr/volume/direct-volume.ts

@@ -21,15 +21,7 @@ import { RepresentationContext, RepresentationParamsGetter } from '../representa
 import { Interval } from '../../mol-data/int';
 import { Loci, EmptyLoci } from '../../mol-model/loci';
 import { PickingId } from '../../mol-geo/geometry/picking';
-import { eachVolumeLoci } from './util';
-
-// avoiding namespace lookup improved performance in Chrome (Aug 2020)
-const v3set = Vec3.set;
-const v3normalize = Vec3.normalize;
-const v3sub = Vec3.sub;
-const v3addScalar = Vec3.addScalar;
-const v3scale = Vec3.scale;
-const v3toArray = Vec3.toArray;
+import { createVolumeTexture2d, createVolumeTexture3d, eachVolumeLoci } from './util';
 
 function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
     const bbox = Box3D();
@@ -40,75 +32,9 @@ function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
 
 // 2d volume texture
 
-function getVolumeTexture2dLayout(dim: Vec3, maxTextureSize: number) {
-    let width = 0;
-    let height = dim[1];
-    let rows = 1;
-    let columns = dim[0];
-    if (maxTextureSize < dim[0] * dim[2]) {
-        columns =  Math.floor(maxTextureSize / dim[0]);
-        rows = Math.ceil(dim[2] / columns);
-        width = columns * dim[0];
-        height *= rows;
-    } else {
-        width = dim[0] * dim[2];
-    }
-    return { width, height, columns, rows };
-}
-
-function createVolumeTexture2d(volume: Volume, maxTextureSize: number) {
-    const { cells: { space, data }, stats: { max, min } } = volume.grid;
-    const dim = space.dimensions as Vec3;
-    const { dataOffset: o } = space;
-    const { width, height } = getVolumeTexture2dLayout(dim, maxTextureSize);
-
-    const array = new Uint8Array(width * height * 4);
-    const textureImage = { array, width, height };
-
-    const diff = max - min;
-    const [ xn, yn, zn ] = dim;
-
-    const n0 = Vec3();
-    const n1 = Vec3();
-
-    const xn1 = xn - 1;
-    const yn1 = yn - 1;
-    const zn1 = zn - 1;
-
-    for (let z = 0; z < zn; ++z) {
-        for (let y = 0; y < yn; ++y) {
-            for (let x = 0; x < xn; ++x) {
-                const column = Math.floor(((z * xn) % width) / xn);
-                const row = Math.floor((z * xn) / width);
-                const px = column * xn + x;
-                const index = 4 * ((row * yn * width) + (y * width) + px);
-                const offset = o(x, y, z);
-
-                v3set(n0,
-                    data[o(Math.max(0, x - 1), y, z)],
-                    data[o(x, Math.max(0, y - 1), z)],
-                    data[o(x, y, Math.max(0, z - 1))]
-                );
-                v3set(n1,
-                    data[o(Math.min(xn1, x + 1), y, z)],
-                    data[o(x, Math.min(yn1, y + 1), z)],
-                    data[o(x, y, Math.min(zn1, z + 1))]
-                );
-                v3normalize(n0, v3sub(n0, n0, n1));
-                v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
-                v3toArray(v3scale(n0, n0, 255), array, index);
-
-                array[index + 3] = ((data[offset] - min) / diff) * 255;
-            }
-        }
-    }
-
-    return textureImage;
-}
-
 export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
     const gridDimension = volume.grid.cells.space.dimensions as Vec3;
-    const textureImage = createVolumeTexture2d(volume, webgl.maxTextureSize);
+    const textureImage = createVolumeTexture2d(volume, 'normals');
     // debugTexture(createImageData(textureImage.array, textureImage.width, textureImage.height), 1/3)
     const transform = Grid.getGridToCartesianTransform(volume.grid);
     const bbox = getBoundingBox(gridDimension, transform);
@@ -123,51 +49,6 @@ export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, v
 
 // 3d volume texture
 
-function createVolumeTexture3d(volume: Volume) {
-    const { cells: { space, data }, stats: { max, min } } = volume.grid;
-    const [ width, height, depth ] = space.dimensions as Vec3;
-    const { dataOffset: o } = space;
-
-    const array = new Uint8Array(width * height * depth * 4);
-    const textureVolume = { array, width, height, depth };
-    const diff = max - min;
-
-    const n0 = Vec3();
-    const n1 = Vec3();
-
-    const width1 = width - 1;
-    const height1 = height - 1;
-    const depth1 = depth - 1;
-
-    let i = 0;
-    for (let z = 0; z < depth; ++z) {
-        for (let y = 0; y < height; ++y) {
-            for (let x = 0; x < width; ++x) {
-                const offset = o(x, y, z);
-
-                v3set(n0,
-                    data[o(Math.max(0, x - 1), y, z)],
-                    data[o(x, Math.max(0, y - 1), z)],
-                    data[o(x, y, Math.max(0, z - 1))]
-                );
-                v3set(n1,
-                    data[o(Math.min(width1, x + 1), y, z)],
-                    data[o(x, Math.min(height1, y + 1), z)],
-                    data[o(x, y, Math.min(depth1, z + 1))]
-                );
-                v3normalize(n0, v3sub(n0, n0, n1));
-                v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
-                v3toArray(v3scale(n0, n0, 255), array, i);
-
-                array[i + 3] = ((data[offset] - min) / diff) * 255;
-                i += 4;
-            }
-        }
-    }
-
-    return textureVolume;
-}
-
 function getUnitToCartn(grid: Grid) {
     if (grid.transform.kind === 'matrix') {
         return {

+ 83 - 3
src/mol-repr/volume/isosurface.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -20,9 +20,13 @@ import { RepresentationContext, RepresentationParamsGetter, Representation } fro
 import { PickingId } from '../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from '../../mol-model/loci';
 import { Interval } from '../../mol-data/int';
-import { Tensor } from '../../mol-math/linear-algebra';
+import { Tensor, Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { fillSerial } from '../../mol-util/array';
-import { eachVolumeLoci } from './util';
+import { createVolumeTexture2d, eachVolumeLoci, getVolumeTexture2dLayout } from './util';
+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';
 
 export const VolumeIsosurfaceParams = {
     isoValue: Volume.IsoValueParam
@@ -91,6 +95,80 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
 
 //
 
+async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) {
+    if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
+
+    const { resources } = ctx.webgl;
+    if (!volume._propertyData['texture2d']) {
+        // TODO: handle disposal
+        volume._propertyData['texture2d'] = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+    }
+    const texture = volume._propertyData['texture2d'];
+
+    const padding = 1;
+    const transform = Grid.getGridToCartesianTransform(volume.grid);
+    const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions as Vec3);
+    const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, padding);
+    const gridTexDim = Vec3.create(width, height, 0);
+    const gridTexScale = Vec2.create(width / texDim, height / texDim);
+    // console.log({ texDim, width, height, gridDimension });
+
+    if (!textureMesh) {
+        // set to power-of-two size required for histopyramid calculation
+        texture.define(texDim, texDim);
+        // load volume into sub-section of texture
+        texture.load(createVolumeTexture2d(volume, 'groups', padding), true);
+    }
+
+    const { max, min } = volume.grid.stats;
+    const diff = max - min;
+    const value = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue;
+    const isoLevel = ((value - min) / diff);
+
+    gridDimension[0] += padding;
+    gridDimension[1] += padding;
+
+    // console.time('calcActiveVoxels');
+    const activeVoxelsTex = calcActiveVoxels(ctx.webgl, texture, gridDimension, gridTexDim, isoLevel, gridTexScale);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('calcActiveVoxels');
+
+    // console.time('createHistogramPyramid');
+    const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, gridTexScale, gridTexDim);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createHistogramPyramid');
+
+    // console.time('createIsosurfaceBuffers');
+    const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, texture, compacted, gridDimension, gridTexDim, transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createIsosurfaceBuffers');
+
+    const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, Volume.getBoundingSphere(volume), textureMesh);
+    // console.log({
+    //     renderables: ctx.webgl.namedComputeRenderables,
+    //     framebuffers: ctx.webgl.namedFramebuffers,
+    //     textures: ctx.webgl.namedTextures,
+    // });
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    return surface;
+}
+
+export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> {
+    return VolumeVisual<TextureMesh, IsosurfaceMeshParams>({
+        defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
+        createGeometry: createVolumeIsosurfaceTextureMesh,
+        createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation),
+        getLoci: getIsosurfaceLoci,
+        eachLocation: eachIsosurface,
+        setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
+            if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
+        },
+        geometryUtils: TextureMesh.Utils
+    }, materialId);
+}
+
+//
+
 export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, lines?: Lines) {
     ctx.runtime.update({ message: 'Marching cubes...' });
 
@@ -136,6 +214,8 @@ export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<Isos
 
 const IsosurfaceVisuals = {
     'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceMeshVisual, getLoci),
+    // TODO: don't enable yet as it breaks state sessions
+    // 'solid-gpu': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface texture-mesh', ctx, getParams, IsosurfaceTextureMeshVisual, getLoci),
     'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual, getLoci),
 };
 

+ 134 - 1
src/mol-repr/volume/util.ts

@@ -8,6 +8,16 @@ import { Volume } from '../../mol-model/volume';
 import { Loci } from '../../mol-model/loci';
 import { Interval, OrderedSet } from '../../mol-data/int';
 import { equalEps } from '../../mol-math/linear-algebra/3d/common';
+import Vec3 from '../../mol-math/linear-algebra/3d/vec3';
+import { encodeFloatRGBtoArray } from '../../mol-util/float-packing';
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3set = Vec3.set;
+const v3normalize = Vec3.normalize;
+const v3sub = Vec3.sub;
+const v3addScalar = Vec3.addScalar;
+const v3scale = Vec3.scale;
+const v3toArray = Vec3.toArray;
 
 export function eachVolumeLoci(loci: Loci, volume: Volume, isoValue: Volume.IsoValue | undefined, apply: (interval: Interval) => boolean) {
     let changed = false;
@@ -17,7 +27,7 @@ export function eachVolumeLoci(loci: Loci, volume: Volume, isoValue: Volume.IsoV
     } else if (Volume.Isosurface.isLoci(loci)) {
         if (!Volume.areEquivalent(loci.volume, volume)) return false;
         if (isoValue) {
-            if (Volume.IsoValue.areSame(loci.isoValue, isoValue, volume.grid.stats)) return false;
+            if (!Volume.IsoValue.areSame(loci.isoValue, isoValue, volume.grid.stats)) return false;
             if (apply(Interval.ofLength(volume.grid.cells.data.length))) changed = true;
         } else {
             // TODO find a cheaper way?
@@ -41,4 +51,127 @@ export function eachVolumeLoci(loci: Loci, volume: Volume, isoValue: Volume.IsoV
         }
     }
     return changed;
+}
+
+//
+
+export function getVolumeTexture2dLayout(dim: Vec3, padding = 0) {
+    const area = dim[0] * dim[1] * dim[2];
+    const squareDim = Math.sqrt(area);
+    const powerOfTwoSize = Math.pow(2, Math.ceil(Math.log(squareDim) / Math.log(2)));
+
+    let width = dim[0] + padding;
+    let height = dim[1] + padding;
+    let rows = 1;
+    let columns = width;
+    if (powerOfTwoSize < width * dim[2]) {
+        columns =  Math.floor(powerOfTwoSize / width);
+        rows = Math.ceil(dim[2] / columns);
+        width *= columns;
+        height *= rows;
+    } else {
+        width *= dim[2];
+    }
+    return { width, height, columns, rows, powerOfTwoSize: height < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
+}
+
+export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups', padding = 0) {
+    const { cells: { space, data }, stats: { max, min } } = volume.grid;
+    const dim = space.dimensions as Vec3;
+    const { dataOffset: o } = space;
+    const { width, height } = getVolumeTexture2dLayout(dim, padding);
+
+    const array = new Uint8Array(width * height * 4);
+    const textureImage = { array, width, height };
+
+    const diff = max - min;
+    const [ xn, yn, zn ] = dim;
+    const xnp = xn + padding;
+    const ynp = yn + padding;
+
+    const n0 = Vec3();
+    const n1 = Vec3();
+
+    const xn1 = xn - 1;
+    const yn1 = yn - 1;
+    const zn1 = zn - 1;
+
+    for (let z = 0; z < zn; ++z) {
+        for (let y = 0; y < yn; ++y) {
+            for (let x = 0; x < xn; ++x) {
+                const column = Math.floor(((z * xnp) % width) / xnp);
+                const row = Math.floor((z * xnp) / width);
+                const px = column * xnp + x;
+                const index = 4 * ((row * ynp * width) + (y * width) + px);
+                const offset = o(x, y, z);
+
+                if (variant === 'groups') {
+                    encodeFloatRGBtoArray(offset, array, index);
+                } else {
+                    v3set(n0,
+                        data[o(Math.max(0, x - 1), y, z)],
+                        data[o(x, Math.max(0, y - 1), z)],
+                        data[o(x, y, Math.max(0, z - 1))]
+                    );
+                    v3set(n1,
+                        data[o(Math.min(xn1, x + 1), y, z)],
+                        data[o(x, Math.min(yn1, y + 1), z)],
+                        data[o(x, y, Math.min(zn1, z + 1))]
+                    );
+                    v3normalize(n0, v3sub(n0, n0, n1));
+                    v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
+                    v3toArray(v3scale(n0, n0, 255), array, index);
+                }
+
+                array[index + 3] = ((data[offset] - min) / diff) * 255;
+            }
+        }
+    }
+
+    return textureImage;
+}
+
+export function createVolumeTexture3d(volume: Volume) {
+    const { cells: { space, data }, stats: { max, min } } = volume.grid;
+    const [ width, height, depth ] = space.dimensions as Vec3;
+    const { dataOffset: o } = space;
+
+    const array = new Uint8Array(width * height * depth * 4);
+    const textureVolume = { array, width, height, depth };
+    const diff = max - min;
+
+    const n0 = Vec3();
+    const n1 = Vec3();
+
+    const width1 = width - 1;
+    const height1 = height - 1;
+    const depth1 = depth - 1;
+
+    let i = 0;
+    for (let z = 0; z < depth; ++z) {
+        for (let y = 0; y < height; ++y) {
+            for (let x = 0; x < width; ++x) {
+                const offset = o(x, y, z);
+
+                v3set(n0,
+                    data[o(Math.max(0, x - 1), y, z)],
+                    data[o(x, Math.max(0, y - 1), z)],
+                    data[o(x, y, Math.max(0, z - 1))]
+                );
+                v3set(n1,
+                    data[o(Math.min(width1, x + 1), y, z)],
+                    data[o(x, Math.min(height1, y + 1), z)],
+                    data[o(x, y, Math.min(depth1, z + 1))]
+                );
+                v3normalize(n0, v3sub(n0, n0, n1));
+                v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
+                v3toArray(v3scale(n0, n0, 255), array, i);
+
+                array[i + 3] = ((data[offset] - min) / diff) * 255;
+                i += 4;
+            }
+        }
+    }
+
+    return textureVolume;
 }

+ 2 - 2
src/mol-theme/color/chain-id.ts

@@ -84,9 +84,9 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
         const asymIdSerialMap = getAsymIdSerialMap(ctx.structure.root, props.asymId);
 
         const labelTable = Array.from(asymIdSerialMap.keys());
-        props.palette.params.valueLabel = (i: number) => labelTable[i];
+        const valueLabel = (i: number) => labelTable[i];
 
-        const palette = getPalette(asymIdSerialMap.size, props);
+        const palette = getPalette(asymIdSerialMap.size, props, { valueLabel });
         legend = palette.legend;
 
         color = (location: Location): Color => {

+ 2 - 2
src/mol-theme/color/entity-source.ts

@@ -131,9 +131,9 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E
         const { seqToSrcByModelEntity, srcKeySerialMap } = getMaps(models);
 
         const labelTable = getLabelTable(srcKeySerialMap);
-        props.palette.params.valueLabel = (i: number) => labelTable[i];
+        const valueLabel = (i: number) => labelTable[i];
 
-        const palette = getPalette(srcKeySerialMap.size, props);
+        const palette = getPalette(srcKeySerialMap.size, props, { valueLabel });
         legend = palette.legend;
 
         const getSrcColor = (location: StructureElement.Location) => {

+ 6 - 4
src/mol-theme/color/operator-hkl.ts

@@ -85,11 +85,13 @@ export function OperatorHklColorTheme(ctx: ThemeDataContext, props: PD.Values<Op
             else labelTable[i] += `, ${label}`;
         });
 
-        props.palette.params.minLabel = formatHkl(min);
-        props.palette.params.maxLabel = formatHkl(max);
-        props.palette.params.valueLabel = (i: number) => labelTable[i];
+        const labelOptions = {
+            minLabel: formatHkl(min),
+            maxLabel: formatHkl(max),
+            valueLabel: (i: number) => labelTable[i]
+        };
 
-        const palette = getPalette(map.size, props);
+        const palette = getPalette(map.size, props, labelOptions);
         legend = palette.legend;
 
         color = (location: Location): Color => {

+ 2 - 2
src/mol-theme/color/operator-name.ts

@@ -43,9 +43,9 @@ export function OperatorNameColorTheme(ctx: ThemeDataContext, props: PD.Values<O
         const operatorNameSerialMap = getOperatorNameSerialMap(ctx.structure.root);
 
         const labelTable = Array.from(operatorNameSerialMap.keys());
-        props.palette.params.valueLabel = (i: number) => labelTable[i];
+        const valueLabel = (i: number) => labelTable[i];
 
-        const palette = getPalette(operatorNameSerialMap.size, props);
+        const palette = getPalette(operatorNameSerialMap.size, props, { valueLabel });
         legend = palette.legend;
 
         color = (location: Location): Color => {

+ 2 - 2
src/mol-theme/color/polymer-id.ts

@@ -94,9 +94,9 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly
         const polymerAsymIdSerialMap = getPolymerAsymIdSerialMap(ctx.structure.root);
 
         const labelTable = Array.from(polymerAsymIdSerialMap.keys());
-        props.palette.params.valueLabel = (i: number) => labelTable[i];
+        const valueLabel = (i: number) => labelTable[i];
 
-        const palette = getPalette(polymerAsymIdSerialMap.size, props);
+        const palette = getPalette(polymerAsymIdSerialMap.size, props, { valueLabel });
         legend = palette.legend;
 
         color = (location: Location): Color => {

+ 12 - 12
src/mol-util/color/palette.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -20,22 +20,14 @@ const DefaultGetPaletteProps = {
 };
 type GetPaletteProps = typeof DefaultGetPaletteProps
 
-const LabelParams = {
-    valueLabel: PD.Value((i: number) => `${i + 1}`, { isHidden: true }),
-    minLabel: PD.Value('Start', { isHidden: true }),
-    maxLabel: PD.Value('End', { isHidden: true })
-};
-
 export function getPaletteParams(props: Partial<GetPaletteProps> = {}) {
     const p = { ...DefaultGetPaletteProps, ...props };
     return {
         palette: PD.MappedStatic(p.type, {
             colors: PD.Group({
-                ...LabelParams,
                 list: PD.ColorList(p.colorList),
             }, { isFlat: true }),
             generate: PD.Group({
-                ...LabelParams,
                 ...DistinctColorsParams,
                 maxCount: PD.Numeric(75, { min: 1, max: 250, step: 1 }),
             }, { isFlat: true })
@@ -51,18 +43,26 @@ export function getPaletteParams(props: Partial<GetPaletteProps> = {}) {
 const DefaultPaletteProps = PD.getDefaultValues(getPaletteParams());
 type PaletteProps = typeof DefaultPaletteProps
 
+const DefaultLabelOptions = {
+    valueLabel: (i: number) => `${i + 1}`,
+    minLabel: 'Start',
+    maxLabel: 'End'
+};
+type LabelOptions = typeof DefaultLabelOptions
+
 export interface Palette {
     color: (i: number) => Color
     legend?: TableLegend | ScaleLegend
 }
 
-export function getPalette(count: number, props: PaletteProps) {
+export function getPalette(count: number, props: PaletteProps, labelOptions: Partial<LabelOptions> = {}) {
     let color: (i: number) => Color;
     let legend: ScaleLegend | TableLegend | undefined;
 
     if (props.palette.name === 'colors' && props.palette.params.list.kind === 'interpolate') {
-        const { list, minLabel, maxLabel } = props.palette.params;
+        const { list } = props.palette.params;
         const domain: [number, number] = [0, count - 1];
+        const { minLabel, maxLabel } = { ...DefaultLabelOptions, ...labelOptions };
 
         let colors = list.colors;
         if (colors.length === 0) colors = getColorListFromName(DefaultGetPaletteProps.colorList).list;
@@ -79,7 +79,7 @@ export function getPalette(count: number, props: PaletteProps) {
             count = Math.min(count, props.palette.params.maxCount);
             colors = distinctColors(count, props.palette.params);
         }
-        const valueLabel = props.palette.params.valueLabel || (i => '' + i);
+        const valueLabel = labelOptions.valueLabel ?? DefaultLabelOptions.valueLabel;
         const colorsLength = colors.length;
         const table: [string, Color][] = [];
         for (let i = 0; i < count; ++i) {

+ 4 - 4
src/mol-util/param-definition.ts

@@ -84,16 +84,16 @@ export namespace ParamDefinition {
         return setInfo<Select<T>>({ type: 'select', defaultValue: checkDefaultKey(defaultValue, options), options, cycle: info?.cycle }, info);
     }
 
-    export interface MultiSelect<E extends string, T = E[]> extends Base<T> {
+    export interface MultiSelect<E extends string> extends Base<E[]> {
         type: 'multi-select'
         /** array of (value, label) tuples */
         options: readonly (readonly [E, string])[],
         emptyValue?: string,
-        _optionSet?: Set<T>
+        _optionSet?: Set<E>
     }
-    export function MultiSelect<E extends string, T = E[]>(defaultValue: T, options: readonly (readonly [E, string])[], info?: Info & { emptyValue?: string }): MultiSelect<E, T> {
+    export function MultiSelect<E extends string>(defaultValue: E[], options: readonly (readonly [E, string])[], info?: Info & { emptyValue?: string }): MultiSelect<E> {
         // TODO: check if default value is a subset of options?
-        const ret = setInfo<MultiSelect<E, T>>({ type: 'multi-select', defaultValue, options }, info);
+        const ret = setInfo<MultiSelect<E>>({ type: 'multi-select', defaultValue, options }, info);
         if (info?.emptyValue) ret.emptyValue = info.emptyValue;
         return ret;
     }

+ 2 - 2
src/tests/browser/marching-cubes.ts

@@ -6,7 +6,7 @@
 
 import './index.html';
 import { resizeCanvas } from '../../mol-canvas3d/util';
-import { Canvas3DParams, Canvas3D } from '../../mol-canvas3d/canvas3d';
+import { Canvas3DParams, Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { ColorNames } from '../../mol-util/color/names';
 import { PositionData, Box3D, Sphere3D } from '../../mol-math/geometry';
 import { OrderedSet } from '../../mol-data/int';
@@ -31,7 +31,7 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.fromCanvas(canvas, PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
     renderer: { backgroundColor: ColorNames.white },
     camera: { mode: 'orthographic' }
 }));

+ 2 - 2
src/tests/browser/render-lines.ts

@@ -6,7 +6,7 @@
 
 import './index.html';
 import { resizeCanvas } from '../../mol-canvas3d/util';
-import { Canvas3D } from '../../mol-canvas3d/canvas3d';
+import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
 import { Mat4 } from '../../mol-math/linear-algebra';
 import { DodecahedronCage } from '../../mol-geo/primitive/dodecahedron';
@@ -23,7 +23,7 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.fromCanvas(canvas);
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
 canvas3d.animate();
 
 function linesRepr() {

+ 2 - 2
src/tests/browser/render-mesh.ts

@@ -6,7 +6,7 @@
 
 import './index.html';
 import { resizeCanvas } from '../../mol-canvas3d/util';
-import { Canvas3D } from '../../mol-canvas3d/canvas3d';
+import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
 import { Mat4 } from '../../mol-math/linear-algebra';
 import { HexagonalPrismCage } from '../../mol-geo/primitive/prism';
@@ -24,7 +24,7 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.fromCanvas(canvas);
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
 canvas3d.animate();
 
 function meshRepr() {

+ 2 - 2
src/tests/browser/render-shape.ts

@@ -7,7 +7,7 @@
 import './index.html';
 import { resizeCanvas } from '../../mol-canvas3d/util';
 import { Representation } from '../../mol-repr/representation';
-import { Canvas3D } from '../../mol-canvas3d/canvas3d';
+import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { lociLabel } from '../../mol-theme/label';
 import { MarkerAction } from '../../mol-util/marker-action';
 import { EveryLoci } from '../../mol-model/loci';
@@ -38,7 +38,7 @@ info.style.color = 'white';
 parent.appendChild(info);
 
 let prevReprLoci = Representation.Loci.Empty;
-const canvas3d = Canvas3D.fromCanvas(canvas);
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
 canvas3d.animate();
 canvas3d.input.move.subscribe(({x, y}) => {
     const pickingId = canvas3d.identify(x, y)?.id;

+ 2 - 2
src/tests/browser/render-spheres.ts

@@ -6,7 +6,7 @@
 
 import './index.html';
 import { resizeCanvas } from '../../mol-canvas3d/util';
-import { Canvas3D } from '../../mol-canvas3d/canvas3d';
+import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
 import { Color } from '../../mol-util/color';
@@ -21,7 +21,7 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.fromCanvas(canvas);
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
 canvas3d.animate();
 
 function spheresRepr() {

+ 2 - 2
src/tests/browser/render-structure.ts

@@ -5,7 +5,7 @@
  */
 
 import './index.html';
-import { Canvas3D } from '../../mol-canvas3d/canvas3d';
+import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { CIF, CifFrame } from '../../mol-io/reader/cif';
 import { Model, Structure } from '../../mol-model/structure';
 import { ColorTheme } from '../../mol-theme/color';
@@ -37,7 +37,7 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.fromCanvas(canvas);
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
 canvas3d.animate();
 
 const info = document.createElement('div');

+ 2 - 2
src/tests/browser/render-text.ts

@@ -5,7 +5,7 @@
  */
 
 import './index.html';
-import { Canvas3D } from '../../mol-canvas3d/canvas3d';
+import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { TextBuilder } from '../../mol-geo/geometry/text/text-builder';
 import { Text } from '../../mol-geo/geometry/text/text';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
@@ -24,7 +24,7 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.fromCanvas(canvas);
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
 canvas3d.animate();
 
 function textRepr() {

+ 1 - 1
webpack.config.common.js

@@ -39,7 +39,7 @@ const sharedConfig = {
         new VersionFile({
             extras: { timestamp: `${new Date().valueOf()}` },
             packageFile: path.resolve(__dirname, 'package.json'),
-            templateString: `export const PLUGIN_VERSION = '<%= package.version %>';\nexport const PLUGIN_VERSION_DATE = new Date(typeof __MOLSTAR_DEBUG_TIMESTAMP__ !== 'undefined' ? __MOLSTAR_DEBUG_TIMESTAMP__ : <%= extras.timestamp %>);`,
+            templateString: `export var PLUGIN_VERSION = '<%= package.version %>';\nexport var PLUGIN_VERSION_DATE = new Date(typeof __MOLSTAR_DEBUG_TIMESTAMP__ !== 'undefined' ? __MOLSTAR_DEBUG_TIMESTAMP__ : <%= extras.timestamp %>);`,
             outputFile: path.resolve(__dirname, 'lib/mol-plugin/version.js')
         })
     ],