Merge pull request #1708 from molstar/volume-improvements

Volume improvements
This commit is contained in:
Alexander Rose
2025-11-16 10:10:11 -08:00
committed by GitHub
26 changed files with 191 additions and 87 deletions

View File

@@ -18,6 +18,10 @@ Note that since we don't clearly distinguish between a public and private interf
- Fix `direct-volume` not drawn in illumination mode
- Fix default trackball animated spin speed
- Use `PluginCommands` to set canvas3d props in camera behavior
- Volume improvements
- Add `Volume.periodicity`
- Wrap isosurfaces for periodic volumes
- Fix dimensions for slices
- Add support for Input Method Editor (IME) to text params input
- Update `guessCifVariant` to detect density files not generated by the VolumeServer

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env node
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* 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>
*/
import * as fs from 'fs';
@@ -38,7 +39,7 @@ function print(volume: Volume) {
}
async function doMesh(volume: Volume, filename: string) {
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5), wrap: 'auto' })).run();
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
// Export the mesh in OBJ format.

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -38,6 +38,7 @@ const IsosurfaceSchema = {
uGridDim: UniformSpec('v3'),
uGridTexDim: UniformSpec('v3'),
uGridDataDim: UniformSpec('v3'),
uGridTransform: UniformSpec('m4'),
uGridTransformAdjoint: UniformSpec('m3'),
uScale: UniformSpec('v2'),
@@ -54,7 +55,7 @@ function valueChannel(ctx: WebGLContext, volumeData: Texture) {
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
}
function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean): ComputeRenderable<IsosurfaceValues> {
function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridDataDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean): ComputeRenderable<IsosurfaceValues> {
if (ctx.namedComputeRenderables[IsosurfaceName]) {
const v = ctx.namedComputeRenderables[IsosurfaceName].values as IsosurfaceValues;
@@ -71,6 +72,7 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
ValueCell.update(v.uGridDim, gridDim);
ValueCell.update(v.uGridTexDim, gridTexDim);
ValueCell.update(v.uGridDataDim, gridDataDim);
ValueCell.update(v.uGridTransform, transform);
ValueCell.update(v.uGridTransformAdjoint, Mat3.adjointFromMat4(Mat3(), transform));
ValueCell.update(v.uScale, scale);
@@ -81,12 +83,12 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
ctx.namedComputeRenderables[IsosurfaceName].update();
} else {
ctx.namedComputeRenderables[IsosurfaceName] = createIsosurfaceRenderable(ctx, activeVoxelsPyramid, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder, constantGroup);
ctx.namedComputeRenderables[IsosurfaceName] = createIsosurfaceRenderable(ctx, activeVoxelsPyramid, activeVoxelsBase, volumeData, gridDim, gridTexDim, gridDataDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder, constantGroup);
}
return ctx.namedComputeRenderables[IsosurfaceName];
}
function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean) {
function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridDataDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean) {
// console.log('uSize', Math.pow(2, levels))
const values: IsosurfaceValues = {
...QuadValues,
@@ -105,6 +107,7 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
uGridDim: ValueCell.create(gridDim),
uGridTexDim: ValueCell.create(gridTexDim),
uGridDataDim: ValueCell.create(gridDataDim),
uGridTransform: ValueCell.create(transform),
uGridTransformAdjoint: ValueCell.create(Mat3.adjointFromMat4(Mat3(), transform)),
uScale: ValueCell.create(scale),
@@ -132,7 +135,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
state.clearColor(0, 0, 0, 0);
}
export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, gridDataDim: Vec3, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
const { drawBuffers } = ctx.extensions;
if (!drawBuffers) throw new Error('need WebGL draw buffers');
@@ -189,7 +192,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
groupTexture.attachFramebuffer(framebuffer, 1);
normalTexture.attachFramebuffer(framebuffer, 2);
const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder, constantGroup);
const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, gridDataDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder, constantGroup);
ctx.state.currentRenderItemId = -1;
framebuffer.bind();
@@ -225,11 +228,11 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
*
* Implementation based on http://www.miaumiau.cat/2016/10/stream-compaction-in-webgl/
*/
export function extractIsosurface(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridTexScale: Vec2, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
export function extractIsosurface(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridDataDim: Vec3, gridTexScale: Vec2, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
if (isTimingMode) ctx.timer.mark('extractIsosurface');
const activeVoxelsTex = calcActiveVoxels(ctx, volumeData, gridDim, gridTexDim, isoValue, gridTexScale);
const compacted = createHistogramPyramid(ctx, activeVoxelsTex, gridTexScale, gridTexDim);
const gv = createIsosurfaceBuffers(ctx, activeVoxelsTex, volumeData, compacted, gridDim, gridTexDim, transform, isoValue, invert, packedGroup, axisOrder, constantGroup, vertexTexture, groupTexture, normalTexture);
const gv = createIsosurfaceBuffers(ctx, activeVoxelsTex, volumeData, compacted, gridDim, gridTexDim, gridDataDim, transform, isoValue, invert, packedGroup, axisOrder, constantGroup, vertexTexture, groupTexture, normalTexture);
if (isTimingMode) ctx.timer.markEnd('extractIsosurface');
return gv;

View File

@@ -22,6 +22,7 @@ uniform bool uInvert;
uniform vec3 uGridDim;
uniform vec3 uGridTexDim;
uniform vec3 uGridDataDim;
uniform mat4 uGridTransform;
uniform mat3 uGridTransformAdjoint;
@@ -93,20 +94,19 @@ vec4 baseVoxel(vec2 pos) {
}
vec4 getGroup(const in vec3 p) {
vec3 gridDim = uGridDim - vec3(1.0, 1.0, 0.0); // remove xy padding
// note that we swap x and z because the texture is flipped around y
#if defined(dAxisOrder_012)
float group = p.z + p.y * gridDim.z + p.x * gridDim.z * gridDim.y; // 210
float group = p.z + p.y * uGridDataDim.z + p.x * uGridDataDim.z * uGridDataDim.y; // 210
#elif defined(dAxisOrder_021)
float group = p.y + p.z * gridDim.y + p.x * gridDim.y * gridDim.z; // 120
float group = p.y + p.z * uGridDataDim.y + p.x * uGridDataDim.y * uGridDataDim.z; // 120
#elif defined(dAxisOrder_102)
float group = p.z + p.x * gridDim.z + p.y * gridDim.z * gridDim.x; // 201
float group = p.z + p.x * uGridDataDim.z + p.y * uGridDataDim.z * uGridDataDim.x; // 201
#elif defined(dAxisOrder_120)
float group = p.x + p.z * gridDim.x + p.y * gridDim.x * gridDim.z; // 021
float group = p.x + p.z * uGridDataDim.x + p.y * uGridDataDim.x * uGridDataDim.z; // 021
#elif defined(dAxisOrder_201)
float group = p.y + p.x * gridDim.y + p.z * gridDim.y * gridDim.x; // 102
float group = p.y + p.x * uGridDataDim.y + p.z * uGridDataDim.y * uGridDataDim.x; // 102
#elif defined(dAxisOrder_210)
float group = p.x + p.y * gridDim.x + p.z * gridDim.x * gridDim.y; // 012
float group = p.x + p.y * uGridDataDim.x + p.z * uGridDataDim.x * uGridDataDim.y; // 012
#endif
return vec4(group > 16777215.5 ? vec3(1.0) : packIntToRGB(group), 1.0);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* 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>
@@ -36,6 +36,7 @@ export type DensityTextureData = {
bbox: Box3D,
gridDim: Vec3,
gridTexDim: Vec3
gridDataDim: Vec3
gridTexScale: Vec2
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -99,8 +99,8 @@ export function GaussianDensityTexture3d(webgl: WebGLContext, position: Position
return finalizeGaussianDensityTexture(data);
}
function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor, resolution, maxRadius }: _GaussianDensityTextureData): GaussianDensityTextureData {
return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridTexScale, radiusFactor, resolution, maxRadius };
function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridDataDim, gridTexScale, radiusFactor, resolution, maxRadius }: _GaussianDensityTextureData): GaussianDensityTextureData {
return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridDataDim, gridTexScale, radiusFactor, resolution, maxRadius };
}
function getTransform(scale: Vec3, bbox: Box3D) {
@@ -118,6 +118,7 @@ type _GaussianDensityTextureData = {
bbox: Box3D,
gridDim: Vec3,
gridTexDim: Vec3
gridDataDim: Vec3
gridTexScale: Vec2
radiusFactor: number
resolution: number
@@ -206,7 +207,7 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
// printTextureImage(readTexture(webgl, minDistTex), { scale: 0.75 });
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor, resolution, maxRadius };
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridDataDim: dim, gridTexScale, radiusFactor, resolution, maxRadius };
}
function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
@@ -262,7 +263,7 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
setupGroupIdRendering(webgl, renderable);
render(texture, false);
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridTexScale, radiusFactor, resolution, maxRadius };
return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridDataDim: dim, gridTexScale, radiusFactor, resolution, maxRadius };
}
//

