Browse Source

Merge pull request #604 from molstar/volume-theme

by-volume-value theme
David Sehnal 2 years ago
parent
commit
920b98e4d1

+ 1 - 0
CHANGELOG.md

@@ -8,6 +8,7 @@ Note that since we don't clearly distinguish between a public and private interf
 
 - Excluded common protein caps `NME` and `ACE` from the ligand selection query
 - Add screen-space shadow post-processing effect
+- Add `external-volume` theme (coloring of arbitrary geometries by user-selected volume)
 
 ## [v3.25.1] - 2022-11-20
 

+ 41 - 4
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -7,7 +7,7 @@
 import { ValueCell } from '../../../mol-util';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+import { LocationIterator, PositionLocation } from '../../../mol-geo/util/location-iterator';
 import { TransformData } from '../transform-data';
 import { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
@@ -20,11 +20,12 @@ import { createEmptyTransparency } from '../transparency-data';
 import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
 import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { createNullTexture, Texture } from '../../../mol-gl/webgl/texture';
-import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
+import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { createEmptyClipping } from '../clipping-data';
 import { NullLocation } from '../../../mol-model/location';
 import { createEmptySubstance } from '../substance-data';
 import { RenderableState } from '../../../mol-gl/renderable';
+import { WebGLContext } from '../../../mol-gl/webgl/context';
 
 export interface TextureMesh {
     readonly kind: 'texture-mesh',
@@ -43,7 +44,10 @@ export interface TextureMesh {
 
     readonly boundingSphere: Sphere3D
 
-    readonly meta: { [k: string]: unknown }
+    readonly meta: {
+        webgl?: WebGLContext
+        [k: string]: unknown
+    }
 }
 
 export namespace TextureMesh {
@@ -131,9 +135,42 @@ export namespace TextureMesh {
         updateBoundingSphere,
         createRenderableState,
         updateRenderableState,
-        createPositionIterator: () => LocationIterator(1, 1, 1, () => NullLocation)
+        createPositionIterator,
     };
 
+    const TextureMeshName = 'texture-mesh';
+
+    function createPositionIterator(textureMesh: TextureMesh, transform: TransformData): LocationIterator {
+        const webgl = textureMesh.meta.webgl;
+        if (!webgl) return LocationIterator(1, 1, 1, () => NullLocation);
+
+        if (!webgl.namedFramebuffers[TextureMeshName]) {
+            webgl.namedFramebuffers[TextureMeshName] = webgl.resources.framebuffer();
+        }
+        const framebuffer = webgl.namedFramebuffers[TextureMeshName];
+        const [width, height] = textureMesh.geoTextureDim.ref.value;
+        const vertices = new Float32Array(width * height * 4);
+        framebuffer.bind();
+        textureMesh.vertexTexture.ref.value.attachFramebuffer(framebuffer, 0);
+        webgl.readPixels(0, 0, width, height, vertices);
+
+        const groupCount = textureMesh.vertexCount;
+        const instanceCount = transform.instanceCount.ref.value;
+        const location = PositionLocation();
+        const p = location.position;
+        const v = vertices;
+        const m = transform.aTransform.ref.value;
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            if (instanceIndex < 0) {
+                Vec3.fromArray(p, v, groupIndex * 4);
+            } else {
+                Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 4, instanceIndex * 16);
+            }
+            return location;
+        };
+        return LocationIterator(groupCount, instanceCount, 1, getLocation);
+    }
+
     function createValues(textureMesh: TextureMesh, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextureMeshValues {
         const { instanceCount, groupCount } = locationIt;
         const positionIt = Utils.createPositionIterator(textureMesh, transform);

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

@@ -270,16 +270,16 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
         #elif defined(dColorType_groupInstance)
             material.rgb = readFromTexture(tColor, vInstance * float(uGroupCount) + group, uColorTexDim).rgb;
         #elif defined(dColorType_vertex)
-            material.rgb = texture3dFrom1dTrilinear(tColor, isoPos, uGridDim, uColorTexDim, 0.0).rgb;
+            material.rgb = texture3dFrom1dTrilinear(tColor, unitPos, uGridDim, uColorTexDim, 0.0).rgb;
         #elif defined(dColorType_vertexInstance)
-            material.rgb = texture3dFrom1dTrilinear(tColor, isoPos, uGridDim, uColorTexDim, vInstance * float(uVertexCount)).rgb;
+            material.rgb = texture3dFrom1dTrilinear(tColor, unitPos, uGridDim, uColorTexDim, vInstance * float(uVertexCount)).rgb;
         #endif
 
         #ifdef dOverpaint
             #if defined(dOverpaintType_groupInstance)
                 overpaint = readFromTexture(tOverpaint, vInstance * float(uGroupCount) + group, uOverpaintTexDim);
             #elif defined(dOverpaintType_vertexInstance)
-                overpaint = texture3dFrom1dTrilinear(tOverpaint, isoPos, uGridDim, uOverpaintTexDim, vInstance * float(uVertexCount));
+                overpaint = texture3dFrom1dTrilinear(tOverpaint, unitPos, uGridDim, uOverpaintTexDim, vInstance * float(uVertexCount));
             #endif
 
             material.rgb = mix(material.rgb, overpaint.rgb, overpaint.a);

+ 7 - 4
src/mol-plugin-ui/controls/parameters.tsx

@@ -18,7 +18,7 @@ import { getPrecision } from '../../mol-util/number';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParamMapping } from '../../mol-util/param-mapping';
 import { camelCaseToWords } from '../../mol-util/string';
-import { PluginUIComponent } from '../base';
+import { PluginReactContext, PluginUIComponent } from '../base';
 import { PluginUIContext } from '../context';
 import { ActionMenu } from './action-menu';
 import { ColorOptions, ColorValueOption, CombinedColorControl } from './color';
@@ -505,10 +505,12 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<
 
     toggle = () => this.setState({ showOptions: !this.state.showOptions });
 
-    items = memoizeLatest((param: PD.ValueRef) => ActionMenu.createItemsFromSelectOptions(param.getOptions()));
+    private get items() {
+        return ActionMenu.createItemsFromSelectOptions(this.props.param.getOptions(this.context));
+    }
 
     renderControl() {
-        const items = this.items(this.props.param);
+        const items = this.items;
         const current = this.props.value.ref ? ActionMenu.findItem(items, this.props.value.ref) : void 0;
         const label = current
             ? current.label
@@ -521,7 +523,7 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<
     renderAddOn() {
         if (!this.state.showOptions) return null;
 
-        const items = this.items(this.props.param);
+        const items = this.items;
         const current = ActionMenu.findItem(items, this.props.value.ref);
 
         return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
@@ -539,6 +541,7 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<
         });
     }
 }
