Browse Source

Merge pull request #173 from molstar/smcol

Color smoothing
Alexander Rose 3 years ago
parent
commit
5edae9d6f7
54 changed files with 1636 additions and 407 deletions
  1. 1 0
      CHANGELOG.md
  2. 204 168
      src/extensions/geo-export/glb-exporter.ts
  3. 64 32
      src/extensions/geo-export/mesh-exporter.ts
  4. 182 80
      src/extensions/geo-export/obj-exporter.ts
  5. 65 47
      src/extensions/geo-export/stl-exporter.ts
  6. 1 0
      src/extensions/pdbe/structure-quality-report/color.ts
  7. 1 0
      src/extensions/rcsb/validation-report/color/density-fit.ts
  8. 1 0
      src/extensions/rcsb/validation-report/color/geometry-quality.ts
  9. 1 0
      src/extensions/rcsb/validation-report/color/random-coil-index.ts
  10. 82 31
      src/mol-geo/geometry/color-data.ts
  11. 229 0
      src/mol-geo/geometry/mesh/color-smoothing.ts
  12. 3 1
      src/mol-geo/geometry/mesh/mesh.ts
  13. 344 0
      src/mol-geo/geometry/texture-mesh/color-smoothing.ts
  14. 2 0
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  15. 5 2
      src/mol-gl/renderable/schema.ts
  16. 1 1
      src/mol-gl/renderable/util.ts
  17. 1 1
      src/mol-gl/renderer.ts
  18. 6 0
      src/mol-gl/shader/chunks/assign-color-varying.glsl.ts
  19. 2 1
      src/mol-gl/shader/chunks/assign-position.glsl.ts
  20. 6 0
      src/mol-gl/shader/chunks/color-vert-params.glsl.ts
  21. 5 1
      src/mol-gl/shader/chunks/common.glsl.ts
  22. 28 0
      src/mol-gl/shader/compute/color-smoothing/accumulate.frag.ts
  23. 51 0
      src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts
  24. 20 0
      src/mol-gl/shader/compute/color-smoothing/normalize.frag.ts
  25. 6 2
      src/mol-gl/shader/mesh.vert.ts
  26. 1 1
      src/mol-gl/webgl/render-target.ts
  27. 11 7
      src/mol-gl/webgl/texture.ts
  28. 1 0
      src/mol-math/geometry/common.ts
  29. 2 0
      src/mol-math/geometry/gaussian-density.ts
  30. 1 1
      src/mol-math/geometry/gaussian-density/cpu.ts
  31. 11 12
      src/mol-math/geometry/gaussian-density/gpu.ts
  32. 2 2
      src/mol-math/geometry/molecular-surface.ts
  33. 1 0
      src/mol-model-props/computed/themes/accessible-surface-area.ts
  34. 19 3
      src/mol-repr/structure/complex-visual.ts
  35. 24 4
      src/mol-repr/structure/units-visual.ts
  36. 83 2
      src/mol-repr/structure/visual/gaussian-surface-mesh.ts
  37. 33 3
      src/mol-repr/structure/visual/molecular-surface-mesh.ts
  38. 103 0
      src/mol-repr/structure/visual/util/color.ts
  39. 10 0
      src/mol-repr/util.ts
  40. 0 1
      src/mol-repr/visual.ts
  41. 2 1
      src/mol-repr/volume/representation.ts
  42. 5 2
      src/mol-theme/color.ts
  43. 1 0
      src/mol-theme/color/atom-id.ts
  44. 1 0
      src/mol-theme/color/element-index.ts
  45. 2 1
      src/mol-theme/color/element-symbol.ts
  46. 1 0
      src/mol-theme/color/hydrophobicity.ts
  47. 1 0
      src/mol-theme/color/illustrative.ts
  48. 1 0
      src/mol-theme/color/occupancy.ts
  49. 1 0
      src/mol-theme/color/partial-charge.ts
  50. 1 0
      src/mol-theme/color/residue-name.ts
  51. 1 0
      src/mol-theme/color/secondary-structure.ts
  52. 1 0
      src/mol-theme/color/sequence-id.ts
  53. 1 0
      src/mol-theme/color/uncertainty.ts
  54. 4 0
      src/mol-util/type-helpers.ts

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@ Note that since we don't clearly distinguish between a public and private interf
     - Change O-S bond distance to allow for NOS bridges (doi:10.1038/s41586-021-03513-3)
     - Added NOS-bridges query & improved disulfide-bridges query
 - Fix #178: ``IndexPairBonds`` for non-single residue structures (bug due to atom reordering).
+- Add volumetric color smoothing for MolecularSurface and GaussianSurface representations (#173)
 
 ## [v2.0.5] - 2021-04-26
 

+ 204 - 168
src/extensions/geo-export/glb-exporter.ts

@@ -4,7 +4,6 @@
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  */
 
-import { BaseValues } from '../../mol-gl/renderable/schema';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
 import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
@@ -12,7 +11,7 @@ import { RuntimeContext } from '../../mol-task';
 import { Color } from '../../mol-util/color/color';
 import { arrayMinMax, fillSerial } from '../../mol-util/array';
 import { NumberArray } from '../../mol-util/type-helpers';
-import { MeshExporter } from './mesh-exporter';
+import { MeshExporter, AddMeshInput } from './mesh-exporter';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3fromArray = Vec3.fromArray;
@@ -48,7 +47,9 @@ export class GlbExporter extends MeshExporter<GlbData> {
         return [ min, max ];
     }
 
-    protected async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, groups: Float32Array | Uint8Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, isGeoTexture: boolean, ctx: RuntimeContext) {
+    protected async addMeshWithColors(input: AddMeshInput) {
+        const { mesh, meshes, values, isGeoTexture, webgl, ctx } = input;
+
         const t = Mat4();
         const n = Mat3();
         const tmpV = Vec3();
@@ -61,185 +62,220 @@ export class GlbExporter extends MeshExporter<GlbData> {
         const dTransparency = values.dTransparency.ref.value;
         const tTransparency = values.tTransparency.ref.value;
         const aTransform = values.aTransform.ref.value;
+        const instanceCount = values.uInstanceCount.ref.value;
 
-        Mat4.fromArray(t, aTransform, instanceIndex * 16);
-        mat3directionTransform(n, t);
+        let interpolatedColors: Uint8Array;
+        if (colorType === 'volume' || colorType === 'volumeInstance') {
+            interpolatedColors = GlbExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
+        }
 
-        const currentProgress = (vertexCount * 3) * instanceIndex;
-        await ctx.update({ isIndeterminate: false, current: currentProgress, max: (vertexCount * 3) * values.uInstanceCount.ref.value });
+        await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
+
+            let vertices: Float32Array;
+            let normals: Float32Array;
+            let indices: Uint32Array | undefined;
+            let groups: Float32Array | Uint8Array;
+            let vertexCount: number;
+            let drawCount: number;
+            if (mesh !== undefined) {
+                vertices = mesh.vertices;
+                normals = mesh.normals;
+                indices = mesh.indices;
+                groups = mesh.groups;
+                vertexCount = mesh.vertexCount;
+                drawCount = mesh.drawCount;
+            } else {
+                const mesh = meshes![instanceIndex];
+                vertices = mesh.vertexBuffer.ref.value;
+                normals = mesh.normalBuffer.ref.value;
+                indices = mesh.indexBuffer.ref.value;
+                groups = mesh.groupBuffer.ref.value;
+                vertexCount = mesh.vertexCount;
+                drawCount = mesh.triangleCount * 3;
+            }
 
-        const vertexArray = new Float32Array(vertexCount * 3);
-        const normalArray = new Float32Array(vertexCount * 3);
-        const colorArray = new Float32Array(vertexCount * 4);
-        let indexArray: Uint32Array;
+            Mat4.fromArray(t, aTransform, instanceIndex * 16);
+            mat3directionTransform(n, t);
 
-        // position
-        for (let i = 0; i < vertexCount; ++i) {
-            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + i });
-            v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
-            v3toArray(tmpV, vertexArray, i * 3);
-        }
+            const vertexArray = new Float32Array(vertexCount * 3);
+            const normalArray = new Float32Array(vertexCount * 3);
+            const colorArray = new Float32Array(vertexCount * 4);
+            let indexArray: Uint32Array;
 
-        // normal
-        for (let i = 0; i < vertexCount; ++i) {
-            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount + i });
-            v3fromArray(tmpV, normals, i * stride);
-            v3transformMat3(tmpV, v3normalize(tmpV, tmpV), n);
-            v3toArray(tmpV, normalArray, i * 3);
-        }
+            // position
+            for (let i = 0; i < vertexCount; ++i) {
+                v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
+                v3toArray(tmpV, vertexArray, i * 3);
+            }
 
-        // color
-        for (let i = 0; i < vertexCount; ++i) {
-            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount * 2 + i });
-
-            let color: Color;
-            switch (colorType) {
-                case 'uniform':
-                    color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
-                    break;
-                case 'instance':
-                    color = Color.fromArray(tColor, instanceIndex * 3);
-                    break;
-                case 'group': {
-                    const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
-                    color = Color.fromArray(tColor, group * 3);
-                    break;
+            // normal
+            for (let i = 0; i < vertexCount; ++i) {
+                v3fromArray(tmpV, normals, i * stride);
+                v3transformMat3(tmpV, v3normalize(tmpV, tmpV), n);
+                v3toArray(tmpV, normalArray, i * 3);
+            }
+
+            // color
+            for (let i = 0; i < vertexCount; ++i) {
+                let color: Color;
+                switch (colorType) {
+                    case 'uniform':
+                        color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
+                        break;
+                    case 'instance':
+                        color = Color.fromArray(tColor, instanceIndex * 3);
+                        break;
+                    case 'group': {
+                        const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
+                        color = Color.fromArray(tColor, group * 3);
+                        break;
+                    }
+                    case 'groupInstance': {
+                        const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
+                        color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
+                        break;
+                    }
+                    case 'vertex':
+                        color = Color.fromArray(tColor, i * 3);
+                        break;
+                    case 'vertexInstance':
+                        color = Color.fromArray(tColor, (instanceIndex * drawCount + i) * 3);
+                        break;
+                    case 'volume':
+                        color = Color.fromArray(interpolatedColors!, i * 3);
+                        break;
+                    case 'volumeInstance':
+                        color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + i) * 3);
+                        break;
+                    default: throw new Error('Unsupported color type.');
                 }
-                case 'groupInstance': {
+
+                let alpha = uAlpha;
+                if (dTransparency) {
                     const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
-                    color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
-                    break;
+                    const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
+                    alpha *= 1 - transparency;
                 }
-                case 'vertex':
-                    color = Color.fromArray(tColor, i * 3);
-                    break;
-                case 'vertexInstance':
-                    color = Color.fromArray(tColor, (instanceIndex * drawCount + i) * 3);
-                    break;
-                default: throw new Error('Unsupported color type.');
-            }
 
-            let alpha = uAlpha;
-            if (dTransparency) {
-                const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
-                const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
-                alpha *= 1 - transparency;
+                color = Color.sRGBToLinear(color);
+                Color.toArrayNormalized(color, colorArray, i * 4);
+                colorArray[i * 4 + 3] = alpha;
             }
 
-            Color.toArrayNormalized(color, colorArray, i * 4);
-            colorArray[i * 4 + 3] = alpha;
-        }
-
-        // face
-        if (isGeoTexture) {
-            indexArray = new Uint32Array(drawCount);
-            fillSerial(indexArray);
-        } else {
-            indexArray = indices!.slice(0, drawCount);
-        }
+            // face
+            if (isGeoTexture) {
+                indexArray = new Uint32Array(drawCount);
+                fillSerial(indexArray);
+            } else {
+                indexArray = indices!.slice(0, drawCount);
+            }
 
-        const [ vertexMin, vertexMax ] = GlbExporter.vecMinMax(vertexArray, 3);
-        const [ normalMin, normalMax ] = GlbExporter.vecMinMax(normalArray, 3);
-        const [ colorMin, colorMax ] = GlbExporter.vecMinMax(colorArray, 4);
-        const [ indexMin, indexMax ] = arrayMinMax(indexArray);
-
-        // binary buffer
-        let vertexBuffer = vertexArray.buffer;
-        let normalBuffer = normalArray.buffer;
-        let colorBuffer = colorArray.buffer;
-        let indexBuffer = indexArray.buffer;
-        if (!IsNativeEndianLittle) {
-            vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
-            normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
-            colorBuffer = flipByteOrder(new Uint8Array(colorBuffer), 4);
-            indexBuffer = flipByteOrder(new Uint8Array(indexBuffer), 4);
+            const [ vertexMin, vertexMax ] = GlbExporter.vecMinMax(vertexArray, 3);
+            const [ normalMin, normalMax ] = GlbExporter.vecMinMax(normalArray, 3);
+            const [ colorMin, colorMax ] = GlbExporter.vecMinMax(colorArray, 4);
+            const [ indexMin, indexMax ] = arrayMinMax(indexArray);
+
+            // binary buffer
+            let vertexBuffer = vertexArray.buffer;
+            let normalBuffer = normalArray.buffer;
+            let colorBuffer = colorArray.buffer;
+            let indexBuffer = indexArray.buffer;
+            if (!IsNativeEndianLittle) {
+                vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
+                normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
+                colorBuffer = flipByteOrder(new Uint8Array(colorBuffer), 4);
+                indexBuffer = flipByteOrder(new Uint8Array(indexBuffer), 4);
+            }
+            this.binaryBuffer.push(vertexBuffer, normalBuffer, colorBuffer, indexBuffer);
+
+            // buffer views
+            const bufferViewOffset = this.bufferViews.length;
+
+            this.bufferViews.push({
+                buffer: 0,
+                byteOffset: this.byteOffset,
+                byteLength: vertexBuffer.byteLength,
+                target: 34962 // ARRAY_BUFFER
+            });
+            this.byteOffset += vertexBuffer.byteLength;
+
+            this.bufferViews.push({
+                buffer: 0,
+                byteOffset: this.byteOffset,
+                byteLength: normalBuffer.byteLength,
+                target: 34962 // ARRAY_BUFFER
+            });
+            this.byteOffset += normalBuffer.byteLength;
+
+            this.bufferViews.push({
+                buffer: 0,
+                byteOffset: this.byteOffset,
+                byteLength: colorBuffer.byteLength,
+                target: 34962 // ARRAY_BUFFER
+            });
+            this.byteOffset += colorBuffer.byteLength;
+
+            this.bufferViews.push({
+                buffer: 0,
+                byteOffset: this.byteOffset,
+                byteLength: indexBuffer.byteLength,
+                target: 34963 // ELEMENT_ARRAY_BUFFER
+            });
+            this.byteOffset += indexBuffer.byteLength;
+
+            // accessors
+            const accessorOffset = this.accessors.length;
+            this.accessors.push({
+                bufferView: bufferViewOffset,
+                byteOffset: 0,
+                componentType: 5126, // FLOAT
+                count: vertexCount,
+                type: 'VEC3',
+                max: vertexMax,
+                min: vertexMin
+            });
+            this.accessors.push({
+                bufferView: bufferViewOffset + 1,
+                byteOffset: 0,
+                componentType: 5126, // FLOAT
+                count: vertexCount,
+                type: 'VEC3',
+                max: normalMax,
+                min: normalMin
+            });
+            this.accessors.push({
+                bufferView: bufferViewOffset + 2,
+                byteOffset: 0,
+                componentType: 5126, // FLOAT
+                count: vertexCount,
+                type: 'VEC4',
+                max: colorMax,
+                min: colorMin
+            });
+            this.accessors.push({
+                bufferView: bufferViewOffset + 3,
+                byteOffset: 0,
+                componentType: 5125, // UNSIGNED_INT
+                count: drawCount,
+                type: 'SCALAR',
+                max: [ indexMax ],
+                min: [ indexMin ]
+            });
+
+            // primitive
+            this.primitives.push({
+                attributes: {
+                    POSITION: accessorOffset,
+                    NORMAL: accessorOffset + 1,
+                    COLOR_0: accessorOffset + 2,
+                },
+                indices: accessorOffset + 3,
+                material: 0
+            });
         }