View File

@@ -151,7 +151,7 @@ namespace Mat3 {
}
export function hasNaN(m: Mat3) {
for (let i = 0; i < 9; i++) if (isNaN(m[i])) return true;
for (let i = 0; i < 9; i++) if (Number.isNaN(m[i])) return true;
return false;
}

View File

@@ -111,7 +111,7 @@ namespace Mat4 {
}
export function hasNaN(m: Mat4) {
for (let i = 0; i < 16; i++) if (isNaN(m[i])) return true;
for (let i = 0; i < 16; i++) if (Number.isNaN(m[i])) return true;
return false;
}

View File

@@ -59,7 +59,7 @@ namespace Quat {
}
export function hasNaN(q: Quat) {
return isNaN(q[0]) || isNaN(q[1]) || isNaN(q[2]) || isNaN(q[3]);
return Number.isNaN(q[0]) || Number.isNaN(q[1]) || Number.isNaN(q[2]) || Number.isNaN(q[3]);
}
export function create(x: number, y: number, z: number, w: number) {

View File

@@ -55,7 +55,7 @@ namespace Vec2 {
}
export function hasNaN(a: Vec2) {
return isNaN(a[0]) || isNaN(a[1]);
return Number.isNaN(a[0]) || Number.isNaN(a[1]);
}
export function toArray<T extends NumberArray>(a: Vec2, out: T, offset: number) {

View File

@@ -52,8 +52,12 @@ export namespace Vec3 {
return _isFinite(a[0]) && _isFinite(a[1]) && _isFinite(a[2]);
}
export function isInteger(a: Vec3): boolean {
return Number.isInteger(a[0]) && Number.isInteger(a[1]) && Number.isInteger(a[2]);
}
export function hasNaN(a: Vec3) {
return isNaN(a[0]) || isNaN(a[1]) || isNaN(a[2]);
return Number.isNaN(a[0]) || Number.isNaN(a[1]) || Number.isNaN(a[2]);
}
export function setNaN(out: Vec3) {

View File

@@ -67,7 +67,7 @@ namespace Vec4 {
}
export function hasNaN(a: Vec4) {
return isNaN(a[0]) || isNaN(a[1]) || isNaN(a[2]) || isNaN(a[3]);
return Number.isNaN(a[0]) || Number.isNaN(a[1]) || Number.isNaN(a[2]) || Number.isNaN(a[3]);
}
export function toArray<T extends NumberArray>(a: Vec4, out: T, offset: number) {

View File

@@ -4,7 +4,7 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Volume } from '../../mol-model/volume';
import { Grid, Volume } from '../../mol-model/volume';
import { Task } from '../../mol-task';
import { SpacegroupCell, Box3D } from '../../mol-math/geometry';
import { Mat4, Tensor, Vec3 } from '../../mol-math/linear-algebra';
@@ -71,19 +71,22 @@ export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3, of
// always calculate stats when all stats related values are zero
const calcStats = header.AMIN === 0 && header.AMAX === 0 && header.AMEAN === 0 && header.ARMS === 0;
const volgrid: Grid = {
transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3(), origin_frac, dimensions_frac)) },
cells: data,
stats: {
min: (Number.isNaN(header.AMIN) || calcStats) ? arrayMin(values) : header.AMIN,
max: (Number.isNaN(header.AMAX) || calcStats) ? arrayMax(values) : header.AMAX,
mean: (Number.isNaN(header.AMEAN) || calcStats) ? arrayMean(values) : header.AMEAN,
sigma: (Number.isNaN(header.ARMS) || header.ARMS === 0) ? arrayRms(values) : header.ARMS
},
};
return {
label: params?.label,
entryId: params?.entryId,
grid: {
transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)) },
cells: data,
stats: {
min: (isNaN(header.AMIN) || calcStats) ? arrayMin(values) : header.AMIN,
max: (isNaN(header.AMAX) || calcStats) ? arrayMax(values) : header.AMAX,
mean: (isNaN(header.AMEAN) || calcStats) ? arrayMean(values) : header.AMEAN,
sigma: (isNaN(header.ARMS) || header.ARMS === 0) ? arrayRms(values) : header.ARMS
},
},
periodicity: Vec3.isInteger(dimensions_frac) ? 'xyz' : 'none',
grid: volgrid,
instances: [{ transform: Mat4.identity() }],
sourceData: Ccp4Format.create(source),
customProperties: new CustomProperties(),

