@aics/vole-core 3.13.0 → 3.14.0

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.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Vol-E core
2
2
 
3
- This is a WebGL canvas-based volume viewer. It can display multichannel volume data with high channel counts, and is optimized for OME-Zarr files. With OME-Zarr, the viewer can prefetch and cache Zarr chunks in browser memory for optimized performance.
3
+ ![NPM Version](https://img.shields.io/npm/v/%40aics%2Fvole-core)
4
+ ![NPM Last Update](https://img.shields.io/npm/last-update/%40aics%2Fvole-core)
5
+
6
+ **Vol-E core** is a WebGL canvas-based volume viewer. It can display multichannel volume data with high channel counts. The viewer is optimized for OME-Zarr files, and can prefetch and cache Zarr chunks in browser memory for performance.
4
7
 
5
8
  The Vol-E core package exposes several key modules:
6
9
 
@@ -61,7 +64,7 @@ loader.loadVolumeData(volume);
61
64
 
62
65
  ## React example
63
66
 
64
- See [vole-app](https://github.com/allen-cell-animated/vole-app) for a complete application that wraps View3D in a React component.
67
+ See [vole-app](https://github.com/allen-cell-animated/vole-app) for a complete application that wraps Vol-E core in a React component.
65
68
 
66
69
  ## Acknowledgements
67
70
 
@@ -1,11 +1,13 @@
1
1
  import { Color, DataTexture, RedFormat, UnsignedByteType, ClampToEdgeWrapping, Scene, OrthographicCamera, WebGLRenderTarget, RGBAFormat, ShaderMaterial, Mesh, PlaneGeometry, OneFactor, CustomBlending, MaxEquation, LinearFilter, Vector2 } from "three";
2
2
  import { renderToBufferVertShader } from "./constants/basicShaders.js";
3
3
  /* babel-plugin-inline-import './constants/shaders/fuseUI.frag' */
4
- const fuseShaderSrcUI = "precision highp float;\nprecision highp int;\nprecision highp usampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\n\n// src texture is the raw volume intensity data\nuniform usampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n uint intensity = texelFetch(srcTexture, vUv, 0).r;\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
4
+ const fuseShaderSrcUI = "precision highp float;\nprecision highp int;\nprecision highp usampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\nuniform uint highlightedId;\n\n// src texture is the raw volume intensity data\nuniform usampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n uint intensity = texelFetch(srcTexture, vUv, 0).r;\n if (intensity == (highlightedId)) {\n gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n return;\n }\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
5
5
  /* babel-plugin-inline-import './constants/shaders/fuseF.frag' */
6
6
  const fuseShaderSrcF = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\n\n// src texture is the raw volume intensity data\nuniform sampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n\n // load from channel\n float intensity = texelFetch(srcTexture, vUv, 0).r;\n\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
7
7
  /* babel-plugin-inline-import './constants/shaders/fuseI.frag' */
8
8
  const fuseShaderSrcI = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\n\n// src texture is the raw volume intensity data\nuniform isampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n int intensity = texelFetch(srcTexture, vUv, 0).r;\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
9
+ /* babel-plugin-inline-import './constants/shaders/colorizeUI.frag' */
10
+ const colorizeSrcUI = "precision highp float;\nprecision highp int;\nprecision highp usampler2D;\nprecision highp sampler3D;\n\nuniform sampler2D featureData;\n/** Min and max feature values that define the endpoints of the color map. Values\n * outside the range will be clamped to the nearest endpoint.\n */\nuniform float featureColorRampMin;\nuniform float featureColorRampMax;\nuniform sampler2D colorRamp;\nuniform usampler2D inRangeIds;\nuniform usampler2D outlierData;\n\nuniform vec3 outlineColor;\n\n/** MUST be synchronized with the DrawMode enum in ColorizeCanvas! */\nconst uint DRAW_MODE_HIDE = 0u;\nconst uint DRAW_MODE_COLOR = 1u;\n\nuniform vec3 outlierColor;\nuniform uint outlierDrawMode;\nuniform vec3 outOfRangeColor;\nuniform uint outOfRangeDrawMode;\n\nuniform uint highlightedId;\n\nuniform bool hideOutOfRange;\n\n// src texture is the raw volume intensity data\nuniform usampler2D srcTexture;\n\nuint getId(ivec2 uv) {\n return texelFetch(srcTexture, uv, 0).r;\n}\n\nvec4 getFloatFromTex(sampler2D tex, int index) {\n int width = textureSize(tex, 0).x;\n ivec2 featurePos = ivec2(index % width, index / width);\n return texelFetch(tex, featurePos, 0);\n}\nuvec4 getUintFromTex(usampler2D tex, int index) {\n int width = textureSize(tex, 0).x;\n ivec2 featurePos = ivec2(index % width, index / width);\n return texelFetch(tex, featurePos, 0);\n}\nvec4 getColorRamp(float val) {\n float width = float(textureSize(colorRamp, 0).x);\n float range = (width - 1.0) / width;\n float adjustedVal = (0.5 / width) + (val * range);\n return texture(colorRamp, vec2(adjustedVal, 0.5));\n}\nvec4 getColorFromDrawMode(uint drawMode, vec3 defaultColor) {\n const uint DRAW_MODE_HIDE = 0u;\n vec3 backgroundColor = vec3(0.0, 0.0, 0.0);\n if (drawMode == DRAW_MODE_HIDE) {\n return vec4(backgroundColor, 0.0);\n } else {\n return vec4(defaultColor, 1.0);\n }\n}\n\nfloat getFeatureVal(uint id) {\n // Data buffer starts at 0, non-background segmentation IDs start at 1\n return getFloatFromTex(featureData, int(id) - 1).r;\n}\nuint getOutlierVal(uint id) {\n // Data buffer starts at 0, non-background segmentation IDs start at 1\n return getUintFromTex(outlierData, int(id) - 1).r;\n}\nbool getIsInRange(uint id) {\n return getUintFromTex(inRangeIds, int(id) - 1).r == 1u;\n}\nbool getIsOutlier(float featureVal, uint outlierVal) {\n return isinf(featureVal) || outlierVal != 0u;\n}\n\nvec4 getObjectColor(ivec2 sUv, float opacity) {\n // Get the segmentation id at this pixel\n uint id = getId(sUv);\n\n // A segmentation id of 0 represents background\n if (id == 0u) {\n return vec4(0,0,0,0);\n }\n\n // color the highlighted object\n if (id == highlightedId) {\n return vec4(outlineColor, 1.0);\n }\n\n\n float featureVal = getFeatureVal(id);\n uint outlierVal = getOutlierVal(id);\n float normFeatureVal = (featureVal - featureColorRampMin) / (featureColorRampMax - featureColorRampMin);\n\n // Use the selected draw mode to handle out of range and outlier values;\n // otherwise color with the color ramp as usual.\n bool isInRange = getIsInRange(id);\n bool isOutlier = getIsOutlier(featureVal, outlierVal);\n\n // Features outside the filtered/thresholded range will all be treated the same (use `outOfRangeDrawColor`).\n // Features inside the range can either be outliers or standard values, and are colored accordingly.\n vec4 color;\n if (isInRange) {\n if (isOutlier) {\n color = getColorFromDrawMode(outlierDrawMode, outlierColor);\n } else {\n color = getColorRamp(normFeatureVal);\n }\n } else {\n color = getColorFromDrawMode(outOfRangeDrawMode, outOfRangeColor);\n }\n color.a *= opacity;\n return color;\n}\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n gl_FragColor = getObjectColor(vUv, 1.0);\n}\n";
9
11
  // This is the owner of the fused RGBA volume texture atlas, and the mask texture atlas.
10
12
  // This module is responsible for updating the fused texture, given the read-only volume channel data.
11
13
  export default class FusedChannelData {
@@ -52,6 +54,7 @@ export default class FusedChannelData {
52
54
  this.fuseMaterialF = this.setupFuseMaterial(fuseShaderSrcF);
53
55
  this.fuseMaterialUI = this.setupFuseMaterial(fuseShaderSrcUI);
54
56
  this.fuseMaterialI = this.setupFuseMaterial(fuseShaderSrcI);
57
+ this.fuseMaterialColorizeUI = this.setupFuseColorizeMaterial(colorizeSrcUI);
55
58
  this.fuseMaterialF.needsUpdate = true;
56
59
  this.fuseMaterialUI.needsUpdate = true;
57
60
  this.fuseMaterialI.needsUpdate = true;
@@ -60,6 +63,9 @@ export default class FusedChannelData {
60
63
  setupFuseMaterial(fragShaderSrc) {
61
64
  return new ShaderMaterial({
62
65
  uniforms: {
66
+ highlightedId: {
67
+ value: -1
68
+ },
63
69
  lutSampler: {
64
70
  value: null
65
71
  },
@@ -74,6 +80,56 @@ export default class FusedChannelData {
74
80
  ...this.fuseMaterialProps
75
81
  });
76
82
  }
83
+ setupFuseColorizeMaterial(fragShaderSrc) {
84
+ return new ShaderMaterial({
85
+ uniforms: {
86
+ highlightedId: {
87
+ value: -1
88
+ },
89
+ featureData: {
90
+ value: null
91
+ },
92
+ outlierData: {
93
+ value: null
94
+ },
95
+ inRangeIds: {
96
+ value: null
97
+ },
98
+ srcTexture: {
99
+ value: null
100
+ },
101
+ featureColorRampMin: {
102
+ value: 0
103
+ },
104
+ featureColorRampMax: {
105
+ value: 1
106
+ },
107
+ colorRamp: {
108
+ value: null
109
+ },
110
+ outlineColor: {
111
+ value: new Color(0xffffff)
112
+ },
113
+ outlierColor: {
114
+ value: new Color(0x444444)
115
+ },
116
+ outOfRangeColor: {
117
+ value: new Color(0x444444)
118
+ },
119
+ outlierDrawMode: {
120
+ value: 0
121
+ },
122
+ outOfRangeDrawMode: {
123
+ value: 0
124
+ },
125
+ hideOutOfRange: {
126
+ value: false
127
+ }
128
+ },
129
+ fragmentShader: fragShaderSrc,
130
+ ...this.fuseMaterialProps
131
+ });
132
+ }
77
133
  getFusedTexture() {
78
134
  return this.fuseRenderTarget.texture;
79
135
  }
@@ -81,14 +137,14 @@ export default class FusedChannelData {
81
137
  this.fuseScene.clear();
82
138
  this.maskTexture.dispose();
83
139
  }
84
- getShader(dtype) {
140
+ getShader(dtype, isColorize) {
85
141
  switch (dtype) {
86
142
  case "float32":
87
143
  return this.fuseMaterialF;
88
144
  case "uint8":
89
145
  case "uint16":
90
146
  case "uint32":
91
- return this.fuseMaterialUI;
147
+ return isColorize ? this.fuseMaterialColorizeUI : this.fuseMaterialUI;
92
148
  case "int8":
93
149
  case "int16":
94
150
  case "int32":
@@ -143,13 +199,30 @@ export default class FusedChannelData {
143
199
  if (!channels[chIndex].loaded) {
144
200
  continue;
145
201
  }
202
+ const isColorize = combination[i].feature !== undefined;
146
203
  // add a draw call per channel here.
147
204
  // must clone the material to keep a unique set of uniforms
148
- const mat = this.getShader(channels[chIndex].dtype).clone();
149
- mat.uniforms.lutSampler.value = channels[chIndex].lutTexture;
150
- // the lut texture is spanning only the data range of the channel, not the datatype range
151
- mat.uniforms.lutMinMax.value = new Vector2(channels[chIndex].rawMin, channels[chIndex].rawMax);
205
+ const mat = this.getShader(channels[chIndex].dtype, isColorize).clone();
152
206
  mat.uniforms.srcTexture.value = channels[chIndex].dataTexture;
207
+ mat.uniforms.highlightedId.value = combination[i].selectedID == undefined ? -1 : combination[i].selectedID;
208
+ if (isColorize) {
209
+ mat.uniforms.featureData.value = combination[i].feature?.idsToFeatureValue;
210
+ mat.uniforms.outlierData.value = combination[i].feature?.outlierData;
211
+ mat.uniforms.inRangeIds.value = combination[i].feature?.inRangeIds;
212
+ mat.uniforms.featureColorRampMin.value = combination[i].feature?.featureMin;
213
+ mat.uniforms.featureColorRampMax.value = combination[i].feature?.featureMax;
214
+ mat.uniforms.colorRamp.value = combination[i].feature?.featureValueToColor;
215
+ mat.uniforms.outlineColor.value = combination[i].feature?.outlineColor;
216
+ mat.uniforms.outlierColor.value = combination[i].feature?.outlierColor;
217
+ mat.uniforms.outOfRangeColor.value = combination[i].feature?.outOfRangeColor;
218
+ mat.uniforms.outlierDrawMode.value = combination[i].feature?.outlierDrawMode;
219
+ mat.uniforms.outOfRangeDrawMode.value = combination[i].feature?.outOfRangeDrawMode;
220
+ mat.uniforms.hideOutOfRange.value = combination[i].feature?.hideOutOfRange;
221
+ } else {
222
+ // the lut texture is spanning only the data range of the channel, not the datatype range
223
+ mat.uniforms.lutMinMax.value = new Vector2(channels[chIndex].rawMin, channels[chIndex].rawMax);
224
+ mat.uniforms.lutSampler.value = channels[chIndex].lutTexture;
225
+ }
153
226
  this.fuseScene.add(new Mesh(this.fuseGeometry, mat));
154
227
  }
155
228
  }
@@ -0,0 +1,244 @@
1
+ import { BoxGeometry, Color, DataTexture, FloatType, Group, Matrix4, Mesh, NearestFilter, RGBAFormat, Scene, ShaderMaterial, Vector2, WebGLRenderTarget } from "three";
2
+ import { pickVertexShaderSrc, pickFragmentShaderSrc, pickShaderUniforms } from "./constants/volumeRayMarchPickShader.js";
3
+ import { VolumeRenderSettings, SettingsFlags } from "./VolumeRenderSettings.js";
4
+ export default class PickVolume {
5
+ needRedraw = false;
6
+ channelToPick = 0;
7
+
8
+ /**
9
+ * Creates a new PickVolume.
10
+ * @param volume The volume that this renderer should render data from.
11
+ * @param settings Optional settings object. If set, updates the renderer with
12
+ * the given settings. Otherwise, uses the default VolumeRenderSettings.
13
+ */
14
+ constructor(volume, settings = new VolumeRenderSettings(volume)) {
15
+ this.volume = volume;
16
+ this.uniforms = pickShaderUniforms();
17
+ [this.geometry, this.geometryMesh] = this.createGeometry(this.uniforms);
18
+ this.geometryTransformNode = new Group();
19
+ this.geometryTransformNode.name = "PickVolumeContainerNode";
20
+ this.geometryTransformNode.add(this.geometryMesh);
21
+ this.scene = new Scene();
22
+ this.scene.name = "PickVolumeScene";
23
+ this.scene.add(this.geometryTransformNode);
24
+ this.emptyPositionTex = new DataTexture(new Uint8Array(Array(16).fill(0)), 2, 2);
25
+
26
+ // buffers:
27
+ this.pickBuffer = new WebGLRenderTarget(2, 2, {
28
+ count: 1,
29
+ minFilter: NearestFilter,
30
+ magFilter: NearestFilter,
31
+ format: RGBAFormat,
32
+ type: FloatType,
33
+ generateMipmaps: false
34
+ });
35
+ // Name our G-Buffer attachments for debugging
36
+ const OBJECTBUFFER = 0;
37
+ this.pickBuffer.textures[OBJECTBUFFER].name = "objectinfo";
38
+ this.settings = settings;
39
+ this.updateSettings(settings, SettingsFlags.ALL);
40
+ // TODO this is doing *more* redundant work! Fix?
41
+ this.updateVolumeDimensions();
42
+ }
43
+ setChannelToPick(channel) {
44
+ this.channelToPick = channel;
45
+ }
46
+ getPickBuffer() {
47
+ return this.pickBuffer;
48
+ }
49
+ updateVolumeDimensions() {
50
+ const {
51
+ normPhysicalSize,
52
+ normRegionSize
53
+ } = this.volume;
54
+ // Set offset
55
+ this.geometryMesh.position.copy(this.volume.getContentCenter().multiply(this.settings.scale));
56
+ // Set scale
57
+ const fullRegionScale = normPhysicalSize.clone().multiply(this.settings.scale);
58
+ this.geometryMesh.scale.copy(fullRegionScale).multiply(normRegionSize);
59
+ this.setUniform("volumeScale", normPhysicalSize);
60
+ this.settings && this.updateSettings(this.settings, SettingsFlags.ROI);
61
+
62
+ // Set atlas dimension uniforms
63
+ const {
64
+ atlasTileDims,
65
+ subregionSize
66
+ } = this.volume.imageInfo;
67
+ const atlasSize = new Vector2(subregionSize.x, subregionSize.y).multiply(atlasTileDims);
68
+ this.setUniform("ATLAS_DIMS", atlasTileDims);
69
+ this.setUniform("textureRes", atlasSize);
70
+ this.setUniform("SLICES", this.volume.imageInfo.volumeSize.z);
71
+
72
+ // (re)create channel data
73
+ }
74
+ viewpointMoved() {
75
+ this.needRedraw = true;
76
+ }
77
+ updateSettings(newSettings, dirtyFlags) {
78
+ if (dirtyFlags === undefined) {
79
+ dirtyFlags = SettingsFlags.ALL;
80
+ }
81
+ this.settings = newSettings;
82
+ if (dirtyFlags & SettingsFlags.VIEW) {
83
+ this.needRedraw = true;
84
+ this.geometryMesh.visible = this.settings.visible;
85
+ // Configure ortho
86
+ this.setUniform("orthoScale", this.settings.orthoScale);
87
+ this.setUniform("isOrtho", this.settings.isOrtho ? 1.0 : 0.0);
88
+ // Ortho line thickness
89
+ const axis = this.settings.viewAxis;
90
+ if (this.settings.isOrtho && axis) {
91
+ // TODO: Does this code do any relevant changes?
92
+ const maxVal = this.settings.bounds.bmax[axis];
93
+ const minVal = this.settings.bounds.bmin[axis];
94
+ const thicknessPct = maxVal - minVal;
95
+ this.setUniform("orthoThickness", thicknessPct);
96
+ } else {
97
+ this.setUniform("orthoThickness", 1.0);
98
+ }
99
+ }
100
+ if (dirtyFlags & SettingsFlags.VIEW || dirtyFlags & SettingsFlags.BOUNDING_BOX) {
101
+ // Update tick marks with either view or bounding box changes
102
+ // this.setUniform("maxProject", this.settings.maxProjectMode ? 1 : 0);
103
+ }
104
+ if (dirtyFlags & SettingsFlags.TRANSFORM) {
105
+ this.needRedraw = true;
106
+ // Set rotation and translation
107
+ this.geometryTransformNode.position.copy(this.settings.translation);
108
+ this.geometryTransformNode.rotation.copy(this.settings.rotation);
109
+ // TODO this does some redundant work. Including a new call to this very function! Fix?
110
+ this.updateVolumeDimensions();
111
+ this.setUniform("flipVolume", this.settings.flipAxes);
112
+ }
113
+ if (dirtyFlags & SettingsFlags.MATERIAL) {
114
+ // nothing
115
+ }
116
+ if (dirtyFlags & SettingsFlags.CAMERA) {
117
+ // nothing
118
+ }
119
+ if (dirtyFlags & SettingsFlags.ROI) {
120
+ this.needRedraw = true;
121
+ // Normalize and set bounds
122
+ const bounds = this.settings.bounds;
123
+ const {
124
+ normRegionSize,
125
+ normRegionOffset
126
+ } = this.volume;
127
+ const offsetToCenter = normRegionSize.clone().divideScalar(2).add(normRegionOffset).subScalar(0.5);
128
+ const bmin = bounds.bmin.clone().sub(offsetToCenter).divide(normRegionSize).clampScalar(-0.5, 0.5);
129
+ const bmax = bounds.bmax.clone().sub(offsetToCenter).divide(normRegionSize).clampScalar(-0.5, 0.5);
130
+ this.setUniform("AABB_CLIP_MIN", bmin);
131
+ this.setUniform("AABB_CLIP_MAX", bmax);
132
+ }
133
+ if (dirtyFlags & SettingsFlags.SAMPLING) {
134
+ this.needRedraw = true;
135
+ const resolution = this.settings.resolution.clone();
136
+ const dpr = window.devicePixelRatio ? window.devicePixelRatio : 1.0;
137
+ const nx = Math.floor(resolution.x / dpr);
138
+ const ny = Math.floor(resolution.y / dpr);
139
+ this.setUniform("iResolution", new Vector2(nx, ny));
140
+ this.pickBuffer.setSize(nx, ny);
141
+ }
142
+ if (dirtyFlags & SettingsFlags.MASK_ALPHA) {
143
+ // nothing
144
+ }
145
+ if (dirtyFlags & SettingsFlags.MASK_DATA) {
146
+ // nothing
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Creates the geometry mesh and material for rendering the volume.
152
+ * @param uniforms object containing uniforms to pass to the shader material.
153
+ * @returns the new geometry and geometry mesh.
154
+ */
155
+ createGeometry(pickUniforms) {
156
+ const geom = new BoxGeometry(1.0, 1.0, 1.0);
157
+
158
+ // shader,vtx and frag.
159
+
160
+ const threePickMaterial = new ShaderMaterial({
161
+ uniforms: pickUniforms,
162
+ vertexShader: pickVertexShaderSrc,
163
+ fragmentShader: pickFragmentShaderSrc,
164
+ depthTest: true,
165
+ depthWrite: false
166
+ });
167
+ const pickMesh = new Mesh(geom, threePickMaterial);
168
+ pickMesh.name = "PickVolume";
169
+ return [geom, pickMesh];
170
+ }
171
+ cleanup() {
172
+ // do i need to empty out the pickscene?
173
+ this.scene.clear(); // remove all children from the scene
174
+ this.geometryTransformNode.clear(); // remove all children from the transform node
175
+
176
+ this.geometry.dispose();
177
+ this.geometryMesh.material.dispose();
178
+ }
179
+ doRender(renderer, camera, depthTexture) {
180
+ if (!this.geometryMesh.visible) {
181
+ return;
182
+ }
183
+ if (!this.needRedraw) {
184
+ return;
185
+ }
186
+ this.needRedraw = false;
187
+ this.setUniform("iResolution", this.settings.resolution);
188
+ this.setUniform("textureRes", this.settings.resolution);
189
+ const depthTex = depthTexture ?? this.emptyPositionTex;
190
+ this.setUniform("textureDepth", depthTex);
191
+ this.setUniform("usingPositionTexture", depthTex.isDepthTexture ? 0 : 1);
192
+ this.setUniform("CLIP_NEAR", camera.near);
193
+ this.setUniform("CLIP_FAR", camera.far);
194
+
195
+ // this.channelData.gpuFuse(renderer);
196
+
197
+ // set up texture from segmentation channel!!!!
198
+ // we need to know the channel index for this.
199
+ // ...channel.dataTexture...
200
+ // TODO TODO TODO FIXME
201
+ this.setUniform("textureAtlas", this.volume.getChannel(this.channelToPick).dataTexture);
202
+ this.geometryTransformNode.updateMatrixWorld(true);
203
+ const mvm = new Matrix4();
204
+ mvm.multiplyMatrices(camera.matrixWorldInverse, this.geometryMesh.matrixWorld);
205
+ mvm.invert();
206
+ this.setUniform("inverseModelViewMatrix", mvm);
207
+ this.setUniform("inverseProjMatrix", camera.projectionMatrixInverse);
208
+ const VOLUME_LAYER = 0;
209
+ // draw into pick buffer...
210
+ camera.layers.set(VOLUME_LAYER);
211
+ renderer.setRenderTarget(this.pickBuffer);
212
+ renderer.autoClear = true;
213
+ const prevClearColor = new Color();
214
+ renderer.getClearColor(prevClearColor);
215
+ const prevClearAlpha = renderer.getClearAlpha();
216
+ renderer.setClearColor(0x000000, 0);
217
+ renderer.render(this.scene, camera);
218
+ renderer.autoClear = true;
219
+ renderer.setClearColor(prevClearColor, prevClearAlpha);
220
+ renderer.setRenderTarget(null);
221
+ }
222
+ get3dObject() {
223
+ return this.geometryTransformNode;
224
+ }
225
+
226
+ //////////////////////////////////////////
227
+ //////////////////////////////////////////
228
+
229
+ setUniform(name, value) {
230
+ if (!this.uniforms[name]) {
231
+ return;
232
+ }
233
+ this.uniforms[name].value = value;
234
+ }
235
+
236
+ // channelcolors is array of {rgbColor, lut} and channeldata is volume.channels
237
+ updateActiveChannels(_channelcolors, _channeldata) {
238
+ // TODO consider if we can use this as a way to assing this.channelToPick?
239
+ // (e.g. put some kind of flag in FuseChannel)
240
+ }
241
+ setRenderUpdateListener(_listener) {
242
+ return;
243
+ }
244
+ }
@@ -149,21 +149,19 @@ export default class RayMarchedAtlasVolume {
149
149
  */
150
150
  createGeometry(uniforms) {
151
151
  const geom = new BoxGeometry(1.0, 1.0, 1.0);
152
- const mesh = new Mesh(geom);
153
- mesh.name = "Volume";
154
152
 
155
153
  // shader,vtx and frag.
156
- const vtxsrc = rayMarchingVertexShaderSrc;
157
- const fgmtsrc = rayMarchingFragmentShaderSrc;
154
+
158
155
  const threeMaterial = new ShaderMaterial({
159
156
  uniforms: uniforms,
160
- vertexShader: vtxsrc,
161
- fragmentShader: fgmtsrc,
157
+ vertexShader: rayMarchingVertexShaderSrc,
158
+ fragmentShader: rayMarchingFragmentShaderSrc,
162
159
  transparent: true,
163
160
  depthTest: true,
164
161
  depthWrite: false
165
162
  });
166
- mesh.material = threeMaterial;
163
+ const mesh = new Mesh(geom, threeMaterial);
164
+ mesh.name = "Volume";
167
165
  return [geom, mesh];
168
166
  }
169
167
  createTickMarks() {
@@ -1,4 +1,4 @@
1
- import { AxesHelper, Vector3, Object3D, Mesh, BoxGeometry, MeshBasicMaterial, OrthographicCamera, PerspectiveCamera, NormalBlending, WebGLRenderer, Scene, DepthTexture, WebGLRenderTarget, NearestFilter, UnsignedByteType, RGBAFormat } from "three";
1
+ import { AxesHelper, BoxGeometry, DepthTexture, Mesh, MeshBasicMaterial, Object3D, OrthographicCamera, PerspectiveCamera, NearestFilter, NormalBlending, RGBAFormat, Scene, UnsignedByteType, Vector2, Vector3, WebGLRenderer, WebGLRenderTarget } from "three";
2
2
  import TrackballControls from "./TrackballControls.js";
3
3
  import Timing from "./Timing.js";
4
4
  import scaleBarSVG from "./constants/scaleBarSVG.js";
@@ -630,4 +630,34 @@ export class ThreeJsPanel {
630
630
  this.controls.addEventListener("end", this.controlEndHandler);
631
631
  }
632
632
  }
633
+ hitTest(offsetX, offsetY, pickBuffer) {
634
+ if (!pickBuffer) {
635
+ return -1;
636
+ }
637
+ const size = new Vector2();
638
+ this.renderer.getSize(size);
639
+ // read from instance buffer pixel!
640
+ const x = offsetX;
641
+ const y = size.y - offsetY;
642
+
643
+ // if the pick buffer is a different size from our render canvas,
644
+ // then we have to transform the mouse event coordinates
645
+ const sx = Math.floor(x / size.x * pickBuffer.width);
646
+ const sy = Math.floor(y / size.y * pickBuffer.height);
647
+
648
+ // read from the instance buffer
649
+ const pixel = new Float32Array(4).fill(-1);
650
+ this.renderer.readRenderTargetPixels(pickBuffer, sx, sy, 1, 1, pixel);
651
+ // For future reference, Simularium stores the following:
652
+ // (typeId), (instanceId), fragViewPos.z, fragPosDepth;
653
+
654
+ if (pixel[3] === -1 || pixel[3] === 0) {
655
+ return -1;
656
+ } else {
657
+ // look up the object from its instance.
658
+ // and round it off to nearest integer
659
+ const instance = Math.round(pixel[1]);
660
+ return instance;
661
+ }
662
+ }
633
663
  }
package/es/View3d.js CHANGED
@@ -96,6 +96,9 @@ export class View3d {
96
96
  getDOMElement() {
97
97
  return this.canvas3d.containerdiv;
98
98
  }
99
+ getCanvasDOMElement() {
100
+ return this.canvas3d.renderer.domElement;
101
+ }
99
102
  getCameraState() {
100
103
  return this.canvas3d.getCameraState();
101
104
  }
@@ -106,9 +109,16 @@ export class View3d {
106
109
 
107
110
  /**
108
111
  * Force a redraw.
112
+ * @param synchronous If true, the redraw will be done synchronously. If false (default), the
113
+ * redraw will be done asynchronously via `requestAnimationFrame`. Redraws should be done async
114
+ * whenever possible for the best performance.
109
115
  */
110
- redraw() {
111
- this.canvas3d.redraw();
116
+ redraw(synchronous = false) {
117
+ if (synchronous) {
118
+ this.canvas3d.onAnimationLoop();
119
+ } else {
120
+ this.canvas3d.redraw();
121
+ }
112
122
  }
113
123
  unsetImage() {
114
124
  if (this.image) {
@@ -206,10 +216,11 @@ export class View3d {
206
216
  }
207
217
  setTime(volume, time, onChannelLoaded) {
208
218
  const timeClamped = Math.max(0, Math.min(time, volume.imageInfo.times - 1));
209
- volume.updateRequiredData({
219
+ const loadPromise = volume.updateRequiredData({
210
220
  time: timeClamped
211
221
  }, onChannelLoaded);
212
222
  this.updateTimestepIndicator(volume);
223
+ return loadPromise;
213
224
  }
214
225
 
215
226
  /**
@@ -232,6 +243,18 @@ export class View3d {
232
243
  this.redraw();
233
244
  }
234
245
 
246
+ /**
247
+ * @description Set the necessary data to colorize a segmentation channel, or turn off colorization.
248
+ * @param volume The volume to set the colorize feature for
249
+ * @param channelIndex The channel that will be colorized. This only makes sense for segmentation volumes.
250
+ * @param featureInfo A collection of all parameters necessary to colorize the channel. Pass null to turn off colorization.
251
+ */
252
+ setChannelColorizeFeature(volume, channelIndex, featureInfo) {
253
+ this.image?.setChannelColorizeFeature(channelIndex, featureInfo);
254
+ this.image?.fuse();
255
+ this.redraw();
256
+ }
257
+
235
258
  /**
236
259
  * Set voxel dimensions - controls volume scaling. For example, the physical measurements of the voxels from a biological data set
237
260
  * @param {Object} volume
@@ -302,6 +325,7 @@ export class View3d {
302
325
  this.canvas3d.setControlHandlers(this.onStartControls.bind(this), this.onChangeControls.bind(this), this.onEndControls.bind(this));
303
326
  this.canvas3d.animateFuncs.push(this.preRender.bind(this));
304
327
  this.canvas3d.animateFuncs.push(img.onAnimate.bind(img));
328
+ this.canvas3d.animateFuncs.push(img.fillPickBuffer.bind(img));
305
329
  this.updatePerspectiveScaleBar(img.volume);
306
330
  this.updateTimestepIndicator(img.volume);
307
331
 
@@ -785,6 +809,45 @@ export class View3d {
785
809
  removeEventListeners() {
786
810
  window.removeEventListener("keydown", this.handleKeydown);
787
811
  }
812
+
813
+ /**
814
+ * @description Set the selected ID for a given channel. This is used to change the appearance of the volume where that id is.
815
+ * @param volume the image to set the selected ID on
816
+ * @param channel the channel index where the selected ID is
817
+ * @param id the selected id
818
+ */
819
+ setSelectedID(volume, channel, id) {
820
+ const needRedraw = this.image?.setSelectedID(channel, id);
821
+ if (needRedraw) {
822
+ this.image?.fuse();
823
+ this.redraw();
824
+ }
825
+ }
826
+
827
+ /**
828
+ * @description Enable or disable picking on a volume. If enabled, the channelIndex is used to determine which channel to pick.
829
+ * @param volume the image to enable picking on
830
+ * @param enabled set true to enable, false to disable
831
+ * @param channelIndex if enabled is set to true, pass the pickable channel index here
832
+ */
833
+ enablePicking(volume, enabled, channelIndex = 0) {
834
+ if (this.image) {
835
+ this.image.enablePicking(enabled, channelIndex);
836
+ }
837
+ }
838
+
839
+ /**
840
+ * @description This function is used to determine if a mouse event occurred over a volume object.
841
+ * @param offsetX mouse event x coordinate
842
+ * @param offsetY mouse event y coordinate
843
+ * @returns id of object that is under offsetX, offsetY. -1 if none
844
+ */
845
+ hitTest(offsetX, offsetY) {
846
+ if (!this.image) {
847
+ return -1;
848
+ }
849
+ return this.canvas3d.hitTest(offsetX, offsetY, this.image.getPickBuffer());
850
+ }
788
851
  setupGui(container) {
789
852
  const pane = new Pane({
790
853
  title: "Advanced Settings",
package/es/Volume.js CHANGED
@@ -139,7 +139,7 @@ export default class Volume {
139
139
  }
140
140
  }
141
141
  if (shouldReload) {
142
- this.loadNewData(onChannelLoaded);
142
+ await this.loadNewData(onChannelLoaded);
143
143
  }
144
144
  }
145
145
  async loadScaleLevelDims() {
package/es/VolumeCache.js CHANGED
@@ -1,3 +1,9 @@
1
+ export const isChunk = data => data.data !== undefined;
2
+ const chunkSize = ({
3
+ data
4
+ }) => Array.isArray(data) ? data.length : data.byteLength;
5
+ const dataSize = data => data.byteLength ?? chunkSize(data);
6
+
1
7
  /** Default: 250MB. Should be large enough to be useful but safe for most any computer that can run the app */
2
8
  const CACHE_MAX_SIZE_DEFAULT = 250_000_000;
3
9
  export default class VolumeCache {
@@ -31,7 +37,7 @@ export default class VolumeCache {
31
37
  */
32
38
  removeEntryFromStore(entry) {
33
39
  this.entries.delete(entry.key);
34
- this.currentSize -= entry.data.byteLength;
40
+ this.currentSize -= dataSize(entry.data);
35
41
  }
36
42
 
37
43
  /**
@@ -98,7 +104,8 @@ export default class VolumeCache {
98
104
  * @returns {boolean} a boolean indicating whether the insertion succeeded.
99
105
  */
100
106
  insert(key, data) {
101
- if (data.byteLength > this.maxSize) {
107
+ const size = dataSize(data);
108
+ if (size > this.maxSize) {
102
109
  console.error("VolumeCache: attempt to insert a single entry larger than the cache");
103
110
  return false;
104
111
  }
@@ -120,7 +127,7 @@ export default class VolumeCache {
120
127
  };
121
128
  this.addEntryAsFirst(newEntry);
122
129
  this.entries.set(key, newEntry);
123
- this.currentSize += data.byteLength;
130
+ this.currentSize += size;
124
131
 
125
132
  // Evict until size is within limit
126
133
  while (this.currentSize > this.maxSize) {