-        this.binaryBuffer.push(vertexBuffer, normalBuffer, colorBuffer, indexBuffer);
-
-        // buffer views
-        const bufferViewOffset = this.bufferViews.length;
-
-        this.bufferViews.push({
-            buffer: 0,
-            byteOffset: this.byteOffset,
-            byteLength: vertexBuffer.byteLength,
-            target: 34962 // ARRAY_BUFFER
-        });
-        this.byteOffset += vertexBuffer.byteLength;
-
-        this.bufferViews.push({
-            buffer: 0,
-            byteOffset: this.byteOffset,
-            byteLength: normalBuffer.byteLength,
-            target: 34962 // ARRAY_BUFFER
-        });
-        this.byteOffset += normalBuffer.byteLength;
-
-        this.bufferViews.push({
-            buffer: 0,
-            byteOffset: this.byteOffset,
-            byteLength: colorBuffer.byteLength,
-            target: 34962 // ARRAY_BUFFER
-        });
-        this.byteOffset += colorBuffer.byteLength;
-
-        this.bufferViews.push({
-            buffer: 0,
-            byteOffset: this.byteOffset,
-            byteLength: indexBuffer.byteLength,
-            target: 34963 // ELEMENT_ARRAY_BUFFER
-        });
-        this.byteOffset += indexBuffer.byteLength;
-
-        // accessors
-        const accessorOffset = this.accessors.length;
-        this.accessors.push({
-            bufferView: bufferViewOffset,
-            byteOffset: 0,
-            componentType: 5126, // FLOAT
-            count: vertexCount,
-            type: 'VEC3',
-            max: vertexMax,
-            min: vertexMin
-        });
-        this.accessors.push({
-            bufferView: bufferViewOffset + 1,
-            byteOffset: 0,
-            componentType: 5126, // FLOAT
-            count: vertexCount,
-            type: 'VEC3',
-            max: normalMax,
-            min: normalMin
-        });
-        this.accessors.push({
-            bufferView: bufferViewOffset + 2,
-            byteOffset: 0,
-            componentType: 5126, // FLOAT
-            count: vertexCount,
-            type: 'VEC4',
-            max: colorMax,
-            min: colorMin
-        });
-        this.accessors.push({
-            bufferView: bufferViewOffset + 3,
-            byteOffset: 0,
-            componentType: 5125, // UNSIGNED_INT
-            count: drawCount,
-            type: 'SCALAR',
-            max: [ indexMax ],
-            min: [ indexMin ]
-        });
-
-        // primitive
-        this.primitives.push({
-            attributes: {
-                POSITION: accessorOffset,
-                NORMAL: accessorOffset + 1,
-                COLOR_0: accessorOffset + 2,
-            },
-            indices: accessorOffset + 3,
-            material: 0
-        });
     }
 
     getData() {

+ 64 - 32
src/extensions/geo-export/mesh-exporter.ts

@@ -14,6 +14,8 @@ import { TextureMeshValues } from '../../mol-gl/renderable/texture-mesh';
 import { BaseValues, SizeValues } from '../../mol-gl/renderable/schema';
 import { TextureImage } from '../../mol-gl/renderable/util';
 import { WebGLContext } from '../../mol-gl/webgl/context';
+import { getTrilinearlyInterpolated } from '../../mol-geo/geometry/mesh/color-smoothing';
+import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
 import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
 import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
@@ -23,9 +25,27 @@ import { RuntimeContext } from '../../mol-task';
 import { decodeFloatRGB } from '../../mol-util/float-packing';
 import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
 
+const GeoExportName = 'geo-export';
+
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3fromArray = Vec3.fromArray;
 
+export interface AddMeshInput {
+    mesh: {
+        vertices: Float32Array
+        normals: Float32Array
+        indices: Uint32Array | undefined
+        groups: Float32Array | Uint8Array
+        vertexCount: number
+        drawCount: number
+    } | undefined
+    meshes: Mesh[] | undefined
+    values: BaseValues
+    isGeoTexture: boolean
+    webgl: WebGLContext | undefined
+    ctx: RuntimeContext
+}
+
 export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
     abstract readonly fileExtension: string;
 
@@ -68,37 +88,58 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         return decodeFloatRGB(r, g, b);
     }
 
-    protected abstract addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, groups: Float32Array | Uint8Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, isGeoTexture: boolean, ctx: RuntimeContext): void;
+    protected static getInterpolatedColors(vertices: Float32Array, vertexCount: number, values: BaseValues, stride: number, colorType: 'volume' | 'volumeInstance', webgl: WebGLContext) {
+        const colorGridTransform = values.uColorGridTransform.ref.value;
+        const colorGridDim = values.uColorGridDim.ref.value;
+        const colorTexDim = values.uColorTexDim.ref.value;
+        const aTransform = values.aTransform.ref.value;
+        const instanceCount = values.uInstanceCount.ref.value;
 
-    private async addMesh(values: MeshValues, ctx: RuntimeContext) {
+        if (!webgl.namedFramebuffers[GeoExportName]) {
+            webgl.namedFramebuffers[GeoExportName] = webgl.resources.framebuffer();
+        }
+        const framebuffer = webgl.namedFramebuffers[GeoExportName];
+
+        const [ width, height ] = values.uColorTexDim.ref.value;
+        const colorGrid = new Uint8Array(width * height * 4);
+
+        framebuffer.bind();
+        values.tColorGrid.ref.value.attachFramebuffer(framebuffer, 0);
+        webgl.readPixels(0, 0, width, height, colorGrid);
+
+        const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer: aTransform, positionBuffer: vertices, colorType, grid: colorGrid, gridDim: colorGridDim, gridTexDim: colorTexDim, gridTransform: colorGridTransform, vertexStride: stride, colorStride: 4 });
+        return interpolated.array;
+    }
+
+    protected abstract addMeshWithColors(inpit: AddMeshInput): void;
+
+    private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
         const aPosition = values.aPosition.ref.value;
         const aNormal = values.aNormal.ref.value;
         const elements = values.elements.ref.value;
         const aGroup = values.aGroup.ref.value;
-        const instanceCount = values.instanceCount.ref.value;
         const vertexCount = values.uVertexCount.ref.value;
         const drawCount = values.drawCount.ref.value;
 
-        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
-            await this.addMeshWithColors(aPosition, aNormal, elements, aGroup, vertexCount, drawCount, values, instanceIndex, false, ctx);
-        }
+        await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices: elements, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, webgl, ctx });
     }
 
-    private async addLines(values: LinesValues, ctx: RuntimeContext) {
+    private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
         // TODO
     }
 
-    private async addPoints(values: PointsValues, ctx: RuntimeContext) {
+    private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
         // TODO
     }
 
-    private async addSpheres(values: SpheresValues, ctx: RuntimeContext) {
+    private async addSpheres(values: SpheresValues, webgl: WebGLContext, ctx: RuntimeContext) {
         const center = Vec3();
 
         const aPosition = values.aPosition.ref.value;
         const aGroup = values.aGroup.ref.value;
         const instanceCount = values.instanceCount.ref.value;
         const vertexCount = values.uVertexCount.ref.value;
+        const meshes: Mesh[] = [];
 
         for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
             const state = MeshBuilder.createState(512, 256);
@@ -112,16 +153,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
                 addSphere(state, center, radius, 2);
             }
 
-            const mesh = MeshBuilder.getMesh(state);
-            const vertices = mesh.vertexBuffer.ref.value;
-            const normals = mesh.normalBuffer.ref.value;
-            const indices = mesh.indexBuffer.ref.value;
-            const groups = mesh.groupBuffer.ref.value;
-            await this.addMeshWithColors(vertices, normals, indices, groups, mesh.vertexCount, indices.length, values, instanceIndex, false, ctx);
+            meshes.push(MeshBuilder.getMesh(state));
         }
+
+        await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
     }
 
-    private async addCylinders(values: CylindersValues, ctx: RuntimeContext) {
+    private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
         const start = Vec3();
         const end = Vec3();
 
@@ -132,6 +170,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         const aGroup = values.aGroup.ref.value;
         const instanceCount = values.instanceCount.ref.value;
         const vertexCount = values.uVertexCount.ref.value;
+        const meshes: Mesh[] = [];
 
         for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
             const state = MeshBuilder.createState(512, 256);
@@ -150,17 +189,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
                 addCylinder(state, start, end, 1, cylinderProps);
             }
 
-            const mesh = MeshBuilder.getMesh(state);
-            const vertices = mesh.vertexBuffer.ref.value;
-            const normals = mesh.normalBuffer.ref.value;
-            const indices = mesh.indexBuffer.ref.value;
-            const groups = mesh.groupBuffer.ref.value;
-            await this.addMeshWithColors(vertices, normals, indices, groups, mesh.vertexCount, indices.length, values, instanceIndex, false, ctx);
+            meshes.push(MeshBuilder.getMesh(state));
         }
+
+        await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
     }
 
     private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
-        const GeoExportName = 'geo-export';
         if (!webgl.namedFramebuffers[GeoExportName]) {
             webgl.namedFramebuffers[GeoExportName] = webgl.resources.framebuffer();
         }
@@ -180,12 +215,9 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         webgl.readPixels(0, 0, width, height, groups);
 
         const vertexCount = values.uVertexCount.ref.value;
-        const instanceCount = values.instanceCount.ref.value;
         const drawCount = values.drawCount.ref.value;
 
-        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
-            await this.addMeshWithColors(vertices, normals, undefined, groups, vertexCount, drawCount, values, instanceIndex, true, ctx);
-        }
+        await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, webgl, ctx });
     }
 
     add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext) {
@@ -193,15 +225,15 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
 
         switch (renderObject.type) {
             case 'mesh':
-                return this.addMesh(renderObject.values as MeshValues, ctx);
+                return this.addMesh(renderObject.values as MeshValues, webgl, ctx);
             case 'lines':
-                return this.addLines(renderObject.values as LinesValues, ctx);
+                return this.addLines(renderObject.values as LinesValues, webgl, ctx);
             case 'points':
-                return this.addPoints(renderObject.values as PointsValues, ctx);
+                return this.addPoints(renderObject.values as PointsValues, webgl, ctx);
             case 'spheres':
-                return this.addSpheres(renderObject.values as SpheresValues, ctx);
+                return this.addSpheres(renderObject.values as SpheresValues, webgl, ctx);
             case 'cylinders':
-                return this.addCylinders(renderObject.values as CylindersValues, ctx);
+                return this.addCylinders(renderObject.values as CylindersValues, webgl, ctx);
             case 'texture-mesh':
                 return this.addTextureMesh(renderObject.values as TextureMeshValues, webgl, ctx);
         }

+ 182 - 80
src/extensions/geo-export/obj-exporter.ts

@@ -4,14 +4,14 @@
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  */
 
-import { BaseValues } from '../../mol-gl/renderable/schema';
+import { sort, arraySwap } from '../../mol-data/util';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
 import { RuntimeContext } from '../../mol-task';
 import { StringBuilder } from '../../mol-util';
 import { Color } from '../../mol-util/color/color';
 import { zip } from '../../mol-util/zip/zip';
-import { MeshExporter } from './mesh-exporter';
+import { MeshExporter, AddMeshInput } from './mesh-exporter';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3fromArray = Vec3.fromArray;
@@ -67,7 +67,73 @@ export class ObjExporter extends MeshExporter<ObjData> {
         }
     }
 
-    protected async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, groups: Float32Array | Uint8Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, isGeoTexture: boolean, ctx: RuntimeContext) {
+    private static quantizeColors(colorArray: Uint8Array, vertexCount: number) {
+        if (vertexCount <= 1024) return;
+        const rgb = Vec3();
+        const min = Vec3();
+        const max = Vec3();
+        const sum = Vec3();
+        const colorMap = new Map<Color, Color>();
+        const colorComparers = [
+            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]),
+            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]),
+            (colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]),
+        ];
+
+        const medianCut = (colors: Color[], l: number, r: number, depth: number) => {
+            if (l > r) return;
+            if (l === r || depth >= 10) {
+                // Find the average color.
+                Vec3.set(sum, 0, 0, 0);
+                for (let i = l; i <= r; ++i) {
+                    Color.toVec3(rgb, colors[i]);
+                    Vec3.add(sum, sum, rgb);
+                }
+                Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1)));
+                const averageColor = Color.fromArray(rgb, 0);
+                for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor);
+                return;
+            }
+
+            // Find the color channel with the greatest range.
+            Vec3.set(min, 255, 255, 255);
+            Vec3.set(max, 0, 0, 0);
+            for (let i = l; i <= r; ++i) {
+                Color.toVec3(rgb, colors[i]);
+                for (let j = 0; j < 3; ++j) {
+                    Vec3.min(min, min, rgb);
+                    Vec3.max(max, max, rgb);
+                }
+            }
+            let k = 0;
+            if (max[1] - min[1] > max[k] - min[k]) k = 1;
+            if (max[2] - min[2] > max[k] - min[k]) k = 2;
+
+            sort(colors, l, r + 1, colorComparers[k], arraySwap);
+
+            const m = (l + r) >> 1;
+            medianCut(colors, l, m, depth + 1);
+            medianCut(colors, m + 1, r, depth + 1);
+        };
+
+        // Create an array of unique colors and use the median cut algorithm.
+        const colorSet = new Set<Color>();
+        for (let i = 0; i < vertexCount; ++i) {
+            colorSet.add(Color.fromArray(colorArray, i * 3));
+        }
+        const colors = Array.from(colorSet);
+        medianCut(colors, 0, colors.length - 1, 0);
+
+        // Map actual colors to quantized colors.
+        for (let i = 0; i < vertexCount; ++i) {
+            const color = colorMap.get(Color.fromArray(colorArray, i * 3));
+            Color.toArray(color!, colorArray, i * 3);
+        }
+    }
+
+    protected async addMeshWithColors(input: AddMeshInput) {
+        const { mesh, meshes, values, isGeoTexture, webgl, ctx } = input;
+
         const obj = this.obj;
         const t = Mat4();
         const n = Mat3();
@@ -81,95 +147,131 @@ export class ObjExporter extends MeshExporter<ObjData> {
         const dTransparency = values.dTransparency.ref.value;
         const tTransparency = values.tTransparency.ref.value;
         const aTransform = values.aTransform.ref.value;
+        const instanceCount = values.uInstanceCount.ref.value;
 
-        Mat4.fromArray(t, aTransform, instanceIndex * 16);
-        mat3directionTransform(n, t);
+        let interpolatedColors: Uint8Array;
+        if (colorType === 'volume' || colorType === 'volumeInstance') {
+            interpolatedColors = ObjExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
+            ObjExporter.quantizeColors(interpolatedColors, mesh!.vertexCount);
+        }
 
-        const currentProgress = (vertexCount * 2 + drawCount) * instanceIndex;
-        await ctx.update({ isIndeterminate: false, current: currentProgress, max: (vertexCount * 2 + drawCount) * values.uInstanceCount.ref.value });
+        await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
 
-        // position
-        for (let i = 0; i < vertexCount; ++i) {
-            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + i });
-            v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
-            StringBuilder.writeSafe(obj, 'v ');
-            StringBuilder.writeFloat(obj, tmpV[0], 1000);
-            StringBuilder.whitespace1(obj);
-            StringBuilder.writeFloat(obj, tmpV[1], 1000);
-            StringBuilder.whitespace1(obj);
-            StringBuilder.writeFloat(obj, tmpV[2], 1000);
-            StringBuilder.newline(obj);
-        }
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
 
