@aics/vole-core 3.13.1 → 3.14.1

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/es/Channel.js CHANGED
@@ -14,6 +14,7 @@ export default class Channel {
14
14
  };
15
15
  this.rawMin = 0;
16
16
  this.rawMax = 255;
17
+ this.time = 0;
17
18
 
18
19
  // on gpu
19
20
  this.dataTexture = new DataTexture(new Uint8Array(), 0, 0);
@@ -147,7 +148,7 @@ export default class Channel {
147
148
 
148
149
  // give the channel fresh data and initialize from that data
149
150
  // data is formatted as a texture atlas where each tile is a z slice of the volume
150
- setFromAtlas(bitsArray, w, h, dtype, rawMin, rawMax, subregionSize) {
151
+ setFromAtlas(bitsArray, w, h, dtype, rawMin, rawMax, subregionSize, time = 0) {
151
152
  this.dtype = dtype;
152
153
  this.imgData = {
153
154
  data: bitsArray,
@@ -157,6 +158,7 @@ export default class Channel {
157
158
  this.rebuildDataTexture(this.imgData.data, w, h);
158
159
  this.loaded = true;
159
160
  this.histogram = new Histogram(bitsArray);
161
+ this.time = time;
160
162
 
161
163
  // reuse old lut but auto-remap it to new data range
162
164
  this.setRawDataRange(rawMin, rawMax);
@@ -193,10 +195,11 @@ export default class Channel {
193
195
  }
194
196
 
195
197
  // give the channel fresh volume data and initialize from that data
196
- setFromVolumeData(bitsArray, vx, vy, vz, ax, ay, rawMin, rawMax, dtype) {
198
+ setFromVolumeData(bitsArray, vx, vy, vz, ax, ay, rawMin, rawMax, dtype, time = 0) {
197
199
  this.dims = [vx, vy, vz];
198
200
  this.volumeData = bitsArray;
199
201
  this.dtype = dtype;
202
+ this.time = time;
200
203
  // TODO FIXME performance hit for shuffling the data and storing 2 versions of it (could do this in worker at least?)
201
204
  this.packToAtlas(vx, vy, vz, ax, ay);
202
205
  this.loaded = true;
@@ -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\n/** \n * Offsets raw IDs sampled from the volume data to get the global\n * ID used to index into the feature and outlier data.\n*/\nuniform uint idOffset;\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 uint rawId = texelFetch(srcTexture, uv, 0).r;\n if (rawId == 0u) {\n return 0u;\n }\n return rawId + idOffset;\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 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 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,59 @@ 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
+ idOffset: {
129
+ value: 0
130
+ }
131
+ },
132
+ fragmentShader: fragShaderSrc,
133
+ ...this.fuseMaterialProps
134
+ });
135
+ }
77
136
  getFusedTexture() {
78
137
  return this.fuseRenderTarget.texture;
79
138
  }
@@ -81,14 +140,14 @@ export default class FusedChannelData {
81
140
  this.fuseScene.clear();
82
141
  this.maskTexture.dispose();
83
142
  }
84
- getShader(dtype) {
143
+ getShader(dtype, isColorize) {
85
144
  switch (dtype) {
86
145
  case "float32":
87
146
  return this.fuseMaterialF;
88
147
  case "uint8":
89
148
  case "uint16":
90
149
  case "uint32":
91
- return this.fuseMaterialUI;
150
+ return isColorize ? this.fuseMaterialColorizeUI : this.fuseMaterialUI;
92
151
  case "int8":
93
152
  case "int16":
94
153
  case "int32":
@@ -143,13 +202,35 @@ export default class FusedChannelData {
143
202
  if (!channels[chIndex].loaded) {
144
203
  continue;
145
204
  }
205
+ const isColorize = combination[i].feature !== undefined;
146
206
  // add a draw call per channel here.
147
207
  // 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);
208
+ const mat = this.getShader(channels[chIndex].dtype, isColorize).clone();
152
209
  mat.uniforms.srcTexture.value = channels[chIndex].dataTexture;
210
+ mat.uniforms.highlightedId.value = combination[i].selectedID == undefined ? -1 : combination[i].selectedID;
211
+ const feature = combination[i].feature;
212
+ if (isColorize && feature) {
213
+ mat.uniforms.featureData.value = feature.idsToFeatureValue;
214
+ mat.uniforms.outlierData.value = feature.outlierData;
215
+ mat.uniforms.inRangeIds.value = feature.inRangeIds;
216
+ mat.uniforms.featureColorRampMin.value = feature.featureMin;
217
+ mat.uniforms.featureColorRampMax.value = feature.featureMax;
218
+ mat.uniforms.colorRamp.value = feature.featureValueToColor;
219
+ mat.uniforms.outlineColor.value = feature.outlineColor;
220
+ mat.uniforms.outlierColor.value = feature.outlierColor;
221
+ mat.uniforms.outOfRangeColor.value = feature.outOfRangeColor;
222
+ mat.uniforms.outlierDrawMode.value = feature.outlierDrawMode;
223
+ mat.uniforms.outOfRangeDrawMode.value = feature.outOfRangeDrawMode;
224
+ mat.uniforms.hideOutOfRange.value = feature.hideOutOfRange;
225
+ // Offset IDs based on the current frame, for data without
226
+ // globally-unique IDs.
227
+ const idOffset = feature.timeToIdOffset[channels[chIndex].time] ?? 0;
228
+ mat.uniforms.idOffset.value = idOffset;
229
+ } else {
230
+ // the lut texture is spanning only the data range of the channel, not the datatype range
231
+ mat.uniforms.lutMinMax.value = new Vector2(channels[chIndex].rawMin, channels[chIndex].rawMax);
232
+ mat.uniforms.lutSampler.value = channels[chIndex].lutTexture;
233
+ }
153
234
  this.fuseScene.add(new Mesh(this.fuseGeometry, mat));
154
235
  }
155
236
  }
@@ -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
@@ -243,6 +243,18 @@ export class View3d {
243
243
  this.redraw();
244
244
  }
245
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
+
246
258
  /**
247
259
  * Set voxel dimensions - controls volume scaling. For example, the physical measurements of the voxels from a biological data set
248
260
  * @param {Object} volume
@@ -313,6 +325,7 @@ export class View3d {
313
325
  this.canvas3d.setControlHandlers(this.onStartControls.bind(this), this.onChangeControls.bind(this), this.onEndControls.bind(this));
314
326
  this.canvas3d.animateFuncs.push(this.preRender.bind(this));
315
327
  this.canvas3d.animateFuncs.push(img.onAnimate.bind(img));
328
+ this.canvas3d.animateFuncs.push(img.fillPickBuffer.bind(img));
316
329
  this.updatePerspectiveScaleBar(img.volume);
317
330
  this.updateTimestepIndicator(img.volume);
318
331
 
@@ -796,6 +809,45 @@ export class View3d {
796
809
  removeEventListeners() {
797
810
  window.removeEventListener("keydown", this.handleKeydown);
798
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
+ }
799
851
  setupGui(container) {
800
852
  const pane = new Pane({
801
853
  title: "Advanced Settings",
package/es/Volume.js CHANGED
@@ -218,8 +218,8 @@ export default class Volume {
218
218
  * @param {number} atlaswidth
219
219
  * @param {number} atlasheight
220
220
  */
221
- setChannelDataFromAtlas(channelIndex, atlasdata, atlaswidth, atlasheight, range, dtype = "uint8") {
222
- this.channels[channelIndex].setFromAtlas(atlasdata, atlaswidth, atlasheight, dtype, range[0], range[1], this.imageInfo.subregionSize);
221
+ setChannelDataFromAtlas(channelIndex, atlasdata, atlaswidth, atlasheight, range, dtype = "uint8", time = 0) {
222
+ this.channels[channelIndex].setFromAtlas(atlasdata, atlaswidth, atlasheight, dtype, range[0], range[1], this.imageInfo.subregionSize, time);
223
223
  this.onChannelLoaded([channelIndex]);
224
224
  }
225
225
 
@@ -229,12 +229,12 @@ export default class Volume {
229
229
  * @param {number} channelIndex
230
230
  * @param {Uint8Array} volumeData
231
231
  */
232
- setChannelDataFromVolume(channelIndex, volumeData, range, dtype = "uint8") {
232
+ setChannelDataFromVolume(channelIndex, volumeData, range, dtype = "uint8", time = 0) {
233
233
  const {
234
234
  subregionSize,
235
235
  atlasTileDims
236
236
  } = this.imageInfo;
237
- this.channels[channelIndex].setFromVolumeData(volumeData, subregionSize.x, subregionSize.y, subregionSize.z, atlasTileDims.x * subregionSize.x, atlasTileDims.y * subregionSize.y, range[0], range[1], dtype);
237
+ this.channels[channelIndex].setFromVolumeData(volumeData, subregionSize.x, subregionSize.y, subregionSize.z, atlasTileDims.x * subregionSize.x, atlasTileDims.y * subregionSize.y, range[0], range[1], dtype, time);
238
238
  this.onChannelLoaded([channelIndex]);
239
239
  }
240
240