@vitessce/spatial-accelerated 3.8.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +52925 -0
  3. package/dist-tsc/SpatialWrapper.d.ts +2 -0
  4. package/dist-tsc/SpatialWrapper.d.ts.map +1 -0
  5. package/dist-tsc/SpatialWrapper.js +45 -0
  6. package/dist-tsc/VolumeDataManager.d.ts +170 -0
  7. package/dist-tsc/VolumeDataManager.d.ts.map +1 -0
  8. package/dist-tsc/VolumeDataManager.js +1273 -0
  9. package/dist-tsc/VolumeDataManager.test.d.ts +2 -0
  10. package/dist-tsc/VolumeDataManager.test.d.ts.map +1 -0
  11. package/dist-tsc/VolumeDataManager.test.js +164 -0
  12. package/dist-tsc/VolumeRenderManager.d.ts +265 -0
  13. package/dist-tsc/VolumeRenderManager.d.ts.map +1 -0
  14. package/dist-tsc/VolumeRenderManager.js +396 -0
  15. package/dist-tsc/VolumeShader.d.ts +262 -0
  16. package/dist-tsc/VolumeShader.d.ts.map +1 -0
  17. package/dist-tsc/VolumeShader.js +73 -0
  18. package/dist-tsc/VolumeView.d.ts +2 -0
  19. package/dist-tsc/VolumeView.d.ts.map +1 -0
  20. package/dist-tsc/VolumeView.js +521 -0
  21. package/dist-tsc/index.d.ts +2 -0
  22. package/dist-tsc/index.d.ts.map +1 -0
  23. package/dist-tsc/index.js +1 -0
  24. package/dist-tsc/shaders/GaussianFragmentShader.d.ts +2 -0
  25. package/dist-tsc/shaders/GaussianFragmentShader.d.ts.map +1 -0
  26. package/dist-tsc/shaders/GaussianFragmentShader.js +242 -0
  27. package/dist-tsc/shaders/GaussianVertexShader.d.ts +2 -0
  28. package/dist-tsc/shaders/GaussianVertexShader.d.ts.map +1 -0
  29. package/dist-tsc/shaders/GaussianVertexShader.js +51 -0
  30. package/dist-tsc/shaders/VolumeFragmentShader.d.ts +2 -0
  31. package/dist-tsc/shaders/VolumeFragmentShader.d.ts.map +1 -0
  32. package/dist-tsc/shaders/VolumeFragmentShader.js +1817 -0
  33. package/dist-tsc/shaders/VolumeVertexShader.d.ts +2 -0
  34. package/dist-tsc/shaders/VolumeVertexShader.d.ts.map +1 -0
  35. package/dist-tsc/shaders/VolumeVertexShader.js +110 -0
  36. package/package.json +63 -0
  37. package/src/SpatialWrapper.js +60 -0
  38. package/src/VolumeDataManager.js +1515 -0
  39. package/src/VolumeDataManager.test.js +180 -0
  40. package/src/VolumeRenderManager.js +589 -0
  41. package/src/VolumeShader.js +74 -0
  42. package/src/VolumeView.js +651 -0
  43. package/src/index.js +1 -0
  44. package/src/shaders/GaussianFragmentShader.js +243 -0
  45. package/src/shaders/GaussianVertexShader.js +52 -0
  46. package/src/shaders/VolumeFragmentShader.js +1818 -0
  47. package/src/shaders/VolumeVertexShader.js +111 -0