View File

@@ -2,6 +2,7 @@
* 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>
*/
import { DensityServer_Data_Database } from '../../mol-io/reader/cif/schema/density-server';
@@ -38,6 +39,7 @@ export function volumeFromDensityServerData(source: DensityServer_Data_Database,
return {
label: params?.label,
entryId: params?.entryId,
periodicity: Vec3.isInteger(dimensions) ? 'xyz' : 'none',
grid: {
transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin, Vec3.add(Vec3.zero(), origin, dimensions)) },
cells: data,

View File

@@ -36,6 +36,7 @@ export function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3, la
return {
label: params?.label,
entryId: params?.entryId,
periodicity: Vec3.isInteger(dimensions_frac) ? 'xyz' : 'none',
grid: {
transform: { kind: 'spacegroup', cell, fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)) },
cells: data,

View File

@@ -31,7 +31,7 @@ namespace Grid {
export type Transform = { kind: 'spacegroup', cell: SpacegroupCell, fractionalBox: Box3D } | { kind: 'matrix', matrix: Mat4 }
const _scale = Mat4.zero(), _translate = Mat4.zero();
const _scale = Mat4(), _translate = Mat4();
export function getGridToCartesianTransform(grid: Grid) {
if (grid.transform.kind === 'matrix') {
return Mat4.copy(Mat4(), grid.transform.matrix);
@@ -39,9 +39,9 @@ namespace Grid {
if (grid.transform.kind === 'spacegroup') {
const { cells: { space } } = grid;
const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3.zero(), Box3D.size(Vec3.zero(), grid.transform.fractionalBox), Vec3.ofArray(space.dimensions)));
const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3(), Box3D.size(Vec3(), grid.transform.fractionalBox), Vec3.ofArray(space.dimensions)));
const translate = Mat4.fromTranslation(_translate, grid.transform.fractionalBox.min);
return Mat4.mul3(Mat4.zero(), grid.transform.cell.fromFractional, translate, scale);
return Mat4.mul3(Mat4(), grid.transform.cell.fromFractional, translate, scale);
}
return Mat4.identity();

View File

@@ -26,6 +26,7 @@ export interface Volume {
transform: Mat4
}>
readonly sourceData: ModelFormat
readonly periodicity?: 'none' | 'xyz'
// TODO use...
customProperties: CustomProperties

View File

@@ -247,7 +247,7 @@ function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, struct
const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
const buffer = textureMesh?.doubleBuffer.get();
const gv = extractIsosurface(webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
const gv = extractIsosurface(webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridDataDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
if (isTimingMode) webgl.timer.markEnd('createGaussianSurfaceTextureMesh');
const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, densityTextureData.maxRadius);
@@ -333,7 +333,7 @@ function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, structure
const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
const buffer = textureMesh?.doubleBuffer.get();
const gv = extractIsosurface(webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
const gv = extractIsosurface(webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridDataDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
if (isTimingMode) webgl.timer.markEnd('createStructureGaussianSurfaceTextureMesh');
const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, densityTextureData.maxRadius);

View File

@@ -20,7 +20,7 @@ import { EmptyLoci, Loci } from '../../mol-model/loci';
import { Interval, OrderedSet } from '../../mol-data/int';
import { Tensor, Vec2, Vec3 } from '../../mol-math/linear-algebra';
import { fillSerial } from '../../mol-util/array';
import { createVolumeCellLocationIterator, createVolumeTexture2d, eachVolumeLoci, getVolumeTexture2dLayout } from './util';
import { createVolumeCellLocationIterator, createVolumeTexture2d, createWrappedVolume, 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';
@@ -31,22 +31,29 @@ import { ValueCell } from '../../mol-util/value-cell';
export const VolumeIsosurfaceParams = {
isoValue: Volume.IsoValueParam,
wrap: PD.Select('auto', PD.arrayToOptions(['off', 'on', 'auto'] as const)),
};
export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
export const VolumeIsosurfaceTextureParams = {
isoValue: Volume.IsoValueParam,
...VolumeIsosurfaceParams,
tryUseGpu: PD.Boolean(true),
gpuDataType: PD.Select('byte', PD.arrayToOptions(['byte', 'float', 'halfFloat'] as const), { hideIf: p => !p.tryUseGpu }),
};
export type VolumeIsosurfaceGpuParams = typeof VolumeIsosurfaceTextureParams
export type VolumeIsosurfaceGpuProps = PD.Values<VolumeIsosurfaceGpuParams>
export type VolumeIsosurfaceTextureParams = typeof VolumeIsosurfaceTextureParams
export type VolumeIsosurfaceTextureProps = PD.Values<VolumeIsosurfaceTextureParams>
function gpuSupport(webgl: WebGLContext) {
return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
}
function shouldWrap(volume: Volume, wrap: VolumeIsosurfaceProps['wrap']) {
if (wrap === 'on') return true;
if (wrap === 'off') return false;
return volume.periodicity === 'xyz';
}
const Padding = 1;
function suitableForGpu(volume: Volume, webgl: WebGLContext) {
@@ -97,12 +104,17 @@ export function eachIsosurface(loci: Loci, volume: Volume, key: number, props: V
export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceProps, mesh?: Mesh) {
ctx.runtime.update({ message: 'Marching cubes...' });
let cells = volume.grid.cells;
if (shouldWrap(volume, props.wrap)) {
cells = createWrappedVolume(volume).grid.cells;
}
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))
scalarField: cells,
idField: Tensor.create(cells.space, Tensor.Data1(ids))
}, mesh).runAsChild(ctx.runtime);
const transform = Grid.getGridToCartesianTransform(volume.grid);
@@ -138,7 +150,10 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
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;
state.createGeometry = (
!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) ||
newProps.wrap !== currentProps.wrap
);
},
geometryUtils: Mesh.Utils,
mustRecreate: (volumekey: VolumeKey, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
@@ -155,11 +170,15 @@ namespace VolumeIsosurfaceTexture {
export function clear(volume: Volume) {
delete volume._propertyData[name];
}
export function get(volume: Volume, webgl: WebGLContext, props: VolumeIsosurfaceGpuProps) {
export function get(volume: Volume, webgl: WebGLContext, props: VolumeIsosurfaceTextureProps) {
const { gpuDataType } = props;
const wrap = shouldWrap(volume, props.wrap);
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 gridDataDim = Vec3.subScalar(Vec3(), gridDimension, wrap ? 1 : 0);
const gridTexScale = Vec2.create(width / texDim, height / texDim);
// console.log({ texDim, width, height, gridDimension });
@@ -167,15 +186,15 @@ namespace VolumeIsosurfaceTexture {
throw new Error('volume too large for gpu isosurface extraction');
}
const dataType = props.gpuDataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.gpuDataType;
const dataType = gpuDataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : gpuDataType;
if (volume._propertyData[name]?.dataType !== dataType) {
if (volume._propertyData[name]?.dataType !== dataType || volume._propertyData[name]?.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 };
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);
@@ -191,12 +210,13 @@ namespace VolumeIsosurfaceTexture {
transform,
gridDimension,
gridTexDim,
gridDataDim,
gridTexScale
};
}
}
function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceGpuProps, textureMesh?: TextureMesh) {
function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceTextureProps, textureMesh?: TextureMesh) {
const { webgl } = ctx;
if (!webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
@@ -204,6 +224,10 @@ function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, k
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;
@@ -214,10 +238,10 @@ function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, k
const boundingSphere = Volume.getBoundingSphere(volume); // getting isosurface bounding-sphere is too expensive here
const create = (textureMesh?: TextureMesh) => {
const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, webgl, props);
const { texture, gridDimension, gridTexDim, gridDataDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, webgl, props);
const buffer = textureMesh?.doubleBuffer.get();
const gv = extractIsosurface(webgl, texture, gridDimension, gridTexDim, gridTexScale, transform, isoLevel, value < 0, false, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
const gv = extractIsosurface(webgl, texture, gridDimension, gridTexDim, gridDataDim, gridTexScale, transform, isoLevel, value < 0, false, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
return TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, boundingSphere, textureMesh);
};
@@ -240,8 +264,11 @@ export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<Is
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;
if (newProps.gpuDataType !== currentProps.gpuDataType) state.createGeometry = true;
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: VolumeKey, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
@@ -261,12 +288,17 @@ export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<Is
export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceProps, lines?: Lines) {
ctx.runtime.update({ message: 'Marching cubes...' });
let cells = volume.grid.cells;
if (shouldWrap(volume, props.wrap)) {
cells = createWrappedVolume(volume).grid.cells;
}
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))
scalarField: cells,
idField: Tensor.create(cells.space, Tensor.Data1(ids))
}, lines).runAsChild(ctx.runtime);
const transform = Grid.getGridToCartesianTransform(volume.grid);
@@ -293,7 +325,10 @@ export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<Isos
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;
state.createGeometry = (
!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats) ||
newProps.wrap !== currentProps.wrap
);
},
geometryUtils: Lines.Utils
}, materialId);

