Support float and half-float data type

- direct-volume rendering
- GPU isosurface extraction
This commit is contained in:
Alexander Rose
2024-12-28 14:29:17 -08:00
parent 6e42c11f5e
commit 2bc9c6fb57
13 changed files with 330 additions and 72 deletions

View File

@@ -5,7 +5,7 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
- Volume UI improvements
- Volume UI improvements
- Render all volume entries instead of selecting them one-by-one
- Toggle visibility of all volumes
- More accessible iso value control
@@ -13,6 +13,7 @@ Note that since we don't clearly distinguish between a public and private interf
- MolViewSpec extension:
- Add validation for discriminated union params
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
- Support float and half-float data type for direct-volume rendering and GPU isosurface extraction
## [v4.10.0] - 2024-12-15

View File

@@ -49,6 +49,7 @@ export interface DirectVolume {
readonly cartnToUnit: ValueCell<Mat4>
readonly packedGroup: ValueCell<boolean>
readonly axisOrder: ValueCell<Vec3>
readonly dataType: ValueCell<'byte' | 'float' | 'halfFloat'>
/** Bounding sphere of the volume */
readonly boundingSphere: Sphere3D
@@ -57,10 +58,10 @@ export interface DirectVolume {
}
export namespace DirectVolume {
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume?: DirectVolume): DirectVolume {
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume?: DirectVolume): DirectVolume {
return directVolume ?
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume) :
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder);
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume) :
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType);
}
function hashCode(directVolume: DirectVolume) {
@@ -71,7 +72,7 @@ export namespace DirectVolume {
]);
}
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3): DirectVolume {
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat'): DirectVolume {
const boundingSphere = Sphere3D();
let currentHash = -1;
@@ -103,6 +104,7 @@ export namespace DirectVolume {
},
packedGroup: ValueCell.create(packedGroup),
axisOrder: ValueCell.create(axisOrder),
dataType: ValueCell.create(dataType),
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(directVolume);
@@ -111,7 +113,7 @@ export namespace DirectVolume {
return directVolume;
}
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume: DirectVolume): DirectVolume {
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume: DirectVolume): DirectVolume {
const width = texture.getWidth();
const height = texture.getHeight();
const depth = texture.getDepth();
@@ -129,6 +131,7 @@ export namespace DirectVolume {
ValueCell.update(directVolume.cartnToUnit, Mat4.invert(Mat4(), unitToCartn));
ValueCell.updateIfChanged(directVolume.packedGroup, packedGroup);
ValueCell.updateIfChanged(directVolume.axisOrder, Vec3.fromArray(directVolume.axisOrder.ref.value, axisOrder, 0));
ValueCell.updateIfChanged(directVolume.dataType, dataType);
return directVolume;
}
@@ -142,7 +145,8 @@ export namespace DirectVolume {
const stats = Grid.One.stats;
const packedGroup = false;
const axisOrder = Vec3.create(0, 1, 2);
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume);
const dataType = 'byte';
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume);
}
export const Params = {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -7,7 +7,7 @@
import { ComputeRenderable, createComputeRenderable } from '../../renderable';
import { WebGLContext } from '../../webgl/context';
import { createComputeRenderItem } from '../../webgl/render-item';
import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
import { Values, TextureSpec, UniformSpec, DefineSpec } from '../../renderable/schema';
import { Texture } from '../../../mol-gl/webgl/texture';
import { ShaderCode } from '../../../mol-gl/shader-code';
import { ValueCell } from '../../../mol-util';
@@ -17,12 +17,14 @@ import { getTriCount } from './tables';
import { quad_vert } from '../../../mol-gl/shader/quad.vert';
import { activeVoxels_frag } from '../../../mol-gl/shader/marching-cubes/active-voxels.frag';
import { isTimingMode } from '../../../mol-util/debug';
import { isWebGL2 } from '../../webgl/compat';
const ActiveVoxelsSchema = {
...QuadSchema,
tTriCount: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dValueChannel: DefineSpec('string', ['red', 'alpha']),
uIsoValue: UniformSpec('f'),
uGridDim: UniformSpec('v3'),
@@ -34,12 +36,17 @@ type ActiveVoxelsValues = Values<typeof ActiveVoxelsSchema>
const ActiveVoxelsName = 'active-voxels';
function valueChannel(ctx: WebGLContext, volumeData: Texture) {
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
}
function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2): ComputeRenderable<ActiveVoxelsValues> {
if (ctx.namedComputeRenderables[ActiveVoxelsName]) {
const v = ctx.namedComputeRenderables[ActiveVoxelsName].values as ActiveVoxelsValues;
ValueCell.update(v.uQuadScale, scale);
ValueCell.update(v.tVolumeData, volumeData);
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
ValueCell.update(v.uGridDim, gridDim);
ValueCell.update(v.uGridTexDim, gridTexDim);
@@ -59,6 +66,7 @@ function createActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gr
uQuadScale: ValueCell.create(scale),
tVolumeData: ValueCell.create(volumeData),
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
uIsoValue: ValueCell.create(isoValue),
uGridDim: ValueCell.create(gridDim),
uGridTexDim: ValueCell.create(gridTexDim),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -28,6 +28,7 @@ const IsosurfaceSchema = {
tActiveVoxelsPyramid: TextureSpec('texture', 'rgba', 'float', 'nearest'),
tActiveVoxelsBase: TextureSpec('texture', 'rgba', 'float', 'nearest'),
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dValueChannel: DefineSpec('string', ['red', 'alpha']),
uIsoValue: UniformSpec('f'),
uSize: UniformSpec('f'),
@@ -48,6 +49,10 @@ type IsosurfaceValues = Values<typeof IsosurfaceSchema>
const IsosurfaceName = 'isosurface';
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> {
if (ctx.namedComputeRenderables[IsosurfaceName]) {
const v = ctx.namedComputeRenderables[IsosurfaceName].values as IsosurfaceValues;
@@ -55,6 +60,7 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
ValueCell.update(v.tActiveVoxelsPyramid, activeVoxelsPyramid);
ValueCell.update(v.tActiveVoxelsBase, activeVoxelsBase);
ValueCell.update(v.tVolumeData, volumeData);
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
ValueCell.updateIfChanged(v.uSize, Math.pow(2, levels));
@@ -87,6 +93,7 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
tActiveVoxelsPyramid: ValueCell.create(activeVoxelsPyramid),
tActiveVoxelsBase: ValueCell.create(activeVoxelsBase),
tVolumeData: ValueCell.create(volumeData),
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
uIsoValue: ValueCell.create(isoValue),
uSize: ValueCell.create(Math.pow(2, levels)),

View File

@@ -38,9 +38,14 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim)
return texture2D(tex, coord);
}
vec4 voxel(vec3 pos) {
float voxelValue(vec3 pos) {
pos = min(max(vec3(0.0), pos), uGridDim - vec3(1.0));
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
#ifdef dValueChannel_red
return v.r;
#else
return v.a;
#endif
}
void main(void) {
@@ -48,14 +53,14 @@ void main(void) {
vec3 posXYZ = index3dFrom2d(uv);
// get MC case as the sum of corners that are below the given iso level
float c = step(voxel(posXYZ).a, uIsoValue)
+ 2. * step(voxel(posXYZ + c1).a, uIsoValue)
+ 4. * step(voxel(posXYZ + c2).a, uIsoValue)
+ 8. * step(voxel(posXYZ + c3).a, uIsoValue)
+ 16. * step(voxel(posXYZ + c4).a, uIsoValue)
+ 32. * step(voxel(posXYZ + c5).a, uIsoValue)
+ 64. * step(voxel(posXYZ + c6).a, uIsoValue)
+ 128. * step(voxel(posXYZ + c7).a, uIsoValue);
float c = step(voxelValue(posXYZ), uIsoValue)
+ 2. * step(voxelValue(posXYZ + c1), uIsoValue)
+ 4. * step(voxelValue(posXYZ + c2), uIsoValue)
+ 8. * step(voxelValue(posXYZ + c3), uIsoValue)
+ 16. * step(voxelValue(posXYZ + c4), uIsoValue)
+ 32. * step(voxelValue(posXYZ + c5), uIsoValue)
+ 64. * step(voxelValue(posXYZ + c6), uIsoValue)
+ 128. * step(voxelValue(posXYZ + c7), uIsoValue);
c *= step(c, 254.);
// handle out of bounds positions

View File

@@ -59,9 +59,14 @@ vec4 voxel(vec3 pos) {
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
}
vec4 voxelPadded(vec3 pos) {
float voxelValuePadded(vec3 pos) {
pos = min(max(vec3(0.0), pos), uGridDim - vec3(vec2(2.0), 1.0)); // remove xy padding
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
#ifdef dValueChannel_red
return v.r;
#else
return v.a;
#endif
}
int idot2(const in ivec2 a, const in ivec2 b) {
@@ -261,8 +266,13 @@ void main(void) {
vec4 d0 = voxel(b0);
vec4 d1 = voxel(b1);
float v0 = d0.a;
float v1 = d1.a;
#ifdef dValueChannel_red
float v0 = d0.r;
float v1 = d1.r;
#else
float v0 = d0.a;
float v1 = d1.a;
#endif
float t = (uIsoValue - v0) / (v0 - v1);
gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz;
@@ -286,14 +296,14 @@ void main(void) {
// normals from gradients
vec3 n0 = -normalize(vec3(
voxelPadded(b0 - c1).a - voxelPadded(b0 + c1).a,
voxelPadded(b0 - c3).a - voxelPadded(b0 + c3).a,
voxelPadded(b0 - c4).a - voxelPadded(b0 + c4).a
voxelValuePadded(b0 - c1) - voxelValuePadded(b0 + c1),
voxelValuePadded(b0 - c3) - voxelValuePadded(b0 + c3),
voxelValuePadded(b0 - c4) - voxelValuePadded(b0 + c4)
));
vec3 n1 = -normalize(vec3(
voxelPadded(b1 - c1).a - voxelPadded(b1 + c1).a,
voxelPadded(b1 - c3).a - voxelPadded(b1 + c3).a,
voxelPadded(b1 - c4).a - voxelPadded(b1 + c4).a
voxelValuePadded(b1 - c1) - voxelValuePadded(b1 + c1),
voxelValuePadded(b1 - c3) - voxelValuePadded(b1 + c3),
voxelValuePadded(b1 - c4) - voxelValuePadded(b1 + c4)
));
gl_FragData[2].xyz = -vec3(
n0.x + t * (n0.x - n1.x),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
@@ -59,14 +59,14 @@ export function getTarget(gl: GLRenderingContext, kind: TextureKind): number {
export function getFormat(gl: GLRenderingContext, format: TextureFormat, type: TextureType): number {
switch (format) {
case 'alpha':
if (isWebGL2(gl) && type === 'float') return gl.RED;
if (isWebGL2(gl) && (type === 'float' || type === 'fp16')) return gl.RED;
else if (isWebGL2(gl) && type === 'int') return gl.RED_INTEGER;
else return gl.ALPHA;
case 'rgb':
if (isWebGL2(gl) && type === 'int') return gl.RGB_INTEGER;
return gl.RGB;
case 'rg':
if (isWebGL2(gl) && type === 'float') return gl.RG;
if (isWebGL2(gl) && (type === 'float' || type === 'fp16')) return gl.RG;
else if (isWebGL2(gl) && type === 'int') return gl.RG_INTEGER;
else throw new Error('texture format "rg" requires webgl2 and type "float" or int"');
case 'rgba':

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 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>
@@ -9,6 +9,7 @@ import { CifWriter } from '../mol-io/writer/cif';
import { CifExportContext } from './structure/export/mmcif';
import { QuerySymbolRuntime } from '../mol-script/runtime/query/compiler';
import { UUID } from '../mol-util';
import { arrayRemoveInPlace } from '../mol-util/array';
export { CustomPropertyDescriptor, CustomProperties };
@@ -61,6 +62,14 @@ class CustomProperties {
this._set.add(desc);
}
remove(desc: CustomPropertyDescriptor<any>) {
if (!this._set.has(desc)) return;
arrayRemoveInPlace(this._list, desc);
this._set.delete(desc);
this.assets(desc);
}
reference(desc: CustomPropertyDescriptor<any>, add: boolean) {
let refs = this._refs.get(desc) || 0;
refs += add ? 1 : -1;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -31,7 +31,7 @@ async function createGaussianDensityVolume(ctx: VisualContext, structure: Struct
const unitToCartn = Mat4.mul(Mat4(), transform, Mat4.fromScaling(Mat4(), gridDim));
const cellDim = Mat4.getScaling(Vec3(), transform);
const axisOrder = Vec3.create(0, 1, 2);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, directVolume);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, 'byte', directVolume);
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, densityTextureData.maxRadius);
vol.setBoundingSphere(sphere);
@@ -89,7 +89,7 @@ async function createUnitsGaussianDensityVolume(ctx: VisualContext, unit: Unit,
const unitToCartn = Mat4.mul(Mat4(), transform, Mat4.fromScaling(Mat4(), gridDim));
const cellDim = Mat4.getScaling(Vec3(), transform);
const axisOrder = Vec3.create(0, 1, 2);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, directVolume);
const vol = DirectVolume.create(bbox, gridDim, transform, unitToCartn, cellDim, texture, stats, true, axisOrder, 'byte', directVolume);
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, densityTextureData.maxRadius);
vol.setBoundingSphere(sphere);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -22,6 +22,7 @@ import { Interval } from '../../mol-data/int';
import { Loci, EmptyLoci } from '../../mol-model/loci';
import { PickingId } from '../../mol-geo/geometry/picking';
import { createVolumeTexture2d, createVolumeTexture3d, eachVolumeLoci, getVolumeTexture2dLayout } from './util';
import { Texture } from '../../mol-gl/webgl/texture';
function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
const bbox = Box3D();
@@ -32,24 +33,35 @@ function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
// 2d volume texture
export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
const gridDimension = volume.grid.cells.space.dimensions as Vec3;
const { width, height } = getVolumeTexture2dLayout(gridDimension);
if (Math.max(width, height) > webgl.maxTextureSize / 2) {
throw new Error('volume too large for direct-volume rendering');
}
const textureImage = createVolumeTexture2d(volume, 'normals');
const dataType = props.dataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.dataType;
const textureImage = createVolumeTexture2d(volume, 'normals', 0, dataType);
// debugTexture(createImageData(textureImage.array, textureImage.width, textureImage.height), 1/3)
const transform = Grid.getGridToCartesianTransform(volume.grid);
const bbox = getBoundingBox(gridDimension, transform);
const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
let texture: Texture;
if (directVolume && directVolume.dataType.ref.value === dataType) {
texture = directVolume.gridTexture.ref.value;
} else {
texture = dataType === 'byte'
? webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear')
: dataType === 'halfFloat'
? webgl.resources.texture('image-float16', 'rgba', 'fp16', 'linear')
: webgl.resources.texture('image-float32', 'rgba', 'float', 'linear');
}
texture.load(textureImage);
const { unitToCartn, cellDim } = getUnitToCartn(volume.grid);
const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, directVolume);
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, dataType, directVolume);
}
// 3d volume texture
@@ -76,33 +88,44 @@ function getUnitToCartn(grid: Grid) {
};
}
export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, directVolume?: DirectVolume) {
export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, volume: Volume, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
const gridDimension = volume.grid.cells.space.dimensions as Vec3;
if (Math.max(...gridDimension) > webgl.max3dTextureSize / 2) {
throw new Error('volume too large for direct-volume rendering');
}
const textureVolume = createVolumeTexture3d(volume);
const dataType = props.dataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.dataType;
const textureVolume = createVolumeTexture3d(volume, dataType);
const transform = Grid.getGridToCartesianTransform(volume.grid);
const bbox = getBoundingBox(gridDimension, transform);
const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear');
let texture: Texture;
if (directVolume && directVolume.dataType.ref.value === dataType) {
texture = directVolume.gridTexture.ref.value;
} else {
texture = dataType === 'byte'
? webgl.resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear')
: dataType === 'halfFloat'
? webgl.resources.texture('volume-float16', 'rgba', 'fp16', 'linear')
: webgl.resources.texture('volume-float32', 'rgba', 'float', 'linear');
}
texture.load(textureVolume);
const { unitToCartn, cellDim } = getUnitToCartn(volume.grid);
const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, directVolume);
return DirectVolume.create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, volume.grid.stats, false, axisOrder, dataType, directVolume);
}
//
export async function createDirectVolume(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
const { runtime, webgl } = ctx;
if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in props');
if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in VisualContext');
return webgl.isWebGL2 ?
createDirectVolume3d(runtime, webgl, volume, directVolume) :
createDirectVolume2d(runtime, webgl, volume, directVolume);
createDirectVolume3d(runtime, webgl, volume, props, directVolume) :
createDirectVolume2d(runtime, webgl, volume, props, directVolume);
}
function getLoci(volume: Volume, props: PD.Values<DirectVolumeParams>) {
@@ -126,6 +149,7 @@ export function eachDirectVolume(loci: Loci, volume: Volume, key: number, props:
export const DirectVolumeParams = {
...DirectVolume.Params,
quality: { ...DirectVolume.Params.quality, isEssential: false },
dataType: PD.Select('byte', PD.arrayToOptions(['byte', 'float', 'halfFloat'] as const)),
};
export type DirectVolumeParams = typeof DirectVolumeParams
export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: Volume) {
@@ -143,6 +167,7 @@ export function DirectVolumeVisual(materialId: number): VolumeVisual<DirectVolum
getLoci: getDirectVolumeLoci,
eachLocation: eachDirectVolume,
setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
state.createGeometry = newProps.dataType !== currentProps.dataType;
},
geometryUtils: DirectVolume.Utils,
dispose: (geometry: DirectVolume) => {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 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>
@@ -32,11 +32,19 @@ import { BaseGeometry } from '../../mol-geo/geometry/base';
import { ValueCell } from '../../mol-util/value-cell';
export const VolumeIsosurfaceParams = {
isoValue: Volume.IsoValueParam
isoValue: Volume.IsoValueParam,
};
export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
export const VolumeIsosurfaceTextureParams = {
isoValue: Volume.IsoValueParam,
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>
function gpuSupport(webgl: WebGLContext) {
return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
}
@@ -117,8 +125,8 @@ export const IsosurfaceMeshParams = {
...Mesh.Params,
...TextureMesh.Params,
...VolumeIsosurfaceParams,
...VolumeIsosurfaceTextureParams,
quality: { ...Mesh.Params.quality, isEssential: false },
tryUseGpu: PD.Boolean(true),
};
export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
@@ -144,9 +152,7 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
namespace VolumeIsosurfaceTexture {
const name = 'volume-isosurface-texture';
export const descriptor = CustomPropertyDescriptor({ name });
export function get(volume: Volume, webgl: WebGLContext) {
const { resources } = webgl;
export function get(volume: Volume, webgl: WebGLContext, props: VolumeIsosurfaceGpuProps) {
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);
@@ -158,12 +164,23 @@ namespace VolumeIsosurfaceTexture {
throw new Error('volume too large for gpu isosurface extraction');
}
const dataType = props.gpuDataType === 'halfFloat' && !webgl.extensions.textureHalfFloat ? 'float' : props.gpuDataType;
if (volume._propertyData[name]?.dataType !== dataType) {
volume.customProperties.remove(descriptor);
delete volume._propertyData[name];
}
if (!volume._propertyData[name]) {
volume._propertyData[name] = resources.texture('image-uint8', 'alpha', 'ubyte', 'linear');
const texture = volume._propertyData[name] as Texture;
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(createVolumeTexture2d(volume, 'data', Padding), true);
texture.load(createVolumeTexture2d(volume, 'data', Padding, dataType), true);
volume.customProperties.add(descriptor);
volume.customProperties.assets(descriptor, [{ dispose: () => texture.destroy() }]);
}
@@ -172,7 +189,7 @@ namespace VolumeIsosurfaceTexture {
gridDimension[1] += Padding;
return {
texture: volume._propertyData[name] as Texture,
texture: volume._propertyData[name].texture as Texture,
transform,
gridDimension,
gridTexDim,
@@ -181,7 +198,7 @@ namespace VolumeIsosurfaceTexture {
}
}
async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) {
async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, key: number, theme: Theme, props: VolumeIsosurfaceGpuProps, textureMesh?: TextureMesh) {
if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
if (volume.grid.cells.data.length <= 1) {
@@ -193,7 +210,7 @@ async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Vol
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 { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, ctx.webgl, props);
const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
const buffer = textureMesh?.doubleBuffer.get();
@@ -216,6 +233,7 @@ export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<Is
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;
},
geometryUtils: TextureMesh.Utils,
mustRecreate: (volumeKey: VolumeKey, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -12,6 +12,8 @@ import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
import { packIntToRGBArray } from '../../mol-util/number-packing';
import { SetUtils } from '../../mol-util/set';
import { Box3D } from '../../mol-math/geometry';
import { toHalfFloat } from '../../mol-util/number-conversion';
import { clamp } from '../../mol-math/interpolate';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3set = Vec3.set;
@@ -97,14 +99,18 @@ export function getVolumeTexture2dLayout(dim: Vec3, padding = 0) {
return { width, height, columns, rows, powerOfTwoSize: height < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 };
}
export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups' | 'data', padding = 0) {
export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'groups' | 'data', padding = 0, type: 'byte' | 'float' | 'halfFloat' = 'byte') {
const { cells: { space, data }, stats: { max, min } } = volume.grid;
const dim = space.dimensions as Vec3;
const { dataOffset: o } = space;
const { width, height } = getVolumeTexture2dLayout(dim, padding);
const itemSize = variant === 'data' ? 1 : 4;
const array = new Uint8Array(width * height * itemSize);
const array = type === 'byte'
? new Uint8Array(width * height * itemSize)
: type === 'halfFloat'
? new Uint16Array(width * height * itemSize)
: new Float32Array(width * height * itemSize);
const textureImage = { array, width, height };
const diff = max - min;
@@ -128,11 +134,29 @@ export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'grou
const index = itemSize * ((row * ynp * width) + (y * width) + px);
const offset = o(x, y, z);
let value: number;
if (type === 'byte') {
value = Math.round(((data[offset] - min) / diff) * 255);
} else if (type === 'halfFloat') {
value = toHalfFloat((data[offset] - min) / diff);
} else {
value = (data[offset] - min) / diff;
}
if (variant === 'data') {
array[index] = Math.round(((data[offset] - min) / diff) * 255);
array[index] = value;
} else {
if (variant === 'groups') {
packIntToRGBArray(offset, array, index);
if (type === 'halfFloat') {
let group = clamp(Math.round(offset), 0, 16777216 - 1) + 1;
array[index + 2] = toHalfFloat(group % 256);
group = Math.floor(group / 256);
array[index + 1] = toHalfFloat(group % 256);
group = Math.floor(group / 256);
array[index] = toHalfFloat(group % 256);
} else {
packIntToRGBArray(offset, array, index);
}
} else {
v3set(n0,
data[o(Math.max(0, x - 1), y, z)],
@@ -146,10 +170,19 @@ export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'grou
);
v3normalize(n0, v3sub(n0, n0, n1));
v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
v3toArray(v3scale(n0, n0, 255), array, index);
if (type === 'byte') {
v3toArray(v3scale(n0, n0, 255), array, index);
} else if (type === 'halfFloat') {
array[index] = toHalfFloat(n0[0]);
array[index + 1] = toHalfFloat(n0[1]);
array[index + 2] = toHalfFloat(n0[2]);
} else {
v3toArray(n0, array, index);
}
}
array[index + 3] = Math.round(((data[offset] - min) / diff) * 255);
array[index + 3] = value;
}
}
}
@@ -158,12 +191,16 @@ export function createVolumeTexture2d(volume: Volume, variant: 'normals' | 'grou
return textureImage;
}
export function createVolumeTexture3d(volume: Volume) {
export function createVolumeTexture3d(volume: Volume, type: 'byte' | 'float' | 'halfFloat' = 'byte') {
const { cells: { space, data }, stats: { max, min } } = volume.grid;
const [width, height, depth] = space.dimensions as Vec3;
const { dataOffset: o } = space;
const array = new Uint8Array(width * height * depth * 4);
const array = type === 'byte'
? new Uint8Array(width * height * depth * 4)
: type === 'halfFloat'
? new Uint16Array(width * height * depth * 4)
: new Float32Array(width * height * depth * 4);
const textureVolume = { array, width, height, depth };
const diff = max - min;
@@ -192,9 +229,19 @@ export function createVolumeTexture3d(volume: Volume) {
);
v3normalize(n0, v3sub(n0, n0, n1));
v3addScalar(n0, v3scale(n0, n0, 0.5), 0.5);
v3toArray(v3scale(n0, n0, 255), array, i);
array[i + 3] = Math.round(((data[offset] - min) / diff) * 255);
if (type === 'byte') {
v3toArray(v3scale(n0, n0, 255), array, i);
array[i + 3] = Math.round(((data[offset] - min) / diff) * 255);
} else if (type === 'halfFloat') {
array[i] = toHalfFloat(n0[0]);
array[i + 1] = toHalfFloat(n0[1]);
array[i + 2] = toHalfFloat(n0[2]);
array[i + 3] = toHalfFloat((data[offset] - min) / diff);
} else {
v3toArray(n0, array, i);
array[i + 3] = (data[offset] - min) / diff;
}
i += 4;
}
}

View File

@@ -0,0 +1,124 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*
* This code has been modified from https://github.com/mrdoob/three.js/,
* copyright (c) 2010-2024 three.js authors. MIT License
*/
// Fast Half Float Conversions, http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf
import { clamp } from '../mol-math/interpolate';
const Tables = generateTables();
function generateTables() {
// float32 to float16 helpers
const buffer = new ArrayBuffer(4);
const floatView = new Float32Array(buffer);
const uint32View = new Uint32Array(buffer);
const baseTable = new Uint32Array(512);
const shiftTable = new Uint32Array(512);
for (let i = 0; i < 256; ++i) {
const e = i - 127;
if (e < -27) { // very small number (0, -0)
baseTable[i] = 0x0000;
baseTable[i | 0x100] = 0x8000;
shiftTable[i] = 24;
shiftTable[i | 0x100] = 24;
} else if (e < -14) { // small number (denorm)
baseTable[i] = 0x0400 >> (-e - 14);
baseTable[i | 0x100] = (0x0400 >> (-e - 14)) | 0x8000;
shiftTable[i] = - e - 1;
shiftTable[i | 0x100] = -e - 1;
} else if (e <= 15) { // normal number
baseTable[i] = (e + 15) << 10;
baseTable[i | 0x100] = ((e + 15) << 10) | 0x8000;
shiftTable[i] = 13;
shiftTable[i | 0x100] = 13;
} else if (e < 128) { // large number (Infinity, -Infinity)
baseTable[i] = 0x7c00;
baseTable[i | 0x100] = 0xfc00;
shiftTable[i] = 24;
shiftTable[i | 0x100] = 24;
} else { // stay (NaN, Infinity, -Infinity)
baseTable[i] = 0x7c00;
baseTable[i | 0x100] = 0xfc00;
shiftTable[i] = 13;
shiftTable[i | 0x100] = 13;
}
}
// float16 to float32 helpers
const mantissaTable = new Uint32Array(2048);
const exponentTable = new Uint32Array(64);
const offsetTable = new Uint32Array(64);
for (let i = 1; i < 1024; ++i) {
let m = i << 13; // zero pad mantissa bits
let e = 0; // zero exponent
// normalized
while ((m & 0x00800000) === 0) {
m <<= 1;
e -= 0x00800000; // decrement exponent
}
m &= ~ 0x00800000; // clear leading 1 bit
e += 0x38800000; // adjust bias
mantissaTable[i] = m | e;
}
for (let i = 1024; i < 2048; ++i) {
mantissaTable[i] = 0x38000000 + ((i - 1024) << 13);
}
for (let i = 1; i < 31; ++i) {
exponentTable[i] = i << 23;
}
exponentTable[31] = 0x47800000;
exponentTable[32] = 0x80000000;
for (let i = 33; i < 63; ++i) {
exponentTable[i] = 0x80000000 + ((i - 32) << 23);
}
exponentTable[63] = 0xc7800000;
for (let i = 1; i < 64; ++i) {
if (i !== 32) {
offsetTable[i] = 1024;
}
}
return {
floatView,
uint32View,
baseTable,
shiftTable,
mantissaTable,
exponentTable,
offsetTable
};
}
/** float32 to float16 */
export function toHalfFloat(val: number) {
val = clamp(val, -65504, 65504);
Tables.floatView[0] = val;
const f = Tables.uint32View[0];
const e = (f >> 23) & 0x1ff;
return Tables.baseTable[e] + ((f & 0x007fffff) >> Tables.shiftTable[e]);
}
/** float16 to float32 */
export function fromHalfFloat(val: number) {
const m = val >> 10;
Tables.uint32View[0] = Tables.mantissaTable[Tables.offsetTable[m] + (val & 0x3ff)] + Tables.exponentTable[m];
return Tables.floatView[0];
}