+ValueRefControl.contextType = PluginReactContext;
 
 export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> {
     state = { isExpanded: false };

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

@@ -247,6 +247,7 @@ async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit,
     const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, densityTextureData.maxRadius);
     const surface = TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
     (surface.meta as GaussianSurfaceMeta).resolution = densityTextureData.resolution;
+    surface.meta.webgl = ctx.webgl;
 
     return surface;
 }
@@ -321,6 +322,7 @@ async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, str
     const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, densityTextureData.maxRadius);
     const surface = TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
     (surface.meta as GaussianSurfaceMeta).resolution = densityTextureData.resolution;
+    surface.meta.webgl = ctx.webgl;
 
     return surface;
 }

+ 1 - 0
src/mol-repr/volume/isosurface.ts

@@ -201,6 +201,7 @@ async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Vol
 
     const groupCount = volume.grid.cells.data.length;
     const surface = TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, Volume.getBoundingSphere(volume), textureMesh);
+    surface.meta.webgl = ctx.webgl;
 
     return surface;
 }

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

@@ -40,6 +40,7 @@ import { VolumeValueColorThemeProvider } from './color/volume-value';
 import { Vec3, Vec4 } from '../mol-math/linear-algebra';
 import { ModelIndexColorThemeProvider } from './color/model-index';
 import { StructureIndexColorThemeProvider } from './color/structure-index';
