Ver Fonte

Merge pull request #105 from molstar/gpu-grid

GPU grid 3d computation wrapper
David Sehnal há 4 anos atrás
pai
commit
2683c5b318

+ 2 - 1
src/cli/state-docs/pd-to-md.ts

@@ -26,7 +26,8 @@ function paramInfo(param: PD.Any, offset: number): string {
         case 'file': return `JavaScript File Handle`;
         case 'file-list': return `JavaScript FileList Handle`;
         case 'select': return `One of ${oToS(param.options)}`;
-        case 'value-ref': return `Reference to a state object.`;
+        case 'value-ref': return `Reference to a runtime defined value.`;
+        case 'data-ref': return `Reference to a computed data value.`;
         case 'text': return 'String';
         case 'interval': return `Interval [min, max]`;
         case 'group': return `Object with:\n${getParams(param.params, offset + 2)}`;

+ 2 - 2
src/examples/alpha-orbitals/index.ts

@@ -20,8 +20,8 @@ import { BehaviorSubject } from 'rxjs';
 import { debounceTime, skip } from 'rxjs/operators';
 import './index.html';
 import { Basis, AlphaOrbital } from '../../extensions/alpha-orbitals/data-model';
-import { canComputeAlphaOrbitalsOnGPU } from '../../extensions/alpha-orbitals/gpu/compute';
 import { PluginCommands } from '../../mol-plugin/commands';
+import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
 require('mol-plugin-ui/skin/light.scss');
 
 interface DemoInput {
@@ -71,7 +71,7 @@ export class AlphaOrbitalsExample {
 
         this.plugin.managers.interactivity.setProps({ granularity: 'element' });
 
-        if (!canComputeAlphaOrbitalsOnGPU(this.plugin.canvas3d?.webgl)) {
+        if (!canComputeGrid3dOnGPU(this.plugin.canvas3d?.webgl)) {
             PluginCommands.Toast.Show(this.plugin, {
                 title: 'Error',
                 message: `Browser/device does not support required WebGL extension (OES_texture_float).`

+ 4 - 4
src/extensions/alpha-orbitals/data-model.ts

@@ -7,8 +7,8 @@
 import { Mat4, Tensor, Vec3 } from '../../mol-math/linear-algebra';
 import { Grid } from '../../mol-model/volume';
 import { SphericalBasisOrder } from './spherical-functions';
-import { Box3D } from '../../mol-math/geometry';
-import { arrayMin, arrayMax, arrayRms } from '../../mol-util/array';
+import { Box3D, RegularGrid3d } from '../../mol-math/geometry';
+import { arrayMin, arrayMax, arrayRms, arrayMean } from '../../mol-util/array';
 
 // Note: generally contracted gaussians are currently not supported.
 export interface SphericalElectronShell {
@@ -95,7 +95,7 @@ export function initCubeGrid(params: CubeGridComputationParams): CubeGridInfo {
 
 const BohrToAngstromFactor = 0.529177210859;
 
-export function createGrid(gridInfo: CubeGridInfo, values: Float32Array, axisOrder: number[]) {
+export function createGrid(gridInfo: RegularGrid3d, values: Float32Array, axisOrder: number[]) {
     const boxSize = Box3D.size(Vec3(), gridInfo.box);
     const boxOrigin = Vec3.clone(gridInfo.box.min);
 
@@ -122,7 +122,7 @@ export function createGrid(gridInfo: CubeGridInfo, values: Float32Array, axisOrd
         stats: {
             min: arrayMin(values),
             max: arrayMax(values),
-            mean: arrayMax(values),
+            mean: arrayMean(values),
             sigma: arrayRms(values),
         },
     };

+ 4 - 3
src/extensions/alpha-orbitals/density.ts

@@ -5,10 +5,11 @@
  */
 
 import { sortArray } from '../../mol-data/util';
+import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { Task } from '../../mol-task';
 import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
-import { canComputeAlphaOrbitalsOnGPU, gpuComputeAlphaOrbitalsDensityGridValues } from './gpu/compute';
+import { gpuComputeAlphaOrbitalsDensityGridValues } from './gpu/compute';
 
 export function createSphericalCollocationDensityGrid(
     params: CubeGridComputationParams, orbitals: AlphaOrbital[], webgl?: WebGLContext
@@ -17,9 +18,9 @@ export function createSphericalCollocationDensityGrid(
         const cubeGrid = initCubeGrid(params);
 
         let matrix: Float32Array;
-        if (canComputeAlphaOrbitalsOnGPU(webgl)) {
+        if (canComputeGrid3dOnGPU(webgl)) {
             // console.time('gpu');
-            matrix = await gpuComputeAlphaOrbitalsDensityGridValues(webgl!, cubeGrid, orbitals, ctx);
+            matrix = await gpuComputeAlphaOrbitalsDensityGridValues(ctx, webgl!, cubeGrid, orbitals);
             // console.timeEnd('gpu');
         } else {
             throw new Error('Missing OES_texture_float WebGL extension.');

+ 60 - 199
src/extensions/alpha-orbitals/gpu/compute.ts

@@ -4,46 +4,72 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { QuadSchema, QuadValues } from '../../../mol-gl/compute/util';
-import { ComputeRenderable, createComputeRenderable } from '../../../mol-gl/renderable';
-import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../../mol-gl/renderable/schema';
-import { ShaderCode } from '../../../mol-gl/shader-code';
-import quad_vert from '../../../mol-gl/shader/quad.vert';
+import { createGrid3dComputeRenderable } from '../../../mol-gl/compute/grid3d';
+import { TextureSpec, UnboxedValues, UniformSpec } from '../../../mol-gl/renderable/schema';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
-import { createComputeRenderItem } from '../../../mol-gl/webgl/render-item';
 import { RuntimeContext } from '../../../mol-task';
 import { ValueCell } from '../../../mol-util';
 import { arrayMin } from '../../../mol-util/array';
-import { isLittleEndian } from '../../../mol-util/is-little-endian';
 import { AlphaOrbital, Basis, CubeGridInfo } from '../data-model';
 import { normalizeBasicOrder, SphericalBasisOrder } from '../spherical-functions';
-import shader_frag from './shader.frag';
+import { MAIN, UTILS } from './shader.frag';
 
-const AlphaOrbitalsSchema = {
-    ...QuadSchema,
-    uDimensions: UniformSpec('v3'),
-    uMin: UniformSpec('v3'),
-    uDelta: UniformSpec('v3'),
+const Schema = {
     tCenters: TextureSpec('image-float32', 'rgba', 'float', 'nearest'),
     tInfo: TextureSpec('image-float32', 'rgba', 'float', 'nearest'),
     tCoeff: TextureSpec('image-float32', 'rgb', 'float', 'nearest'),
     tAlpha: TextureSpec('image-float32', 'alpha', 'float', 'nearest'),
-    uWidth: UniformSpec('f'),
     uNCenters: UniformSpec('i'),
     uNAlpha: UniformSpec('i'),
     uNCoeff: UniformSpec('i'),
     uMaxCoeffs: UniformSpec('i'),
-    uLittleEndian: UniformSpec('b'),
-    uDensity: UniformSpec('b'),
-    uOccupancy: UniformSpec('f'),
-    tCumulativeSum: TextureSpec('texture', 'rgba', 'ubyte', 'nearest')
 };
-type AlphaOrbitalsSchema = Values<typeof AlphaOrbitalsSchema>
-const AlphaOrbitalsName = 'alpha-orbitals';
-const AlphaOrbitalsTex0 = 'alpha-orbitals-0';
-const AlphaOrbitalsTex1 = 'alpha-orbitals-1';
-const AlphaOrbitalsShaderCode = ShaderCode(AlphaOrbitalsName, quad_vert, shader_frag);
-type AlphaOrbitalsRenderable = ComputeRenderable<AlphaOrbitalsSchema>
+
+const Orbitals = createGrid3dComputeRenderable({
+    schema: Schema,
+    loopBounds: ['uNCenters', 'uMaxCoeffs'],
+    mainCode: MAIN,
+    utilCode: UTILS,
+    returnCode: 'v',
+    values(params: { grid: CubeGridInfo, orbital: AlphaOrbital }) {
+        return createTextureData(params.grid, params.orbital);
+    }
+});
+
+const Density = createGrid3dComputeRenderable({
+    schema: {
+        ...Schema,
+        uOccupancy: UniformSpec('f'),
+    },
+    loopBounds: ['uNCenters', 'uMaxCoeffs'],
+    mainCode: MAIN,
+    utilCode: UTILS,
+    returnCode: 'current + uOccupancy * v * v',
+    values(params: { grid: CubeGridInfo, orbitals: AlphaOrbital[] }) {
+        return {
+            ...createTextureData(params.grid, params.orbitals[0]),
+            uOccupancy: 0
+        };
+    },
+    cumulative: {
+        states(params: { grid: CubeGridInfo, orbitals: AlphaOrbital[] }) {
+            return params.orbitals.filter(o => o.occupancy !== 0);
+        },
+        update({ grid }, state: AlphaOrbital, values) {
+            const alpha = getNormalizedAlpha(grid.params.basis, state.alpha, grid.params.sphericalOrder);
+            ValueCell.updateIfChanged(values.uOccupancy, state.occupancy);
+            ValueCell.update(values.tAlpha, { width: alpha.length, height: 1, array: alpha });
+        }
+    }
+});
+
+export function gpuComputeAlphaOrbitalsGridValues(ctx: RuntimeContext, webgl: WebGLContext, grid: CubeGridInfo, orbital: AlphaOrbital) {
+    return Orbitals(ctx, webgl, grid, { grid, orbital });
+}
+
+export function gpuComputeAlphaOrbitalsDensityGridValues(ctx: RuntimeContext, webgl: WebGLContext, grid: CubeGridInfo, orbitals: AlphaOrbital[]) {
+    return Density(ctx, webgl, grid, { grid, orbitals });
+}
 
 function getNormalizedAlpha(basis: Basis, alphaOrbitals: number[], sphericalOrder: SphericalBasisOrder) {
     const alpha = new Float32Array(alphaOrbitals.length);
@@ -62,7 +88,7 @@ function getNormalizedAlpha(basis: Basis, alphaOrbitals: number[], sphericalOrde
     return alpha;
 }
 
-function createTextureData(grid: CubeGridInfo, orbital: AlphaOrbital) {
+function createTextureData(grid: CubeGridInfo, orbital: AlphaOrbital): UnboxedValues<typeof Schema> {
     const { basis, sphericalOrder, cutoffThreshold } = grid.params;
 
     let centerCount = 0;
@@ -131,179 +157,14 @@ function createTextureData(grid: CubeGridInfo, orbital: AlphaOrbital) {
         }
     }
 
-    return { nCenters: centerCount, nAlpha: baseCount, nCoeff: coeffCount, maxCoeffs, centers, info, alpha, coeff };
-}
-
-function createAlphaOrbitalsRenderable(ctx: WebGLContext, grid: CubeGridInfo, orbital: AlphaOrbital): AlphaOrbitalsRenderable {
-    const data = createTextureData(grid, orbital);
-
-    const [nx, ny, nz] = grid.dimensions;
-    const width = Math.ceil(Math.sqrt(nx * ny * nz));
-
-    if (!ctx.namedFramebuffers[AlphaOrbitalsName]) {
-        ctx.namedFramebuffers[AlphaOrbitalsName] = ctx.resources.framebuffer();
-    }
-    if (!ctx.namedTextures[AlphaOrbitalsTex0]) {
-        ctx.namedTextures[AlphaOrbitalsTex0] = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
-    }
-    if (!ctx.namedTextures[AlphaOrbitalsTex1]) {
-        ctx.namedTextures[AlphaOrbitalsTex1] = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
-    }
-
-    const values: AlphaOrbitalsSchema = {
-        ...QuadValues,
-        uDimensions: ValueCell.create(grid.dimensions),
-        uMin: ValueCell.create(grid.box.min),
-        uDelta: ValueCell.create(grid.delta),
-        uWidth: ValueCell.create(width),
-        uNCenters: ValueCell.create(data.nCenters),
-        uNAlpha: ValueCell.create(data.nAlpha),
-        uNCoeff: ValueCell.create(data.nCoeff),
-        uMaxCoeffs: ValueCell.create(data.maxCoeffs),
-        tCenters: ValueCell.create({ width: data.nCenters, height: 1, array: data.centers }),
-        tInfo: ValueCell.create({ width: data.nCenters, height: 1, array: data.info }),
-        tCoeff: ValueCell.create({ width: data.nCoeff, height: 1, array: data.coeff }),
-        tAlpha: ValueCell.create({ width: data.nAlpha, height: 1, array: data.alpha }),
-        uLittleEndian: ValueCell.create(isLittleEndian()),
-        uDensity: ValueCell.create(false),
-        uOccupancy: ValueCell.create(0),
-        tCumulativeSum: ValueCell.create(ctx.namedTextures[AlphaOrbitalsTex1])
+    return {
+        uNCenters: centerCount,
+        uNAlpha: baseCount,
+        uNCoeff: coeffCount,
+        uMaxCoeffs: maxCoeffs,
+        tCenters: { width: centerCount, height: 1, array: centers },
+        tInfo: { width: centerCount, height: 1, array: info },
+        tCoeff: { width: coeffCount, height: 1, array: coeff },
+        tAlpha: { width: baseCount, height: 1, array: alpha },
     };
-
-    const schema = { ...AlphaOrbitalsSchema };
-    if (!ctx.isWebGL2) {
-        // workaround for webgl1 limitation that loop counters need to be `const`
-        (schema.uNCenters as any) = DefineSpec('number');
-        (schema.uMaxCoeffs as any) = DefineSpec('number');
-    }
-
-    const renderItem = createComputeRenderItem(ctx, 'triangles', AlphaOrbitalsShaderCode, schema, values);
-
-    return createComputeRenderable(renderItem, values);
-}
-
-function getAlphaOrbitalsRenderable(ctx: WebGLContext, grid: CubeGridInfo, orbital: AlphaOrbital): AlphaOrbitalsRenderable {
-    if (ctx.namedComputeRenderables[AlphaOrbitalsName]) {
-        const v = ctx.namedComputeRenderables[AlphaOrbitalsName].values as AlphaOrbitalsSchema;
-
-        const data = createTextureData(grid, orbital);
-
-        const [nx, ny, nz] = grid.dimensions;
-        const width = Math.ceil(Math.sqrt(nx * ny * nz));
-
-        ValueCell.update(v.uDimensions, grid.dimensions);
-        ValueCell.update(v.uMin, grid.box.min);
-        ValueCell.update(v.uDelta, grid.delta);
-        ValueCell.updateIfChanged(v.uWidth, width);
-        ValueCell.updateIfChanged(v.uNCenters, data.nCenters);
-        ValueCell.updateIfChanged(v.uNAlpha, data.nAlpha);
-        ValueCell.updateIfChanged(v.uNCoeff, data.nCoeff);
-        ValueCell.updateIfChanged(v.uMaxCoeffs, data.maxCoeffs);
-        ValueCell.update(v.tCenters, { width: data.nCenters, height: 1, array: data.centers });
-        ValueCell.update(v.tInfo, { width: data.nCenters, height: 1, array: data.info });
-        ValueCell.update(v.tCoeff, { width: data.nCoeff, height: 1, array: data.coeff });
-        ValueCell.update(v.tAlpha, { width: data.nAlpha, height: 1, array: data.alpha });
-        ValueCell.updateIfChanged(v.uLittleEndian, isLittleEndian());
-        ValueCell.updateIfChanged(v.uDensity, false);
-        ValueCell.updateIfChanged(v.uOccupancy, 0);
-        ValueCell.updateIfChanged(v.tCumulativeSum, ctx.namedTextures[AlphaOrbitalsTex1]);
-
-        ctx.namedComputeRenderables[AlphaOrbitalsName].update();
-    } else {
-        ctx.namedComputeRenderables[AlphaOrbitalsName] = createAlphaOrbitalsRenderable(ctx, grid, orbital);
-    }
-    return ctx.namedComputeRenderables[AlphaOrbitalsName];
-}
-
-export function gpuComputeAlphaOrbitalsGridValues(webgl: WebGLContext, grid: CubeGridInfo, orbital: AlphaOrbital) {
-    const [nx, ny, nz] = grid.dimensions;
-    const renderable = getAlphaOrbitalsRenderable(webgl, grid, orbital);
-    const width = renderable.values.uWidth.ref.value;
-
-    const framebuffer = webgl.namedFramebuffers[AlphaOrbitalsName];
-    webgl.namedTextures[AlphaOrbitalsTex0].define(width, width);
-    webgl.namedTextures[AlphaOrbitalsTex0].attachFramebuffer(framebuffer, 'color0');
-
-    const { gl, state } = webgl;
-    framebuffer.bind();
-    gl.viewport(0, 0, width, width);
-    gl.scissor(0, 0, width, width);
-    state.disable(gl.SCISSOR_TEST);
-    state.disable(gl.BLEND);
-    state.disable(gl.DEPTH_TEST);
-    state.depthMask(false);
-    renderable.render();
-
-    const array = new Uint8Array(width * width * 4);
-    webgl.readPixels(0, 0, width, width, array);
-    return new Float32Array(array.buffer, array.byteOffset, nx * ny * nz);
-}
-
-export function canComputeAlphaOrbitalsOnGPU(webgl?: WebGLContext) {
-    return !!webgl?.extensions.textureFloat;
-}
-
-export async function gpuComputeAlphaOrbitalsDensityGridValues(webgl: WebGLContext, grid: CubeGridInfo, orbitals: AlphaOrbital[], ctx: RuntimeContext) {
-    await ctx.update({ message: 'Initializing...', isIndeterminate: true });
-
-    const [nx, ny, nz] = grid.dimensions;
-    const renderable = getAlphaOrbitalsRenderable(webgl, grid, orbitals[0]);
-    const width = renderable.values.uWidth.ref.value;
-
-    if (!webgl.namedFramebuffers[AlphaOrbitalsName]) {
-        webgl.namedFramebuffers[AlphaOrbitalsName] = webgl.resources.framebuffer();
-    }
-    const framebuffer = webgl.namedFramebuffers[AlphaOrbitalsName];
-    const tex = [webgl.namedTextures[AlphaOrbitalsTex0], webgl.namedTextures[AlphaOrbitalsTex1]];
-
-    tex[0].define(width, width);
-    tex[1].define(width, width);
-
-    const values = renderable.values as AlphaOrbitalsSchema;
-    const { gl, state } = webgl;
-
-    gl.viewport(0, 0, width, width);
-    gl.scissor(0, 0, width, width);
-    state.disable(gl.SCISSOR_TEST);
-    state.disable(gl.BLEND);
-    state.disable(gl.DEPTH_TEST);
-    state.depthMask(false);
-
-    gl.clearColor(0, 0, 0, 0);
-
-    tex[0].attachFramebuffer(framebuffer, 'color0');
-    gl.clear(gl.COLOR_BUFFER_BIT);
-
-    tex[1].attachFramebuffer(framebuffer, 'color0');
-    gl.clear(gl.COLOR_BUFFER_BIT);
-
-    ValueCell.update(values.uDensity, true);
-
-    const nonZero = orbitals.filter(o => o.occupancy !== 0);
-    await ctx.update({ message: 'Computing...', isIndeterminate: false, current: 0, max: nonZero.length });
-    for (let i = 0; i < nonZero.length; i++) {
-        const alpha = getNormalizedAlpha(grid.params.basis, nonZero[i].alpha, grid.params.sphericalOrder);
-
-        ValueCell.update(values.uOccupancy, nonZero[i].occupancy);
-        ValueCell.update(values.tCumulativeSum, tex[(i + 1) % 2]);
-        ValueCell.update(values.tAlpha, { width: alpha.length, height: 1, array: alpha });
-        tex[i % 2].attachFramebuffer(framebuffer, 'color0');
-        gl.viewport(0, 0, width, width);
-        gl.scissor(0, 0, width, width);
-        state.disable(gl.SCISSOR_TEST);
-        state.disable(gl.BLEND);
-        state.disable(gl.DEPTH_TEST);
-        state.depthMask(false);
-        renderable.update();
-        renderable.render();
-
-        if (i !== nonZero.length - 1 && ctx.shouldUpdate) {
-            await ctx.update({ current: i + 1 });
-        }
-    }
-
-    const array = new Uint8Array(width * width * 4);
-    webgl.readPixels(0, 0, width, width, array);
-
-    return new Float32Array(array.buffer, array.byteOffset, nx * ny * nz);
 }

+ 7 - 193
src/extensions/alpha-orbitals/gpu/shader.frag.ts

@@ -4,165 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-export default `
-precision highp float;
-precision highp int;
-precision highp sampler2D;
-
-uniform vec2 uQuadShift;
-uniform vec3 uDimensions;
-uniform vec3 uMin;
-uniform vec3 uDelta;
-
-uniform sampler2D tCenters;
-uniform sampler2D tInfo;
-uniform sampler2D tCoeff;
-uniform sampler2D tAlpha;
-
-uniform float uWidth;
-
-#ifndef uNCenters
-    uniform int uNCenters;
-#endif
-
-uniform int uNCoeff;
-uniform int uNAlpha;
-
-uniform bool uDensity;
-uniform float uOccupancy;
-uniform sampler2D tCumulativeSum;
-
-uniform bool uLittleEndian;
-
-//////////////////////////////////////////////////////////
-
-// floatToRgba adapted from https://github.com/equinor/glsl-float-to-rgba
-// MIT License, Copyright (c) 2020 Equinor
-
-float shiftRight (float v, float amt) {
-  v = floor(v) + 0.5;
-  return floor(v / exp2(amt));
-}
-float shiftLeft (float v, float amt) {
-    return floor(v * exp2(amt) + 0.5);
-}
-float maskLast (float v, float bits) {
-    return mod(v, shiftLeft(1.0, bits));
-}
-float extractBits (float num, float from, float to) {
-    from = floor(from + 0.5); to = floor(to + 0.5);
-    return maskLast(shiftRight(num, from), to - from);
-}
-
-vec4 floatToRgba(float texelFloat) {
-    if (texelFloat == 0.0) return vec4(0, 0, 0, 0);
-    float sign = texelFloat > 0.0 ? 0.0 : 1.0;
-    texelFloat = abs(texelFloat);
-    float exponent = floor(log2(texelFloat));
-    float biased_exponent = exponent + 127.0;
-    float fraction = ((texelFloat / exp2(exponent)) - 1.0) * 8388608.0;
-    float t = biased_exponent / 2.0;
-    float last_bit_of_biased_exponent = fract(t) * 2.0;
-    float remaining_bits_of_biased_exponent = floor(t);
-    float byte4 = extractBits(fraction, 0.0, 8.0) / 255.0;
-    float byte3 = extractBits(fraction, 8.0, 16.0) / 255.0;
-    float byte2 = (last_bit_of_biased_exponent * 128.0 + extractBits(fraction, 16.0, 23.0)) / 255.0;
-    float byte1 = (sign * 128.0 + remaining_bits_of_biased_exponent) / 255.0;
-    return (
-        uLittleEndian
-            ? vec4(byte4, byte3, byte2, byte1)
-            : vec4(byte1, byte2, byte3, byte4)
-    );
-}
-
-///////////////////////////////////////////////////////
-
-// rgbaToFloat adapted from https://github.com/ihmeuw/glsl-rgba-to-float
-// BSD 3-Clause License
-//
-// Copyright (c) 2019, Institute for Health Metrics and Evaluation All rights reserved.
-// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-//  - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-//  - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
-//  - Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
-// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
-// IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
-// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-// OF THE POSSIBILITY OF SUCH DAMAGE.
-
-ivec4 floatsToBytes(vec4 inputFloats) {
-  ivec4 bytes = ivec4(inputFloats * 255.0);
-  return (
-    uLittleEndian
-    ? bytes.abgr
-    : bytes
-  );
-}
-
-// Break the four bytes down into an array of 32 bits.
-void bytesToBits(const in ivec4 bytes, out bool bits[32]) {
-  for (int channelIndex = 0; channelIndex < 4; ++channelIndex) {
-    float acc = float(bytes[channelIndex]);
-    for (int indexInByte = 7; indexInByte >= 0; --indexInByte) {
-      float powerOfTwo = exp2(float(indexInByte));
-      bool bit = acc >= powerOfTwo;
-      bits[channelIndex * 8 + (7 - indexInByte)] = bit;
-      acc = mod(acc, powerOfTwo);
-    }
-  }
-}
-
-// Compute the exponent of the 32-bit float.
-float getExponent(bool bits[32]) {
-  const int startIndex = 1;
-  const int bitStringLength = 8;
-  const int endBeforeIndex = startIndex + bitStringLength;
-  float acc = 0.0;
-  int pow2 = bitStringLength - 1;
-  for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {
-    acc += float(bits[bitIndex]) * exp2(float(pow2--));
-  }
-  return acc;
-}
-
-// Compute the mantissa of the 32-bit float.
-float getMantissa(bool bits[32], bool subnormal) {
-  const int startIndex = 9;
-  const int bitStringLength = 23;
-  const int endBeforeIndex = startIndex + bitStringLength;
-  // Leading/implicit/hidden bit convention:
-  // If the number is not subnormal (with exponent 0), we add a leading 1 digit.
-  float acc = float(!subnormal) * exp2(float(bitStringLength));
-  int pow2 = bitStringLength - 1;
-  for (int bitIndex = startIndex; bitIndex < endBeforeIndex; ++bitIndex) {
-    acc += float(bits[bitIndex]) * exp2(float(pow2--));
-  }
-  return acc;
-}
-
-// Parse the float from its 32 bits.
-float bitsToFloat(bool bits[32]) {
-  float signBit = float(bits[0]) * -2.0 + 1.0;
-  float exponent = getExponent(bits);
-  bool subnormal = abs(exponent - 0.0) < 0.01;
-  float mantissa = getMantissa(bits, subnormal);
-  float exponentBias = 127.0;
-  return signBit * mantissa * exp2(exponent - exponentBias - 23.0);
-}
-
-float rgbaToFloat(vec4 texelRGBA) {
-  ivec4 rgbaBytes = floatsToBytes(texelRGBA);
-  bool bits[32];
-  bytesToBits(rgbaBytes, bits);
-  return bitsToFloat(bits);
-}
-
-///////////////////////////////////////////////////////
-
+export const UTILS = `
 float L1(vec3 p, float a0, float a1, float a2) {
     return a0 * p.z + a1 * p.x + a2 * p.y;
 }
@@ -213,12 +55,10 @@ float L4(vec3 p, float a0, float a1, float a2, float a3, float a4, float a5, flo
 }
 
 float alpha(float offset, float f) {
-    #ifdef uMaxCoeffs
+    #ifdef WEBGL1
         // in webgl1, the value is in the alpha channel!
         return texture2D(tAlpha, vec2(offset * f, 0.5)).a;
-    #endif
-
-    #ifndef uMaxCoeffs
+    #else
         return texture2D(tAlpha, vec2(offset * f, 0.5)).x;
     #endif
 }
@@ -249,7 +89,7 @@ float Y(int L, vec3 X, float aO, float fA) {
     return 0.0;
 }
 
-#ifndef uMaxCoeffs
+#ifndef WEBGL1
     float R(float R2, int start, int end, float fCoeff) {
         float gauss = 0.0;
         for (int i = start; i < end; i++) {
@@ -258,9 +98,7 @@ float Y(int L, vec3 X, float aO, float fA) {
         }
         return gauss;
     }
-#endif
-
-#ifdef uMaxCoeffs
+#else
     float R(float R2, int start, int end, float fCoeff) {
         float gauss = 0.0;
         int o = start;
@@ -274,28 +112,13 @@ float Y(int L, vec3 X, float aO, float fA) {
         return gauss;
     }
 #endif
+`;
 
-float intDiv(float a, float b) { return float(int(a) / int(b)); }
-float intMod(float a, float b) { return a - b * float(int(a) / int(b)); }
-
-void main(void) {
-    float offset = floor(gl_FragCoord.x) + floor(gl_FragCoord.y) * uWidth;
-
-    // axis order fast to slow Z, Y, X
-    // TODO: support arbitrary axis orders?
-    float k = intMod(offset, uDimensions.z), kk = intDiv(offset, uDimensions.z);
-    float j = intMod(kk, uDimensions.y);
-    float i = intDiv(kk, uDimensions.y);
-
-    vec3 xyz = uMin + uDelta * vec3(i, j, k);
-
+export const MAIN = `
     float fCenter = 1.0 / float(uNCenters - 1);
     float fCoeff = 1.0 / float(uNCoeff - 1);
     float fA = 1.0 / float(uNAlpha - 1);
 
-    // gl_FragColor = floatToRgba(offset);
-    // return;
-
     float v = 0.0;
 
     for (int i = 0; i < uNCenters; i++) {
@@ -319,13 +142,4 @@ void main(void) {
 
         v += R(R2, coeffStart, coeffEnd, fCoeff) * Y(L, X, aO, fA);
     }
-
-    
-    if (uDensity) {
-        float current = rgbaToFloat(texture2D(tCumulativeSum, gl_FragCoord.xy / vec2(uWidth, uWidth)));
-        gl_FragColor = floatToRgba(current + uOccupancy * v * v);
-    } else {
-         gl_FragColor = floatToRgba(v);
-    }
-}
 `;

+ 6 - 3
src/extensions/alpha-orbitals/orbitals.ts

@@ -7,11 +7,14 @@
  */
 
 import { sortArray } from '../../mol-data/util';
+import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { Task } from '../../mol-task';
 import { sphericalCollocation } from './collocation';
 import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
-import { canComputeAlphaOrbitalsOnGPU, gpuComputeAlphaOrbitalsGridValues } from './gpu/compute';
+import { gpuComputeAlphaOrbitalsGridValues } from './gpu/compute';
+
+// setDebugMode(true);
 
 export function createSphericalCollocationGrid(
     params: CubeGridComputationParams, orbital: AlphaOrbital, webgl?: WebGLContext
@@ -20,9 +23,9 @@ export function createSphericalCollocationGrid(
         const cubeGrid = initCubeGrid(params);
 
         let matrix: Float32Array;
-        if (canComputeAlphaOrbitalsOnGPU(webgl)) {
+        if (canComputeGrid3dOnGPU(webgl)) {
             // console.time('gpu');
-            matrix = gpuComputeAlphaOrbitalsGridValues(webgl!, cubeGrid, orbital);
+            matrix = await gpuComputeAlphaOrbitalsGridValues(ctx, webgl!, cubeGrid, orbital);
             // console.timeEnd('gpu');
         } else {
             // console.time('cpu');

+ 223 - 0
src/mol-gl/compute/grid3d.ts

@@ -0,0 +1,223 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { RenderableSchema, Values, UnboxedValues, UniformSpec, TextureSpec, DefineSpec, RenderableValues } from '../renderable/schema';
+import { WebGLContext } from '../webgl/context';
+import { getRegularGrid3dDelta, RegularGrid3d } from '../../mol-math/geometry/common';
+import shader_template from '../shader/util/grid3d-template.frag';
+import quad_vert from '../shader/quad.vert';
+import { ShaderCode } from '../shader-code';
+import { UUID, ValueCell } from '../../mol-util';
+import { objectForEach } from '../../mol-util/object';
+import { getUniformGlslType, isUniformValueScalar } from '../webgl/uniform';
+import { QuadSchema, QuadValues } from './util';
+import { createComputeRenderItem } from '../webgl/render-item';
+import { createComputeRenderable } from '../renderable';
+import { isLittleEndian } from '../../mol-util/is-little-endian';
+import { RuntimeContext } from '../../mol-task';
+
+export function canComputeGrid3dOnGPU(webgl?: WebGLContext) {
+    return !!webgl?.extensions.textureFloat;
+}
+
+export interface Grid3DComputeRenderableSpec<S extends RenderableSchema, P, CS> {
+    schema: S,
+    // indicate which params are loop bounds for WebGL1 compat
+    loopBounds?: (keyof S)[]
+    utilCode?: string,
+    mainCode: string,
+    returnCode: string,
+
+    values(params: P, grid: RegularGrid3d): UnboxedValues<S>,
+
+    cumulative?: {
+        states(params: P): CS[],
+        update(params: P, state: CS, values: Values<S>): void,
+        // call gl.readPixes every 'yieldPeriod' states to split the computation
+        // into multiple parts, if not set, the computation will be synchronous
+        yieldPeriod?: number
+    }
+}
+
+const FrameBufferName = 'grid3d-computable' as const;
+const Texture0Name = 'grid3d-computable-0' as const;
+const Texture1Name = 'grid3d-computable-1' as const;
+
+const SchemaBase = {
+    ...QuadSchema,
+    uDimensions: UniformSpec('v3'),
+    uMin: UniformSpec('v3'),
+    uDelta: UniformSpec('v3'),
+    uWidth: UniformSpec('f'),
+    uLittleEndian: UniformSpec('b'),
+};
+
+const CumulativeSumSchema = {
+    tCumulativeSum: TextureSpec('texture', 'rgba', 'ubyte', 'nearest')
+};
+
+export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>(spec: Grid3DComputeRenderableSpec<S, P, CS>) {
+    const id = UUID.create22();
+
+    const uniforms: string[] = [];
+
+    objectForEach(spec.schema, (u, k) => {
+        if (u.type === 'define') return;
+        if (u.kind.indexOf('[]') >= 0) throw new Error('array uniforms are not supported');
+        const isBound = (spec.loopBounds?.indexOf(k) ?? -1) >= 0;
+        if (isBound) uniforms.push(`#ifndef ${k}`);
+        if (u.type === 'uniform') uniforms.push(`uniform ${getUniformGlslType(u.kind as any)} ${k};`);
+        else if (u.type === 'texture') uniforms.push(`uniform sampler2D ${k};`);
+        if (isBound) uniforms.push(`#endif`);
+    });
+
+    const code = shader_template
+        .replace('{UNIFORMS}', uniforms.join('\n'))
+        .replace('{UTILS}', spec.utilCode ?? '')
+        .replace('{MAIN}', spec.mainCode)
+        .replace('{RETURN}', spec.returnCode);
+
+    const shader = ShaderCode(id, quad_vert, code);
+
+    return async (ctx: RuntimeContext, webgl: WebGLContext, grid: RegularGrid3d, params: P) => {
+        const schema: RenderableSchema = {
+            ...SchemaBase,
+            ...(spec.cumulative ? CumulativeSumSchema : {}),
+            ...spec.schema,
+        };
+
+        if (!webgl.isWebGL2) {
+            if (spec.loopBounds) {
+                for (const b of spec.loopBounds) {
+                    (schema as any)[b] = DefineSpec('number');
+                }
+            }
+            (schema as any)['WEBGL1'] = DefineSpec('boolean');
+        }
+
+        if (spec.cumulative) {
+            (schema as any)['CUMULATIVE'] = DefineSpec('boolean');
+        }
+
+        if (!webgl.namedFramebuffers[FrameBufferName]) {
+            webgl.namedFramebuffers[FrameBufferName] = webgl.resources.framebuffer();
+        }
+        const framebuffer = webgl.namedFramebuffers[FrameBufferName];
+
+        if (!webgl.namedTextures[Texture0Name]) {
+            webgl.namedTextures[Texture0Name] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        }
+        if (spec.cumulative && !webgl.namedTextures[Texture1Name]) {
+            webgl.namedTextures[Texture1Name] = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        }
+
+        const tex = [webgl.namedTextures[Texture0Name], webgl.namedTextures[Texture1Name]];
+
+        const [nx, ny, nz] = grid.dimensions;
+        const uWidth = Math.ceil(Math.sqrt(nx * ny * nz));
+
+        const values: UnboxedValues<S & typeof SchemaBase> = {
+            uDimensions: grid.dimensions,
+            uMin: grid.box.min,
+            uDelta: getRegularGrid3dDelta(grid),
+            uWidth,
+            uLittleEndian: isLittleEndian(),
+            ...spec.values(params, grid)
+        } as any;
+
+        if (!webgl.isWebGL2) {
+            (values as any).WEBGL1 = true;
+        }
+        if (spec.cumulative) {
+            (values as any).tCumulativeSum = tex[0];
+            (values as any).CUMULATIVE = true;
+        }
+
+        let renderable = webgl.namedComputeRenderables[id];
+        let cells: RenderableValues;
+        if (renderable) {
+            cells = renderable.values as RenderableValues;
+            objectForEach(values, (c, k) => {
+                const s = schema[k];
+                if (s?.type === 'value' || s?.type === 'attribute') return;
+
+                if (!s || !isUniformValueScalar(s.kind as any)) {
+                    ValueCell.update(cells[k], c);
+                } else {
+                    ValueCell.updateIfChanged(cells[k], c);
+                }
+            });
+        } else {
+            cells = {} as any;
+            objectForEach(QuadValues, (v, k) => (cells as any)[k] = v);
+            objectForEach(values, (v, k) => (cells as any)[k] = ValueCell.create(v));
+            renderable = createComputeRenderable(createComputeRenderItem(webgl, 'triangles', shader, schema, cells), cells);
+        }
+
+        const array = new Uint8Array(uWidth * uWidth * 4);
+        if (spec.cumulative) {
+            const { gl } = webgl;
+
+            const states = spec.cumulative.states(params);
+
+            tex[0].define(uWidth, uWidth);
+            tex[1].define(uWidth, uWidth);
+
+            resetGl(webgl, uWidth);
+            gl.clearColor(0, 0, 0, 0);
+
+            tex[0].attachFramebuffer(framebuffer, 'color0');
+            gl.clear(gl.COLOR_BUFFER_BIT);
+
+            tex[1].attachFramebuffer(framebuffer, 'color0');
+            gl.clear(gl.COLOR_BUFFER_BIT);
+
+            if (spec.cumulative.yieldPeriod) {
+                await ctx.update({ message: 'Computing...', isIndeterminate: false, current: 0, max: states.length });
+            }
+
+            const yieldPeriod = Math.max(1, spec.cumulative.yieldPeriod ?? 1 | 0);
+
+            for (let i = 0; i < states.length; i++) {
+                ValueCell.update(cells.tCumulativeSum, tex[(i + 1) % 2]);
+                tex[i % 2].attachFramebuffer(framebuffer, 'color0');
+                resetGl(webgl, uWidth);
+                spec.cumulative.update(params, states[i], cells as any);
+                renderable.update();
+                renderable.render();
+
+                if (spec.cumulative.yieldPeriod && i !== states.length - 1) {
+                    if (i % yieldPeriod === yieldPeriod - 1) {
+                        webgl.readPixels(0, 0, 1, 1, array);
+                    }
+                    if (ctx.shouldUpdate) {
+                        await ctx.update({ current: i + 1 });
+                    }
+                }
+            }
+        } else {
+            tex[0].define(uWidth, uWidth);
+            tex[0].attachFramebuffer(framebuffer, 'color0');
+            framebuffer.bind();
+            resetGl(webgl, uWidth);
+            renderable.update();
+            renderable.render();
+        }
+
+        webgl.readPixels(0, 0, uWidth, uWidth, array);
+        return new Float32Array(array.buffer, array.byteOffset, nx * ny * nz);
+    };
+}
+
+function resetGl(webgl: WebGLContext, w: number) {
+    const { gl, state } = webgl;
+    gl.viewport(0, 0, w, w);
+    gl.scissor(0, 0, w, w);
+    state.disable(gl.SCISSOR_TEST);
+    state.disable(gl.BLEND);
+    state.disable(gl.DEPTH_TEST);
+    state.depthMask(false);
+}

+ 2 - 0
src/mol-gl/renderable/schema.ts

@@ -29,6 +29,7 @@ export type ValueKind = keyof ValueKindType
 export type KindValue = UniformKindValue & DataTypeArrayType & TextureKindValue & ValueKindType
 
 export type Values<S extends RenderableSchema> = { readonly [k in keyof S]: ValueCell<KindValue[S[k]['kind']]> }
+export type UnboxedValues<S extends RenderableSchema> = { readonly [k in keyof S]: KindValue[S[k]['kind']] }
 
 export function splitValues(schema: RenderableSchema, values: RenderableValues) {
     const attributeValues: AttributeValues = {};
@@ -102,6 +103,7 @@ export type RenderableSchema = {
 }
 export type RenderableValues = { readonly [k: string]: ValueCell<any> }
 
+
 //
 
 export const GlobalUniformSchema = {

+ 181 - 0
src/mol-gl/shader/util/grid3d-template.frag.ts

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

+ 26 - 0
src/mol-gl/webgl/uniform.ts

@@ -79,4 +79,30 @@ export function getUniformSetters(schema: RenderableSchema) {
         }
     });
     return setters;
+}
+
+export function getUniformGlslType(kind: UniformKind): string {
+    switch (kind) {
+        case 'f': return 'float';
+        case 'i': return 'int';
+        case 't': return 'sampler2D';
+        case 'b': return 'bool';
+        case 'v2': return 'vec2';
+        case 'v3': return 'vec3';
+        case 'v4': return 'vec4';
+        case 'm3': return 'mat3';
+        case 'm4': return 'mat4';
+    }
+    throw new Error(`${kind} has no primitive GLSL type.`);
+}
+
+export function isUniformValueScalar(kind: UniformKind): boolean {
+    switch (kind) {
+        case 'f':
+        case 'i':
+        case 'b':
+            return true;
+        default:
+            return false;
+    }
 }

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

@@ -37,6 +37,15 @@ export type DensityTextureData = {
     gridTexScale: Vec2
 }
 
+export interface RegularGrid3d {
+    box: Box3D,
+    dimensions: Vec3
+}
+
+export function getRegularGrid3dDelta({ box, dimensions }: RegularGrid3d) {
+    return Vec3.div(Vec3(), Box3D.size(Vec3(), box), Vec3.subScalar(Vec3(), dimensions, 1));
+}
+
 export function fillGridDim(length: number, start: number, step: number) {
     const a = new Float32Array(length);
     for (let i = 0; i < a.length; i++) {

+ 1 - 0
src/mol-plugin-ui/controls/parameters.tsx

@@ -186,6 +186,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         case 'file-list': return FileListControl;
         case 'select': return SelectControl;
         case 'value-ref': return ValueRefControl;
+        case 'data-ref': return void 0;
         case 'text': return TextControl;
         case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
             ? BoundedIntervalControl : IntervalControl;

+ 9 - 3
src/mol-state/state.ts

@@ -328,6 +328,8 @@ class State {
         const oldTree = this._tree;
         this._tree = _tree;
 
+        const cells = this.cells;
+
         const ctx: UpdateContext = {
             parent: this,
             editInfo: StateBuilder.is(tree) ? tree.editInfo : void 0,
@@ -346,7 +348,9 @@ class State {
             changed: false,
             hadError: false,
             wasAborted: false,
-            newCurrent: void 0
+            newCurrent: void 0,
+
+            getCellData: ref => cells.get(ref)!.obj?.data
         };
 
         this.errorFree = true;
@@ -454,7 +458,9 @@ interface UpdateContext {
     changed: boolean,
     hadError: boolean,
     wasAborted: boolean,
-    newCurrent?: Ref
+    newCurrent?: Ref,
+
+    getCellData: (ref: string) => any
 }
 
 async function update(ctx: UpdateContext) {
@@ -841,7 +847,7 @@ function resolveParams(ctx: UpdateContext, transform: StateTransform, src: State
     (transform.params as any) = transform.params
         ? assignIfUndefined(transform.params, defaultValues)
         : defaultValues;
-    ParamDefinition.resolveValueRefs(definition, transform.params);
+    ParamDefinition.resolveRefs(definition, transform.params, ctx.getCellData);
     return { definition, values: transform.params };
 }
 

+ 19 - 8
src/mol-util/param-definition.ts

@@ -296,6 +296,13 @@ export namespace ParamDefinition {
         return setInfo<ValueRef<T>>({ type: 'value-ref', defaultValue: { ref: info?.defaultRef ?? '', getValue: unsetGetValue as any }, getOptions, resolveRef }, info);
     }
 
+    export interface DataRef<T = any> extends Base<{ ref: string, getValue: () => T }> {
+        type: 'data-ref'
+    }
+    export function DataRef<T>(info?: Info & { defaultRef?: string }) {
+        return setInfo<DataRef<T>>({ type: 'data-ref', defaultValue: { ref: info?.defaultRef ?? '', getValue: unsetGetValue as any } }, info);
+    }
+
     export interface Converted<T, C> extends Base<T> {
         type: 'converted',
         converted: Any,
@@ -329,7 +336,7 @@ export namespace ParamDefinition {
 
     export type Any =
         | Value<any> | Select<any> | MultiSelect<any> | BooleanParam | Text | Color | Vec3 | Mat4 | Numeric | FileParam | UrlParam | FileListParam | Interval | LineGraph
-        | ColorList | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> | Script | ObjectList | ValueRef
+        | ColorList | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> | Script | ObjectList | ValueRef | DataRef
 
     export type Params = { [k: string]: Any }
     export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] }
@@ -360,29 +367,33 @@ export namespace ParamDefinition {
         return () => resolve(ref);
     }
 
-    function resolveRefValue(p: Any, value: any) {
+    function resolveRefValue(p: Any, value: any, getData: (ref: string) => any) {
         if (!value) return;
 
         if (p.type === 'value-ref') {
             const v = value as ValueRef['defaultValue'];
             if (!v.ref) v.getValue = () => { throw new Error('Unset ref in ValueRef value.'); };
             else v.getValue = _resolveRef(p.resolveRef, v.ref);
+        } else if (p.type === 'data-ref') {
+            const v = value as ValueRef['defaultValue'];
+            if (!v.ref) v.getValue = () => { throw new Error('Unset ref in ValueRef value.'); };
+            else v.getValue = _resolveRef(getData, v.ref);
         } else if (p.type === 'group') {
-            resolveValueRefs(p.params, value);
+            resolveRefs(p.params, value, getData);
         } else if (p.type === 'mapped') {
             const v = value as NamedParams;
             const param = p.map(v.name);
-            resolveRefValue(param, v.params);
+            resolveRefValue(param, v.params, getData);
         } else if (p.type === 'object-list') {
             if (!hasValueRef(p.element)) return;
             for (const e of value) {
-                resolveValueRefs(p.element, e);
+                resolveRefs(p.element, e, getData);
             }
         }
     }
 
     function hasParamValueRef(p: Any) {
-        if (p.type === 'value-ref') {
+        if (p.type === 'value-ref' || p.type === 'data-ref') {
             return true;
         } else if (p.type === 'group') {
             if (hasValueRef(p.params)) return true;
@@ -403,9 +414,9 @@ export namespace ParamDefinition {
         return false;
     }
 
-    export function resolveValueRefs(params: Params, values: any) {
+    export function resolveRefs(params: Params, values: any, getData: (ref: string) => any) {
         for (const n of Object.keys(params)) {
-            resolveRefValue(params[n], values?.[n]);
+            resolveRefValue(params[n], values?.[n], getData);
         }
     }