Browse Source

Merge pull request #129 from molstar/gpu

Gpu acceleration for isosurface & gaussian-surface
Alexander Rose 4 years ago
parent
commit
78b5c9aac4
41 changed files with 596 additions and 459 deletions
  1. 10 16
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  2. 40 20
      src/mol-gl/compute/histogram-pyramid/reduction.ts
  3. 19 10
      src/mol-gl/compute/histogram-pyramid/sum.ts
  4. 5 4
      src/mol-gl/compute/marching-cubes/active-voxels.ts
  5. 77 81
      src/mol-gl/compute/marching-cubes/isosurface.ts
  6. 5 6
      src/mol-gl/renderable/texture-mesh.ts
  7. 2 2
      src/mol-gl/shader/chunks/assign-color-varying.glsl.ts
  8. 1 2
      src/mol-gl/shader/chunks/assign-group.glsl.ts
  9. 1 1
      src/mol-gl/shader/chunks/assign-position.glsl.ts
  10. 0 8
      src/mol-gl/shader/chunks/color-vert-params.glsl.ts
  11. 7 0
      src/mol-gl/shader/chunks/common-vert-params.glsl.ts
  12. 1 0
      src/mol-gl/shader/chunks/common.glsl.ts
  13. 8 1
      src/mol-gl/shader/chunks/read-from-texture.glsl.ts
  14. 44 20
      src/mol-gl/shader/histogram-pyramid/reduction.frag.ts
  15. 14 6
      src/mol-gl/shader/histogram-pyramid/sum.frag.ts
  16. 119 53
      src/mol-gl/shader/marching-cubes/isosurface.frag.ts
  17. 7 8
      src/mol-gl/shader/mesh.vert.ts
  18. 3 0
      src/mol-gl/webgl/context.ts
  19. 6 1
      src/mol-gl/webgl/render-item.ts
  20. 1 0
      src/mol-gl/webgl/texture.ts
  21. 7 5
      src/mol-math/geometry/gaussian-density/gpu.ts
  22. 1 1
      src/mol-model/structure/structure/structure.ts
  23. 4 4
      src/mol-repr/structure/complex-representation.ts
  24. 1 1
      src/mol-repr/structure/complex-visual.ts
  25. 9 9
      src/mol-repr/structure/units-representation.ts
  26. 1 1
      src/mol-repr/structure/units-visual.ts
  27. 7 7
      src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts
  28. 7 7
      src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts
  29. 9 8
      src/mol-repr/structure/visual/element-sphere.ts
  30. 11 6
      src/mol-repr/structure/visual/gaussian-density-volume.ts
  31. 46 68
      src/mol-repr/structure/visual/gaussian-surface-mesh.ts
  32. 1 1
      src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
  33. 16 5
      src/mol-repr/structure/visual/util/common.ts
  34. 8 13
      src/mol-repr/structure/visual/util/gaussian.ts
  35. 13 11
      src/mol-repr/util.ts
  36. 1 1
      src/mol-repr/visual.ts
  37. 10 1
      src/mol-repr/volume/direct-volume.ts
  38. 40 42
      src/mol-repr/volume/isosurface.ts
  39. 5 5
      src/mol-repr/volume/representation.ts
  40. 26 21
      src/mol-repr/volume/util.ts
  41. 3 3
      src/tests/browser/marching-cubes.ts

+ 10 - 16
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -21,7 +21,6 @@ import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
 import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Texture } from '../../../mol-gl/webgl/texture';
 import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
-import { fillSerial } from '../../../mol-util/array';
 import { createEmptyClipping } from '../clipping-data';
 import { NullLocation } from '../../../mol-model/location';
 
