Browse Source

volume improvements and slice repr

Alexander Rose 5 years ago
parent
commit
376d4b4ee1

+ 3 - 4
src/mol-geo/geometry/image/image.ts

@@ -98,8 +98,7 @@ namespace Image {
         return image;
     }
 
-    function update(imageTexture: TextureImage<Uint8Array | Float32Array>, corners: Float32Array, groupTexture: TextureImage<Float32Array>, image: Image): Image {
-
+    function update(imageTexture: TextureImage<Float32Array>, corners: Float32Array, groupTexture: TextureImage<Float32Array>, image: Image): Image {
         const width = imageTexture.width;
         const height = imageTexture.height;
 
@@ -116,7 +115,7 @@ namespace Image {
 
     export const Params = {
         ...BaseGeometry.Params,
-        interpolation: PD.Select('bspline', PD.objectToOptions(InterpolationTypes), { isEssential: true }),
+        interpolation: PD.Select('bspline', PD.objectToOptions(InterpolationTypes)),
     };
     export type Params = typeof Params
 
@@ -176,7 +175,7 @@ namespace Image {
     }
 
     function updateValues(values: ImageValues, props: PD.Values<Params>) {
-        ValueCell.updateIfChanged(values.uAlpha, props.alpha);
+        BaseGeometry.updateValues(values, props);
         ValueCell.updateIfChanged(values.dInterpolation, props.interpolation);
     }
 

+ 3 - 5
src/mol-model/loci.ts

@@ -17,7 +17,6 @@ import { FiniteArray } from '../mol-util/type-helpers';
 import { BoundaryHelper } from '../mol-math/geometry/boundary-helper';
 import { stringToWords } from '../mol-util/string';
 import { Volume } from './volume/volume';
-import { VolumeData } from './volume';
 
 /** A Loci that includes every loci */
 export const EveryLoci = { kind: 'every-loci' as 'every-loci' };
@@ -162,12 +161,11 @@ namespace Loci {
         } else if (loci.kind === 'data-loci') {
             return loci.getBoundingSphere(boundingSphere);
         } else if (loci.kind === 'volume-loci') {
-            return VolumeData.getBoundingSphere(loci.volume, boundingSphere);
+            return Volume.getBoundingSphere(loci.volume, boundingSphere);
         } else if (loci.kind === 'isosurface-loci') {
-            return VolumeData.getBoundingSphere(loci.volume, boundingSphere);
+            return Volume.Isosurface.getBoundingSphere(loci.volume, boundingSphere);
         } else if (loci.kind === 'cell-loci') {
-            // TODO
-            return VolumeData.getBoundingSphere(loci.volume, boundingSphere);
+            return Volume.Cell.getBoundingSphere(loci.volume, boundingSphere);
         }
     }
 

+ 4 - 10
src/mol-model/volume/data.ts

@@ -1,11 +1,11 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { SpacegroupCell, Box3D, Sphere3D } from '../../mol-math/geometry';
+import { SpacegroupCell, Box3D } from '../../mol-math/geometry';
 import { Tensor, Mat4, Vec3 } from '../../mol-math/linear-algebra';
 import { equalEps } from '../../mol-math/linear-algebra/3d/common';
 
@@ -52,12 +52,6 @@ namespace VolumeData {
     export function areEquivalent(volA: VolumeData, volB: VolumeData) {
         return volA === volB;
     }
-
-    export function getBoundingSphere(volume: VolumeData, boundingSphere?: Sphere3D) {
-        if (!boundingSphere) boundingSphere = Sphere3D();
-        // TODO
-        return boundingSphere;
-    }
 }
 
 type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative
@@ -91,8 +85,8 @@ namespace VolumeIsoValue {
 
     export function toString(value: VolumeIsoValue) {
         return value.kind === 'relative'
-            ? `${value.relativeValue} σ`
-            : `${value.absoluteValue}`;
+            ? `${value.relativeValue.toFixed(2)} σ`
+            : `${value.absoluteValue.toPrecision(4)}`;
     }
 }
 

+ 38 - 0
src/mol-model/volume/volume.ts

@@ -6,6 +6,8 @@
 
 import { VolumeData, VolumeIsoValue } from './data';
 import { OrderedSet } from '../../mol-data/int';
+import { Sphere3D } from '../../mol-math/geometry';
+import { Vec3 } from '../../mol-math/linear-algebra';
 
 export namespace Volume {
     export type CellIndex = { readonly '@type': 'cell-index' } & number
@@ -16,12 +18,42 @@ export namespace Volume {
     export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume; }
     export function isLociEmpty(loci: Loci) { return loci.volume.data.data.length === 0; }
 
+    export function getBoundingSphere(volume: VolumeData, boundingSphere?: Sphere3D) {
+        if (!boundingSphere) boundingSphere = Sphere3D();
+
+        const transform = VolumeData.getGridToCartesianTransform(volume);
+        const [x, y, z] = volume.data.space.dimensions;
+
+        const cpA = Vec3.create(0, 0, 0); Vec3.transformMat4(cpA, cpA, transform);
+        const cpB = Vec3.create(x, y, z); Vec3.transformMat4(cpB, cpB, transform);
+        const cpC = Vec3.create(x, 0, 0); Vec3.transformMat4(cpC, cpC, transform);
+        const cpD = Vec3.create(0, y, z); Vec3.transformMat4(cpD, cpC, transform);
+
+        const cpE = Vec3.create(0, 0, z); Vec3.transformMat4(cpE, cpE, transform);
+        const cpF = Vec3.create(x, 0, z); Vec3.transformMat4(cpF, cpF, transform);
+        const cpG = Vec3.create(x, y, 0); Vec3.transformMat4(cpG, cpG, transform);
+        const cpH = Vec3.create(0, y, 0); Vec3.transformMat4(cpH, cpH, transform);
+
+        const center = Vec3();
+        Vec3.add(center, cpA, cpB);
+        Vec3.scale(center, center, 0.5);
+        const d = Math.max(Vec3.distance(cpA, cpB), Vec3.distance(cpC, cpD));
+        Sphere3D.set(boundingSphere, center, d / 2);
+        Sphere3D.setExtrema(boundingSphere, [cpA, cpB, cpC, cpD, cpE, cpF, cpG, cpH]);
+
+        return boundingSphere;
+    }
+
     export namespace Isosurface {
         export interface Loci { readonly kind: 'isosurface-loci', readonly volume: VolumeData, readonly isoValue: VolumeIsoValue }
         export function Loci(volume: VolumeData, isoValue: VolumeIsoValue): Loci { return { kind: 'isosurface-loci', volume, isoValue }; }
         export function isLoci(x: any): x is Loci { return !!x && x.kind === 'isosurface-loci'; }
         export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume && VolumeIsoValue.areSame(a.isoValue, b.isoValue, a.volume.dataStats); }
         export function isLociEmpty(loci: Loci) { return loci.volume.data.data.length === 0; }
+
+        export function getBoundingSphere(volume: VolumeData, boundingSphere?: Sphere3D) {
+            return Volume.getBoundingSphere(volume, boundingSphere);
+        }
     }
 
     export namespace Cell {
@@ -30,5 +62,11 @@ export namespace Volume {
         export function isLoci(x: any): x is Loci { return !!x && x.kind === 'cell-loci'; }
         export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume && OrderedSet.areEqual(a.indices, b.indices); }
         export function isLociEmpty(loci: Loci) { return OrderedSet.size(loci.indices) === 0; }
+
+        export function getBoundingSphere(volume: VolumeData, boundingSphere?: Sphere3D) {
+            if (!boundingSphere) boundingSphere = Sphere3D();
+            // TODO get sphere reflecting cell coords and size
+            return Volume.getBoundingSphere(volume, boundingSphere);
+        }
     }
 }

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -122,12 +122,12 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
     const transform = VolumeData.getGridToCartesianTransform(volume);
     ctx.runtime.update({ message: 'Transforming mesh...' });
     Mesh.transform(surface, transform);
-    console.log(surface, Tensor.create(volume.data.space, Tensor.Data1(ids)));
     return surface;
 }
 
 export const IsosurfaceMeshParams = {
     ...Mesh.Params,
+    quality: { ...Mesh.Params.quality, isEssential: false },
     ...VolumeIsosurfaceParams
 };
 export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
