algorithm.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /**
  2. * Copyright (c) 2018-2020 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 { Task, RuntimeContext } from '../../../mol-task';
  8. import { Tensor } from '../../../mol-math/linear-algebra';
  9. import { Mesh } from '../../geometry/mesh/mesh';
  10. import { Index, EdgeIdInfo, CubeEdges, EdgeTable, TriTable } from './tables';
  11. import { defaults } from '../../../mol-util';
  12. import { MarchinCubesBuilder, MarchinCubesMeshBuilder, MarchinCubesLinesBuilder } from './builder';
  13. import { Lines } from '../../geometry/lines/lines';
  14. /**
  15. * The parameters required by the algorithm.
  16. */
  17. export interface MarchingCubesParams {
  18. isoLevel: number,
  19. scalarField: Tensor,
  20. bottomLeft?: ReadonlyArray<number>,
  21. topRight?: ReadonlyArray<number>,
  22. idField?: Tensor,
  23. }
  24. interface MarchingCubesInputParams extends MarchingCubesParams {
  25. bottomLeft: ReadonlyArray<number>,
  26. topRight: ReadonlyArray<number>,
  27. }
  28. function getInputParams(params: MarchingCubesParams): MarchingCubesInputParams {
  29. return {
  30. ...params,
  31. bottomLeft: defaults(params.bottomLeft, [0, 0, 0] as ReadonlyArray<number>),
  32. topRight: defaults(params.topRight, params.scalarField.space.dimensions)
  33. };
  34. }
  35. function getExtent(inputParams: MarchingCubesInputParams) {
  36. return {
  37. dX: inputParams.topRight[0] - inputParams.bottomLeft[0],
  38. dY: inputParams.topRight[1] - inputParams.bottomLeft[1],
  39. dZ: inputParams.topRight[2] - inputParams.bottomLeft[2]
  40. };
  41. }
  42. export function computeMarchingCubesMesh(params: MarchingCubesParams, mesh?: Mesh) {
  43. return Task.create('Marching Cubes Mesh', async ctx => {
  44. const inputParams = getInputParams(params);
  45. const { dX, dY, dZ } = getExtent(inputParams);
  46. // TODO should it be configurable? Scalar fields can produce meshes with vastly different densities.
  47. const vertexChunkSize = Math.min(262144, Math.max(dX * dY * dZ / 32, 1024));
  48. const builder = MarchinCubesMeshBuilder(vertexChunkSize, mesh);
  49. await (new MarchingCubesComputation(ctx, builder, inputParams)).run();
  50. return builder.get();
  51. });
  52. }
  53. export function computeMarchingCubesLines(params: MarchingCubesParams, lines?: Lines) {
  54. return Task.create('Marching Cubes Lines', async ctx => {
  55. const inputParams = getInputParams(params);
  56. const { dX, dY, dZ } = getExtent(inputParams);
  57. // TODO should it be configurable? Scalar fields can produce meshes with vastly different densities.
  58. const vertexChunkSize = Math.min(262144, Math.max(dX * dY * dZ / 32, 1024));
  59. const builder = MarchinCubesLinesBuilder(vertexChunkSize, lines);
  60. await (new MarchingCubesComputation(ctx, builder, inputParams)).run();
  61. return builder.get();
  62. });
  63. }
  64. class MarchingCubesComputation {
  65. private size: number;
  66. private sliceSize: number;
  67. private edgeFilter: number;
  68. private minX = 0; private minY = 0; private minZ = 0;
  69. private maxX = 0; private maxY = 0; private maxZ = 0;
  70. private state: MarchingCubesState;
  71. private async doSlices() {
  72. let done = 0;
  73. this.edgeFilter = 15;
  74. for (let k = this.minZ; k < this.maxZ; k++, this.edgeFilter &= ~4) {
  75. this.slice(k);
  76. done += this.sliceSize;
  77. if (this.ctx.shouldUpdate) {
  78. await this.ctx.update({ message: 'Computing surface...', current: done, max: this.size });
  79. }
  80. }
  81. }
  82. private slice(k: number) {
  83. this.edgeFilter |= 2;
  84. for (let j = this.minY; j < this.maxY; j++, this.edgeFilter &= ~2) {
  85. this.edgeFilter |= 1;
  86. for (let i = this.minX; i < this.maxX; i++, this.edgeFilter &= ~1) {
  87. this.state.processCell(i, j, k, this.edgeFilter);
  88. }
  89. }
  90. this.state.clearEdgeVertexIndexSlice(k);
  91. }
  92. async run() {
  93. await this.doSlices();
  94. }
  95. constructor(private ctx: RuntimeContext, builder: MarchinCubesBuilder<any>, params: MarchingCubesInputParams) {
  96. this.state = new MarchingCubesState(builder, params);
  97. this.minX = params.bottomLeft[0];
  98. this.minY = params.bottomLeft[1];
  99. this.minZ = params.bottomLeft[2];
  100. this.maxX = params.topRight[0] - 1;
  101. this.maxY = params.topRight[1] - 1;
  102. this.maxZ = params.topRight[2] - 1;
  103. this.size = (this.maxX - this.minX) * (this.maxY - this.minY) * (this.maxZ - this.minZ);
  104. this.sliceSize = (this.maxX - this.minX) * (this.maxY - this.minY);
  105. }
  106. }
  107. class MarchingCubesState {
  108. nX: number; nY: number; nZ: number;
  109. isoLevel: number;
  110. scalarFieldGet: Tensor.Space['get'];
  111. scalarField: Tensor.Data;
  112. idFieldGet?: Tensor.Space['get'];
  113. idField?: Tensor.Data;
  114. // two layers of vertex indices. Each vertex has 3 edges associated.
  115. verticesOnEdges: Int32Array;
  116. vertList: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  117. i: number = 0; j: number = 0; k: number = 0;
  118. private get3dOffsetFromEdgeInfo(index: Index) {
  119. return (this.nX * (((this.k + index.k) % 2) * this.nY + this.j + index.j) + this.i + index.i);
  120. }
  121. /**
  122. * This clears the "vertex index buffer" for the slice that will not be accessed anymore.
  123. */
  124. clearEdgeVertexIndexSlice(k: number) {
  125. // clear either the top or bottom half of the buffer...
  126. const start = k % 2 === 0 ? 0 : 3 * this.nX * this.nY;
  127. const end = k % 2 === 0 ? 3 * this.nX * this.nY : this.verticesOnEdges.length;
  128. this.verticesOnEdges.fill(0, start, end);
  129. }
  130. private interpolate(edgeNum: number) {
  131. const info = EdgeIdInfo[edgeNum];
  132. const edgeId = 3 * this.get3dOffsetFromEdgeInfo(info) + info.e;
  133. const ret = this.verticesOnEdges[edgeId];
  134. if (ret > 0) return ret - 1;
  135. const sf = this.scalarField;
  136. const sfg = this.scalarFieldGet;
  137. const edge = CubeEdges[edgeNum];
  138. const a = edge.a, b = edge.b;
  139. const li = a.i + this.i, lj = a.j + this.j, lk = a.k + this.k;
  140. const hi = b.i + this.i, hj = b.j + this.j, hk = b.k + this.k;
  141. const v0 = sfg(sf, li, lj, lk);
  142. const v1 = sfg(sf, hi, hj, hk);
  143. const t = (this.isoLevel - v0) / (v0 - v1);
  144. if (this.idField) {
  145. const u = this.idFieldGet!(this.idField, li, lj, lk);
  146. const v = this.idFieldGet!(this.idField, hi, hj, hk);
  147. let a = t < 0.5 ? u : v;
  148. // -1 means 'no id', check if the other cell has an id
  149. if (a === -1) a = t < 0.5 ? v : u;
  150. // -2 means 'ignore this cell'
  151. if (a === -2) return -1;
  152. this.builder.addGroup(a);
  153. } else {
  154. this.builder.addGroup(0);
  155. }
  156. const id = this.builder.addVertex(
  157. li + t * (li - hi),
  158. lj + t * (lj - hj),
  159. lk + t * (lk - hk)
  160. );
  161. this.verticesOnEdges[edgeId] = id + 1;
  162. // TODO cache scalarField differences for slices
  163. // TODO make calculation optional
  164. const n0x = sfg(sf, Math.max(0, li - 1), lj, lk) - sfg(sf, Math.min(this.nX - 1, li + 1), lj, lk);
  165. const n0y = sfg(sf, li, Math.max(0, lj - 1), lk) - sfg(sf, li, Math.min(this.nY - 1, lj + 1), lk);
  166. const n0z = sfg(sf, li, lj, Math.max(0, lk - 1)) - sfg(sf, li, lj, Math.min(this.nZ, lk + 1));
  167. const n1x = sfg(sf, Math.max(0, hi - 1), hj, hk) - sfg(sf, Math.min(this.nX - 1, hi + 1), hj, hk);
  168. const n1y = sfg(sf, hi, Math.max(0, hj - 1), hk) - sfg(sf, hi, Math.min(this.nY - 1, hj + 1), hk);
  169. const n1z = sfg(sf, hi, hj, Math.max(0, hk - 1)) - sfg(sf, hi, hj, Math.min(this.nZ - 1, hk + 1));
  170. const nx = n0x + t * (n0x - n1x);
  171. const ny = n0y + t * (n0y - n1y);
  172. const nz = n0z + t * (n0z - n1z);
  173. // ensure normal-direction is the same for negative and positive iso-levels
  174. if (this.isoLevel >= 0) {
  175. this.builder.addNormal(nx, ny, nz);
  176. } else {
  177. this.builder.addNormal(-nx, -ny, -nz);
  178. }
  179. return id;
  180. }
  181. constructor(private builder: MarchinCubesBuilder<any>, params: MarchingCubesInputParams) {
  182. const dims = params.scalarField.space.dimensions;
  183. this.nX = dims[0]; this.nY = dims[1]; this.nZ = dims[2];
  184. this.isoLevel = params.isoLevel;
  185. this.scalarFieldGet = params.scalarField.space.get;
  186. this.scalarField = params.scalarField.data;
  187. if (params.idField) {
  188. this.idField = params.idField.data;
  189. this.idFieldGet = params.idField.space.get;
  190. }
  191. // two layers of vertex indices. Each vertex has 3 edges associated.
  192. this.verticesOnEdges = new Int32Array(3 * this.nX * this.nY * 2);
  193. }
  194. get(i: number, j: number, k: number) {
  195. return this.scalarFieldGet(this.scalarField, i, j, k);
  196. }
  197. processCell(i: number, j: number, k: number, edgeFilter: number) {
  198. let tableIndex = 0;
  199. if (this.get(i, j, k) < this.isoLevel) tableIndex |= 1;
  200. if (this.get(i + 1, j, k) < this.isoLevel) tableIndex |= 2;
  201. if (this.get(i + 1, j + 1, k) < this.isoLevel) tableIndex |= 4;
  202. if (this.get(i, j + 1, k) < this.isoLevel) tableIndex |= 8;
  203. if (this.get(i, j, k + 1) < this.isoLevel) tableIndex |= 16;
  204. if (this.get(i + 1, j, k + 1) < this.isoLevel) tableIndex |= 32;
  205. if (this.get(i + 1, j + 1, k + 1) < this.isoLevel) tableIndex |= 64;
  206. if (this.get(i, j + 1, k + 1) < this.isoLevel) tableIndex |= 128;
  207. if (tableIndex === 0 || tableIndex === 255) return;
  208. this.i = i; this.j = j; this.k = k;
  209. const edgeInfo = EdgeTable[tableIndex];
  210. if ((edgeInfo & 1) > 0) this.vertList[0] = this.interpolate(0); // 0 1
  211. if ((edgeInfo & 2) > 0) this.vertList[1] = this.interpolate(1); // 1 2
  212. if ((edgeInfo & 4) > 0) this.vertList[2] = this.interpolate(2); // 2 3
  213. if ((edgeInfo & 8) > 0) this.vertList[3] = this.interpolate(3); // 0 3
  214. if ((edgeInfo & 16) > 0) this.vertList[4] = this.interpolate(4); // 4 5
  215. if ((edgeInfo & 32) > 0) this.vertList[5] = this.interpolate(5); // 5 6
  216. if ((edgeInfo & 64) > 0) this.vertList[6] = this.interpolate(6); // 6 7
  217. if ((edgeInfo & 128) > 0) this.vertList[7] = this.interpolate(7); // 4 7
  218. if ((edgeInfo & 256) > 0) this.vertList[8] = this.interpolate(8); // 0 4
  219. if ((edgeInfo & 512) > 0) this.vertList[9] = this.interpolate(9); // 1 5
  220. if ((edgeInfo & 1024) > 0) this.vertList[10] = this.interpolate(10); // 2 6
  221. if ((edgeInfo & 2048) > 0) this.vertList[11] = this.interpolate(11); // 3 7
  222. const triInfo = TriTable[tableIndex];
  223. for (let t = 0; t < triInfo.length; t += 3) {
  224. const l = triInfo[t], m = triInfo[t + 1], n = triInfo[t + 2];
  225. // ensure winding-order is the same for negative and positive iso-levels
  226. if (this.isoLevel >= 0) {
  227. this.builder.addTriangle(this.vertList, l, m, n, edgeFilter);
  228. } else {
  229. this.builder.addTriangle(this.vertList, n, m, l, edgeFilter);
  230. }
  231. }
  232. }
  233. }