molstar
Version:
A comprehensive macromolecular library.
298 lines (297 loc) • 16.2 kB
JavaScript
/**
* Copyright (c) 2018-2025 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>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.IsosurfaceRepresentationProvider = exports.IsosurfaceParams = exports.IsosurfaceWireframeParams = exports.IsosurfaceMeshParams = exports.VolumeIsosurfaceTextureParams = exports.VolumeIsosurfaceParams = void 0;
exports.IsosurfaceVisual = IsosurfaceVisual;
exports.eachIsosurface = eachIsosurface;
exports.createVolumeIsosurfaceMesh = createVolumeIsosurfaceMesh;
exports.IsosurfaceMeshVisual = IsosurfaceMeshVisual;
exports.IsosurfaceTextureMeshVisual = IsosurfaceTextureMeshVisual;
exports.createVolumeIsosurfaceWireframe = createVolumeIsosurfaceWireframe;
exports.IsosurfaceWireframeVisual = IsosurfaceWireframeVisual;
exports.getIsosurfaceParams = getIsosurfaceParams;
exports.IsosurfaceRepresentation = IsosurfaceRepresentation;
const param_definition_1 = require("../../mol-util/param-definition");
const volume_1 = require("../../mol-model/volume");
const mesh_1 = require("../../mol-geo/geometry/mesh/mesh");
const algorithm_1 = require("../../mol-geo/util/marching-cubes/algorithm");
const representation_1 = require("./representation");
const location_iterator_1 = require("../../mol-geo/util/location-iterator");
const location_1 = require("../../mol-model/location");
const lines_1 = require("../../mol-geo/geometry/lines/lines");
const representation_2 = require("../representation");
const loci_1 = require("../../mol-model/loci");
const int_1 = require("../../mol-data/int");
const linear_algebra_1 = require("../../mol-math/linear-algebra");
const array_1 = require("../../mol-util/array");
const util_1 = require("./util");
const texture_mesh_1 = require("../../mol-geo/geometry/texture-mesh/texture-mesh");
const isosurface_1 = require("../../mol-gl/compute/marching-cubes/isosurface");
const custom_property_1 = require("../../mol-model/custom-property");
const base_1 = require("../../mol-geo/geometry/base");
const value_cell_1 = require("../../mol-util/value-cell");
exports.VolumeIsosurfaceParams = {
isoValue: volume_1.Volume.IsoValueParam,
};
exports.VolumeIsosurfaceTextureParams = {
isoValue: volume_1.Volume.IsoValueParam,
tryUseGpu: param_definition_1.ParamDefinition.Boolean(true),
gpuDataType: param_definition_1.ParamDefinition.Select('byte', param_definition_1.ParamDefinition.arrayToOptions(['byte', 'float', 'halfFloat']), { hideIf: p => !p.tryUseGpu }),
};
function gpuSupport(webgl) {
return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
}
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 } = (0, util_1.getVolumeTexture2dLayout)(gridDim, Padding);
return powerOfTwoSize <= webgl.maxTextureSize / 2;
}
function IsosurfaceVisual(materialId, volume, key, props, webgl) {
if (props.tryUseGpu && webgl && gpuSupport(webgl) && suitableForGpu(volume, webgl)) {
return IsosurfaceTextureMeshVisual(materialId);
}
return IsosurfaceMeshVisual(materialId);
}
function getLoci(volume, props) {
return volume_1.Volume.Isosurface.Loci(volume, props.isoValue);
}
function getIsosurfaceLoci(pickingId, volume, key, props, id) {
const { objectId, groupId } = pickingId;
if (id === objectId) {
const granularity = volume_1.Volume.PickingGranularity.get(volume);
if (granularity === 'volume') {
return volume_1.Volume.Loci(volume);
}
else if (granularity === 'object') {
return volume_1.Volume.Isosurface.Loci(volume, props.isoValue);
}
else {
return volume_1.Volume.Cell.Loci(volume, int_1.Interval.ofSingleton(groupId));
}
}
return loci_1.EmptyLoci;
}
function eachIsosurface(loci, volume, key, props, apply) {
return (0, util_1.eachVolumeLoci)(loci, volume, { isoValue: props.isoValue }, apply);
}
//
async function createVolumeIsosurfaceMesh(ctx, volume, key, theme, props, mesh) {
ctx.runtime.update({ message: 'Marching cubes...' });
const ids = (0, array_1.fillSerial)(new Int32Array(volume.grid.cells.data.length));
const surface = await (0, algorithm_1.computeMarchingCubesMesh)({
isoLevel: volume_1.Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue,
scalarField: volume.grid.cells,
idField: linear_algebra_1.Tensor.create(volume.grid.cells.space, linear_algebra_1.Tensor.Data1(ids))
}, mesh).runAsChild(ctx.runtime);
const transform = volume_1.Grid.getGridToCartesianTransform(volume.grid);
mesh_1.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_1.Mesh.uniformTriangleGroup(surface, false);
value_cell_1.ValueCell.updateIfChanged(surface.varyingGroup, false);
}
else {
value_cell_1.ValueCell.updateIfChanged(surface.varyingGroup, true);
}
surface.setBoundingSphere(volume_1.Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
return surface;
}
exports.IsosurfaceMeshParams = {
...mesh_1.Mesh.Params,
...texture_mesh_1.TextureMesh.Params,
...exports.VolumeIsosurfaceParams,
...exports.VolumeIsosurfaceTextureParams,
quality: { ...mesh_1.Mesh.Params.quality, isEssential: false },
};
function IsosurfaceMeshVisual(materialId) {
return (0, representation_1.VolumeVisual)({
defaultProps: param_definition_1.ParamDefinition.getDefaultValues(exports.IsosurfaceMeshParams),
createGeometry: createVolumeIsosurfaceMesh,
createLocationIterator: (volume) => (0, location_iterator_1.LocationIterator)(volume.grid.cells.data.length, 1, 1, () => location_1.NullLocation),
getLoci: getIsosurfaceLoci,
eachLocation: eachIsosurface,
setUpdateState: (state, volume, newProps, currentProps) => {
if (!volume_1.Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats))
state.createGeometry = true;
},
geometryUtils: mesh_1.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 = (0, custom_property_1.CustomPropertyDescriptor)({ name });
function clear(volume) {
delete volume._propertyData[name];
}
VolumeIsosurfaceTexture.clear = clear;
function get(volume, webgl, props) {
var _a;
const transform = volume_1.Grid.getGridToCartesianTransform(volume.grid);
const gridDimension = linear_algebra_1.Vec3.clone(volume.grid.cells.space.dimensions);
const { width, height, powerOfTwoSize: texDim } = (0, util_1.getVolumeTexture2dLayout)(gridDimension, Padding);
const gridTexDim = linear_algebra_1.Vec3.create(width, height, 0);
const gridTexScale = linear_algebra_1.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 = props.gpuDataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.gpuDataType;
if (((_a = volume._propertyData[name]) === null || _a === void 0 ? void 0 : _a.dataType) !== dataType) {
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 };
texture.define(texDim, texDim);
// load volume into sub-section of texture
texture.load((0, util_1.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,
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 texture_mesh_1.TextureMesh.createEmpty(textureMesh);
}
const { max, min } = volume.grid.stats;
const diff = max - min;
const value = volume_1.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_1.Volume.getBoundingSphere(volume); // getting isosurface bounding-sphere is too expensive here
const create = (textureMesh) => {
const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, webgl, props);
const buffer = textureMesh === null || textureMesh === void 0 ? void 0 : textureMesh.doubleBuffer.get();
const gv = (0, isosurface_1.extractIsosurface)(webgl, texture, gridDimension, gridTexDim, 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 texture_mesh_1.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;
}
function IsosurfaceTextureMeshVisual(materialId) {
return (0, representation_1.VolumeVisual)({
defaultProps: param_definition_1.ParamDefinition.getDefaultValues(exports.IsosurfaceMeshParams),
createGeometry: createVolumeIsosurfaceTextureMesh,
createLocationIterator: (volume) => (0, location_iterator_1.LocationIterator)(volume.grid.cells.data.length, 1, 1, () => location_1.NullLocation),
getLoci: getIsosurfaceLoci,
eachLocation: eachIsosurface,
setUpdateState: (state, volume, newProps, currentProps) => {
if (!volume_1.Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats))
state.createGeometry = true;
if (newProps.gpuDataType !== currentProps.gpuDataType)
state.createGeometry = true;
},
geometryUtils: texture_mesh_1.TextureMesh.Utils,
mustRecreate: (volumeKey, props, webgl) => {
return !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);
}
//
async function createVolumeIsosurfaceWireframe(ctx, volume, key, theme, props, lines) {
ctx.runtime.update({ message: 'Marching cubes...' });
const ids = (0, array_1.fillSerial)(new Int32Array(volume.grid.cells.data.length));
const wireframe = await (0, algorithm_1.computeMarchingCubesLines)({
isoLevel: volume_1.Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue,
scalarField: volume.grid.cells,
idField: linear_algebra_1.Tensor.create(volume.grid.cells.space, linear_algebra_1.Tensor.Data1(ids))
}, lines).runAsChild(ctx.runtime);
const transform = volume_1.Grid.getGridToCartesianTransform(volume.grid);
lines_1.Lines.transform(wireframe, transform);
wireframe.setBoundingSphere(volume_1.Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
return wireframe;
}
exports.IsosurfaceWireframeParams = {
...lines_1.Lines.Params,
...exports.VolumeIsosurfaceParams,
quality: { ...lines_1.Lines.Params.quality, isEssential: false },
sizeFactor: param_definition_1.ParamDefinition.Numeric(3, { min: 0, max: 10, step: 0.1 }),
};
function IsosurfaceWireframeVisual(materialId) {
return (0, representation_1.VolumeVisual)({
defaultProps: param_definition_1.ParamDefinition.getDefaultValues(exports.IsosurfaceWireframeParams),
createGeometry: createVolumeIsosurfaceWireframe,
createLocationIterator: (volume) => (0, location_iterator_1.LocationIterator)(volume.grid.cells.data.length, 1, 1, () => location_1.NullLocation),
getLoci: getIsosurfaceLoci,
eachLocation: eachIsosurface,
setUpdateState: (state, volume, newProps, currentProps) => {
if (!volume_1.Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats))
state.createGeometry = true;
},
geometryUtils: lines_1.Lines.Utils
}, materialId);
}
//
const IsosurfaceVisuals = {
'solid': (ctx, getParams) => (0, representation_1.VolumeRepresentation)('Isosurface mesh', ctx, getParams, IsosurfaceVisual, getLoci),
'wireframe': (ctx, getParams) => (0, representation_1.VolumeRepresentation)('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual, getLoci),
};
exports.IsosurfaceParams = {
...exports.IsosurfaceMeshParams,
...exports.IsosurfaceWireframeParams,
visuals: param_definition_1.ParamDefinition.MultiSelect(['solid'], param_definition_1.ParamDefinition.objectToOptions(IsosurfaceVisuals)),
bumpFrequency: param_definition_1.ParamDefinition.Numeric(1, { min: 0, max: 10, step: 0.1 }, base_1.BaseGeometry.ShadingCategory),
};
function getIsosurfaceParams(ctx, volume) {
const p = param_definition_1.ParamDefinition.clone(exports.IsosurfaceParams);
p.isoValue = volume_1.Volume.createIsoValueParam(volume_1.Volume.IsoValue.relative(2), volume.grid.stats);
return p;
}
function IsosurfaceRepresentation(ctx, getParams) {
return representation_2.Representation.createMulti('Isosurface', ctx, getParams, representation_2.Representation.StateBuilder, IsosurfaceVisuals);
}
exports.IsosurfaceRepresentationProvider = (0, representation_1.VolumeRepresentationProvider)({
name: 'isosurface',
label: 'Isosurface',
description: 'Displays a triangulated isosurface of volumetric data.',
factory: IsosurfaceRepresentation,
getParams: getIsosurfaceParams,
defaultValues: param_definition_1.ParamDefinition.getDefaultValues(exports.IsosurfaceParams),
defaultColorTheme: { name: 'uniform' },
defaultSizeTheme: { name: 'uniform' },
isApplicable: (volume) => !volume_1.Volume.isEmpty(volume) && !volume_1.Volume.Segmentation.get(volume)
});
;