@aics/vole-core 3.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE.txt +26 -0
  2. package/README.md +119 -0
  3. package/es/Atlas2DSlice.js +224 -0
  4. package/es/Channel.js +264 -0
  5. package/es/FileSaver.js +31 -0
  6. package/es/FusedChannelData.js +192 -0
  7. package/es/Histogram.js +250 -0
  8. package/es/ImageInfo.js +127 -0
  9. package/es/Light.js +74 -0
  10. package/es/Lut.js +500 -0
  11. package/es/MarchingCubes.js +507 -0
  12. package/es/MeshVolume.js +334 -0
  13. package/es/NaiveSurfaceNets.js +251 -0
  14. package/es/PathTracedVolume.js +482 -0
  15. package/es/RayMarchedAtlasVolume.js +250 -0
  16. package/es/RenderToBuffer.js +31 -0
  17. package/es/ThreeJsPanel.js +633 -0
  18. package/es/Timing.js +28 -0
  19. package/es/TrackballControls.js +538 -0
  20. package/es/View3d.js +848 -0
  21. package/es/Volume.js +352 -0
  22. package/es/VolumeCache.js +161 -0
  23. package/es/VolumeDims.js +16 -0
  24. package/es/VolumeDrawable.js +702 -0
  25. package/es/VolumeMaker.js +101 -0
  26. package/es/VolumeRenderImpl.js +1 -0
  27. package/es/VolumeRenderSettings.js +203 -0
  28. package/es/constants/basicShaders.js +29 -0
  29. package/es/constants/colors.js +59 -0
  30. package/es/constants/denoiseShader.js +43 -0
  31. package/es/constants/lights.js +42 -0
  32. package/es/constants/materials.js +85 -0
  33. package/es/constants/pathtraceOutputShader.js +13 -0
  34. package/es/constants/scaleBarSVG.js +21 -0
  35. package/es/constants/time.js +34 -0
  36. package/es/constants/volumePTshader.js +153 -0
  37. package/es/constants/volumeRayMarchShader.js +123 -0
  38. package/es/constants/volumeSliceShader.js +115 -0
  39. package/es/index.js +21 -0
  40. package/es/loaders/IVolumeLoader.js +131 -0
  41. package/es/loaders/JsonImageInfoLoader.js +255 -0
  42. package/es/loaders/OmeZarrLoader.js +495 -0
  43. package/es/loaders/OpenCellLoader.js +65 -0
  44. package/es/loaders/RawArrayLoader.js +89 -0
  45. package/es/loaders/TiffLoader.js +219 -0
  46. package/es/loaders/VolumeLoadError.js +44 -0
  47. package/es/loaders/VolumeLoaderUtils.js +221 -0
  48. package/es/loaders/index.js +40 -0
  49. package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +143 -0
  50. package/es/loaders/zarr_utils/WrappedStore.js +51 -0
  51. package/es/loaders/zarr_utils/types.js +24 -0
  52. package/es/loaders/zarr_utils/utils.js +225 -0
  53. package/es/loaders/zarr_utils/validation.js +49 -0
  54. package/es/test/ChunkPrefetchIterator.test.js +208 -0
  55. package/es/test/RequestQueue.test.js +442 -0
  56. package/es/test/SubscribableRequestQueue.test.js +244 -0
  57. package/es/test/VolumeCache.test.js +118 -0
  58. package/es/test/VolumeRenderSettings.test.js +71 -0
  59. package/es/test/lut.test.js +671 -0
  60. package/es/test/num_utils.test.js +140 -0
  61. package/es/test/volume.test.js +98 -0
  62. package/es/test/zarr_utils.test.js +358 -0
  63. package/es/types/Atlas2DSlice.d.ts +41 -0
  64. package/es/types/Channel.d.ts +44 -0
  65. package/es/types/FileSaver.d.ts +6 -0
  66. package/es/types/FusedChannelData.d.ts +26 -0
  67. package/es/types/Histogram.d.ts +57 -0
  68. package/es/types/ImageInfo.d.ts +87 -0
  69. package/es/types/Light.d.ts +27 -0
  70. package/es/types/Lut.d.ts +67 -0
  71. package/es/types/MarchingCubes.d.ts +53 -0
  72. package/es/types/MeshVolume.d.ts +40 -0
  73. package/es/types/NaiveSurfaceNets.d.ts +11 -0
  74. package/es/types/PathTracedVolume.d.ts +65 -0
  75. package/es/types/RayMarchedAtlasVolume.d.ts +41 -0
  76. package/es/types/RenderToBuffer.d.ts +17 -0
  77. package/es/types/ThreeJsPanel.d.ts +107 -0
  78. package/es/types/Timing.d.ts +11 -0
  79. package/es/types/TrackballControls.d.ts +51 -0
  80. package/es/types/View3d.d.ts +357 -0
  81. package/es/types/Volume.d.ts +152 -0
  82. package/es/types/VolumeCache.d.ts +43 -0
  83. package/es/types/VolumeDims.d.ts +28 -0
  84. package/es/types/VolumeDrawable.d.ts +108 -0
  85. package/es/types/VolumeMaker.d.ts +49 -0
  86. package/es/types/VolumeRenderImpl.d.ts +22 -0
  87. package/es/types/VolumeRenderSettings.d.ts +98 -0
  88. package/es/types/constants/basicShaders.d.ts +4 -0
  89. package/es/types/constants/colors.d.ts +2 -0
  90. package/es/types/constants/denoiseShader.d.ts +40 -0
  91. package/es/types/constants/lights.d.ts +38 -0
  92. package/es/types/constants/materials.d.ts +20 -0
  93. package/es/types/constants/pathtraceOutputShader.d.ts +11 -0
  94. package/es/types/constants/scaleBarSVG.d.ts +2 -0
  95. package/es/types/constants/time.d.ts +19 -0
  96. package/es/types/constants/volumePTshader.d.ts +137 -0
  97. package/es/types/constants/volumeRayMarchShader.d.ts +117 -0
  98. package/es/types/constants/volumeSliceShader.d.ts +109 -0
  99. package/es/types/glsl.d.js +0 -0
  100. package/es/types/index.d.ts +28 -0
  101. package/es/types/loaders/IVolumeLoader.d.ts +113 -0
  102. package/es/types/loaders/JsonImageInfoLoader.d.ts +80 -0
  103. package/es/types/loaders/OmeZarrLoader.d.ts +87 -0
  104. package/es/types/loaders/OpenCellLoader.d.ts +9 -0
  105. package/es/types/loaders/RawArrayLoader.d.ts +33 -0
  106. package/es/types/loaders/TiffLoader.d.ts +45 -0
  107. package/es/types/loaders/VolumeLoadError.d.ts +18 -0
  108. package/es/types/loaders/VolumeLoaderUtils.d.ts +38 -0
  109. package/es/types/loaders/index.d.ts +22 -0
  110. package/es/types/loaders/zarr_utils/ChunkPrefetchIterator.d.ts +22 -0
  111. package/es/types/loaders/zarr_utils/WrappedStore.d.ts +24 -0
  112. package/es/types/loaders/zarr_utils/types.d.ts +94 -0
  113. package/es/types/loaders/zarr_utils/utils.d.ts +23 -0
  114. package/es/types/loaders/zarr_utils/validation.d.ts +7 -0
  115. package/es/types/test/ChunkPrefetchIterator.test.d.ts +1 -0
  116. package/es/types/test/RequestQueue.test.d.ts +1 -0
  117. package/es/types/test/SubscribableRequestQueue.test.d.ts +1 -0
  118. package/es/types/test/VolumeCache.test.d.ts +1 -0
  119. package/es/types/test/VolumeRenderSettings.test.d.ts +1 -0
  120. package/es/types/test/lut.test.d.ts +1 -0
  121. package/es/types/test/num_utils.test.d.ts +1 -0
  122. package/es/types/test/volume.test.d.ts +1 -0
  123. package/es/types/test/zarr_utils.test.d.ts +1 -0
  124. package/es/types/types.d.ts +115 -0
  125. package/es/types/utils/RequestQueue.d.ts +112 -0
  126. package/es/types/utils/SubscribableRequestQueue.d.ts +52 -0
  127. package/es/types/utils/num_utils.d.ts +43 -0
  128. package/es/types/workers/VolumeLoaderContext.d.ts +106 -0
  129. package/es/types/workers/types.d.ts +101 -0
  130. package/es/types/workers/util.d.ts +3 -0
  131. package/es/types.js +75 -0
  132. package/es/typings.d.js +0 -0
  133. package/es/utils/RequestQueue.js +267 -0
  134. package/es/utils/SubscribableRequestQueue.js +187 -0
  135. package/es/utils/num_utils.js +231 -0
  136. package/es/workers/FetchTiffWorker.js +153 -0
  137. package/es/workers/VolumeLoadWorker.js +129 -0
  138. package/es/workers/VolumeLoaderContext.js +271 -0
  139. package/es/workers/types.js +41 -0
  140. package/es/workers/util.js +8 -0
  141. package/package.json +83 -0