-        // normal
-        for (let i = 0; i < vertexCount; ++i) {
-            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount + i });
-            v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
-            StringBuilder.writeSafe(obj, 'vn ');
-            StringBuilder.writeFloat(obj, tmpV[0], 100);
-            StringBuilder.whitespace1(obj);
-            StringBuilder.writeFloat(obj, tmpV[1], 100);
-            StringBuilder.whitespace1(obj);
-            StringBuilder.writeFloat(obj, tmpV[2], 100);
-            StringBuilder.newline(obj);
-        }
+            let vertices: Float32Array;
+            let normals: Float32Array;
+            let indices: Uint32Array | undefined;
+            let groups: Float32Array | Uint8Array;
+            let vertexCount: number;
+            let drawCount: number;
+            if (mesh !== undefined) {
+                vertices = mesh.vertices;
+                normals = mesh.normals;
+                indices = mesh.indices;
+                groups = mesh.groups;
+                vertexCount = mesh.vertexCount;
+                drawCount = mesh.drawCount;
+            } else {
+                const mesh = meshes![instanceIndex];
+                vertices = mesh.vertexBuffer.ref.value;
+                normals = mesh.normalBuffer.ref.value;
+                indices = mesh.indexBuffer.ref.value;
+                groups = mesh.groupBuffer.ref.value;
+                vertexCount = mesh.vertexCount;
+                drawCount = mesh.triangleCount * 3;
+            }
 
-        // face
-        for (let i = 0; i < drawCount; i += 3) {
-            if (i % 3000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount * 2 + i });
-            let color: Color;
-            switch (colorType) {
-                case 'uniform':
-                    color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
-                    break;
-                case 'instance':
-                    color = Color.fromArray(tColor, instanceIndex * 3);
-                    break;
-                case 'group': {
-                    const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
-                    color = Color.fromArray(tColor, group * 3);
-                    break;
+            Mat4.fromArray(t, aTransform, instanceIndex * 16);
+            mat3directionTransform(n, t);
+
+            // position
+            for (let i = 0; i < vertexCount; ++i) {
+                v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
+                StringBuilder.writeSafe(obj, 'v ');
+                StringBuilder.writeFloat(obj, tmpV[0], 1000);
+                StringBuilder.whitespace1(obj);
+                StringBuilder.writeFloat(obj, tmpV[1], 1000);
+                StringBuilder.whitespace1(obj);
+                StringBuilder.writeFloat(obj, tmpV[2], 1000);
+                StringBuilder.newline(obj);
+            }
+
+            // normal
+            for (let i = 0; i < vertexCount; ++i) {
+                v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
+                StringBuilder.writeSafe(obj, 'vn ');
+                StringBuilder.writeFloat(obj, tmpV[0], 100);
+                StringBuilder.whitespace1(obj);
+                StringBuilder.writeFloat(obj, tmpV[1], 100);
+                StringBuilder.whitespace1(obj);
+                StringBuilder.writeFloat(obj, tmpV[2], 100);
+                StringBuilder.newline(obj);
+            }
+
+            // face
+            for (let i = 0; i < drawCount; i += 3) {
+                let color: Color;
+                switch (colorType) {
+                    case 'uniform':
+                        color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
+                        break;
+                    case 'instance':
+                        color = Color.fromArray(tColor, instanceIndex * 3);
+                        break;
+                    case 'group': {
+                        const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
+                        color = Color.fromArray(tColor, group * 3);
+                        break;
+                    }
+                    case 'groupInstance': {
+                        const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
+                        color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
+                        break;
+                    }
+                    case 'vertex':
+                        color = Color.fromArray(tColor, indices![i] * 3);
+                        break;
+                    case 'vertexInstance':
+                        color = Color.fromArray(tColor, (instanceIndex * drawCount + indices![i]) * 3);
+                        break;
+                    case 'volume':
+                        color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3);
+                        break;
+                    case 'volumeInstance':
+                        color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3);
+                        break;
+                    default: throw new Error('Unsupported color type.');
                 }
-                case 'groupInstance': {
+
+                let alpha = uAlpha;
+                if (dTransparency) {
                     const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
-                    color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
-                    break;
+                    const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
+                    alpha *= 1 - transparency;
                 }
-                case 'vertex':
-                    color = Color.fromArray(tColor, indices![i] * 3);
-                    break;
-                case 'vertexInstance':
-                    color = Color.fromArray(tColor, (instanceIndex * drawCount + indices![i]) * 3);
-                    break;
-                default: throw new Error('Unsupported color type.');
-            }
 
-            let alpha = uAlpha;
-            if (dTransparency) {
-                const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
-                const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
-                alpha *= 1 - transparency;
+                this.updateMaterial(color, alpha);
+
+                const v1 = this.vertexOffset + (isGeoTexture ? i : indices![i]) + 1;
+                const v2 = this.vertexOffset + (isGeoTexture ? i + 1 : indices![i + 1]) + 1;
+                const v3 = this.vertexOffset + (isGeoTexture ? i + 2 : indices![i + 2]) + 1;
+                StringBuilder.writeSafe(obj, 'f ');
+                StringBuilder.writeInteger(obj, v1);
+                StringBuilder.writeSafe(obj, '//');
+                StringBuilder.writeIntegerAndSpace(obj, v1);
+                StringBuilder.writeInteger(obj, v2);
+                StringBuilder.writeSafe(obj, '//');
+                StringBuilder.writeIntegerAndSpace(obj, v2);
+                StringBuilder.writeInteger(obj, v3);
+                StringBuilder.writeSafe(obj, '//');
+                StringBuilder.writeInteger(obj, v3);
+                StringBuilder.newline(obj);
             }
 
-            this.updateMaterial(color, alpha);
-
-            const v1 = this.vertexOffset + (isGeoTexture ? i : indices![i]) + 1;
-            const v2 = this.vertexOffset + (isGeoTexture ? i + 1 : indices![i + 1]) + 1;
-            const v3 = this.vertexOffset + (isGeoTexture ? i + 2 : indices![i + 2]) + 1;
-            StringBuilder.writeSafe(obj, 'f ');
-            StringBuilder.writeInteger(obj, v1);
-            StringBuilder.writeSafe(obj, '//');
-            StringBuilder.writeIntegerAndSpace(obj, v1);
-            StringBuilder.writeInteger(obj, v2);
-            StringBuilder.writeSafe(obj, '//');
-            StringBuilder.writeIntegerAndSpace(obj, v2);
-            StringBuilder.writeInteger(obj, v3);
-            StringBuilder.writeSafe(obj, '//');
-            StringBuilder.writeInteger(obj, v3);
-            StringBuilder.newline(obj);
+            this.vertexOffset += vertexCount;
         }