@@ -0,0 +1,1818 @@
1
+ // Attribution: Originally written by @alexandrairger
2
+ // Moved from .glsl file to .js file in this commit: https://github.com/vitessce/vitessce/pull/2264/commits/7f17fc003674a4ed1a8de6140db802781871cf88
3
+ /*
4
+ * This shader implements ray-marching volume rendering with:
5
+ * - Multi-resolution level-of-detail (LOD) support
6
+ * - Brick-based caching system for large datasets
7
+ * - Multi-channel rendering with independent color/opacity
8
+ * - Adaptive sampling based on distance and resolution
9
+ * - Support for different rendering styles (MIP, MinIP, standard)
10
+ *
11
+ * ========================================
12
+ * BRICK CACHE AND PAGE TABLE EXPLANATION
13
+ * ========================================
14
+ * Volumes can be gigabytes in size, but GPU memory is limited.
15
+ * We need a way to efficiently render these datasets without
16
+ * loading everything into memory at once.
17
+ *
18
+ * SOLUTION: A two-tier caching system using a Brick Cache and a Page Table:
19
+ *
20
+ * 1. BRICK CACHE (The Data Storage):
21
+ * - Think of the 3D dataset as a large cube divided into smaller 32x32x32
22
+ * voxel "bricks"
23
+ * - The Brick Cache is a 3D texture (2048x2048x128) that acts as a "cache"
24
+ * or "working memory" for these bricks
25
+ * - Only the most recently used bricks are kept in this cache
26
+ * - When you need data from a brick that's not in the cache, it gets loaded
27
+ * from disk and stored in the cache (replacing the least recently used brick)
28
+ *
29
+ * 2. PAGE TABLE (The Index/Map):
30
+ * - The Page Table is an index that tells us where
31
+ * each brick is stored in the Brick Cache
32
+ * - Each entry in the Page Table corresponds to one brick in the 3D dataset
33
+ * - The Page Table entry contains:
34
+ * * Whether the brick is currently loaded in the cache (resident flag)
35
+ * In other words, the brick is in the cache and available for immediate use.
36
+ * * Whether the brick has been initialized with data (initialized flag)
37
+ * In other words, the data is known to be available in the CPU, but
38
+ * we still need to request this data to be loaded into the brick cache.
39
+ * * The minimum and maximum values in the brick (for optimization)
40
+ * * The exact coordinates where the brick is stored in the Brick Cache
41
+ * - This is similar to how a library's card catalog tells you which shelf
42
+ * and position a book is located
43
+ *
44
+ * HOW IT WORKS TOGETHER:
45
+ * 1. When rendering a pixel, the shader needs to sample data from a specific
46
+ * location in the 3D dataset
47
+ * 2. It calculates which brick contains that location
48
+ * 3. It looks up that brick in the Page Table to see if it's loaded in the cache
49
+ * 4. If loaded: it uses the coordinates from the Page Table to sample from the
50
+ * Brick Cache
51
+ * 5. If not loaded: it requests the brick to be loaded (and continues with
52
+ * lower resolution data until the brick arrives)
53
+ * The request is performed by modifying the gRequest value
54
+ * to append the coordinate of the brick being requested.
55
+ *
56
+ * MULTI-RESOLUTION ADAPTATION:
57
+ * The system supports multiple levels of detail (LOD) to handle different viewing
58
+ * distances and performance requirements:
59
+ *
60
+ * RESOLUTION LEVELS:
61
+ * - Level 0: Highest detail (1:1 scale) - every voxel is represented
62
+ * - Level 1: 2:1 scale - every 2x2x2 block of voxels becomes 1 voxel
63
+ * - Level 2: 4:1 scale - every 4x4x4 block of voxels becomes 1 voxel
64
+ * - Level 3: 8:1 scale - every 8x8x8 block of voxels becomes 1 voxel
65
+ * - ...and so on up to Level 9 (512:1 scale)
66
+ *
67
+ * HOW BRICK CACHE HANDLES MULTIPLE RESOLUTIONS:
68
+ * - Each resolution level has its own set of bricks in the same cache
69
+ * - Higher resolution levels use more bricks (more detail)
70
+ * - Lower resolution levels use fewer bricks (less detail)
71
+ * - The cache can store bricks from different resolution levels simultaneously
72
+ * - When you zoom in, the system loads higher resolution bricks
73
+ * - When you zoom out, it can use lower resolution bricks (faster rendering)
74
+ *
75
+ * HOW PAGE TABLE HANDLES MULTIPLE RESOLUTIONS:
76
+ * - Each resolution level has its own "anchor point" in the page table
77
+ * - The anchor point defines where that resolution's brick entries start
78
+ * - Different channels can have different resolution ranges available
79
+ * - The system automatically selects the best available resolution for each channel
80
+ * - If a high-resolution brick isn't loaded, it falls back to lower resolution
81
+ *
82
+ * ADAPTIVE RESOLUTION SELECTION:
83
+ * - Distance-based: Objects far from the camera use lower resolution
84
+ * - Performance-based: When rendering is slow, it uses lower resolution
85
+ * - Quality-based: When high quality is needed, it requests higher resolution
86
+ * - Channel-specific: Each channel can have different resolution requirements
87
+ *
88
+ * EXAMPLE SCENARIO:
89
+ * 1. User is viewing a large dataset from far away → Uses Level 5-7 bricks
90
+ * 2. User zooms in on a specific region → System requests Level 2-3 bricks
91
+ * 3. User continues zooming → System loads Level 0-1 bricks for maximum detail
92
+ * 4. If Level 0 bricks aren't loaded yet → Temporarily shows Level 1 bricks
93
+ * 5. Once Level 0 bricks arrive → Smoothly transitions to highest detail
94
+ *
95
+ * Diagrams:
96
+ *
97
+ * 1. Ray-Marching Through Volume:
98
+ * Camera
99
+ * |
100
+ * v
101
+ * ┌─────────────────────────────────────┐
102
+ * │ │
103
+ * │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
104
+ * │ │B0 │ │B1 │ │B2 │ │B3 │ ... │ <- Bricks (32x32x32 voxels)
105
+ * │ └───┘ └───┘ └───┘ └───┘ │
106
+ * │ │
107
+ * │ Ray marches through volume │
108
+ * │ sampling at regular intervals │
109
+ * │ │
110
+ * └─────────────────────────────────────┘
111
+ *
112
+ * 2. Multi-Resolution LOD System:
113
+ * Resolution Levels (0 = highest, 9 = lowest):
114
+ *
115
+ * Level 0: ████████████████████████████ (1:1 scale)
116
+ * Level 1: ████████░░░░░░░░████████░░░░ (2:1 scale)
117
+ * Level 2: ████░░░░░░░░░░░░████░░░░░░░░ (4:1 scale)
118
+ * Level 3: ██░░░░░░░░░░░░░░██░░░░░░░░░░ (8:1 scale)
119
+ * ...
120
+ * Level 9: █░░░░░░░░░░░░░░░░░░░░░░░░░░░ (512:1 scale)
121
+ *
122
+ * 3. Brick Cache Layout:
123
+ * ┌─────────────────────────────────────┐
124
+ * │ 2048 x 2048 x 128 texture │
125
+ * │ │
126
+ * │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
127
+ * │ │B0 │ │B1 │ │B2 │ │B3 │ ... │ <- Brick cache entries
128
+ * │ └───┘ └───┘ └───┘ └───┘ │
129
+ * │ │
130
+ * │ Each brick = 32x32x32 voxels │
131
+ * │ Total: 64x64x16 = 65,536 bricks │
132
+ * └─────────────────────────────────────┘
133
+ *
134
+ * 4. Page Table Structure:
135
+ * ┌─────────────────────────────────────┐
136
+ * │ 32-bit entry per brick: │
137
+ * │ │
138
+ * │ [31] Resident flag (1=loaded) │
139
+ * │ [30] Initialized flag (1=ready) │
140
+ * │ [29:23] Min value (7 bits) │
141
+ * │ [22:16] Max value (7 bits) │
142
+ * │ [15:10] X offset in cache (6 bits) │
143
+ * │ [9:4] Y offset in cache (6 bits) │
144
+ * │ [3:0] Z offset in cache (4 bits) │
145
+ * └─────────────────────────────────────┘
146
+ *
147
+ * 5. Multi-Channel Rendering:
148
+ * Channel 0: ████████████████████████████
149
+ * Channel 1: ░░░░░░░░░░░░░░░░░░░░░░░░░░░░
150
+ * Channel 2: ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
151
+ * Channel 3: ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
152
+ * Channel 4: ████████████████████████████
153
+ * Channel 5: ░░░░░░░░░░░░░░░░░░░░░░░░░░░░
154
+ * Channel 6: ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
155
+ *
156
+ * Each channel has independent:
157
+ * - Color mapping
158
+ * - Opacity
159
+ * - Resolution range
160
+ * - Contrast limits
161
+ *
162
+ * 6. Adaptive Sampling:
163
+ * Distance-based LOD selection:
164
+ *
165
+ * Near: ████████████████████████████ (High res, small steps)
166
+ * Medium: ████████░░░░░░░░████████░░░░ (Medium res, medium steps)
167
+ * Far: ██░░░░░░░░░░░░░░██░░░░░░░░░░ (Low res, large steps)
168
+ *
169
+ * Other features:
170
+ * - Ray-box intersection for volume bounds
171
+ * - Adaptive step size based on LOD
172
+ * - Trilinear interpolation for smooth sampling
173
+ * - Early termination for opaque regions
174
+ * - Request/usage tracking for brick loading
175
+ * - Support for clipping planes
176
+ * - Jittered sampling for noise reduction
177
+ */
178
+
179
+ // lang: glsl
180
+ export const volumeFragmentShader = `//
181
+ // #include <packing>
182
+ precision highp float;
183
+ precision highp int;
184
+ precision highp sampler3D;
185
+ precision highp usampler3D;
186
+
187
+ // ========================================
188
+ // INPUT VARIABLES (from vertex shader)
189
+ // ========================================
190
+ // Unnormalized ray direction from camera
191
+ in vec3 rayDirUnnorm;
192
+ // Camera position in world space
193
+ in vec3 cameraCorrected;
194
+
195
+ // ========================================
196
+ // TEXTURE SAMPLERS
197
+ // ========================================
198
+ // 3D texture containing cached brick data (2048x2048x128)
199
+ // (2048*2048*128)/(32*32*32) = 16,384 bricks can be stored?
200
+ uniform sampler3D brickCacheTex;
201
+ // 3D texture containing page table entries (brick metadata)
202
+ uniform usampler3D pageTableTex;
203
+
204
+ // ========================================
205
+ // RENDERING PARAMETERS/CONSTANTS
206
+ // ========================================
207
+ // Rendering style: 0=MIP, 1=MinIP, 2=standard volume rendering
208
+ uniform int u_renderstyle;
209
+ // Global opacity multiplier for volume rendering
210
+ uniform float opacity;
211
+
212
+ // ========================================
213
+ // CONTRAST LIMITS (per channel)
214
+ // per channel min/max values for value normalization
215
+ // ========================================
216
+ uniform vec2 clim0;
217
+ uniform vec2 clim1;
218
+ uniform vec2 clim2;
219
+ uniform vec2 clim3;
220
+ uniform vec2 clim4;
221
+ uniform vec2 clim5;
222
+ uniform vec2 clim6;
223
+
224
+ // ========================================
225
+ // CLIPPING PLANES
226
+ // e.g., for X-axis clipping: (min_x, max_x) or (-1, -1) if disabled
227
+ // ========================================
228
+ uniform vec2 xClip;
229
+ uniform vec2 yClip;
230
+ uniform vec2 zClip;
231
+
232
+ // ========================================
233
+ // CHANNEL COLORS AND OPACITIES
234
+ // rgb -- color values, a -- visibility (boolean)
235
+ // ========================================
236
+ uniform vec4 color0;
237
+ uniform vec4 color1;
238
+ uniform vec4 color2;
239
+ uniform vec4 color3;
240
+ uniform vec4 color4;
241
+ uniform vec4 color5;
242
+ uniform vec4 color6;
243
+
244
+ // maps colors to physical spaces
245
+ uniform int channelMapping[7];
246
+
247
+ // ========================================
248
+ // VOLUME AND RESOLUTION PARAMETERS
249
+ // ========================================
250
+ // Volume bounding box size in world space
251
+ uniform highp vec3 boxSize;
252
+ // Rendering resolution level (affects step size)
253
+ // stepsize, correlates with resolution
254
+ uniform int renderRes;
255
+ // Volume dimensions in voxels (x, y, z)
256
+ // resolution 0 voxel extents
257
+ uniform uvec3 voxelExtents;
258
+ // Global resolution range: (min_res, max_res)
259
+ // global range of requested resolutions
260
+ uniform ivec2 resGlobal;
261
+ // Maximum number of active channels
262
+ // max number of channels (relevant for the cache statistics)
263
+ // between 1 and 7
264
+ uniform int maxChannels;
265
+
266
+ // ========================================
267
+ // PER-CHANNEL RESOLUTION RANGES
268
+ // per color channel resolution range
269
+ // Each channel can have different available resolution levels
270
+ // e.g., for Channel 0: (min_res, max_res)
271
+ // ========================================
272
+ uniform ivec2 res0;
273
+ uniform ivec2 res1;
274
+ uniform ivec2 res2;
275
+ uniform ivec2 res3;
276
+ uniform ivec2 res4;
277
+ uniform ivec2 res5;
278
+ uniform ivec2 res6;
279
+ // Channel 7: unused
280
+ uniform ivec2 res7;
281
+
282
+ // ========================================
283
+ // LEVEL-OF-DETAIL PARAMETERS
284
+ // controls how fast we decrease the resolution
285
+ // ========================================
286
+ // LOD factor for distance-based resolution selection
287
+ uniform float lodFactor;
288
+
289
+ // ========================================
290
+ // ANCHOR POINTS (per resolution level)
291
+ // per resolution anchor point for pagetable
292
+ // ========================================
293
+ // Anchor points define the origin of page table for each resolution level
294
+ // Resolution 0 anchor point (highest detail)
295
+ uniform uvec3 anchor0;
296
+ uniform uvec3 anchor1;
297
+ uniform uvec3 anchor2;
298
+ uniform uvec3 anchor3;
299
+ uniform uvec3 anchor4;
300
+ uniform uvec3 anchor5;
301
+ uniform uvec3 anchor6;
302
+ uniform uvec3 anchor7;
303
+ uniform uvec3 anchor8;
304
+ uniform uvec3 anchor9;
305
+ // Resolution 9 anchor point (lowest detail)
306
+
307
+ // ========================================
308
+ // SCALE FACTORS (per resolution level)
309
+ // per resolution downsample factor
310
+ // ========================================
311
+ // Scale factors determine voxel size at each resolution level
312
+ // Resolution 0 scale factors (should be 1,1,1)
313
+ uniform vec3 scale0;
314
+ uniform vec3 scale1;
315
+ uniform vec3 scale2;
316
+ uniform vec3 scale3;
317
+ uniform vec3 scale4;
318
+ uniform vec3 scale5;
319
+ uniform vec3 scale6;
320
+ uniform vec3 scale7;
321
+ uniform vec3 scale8;
322
+ uniform vec3 scale9;
323
+ // Resolution 9 scale factors
324
+
325
+ // ========================================
326
+ // VARYING VARIABLES (unused but required)
327
+ // ========================================
328
+ // Fragment position (unused)
329
+ varying vec4 glPosition;
330
+ // World space coordinates (used for depth only)
331
+ varying vec3 worldSpaceCoords;
332
+
333
+ // ========================================
334
+ // OUTPUT VARIABLES (multiple render targets)
335
+ // output buffers
336
+ // ========================================
337
+ // Final rendered color (sRGB)
338
+ layout(location = 0) out vec4 gColor;
339
+ // Brick loading requests (packed coordinates)
340
+ layout(location = 1) out vec4 gRequest;
341
+ // Brick usage tracking (for cache management)
342
+ layout(location = 2) out vec4 gUsage;
343
+
344
+ // ========================================
345
+ // CONSTANTS
346
+ // ========================================
347
+ // Size of each brick in voxels (32x32x32)
348
+ const float BRICK_SIZE = 32.0;
349
+ // Brick cache texture width
350
+ const float BRICK_CACHE_SIZE_X = 2048.0;
351
+ // Brick cache texture height
352
+ const float BRICK_CACHE_SIZE_Y = 2048.0;
353
+ // Brick cache texture depth
354
+ const float BRICK_CACHE_SIZE_Z = 128.0;
355
+ // Number of bricks in X (64)
356
+ const float BRICK_CACHE_BRICKS_X = BRICK_CACHE_SIZE_X / BRICK_SIZE;
357
+ // Number of bricks in Y (64)
358
+ const float BRICK_CACHE_BRICKS_Y = BRICK_CACHE_SIZE_Y / BRICK_SIZE;
359
+ // Number of bricks in Z (4)
360
+ const float BRICK_CACHE_BRICKS_Z = BRICK_CACHE_SIZE_Z / BRICK_SIZE;
361
+
362
+ // ========================================
363
+ // RAY-VOLUME INTERSECTION
364
+ // calculating the intersection of the ray with the bounding box
365
+ // ========================================
366
+ // Calculates the intersection of a ray with the volume's bounding box
367
+ // Returns (entry_time, exit_time) for the ray-box intersection
368
+ // Handles clipping planes by adjusting the bounding box
369
+ //
370
+ // Parameters:
371
+ // orig - vec3: Ray origin point in world space
372
+ // dir - vec3: Ray direction vector (should be normalized)
373
+ //
374
+ // Returns:
375
+ // vec2: (entry_time, exit_time) where:
376
+ // - entry_time: Distance along ray to enter the volume
377
+ // - exit_time: Distance along ray to exit the volume
378
+ // - If no intersection: entry_time > exit_time
379
+ vec2 intersect_hit(vec3 orig, vec3 dir) {
380
+ // Start with full volume bounds
381
+ vec3 boxMin = vec3(-0.5) * boxSize;
382
+ vec3 boxMax = vec3(0.5) * boxSize;
383
+
384
+ // Apply clipping planes if they're active (xClip.x > -1.0 means active)
385
+ if (xClip.x > -1.0) {
386
+ boxMin.x = xClip.x - (boxSize.x / 2.0);
387
+ if (xClip.y < boxSize.x)
388
+ boxMax.x = xClip.y - (boxSize.x / 2.0);
389
+ }
390
+ if (yClip.x > -1.0) {
391
+ boxMin.y = yClip.x - (boxSize.y / 2.0);
392
+ if (yClip.y < boxSize.y)
393
+ boxMax.y = yClip.y - (boxSize.y / 2.0);
394
+ }
395
+ if (zClip.x > -1.0) {
396
+ boxMin.z = zClip.x - (boxSize.z / 2.0);
397
+ if (zClip.y < boxSize.z)
398
+ boxMax.z = zClip.y - (boxSize.z / 2.0);
399
+ }
400
+
401
+ // Standard ray-box intersection algorithm
402
+ vec3 invDir = 1.0 / dir;
403
+ vec3 tmin0 = (boxMin - orig) * invDir;
404
+ vec3 tmax0 = (boxMax - orig) * invDir;
405
+ vec3 tmin = min(tmin0, tmax0);
406
+ vec3 tmax = max(tmin0, tmax0);
407
+ float t0 = max(tmin.x, max(tmin.y, tmin.z)); // Entry time
408
+ float t1 = min(tmax.x, min(tmax.y, tmax.z)); // Exit time
409
+ return vec2(t0, t1);
410
+ }
411
+
412
+ // ========================================
413
+ // UTILITY FUNCTIONS
414
+ // ========================================
415
+
416
+ // Pseudo-random number generator for jittered sampling
417
+ // random number generator based on the uv coordinate
418
+ // Author @patriciogv - 2015
419
+ // http://patriciogonzalezvivo.com
420
+ //
421
+ // Parameters:
422
+ // None (uses gl_FragCoord.xy as input)
423
+ //
424
+ // Returns:
425
+ // float: Random value between 0.0 and 1.0 based on fragment coordinates
426
+ float random() {
427
+ return fract(sin(dot(gl_FragCoord.xy, vec2(12.9898,78.233)))* 43758.5453123);
428
+ }
429
+
430
+ // Convert from linear RGB to sRGB color space
431
+ // Implements the standard sRGB transfer function for gamma correction
432
+ //
433
+ // Parameters:
434
+ // x - float: Linear RGB value between 0.0 and 1.0
435
+ //
436
+ // Returns:
437
+ // float: sRGB value between 0.0 and 1.0
438
+ float linear_to_srgb(float x) {
439
+ if (x <= 0.0031308f) {
440
+ return 12.92f * x;
441
+ }
442
+ return 1.055f * pow(x, 1.f / 2.4f) - 0.055f;
443
+ }
444
+
445
+ // Convert from linear RGB to sRGB color space (vector version)
446
+ // Applies sRGB conversion to each RGB component while preserving alpha
447
+ //
448
+ // Parameters:
449
+ // x - vec4: Linear RGBA color with components between 0.0 and 1.0
450
+ //
451
+ // Returns:
452
+ // vec4: sRGB RGBA color with components between 0.0 and 1.0
453
+ vec4 linear_to_srgb(vec4 x) {
454
+ return vec4(linear_to_srgb(x.r), linear_to_srgb(x.g), linear_to_srgb(x.b), x.a);
455
+ }
456
+
457
+ // ========================================
458
+ // PAGE TABLE COORDINATE PACKING
459
+ // transform the pagetable coordinate into a RGBA8 value
460
+ // ========================================
461
+ // Packs 3D page table coordinates into RGBA8 texture format
462
+ // Uses 10 bits for X, 10 bits for Y, 12 bits for Z
463
+ //
464
+ // Parameters:
465
+ // coord - uvec3: 3D coordinates to pack (X, Y, Z components)
466
+ // - X coordinate: 10-bit unsigned integer (0-1023)
467
+ // - Y coordinate: 10-bit unsigned integer (0-1023)
468
+ // - Z coordinate: 12-bit unsigned integer (0-4095)
469
+ //
470
+ // Returns:
471
+ // vec4: RGBA8 encoded coordinates with components between 0.0 and 1.0
472
+ // - R: Upper 8 bits of packed 32-bit value
473
+ // - G: Middle-upper 8 bits of packed 32-bit value
474
+ // - B: Middle-lower 8 bits of packed 32-bit value
475
+ // - A: Lower 8 bits of packed 32-bit value
476
+ vec4 packPTCoordToRGBA8(uvec3 coord) {
477
+
478
+ uint x = coord.x & 0x3FFu; // 10 bits for X coordinate
479
+ uint y = coord.y & 0x3FFu; // 10 bits for Y coordinate
480
+ uint z = coord.z & 0xFFFu; // 12 bits for Z coordinate
481
+
482
+ // Pack into 32-bit integer
483
+ uint packed =
484
+ (x << 22u) |
485
+ (y << 12u) |
486
+ (z);
487
+
488
+ // Decompose into RGBA8 format
489
+ return vec4(
490
+ float((packed >> 24u) & 0xFFu) / 255.0,
491
+ float((packed >> 16u) & 0xFFu) / 255.0,
492
+ float((packed >> 8u) & 0xFFu) / 255.0,
493
+ float(packed & 0xFFu) / 255.0
494
+ );
495
+ }
496
+
497
+ // ========================================
498
+ // RESOLUTION AND ANCHOR POINT ACCESSORS
499
+ // ========================================
500
+
501
+ // Get anchor point for a specific resolution level
502
+ // Anchor points define the origin of the page table for each resolution
503
+ //
504
+ // Parameters:
505
+ // index - int: Resolution level index (0-9, where 0 is highest resolution)
506
+ //
507
+ // Returns:
508
+ // uvec3: 3D anchor point coordinates in the page table, or (-1, -1, -1) if invalid
509
+ uvec3 getAnchorPoint(int index) {
510
+ if (index == 0) return anchor0;
511
+ if (index == 1) return anchor1;
512
+ if (index == 2) return anchor2;
513
+ if (index == 3) return anchor3;
514
+ if (index == 4) return anchor4;
515
+ if (index == 5) return anchor5;
516
+ if (index == 6) return anchor6;
517
+ if (index == 7) return anchor7;
518
+ if (index == 8) return anchor8;
519
+ if (index == 9) return anchor9;
520
+ return uvec3(-1, -1, -1);
521
+ }
522
+
523
+ // Find the lowest available resolution level
524
+ // Returns the highest resolution index that has valid data
525
+ //
526
+ // Parameters:
527
+ // None
528
+ //
529
+ // Returns:
530
+ // int: Highest resolution level index (0-9) that has valid anchor point data
531
+ // Returns 9 if no valid resolution levels are found
532
+ int getLowestRes() {
533
+ for (int i = 0; i < 10; i++) {
534
+ if (getAnchorPoint(i) == uvec3(0,0,0)) {
535
+ return i - 1;
536
+ }
537
+ }
538
+ return 9;
539
+ }
540
+
541
+ // Get the downsample factor for a resolution level
542
+ // Scale factors determine the voxel size at each resolution
543
+ //
544
+ // Parameters:
545
+ // index - int: Resolution level index (0-9, where 0 is highest resolution)
546
+ //
547
+ // Returns:
548
+ // vec3: Scale factors (x, y, z) for the resolution level, or (-1, -1, -1) if invalid
549
+ // Higher scale factors indicate larger voxels (lower resolution)
550
+ vec3 getScale(int index) {
551
+ if (index == 0) return scale0;
552
+ if (index == 1) return scale1;
553
+ if (index == 2) return scale2;
554
+ if (index == 3) return scale3;
555
+ if (index == 4) return scale4;
556
+ if (index == 5) return scale5;
557
+ if (index == 6) return scale6;
558
+ if (index == 7) return scale7;
559
+ if (index == 8) return scale8;
560
+ if (index == 9) return scale9;
561
+ return vec3(-1.0, -1.0, -1.0);
562
+ }
563
+
564
+ // Get the resolution range for a color channel
565
+ // Returns (min_res, max_res) for the channel
566
+ //
567
+ // Parameters:
568
+ // index - int: Channel index (0-6)
569
+ //
570
+ // Returns:
571
+ // ivec2: Resolution range as (min_resolution_level, max_resolution_level)
572
+ // Returns (-1, -1) if channel index is invalid
573
+ ivec2 getRes(int index) {
574
+ if (index == 0) return res0;
575
+ if (index == 1) return res1;
576
+ if (index == 2) return res2;
577
+ if (index == 3) return res3;
578
+ if (index == 4) return res4;
579
+ if (index == 5) return res5;
580
+ if (index == 6) return res6;
581
+ return ivec2(-1, -1);
582
+ }
583
+
584
+ // Get the min/max values (contrast limits) for a color channel
585
+ // Returns (min_value, max_value) for normalization
586
+ //
587
+ // Parameters:
588
+ // index - int: Channel index (0-6)
589
+ //
590
+ // Returns:
591
+ // vec2: Contrast limits as (min_value, max_value) for data normalization
592
+ // Returns (-1.0, -1.0) if channel index is invalid
593
+ vec2 getClim(int index) {
594
+ if (index == 0) return clim0;
595
+ if (index == 1) return clim1;
596
+ if (index == 2) return clim2;
597
+ if (index == 3) return clim3;
598
+ if (index == 4) return clim4;
599
+ if (index == 5) return clim5;
600
+ if (index == 6) return clim6;
601
+ return vec2(-1.0, -1.0);
602
+ }
603
+
604
+ // ========================================
605
+ // COORDINATE TRANSFORMATIONS
606
+ // ========================================
607
+
608
+ // Convert normalized coordinates (0-1) to voxel coordinates.
609
+ // get the voxel coordinate in the specified resolution from the normalized coordinate
610
+ //
611
+ // Parameters:
612
+ // normalized - vec3: Normalized coordinates in range [0,1] for each axis
613
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
614
+ //
615
+ // Returns:
616
+ // vec3: Voxel coordinates in the volume space at the specified resolution
617
+ vec3 getVoxelFromNormalized(vec3 normalized, int res) {
618
+ vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution
619
+ vec3 voxel = normalized * extents;
620
+ return voxel;
621
+ }
622
+
623
+ // Convert voxel coordinates to normalized coordinates (0-1)
624
+ // get the normalized coordinate based on the voxel coordinate in the specified resolution
625
+ //
626
+ // Parameters:
627
+ // voxel - vec3: Voxel coordinates in the volume space
628
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
629
+ //
630
+ // Returns:
631
+ // vec3: Normalized coordinates in range [0,1] for each axis
632
+ vec3 getNormalizedFromVoxel(vec3 voxel, int res) {
633
+ vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution
634
+ vec3 normalized = voxel / extents;
635
+ return normalized;
636
+ }
637
+
638
+ // Convert normalized coordinates to brick coordinates.
639
+ // get the brick coordinate in the specified resolution based on the normalized coordinate
640
+ // needed for pagetable calculations
641
+ //
642
+ // Parameters:
643
+ // normalized - vec3: Normalized coordinates in range [0,1] for each axis
644
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
645
+ //
646
+ // Returns:
647
+ // vec3: Brick coordinates (each brick is 32x32x32 voxels)
648
+ vec3 getBrickFromNormalized(vec3 normalized, int res) {
649
+ vec3 voxel = getVoxelFromNormalized(normalized, res);
650
+ vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels
651
+ return brick;
652
+ }
653
+
654
+ // Convert voxel coordinates to brick coordinates.
655
+ // get the brick coordinate in the specified resolution based on the voxel coordinate
656
+ //
657
+ // Parameters:
658
+ // voxel - vec3: Voxel coordinates in the volume space
659
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
660
+ //
661
+ // Returns:
662
+ // vec3: Brick coordinates (each brick is 32x32x32 voxels)
663
+ vec3 getBrickFromVoxel(vec3 voxel, int res) {
664
+ vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels
665
+ return brick;
666
+ }
667
+
668
+ // ========================================
669
+ // CHANNEL-SPECIFIC ACCESSORS
670
+ // ========================================
671
+
672
+ // Get channel offset in page table.
673
+ // get the vector for the specified channel slot in the pagetable
674
+ // Different channels are stored at different Z-offsets in the page table
675
+ //
676
+ // Parameters:
677
+ // index - int: Channel index (0-6)
678
+ //
679
+ // Returns:
680
+ // uvec3: 3D offset coordinates for the channel in the page table
681
+ uvec3 getChannelOffset(int index) {
682
+ if (index == 0) return uvec3(0, 0, 1);
683
+ if (index == 1) return uvec3(0, 1, 0);
684
+ if (index == 2) return uvec3(0, 1, 1);
685
+ if (index == 3) return uvec3(1, 0, 0);
686
+ if (index == 4) return uvec3(1, 0, 1);
687
+ if (index == 5) return uvec3(1, 1, 0);
688
+ if (index == 6) return uvec3(1, 1, 1);
689
+ return uvec3(0, 0, 0);
690
+ }
691
+
692
+ // Get color for a channel.
693
+ // get the color per color channel
694
+ //
695
+ // Parameters:
696
+ // index - int: Channel index (0-6)
697
+ //
698
+ // Returns:
699
+ // vec3: RGB color values for the specified channel
700
+ vec3 getChannelColor(int index) {
701
+ if (index == 0) return color0.xyz;
702
+ if (index == 1) return color1.xyz;
703
+ if (index == 2) return color2.xyz;
704
+ if (index == 3) return color3.xyz;
705
+ if (index == 4) return color4.xyz;
706
+ if (index == 5) return color5.xyz;
707
+ if (index == 6) return color6.xyz;
708
+ return vec3(0.0, 0.0, 0.0);
709
+ }
710
+
711
+ // Get opacity for a channel
712
+ // get the opacity (used as visibility) per color channel
713
+ //
714
+ // Parameters:
715
+ // index - int: Channel index (0-6)
716
+ //
717
+ // Returns:
718
+ // float: Opacity value (0.0-1.0) for the specified channel
719
+ float getChannelOpacity(int index) {
720
+ if (index == 0) return color0.w;
721
+ if (index == 1) return color1.w;
722
+ if (index == 2) return color2.w;
723
+ if (index == 3) return color3.w;
724
+ if (index == 4) return color4.w;
725
+ if (index == 5) return color5.w;
726
+ if (index == 6) return color6.w;
727
+ return 0.0;
728
+ }
729
+
730
+ // ========================================
731
+ // PAGE TABLE DECODING
732
+ // ========================================
733
+
734
+ /**
735
+ * retrieving the brick based on:
736
+ * location -- normalized coordinate
737
+ * targetRes -- target resolution
738
+ * channel -- physical channel slot
739
+ * rnd -- random number for jittering requests
740
+ * query -- whether to query the brick (we dont query for interblock interpolation)
741
+ * colorIndex -- color index for querying the min max values
742
+ *
743
+ * returns:
744
+ * w >= 0 -- xyz contains brick cache coordinate, w stores resolution
745
+ * w == -1 -- not resident in any resolution, should be treated as empty
746
+ * w == -2 -- empty (with respect to current transfer function)
747
+ * w == -3 -- constant full (with respect to current transfer function)
748
+ * w == -4 -- constant value within range, x stores that value
749
+ *
750
+ * bit layout:
751
+ * [1] 31 | 0 — flag resident
752
+ * [1] 30 | 1 — flag init
753
+ * [7] 23…29 | 2…8 — min → 128
754
+ * [7] 16…22 | 9…15 — max → 128
755
+ * [6] 10…15 | 16…21 — x offset in brick cache → max 64
756
+ * [6] 4…9 | 22…27 — y offset in brick cache → max 64
757
+ * [4] 0…3 | 28…31 — z offset in brick cache → max 16, effectively 4
758
+ */
759
+
760
+ /*
761
+ Page table entry format (32 bits):
762
+ [31] | 0 — flag resident (1=loaded in cache)
763
+ [30] | 1 — flag init (1=initialized)
764
+ [29:23] | 2…8 — min value (7 bits) → 128 levels
765
+ [22:16] | 9…15 — max value (7 bits) → 128 levels
766
+ [15:10] | 16…21 — x offset in brick cache (6 bits) → 64 bricks
767
+ [9:4] | 22…27 — y offset in brick cache (6 bits) → 64 bricks
768
+ [3:0] | 28…31 — z offset in brick cache (4 bits) → 16 bricks
769
+ */
770
+
771
+ // Query page table to find brick location and status
772
+ // Searches for a brick at the specified location across multiple resolution levels
773
+ // and returns its cache coordinates and status information
774
+ //
775
+ // Parameters:
776
+ // location - vec3: Normalized coordinates (0-1) within the volume
777
+ // targetRes - int: Target resolution level to start searching from
778
+ // channel - int: Channel index (0-6) to query
779
+ // rnd - float: Random value (0-1) used for brick loading request selection
780
+ // query - bool: Whether to allow brick loading requests (true) or just query (false)
781
+ // colorIndex - int
782
+ //
783
+ // Returns:
784
+ // ivec4: (x_offset, y_offset, z_offset, status) where:
785
+ // - x_offset, y_offset, z_offset: Brick cache coordinates if found
786
+ // - status: Resolution level (>=0) if found, or status code:
787
+ // * -1: Not found at any resolution level
788
+ // * -2: Empty brick (all values below threshold)
789
+ // * -3: Constant full brick (all values above threshold)
790
+ // * -4: Constant value brick (uniform value)
791
+ // add maxres here
792
+ ivec4 getBrickLocation(vec3 location, int targetRes, int channel, float rnd, bool query, int colorIndex) {
793
+
794
+ // min max for current color
795
+ vec2 clim = getClim(colorIndex);
796
+
797
+ // resolution ranges, TODO: connect this back to color
798
+ int channelMin = getRes(channel).x;
799
+ int channelMax = getRes(channel).y;
800
+
801
+ // Clamp resolution to channel's available range
802
+ int currentRes = clamp(targetRes, channelMin, channelMax);
803
+ currentRes = clamp(currentRes, resGlobal.x, resGlobal.y);
804
+ int lowestRes = clamp(resGlobal.y, channelMin, channelMax);
805
+
806
+ // Determine if this channel should request brick loading.
807
+ // request the current channel based on probability
808
+ bool requestChannel = false;
809
+ if (int(floor(rnd * float(maxChannels))) == colorIndex) {
810
+ requestChannel = true;
811
+ }
812
+
813
+ // Try progressively lower resolutions until we find data.
814
+ // loop through resolutions
815
+ while (currentRes <= lowestRes) {
816
+
817
+ // Calculate page table coordinates for this brick
818
+ uvec3 anchorPoint = getAnchorPoint(currentRes);
819
+ vec3 brickLocation = getBrickFromNormalized(location, currentRes);
820
+ uvec3 channelOffset = getChannelOffset(channel);
821
+ vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;
822
+
823
+ // Special handling for resolution 0 (highest detail)
824
+ if (currentRes == 0) {
825
+ int zExtent = int(ceil(float(voxelExtents.z) / 32.0));
826
+ coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;
827
+ }
828
+
829
+ // Query the page table.
830
+ // get PT entry
831
+ uint ptEntry = texelFetch(pageTableTex, ivec3(coordinate), 0).r;
832
+
833
+ // Check if brick is initialized.
834
+ // check if the PT entry is initialized
835
+ uint isInit = (ptEntry >> 30u) & 1u;
836
+ if (isInit == 0u) {
837
+ currentRes++;
838
+ // Request brick loading if needed
839
+ if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {
840
+ gRequest = packPTCoordToRGBA8(uvec3(coordinate));
841
+ }
842
+ continue;
843
+ }
844
+
845
+ // Extract min/max values from page table entry.
846
+ // get the min max values of the brick
847
+ uint umin = ((ptEntry >> 23u) & 0x7Fu);
848
+ uint umax = ((ptEntry >> 16u) & 0x7Fu);
849
+ float min = float(int(umin)) / 127.0;
850
+ float max = float(int(umax)) / 127.0;
851
+
852
+ // Check if brick is empty (all values below threshold).
853
+ // exit early if brick is constant
854
+ if (float(max) <= clim.x) {
855
+ return ivec4(0,0,0,-2);
856
+ // EMPTY
857
+ } else if (float(min) >= clim.y) {
858
+ return ivec4(0,0,0,-3); // CONSTANT FULL
859
+ } else if ((umax - umin) < 2u) {
860
+ return ivec4(min,0,0,-4); // CONSTANT OTHER VALUE
861
+ }
862
+
863
+ // Check if brick is resident in cache.
864
+ // return brick cache location if resident
865
+ // continue to next resolution if not resident
866
+ uint isResident = (ptEntry >> 31u) & 1u;
867
+ if (isResident == 0u) {
868
+ currentRes++;
869
+ // Request brick loading if needed
870
+ if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {
871
+ gRequest = packPTCoordToRGBA8(uvec3(coordinate));
872
+ }
873
+ continue;
874
+ } else {
875
+ // Extract brick cache coordinates
876
+ uint xBrickCache = (ptEntry >> 10u) & 0x3Fu;
877
+ uint yBrickCache = (ptEntry >> 4u) & 0x3Fu;
878
+ uint zBrickCache = ptEntry & 0xFu;
879
+ uvec3 brickCacheCoord = uvec3(xBrickCache, yBrickCache, zBrickCache);
880
+
881
+ return ivec4(brickCacheCoord, currentRes);
882
+ }
883
+ }
884
+
885
+ // not resident in any resolution, should be treated as empty
886
+ return ivec4(0,0,0,-1); // Not found
887
+ }
888
+
889
+ // Request brick loading for a specific location and resolution.
890
+ // Initiates a request to load a brick from disk into the brick cache.
891
+ // set the brick request for the specified slot channel
892
+ //
893
+ // Parameters:
894
+ // location - vec3: Normalized world space coordinates (0.0 to 1.0) where the brick is needed
895
+ // targetRes - int: Target resolution level (0-9, where 0 is highest resolution)
896
+ // channel - int: Channel index (0-6) for multi-channel datasets
897
+ // rnd - float: Random value between 0.0 and 1.0 used for load balancing across channels
898
+ //
899
+ // Returns:
900
+ // void: No return value, but sets gRequest output variable if conditions are met
901
+ void setBrickRequest(vec3 location, int targetRes, int channel, float rnd) {
902
+ uvec3 anchorPoint = getAnchorPoint(targetRes);
903
+ vec3 brickLocation = getBrickFromNormalized(location, targetRes);
904
+ uvec3 channelOffset = getChannelOffset(channel);
905
+ vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;
906
+
907
+ // Special handling for resolution 0
908
+ if (targetRes == 0) {
909
+ int zExtent = int(ceil(float(voxelExtents.z) / 32.0));
910
+ coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;
911
+ }
912
+
913
+ // Pack coordinates and set request
914
+ if (int(floor(rnd * float(maxChannels))) == channel) {
915
+ gRequest = packPTCoordToRGBA8(uvec3(coordinate));
916
+ }
917
+ }
918
+
919
+ // Track brick usage for cache management.
920
+ // Records which brick in the cache is being accessed for LRU (Least Recently Used) eviction.
921
+ // set the usage for the specified brick
922
+ //
923
+ // Parameters:
924
+ // brickCacheOffset - ivec3: 3D coordinates of the brick within the brick cache texture
925
+ // t_hit_min_os - float: Ray entry time in object space (start of ray-volume intersection)
926
+ // t_hit_max_os - float: Ray exit time in object space (end of ray-volume intersection)
927
+ // t_os - float: Current sampling position along the ray in object space
928
+ // rnd - float: Random value between 0.0 and 1.0 used for probabilistic usage tracking
929
+ //
930
+ // Returns:
931
+ // void: No return value, but sets gUsage output variable to track cache access patterns
932
+ void setUsage(ivec3 brickCacheOffset, float t_hit_min_os, float t_hit_max_os, float t_os, float rnd) {
933
+ float normalized_t_os = (t_os - t_hit_min_os) / (t_hit_max_os - t_hit_min_os); // Normalize to 0-1
934
+ if (normalized_t_os <= rnd || gUsage == vec4(0.0, 0.0, 0.0, 0.0)) {
935
+ gUsage = vec4(vec3(brickCacheOffset) / 255.0, 1.0);
936
+ }
937
+ }
938
+
939
+ // ========================================
940
+ // UTILITY FUNCTIONS
941
+ // ========================================
942
+
943
+ // Get maximum component of a 3D vector.
944
+ // get the max value of a vec3
945
+ //
946
+ // Parameters:
947
+ // v - vec3: Input 3D vector
948
+ //
949
+ // Returns:
950
+ // float: The maximum value among the x, y, and z components
951
+ float vec3_max(vec3 v) {
952
+ return max(v.x, max(v.y, v.z));
953
+ }
954
+
955
+ // Get minimum component of a 3D vector.
956
+ // get the min value of a vec3
957
+ //
958
+ // Parameters:
959
+ // v - vec3: Input 3D vector
960
+ //
961
+ // Returns:
962
+ // float: The minimum value among the x, y, and z components
963
+ float vec3_min(vec3 v) {
964
+ return min(v.x, min(v.y, v.z));
965
+ }
966
+
967
+ // Calculate level-of-detail based on distance.
968
+ // Uses logarithmic scaling to determine appropriate resolution level.
969
+ // get the LOD based on the distance to the camera
970
+ //
971
+ // Parameters:
972
+ // distance - float: Distance from camera to sampling point
973
+ // highestRes - int: Highest available resolution level (typically 0)
974
+ // lowestRes - int: Lowest available resolution level (typically 9)
975
+ // lodFactor - float: Scaling factor that controls LOD sensitivity
976
+ //
977
+ // Returns:
978
+ // int: Resolution level index (0-9) where 0 is highest resolution
979
+ int getLOD(float distance, int highestRes, int lowestRes, float lodFactor) {
980
+ int lod = int(log2(distance * lodFactor));
981
+ return clamp(lod, highestRes, lowestRes);
982
+ }
983
+
984
+ // Calculate step size for ray marching at a given resolution.
985
+ // Determines optimal sampling step size based on voxel dimensions and ray direction.
986
+ // get the voxel step in object space
987
+ //
988
+ // Parameters:
989
+ // res - int: Resolution level index (0-9, where 0 is highest resolution)
990
+ // osDir - vec3: Ray direction vector in object space (should be normalized)
991
+ //
992
+ // Returns:
993
+ // float: Optimal step size in object space units for stable ray marching
994
+ float voxelStepOS(int res, vec3 osDir) {
995
+ vec3 voxelSize = getScale(res) / vec3(voxelExtents);
996
+ vec3 dt_vec = voxelSize / abs(osDir);
997
+ return min(dt_vec.x, min(dt_vec.y, dt_vec.z));
998
+ }
999
+
1000
+ // ========================================
1001
+ // INTERPOLATION FUNCTIONS
1002
+ // ========================================
1003
+
1004
+ // Linear interpolation between two values.
1005
+ // Performs smooth interpolation between two scalar values.
1006
+ //
1007
+ // Parameters:
1008
+ // v0 - float: First value to interpolate from
1009
+ // v1 - float: Second value to interpolate to
1010
+ // fx - float: Interpolation factor between 0.0 and 1.0
1011
+ //
1012
+ // Returns:
1013
+ // float: Interpolated value between v0 and v1 based on fx
1014
+ float lerp(float v0, float v1, float fx) {
1015
+ return mix(v0, v1, fx); // (1-fx)·v0 + fx·v1
1016
+ }
1017
+
1018
+ // Bilinear interpolation between four values
1019
+ // Performs 2D interpolation using four corner values arranged in a square
1020
+ //
1021
+ // Parameters:
1022
+ // v00 - float: Bottom-left corner value
1023
+ // v10 - float: Bottom-right corner value
1024
+ // v01 - float: Top-left corner value
1025
+ // v11 - float: Top-right corner value
1026
+ // f - vec2: 2D interpolation factors (x, y) between 0.0 and 1.0
1027
+ //
1028
+ // Returns:
1029
+ // float: Interpolated value from the four corner values
1030
+ float bilerp(float v00, float v10, float v01, float v11, vec2 f) {
1031
+ float c0 = mix(v00, v10, f.x); // Interpolate in X on bottom row
1032
+ float c1 = mix(v01, v11, f.x); // Interpolate in X on top row
1033
+ return mix(c0, c1, f.y); // Now interpolate those in Y
1034
+ }
1035
+
1036
+ // Trilinear interpolation between eight values
1037
+ // Performs 3D interpolation using eight corner values arranged in a cube
1038
+ //
1039
+ // Parameters:
1040
+ // v000 - float: Bottom-left-back corner value
1041
+ // v100 - float: Bottom-right-back corner value
1042
+ // v010 - float: Bottom-left-front corner value
1043
+ // v110 - float: Bottom-right-front corner value
1044
+ // v001 - float: Top-left-back corner value
1045
+ // v101 - float: Top-right-back corner value
1046
+ // v011 - float: Top-left-front corner value
1047
+ // v111 - float: Top-right-front corner value
1048
+ // f - vec3: 3D interpolation factors (x, y, z) between 0.0 and 1.0
1049
+ //
1050
+ // Returns:
1051
+ // float: Interpolated value from the eight corner values
1052
+ float trilerp(
1053
+ float v000, float v100, float v010, float v110,
1054
+ float v001, float v101, float v011, float v111,
1055
+ vec3 f) { // f = fract(coord)
1056
+ // Interpolate along X for each of the four bottom-face voxels
1057
+ float c00 = mix(v000, v100, f.x);
1058
+ float c10 = mix(v010, v110, f.x);
1059
+ float c01 = mix(v001, v101, f.x);
1060
+ float c11 = mix(v011, v111, f.x);
1061
+
1062
+ // Interpolate those along Y
1063
+ float c0 = mix(c00, c10, f.y);
1064
+ float c1 = mix(c01, c11, f.y);
1065
+
1066
+ // Final interpolation along Z
1067
+ return mix(c0, c1, f.z);
1068
+ }
1069
+
1070
+ // ========================================
1071
+ // BRICK CACHE SAMPLING
1072
+ // ========================================
1073
+
1074
+ // Sample a value from the brick cache texture.
1075
+ // Converts brick coordinates and voxel position to texture coordinates for sampling.
1076
+ // sample the brick cache based on the brick cache coordinate and the in-brick coordinate
1077
+ //
1078
+ // Parameters:
1079
+ // brickCacheCoord - vec3: 3D coordinates of the brick within the brick cache
1080
+ // voxelInBrick - vec3: 3D coordinates of the voxel within the brick (0-31 in each dimension)
1081
+ //
1082
+ // Returns:
1083
+ // float: Sampled voxel value from the brick cache texture (typically normalized 0.0-1.0)
1084
+ float sampleBrick(vec3 brickCacheCoord, vec3 voxelInBrick) {
1085
+ vec3 brickCacheCoordNormalized = vec3(
1086
+ (float(brickCacheCoord.x) * BRICK_SIZE + float(voxelInBrick.x)) / BRICK_CACHE_SIZE_X,
1087
+ (float(brickCacheCoord.y) * BRICK_SIZE + float(voxelInBrick.y)) / BRICK_CACHE_SIZE_Y,
1088
+ (float(brickCacheCoord.z) * BRICK_SIZE + float(voxelInBrick.z)) / BRICK_CACHE_SIZE_Z
1089
+ );
1090
+ return texture(brickCacheTex, brickCacheCoordNormalized).r;
1091
+ }
1092
+
1093
+ /**
1094
+ * main renderloop
1095
+ */
1096
+ void main(void) {
1097
+
1098
+ // ========================================
1099
+ // INITIALIZATION
1100
+ // ========================================
1101
+
1102
+ // Initialize all render targets (multiple output textures)
1103
+ gRequest = vec4(0,0,0,0); // Brick loading requests
1104
+ gUsage = vec4(0,0,0,0); // Brick usage tracking
1105
+ gColor = vec4(0.0, 0.0, 0.0, 0.0); // Final color output
1106
+
1107
+ // out color sums up our accumulated value before writing it into the gColor buffer
1108
+ vec4 outColor = vec4(0.0, 0.0, 0.0, 0.0); // Accumulated color
1109
+
1110
+ // Generate random number for jittered sampling (reduces artifacts)
1111
+ float rnd = random();
1112
+
1113
+ // Get the lowest available resolution level
1114
+ int lowestDataRes = getLowestRes();
1115
+
1116
+ // ========================================
1117
+ // RAY-VOLUME INTERSECTION
1118
+ // ========================================
1119
+
1120
+ // Normalize the view ray direction
1121
+ vec3 ws_rayDir = normalize(rayDirUnnorm);
1122
+
1123
+ // Calculate intersection with volume bounding box
1124
+ // Returns (entry_time, exit_time) for the ray-box intersection
1125
+ vec2 t_hit = intersect_hit(cameraCorrected, ws_rayDir);
1126
+ if (t_hit.x >= t_hit.y) { discard; } // Ray misses volume entirely
1127
+
1128
+ t_hit.x = max(t_hit.x, 0.0); // Clamp entry to 0 (no negative distances)
1129
+ float t = t_hit.x;
1130
+
1131
+ // Calculate distance from camera for LOD selection
1132
+ float distance = abs((cameraCorrected / boxSize).z + (ws_rayDir / boxSize).z * t );
1133
+
1134
+ // ========================================
1135
+ // COORDINATE SPACE CONVERSION
1136
+ // ========================================
1137
+
1138
+ // Convert from world space to object space (normalized 0-1 coordinates)
1139
+ float ws2os = length(ws_rayDir / boxSize); // Scale factor for conversion
1140
+ float t_hit_min_os = t_hit.x * ws2os; // Entry point in object space
1141
+ float t_hit_max_os = t_hit.y * ws2os; // Exit point in object space
1142
+ float t_os = t_hit_min_os; // Current position in object space
1143
+
1144
+ // Calculate effective LOD factor based on volume size.
1145
+ // voxel edge is the max extent of the volume
1146
+ float voxelEdge = float(max(voxelExtents.x, max(voxelExtents.y, voxelExtents.z)));
1147
+
1148
+ // calculate LOD factor based on the voxel edge
1149
+ float lodFactorEffective = lodFactor * voxelEdge / 256.0;
1150
+
1151
+ // ========================================
1152
+ // RESOLUTION AND SAMPLING SETUP
1153
+ // ========================================
1154
+
1155
+ // Determine target resolution based on distance (LOD)
1156
+ int targetRes = getLOD(t, 0, 9, lodFactorEffective);
1157
+
1158
+ // Set adaptive stepping resolution
1159
+ int stepResAdaptive = renderRes;
1160
+ int stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);
1161
+
1162
+ // Convert ray to object space coordinates
1163
+ vec3 os_rayDir = normalize(ws_rayDir / boxSize);
1164
+ vec3 os_rayOrigin = cameraCorrected / boxSize + vec3(0.5);
1165
+
1166
+ // Calculate step size based on current resolution
1167
+ float dt = voxelStepOS(stepResEffective, os_rayDir);
1168
+
1169
+ // ========================================
1170
+ // SAMPLING POSITION INITIALIZATION
1171
+ // ========================================
1172
+
1173
+ // Convert to normalized sampling coordinates (0-1 range)
1174
+ vec3 p = cameraCorrected + t_hit.x * ws_rayDir;
1175
+ p = p / boxSize + vec3(0.5); // Transform to 0-1 range
1176
+
1177
+ // Calculate step vector in normalized space
1178
+ vec3 dp = (os_rayDir * dt);
1179
+
1180
+ // Apply jittered sampling to reduce artifacts
1181
+ p += dp * (rnd);
1182
+ // Avoid boundary issues
1183
+ p = clamp(p, 0.0 + 0.0000028, 1.0 - 0.0000028);
1184
+
1185
+ // ========================================
1186
+ // RENDERING VARIABLES
1187
+ // ========================================
1188
+
1189
+ // Color accumulation for front-to-back compositing.
1190
+ // color accumulation variables, are calculated per 'slice'
1191
+ vec3 rgbCombo = vec3(0.0);
1192
+ float total = 0.0;
1193
+
1194
+ // For alpha blending.
1195
+ // alpha accumulation variable runs globally
1196
+ float alphaMultiplicator = 1.0;
1197
+
1198
+ // Request tracking (for brick loading).
1199
+ // if we have a request for a brick which not visible in lower
1200
+ // resolutions, we can overwrite it once
1201
+ bool overWrittenRequest = false;
1202
+
1203
+ // Current state tracking
1204
+ vec3 currentTargetResPTCoord = vec3(0,0,0);
1205
+ int currentLOD = targetRes;
1206
+
1207
+ // ========================================
1208
+ // CHANNEL-SPECIFIC CONSTANTS
1209
+ // ========================================
1210
+
1211
+ // Pre-compute channel properties for efficiency.
1212
+ // constants per color channel
1213
+ vec3 [] c_color = vec3[7](getChannelColor(0), getChannelColor(1), getChannelColor(2), getChannelColor(3), getChannelColor(4), getChannelColor(5), getChannelColor(6));
1214
+ float [] c_opacity = float[7](getChannelOpacity(0), getChannelOpacity(1), getChannelOpacity(2), getChannelOpacity(3), getChannelOpacity(4), getChannelOpacity(5), getChannelOpacity(6));
1215
+ // resolution ranges (currently) per color channel
1216
+ // TODO: figure out how to hook it up with frontend
1217
+ int [] c_res_min = int[7](getRes(0).x, getRes(1).x, getRes(2).x, getRes(3).x, getRes(4).x, getRes(5).x, getRes(6).x);
1218
+ int [] c_res_max = int[7](getRes(0).y, getRes(1).y, getRes(2).y, getRes(3).y, getRes(4).y, getRes(5).y, getRes(6).y);
1219
+
1220
+ // ========================================
1221
+ // PER-CHANNEL STATE ARRAYS
1222
+ // ========================================
1223
+
1224
+ // Current state for each channel.
1225
+ // current state variables per color channel
1226
+
1227
+ // current resolution
1228
+ int [] c_res_current = int[7](0,0,0,0,0,0,0);
1229
+ // current value
1230
+ float [] c_val_current = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1231
+ // current brick cache coordinate
1232
+ vec3 [] c_brickCacheCoord_current = vec3[7](vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
1233
+ // current voxel in current resolution
1234
+ vec3 [] c_voxel_current = vec3[7](vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
1235
+ // current pagetable coordinate
1236
+ vec3 [] c_ptCoord_current = vec3[7](vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
1237
+ // current render mode -- 0: empty (add 0), 1: constant (add current val), 2: voxel (query new voxel)
1238
+ // upon change of PT we re-query anyways
1239
+ int [] c_renderMode_current = int[7](-1, -1, -1, -1, -1, -1, -1);
1240
+
1241
+ // Adjacent brick caching for interpolation
1242
+ // current pagetable coordinate of the adjacent bricks in X, Y, Z or diagonal direction
1243
+ vec3 [] c_PT_X_adjacent = vec3[7](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1244
+ vec3 [] c_PT_Y_adjacent = vec3[7](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1245
+ vec3 [] c_PT_Z_adjacent = vec3[7](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1246
+ vec3 [] c_PT_XYZ_adjacent = vec3[7](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1247
+ // corresponding brick coordinates of the adjacent bricks in X, Y, Z or diagonal direction
1248
+ vec4 [] c_brick_X_adjacent = vec4[7](vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0));
1249
+ vec4 [] c_brick_Y_adjacent = vec4[7](vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0));
1250
+ vec4 [] c_brick_Z_adjacent = vec4[7](vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0));
1251
+ vec4 [] c_brick_XYZ_adjacent = vec4[7](vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0), vec4(-1.0));
1252
+
1253
+ // Min/max tracking for MIP/MinIP rendering.
1254
+ // min and max values of the current color
1255
+ // used for minimum/maximum intensity projection
1256
+ float [] c_minVal = float[7](-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0);
1257
+ float [] c_maxVal = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1258
+
1259
+ // Per-resolution coordinate tracking
1260
+ vec3 [] r_ptCoord = vec3[10](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1261
+ vec3 [] r_voxel = vec3[10](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1262
+ vec3 [] r_prevPTCoord = vec3[10](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1263
+ vec3 [] r_prevVoxel = vec3[10](vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0), vec3(-1.0));
1264
+
1265
+ // resolution changed flag
1266
+ bool resolutionChanged = false;
1267
+ // number of repetitions (for debugging purposes)
1268
+ int reps = 0;
1269
+
1270
+ // ========================================
1271
+ // MAIN RAY-MARCHING LOOP
1272
+ // ========================================
1273
+
1274
+ // while we are 'in' the volume.
1275
+ // Continue marching until we exit the volume or reach maximum opacity.
1276
+ while (t_os < t_hit_max_os && t_os >= t_hit_min_os
1277
+ && vec3_max(p) < 1.0 && vec3_min(p) >= 0.0
1278
+ ) {
1279
+
1280
+ // Reset per-sample accumulation.
1281
+ // initialize slice values
1282
+ vec3 rgbCombo = vec3(0.0);
1283
+ float total = 0.0;
1284
+
1285
+ // Update target resolution based on current distance (with jitter).
1286
+ // calculate target resolution based on distance and lod factor
1287
+ targetRes = getLOD(t, 0, 9, lodFactorEffective * (0.999 + 0.002 * rnd));
1288
+
1289
+ // ========================================
1290
+ // RESOLUTION CHANGE HANDLING
1291
+ // ========================================
1292
+
1293
+ // if target resolution changed, update the current resolution and stepsize
1294
+ if (targetRes != currentLOD) {
1295
+ currentLOD = targetRes;
1296
+ stepResAdaptive++;
1297
+ stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);
1298
+
1299
+ // Adjust sampling position for new resolution
1300
+ p -= dp * rnd;
1301
+ dt = voxelStepOS(stepResEffective, os_rayDir);
1302
+ dp = os_rayDir * dt;
1303
+ p += dp * rnd;
1304
+ resolutionChanged = true;
1305
+
1306
+ // Check bounds after resolution change
1307
+ if (p.x < 0.0 || p.x >= 1.0 || p.y < 0.0 || p.y >= 1.0 || p.z < 0.0 || p.z >= 1.0) {
1308
+ break;
1309
+ }
1310
+ } else {
1311
+ resolutionChanged = false;
1312
+ }
1313
+
1314
+ // ========================================
1315
+ // UPDATE COORDINATES FOR ALL RESOLUTIONS
1316
+ // ========================================
1317
+
1318
+ // Calculate page table coordinates and voxel positions for all resolution levels
1319
+ for (int r = 0; r < 10; r++) {
1320
+ r_prevPTCoord[r] = r_ptCoord[r];
1321
+ r_prevVoxel[r] = r_voxel[r];
1322
+ r_ptCoord[r] = getBrickFromNormalized(p, r);
1323
+ r_voxel[r] = getVoxelFromNormalized(p, r);
1324
+ }
1325
+
1326
+ // ========================================
1327
+ // RENDER MODE DEFINITIONS
1328
+ // ========================================
1329
+ // 0: empty brick (no data)
1330
+ // 1: constant brick (uniform value)
1331
+ // 2: voxel brick (variable data)
1332
+
1333
+ // initialize the per channel slice values
1334
+ vec3 sliceColor = vec3(0.0);
1335
+ float sliceAlpha = 0.0;
1336
+
1337
+ // ========================================
1338
+ // MULTI-CHANNEL SAMPLING
1339
+ // ========================================
1340
+
1341
+ // Process each channel independently.
1342
+ // iterate over up to 7 channels by color
1343
+ for (int c = 0; c < 7; c++) {
1344
+ // Skip channels with zero opacity.
1345
+ // skip if opacity is 0 or if color is not mapped to a physical slot
1346
+ if (c_opacity[c] <= 0.000001) {
1347
+ continue;
1348
+ } else if (channelMapping[c] == -1) {
1349
+ continue;
1350
+ }
1351
+
1352
+ // physical slot in pagetable
1353
+ int slot = channelMapping[c];
1354
+
1355
+ // keep track of status
1356
+ bool newBrick = false;
1357
+ bool newVoxel = false;
1358
+ // best possible resolution
1359
+ int bestRes = clamp(targetRes, c_res_min[c], c_res_max[c]);
1360
+
1361
+ // Check if we need to load a new brick at a better resolution.
1362
+ // check if any new better resolution could be available, if so, we need to re-query the brick
1363
+ bool betterResChanged = false;
1364
+ for (int r = bestRes; r <= c_res_current[c]; r++ ) {
1365
+ if (r_ptCoord[r] != r_prevPTCoord[r]) {
1366
+ betterResChanged = true;
1367
+ break;
1368
+ }
1369
+ }
1370
+
1371
+ // Determine if we need to load new brick data.
1372
+ // check if we need to re-query the brick / voxel or reuse past 'val'
1373
+ if (r_ptCoord[bestRes] != r_prevPTCoord[bestRes]
1374
+ || c_renderMode_current[c] == -1
1375
+ || resolutionChanged == true
1376
+ || betterResChanged
1377
+ ) {
1378
+ newBrick = true;
1379
+ newVoxel = true;
1380
+ } else if (c_renderMode_current[c] == 2) {
1381
+ newVoxel = true;
1382
+ } else if (c_renderMode_current[c] == 0) {
1383
+ continue; // Skip empty bricks
1384
+ }
1385
+
1386
+ // ========================================
1387
+ // BRICK LOADING AND CACHING
1388
+ // ========================================
1389
+
1390
+ // check if a new brick is available in the best possible resolution
1391
+ if (newBrick) {
1392
+ // Query page table for brick location and status
1393
+ ivec4 brickCacheInfo = getBrickLocation(p, bestRes, slot, rnd, true, c);
1394
+ // check information about the newly queried brick
1395
+ // if the res is not at best res, possibly the same as previous brick
1396
+ if (brickCacheInfo.w == -1 || brickCacheInfo.w == -2) {
1397
+ // Empty brick - no data available.
1398
+ // we can skip the rest of the loop
1399
+ c_val_current[c] = 0.0;
1400
+ c_renderMode_current[c] = 0;
1401
+ c_minVal[c] = 0.0;
1402
+ continue;
1403
+ } else if (brickCacheInfo.w == -3) {
1404
+ // Solid brick - constant maximum value.
1405
+ // we set the value and do not need to query a voxel
1406
+ c_val_current[c] = 1.0;
1407
+ c_renderMode_current[c] = 1;
1408
+ c_maxVal[c] = 1.0;
1409
+ newVoxel = false;
1410
+ } else if (brickCacheInfo.w == -4) {
1411
+ // Constant brick - uniform value.
1412
+ // static value -- we set the value and do not need to query a voxel
1413
+ float val = float(brickCacheInfo.x);
1414
+ c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));
1415
+ c_renderMode_current[c] = 1;
1416
+ newVoxel = false;
1417
+ } else if (brickCacheInfo.w >= 0) {
1418
+ // Voxel brick - variable data, load from cache.
1419
+ // new brick -- we set the coordinate and resolution and need to query a voxel
1420
+ c_res_current[c] = brickCacheInfo.w;
1421
+ c_ptCoord_current[c] = r_ptCoord[c_res_current[c]];
1422
+ c_brickCacheCoord_current[c] = vec3(brickCacheInfo.xyz);
1423
+ c_renderMode_current[c] = 2;
1424
+ newVoxel = true;
1425
+
1426
+ // Track brick usage for cache management.
1427
+ // we set the usage of the brick based on the channel and the relative distance into the cube
1428
+ if (int(floor(rnd * float(maxChannels))) == c) {
1429
+ setUsage(brickCacheInfo.xyz, t_hit_min_os, t_hit_max_os, t_os, rnd);
1430
+ }
1431
+ }
1432
+ }
1433
+
1434
+ // ========================================
1435
+ // VOXEL SAMPLING WITH INTERPOLATION
1436
+ // ========================================
1437
+
1438
+ // we need to query a new voxel e.g. sample the brick cache
1439
+ if (newVoxel) {
1440
+ c_voxel_current[c] = r_voxel[c_res_current[c]];
1441
+
1442
+ // we clamp the coordinate to be inside the brick and sample the volume
1443
+ reps++;
1444
+
1445
+ // Calculate position within the brick (0-31 range)
1446
+ vec3 voxelInBrick = mod(c_voxel_current[c], 32.0);
1447
+ vec3 clampedVoxelInBrick = clamp(voxelInBrick, 0.5, 31.5);
1448
+ // Sample the brick cache texture
1449
+ float val = sampleBrick(c_brickCacheCoord_current[c].xyz, clampedVoxelInBrick);
1450
+
1451
+ // ========================================
1452
+ // HIGH-QUALITY INTERPOLATION (renderRes == 0)
1453
+ // ========================================
1454
+
1455
+ // interblock interpolation
1456
+ if (renderRes == 0) {
1457
+ // calculate what axis we need to interpolate
1458
+
1459
+ // Check if we're near brick boundaries (need interpolation)
1460
+ bvec3 clampedMin = lessThan(voxelInBrick, clampedVoxelInBrick);
1461
+ bvec3 clampedMax = greaterThan(voxelInBrick, clampedVoxelInBrick);
1462
+ bvec3 clamped = bvec3(clampedMin.x || clampedMax.x, clampedMin.y || clampedMax.y, clampedMin.z || clampedMax.z);
1463
+ vec3 diff = voxelInBrick - clampedVoxelInBrick;
1464
+
1465
+ if (any(clampedMin) || any(clampedMax)) {
1466
+ int boundaryAxes = int(clamped.x) + int(clamped.y) + int(clamped.z);
1467
+ float f = 0.0;
1468
+
1469
+ if (boundaryAxes == 1) {
1470
+ // Linear interpolation across one boundary
1471
+ vec3 otherGlobalVoxelPos = vec3(0,0,0);
1472
+ vec3 otherP = vec3(0,0,0);
1473
+ float otherVoxelVal = 0.0;
1474
+
1475
+ // Determine which axis we're interpolating across
1476
+ if (clampedMin.x) {
1477
+ otherGlobalVoxelPos = c_voxel_current[c] - vec3(1.0, 0.0, 0.0);
1478
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1479
+ f = abs(diff.x);
1480
+ } else if (clampedMax.x) {
1481
+ otherGlobalVoxelPos = c_voxel_current[c] + vec3(1.0, 0.0, 0.0);
1482
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1483
+ f = abs(diff.x);
1484
+ } else if (clampedMin.y) {
1485
+ otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 1.0, 0.0);
1486
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1487
+ f = abs(diff.y);
1488
+ } else if (clampedMax.y) {
1489
+ otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 1.0, 0.0);
1490
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1491
+ f = abs(diff.y);
1492
+ } else if (clampedMin.z) {
1493
+ otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 0.0, 1.0);
1494
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1495
+ f = abs(diff.z);
1496
+ } else if (clampedMax.z) {
1497
+ otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 0.0, 1.0);
1498
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1499
+ f = abs(diff.z);
1500
+ }
1501
+
1502
+ // Sample the neighboring voxel
1503
+ vec3 otherPTcoord = getBrickFromNormalized(otherP, c_res_current[c]);
1504
+ otherPTcoord = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1505
+ vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0);
1506
+ otherVoxelInBrick -= diff;
1507
+
1508
+ // Check if neighbor is outside volume bounds
1509
+ if (otherP.x < 0.0 || otherP.x >= 1.0 || otherP.y < 0.0 || otherP.y >= 1.0 || otherP.z < 0.0 || otherP.z >= 1.0) {
1510
+ otherVoxelVal = val;
1511
+ } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) {
1512
+ // Use cached adjacent brick
1513
+ otherVoxelVal = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick);
1514
+ } else {
1515
+ // Load new adjacent brick
1516
+ ivec4 otherBrickCacheInfo = ivec4(-1);
1517
+ if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz) {
1518
+ otherBrickCacheInfo = ivec4(c_brick_XYZ_adjacent[c]);
1519
+ } else {
1520
+ otherBrickCacheInfo = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c);
1521
+ }
1522
+ if (otherBrickCacheInfo.w == -1 || otherBrickCacheInfo.w == -2) {
1523
+ otherVoxelVal = val;
1524
+ } else if (otherBrickCacheInfo.w == -3) {
1525
+ otherVoxelVal = 1.0;
1526
+ } else if (otherBrickCacheInfo.w == -4) {
1527
+ otherVoxelVal = float(otherBrickCacheInfo.x);
1528
+ } else {
1529
+ // TODO: we do not recalculate the voxelInBrick based on the resolution
1530
+ otherVoxelVal = sampleBrick(vec3(otherBrickCacheInfo.xyz), otherVoxelInBrick);
1531
+ }
1532
+ c_PT_XYZ_adjacent[c] = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
1533
+ c_brick_XYZ_adjacent[c] = vec4(otherBrickCacheInfo);
1534
+ }
1535
+
1536
+ // Perform linear interpolation
1537
+ float originalVal = val;
1538
+ val = lerp(originalVal, otherVoxelVal, f);
1539
+ } else if (boundaryAxes == 2) {
1540
+ // Bilinear interpolation across two boundaries
1541
+ vec3 offA = vec3(0.0);
1542
+ vec3 offB = vec3(0.0);
1543
+ vec2 f = vec2(0.0);
1544
+
1545
+ // Determine which two axes we're interpolating across
1546
+ if (clamped.x && clamped.y) {
1547
+ offA.x = clampedMin.x ? -1.0 : 1.0;
1548
+ offB.y = clampedMin.y ? -1.0 : 1.0;
1549
+ f = vec2(abs(diff.x), abs(diff.y));
1550
+ } else if (clamped.x && clamped.z) {
1551
+ offA.x = clampedMin.x ? -1.0 : 1.0;
1552
+ offB.z = clampedMin.z ? -1.0 : 1.0;
1553
+ f = vec2(abs(diff.x), abs(diff.z));
1554
+ } else if (clamped.y && clamped.z) {
1555
+ offA.y = clampedMin.y ? -1.0 : 1.0;
1556
+ offB.z = clampedMin.z ? -1.0 : 1.0;
1557
+ f = vec2(abs(diff.y), abs(diff.z));
1558
+ }
1559
+
1560
+ // Macro for sampling at offset positions
1561
+ #define SAMPLE_AT_OFFSET(OFF, DEST) \
1562
+ { \
1563
+ vec3 otherGlobalVoxelPos = c_voxel_current[c] + (OFF); \
1564
+ vec3 otherP = getNormalizedFromVoxel( \
1565
+ otherGlobalVoxelPos, c_res_current[c]); \
1566
+ \
1567
+ if ( any(lessThan(otherP, vec3(0.0))) \
1568
+ || any(greaterThanEqual(otherP, vec3(1.0))) ) { \
1569
+ DEST = val; \
1570
+ } else { \
1571
+ vec3 otherPTcoord = getBrickFromNormalized( \
1572
+ otherP, c_res_current[c]); \
1573
+ vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0) - diff; \
1574
+ \
1575
+ bool matched = false; \
1576
+ if (otherPTcoord == c_PT_X_adjacent[c].xyz && c_brick_X_adjacent[c].w >= 0.0) { \
1577
+ DEST = sampleBrick(c_brick_X_adjacent[c].xyz, otherVoxelInBrick); \
1578
+ matched = true; \
1579
+ } else if (otherPTcoord == c_PT_Y_adjacent[c].xyz && c_brick_Y_adjacent[c].w >= 0.0) { \
1580
+ DEST = sampleBrick(c_brick_Y_adjacent[c].xyz, otherVoxelInBrick); \
1581
+ matched = true; \
1582
+ } else if (otherPTcoord == c_PT_Z_adjacent[c].xyz && c_brick_Z_adjacent[c].w >= 0.0) { \
1583
+ DEST = sampleBrick(c_brick_Z_adjacent[c].xyz, otherVoxelInBrick); \
1584
+ matched = true; \
1585
+ } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) { \
1586
+ DEST = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick); \
1587
+ matched = true; \
1588
+ } \
1589
+ ivec4 info = ivec4(-1); \
1590
+ if (otherPTcoord == c_PT_X_adjacent[c].xyz) { info = ivec4(c_brick_X_adjacent[c]); } \
1591
+ else if (otherPTcoord == c_PT_Y_adjacent[c].xyz) { info = ivec4(c_brick_Y_adjacent[c]); } \
1592
+ else if (otherPTcoord == c_PT_Z_adjacent[c].xyz) { info = ivec4(c_brick_Z_adjacent[c]); } \
1593
+ else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz) { info = ivec4(c_brick_XYZ_adjacent[c]); } \
1594
+ else { info = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c); } \
1595
+ \
1596
+ if (!matched) { \
1597
+ if (info.w == -1 || info.w == -2) { \
1598
+ DEST = val; \
1599
+ } else if (info.w == -3) { \
1600
+ DEST = 1.0; \
1601
+ } else if (info.w == -4) { \
1602
+ DEST = float(info.x); \
1603
+ } else { \
1604
+ DEST = sampleBrick(vec3(info.xyz), otherVoxelInBrick); \
1605
+ } \
1606
+ if (abs((OFF).x) > 0.5 && abs((OFF).y) < 0.5 && abs((OFF).z) < 0.5) { \
1607
+ c_PT_X_adjacent[c] = otherPTcoord; \
1608
+ c_brick_X_adjacent[c] = vec4(info); \
1609
+ } else if (abs((OFF).y) > 0.5 && abs((OFF).x) < 0.5 && abs((OFF).z) < 0.5) { \
1610
+ c_PT_Y_adjacent[c] = otherPTcoord; \
1611
+ c_brick_Y_adjacent[c] = vec4(info); \
1612
+ } else if (abs((OFF).z) > 0.5 && abs((OFF).x) < 0.5 && abs((OFF).y) < 0.5) { \
1613
+ c_PT_Z_adjacent[c] = otherPTcoord; \
1614
+ c_brick_Z_adjacent[c] = vec4(info); \
1615
+ } else { \
1616
+ c_PT_XYZ_adjacent[c] = otherPTcoord; \
1617
+ c_brick_XYZ_adjacent[c] = vec4(info); \
1618
+ } \
1619
+ } \
1620
+ } \
1621
+ }
1622
+
1623
+ // Sample the four corners for bilinear interpolation
1624
+ float v00 = val;
1625
+ float v10; float v01; float v11;
1626
+ SAMPLE_AT_OFFSET(offA, v10);
1627
+ SAMPLE_AT_OFFSET(offB, v01);
1628
+ SAMPLE_AT_OFFSET(offA + offB, v11);
1629
+
1630
+ val = bilerp(v00, v10, v01, v11, f);
1631
+
1632
+ #undef SAMPLE_AT_OFFSET
1633
+
1634
+ } else if (boundaryAxes == 3) {
1635
+ // Trilinear interpolation across all three boundaries
1636
+
1637
+ vec3 offA = vec3(0.0);
1638
+ vec3 offB = vec3(0.0);
1639
+ vec3 offC = vec3(0.0);
1640
+ vec3 f = vec3(0.0);
1641
+
1642
+ offA.x = clampedMin.x ? -1.0 : 1.0;
1643
+ offB.y = clampedMin.y ? -1.0 : 1.0;
1644
+ offC.z = clampedMin.z ? -1.0 : 1.0;
1645
+
1646
+ f = vec3(abs(diff.x), abs(diff.y), abs(diff.z));
1647
+
1648
+ // Macro for sampling at offset positions
1649
+ #define SAMPLE_AT_OFFSET(OFF, DEST) \
1650
+ { \
1651
+ vec3 otherGlobalVoxelPos = c_voxel_current[c] + (OFF); \
1652
+ vec3 otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]); \
1653
+ if (any(lessThan(otherP, vec3(0.0))) || any(greaterThanEqual(otherP, vec3(1.0)))) { \
1654
+ DEST = val; \
1655
+ } else { \
1656
+ vec3 otherPTcoord = getBrickFromNormalized(otherP, c_res_current[c]); \
1657
+ vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0) - diff; \
1658
+ if (otherPTcoord == c_PT_X_adjacent[c] && c_brick_X_adjacent[c].w >= 0.0) { \
1659
+ DEST = sampleBrick(c_brick_X_adjacent[c].xyz, otherVoxelInBrick); \
1660
+ } else if (otherPTcoord == c_PT_Y_adjacent[c].xyz && c_brick_Y_adjacent[c].w >= 0.0) { \
1661
+ DEST = sampleBrick(c_brick_Y_adjacent[c].xyz, otherVoxelInBrick); \
1662
+ } else if (otherPTcoord == c_PT_Z_adjacent[c].xyz && c_brick_Z_adjacent[c].w >= 0.0) { \
1663
+ DEST = sampleBrick(c_brick_Z_adjacent[c].xyz, otherVoxelInBrick); \
1664
+ } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) { \
1665
+ DEST = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick); \
1666
+ } else { \
1667
+ ivec4 otherBrickCacheInfo = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c); \
1668
+ vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0) - diff; \
1669
+ if (otherBrickCacheInfo.w == -1 || otherBrickCacheInfo.w == -2) { \
1670
+ DEST = val; \
1671
+ } else if (otherBrickCacheInfo.w == -3) { \
1672
+ DEST = 1.0; \
1673
+ } else if (otherBrickCacheInfo.w == -4) { \
1674
+ DEST = float(otherBrickCacheInfo.x); \
1675
+ } else { \
1676
+ DEST = sampleBrick(vec3(otherBrickCacheInfo.xyz), otherVoxelInBrick); \
1677
+ } \
1678
+ } \
1679
+ } \
1680
+ }
1681
+
1682
+ // Sample all eight corners for trilinear interpolation
1683
+ float v000 = val;
1684
+ float v100; float v010; float v001; float v110; float v101; float v011; float v111;
1685
+ SAMPLE_AT_OFFSET(offA, v100);
1686
+ SAMPLE_AT_OFFSET(offB, v010);
1687
+ SAMPLE_AT_OFFSET(offC, v001);
1688
+ SAMPLE_AT_OFFSET(offA + offB, v110);
1689
+ SAMPLE_AT_OFFSET(offA + offC, v101);
1690
+ SAMPLE_AT_OFFSET(offB + offC, v011);
1691
+ SAMPLE_AT_OFFSET(offA + offB + offC, v111);
1692
+
1693
+ val = trilerp(v000, v100, v010, v001, v110, v101, v011, v111, f);
1694
+
1695
+ #undef SAMPLE_AT_OFFSET
1696
+
1697
+ }
1698
+
1699
+ } else {
1700
+ // No boundary interpolation needed - clear adjacent brick cache.
1701
+ // no adjacent bricks -> reset the adjacent trackers
1702
+ c_PT_X_adjacent[c] = c_PT_Y_adjacent[c] = c_PT_Z_adjacent[c] = c_PT_XYZ_adjacent[c] = vec3(-1.0);
1703
+ c_brick_X_adjacent[c] = c_brick_Y_adjacent[c] = c_brick_Z_adjacent[c] = c_brick_XYZ_adjacent[c] = vec4(-1.0);
1704
+ }
1705
+ }
1706
+
1707
+ // ========================================
1708
+ // VALUE NORMALIZATION AND TRACKING
1709
+ // ========================================
1710
+
1711
+ // Normalize value to 0-1 range using channel-specific contrast limits.
1712
+ // we normalize the (accumulated) value to the range of the color channel
1713
+ c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));
1714
+
1715
+ // Track min/max values for MIP/MinIP rendering.
1716
+ // update the min and max values for the min/max projection
1717
+ if (c_minVal[c] == -1.0) {
1718
+ c_minVal[c] = c_val_current[c];
1719
+ } else {
1720
+ c_minVal[c] = min(c_minVal[c], c_val_current[c]);
1721
+ }
1722
+ c_maxVal[c] = max(c_maxVal[c], c_val_current[c]);
1723
+
1724
+ }
1725
+
1726
+ // ========================================
1727
+ // BRICK REQUEST GENERATION
1728
+ // ========================================
1729
+
1730
+ // Request higher resolution bricks if we're using lower resolution than optimal.
1731
+ // potentially overwrite brick request
1732
+ if (!overWrittenRequest
1733
+ && c_res_current[c] != bestRes
1734
+ && c_val_current[c] > 0.0
1735
+ && c_renderMode_current[c] == 2
1736
+ && int(floor(rnd * float(maxChannels))) == c) {
1737
+ setBrickRequest(p, bestRes, slot, rnd);
1738
+ overWrittenRequest = true;
1739
+ }
1740
+
1741
+ // ========================================
1742
+ // CHANNEL COMPOSITING
1743
+ // ========================================
1744
+
1745
+ // Accumulate this channel's contribution.
1746
+ // sum up the values onto the slice values
1747
+ total += c_val_current[c];
1748
+ rgbCombo += c_val_current[c] * c_color[c];
1749
+
1750
+ }
1751
+
1752
+ // ========================================
1753
+ // FRONT-TO-BACK COMPOSITING
1754
+ // ========================================
1755
+
1756
+ // Clamp total intensity and calculate alpha.
1757
+ // add the calculated slice to the total color
1758
+ total = clamp(total, 0.0, 1.0);
1759
+ sliceAlpha = total * opacity * dt * 32.0; // Scale by step size and brick size
1760
+ sliceColor = rgbCombo;
1761
+
1762
+ // Front-to-back alpha blending
1763
+ outColor.rgb += sliceAlpha * alphaMultiplicator * sliceColor;
1764
+ outColor.a += sliceAlpha * alphaMultiplicator;
1765
+ alphaMultiplicator *= (1.0 - sliceAlpha);
1766
+
1767
+ // Early termination for opaque regions (standard rendering only).
1768
+ // check if we can exit early
1769
+ if (outColor.a > 0.99 && u_renderstyle == 0) { break; }
1770
+
1771
+ // ========================================
1772
+ // ADVANCE RAY POSITION
1773
+ // ========================================
1774
+
1775
+ // Move to next sample position
1776
+ t += dt;
1777
+ p += dp;
1778
+ t_os += dt;
1779
+ }
1780
+
1781
+ // ========================================
1782
+ // RENDERING STYLE POST-PROCESSING
1783
+ // ========================================
1784
+
1785
+ if (u_renderstyle == 1) {
1786
+ // Minimum Intensity Projection (MinIP)
1787
+ // Shows the minimum value encountered along each ray
1788
+ outColor = vec4(0.0);
1789
+ for (int c = 0; c < 7; c++) {
1790
+ if (c_color[c] != vec3(0.0, 0.0, 0.0)) {
1791
+ outColor.rgb += c_minVal[c] * c_color[c];
1792
+ outColor.a += c_minVal[c];
1793
+ }
1794
+ }
1795
+ } else if (u_renderstyle == 0) {
1796
+ // Maximum Intensity Projection (MIP)
1797
+ // Shows the maximum value encountered along each ray
1798
+ outColor = vec4(0.0);
1799
+ for (int c = 0; c < 7; c++) {
1800
+ if (c_color[c] != vec3(0.0, 0.0, 0.0)) {
1801
+ outColor.rgb += c_maxVal[c] * c_color[c];
1802
+ }
1803
+ }
1804
+ outColor.a = 1.0;
1805
+ }
1806
+
1807
+ // ========================================
1808
+ // FINAL OUTPUT
1809
+ // ========================================
1810
+
1811
+ // Convert from linear to sRGB color space and set all render targets
1812
+ gColor = vec4(linear_to_srgb(outColor.r),
1813
+ linear_to_srgb(outColor.g),
1814
+ linear_to_srgb(outColor.b),
1815
+ outColor.a);
1816
+
1817
+ }
1818
+ `;