@@ -167,6 +167,7 @@ export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume
 
 export const IsosurfaceWireframeParams = {
     ...Lines.Params,
+    quality: { ...Lines.Params.quality, isEssential: false },
     sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
     ...VolumeIsosurfaceParams
 };

+ 141 - 55
src/mol-repr/volume/slice.ts

@@ -6,9 +6,8 @@
 
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Image } from '../../mol-geo/geometry/image/image';
-import { BaseGeometry } from '../../mol-geo/geometry/base';
 import { ThemeRegistryContext, Theme } from '../../mol-theme/theme';
-import { VolumeData } from '../../mol-model/volume';
+import { VolumeData, VolumeIsoValue } from '../../mol-model/volume';
 import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
 import { LocationIterator } from '../../mol-geo/util/location-iterator';
 import { VisualUpdateState } from '../util';
@@ -18,57 +17,118 @@ import { VisualContext } from '../visual';
 import { Volume } from '../../mol-model/volume/volume';
 import { PickingId } from '../../mol-geo/geometry/picking';
 import { EmptyLoci, Loci } from '../../mol-model/loci';
-import { Interval, OrderedSet } from '../../mol-data/int';
-import { fillSerial } from '../../mol-util/array';
+import { Interval, OrderedSet, SortedArray } from '../../mol-data/int';
+import { transformPositionArray } from '../../mol-geo/util';
+import { equalEps } from '../../mol-math/linear-algebra/3d/common';
+import { RenderableState } from '../../mol-gl/renderable';
+import { createIsoValueParam, IsoValueParam } from './isosurface';
+import { Color } from '../../mol-util/color';
+import { ColorTheme } from '../../mol-theme/color';
 
 export async function createImage(ctx: VisualContext, volume: VolumeData, theme: Theme, props: PD.Values<SliceParams>, image?: Image) {
-    const dim = parseInt(props.dimension.name.toString());
-    // const index = props.dimension.params;
-
-    const { space } = volume.data;
-
-    let width: number, height: number;
-    if (dim === 0) {
-        width = space.dimensions[1];
-        height = space.dimensions[2];
-    } else if (dim === 1) {
-        width = space.dimensions[0];
-        height = space.dimensions[2];
-    } else {
-        width = space.dimensions[0];
-        height = space.dimensions[1];
+    const { dimension: { name: dim }, isoValue } = props;
+
+    const { space, data: data } = volume.data;
+    const { min, max } = volume.dataStats;
+    const isoVal = VolumeIsoValue.toAbsolute(isoValue, volume.dataStats).absoluteValue;
+
+    // TODO more color themes
+    const color = theme.color.color(NullLocation, false);
+    const [r, g, b] = Color.toRgbNormalized(color);
+
+    const {
+        width, height,
+        x, y, z,
+        x0, y0, z0,
+        nx, ny, nz
+    } = getSliceInfo(volume, props);
+
+    const corners = new Float32Array(
+        dim === 'x' ? [x, 0, 0,  x, y, 0,  x, 0, z,  x, y, z] :
+            dim === 'y' ? [0, y, 0,  x, y, 0,  0, y, z,  x, y, z] :
+                [0, 0, z,  0, y, z,  x, 0, z,  x, y, z]
+    );
+
+    const imageArray = new Float32Array(width * height * 4);
+    const groupArray = getGroupArray(volume, props);
+
+    let i = 0;
+    for (let iy = y0; iy < ny; ++iy) {
+        for (let ix = x0; ix < nx; ++ix) {
+            for (let iz = z0; iz < nz; ++iz) {
+                const val = space.get(data, ix, iy, iz);
+                const normVal = (val - min) / (max - min);
+
+                imageArray[i] = r * normVal * 2;
+                imageArray[i + 1] = g * normVal * 2;
+                imageArray[i + 2] = b * normVal * 2;
+                imageArray[i + 3] = val >= isoVal ? 1 : 0;
+
+                i += 4;
+            }
+        }
     }
 
-    const n = width * height;
+    const imageTexture = { width, height, array: imageArray, flipY: true };
+    const groupTexture = { width, height, array: groupArray, flipY: true };
 
-    // TODO fill with volume data values
-    const imageTexture = { width, height, array: new Float32Array(n * 4) };
+    const transform = VolumeData.getGridToCartesianTransform(volume);
+    transformPositionArray(transform, corners, 0, 4);
 
-    for (let i = 0, il = n * 4; i < il; i += 4) {
-        imageTexture.array[i] = 0;
-        imageTexture.array[i + 1] = Math.random();
-        imageTexture.array[i + 2] = Math.random();
-        imageTexture.array[i + 3] = 1;
-    }
+    return Image.create(imageTexture, corners, groupTexture, image);
+}
 
-    // TODO fill with linearized index into volume
-    //      (to be used for picking which needs a volume location/loci)
-    const groupTexture = { width, height, array: fillSerial(new Float32Array(n)) };
+function getSliceInfo(volume: VolumeData, props: PD.Values<SliceParams>) {
+    const { dimension: { name: dim, params: index } } = props;
+    const { space } = volume.data;
 
-    // TODO four corners of a plane
-    const corners = new Float32Array([
-        0, 0, 0,
-        0, 50, 0,
-        0, 0, 50,
-        0, 50, 50
-    ]);
+    let width, height;
+    let x, y, z;
+    let x0 = 0, y0 = 0, z0 = 0;
+    let [nx, ny, nz] = space.dimensions;
+
+    if (dim === 'x') {
+        x = index, y = ny - 1, z = nz - 1;
+        width = nz, height = ny;
+        x0 = x, nx = x0 + 1;
+    } else if (dim === 'y') {
+        x = nx - 1, y = index, z = nz - 1;
+        width = nz, height = nx;
+        y0 = y, ny = y0 + 1;
+    } else {
+        x = nx - 1, y = ny - 1, z = index;
+        width = nx, height = ny;
+        z0 = z, nz = z0 + 1;
+    }
+    return {
+        width, height,
+        x, y, z,
+        x0, y0, z0,
+        nx, ny, nz
+    };
+}
 
-    return Image.create(imageTexture, corners, groupTexture, image);
+function getGroupArray(volume: VolumeData, props: PD.Values<SliceParams>) {
+    const { space } = volume.data;
+    const { width, height, x0, y0, z0, nx, ny, nz } = getSliceInfo(volume, props);
+    const groupArray = new Float32Array(width * height);
+
+    let j = 0;
+    for (let iy = y0; iy < ny; ++iy) {
+        for (let ix = x0; ix < nx; ++ix) {
+            for (let iz = z0; iz < nz; ++iz) {
+                groupArray[j] = space.dataOffset(ix, iy, iz);
+                j += 1;
+            }
+        }
+    }
+    return groupArray;
 }
 
 function getLoci(volume: VolumeData, props: PD.Values<SliceParams>) {
-    // TODO only slice indices
-    return Volume.Loci(volume);
+    // TODO cache somehow?
+    const groupArray = getGroupArray(volume, props);
+    return Volume.Cell.Loci(volume, SortedArray.ofUnsortedArray(groupArray));
 }
 
 function getSliceLoci(pickingId: PickingId, volume: VolumeData, props: PD.Values<SliceParams>, id: number) {
@@ -86,8 +146,15 @@ function eachSlice(loci: Loci, volume: VolumeData, props: PD.Values<SliceParams>
         if (apply(Interval.ofLength(volume.data.data.length))) changed = true;
     } else if (Volume.Isosurface.isLoci(loci)) {
         if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
-        // TODO check isoValue
-        if (apply(Interval.ofLength(volume.data.data.length))) changed = true;
+        // TODO find a cheaper way?
+        const { dataStats, data: { data } } = volume;
+        const eps = dataStats.sigma;
+        const v = VolumeIsoValue.toAbsolute(loci.isoValue, dataStats).absoluteValue;
+        for (let i = 0, il = data.length; i < il; ++i) {
+            if (equalEps(v, data[i], eps)) {
+                if (apply(Interval.ofSingleton(i))) changed = true;
+            }
+        }
     } else if (Volume.Cell.isLoci(loci)) {
         if (!VolumeData.areEquivalent(loci.volume, volume)) return false;
         if (Interval.is(loci.indices)) {
@@ -104,22 +171,25 @@ function eachSlice(loci: Loci, volume: VolumeData, props: PD.Values<SliceParams>
 //
 
 export const SliceParams = {
-    ...BaseGeometry.Params,
     ...Image.Params,
-    dimension: PD.MappedStatic(0, {
-        0: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
-        1: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
-        2: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
+    quality: { ...Image.Params.quality, isEssential: false },
+    dimension: PD.MappedStatic('x', {
+        x: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
+        y: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
+        z: PD.Numeric(0, { min: 0, max: 0, step: 1 }),
     }, { isEssential: true }),
+    isoValue: IsoValueParam,
 };
 export type SliceParams = typeof SliceParams
 export function getSliceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
     const p = PD.clone(SliceParams);
-    p.dimension = PD.MappedStatic(0, {
-        0: PD.Numeric(0, { min: 0, max: volume.data.space.dimensions[0], step: 1 }),
-        1: PD.Numeric(0, { min: 0, max: volume.data.space.dimensions[1], step: 1 }),
-        2: PD.Numeric(0, { min: 0, max: volume.data.space.dimensions[2], step: 1 }),
+    const dim = volume.data.space.dimensions;
+    p.dimension = PD.MappedStatic('x', {
+        x: PD.Numeric(0, { min: 0, max: dim[0] - 1, step: 1 }),
+        y: PD.Numeric(0, { min: 0, max: dim[1] - 1, step: 1 }),
+        z: PD.Numeric(0, { min: 0, max: dim[2] - 1, step: 1 }),
     }, { isEssential: true });
+    p.isoValue = createIsoValueParam(VolumeIsoValue.absolute(volume.dataStats.min), volume.dataStats);
     return p;
 }
 
@@ -130,16 +200,32 @@ export function SliceVisual(materialId: number): VolumeVisual<SliceParams> {
         createLocationIterator: (volume: VolumeData) => LocationIterator(volume.data.data.length, 1, () => NullLocation),
         getLoci: getSliceLoci,
         eachLocation: eachSlice,
-        setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<SliceParams>, currentProps: PD.Values<SliceParams>) => {
+        setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<SliceParams>, currentProps: PD.Values<SliceParams>, newTheme: Theme, currentTheme: Theme) => {
             state.createGeometry = (
                 newProps.dimension.name !== currentProps.dimension.name ||
-                newProps.dimension.params !== currentProps.dimension.params
+                newProps.dimension.params !== currentProps.dimension.params ||
+                !VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats) ||
+                !ColorTheme.areEqual(newTheme.color, currentTheme.color)
             );
         },
-        geometryUtils: Image.Utils
+        geometryUtils: {
+            ...Image.Utils,
+            createRenderableState: (props: PD.Values<SliceParams>) => {
+                const state = Image.Utils.createRenderableState(props);
+                updateRenderableState(state, props);
+                return state;
+            },
+            updateRenderableState
+        }
     }, materialId);
 }
 
+function updateRenderableState(state: RenderableState, props: PD.Values<SliceParams>) {
+    Image.Utils.updateRenderableState(state, props);
+    state.opaque = false;
+    state.writeDepth = true;
+}
+
 export function SliceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, SliceParams>): VolumeRepresentation<SliceParams> {
     return VolumeRepresentation('Slice', ctx, getParams, SliceVisual, getLoci);
 }