@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 +5 -2
- package/es/FusedChannelData.js +80 -7
- package/es/PickVolume.js +244 -0
- package/es/RayMarchedAtlasVolume.js +5 -7
- package/es/ThreeJsPanel.js +31 -1
- package/es/View3d.js +66 -3
- package/es/Volume.js +1 -1
- package/es/VolumeCache.js +10 -3
- package/es/VolumeDrawable.js +85 -3
- package/es/constants/volumeRayMarchPickShader.js +91 -0
- package/es/constants/volumeRayMarchShader.js +1 -1
- package/es/loaders/JsonImageInfoLoader.js +2 -1
- package/es/loaders/OmeZarrLoader.js +30 -31
- package/es/loaders/VolumeLoadError.js +1 -1
- package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +7 -0
- package/es/loaders/zarr_utils/validation.js +18 -7
- package/es/loaders/zarr_utils/wrapArray.js +39 -0
- 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 +35 -3
- package/es/types/VolumeCache.d.ts +5 -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/loaders/zarr_utils/types.d.ts +17 -12
- package/es/types/loaders/zarr_utils/validation.d.ts +14 -2
- package/es/types/loaders/zarr_utils/wrapArray.d.ts +7 -0
- package/es/types/types.d.ts +17 -1
- package/package.json +3 -3
- package/es/loaders/zarr_utils/WrappedStore.js +0 -51
- package/es/types/loaders/zarr_utils/WrappedStore.d.ts +0 -24
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Vol-E core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
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
|
|
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
|
|
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\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
|
}
|
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
|
@@ -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
|
-
|
|
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
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
|
|
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
|
-
|
|
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 +=
|
|
130
|
+
this.currentSize += size;
|
|
124
131
|
|
|
125
132
|
// Evict until size is within limit
|
|
126
133
|
while (this.currentSize > this.maxSize) {
|