@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.
@@ -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, WebGLRenderer, Scene, DepthTexture } from "three";
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
  }
@@ -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
  }