-
-        this.vertexOffset += vertexCount;
     }
 
     getData() {

+ 65 - 47
src/extensions/geo-export/stl-exporter.ts

@@ -4,12 +4,11 @@
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  */
 
-import { BaseValues } from '../../mol-gl/renderable/schema';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
 import { PLUGIN_VERSION } from '../../mol-plugin/version';
 import { RuntimeContext } from '../../mol-task';
-import { MeshExporter } from './mesh-exporter';
+import { MeshExporter, AddMeshInput } from './mesh-exporter';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3fromArray = Vec3.fromArray;
@@ -28,7 +27,9 @@ export class StlExporter extends MeshExporter<StlData> {
     private triangleBuffers: ArrayBuffer[] = [];
     private triangleCount = 0;
 
-    protected async addMeshWithColors(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, groups: Float32Array | Uint8Array, vertexCount: number, drawCount: number, values: BaseValues, instanceIndex: number, isGeoTexture: boolean, ctx: RuntimeContext) {
+    protected async addMeshWithColors(input: AddMeshInput) {
+        const { mesh, meshes, values, isGeoTexture, ctx } = input;
+
         const t = Mat4();
         const tmpV = Vec3();
         const v1 = Vec3();
@@ -36,51 +37,68 @@ export class StlExporter extends MeshExporter<StlData> {
         const v3 = Vec3();
         const stride = isGeoTexture ? 4 : 3;
 
-        const aTransform = values.aTransform.ref.value;
-        Mat4.fromArray(t, aTransform, instanceIndex * 16);
-
-        const currentProgress = (vertexCount + drawCount) * instanceIndex;
-        await ctx.update({ isIndeterminate: false, current: currentProgress, max: (vertexCount + drawCount) * values.uInstanceCount.ref.value });
-
-        // position
-        const vertexArray = new Float32Array(vertexCount * 3);
-        for (let i = 0; i < vertexCount; ++i) {
-            if (i % 1000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + i });
-            v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
-            v3toArray(tmpV, vertexArray, i * 3);
+        const instanceCount = values.uInstanceCount.ref.value;
+
+        for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
+            if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
+
+            let vertices: Float32Array;
+            let indices: Uint32Array | undefined;
+            let vertexCount: number;
+            let drawCount: number;
+            if (mesh !== undefined) {
+                vertices = mesh.vertices;
+                indices = mesh.indices;
+                vertexCount = mesh.vertexCount;
+                drawCount = mesh.drawCount;
+            } else {
+                const mesh = meshes![instanceIndex];
+                vertices = mesh.vertexBuffer.ref.value;
+                indices = mesh.indexBuffer.ref.value;
+                vertexCount = mesh.vertexCount;
+                drawCount = mesh.triangleCount * 3;
+            }
+
+            const aTransform = values.aTransform.ref.value;
+            Mat4.fromArray(t, aTransform, instanceIndex * 16);
+
+            // position
+            const vertexArray = new Float32Array(vertexCount * 3);
+            for (let i = 0; i < vertexCount; ++i) {
+                v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
+                v3toArray(tmpV, vertexArray, i * 3);
+            }
+
+            // face
+            const triangleBuffer = new ArrayBuffer(50 * drawCount);
+            const dataView = new DataView(triangleBuffer);
+            for (let i = 0; i < drawCount; i += 3) {
+                v3fromArray(v1, vertexArray, (isGeoTexture ? i : indices![i]) * 3);
+                v3fromArray(v2, vertexArray, (isGeoTexture ? i + 1 : indices![i + 1]) * 3);
+                v3fromArray(v3, vertexArray, (isGeoTexture ? i + 2 : indices![i + 2]) * 3);
+                v3triangleNormal(tmpV, v1, v2, v3);
+
+                const byteOffset = 50 * i;
+                dataView.setFloat32(byteOffset, tmpV[0], true);
+                dataView.setFloat32(byteOffset + 4, tmpV[1], true);
+                dataView.setFloat32(byteOffset + 8, tmpV[2], true);
+
+                dataView.setFloat32(byteOffset + 12, v1[0], true);
+                dataView.setFloat32(byteOffset + 16, v1[1], true);
+                dataView.setFloat32(byteOffset + 20, v1[2], true);
+
+                dataView.setFloat32(byteOffset + 24, v2[0], true);
+                dataView.setFloat32(byteOffset + 28, v2[1], true);
+                dataView.setFloat32(byteOffset + 32, v2[2], true);
+
+                dataView.setFloat32(byteOffset + 36, v3[0], true);
+                dataView.setFloat32(byteOffset + 40, v3[1], true);
+                dataView.setFloat32(byteOffset + 44, v3[2], true);
+            }
+
+            this.triangleBuffers.push(triangleBuffer);
+            this.triangleCount += drawCount;
         }
-
-        // face
-        const triangleBuffer = new ArrayBuffer(50 * drawCount);
-        const dataView = new DataView(triangleBuffer);
-        for (let i = 0; i < drawCount; i += 3) {
-            if (i % 3000 === 0 && ctx.shouldUpdate) await ctx.update({ current: currentProgress + vertexCount + i });
-
-            v3fromArray(v1, vertexArray, (isGeoTexture ? i : indices![i]) * 3);
-            v3fromArray(v2, vertexArray, (isGeoTexture ? i + 1 : indices![i + 1]) * 3);
-            v3fromArray(v3, vertexArray, (isGeoTexture ? i + 2 : indices![i + 2]) * 3);
-            v3triangleNormal(tmpV, v1, v2, v3);
-
-            const byteOffset = 50 * i;
-            dataView.setFloat32(byteOffset, tmpV[0], true);
-            dataView.setFloat32(byteOffset + 4, tmpV[1], true);
-            dataView.setFloat32(byteOffset + 8, tmpV[2], true);
-
-            dataView.setFloat32(byteOffset + 12, v1[0], true);
-            dataView.setFloat32(byteOffset + 16, v1[1], true);
-            dataView.setFloat32(byteOffset + 20, v1[2], true);
-
-            dataView.setFloat32(byteOffset + 24, v2[0], true);
-            dataView.setFloat32(byteOffset + 28, v2[1], true);
-            dataView.setFloat32(byteOffset + 32, v2[2], true);
-
-            dataView.setFloat32(byteOffset + 36, v3[0], true);
-            dataView.setFloat32(byteOffset + 40, v3[1], true);
-            dataView.setFloat32(byteOffset + 44, v3[2], true);
-        }
-
-        this.triangleBuffers.push(triangleBuffer);
-        this.triangleCount += drawCount;
     }
 
     getData() {

+ 1 - 0
src/extensions/pdbe/structure-quality-report/color.ts

@@ -79,6 +79,7 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
     return {
         factory: StructureQualityReportColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color: color,
         props: props,
         description: 'Assigns residue colors according to the number of quality issues or a specific quality issue. Data from wwPDB Validation Report, obtained via PDBe.',

+ 1 - 0
src/extensions/rcsb/validation-report/color/density-fit.ts

@@ -58,6 +58,7 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
     return {
         factory: DensityFitColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         contextHash,

+ 1 - 0
src/extensions/rcsb/validation-report/color/geometry-quality.ts

@@ -96,6 +96,7 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
     return {
         factory: GeometryQualityColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         contextHash,

+ 1 - 0
src/extensions/rcsb/validation-report/color/random-coil-index.ts

@@ -49,6 +49,7 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
     return {
         factory: RandomCoilIndexColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         contextHash,

+ 82 - 31
src/mol-geo/geometry/color-data.ts

@@ -1,25 +1,30 @@
 /**
- * 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>
+ * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import { ValueCell } from '../../mol-util';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 import { Color } from '../../mol-util/color';
-import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
 import { LocationIterator } from '../util/location-iterator';
 import { NullLocation } from '../../mol-model/location';
 import { LocationColor, ColorTheme } from '../../mol-theme/color';
 import { Geometry } from './geometry';
+import { createNullTexture, Texture } from '../../mol-gl/webgl/texture';
 
-export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 'vertex' | 'vertexInstance'
+export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 'vertex' | 'vertexInstance' | 'volume' | 'volumeInstance'
 
 export type ColorData = {
     uColor: ValueCell<Vec3>,
     tColor: ValueCell<TextureImage<Uint8Array>>,
+    tColorGrid: ValueCell<Texture>,
     tPalette: ValueCell<TextureImage<Uint8Array>>,
     uColorTexDim: ValueCell<Vec2>,
+    uColorGridDim: ValueCell<Vec3>,
+    uColorGridTransform: ValueCell<Vec4>,
     dColorType: ValueCell<string>,
     dUsePalette: ValueCell<boolean>,
 }
@@ -43,9 +48,44 @@ function _createColors(locationIt: LocationIterator, positionIt: LocationIterato
         case 'groupInstance': return createGroupInstanceColor(locationIt, colorTheme.color, colorData);
         case 'vertex': return createVertexColor(positionIt, colorTheme.color, colorData);
         case 'vertexInstance': return createVertexInstanceColor(positionIt, colorTheme.color, colorData);
+        case 'volume': return createGridColor((colorTheme as any).grid, 'volume', colorData);
+        case 'volumeInstance': return createGridColor((colorTheme as any).grid, 'volumeInstance', colorData);
     }
 }
 
+function updatePaletteTexture(palette: ColorTheme.Palette, cell: ValueCell<TextureImage<Uint8Array>>) {
+    let isSynced = true;
+    const texture = cell.ref.value;
+    if (palette.colors.length !== texture.width || texture.filter !== palette.filter) {
+        isSynced = false;
+    } else {
+        const data = texture.array;
+        let o = 0;
+        for (const c of palette.colors) {
+            const [r, g, b] = Color.toRgb(c);
+            if (data[o++] !== r || data[o++] !== g || data[o++] !== b) {
+                isSynced = false;
+                break;
+            }
+        }
+    }
+
+    if (isSynced) return;
+
+    const array = new Uint8Array(palette.colors.length * 3);
+    let o = 0;
+    for (const c of palette.colors) {
+        const [r, g, b] = Color.toRgb(c);
+        array[o++] = r;
+        array[o++] = g;
+        array[o++] = b;
+    }
+
+    ValueCell.update(cell, { array, height: 1, width: palette.colors.length, filter: palette.filter });
+}
+
+//
+
 export function createValueColor(value: Color, colorData?: ColorData): ColorData {
     if (colorData) {
         ValueCell.update(colorData.uColor, Color.toVec3Normalized(colorData.uColor.ref.value, value));
@@ -55,8 +95,11 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
         return {
             uColor: ValueCell.create(Color.toVec3Normalized(Vec3(), value)),
             tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
+            tColorGrid: ValueCell.create(createNullTexture()),
             tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
             uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
+            uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
+            uColorGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
             dColorType: ValueCell.create('uniform'),
             dUsePalette: ValueCell.create(false),
         };
@@ -68,7 +111,9 @@ function createUniformColor(locationIt: LocationIterator, color: LocationColor,
     return createValueColor(color(NullLocation, false), colorData);
 }
 
-function createTextureColor(colors: TextureImage<Uint8Array>, type: ColorType, colorData?: ColorData): ColorData {
+//
+
+export function createTextureColor(colors: TextureImage<Uint8Array>, type: ColorType, colorData?: ColorData): ColorData {
     if (colorData) {
         ValueCell.update(colorData.tColor, colors);
         ValueCell.update(colorData.uColorTexDim, Vec2.create(colors.width, colors.height));
@@ -78,8 +123,11 @@ function createTextureColor(colors: TextureImage<Uint8Array>, type: ColorType, c
         return {
             uColor: ValueCell.create(Vec3()),
             tColor: ValueCell.create(colors),
+            tColorGrid: ValueCell.create(createNullTexture()),
             tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
             uColorTexDim: ValueCell.create(Vec2.create(colors.width, colors.height)),
+            uColorGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
+            uColorGridTransform: ValueCell.create(Vec4.create(0, 0, 0, 1)),
             dColorType: ValueCell.create(type),
             dUsePalette: ValueCell.create(false),
         };
@@ -156,33 +204,36 @@ function createVertexInstanceColor(locationIt: LocationIterator, color: Location
     return createTextureColor(colors, 'vertexInstance', colorData);
 }
 
-function updatePaletteTexture(palette: ColorTheme.Palette, cell: ValueCell<TextureImage<Uint8Array>>) {
-    let isSynced = true;
-    const texture = cell.ref.value;
-    if (palette.colors.length !== texture.width || texture.filter !== palette.filter) {
-        isSynced = false;
-    } else {
-        const data = texture.array;
-        let o = 0;
-        for (const c of palette.colors) {
-            const [r, g, b] = Color.toRgb(c);
-            if (data[o++] !== r || data[o++] !== g || data[o++] !== b) {
-                isSynced = false;
-                break;
-            }
-        }
-    }
+//
 
-    if (isSynced) return;
+interface ColorVolume {
+    colors: Texture
+    dimension: Vec3
+    transform: Vec4
+}
 
-    const array = new Uint8Array(palette.colors.length * 3);
-    let o = 0;
-    for (const c of palette.colors) {
-        const [r, g, b] = Color.toRgb(c);
-        array[o++] = r;
-        array[o++] = g;
-        array[o++] = b;
+export function createGridColor(grid: ColorVolume, type: ColorType, colorData?: ColorData): ColorData {
+    const { colors, dimension, transform } = grid;
+    const width = colors.getWidth();
+    const height = colors.getHeight();
+    if (colorData) {
+        ValueCell.update(colorData.tColorGrid, colors);
+        ValueCell.update(colorData.uColorTexDim, Vec2.create(width, height));
+        ValueCell.update(colorData.uColorGridDim, Vec3.clone(dimension));
+        ValueCell.update(colorData.uColorGridTransform, Vec4.clone(transform));
+        ValueCell.updateIfChanged(colorData.dColorType, type);
+        return colorData;
+    } else {
+        return {
+            uColor: ValueCell.create(Vec3()),
+            tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
+            tColorGrid: ValueCell.create(colors),
+            tPalette: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
+            uColorTexDim: ValueCell.create(Vec2.create(width, height)),
+            uColorGridDim: ValueCell.create(Vec3.clone(dimension)),
+            uColorGridTransform: ValueCell.create(Vec4.clone(transform)),
+            dColorType: ValueCell.create(type),
+            dUsePalette: ValueCell.create(false),
+        };
     }
-
-    ValueCell.update(cell, { array, height: 1, width: palette.colors.length, filter: palette.filter });
-}
+}

+ 229 - 0
src/mol-geo/geometry/mesh/color-smoothing.ts

@@ -0,0 +1,229 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createTextureImage, TextureImage } from '../../../mol-gl/renderable/util';
+import { WebGLContext } from '../../../mol-gl/webgl/context';
+import { Texture } from '../../../mol-gl/webgl/texture';
+import { Box3D, Sphere3D } from '../../../mol-math/geometry';
+import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
+import { getVolumeTexture2dLayout } from '../../../mol-repr/volume/util';
+import { Color } from '../../../mol-util/color';
+
+interface ColorSmoothingInput {
+    vertexCount: number
+    instanceCount: number
+    groupCount: number
+    transformBuffer: Float32Array
+    instanceBuffer: Float32Array
+    positionBuffer: Float32Array
+    groupBuffer: Float32Array
+    colorData: TextureImage<Uint8Array>
+    colorType: 'group' | 'groupInstance'
+    boundingSphere: Sphere3D
+    invariantBoundingSphere: Sphere3D
+}
+
+export function calcMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl?: WebGLContext, texture?: Texture) {
+    const { colorType, vertexCount, groupCount, positionBuffer, transformBuffer, groupBuffer } = input;
+
+    const isInstanceType = colorType.endsWith('Instance');
+    const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
+
+    const scaleFactor = 1 / resolution;
+    const scaledBox = Box3D.scale(Box3D(), box, scaleFactor);
+    const gridDim = Box3D.size(Vec3(), scaledBox);
+    Vec3.ceil(gridDim, gridDim);
+    Vec3.add(gridDim, gridDim, Vec3.create(2, 2, 2));
+    const { min } = box;
+
+    const [ xn, yn ] = gridDim;
+    const { width, height } = getVolumeTexture2dLayout(gridDim);
+    // console.log({ width, height, dim });
+
+    const itemSize = 3;
+    const data = new Float32Array(width * height * itemSize);
+    const count = new Float32Array(width * height);
+
+    const grid = new Uint8Array(width * height * itemSize);
+    const textureImage: TextureImage<Uint8Array> = { array: grid, width, height, filter: 'linear' };
+
+    const instanceCount = isInstanceType ? input.instanceCount : 1;
+    const colors = input.colorData.array;
+
+    function getIndex(x: number, y: number, z: number) {
+        const column = Math.floor(((z * xn) % width) / xn);
+        const row = Math.floor((z * xn) / width);
+        const px = column * xn + x;
+        return itemSize * ((row * yn * width) + (y * width) + px);
+    }
+
+    const p = 2;
+    const [dimX, dimY, dimZ] = gridDim;
+    const v = Vec3();
+
+    for (let i = 0; i < instanceCount; ++i) {
+        for (let j = 0; j < vertexCount; j += stride) {
+            Vec3.fromArray(v, positionBuffer, j * 3);
+            if (isInstanceType) Vec3.transformMat4Offset(v, v, transformBuffer, 0, 0, i * 16);
+            Vec3.sub(v, v, min);
+            Vec3.scale(v, v, scaleFactor);
+            const [vx, vy, vz] = v;
+
+            // vertex mapped to grid
+            const x = Math.floor(vx);
+            const y = Math.floor(vy);
+            const z = Math.floor(vz);
+
+            // group colors
+            const ci = i * groupCount + groupBuffer[j];
+            const r = colors[ci * 3];
+            const g = colors[ci * 3 + 1];
+            const b = colors[ci * 3 + 2];
+
+            // Extents of grid to consider for this atom
+            const begX = Math.max(0, x - p);
+            const begY = Math.max(0, y - p);
+            const begZ = Math.max(0, z - p);
+
+            // Add two to these points:
+            // - x, y, z are floor'd values so this ensures coverage
+            // - these are loop limits (exclusive)
+            const endX = Math.min(dimX, x + p + 2);
+            const endY = Math.min(dimY, y + p + 2);
+            const endZ = Math.min(dimZ, z + p + 2);
+
+            for (let xi = begX; xi < endX; ++xi) {
+                const dx = xi - vx;
+                for (let yi = begY; yi < endY; ++yi) {
+                    const dy = yi - vy;
+                    for (let zi = begZ; zi < endZ; ++zi) {
+                        const dz = zi - vz;
+                        const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
+                        if (d > p) continue;
+
+                        let s = p - d;
+                        const index = getIndex(xi, yi, zi);
+                        data[index] += r * s;
+                        data[index + 1] += g * s;
+                        data[index + 2] += b * s;
+                        count[index / 3] += s;
+                    }
+                }
+            }
+        }
+    }
+
+    for (let i = 0, il = count.length; i < il; ++i) {
+        const i3 = i * 3;
+        const c = count[i];
+        grid[i3] = Math.round(data[i3] / c);
+        grid[i3 + 1] = Math.round(data[i3 + 1] / c);
+        grid[i3 + 2] = Math.round(data[i3 + 2] / c);
+    }
+
+    const gridTexDim = Vec2.create(width, height);
+    const gridTransform = Vec4.create(min[0], min[1], min[2], scaleFactor);
+    const type = isInstanceType ? 'volumeInstance' as const : 'volume' as const;
+
+    if (webgl) {
+        if (!texture) texture = webgl.resources.texture('image-uint8', 'rgb', 'ubyte', 'linear');
+        texture.load(textureImage);
+
+        return { kind: 'volume' as const, texture, gridTexDim, gridDim, gridTransform, type };
+    } else {
+        const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer, positionBuffer, colorType: type, grid, gridDim, gridTexDim, gridTransform, vertexStride: 3, colorStride: 3 });
+
+        return {
+            kind: 'vertex' as const,
+            texture: interpolated,
+            texDim: Vec2.create(interpolated.width, interpolated.height),
+            type: isInstanceType ? 'vertexInstance' : 'vertex'
+        };
+    }
+}
+
+//
+
+interface ColorInterpolationInput {
+    vertexCount: number
+    instanceCount: number
+    transformBuffer: Float32Array
+    positionBuffer: Float32Array
+    colorType: 'volumeInstance' | 'volume'
+    grid: Uint8Array // 2d layout
+    gridTexDim: Vec2
+    gridDim: Vec3
+    gridTransform: Vec4
+    vertexStride: number
+    colorStride: number
+}
+
+export function getTrilinearlyInterpolated(input: ColorInterpolationInput): TextureImage<Uint8Array> {
+    const { vertexCount, positionBuffer, transformBuffer, grid, gridDim, gridTexDim, gridTransform, vertexStride, colorStride } = input;
+
+    const isInstanceType = input.colorType.endsWith('Instance');
+    const instanceCount = isInstanceType ? input.instanceCount : 1;
+    const image = createTextureImage(Math.max(1, instanceCount * vertexCount), 3, Uint8Array);
+    const { array } = image;
+
+    const [xn, yn] = gridDim;
+    const width = gridTexDim[0];
+    const min = Vec3.fromArray(Vec3(), gridTransform, 0);
+    const scaleFactor = gridTransform[3];
+
+    function getIndex(x: number, y: number, z: number) {
+        const column = Math.floor(((z * xn) % width) / xn);
+        const row = Math.floor((z * xn) / width);
+        const px = column * xn + x;
+        return colorStride * ((row * yn * width) + (y * width) + px);
+    }
+
+    const v = Vec3();
+    const v0 = Vec3();
+    const v1 = Vec3();
+    const vd = Vec3();
+
+    for (let i = 0; i < instanceCount; ++i) {
+        for (let j = 0; j < vertexCount; ++j) {
+            Vec3.fromArray(v, positionBuffer, j * vertexStride);
+            if (isInstanceType) Vec3.transformMat4Offset(v, v, transformBuffer, 0, 0, i * 16);
+            Vec3.sub(v, v, min);
+            Vec3.scale(v, v, scaleFactor);
+
+            Vec3.floor(v0, v);
+            Vec3.ceil(v1, v);
+
+            Vec3.sub(vd, v, v0);
+            Vec3.sub(v, v1, v0);
+            Vec3.div(vd, vd, v);
+
+            const [x0, y0, z0] = v0;
+            const [x1, y1, z1] = v1;
+            const [xd, yd, zd] = vd;
+
+            const s000 = Color.fromArray(grid, getIndex(x0, y0, z0));
+            const s100 = Color.fromArray(grid, getIndex(x1, y0, z0));
+            const s001 = Color.fromArray(grid, getIndex(x0, y0, z1));
+            const s101 = Color.fromArray(grid, getIndex(x1, y0, z1));
+            const s010 = Color.fromArray(grid, getIndex(x0, y1, z0));
+            const s110 = Color.fromArray(grid, getIndex(x1, y1, z0));
+            const s011 = Color.fromArray(grid, getIndex(x0, y1, z1));
+            const s111 = Color.fromArray(grid, getIndex(x1, y1, z1));
+
+            const s00 = Color.interpolate(s000, s100, xd);
+            const s01 = Color.interpolate(s001, s101, xd);
+            const s10 = Color.interpolate(s010, s110, xd);
+            const s11 = Color.interpolate(s011, s111, xd);
+
+            const s0 = Color.interpolate(s00, s10, yd);
+            const s1 = Color.interpolate(s01, s11, yd);
+
+            Color.toArray(Color.interpolate(s0, s1, zd), array, (i * vertexCount + j) * 3);
+        }
+    }
+
+    return image;
+}

+ 3 - 1
src/mol-geo/geometry/mesh/mesh.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>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -49,6 +49,8 @@ export interface Mesh {
     readonly groupMapping: GroupMapping
 
     setBoundingSphere(boundingSphere: Sphere3D): void
+
+    meta?: unknown
 }
 
 export namespace Mesh {

+ 344 - 0
src/mol-geo/geometry/texture-mesh/color-smoothing.ts

@@ -0,0 +1,344 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from '../../../mol-util';
+import { createComputeRenderable, ComputeRenderable } from '../../../mol-gl/renderable';
+import { WebGLContext } from '../../../mol-gl/webgl/context';
+import { Texture } from '../../../mol-gl/webgl/texture';
+import { ShaderCode } from '../../../mol-gl/shader-code';
+import { createComputeRenderItem } from '../../../mol-gl/webgl/render-item';
+import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, Values, DefineSpec } from '../../../mol-gl/renderable/schema';
+import { quad_vert } from '../../../mol-gl/shader/quad.vert';
+import { normalize_frag } from '../../../mol-gl/shader/compute/color-smoothing/normalize.frag';
+import { QuadSchema, QuadValues } from '../../../mol-gl/compute/util';
+import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
+import { Box3D, Sphere3D } from '../../../mol-math/geometry';
+import { accumulate_frag } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.frag';
+import { accumulate_vert } from '../../../mol-gl/shader/compute/color-smoothing/accumulate.vert';
+import { TextureImage } from '../../../mol-gl/renderable/util';
+
+export const ColorAccumulateSchema = {
+    drawCount: ValueSpec('number'),
+    instanceCount: ValueSpec('number'),
+    stride: ValueSpec('number'),
+
+    uTotalCount: UniformSpec('i'),
+    uInstanceCount: UniformSpec('i'),
+    uGroupCount: UniformSpec('i'),
+
+    aTransform: AttributeSpec('float32', 16, 1),
+    aInstance: AttributeSpec('float32', 1, 1),
+    aSample: AttributeSpec('float32', 1, 0),
+
+    uGeoTexDim: UniformSpec('v2', 'buffered'),
+    tPosition: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tGroup: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+
+    uColorTexDim: UniformSpec('v2'),
+    tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
+    dColorType: DefineSpec('string', ['group', 'groupInstance', 'vertex', 'vertexInstance']),
+
+    uCurrentSlice: UniformSpec('f'),
+    uCurrentX: UniformSpec('f'),
+    uCurrentY: UniformSpec('f'),
+    uBboxMin: UniformSpec('v3', 'material'),
+    uBboxSize: UniformSpec('v3', 'material'),
+    uResolution: UniformSpec('f', 'material'),
+};
+type ColorAccumulateValues = Values<typeof ColorAccumulateSchema>
+const ColorAccumulateName = 'color-accumulate';
+
+interface AccumulateInput {
+    vertexCount: number
+    instanceCount: number
+    groupCount: number
+    transformBuffer: Float32Array
+    instanceBuffer: Float32Array
+    positionTexture: Texture
+    groupTexture: Texture
+    colorData: TextureImage<Uint8Array>
+    colorType: 'group' | 'groupInstance'
+}
+
+function getSampleBuffer(sampleCount: number, stride: number) {
+    const sampleBuffer = new Float32Array(sampleCount);
+    for (let i = 0; i < sampleCount; ++i) {
+        sampleBuffer[i] = i * stride;
+    }
+    return sampleBuffer;
+}
+
+function getAccumulateRenderable(ctx: WebGLContext, input: AccumulateInput, box: Box3D, resolution: number, stride: number): ComputeRenderable<ColorAccumulateValues> {
+    if (ctx.namedComputeRenderables[ColorAccumulateName]) {
+        const extent = Vec3.sub(Vec3(), box.max, box.min);
+        const v = ctx.namedComputeRenderables[ColorAccumulateName].values as ColorAccumulateValues;
+
+        const sampleCount = Math.round(input.vertexCount / stride);
+        if (sampleCount > v.drawCount.ref.value || stride !== v.stride.ref.value) {
+            ValueCell.update(v.aSample, getSampleBuffer(sampleCount, stride));
+        }
+
+        ValueCell.updateIfChanged(v.drawCount, sampleCount);
+        ValueCell.updateIfChanged(v.instanceCount, input.instanceCount);
+        ValueCell.updateIfChanged(v.stride, stride);
+
+        ValueCell.updateIfChanged(v.uTotalCount, input.vertexCount);
+        ValueCell.updateIfChanged(v.uInstanceCount, input.instanceCount);
+        ValueCell.updateIfChanged(v.uGroupCount, input.groupCount);
+
+        ValueCell.update(v.aTransform, input.transformBuffer);
+        ValueCell.update(v.aInstance, input.instanceBuffer);
+
+        ValueCell.update(v.uGeoTexDim, Vec2.set(v.uGeoTexDim.ref.value, input.positionTexture.getWidth(), input.positionTexture.getHeight()));
+        ValueCell.update(v.tPosition, input.positionTexture);
+        ValueCell.update(v.tGroup, input.groupTexture);
+
+        ValueCell.update(v.uColorTexDim, Vec2.set(v.uColorTexDim.ref.value, input.colorData.width, input.colorData.height));
+        ValueCell.update(v.tColor, input.colorData);
+        ValueCell.updateIfChanged(v.dColorType, input.colorType);
+
+        ValueCell.updateIfChanged(v.uCurrentSlice, 0);
+        ValueCell.updateIfChanged(v.uCurrentX, 0);
+        ValueCell.updateIfChanged(v.uCurrentY, 0);
+        ValueCell.update(v.uBboxMin, box.min);
+        ValueCell.update(v.uBboxSize, extent);
+        ValueCell.updateIfChanged(v.uResolution, resolution);
+
+        ctx.namedComputeRenderables[ColorAccumulateName].update();
+    } else {
+        ctx.namedComputeRenderables[ColorAccumulateName] = createAccumulateRenderable(ctx, input, box, resolution, stride);
+    }
+    return ctx.namedComputeRenderables[ColorAccumulateName];
+}
+
+function createAccumulateRenderable(ctx: WebGLContext, input: AccumulateInput, box: Box3D, resolution: number, stride: number) {
+    const extent = Vec3.sub(Vec3(), box.max, box.min);
+    const sampleCount = Math.round(input.vertexCount / stride);
+
+    const values: ColorAccumulateValues = {
+        drawCount: ValueCell.create(sampleCount),
+        instanceCount: ValueCell.create(input.instanceCount),
+        stride: ValueCell.create(stride),
+
+        uTotalCount: ValueCell.create(input.vertexCount),
+        uInstanceCount: ValueCell.create(input.instanceCount),
+        uGroupCount: ValueCell.create(input.groupCount),
+
+        aTransform: ValueCell.create(input.transformBuffer),
+        aInstance: ValueCell.create(input.instanceBuffer),
+        aSample: ValueCell.create(getSampleBuffer(sampleCount, stride)),
+
+        uGeoTexDim: ValueCell.create(Vec2.create(input.positionTexture.getWidth(), input.positionTexture.getHeight())),
+        tPosition: ValueCell.create(input.positionTexture),
+        tGroup: ValueCell.create(input.groupTexture),
+
+        uColorTexDim: ValueCell.create(Vec2.create(input.colorData.width, input.colorData.height)),
+        tColor: ValueCell.create(input.colorData),
+        dColorType: ValueCell.create(input.colorType),
+
+        uCurrentSlice: ValueCell.create(0),
+        uCurrentX: ValueCell.create(0),
+        uCurrentY: ValueCell.create(0),
+        uBboxMin: ValueCell.create(box.min),
+        uBboxSize: ValueCell.create(extent),
+        uResolution: ValueCell.create(resolution),
+    };
+
+    const schema = { ...ColorAccumulateSchema };
+    const shaderCode = ShaderCode('accumulate', accumulate_vert, accumulate_frag);
+    const renderItem = createComputeRenderItem(ctx, 'points', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+function setAccumulateDefaults(ctx: WebGLContext) {
+    const { gl, state } = ctx;
+    state.disable(gl.CULL_FACE);
+    state.enable(gl.BLEND);
+    state.disable(gl.DEPTH_TEST);
+    state.enable(gl.SCISSOR_TEST);
+    state.depthMask(false);
+    state.clearColor(0, 0, 0, 0);
+    state.blendFunc(gl.ONE, gl.ONE);
+    state.blendEquation(gl.FUNC_ADD);
+}
+
+//
+
+export const ColorNormalizeSchema = {
+    ...QuadSchema,
+
+    tColor: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+
+};
+type ColorNormalizeValues = Values<typeof ColorNormalizeSchema>
+const ColorNormalizeName = 'color-normalize';
+
+function getNormalizeRenderable(ctx: WebGLContext, color: Texture): ComputeRenderable<ColorNormalizeValues> {
+    if (ctx.namedComputeRenderables[ColorNormalizeName]) {
+        const v = ctx.namedComputeRenderables[ColorNormalizeName].values as ColorNormalizeValues;
+
+        ValueCell.update(v.tColor, color);
+        ValueCell.update(v.uTexSize, Vec2.set(v.uTexSize.ref.value, color.getWidth(), color.getHeight()));
+
+        ctx.namedComputeRenderables[ColorNormalizeName].update();
+    } else {
+        ctx.namedComputeRenderables[ColorNormalizeName] = createColorNormalizeRenderable(ctx, color);
+    }
+    return ctx.namedComputeRenderables[ColorNormalizeName];
+}
+
+function createColorNormalizeRenderable(ctx: WebGLContext, color: Texture) {
+    const values: ColorNormalizeValues = {
+        ...QuadValues,
+        tColor: ValueCell.create(color),
+        uTexSize: ValueCell.create(Vec2.create(color.getWidth(), color.getHeight())),
+    };
+
+    const schema = { ...ColorNormalizeSchema };
+    const shaderCode = ShaderCode('normalize', quad_vert, normalize_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+function setNormalizeDefaults(ctx: WebGLContext) {
+    const { gl, state } = ctx;
+    state.disable(gl.CULL_FACE);
+    state.enable(gl.BLEND);
+    state.disable(gl.DEPTH_TEST);
+    state.enable(gl.SCISSOR_TEST);
+    state.depthMask(false);
+    state.clearColor(0, 0, 0, 0);
+    state.blendFunc(gl.ONE, gl.ONE);
+    state.blendEquation(gl.FUNC_ADD);
+}
+
+//
+
+function getTexture2dSize(gridDim: Vec3) {
+    const area = gridDim[0] * gridDim[1] * gridDim[2];
+    const squareDim = Math.sqrt(area);
+    const powerOfTwoSize = Math.pow(2, Math.ceil(Math.log(squareDim) / Math.log(2)));
+
+    let texDimX = 0;
+    let texDimY = gridDim[1];
+    let texRows = 1;
+    let texCols = gridDim[2];
+    if (powerOfTwoSize < gridDim[0] * gridDim[2]) {
+        texCols = Math.floor(powerOfTwoSize / gridDim[0]);
+        texRows = Math.ceil(gridDim[2] / texCols);
+        texDimX = texCols * gridDim[0];
+        texDimY *= texRows;
+    } else {
+        texDimX = gridDim[0] * gridDim[2];
+    }
+    // console.log(texDimX, texDimY, texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2);
+    return { texDimX, texDimY, texRows, texCols, powerOfTwoSize: texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
+}
+
+interface ColorSmoothingInput extends AccumulateInput {
+    boundingSphere: Sphere3D
+    invariantBoundingSphere: Sphere3D
+}
+
+export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolution: number, stride: number, webgl: WebGLContext, texture?: Texture) {
+    const { gl, resources, state, extensions: { colorBufferHalfFloat, textureHalfFloat } } = webgl;
+
+    const isInstanceType = input.colorType.endsWith('Instance');
+    const box = Box3D.fromSphere3D(Box3D(), isInstanceType ? input.boundingSphere : input.invariantBoundingSphere);
+
+    const scaleFactor = 1 / resolution;
+    const scaledBox = Box3D.scale(Box3D(), box, scaleFactor);
+    const gridDim = Box3D.size(Vec3(), scaledBox);
+    Vec3.ceil(gridDim, gridDim);
+    Vec3.add(gridDim, gridDim, Vec3.create(2, 2, 2));
+    const { min } = box;
+
+    const [ dx, dy, dz ] = gridDim;
+    const { texDimX: width, texDimY: height, texCols } = getTexture2dSize(gridDim);
+    // console.log({ width, height, texCols, dim, resolution });
+
+    if (!webgl.namedTextures[ColorAccumulateName]) {
+        webgl.namedTextures[ColorAccumulateName] = colorBufferHalfFloat && textureHalfFloat
+            ? resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
+            : resources.texture('image-float32', 'rgba', 'float', 'nearest');
+    }
+    const accumulateTexture = webgl.namedTextures[ColorAccumulateName];
+    accumulateTexture.define(width, height);
+
+    const accumulateRenderable = getAccumulateRenderable(webgl, input, box, resolution, stride);
+
+    //
+
+    const { uCurrentSlice, uCurrentX, uCurrentY } = accumulateRenderable.values;
+
+    if (!webgl.namedFramebuffers[ColorAccumulateName]) {
+        webgl.namedFramebuffers[ColorAccumulateName] = webgl.resources.framebuffer();
+    }
+    const framebuffer = webgl.namedFramebuffers[ColorAccumulateName];
+    framebuffer.bind();
+
+    setAccumulateDefaults(webgl);
+    state.currentRenderItemId = -1;
+    accumulateTexture.attachFramebuffer(framebuffer, 0);
+    gl.viewport(0, 0, width, height);
+    gl.scissor(0, 0, width, height);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    ValueCell.update(uCurrentY, 0);
+    let currCol = 0;
+    let currY = 0;
+    let currX = 0;
+    for (let i = 0; i < dz; ++i) {
+        if (currCol >= texCols) {
+            currCol -= texCols;
+            currY += dy;
+            currX = 0;
+            ValueCell.update(uCurrentY, currY);
+        }
+        // console.log({ i, currX, currY });
+        ValueCell.update(uCurrentX, currX);
+        ValueCell.update(uCurrentSlice, i);
+        gl.viewport(currX, currY, dx, dy);
+        gl.scissor(currX, currY, dx, dy);
+        accumulateRenderable.render();
+        ++currCol;
+        currX += dx;
+    }
+
+    // const accImage = new Float32Array(width * height * 4);
+    // accumulateTexture.attachFramebuffer(framebuffer, 0);
+    // webgl.readPixels(0, 0, width, height, accImage);
+    // console.log(accImage);
+    // printTextureImage({ array: accImage, width, height }, 1 / 4);
+
+    // normalize
+
+    if (!texture) texture = resources.texture('image-uint8', 'rgb', 'ubyte', 'linear');
+    texture.define(width, height);
+
+    const normalizeRenderable = getNormalizeRenderable(webgl, accumulateTexture);
+
+    setNormalizeDefaults(webgl);
+    state.currentRenderItemId = -1;
+    texture.attachFramebuffer(framebuffer, 0);
+    gl.viewport(0, 0, width, height);
+    gl.scissor(0, 0, width, height);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    normalizeRenderable.render();
+
+    // const normImage = new Uint8Array(width * height * 4);
+    // texture.attachFramebuffer(framebuffer, 0);
+    // webgl.readPixels(0, 0, width, height, normImage);
+    // console.log(normImage);
+    // printTextureImage({ array: normImage, width, height }, 1 / 4);
+
+    const gridTransform = Vec4.create(min[0], min[1], min[2], scaleFactor);
+    const type = isInstanceType ? 'volumeInstance' : 'volume';
+
+    return { texture, gridDim, gridTexDim: Vec2.create(width, height), gridTransform, type };
+}

+ 2 - 0
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -39,6 +39,8 @@ export interface TextureMesh {
     readonly doubleBuffer: TextureMesh.DoubleBuffer
 
     readonly boundingSphere: Sphere3D
+
+    meta?: unknown
 }
 
 export namespace TextureMesh {

+ 5 - 2
src/mol-gl/renderable/schema.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>
  */
@@ -184,9 +184,12 @@ export const ColorSchema = {
     // aColor: AttributeSpec('float32', 3, 0), // TODO
     uColor: UniformSpec('v3', 'material'),
     uColorTexDim: UniformSpec('v2'),
+    uColorGridDim: UniformSpec('v3'),
+    uColorGridTransform: UniformSpec('v4'),
     tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
     tPalette: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
-    dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'groupInstance', 'vertex', 'vertexInstance']),
+    tColorGrid: TextureSpec('texture', 'rgb', 'ubyte', 'linear'),
+    dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'groupInstance', 'vertex', 'vertexInstance', 'volume', 'volumeInstance']),
     dUsePalette: DefineSpec('boolean'),
 } as const;
 export type ColorSchema = typeof ColorSchema

+ 1 - 1
src/mol-gl/renderable/util.ts

@@ -74,7 +74,7 @@ export function printImageData(imageData: ImageData, scale = 1, pixelated = fals
             // not supported in Firefox and IE
             img.style.imageRendering = 'pixelated';
         }
-        img.style.position = 'absolute';
+        img.style.position = 'relative';
         img.style.top = '0px';
         img.style.left = '0px';
         img.style.border = 'solid grey';

+ 1 - 1
src/mol-gl/renderer.ts

@@ -190,7 +190,7 @@ namespace Renderer {
 
         let transparentBackground = false;
 
-        const nullDepthTexture = createNullTexture(gl, 'image-depth');
+        const nullDepthTexture = createNullTexture(gl);
         const sharedTexturesList: Textures = [
             ['tDepth', nullDepthTexture]
         ];

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

@@ -12,6 +12,12 @@ export const assign_color_varying = `
         vColor.rgb = readFromTexture(tColor, VertexID, uColorTexDim).rgb;
     #elif defined(dColorType_vertexInstance)
         vColor.rgb = readFromTexture(tColor, int(aInstance) * uVertexCount + VertexID, uColorTexDim).rgb;
+    #elif defined(dColorType_volume)
+        vec3 gridPos = (uColorGridTransform.w * (position - uColorGridTransform.xyz)) / uColorGridDim;
+        vColor.rgb = texture3dFrom2dLinear(tColorGrid, gridPos, uColorGridDim, uColorTexDim).rgb;
+    #elif defined(dColorType_volumeInstance)
+        vec3 gridPos = (uColorGridTransform.w * (vModelPosition - uColorGridTransform.xyz)) / uColorGridDim;
+        vColor.rgb = texture3dFrom2dLinear(tColorGrid, gridPos, uColorGridDim, uColorTexDim).rgb;
     #endif
 
     #ifdef dUsePalette

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

@@ -7,7 +7,8 @@ mat4 modelView = uView * model;
     vec3 position = aPosition;
 #endif
 vec4 position4 = vec4(position, 1.0);
-vModelPosition = (model * position4).xyz; // for clipping in frag shader
+// for accessing tColorGrid in vert shader and for clipping in frag shader
+vModelPosition = (model * position4).xyz;
 vec4 mvPosition = modelView * position4;
 vViewPosition = mvPosition.xyz;
 gl_Position = uProjection * mvPosition;

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

@@ -9,6 +9,12 @@ export const color_vert_params = `
         varying vec4 vColor;
         uniform vec2 uColorTexDim;
         uniform sampler2D tColor;
+    #elif defined(dColorType_grid)
+        varying vec4 vColor;
+        uniform vec2 uColorTexDim;
+        uniform vec3 uColorGridDim;
+        uniform vec4 uColorGridTransform;
+        uniform sampler2D tColorGrid;
     #endif
 
     #ifdef dOverpaint

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

@@ -13,7 +13,11 @@ export const common = `
     #define dColorType_texture
 #endif
 
-#if defined(dColorType_attribute) || defined(dColorType_texture)
+#if defined(dColorType_volume) || defined(dColorType_volumeInstance)
+    #define dColorType_grid
+#endif
+
+#if defined(dColorType_attribute) || defined(dColorType_texture) || defined(dColorType_grid)
     #define dColorType_varying
 #endif
 

+ 28 - 0
src/mol-gl/shader/compute/color-smoothing/accumulate.frag.ts

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export const accumulate_frag = `
+precision highp float;
+
+varying vec3 vPosition;
+varying vec3 vColor;
+
+uniform float uCurrentSlice;
+uniform float uCurrentX;
+uniform float uCurrentY;
+uniform float uResolution;
+
+const float p = 2.0;
+
+void main() {
+    vec2 v = gl_FragCoord.xy - vec2(uCurrentX, uCurrentY) - 0.5;
+    vec3 fragPos = vec3(v.x, v.y, uCurrentSlice);
+    float dist = distance(fragPos, vPosition);
+    if (dist > p) discard;
+
+    gl_FragColor = vec4(vColor, 1.0) * (p - dist);
+}
+`;

+ 51 - 0
src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export const accumulate_vert = `
+precision highp float;
+
+#include common
+#include read_from_texture
+
+uniform int uTotalCount;
+uniform int uGroupCount;
+
+attribute float aSample;
+#define SampleID int(aSample)
+
+attribute mat4 aTransform;
+attribute float aInstance;
+
+uniform vec2 uGeoTexDim;
+uniform sampler2D tPosition;
+uniform sampler2D tGroup;
+
+uniform vec2 uColorTexDim;
+uniform sampler2D tColor;
+
+varying vec3 vPosition;
+varying vec3 vColor;
+
+uniform vec3 uBboxSize;
+uniform vec3 uBboxMin;
+uniform float uResolution;
+
+void main() {
+    vec3 position = readFromTexture(tPosition, SampleID, uGeoTexDim).xyz;
+    float group = decodeFloatRGB(readFromTexture(tGroup, SampleID, uGeoTexDim).rgb);
+
+    position = (aTransform * vec4(position, 1.0)).xyz;
+    gl_PointSize = 7.0;
+    vPosition = (position - uBboxMin) / uResolution;
+    gl_Position = vec4(((position - uBboxMin) / uBboxSize) * 2.0 - 1.0, 1.0);
+
+    #if defined(dColorType_group)
+        vColor = readFromTexture(tColor, group, uColorTexDim).rgb;
+    #elif defined(dColorType_groupInstance)
+        vColor = readFromTexture(tColor, aInstance * float(uGroupCount) + group, uColorTexDim).rgb;
+    #endif
+}
+`;

+ 20 - 0
src/mol-gl/shader/compute/color-smoothing/normalize.frag.ts

@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export const normalize_frag = `
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tColor;
+uniform vec2 uTexSize;
+
+void main(void) {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    vec4 color = texture2D(tColor, coords);
+
+    gl_FragColor.rgb = color.rgb / color.a;
+}
+`;

+ 6 - 2
src/mol-gl/shader/mesh.vert.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>
  */
@@ -15,6 +15,10 @@ precision highp sampler2D;
 #include color_vert_params
 #include common_clip
 
+#if defined(dColorType_grid)
+    #include texture3d_from_2d_linear
+#endif
+
 #ifdef dGeoTexture
     uniform vec2 uGeoTexDim;
     uniform sampler2D tPosition;
@@ -32,10 +36,10 @@ varying vec3 vNormal;
 
 void main(){
     #include assign_group
-    #include assign_color_varying
     #include assign_marker_varying
     #include assign_clipping_varying
     #include assign_position
+    #include assign_color_varying
     #include clip_instance
 
     #ifdef dGeoTexture

+ 1 - 1
src/mol-gl/webgl/render-target.ts

@@ -88,7 +88,7 @@ export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResou
 export function createNullRenderTarget(gl: GLRenderingContext): RenderTarget {
     return {
         id: getNextRenderTargetId(),
-        texture: createNullTexture(gl, 'image-uint8'),
+        texture: createNullTexture(gl),
         framebuffer: createNullFramebuffer(),
 
         getWidth: () => 0,

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

@@ -394,7 +394,7 @@ export function createTextures(ctx: WebGLContext, schema: RenderableSchema, valu
 
 /**
  * Loads an image from a url to a textures and triggers update asynchronously.
- * This will not work on node.js with a polyfill for HTMLImageElement.
+ * This will not work on node.js without a polyfill for `HTMLImageElement`.
  */
 export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture: Texture) {
     const img = new Image();
@@ -407,8 +407,8 @@ export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture:
 
 //
 
-export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Texture {
-    const target = getTarget(gl, kind);
+export function createNullTexture(gl?: GLRenderingContext): Texture {
+    const target = gl?.TEXTURE_2D ?? 3553;
     return {
         id: getNextTextureId(),
         target,
@@ -424,12 +424,16 @@ export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Te
         define: () => {},
         load: () => {},
         bind: (id: TextureId) => {
-            gl.activeTexture(gl.TEXTURE0 + id);
-            gl.bindTexture(target, null);
+            if (gl) {
+                gl.activeTexture(gl.TEXTURE0 + id);
+                gl.bindTexture(target, null);
+            }
         },
         unbind: (id: TextureId) => {
-            gl.activeTexture(gl.TEXTURE0 + id);
-            gl.bindTexture(target, null);
+            if (gl) {
+                gl.activeTexture(gl.TEXTURE0 + id);
+                gl.bindTexture(target, null);
+            }
         },
         attachFramebuffer: () => {},
         detachFramebuffer: () => {},

+ 1 - 0
src/mol-math/geometry/common.ts

@@ -26,6 +26,7 @@ export type DensityData = {
     transform: Mat4,
     field: Tensor,
     idField: Tensor,
+    resolution: number
 }
 
 export type DensityTextureData = {

+ 2 - 0
src/mol-math/geometry/gaussian-density.ts

@@ -21,10 +21,12 @@ export type GaussianDensityProps = typeof DefaultGaussianDensityProps
 
 export type GaussianDensityData = {
     radiusFactor: number
+    resolution: number
 } & DensityData
 
 export type GaussianDensityTextureData = {
     radiusFactor: number
+    resolution: number
 } & DensityTextureData
 
 export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps) {

+ 1 - 1
src/mol-math/geometry/gaussian-density/cpu.ts

@@ -129,5 +129,5 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position
     Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
     Mat4.setTranslation(transform, expandedBox.min);
 
-    return { field, idField, transform, radiusFactor: 1 };
+    return { field, idField, transform, radiusFactor: 1, resolution };
 }

+ 11 - 12
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -22,7 +22,7 @@ import { gaussianDensity_vert } from '../../../mol-gl/shader/gaussian-density.ve
 import { gaussianDensity_frag } from '../../../mol-gl/shader/gaussian-density.frag';
 import { Framebuffer } from '../../../mol-gl/webgl/framebuffer';
 
-export const GaussianDensitySchema = {
+const GaussianDensitySchema = {
     drawCount: ValueSpec('number'),
     instanceCount: ValueSpec('number'),
 
@@ -49,9 +49,6 @@ export const GaussianDensitySchema = {
 type GaussianDensityValues = Values<typeof GaussianDensitySchema>
 type GaussianDensityRenderable = ComputeRenderable<GaussianDensityValues>
 const GaussianDensityName = 'gaussian-density';
-const GaussianDensityShaderCode = ShaderCode(
-    GaussianDensityName, gaussianDensity_vert, gaussianDensity_frag
-);
 
 function getFramebuffer(webgl: WebGLContext): Framebuffer {
     if (!webgl.namedFramebuffers[GaussianDensityName]) {
@@ -73,12 +70,12 @@ export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (
     // it's faster than texture3d
     // console.time('GaussianDensityTexture2d')
     const tmpTexture = getTexture('tmp', webgl, 'image-uint8', 'rgba', 'ubyte', 'linear');
-    const { scale, bbox, texture, gridDim, gridTexDim, radiusFactor } = calcGaussianDensityTexture2d(webgl, position, box, radius, false, props, tmpTexture);
+    const { scale, bbox, texture, gridDim, gridTexDim, radiusFactor, resolution } = calcGaussianDensityTexture2d(webgl, position, box, radius, false, props, tmpTexture);
     // webgl.waitForGpuCommandsCompleteSync()
     // console.timeEnd('GaussianDensityTexture2d')
     const { field, idField } = fieldFromTexture2d(webgl, texture, gridDim, gridTexDim);
 
-    return { field, idField, transform: getTransform(scale, bbox), radiusFactor };
+    return { field, idField, transform: getTransform(scale, bbox), radiusFactor, resolution };
 }
 
 export function GaussianDensityTexture(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, oldTexture?: Texture): GaussianDensityTextureData {
@@ -95,8 +92,8 @@ export function GaussianDensityTexture3d(webgl: WebGLContext, position: Position
     return finalizeGaussianDensityTexture(calcGaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture));
 }
 
-function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor }: _GaussianDensityTextureData): GaussianDensityTextureData {
-    return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor };
+function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor, resolution }: _GaussianDensityTextureData): GaussianDensityTextureData {
+    return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor, resolution };
 }
 
 function getTransform(scale: Vec3, bbox: Box3D) {
@@ -116,6 +113,7 @@ type _GaussianDensityTextureData = {
     gridTexDim: Vec3
     gridTexScale: Vec2
     radiusFactor: number
+    resolution: number
 }
 
 function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
@@ -200,7 +198,7 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
 
     // printTexture(webgl, minDistTex, 0.75);
 
-    return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor };
+    return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor, resolution };
 }
 
 function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
@@ -256,7 +254,7 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
     setupGroupIdRendering(webgl, renderable);
     render(texture, false);
 
-    return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridTexScale, radiusFactor };
+    return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridTexScale, radiusFactor, resolution };
 }
 
 //
@@ -362,7 +360,8 @@ function createGaussianDensityRenderable(webgl: WebGLContext, drawCount: number,
     };
 
     const schema = { ...GaussianDensitySchema };
-    const renderItem =  createComputeRenderItem(webgl, 'points', GaussianDensityShaderCode, schema, values);
+    const shaderCode = ShaderCode(GaussianDensityName, gaussianDensity_vert, gaussianDensity_frag);
+    const renderItem =  createComputeRenderItem(webgl, 'points', shaderCode, schema, values);
 
     return createComputeRenderable(renderItem, values);
 }
@@ -430,7 +429,7 @@ function getTexture2dSize(gridDim: Vec3) {
     return { texDimX, texDimY, texRows, texCols, powerOfTwoSize: texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
 }
 
-export function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3, texDim: Vec3) {
+function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3, texDim: Vec3) {
     // console.time('fieldFromTexture2d')
     const [ dx, dy, dz ] = dim;
     const [ width, height ] = texDim;

+ 2 - 2
src/mol-math/geometry/molecular-surface.ts

@@ -49,7 +49,7 @@ function getAngleTables (probePositions: number): AnglesTables {
 export const MolecularSurfaceCalculationParams = {
     probeRadius: PD.Numeric(1.4, { min: 0, max: 10, step: 0.1 }, { description: 'Radius of the probe tracing the molecular surface.' }),
     resolution: PD.Numeric(0.5, { min: 0.01, max: 20, step: 0.01 }, { description: 'Grid resolution/cell spacing.', ...BaseGeometry.CustomQualityParamInfo }),
-    probePositions: PD.Numeric(30, { min: 12, max: 90, step: 1 }, { description: 'Number of positions tested for probe target intersection.', ...BaseGeometry.CustomQualityParamInfo }),
+    probePositions: PD.Numeric(36, { min: 12, max: 90, step: 1 }, { description: 'Number of positions tested for probe target intersection.', ...BaseGeometry.CustomQualityParamInfo }),
 };
 export const DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(MolecularSurfaceCalculationParams);
 export type MolecularSurfaceCalculationProps = typeof DefaultMolecularSurfaceCalculationProps
@@ -370,5 +370,5 @@ export async function calcMolecularSurface(ctx: RuntimeContext, position: Requir
     Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
     Mat4.setTranslation(transform, expandedBox.min);
     // console.log({ field, idField, transform, updateChunk })
-    return { field, idField, transform };
+    return { field, idField, transform, resolution };
 }

+ 1 - 0
src/mol-model-props/computed/themes/accessible-surface-area.ts

@@ -64,6 +64,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
     return {
         factory: AccessibleSurfaceAreaColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         contextHash,

+ 19 - 3
src/mol-repr/structure/complex-visual.ts

@@ -11,7 +11,7 @@ import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
 import { LocationIterator } from '../../mol-geo/util/location-iterator';
 import { Theme } from '../../mol-theme/theme';
 import { createIdentityTransform } from '../../mol-geo/geometry/transform-data';
-import { createRenderObject, GraphicsRenderObject } from '../../mol-gl/render-object';
+import { createRenderObject, GraphicsRenderObject, RenderObjectValues } from '../../mol-gl/render-object';
 import { PickingId } from '../../mol-geo/geometry/picking';
 import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
 import { Interval } from '../../mol-data/int';
@@ -34,6 +34,8 @@ import { createMarkers } from '../../mol-geo/geometry/marker-data';
 import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams, StructureCylindersParams, StructureTextureMeshParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { isPromiseLike } from '../../mol-util/type-helpers';
 
 export interface  ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
 
@@ -53,6 +55,7 @@ interface ComplexVisualBuilder<P extends StructureParams, G extends Geometry> {
     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?: (structure: Structure, props: PD.Values<P>) => boolean
+    processValues?: (values: RenderObjectValues<G['kind']>, geometry: G, props: PD.Values<P>, theme: Theme, webgl?: WebGLContext) => void
     dispose?: (geometry: G) => void
 }
 
@@ -61,7 +64,7 @@ interface ComplexVisualGeometryBuilder<P extends StructureParams, G extends Geom
 }
 
 export function ComplexVisual<G extends Geometry, P extends StructureParams & Geometry.Params<G>>(builder: ComplexVisualGeometryBuilder<P, G>, materialId: number): ComplexVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, dispose } = builder;
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, processValues, dispose } = builder;
     const { updateValues, updateBoundingSphere, updateRenderableState, createPositionIterator } = builder.geometryUtils;
     const updateState = VisualUpdateState.create();
 
@@ -198,6 +201,12 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
         }
     }
 
+    function finalize(ctx: VisualContext) {
+        if (renderObject) {
+            processValues?.(renderObject.values, geometry, currentProps, currentTheme, ctx.webgl);
+        }
+    }
+
     return {
         get groupCount() { return locationIt ? locationIt.count : 0; },
         get renderObject () { return locationIt && locationIt.count ? renderObject : undefined; },
@@ -205,10 +214,17 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
             prepareUpdate(theme, props, structure || currentStructure);
             if (updateState.createGeometry) {
                 const newGeometry = createGeometry(ctx, newStructure, newTheme, newProps, geometry);
-                return newGeometry instanceof Promise ? newGeometry.then(update) : update(newGeometry);
+                if (isPromiseLike(newGeometry)) {
+                    return newGeometry.then(g => {
+                        update(g);
+                        finalize(ctx);
+                    });
+                }
+                update(newGeometry);
             } else {
                 update();
             }
+            finalize(ctx);
         },
         getLoci(pickingId: PickingId) {
             return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci;

+ 24 - 4
src/mol-repr/structure/units-visual.ts

@@ -12,7 +12,7 @@ import { Geometry, GeometryUtils } from '../../mol-geo/geometry/geometry';
 import { LocationIterator } from '../../mol-geo/util/location-iterator';
 import { Theme } from '../../mol-theme/theme';
 import { createUnitsTransform, includesUnitKind } from './visual/util/common';
-import { createRenderObject, GraphicsRenderObject } from '../../mol-gl/render-object';
+import { createRenderObject, GraphicsRenderObject, RenderObjectValues } from '../../mol-gl/render-object';
 import { PickingId } from '../../mol-geo/geometry/picking';
 import { Loci, isEveryLoci, EmptyLoci } from '../../mol-model/loci';
 import { Interval } from '../../mol-data/int';
@@ -38,6 +38,8 @@ import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 import { SizeValues } from '../../mol-gl/renderable/schema';
 import { StructureParams, StructureMeshParams, StructureSpheresParams, StructurePointsParams, StructureLinesParams, StructureTextParams, StructureDirectVolumeParams, StructureTextureMeshParams, StructureCylindersParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { isPromiseLike } from '../../mol-util/type-helpers';
 
 export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
 
@@ -59,6 +61,7 @@ interface UnitsVisualBuilder<P extends StructureParams, G extends Geometry> {
     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?: (structureGroup: StructureGroup, props: PD.Values<P>) => boolean
+    processValues?: (values: RenderObjectValues<G['kind']>, geometry: G, props: PD.Values<P>, theme: Theme, webgl?: WebGLContext) => void
     dispose?: (geometry: G) => void
 }
 
@@ -67,7 +70,7 @@ interface UnitsVisualGeometryBuilder<P extends StructureParams, G extends Geomet
 }
 
 export function UnitsVisual<G extends Geometry, P extends StructureParams & Geometry.Params<G>>(builder: UnitsVisualGeometryBuilder<P, G>, materialId: number): UnitsVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, dispose } = builder;
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, processValues, dispose } = builder;
     const { createEmpty: createEmptyGeometry, updateValues, updateBoundingSphere, updateRenderableState, createPositionIterator } = builder.geometryUtils;
     const updateState = VisualUpdateState.create();
 
@@ -157,7 +160,11 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
                 updateState.updateColor = true;
                 updateState.updateSize = true;
             }
-            if (newTheme.color.granularity.startsWith('vertex')) {
+            if (newTheme.color.granularity.startsWith('vertex') ||
+                renderObject.values.dColorType.ref.value.startsWith('vertex') ||
+                newTheme.color.granularity.startsWith('volume') ||
+                renderObject.values.dColorType.ref.value.startsWith('volume')
+            ) {
                 updateState.updateColor = true;
             }
         }
@@ -250,6 +257,12 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
         }
     }
 
+    function finalize(ctx: VisualContext) {
+        if (renderObject) {
+            processValues?.(renderObject.values, geometry, currentProps, currentTheme, ctx.webgl);
+        }
+    }
+
     return {
         get groupCount() { return locationIt ? locationIt.count : 0; },
         get renderObject () { return locationIt && locationIt.count ? renderObject : undefined; },
@@ -257,10 +270,17 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
             prepareUpdate(theme, props, structureGroup || currentStructureGroup);
             if (updateState.createGeometry) {
                 const newGeometry = _createGeometry(ctx, newStructureGroup.group.units[0], newStructureGroup.structure, newTheme, newProps, geometry);
-                return newGeometry instanceof Promise ? newGeometry.then(update) : update(newGeometry as G);
+                if (isPromiseLike(newGeometry)) {
+                    return newGeometry.then(g => {
+                        update(g);
+                        finalize(ctx);
+                    });
+                }
+                update(newGeometry);
             } else {
                 update();
             }
+            finalize(ctx);
         },
         getLoci(pickingId: PickingId) {
             return renderObject ? getLoci(pickingId, currentStructureGroup, renderObject.id) : EmptyLoci;

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

@@ -20,9 +20,14 @@ import { Sphere3D } from '../../../mol-math/geometry';
 import { ComplexVisual, ComplexMeshParams, ComplexMeshVisual, ComplexTextureMeshVisual, ComplexTextureMeshParams } from '../complex-visual';
 import { getUnitExtraRadius, getStructureExtraRadius, getVolumeSliceInfo } from './util/common';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
+import { MeshValues } from '../../../mol-gl/renderable/mesh';
+import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
+import { Texture } from '../../../mol-gl/webgl/texture';
+import { applyMeshColorSmoothing, applyTextureMeshColorSmoothing, ColorSmoothingParams, getColorSmoothingProps } from './util/color';
 
 const SharedParams = {
     ...GaussianDensityParams,
+    ...ColorSmoothingParams,
     ignoreHydrogens: PD.Boolean(false),
     tryUseGpu: PD.Boolean(true),
 };
@@ -71,11 +76,16 @@ export function StructureGaussianSurfaceVisual(materialId: number, structure: St
     return StructureGaussianSurfaceMeshVisual(materialId);
 }
 
+type GaussianSurfaceMeta = {
+    resolution?: number
+    colorTexture?: Texture
+}
+
 //
 
 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).runInContext(ctx.runtime);
+    const { transform, field, idField, radiusFactor, resolution } = await computeUnitGaussianDensity(structure, unit, props).runInContext(ctx.runtime);
 
     const params = {
         isoLevel: Math.exp(-smoothness) / radiusFactor,
@@ -83,6 +93,7 @@ async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structu
         idField
     };
     const surface = await computeMarchingCubesMesh(params, mesh).runAsChild(ctx.runtime);
+    (surface.meta as GaussianSurfaceMeta) = { resolution };
 
     Mesh.transform(surface, transform);
     if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface);
@@ -107,9 +118,26 @@ export function GaussianSurfaceMeshVisual(materialId: number): UnitsVisual<Gauss
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+            if (newProps.smoothColors.name !== currentProps.smoothColors.name) {
+                state.updateColor = true;
+            } else if (newProps.smoothColors.name === 'on' && currentProps.smoothColors.name === 'on') {
+                if (newProps.smoothColors.params.resolutionFactor !== currentProps.smoothColors.params.resolutionFactor) state.updateColor = true;
+                if (newProps.smoothColors.params.sampleStride !== currentProps.smoothColors.params.sampleStride) state.updateColor = true;
+            }
         },
         mustRecreate: (structureGroup: StructureGroup, props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
             return props.tryUseGpu && !!webgl && suitableForGpu(structureGroup.structure, props, webgl);
+        },
+        processValues: (values: MeshValues, geometry: Mesh, props: PD.Values<GaussianSurfaceMeshParams>, theme: Theme, webgl?: WebGLContext) => {
+            const { resolution, colorTexture } = geometry.meta as GaussianSurfaceMeta;
+            const csp = getColorSmoothingProps(props, theme, resolution, webgl);
+            if (csp) {
+                applyMeshColorSmoothing(values, csp.resolution, csp.stride, csp.webgl, colorTexture);
+                (geometry.meta as GaussianSurfaceMeta).colorTexture = values.tColorGrid.ref.value;
+            }
+        },
+        dispose: (geometry: Mesh) => {
+            (geometry.meta as GaussianSurfaceMeta).colorTexture?.destroy();
         }
     }, materialId);
 }
@@ -118,7 +146,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).runInContext(ctx.runtime);
+    const { transform, field, idField, radiusFactor, resolution } = await computeStructureGaussianDensity(structure, props).runInContext(ctx.runtime);
 
     const params = {
         isoLevel: Math.exp(-smoothness) / radiusFactor,
@@ -126,6 +154,7 @@ async function createStructureGaussianSurfaceMesh(ctx: VisualContext, structure:
         idField
     };
     const surface = await computeMarchingCubesMesh(params, mesh).runAsChild(ctx.runtime);
+    (surface.meta as GaussianSurfaceMeta) = { resolution };
 
     Mesh.transform(surface, transform);
     if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface);
@@ -149,9 +178,26 @@ export function StructureGaussianSurfaceMeshVisual(materialId: number): ComplexV
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true;
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
+            if (newProps.smoothColors.name !== currentProps.smoothColors.name) {
+                state.updateColor = true;
+            } else if (newProps.smoothColors.name === 'on' && currentProps.smoothColors.name === 'on') {
+                if (newProps.smoothColors.params.resolutionFactor !== currentProps.smoothColors.params.resolutionFactor) state.updateColor = true;
+                if (newProps.smoothColors.params.sampleStride !== currentProps.smoothColors.params.sampleStride) state.updateColor = true;
+            }
         },
         mustRecreate: (structure: Structure, props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
             return props.tryUseGpu && !!webgl && suitableForGpu(structure, props, webgl);
+        },
+        processValues: (values: MeshValues, geometry: Mesh, props: PD.Values<GaussianSurfaceMeshParams>, theme: Theme, webgl?: WebGLContext) => {
+            const { resolution, colorTexture } = geometry.meta as GaussianSurfaceMeta;
+            const csp = getColorSmoothingProps(props, theme, resolution, webgl);
+            if (csp) {
+                applyMeshColorSmoothing(values, csp.resolution, csp.stride, csp.webgl, colorTexture);
+                (geometry.meta as GaussianSurfaceMeta).colorTexture = values.tColorGrid.ref.value;
+            }
+        },
+        dispose: (geometry: Mesh) => {
+            (geometry.meta as GaussianSurfaceMeta).colorTexture?.destroy();
         }
     }, materialId);
 }
@@ -186,6 +232,7 @@ async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit,
 
     const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure));
     const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
+    (surface.meta as GaussianSurfaceMeta) = { resolution: densityTextureData.resolution };
 
     return surface;
 }
@@ -204,15 +251,31 @@ export function GaussianSurfaceTextureMeshVisual(materialId: number): UnitsVisua
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+            if (newProps.smoothColors.name !== currentProps.smoothColors.name) {
+                state.updateColor = true;
+            } else if (newProps.smoothColors.name === 'on' && currentProps.smoothColors.name === 'on') {
+                if (newProps.smoothColors.params.resolutionFactor !== currentProps.smoothColors.params.resolutionFactor) state.updateColor = true;
+                if (newProps.smoothColors.params.sampleStride !== currentProps.smoothColors.params.sampleStride) state.updateColor = true;
+            }
         },
         mustRecreate: (structureGroup: StructureGroup, props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
             return !props.tryUseGpu || !webgl || !suitableForGpu(structureGroup.structure, props, webgl);
         },
+        processValues: (values: TextureMeshValues, geometry: TextureMesh, props: PD.Values<GaussianSurfaceMeshParams>, theme: Theme, webgl?: WebGLContext) => {
+            const { resolution, colorTexture } = geometry.meta as GaussianSurfaceMeta;
+            const csp = getColorSmoothingProps(props, theme, resolution, webgl);
+            if (csp) {
+                applyTextureMeshColorSmoothing(values, csp.resolution, csp.stride, csp.webgl, colorTexture);
+                (geometry.meta as GaussianSurfaceMeta).colorTexture = values.tColorGrid.ref.value;
+            }
+        },
         dispose: (geometry: TextureMesh) => {
             geometry.vertexTexture.ref.value.destroy();
             geometry.groupTexture.ref.value.destroy();
             geometry.normalTexture.ref.value.destroy();
             geometry.doubleBuffer.destroy();
+
+            (geometry.meta as GaussianSurfaceMeta).colorTexture?.destroy();
         }
     }, materialId);
 }
@@ -245,6 +308,7 @@ async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, str
 
     const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure));
     const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
+    (surface.meta as GaussianSurfaceMeta) = { resolution: densityTextureData.resolution };
 
     return surface;
 }
@@ -262,15 +326,32 @@ export function StructureGaussianSurfaceTextureMeshVisual(materialId: number): C
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true;
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
+            if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+            if (newProps.smoothColors.name !== currentProps.smoothColors.name) {
+                state.updateColor = true;
+            } else if (newProps.smoothColors.name === 'on' && currentProps.smoothColors.name === 'on') {
+                if (newProps.smoothColors.params.resolutionFactor !== currentProps.smoothColors.params.resolutionFactor) state.updateColor = true;
+                if (newProps.smoothColors.params.sampleStride !== currentProps.smoothColors.params.sampleStride) state.updateColor = true;
+            }
         },
         mustRecreate: (structure: Structure, props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
             return !props.tryUseGpu || !webgl || !suitableForGpu(structure, props, webgl);
         },
+        processValues: (values: TextureMeshValues, geometry: TextureMesh, props: PD.Values<GaussianSurfaceMeshParams>, theme: Theme, webgl?: WebGLContext) => {
+            const { resolution, colorTexture } = geometry.meta as GaussianSurfaceMeta;
+            const csp = getColorSmoothingProps(props, theme, resolution, webgl);
+            if (csp) {
+                applyTextureMeshColorSmoothing(values, csp.resolution, csp.stride, csp.webgl, colorTexture);
+                (geometry.meta as GaussianSurfaceMeta).colorTexture = values.tColorGrid.ref.value;
+            }
+        },
         dispose: (geometry: TextureMesh) => {
             geometry.vertexTexture.ref.value.destroy();
             geometry.groupTexture.ref.value.destroy();
             geometry.normalTexture.ref.value.destroy();
             geometry.doubleBuffer.destroy();
+
+            (geometry.meta as GaussianSurfaceMeta).colorTexture?.destroy();
         }
     }, materialId);
 }

+ 33 - 3
src/mol-repr/structure/visual/molecular-surface-mesh.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -17,18 +17,29 @@ import { ElementIterator, getElementLoci, eachElement } from './util/element';
 import { VisualUpdateState } from '../../util';
 import { CommonSurfaceParams, getUnitExtraRadius } from './util/common';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { MeshValues } from '../../../mol-gl/renderable/mesh';
+import { Texture } from '../../../mol-gl/webgl/texture';
+import { WebGLContext } from '../../../mol-gl/webgl/context';
+import { applyMeshColorSmoothing, ColorSmoothingParams, getColorSmoothingProps } from './util/color';
 
 export const MolecularSurfaceMeshParams = {
     ...UnitsMeshParams,
     ...MolecularSurfaceCalculationParams,
-    ...CommonSurfaceParams
+    ...CommonSurfaceParams,
+    ...ColorSmoothingParams,
 };
 export type MolecularSurfaceMeshParams = typeof MolecularSurfaceMeshParams
 
+type MolecularSurfaceMeta = {
+    resolution?: number
+    colorTexture?: Texture
+}
+
 //
 
 async function createMolecularSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: MolecularSurfaceProps, mesh?: Mesh): Promise<Mesh> {
-    const { transform, field, idField } = await computeUnitMolecularSurface(structure, unit, props).runInContext(ctx.runtime);
+    const { transform, field, idField, resolution } = await computeUnitMolecularSurface(structure, unit, props).runInContext(ctx.runtime);
+
     const params = {
         isoLevel: props.probeRadius,
         scalarField: field,
@@ -41,6 +52,7 @@ async function createMolecularSurfaceMesh(ctx: VisualContext, unit: Unit, struct
 
     const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.probeRadius + getUnitExtraRadius(unit));
     surface.setBoundingSphere(sphere);
+    (surface.meta as MolecularSurfaceMeta) = { resolution };
 
     return surface;
 }
@@ -57,7 +69,25 @@ export function MolecularSurfaceMeshVisual(materialId: number): UnitsVisual<Mole
             if (newProps.probeRadius !== currentProps.probeRadius) state.createGeometry = true;
             if (newProps.probePositions !== currentProps.probePositions) state.createGeometry = true;
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
+            if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+            if (newProps.smoothColors.name !== currentProps.smoothColors.name) {
+                state.updateColor = true;
+            } else if (newProps.smoothColors.name === 'on' && currentProps.smoothColors.name === 'on') {
+                if (newProps.smoothColors.params.resolutionFactor !== currentProps.smoothColors.params.resolutionFactor) state.updateColor = true;
+                if (newProps.smoothColors.params.sampleStride !== currentProps.smoothColors.params.sampleStride) state.updateColor = true;
+            }
+        },
+        processValues: (values: MeshValues, geometry: Mesh, props: PD.Values<MolecularSurfaceMeshParams>, theme: Theme, webgl?: WebGLContext) => {
+            const { resolution, colorTexture } = geometry.meta as MolecularSurfaceMeta;
+            const csp = getColorSmoothingProps(props, theme, resolution, webgl);
+            if (csp) {
+                applyMeshColorSmoothing(values, csp.resolution, csp.stride, csp.webgl, colorTexture);
+                (geometry.meta as MolecularSurfaceMeta).colorTexture = values.tColorGrid.ref.value;
+            }
+        },
+        dispose: (geometry: Mesh) => {
+            (geometry.meta as MolecularSurfaceMeta).colorTexture?.destroy();
         }
     }, materialId);
 }

+ 103 - 0
src/mol-repr/structure/visual/util/color.ts

@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { calcMeshColorSmoothing } from '../../../../mol-geo/geometry/mesh/color-smoothing';
+import { calcTextureMeshColorSmoothing } from '../../../../mol-geo/geometry/texture-mesh/color-smoothing';
+import { MeshValues } from '../../../../mol-gl/renderable/mesh';
+import { TextureMeshValues } from '../../../../mol-gl/renderable/texture-mesh';
+import { WebGLContext } from '../../../../mol-gl/webgl/context';
+import { Texture } from '../../../../mol-gl/webgl/texture';
+import { smoothstep } from '../../../../mol-math/interpolate';
+import { Theme } from '../../../../mol-theme/theme';
+import { ValueCell } from '../../../../mol-util';
+import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
+
+export const ColorSmoothingParams = {
+    smoothColors: PD.MappedStatic('auto', {
+        auto: PD.Group({}),
+        on: PD.Group({
+            resolutionFactor: PD.Numeric(2, { min: 1, max: 6, step: 0.1 }),
+            sampleStride: PD.Numeric(3, { min: 1, max: 12, step: 1 }),
+        }),
+        off: PD.Group({})
+    }),
+};
+export type ColorSmoothingParams = typeof ColorSmoothingParams
+
+export function getColorSmoothingProps(props: PD.Values<ColorSmoothingParams>, theme: Theme, resolution?: number, webgl?: WebGLContext) {
+    if ((props.smoothColors.name === 'on' || (props.smoothColors.name === 'auto' && theme.color.preferSmoothing)) && resolution && resolution < 3 && webgl) {
+        let stride = 3;
+        if (props.smoothColors.name === 'on') {
+            resolution *= props.smoothColors.params.resolutionFactor;
+            stride = props.smoothColors.params.sampleStride;
+        } else {
+            // https://graphtoy.com/?f1(x,t)=(2-smoothstep(0,1.1,x))*x&coords=0.7,0.6,1.8
+            resolution *= 2 - smoothstep(0, 1.1, resolution);
+            resolution = Math.max(0.5, resolution);
+        }
+        return { resolution, stride, webgl };
+    };
+}
+
+function isSupportedColorType(x: string): x is 'group' | 'groupInstance' {
+    return x === 'group' || x === 'groupInstance';
+}
+
+export function applyMeshColorSmoothing(values: MeshValues, resolution: number, stride: number, webgl?: WebGLContext, colorTexture?: Texture) {
+    if (!isSupportedColorType(values.dColorType.ref.value)) return;
+
+    const smoothingData = calcMeshColorSmoothing({
+        vertexCount: values.uVertexCount.ref.value,
+        instanceCount: values.uInstanceCount.ref.value,
+        groupCount: values.uGroupCount.ref.value,
+        transformBuffer: values.aTransform.ref.value,
+        instanceBuffer: values.aInstance.ref.value,
+        positionBuffer: values.aPosition.ref.value,
+        groupBuffer: values.aGroup.ref.value,
+        colorData: values.tColor.ref.value,
+        colorType: values.dColorType.ref.value,
+        boundingSphere: values.boundingSphere.ref.value,
+        invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
+    }, resolution, stride, webgl, colorTexture);
+
+    if (smoothingData.kind === 'volume') {
+        ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
+        ValueCell.update(values.tColorGrid, smoothingData.texture);
+        ValueCell.update(values.uColorTexDim, smoothingData.gridTexDim);
+        ValueCell.update(values.uColorGridDim, smoothingData.gridDim);
+        ValueCell.update(values.uColorGridTransform, smoothingData.gridTransform);
+    } else if (smoothingData.kind === 'vertex') {
+        ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
+        ValueCell.update(values.tColor, smoothingData.texture);
+        ValueCell.update(values.uColorTexDim, smoothingData.texDim);
+    }
+}
+
+export function applyTextureMeshColorSmoothing(values: TextureMeshValues, resolution: number, stride: number, webgl: WebGLContext, colorTexture?: Texture) {
+    if (!isSupportedColorType(values.dColorType.ref.value)) return;
+
+    stride *= 3; // triple because TextureMesh is never indexed (no elements buffer)
+
+    const smoothingData = calcTextureMeshColorSmoothing({
+        vertexCount: values.uVertexCount.ref.value,
+        instanceCount: values.uInstanceCount.ref.value,
+        groupCount: values.uGroupCount.ref.value,
+        transformBuffer: values.aTransform.ref.value,
+        instanceBuffer: values.aInstance.ref.value,
+        positionTexture: values.tPosition.ref.value,
+        groupTexture: values.tGroup.ref.value,
+        colorData: values.tColor.ref.value,
+        colorType: values.dColorType.ref.value,
+        boundingSphere: values.boundingSphere.ref.value,
+        invariantBoundingSphere: values.invariantBoundingSphere.ref.value,
+    }, resolution, stride, webgl, colorTexture);
+
+    ValueCell.updateIfChanged(values.dColorType, smoothingData.type);
+    ValueCell.update(values.tColorGrid, smoothingData.texture);
+    ValueCell.update(values.uColorTexDim, smoothingData.gridTexDim);
+    ValueCell.update(values.uColorGridDim, smoothingData.gridDim);
+    ValueCell.update(values.uColorGridTransform, smoothingData.gridTransform);
+}

+ 10 - 0
src/mol-repr/util.ts

@@ -53,6 +53,7 @@ export interface QualityProps {
     radialSegments: number
     linearSegments: number
     resolution: number
+    probePositions: number
     doubleSided: boolean
     xrayShaded: boolean
     alpha: number
@@ -110,6 +111,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
     let radialSegments = defaults(props.radialSegments, 12);
     let linearSegments = defaults(props.linearSegments, 8);
     let resolution = defaults(props.resolution, 2);
+    let probePositions = defaults(props.probePositions, 12);
     let doubleSided = defaults(props.doubleSided, true);
 
     let volume = 0;
@@ -130,6 +132,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             radialSegments = 36;
             linearSegments = 18;
             resolution = 0.1;
+            probePositions = 72;
             doubleSided = true;
             break;
         case 'higher':
@@ -137,6 +140,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             radialSegments = 28;
             linearSegments = 14;
             resolution = 0.3;
+            probePositions = 48;
             doubleSided = true;
             break;
         case 'high':
@@ -144,6 +148,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             radialSegments = 20;
             linearSegments = 10;
             resolution = 0.5;
+            probePositions = 36;
             doubleSided = true;
             break;
         case 'medium':
@@ -151,6 +156,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             radialSegments = 12;
             linearSegments = 8;
             resolution = 0.8;
+            probePositions = 24;
             doubleSided = true;
             break;
         case 'low':
@@ -158,6 +164,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             radialSegments = 8;
             linearSegments = 3;
             resolution = 1.3;
+            probePositions = 24;
             doubleSided = false;
             break;
         case 'lower':
@@ -165,6 +172,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             radialSegments = 4;
             linearSegments = 2;
             resolution = 3;
+            probePositions = 12;
             doubleSided = false;
             break;
         case 'lowest':
@@ -172,6 +180,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
             radialSegments = 2;
             linearSegments = 1;
             resolution = 8;
+            probePositions = 12;
             doubleSided = false;
             break;
         case 'custom':
@@ -192,6 +201,7 @@ export function getQualityProps(props: Partial<QualityProps>, data?: any) {
         radialSegments,
         linearSegments,
         resolution,
+        probePositions,
         doubleSided
     };
 }

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

@@ -28,7 +28,6 @@ export interface VisualContext {
     readonly runtime: RuntimeContext
     readonly webgl?: WebGLContext
 }
-// export type VisualFactory<D, P extends PD.Params> = (ctx: VisualContext) => Visual<D, P>
 
 export { Visual };
 interface Visual<D, P extends PD.Params> {

+ 2 - 1
src/mol-repr/volume/representation.ts

@@ -31,6 +31,7 @@ import { Task } from '../../mol-task';
 import { SizeValues } from '../../mol-gl/renderable/schema';
 import { Clipping } from '../../mol-theme/clipping';
 import { WebGLContext } from '../../mol-gl/webgl/context';
+import { isPromiseLike } from '../../mol-util/type-helpers';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<Volume, P> { }
 
@@ -173,7 +174,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
             prepareUpdate(theme, props, volume || currentVolume);
             if (updateState.createGeometry) {
                 const newGeometry = createGeometry(ctx, newVolume, newTheme, newProps, geometry);
-                return newGeometry instanceof Promise ? newGeometry.then(update) : update(newGeometry);
+                return isPromiseLike(newGeometry) ? newGeometry.then(update) : update(newGeometry);
             } else {
                 update();
             }

+ 5 - 2
src/mol-theme/color.ts

@@ -45,9 +45,12 @@ interface ColorTheme<P extends PD.Params> {
     readonly granularity: ColorType
     readonly color: LocationColor
     readonly props: Readonly<PD.Values<P>>
-    // if palette is defined, 24bit RGB color value normalized to interval [0, 1]
-    // is used as index to the colors
+    /**
+     * if palette is defined, 24bit RGB color value normalized to interval [0, 1]
+     * is used as index to the colors
+     */
     readonly palette?: Readonly<ColorTheme.Palette>
+    readonly preferSmoothing?: boolean
     readonly contextHash?: number
     readonly description?: string
     readonly legend?: Readonly<ScaleLegend | TableLegend>

+ 1 - 0
src/mol-theme/color/atom-id.ts

@@ -72,6 +72,7 @@ export function AtomIdColorTheme(ctx: ThemeDataContext, props: PD.Values<AtomIdC
     return {
         factory: AtomIdColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/element-index.ts

@@ -64,6 +64,7 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<E
     return {
         factory: ElementIndexColorTheme,
         granularity: 'groupInstance',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 2 - 1
src/mol-theme/color/element-symbol.ts

@@ -77,11 +77,12 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<
         return DefaultElementSymbolColor;
     }
 
-    const granularity = props.carbonColor.name === 'element-symbol' ? 'group' : 'groupInstance';
+    const granularity = props.carbonColor.name === 'operator-name' ? 'groupInstance' : 'group';
 
     return {
         factory: ElementSymbolColorTheme,
         granularity,
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/hydrophobicity.ts

@@ -84,6 +84,7 @@ export function HydrophobicityColorTheme(ctx: ThemeDataContext, props: PD.Values
     return {
         factory: HydrophobicityColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/illustrative.ts

@@ -47,6 +47,7 @@ export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<I
     return {
         factory: IllustrativeColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/occupancy.ts

@@ -50,6 +50,7 @@ export function OccupancyColorTheme(ctx: ThemeDataContext, props: PD.Values<Occu
     return {
         factory: OccupancyColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/partial-charge.ts

@@ -48,6 +48,7 @@ export function PartialChargeColorTheme(ctx: ThemeDataContext, props: PD.Values<
     return {
         factory: PartialChargeColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/residue-name.ts

@@ -119,6 +119,7 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: PD.Values<Re
     return {
         factory: ResidueNameColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/secondary-structure.ts

@@ -102,6 +102,7 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Va
     return {
         factory: SecondaryStructureColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         contextHash,

+ 1 - 0
src/mol-theme/color/sequence-id.ts

@@ -102,6 +102,7 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Seq
     return {
         factory: SequenceIdColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 1 - 0
src/mol-theme/color/uncertainty.ts

@@ -54,6 +54,7 @@ export function UncertaintyColorTheme(ctx: ThemeDataContext, props: PD.Values<Un
     return {
         factory: UncertaintyColorTheme,
         granularity: 'group',
+        preferSmoothing: true,
         color,
         props,
         description: Description,

+ 4 - 0
src/mol-util/type-helpers.ts

@@ -29,4 +29,8 @@ export interface FiniteArray<T, L extends number = number> extends ReadonlyArray
 
 export function assertUnreachable(x: never): never {
     throw new Error('unreachable');
+}
+
+export function isPromiseLike<T = any>(x: any): x is Promise<T> {
+    return typeof x?.then === 'function';
 }