+import { ExternalVolumeColorThemeProvider } from './color/external-volume';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
@@ -152,6 +153,7 @@ namespace ColorTheme {
         'unit-index': UnitIndexColorThemeProvider,
         'uniform': UniformColorThemeProvider,
         'volume-value': VolumeValueColorThemeProvider,
+        'external-volume': ExternalVolumeColorThemeProvider,
     };
     type _BuiltIn = typeof BuiltIn
     export type BuiltIn = keyof _BuiltIn

+ 159 - 0
src/mol-theme/color/external-volume.ts

@@ -0,0 +1,159 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Color, ColorScale } from '../../mol-util/color';
+import { Location } from '../../mol-model/location';
+import { ColorTheme } from '../color';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ThemeDataContext } from '../theme';
+import { Grid, Volume } from '../../mol-model/volume';
+import { type PluginContext } from '../../mol-plugin/context';
+import { isPositionLocation } from '../../mol-geo/util/location-iterator';
+import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
+import { lerp } from '../../mol-math/interpolate';
+
+const Description = `Assigns a color based volume value at a given vertex.`;
+
+export const ExternalVolumeColorThemeParams = {
+    volume: PD.ValueRef<Volume>(
+        (ctx: PluginContext) => {
+            const volumes = ctx.state.data.selectQ(q => q.root.subtree().filter(c => Volume.is(c.obj?.data)));
+            return volumes.map(v => [v.transform.ref, v.obj?.label ?? '<unknown>'] as [string, string]);
+        },
+        (ref, getData) => getData(ref),
+    ),
+    coloring: PD.MappedStatic('absolute-value', {
+        'absolute-value': PD.Group({
+            domain: PD.MappedStatic('auto', {
+                custom: PD.Interval([-1, 1]),
+                auto: PD.Group({
+                    symmetric: PD.Boolean(false, { description: 'If true the automatic range is determined as [-|max|, |max|].' })
+                })
+            }),
+            list: PD.ColorList('red-white-blue', { presetKind: 'scale' })
+        }),
+        'relative-value': PD.Group({
+            domain: PD.MappedStatic('auto', {
+                custom: PD.Interval([-1, 1]),
+                auto: PD.Group({
+                    symmetric: PD.Boolean(false, { description: 'If true the automatic range is determined as [-|max|, |max|].' })
+                })
+            }),
+            list: PD.ColorList('red-white-blue', { presetKind: 'scale' })
+        })
+    }),
+    defaultColor: PD.Color(Color(0xcccccc)),
+};
+export type ExternalVolumeColorThemeParams = typeof ExternalVolumeColorThemeParams
+
+export function ExternalVolumeColorTheme(ctx: ThemeDataContext, props: PD.Values<ExternalVolumeColorThemeParams>): ColorTheme<ExternalVolumeColorThemeParams> {
+    let volume: Volume | undefined;
+    try {
+        volume = props.volume.getValue();
+    } catch {
+        // .getValue() is resolved during state reconciliation => would throw from UI
+    }
+
+    // NOTE: this will currently be slow for with GPU/texture meshes due to slow iteration
+    // TODO: create texture to be able to do the sampling on the GPU
+
+    let color;
+    if (volume) {
+        const coloring = props.coloring.params;
+        const { stats } = volume.grid;
+        const domain: [number, number] = coloring.domain.name === 'custom' ? coloring.domain.params : [stats.min, stats.max];
+
+        const isRelative = props.coloring.name === 'relative-value';
+        if (coloring.domain.name === 'auto' && isRelative) {
+            domain[0] = (domain[0] - stats.mean) / stats.sigma;
+            domain[1] = (domain[1] - stats.mean) / stats.sigma;
+        }
+
+        if (props.coloring.params.domain.name === 'auto' && props.coloring.params.domain.params.symmetric) {
+            const max = Math.max(Math.abs(domain[0]), Math.abs(domain[1]));
+            domain[0] = -max;
+            domain[1] = max;
+        }
+
+        const scale = ColorScale.create({ domain, listOrName: coloring.list.colors });
+
+        const cartnToGrid = Grid.getGridToCartesianTransform(volume.grid);
+        Mat4.invert(cartnToGrid, cartnToGrid);
+        const gridCoords = Vec3();
+
+        const { dimensions, get } = volume.grid.cells.space;
+        const data = volume.grid.cells.data;
+
+        const [mi, mj, mk] = dimensions;
+
+        color = (location: Location): Color => {
+            if (!isPositionLocation(location)) {
+                return props.defaultColor;
+            }
+
+            Vec3.copy(gridCoords, location.position);
+            Vec3.transformMat4(gridCoords, gridCoords, cartnToGrid);
+
+            const i = Math.floor(gridCoords[0]);
+            const j = Math.floor(gridCoords[1]);
+            const k = Math.floor(gridCoords[2]);
+
+            if (i < 0 || i >= mi || j < 0 || j >= mj || k < 0 || k >= mk) {
+                return props.defaultColor;
+            }
+
+            const u = gridCoords[0] - i;
+            const v = gridCoords[1] - j;
+            const w = gridCoords[2] - k;
+
+            // Tri-linear interpolation for the value
+            const ii = Math.min(i + 1, mi - 1);
+            const jj = Math.min(j + 1, mj - 1);
+            const kk = Math.min(k + 1, mk - 1);
+
+            let a = get(data, i, j, k);
+            let b = get(data, ii, j, k);
+            let c = get(data, i, jj, k);
+            let d = get(data, ii, jj, k);
+            const x = lerp(lerp(a, b, u), lerp(c, d, u), v);
+
+            a = get(data, i, j, kk);
+            b = get(data, ii, j, kk);
+            c = get(data, i, jj, kk);
+            d = get(data, ii, jj, kk);
+            const y = lerp(lerp(a, b, u), lerp(c, d, u), v);
+
+            let value = lerp(x, y, w);
+            if (isRelative) {
+                value = (value - stats.mean) / stats.sigma;
+            }
+
+            return scale.color(value);
+        };
+    } else {
+        color = () => props.defaultColor;
+    }
+
+    return {
+        factory: ExternalVolumeColorTheme,
+        granularity: 'vertex',
+        preferSmoothing: true,
+        color,
+        props,
+        description: Description,
+        // TODO: figure out how to do legend for this
+    };
+}
+
+export const ExternalVolumeColorThemeProvider: ColorTheme.Provider<ExternalVolumeColorThemeParams, 'external-volume'> = {
+    name: 'external-volume',
+    label: 'External Volume',
+    category: ColorTheme.Category.Misc,
+    factory: ExternalVolumeColorTheme,
+    getParams: () => ExternalVolumeColorThemeParams,
+    defaultValues: PD.getDefaultValues(ExternalVolumeColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => true,
+};

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

@@ -290,9 +290,9 @@ export namespace ParamDefinition {
     // getValue needs to be assigned by a runtime because it might not be serializable
     export interface ValueRef<T = any> extends Base<{ ref: string, getValue: () => T }> {
         type: 'value-ref',
-        resolveRef: (ref: string) => T,
+        resolveRef: (ref: string, getData: (ref: string) => any) => T,
         // a provider because the list changes over time
-        getOptions: () => Select<string>['options'],
+        getOptions: (ctx: any) => Select<string>['options'],
     }
     export function ValueRef<T>(getOptions: ValueRef['getOptions'], resolveRef: ValueRef<T>['resolveRef'], info?: Info & { defaultRef?: string }) {
         return setInfo<ValueRef<T>>({ type: 'value-ref', defaultValue: { ref: info?.defaultRef ?? '', getValue: unsetGetValue as any }, getOptions, resolveRef }, info);
@@ -365,8 +365,8 @@ export namespace ParamDefinition {
         return d as Values<T>;
     }
 
-    function _resolveRef(resolve: (ref: string) => any, ref: string) {
-        return () => resolve(ref);
+    function _resolveRef(resolve: (ref: string, getData: (ref: string) => any) => any, ref: string, getData: (ref: string) => any) {
+        return () => resolve(ref, getData);
     }
 
     function resolveRefValue(p: Any, value: any, getData: (ref: string) => any) {
@@ -375,11 +375,11 @@ export namespace ParamDefinition {
         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 v.getValue = _resolveRef(p.resolveRef, v.ref, getData);
         } 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 v.getValue = _resolveRef(getData, v.ref, getData);
         } else if (p.type === 'group') {
             resolveRefs(p.params, value, getData);
         } else if (p.type === 'mapped') {