@@ -0,0 +1,702 @@
1
+ import { Vector3, Object3D, Euler, Vector2, Box3 } from "three";
2
+ import MeshVolume from "./MeshVolume.js";
3
+ import RayMarchedAtlasVolume from "./RayMarchedAtlasVolume.js";
4
+ import PathTracedVolume from "./PathTracedVolume.js";
5
+ import { LUT_ARRAY_LENGTH } from "./Lut.js";
6
+ import { RenderMode } from "./types.js";
7
+ import Atlas2DSlice from "./Atlas2DSlice.js";
8
+ import { VolumeRenderSettings, SettingsFlags, Axis } from "./VolumeRenderSettings.js";
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+
12
+ export const colorArrayToObject = ([r, g, b]) => ({
13
+ r,
14
+ g,
15
+ b
16
+ });
17
+ export const colorObjectToArray = ({
18
+ r,
19
+ g,
20
+ b
21
+ }) => [r, g, b];
22
+
23
+ // A renderable multichannel volume image with 8-bits per channel intensity values.
24
+ export default class VolumeDrawable {
25
+ constructor(volume, options) {
26
+ // THE VOLUME DATA
27
+ this.volume = volume;
28
+ this.settings = new VolumeRenderSettings(volume);
29
+ this.onChannelDataReadyCallback = undefined;
30
+ this.viewMode = Axis.NONE; // 3D mode
31
+
32
+ this.channelColors = this.volume.channelColorsDefault.slice();
33
+ this.channelOptions = new Array(this.volume.imageInfo.numChannels).fill({});
34
+ this.fusion = this.channelColors.map((col, index) => {
35
+ let rgbColor;
36
+ // take copy of original channel color
37
+ if (col[0] === 0 && col[1] === 0 && col[2] === 0) {
38
+ rgbColor = 0;
39
+ } else {
40
+ rgbColor = [col[0], col[1], col[2]];
41
+ }
42
+ return {
43
+ chIndex: index,
44
+ lut: new Uint8Array(LUT_ARRAY_LENGTH),
45
+ rgbColor: rgbColor
46
+ };
47
+ });
48
+ this.sceneRoot = new Object3D(); //create an empty container
49
+
50
+ this.meshVolume = new MeshVolume(this.volume);
51
+ options.renderMode = options.renderMode || RenderMode.RAYMARCH;
52
+ switch (options.renderMode) {
53
+ case RenderMode.PATHTRACE:
54
+ this.renderMode = RenderMode.PATHTRACE;
55
+ this.volumeRendering = new PathTracedVolume(this.volume, this.settings);
56
+ break;
57
+ case RenderMode.SLICE: // default to raymarch even when slice is selected
58
+ case RenderMode.RAYMARCH:
59
+ default:
60
+ this.renderMode = RenderMode.RAYMARCH;
61
+ this.volumeRendering = new RayMarchedAtlasVolume(this.volume, this.settings);
62
+ }
63
+
64
+ // draw meshes first, and volume last, for blending and depth test reasons with raymarch
65
+ if (options.renderMode === RenderMode.RAYMARCH || options.renderMode === RenderMode.SLICE) {
66
+ this.sceneRoot.add(this.meshVolume.get3dObject());
67
+ }
68
+ this.sceneRoot.add(this.volumeRendering.get3dObject());
69
+ // draw meshes last (as overlay) for pathtrace? (or not at all?)
70
+ //this.PT && this.sceneRoot.add(this.meshVolume.get3dObject());
71
+
72
+ this.sceneRoot.position.set(0, 0, 0);
73
+ this.updateScale();
74
+
75
+ // apply the volume's default transformation
76
+ this.settings.translation = new Vector3().fromArray(this.volume.getTranslation());
77
+ this.settings.rotation = new Euler().fromArray(this.volume.getRotation());
78
+ this.setOptions(options);
79
+ // this.volumeRendering.setZSlice(this.zSlice);
80
+ }
81
+
82
+ /**
83
+ * Updates whether a channel's data must be loaded for rendering,
84
+ * based on if its volume or isosurface is enabled, or whether it is needed for masking.
85
+ * Calls `Volume.updateRequiredData` to update the list of required channels if necessary.
86
+ */
87
+ updateChannelDataRequired(channelIndex) {
88
+ const {
89
+ enabled,
90
+ isosurfaceEnabled
91
+ } = this.channelOptions[channelIndex];
92
+ const channelIsRequired = enabled || isosurfaceEnabled || channelIndex === this.settings.maskChannelIndex;
93
+ const requiredChannels = this.volume.loadSpecRequired.channels;
94
+ if (requiredChannels.includes(channelIndex)) {
95
+ if (!channelIsRequired) {
96
+ // This channel is currently marked required, but both its volume and isosurface are off, and it's not a mask. Remove it!
97
+ this.volume.updateRequiredData({
98
+ channels: requiredChannels.filter(i => i !== channelIndex)
99
+ });
100
+ }
101
+ } else {
102
+ if (channelIsRequired) {
103
+ // This channel is not marked required, but either its volume or isosurface is on, or it is a mask. Add it!
104
+ this.volume.updateRequiredData({
105
+ channels: [...requiredChannels, channelIndex]
106
+ });
107
+ }
108
+ }
109
+ }
110
+ setOptions(options) {
111
+ options = options || {};
112
+ if (options.maskChannelIndex !== undefined) {
113
+ this.setChannelAsMask(options.maskChannelIndex);
114
+ }
115
+ if (options.maskAlpha !== undefined) {
116
+ this.setMaskAlpha(options.maskAlpha);
117
+ }
118
+ if (options.clipBounds !== undefined) {
119
+ this.settings.bounds = {
120
+ bmin: new Vector3(options.clipBounds[0], options.clipBounds[2], options.clipBounds[4]),
121
+ bmax: new Vector3(options.clipBounds[1], options.clipBounds[3], options.clipBounds[5])
122
+ };
123
+ // note: dropping isOrthoAxis argument
124
+ this.setAxisClip(Axis.X, options.clipBounds[0], options.clipBounds[1]);
125
+ this.setAxisClip(Axis.Y, options.clipBounds[2], options.clipBounds[3]);
126
+ this.setAxisClip(Axis.Z, options.clipBounds[4], options.clipBounds[5]);
127
+ }
128
+ if (options.translation !== undefined) {
129
+ this.setTranslation(new Vector3().fromArray(options.translation));
130
+ }
131
+ if (options.rotation !== undefined) {
132
+ this.setRotation(new Euler().fromArray(options.rotation));
133
+ }
134
+ if (options.renderMode !== undefined) {
135
+ this.setVolumeRendering(options.renderMode);
136
+ }
137
+ if (options.primaryRayStepSize !== undefined || options.secondaryRayStepSize !== undefined) {
138
+ this.setRayStepSizes(options.primaryRayStepSize, options.secondaryRayStepSize);
139
+ }
140
+ if (options.showBoundingBox !== undefined) {
141
+ this.setShowBoundingBox(options.showBoundingBox);
142
+ }
143
+ if (options.boundingBoxColor !== undefined) {
144
+ this.setBoundingBoxColor(options.boundingBoxColor);
145
+ }
146
+ if (options.channels !== undefined) {
147
+ // store channel options here!
148
+ this.channelOptions = options.channels;
149
+ this.channelOptions.forEach((channelOptions, channelIndex) => {
150
+ this.setChannelOptions(channelIndex, channelOptions);
151
+ });
152
+ }
153
+ }
154
+ setChannelOptions(channelIndex, options) {
155
+ // merge to current channel options
156
+ this.channelOptions[channelIndex] = Object.assign(this.channelOptions[channelIndex], options);
157
+ if (options.enabled !== undefined) {
158
+ this.setVolumeChannelEnabled(channelIndex, options.enabled);
159
+ }
160
+ if (options.color !== undefined) {
161
+ this.updateChannelColor(channelIndex, options.color);
162
+ }
163
+ if (options.isosurfaceEnabled !== undefined) {
164
+ const hasIso = this.hasIsosurface(channelIndex);
165
+ if (hasIso !== options.isosurfaceEnabled) {
166
+ if (hasIso && !options.isosurfaceEnabled) {
167
+ this.meshVolume.destroyIsosurface(channelIndex);
168
+ } else if (!hasIso && options.isosurfaceEnabled && this.volume.channels[channelIndex].loaded) {
169
+ const {
170
+ isovalue,
171
+ isosurfaceOpacity
172
+ } = options;
173
+ this.meshVolume.createIsosurface(channelIndex, this.channelColors[channelIndex], isovalue, isosurfaceOpacity);
174
+ }
175
+ this.updateChannelDataRequired(channelIndex);
176
+ } else if (options.isosurfaceEnabled) {
177
+ if (options.isovalue !== undefined) {
178
+ this.meshVolume.updateIsovalue(channelIndex, options.isovalue);
179
+ }
180
+ if (options.isosurfaceOpacity !== undefined) {
181
+ this.meshVolume.updateOpacity(channelIndex, options.isosurfaceOpacity);
182
+ }
183
+ }
184
+ } else {
185
+ if (options.isovalue !== undefined) {
186
+ this.meshVolume.updateIsovalue(channelIndex, options.isovalue);
187
+ }
188
+ if (options.isosurfaceOpacity !== undefined) {
189
+ this.meshVolume.updateOpacity(channelIndex, options.isosurfaceOpacity);
190
+ }
191
+ }
192
+ }
193
+ setRayStepSizes(primary, secondary) {
194
+ if (primary === this.settings.primaryRayStepSize && secondary === this.settings.secondaryRayStepSize) {
195
+ return;
196
+ }
197
+ if (primary !== undefined) {
198
+ this.settings.primaryRayStepSize = primary;
199
+ }
200
+ if (secondary !== undefined) {
201
+ this.settings.secondaryRayStepSize = secondary;
202
+ }
203
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
204
+ }
205
+ updateScale() {
206
+ const {
207
+ normPhysicalSize,
208
+ normRegionSize
209
+ } = this.volume;
210
+ const scale = normPhysicalSize.clone().multiply(normRegionSize).multiply(this.settings.scale);
211
+ this.meshVolume.setScale(scale, this.volume.getContentCenter().multiply(this.settings.scale));
212
+ // TODO only `RayMarchedAtlasVolume` handles scale properly. Get the others on board too!
213
+ this.volumeRendering.updateVolumeDimensions();
214
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
215
+ }
216
+ setOrthoScale(value) {
217
+ if (this.settings.orthoScale === value) {
218
+ return;
219
+ }
220
+ this.settings.orthoScale = value;
221
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
222
+ }
223
+ setResolution(x, y) {
224
+ const resolution = new Vector2(x, y);
225
+ if (!this.settings.resolution.equals(resolution)) {
226
+ this.meshVolume.setResolution(x, y);
227
+ this.settings.resolution = resolution;
228
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
229
+ }
230
+ }
231
+
232
+ // Set clipping range (between -0.5 and 0.5) for a given axis.
233
+ // Calling this allows the rendering to compensate for changes in thickness in orthographic views that affect how bright the volume is.
234
+ // @param {number} axis 0, 1, or 2 for x, y, or z axis
235
+ // @param {number} minval -0.5..0.5, should be less than maxval
236
+ // @param {number} maxval -0.5..0.5, should be greater than minval
237
+ // @param {boolean} isOrthoAxis is this an orthographic projection or just a clipping of the range for perspective view
238
+ setAxisClip(axis, minval, maxval, isOrthoAxis) {
239
+ // Skip settings update if nothing has changed
240
+ if (this.settings.bounds.bmax[axis] === maxval && this.settings.bounds.bmin[axis] === minval && this.settings.viewAxis === axis && this.settings.isOrtho === (isOrthoAxis || false)) {
241
+ return;
242
+ }
243
+ this.settings.bounds.bmax[axis] = maxval;
244
+ this.settings.bounds.bmin[axis] = minval;
245
+ this.settings.viewAxis = axis;
246
+ this.settings.isOrtho = isOrthoAxis || false;
247
+
248
+ // Configure mesh volume when in an orthographic axis alignment
249
+ if (axis !== Axis.NONE && this.renderMode !== RenderMode.PATHTRACE) {
250
+ this.meshVolume.setAxisClip(axis, minval, maxval, !!isOrthoAxis);
251
+ }
252
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI | SettingsFlags.VIEW);
253
+ }
254
+ modeStringToAxis(mode) {
255
+ const modeToAxis = {
256
+ X: Axis.X,
257
+ YZ: Axis.X,
258
+ Y: Axis.Y,
259
+ XZ: Axis.Y,
260
+ Z: Axis.Z,
261
+ XY: Axis.Z
262
+ };
263
+ return modeToAxis[mode] || Axis.NONE;
264
+ }
265
+ /**
266
+ * Sets the camera mode of the VolumeDrawable.
267
+ * @param mode Mode can be "3D", or "XY" or "Z", or "YZ" or "X", or "XZ" or "Y".
268
+ */
269
+ setViewMode(mode, volumeRenderModeHint) {
270
+ const axis = this.modeStringToAxis(mode);
271
+ this.viewMode = axis;
272
+ // Force a volume render reset if we have switched to or from Z mode while raymarching is enabled.
273
+ if (axis === Axis.Z) {
274
+ // If currently in 3D raymarch mode, hotswap the 2D slice
275
+ if (this.renderMode === RenderMode.RAYMARCH || this.renderMode === RenderMode.PATHTRACE) {
276
+ this.setVolumeRendering(RenderMode.SLICE);
277
+ }
278
+ } else {
279
+ // If in 2D slice mode, switch back to 3D raymarch mode
280
+ if (this.renderMode === RenderMode.SLICE) {
281
+ this.setVolumeRendering(volumeRenderModeHint);
282
+ }
283
+ }
284
+ if (this.settings.viewAxis !== axis) {
285
+ this.settings.viewAxis = axis;
286
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
287
+ }
288
+ }
289
+
290
+ // Tell this image that it needs to be drawn in an orthographic mode
291
+ // @param {boolean} isOrtho is this an orthographic projection or a perspective view
292
+ setIsOrtho(isOrtho) {
293
+ if (this.settings.isOrtho === isOrtho) {
294
+ return;
295
+ }
296
+ this.settings.isOrtho = isOrtho;
297
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
298
+ }
299
+ setInterpolationEnabled(active) {
300
+ if (this.settings.useInterpolation === active) {
301
+ return;
302
+ }
303
+ this.settings.useInterpolation = active;
304
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
305
+ }
306
+ setOrthoThickness(value) {
307
+ if (this.renderMode === RenderMode.PATHTRACE) {
308
+ return;
309
+ }
310
+ this.meshVolume.setOrthoThickness(value);
311
+ // No settings update because ortho thickness is calculated in the renderers
312
+ }
313
+
314
+ // Set parameters for gamma curve for volume rendering.
315
+ // @param {number} gmin 0..1
316
+ // @param {number} glevel 0..1
317
+ // @param {number} gmax 0..1, should be > gmin
318
+ setGamma(gmin, glevel, gmax) {
319
+ if (this.settings.gammaMin === gmin && this.settings.gammaLevel === glevel && this.settings.gammaMax === gmax) {
320
+ return;
321
+ }
322
+ this.settings.gammaMin = gmin;
323
+ this.settings.gammaLevel = glevel;
324
+ this.settings.gammaMax = gmax;
325
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.CAMERA);
326
+ }
327
+ setFlipAxes(flipX, flipY, flipZ) {
328
+ const flipAxes = new Vector3(flipX, flipY, flipZ);
329
+ if (!this.settings.flipAxes.equals(flipAxes)) {
330
+ this.settings.flipAxes = flipAxes;
331
+ this.meshVolume.setFlipAxes(flipX, flipY, flipZ);
332
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
333
+ }
334
+ }
335
+ setMaxProjectMode(isMaxProject) {
336
+ if (this.settings.maxProjectMode === isMaxProject) {
337
+ return;
338
+ }
339
+ this.settings.maxProjectMode = isMaxProject;
340
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
341
+ }
342
+ onAnimate(renderer, camera, depthTexture) {
343
+ // TODO: this is inefficient, as this work is duplicated by threejs.
344
+ // we need camera matrix up to date before giving the 3d objects a chance to use it.
345
+ camera.updateMatrixWorld(true);
346
+ camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
347
+
348
+ // TODO confirm sequence
349
+ this.volumeRendering.doRender(renderer, camera, depthTexture);
350
+ if (this.renderMode !== RenderMode.PATHTRACE) {
351
+ this.meshVolume.doRender();
352
+ }
353
+ }
354
+ getViewMode() {
355
+ return this.viewMode;
356
+ }
357
+ getIsovalue(channel) {
358
+ return this.meshVolume.getIsovalue(channel);
359
+ }
360
+ hasIsosurface(channel) {
361
+ return this.meshVolume.hasIsosurface(channel);
362
+ }
363
+ fuse() {
364
+ if (!this.volume) {
365
+ return;
366
+ }
367
+ this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
368
+ }
369
+ setRenderUpdateListener(callback) {
370
+ this.renderUpdateListener = callback;
371
+ if (this.renderMode === RenderMode.PATHTRACE) {
372
+ this.volumeRendering.setRenderUpdateListener(callback);
373
+ }
374
+ }
375
+ updateShadingMethod(isbrdf) {
376
+ if (this.renderMode === RenderMode.PATHTRACE) {
377
+ this.volumeRendering.updateShadingMethod(isbrdf ? 1 : 0);
378
+ }
379
+ }
380
+ updateMaterial() {
381
+ this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
382
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
383
+ }
384
+ updateLuts() {
385
+ this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
386
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
387
+ }
388
+ setVoxelSize(values) {
389
+ this.volume.setVoxelSize(values);
390
+ this.updateScale();
391
+ }
392
+ cleanup() {
393
+ this.meshVolume.cleanup();
394
+ this.volumeRendering.cleanup();
395
+ }
396
+ getChannel(channelIndex) {
397
+ return this.volume.getChannel(channelIndex);
398
+ }
399
+ onChannelLoaded(batch) {
400
+ for (let j = 0; j < batch.length; ++j) {
401
+ const idx = batch[j];
402
+ const channelOptions = this.channelOptions[idx];
403
+ // TODO: this is a relatively crude way to ensure that channel settings are synced up when volume data is loaded.
404
+ // Can we instead audit which settings updated by `setChannelOptions` actually need to be reset on load?
405
+ this.setChannelOptions(idx, channelOptions);
406
+ if (channelOptions.isosurfaceEnabled) {
407
+ this.meshVolume.destroyIsosurface(idx);
408
+ const {
409
+ isovalue,
410
+ isosurfaceOpacity
411
+ } = channelOptions;
412
+ this.meshVolume.createIsosurface(idx, this.channelColors[idx], isovalue, isosurfaceOpacity);
413
+ }
414
+ }
415
+
416
+ // let the outside world have a chance
417
+ this.onChannelDataReadyCallback?.();
418
+ }
419
+ onChannelAdded(newChannelIndex) {
420
+ this.channelColors[newChannelIndex] = this.volume.channelColorsDefault[newChannelIndex];
421
+ this.fusion[newChannelIndex] = {
422
+ chIndex: newChannelIndex,
423
+ lut: new Uint8Array[LUT_ARRAY_LENGTH](),
424
+ rgbColor: [this.channelColors[newChannelIndex][0], this.channelColors[newChannelIndex][1], this.channelColors[newChannelIndex][2]]
425
+ };
426
+ this.settings.diffuse[newChannelIndex] = [this.channelColors[newChannelIndex][0], this.channelColors[newChannelIndex][1], this.channelColors[newChannelIndex][2]];
427
+ this.settings.specular[newChannelIndex] = [0, 0, 0];
428
+ this.settings.emissive[newChannelIndex] = [0, 0, 0];
429
+ this.settings.glossiness[newChannelIndex] = 0;
430
+ }
431
+
432
+ // Save a channel's isosurface as a triangle mesh to either STL or GLTF2 format. File will be named automatically, using image name and channel name.
433
+ // @param {string} type Either 'GLTF' or 'STL'
434
+ saveChannelIsosurface(channelIndex, type) {
435
+ this.meshVolume.saveChannelIsosurface(channelIndex, type, this.volume.name);
436
+ }
437
+
438
+ // Hide or display volume data for a channel
439
+ setVolumeChannelEnabled(channelIndex, enabled) {
440
+ // flip the color to the "null" value
441
+ this.fusion[channelIndex].rgbColor = enabled ? this.channelColors[channelIndex] : 0;
442
+ // if all are nulled out, then hide the volume element from the scene.
443
+ this.settings.visible = !this.fusion.every(elem => elem.rgbColor === 0);
444
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
445
+
446
+ // add or remove this channel from the list of required channels to load
447
+ this.updateChannelDataRequired(channelIndex);
448
+ }
449
+ isVolumeChannelEnabled(channelIndex) {
450
+ // the zero value for the fusion rgbColor is the indicator that a channel is hidden.
451
+ return this.fusion[channelIndex].rgbColor !== 0;
452
+ }
453
+
454
+ // Set the color for a channel
455
+ // @param {Array.<number>} colorrgb [r,g,b]
456
+ updateChannelColor(channelIndex, colorrgb) {
457
+ if (!this.channelColors[channelIndex]) {
458
+ return;
459
+ }
460
+ this.channelColors[channelIndex] = colorrgb;
461
+ this.settings.diffuse[channelIndex] = colorrgb;
462
+ // if volume channel is zero'ed out, then don't update it until it is switched on again.
463
+ if (this.fusion[channelIndex].rgbColor !== 0) {
464
+ this.fusion[channelIndex].rgbColor = colorrgb;
465
+ }
466
+ this.meshVolume.updateMeshColors(this.channelColors);
467
+ }
468
+
469
+ // TODO remove this from public interface?
470
+ updateMeshColors() {
471
+ this.meshVolume.updateMeshColors(this.channelColors);
472
+ }
473
+
474
+ // Get the color for a channel
475
+ // @return {Array.<number>} The color as array of [r,g,b]
476
+ getChannelColor(channelIndex) {
477
+ return this.channelColors[channelIndex];
478
+ }
479
+
480
+ // Set the material for a channel
481
+ // @param {number} channelIndex
482
+ // @param {Array.<number>} colorrgb [r,g,b]
483
+ // @param {Array.<number>} specularrgb [r,g,b]
484
+ // @param {Array.<number>} emissivergb [r,g,b]
485
+ // @param {number} glossiness
486
+ updateChannelMaterial(channelIndex, colorrgb, specularrgb, emissivergb, glossiness) {
487
+ if (!this.channelColors[channelIndex]) {
488
+ return;
489
+ }
490
+ this.updateChannelColor(channelIndex, colorrgb);
491
+ this.settings.diffuse[channelIndex] = colorrgb;
492
+ this.settings.specular[channelIndex] = specularrgb;
493
+ this.settings.emissive[channelIndex] = emissivergb;
494
+ this.settings.glossiness[channelIndex] = glossiness;
495
+ }
496
+ setDensity(density) {
497
+ this.settings.density = density;
498
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
499
+ }
500
+
501
+ /**
502
+ * Get the global density of the volume data
503
+ */
504
+ getDensity() {
505
+ return this.settings.density;
506
+ }
507
+ setBrightness(brightness) {
508
+ this.settings.brightness = brightness;
509
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.CAMERA);
510
+ }
511
+ getBrightness() {
512
+ return this.settings.brightness;
513
+ }
514
+ setChannelAsMask(channelIndex) {
515
+ if (!this.volume.channels[channelIndex] || !this.volume.channels[channelIndex].loaded) {
516
+ return;
517
+ }
518
+ this.settings.maskChannelIndex = channelIndex;
519
+ this.updateChannelDataRequired(channelIndex);
520
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.MASK_DATA);
521
+ }
522
+ setMaskAlpha(maskAlpha) {
523
+ this.settings.maskAlpha = maskAlpha;
524
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.MASK_ALPHA);
525
+ }
526
+ setShowBoundingBox(showBoundingBox) {
527
+ this.settings.showBoundingBox = showBoundingBox;
528
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.BOUNDING_BOX);
529
+ }
530
+ setBoundingBoxColor(color) {
531
+ this.settings.boundingBoxColor = color;
532
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.BOUNDING_BOX);
533
+ }
534
+ getIntensity(c, x, y, z) {
535
+ return this.volume.getIntensity(c, x, y, z);
536
+ }
537
+ onStartControls() {
538
+ if (this.renderMode === RenderMode.PATHTRACE) {
539
+ this.volumeRendering.onStartControls();
540
+ }
541
+ }
542
+ onChangeControls() {
543
+ if (this.renderMode === RenderMode.PATHTRACE) {
544
+ this.volumeRendering.onChangeControls();
545
+ }
546
+ }
547
+ onEndControls() {
548
+ if (this.renderMode === RenderMode.PATHTRACE) {
549
+ this.volumeRendering.onEndControls();
550
+ }
551
+ }
552
+ onResetCamera() {
553
+ this.volumeRendering.viewpointMoved();
554
+ }
555
+ onCameraChanged(fov, focalDistance, apertureSize) {
556
+ if (this.renderMode === RenderMode.PATHTRACE) {
557
+ this.volumeRendering.updateCamera(fov, focalDistance, apertureSize);
558
+ }
559
+ }
560
+
561
+ // values are in 0..1 range
562
+ updateClipRegion(xmin, xmax, ymin, ymax, zmin, zmax) {
563
+ this.settings.bounds.bmin = new Vector3(xmin - 0.5, ymin - 0.5, zmin - 0.5);
564
+ this.settings.bounds.bmax = new Vector3(xmax - 0.5, ymax - 0.5, zmax - 0.5);
565
+ this.meshVolume.updateClipRegion(xmin, xmax, ymin, ymax, zmin, zmax);
566
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI);
567
+ }
568
+ updateLights(state) {
569
+ if (this.renderMode === RenderMode.PATHTRACE) {
570
+ this.volumeRendering.updateLights(state);
571
+ }
572
+ }
573
+ setPixelSamplingRate(value) {
574
+ this.settings.pixelSamplingRate = value;
575
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
576
+ }
577
+ setVolumeRendering(newRenderMode) {
578
+ // Skip reassignment of Pathtrace renderer if already using
579
+ if (newRenderMode === RenderMode.PATHTRACE && this.renderMode === RenderMode.PATHTRACE) {
580
+ return;
581
+ }
582
+
583
+ // remove old 3d object from scene
584
+ if (this.renderMode === RenderMode.SLICE || this.renderMode === RenderMode.RAYMARCH) {
585
+ this.sceneRoot.remove(this.meshVolume.get3dObject());
586
+ }
587
+ this.sceneRoot.remove(this.volumeRendering.get3dObject());
588
+
589
+ // destroy old resources.
590
+ this.volumeRendering.cleanup();
591
+
592
+ // create new
593
+ switch (newRenderMode) {
594
+ case RenderMode.PATHTRACE:
595
+ this.volumeRendering = new PathTracedVolume(this.volume, this.settings);
596
+ this.volume.updateRequiredData({
597
+ subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1))
598
+ });
599
+ this.volumeRendering.setRenderUpdateListener(this.renderUpdateListener);
600
+ break;
601
+ case RenderMode.SLICE:
602
+ this.volumeRendering = new Atlas2DSlice(this.volume, this.settings);
603
+ // `updateRequiredData` called on construction, via `updateSettings`
604
+ break;
605
+ case RenderMode.RAYMARCH:
606
+ default:
607
+ this.volumeRendering = new RayMarchedAtlasVolume(this.volume, this.settings);
608
+ this.volume.updateRequiredData({
609
+ subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1))
610
+ });
611
+ break;
612
+ }
613
+ if (newRenderMode === RenderMode.RAYMARCH || newRenderMode === RenderMode.SLICE) {
614
+ if (this.renderUpdateListener) {
615
+ this.renderUpdateListener(0);
616
+ }
617
+ this.sceneRoot.add(this.meshVolume.get3dObject());
618
+ }
619
+
620
+ // add new 3d object to scene
621
+ this.sceneRoot.add(this.volumeRendering.get3dObject());
622
+ this.renderMode = newRenderMode;
623
+ this.fuse();
624
+ }
625
+ setTranslation(xyz) {
626
+ this.settings.translation.copy(xyz);
627
+ this.meshVolume.setTranslation(this.settings.translation);
628
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
629
+ }
630
+ setRotation(eulerXYZ) {
631
+ this.settings.rotation.copy(eulerXYZ);
632
+ this.meshVolume.setRotation(this.settings.rotation);
633
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
634
+ }
635
+ setScale(xyz) {
636
+ this.settings.scale.copy(xyz);
637
+ this.updateScale();
638
+ }
639
+ setupGui(pane) {
640
+ pane.addInput(this.settings, "translation").on("change", ({
641
+ value
642
+ }) => this.setTranslation(value));
643
+ pane.addInput(this.settings, "rotation").on("change", ({
644
+ value
645
+ }) => this.setRotation(value));
646
+ const scaleFolder = pane.addFolder({
647
+ title: "Multiscale loading"
648
+ });
649
+ // global setting to define texture size (aka gpu memory footprint per-channel)
650
+ scaleFolder.addInput(this.volume.loadSpecRequired, "maxAtlasEdge").on("change", ({
651
+ value
652
+ }) => this.volume.updateRequiredData({
653
+ maxAtlasEdge: value
654
+ }));
655
+ // explicit or automatic???
656
+ scaleFolder.addInput(this.volume.loadSpecRequired, "useExplicitLevel").on("change", ({
657
+ value
658
+ }) => this.volume.updateRequiredData({
659
+ useExplicitLevel: value
660
+ }));
661
+ // only relevant when not using explicit level
662
+ scaleFolder.addInput(this.volume.loadSpecRequired, "scaleLevelBias").on("change", ({
663
+ value
664
+ }) => this.volume.updateRequiredData({
665
+ scaleLevelBias: value
666
+ }));
667
+ // which level to load
668
+ scaleFolder.addInput(this.volume.loadSpecRequired, "multiscaleLevel").on("change", ({
669
+ value
670
+ }) => this.volume.updateRequiredData({
671
+ multiscaleLevel: value
672
+ }));
673
+ const pathtrace = pane.addFolder({
674
+ title: "Pathtrace",
675
+ expanded: false
676
+ });
677
+ pathtrace.addInput(this.settings, "primaryRayStepSize", {
678
+ min: 1,
679
+ max: 100
680
+ }).on("change", ({
681
+ value
682
+ }) => this.setRayStepSizes(value));
683
+ pathtrace.addInput(this.settings, "secondaryRayStepSize", {
684
+ min: 1,
685
+ max: 100
686
+ }).on("change", ({
687
+ value
688
+ }) => this.setRayStepSizes(undefined, value));
689
+ }
690
+ setZSlice(slice) {
691
+ const sizez = this.volume.imageInfo.volumeSize.z;
692
+ if (this.settings.zSlice !== slice && slice < sizez && slice >= 0) {
693
+ this.settings.zSlice = slice;
694
+ this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI);
695
+ return true;
696
+ }
697
+ return false;
698
+ }
699
+ get showBoundingBox() {
700
+ return this.settings.showBoundingBox;
701
+ }
702
+ }