@aics/vole-core 3.13.1 → 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/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 +52 -0
- package/es/VolumeDrawable.js +85 -3
- package/es/constants/volumeRayMarchPickShader.js +91 -0
- package/es/constants/volumeRayMarchShader.js +1 -1
- 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/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 +17 -1
- package/package.json +1 -1
package/es/VolumeDrawable.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Vector3, Object3D, Euler, Vector2, Box3 } from "three";
|
|
|
2
2
|
import MeshVolume from "./MeshVolume.js";
|
|
3
3
|
import RayMarchedAtlasVolume from "./RayMarchedAtlasVolume.js";
|
|
4
4
|
import PathTracedVolume from "./PathTracedVolume.js";
|
|
5
|
+
import PickVolume from "./PickVolume.js";
|
|
5
6
|
import { LUT_ARRAY_LENGTH } from "./Lut.js";
|
|
6
7
|
import { RenderMode } from "./types.js";
|
|
7
8
|
import Atlas2DSlice from "./Atlas2DSlice.js";
|
|
@@ -42,7 +43,8 @@ export default class VolumeDrawable {
|
|
|
42
43
|
return {
|
|
43
44
|
chIndex: index,
|
|
44
45
|
lut: new Uint8Array(LUT_ARRAY_LENGTH),
|
|
45
|
-
rgbColor: rgbColor
|
|
46
|
+
rgbColor: rgbColor,
|
|
47
|
+
selectedID: -1
|
|
46
48
|
};
|
|
47
49
|
});
|
|
48
50
|
this.sceneRoot = new Object3D(); //create an empty container
|
|
@@ -60,6 +62,9 @@ export default class VolumeDrawable {
|
|
|
60
62
|
this.renderMode = RenderMode.RAYMARCH;
|
|
61
63
|
this.volumeRendering = new RayMarchedAtlasVolume(this.volume, this.settings);
|
|
62
64
|
}
|
|
65
|
+
if (this.pickRendering) {
|
|
66
|
+
this.pickRendering = new PickVolume(this.volume, this.settings);
|
|
67
|
+
}
|
|
63
68
|
|
|
64
69
|
// draw meshes first, and volume last, for blending and depth test reasons with raymarch
|
|
65
70
|
if (options.renderMode === RenderMode.RAYMARCH || options.renderMode === RenderMode.SLICE) {
|
|
@@ -78,7 +83,9 @@ export default class VolumeDrawable {
|
|
|
78
83
|
this.setOptions(options);
|
|
79
84
|
// this.volumeRendering.setZSlice(this.zSlice);
|
|
80
85
|
}
|
|
81
|
-
|
|
86
|
+
getPickBuffer() {
|
|
87
|
+
return this.pickRendering?.getPickBuffer();
|
|
88
|
+
}
|
|
82
89
|
/**
|
|
83
90
|
* Updates whether a channel's data must be loaded for rendering,
|
|
84
91
|
* based on if its volume or isosurface is enabled, or whether it is needed for masking.
|
|
@@ -201,6 +208,7 @@ export default class VolumeDrawable {
|
|
|
201
208
|
this.settings.secondaryRayStepSize = secondary;
|
|
202
209
|
}
|
|
203
210
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
211
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
204
212
|
}
|
|
205
213
|
updateScale() {
|
|
206
214
|
const {
|
|
@@ -212,6 +220,8 @@ export default class VolumeDrawable {
|
|
|
212
220
|
// TODO only `RayMarchedAtlasVolume` handles scale properly. Get the others on board too!
|
|
213
221
|
this.volumeRendering.updateVolumeDimensions();
|
|
214
222
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
223
|
+
this.pickRendering?.updateVolumeDimensions();
|
|
224
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
215
225
|
}
|
|
216
226
|
setOrthoScale(value) {
|
|
217
227
|
if (this.settings.orthoScale === value) {
|
|
@@ -219,6 +229,7 @@ export default class VolumeDrawable {
|
|
|
219
229
|
}
|
|
220
230
|
this.settings.orthoScale = value;
|
|
221
231
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
232
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
222
233
|
}
|
|
223
234
|
setResolution(x, y) {
|
|
224
235
|
const resolution = new Vector2(x, y);
|
|
@@ -226,6 +237,7 @@ export default class VolumeDrawable {
|
|
|
226
237
|
this.meshVolume.setResolution(x, y);
|
|
227
238
|
this.settings.resolution = resolution;
|
|
228
239
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
240
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
229
241
|
}
|
|
230
242
|
}
|
|
231
243
|
|
|
@@ -250,6 +262,7 @@ export default class VolumeDrawable {
|
|
|
250
262
|
this.meshVolume.setAxisClip(axis, minval, maxval, !!isOrthoAxis);
|
|
251
263
|
}
|
|
252
264
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI | SettingsFlags.VIEW);
|
|
265
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.ROI | SettingsFlags.VIEW);
|
|
253
266
|
}
|
|
254
267
|
modeStringToAxis(mode) {
|
|
255
268
|
const modeToAxis = {
|
|
@@ -284,6 +297,7 @@ export default class VolumeDrawable {
|
|
|
284
297
|
if (this.settings.viewAxis !== axis) {
|
|
285
298
|
this.settings.viewAxis = axis;
|
|
286
299
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
300
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
287
301
|
}
|
|
288
302
|
}
|
|
289
303
|
|
|
@@ -295,6 +309,7 @@ export default class VolumeDrawable {
|
|
|
295
309
|
}
|
|
296
310
|
this.settings.isOrtho = isOrtho;
|
|
297
311
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
312
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
298
313
|
}
|
|
299
314
|
setInterpolationEnabled(active) {
|
|
300
315
|
if (this.settings.useInterpolation === active) {
|
|
@@ -302,6 +317,7 @@ export default class VolumeDrawable {
|
|
|
302
317
|
}
|
|
303
318
|
this.settings.useInterpolation = active;
|
|
304
319
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
320
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
305
321
|
}
|
|
306
322
|
setOrthoThickness(value) {
|
|
307
323
|
if (this.renderMode === RenderMode.PATHTRACE) {
|
|
@@ -323,6 +339,7 @@ export default class VolumeDrawable {
|
|
|
323
339
|
this.settings.gammaLevel = glevel;
|
|
324
340
|
this.settings.gammaMax = gmax;
|
|
325
341
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.CAMERA);
|
|
342
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.CAMERA);
|
|
326
343
|
}
|
|
327
344
|
setFlipAxes(flipX, flipY, flipZ) {
|
|
328
345
|
const flipAxes = new Vector3(flipX, flipY, flipZ);
|
|
@@ -330,6 +347,7 @@ export default class VolumeDrawable {
|
|
|
330
347
|
this.settings.flipAxes = flipAxes;
|
|
331
348
|
this.meshVolume.setFlipAxes(flipX, flipY, flipZ);
|
|
332
349
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
350
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
333
351
|
}
|
|
334
352
|
}
|
|
335
353
|
setMaxProjectMode(isMaxProject) {
|
|
@@ -338,6 +356,7 @@ export default class VolumeDrawable {
|
|
|
338
356
|
}
|
|
339
357
|
this.settings.maxProjectMode = isMaxProject;
|
|
340
358
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
359
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
341
360
|
}
|
|
342
361
|
onAnimate(renderer, camera, depthTexture) {
|
|
343
362
|
// TODO: this is inefficient, as this work is duplicated by threejs.
|
|
@@ -351,6 +370,22 @@ export default class VolumeDrawable {
|
|
|
351
370
|
this.meshVolume.doRender();
|
|
352
371
|
}
|
|
353
372
|
}
|
|
373
|
+
enablePicking(enabled, channelIndex) {
|
|
374
|
+
// TODO delete the whole pickRendering, or just enable/disable it and keep it around?
|
|
375
|
+
// the current implementation will delete and recreate the pickRendering object
|
|
376
|
+
if (enabled) {
|
|
377
|
+
if (!this.pickRendering) {
|
|
378
|
+
this.pickRendering = new PickVolume(this.volume, this.settings);
|
|
379
|
+
}
|
|
380
|
+
this.pickRendering.setChannelToPick(channelIndex);
|
|
381
|
+
} else {
|
|
382
|
+
this.pickRendering?.cleanup();
|
|
383
|
+
this.pickRendering = undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
fillPickBuffer(renderer, camera, depthTexture) {
|
|
387
|
+
this.pickRendering?.doRender(renderer, camera, depthTexture);
|
|
388
|
+
}
|
|
354
389
|
getViewMode() {
|
|
355
390
|
return this.viewMode;
|
|
356
391
|
}
|
|
@@ -360,11 +395,23 @@ export default class VolumeDrawable {
|
|
|
360
395
|
hasIsosurface(channel) {
|
|
361
396
|
return this.meshVolume.hasIsosurface(channel);
|
|
362
397
|
}
|
|
398
|
+
setSelectedID(channelIndex, id) {
|
|
399
|
+
if (this.fusion.length > 0) {
|
|
400
|
+
// TODO does it make sense to do this for a particular channel?
|
|
401
|
+
if (id !== this.fusion[channelIndex].selectedID) {
|
|
402
|
+
this.fusion[channelIndex].selectedID = id;
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
363
408
|
fuse() {
|
|
364
409
|
if (!this.volume) {
|
|
365
410
|
return;
|
|
366
411
|
}
|
|
367
412
|
this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
|
|
413
|
+
// pickRendering only really works with one channel so we don't need to call
|
|
414
|
+
// its updateActiveChannels method
|
|
368
415
|
}
|
|
369
416
|
setRenderUpdateListener(callback) {
|
|
370
417
|
this.renderUpdateListener = callback;
|
|
@@ -380,10 +427,14 @@ export default class VolumeDrawable {
|
|
|
380
427
|
updateMaterial() {
|
|
381
428
|
this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
|
|
382
429
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
430
|
+
this.pickRendering?.updateActiveChannels(this.fusion, this.volume.channels);
|
|
431
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
383
432
|
}
|
|
384
433
|
updateLuts() {
|
|
385
434
|
this.volumeRendering.updateActiveChannels(this.fusion, this.volume.channels);
|
|
386
435
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
436
|
+
this.pickRendering?.updateActiveChannels(this.fusion, this.volume.channels);
|
|
437
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
387
438
|
}
|
|
388
439
|
setVoxelSize(values) {
|
|
389
440
|
this.volume.setVoxelSize(values);
|
|
@@ -392,6 +443,7 @@ export default class VolumeDrawable {
|
|
|
392
443
|
cleanup() {
|
|
393
444
|
this.meshVolume.cleanup();
|
|
394
445
|
this.volumeRendering.cleanup();
|
|
446
|
+
this.pickRendering?.cleanup();
|
|
395
447
|
}
|
|
396
448
|
getChannel(channelIndex) {
|
|
397
449
|
return this.volume.getChannel(channelIndex);
|
|
@@ -421,7 +473,8 @@ export default class VolumeDrawable {
|
|
|
421
473
|
this.fusion[newChannelIndex] = {
|
|
422
474
|
chIndex: newChannelIndex,
|
|
423
475
|
lut: new Uint8Array[LUT_ARRAY_LENGTH](),
|
|
424
|
-
rgbColor: [this.channelColors[newChannelIndex][0], this.channelColors[newChannelIndex][1], this.channelColors[newChannelIndex][2]]
|
|
476
|
+
rgbColor: [this.channelColors[newChannelIndex][0], this.channelColors[newChannelIndex][1], this.channelColors[newChannelIndex][2]],
|
|
477
|
+
selectedID: -1
|
|
425
478
|
};
|
|
426
479
|
this.settings.diffuse[newChannelIndex] = [this.channelColors[newChannelIndex][0], this.channelColors[newChannelIndex][1], this.channelColors[newChannelIndex][2]];
|
|
427
480
|
this.settings.specular[newChannelIndex] = [0, 0, 0];
|
|
@@ -442,6 +495,7 @@ export default class VolumeDrawable {
|
|
|
442
495
|
// if all are nulled out, then hide the volume element from the scene.
|
|
443
496
|
this.settings.visible = !this.fusion.every(elem => elem.rgbColor === 0);
|
|
444
497
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
498
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.VIEW);
|
|
445
499
|
|
|
446
500
|
// add or remove this channel from the list of required channels to load
|
|
447
501
|
this.updateChannelDataRequired(channelIndex);
|
|
@@ -496,6 +550,7 @@ export default class VolumeDrawable {
|
|
|
496
550
|
setDensity(density) {
|
|
497
551
|
this.settings.density = density;
|
|
498
552
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
553
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
499
554
|
}
|
|
500
555
|
|
|
501
556
|
/**
|
|
@@ -507,6 +562,7 @@ export default class VolumeDrawable {
|
|
|
507
562
|
setBrightness(brightness) {
|
|
508
563
|
this.settings.brightness = brightness;
|
|
509
564
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.CAMERA);
|
|
565
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.CAMERA);
|
|
510
566
|
}
|
|
511
567
|
getBrightness() {
|
|
512
568
|
return this.settings.brightness;
|
|
@@ -518,18 +574,32 @@ export default class VolumeDrawable {
|
|
|
518
574
|
this.settings.maskChannelIndex = channelIndex;
|
|
519
575
|
this.updateChannelDataRequired(channelIndex);
|
|
520
576
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MASK_DATA);
|
|
577
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.MASK_DATA);
|
|
578
|
+
}
|
|
579
|
+
setChannelColorizeFeature(channelIndex, featureInfo) {
|
|
580
|
+
// TODO only one channel can ever have this?
|
|
581
|
+
if (!featureInfo) {
|
|
582
|
+
this.fusion[channelIndex].feature = undefined;
|
|
583
|
+
} else {
|
|
584
|
+
this.fusion[channelIndex].feature = featureInfo;
|
|
585
|
+
}
|
|
586
|
+
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
587
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.MATERIAL);
|
|
521
588
|
}
|
|
522
589
|
setMaskAlpha(maskAlpha) {
|
|
523
590
|
this.settings.maskAlpha = maskAlpha;
|
|
524
591
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.MASK_ALPHA);
|
|
592
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.MASK_ALPHA);
|
|
525
593
|
}
|
|
526
594
|
setShowBoundingBox(showBoundingBox) {
|
|
527
595
|
this.settings.showBoundingBox = showBoundingBox;
|
|
528
596
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.BOUNDING_BOX);
|
|
597
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.BOUNDING_BOX);
|
|
529
598
|
}
|
|
530
599
|
setBoundingBoxColor(color) {
|
|
531
600
|
this.settings.boundingBoxColor = color;
|
|
532
601
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.BOUNDING_BOX);
|
|
602
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.BOUNDING_BOX);
|
|
533
603
|
}
|
|
534
604
|
getIntensity(c, x, y, z) {
|
|
535
605
|
return this.volume.getIntensity(c, x, y, z);
|
|
@@ -543,6 +613,7 @@ export default class VolumeDrawable {
|
|
|
543
613
|
if (this.renderMode === RenderMode.PATHTRACE) {
|
|
544
614
|
this.volumeRendering.onChangeControls();
|
|
545
615
|
}
|
|
616
|
+
this.pickRendering?.viewpointMoved();
|
|
546
617
|
}
|
|
547
618
|
onEndControls() {
|
|
548
619
|
if (this.renderMode === RenderMode.PATHTRACE) {
|
|
@@ -551,11 +622,13 @@ export default class VolumeDrawable {
|
|
|
551
622
|
}
|
|
552
623
|
onResetCamera() {
|
|
553
624
|
this.volumeRendering.viewpointMoved();
|
|
625
|
+
this.pickRendering?.viewpointMoved();
|
|
554
626
|
}
|
|
555
627
|
onCameraChanged(fov, focalDistance, apertureSize) {
|
|
556
628
|
if (this.renderMode === RenderMode.PATHTRACE) {
|
|
557
629
|
this.volumeRendering.updateCamera(fov, focalDistance, apertureSize);
|
|
558
630
|
}
|
|
631
|
+
this.pickRendering?.viewpointMoved();
|
|
559
632
|
}
|
|
560
633
|
|
|
561
634
|
// values are in 0..1 range
|
|
@@ -564,6 +637,7 @@ export default class VolumeDrawable {
|
|
|
564
637
|
this.settings.bounds.bmax = new Vector3(xmax - 0.5, ymax - 0.5, zmax - 0.5);
|
|
565
638
|
this.meshVolume.updateClipRegion(xmin, xmax, ymin, ymax, zmin, zmax);
|
|
566
639
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI);
|
|
640
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.ROI);
|
|
567
641
|
}
|
|
568
642
|
updateLights(state) {
|
|
569
643
|
if (this.renderMode === RenderMode.PATHTRACE) {
|
|
@@ -573,6 +647,7 @@ export default class VolumeDrawable {
|
|
|
573
647
|
setPixelSamplingRate(value) {
|
|
574
648
|
this.settings.pixelSamplingRate = value;
|
|
575
649
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
650
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.SAMPLING);
|
|
576
651
|
}
|
|
577
652
|
setVolumeRendering(newRenderMode) {
|
|
578
653
|
// Skip reassignment of Pathtrace renderer if already using
|
|
@@ -588,6 +663,7 @@ export default class VolumeDrawable {
|
|
|
588
663
|
|
|
589
664
|
// destroy old resources.
|
|
590
665
|
this.volumeRendering.cleanup();
|
|
666
|
+
this.pickRendering?.cleanup();
|
|
591
667
|
|
|
592
668
|
// create new
|
|
593
669
|
switch (newRenderMode) {
|
|
@@ -610,6 +686,9 @@ export default class VolumeDrawable {
|
|
|
610
686
|
});
|
|
611
687
|
break;
|
|
612
688
|
}
|
|
689
|
+
if (this.pickRendering) {
|
|
690
|
+
this.pickRendering = new PickVolume(this.volume, this.settings);
|
|
691
|
+
}
|
|
613
692
|
if (newRenderMode === RenderMode.RAYMARCH || newRenderMode === RenderMode.SLICE) {
|
|
614
693
|
if (this.renderUpdateListener) {
|
|
615
694
|
this.renderUpdateListener(0);
|
|
@@ -626,11 +705,13 @@ export default class VolumeDrawable {
|
|
|
626
705
|
this.settings.translation.copy(xyz);
|
|
627
706
|
this.meshVolume.setTranslation(this.settings.translation);
|
|
628
707
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
708
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
629
709
|
}
|
|
630
710
|
setRotation(eulerXYZ) {
|
|
631
711
|
this.settings.rotation.copy(eulerXYZ);
|
|
632
712
|
this.meshVolume.setRotation(this.settings.rotation);
|
|
633
713
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
714
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.TRANSFORM);
|
|
634
715
|
}
|
|
635
716
|
setScale(xyz) {
|
|
636
717
|
this.settings.scale.copy(xyz);
|
|
@@ -692,6 +773,7 @@ export default class VolumeDrawable {
|
|
|
692
773
|
if (this.settings.zSlice !== slice && slice < sizez && slice >= 0) {
|
|
693
774
|
this.settings.zSlice = slice;
|
|
694
775
|
this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI);
|
|
776
|
+
this.pickRendering?.updateSettings(this.settings, SettingsFlags.ROI);
|
|
695
777
|
return true;
|
|
696
778
|
}
|
|
697
779
|
return false;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Vector2, Vector3, Matrix4, Texture } from "three";
|
|
2
|
+
/* babel-plugin-inline-import './shaders/raymarch.vert' */
|
|
3
|
+
const rayMarchVertexShader = "// switch on high precision floats\n#ifdef GL_ES\nprecision highp float;\n#endif\n\nvarying vec3 pObj;\n\nvoid main() {\n pObj = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n";
|
|
4
|
+
/* babel-plugin-inline-import './shaders/volumePick.frag' */
|
|
5
|
+
const rayMarchFragmentShader = "\n#ifdef GL_ES\nprecision highp float;\nprecision highp usampler2D;\n#endif\n\n#define M_PI 3.14159265358979323846\n\nuniform vec2 iResolution;\nuniform vec2 textureRes;\n\n//uniform float maskAlpha;\nuniform vec2 ATLAS_DIMS;\nuniform vec3 AABB_CLIP_MIN;\nuniform float CLIP_NEAR;\nuniform vec3 AABB_CLIP_MAX;\nuniform float CLIP_FAR;\n// one raw channel atlas that has segmentation data\nuniform usampler2D textureAtlas;\n//uniform sampler2D textureAtlasMask;\nuniform sampler2D textureDepth;\nuniform int usingPositionTexture;\nuniform int BREAK_STEPS;\nuniform float SLICES;\nuniform float isOrtho;\nuniform float orthoThickness;\nuniform float orthoScale;\nuniform int maxProject;\nuniform vec3 flipVolume;\nuniform vec3 volumeScale;\n\n// view space to axis-aligned volume box\nuniform mat4 inverseModelViewMatrix;\nuniform mat4 inverseProjMatrix;\n\nvarying vec3 pObj;\n\nfloat powf(float a, float b) {\n return pow(a,b);\n}\n\nfloat rand(vec2 co) {\n float threadId = gl_FragCoord.x/(gl_FragCoord.y + 1.0);\n float bigVal = threadId*1299721.0/911.0;\n vec2 smallVal = vec2(threadId*7927.0/577.0, threadId*104743.0/1039.0);\n return fract(sin(dot(co, smallVal)) * bigVal);\n}\n\nvec2 offsetFrontBack(float t) {\n int a = int(t);\n int ax = int(ATLAS_DIMS.x);\n vec2 os = vec2(float(a - (a / ax) * ax), float(a / ax)) / ATLAS_DIMS;\n return clamp(os, vec2(0.0), vec2(1.0) - vec2(1.0) / ATLAS_DIMS);\n}\n\nuint sampleAtlasNearest(usampler2D tex, vec4 pos) {\n uint bounds = uint(pos[0] >= 0.0 && pos[0] <= 1.0 &&\n pos[1] >= 0.0 && pos[1] <= 1.0 &&\n pos[2] >= 0.0 && pos[2] <= 1.0 );\n float nSlices = float(SLICES);\n\n vec2 loc0 = ((pos.xy - 0.5) * flipVolume.xy + 0.5) / ATLAS_DIMS;\n\n // No interpolation - sample just one slice at a pixel center.\n // Ideally this would be accomplished in part by switching this texture to linear\n // filtering, but three makes this difficult to do through a WebGLRenderTarget.\n loc0 = floor(loc0 * textureRes) / textureRes;\n loc0 += vec2(0.5) / textureRes;\n\n float z = min(floor(pos.z * nSlices), nSlices-1.0);\n \n if (flipVolume.z == -1.0) {\n z = nSlices - z - 1.0;\n }\n\n vec2 o = offsetFrontBack(z) + loc0;\n uint voxelColor = texture2D(tex, o).x;\n\n // Apply mask\n// float voxelMask = texture2D(textureAtlasMask, o).x;\n// voxelMask = mix(voxelMask, 1.0, maskAlpha);\n// voxelColor.rgb *= voxelMask;\n\n return bounds*voxelColor;\n}\n\nbool intersectBox(in vec3 r_o, in vec3 r_d, in vec3 boxMin, in vec3 boxMax,\n out float tnear, out float tfar) {\n // compute intersection of ray with all six bbox planes\n vec3 invR = vec3(1.0,1.0,1.0) / r_d;\n vec3 tbot = invR * (boxMin - r_o);\n vec3 ttop = invR * (boxMax - r_o);\n\n // re-order intersections to find smallest and largest on each axis\n vec3 tmin = min(ttop, tbot);\n vec3 tmax = max(ttop, tbot);\n\n // find the largest tmin and the smallest tmax\n float largest_tmin = max(max(tmin.x, tmin.y), tmin.z);\n float smallest_tmax = min(min(tmax.x, tmax.y), tmax.z);\n\n tnear = largest_tmin;\n tfar = smallest_tmax;\n\n // use >= here?\n return(smallest_tmax > largest_tmin);\n}\n\nvec4 integrateVolume(vec4 eye_o,vec4 eye_d,\n float tnear, float tfar,\n float clipNear, float clipFar,\n usampler2D textureAtlas\n ) {\n uint C = 0u;\n // march along ray from front to back, accumulating color\n\n // estimate step length\n const int maxSteps = 512;\n // modify the 3 components of eye_d by volume scale\n float scaledSteps = float(BREAK_STEPS) * length((eye_d.xyz/volumeScale));\n float csteps = clamp(float(scaledSteps), 1.0, float(maxSteps));\n float invstep = (tfar-tnear)/csteps;\n // special-casing the single slice to remove the random ray dither.\n // this removes a Moire pattern visible in single slice images, which we want to view as 2D images as best we can.\n float r = (SLICES==1.0) ? 0.0 : rand(eye_d.xy);\n // if ortho and clipped, make step size smaller so we still get same number of steps\n float tstep = invstep*orthoThickness;\n float tfarsurf = r*tstep;\n float overflow = mod((tfarsurf - tfar),tstep); // random dithering offset\n float t = tnear + overflow;\n t += r*tstep; // random dithering offset\n float tdist = 0.0;\n int numSteps = 0;\n vec4 pos, col;\n for (int i = 0; i < maxSteps; i++) {\n pos = eye_o + eye_d*t;\n // !!! assume box bounds are -0.5 .. 0.5. pos = (pos-min)/(max-min)\n // scaling is handled by model transform and already accounted for before we get here.\n // AABB clip is independent of this and is only used to determine tnear and tfar.\n pos.xyz = (pos.xyz-(-0.5))/((0.5)-(-0.5)); //0.5 * (pos + 1.0); // map position from [boxMin, boxMax] to [0, 1] coordinates\n\n uint col = sampleAtlasNearest(textureAtlas, pos);\n\n // FOR INTERSECTION / PICKING, the FIRST nonzero intensity terminates the raymarch\n\n if (maxProject != 0) {\n C = max(col, C);\n } else {\n if (col > 0u) {\n C = col;\n break;\n }\n }\n t += tstep;\n numSteps = i;\n\n if (t > tfar || t > tnear+clipFar ) break;\n }\n\n return vec4(float(C));\n}\n\nvoid main() {\n gl_FragColor = vec4(0.0);\n vec2 vUv = gl_FragCoord.xy/iResolution.xy;\n\n vec3 eyeRay_o, eyeRay_d;\n\n if (isOrtho == 0.0) {\n // for perspective rays:\n // world space camera coordinates\n // transform to object space\n eyeRay_o = (inverseModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;\n eyeRay_d = normalize(pObj - eyeRay_o);\n } else {\n // for ortho rays:\n float zDist = 2.0;\n eyeRay_d = (inverseModelViewMatrix*vec4(0.0, 0.0, -zDist, 0.0)).xyz;\n vec4 ray_o = vec4(2.0*vUv - 1.0, 1.0, 1.0);\n ray_o.xy *= orthoScale;\n ray_o.x *= iResolution.x/iResolution.y;\n eyeRay_o = (inverseModelViewMatrix*ray_o).xyz;\n }\n\n // -0.5..0.5 is full box. AABB_CLIP lets us clip to a box shaped ROI to look at\n // I am applying it here at the earliest point so that the ray march does\n // not waste steps. For general shaped ROI, this has to be handled more\n // generally (obviously)\n vec3 boxMin = AABB_CLIP_MIN;\n vec3 boxMax = AABB_CLIP_MAX;\n\n float tnear, tfar;\n bool hit = intersectBox(eyeRay_o, eyeRay_d, boxMin, boxMax, tnear, tfar);\n\n if (!hit) {\n // return background color if ray misses the cube\n // is this safe to do when there is other geometry / gObjects drawn?\n gl_FragColor = vec4(0.0); //C1;//vec4(0.0);\n return;\n }\n\n float clipNear = 0.0;//-(dot(eyeRay_o.xyz, eyeNorm) + dNear) / dot(eyeRay_d.xyz, eyeNorm);\n float clipFar = 10000.0;//-(dot(eyeRay_o.xyz,-eyeNorm) + dFar ) / dot(eyeRay_d.xyz,-eyeNorm);\n\n // Sample the depth/position texture\n // If this is a depth texture, the r component is a depth value. If this is a position texture,\n // the xyz components are a view space position and w is 1.0 iff there's a mesh at this fragment.\n vec4 meshPosSample = texture2D(textureDepth, vUv);\n // Note: we make a different check for whether a mesh is present with depth vs. position textures.\n // Here's the check for depth textures:\n bool hasDepthValue = usingPositionTexture == 0 && meshPosSample.r < 1.0;\n\n // If there's a depth-contributing mesh at this fragment, we may need to terminate the ray early\n if (hasDepthValue || (usingPositionTexture == 1 && meshPosSample.a > 0.0)) {\n if (hasDepthValue) {\n // We're working with a depth value, so we need to convert back to view space position\n // Get a projection space position from depth and uv, and unproject back to view space\n vec4 meshProj = vec4(vUv * 2.0 - 1.0, meshPosSample.r * 2.0 - 1.0, 1.0);\n vec4 meshView = inverseProjMatrix * meshProj;\n meshPosSample = vec4(meshView.xyz / meshView.w, 1.0);\n }\n // Transform the mesh position to object space\n vec4 meshObj = inverseModelViewMatrix * meshPosSample;\n\n // Derive a t value for the mesh intersection\n // NOTE: divides by 0 when `eyeRay_d.z` is 0. Could be mitigated by picking another component\n // to derive with when z is 0, but I found this was rare enough in practice to be acceptable.\n float tMesh = (meshObj.z - eyeRay_o.z) / eyeRay_d.z;\n if (tMesh < tfar) {\n clipFar = tMesh - tnear;\n }\n }\n\n vec4 C = integrateVolume(vec4(eyeRay_o,1.0), vec4(eyeRay_d,0.0),\n tnear, tfar, //intersections of box\n clipNear, clipFar,\n textureAtlas);\n\n gl_FragColor = C;\n return;\n}\n";
|
|
6
|
+
export const pickVertexShaderSrc = rayMarchVertexShader;
|
|
7
|
+
export const pickFragmentShaderSrc = rayMarchFragmentShader;
|
|
8
|
+
export const pickShaderUniforms = () => {
|
|
9
|
+
return {
|
|
10
|
+
iResolution: {
|
|
11
|
+
type: "v2",
|
|
12
|
+
value: new Vector2(100, 100)
|
|
13
|
+
},
|
|
14
|
+
textureRes: {
|
|
15
|
+
type: "v2",
|
|
16
|
+
value: new Vector2(1.0, 1.0)
|
|
17
|
+
},
|
|
18
|
+
ATLAS_DIMS: {
|
|
19
|
+
type: "v2",
|
|
20
|
+
value: new Vector2(6, 6)
|
|
21
|
+
},
|
|
22
|
+
AABB_CLIP_MIN: {
|
|
23
|
+
type: "v3",
|
|
24
|
+
value: new Vector3(-0.5, -0.5, -0.5)
|
|
25
|
+
},
|
|
26
|
+
CLIP_NEAR: {
|
|
27
|
+
type: "f",
|
|
28
|
+
value: 0.1
|
|
29
|
+
},
|
|
30
|
+
AABB_CLIP_MAX: {
|
|
31
|
+
type: "v3",
|
|
32
|
+
value: new Vector3(0.5, 0.5, 0.5)
|
|
33
|
+
},
|
|
34
|
+
CLIP_FAR: {
|
|
35
|
+
type: "f",
|
|
36
|
+
value: 20.0
|
|
37
|
+
},
|
|
38
|
+
textureAtlas: {
|
|
39
|
+
type: "t",
|
|
40
|
+
value: new Texture()
|
|
41
|
+
},
|
|
42
|
+
textureDepth: {
|
|
43
|
+
type: "t",
|
|
44
|
+
value: new Texture()
|
|
45
|
+
},
|
|
46
|
+
usingPositionTexture: {
|
|
47
|
+
type: "i",
|
|
48
|
+
value: 0
|
|
49
|
+
},
|
|
50
|
+
BREAK_STEPS: {
|
|
51
|
+
type: "i",
|
|
52
|
+
value: 128
|
|
53
|
+
},
|
|
54
|
+
SLICES: {
|
|
55
|
+
type: "f",
|
|
56
|
+
value: 50
|
|
57
|
+
},
|
|
58
|
+
isOrtho: {
|
|
59
|
+
type: "f",
|
|
60
|
+
value: 0.0
|
|
61
|
+
},
|
|
62
|
+
orthoThickness: {
|
|
63
|
+
type: "f",
|
|
64
|
+
value: 1.0
|
|
65
|
+
},
|
|
66
|
+
orthoScale: {
|
|
67
|
+
type: "f",
|
|
68
|
+
value: 0.5 // needs to come from ThreeJsPanel's setting
|
|
69
|
+
},
|
|
70
|
+
maxProject: {
|
|
71
|
+
type: "i",
|
|
72
|
+
value: 0
|
|
73
|
+
},
|
|
74
|
+
flipVolume: {
|
|
75
|
+
type: "v3",
|
|
76
|
+
value: new Vector3(1.0, 1.0, 1.0)
|
|
77
|
+
},
|
|
78
|
+
volumeScale: {
|
|
79
|
+
type: "v3",
|
|
80
|
+
value: new Vector3(1.0, 1.0, 1.0)
|
|
81
|
+
},
|
|
82
|
+
inverseModelViewMatrix: {
|
|
83
|
+
type: "m4",
|
|
84
|
+
value: new Matrix4()
|
|
85
|
+
},
|
|
86
|
+
inverseProjMatrix: {
|
|
87
|
+
type: "m4",
|
|
88
|
+
value: new Matrix4()
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -2,7 +2,7 @@ import { Vector2, Vector3, Matrix4, Texture } from "three";
|
|
|
2
2
|
/* babel-plugin-inline-import './shaders/raymarch.vert' */
|
|
3
3
|
const rayMarchVertexShader = "// switch on high precision floats\n#ifdef GL_ES\nprecision highp float;\n#endif\n\nvarying vec3 pObj;\n\nvoid main() {\n pObj = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n";
|
|
4
4
|
/* babel-plugin-inline-import './shaders/raymarch.frag' */
|
|
5
|
-
const rayMarchFragmentShader = "\n#ifdef GL_ES\nprecision highp float;\n#endif\n\n#define M_PI 3.14159265358979323846\n\nuniform vec2 iResolution;\nuniform vec2 textureRes;\nuniform float GAMMA_MIN;\nuniform float GAMMA_MAX;\nuniform float GAMMA_SCALE;\nuniform float BRIGHTNESS;\nuniform float DENSITY;\nuniform float maskAlpha;\nuniform vec2 ATLAS_DIMS;\nuniform vec3 AABB_CLIP_MIN;\nuniform float CLIP_NEAR;\nuniform vec3 AABB_CLIP_MAX;\nuniform float CLIP_FAR;\nuniform sampler2D textureAtlas;\nuniform sampler2D textureAtlasMask;\nuniform sampler2D textureDepth;\nuniform int usingPositionTexture;\nuniform int BREAK_STEPS;\nuniform float SLICES;\nuniform float isOrtho;\nuniform float orthoThickness;\nuniform float orthoScale;\nuniform int maxProject;\nuniform bool interpolationEnabled;\nuniform vec3 flipVolume;\nuniform vec3 volumeScale;\n\n// view space to axis-aligned volume box\nuniform mat4 inverseModelViewMatrix;\nuniform mat4 inverseProjMatrix;\n\nvarying vec3 pObj;\n\nfloat powf(float a, float b) {\n return pow(a,b);\n}\n\nfloat rand(vec2 co) {\n float threadId = gl_FragCoord.x/(gl_FragCoord.y + 1.0);\n float bigVal = threadId*1299721.0/911.0;\n vec2 smallVal = vec2(threadId*7927.0/577.0, threadId*104743.0/1039.0);\n return fract(sin(dot(co, smallVal)) * bigVal);\n}\n\nvec4 luma2Alpha(vec4 color, float vmin, float vmax, float C) {\n float x = dot(color.rgb, vec3(0.2125, 0.7154, 0.0721));\n // float x = max(color[2], max(color[0],color[1]));\n float xi = (x-vmin)/(vmax-vmin);\n xi = clamp(xi,0.0,1.0);\n float y = pow(xi,C);\n y = clamp(y,0.0,1.0);\n color[3] = y;\n return color;\n}\n\nvec2 offsetFrontBack(float t) {\n int a = int(t);\n int ax = int(ATLAS_DIMS.x);\n vec2 os = vec2(float(a - (a / ax) * ax), float(a / ax)) / ATLAS_DIMS;\n return clamp(os, vec2(0.0), vec2(1.0) - vec2(1.0) / ATLAS_DIMS);\n}\n\nvec4 sampleAtlasLinear(sampler2D tex, vec4 pos) {\n float bounds = float(pos[0] >= 0.0 && pos[0] <= 1.0 &&\n pos[1] >= 0.0 && pos[1] <= 1.0 &&\n pos[2] >= 0.0 && pos[2] <= 1.0 );\n float nSlices = float(SLICES);\n // get location within atlas tile\n // TODO: get loc1 which follows ray to next slice along ray direction\n // when flipvolume = 1: pos\n // when flipvolume = -1: 1-pos\n vec2 loc0 = ((pos.xy - 0.5) * flipVolume.xy + 0.5) / ATLAS_DIMS;\n\n // loc ranges from 0 to 1/ATLAS_DIMS\n // shrink loc0 to within one half edge texel - so as not to sample across edges of tiles.\n loc0 = vec2(0.5) / textureRes + loc0 * (vec2(1.0) - ATLAS_DIMS / textureRes);\n \n // interpolate between two slices\n float z = (pos.z)*(nSlices-1.0);\n float z0 = floor(z);\n float t = z-z0; //mod(z, 1.0);\n float z1 = min(z0+1.0, nSlices-1.0);\n\n // flipped:\n if (flipVolume.z == -1.0) {\n z0 = nSlices - z0 - 1.0;\n z1 = nSlices - z1 - 1.0;\n t = 1.0 - t;\n }\n\n // get slice offsets in texture atlas\n vec2 o0 = offsetFrontBack(z0) + loc0;\n vec2 o1 = offsetFrontBack(z1) + loc0;\n\n vec4 slice0Color = texture2D(tex, o0);\n vec4 slice1Color = texture2D(tex, o1);\n // NOTE we could premultiply the mask in the fuse function,\n // but that is slower to update the maskAlpha value than here in the shader.\n // it is a memory vs perf tradeoff. Do users really need to update the maskAlpha at realtime speed?\n float slice0Mask = texture2D(textureAtlasMask, o0).x;\n float slice1Mask = texture2D(textureAtlasMask, o1).x;\n // or use max for conservative 0 or 1 masking?\n float maskVal = mix(slice0Mask, slice1Mask, t);\n // take mask from 0..1 to alpha..1\n maskVal = mix(maskVal, 1.0, maskAlpha);\n vec4 retval = mix(slice0Color, slice1Color, t);\n // only mask the rgb, not the alpha(?)\n retval.rgb *= maskVal;\n return bounds*retval;\n}\n\nvec4 sampleAtlasNearest(sampler2D tex, vec4 pos) {\n float bounds = float(pos[0] >= 0.0 && pos[0] <= 1.0 &&\n pos[1] >= 0.0 && pos[1] <= 1.0 &&\n pos[2] >= 0.0 && pos[2] <= 1.0 );\n float nSlices = float(SLICES);\n\n vec2 loc0 = ((pos.xy - 0.5) * flipVolume.xy + 0.5) / ATLAS_DIMS;\n\n // No interpolation - sample just one slice at a pixel center.\n // Ideally this would be accomplished in part by switching this texture to linear\n // filtering, but three makes this difficult to do through a WebGLRenderTarget.\n loc0 = floor(loc0 * textureRes) / textureRes;\n loc0 += vec2(0.5) / textureRes;\n\n float z = min(floor(pos.z * nSlices), nSlices-1.0);\n \n if (flipVolume.z == -1.0) {\n z = nSlices - z - 1.0;\n }\n\n vec2 o = offsetFrontBack(z) + loc0;\n vec4 voxelColor = texture2D(tex, o);\n\n // Apply mask\n float voxelMask = texture2D(textureAtlasMask, o).x;\n voxelMask = mix(voxelMask, 1.0, maskAlpha);\n voxelColor.rgb *= voxelMask;\n\n return bounds*voxelColor;\n}\n\nbool intersectBox(in vec3 r_o, in vec3 r_d, in vec3 boxMin, in vec3 boxMax,\n out float tnear, out float tfar) {\n // compute intersection of ray with all six bbox planes\n vec3 invR = vec3(1.0,1.0,1.0) / r_d;\n vec3 tbot = invR * (boxMin - r_o);\n vec3 ttop = invR * (boxMax - r_o);\n\n // re-order intersections to find smallest and largest on each axis\n vec3 tmin = min(ttop, tbot);\n vec3 tmax = max(ttop, tbot);\n\n // find the largest tmin and the smallest tmax\n float largest_tmin = max(max(tmin.x, tmin.y), max(tmin.x, tmin.z));\n float smallest_tmax = min(min(tmax.x, tmax.y), min(tmax.x, tmax.z));\n\n tnear = largest_tmin;\n tfar = smallest_tmax;\n\n // use >= here?\n return(smallest_tmax > largest_tmin);\n}\n\nvec4 accumulate(vec4 col, float s, vec4 C) {\n float stepScale = (1.0 - powf((1.0-col.w),s));\n col.w = stepScale;\n col.xyz *= col.w;\n col = clamp(col,0.0,1.0);\n\n C = (1.0-C.w)*col + C;\n return C;\n}\n\nvec4 integrateVolume(vec4 eye_o,vec4 eye_d,\n float tnear, float tfar,\n float clipNear, float clipFar,\n sampler2D textureAtlas\n ) {\n vec4 C = vec4(0.0);\n // march along ray from front to back, accumulating color\n\n // estimate step length\n const int maxSteps = 512;\n // modify the 3 components of eye_d by volume scale\n float scaledSteps = float(BREAK_STEPS) * length((eye_d.xyz/volumeScale));\n float csteps = clamp(float(scaledSteps), 1.0, float(maxSteps));\n float invstep = (tfar-tnear)/csteps;\n // special-casing the single slice to remove the random ray dither.\n // this removes a Moire pattern visible in single slice images, which we want to view as 2D images as best we can.\n float r = (SLICES==1.0) ? 0.0 : rand(eye_d.xy);\n // if ortho and clipped, make step size smaller so we still get same number of steps\n float tstep = invstep*orthoThickness;\n float tfarsurf = r*tstep;\n float overflow = mod((tfarsurf - tfar),tstep); // random dithering offset\n float t = tnear + overflow;\n t += r*tstep; // random dithering offset\n float tdist = 0.0;\n int numSteps = 0;\n vec4 pos, col;\n // We need to be able to scale the alpha contrib with number of ray steps,\n // in order to make the final color invariant to the step size(?)\n // use maxSteps (a constant) as the numerator... Not sure if this is sound.\n float s = 0.5 * float(maxSteps) / csteps;\n for (int i = 0; i < maxSteps; i++) {\n pos = eye_o + eye_d*t;\n // !!! assume box bounds are -0.5 .. 0.5. pos = (pos-min)/(max-min)\n // scaling is handled by model transform and already accounted for before we get here.\n // AABB clip is independent of this and is only used to determine tnear and tfar.\n pos.xyz = (pos.xyz-(-0.5))/((0.5)-(-0.5)); //0.5 * (pos + 1.0); // map position from [boxMin, boxMax] to [0, 1] coordinates\n\n vec4 col = interpolationEnabled ? sampleAtlasLinear(textureAtlas, pos) : sampleAtlasNearest(textureAtlas, pos);\n\n if (maxProject != 0) {\n col.xyz *= BRIGHTNESS;\n C = max(col, C);\n } else {\n col = luma2Alpha(col, GAMMA_MIN, GAMMA_MAX, GAMMA_SCALE);\n col.xyz *= BRIGHTNESS;\n // for practical use the density only matters for regular volume integration\n col.w *= DENSITY;\n C = accumulate(col, s, C);\n }\n t += tstep;\n numSteps = i;\n\n if (t > tfar || t > tnear+clipFar ) break;\n if (C.w > 1.0 ) break;\n }\n\n return C;\n}\n\nvoid main() {\n gl_FragColor = vec4(0.0);\n vec2 vUv = gl_FragCoord.xy/iResolution.xy;\n\n vec3 eyeRay_o, eyeRay_d;\n\n if (isOrtho == 0.0) {\n // for perspective rays:\n // world space camera coordinates\n // transform to object space\n eyeRay_o = (inverseModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;\n eyeRay_d = normalize(pObj - eyeRay_o);\n } else {\n // for ortho rays:\n float zDist = 2.0;\n eyeRay_d = (inverseModelViewMatrix*vec4(0.0, 0.0, -zDist, 0.0)).xyz;\n vec4 ray_o = vec4(2.0*vUv - 1.0, 1.0, 1.0);\n ray_o.xy *= orthoScale;\n ray_o.x *= iResolution.x/iResolution.y;\n eyeRay_o = (inverseModelViewMatrix*ray_o).xyz;\n }\n\n // -0.5..0.5 is full box. AABB_CLIP lets us clip to a box shaped ROI to look at\n // I am applying it here at the earliest point so that the ray march does\n // not waste steps. For general shaped ROI, this has to be handled more\n // generally (obviously)\n vec3 boxMin = AABB_CLIP_MIN;\n vec3 boxMax = AABB_CLIP_MAX;\n\n float tnear, tfar;\n bool hit = intersectBox(eyeRay_o, eyeRay_d, boxMin, boxMax, tnear, tfar);\n\n if (!hit) {\n // return background color if ray misses the cube\n // is this safe to do when there is other geometry / gObjects drawn?\n gl_FragColor = vec4(0.0); //C1;//vec4(0.0);\n return;\n }\n\n float clipNear = 0.0;//-(dot(eyeRay_o.xyz, eyeNorm) + dNear) / dot(eyeRay_d.xyz, eyeNorm);\n float clipFar = 10000.0;//-(dot(eyeRay_o.xyz,-eyeNorm) + dFar ) / dot(eyeRay_d.xyz,-eyeNorm);\n\n // Sample the depth/position texture\n // If this is a depth texture, the r component is a depth value. If this is a position texture,\n // the xyz components are a view space position and w is 1.0 iff there's a mesh at this fragment.\n vec4 meshPosSample = texture2D(textureDepth, vUv);\n // Note: we make a different check for whether a mesh is present with depth vs. position textures.\n // Here's the check for depth textures:\n bool hasDepthValue = usingPositionTexture == 0 && meshPosSample.r < 1.0;\n\n // If there's a depth-contributing mesh at this fragment, we may need to terminate the ray early\n if (hasDepthValue || (usingPositionTexture == 1 && meshPosSample.a > 0.0)) {\n if (hasDepthValue) {\n // We're working with a depth value, so we need to convert back to view space position\n // Get a projection space position from depth and uv, and unproject back to view space\n vec4 meshProj = vec4(vUv * 2.0 - 1.0, meshPosSample.r * 2.0 - 1.0, 1.0);\n vec4 meshView = inverseProjMatrix * meshProj;\n meshPosSample = vec4(meshView.xyz / meshView.w, 1.0);\n }\n // Transform the mesh position to object space\n vec4 meshObj = inverseModelViewMatrix * meshPosSample;\n\n // Derive a t value for the mesh intersection\n // NOTE: divides by 0 when `eyeRay_d.z` is 0. Could be mitigated by picking another component\n // to derive with when z is 0, but I found this was rare enough in practice to be acceptable.\n float tMesh = (meshObj.z - eyeRay_o.z) / eyeRay_d.z;\n if (tMesh < tfar) {\n clipFar = tMesh - tnear;\n }\n }\n\n vec4 C = integrateVolume(vec4(eyeRay_o,1.0), vec4(eyeRay_d,0.0),\n tnear, tfar, //intersections of box\n clipNear, clipFar,\n textureAtlas);\n\n C = clamp(C, 0.0, 1.0);\n gl_FragColor = C;\n return;\n}\n";
|
|
5
|
+
const rayMarchFragmentShader = "\n#ifdef GL_ES\nprecision highp float;\n#endif\n\n#define M_PI 3.14159265358979323846\n\nuniform vec2 iResolution;\nuniform vec2 textureRes;\nuniform float GAMMA_MIN;\nuniform float GAMMA_MAX;\nuniform float GAMMA_SCALE;\nuniform float BRIGHTNESS;\nuniform float DENSITY;\nuniform float maskAlpha;\nuniform vec2 ATLAS_DIMS;\nuniform vec3 AABB_CLIP_MIN;\nuniform float CLIP_NEAR;\nuniform vec3 AABB_CLIP_MAX;\nuniform float CLIP_FAR;\nuniform sampler2D textureAtlas;\nuniform sampler2D textureAtlasMask;\nuniform sampler2D textureDepth;\nuniform int usingPositionTexture;\nuniform int BREAK_STEPS;\nuniform float SLICES;\nuniform float isOrtho;\nuniform float orthoThickness;\nuniform float orthoScale;\nuniform int maxProject;\nuniform bool interpolationEnabled;\nuniform vec3 flipVolume;\nuniform vec3 volumeScale;\n\n// view space to axis-aligned volume box\nuniform mat4 inverseModelViewMatrix;\nuniform mat4 inverseProjMatrix;\n\nvarying vec3 pObj;\n\nfloat powf(float a, float b) {\n return pow(a,b);\n}\n\nfloat rand(vec2 co) {\n float threadId = gl_FragCoord.x/(gl_FragCoord.y + 1.0);\n float bigVal = threadId*1299721.0/911.0;\n vec2 smallVal = vec2(threadId*7927.0/577.0, threadId*104743.0/1039.0);\n return fract(sin(dot(co, smallVal)) * bigVal);\n}\n\nvec4 luma2Alpha(vec4 color, float vmin, float vmax, float C) {\n float x = dot(color.rgb, vec3(0.2125, 0.7154, 0.0721));\n // float x = max(color[2], max(color[0],color[1]));\n float xi = (x-vmin)/(vmax-vmin);\n xi = clamp(xi,0.0,1.0);\n float y = pow(xi,C);\n y = clamp(y,0.0,1.0);\n color[3] = y;\n return color;\n}\n\nvec2 offsetFrontBack(float t) {\n int a = int(t);\n int ax = int(ATLAS_DIMS.x);\n vec2 os = vec2(float(a - (a / ax) * ax), float(a / ax)) / ATLAS_DIMS;\n return clamp(os, vec2(0.0), vec2(1.0) - vec2(1.0) / ATLAS_DIMS);\n}\n\nvec4 sampleAtlasLinear(sampler2D tex, vec4 pos) {\n float bounds = float(pos[0] >= 0.0 && pos[0] <= 1.0 &&\n pos[1] >= 0.0 && pos[1] <= 1.0 &&\n pos[2] >= 0.0 && pos[2] <= 1.0 );\n float nSlices = float(SLICES);\n // get location within atlas tile\n // TODO: get loc1 which follows ray to next slice along ray direction\n // when flipvolume = 1: pos\n // when flipvolume = -1: 1-pos\n vec2 loc0 = ((pos.xy - 0.5) * flipVolume.xy + 0.5) / ATLAS_DIMS;\n\n // loc ranges from 0 to 1/ATLAS_DIMS\n // shrink loc0 to within one half edge texel - so as not to sample across edges of tiles.\n loc0 = vec2(0.5) / textureRes + loc0 * (vec2(1.0) - ATLAS_DIMS / textureRes);\n \n // interpolate between two slices\n float z = (pos.z)*(nSlices-1.0);\n float z0 = floor(z);\n float t = z-z0; //mod(z, 1.0);\n float z1 = min(z0+1.0, nSlices-1.0);\n\n // flipped:\n if (flipVolume.z == -1.0) {\n z0 = nSlices - z0 - 1.0;\n z1 = nSlices - z1 - 1.0;\n t = 1.0 - t;\n }\n\n // get slice offsets in texture atlas\n vec2 o0 = offsetFrontBack(z0) + loc0;\n vec2 o1 = offsetFrontBack(z1) + loc0;\n\n vec4 slice0Color = texture2D(tex, o0);\n vec4 slice1Color = texture2D(tex, o1);\n // NOTE we could premultiply the mask in the fuse function,\n // but that is slower to update the maskAlpha value than here in the shader.\n // it is a memory vs perf tradeoff. Do users really need to update the maskAlpha at realtime speed?\n float slice0Mask = texture2D(textureAtlasMask, o0).x;\n float slice1Mask = texture2D(textureAtlasMask, o1).x;\n // or use max for conservative 0 or 1 masking?\n float maskVal = mix(slice0Mask, slice1Mask, t);\n // take mask from 0..1 to alpha..1\n maskVal = mix(maskVal, 1.0, maskAlpha);\n vec4 retval = mix(slice0Color, slice1Color, t);\n // only mask the rgb, not the alpha(?)\n retval.rgb *= maskVal;\n return bounds*retval;\n}\n\nvec4 sampleAtlasNearest(sampler2D tex, vec4 pos) {\n float bounds = float(pos[0] >= 0.0 && pos[0] <= 1.0 &&\n pos[1] >= 0.0 && pos[1] <= 1.0 &&\n pos[2] >= 0.0 && pos[2] <= 1.0 );\n float nSlices = float(SLICES);\n\n vec2 loc0 = ((pos.xy - 0.5) * flipVolume.xy + 0.5) / ATLAS_DIMS;\n\n // No interpolation - sample just one slice at a pixel center.\n // Ideally this would be accomplished in part by switching this texture to linear\n // filtering, but three makes this difficult to do through a WebGLRenderTarget.\n loc0 = floor(loc0 * textureRes) / textureRes;\n loc0 += vec2(0.5) / textureRes;\n\n float z = min(floor(pos.z * nSlices), nSlices-1.0);\n \n if (flipVolume.z == -1.0) {\n z = nSlices - z - 1.0;\n }\n\n vec2 o = offsetFrontBack(z) + loc0;\n vec4 voxelColor = texture2D(tex, o);\n\n // Apply mask\n float voxelMask = texture2D(textureAtlasMask, o).x;\n voxelMask = mix(voxelMask, 1.0, maskAlpha);\n voxelColor.rgb *= voxelMask;\n\n return bounds*voxelColor;\n}\n\nbool intersectBox(in vec3 r_o, in vec3 r_d, in vec3 boxMin, in vec3 boxMax,\n out float tnear, out float tfar) {\n // compute intersection of ray with all six bbox planes\n vec3 invR = vec3(1.0,1.0,1.0) / r_d;\n vec3 tbot = invR * (boxMin - r_o);\n vec3 ttop = invR * (boxMax - r_o);\n\n // re-order intersections to find smallest and largest on each axis\n vec3 tmin = min(ttop, tbot);\n vec3 tmax = max(ttop, tbot);\n\n // find the largest tmin and the smallest tmax\n float largest_tmin = max(max(tmin.x, tmin.y), tmin.z);\n float smallest_tmax = min(min(tmax.x, tmax.y), tmax.z);\n\n tnear = largest_tmin;\n tfar = smallest_tmax;\n\n // use >= here?\n return(smallest_tmax > largest_tmin);\n}\n\nvec4 accumulate(vec4 col, float s, vec4 C) {\n float stepScale = (1.0 - powf((1.0-col.w),s));\n col.w = stepScale;\n col.xyz *= col.w;\n col = clamp(col,0.0,1.0);\n\n C = (1.0-C.w)*col + C;\n return C;\n}\n\nvec4 integrateVolume(vec4 eye_o,vec4 eye_d,\n float tnear, float tfar,\n float clipNear, float clipFar,\n sampler2D textureAtlas\n ) {\n vec4 C = vec4(0.0);\n // march along ray from front to back, accumulating color\n\n // estimate step length\n const int maxSteps = 512;\n // modify the 3 components of eye_d by volume scale\n float scaledSteps = float(BREAK_STEPS) * length((eye_d.xyz/volumeScale));\n float csteps = clamp(float(scaledSteps), 1.0, float(maxSteps));\n float invstep = (tfar-tnear)/csteps;\n // special-casing the single slice to remove the random ray dither.\n // this removes a Moire pattern visible in single slice images, which we want to view as 2D images as best we can.\n float r = (SLICES==1.0) ? 0.0 : rand(eye_d.xy);\n // if ortho and clipped, make step size smaller so we still get same number of steps\n float tstep = invstep*orthoThickness;\n float tfarsurf = r*tstep;\n float overflow = mod((tfarsurf - tfar),tstep); // random dithering offset\n float t = tnear + overflow;\n t += r*tstep; // random dithering offset\n float tdist = 0.0;\n int numSteps = 0;\n vec4 pos, col;\n // We need to be able to scale the alpha contrib with number of ray steps,\n // in order to make the final color invariant to the step size(?)\n // use maxSteps (a constant) as the numerator... Not sure if this is sound.\n float s = 0.5 * float(maxSteps) / csteps;\n for (int i = 0; i < maxSteps; i++) {\n pos = eye_o + eye_d*t;\n // !!! assume box bounds are -0.5 .. 0.5. pos = (pos-min)/(max-min)\n // scaling is handled by model transform and already accounted for before we get here.\n // AABB clip is independent of this and is only used to determine tnear and tfar.\n pos.xyz = (pos.xyz-(-0.5))/((0.5)-(-0.5)); //0.5 * (pos + 1.0); // map position from [boxMin, boxMax] to [0, 1] coordinates\n\n vec4 col = interpolationEnabled ? sampleAtlasLinear(textureAtlas, pos) : sampleAtlasNearest(textureAtlas, pos);\n\n if (maxProject != 0) {\n col.xyz *= BRIGHTNESS;\n C = max(col, C);\n } else {\n col = luma2Alpha(col, GAMMA_MIN, GAMMA_MAX, GAMMA_SCALE);\n col.xyz *= BRIGHTNESS;\n // for practical use the density only matters for regular volume integration\n col.w *= DENSITY;\n C = accumulate(col, s, C);\n }\n t += tstep;\n numSteps = i;\n\n if (t > tfar || t > tnear+clipFar ) break;\n if (C.w > 1.0 ) break;\n }\n\n return C;\n}\n\nvoid main() {\n gl_FragColor = vec4(0.0);\n vec2 vUv = gl_FragCoord.xy/iResolution.xy;\n\n vec3 eyeRay_o, eyeRay_d;\n\n if (isOrtho == 0.0) {\n // for perspective rays:\n // world space camera coordinates\n // transform to object space\n eyeRay_o = (inverseModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;\n eyeRay_d = normalize(pObj - eyeRay_o);\n } else {\n // for ortho rays:\n float zDist = 2.0;\n eyeRay_d = (inverseModelViewMatrix*vec4(0.0, 0.0, -zDist, 0.0)).xyz;\n vec4 ray_o = vec4(2.0*vUv - 1.0, 1.0, 1.0);\n ray_o.xy *= orthoScale;\n ray_o.x *= iResolution.x/iResolution.y;\n eyeRay_o = (inverseModelViewMatrix*ray_o).xyz;\n }\n\n // -0.5..0.5 is full box. AABB_CLIP lets us clip to a box shaped ROI to look at\n // I am applying it here at the earliest point so that the ray march does\n // not waste steps. For general shaped ROI, this has to be handled more\n // generally (obviously)\n vec3 boxMin = AABB_CLIP_MIN;\n vec3 boxMax = AABB_CLIP_MAX;\n\n float tnear, tfar;\n bool hit = intersectBox(eyeRay_o, eyeRay_d, boxMin, boxMax, tnear, tfar);\n\n if (!hit) {\n // return background color if ray misses the cube\n // is this safe to do when there is other geometry / gObjects drawn?\n gl_FragColor = vec4(0.0); //C1;//vec4(0.0);\n return;\n }\n\n float clipNear = 0.0;//-(dot(eyeRay_o.xyz, eyeNorm) + dNear) / dot(eyeRay_d.xyz, eyeNorm);\n float clipFar = 10000.0;//-(dot(eyeRay_o.xyz,-eyeNorm) + dFar ) / dot(eyeRay_d.xyz,-eyeNorm);\n\n // Sample the depth/position texture\n // If this is a depth texture, the r component is a depth value. If this is a position texture,\n // the xyz components are a view space position and w is 1.0 iff there's a mesh at this fragment.\n vec4 meshPosSample = texture2D(textureDepth, vUv);\n // Note: we make a different check for whether a mesh is present with depth vs. position textures.\n // Here's the check for depth textures:\n bool hasDepthValue = usingPositionTexture == 0 && meshPosSample.r < 1.0;\n\n // If there's a depth-contributing mesh at this fragment, we may need to terminate the ray early\n if (hasDepthValue || (usingPositionTexture == 1 && meshPosSample.a > 0.0)) {\n if (hasDepthValue) {\n // We're working with a depth value, so we need to convert back to view space position\n // Get a projection space position from depth and uv, and unproject back to view space\n vec4 meshProj = vec4(vUv * 2.0 - 1.0, meshPosSample.r * 2.0 - 1.0, 1.0);\n vec4 meshView = inverseProjMatrix * meshProj;\n meshPosSample = vec4(meshView.xyz / meshView.w, 1.0);\n }\n // Transform the mesh position to object space\n vec4 meshObj = inverseModelViewMatrix * meshPosSample;\n\n // Derive a t value for the mesh intersection\n // NOTE: divides by 0 when `eyeRay_d.z` is 0. Could be mitigated by picking another component\n // to derive with when z is 0, but I found this was rare enough in practice to be acceptable.\n float tMesh = (meshObj.z - eyeRay_o.z) / eyeRay_d.z;\n if (tMesh < tfar) {\n clipFar = tMesh - tnear;\n }\n }\n\n vec4 C = integrateVolume(vec4(eyeRay_o,1.0), vec4(eyeRay_d,0.0),\n tnear, tfar, //intersections of box\n clipNear, clipFar,\n textureAtlas);\n\n C = clamp(C, 0.0, 1.0);\n gl_FragColor = C;\n return;\n}\n";
|
|
6
6
|
export const rayMarchingVertexShaderSrc = rayMarchVertexShader;
|
|
7
7
|
export const rayMarchingFragmentShaderSrc = rayMarchFragmentShader;
|
|
8
8
|
export const rayMarchingShaderUniforms = () => {
|
|
@@ -11,12 +11,14 @@ export default class FusedChannelData {
|
|
|
11
11
|
private fuseMaterialF;
|
|
12
12
|
private fuseMaterialUI;
|
|
13
13
|
private fuseMaterialI;
|
|
14
|
+
private fuseMaterialColorizeUI;
|
|
14
15
|
private fuseMaterialProps;
|
|
15
16
|
private fuseScene;
|
|
16
17
|
private quadCamera;
|
|
17
18
|
private fuseRenderTarget;
|
|
18
19
|
constructor(atlasX: number, atlasY: number);
|
|
19
20
|
private setupFuseMaterial;
|
|
21
|
+
private setupFuseColorizeMaterial;
|
|
20
22
|
getFusedTexture(): Texture;
|
|
21
23
|
cleanup(): void;
|
|
22
24
|
private getShader;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DepthTexture, Group, OrthographicCamera, PerspectiveCamera, Texture, WebGLRenderer, WebGLRenderTarget } from "three";
|
|
2
|
+
import { Volume } from "./index.js";
|
|
3
|
+
import Channel from "./Channel.js";
|
|
4
|
+
import type { VolumeRenderImpl } from "./VolumeRenderImpl.js";
|
|
5
|
+
import type { FuseChannel } from "./types.js";
|
|
6
|
+
import { VolumeRenderSettings, SettingsFlags } from "./VolumeRenderSettings.js";
|
|
7
|
+
export default class PickVolume implements VolumeRenderImpl {
|
|
8
|
+
private settings;
|
|
9
|
+
volume: Volume;
|
|
10
|
+
private geometry;
|
|
11
|
+
private geometryMesh;
|
|
12
|
+
private geometryTransformNode;
|
|
13
|
+
private scene;
|
|
14
|
+
private uniforms;
|
|
15
|
+
private emptyPositionTex;
|
|
16
|
+
needRedraw: boolean;
|
|
17
|
+
private pickBuffer;
|
|
18
|
+
private channelToPick;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new PickVolume.
|
|
21
|
+
* @param volume The volume that this renderer should render data from.
|
|
22
|
+
* @param settings Optional settings object. If set, updates the renderer with
|
|
23
|
+
* the given settings. Otherwise, uses the default VolumeRenderSettings.
|
|
24
|
+
*/
|
|
25
|
+
constructor(volume: Volume, settings?: VolumeRenderSettings);
|
|
26
|
+
setChannelToPick(channel: number): void;
|
|
27
|
+
getPickBuffer(): WebGLRenderTarget;
|
|
28
|
+
updateVolumeDimensions(): void;
|
|
29
|
+
viewpointMoved(): void;
|
|
30
|
+
updateSettings(newSettings: VolumeRenderSettings, dirtyFlags?: number | SettingsFlags): void;
|
|
31
|
+
/**
|
|
32
|
+
* Creates the geometry mesh and material for rendering the volume.
|
|
33
|
+
* @param uniforms object containing uniforms to pass to the shader material.
|
|
34
|
+
* @returns the new geometry and geometry mesh.
|
|
35
|
+
*/
|
|
36
|
+
private createGeometry;
|
|
37
|
+
cleanup(): void;
|
|
38
|
+
doRender(renderer: WebGLRenderer, camera: PerspectiveCamera | OrthographicCamera, depthTexture?: DepthTexture | Texture | null): void;
|
|
39
|
+
get3dObject(): Group;
|
|
40
|
+
private setUniform;
|
|
41
|
+
updateActiveChannels(_channelcolors: FuseChannel[], _channeldata: Channel[]): void;
|
|
42
|
+
setRenderUpdateListener(_listener?: ((iteration: number) => void) | undefined): void;
|
|
43
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Color, Event, EventListener, OrthographicCamera, PerspectiveCamera,
|
|
1
|
+
import { Color, DepthTexture, Event, EventListener, OrthographicCamera, PerspectiveCamera, Scene, WebGLRenderer, WebGLRenderTarget } from "three";
|
|
2
2
|
import TrackballControls from "./TrackballControls.js";
|
|
3
3
|
import { ViewportCorner } from "./types.js";
|
|
4
4
|
export declare const VOLUME_LAYER = 0;
|
|
@@ -104,4 +104,5 @@ export declare class ThreeJsPanel {
|
|
|
104
104
|
stopRenderLoop(): void;
|
|
105
105
|
removeControlHandlers(): void;
|
|
106
106
|
setControlHandlers(onstart: EventListener<Event, "start", TrackballControls>, onchange: EventListener<Event, "change", TrackballControls>, onend: EventListener<Event, "end", TrackballControls>): void;
|
|
107
|
+
hitTest(offsetX: number, offsetY: number, pickBuffer: WebGLRenderTarget | undefined): number;
|
|
107
108
|
}
|
package/es/types/View3d.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { CameraState } from "./ThreeJsPanel.js";
|
|
|
2
2
|
import VolumeDrawable from "./VolumeDrawable.js";
|
|
3
3
|
import { Light } from "./Light.js";
|
|
4
4
|
import Volume from "./Volume.js";
|
|
5
|
-
import { type VolumeChannelDisplayOptions, type VolumeDisplayOptions, ViewportCorner, RenderMode } from "./types.js";
|
|
5
|
+
import { type ColorizeFeature, type VolumeChannelDisplayOptions, type VolumeDisplayOptions, ViewportCorner, RenderMode } from "./types.js";
|
|
6
6
|
import { PerChannelCallback } from "./loaders/IVolumeLoader.js";
|
|
7
7
|
import VolumeLoaderContext from "./workers/VolumeLoaderContext.js";
|
|
8
8
|
export declare const RENDERMODE_RAYMARCH = RenderMode.RAYMARCH;
|
|
@@ -108,6 +108,13 @@ export declare class View3d {
|
|
|
108
108
|
* @param {number} maskChannelIndex
|
|
109
109
|
*/
|
|
110
110
|
setVolumeChannelAsMask(volume: Volume, maskChannelIndex: number): void;
|
|
111
|
+
/**
|
|
112
|
+
* @description Set the necessary data to colorize a segmentation channel, or turn off colorization.
|
|
113
|
+
* @param volume The volume to set the colorize feature for
|
|
114
|
+
* @param channelIndex The channel that will be colorized. This only makes sense for segmentation volumes.
|
|
115
|
+
* @param featureInfo A collection of all parameters necessary to colorize the channel. Pass null to turn off colorization.
|
|
116
|
+
*/
|
|
117
|
+
setChannelColorizeFeature(volume: Volume, channelIndex: number, featureInfo: ColorizeFeature | null): void;
|
|
111
118
|
/**
|
|
112
119
|
* Set voxel dimensions - controls volume scaling. For example, the physical measurements of the voxels from a biological data set
|
|
113
120
|
* @param {Object} volume
|
|
@@ -357,5 +364,26 @@ export declare class View3d {
|
|
|
357
364
|
hasWebGL2(): boolean;
|
|
358
365
|
handleKeydown: (event: KeyboardEvent) => void;
|
|
359
366
|
removeEventListeners(): void;
|
|
367
|
+
/**
|
|
368
|
+
* @description Set the selected ID for a given channel. This is used to change the appearance of the volume where that id is.
|
|
369
|
+
* @param volume the image to set the selected ID on
|
|
370
|
+
* @param channel the channel index where the selected ID is
|
|
371
|
+
* @param id the selected id
|
|
372
|
+
*/
|
|
373
|
+
setSelectedID(volume: Volume, channel: number, id: number): void;
|
|
374
|
+
/**
|
|
375
|
+
* @description Enable or disable picking on a volume. If enabled, the channelIndex is used to determine which channel to pick.
|
|
376
|
+
* @param volume the image to enable picking on
|
|
377
|
+
* @param enabled set true to enable, false to disable
|
|
378
|
+
* @param channelIndex if enabled is set to true, pass the pickable channel index here
|
|
379
|
+
*/
|
|
380
|
+
enablePicking(volume: Volume, enabled: boolean, channelIndex?: number): void;
|
|
381
|
+
/**
|
|
382
|
+
* @description This function is used to determine if a mouse event occurred over a volume object.
|
|
383
|
+
* @param offsetX mouse event x coordinate
|
|
384
|
+
* @param offsetY mouse event y coordinate
|
|
385
|
+
* @returns id of object that is under offsetX, offsetY. -1 if none
|
|
386
|
+
*/
|
|
387
|
+
hitTest(offsetX: number, offsetY: number): number;
|
|
360
388
|
private setupGui;
|
|
361
389
|
}
|