123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- /**
- * Copyright (c) 2018-2022 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 { ParamDefinition as PD } from '../../mol-util/param-definition';
- import { Grid, Volume } from '../../mol-model/volume';
- import { VisualContext } from '../visual';
- import { Theme, ThemeRegistryContext } from '../../mol-theme/theme';
- import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
- import { computeMarchingCubesMesh, computeMarchingCubesLines } from '../../mol-geo/util/marching-cubes/algorithm';
- import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider, VolumeKey } from './representation';
- import { LocationIterator } from '../../mol-geo/util/location-iterator';
- import { NullLocation } from '../../mol-model/location';
- import { VisualUpdateState } from '../util';
- import { Lines } from '../../mol-geo/geometry/lines/lines';
- import { RepresentationContext, RepresentationParamsGetter, Representation } from '../representation';
- import { PickingId } from '../../mol-geo/geometry/picking';
- import { EmptyLoci, Loci } from '../../mol-model/loci';
- import { Interval } from '../../mol-data/int';
- import { Tensor, Vec2, Vec3 } from '../../mol-math/linear-algebra';
- import { fillSerial } from '../../mol-util/array';
- import { createVolumeTexture2d, eachVolumeLoci, getVolumeTexture2dLayout } from './util';
- import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
- import { extractIsosurface } from '../../mol-gl/compute/marching-cubes/isosurface';
- import { WebGLContext } from '../../mol-gl/webgl/context';
- import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
- import { Texture } from '../../mol-gl/webgl/texture';
- import { BaseGeometry } from '../../mol-geo/geometry/base';
- import { ValueCell } from '../../mol-util/value-cell';
- export const VolumeIsosurfaceParams = {
- isoValue: Volume.IsoValueParam
- };
- export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
- export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
- function gpuSupport(webgl: WebGLContext) {
- return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
- }
- const Padding = 1;
- function suitableForGpu(volume: Volume, webgl: WebGLContext) {
- // small volumes are about as fast or faster on CPU vs integrated GPU
- if (volume.grid.cells.data.length < Math.pow(10, 3)) return false;
- // the GPU is much more memory contraint, especially true for integrated GPUs,
- // fallback to CPU for large volumes
- const gridDim = volume.grid.cells.space.dimensions as Vec3;
- const { powerOfTwoSize } = getVolumeTexture2dLayout(gridDim, Padding);
- return powerOfTwoSize <= webgl.maxTextureSize / 2;
- }
- export function IsosurfaceVisual(materialId: number, volume: Volume, key: number, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) {
- if (props.tryUseGpu && webgl && gpuSupport(webgl) && suitableForGpu(volume, webgl)) {
- return IsosurfaceTextureMeshVisual(materialId);
- }
- return IsosurfaceMeshVisual(materialId);
- }
- function getLoci(volume: Volume, props: VolumeIsosurfaceProps) {
- return Volume.Isosurface.Loci(volume, props.isoValue);
- }
- function getIsosurfaceLoci(pickingId: PickingId, volume: Volume, key: number, props: VolumeIsosurfaceProps, id: number) {
- const { objectId, groupId } = pickingId;
- if (id === objectId) {
- const granularity = Volume.PickingGranularity.get(volume);
- if (granularity === 'volume') {
- return Volume.Loci(volume);
- } else if (granularity === 'object') {
- return Volume.Isosurface.Loci(volume, props.isoValue);
- } else {
- return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
- }
- }
- return EmptyLoci;
- }
- export function eachIsosurface(loci: Loci, volume: Volume, key: number, props: VolumeIsosurfaceProps, apply: (interval: Interval) => boolean) {
- return eachVolumeLoci(loci, volume, { isoValue: props.isoValue }, apply);
- }
- //
- export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceProps, mesh?: Mesh) {
- ctx.runtime.update({ message: 'Marching cubes...' });
- const ids = fillSerial(new Int32Array(volume.grid.cells.data.length));
- const surface = await computeMarchingCubesMesh({
- isoLevel: Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue,
- scalarField: volume.grid.cells,
- idField: Tensor.create(volume.grid.cells.space, Tensor.Data1(ids))
- }, mesh).runAsChild(ctx.runtime);
- const transform = Grid.getGridToCartesianTransform(volume.grid);
- Mesh.transform(surface, transform);
- if (ctx.webgl && !ctx.webgl.isWebGL2) {
- // 2nd arg means not to split triangles based on group id. Splitting triangles
- // is too expensive if each cell has its own group id as is the case here.
- Mesh.uniformTriangleGroup(surface, false);
- ValueCell.updateIfChanged(surface.varyingGroup, false);
- } else {
- ValueCell.updateIfChanged(surface.varyingGroup, true);
- }
- surface.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
- return surface;
- }
- export const IsosurfaceMeshParams = {
- ...Mesh.Params,
- ...TextureMesh.Params,
- ...VolumeIsosurfaceParams,
- quality: { ...Mesh.Params.quality, isEssential: false },
- tryUseGpu: PD.Boolean(true),
- };
- export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
- export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> {
- return VolumeVisual<Mesh, IsosurfaceMeshParams>({
- defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
- createGeometry: createVolumeIsosurfaceMesh,
- createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation),
- getLoci: getIsosurfaceLoci,
- eachLocation: eachIsosurface,
- setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
- if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
- },
- geometryUtils: Mesh.Utils,
- mustRecreate: (volumekey: VolumeKey, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
- return props.tryUseGpu && !!webgl && suitableForGpu(volumekey.volume, webgl);
- }
- }, materialId);
- }
- //
- namespace VolumeIsosurfaceTexture {
- const name = 'volume-isosurface-texture';
- export const descriptor = CustomPropertyDescriptor({ name });
- export function get(volume: Volume, webgl: WebGLContext) {
- const { resources } = webgl;
- const transform = Grid.getGridToCartesianTransform(volume.grid);
- const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions as Vec3);
- const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, Padding);
- const gridTexDim = Vec3.create(width, height, 0);
- const gridTexScale = Vec2.create(width / texDim, height / texDim);
- // console.log({ texDim, width, height, gridDimension });
- if (texDim > webgl.maxTextureSize / 2) {
- throw new Error('volume too large for gpu isosurface extraction');
- }
- if (!volume._propertyData[name]) {
- volume._propertyData[name] = resources.texture('image-uint8', 'alpha', 'ubyte', 'linear');
- const texture = volume._propertyData[name] as Texture;
- texture.define(texDim, texDim);
- // load volume into sub-section of texture
- texture.load(createVolumeTexture2d(volume, 'data', Padding), true);
- volume.customProperties.add(descriptor);
- volume.customProperties.assets(descriptor, [{ dispose: () => texture.destroy() }]);
- }
- gridDimension[0] += Padding;
- gridDimension[1] += Padding;
- return {
- texture: volume._propertyData[name] as Texture,
- transform,
- gridDimension,
- gridTexDim,
- gridTexScale
- };
- }
- }
- async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) {
- if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
- if (volume.grid.cells.data.length <= 1) {
- return TextureMesh.createEmpty(textureMesh);
- }
- const { max, min } = volume.grid.stats;
- const diff = max - min;
- const value = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue;
- const isoLevel = ((value - min) / diff);
- const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, ctx.webgl);
- const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
- const buffer = textureMesh?.doubleBuffer.get();
- const gv = extractIsosurface(ctx.webgl, texture, gridDimension, gridTexDim, gridTexScale, transform, isoLevel, value < 0, false, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
- const groupCount = volume.grid.cells.data.length;
- const boundingSphere = Volume.getBoundingSphere(volume); // getting isosurface bounding-sphere is too expensive here
- const surface = TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
- surface.meta.webgl = ctx.webgl;
- return surface;
- }
- export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> {
- return VolumeVisual<TextureMesh, IsosurfaceMeshParams>({
- defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
- createGeometry: createVolumeIsosurfaceTextureMesh,
- createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation),
- getLoci: getIsosurfaceLoci,
- eachLocation: eachIsosurface,
- setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
- if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
- },
- geometryUtils: TextureMesh.Utils,
- mustRecreate: (volumeKey: VolumeKey, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
- return !props.tryUseGpu || !webgl || !suitableForGpu(volumeKey.volume, webgl);
- },
- dispose: (geometry: TextureMesh) => {
- geometry.vertexTexture.ref.value.destroy();
- geometry.groupTexture.ref.value.destroy();
- geometry.normalTexture.ref.value.destroy();
- geometry.doubleBuffer.destroy();
- }
- }, materialId);
- }
- //
- export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceProps, lines?: Lines) {
- ctx.runtime.update({ message: 'Marching cubes...' });
- const ids = fillSerial(new Int32Array(volume.grid.cells.data.length));
- const wireframe = await computeMarchingCubesLines({
- isoLevel: Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue,
- scalarField: volume.grid.cells,
- idField: Tensor.create(volume.grid.cells.space, Tensor.Data1(ids))
- }, lines).runAsChild(ctx.runtime);
- const transform = Grid.getGridToCartesianTransform(volume.grid);
- Lines.transform(wireframe, transform);
- wireframe.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
- return wireframe;
- }
- export const IsosurfaceWireframeParams = {
- ...Lines.Params,
- ...VolumeIsosurfaceParams,
- quality: { ...Lines.Params.quality, isEssential: false },
- sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
- };
- export type IsosurfaceWireframeParams = typeof IsosurfaceWireframeParams
- export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<IsosurfaceWireframeParams> {
- return VolumeVisual<Lines, IsosurfaceWireframeParams>({
- defaultProps: PD.getDefaultValues(IsosurfaceWireframeParams),
- createGeometry: createVolumeIsosurfaceWireframe,
- createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation),
- getLoci: getIsosurfaceLoci,
- eachLocation: eachIsosurface,
- setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceWireframeParams>, currentProps: PD.Values<IsosurfaceWireframeParams>) => {
- if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
- },
- geometryUtils: Lines.Utils
- }, materialId);
- }
- //
- const IsosurfaceVisuals = {
- 'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceVisual, getLoci),
- 'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual, getLoci),
- };
- export const IsosurfaceParams = {
- ...IsosurfaceMeshParams,
- ...IsosurfaceWireframeParams,
- visuals: PD.MultiSelect(['solid'], PD.objectToOptions(IsosurfaceVisuals)),
- bumpFrequency: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
- };
- export type IsosurfaceParams = typeof IsosurfaceParams
- export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: Volume) {
- const p = PD.clone(IsosurfaceParams);
- p.isoValue = Volume.createIsoValueParam(Volume.IsoValue.relative(2), volume.grid.stats);
- return p;
- }
- export type IsosurfaceRepresentation = VolumeRepresentation<IsosurfaceParams>
- export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceParams>): IsosurfaceRepresentation {
- return Representation.createMulti('Isosurface', ctx, getParams, Representation.StateBuilder, IsosurfaceVisuals as unknown as Representation.Def<Volume, IsosurfaceParams>);
- }
- export const IsosurfaceRepresentationProvider = VolumeRepresentationProvider({
- name: 'isosurface',
- label: 'Isosurface',
- description: 'Displays a triangulated isosurface of volumetric data.',
- factory: IsosurfaceRepresentation,
- getParams: getIsosurfaceParams,
- defaultValues: PD.getDefaultValues(IsosurfaceParams),
- defaultColorTheme: { name: 'uniform' },
- defaultSizeTheme: { name: 'uniform' },
- isApplicable: (volume: Volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume)
- });
|