@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 +5 -2
- package/es/FusedChannelData.js +88 -7
- package/es/PickVolume.js +244 -0
- package/es/RayMarchedAtlasVolume.js +5 -7
- package/es/ThreeJsPanel.js +31 -1
- package/es/View3d.js +52 -0
- package/es/Volume.js +4 -4
- package/es/VolumeDrawable.js +85 -3
- package/es/constants/volumeRayMarchPickShader.js +91 -0
- package/es/constants/volumeRayMarchShader.js +1 -1
- package/es/loaders/IVolumeLoader.js +3 -2
- package/es/types/Channel.d.ts +3 -2
- package/es/types/FusedChannelData.d.ts +2 -0
- package/es/types/PickVolume.d.ts +43 -0
- package/es/types/ThreeJsPanel.d.ts +2 -1
- package/es/types/View3d.d.ts +29 -1
- package/es/types/Volume.d.ts +2 -2
- package/es/types/VolumeDrawable.d.ts +8 -2
- package/es/types/constants/volumeRayMarchPickShader.d.ts +85 -0
- package/es/types/index.d.ts +2 -2
- package/es/types/types.d.ts +29 -1
- package/package.json +1 -1
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;
|
package/es/FusedChannelData.js
CHANGED
|
@@ -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
|
}
|
package/es/PickVolume.js
ADDED
|
@@ -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
|
-
|
|
157
|
-
const fgmtsrc = rayMarchingFragmentShaderSrc;
|
|
154
|
+
|
|
158
155
|
const threeMaterial = new ShaderMaterial({
|
|
159
156
|
uniforms: uniforms,
|
|
160
|
-
vertexShader:
|
|
161
|
-
fragmentShader:
|
|
157
|
+
vertexShader: rayMarchingVertexShaderSrc,
|
|
158
|
+
fragmentShader: rayMarchingFragmentShaderSrc,
|
|
162
159
|
transparent: true,
|
|
163
160
|
depthTest: true,
|
|
164
161
|
depthWrite: false
|
|
165
162
|
});
|
|
166
|
-
mesh
|
|
163
|
+
const mesh = new Mesh(geom, threeMaterial);
|
|
164
|
+
mesh.name = "Volume";
|
|
167
165
|
return [geom, mesh];
|
|
168
166
|
}
|
|
169
167
|
createTickMarks() {
|
package/es/ThreeJsPanel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AxesHelper,
|
|
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
|
|