View File

@@ -222,6 +222,7 @@ function getSegmentTexture(volume: Volume, segment: Volume.SegmentIndex, webgl:
const gridDimension = Box3D.size(Vec3(), bbox);
const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, Padding);
const gridTexDim = Vec3.create(width, height, 0);
const gridDataDim = Vec3.clone(gridDimension);
const gridTexScale = Vec2.create(width / texDim, height / texDim);
// console.log({ texDim, width, height, gridDimension });
@@ -247,6 +248,7 @@ function getSegmentTexture(volume: Volume, segment: Volume.SegmentIndex, webgl:
transform,
gridDimension,
gridTexDim,
gridDataDim,
gridTexScale
};
}
@@ -258,11 +260,11 @@ async function createVolumeSegmentTextureMesh(ctx: VisualContext, volume: Volume
return TextureMesh.createEmpty(textureMesh);
}
const { texture, gridDimension, gridTexDim, gridTexScale, transform } = getSegmentTexture(volume, segment, ctx.webgl);
const { texture, gridDimension, gridTexDim, gridDataDim, gridTexScale, transform } = getSegmentTexture(volume, segment, 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, 0.5, false, false, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
const gv = extractIsosurface(ctx.webgl, texture, gridDimension, gridTexDim, gridDataDim, gridTexScale, transform, 0.5, false, false, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
const groupCount = volume.grid.cells.data.length;
const instances = Interval.ofLength(volume.instances.length as Volume.InstanceIndex);

View File

@@ -92,9 +92,9 @@ function getFrame(volume: Volume, props: SliceProps) {
const cartnToGrid = Mat4.invert(Mat4(), gridToCartn);
const [nx, ny, nz] = volume.grid.cells.space.dimensions;
const a = nx - 1;
const b = ny - 1;
const c = nz - 1;
const a = nx;
const b = ny;
const c = nz;
const dirA = Vec3.create(a, 0, 0);
const dirB = Vec3.create(0, b, 0);
@@ -287,9 +287,9 @@ async function createPlaneImage(ctx: VisualContext, volume: Volume, key: number,
const cartnToGrid = Mat4.invert(Mat4(), gridToCartn);
const [mx, my, mz] = volume.grid.cells.space.dimensions;
const a = mx - 1;
const b = my - 1;
const c = mz - 1;
const a = mx;
const b = my;
const c = mz;
const resolution = Math.max(a, b, c) / Math.max(mx, my, mz);
const scaleFactor = 1 / resolution;
@@ -399,32 +399,32 @@ function getSliceInfo(grid: Grid, props: SliceProps) {
let [nx, ny, nz] = space.dimensions;
if (dim === 'x') {
x = index, y = ny - 1, z = nz - 1;
x = index, y = ny, z = nz;
width = nz, height = ny;
x0 = x, nx = x0 + 1;
} else if (dim === 'y') {
x = nx - 1, y = index, z = nz - 1;
x = nx, y = index, z = nz;
width = nz, height = nx;
y0 = y, ny = y0 + 1;
} else if (dim === 'z') {
x = nx - 1, y = ny - 1, z = index;
x = nx, y = ny, z = index;
width = nx, height = ny;
z0 = z, nz = z0 + 1;
} else if (dim === 'relativeX') {
x = getRelativeIndex(nx, index);
y = ny - 1;
z = nz - 1;
y = ny;
z = nz;
width = nz, height = ny;
x0 = x, nx = x0 + 1;
} else if (dim === 'relativeY') {
x = nx - 1;
x = nx;
y = getRelativeIndex(ny, index);
z = nz - 1;
z = nz;
width = nz, height = nx;
y0 = y, ny = y0 + 1;
} else /* if (dim === 'relativeZ') */ {
x = nx - 1;
y = ny - 1;
x = nx;
y = ny;
z = getRelativeIndex(nz, index);
width = nx, height = ny;
z0 = z, nz = z0 + 1;

View File

@@ -4,7 +4,7 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Volume } from '../../mol-model/volume';
import { Grid, Volume } from '../../mol-model/volume';
import { Loci } from '../../mol-model/loci';
import { Interval, OrderedSet, SortedArray } from '../../mol-data/int';
import { equalEps } from '../../mol-math/linear-algebra/3d/common';
@@ -15,6 +15,7 @@ import { Box3D } from '../../mol-math/geometry';
import { toHalfFloat } from '../../mol-util/number-conversion';
import { clamp } from '../../mol-math/interpolate';
import { LocationIterator } from '../../mol-geo/util/location-iterator';
import { Tensor } from '../../mol-math/linear-algebra/tensor';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3set = Vec3.set;
@@ -351,3 +352,48 @@ export function createSegmentTexture2d(volume: Volume, set: number[], bbox: Box3
return textureImage;
}
/**
* Create a new volume that is wrapped by one cell in all dimensions.
* Reuses the original volume grid data with new data accessors.
* Only intended for isosurface construction.
*/
export function createWrappedVolume(volume: Volume): Volume {
const { grid } = volume;
const { space } = grid.cells;
const { get, set, add, dataOffset } = space;
const [xn, yn, zn] = space.dimensions as Vec3;
const _dimensions = Vec3.create(xn + 1, yn + 1, zn + 1);
const _get = (data: Tensor.Data, x: number, y: number, z: number) => get(data, x % xn, y % yn, z % zn);
const _set = (data: Tensor.Data, x: number, y: number, z: number, d: number) => set(data, x % xn, y % yn, z % zn, d);
const _add = (data: Tensor.Data, x: number, y: number, z: number, d: number) => add(data, x % xn, y % yn, z % zn, d);
const _dataOffset = (x: number, y: number, z: number) => dataOffset(x % xn, y % yn, z % zn);
const _space: Tensor.Space = {
...space,
dimensions: _dimensions,
get: _get,
set: _set,
add: _add,
dataOffset: _dataOffset,
};
const matrix = Grid.getGridToCartesianTransform(volume.grid);
const _transform: Grid.Transform = { kind: 'matrix', matrix };
const _grid: Grid = {
...grid,
transform: _transform,
cells: {
...grid.cells,
space: _space
}
};
return {
...volume,
grid: _grid
};
}

View File

@@ -105,7 +105,7 @@ export function ExternalVolumeColorTheme(ctx: ThemeDataContext, props: PD.Values
}
const value = getTrilinearlyInterpolated(position);
if (isNaN(value)) return defaultColor;
if (Number.isNaN(value)) return defaultColor;
if (usePalette) {
return (clamp((value - domain[0]) / (domain[1] - domain[0]), 0, 1) * ColorTheme.PaletteScale) as Color;

View File

@@ -87,7 +87,7 @@ export function VolumeValueColorTheme(ctx: ThemeDataContext, props: PD.Values<Vo
}
const value = getTrilinearlyInterpolated(location.position);
if (isNaN(value)) return props.defaultColor;
if (Number.isNaN(value)) return props.defaultColor;
return (clamp((value - domain[0]) / (domain[1] - domain[0]), 0, 1) * ColorTheme.PaletteScale) as Color;
};

View File

@@ -52,7 +52,7 @@ export function getArrayDigitCount(xs: ArrayLike<number>, maxDigits: number, del
export function isInteger(s: string) {
s = s.trim();
const n = parseInt(s, 10);
return isNaN(n) ? false : n.toString() === s;
return Number.isNaN(n) ? false : n.toString() === s;
}
export function getPrecision(v: number) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -85,7 +85,7 @@ async function init() {
console.timeEnd('gpu mc pyramid');
console.time('gpu mc vert');
const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoValue, false, true, Vec3.create(0, 1, 2), true);
const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridDataDim, densityTextureData.transform, isoValue, false, true, Vec3.create(0, 1, 2), true);
webgl.waitForGpuCommandsCompleteSync();
console.timeEnd('gpu mc vert');
console.timeEnd('gpu mc');