@@ -34,22 +33,22 @@ export interface TextureMesh {
     groupCount: number,
 
     readonly geoTextureDim: ValueCell<Vec2>,
-    /** texture has vertex positions in XYZ and group id in W */
-    readonly vertexGroupTexture: ValueCell<Texture>,
+    readonly vertexTexture: ValueCell<Texture>,
+    readonly groupTexture: ValueCell<Texture>,
     readonly normalTexture: ValueCell<Texture>,
 
     readonly boundingSphere: Sphere3D
 }
 
 export namespace TextureMesh {
-    export function create(vertexCount: number, groupCount: number, vertexGroupTexture: Texture, normalTexture: Texture, boundingSphere: Sphere3D, textureMesh?: TextureMesh): TextureMesh {
-        const width = vertexGroupTexture.getWidth();
-        const height = vertexGroupTexture.getHeight();
+    export function create(vertexCount: number, groupCount: number, vertexTexture: Texture, groupTexture: Texture, normalTexture: Texture, boundingSphere: Sphere3D, textureMesh?: TextureMesh): TextureMesh {
+        const width = vertexTexture.getWidth();
+        const height = vertexTexture.getHeight();
         if (textureMesh) {
             textureMesh.vertexCount = vertexCount;
             textureMesh.groupCount = groupCount;
             ValueCell.update(textureMesh.geoTextureDim, Vec2.set(textureMesh.geoTextureDim.ref.value, width, height));
-            ValueCell.update(textureMesh.vertexGroupTexture, vertexGroupTexture);
+            ValueCell.update(textureMesh.vertexTexture, vertexTexture);
             ValueCell.update(textureMesh.normalTexture, normalTexture);
             Sphere3D.copy(textureMesh.boundingSphere, boundingSphere);
             return textureMesh;
@@ -59,7 +58,8 @@ export namespace TextureMesh {
                 vertexCount,
                 groupCount,
                 geoTextureDim: ValueCell.create(Vec2.create(width, height)),
-                vertexGroupTexture: ValueCell.create(vertexGroupTexture),
+                vertexTexture: ValueCell.create(vertexTexture),
+                groupTexture: ValueCell.create(groupTexture),
                 normalTexture: ValueCell.create(normalTexture),
                 boundingSphere: Sphere3D.clone(boundingSphere),
             };
@@ -109,11 +109,10 @@ export namespace TextureMesh {
 
         return {
             uGeoTexDim: textureMesh.geoTextureDim,
-            tPositionGroup: textureMesh.vertexGroupTexture,
+            tPosition: textureMesh.vertexTexture,
+            tGroup: textureMesh.groupTexture,
             tNormal: textureMesh.normalTexture,
 
-            // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup
-            aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount))),
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
             uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
@@ -148,11 +147,6 @@ export namespace TextureMesh {
         ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
         ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
         ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
-
-        if (values.drawCount.ref.value > values.aGroup.ref.value.length) {
-            // console.log('updating vertex ids in aGroup to handle larger drawCount')
-            ValueCell.update(values.aGroup, fillSerial(new Float32Array(values.drawCount.ref.value)));
-        }
     }
 
     function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {

+ 40 - 20
src/mol-gl/compute/histogram-pyramid/reduction.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createComputeRenderable } from '../../renderable';
+import { createComputeRenderable, ComputeRenderable } from '../../renderable';
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
 import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
@@ -18,41 +18,46 @@ import { Framebuffer } from '../../../mol-gl/webgl/framebuffer';
 import { isPowerOfTwo } from '../../../mol-math/misc';
 import quad_vert from '../../../mol-gl/shader/quad.vert';
 import reduction_frag from '../../../mol-gl/shader/histogram-pyramid/reduction.frag';
+import { isWebGL2 } from '../../webgl/compat';
 
 const HistopyramidReductionSchema = {
     ...QuadSchema,
+    tInputLevel: TextureSpec('texture', 'rgba', 'float', 'nearest'),
     tPreviousLevel: TextureSpec('texture', 'rgba', 'float', 'nearest'),
     uSize: UniformSpec('f'),
     uTexSize: UniformSpec('f'),
     uFirst: UniformSpec('b'),
 };
+type HistopyramidReductionValues = Values<typeof HistopyramidReductionSchema>
 
 const HistogramPyramidName = 'histogram-pyramid';
 
-function getHistopyramidReductionRenderable(ctx: WebGLContext, initialTexture: Texture) {
+function getHistopyramidReductionRenderable(ctx: WebGLContext, inputLevel: Texture, previousLevel: Texture): ComputeRenderable<HistopyramidReductionValues> {
     if (ctx.namedComputeRenderables[HistogramPyramidName]) {
-        const v = ctx.namedComputeRenderables[HistogramPyramidName].values;
+        const v = ctx.namedComputeRenderables[HistogramPyramidName].values as HistopyramidReductionValues;
 
-        ValueCell.update(v.tPreviousLevel, initialTexture);
+        ValueCell.update(v.tInputLevel, inputLevel);
+        ValueCell.update(v.tPreviousLevel, previousLevel);
 
         ctx.namedComputeRenderables[HistogramPyramidName].update();
     } else {
-        ctx.namedComputeRenderables[HistogramPyramidName] = createHistopyramidReductionRenderable(ctx, initialTexture);
+        ctx.namedComputeRenderables[HistogramPyramidName] = createHistopyramidReductionRenderable(ctx, inputLevel, previousLevel);
     }
     return ctx.namedComputeRenderables[HistogramPyramidName];
 }
 
-function createHistopyramidReductionRenderable(ctx: WebGLContext, initialTexture: Texture) {
-    const values: Values<typeof HistopyramidReductionSchema> = {
+function createHistopyramidReductionRenderable(ctx: WebGLContext, inputLevel: Texture, previousLevel: Texture) {
+    const values: HistopyramidReductionValues = {
         ...QuadValues,
-        tPreviousLevel: ValueCell.create(initialTexture),
+        tInputLevel: ValueCell.create(inputLevel),
+        tPreviousLevel: ValueCell.create(previousLevel),
         uSize: ValueCell.create(0),
         uTexSize: ValueCell.create(0),
         uFirst: ValueCell.create(true),
     };
 
     const schema = { ...HistopyramidReductionSchema };
-    const shaderCode = ShaderCode('reduction', quad_vert, reduction_frag);
+    const shaderCode = ShaderCode('reduction', quad_vert, reduction_frag, {}, { 0: 'ivec4' });
     const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
 
     return createComputeRenderable(renderItem, values);
@@ -64,11 +69,13 @@ function getLevelTextureFramebuffer(ctx: WebGLContext, level: number) {
     let textureFramebuffer = LevelTexturesFramebuffers[level];
     const size = Math.pow(2, level);
     if (textureFramebuffer === undefined) {
-        const texture = getTexture(`level${level}`, ctx, 'image-float32', 'rgba', 'float', 'nearest');
+        const texture = ctx.isWebGL2
+            ? getTexture(`level${level}`, ctx, 'image-int32', 'alpha', 'int', 'nearest')
+            : getTexture(`level${level}`, ctx, 'image-uint8', 'rgba', 'ubyte', 'nearest');
+        texture.define(size, size);
         const framebuffer = getFramebuffer(`level${level}`, ctx);
         texture.attachFramebuffer(framebuffer, 0);
         textureFramebuffer = { texture, framebuffer };
-        textureFramebuffer.texture.define(size, size);
         LevelTexturesFramebuffers[level] = textureFramebuffer;
     }
     return textureFramebuffer;
@@ -122,20 +129,29 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     // This part set the levels
     const levels = Math.ceil(Math.log(w) / Math.log(2));
     const maxSize = Math.pow(2, levels);
-    // console.log('levels', levels, 'maxSize', maxSize, 'input', w);
+    const maxSizeX = Math.pow(2, levels);
+    const maxSizeY = Math.pow(2, levels - 1);
+    // console.log('levels', levels, 'maxSize', maxSize, [maxSizeX, maxSizeY], 'input', w);
 
-    const pyramidTex = getTexture('pyramid', ctx, 'image-float32', 'rgba', 'float', 'nearest');
-    pyramidTex.define(maxSize, maxSize);
+    const pyramidTex = ctx.isWebGL2
+        ? getTexture('pyramid', ctx, 'image-int32', 'alpha', 'int', 'nearest')
+        : getTexture('pyramid', ctx, 'image-uint8', 'rgba', 'ubyte', 'nearest');
+    pyramidTex.define(maxSizeX, maxSizeY);
 
     const framebuffer = getFramebuffer('pyramid', ctx);
     pyramidTex.attachFramebuffer(framebuffer, 0);
-    gl.viewport(0, 0, maxSize, maxSize);
-    gl.clear(gl.COLOR_BUFFER_BIT);
+
+    gl.viewport(0, 0, maxSizeX, maxSizeY);
+    if (isWebGL2(gl)) {
+        gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
+    } else {
+        gl.clear(gl.COLOR_BUFFER_BIT);
+    }
 
     const levelTexturesFramebuffers: TextureFramebuffer[] = [];
     for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i));
 
-    const renderable = getHistopyramidReductionRenderable(ctx, inputTexture);
+    const renderable = getHistopyramidReductionRenderable(ctx, inputTexture, levelTexturesFramebuffers[0].texture);
     ctx.state.currentRenderItemId = -1;
     setRenderingDefaults(ctx);
 
@@ -146,7 +162,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
         tf.framebuffer.bind();
 
         const size = Math.pow(2, currLevel);
-        // console.log('size', size, 'draw-level', currLevel, 'read-level', levels - i)
+        // console.log('size', size, 'draw-level', currLevel, 'read-level', levels - i);
 
         ValueCell.update(renderable.values.uSize, Math.pow(2, i + 1) / maxSize);
         ValueCell.update(renderable.values.uTexSize, size);
@@ -158,7 +174,11 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
         ctx.state.currentRenderItemId = -1;
         gl.viewport(0, 0, size, size);
         gl.scissor(0, 0, size, size);
-        gl.clear(gl.COLOR_BUFFER_BIT);
+        if (isWebGL2(gl)) {
+            gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
+        } else {
+            gl.clear(gl.COLOR_BUFFER_BIT);
+        }
         gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
         renderable.render();
 
@@ -179,7 +199,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     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);
+    // console.log('height', height, 'finalCount', count, 'scale', scale);
 
     return { pyramidTex, count, height, levels, scale };
 }

+ 19 - 10
src/mol-gl/compute/histogram-pyramid/sum.ts

@@ -1,10 +1,10 @@
 /**
- * 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>
  */
 
-import { createComputeRenderable } from '../../renderable';
+import { ComputeRenderable, createComputeRenderable } from '../../renderable';
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
 import { Values, TextureSpec } from '../../renderable/schema';
@@ -15,17 +15,19 @@ import { decodeFloatRGB } from '../../../mol-util/float-packing';
 import { QuadSchema, QuadValues } from '../util';
 import quad_vert from '../../../mol-gl/shader/quad.vert';
 import sum_frag from '../../../mol-gl/shader/histogram-pyramid/sum.frag';
+import { isWebGL2 } from '../../webgl/compat';
 
 const HistopyramidSumSchema = {
     ...QuadSchema,
     tTexture: TextureSpec('texture', 'rgba', 'float', 'nearest'),
 };
+type HistopyramidSumValues = Values<typeof HistopyramidSumSchema>
 
 const HistopyramidSumName = 'histopyramid-sum';
 
-function getHistopyramidSumRenderable(ctx: WebGLContext, texture: Texture) {
+function getHistopyramidSumRenderable(ctx: WebGLContext, texture: Texture): ComputeRenderable<HistopyramidSumValues> {
     if (ctx.namedComputeRenderables[HistopyramidSumName]) {
-        const v = ctx.namedComputeRenderables[HistopyramidSumName].values;
+        const v = ctx.namedComputeRenderables[HistopyramidSumName].values as HistopyramidSumValues;
 
         ValueCell.update(v.tTexture, texture);
 
@@ -37,13 +39,13 @@ function getHistopyramidSumRenderable(ctx: WebGLContext, texture: Texture) {
 }
 
 function createHistopyramidSumRenderable(ctx: WebGLContext, texture: Texture) {
-    const values: Values<typeof HistopyramidSumSchema> = {
+    const values: HistopyramidSumValues = {
         ...QuadValues,
         tTexture: ValueCell.create(texture),
     };
 
     const schema = { ...HistopyramidSumSchema };
-    const shaderCode = ShaderCode('sum', quad_vert, sum_frag);
+    const shaderCode = ShaderCode('sum', quad_vert, sum_frag, {}, { 0: 'ivec4' });
     const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
 
     return createComputeRenderable(renderItem, values);
@@ -60,7 +62,9 @@ function setRenderingDefaults(ctx: WebGLContext) {
     state.clearColor(0, 0, 0, 0);
 }
 
-const sumArray = new Uint8Array(4);
+const sumBytes = new Uint8Array(4);
+const sumInts = new Int32Array(4);
+
 export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
     const { gl, resources } = ctx;
 
@@ -73,7 +77,9 @@ export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture
     const framebuffer = ctx.namedFramebuffers[HistopyramidSumName];
 
     if (!ctx.namedTextures[HistopyramidSumName]) {
-        ctx.namedTextures[HistopyramidSumName] = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        ctx.namedTextures[HistopyramidSumName] = isWebGL2(gl)
+            ? resources.texture('image-int32', 'rgba', 'int', 'nearest')
+            : resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
         ctx.namedTextures[HistopyramidSumName].define(1, 1);
     }
     const sumTexture = ctx.namedTextures[HistopyramidSumName];
@@ -84,8 +90,11 @@ export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture
     gl.viewport(0, 0, 1, 1);
     renderable.render();
     gl.finish();
-    ctx.readPixels(0, 0, 1, 1, sumArray);
+
+    ctx.readPixels(0, 0, 1, 1, isWebGL2(gl) ? sumInts : sumBytes);
     ctx.unbindFramebuffer();
 
-    return decodeFloatRGB(sumArray[0], sumArray[1], sumArray[2]);
+    return isWebGL2(gl)
+        ? sumInts[0]
+        : decodeFloatRGB(sumBytes[0], sumBytes[1], sumBytes[2]);
 }

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

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createComputeRenderable } from '../../renderable';
+import { ComputeRenderable, createComputeRenderable } from '../../renderable';
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
 import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
@@ -29,12 +29,13 @@ const ActiveVoxelsSchema = {
 
     uScale: UniformSpec('v2'),
 };
+type ActiveVoxelsValues = Values<typeof ActiveVoxelsSchema>
 
 const ActiveVoxelsName = 'active-voxels';
 
-function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2) {
+function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2): ComputeRenderable<ActiveVoxelsValues> {
     if (ctx.namedComputeRenderables[ActiveVoxelsName]) {
-        const v = ctx.namedComputeRenderables[ActiveVoxelsName].values;
+        const v = ctx.namedComputeRenderables[ActiveVoxelsName].values as ActiveVoxelsValues;
 
         ValueCell.update(v.uQuadScale, scale);
         ValueCell.update(v.tVolumeData, volumeData);
@@ -51,7 +52,7 @@ function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridD
 }
 
 function createActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2) {
-    const values: Values<typeof ActiveVoxelsSchema> = {
+    const values: ActiveVoxelsValues = {
         ...QuadValues,
         tTriCount: ValueCell.create(getTriCount()),
 

+ 77 - 81
src/mol-gl/compute/marching-cubes/isosurface.ts

@@ -4,19 +4,21 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createComputeRenderable } from '../../renderable';
+import { ComputeRenderable, createComputeRenderable } from '../../renderable';
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
-import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
+import { Values, TextureSpec, UniformSpec, DefineSpec } from '../../renderable/schema';
 import { Texture } from '../../../mol-gl/webgl/texture';
 import { ShaderCode } from '../../../mol-gl/shader-code';
 import { ValueCell } from '../../../mol-util';
 import { Vec3, Vec2, Mat4 } from '../../../mol-math/linear-algebra';
 import { QuadSchema, QuadValues } from '../util';
-import { HistogramPyramid } from '../histogram-pyramid/reduction';
+import { createHistogramPyramid, HistogramPyramid } from '../histogram-pyramid/reduction';
 import { getTriIndices } from './tables';
 import quad_vert from '../../../mol-gl/shader/quad.vert';
 import isosurface_frag from '../../../mol-gl/shader/marching-cubes/isosurface.frag';
+import { calcActiveVoxels } from './active-voxels';
+import { isWebGL2 } from '../../webgl/compat';
 
 const IsosurfaceSchema = {
     ...QuadSchema,
@@ -34,15 +36,17 @@ const IsosurfaceSchema = {
     uGridDim: UniformSpec('v3'),
     uGridTexDim: UniformSpec('v3'),
     uGridTransform: UniformSpec('m4'),
-
     uScale: UniformSpec('v2'),
+
+    dPackedGroup: DefineSpec('boolean')
 };
+type IsosurfaceValues = Values<typeof IsosurfaceSchema>
 
 const IsosurfaceName = 'isosurface';
 
-function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, height: number) {
+function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, packedGroup: boolean): ComputeRenderable<IsosurfaceValues> {
     if (ctx.namedComputeRenderables[IsosurfaceName]) {
-        const v = ctx.namedComputeRenderables[IsosurfaceName].values;
+        const v = ctx.namedComputeRenderables[IsosurfaceName].values as IsosurfaceValues;
 
         ValueCell.update(v.tActiveVoxelsPyramid, activeVoxelsPyramid);
         ValueCell.update(v.tActiveVoxelsBase, activeVoxelsBase);
@@ -58,16 +62,18 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
         ValueCell.update(v.uGridTransform, transform);
         ValueCell.update(v.uScale, scale);
 
+        ValueCell.update(v.dPackedGroup, packedGroup);
+
         ctx.namedComputeRenderables[IsosurfaceName].update();
     } else {
-        ctx.namedComputeRenderables[IsosurfaceName] = createIsosurfaceRenderable(ctx, activeVoxelsPyramid, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, height);
+        ctx.namedComputeRenderables[IsosurfaceName] = createIsosurfaceRenderable(ctx, activeVoxelsPyramid, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, packedGroup);
     }
     return ctx.namedComputeRenderables[IsosurfaceName];
 }
 
-function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, height: number) {
+function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, packedGroup: boolean) {
     // console.log('uSize', Math.pow(2, levels))
-    const values: Values<typeof IsosurfaceSchema> = {
+    const values: IsosurfaceValues = {
         ...QuadValues,
         tTriIndices: ValueCell.create(getTriIndices()),
 
@@ -84,6 +90,8 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
         uGridTexDim: ValueCell.create(gridTexDim),
         uGridTransform: ValueCell.create(transform),
         uScale: ValueCell.create(scale),
+
+        dPackedGroup: ValueCell.create(packedGroup)
     };
 
     const schema = { ...IsosurfaceSchema };
@@ -104,65 +112,72 @@ function setRenderingDefaults(ctx: WebGLContext) {
     state.clearColor(0, 0, 0, 0);
 }
 
-export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, vertexGroupTexture?: Texture, normalTexture?: Texture) {
-    const { gl, resources } = ctx;
+export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, packedGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
+    const { gl, resources, extensions } = ctx;
     const { pyramidTex, height, levels, scale, count } = histogramPyramid;
     const width = pyramidTex.getWidth();
 
-    // console.log('iso', 'gridDim', gridDim, 'scale', scale, 'gridTexDim', gridTexDim)
-    // console.log('iso volumeData', volumeData)
+    // console.log('width', width, 'height', height);
+    // console.log('iso', 'gridDim', gridDim, 'scale', scale, 'gridTexDim', gridTexDim);
+    // console.log('iso volumeData', volumeData);
 
     if (!ctx.namedFramebuffers[IsosurfaceName]) {
         ctx.namedFramebuffers[IsosurfaceName] = resources.framebuffer();
     }
     const framebuffer = ctx.namedFramebuffers[IsosurfaceName];
 
-    if (!vertexGroupTexture) {
-        vertexGroupTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest');
-    }
-    vertexGroupTexture.define(width, height);
-
-    if (!normalTexture) {
-        normalTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest');
-    }
-    normalTexture.define(width, height);
+    if (isWebGL2(gl)) {
+        if (!vertexTexture) {
+            vertexTexture = extensions.colorBufferHalfFloat && extensions.textureHalfFloat
+                ? resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
+                : resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        }
+
+        if (!groupTexture) {
+            groupTexture = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        }
+
+        if (!normalTexture) {
+            normalTexture = extensions.colorBufferHalfFloat && extensions.textureHalfFloat
+                ? resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
+                : resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        }
+    } else {
+        // in webgl1 drawbuffers must be in the same format for some reason
+        // this is quite wasteful but good enough for medium size meshes
 
-    // const infoTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-    // infoTex.define(pyramidTex.width, pyramidTex.height)
+        if (!vertexTexture) {
+            vertexTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        }
 
-    // const pointTexA = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-    // pointTexA.define(pyramidTex.width, pyramidTex.height)
+        if (!groupTexture) {
+            groupTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        }
 
-    // const pointTexB = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-    // pointTexB.define(pyramidTex.width, pyramidTex.height)
+        if (!normalTexture) {
+            normalTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        }
+    }
 
-    // const coordTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-    // coordTex.define(pyramidTex.width, pyramidTex.height)
+    vertexTexture.define(width, height);
+    groupTexture.define(width, height);
+    normalTexture.define(width, height);
 
-    // const indexTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-    // indexTex.define(pyramidTex.width, pyramidTex.height)
+    vertexTexture.attachFramebuffer(framebuffer, 0);
+    groupTexture.attachFramebuffer(framebuffer, 1);
+    normalTexture.attachFramebuffer(framebuffer, 2);
 
-    const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, height);
+    const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, packedGroup);
     ctx.state.currentRenderItemId = -1;
 
-    vertexGroupTexture.attachFramebuffer(framebuffer, 0);
-    normalTexture.attachFramebuffer(framebuffer, 1);
-    // infoTex.attachFramebuffer(framebuffer, 1)
-    // pointTexA.attachFramebuffer(framebuffer, 2)
-    // pointTexB.attachFramebuffer(framebuffer, 3)
-    // coordTex.attachFramebuffer(framebuffer, 4)
-    // indexTex.attachFramebuffer(framebuffer, 5)
-
     const { drawBuffers } = ctx.extensions;
     if (!drawBuffers) throw new Error('need WebGL draw buffers');
 
+    framebuffer.bind();
     drawBuffers.drawBuffers([
         drawBuffers.COLOR_ATTACHMENT0,
         drawBuffers.COLOR_ATTACHMENT1,
-        // drawBuffers.COLOR_ATTACHMENT2,
-        // drawBuffers.COLOR_ATTACHMENT3,
-        // drawBuffers.COLOR_ATTACHMENT4,
-        // drawBuffers.COLOR_ATTACHMENT5
+        drawBuffers.COLOR_ATTACHMENT2,
     ]);
 
     setRenderingDefaults(ctx);
@@ -172,45 +187,26 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
 
     gl.flush();
 
-    // const vgt = readTexture(ctx, vertexGroupTexture, pyramidTex.width, height)
-    // console.log('vertexGroupTexture', vgt.array.subarray(0, 4 * count))
-
-    // const vt = readTexture(ctx, verticesTex, pyramidTex.width, height)
-    // console.log('vt', vt)
-    // const vertices = new Float32Array(3 * compacted.count)
-    // for (let i = 0; i < compacted.count; ++i) {
-    //     vertices[i * 3] = vt.array[i * 4]
-    //     vertices[i * 3 + 1] = vt.array[i * 4 + 1]
-    //     vertices[i * 3 + 2] = vt.array[i * 4 + 2]
-    // }
-    // console.log('vertices', vertices)
-
-    // const it = readTexture(ctx, infoTex, pyramidTex.width, height)
-    // console.log('info', it.array.subarray(0, 4 * compacted.count))
-
-    // const pat = readTexture(ctx, pointTexA, pyramidTex.width, height)
-    // console.log('point a', pat.array.subarray(0, 4 * compacted.count))
-
-    // const pbt = readTexture(ctx, pointTexB, pyramidTex.width, height)
-    // console.log('point b', pbt.array.subarray(0, 4 * compacted.count))
+    return { vertexTexture, groupTexture, normalTexture, vertexCount: count };
+}
 
-    // const ct = readTexture(ctx, coordTex, pyramidTex.width, height)
-    // console.log('coord', ct.array.subarray(0, 4 * compacted.count))
+//
 
-    // const idxt = readTexture(ctx, indexTex, pyramidTex.width, height)
-    // console.log('index', idxt.array.subarray(0, 4 * compacted.count))
+export function extractIsosurface(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridTexScale: Vec2, transform: Mat4, isoValue: number, packedGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
+    // console.time('calcActiveVoxels');
+    const activeVoxelsTex = calcActiveVoxels(ctx, volumeData, gridDim, gridTexDim, isoValue, gridTexScale);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('calcActiveVoxels');
 
-    // const { field, idField } = await fieldFromTexture2d(ctx, volumeData, gridDimensions)
-    // console.log({ field, idField })
+    // console.time('createHistogramPyramid');
+    const compacted = createHistogramPyramid(ctx, activeVoxelsTex, gridTexScale, gridTexDim);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createHistogramPyramid');
 
-    // const valuesA = new Float32Array(compacted.count)
-    // const valuesB = new Float32Array(compacted.count)
-    // for (let i = 0; i < compacted.count; ++i) {
-    //     valuesA[i] = field.space.get(field.data, pat.array[i * 4], pat.array[i * 4 + 1], pat.array[i * 4 + 2])
-    //     valuesB[i] = field.space.get(field.data, pbt.array[i * 4], pbt.array[i * 4 + 1], pbt.array[i * 4 + 2])
-    // }
-    // console.log('valuesA', valuesA)
-    // console.log('valuesB', valuesB)
+    // console.time('createIsosurfaceBuffers');
+    const gv = createIsosurfaceBuffers(ctx, activeVoxelsTex, volumeData, compacted, gridDim, gridTexDim, transform, isoValue, packedGroup, vertexTexture, groupTexture, normalTexture);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createIsosurfaceBuffers');
 
-    return { vertexGroupTexture, normalTexture, vertexCount: count };
+    return gv;
 }

+ 5 - 6
src/mol-gl/renderable/texture-mesh.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 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>
  */
@@ -7,17 +7,16 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema, AttributeSpec } from './schema';
+import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema } from './schema';
 import { MeshShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 
 export const TextureMeshSchema = {
     ...BaseSchema,
-    aGroup: AttributeSpec('float32', 1, 0),
     uGeoTexDim: UniformSpec('v2'),
-    /** texture has vertex positions in XYZ and group id in W */
-    tPositionGroup: TextureSpec('texture', 'rgba', 'float', 'nearest'),
-    tNormal: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tPosition: TextureSpec('texture', 'rgb', 'float', 'nearest'),
+    tGroup: TextureSpec('texture', 'alpha', 'float', 'nearest'),
+    tNormal: TextureSpec('texture', 'rgb', 'float', 'nearest'),
 
     dFlatShaded: DefineSpec('boolean'),
     dDoubleSided: DefineSpec('boolean'),

+ 2 - 2
src/mol-gl/shader/chunks/assign-color-varying.glsl.ts

@@ -9,9 +9,9 @@ export default `
     #elif defined(dColorType_groupInstance)
         vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + group, uColorTexDim).rgb;
     #elif defined(dColorType_vertex)
-        vColor.rgb = readFromTexture(tColor, aVertex, uColorTexDim).rgb;
+        vColor.rgb = readFromTexture(tColor, VertexID, uColorTexDim).rgb;
     #elif defined(dColorType_vertexInstance)
-        vColor.rgb = readFromTexture(tColor, aInstance * float(uVertexCount) + aVertex, uColorTexDim).rgb;
+        vColor.rgb = readFromTexture(tColor, int(aInstance) * uVertexCount + VertexID, uColorTexDim).rgb;
     #endif
 
     #ifdef dOverpaint

+ 1 - 2
src/mol-gl/shader/chunks/assign-group.glsl.ts

@@ -1,7 +1,6 @@
 export default `
 #ifdef dGeoTexture
-    // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup
-    float group = readFromTexture(tPositionGroup, aGroup, uGeoTexDim).w;
+    float group = decodeFloatRGB(readFromTexture(tGroup, VertexID, uGeoTexDim).rgb);
 #else
     float group = aGroup;
 #endif

+ 1 - 1
src/mol-gl/shader/chunks/assign-position.glsl.ts

@@ -2,7 +2,7 @@ export default `
 mat4 model = uModel * aTransform;
 mat4 modelView = uView * model;
 #ifdef dGeoTexture
-    vec3 position = readFromTexture(tPositionGroup, aGroup, uGeoTexDim).xyz;
+    vec3 position = readFromTexture(tPosition, VertexID, uGeoTexDim).xyz;
 #else
     vec3 position = aPosition;
 #endif

+ 0 - 8
src/mol-gl/shader/chunks/color-vert-params.glsl.ts

@@ -11,14 +11,6 @@ export default `
         uniform sampler2D tColor;
     #endif
 
-    #if defined(dColorType_vertex) || defined(dColorType_vertexInstance)
-        #if __VERSION__ == 100
-            attribute float aVertex;
-        #else
-            #define aVertex float(gl_VertexID)
-        #endif
-    #endif
-
     #ifdef dOverpaint
         varying vec4 vOverpaint;
         uniform vec2 uOverpaintTexDim;

+ 7 - 0
src/mol-gl/shader/chunks/common-vert-params.glsl.ts

@@ -36,4 +36,11 @@ uniform sampler2D tMarker;
 
 varying vec3 vModelPosition;
 varying vec3 vViewPosition;
+
+#if __VERSION__ == 100
+    attribute float aVertex;
+    #define VertexID int(aVertex)
+#else
+    #define VertexID gl_VertexID
+#endif
 `;

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

@@ -28,6 +28,7 @@ export default `
 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)); }
+int imod(const in int a, const in int b) { return a - b * (a / b); }
 
 float pow2(const in float x) { return x * x; }
 

+ 8 - 1
src/mol-gl/shader/chunks/read-from-texture.glsl.ts

@@ -5,10 +5,17 @@
  */
 
 export default `
-vec4 readFromTexture (const in sampler2D tex, const in float i, const in vec2 dim) {
+vec4 readFromTexture(const in sampler2D tex, const in float i, const in vec2 dim) {
     float x = intMod(i, dim.x);
     float y = floor(intDiv(i, dim.x));
     vec2 uv = (vec2(x, y) + 0.5) / dim;
     return texture2D(tex, uv);
 }
+
+vec4 readFromTexture(const in sampler2D tex, const in int i, const in vec2 dim) {
+    int x = imod(i, int(dim.x));
+    int y = i / int(dim.x);
+    vec2 uv = (vec2(x, y) + 0.5) / dim;
+    return texture2D(tex, uv);
+}
 `;

+ 44 - 20
src/mol-gl/shader/histogram-pyramid/reduction.frag.ts

@@ -1,35 +1,59 @@
 export default `
 precision highp float;
+precision highp int;
 precision highp sampler2D;
 
-// input texture (previous level used to evaluate the new level)
-uniform sampler2D tPreviousLevel;
+uniform sampler2D tInputLevel;
+
+// previous level used to evaluate the new level
+#if __VERSION__ == 100
+    uniform sampler2D tPreviousLevel;
+#else
+    precision highp isampler2D;
+    uniform isampler2D tPreviousLevel;
+#endif
 
 // inverted size of the previous level texture.
 uniform float uSize;
 uniform float uTexSize;
 uniform bool uFirst;
 
+#include common
+
 void main(void) {
     float k = 0.5 * uSize;
     vec2 position = floor((gl_FragCoord.xy / uTexSize) / uSize) * uSize;
-    float a, b, c, d;
-
-    if (uFirst) {
-        a = texture2D(tPreviousLevel, position).r * 255.0;
-        b = texture2D(tPreviousLevel, position + vec2(k, 0.0)).r * 255.0;
-        c = texture2D(tPreviousLevel, position + vec2(0.0, k)).r * 255.0;
-        d = texture2D(tPreviousLevel, position + vec2(k, k)).r * 255.0;
-    } else {
-        a = texture2D(tPreviousLevel, position).r;
-        b = texture2D(tPreviousLevel, position + vec2(k, 0.0)).r;
-        c = texture2D(tPreviousLevel, position + vec2(0.0, k)).r;
-        d = texture2D(tPreviousLevel, position + vec2(k, k)).r;
-    }
-
-    gl_FragColor.a = a;
-    gl_FragColor.b = a + b;
-    gl_FragColor.g = gl_FragColor.b + c;
-    gl_FragColor.r = gl_FragColor.g + d;
+
+    #if __VERSION__ == 100
+        float a, b, c, d;
+
+        if (uFirst) {
+            a = texture2D(tInputLevel, position).r * 255.0;
+            b = texture2D(tInputLevel, position + vec2(k, 0.0)).r * 255.0;
+            c = texture2D(tInputLevel, position + vec2(0.0, k)).r * 255.0;
+            d = texture2D(tInputLevel, position + vec2(k, k)).r * 255.0;
+        } else {
+            a = decodeFloatRGB(texture2D(tPreviousLevel, position).rgb);
+            b = decodeFloatRGB(texture2D(tPreviousLevel, position + vec2(k, 0.0)).rgb);
+            c = decodeFloatRGB(texture2D(tPreviousLevel, position + vec2(0.0, k)).rgb);
+            d = decodeFloatRGB(texture2D(tPreviousLevel, position + vec2(k, k)).rgb);
+        }
+        gl_FragColor = vec4(encodeFloatRGB(a + b + c + d), 1.0);
+    #else
+        int a, b, c, d;
+
+        if (uFirst) {
+            a = int(texture2D(tInputLevel, position).r * 255.0);
+            b = int(texture2D(tInputLevel, position + vec2(k, 0.0)).r * 255.0);
+            c = int(texture2D(tInputLevel, position + vec2(0.0, k)).r * 255.0);
+            d = int(texture2D(tInputLevel, position + vec2(k, k)).r * 255.0);
+        } else {
+            a = texture2D(tPreviousLevel, position).r;
+            b = texture2D(tPreviousLevel, position + vec2(k, 0.0)).r;
+            c = texture2D(tPreviousLevel, position + vec2(0.0, k)).r;
+            d = texture2D(tPreviousLevel, position + vec2(k, k)).r;
+        }
+        gl_FragColor = ivec4(a + b + c + d);
+    #endif
 }
 `;

+ 14 - 6
src/mol-gl/shader/histogram-pyramid/sum.frag.ts

@@ -1,18 +1,26 @@
 /**
- * Copyright (c) 2019 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>
  */
 
 export default `
 precision highp float;
-precision highp sampler2D;
+precision highp int;
 
-uniform sampler2D tTexture;
-
-#include common
+#if __VERSION__ == 100
+    precision highp sampler2D;
+    uniform sampler2D tTexture;
+#else
+    precision highp isampler2D;
+    uniform isampler2D tTexture;
+#endif
 
 void main(void) {
-    gl_FragColor = vec4(encodeFloatRGB(texture2D(tTexture, vec2(0.5)).r), 1.0);
+    #if __VERSION__ == 100
+        gl_FragColor = texture2D(tTexture, vec2(0.5));
+    #else
+        gl_FragColor = ivec4(texture2D(tTexture, vec2(0.5)).r);
+    #endif
 }
 `;

+ 119 - 53
src/mol-gl/shader/marching-cubes/isosurface.frag.ts

@@ -3,7 +3,13 @@ precision highp float;
 precision highp int;
 precision highp sampler2D;
 
-uniform sampler2D tActiveVoxelsPyramid;
+#if __VERSION__ == 100
+    uniform sampler2D tActiveVoxelsPyramid;
+#else
+    precision highp isampler2D;
+    uniform isampler2D tActiveVoxelsPyramid;
+#endif
+
 uniform sampler2D tActiveVoxelsBase;
 uniform sampler2D tVolumeData;
 uniform sampler2D tTriIndices;
@@ -52,65 +58,114 @@ vec4 voxel(vec3 pos) {
     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));
+vec4 voxelPadded(vec3 pos) {
+    pos = min(max(vec3(0.0), pos), uGridDim - vec3(vec2(2.0), 1.0)); // remove xy padding
     return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
 }
 
+int idot2(const in ivec2 a, const in ivec2 b) {
+    return a.x * b.x + a.y * b.y;
+}
+
+int idot4(const in ivec4 a, const in ivec4 b) {
+    return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
+}
+
+#if __VERSION__ == 100
+    int pyramidVoxel(vec2 pos) {
+        return int(decodeFloatRGB(texture2D(tActiveVoxelsPyramid, pos / (vec2(1.0, 0.5) * uSize)).rgb));
+    }
+#else
+    int pyramidVoxel(vec2 pos) {
+        return texture2D(tActiveVoxelsPyramid, pos / (vec2(1.0, 0.5) * uSize)).r;
+    }
+#endif
+
+vec4 baseVoxel(vec2 pos) {
+    return texture2D(tActiveVoxelsBase, pos / uSize);
+}
+
 void main(void) {
     // get 1D index
-    float vI = dot(floor(uSize * (gl_FragCoord.xy / uSize)), vec2(1.0, uSize));
+    int vI = int(gl_FragCoord.x) + int(gl_FragCoord.y) * int(uSize);
 
     // ignore 1D indices outside of the grid
-    if(vI >= uCount) discard;
-
-    float offset = uSize - 2.;
-    float k = 1. / uSize;
-
-    vec2 relativePosition = k * vec2(offset, 0.);
-    vec4 partialSums = texture2D(tActiveVoxelsPyramid, relativePosition);
-    float start = 0.;
-    vec4 starts = vec4(0.);
-    vec4 ends = vec4(0.);
-    float diff = 2.;
-    vec4 m = vec4(0.);
-    vec2 position = vec2(0.);
-    vec4 vI4 = vec4(vI);
+    if(vI >= int(uCount)) discard;
+
+    ivec2 offset = ivec2(int(uSize) - 2, 0);
+
+    int start = 0;
+    ivec4 starts = ivec4(0);
+    ivec4 ends = ivec4(0);
+    int diff = 2;
+    ivec4 m = ivec4(0);
+    ivec2 position = ivec2(0);
+    ivec4 vI4 = ivec4(vI);
+
+    ivec2 relativePosition = ivec2(0);
+    int end = 0;
+    ivec2 pos1 = ivec2(0);
+    ivec2 pos2 = ivec2(0);
+    ivec2 pos3 = ivec2(0);
+    ivec2 pos4 = ivec2(0);
+    ivec3 vI3 = ivec3(vI);
+    ivec3 mask = ivec3(0);
 
     // traverse the different levels of the pyramid
     for(int i = 1; i < 14; i++) {
         if(float(i) >= uLevels) break;
 
-        offset -= diff;
-        diff *= 2.;
-        relativePosition = position + k * vec2(offset, 0.);
-
-        ends = partialSums.wzyx + vec4(start);
-        starts = vec4(start, ends.xyz);
-        m = vec4(greaterThanEqual(vI4, starts)) * vec4(lessThan(vI4, ends));
-        relativePosition += m.y * vec2(k, 0.) + m.z * vec2(0., k) + m.w * vec2(k, k);
-
-        start = dot(m, starts);
-        position = 2. * (relativePosition - k * vec2(offset, 0.));
-        partialSums = texture2D(tActiveVoxelsPyramid, relativePosition);
+        offset.x -= diff;
+        diff *= 2;
+        relativePosition = position + offset;
+
+        end = start + pyramidVoxel(vec2(relativePosition));
+        pos1 = ivec2(relativePosition);
+        starts.x = start;
+        ends.x = end;
+        pos2 = ivec2(relativePosition + ivec2(1, 0));
+        starts.y = ends.x;
+        ends.y = ends.x + pyramidVoxel(vec2(pos2));
+        pos3 = relativePosition + ivec2(0, 1);
+        starts.z = ends.y;
+        ends.z = ends.y + pyramidVoxel(vec2(pos3));
+        pos4 = relativePosition + ivec2(1, 1);
+        starts.w = ends.z;
+        mask = ivec3(greaterThanEqual(vI3, starts.rgb)) * ivec3(lessThan(vI3, ends.rgb));
+        m = ivec4(mask, 1 - int(any(bvec3(mask))));
+
+        relativePosition = m.x * pos1 + m.y * pos2 + m.z * pos3 + m.w * pos4;
+        start = idot4(m, starts);
+        position = 2 * (relativePosition - offset);
     }
 
-    ends = partialSums.wzyx + vec4(start);
-    starts = vec4(start, ends.xyz);
-    m = vec4(greaterThanEqual(vI4, starts)) * vec4(lessThan(vI4, ends));
-    position += m.y * vec2(k, 0.) + m.z * vec2(0., k) + m.w * vec2(k, k);
-
-    vec2 coord2d = position / uScale;
+    end = start + int(baseVoxel(vec2(position)).r * 255.0);
+    pos1 = position;
+    starts.x = start;
+    ends.x = end;
+    pos2 = position + ivec2(1, 0);
+    starts.y = ends.x;
+    ends.y = ends.x + int(baseVoxel(vec2(pos2)).r * 255.0);
+    pos3 = position + ivec2(0, 1);
+    starts.z = ends.y;
+    ends.z = ends.y + int(baseVoxel(vec2(pos3)).r * 255.0);
+    pos4 = position + ivec2(1, 1);
+    starts.w = ends.z;
+    mask = ivec3(greaterThanEqual(vI3, starts.rgb)) * ivec3(lessThan(vI3, ends.rgb));
+    m = ivec4(mask, 1 - int(any(bvec3(mask))));
+    position = m.x * pos1 + m.y * pos2 + m.z * pos3 + m.w * pos4;
+
+    vec2 coord2d = (vec2(position) / uSize) / uScale;
     vec3 coord3d = floor(index3dFrom2d(coord2d) + 0.5);
 
-    float edgeIndex = floor(texture2D(tActiveVoxelsBase, position).a * 255.0 + 0.5);
+    float edgeIndex = floor(baseVoxel(vec2(position)).a * 255.0 + 0.5);
 
     // current vertex for the up to 15 MC cases
-    float currentVertex = vI - dot(m, starts);
+    int currentVertex = vI - idot4(m, starts);
 
     // get index into triIndices table
-    float mcIndex = 16. * edgeIndex + currentVertex;
-    vec4 mcData = texture2D(tTriIndices, vec2(intMod(mcIndex, 64.), floor(mcIndex / 64.)) / 64.);
+    int mcIndex = 16 * int(edgeIndex) + currentVertex;
+    vec4 mcData = texture2D(tTriIndices, vec2(imod(mcIndex, 64), mcIndex / 64) / 64.);
 
     // bit mask to avoid conditionals (see comment below) for getting MC case corner
     vec4 m0 = vec4(floor(mcData.a * 255.0 + 0.5));
@@ -188,30 +243,41 @@ void main(void) {
     // 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);
+        // so we ensure a constant group id per triangle here
+        #ifdef dPackedGroup
+            gl_FragData[1] = vec4(voxel(coord3d).rgb, 1.0);
+        #else
+            vec3 gridDim = uGridDim - vec3(1.0, 1.0, 0.0); // remove xy padding
+            float group = coord3d.z + coord3d.y * gridDim.z + coord3d.x * gridDim.z * gridDim.y;
+            gl_FragData[1] = vec4(group > 16777215.5 ? vec3(1.0) : encodeFloatRGB(group), 1.0);
+        #endif
     #else
-        gl_FragData[0].w = t < 0.5 ? decodeFloatRGB(d0.rgb) : decodeFloatRGB(d1.rgb);
+        #ifdef dPackedGroup
+            gl_FragData[1] = vec4(t < 0.5 ? d0.rgb : d1.rgb, 1.0);
+        #else
+            vec3 b = t < 0.5 ? b0 : b1;
+            vec3 gridDim = uGridDim - vec3(1.0, 1.0, 0.0); // remove xy padding
+            float group = b.z + b.y * gridDim.z + b.x * gridDim.z * gridDim.y;
+            gl_FragData[1] = vec4(group > 16777215.5 ? vec3(1.0) : encodeFloatRGB(group), 1.0);
+        #endif
     #endif
 
     // normals from gradients
     vec3 n0 = -normalize(vec3(
-        voxel2(b0 - c1).a - voxel2(b0 + c1).a,
-        voxel2(b0 - c3).a - voxel2(b0 + c3).a,
-        voxel2(b0 - c4).a - voxel2(b0 + c4).a
+        voxelPadded(b0 - c1).a - voxelPadded(b0 + c1).a,
+        voxelPadded(b0 - c3).a - voxelPadded(b0 + c3).a,
+        voxelPadded(b0 - c4).a - voxelPadded(b0 + c4).a
     ));
     vec3 n1 = -normalize(vec3(
-        voxel2(b1 - c1).a - voxel2(b1 + c1).a,
-        voxel2(b1 - c3).a - voxel2(b1 + c3).a,
-        voxel2(b1 - c4).a - voxel2(b1 + c4).a
+        voxelPadded(b1 - c1).a - voxelPadded(b1 + c1).a,
+        voxelPadded(b1 - c3).a - voxelPadded(b1 + c3).a,
+        voxelPadded(b1 - c4).a - voxelPadded(b1 + c4).a
     ));
-    gl_FragData[1].xyz = -vec3(
+    mat3 normalMatrix = transpose3(inverse3(mat3(uGridTransform)));
+    gl_FragData[2].xyz = normalMatrix * -vec3(
         n0.x + t * (n0.x - n1.x),
         n0.y + t * (n0.y - n1.y),
         n0.z + t * (n0.z - n1.z)
     );
-
-    mat3 normalMatrix = transpose3(inverse3(mat3(uGridTransform)));
-    gl_FragData[1].xyz = normalMatrix * gl_FragData[1].xyz;
 }
 `;

+ 7 - 8
src/mol-gl/shader/mesh.vert.ts

@@ -7,6 +7,7 @@
 export default `
 precision highp float;
 precision highp int;
+precision highp sampler2D;
 
 #include common
 #include read_from_texture
@@ -16,19 +17,17 @@ precision highp int;
 
 #ifdef dGeoTexture
     uniform vec2 uGeoTexDim;
-    uniform sampler2D tPositionGroup;
+    uniform sampler2D tPosition;
+    uniform sampler2D tGroup;
+    uniform sampler2D tNormal;
 #else
     attribute vec3 aPosition;
+    attribute float aGroup;
+    attribute vec3 aNormal;
 #endif
 attribute mat4 aTransform;
 attribute float aInstance;
-attribute float aGroup;
 
-#ifdef dGeoTexture
-    uniform sampler2D tNormal;
-#else
-    attribute vec3 aNormal;
-#endif
 varying vec3 vNormal;
 
 void main(){
@@ -40,7 +39,7 @@ void main(){
     #include clip_instance
 
     #ifdef dGeoTexture
-        vec3 normal = readFromTexture(tNormal, aGroup, uGeoTexDim).xyz;
+        vec3 normal = readFromTexture(tNormal, VertexID, uGeoTexDim).xyz;
     #else
         vec3 normal = aNormal;
     #endif

+ 3 - 0
src/mol-gl/webgl/context.ts

@@ -188,6 +188,7 @@ export interface WebGLContext {
     readonly resources: WebGLResources
 
     readonly maxTextureSize: number
+    readonly max3dTextureSize: number
     readonly maxRenderbufferSize: number
     readonly maxDrawBuffers: number
     readonly maxTextureImageUnits: number
@@ -223,6 +224,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
 
     const parameters = {
         maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
+        max3dTextureSize: isWebGL2(gl) ? gl.getParameter(gl.MAX_3D_TEXTURE_SIZE) as number : 0,
         maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
         maxDrawBuffers: extensions.drawBuffers ? gl.getParameter(extensions.drawBuffers.MAX_DRAW_BUFFERS) as number : 0,
         maxTextureImageUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number,
@@ -289,6 +291,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         resources,
 
         get maxTextureSize () { return parameters.maxTextureSize; },
+        get max3dTextureSize () { return parameters.max3dTextureSize; },
         get maxRenderbufferSize () { return parameters.maxRenderbufferSize; },
         get maxDrawBuffers () { return parameters.maxDrawBuffers; },
         get maxTextureImageUnits () { return parameters.maxTextureImageUnits; },

+ 6 - 1
src/mol-gl/webgl/render-item.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>
  */
@@ -338,6 +338,11 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                 });
                 attributeBuffers.forEach(([_, buffer]) => buffer.destroy());
                 if (elementsBuffer) elementsBuffer.destroy();
+
+                stats.drawCount -= drawCount;
+                stats.instanceCount -= instanceCount;
+                stats.instancedDrawCount -= instanceCount * drawCount;
+
                 destroyed = true;
             }
         }

+ 1 - 0
src/mol-gl/webgl/texture.ts

@@ -57,6 +57,7 @@ export function getFormat(gl: GLRenderingContext, format: TextureFormat, type: T
     switch (format) {
         case 'alpha':
             if (isWebGL2(gl) && type === 'float') return gl.RED;
+            else if (isWebGL2(gl) && type === 'int') return gl.RED_INTEGER;
             else return gl.ALPHA;
         case 'rgb':
             if (isWebGL2(gl) && type === 'int') return gl.RGB_INTEGER;

+ 7 - 5
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -120,7 +120,7 @@ type _GaussianDensityTextureData = {
 
 function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
     // console.log('2d');
-    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = webgl;
+    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat, blendMinMax } } = webgl;
     const { smoothness, resolution } = props;
 
     const { drawCount, positions, radii, groups, scale, expandedBox, dim, maxRadius } = prepareGaussianDensityData(position, box, radius, props);
@@ -190,11 +190,13 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
     setupDensityRendering(webgl, renderable);
     render(texture, true);
 
-    setupMinDistanceRendering(webgl, renderable);
-    render(minDistTex, true);
+    if (blendMinMax) {
+        setupMinDistanceRendering(webgl, renderable);
+        render(minDistTex, true);
 
-    setupGroupIdRendering(webgl, renderable);
-    render(texture, false);
+        setupGroupIdRendering(webgl, renderable);
+        render(texture, false);
+    }
 
     // printTexture(webgl, minDistTex, 0.75);
 

+ 1 - 1
src/mol-model/structure/structure/structure.ts

@@ -1165,7 +1165,7 @@ namespace Structure {
         /** Must be lower to be medium */
         mediumResidueCount: 5000,
         /** Must be lower to be large (big ribosomes like 4UG0 should still be `large`) */
-        largeResidueCount: 20000,
+        largeResidueCount: 30000,
         /**
          * Structures above `largeResidueCount` are consider huge when they have
          * a `highSymmetryUnitCount` or gigantic when not

+ 4 - 4
src/mol-repr/structure/complex-representation.ts

@@ -22,7 +22,7 @@ import { Clipping } from '../../mol-theme/clipping';
 import { Transparency } from '../../mol-theme/transparency';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 
-export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, props?: PD.Values<P>, webgl?: WebGLContext) => ComplexVisual<P>): StructureRepresentation<P> {
+export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, structure: Structure, props: PD.Values<P>, webgl?: WebGLContext) => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0;
     const { webgl } = ctx;
     const updated = new Subject<number>();
@@ -47,11 +47,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         return Task.create('Creating or updating ComplexRepresentation', async runtime => {
             let newVisual = false;
             if (!visual) {
-                visual = visualCtor(materialId, _props, webgl);
+                visual = visualCtor(materialId, _structure, _props, webgl);
                 newVisual = true;
-            } else if (visual.mustRecreate?.(_props, webgl)) {
+            } else if (visual.mustRecreate?.(_structure, _props, webgl)) {
                 visual.destroy();
-                visual = visualCtor(materialId, _props, webgl);
+                visual = visualCtor(materialId, _structure, _props, webgl);
                 newVisual = true;
             }
             const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, structure);

+ 1 - 1
src/mol-repr/structure/complex-visual.ts

@@ -52,7 +52,7 @@ interface ComplexVisualBuilder<P extends StructureParams, G extends Geometry> {
     getLoci(pickingId: PickingId, structure: Structure, id: number): Loci
     eachLocation(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, isMarking: boolean): boolean,
     setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure): void
-    mustRecreate?: (props: PD.Values<P>) => boolean
+    mustRecreate?: (structure: Structure, props: PD.Values<P>) => boolean
     dispose?: (geometry: G) => void
 }
 

+ 9 - 9
src/mol-repr/structure/units-representation.ts

@@ -28,7 +28,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 
 export interface UnitsVisual<P extends StructureParams> extends Visual<StructureGroup, P> { }
 
-export function UnitsRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, props?: PD.Values<P>, webgl?: WebGLContext) => UnitsVisual<P>): StructureRepresentation<P> {
+export function UnitsRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, structure: Structure, props: PD.Values<P>, webgl?: WebGLContext) => UnitsVisual<P>): StructureRepresentation<P> {
     let version = 0;
     const { webgl } = ctx;
     const updated = new Subject<number>();
@@ -59,7 +59,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                 _groups = structure.unitSymmetryGroups;
                 for (let i = 0; i < _groups.length; i++) {
                     const group = _groups[i];
-                    const visual = visualCtor(materialId, _props, webgl);
+                    const visual = visualCtor(materialId, structure, _props, webgl);
                     const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
                     if (promise) await promise;
                     setVisualState(visual, group, _state); // current state for new visual
@@ -82,9 +82,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                         // console.log('old', visualGroup.group)
                         // console.log('new', group)
                         let { visual } = visualGroup;
-                        if (visual.mustRecreate?.(_props, webgl)) {
+                        if (visual.mustRecreate?.({ group, structure }, _props, webgl)) {
                             visual.destroy();
-                            visual = visualCtor(materialId, _props, webgl);
+                            visual = visualCtor(materialId, structure, _props, webgl);
                             const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
                             if (promise) await promise;
                             setVisualState(visual, group, _state); // current state for new visual
@@ -104,7 +104,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                     } else {
                         // console.log(label, 'did not find visualGroup to reuse, creating new');
                         // newGroups.push(group)
-                        const visual = visualCtor(materialId, _props, webgl);
+                        const visual = visualCtor(materialId, structure, _props, webgl);
                         const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
                         if (promise) await promise;
                         setVisualState(visual, group, _state); // current state for new visual
@@ -129,9 +129,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                     const visualGroup = visuals.get(group.hashCode);
                     if (visualGroup) {
                         let { visual } = visualGroup;
-                        if (visual.mustRecreate?.(_props, ctx.webgl)) {
+                        if (visual.mustRecreate?.({ group, structure }, _props, ctx.webgl)) {
                             visual.destroy();
-                            visual = visualCtor(materialId, _props, ctx.webgl);
+                            visual = visualCtor(materialId, structure, _props, ctx.webgl);
                             visualGroup.visual = visual;
                             const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
                             if (promise) await promise;
@@ -153,9 +153,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                 visuals.forEach(vg => visualsList.push(vg));
                 for (let i = 0, il = visualsList.length; i < il; ++i) {
                     let { visual, group } = visualsList[i];
-                    if (visual.mustRecreate?.(_props, ctx.webgl)) {
+                    if (visual.mustRecreate?.({ group, structure: _structure }, _props, ctx.webgl)) {
                         visual.destroy();
-                        visual = visualCtor(materialId, _props, webgl);
+                        visual = visualCtor(materialId, _structure, _props, webgl);
                         visualsList[i].visual = visual;
                         const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure: _structure });
                         if (promise) await promise;

+ 1 - 1
src/mol-repr/structure/units-visual.ts

@@ -58,7 +58,7 @@ interface UnitsVisualBuilder<P extends StructureParams, G extends Geometry> {
     getLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number): Loci
     eachLocation(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean, isMarking: boolean): boolean
     setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup): void
-    mustRecreate?: (props: PD.Values<P>) => boolean
+    mustRecreate?: (structureGroup: StructureGroup, props: PD.Values<P>) => boolean
     dispose?: (geometry: G) => void
 }
 

+ 7 - 7
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -157,12 +157,12 @@ export const InterUnitBondCylinderParams = {
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
-    useImpostor: PD.Boolean(true),
+    tryUseImpostor: PD.Boolean(true),
 };
 export type InterUnitBondCylinderParams = typeof InterUnitBondCylinderParams
 
-export function InterUnitBondCylinderVisual(materialId: number, props?: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) {
-    return props?.useImpostor && webgl && webgl.extensions.fragDepth
+export function InterUnitBondCylinderVisual(materialId: number, structure: Structure, props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) {
+    return props.tryUseImpostor && webgl && webgl.extensions.fragDepth
         ? InterUnitBondCylinderImpostorVisual(materialId)
         : InterUnitBondCylinderMeshVisual(materialId);
 }
@@ -187,8 +187,8 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );
         },
-        mustRecreate: (props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
-            return !props.useImpostor || !webgl;
+        mustRecreate: (structure: Structure, props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return !props.tryUseImpostor || !webgl;
         }
     }, materialId);
 }
@@ -216,8 +216,8 @@ export function InterUnitBondCylinderMeshVisual(materialId: number): ComplexVisu
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );
         },
-        mustRecreate: (props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
-            return props.useImpostor && !!webgl;
+        mustRecreate: (structure: Structure, props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return props.tryUseImpostor && !!webgl;
         }
     }, materialId);
 }

+ 7 - 7
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -141,12 +141,12 @@ export const IntraUnitBondCylinderParams = {
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
-    useImpostor: PD.Boolean(true),
+    tryUseImpostor: PD.Boolean(true),
 };
 export type IntraUnitBondCylinderParams = typeof IntraUnitBondCylinderParams
 
-export function IntraUnitBondCylinderVisual(materialId: number, props?: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) {
-    return props?.useImpostor && webgl && webgl.extensions.fragDepth
+export function IntraUnitBondCylinderVisual(materialId: number, structure: Structure, props: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) {
+    return props.tryUseImpostor && webgl && webgl.extensions.fragDepth
         ? IntraUnitBondCylinderImpostorVisual(materialId)
         : IntraUnitBondCylinderMeshVisual(materialId);
 }
@@ -182,8 +182,8 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
                 }
             }
         },
-        mustRecreate: (props: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) => {
-            return !props.useImpostor || !webgl;
+        mustRecreate: (structureGroup: StructureGroup, props: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return !props.tryUseImpostor || !webgl;
         }
     }, materialId);
 }
@@ -222,8 +222,8 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual
                 }
             }
         },
-        mustRecreate: (props: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) => {
-            return props.useImpostor && !!webgl;
+        mustRecreate: (structureGroup: StructureGroup, props: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return props.tryUseImpostor && !!webgl;
         }
     }, materialId);
 }

+ 9 - 8
src/mol-repr/structure/visual/element-sphere.ts

@@ -6,11 +6,12 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { UnitsMeshParams, UnitsSpheresParams, UnitsVisual, UnitsSpheresVisual, UnitsMeshVisual } from '../units-visual';
+import { UnitsMeshParams, UnitsSpheresParams, UnitsVisual, UnitsSpheresVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { createElementSphereImpostor, ElementIterator, getElementLoci, eachElement, createElementSphereMesh } from './util/element';
 import { VisualUpdateState } from '../../util';
 import { BaseGeometry } from '../../../mol-geo/geometry/base';
+import { Structure } from '../../../mol-model/structure';
 
 export const ElementSphereParams = {
     ...UnitsMeshParams,
@@ -19,12 +20,12 @@ export const ElementSphereParams = {
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     ignoreHydrogens: PD.Boolean(false),
     traceOnly: PD.Boolean(false),
-    useImpostor: PD.Boolean(true),
+    tryUseImpostor: PD.Boolean(true),
 };
 export type ElementSphereParams = typeof ElementSphereParams
 
-export function ElementSphereVisual(materialId: number, props?: PD.Values<ElementSphereParams>, webgl?: WebGLContext) {
-    return props?.useImpostor && webgl && webgl.extensions.fragDepth
+export function ElementSphereVisual(materialId: number, structure: Structure, props: PD.Values<ElementSphereParams>, webgl?: WebGLContext) {
+    return props.tryUseImpostor && webgl && webgl.extensions.fragDepth
         ? ElementSphereImpostorVisual(materialId)
         : ElementSphereMeshVisual(materialId);
 }
@@ -42,8 +43,8 @@ export function ElementSphereImpostorVisual(materialId: number): UnitsVisual<Ele
                 newProps.traceOnly !== currentProps.traceOnly
             );
         },
-        mustRecreate: (props: PD.Values<ElementSphereParams>, webgl?: WebGLContext) => {
-            return !props.useImpostor || !webgl;
+        mustRecreate: (structureGroup: StructureGroup, props: PD.Values<ElementSphereParams>, webgl?: WebGLContext) => {
+            return !props.tryUseImpostor || !webgl;
         }
     }, materialId);
 }
@@ -63,8 +64,8 @@ export function ElementSphereMeshVisual(materialId: number): UnitsVisual<Element
                 newProps.traceOnly !== currentProps.traceOnly
             );
         },
-        mustRecreate: (props: PD.Values<ElementSphereParams>, webgl?: WebGLContext) => {
-            return props.useImpostor && !!webgl;
+        mustRecreate: (structureGroup: StructureGroup, props: PD.Values<ElementSphereParams>, webgl?: WebGLContext) => {
+            return props.tryUseImpostor && !!webgl;
         }
     }, materialId);
 }

+ 11 - 6
src/mol-repr/structure/visual/gaussian-density-volume.ts

@@ -20,11 +20,12 @@ import { getStructureExtraRadius, getUnitExtraRadius } from './util/common';
 
 async function createGaussianDensityVolume(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityProps, directVolume?: DirectVolume): Promise<DirectVolume> {
     const { runtime, webgl } = ctx;
-    if (webgl === undefined) throw new Error('createGaussianDensityVolume requires `webgl` object in VisualContext');
+    if (!webgl || !webgl.extensions.blendMinMax) {
+        throw new Error('GaussianDensityVolume requires `webgl` and `blendMinMax` extension');
+    }
 
-    const p = { ...props, useGpu: true };
     const oldTexture = directVolume ? directVolume.gridTexture.ref.value : undefined;
-    const densityTextureData = await computeStructureGaussianDensityTexture(structure, p, webgl, oldTexture).runInContext(runtime);
+    const densityTextureData = await computeStructureGaussianDensityTexture(structure, props, webgl, oldTexture).runInContext(runtime);
     const { transform, texture, bbox, gridDim } = densityTextureData;
     const stats = { min: 0, max: 1, mean: 0.04, sigma: 0.01 };
 
@@ -71,11 +72,15 @@ export function GaussianDensityVolumeVisual(materialId: number): ComplexVisual<G
 
 async function createUnitsGaussianDensityVolume(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, directVolume?: DirectVolume): Promise<DirectVolume> {
     const { runtime, webgl } = ctx;
-    if (webgl === undefined) throw new Error('createUnitGaussianDensityVolume requires `webgl` object in VisualContext');
+    if (!webgl) {
+        // gpu gaussian density also needs blendMinMax but there is no fallback here so
+        // we allow it here with the results that there is no group id assignment and
+        // hence no group-based coloring or picking
+        throw new Error('GaussianDensityVolume requires `webgl`');
+    }
 
-    const p = { ...props, useGpu: true };
     const oldTexture = directVolume ? directVolume.gridTexture.ref.value : undefined;
-    const densityTextureData = await computeUnitGaussianDensityTexture(structure, unit, p, webgl, oldTexture).runInContext(runtime);
+    const densityTextureData = await computeUnitGaussianDensityTexture(structure, unit, props, webgl, oldTexture).runInContext(runtime);
     const { transform, texture, bbox, gridDim } = densityTextureData;
     const stats = { min: 0, max: 1, mean: 0.04, sigma: 0.01 };
 

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

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { UnitsMeshParams, UnitsTextureMeshParams, UnitsVisual, UnitsMeshVisual, UnitsTextureMeshVisual } from '../units-visual';
+import { UnitsMeshParams, UnitsTextureMeshParams, UnitsVisual, UnitsMeshVisual, UnitsTextureMeshVisual, StructureGroup } from '../units-visual';
 import { GaussianDensityParams, computeUnitGaussianDensity, computeUnitGaussianDensityTexture2d, GaussianDensityProps, computeStructureGaussianDensity, computeStructureGaussianDensityTexture2d } from './util/gaussian';
 import { VisualContext } from '../../visual';
 import { Unit, Structure } from '../../../mol-model/structure';
@@ -15,19 +15,18 @@ import { computeMarchingCubesMesh } from '../../../mol-geo/util/marching-cubes/a
 import { ElementIterator, getElementLoci, eachElement, getSerialElementLoci, eachSerialElement } from './util/element';
 import { VisualUpdateState } 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';
+import { extractIsosurface } from '../../../mol-gl/compute/marching-cubes/isosurface';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { ComplexVisual, ComplexMeshParams, ComplexMeshVisual, ComplexTextureMeshVisual, ComplexTextureMeshParams } from '../complex-visual';
-import { getUnitExtraRadius, getStructureExtraRadius } from './util/common';
+import { getUnitExtraRadius, getStructureExtraRadius, getVolumeSliceInfo } from './util/common';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
 
 const SharedParams = {
     ...GaussianDensityParams,
     ignoreHydrogens: PD.Boolean(false),
-    useGpu: PD.Boolean(false),
+    tryUseGpu: PD.Boolean(true),
 };
+type SharedParams = typeof SharedParams
 
 export const GaussianSurfaceMeshParams = {
     ...UnitsMeshParams,
@@ -47,23 +46,36 @@ function gpuSupport(webgl: WebGLContext) {
     return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.blendMinMax && webgl.extensions.drawBuffers;
 }
 
-export function GaussianSurfaceVisual(materialId: number, props?: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) {
-    return props?.useGpu && webgl && gpuSupport(webgl)
-        ? GaussianSurfaceTextureMeshVisual(materialId)
-        : GaussianSurfaceMeshVisual(materialId);
+function suitableForGpu(structure: Structure, props: PD.Values<SharedParams>, webgl: WebGLContext) {
+    // lower resolutions are about as fast on CPU vs integrated GPU,
+    // very low resolutions have artifacts when calculated on GPU
+    if (props.resolution > 1) return false;
+    // the GPU is much more memory contraint, especially true for integrated GPUs,
+    // being conservative here still allows for small and medium sized assemblies
+    const d = webgl.maxTextureSize / 3;
+    const { areaCells, maxAreaCells } = getVolumeSliceInfo(structure.boundary.box, props.resolution, d * d);
+    return areaCells < maxAreaCells;
 }
 
-export function StructureGaussianSurfaceVisual(materialId: number, props?: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) {
-    return props?.useGpu && webgl && gpuSupport(webgl)
-        ? StructureGaussianSurfaceTextureMeshVisual(materialId)
-        : StructureGaussianSurfaceMeshVisual(materialId);
+export function GaussianSurfaceVisual(materialId: number, structure: Structure, props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) {
+    if (props.tryUseGpu && webgl && gpuSupport(webgl) && suitableForGpu(structure, props, webgl)) {
+        return GaussianSurfaceTextureMeshVisual(materialId);
+    }
+    return GaussianSurfaceMeshVisual(materialId);
+}
+
+export function StructureGaussianSurfaceVisual(materialId: number, structure: Structure, props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) {
+    if (props.tryUseGpu && webgl && gpuSupport(webgl) && suitableForGpu(structure, props, webgl)) {
+        return StructureGaussianSurfaceTextureMeshVisual(materialId);
+    }
+    return StructureGaussianSurfaceMeshVisual(materialId);
 }
 
 //
 
 async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, mesh?: Mesh): Promise<Mesh> {
     const { smoothness } = props;
-    const { transform, field, idField, radiusFactor } = await computeUnitGaussianDensity(structure, unit, props, ctx.webgl).runInContext(ctx.runtime);
+    const { transform, field, idField, radiusFactor } = await computeUnitGaussianDensity(structure, unit, props).runInContext(ctx.runtime);
 
     const params = {
         isoLevel: Math.exp(-smoothness) / radiusFactor,
@@ -96,8 +108,8 @@ export function GaussianSurfaceMeshVisual(materialId: number): UnitsVisual<Gauss
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
         },
-        mustRecreate: (props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
-            return props.useGpu && !!webgl;
+        mustRecreate: (structureGroup: StructureGroup, props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return props.tryUseGpu && !!webgl && suitableForGpu(structureGroup.structure, props, webgl);
         }
     }, materialId);
 }
@@ -106,7 +118,7 @@ export function GaussianSurfaceMeshVisual(materialId: number): UnitsVisual<Gauss
 
 async function createStructureGaussianSurfaceMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityProps, mesh?: Mesh): Promise<Mesh> {
     const { smoothness } = props;
-    const { transform, field, idField, radiusFactor } = await computeStructureGaussianDensity(structure, props, ctx.webgl).runInContext(ctx.runtime);
+    const { transform, field, idField, radiusFactor } = await computeStructureGaussianDensity(structure, props).runInContext(ctx.runtime);
 
     const params = {
         isoLevel: Math.exp(-smoothness) / radiusFactor,
@@ -138,8 +150,8 @@ export function StructureGaussianSurfaceMeshVisual(materialId: number): ComplexV
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
         },
-        mustRecreate: (props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
-            return props.useGpu && !!webgl;
+        mustRecreate: (structure: Structure, props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return props.tryUseGpu && !!webgl && suitableForGpu(structure, props, webgl);
         }
     }, materialId);
 }
@@ -169,29 +181,11 @@ async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit,
 
     const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
 
-    // console.time('calcActiveVoxels');
-    const activeVoxelsTex = calcActiveVoxels(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoLevel, densityTextureData.gridTexScale);
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('calcActiveVoxels');
-
-    // console.time('createHistogramPyramid');
-    const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, densityTextureData.gridTexScale, densityTextureData.gridTexDim);
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('createHistogramPyramid');
-
-    // console.time('createIsosurfaceBuffers');
-    const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined);
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('createIsosurfaceBuffers');
+    const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, true, textureMesh?.vertexTexture.ref.value, textureMesh?.groupTexture.ref.value, textureMesh?.normalTexture.ref.value);
 
     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,
-    //     framebuffers: ctx.webgl.namedFramebuffers,
-    //     textures: ctx.webgl.namedTextures,
-    // });
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
+
     return surface;
 }
 
@@ -210,12 +204,13 @@ export function GaussianSurfaceTextureMeshVisual(materialId: number): UnitsVisua
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
         },
-        mustRecreate: (props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
-            return !props.useGpu || !webgl;
+        mustRecreate: (structureGroup: StructureGroup, props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return !props.tryUseGpu || !webgl || !suitableForGpu(structureGroup.structure, props, webgl);
         },
         dispose: (geometry: TextureMesh) => {
+            geometry.vertexTexture.ref.value.destroy();
+            geometry.groupTexture.ref.value.destroy();
             geometry.normalTexture.ref.value.destroy();
-            geometry.vertexGroupTexture.ref.value.destroy();
         }
     }, materialId);
 }
@@ -243,29 +238,11 @@ async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, str
 
     const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
 
-    // console.time('calcActiveVoxels');
-    const activeVoxelsTex = calcActiveVoxels(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoLevel, densityTextureData.gridTexScale);
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('calcActiveVoxels');
-
-    // console.time('createHistogramPyramid');
-    const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, densityTextureData.gridTexScale, densityTextureData.gridTexDim);
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('createHistogramPyramid');
-
-    // console.time('createIsosurfaceBuffers');
-    const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined);
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
-    // console.timeEnd('createIsosurfaceBuffers');
+    const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, true, textureMesh?.vertexTexture.ref.value, textureMesh?.groupTexture.ref.value, textureMesh?.normalTexture.ref.value);
 
     const boundingSphere = Sphere3D.expand(Sphere3D(), structure.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,
-    //     framebuffers: ctx.webgl.namedFramebuffers,
-    //     textures: ctx.webgl.namedTextures,
-    // });
-    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
+
     return surface;
 }
 
@@ -283,12 +260,13 @@ export function StructureGaussianSurfaceTextureMeshVisual(materialId: number): C
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
         },
-        mustRecreate: (props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
-            return !props.useGpu || !webgl;
+        mustRecreate: (structure: Structure, props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return !props.tryUseGpu || !webgl || !suitableForGpu(structure, props, webgl);
         },
         dispose: (geometry: TextureMesh) => {
+            geometry.vertexTexture.ref.value.destroy();
+            geometry.groupTexture.ref.value.destroy();
             geometry.normalTexture.ref.value.destroy();
-            geometry.vertexGroupTexture.ref.value.destroy();
         }
     }, materialId);
 }

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

@@ -19,7 +19,7 @@ import { getUnitExtraRadius } from './util/common';
 
 async function createGaussianWireframe(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, lines?: Lines): Promise<Lines> {
     const { smoothness } = props;
-    const { transform, field, idField } = await computeUnitGaussianDensity(structure, unit, props, ctx.webgl).runInContext(ctx.runtime);
+    const { transform, field, idField } = await computeUnitGaussianDensity(structure, unit, props).runInContext(ctx.runtime);
 
     const params = {
         isoLevel: Math.exp(-smoothness),

+ 16 - 5
src/mol-repr/structure/visual/util/common.ts

@@ -102,13 +102,24 @@ export function includesUnitKind(unitKinds: UnitKind[], unit: Unit) {
 
 const DefaultMaxCells = 500_000_000;
 
-/** guard against overly high resolution for the given box size */
-export function ensureReasonableResolution<T>(box: Box3D, props: { resolution: number } & T, maxCells = DefaultMaxCells) {
+export function getVolumeSliceInfo(box: Box3D, resolution: number, maxCells = DefaultMaxCells) {
     const size = Box3D.size(Vec3(), box);
-    const maxArea = Math.floor(Math.cbrt(maxCells) * Math.cbrt(maxCells));
+    Vec3.ceil(size, size);
+    size.sort((a, b) => b - a); // descending
+    const maxAreaCells = Math.floor(Math.cbrt(maxCells) * Math.cbrt(maxCells));
     const area = size[0] * size[1];
-    const maxAreaCells = Math.ceil(area / props.resolution);
-    const resolution = maxAreaCells > maxArea ? area / maxArea : props.resolution;
+    const areaCells = Math.ceil(area / (resolution * resolution));
+    return { area, areaCells, maxAreaCells };
+}
+
+/**
+ * Guard against overly high resolution for the given box size.
+ * Internally it uses the largest 2d slice of the box to determine the
+ * maximum resolution to account for the 2d texture layout on the GPU.
+ */
+export function ensureReasonableResolution<T>(box: Box3D, props: { resolution: number } & T, maxCells = DefaultMaxCells) {
+    const { area, areaCells, maxAreaCells } = getVolumeSliceInfo(box, props.resolution, maxCells);
+    const resolution = areaCells > maxAreaCells ? Math.sqrt(area / maxAreaCells) : props.resolution;
     return { ...props, resolution };
 }
 

+ 8 - 13
src/mol-repr/structure/visual/util/gaussian.ts

@@ -25,21 +25,16 @@ export type GaussianDensityProps = typeof DefaultGaussianDensityProps
 
 //
 
-function getTextureMaxCells(webgl: WebGLContext) {
-    const d = Math.min(webgl.maxTextureSize / 2, 4096);
-    return d * d;
-}
-
-function largestUnitBox(structure: Structure) {
-    const units = structure.unitsSortedByVolume;
-    return units[units.length - 1].lookup3d.boundary.box;
+export function getTextureMaxCells(webgl: WebGLContext, structure?: Structure) {
+    const d = webgl.maxTextureSize / 3;
+    return (d * d) / Math.max(1, (structure ? structure.units.length / 16 : 1));
 }
 
 //
 
-export function computeUnitGaussianDensity(structure: Structure, unit: Unit, props: GaussianDensityProps, webgl?: WebGLContext) {
+export function computeUnitGaussianDensity(structure: Structure, unit: Unit, props: GaussianDensityProps) {
     const { box } = unit.lookup3d.boundary;
-    const p = ensureReasonableResolution(largestUnitBox(structure), props);
+    const p = ensureReasonableResolution(box, props);
     const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
     return Task.create('Gaussian Density', async ctx => {
         return await GaussianDensityCPU(ctx, position, box, radius, p);
@@ -48,7 +43,7 @@ export function computeUnitGaussianDensity(structure: Structure, unit: Unit, pro
 
 export function computeUnitGaussianDensityTexture(structure: Structure, unit: Unit, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     const { box } = unit.lookup3d.boundary;
-    const p = ensureReasonableResolution(largestUnitBox(structure), props, getTextureMaxCells(webgl));
+    const p = ensureReasonableResolution(box, props, getTextureMaxCells(webgl, structure));
     const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
     return Task.create('Gaussian Density', async ctx => {
         return GaussianDensityTexture(webgl, position, box, radius, p, texture);
@@ -57,7 +52,7 @@ export function computeUnitGaussianDensityTexture(structure: Structure, unit: Un
 
 export function computeUnitGaussianDensityTexture2d(structure: Structure, unit: Unit, powerOfTwo: boolean, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     const { box } = unit.lookup3d.boundary;
-    const p = ensureReasonableResolution(largestUnitBox(structure), props, getTextureMaxCells(webgl));
+    const p = ensureReasonableResolution(box, props, getTextureMaxCells(webgl, structure));
     const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
     return Task.create('Gaussian Density', async ctx => {
         return GaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, p, texture);
@@ -66,7 +61,7 @@ export function computeUnitGaussianDensityTexture2d(structure: Structure, unit:
 
 //
 
-export function computeStructureGaussianDensity(structure: Structure, props: GaussianDensityProps, webgl?: WebGLContext) {
+export function computeStructureGaussianDensity(structure: Structure, props: GaussianDensityProps) {
     const { box } = structure.lookup3d.boundary;
     const p = ensureReasonableResolution(box, props);
     const { position, radius } = getStructureConformationAndRadius(structure, props.ignoreHydrogens, props.traceOnly);

+ 13 - 11
src/mol-repr/util.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>
  */
@@ -59,9 +59,9 @@ export interface QualityProps {
 }
 
 export const DefaultQualityThresholds = {
-    lowestElementCount: 500_000,
-    lowerElementCount: 200_000,
-    lowElementCount: 60_000,
+    lowestElementCount: 1_000_000,
+    lowerElementCount: 500_000,
+    lowElementCount: 100_000,
     mediumElementCount: 20_000,
     highElementCount: 2_000,
     coarseGrainedFactor: 10,
@@ -129,49 +129,49 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             detail = 3;
             radialSegments = 36;
             linearSegments = 18;
-            resolution = Math.max(volume / 500_000_000, 0.1);
+            resolution = 0.1;
             doubleSided = true;
             break;
         case 'higher':
             detail = 3;
             radialSegments = 28;
             linearSegments = 14;
-            resolution = Math.max(volume / 400_000_000, 0.3);
+            resolution = 0.3;
             doubleSided = true;
             break;
         case 'high':
             detail = 2;
             radialSegments = 20;
             linearSegments = 10;
-            resolution = Math.max(volume / 300_000_000, 0.5);
+            resolution = 0.5;
             doubleSided = true;
             break;
         case 'medium':
             detail = 1;
             radialSegments = 12;
             linearSegments = 8;
-            resolution = Math.max(volume / 200_000_000, 1);
+            resolution = 0.8;
             doubleSided = true;
             break;
         case 'low':
             detail = 0;
             radialSegments = 8;
             linearSegments = 3;
-            resolution = Math.max(volume / 150_000_000, 2);
+            resolution = 1.3;
             doubleSided = false;
             break;
         case 'lower':
             detail = 0;
             radialSegments = 4;
             linearSegments = 2;
-            resolution = Math.max(volume / 100_000_000, 4);
+            resolution = 3;
             doubleSided = false;
             break;
         case 'lowest':
             detail = 0;
             radialSegments = 2;
             linearSegments = 1;
-            resolution = Math.max(volume / 75_000_000, 8);
+            resolution = 8;
             doubleSided = false;
             break;
         case 'custom':
@@ -179,6 +179,8 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             break;
     }
 
+    // max resolution based on volume (for 'auto' quality)
+    resolution = Math.max(resolution, volume / 500_000_000);
     resolution = Math.min(resolution, 20);
 
     if ((props.alpha !== undefined && props.alpha < 1) || !!props.xrayShaded) {

+ 1 - 1
src/mol-repr/visual.ts

@@ -47,7 +47,7 @@ interface Visual<D, P extends PD.Params> {
     setTransparency: (transparency: Transparency) => void
     setClipping: (clipping: Clipping) => void
     destroy: () => void
-    mustRecreate?: (props: PD.Values<P>, webgl?: WebGLContext) => boolean
+    mustRecreate?: (data: D, props: PD.Values<P>, webgl?: WebGLContext) => boolean
 }
 namespace Visual {
     export type LociApply = (loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) => boolean

+ 10 - 1
src/mol-repr/volume/direct-volume.ts

@@ -21,7 +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 { createVolumeTexture2d, createVolumeTexture3d, eachVolumeLoci } from './util';
+import { createVolumeTexture2d, createVolumeTexture3d, eachVolumeLoci, getVolumeTexture2dLayout } from './util';
 
 function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
     const bbox = Box3D();
@@ -34,6 +34,11 @@ function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
 
 export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
     const gridDimension = volume.grid.cells.space.dimensions as Vec3;
+    const { width, height } = getVolumeTexture2dLayout(gridDimension);
+    if(Math.max(width, height) > webgl.maxTextureSize / 2) {
+        throw new Error('volume too large for direct-volume rendering');
+    }
+
     const textureImage = createVolumeTexture2d(volume, 'normals');
     // debugTexture(createImageData(textureImage.array, textureImage.width, textureImage.height), 1/3)
     const transform = Grid.getGridToCartesianTransform(volume.grid);
@@ -72,6 +77,10 @@ function getUnitToCartn(grid: Grid) {
 
 export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
     const gridDimension = volume.grid.cells.space.dimensions as Vec3;
+    if(Math.max(...gridDimension) > webgl.max3dTextureSize / 2) {
+        throw new Error('volume too large for direct-volume rendering');
+    }
+
     const textureVolume = createVolumeTexture3d(volume);
     const transform = Grid.getGridToCartesianTransform(volume.grid);
     const bbox = getBoundingBox(gridDimension, transform);

+ 40 - 42
src/mol-repr/volume/isosurface.ts

@@ -24,9 +24,7 @@ import { Tensor, Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { fillSerial } from '../../mol-util/array';
 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';
+import { extractIsosurface } from '../../mol-gl/compute/marching-cubes/isosurface';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
 import { Texture } from '../../mol-gl/webgl/texture';
@@ -41,10 +39,19 @@ function gpuSupport(webgl: WebGLContext) {
     return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
 }
 
-export function IsosurfaceVisual(materialId: number, props?: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) {
-    return props?.useGpu && webgl && gpuSupport(webgl)
-        ? IsosurfaceTextureMeshVisual(materialId)
-        : IsosurfaceMeshVisual(materialId);
+const Padding = 1;
+
+function suitableForGpu(volume: Volume, webgl: WebGLContext) {
+    const gridDim = volume.grid.cells.space.dimensions as Vec3;
+    const { powerOfTwoSize } = getVolumeTexture2dLayout(gridDim, Padding);
+    return powerOfTwoSize <= webgl.maxTextureSize / 2;
+}
+
+export function IsosurfaceVisual(materialId: number, volume: Volume, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) {
+    if (props.tryUseGpu && webgl && gpuSupport(webgl) && suitableForGpu(volume, webgl)) {
+        return IsosurfaceTextureMeshVisual(materialId);
+    }
+    return IsosurfaceMeshVisual(materialId);
 }
 
 function getLoci(volume: Volume, props: VolumeIsosurfaceProps) {
@@ -78,7 +85,11 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
 
     const transform = Grid.getGridToCartesianTransform(volume.grid);
     Mesh.transform(surface, transform);
-    if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface);
+    if (ctx.webgl && !ctx.webgl.isWebGL2) {
+        // 2nd arg means not to split triangles based on group id. Splitting triangles
+        // is too expensive if each cell has its own group id as is the case here.
+        Mesh.uniformTriangleGroup(surface, false);
+    }
 
     surface.setBoundingSphere(Volume.getBoundingSphere(volume));
 
@@ -90,7 +101,7 @@ export const IsosurfaceMeshParams = {
     ...TextureMesh.Params,
     ...VolumeIsosurfaceParams,
     quality: { ...Mesh.Params.quality, isEssential: false },
-    useGpu: PD.Boolean(false),
+    tryUseGpu: PD.Boolean(true),
 };
 export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
 
@@ -105,8 +116,8 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
             if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
         },
         geometryUtils: Mesh.Utils,
-        mustRecreate: (props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
-            return props.useGpu && !!webgl;
+        mustRecreate: (volume: Volume, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
+            return props.tryUseGpu && !!webgl && suitableForGpu(volume, webgl);
         }
     }, materialId);
 }
@@ -119,26 +130,30 @@ namespace VolumeIsosurfaceTexture {
     export function get(volume: Volume, webgl: WebGLContext) {
         const { resources } = webgl;
 
-        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 { 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 (texDim > webgl.maxTextureSize / 2) {
+            throw new Error('volume too large for gpu isosurface extraction');
+        }
+
         if (!volume._propertyData[name]) {
-            volume._propertyData[name] = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+            volume._propertyData[name] = resources.texture('image-uint8', 'alpha', 'ubyte', 'linear');
             const texture = volume._propertyData[name] as Texture;
             texture.define(texDim, texDim);
             // load volume into sub-section of texture
-            texture.load(createVolumeTexture2d(volume, 'groups', padding), true);
+            texture.load(createVolumeTexture2d(volume, 'data', Padding), true);
             volume.customProperties.add(descriptor);
             volume.customProperties.assets(descriptor, [{ dispose: () => texture.destroy() }]);
         }
 
-        gridDimension[0] += padding;
-        gridDimension[1] += padding;
+        gridDimension[0] += Padding;
+        gridDimension[1] += Padding;
 
         return {
             texture: volume._propertyData[name] as Texture,
@@ -160,28 +175,10 @@ async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Vol
 
     const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, ctx.webgl);
 
-    // 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();
+    const gv = extractIsosurface(ctx.webgl, texture, gridDimension, gridTexDim, gridTexScale, transform, isoLevel, false, textureMesh?.vertexTexture.ref.value, textureMesh?.groupTexture.ref.value, textureMesh?.normalTexture.ref.value);
+
+    const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, Volume.getBoundingSphere(volume), textureMesh);
+
     return surface;
 }
 
@@ -196,12 +193,13 @@ export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<Is
             if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
         },
         geometryUtils: TextureMesh.Utils,
-        mustRecreate: (props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
-            return !props.useGpu || !webgl;
+        mustRecreate: (volume: Volume, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
+            return !props.tryUseGpu || !webgl || !suitableForGpu(volume, webgl);
         },
         dispose: (geometry: TextureMesh) => {
+            geometry.vertexTexture.ref.value.destroy();
+            geometry.groupTexture.ref.value.destroy();
             geometry.normalTexture.ref.value.destroy();
-            geometry.vertexGroupTexture.ref.value.destroy();
         }
     }, materialId);
 }

+ 5 - 5
src/mol-repr/volume/representation.ts

@@ -49,7 +49,7 @@ interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
     getLoci(pickingId: PickingId, volume: Volume, props: PD.Values<P>, id: number): Loci
     eachLocation(loci: Loci, volume: Volume, props: PD.Values<P>, apply: (interval: Interval) => boolean): boolean
     setUpdateState(state: VisualUpdateState, volume: Volume, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
-    mustRecreate?: (props: PD.Values<P>) => boolean
+    mustRecreate?: (volume: Volume, props: PD.Values<P>) => boolean
     dispose?: (geometry: G) => void
 }
 
@@ -231,7 +231,7 @@ export const VolumeParams = {
 };
 export type VolumeParams = typeof VolumeParams
 
-export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, P>, visualCtor: (materialId: number, props?: PD.Values<P>, webgl?: WebGLContext) => VolumeVisual<P>, getLoci: (volume: Volume, props: PD.Values<P>) => Loci): VolumeRepresentation<P> {
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, P>, visualCtor: (materialId: number, volume: Volume, props: PD.Values<P>, webgl?: WebGLContext) => VolumeVisual<P>, getLoci: (volume: Volume, props: PD.Values<P>) => Loci): VolumeRepresentation<P> {
     let version = 0;
     const { webgl } = ctx;
     const updated = new Subject<number>();
@@ -256,10 +256,10 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
 
         return Task.create('Creating or updating VolumeRepresentation', async runtime => {
             if (!visual) {
-                visual = visualCtor(materialId, _props, webgl);
-            } else if (visual.mustRecreate?.(_props, webgl)) {
+                visual = visualCtor(materialId, _volume, _props, webgl);
+            } else if (visual.mustRecreate?.(_volume, _props, webgl)) {
                 visual.destroy();
-                visual = visualCtor(materialId, _props, webgl);
+                visual = visualCtor(materialId, _volume, _props, webgl);
             }
             const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, volume);
             if (promise) await promise;

+ 26 - 21
src/mol-repr/volume/util.ts

@@ -75,13 +75,14 @@ export function getVolumeTexture2dLayout(dim: Vec3, padding = 0) {
     return { width, height, columns, rows, powerOfTwoSize: height < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
 }
 
-export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups', padding = 0) {
+export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups' | 'data', 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 itemSize = variant === 'data' ? 1 : 4;
+    const array = new Uint8Array(width * height * itemSize);
     const textureImage = { array, width, height };
 
     const diff = max - min;
@@ -102,28 +103,32 @@ export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'grou
                 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 index = itemSize * ((row * ynp * width) + (y * width) + px);
                 const offset = o(x, y, z);
 
-                if (variant === 'groups') {
-                    encodeFloatRGBtoArray(offset, array, index);
+                if (variant === 'data') {
+                    array[index] = Math.round(((data[offset] - min) / diff) * 255);
                 } 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);
+                    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] = Math.round(((data[offset] - min) / diff) * 255);
                 }
-
-                array[index + 3] = ((data[offset] - min) / diff) * 255;
             }
         }
     }
@@ -167,7 +172,7 @@ export function createVolumeTexture3d(volume: Volume) {
                 v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
                 v3toArray(v3scale(n0, n0, 255), array, i);
 
-                array[i + 3] = ((data[offset] - min) / diff) * 255;
+                array[i + 3] = Math.round(((data[offset] - min) / diff) * 255);
                 i += 4;
             }
         }

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

@@ -73,7 +73,7 @@ async function init() {
         console.timeEnd('gpu mc pyramid2');
 
         console.time('gpu mc vert2');
-        createIsosurfaceBuffers(webgl, activeVoxelsTex2, densityTextureData2.texture, compacted2, densityTextureData2.gridDim, densityTextureData2.gridTexDim, densityTextureData2.transform, isoValue);
+        createIsosurfaceBuffers(webgl, activeVoxelsTex2, densityTextureData2.texture, compacted2, densityTextureData2.gridDim, densityTextureData2.gridTexDim, densityTextureData2.transform, isoValue, true);
         webgl.waitForGpuCommandsCompleteSync();
         console.timeEnd('gpu mc vert2');
         console.timeEnd('gpu mc2');
@@ -96,7 +96,7 @@ async function init() {
     console.timeEnd('gpu mc pyramid');
 
     console.time('gpu mc vert');
-    const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoValue);
+    const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoValue, true);
     webgl.waitForGpuCommandsCompleteSync();
     console.timeEnd('gpu mc vert');
     console.timeEnd('gpu mc');
@@ -104,7 +104,7 @@ async function init() {
     console.log({ ...webgl.stats, programCount: webgl.stats.resourceCounts.program, shaderCount: webgl.stats.resourceCounts.shader });
 
     const mcBoundingSphere = Sphere3D.fromBox3D(Sphere3D(), densityTextureData.bbox);
-    const mcIsosurface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, mcBoundingSphere);
+    const mcIsosurface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, mcBoundingSphere);
     const mcIsoSurfaceProps = { doubleSided: true, flatShaded: true, alpha: 1.0 };
     const mcIsoSurfaceValues = TextureMesh.Utils.createValuesSimple(mcIsosurface, mcIsoSurfaceProps, Color(0x112299), 1);
     // console.log('mcIsoSurfaceValues', mcIsoSurfaceValues)