UNPKG

molstar

Version:

A comprehensive macromolecular library.

323 lines (322 loc) 16.2 kB
/** * Copyright (c) 2018-2026 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.js'; import { Grid, Volume } from '../../mol-model/volume.js'; import { Mesh } from '../../mol-geo/geometry/mesh/mesh.js'; import { computeMarchingCubesMesh, computeMarchingCubesLines } from '../../mol-geo/util/marching-cubes/algorithm.js'; import { VolumeRepresentation, VolumeRepresentationProvider } from './representation.js'; import { Lines } from '../../mol-geo/geometry/lines/lines.js'; import { Representation } from '../representation.js'; import { PickingId } from '../../mol-geo/geometry/picking.js'; import { EmptyLoci } from '../../mol-model/loci.js'; import { Tensor, Vec2, Vec3 } from '../../mol-math/linear-algebra.js'; import { fillSerial } from '../../mol-util/array.js'; import { createVolumeCellLocationIterator, createVolumeTexture2d, createWrappedTensor, createWrappedVolume, eachVolumeLoci, getVolumeTexture2dLayout } from './util.js'; import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh.js'; import { extractIsosurface } from '../../mol-gl/compute/marching-cubes/isosurface.js'; import { CustomPropertyDescriptor } from '../../mol-model/custom-property.js'; import { BaseGeometry } from '../../mol-geo/geometry/base.js'; import { ValueCell } from '../../mol-util/value-cell.js'; import { Interval } from '../../mol-data/int/interval.js'; import { OrderedSet } from '../../mol-data/int/ordered-set.js'; import { VolumeVisual } from './visual.js'; export const VolumeIsosurfaceParams = { isoValue: Volume.IsoValueParam, wrap: PD.Select('auto', PD.arrayToOptions(['off', 'on', 'auto'])), floodfill: PD.Select('off', PD.arrayToOptions(['off', 'inside', 'outside']), { description: 'If and how to floodfill the volume. Note that this disables GPU support.' }), }; export const VolumeIsosurfaceTextureParams = { ...VolumeIsosurfaceParams, tryUseGpu: PD.Boolean(true, { hideIf: p => p.floodfill !== 'off' }), gpuDataType: PD.Select('byte', PD.arrayToOptions(['byte', 'float', 'halfFloat']), { hideIf: p => !p.tryUseGpu || p.floodfill !== 'off' }), }; function gpuSupport(webgl) { return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers; } function shouldWrap(volume, wrap) { if (wrap === 'on') return true; if (wrap === 'off') return false; return volume.periodicity === 'xyz'; } const Padding = 1; function suitableForGpu(volume, webgl) { // 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; const { powerOfTwoSize } = getVolumeTexture2dLayout(gridDim, Padding); return powerOfTwoSize <= webgl.maxTextureSize / 2; } export function IsosurfaceVisual(materialId, volume, key, props, webgl) { if (props.floodfill === 'off' && props.tryUseGpu && webgl && gpuSupport(webgl) && suitableForGpu(volume, webgl)) { return IsosurfaceTextureMeshVisual(materialId); } return IsosurfaceMeshVisual(materialId); } function getLoci(volume, props) { const instances = Interval.ofLength(volume.instances.length); return Volume.Isosurface.Loci(volume, props.isoValue, instances); } function getIsosurfaceLoci(pickingId, volume, key, props, id) { const { objectId, groupId, instanceId } = pickingId; if (id === objectId) { const granularity = Volume.PickingGranularity.get(volume); const instances = OrderedSet.ofSingleton(instanceId); if (granularity === 'volume') { return Volume.Loci(volume, instances); } else if (granularity === 'object' || groupId === PickingId.Null) { return Volume.Isosurface.Loci(volume, props.isoValue, instances); } else { const indices = Interval.ofSingleton(groupId); return Volume.Cell.Loci(volume, [{ indices, instances }]); } } return EmptyLoci; } export function eachIsosurface(loci, volume, key, props, apply) { return eachVolumeLoci(loci, volume, { isoValue: props.isoValue }, apply); } // export async function createVolumeIsosurfaceMesh(ctx, volume, key, theme, props, mesh) { ctx.runtime.update({ message: 'Marching cubes...' }); const isoLevel = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue; let scalarField = volume.grid.cells; if (props.floodfill !== 'off') { scalarField = Tensor.createFloodfilled(scalarField, isoLevel, props.floodfill); } if (shouldWrap(volume, props.wrap)) { scalarField = createWrappedTensor(scalarField); } const ids = fillSerial(new Int32Array(volume.grid.cells.data.length)); const surface = await computeMarchingCubesMesh({ isoLevel, scalarField: scalarField, idField: Tensor.create(scalarField.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, ...VolumeIsosurfaceTextureParams, quality: { ...Mesh.Params.quality, isEssential: false }, }; export function IsosurfaceMeshVisual(materialId) { return VolumeVisual({ defaultProps: PD.getDefaultValues(IsosurfaceMeshParams), createGeometry: createVolumeIsosurfaceMesh, createLocationIterator: createVolumeCellLocationIterator, getLoci: getIsosurfaceLoci, eachLocation: eachIsosurface, setUpdateState: (state, volume, newProps, currentProps) => { state.createGeometry = (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) || newProps.wrap !== currentProps.wrap || newProps.floodfill !== currentProps.floodfill); }, geometryUtils: Mesh.Utils, mustRecreate: (volumekey, props, webgl) => { return props.tryUseGpu && !!webgl && suitableForGpu(volumekey.volume, webgl); } }, materialId); } // var VolumeIsosurfaceTexture; (function (VolumeIsosurfaceTexture) { const name = 'volume-isosurface-texture'; VolumeIsosurfaceTexture.descriptor = CustomPropertyDescriptor({ name }); function clear(volume) { delete volume._propertyData[name]; } VolumeIsosurfaceTexture.clear = clear; function get(volume, webgl, props) { var _a, _b; const { gpuDataType } = props; const wrap = shouldWrap(volume, props.wrap); const transform = Grid.getGridToCartesianTransform(volume.grid); const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions); const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, Padding); const gridTexDim = Vec3.create(width, height, 0); const gridDataDim = Vec3.subScalar(Vec3(), gridDimension, wrap ? 1 : 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'); } const dataType = gpuDataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : gpuDataType; if (((_a = volume._propertyData[name]) === null || _a === void 0 ? void 0 : _a.dataType) !== dataType || ((_b = volume._propertyData[name]) === null || _b === void 0 ? void 0 : _b.wrap) !== wrap) { const texture = dataType === 'byte' ? webgl.resources.texture('image-uint8', 'alpha', 'ubyte', 'linear') : dataType === 'halfFloat' ? webgl.resources.texture('image-float16', 'alpha', 'fp16', 'linear') : webgl.resources.texture('image-float32', 'alpha', 'float', 'linear'); volume._propertyData[name] = { texture, dataType, wrap }; texture.define(texDim, texDim); // load volume into sub-section of texture texture.load(createVolumeTexture2d(volume, 'data', Padding, dataType), true); volume.customProperties.add(VolumeIsosurfaceTexture.descriptor); volume.customProperties.assets(VolumeIsosurfaceTexture.descriptor, [{ dispose: () => texture.destroy() }]); } gridDimension[0] += Padding; gridDimension[1] += Padding; return { texture: volume._propertyData[name].texture, transform, gridDimension, gridTexDim, gridDataDim, gridTexScale }; } VolumeIsosurfaceTexture.get = get; })(VolumeIsosurfaceTexture || (VolumeIsosurfaceTexture = {})); function createVolumeIsosurfaceTextureMesh(ctx, volume, key, theme, props, textureMesh) { const { webgl } = ctx; if (!webgl) throw new Error('webgl context required to create volume isosurface texture-mesh'); if (volume.grid.cells.data.length <= 1) { return TextureMesh.createEmpty(textureMesh); } if (shouldWrap(volume, props.wrap)) { volume = createWrappedVolume(volume); } 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 axisOrder = volume.grid.cells.space.axisOrderSlowToFast; const groupCount = volume.grid.cells.data.length; const boundingSphere = Volume.getBoundingSphere(volume); // getting isosurface bounding-sphere is too expensive here const create = (textureMesh) => { const { texture, gridDimension, gridTexDim, gridDataDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, webgl, props); const buffer = textureMesh === null || textureMesh === void 0 ? void 0 : textureMesh.doubleBuffer.get(); const gv = extractIsosurface(webgl, texture, gridDimension, gridTexDim, gridDataDim, gridTexScale, transform, isoLevel, value < 0, false, axisOrder, true, buffer === null || buffer === void 0 ? void 0 : buffer.vertex, buffer === null || buffer === void 0 ? void 0 : buffer.group, buffer === null || buffer === void 0 ? void 0 : buffer.normal); return TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh); }; const surface = create(textureMesh); surface.meta.webgl = webgl; surface.meta.reset = () => { VolumeIsosurfaceTexture.clear(volume); create(surface); }; return surface; } export function IsosurfaceTextureMeshVisual(materialId) { return VolumeVisual({ defaultProps: PD.getDefaultValues(IsosurfaceMeshParams), createGeometry: createVolumeIsosurfaceTextureMesh, createLocationIterator: createVolumeCellLocationIterator, getLoci: getIsosurfaceLoci, eachLocation: eachIsosurface, setUpdateState: (state, volume, newProps, currentProps) => { state.createGeometry = (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) || newProps.gpuDataType !== currentProps.gpuDataType || newProps.wrap !== currentProps.wrap); }, geometryUtils: TextureMesh.Utils, mustRecreate: (volumeKey, props, webgl) => { return props.floodfill !== 'off' || !props.tryUseGpu || !webgl || !suitableForGpu(volumeKey.volume, webgl); }, dispose: (geometry) => { geometry.vertexTexture.ref.value.destroy(); geometry.groupTexture.ref.value.destroy(); geometry.normalTexture.ref.value.destroy(); geometry.doubleBuffer.destroy(); } }, materialId); } // export async function createVolumeIsosurfaceWireframe(ctx, volume, key, theme, props, lines) { ctx.runtime.update({ message: 'Marching cubes...' }); const isoLevel = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue; let scalarField = volume.grid.cells; if (props.floodfill !== 'off') { scalarField = Tensor.createFloodfilled(scalarField, isoLevel, props.floodfill); } if (shouldWrap(volume, props.wrap)) { scalarField = createWrappedTensor(scalarField); } const ids = fillSerial(new Int32Array(volume.grid.cells.data.length)); const wireframe = await computeMarchingCubesLines({ isoLevel, scalarField, idField: Tensor.create(scalarField.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 function IsosurfaceWireframeVisual(materialId) { return VolumeVisual({ defaultProps: PD.getDefaultValues(IsosurfaceWireframeParams), createGeometry: createVolumeIsosurfaceWireframe, createLocationIterator: createVolumeCellLocationIterator, getLoci: getIsosurfaceLoci, eachLocation: eachIsosurface, setUpdateState: (state, volume, newProps, currentProps) => { state.createGeometry = (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) || newProps.wrap !== currentProps.wrap || newProps.floodfill !== currentProps.floodfill); }, geometryUtils: Lines.Utils }, materialId); } // const IsosurfaceVisuals = { 'solid': (ctx, getParams) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceVisual, getLoci), 'wireframe': (ctx, getParams) => 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 function getIsosurfaceParams(ctx, volume) { const p = PD.clone(IsosurfaceParams); p.isoValue = Volume.createIsoValueParam(Volume.IsoValue.relative(2), volume.grid.stats); return p; } export function IsosurfaceRepresentation(ctx, getParams) { return Representation.createMulti('Isosurface', ctx, getParams, Representation.StateBuilder, IsosurfaceVisuals); } 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.isEmpty(volume) && !Volume.Segmentation.get(volume) });