isosurface.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /**
  2. * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  8. import { Grid, Volume } from '../../mol-model/volume';
  9. import { VisualContext } from '../visual';
  10. import { Theme, ThemeRegistryContext } from '../../mol-theme/theme';
  11. import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
  12. import { computeMarchingCubesMesh, computeMarchingCubesLines } from '../../mol-geo/util/marching-cubes/algorithm';
  13. import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
  14. import { LocationIterator } from '../../mol-geo/util/location-iterator';
  15. import { NullLocation } from '../../mol-model/location';
  16. import { VisualUpdateState } from '../util';
  17. import { Lines } from '../../mol-geo/geometry/lines/lines';
  18. import { RepresentationContext, RepresentationParamsGetter, Representation } from '../representation';
  19. import { PickingId } from '../../mol-geo/geometry/picking';
  20. import { EmptyLoci, Loci } from '../../mol-model/loci';
  21. import { Interval } from '../../mol-data/int';
  22. import { Tensor, Vec2, Vec3 } from '../../mol-math/linear-algebra';
  23. import { fillSerial } from '../../mol-util/array';
  24. import { createVolumeTexture2d, eachVolumeLoci, getVolumeTexture2dLayout } from './util';
  25. import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
  26. import { extractIsosurface } from '../../mol-gl/compute/marching-cubes/isosurface';
  27. import { WebGLContext } from '../../mol-gl/webgl/context';
  28. import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
  29. import { Texture } from '../../mol-gl/webgl/texture';
  30. import { BaseGeometry } from '../../mol-geo/geometry/base';
  31. import { ValueCell } from '../../mol-util/value-cell';
  32. export const VolumeIsosurfaceParams = {
  33. isoValue: Volume.IsoValueParam
  34. };
  35. export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
  36. export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
  37. function gpuSupport(webgl: WebGLContext) {
  38. return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
  39. }
  40. const Padding = 1;
  41. function suitableForGpu(volume: Volume, webgl: WebGLContext) {
  42. // small volumes are about as fast or faster on CPU vs integrated GPU
  43. if (volume.grid.cells.data.length < Math.pow(10, 3)) return false;
  44. // the GPU is much more memory contraint, especially true for integrated GPUs,
  45. // fallback to CPU for large volumes
  46. const gridDim = volume.grid.cells.space.dimensions as Vec3;
  47. const { powerOfTwoSize } = getVolumeTexture2dLayout(gridDim, Padding);
  48. return powerOfTwoSize <= webgl.maxTextureSize / 2;
  49. }
  50. export function IsosurfaceVisual(materialId: number, volume: Volume, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) {
  51. if (props.tryUseGpu && webgl && gpuSupport(webgl) && suitableForGpu(volume, webgl)) {
  52. return IsosurfaceTextureMeshVisual(materialId);
  53. }
  54. return IsosurfaceMeshVisual(materialId);
  55. }
  56. function getLoci(volume: Volume, props: VolumeIsosurfaceProps) {
  57. return Volume.Isosurface.Loci(volume, props.isoValue);
  58. }
  59. function getIsosurfaceLoci(pickingId: PickingId, volume: Volume, props: VolumeIsosurfaceProps, id: number) {
  60. const { objectId, groupId } = pickingId;
  61. if (id === objectId) {
  62. return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
  63. }
  64. return EmptyLoci;
  65. }
  66. export function eachIsosurface(loci: Loci, volume: Volume, props: VolumeIsosurfaceProps, apply: (interval: Interval) => boolean) {
  67. return eachVolumeLoci(loci, volume, props.isoValue, apply);
  68. }
  69. //
  70. export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, mesh?: Mesh) {
  71. ctx.runtime.update({ message: 'Marching cubes...' });
  72. const ids = fillSerial(new Int32Array(volume.grid.cells.data.length));
  73. const surface = await computeMarchingCubesMesh({
  74. isoLevel: Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue,
  75. scalarField: volume.grid.cells,
  76. idField: Tensor.create(volume.grid.cells.space, Tensor.Data1(ids))
  77. }, mesh).runAsChild(ctx.runtime);
  78. const transform = Grid.getGridToCartesianTransform(volume.grid);
  79. Mesh.transform(surface, transform);
  80. if (ctx.webgl && !ctx.webgl.isWebGL2) {
  81. // 2nd arg means not to split triangles based on group id. Splitting triangles
  82. // is too expensive if each cell has its own group id as is the case here.
  83. Mesh.uniformTriangleGroup(surface, false);
  84. ValueCell.updateIfChanged(surface.varyingGroup, false);
  85. } else {
  86. ValueCell.updateIfChanged(surface.varyingGroup, true);
  87. }
  88. surface.setBoundingSphere(Volume.getBoundingSphere(volume));
  89. return surface;
  90. }
  91. export const IsosurfaceMeshParams = {
  92. ...Mesh.Params,
  93. ...TextureMesh.Params,
  94. ...VolumeIsosurfaceParams,
  95. quality: { ...Mesh.Params.quality, isEssential: false },
  96. tryUseGpu: PD.Boolean(true),
  97. };
  98. export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
  99. export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> {
  100. return VolumeVisual<Mesh, IsosurfaceMeshParams>({
  101. defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
  102. createGeometry: createVolumeIsosurfaceMesh,
  103. createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation),
  104. getLoci: getIsosurfaceLoci,
  105. eachLocation: eachIsosurface,
  106. setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
  107. if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
  108. },
  109. geometryUtils: Mesh.Utils,
  110. mustRecreate: (volume: Volume, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
  111. return props.tryUseGpu && !!webgl && suitableForGpu(volume, webgl);
  112. }
  113. }, materialId);
  114. }
  115. //
  116. namespace VolumeIsosurfaceTexture {
  117. const name = 'volume-isosurface-texture';
  118. export const descriptor = CustomPropertyDescriptor({ name });
  119. export function get(volume: Volume, webgl: WebGLContext) {
  120. const { resources } = webgl;
  121. const transform = Grid.getGridToCartesianTransform(volume.grid);
  122. const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions as Vec3);
  123. const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, Padding);
  124. const gridTexDim = Vec3.create(width, height, 0);
  125. const gridTexScale = Vec2.create(width / texDim, height / texDim);
  126. // console.log({ texDim, width, height, gridDimension });
  127. if (texDim > webgl.maxTextureSize / 2) {
  128. throw new Error('volume too large for gpu isosurface extraction');
  129. }
  130. if (!volume._propertyData[name]) {
  131. volume._propertyData[name] = resources.texture('image-uint8', 'alpha', 'ubyte', 'linear');
  132. const texture = volume._propertyData[name] as Texture;
  133. texture.define(texDim, texDim);
  134. // load volume into sub-section of texture
  135. texture.load(createVolumeTexture2d(volume, 'data', Padding), true);
  136. volume.customProperties.add(descriptor);
  137. volume.customProperties.assets(descriptor, [{ dispose: () => texture.destroy() }]);
  138. }
  139. gridDimension[0] += Padding;
  140. gridDimension[1] += Padding;
  141. return {
  142. texture: volume._propertyData[name] as Texture,
  143. transform,
  144. gridDimension,
  145. gridTexDim,
  146. gridTexScale
  147. };
  148. }
  149. }
  150. async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) {
  151. if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
  152. if (volume.grid.cells.data.length <= 1) {
  153. return TextureMesh.createEmpty(textureMesh);
  154. }
  155. const { max, min } = volume.grid.stats;
  156. const diff = max - min;
  157. const value = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue;
  158. const isoLevel = ((value - min) / diff);
  159. const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, ctx.webgl);
  160. const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
  161. const buffer = textureMesh?.doubleBuffer.get();
  162. const gv = extractIsosurface(ctx.webgl, texture, gridDimension, gridTexDim, gridTexScale, transform, isoLevel, value < 0, false, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
  163. const groupCount = volume.grid.cells.data.length;
  164. const surface = TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, Volume.getBoundingSphere(volume), textureMesh);
  165. return surface;
  166. }
  167. export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> {
  168. return VolumeVisual<TextureMesh, IsosurfaceMeshParams>({
  169. defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
  170. createGeometry: createVolumeIsosurfaceTextureMesh,
  171. createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation),
  172. getLoci: getIsosurfaceLoci,
  173. eachLocation: eachIsosurface,
  174. setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
  175. if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
  176. },
  177. geometryUtils: TextureMesh.Utils,
  178. mustRecreate: (volume: Volume, props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
  179. return !props.tryUseGpu || !webgl || !suitableForGpu(volume, webgl);
  180. },
  181. dispose: (geometry: TextureMesh) => {
  182. geometry.vertexTexture.ref.value.destroy();
  183. geometry.groupTexture.ref.value.destroy();
  184. geometry.normalTexture.ref.value.destroy();
  185. geometry.doubleBuffer.destroy();
  186. }
  187. }, materialId);
  188. }
  189. //
  190. export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, lines?: Lines) {
  191. ctx.runtime.update({ message: 'Marching cubes...' });
  192. const ids = fillSerial(new Int32Array(volume.grid.cells.data.length));
  193. const wireframe = await computeMarchingCubesLines({
  194. isoLevel: Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue,
  195. scalarField: volume.grid.cells,
  196. idField: Tensor.create(volume.grid.cells.space, Tensor.Data1(ids))
  197. }, lines).runAsChild(ctx.runtime);
  198. const transform = Grid.getGridToCartesianTransform(volume.grid);
  199. Lines.transform(wireframe, transform);
  200. wireframe.setBoundingSphere(Volume.getBoundingSphere(volume));
  201. return wireframe;
  202. }
  203. export const IsosurfaceWireframeParams = {
  204. ...Lines.Params,
  205. ...VolumeIsosurfaceParams,
  206. quality: { ...Lines.Params.quality, isEssential: false },
  207. sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
  208. };
  209. export type IsosurfaceWireframeParams = typeof IsosurfaceWireframeParams
  210. export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<IsosurfaceWireframeParams> {
  211. return VolumeVisual<Lines, IsosurfaceWireframeParams>({
  212. defaultProps: PD.getDefaultValues(IsosurfaceWireframeParams),
  213. createGeometry: createVolumeIsosurfaceWireframe,
  214. createLocationIterator: (volume: Volume) => LocationIterator(volume.grid.cells.data.length, 1, 1, () => NullLocation),
  215. getLoci: getIsosurfaceLoci,
  216. eachLocation: eachIsosurface,
  217. setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceWireframeParams>, currentProps: PD.Values<IsosurfaceWireframeParams>) => {
  218. if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
  219. },
  220. geometryUtils: Lines.Utils
  221. }, materialId);
  222. }
  223. //
  224. const IsosurfaceVisuals = {
  225. 'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceVisual, getLoci),
  226. 'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual, getLoci),
  227. };
  228. export const IsosurfaceParams = {
  229. ...IsosurfaceMeshParams,
  230. ...IsosurfaceWireframeParams,
  231. visuals: PD.MultiSelect(['solid'], PD.objectToOptions(IsosurfaceVisuals)),
  232. bumpFrequency: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
  233. };
  234. export type IsosurfaceParams = typeof IsosurfaceParams
  235. export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: Volume) {
  236. const p = PD.clone(IsosurfaceParams);
  237. p.isoValue = Volume.createIsoValueParam(Volume.IsoValue.relative(2), volume.grid.stats);
  238. return p;
  239. }
  240. export type IsosurfaceRepresentation = VolumeRepresentation<IsosurfaceParams>
  241. export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceParams>): IsosurfaceRepresentation {
  242. return Representation.createMulti('Isosurface', ctx, getParams, Representation.StateBuilder, IsosurfaceVisuals as unknown as Representation.Def<Volume, IsosurfaceParams>);
  243. }
  244. export const IsosurfaceRepresentationProvider = VolumeRepresentationProvider({
  245. name: 'isosurface',
  246. label: 'Isosurface',
  247. description: 'Displays a triangulated isosurface of volumetric data.',
  248. factory: IsosurfaceRepresentation,
  249. getParams: getIsosurfaceParams,
  250. defaultValues: PD.getDefaultValues(IsosurfaceParams),
  251. defaultColorTheme: { name: 'uniform' },
  252. defaultSizeTheme: { name: 'uniform' },
  253. isApplicable: (volume: Volume) => !Volume.isEmpty(volume)
  254. });