@vitessce/all 4.0.0-test.1 → 4.0.0-test.2

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.
@@ -0,0 +1,3855 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useRef, useState, useMemo, useEffect, Suspense } from "react";
3
+ import { useThree, useFrame, Canvas } from "@react-three/fiber";
4
+ import { OrbitControls } from "@react-three/drei";
5
+ import * as THREE from "three";
6
+ import { Data3DTexture, RedFormat, UnsignedByteType, LinearFilter, RedIntegerFormat, UnsignedIntType, NearestFilter, Vector3, Vector2, Vector4, UniformsUtils, WebGLMultipleRenderTargets } from "three";
7
+ import { l as log, i as isEqual, C as CoordinationType, u as useEventCallback } from "./index-BYPSbKAg.js";
8
+ const LogLevel = {
9
+ INFO: "info",
10
+ WARN: "warn",
11
+ ERROR: "error",
12
+ DEBUG: "debug",
13
+ TRACE: "trace"
14
+ // default value
15
+ };
16
+ const OrderedLogLevels = [
17
+ LogLevel.TRACE,
18
+ LogLevel.DEBUG,
19
+ LogLevel.INFO,
20
+ LogLevel.WARN,
21
+ LogLevel.ERROR
22
+ ];
23
+ function getLogLevel() {
24
+ return log.getLevel();
25
+ }
26
+ function atLeastLogLevel(someLevel) {
27
+ const currLevel = getLogLevel();
28
+ const numericTarget = OrderedLogLevels.indexOf(someLevel);
29
+ return currLevel <= numericTarget;
30
+ }
31
+ const BRICK_SIZE = 32;
32
+ const BRICK_CACHE_SIZE_X = 64;
33
+ const BRICK_CACHE_SIZE_Y = 64;
34
+ const BRICK_CACHE_SIZE_Z = 4;
35
+ const TOTAL_NUM_BRICKS = BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y * BRICK_CACHE_SIZE_Z;
36
+ const BRICK_CACHE_SIZE_VOXELS_X = BRICK_CACHE_SIZE_X * BRICK_SIZE;
37
+ const BRICK_CACHE_SIZE_VOXELS_Y = BRICK_CACHE_SIZE_Y * BRICK_SIZE;
38
+ const BRICK_CACHE_SIZE_VOXELS_Z = BRICK_CACHE_SIZE_Z * BRICK_SIZE;
39
+ const INIT_STATUS = {
40
+ NOT_STARTED: "not_started",
41
+ IN_PROGRESS: "in_progress",
42
+ COMPLETE: "complete",
43
+ FAILED: "failed"
44
+ };
45
+ function logWithColor$2(message) {
46
+ if (atLeastLogLevel(LogLevel.DEBUG)) {
47
+ console.warn(`%cDM: ${message}`, "background: blue; color: white; padding: 2px; border-radius: 3px;");
48
+ }
49
+ }
50
+ function _resolutionStatsToShapes(multiResolutionStats) {
51
+ return multiResolutionStats.map((stats) => {
52
+ const { dims } = stats;
53
+ const shape = [
54
+ // Other spatial-accelerated code assumes TCZYX dimension order
55
+ dims.t,
56
+ // TODO: standardize lowercase/uppercase dim names at store-level
57
+ dims.c,
58
+ // TODO: handle case when dimension(s) are missing
59
+ dims.z,
60
+ dims.y,
61
+ dims.x
62
+ ];
63
+ return shape;
64
+ });
65
+ }
66
+ function _resolutionStatsToBrickLayout(multiResolutionStats) {
67
+ return multiResolutionStats.map((stats) => {
68
+ const shape = [
69
+ stats.depth,
70
+ // z
71
+ stats.height,
72
+ // y
73
+ stats.width
74
+ // x
75
+ ];
76
+ return [
77
+ Math.ceil((shape[0] || 1) / BRICK_SIZE),
78
+ Math.ceil((shape[1] || 1) / BRICK_SIZE),
79
+ Math.ceil((shape[2] || 1) / BRICK_SIZE)
80
+ ];
81
+ });
82
+ }
83
+ function _initMRMCPT(zarrStoreBrickLayout, channelsZarrMappingsLength) {
84
+ log.debug("_initMRMCPT", zarrStoreBrickLayout, channelsZarrMappingsLength);
85
+ const PT = {
86
+ channelOffsets: [
87
+ [0, 0, 1],
88
+ [0, 1, 0],
89
+ [0, 1, 1],
90
+ [1, 0, 0],
91
+ [1, 0, 1],
92
+ [1, 1, 0],
93
+ [1, 1, 1]
94
+ ],
95
+ anchors: [],
96
+ offsets: [],
97
+ xExtent: 0,
98
+ // includes the offset inclusive
99
+ yExtent: 0,
100
+ // includes the offset inclusive
101
+ zExtent: 0,
102
+ // includes the offset inclusive
103
+ z0Extent: 0,
104
+ // l0 z extent
105
+ zTotal: 0
106
+ // original z extent plus the l0 z extent times the channel count
107
+ };
108
+ PT.xExtent = 1;
109
+ PT.yExtent = 1;
110
+ PT.zExtent = 1;
111
+ const l0z = zarrStoreBrickLayout[0][0];
112
+ PT.z0Extent = l0z;
113
+ PT.lowestDataRes = zarrStoreBrickLayout.length - 1;
114
+ for (let i = zarrStoreBrickLayout.length - 1; i > 0; i--) {
115
+ PT.anchors.push([
116
+ PT.xExtent,
117
+ PT.yExtent,
118
+ PT.zExtent
119
+ ]);
120
+ PT.xExtent += zarrStoreBrickLayout[i][2];
121
+ PT.yExtent += zarrStoreBrickLayout[i][1];
122
+ PT.zExtent += zarrStoreBrickLayout[i][0];
123
+ }
124
+ PT.anchors.push([0, 0, PT.zExtent]);
125
+ PT.anchors.reverse();
126
+ PT.zTotal = PT.zExtent + channelsZarrMappingsLength * l0z;
127
+ const brickCacheData = new Uint8Array(BRICK_CACHE_SIZE_VOXELS_X * BRICK_CACHE_SIZE_VOXELS_Y * BRICK_CACHE_SIZE_VOXELS_Z);
128
+ brickCacheData.fill(0);
129
+ const pageTableData = new Uint32Array(PT.xExtent * PT.yExtent * PT.zTotal);
130
+ pageTableData.fill(0);
131
+ const bcTHREE = new Data3DTexture(brickCacheData, BRICK_CACHE_SIZE_VOXELS_X, BRICK_CACHE_SIZE_VOXELS_Y, BRICK_CACHE_SIZE_VOXELS_Z);
132
+ bcTHREE.format = RedFormat;
133
+ bcTHREE.type = UnsignedByteType;
134
+ bcTHREE.internalFormat = "R8";
135
+ bcTHREE.minFilter = LinearFilter;
136
+ bcTHREE.magFilter = LinearFilter;
137
+ bcTHREE.generateMipmaps = false;
138
+ bcTHREE.needsUpdate = true;
139
+ const ptTHREE = new Data3DTexture(pageTableData, PT.xExtent, PT.yExtent, PT.zTotal);
140
+ ptTHREE.format = RedIntegerFormat;
141
+ ptTHREE.type = UnsignedIntType;
142
+ ptTHREE.internalFormat = "R32UI";
143
+ ptTHREE.minFilter = NearestFilter;
144
+ ptTHREE.magFilter = NearestFilter;
145
+ ptTHREE.generateMipmaps = false;
146
+ ptTHREE.needsUpdate = true;
147
+ log.debug("_initMRMCPT", PT, ptTHREE, bcTHREE);
148
+ return {
149
+ PT,
150
+ ptTHREE,
151
+ bcTHREE
152
+ };
153
+ }
154
+ function _packPT(min, max, bcX, bcY, bcZ) {
155
+ const clamp7 = (v) => Math.max(0, Math.min(127, Math.floor(v / 2)));
156
+ return (1 << 31 | 1 << 30 | clamp7(min) << 23 | clamp7(max) << 16 | (bcX & 63) << 10 | (bcY & 63) << 4 | bcZ & 15) >>> 0;
157
+ }
158
+ function _ptToZarr(ptx, pty, ptz, ptInfo) {
159
+ const { PT_zExtent, PT_z0Extent, PT_anchors } = ptInfo;
160
+ let channel = -1;
161
+ let resolution = -1;
162
+ let x = -1;
163
+ let y = -1;
164
+ let z = -1;
165
+ if (ptz >= PT_zExtent) {
166
+ resolution = 0;
167
+ x = ptx;
168
+ y = pty;
169
+ z = (ptz - PT_zExtent) % PT_z0Extent;
170
+ channel = Math.floor((ptz - PT_zExtent) / PT_z0Extent);
171
+ } else {
172
+ for (let i = 1; i < PT_anchors.length; i++) {
173
+ if (ptx < PT_anchors[i][0] && pty < PT_anchors[i][1] && ptz < PT_anchors[i][2]) ;
174
+ else {
175
+ resolution = i;
176
+ const channelMask = [0, 0, 0];
177
+ if (ptx >= PT_anchors[i][0]) {
178
+ channelMask[0] = 1;
179
+ }
180
+ if (pty >= PT_anchors[i][1]) {
181
+ channelMask[1] = 1;
182
+ }
183
+ if (ptz >= PT_anchors[i][2]) {
184
+ channelMask[2] = 1;
185
+ }
186
+ const binaryChannel = channelMask[0] << 2 | channelMask[1] << 1 | channelMask[2];
187
+ channel = Math.max(1, Math.min(7, binaryChannel)) - 1;
188
+ const thisOffset = channelMask.map((v, j) => v * PT_anchors[i][j]);
189
+ x = ptx - thisOffset[0];
190
+ y = pty - thisOffset[1];
191
+ z = ptz - thisOffset[2];
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ return {
197
+ channel,
198
+ resolution,
199
+ x,
200
+ y,
201
+ z
202
+ };
203
+ }
204
+ const DEFAULT_SIGMA_NORMALIZED = 0.25;
205
+ function _requestBufferToRequestObjects(buffer, k, optsForWeighting) {
206
+ const counts = /* @__PURE__ */ new Map();
207
+ const { width, height, sigmaNormalized } = optsForWeighting;
208
+ const useWeighting = Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0 && typeof sigmaNormalized === "number";
209
+ if (!useWeighting) {
210
+ log.warn("_requestBufferToRequestObjects: proceeding without weighting");
211
+ }
212
+ const maxPixels = Math.floor(buffer.length / 4);
213
+ for (let pix = 0; pix < maxPixels; pix += 1) {
214
+ const i = pix * 4;
215
+ const r = buffer[i];
216
+ const g = buffer[i + 1];
217
+ const b = buffer[i + 2];
218
+ const a = buffer[i + 3];
219
+ if ((r | g | b | a) === 0) {
220
+ continue;
221
+ }
222
+ const packed = (r << 24 | g << 16 | b << 8 | a) >>> 0;
223
+ let weight = 1;
224
+ if (useWeighting) {
225
+ const x = pix % width;
226
+ const y = Math.floor(pix / width);
227
+ const dx = x - width / 2;
228
+ const dy = y - height / 2;
229
+ const nd = Math.sqrt(dx * dx / (width * width) + dy * dy / (height * height));
230
+ const norm = nd / sigmaNormalized;
231
+ weight = Math.exp(-0.5 * norm * norm);
232
+ }
233
+ counts.set(packed, (counts.get(packed) || 0) + weight);
234
+ }
235
+ const sorted = Array.from(counts.entries()).toSorted((a, b) => b[1] - a[1]);
236
+ const requests = sorted.slice(0, k).map(([packed]) => ({
237
+ x: packed >> 22 & 1023,
238
+ y: packed >> 12 & 1023,
239
+ z: packed & 4095
240
+ }));
241
+ return { requests, origRequestCount: counts.size };
242
+ }
243
+ class VolumeDataManager {
244
+ constructor(glParam) {
245
+ logWithColor$2("CLASS INITIALIZING");
246
+ log.debug("VolumeDataManager constructor", { glParam, glParamContext: glParam.getContext?.() });
247
+ const gl = glParam.getContext?.() || glParam;
248
+ const renderer = glParam;
249
+ if (gl.domElement && gl.getContext) {
250
+ this.gl = gl.getContext();
251
+ } else if (gl.isWebGLRenderer) {
252
+ this.gl = gl.getContext();
253
+ } else {
254
+ this.gl = gl;
255
+ }
256
+ this.renderer = renderer;
257
+ if (!this.gl || typeof this.gl.getParameter !== "function") {
258
+ log.debug("Unable to get WebGL context, using mock context");
259
+ this.gl = {
260
+ getParameter: (param) => {
261
+ const defaults = {
262
+ MAX_TEXTURE_SIZE: 4096,
263
+ MAX_3D_TEXTURE_SIZE: 256,
264
+ MAX_RENDERBUFFER_SIZE: 4096,
265
+ MAX_UNIFORM_BUFFER_BINDINGS: 16
266
+ };
267
+ return defaults[param] || 0;
268
+ },
269
+ isContextLost: () => false,
270
+ MAX_TEXTURE_SIZE: "MAX_TEXTURE_SIZE",
271
+ MAX_3D_TEXTURE_SIZE: "MAX_3D_TEXTURE_SIZE",
272
+ MAX_RENDERBUFFER_SIZE: "MAX_RENDERBUFFER_SIZE",
273
+ MAX_UNIFORM_BUFFER_BINDINGS: "MAX_UNIFORM_BUFFER_BINDINGS"
274
+ };
275
+ }
276
+ this._originalGlParam = glParam;
277
+ this._isContextLost = false;
278
+ this._contextRestoredCallbacks = [];
279
+ if (this.gl && this.gl.canvas) {
280
+ this.gl.canvas.addEventListener("webglcontextlost", this._handleContextLost.bind(this));
281
+ this.gl.canvas.addEventListener("webglcontextrestored", this._handleContextRestored.bind(this));
282
+ }
283
+ log.debug("GL CONSTANTS");
284
+ log.debug(this.gl);
285
+ log.debug(this.gl.TEXTURE0);
286
+ log.debug(this.gl.textures);
287
+ log.debug("RENDERER");
288
+ log.debug(this.renderer);
289
+ this.deviceLimits = {
290
+ maxTextureSize: this.gl.getParameter(this.gl.MAX_TEXTURE_SIZE),
291
+ max3DTextureSize: this.gl.getParameter(this.gl.MAX_3D_TEXTURE_SIZE),
292
+ maxRenderbufferSize: this.gl.getParameter(this.gl.MAX_RENDERBUFFER_SIZE),
293
+ maxUniformBufferBindings: this.gl.getParameter(this.gl.MAX_UNIFORM_BUFFER_BINDINGS)
294
+ };
295
+ this.zarrStore = {
296
+ resolutions: null,
297
+ // 6 (the number of resolutions aka. pyramid levels in the file)
298
+ chunkSize: [],
299
+ // [32, 32, 32]
300
+ shapes: [],
301
+ // [[795, 1024, 1024], ..., [64, 64, 64], [32, 32, 32]]
302
+ arrays: [],
303
+ // [array0, array1, array2, array3, array4, array5]
304
+ dtype: "",
305
+ // 'uint8'
306
+ physicalSizeTotal: [],
307
+ // [795 x 0.0688, 1024 x 0.03417, 1024 x 0.03417]
308
+ physicalSizeVoxel: [],
309
+ // [0.0688, 0.03417, 0.03417]
310
+ brickLayout: [],
311
+ // [[25, 32, 32],[13, 16, 16], ..., [2,2,2],[1,1,1]]
312
+ // store: '', // ref to this.store
313
+ // group: '', // ref to this.group
314
+ channelCount: 1,
315
+ // MAX 7 TODO: get from zarr metadata
316
+ scales: [],
317
+ // downsample ratios, [x,y,z] per resolution level
318
+ lowestDataRes: 0
319
+ // lowest resolution level with data
320
+ };
321
+ this.ptTHREE = null;
322
+ this.bcTHREE = null;
323
+ this.channels = {
324
+ maxChannels: 7,
325
+ // lower when dataset has fewer, dictates page table size
326
+ zarrMappings: [],
327
+ // stores the zarr channel index for every one of the up to 7 channels
328
+ colorMappings: [],
329
+ // stores the PT slot for every color
330
+ downsampleMin: [],
331
+ // stores the downsample min for every one of the up to 7 channels
332
+ downsampleMax: []
333
+ // stores the downsample max for every one of the up to 7 channels
334
+ };
335
+ this.PT = {
336
+ channelOffsets: [
337
+ [0, 0, 1],
338
+ [0, 1, 0],
339
+ [0, 1, 1],
340
+ [1, 0, 0],
341
+ [1, 0, 1],
342
+ [1, 1, 0],
343
+ [1, 1, 1]
344
+ ],
345
+ anchors: [],
346
+ offsets: [],
347
+ xExtent: 0,
348
+ // includes the offset inclusive
349
+ yExtent: 0,
350
+ // includes the offset inclusive
351
+ zExtent: 0,
352
+ // includes the offset inclusive
353
+ z0Extent: 0,
354
+ // l0 z extent
355
+ zTotal: 0
356
+ // original z extent plus the l0 z extent times the channel count
357
+ };
358
+ this.bricksEverLoaded = /* @__PURE__ */ new Set();
359
+ this.isBusy = false;
360
+ this.BCTimeStamps = new Array(TOTAL_NUM_BRICKS).fill(0);
361
+ this.BCMinMax = new Array(TOTAL_NUM_BRICKS).fill([0, 0]);
362
+ this.BCFull = false;
363
+ this.BCUnusedIndex = 0;
364
+ this.bc2pt = new Array(TOTAL_NUM_BRICKS).fill(null);
365
+ this.LRUStack = [];
366
+ this.triggerUsage = true;
367
+ this.triggerRequest = false;
368
+ this.timeStamp = 0;
369
+ this.k = 40;
370
+ this.noNewRequests = false;
371
+ this.manuallyStopped = false;
372
+ this.needsBailout = false;
373
+ this.initStatus = INIT_STATUS.NOT_STARTED;
374
+ this.initError = null;
375
+ this._lastChannelConfig = null;
376
+ this.currentRequestCount = 0;
377
+ this.totalBricksRequested = 0;
378
+ logWithColor$2("VolumeDataManager constructor complete");
379
+ }
380
+ /**
381
+ * Get loading progress information
382
+ * @returns {Object} Loading progress with bricksLoaded, totalBricks, isLoading, and percentage
383
+ */
384
+ getLoadingProgress() {
385
+ const bricksLoaded = this.bricksEverLoaded.size;
386
+ const isLoading = !this.noNewRequests && this.currentRequestCount > 0;
387
+ const percentage = this.totalBricksRequested > 0 ? (this.totalBricksRequested - this.currentRequestCount) / this.totalBricksRequested * 100 : 0;
388
+ return {
389
+ bricksLoaded,
390
+ currentRequestCount: this.currentRequestCount,
391
+ totalBricksRequested: this.totalBricksRequested,
392
+ isLoading,
393
+ percentage,
394
+ noNewRequests: this.noNewRequests
395
+ };
396
+ }
397
+ /**
398
+ * Manually stop loading and render at highest resolution
399
+ */
400
+ stopLoading() {
401
+ log.debug("Manually stopping data loading");
402
+ this.noNewRequests = true;
403
+ this.manuallyStopped = true;
404
+ this.needsBailout = true;
405
+ this.currentRequestCount = 0;
406
+ this.totalBricksRequested = 0;
407
+ }
408
+ /**
409
+ * Manually restart loading
410
+ */
411
+ restartLoading() {
412
+ log.debug("Manually restarting data loading");
413
+ this.noNewRequests = false;
414
+ this.manuallyStopped = false;
415
+ this.needsBailout = false;
416
+ this.triggerUsage = true;
417
+ this.currentRequestCount = 0;
418
+ this.totalBricksRequested = 0;
419
+ }
420
+ /**
421
+ * Handle WebGL context loss
422
+ */
423
+ _handleContextLost(event) {
424
+ logWithColor$2("CONTEXT LOST");
425
+ log.warn("WebGL context lost, preventing default and setting flag");
426
+ event.preventDefault();
427
+ this._isContextLost = true;
428
+ if (this.channels && this.channels.zarrMappings) {
429
+ this._lastChannelConfig = {
430
+ zarrMappings: [...this.channels.zarrMappings],
431
+ colorMappings: [...this.channels.colorMappings],
432
+ downsampleMin: [...this.channels.downsampleMin],
433
+ downsampleMax: [...this.channels.downsampleMax]
434
+ };
435
+ }
436
+ }
437
+ /**
438
+ * Handle WebGL context restoration
439
+ */
440
+ _handleContextRestored(event) {
441
+ logWithColor$2("CONTEXT RESTORED");
442
+ log.warn("WebGL context restored, reinitializing textures");
443
+ this._isContextLost = false;
444
+ if (this._originalGlParam && this._originalGlParam.getContext) {
445
+ this.gl = this._originalGlParam.getContext();
446
+ }
447
+ if (this._lastChannelConfig) {
448
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
449
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
450
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
451
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
452
+ log.debug("Restored channel configuration after context loss");
453
+ }
454
+ if (this.PT && this.zarrStore && this.zarrStore.brickLayout) {
455
+ try {
456
+ this.initMRMCPT();
457
+ log.debug("Successfully reinitialized MRMCPT after context restoration");
458
+ } catch (error) {
459
+ log.error("Failed to reinitialize MRMCPT after context restoration:", error);
460
+ }
461
+ }
462
+ this._contextRestoredCallbacks.forEach((callback) => {
463
+ try {
464
+ callback();
465
+ } catch (error) {
466
+ log.error("Error in context restored callback:", error);
467
+ }
468
+ });
469
+ }
470
+ /**
471
+ * Check if WebGL context is lost
472
+ */
473
+ isContextLost() {
474
+ if (this._isContextLost)
475
+ return true;
476
+ if (this.gl && typeof this.gl.isContextLost === "function") {
477
+ return this.gl.isContextLost();
478
+ }
479
+ return false;
480
+ }
481
+ /**
482
+ * Register a callback to be called when context is restored
483
+ */
484
+ onContextRestored(callback) {
485
+ if (typeof callback === "function") {
486
+ this._contextRestoredCallbacks.push(callback);
487
+ }
488
+ }
489
+ initImages(images, imageLayerScopes) {
490
+ logWithColor$2("INIT IMAGES");
491
+ this.images = images;
492
+ this.imageLayerScopes = imageLayerScopes;
493
+ }
494
+ /**
495
+ * Initialize the VolumeDataManager with Zarr store details and device limits
496
+ * This should be called ONCE at website initialization
497
+ * TODO(mark): merge this with the constructor?
498
+ * @returns {Promise<Object>} Object with Zarr store details and device limits
499
+ */
500
+ async init(config) {
501
+ logWithColor$2("INIT()");
502
+ if (this.initStatus !== INIT_STATUS.NOT_STARTED) {
503
+ log.debug("VolumeDataManager init() was called more than once!");
504
+ if (this.initStatus === INIT_STATUS.COMPLETE) {
505
+ return {
506
+ success: true,
507
+ deviceLimits: this.deviceLimits,
508
+ zarrStore: this.zarrStore,
509
+ // physicalScale: this.physicalScale,
510
+ physicalSizeTotal: this.zarrStore.physicalSizeTotal,
511
+ physicalSizeVoxel: this.zarrStore.physicalSizeVoxel,
512
+ error: null
513
+ };
514
+ }
515
+ if (this.initStatus === INIT_STATUS.FAILED) {
516
+ return {
517
+ success: false,
518
+ error: this.initError || "Unknown initialization error"
519
+ };
520
+ }
521
+ return {
522
+ success: false,
523
+ pending: true,
524
+ error: "Initialization in progress"
525
+ };
526
+ }
527
+ this.initStatus = INIT_STATUS.IN_PROGRESS;
528
+ logWithColor$2("INIT() IN PROGRESS");
529
+ try {
530
+ const imageWrapper = this.images?.[this.imageLayerScopes?.[0]]?.image?.instance;
531
+ this.ngffMetadata = imageWrapper.vivLoader.metadata;
532
+ log.debug("ngffMetadata", this.ngffMetadata);
533
+ if (!imageWrapper || imageWrapper.getType() !== "ome-zarr") {
534
+ throw new Error("Invalid imageWrapper or not an OME-Zarr image");
535
+ }
536
+ const multiResolutionStats = imageWrapper.getMultiResolutionStats();
537
+ const shapes = _resolutionStatsToShapes(multiResolutionStats);
538
+ const resolutions = multiResolutionStats.length;
539
+ this.zarrStore.resolutions = resolutions;
540
+ const vivData = imageWrapper.getData();
541
+ if (!Array.isArray(vivData) || vivData.length < 1) {
542
+ throw new Error("Not a multiresolution loader");
543
+ }
544
+ if (!isEqual(vivData[0].labels, ["t", "c", "z", "y", "x"])) {
545
+ throw new Error("Expected OME-Zarr data with dimensions [t, c, z, y, x]");
546
+ }
547
+ log.debug("vivData", vivData);
548
+ const arrays = vivData.map((resolutionData) => resolutionData._data);
549
+ const scales = new Array(resolutions).fill(null);
550
+ if (arrays.length > 0) {
551
+ const array0 = arrays[0];
552
+ this.zarrStore = {
553
+ resolutions,
554
+ chunkSize: array0.chunks,
555
+ shapes,
556
+ arrays,
557
+ dtype: array0.dtype,
558
+ physicalSizeTotal: [],
559
+ // Will be populated if metadata exists
560
+ physicalSizeVoxel: [],
561
+ // Will be populated if metadata exists
562
+ brickLayout: [],
563
+ // Calculate from shapes and chunk sizes
564
+ // store: this.store,
565
+ // group: this.group,
566
+ channelCount: shapes[0][1],
567
+ scales
568
+ };
569
+ this.channels.colorMappings = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(-1);
570
+ this.channels.zarrMappings = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(void 0);
571
+ this.channels.downsampleMin = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(void 0);
572
+ this.channels.downsampleMax = new Array(Math.min(this.zarrStore.channelCount, 7)).fill(void 0);
573
+ if (array0.meta && array0.meta.physicalSizes) {
574
+ const { x, y, z } = array0.meta.physicalSizes;
575
+ const zSize = z?.size || 1;
576
+ const ySize = y?.size || 1;
577
+ const xSize = x?.size || 1;
578
+ this.zarrStore.physicalSizeVoxel = [zSize, ySize, xSize];
579
+ if (array0.shape && array0.shape.length >= 5) {
580
+ this.zarrStore.physicalSizeTotal = [
581
+ (array0.shape[2] || 1) * zSize,
582
+ (array0.shape[3] || 1) * ySize,
583
+ (array0.shape[4] || 1) * xSize
584
+ ];
585
+ }
586
+ } else {
587
+ this.zarrStore.physicalSizeVoxel = [1, 1, 1];
588
+ this.zarrStore.physicalSizeTotal = [
589
+ array0.shape[2] || 1,
590
+ array0.shape[3] || 1,
591
+ array0.shape[4] || 1
592
+ ];
593
+ }
594
+ const { multiscales } = this.ngffMetadata;
595
+ if (!multiscales) {
596
+ throw new Error("Expected multiscales metadata in group.attrs");
597
+ }
598
+ if (multiscales?.[0]?.datasets?.[0]?.coordinateTransformations) {
599
+ for (let i = 0; i < resolutions; i++) {
600
+ if (multiscales?.[0]?.datasets?.[i]?.coordinateTransformations?.[0]?.scale) {
601
+ const { scale } = multiscales[0].datasets[i].coordinateTransformations[0];
602
+ scales[i] = [scale[4], scale[3], scale[2]];
603
+ }
604
+ }
605
+ } else {
606
+ log.error("no coordinateTransformations available, assuming downsampling ratio of 2 per dimension");
607
+ for (let i = 0; i < resolutions; i++) {
608
+ const scale = 2 ** i;
609
+ scales[i] = [scale, scale, scale];
610
+ }
611
+ }
612
+ this.zarrStore.scales = scales;
613
+ const { coordinateTransformations } = this.ngffMetadata;
614
+ if (coordinateTransformations?.[0]?.scale) {
615
+ const { scale } = coordinateTransformations[0];
616
+ const scaleLength = scale.length;
617
+ if (scaleLength >= 3) {
618
+ const zScale = scale[scaleLength - 3];
619
+ const yScale = scale[scaleLength - 2];
620
+ const xScale = scale[scaleLength - 1];
621
+ this.zarrStore.physicalSizeVoxel = [zScale, yScale, xScale];
622
+ if (array0.shape && array0.shape.length >= 5) {
623
+ this.zarrStore.physicalSizeTotal = [
624
+ (array0.shape[2] || 1) * zScale,
625
+ (array0.shape[3] || 1) * yScale,
626
+ (array0.shape[4] || 1) * xScale
627
+ ];
628
+ }
629
+ }
630
+ }
631
+ this.zarrStore.brickLayout = _resolutionStatsToBrickLayout(imageWrapper.getMultiResolutionStats());
632
+ log.debug("config", config);
633
+ const { omero } = this.ngffMetadata || {};
634
+ if (!omero) {
635
+ throw new Error("Expected omero metadata in ngffMetadata");
636
+ }
637
+ log.debug("omero", omero);
638
+ Object.keys(config).forEach((key, i) => {
639
+ const configChannel = config[key].spatialTargetC;
640
+ this.channels.zarrMappings[i] = configChannel;
641
+ this.channels.colorMappings[i] = i;
642
+ this.channels.downsampleMin[i] = omero?.channels?.[configChannel]?.window?.min || 0;
643
+ this.channels.downsampleMax[i] = omero?.channels?.[configChannel]?.window?.max || 65535;
644
+ });
645
+ log.debug("zarrMappings after init", this.channels.zarrMappings);
646
+ log.debug("colorMappings after init", this.channels.colorMappings);
647
+ log.debug("downsampleMin after init", this.channels.downsampleMin);
648
+ log.debug("downsampleMax after init", this.channels.downsampleMax);
649
+ this.initMRMCPT();
650
+ }
651
+ this.initStatus = INIT_STATUS.COMPLETE;
652
+ logWithColor$2("INIT() COMPLETE");
653
+ return {
654
+ success: true,
655
+ deviceLimits: this.deviceLimits,
656
+ zarrStore: this.zarrStore,
657
+ // physicalScale: this.physicalScale,
658
+ physicalSizeTotal: this.zarrStore.physicalSizeTotal,
659
+ physicalSizeVoxel: this.zarrStore.physicalSizeVoxel,
660
+ error: null
661
+ };
662
+ } catch (error) {
663
+ logWithColor$2("INIT() FAILED");
664
+ log.error("Error initializing VolumeDataManager:", error);
665
+ this.initStatus = INIT_STATUS.FAILED;
666
+ this.initError = error.message || "Unknown error";
667
+ return {
668
+ success: false,
669
+ error: this.initError
670
+ };
671
+ }
672
+ }
673
+ /**
674
+ * Initialize the BrickCache and PageTable
675
+ * MRMCPT: multi-resolution multi-channel page table
676
+ *
677
+ * Depends on:
678
+ * - zarrStore.brickLayout
679
+ * - zarrMappings.length (zarrMappings: the zarr channel index for every one of the up to 7 channels)
680
+ * -
681
+ */
682
+ initMRMCPT() {
683
+ logWithColor$2("initMRMCPT");
684
+ const { PT, ptTHREE, bcTHREE } = _initMRMCPT(this.zarrStore.brickLayout, this.channels.zarrMappings.length);
685
+ this.PT = PT;
686
+ this.ptTHREE = ptTHREE;
687
+ this.bcTHREE = bcTHREE;
688
+ logWithColor$2("initMRMCPT() COMPLETE");
689
+ }
690
+ async initTexture() {
691
+ const requests = [
692
+ { x: 0, y: 0, z: 1 }
693
+ ];
694
+ logWithColor$2("initTexture - loading first brick");
695
+ await this.handleBrickRequests(requests);
696
+ }
697
+ updateChannels(channelProps) {
698
+ logWithColor$2("updateChannels");
699
+ log.debug("channelProps", channelProps);
700
+ log.debug("this.channels.zarrMappings", this.channels.zarrMappings);
701
+ log.debug("this.channels.colorMappings", this.channels.colorMappings);
702
+ log.debug("this.channels.downsampleMin", this.channels.downsampleMin);
703
+ log.debug("this.channels.downsampleMax", this.channels.downsampleMax);
704
+ if (this.channels.zarrMappings.length === 0) {
705
+ log.debug("channels not initialized yet");
706
+ return;
707
+ }
708
+ const requestedZarrChannels = Object.values(channelProps).map((channelData) => channelData.spatialTargetC).filter((targetC) => targetC !== void 0);
709
+ const currentZarrChannels = this.channels.zarrMappings.filter((mapping) => mapping !== void 0);
710
+ const requestedSorted = [...new Set(requestedZarrChannels)].sort((a, b) => a - b);
711
+ const currentSorted = [...new Set(currentZarrChannels)].sort((a, b) => a - b);
712
+ if (requestedSorted.length === currentSorted.length && requestedSorted.every((val, index) => val === currentSorted[index])) {
713
+ log.debug("Channel mappings unchanged, skipping update");
714
+ }
715
+ log.debug("Channel mappings changed:", {
716
+ current: currentSorted,
717
+ requested: requestedSorted
718
+ });
719
+ Object.entries(channelProps).forEach(([uiChannelKey, channelData]) => {
720
+ const targetZarrChannel = channelData.spatialTargetC;
721
+ log.debug(`UI channel "${uiChannelKey}" wants zarr channel ${targetZarrChannel}`);
722
+ const existingSlotIndex = this.channels.zarrMappings.indexOf(targetZarrChannel);
723
+ if (existingSlotIndex === -1) {
724
+ const nextFreeSlot = this.channels.zarrMappings.findIndex((slot) => slot === void 0);
725
+ if (nextFreeSlot !== -1) {
726
+ this.channels.zarrMappings[nextFreeSlot] = targetZarrChannel;
727
+ log.debug("channelData", channelData);
728
+ log.debug("this.ngffMetadata?.omero?.channels", this.ngffMetadata?.omero?.channels);
729
+ log.debug("targetZarrChannel", targetZarrChannel);
730
+ this.channels.downsampleMin[nextFreeSlot] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.min || 0;
731
+ this.channels.downsampleMax[nextFreeSlot] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.max || 65535;
732
+ log.debug(`Mapped zarr channel ${targetZarrChannel} to slot ${nextFreeSlot}`);
733
+ log.debug("channels", this.channels);
734
+ } else {
735
+ log.debug("No free slots found, looking for unused mapped channels");
736
+ const currentlyMapped = this.channels.zarrMappings.filter((mapping) => mapping !== void 0);
737
+ const stillRequested = requestedZarrChannels;
738
+ const unusedMappedChannels = currentlyMapped.filter((mappedChannel) => !stillRequested.includes(mappedChannel));
739
+ log.debug("Currently mapped:", currentlyMapped);
740
+ log.debug("Still requested:", stillRequested);
741
+ log.debug("Unused mapped channels:", unusedMappedChannels);
742
+ if (unusedMappedChannels.length > 0) {
743
+ const slotToReuse = this.channels.zarrMappings.findIndex((mapping) => unusedMappedChannels.includes(mapping));
744
+ if (slotToReuse !== -1) {
745
+ const oldZarrChannel = this.channels.zarrMappings[slotToReuse];
746
+ this.channels.zarrMappings[slotToReuse] = targetZarrChannel;
747
+ this.channels.downsampleMin[slotToReuse] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.min || 0;
748
+ this.channels.downsampleMax[slotToReuse] = this.ngffMetadata?.omero?.channels?.[targetZarrChannel]?.window?.max || 65535;
749
+ log.debug(`Reused slot ${slotToReuse}: ${oldZarrChannel} -> ${targetZarrChannel}`);
750
+ this._purgeChannel(slotToReuse);
751
+ } else {
752
+ log.error("Could not find slot to reuse - this should not happen");
753
+ }
754
+ } else {
755
+ log.error("All slots are full and all mapped channels are still in use");
756
+ }
757
+ }
758
+ } else {
759
+ log.debug(`Zarr channel ${targetZarrChannel} already mapped to slot ${existingSlotIndex}`);
760
+ }
761
+ });
762
+ const newColorMappings = Object.values(channelProps).map((ch) => {
763
+ const slot = this.channels.zarrMappings.indexOf(ch.spatialTargetC);
764
+ return slot !== -1 ? slot : -1;
765
+ });
766
+ while (newColorMappings.length < 7) {
767
+ newColorMappings.push(-1);
768
+ }
769
+ log.debug("newColorMappings", newColorMappings);
770
+ this.channels.colorMappings = newColorMappings;
771
+ log.debug("updatedChannels", this.channels);
772
+ this._lastChannelConfig = {
773
+ zarrMappings: [...this.channels.zarrMappings],
774
+ colorMappings: [...this.channels.colorMappings],
775
+ downsampleMin: [...this.channels.downsampleMin],
776
+ downsampleMax: [...this.channels.downsampleMax]
777
+ };
778
+ }
779
+ /**
780
+ * Try to load a resolution level
781
+ * @param {number} resolutionIndex - The resolution level to load
782
+ * @param {Array} arrays - Array to store the loaded arrays
783
+ * @returns {Promise} Promise resolving when the resolution is loaded or rejected
784
+ */
785
+ /*
786
+ async tryLoadResolution(resolutionIndex, arrays) {
787
+ logWithColor('tryLoadResolution');
788
+ log.debug(resolutionIndex, arrays);
789
+ try {
790
+ const array = await zarrita.open(this.group.resolve(String(resolutionIndex)));
791
+ // Create new arrays to avoid modifying parameters directly
792
+ const newArrays = [...arrays];
793
+ newArrays[resolutionIndex] = array;
794
+ // Update the original arrays
795
+ Object.assign(arrays, newArrays);
796
+ logWithColor('tryLoadResolution() COMPLETE');
797
+ return { success: true, level: resolutionIndex };
798
+ } catch (err) {
799
+ log.error(`Failed to load resolution ${resolutionIndex}:`, err);
800
+ return { success: false, level: resolutionIndex, error: err.message };
801
+ }
802
+ }
803
+ */
804
+ /**
805
+ * Get physical dimensions
806
+ * @returns {Array} Physical dimensions [X, Y, Z]
807
+ */
808
+ getPhysicalDimensionsXYZ() {
809
+ log.debug("getPhysicalDimensionsXYZ");
810
+ log.debug("this.zarrStore.physicalSizeTotal", this.zarrStore.physicalSizeTotal);
811
+ const out = [
812
+ this.zarrStore.physicalSizeTotal[2],
813
+ this.zarrStore.physicalSizeTotal[1],
814
+ this.zarrStore.physicalSizeTotal[0]
815
+ ];
816
+ log.debug("out", out);
817
+ return out;
818
+ }
819
+ /**
820
+ * Get the maximum resolution
821
+ * @returns {number} Maximum resolution
822
+ */
823
+ getMaxResolutionXYZ() {
824
+ log.debug("getMaxResolutionXYZ");
825
+ log.debug("this.zarrStore.shapes", this.zarrStore.shapes);
826
+ const out = [
827
+ this.zarrStore.shapes[0][4],
828
+ this.zarrStore.shapes[0][3],
829
+ this.zarrStore.shapes[0][2]
830
+ ];
831
+ log.debug("out", out);
832
+ return out;
833
+ }
834
+ getOriginalScaleXYZ() {
835
+ logWithColor$2("getOriginalScaleXYZ");
836
+ log.debug("this.zarrStore.physicalSizeVoxel", this.zarrStore.physicalSizeVoxel);
837
+ const out = [
838
+ this.zarrStore.physicalSizeVoxel[2],
839
+ this.zarrStore.physicalSizeVoxel[1],
840
+ this.zarrStore.physicalSizeVoxel[0]
841
+ ];
842
+ log.debug("out", out);
843
+ return out;
844
+ }
845
+ getNormalizedScaleXYZ() {
846
+ log.debug("getNormalizedScaleXYZ");
847
+ const out = [
848
+ 1,
849
+ this.zarrStore.physicalSizeVoxel[1] / this.zarrStore.physicalSizeVoxel[2],
850
+ this.zarrStore.physicalSizeVoxel[0] / this.zarrStore.physicalSizeVoxel[0]
851
+ ];
852
+ log.debug("out", out);
853
+ return out;
854
+ }
855
+ getBoxDimensionsXYZ() {
856
+ log.debug("getBoxDimensionsXYZ");
857
+ log.debug("this.zarrStore.shapes", this.zarrStore.shapes);
858
+ const out = [
859
+ 1,
860
+ this.zarrStore.shapes[0][3] / this.zarrStore.shapes[0][4],
861
+ this.zarrStore.shapes[0][2] / this.zarrStore.shapes[0][4]
862
+ ];
863
+ log.debug("out", out);
864
+ return out;
865
+ }
866
+ /**
867
+ * Load a specific Zarr chunk based on [t,c,z,y,x] coordinates
868
+ * @param {number} t - Time point (default 0)
869
+ * @param {number} c - Channel (default 0)
870
+ * @param {number} z - Z coordinate
871
+ * @param {number} y - Y coordinate
872
+ * @param {number} x - X coordinate
873
+ * @param {number} resolution - Resolution level
874
+ * @returns {Promise<Uint8Array>} 32x32x32 chunk data
875
+ */
876
+ async loadZarrChunk(t = 0, c = 0, z, y, x, resolution) {
877
+ if (!this.zarrStore || !this.zarrStore.arrays[resolution]) {
878
+ throw new Error("Zarr store or resolution not initialized");
879
+ }
880
+ log.debug("loadZarrChunk", { t, c, z, y, x, resolution });
881
+ const array = this.zarrStore.arrays[resolution];
882
+ const chunkEntry = await array.getChunk([t, c, z, y, x]);
883
+ if (!chunkEntry) {
884
+ throw new Error(`No chunk found at coordinates [${t},${c},${z},${y},${x}]`);
885
+ }
886
+ if (chunkEntry.data.length !== BRICK_SIZE * BRICK_SIZE * BRICK_SIZE) {
887
+ throw new Error(`Unexpected chunk size: ${chunkEntry.data.length}`);
888
+ }
889
+ return chunkEntry.data;
890
+ }
891
+ /**
892
+ * Process the buffer of brick requests from the shader, turning them into
893
+ * actual Promises for Zarr chunks on the JS side.
894
+ * @param {Uint8Array} buffer The bufRequest (of length width*height*4)
895
+ * containing the brick requests from the shader.
896
+ * @param {object} optsForWeighting
897
+ * @param {number} optsForWeighting.width The width of the render target.
898
+ * @param {number} optsForWeighting.height The height of the render target.
899
+ * @param {number} optsForWeighting.sigmaNormalized The normalized sigma value
900
+ * to use for weighting the brick requests based on their distance from
901
+ * the center of the render target.
902
+ */
903
+ async processRequestData(buffer, optsForWeighting) {
904
+ if (this.initStatus !== INIT_STATUS.COMPLETE) {
905
+ log.debug("processRequestData: not yet initialized, skipping");
906
+ return;
907
+ }
908
+ if (this.isBusy) {
909
+ log.debug("processRequestData: already busy, skipping");
910
+ return;
911
+ }
912
+ if (this.noNewRequests) {
913
+ log.debug("processRequestData: loading stopped by user, skipping");
914
+ return;
915
+ }
916
+ if (this.isContextLost()) {
917
+ log.debug("processRequestData: WebGL context is lost, skipping");
918
+ return;
919
+ }
920
+ this.isBusy = true;
921
+ this.triggerRequest = false;
922
+ const { requests, origRequestCount } = _requestBufferToRequestObjects(buffer, this.k, optsForWeighting);
923
+ if (requests.length === 0) {
924
+ this.noNewRequests = true;
925
+ }
926
+ log.debug(`processRequestData: handling ${requests.length} requests of ${origRequestCount}`);
927
+ await this.handleBrickRequests(requests);
928
+ this.triggerUsage = true;
929
+ this.isBusy = false;
930
+ }
931
+ async processUsageData(buffer) {
932
+ if (this.isBusy) {
933
+ log.debug("processUsageData: already busy, skipping");
934
+ this.needsBailout = true;
935
+ return;
936
+ }
937
+ if (this.isContextLost()) {
938
+ log.debug("processUsageData: WebGL context is lost, skipping");
939
+ return;
940
+ }
941
+ this.isBusy = true;
942
+ this.triggerUsage = false;
943
+ const now = ++this.timeStamp;
944
+ const usedBricks = /* @__PURE__ */ new Set();
945
+ for (let i = 0; i < buffer.length; i += 4) {
946
+ const x = buffer[i];
947
+ const y = buffer[i + 1];
948
+ const z = buffer[i + 2];
949
+ if ((x | y | z) === 0) {
950
+ continue;
951
+ }
952
+ const bcIndex = z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y + y * BRICK_CACHE_SIZE_X + x;
953
+ if (bcIndex < this.BCTimeStamps.length) {
954
+ usedBricks.add(bcIndex);
955
+ }
956
+ }
957
+ Array.from(usedBricks).forEach((bcIndex) => {
958
+ this.BCTimeStamps[bcIndex] = now;
959
+ });
960
+ if (this.BCFull)
961
+ this._buildLRU();
962
+ this.triggerRequest = true;
963
+ this.isBusy = false;
964
+ }
965
+ // Helper method to update PT entries for evicted bricks
966
+ _evictBrick(bcIndex) {
967
+ const pt = this.bc2pt[bcIndex];
968
+ if (!pt)
969
+ return;
970
+ const [min, max] = this.BCMinMax[bcIndex] || [0, 0];
971
+ const ptVal = (0 << 31 | 1 << 30 | Math.min(127, min >> 1) << 23 | Math.min(127, max >> 1) << 16) >>> 0;
972
+ this._updatePTEntry(pt.x, pt.y, pt.z, ptVal);
973
+ this.bc2pt[bcIndex] = null;
974
+ }
975
+ _purgeChannel(ptChannelIndex) {
976
+ log.debug("purging channel", ptChannelIndex);
977
+ log.debug("corresponding zarr channel", this.channels.zarrMappings[ptChannelIndex]);
978
+ if (!this.ptTHREE) {
979
+ log.error("pagetable texture not initialized");
980
+ return;
981
+ }
982
+ if (this.isContextLost()) {
983
+ log.warn("WebGL context is lost, skipping channel purge");
984
+ return;
985
+ }
986
+ this.channels.downsampleMin[ptChannelIndex] = void 0;
987
+ this.channels.downsampleMax[ptChannelIndex] = void 0;
988
+ this.channels.zarrMappings[ptChannelIndex] = void 0;
989
+ const channelMask = this.PT.channelOffsets[ptChannelIndex];
990
+ log.debug("channelMask", channelMask);
991
+ log.error("TODO: not tested yet");
992
+ const { gl } = this;
993
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
994
+ gl.activeTexture(gl.TEXTURE0);
995
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
996
+ for (let r = 0; r < this.zarrStore.resolutions; r++) {
997
+ const anchor = [
998
+ this.PT.anchors[r][2] * channelMask[2],
999
+ this.PT.anchors[r][1] * channelMask[1],
1000
+ this.PT.anchors[r][0] * channelMask[0]
1001
+ ];
1002
+ log.debug("anchor", anchor);
1003
+ const extents = this.zarrStore.brickLayout[r];
1004
+ const size = extents[0] * extents[1] * extents[2];
1005
+ log.debug("extents", extents);
1006
+ log.debug("size", size);
1007
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, anchor[0], anchor[1], anchor[2], extents[0], extents[1], extents[2], gl.RED_INTEGER, gl.UNSIGNED_INT, new Uint32Array(size));
1008
+ }
1009
+ gl.bindTexture(gl.TEXTURE_3D, null);
1010
+ }
1011
+ // Update a PT entry
1012
+ _updatePTEntry(ptX, ptY, ptZ, ptVal) {
1013
+ if (!this.ptTHREE)
1014
+ return;
1015
+ if (this.isContextLost()) {
1016
+ log.warn("WebGL context is lost, skipping PT entry update");
1017
+ return;
1018
+ }
1019
+ const { gl } = this;
1020
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1021
+ gl.activeTexture(gl.TEXTURE0);
1022
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1023
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, ptX, ptY, ptZ, 1, 1, 1, gl.RED_INTEGER, gl.UNSIGNED_INT, new Uint32Array([ptVal]));
1024
+ gl.bindTexture(gl.TEXTURE_3D, null);
1025
+ }
1026
+ /* ------------------------------------------------------------- *
1027
+ * 2. Allocate the next n free bricks in the brick cache *
1028
+ * ------------------------------------------------------------- */
1029
+ /**
1030
+ *
1031
+ * @param {number} n The number of slots to allocate
1032
+ * @returns {{ bcIndex, x, y, z }[]} Array of brick cache coordinates for the allocated slots.
1033
+ */
1034
+ _allocateBCSlots(n) {
1035
+ let slots = [];
1036
+ const total = BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y * BRICK_CACHE_SIZE_Z;
1037
+ if (!this.BCFull && this.BCUnusedIndex + n > total) {
1038
+ this.BCFull = true;
1039
+ log.debug("BRICK CACHE FULL");
1040
+ }
1041
+ if (!this.BCFull) {
1042
+ for (let i = 0; i < n; ++i) {
1043
+ const bcIndex = (this.BCUnusedIndex + i) % total;
1044
+ const z = Math.floor(bcIndex / (BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y));
1045
+ const rem = bcIndex - z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y;
1046
+ const y = Math.floor(rem / BRICK_CACHE_SIZE_X);
1047
+ const x = rem % BRICK_CACHE_SIZE_X;
1048
+ slots.push({ bcIndex, x, y, z });
1049
+ }
1050
+ this.BCUnusedIndex += n;
1051
+ } else {
1052
+ if (this.LRUStack.length < n)
1053
+ this._buildLRU();
1054
+ slots = this.LRUStack.splice(0, n).map((bcIndex) => {
1055
+ this._evictBrick(bcIndex);
1056
+ const z = Math.floor(bcIndex / (BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y));
1057
+ const rem = bcIndex - z * BRICK_CACHE_SIZE_X * BRICK_CACHE_SIZE_Y;
1058
+ const y = Math.floor(rem / BRICK_CACHE_SIZE_X);
1059
+ const x = rem % BRICK_CACHE_SIZE_X;
1060
+ return { bcIndex, x, y, z };
1061
+ });
1062
+ }
1063
+ return slots;
1064
+ }
1065
+ /* ------------------------------------------------------------- *
1066
+ * 4. Upload one brick + PT entry *
1067
+ * ------------------------------------------------------------- */
1068
+ async _uploadBrick(ptCoord, bcSlot) {
1069
+ log.debug("uploading brick", ptCoord, bcSlot);
1070
+ if (this.isContextLost()) {
1071
+ log.warn("WebGL context is lost, skipping brick upload");
1072
+ return;
1073
+ }
1074
+ if (ptCoord.x >= this.PT.xExtent || ptCoord.y >= this.PT.yExtent || ptCoord.z >= this.PT.zTotal || ptCoord.x < 0 || ptCoord.y < 0 || ptCoord.z < 0) {
1075
+ log.error("this.PT", this.PT);
1076
+ log.error("ptCoord out of bounds", ptCoord);
1077
+ return;
1078
+ }
1079
+ const { channel, resolution, x, y, z } = _ptToZarr(ptCoord.x, ptCoord.y, ptCoord.z, { PT_zExtent: this.PT.zExtent, PT_z0Extent: this.PT.z0Extent, PT_anchors: this.PT.anchors });
1080
+ if (!this.channels || !this.channels.zarrMappings || this.channels.zarrMappings.length === 0) {
1081
+ log.error("Channel mappings not initialized, skipping brick upload");
1082
+ return;
1083
+ }
1084
+ if (channel < 0 || channel >= this.channels.zarrMappings.length) {
1085
+ log.error("Channel index out of bounds", { channel, mappingsLength: this.channels.zarrMappings.length });
1086
+ return;
1087
+ }
1088
+ const zarrChannel = this.channels.zarrMappings[channel];
1089
+ if (zarrChannel === void 0 || zarrChannel === -1) {
1090
+ log.warn("zarrChannel is undefined or -1", {
1091
+ zarrChannel,
1092
+ channel,
1093
+ ptCoord,
1094
+ channelMappings: this.channels.zarrMappings,
1095
+ contextLost: this.isContextLost()
1096
+ });
1097
+ if (this._lastChannelConfig && this._lastChannelConfig.zarrMappings[channel] !== void 0) {
1098
+ log.warn("Attempting to use last known channel config");
1099
+ this.channels.zarrMappings = [...this._lastChannelConfig.zarrMappings];
1100
+ this.channels.colorMappings = [...this._lastChannelConfig.colorMappings];
1101
+ this.channels.downsampleMin = [...this._lastChannelConfig.downsampleMin];
1102
+ this.channels.downsampleMax = [...this._lastChannelConfig.downsampleMax];
1103
+ const restoredZarrChannel = this.channels.zarrMappings[channel];
1104
+ if (restoredZarrChannel !== void 0 && restoredZarrChannel !== -1) {
1105
+ log.debug("Successfully restored channel mapping, continuing with upload");
1106
+ } else {
1107
+ log.error("Could not restore valid channel mapping, aborting brick upload");
1108
+ return;
1109
+ }
1110
+ } else {
1111
+ log.error("No fallback channel config available, aborting brick upload");
1112
+ return;
1113
+ }
1114
+ }
1115
+ log.debug("starting to load zarr chunk", { resolution, z, y, x, zarrChannel });
1116
+ let chunk = await this.loadZarrChunk(0, zarrChannel, z, y, x, resolution);
1117
+ log.debug("chunk", chunk);
1118
+ if (chunk instanceof Uint16Array) {
1119
+ log.debug("chunk is Uint16Array, converting to Uint8Array");
1120
+ if (this.channels.downsampleMin[channel] === void 0) {
1121
+ const channelId = this.channels.zarrMappings[channel];
1122
+ log.debug("channelId was not found in this.channels.downsampleMin[channel]", channelId);
1123
+ this.channels.downsampleMin[channel] = this.ngffMetadata?.omero?.channels?.[channelId]?.window?.min || 0;
1124
+ this.channels.downsampleMax[channel] = this.ngffMetadata?.omero?.channels?.[channelId]?.window?.max || 65535;
1125
+ log.debug("this.channels.downsampleMin[channel]", this.channels.downsampleMin[channel]);
1126
+ log.debug("this.channels.downsampleMax[channel]", this.channels.downsampleMax[channel]);
1127
+ }
1128
+ const uint16View = new Uint16Array(chunk.buffer);
1129
+ const uint8Chunk = new Uint8Array(chunk.length);
1130
+ const minVal = this.channels.downsampleMin[channel];
1131
+ const maxVal = this.channels.downsampleMax[channel];
1132
+ const range = maxVal - minVal;
1133
+ for (let i = 0; i < uint16View.length; i++) {
1134
+ const val = uint16View[i];
1135
+ const clamped = Math.max(minVal, Math.min(maxVal, val));
1136
+ uint8Chunk[i] = (clamped - minVal) * 255 / range | 0;
1137
+ }
1138
+ chunk = uint8Chunk;
1139
+ }
1140
+ if (!(chunk instanceof Uint8Array)) {
1141
+ throw new Error(`Unsupported chunk type: ${chunk.constructor.name}. Expected Uint8Array.`);
1142
+ }
1143
+ let min = 255;
1144
+ let max = 0;
1145
+ for (let i = 0; i < chunk.length; ++i) {
1146
+ const v = chunk[i];
1147
+ if (v < min)
1148
+ min = v;
1149
+ if (v > max)
1150
+ max = v;
1151
+ }
1152
+ const { gl } = this;
1153
+ const texBC = this.renderer.properties.get(this.bcTHREE).__webglTexture;
1154
+ gl.activeTexture(gl.TEXTURE2);
1155
+ gl.bindTexture(gl.TEXTURE_3D, texBC);
1156
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
1157
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, bcSlot.x * BRICK_SIZE, bcSlot.y * BRICK_SIZE, bcSlot.z * BRICK_SIZE, BRICK_SIZE, BRICK_SIZE, BRICK_SIZE, gl.RED, gl.UNSIGNED_BYTE, chunk);
1158
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
1159
+ gl.bindTexture(gl.TEXTURE_3D, null);
1160
+ const error = gl.getError();
1161
+ if (error !== gl.NO_ERROR) {
1162
+ log.error("WebGL error during brick upload:", error, chunk);
1163
+ }
1164
+ if (channel >= this.channels.zarrMappings.length) {
1165
+ log.debug("channel is out of bounds", channel);
1166
+ min = 255;
1167
+ max = 255;
1168
+ }
1169
+ const ptVal = _packPT(min, max, bcSlot.x, bcSlot.y, bcSlot.z);
1170
+ const texPT = this.renderer.properties.get(this.ptTHREE).__webglTexture;
1171
+ gl.activeTexture(gl.TEXTURE0);
1172
+ gl.bindTexture(gl.TEXTURE_3D, texPT);
1173
+ gl.texSubImage3D(gl.TEXTURE_3D, 0, ptCoord.x, ptCoord.y, ptCoord.z, 1, 1, 1, gl.RED_INTEGER, gl.UNSIGNED_INT, new Uint32Array([ptVal]));
1174
+ gl.bindTexture(gl.TEXTURE_3D, null);
1175
+ const error2 = gl.getError();
1176
+ if (error2 !== gl.NO_ERROR) {
1177
+ log.error("WebGL error during pagetable upload:", error2, chunk);
1178
+ }
1179
+ this.BCTimeStamps[bcSlot.bcIndex] = this.timeStamp;
1180
+ this.BCMinMax[bcSlot.bcIndex] = [min, max];
1181
+ this.bc2pt[bcSlot.bcIndex] = ptCoord;
1182
+ }
1183
+ /* ------------------------------------------------------------- *
1184
+ * 5. Public: handle a batch of PT requests (array of {x,y,z}) *
1185
+ * ------------------------------------------------------------- */
1186
+ async handleBrickRequests(ptRequests) {
1187
+ if (ptRequests.length === 0)
1188
+ return;
1189
+ this.totalBricksRequested = ptRequests.length;
1190
+ this.currentRequestCount = ptRequests.length;
1191
+ const slots = this._allocateBCSlots(ptRequests.length);
1192
+ log.debug("Handling brick requests:", { requestCount: ptRequests.length, slotCount: slots.length });
1193
+ log.debug("handleBrickRequests: starting for loop");
1194
+ for (let i = 0; i < ptRequests.length; ++i) {
1195
+ await this._uploadBrick(ptRequests[i], slots[i]);
1196
+ this.currentRequestCount = ptRequests.length - i - 1;
1197
+ const rlength = this.bricksEverLoaded.size;
1198
+ this.bricksEverLoaded.add(`${ptRequests[i].x},${ptRequests[i].y},${ptRequests[i].z}`);
1199
+ if (rlength === this.bricksEverLoaded.size) {
1200
+ log.debug("DUPLICATE BRICK LOADED", ptRequests[i]);
1201
+ }
1202
+ if (this.needsBailout) {
1203
+ log.debug("Bailing out of handleBrickRequests early due to needsBailout flag");
1204
+ this.needsBailout = false;
1205
+ this.currentRequestCount = 0;
1206
+ break;
1207
+ }
1208
+ }
1209
+ this.currentRequestCount = 0;
1210
+ log.debug("this.bricksEverLoaded", this.bricksEverLoaded);
1211
+ }
1212
+ /* --------------------------------------------------------- *
1213
+ * Rebuild the LRUStack with the k least-recently-used bricks *
1214
+ * --------------------------------------------------------- */
1215
+ _buildLRU() {
1216
+ const brickIndicesWithTimes = this.BCTimeStamps.map((time, index) => ({ index, time }));
1217
+ this.LRUStack = brickIndicesWithTimes.sort((a, b) => a.time - b.time).slice(0, this.k).map((item) => item.index);
1218
+ }
1219
+ }
1220
+ const volumeVertexShader = `//
1221
+ // Output: Unnormalized ray direction from camera to each vertex
1222
+ // Used by fragment shader for ray marching through the volume
1223
+ out vec3 rayDirUnnorm;
1224
+
1225
+ // Output: Camera position transformed into volume's local coordinate system
1226
+ // Used to calculate ray origins in the fragment shader
1227
+ out vec3 cameraCorrected;
1228
+
1229
+ // Volume scale uniform (likely for anisotropic voxels)
1230
+ uniform vec3 u_vol_scale;
1231
+
1232
+ // Volume size uniform
1233
+ uniform vec3 u_size;
1234
+
1235
+ // Output: Vertex positions normalized to [0,1] range within volume bounds
1236
+ // Standard coordinate system for volume sampling
1237
+ varying vec3 worldSpaceCoords;
1238
+
1239
+ // Output: Texture coordinates for sampling volume data
1240
+ varying vec2 vUv;
1241
+
1242
+ // Output: Final clip-space position (stored for fragment shader access)
1243
+ varying vec4 glPosition;
1244
+
1245
+ // Volume bounding box size uniform
1246
+ uniform highp vec3 boxSize;
1247
+
1248
+ void main()
1249
+ {
1250
+ // Transform vertex positions from [-0.5, 0.5] range to [0, 1] range
1251
+ // This is the standard coordinate system for volume sampling
1252
+ //
1253
+ // Mathematical transformation:
1254
+ // worldSpaceCoords = (position / boxSize) + 0.5
1255
+ //
1256
+ // Example:
1257
+ // position = (-0.5, -0.5, -0.5) → worldSpaceCoords = (0, 0, 0)
1258
+ // position = ( 0.0, 0.0, 0.0) → worldSpaceCoords = (0.5, 0.5, 0.5)
1259
+ // position = ( 0.5, 0.5, 0.5) → worldSpaceCoords = (1, 1, 1)
1260
+ worldSpaceCoords = position / boxSize + vec3(0.5, 0.5, 0.5); //move it from [-0.5;0.5] to [0,1]
1261
+
1262
+ // Transform camera position into volume's local coordinate system
1263
+ // This gives us the ray origin in volume space
1264
+ cameraCorrected = (inverse(modelMatrix) * vec4(cameraPosition, 1.)).xyz;
1265
+
1266
+ // Calculate unnormalized ray direction from camera to each vertex
1267
+ // Used by fragment shader for ray marching through the volume
1268
+ rayDirUnnorm = position - cameraCorrected;
1269
+
1270
+ // Apply standard MVP transformation to get clip-space coordinates
1271
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1272
+
1273
+ // Store clip-space position for fragment shader access
1274
+ glPosition = gl_Position;
1275
+
1276
+ // Pass through texture coordinates for volume sampling
1277
+ vUv = uv;
1278
+ }
1279
+ `;
1280
+ const volumeFragmentShader = `//
1281
+ // #include <packing>
1282
+ precision highp float;
1283
+ precision highp int;
1284
+ precision highp sampler3D;
1285
+ precision highp usampler3D;
1286
+
1287
+ // ========================================
1288
+ // INPUT VARIABLES (from vertex shader)
1289
+ // ========================================
1290
+ // Unnormalized ray direction from camera
1291
+ in vec3 rayDirUnnorm;
1292
+ // Camera position in world space
1293
+ in vec3 cameraCorrected;
1294
+
1295
+ // ========================================
1296
+ // TEXTURE SAMPLERS
1297
+ // ========================================
1298
+ // 3D texture containing cached brick data (2048x2048x128)
1299
+ // (2048*2048*128)/(32*32*32) = 16,384 bricks can be stored?
1300
+ uniform sampler3D brickCacheTex;
1301
+ // 3D texture containing page table entries (brick metadata)
1302
+ uniform usampler3D pageTableTex;
1303
+
1304
+ // ========================================
1305
+ // RENDERING PARAMETERS/CONSTANTS
1306
+ // ========================================
1307
+ // Rendering style: 0=MIP, 1=MinIP, 2=standard volume rendering, 3=DEBUG
1308
+ uniform int u_renderstyle;
1309
+ // Global opacity multiplier for volume rendering
1310
+ uniform float opacity;
1311
+
1312
+ // ========================================
1313
+ // CONTRAST LIMITS (per channel)
1314
+ // per channel min/max values for value normalization
1315
+ // ========================================
1316
+ uniform vec2 clim0;
1317
+ uniform vec2 clim1;
1318
+ uniform vec2 clim2;
1319
+ uniform vec2 clim3;
1320
+ uniform vec2 clim4;
1321
+ uniform vec2 clim5;
1322
+ uniform vec2 clim6;
1323
+
1324
+ // ========================================
1325
+ // CLIPPING PLANES
1326
+ // e.g., for X-axis clipping: (min_x, max_x) or (-1, -1) if disabled
1327
+ // ========================================
1328
+ uniform vec2 xClip;
1329
+ uniform vec2 yClip;
1330
+ uniform vec2 zClip;
1331
+
1332
+ // ========================================
1333
+ // CHANNEL COLORS AND OPACITIES
1334
+ // rgb -- color values, a -- visibility (boolean)
1335
+ // ========================================
1336
+ uniform vec4 color0;
1337
+ uniform vec4 color1;
1338
+ uniform vec4 color2;
1339
+ uniform vec4 color3;
1340
+ uniform vec4 color4;
1341
+ uniform vec4 color5;
1342
+ uniform vec4 color6;
1343
+
1344
+ // maps colors to physical spaces
1345
+ uniform int channelMapping[7];
1346
+
1347
+ // ========================================
1348
+ // VOLUME AND RESOLUTION PARAMETERS
1349
+ // ========================================
1350
+ // Volume bounding box size in world space
1351
+ uniform highp vec3 boxSize;
1352
+ // Rendering resolution level (affects step size)
1353
+ // stepsize, correlates with resolution
1354
+ uniform int renderRes;
1355
+ // Volume dimensions in voxels (x, y, z)
1356
+ // resolution 0 voxel extents
1357
+ uniform uvec3 voxelExtents;
1358
+ // Global resolution range: (min_res, max_res)
1359
+ // global range of requested resolutions
1360
+ uniform ivec2 resGlobal;
1361
+ // Maximum number of active channels
1362
+ // max number of channels (relevant for the cache statistics)
1363
+ // between 1 and 7
1364
+ uniform int maxChannels;
1365
+
1366
+ // ========================================
1367
+ // PER-CHANNEL RESOLUTION RANGES
1368
+ // per color channel resolution range
1369
+ // Each channel can have different available resolution levels
1370
+ // e.g., for Channel 0: (min_res, max_res)
1371
+ // ========================================
1372
+ uniform ivec2 res0;
1373
+ uniform ivec2 res1;
1374
+ uniform ivec2 res2;
1375
+ uniform ivec2 res3;
1376
+ uniform ivec2 res4;
1377
+ uniform ivec2 res5;
1378
+ uniform ivec2 res6;
1379
+ // Channel 7: unused
1380
+ uniform ivec2 res7;
1381
+
1382
+ // ========================================
1383
+ // LEVEL-OF-DETAIL PARAMETERS
1384
+ // controls how fast we decrease the resolution
1385
+ // ========================================
1386
+ // LOD factor for distance-based resolution selection
1387
+ uniform float lodFactor;
1388
+
1389
+ // ========================================
1390
+ // ANCHOR POINTS (per resolution level)
1391
+ // per resolution anchor point for pagetable
1392
+ // ========================================
1393
+ // Anchor points define the origin of page table for each resolution level
1394
+ // Resolution 0 anchor point (highest detail)
1395
+ uniform uvec3 anchor0;
1396
+ uniform uvec3 anchor1;
1397
+ uniform uvec3 anchor2;
1398
+ uniform uvec3 anchor3;
1399
+ uniform uvec3 anchor4;
1400
+ uniform uvec3 anchor5;
1401
+ uniform uvec3 anchor6;
1402
+ uniform uvec3 anchor7;
1403
+ uniform uvec3 anchor8;
1404
+ uniform uvec3 anchor9;
1405
+ // Resolution 9 anchor point (lowest detail)
1406
+
1407
+ // ========================================
1408
+ // SCALE FACTORS (per resolution level)
1409
+ // per resolution downsample factor
1410
+ // ========================================
1411
+ // Scale factors determine voxel size at each resolution level
1412
+ // Resolution 0 scale factors (should be 1,1,1)
1413
+ uniform vec3 scale0;
1414
+ uniform vec3 scale1;
1415
+ uniform vec3 scale2;
1416
+ uniform vec3 scale3;
1417
+ uniform vec3 scale4;
1418
+ uniform vec3 scale5;
1419
+ uniform vec3 scale6;
1420
+ uniform vec3 scale7;
1421
+ uniform vec3 scale8;
1422
+ uniform vec3 scale9;
1423
+ // Resolution 9 scale factors
1424
+
1425
+ // ========================================
1426
+ // VARYING VARIABLES (unused but required)
1427
+ // ========================================
1428
+ // Fragment position (unused)
1429
+ varying vec4 glPosition;
1430
+ // World space coordinates (used for depth only)
1431
+ varying vec3 worldSpaceCoords;
1432
+
1433
+ // ========================================
1434
+ // OUTPUT VARIABLES (multiple render targets)
1435
+ // output buffers
1436
+ // ========================================
1437
+ // Final rendered color (sRGB)
1438
+ layout(location = 0) out vec4 gColor;
1439
+ // Brick loading requests (packed coordinates)
1440
+ layout(location = 1) out vec4 gRequest;
1441
+ // Brick usage tracking (for cache management)
1442
+ layout(location = 2) out vec4 gUsage;
1443
+
1444
+ // ========================================
1445
+ // CONSTANTS
1446
+ // ========================================
1447
+ // Size of each brick in voxels (32x32x32)
1448
+ const float BRICK_SIZE = 32.0;
1449
+ // Brick cache texture width
1450
+ const float BRICK_CACHE_SIZE_X = 2048.0;
1451
+ // Brick cache texture height
1452
+ const float BRICK_CACHE_SIZE_Y = 2048.0;
1453
+ // Brick cache texture depth
1454
+ const float BRICK_CACHE_SIZE_Z = 128.0;
1455
+ // Number of bricks in X (64)
1456
+ const float BRICK_CACHE_BRICKS_X = BRICK_CACHE_SIZE_X / BRICK_SIZE;
1457
+ // Number of bricks in Y (64)
1458
+ const float BRICK_CACHE_BRICKS_Y = BRICK_CACHE_SIZE_Y / BRICK_SIZE;
1459
+ // Number of bricks in Z (4)
1460
+ const float BRICK_CACHE_BRICKS_Z = BRICK_CACHE_SIZE_Z / BRICK_SIZE;
1461
+
1462
+ // ========================================
1463
+ // RAY-VOLUME INTERSECTION
1464
+ // calculating the intersection of the ray with the bounding box
1465
+ // ========================================
1466
+ // Calculates the intersection of a ray with the volume's bounding box
1467
+ // Returns (entry_time, exit_time) for the ray-box intersection
1468
+ // Handles clipping planes by adjusting the bounding box
1469
+ //
1470
+ // Parameters:
1471
+ // orig - vec3: Ray origin point in world space
1472
+ // dir - vec3: Ray direction vector (should be normalized)
1473
+ //
1474
+ // Returns:
1475
+ // vec2: (entry_time, exit_time) where:
1476
+ // - entry_time: Distance along ray to enter the volume
1477
+ // - exit_time: Distance along ray to exit the volume
1478
+ // - If no intersection: entry_time > exit_time
1479
+ vec2 intersect_hit(vec3 orig, vec3 dir) {
1480
+ // Start with full volume bounds
1481
+ vec3 boxMin = vec3(-0.5) * boxSize;
1482
+ vec3 boxMax = vec3(0.5) * boxSize;
1483
+
1484
+ // Apply clipping planes if they're active (xClip.x > -1.0 means active)
1485
+ if (xClip.x > -1.0) {
1486
+ boxMin.x = xClip.x - (boxSize.x / 2.0);
1487
+ if (xClip.y < boxSize.x)
1488
+ boxMax.x = xClip.y - (boxSize.x / 2.0);
1489
+ }
1490
+ if (yClip.x > -1.0) {
1491
+ boxMin.y = yClip.x - (boxSize.y / 2.0);
1492
+ if (yClip.y < boxSize.y)
1493
+ boxMax.y = yClip.y - (boxSize.y / 2.0);
1494
+ }
1495
+ if (zClip.x > -1.0) {
1496
+ boxMin.z = zClip.x - (boxSize.z / 2.0);
1497
+ if (zClip.y < boxSize.z)
1498
+ boxMax.z = zClip.y - (boxSize.z / 2.0);
1499
+ }
1500
+
1501
+ // Standard ray-box intersection algorithm
1502
+ vec3 invDir = 1.0 / dir;
1503
+ vec3 tmin0 = (boxMin - orig) * invDir;
1504
+ vec3 tmax0 = (boxMax - orig) * invDir;
1505
+ vec3 tmin = min(tmin0, tmax0);
1506
+ vec3 tmax = max(tmin0, tmax0);
1507
+ float t0 = max(tmin.x, max(tmin.y, tmin.z)); // Entry time
1508
+ float t1 = min(tmax.x, min(tmax.y, tmax.z)); // Exit time
1509
+ return vec2(t0, t1);
1510
+ }
1511
+
1512
+ // ========================================
1513
+ // UTILITY FUNCTIONS
1514
+ // ========================================
1515
+
1516
+ // Pseudo-random number generator for jittered sampling
1517
+ // random number generator based on the uv coordinate
1518
+ // Author @patriciogv - 2015
1519
+ // http://patriciogonzalezvivo.com
1520
+ //
1521
+ // Parameters:
1522
+ // None (uses gl_FragCoord.xy as input)
1523
+ //
1524
+ // Returns:
1525
+ // float: Random value between 0.0 and 1.0 based on fragment coordinates
1526
+ float random() {
1527
+ return fract(sin(dot(gl_FragCoord.xy, vec2(12.9898,78.233)))* 43758.5453123);
1528
+ }
1529
+
1530
+ // Convert from linear RGB to sRGB color space
1531
+ // Implements the standard sRGB transfer function for gamma correction
1532
+ //
1533
+ // Parameters:
1534
+ // x - float: Linear RGB value between 0.0 and 1.0
1535
+ //
1536
+ // Returns:
1537
+ // float: sRGB value between 0.0 and 1.0
1538
+ float linear_to_srgb(float x) {
1539
+ if (x <= 0.0031308f) {
1540
+ return 12.92f * x;
1541
+ }
1542
+ return 1.055f * pow(x, 1.f / 2.4f) - 0.055f;
1543
+ }
1544
+
1545
+ // Convert from linear RGB to sRGB color space (vector version)
1546
+ // Applies sRGB conversion to each RGB component while preserving alpha
1547
+ //
1548
+ // Parameters:
1549
+ // x - vec4: Linear RGBA color with components between 0.0 and 1.0
1550
+ //
1551
+ // Returns:
1552
+ // vec4: sRGB RGBA color with components between 0.0 and 1.0
1553
+ vec4 linear_to_srgb(vec4 x) {
1554
+ return vec4(linear_to_srgb(x.r), linear_to_srgb(x.g), linear_to_srgb(x.b), x.a);
1555
+ }
1556
+
1557
+ // ========================================
1558
+ // PAGE TABLE COORDINATE PACKING
1559
+ // transform the pagetable coordinate into a RGBA8 value
1560
+ // ========================================
1561
+ // Packs 3D page table coordinates into RGBA8 texture format
1562
+ // Uses 10 bits for X, 10 bits for Y, 12 bits for Z
1563
+ //
1564
+ // Parameters:
1565
+ // coord - uvec3: 3D coordinates to pack (X, Y, Z components)
1566
+ // - X coordinate: 10-bit unsigned integer (0-1023)
1567
+ // - Y coordinate: 10-bit unsigned integer (0-1023)
1568
+ // - Z coordinate: 12-bit unsigned integer (0-4095)
1569
+ //
1570
+ // Returns:
1571
+ // vec4: RGBA8 encoded coordinates with components between 0.0 and 1.0
1572
+ // - R: Upper 8 bits of packed 32-bit value
1573
+ // - G: Middle-upper 8 bits of packed 32-bit value
1574
+ // - B: Middle-lower 8 bits of packed 32-bit value
1575
+ // - A: Lower 8 bits of packed 32-bit value
1576
+ vec4 packPTCoordToRGBA8(uvec3 coord) {
1577
+
1578
+ uint x = coord.x & 0x3FFu; // 10 bits for X coordinate
1579
+ uint y = coord.y & 0x3FFu; // 10 bits for Y coordinate
1580
+ uint z = coord.z & 0xFFFu; // 12 bits for Z coordinate
1581
+
1582
+ // Pack into 32-bit integer
1583
+ uint packed =
1584
+ (x << 22u) |
1585
+ (y << 12u) |
1586
+ (z);
1587
+
1588
+ // Decompose into RGBA8 format
1589
+ return vec4(
1590
+ float((packed >> 24u) & 0xFFu) / 255.0,
1591
+ float((packed >> 16u) & 0xFFu) / 255.0,
1592
+ float((packed >> 8u) & 0xFFu) / 255.0,
1593
+ float(packed & 0xFFu) / 255.0
1594
+ );
1595
+ }
1596
+
1597
+ // ========================================
1598
+ // RESOLUTION AND ANCHOR POINT ACCESSORS
1599
+ // ========================================
1600
+
1601
+ // Get anchor point for a specific resolution level
1602
+ // Anchor points define the origin of the page table for each resolution
1603
+ //
1604
+ // Parameters:
1605
+ // index - int: Resolution level index (0-9, where 0 is highest resolution)
1606
+ //
1607
+ // Returns:
1608
+ // uvec3: 3D anchor point coordinates in the page table, or (-1, -1, -1) if invalid
1609
+ uvec3 getAnchorPoint(int index) {
1610
+ if (index == 0) return anchor0;
1611
+ if (index == 1) return anchor1;
1612
+ if (index == 2) return anchor2;
1613
+ if (index == 3) return anchor3;
1614
+ if (index == 4) return anchor4;
1615
+ if (index == 5) return anchor5;
1616
+ if (index == 6) return anchor6;
1617
+ if (index == 7) return anchor7;
1618
+ if (index == 8) return anchor8;
1619
+ if (index == 9) return anchor9;
1620
+ return uvec3(-1, -1, -1);
1621
+ }
1622
+
1623
+ // Find the lowest available resolution level
1624
+ // Returns the highest resolution index that has valid data
1625
+ //
1626
+ // Parameters:
1627
+ // None
1628
+ //
1629
+ // Returns:
1630
+ // int: Highest resolution level index (0-9) that has valid anchor point data
1631
+ // Returns 9 if no valid resolution levels are found
1632
+ int getLowestRes() {
1633
+ for (int i = 0; i < 10; i++) {
1634
+ if (getAnchorPoint(i) == uvec3(0,0,0)) {
1635
+ return i - 1;
1636
+ }
1637
+ }
1638
+ return 9;
1639
+ }
1640
+
1641
+ // Get the downsample factor for a resolution level
1642
+ // Scale factors determine the voxel size at each resolution
1643
+ //
1644
+ // Parameters:
1645
+ // index - int: Resolution level index (0-9, where 0 is highest resolution)
1646
+ //
1647
+ // Returns:
1648
+ // vec3: Scale factors (x, y, z) for the resolution level, or (-1, -1, -1) if invalid
1649
+ // Higher scale factors indicate larger voxels (lower resolution)
1650
+ vec3 getScale(int index) {
1651
+ if (index == 0) return scale0;
1652
+ if (index == 1) return scale1;
1653
+ if (index == 2) return scale2;
1654
+ if (index == 3) return scale3;
1655
+ if (index == 4) return scale4;
1656
+ if (index == 5) return scale5;
1657
+ if (index == 6) return scale6;
1658
+ if (index == 7) return scale7;
1659
+ if (index == 8) return scale8;
1660
+ if (index == 9) return scale9;
1661
+ return vec3(-1.0, -1.0, -1.0);
1662
+ }
1663
+
1664
+ // Get the resolution range for a color channel
1665
+ // Returns (min_res, max_res) for the channel
1666
+ //
1667
+ // Parameters:
1668
+ // index - int: Channel index (0-6)
1669
+ //
1670
+ // Returns:
1671
+ // ivec2: Resolution range as (min_resolution_level, max_resolution_level)
1672
+ // Returns (-1, -1) if channel index is invalid
1673
+ ivec2 getRes(int index) {
1674
+ if (index == 0) return res0;
1675
+ if (index == 1) return res1;
1676
+ if (index == 2) return res2;
1677
+ if (index == 3) return res3;
1678
+ if (index == 4) return res4;
1679
+ if (index == 5) return res5;
1680
+ if (index == 6) return res6;
1681
+ return ivec2(-1, -1);
1682
+ }
1683
+
1684
+ // Get the min/max values (contrast limits) for a color channel
1685
+ // Returns (min_value, max_value) for normalization
1686
+ //
1687
+ // Parameters:
1688
+ // index - int: Channel index (0-6)
1689
+ //
1690
+ // Returns:
1691
+ // vec2: Contrast limits as (min_value, max_value) for data normalization
1692
+ // Returns (-1.0, -1.0) if channel index is invalid
1693
+ vec2 getClim(int index) {
1694
+ if (index == 0) return clim0;
1695
+ if (index == 1) return clim1;
1696
+ if (index == 2) return clim2;
1697
+ if (index == 3) return clim3;
1698
+ if (index == 4) return clim4;
1699
+ if (index == 5) return clim5;
1700
+ if (index == 6) return clim6;
1701
+ return vec2(-1.0, -1.0);
1702
+ }
1703
+
1704
+ // ========================================
1705
+ // COORDINATE TRANSFORMATIONS
1706
+ // ========================================
1707
+
1708
+ // Convert normalized coordinates (0-1) to voxel coordinates.
1709
+ // get the voxel coordinate in the specified resolution from the normalized coordinate
1710
+ //
1711
+ // Parameters:
1712
+ // normalized - vec3: Normalized coordinates in range [0,1] for each axis
1713
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
1714
+ //
1715
+ // Returns:
1716
+ // vec3: Voxel coordinates in the volume space at the specified resolution
1717
+ vec3 getVoxelFromNormalized(vec3 normalized, int res) {
1718
+ vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution
1719
+ vec3 voxel = normalized * extents;
1720
+ return voxel;
1721
+ }
1722
+
1723
+ // Convert voxel coordinates to normalized coordinates (0-1)
1724
+ // get the normalized coordinate based on the voxel coordinate in the specified resolution
1725
+ //
1726
+ // Parameters:
1727
+ // voxel - vec3: Voxel coordinates in the volume space
1728
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
1729
+ //
1730
+ // Returns:
1731
+ // vec3: Normalized coordinates in range [0,1] for each axis
1732
+ vec3 getNormalizedFromVoxel(vec3 voxel, int res) {
1733
+ vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution
1734
+ vec3 normalized = voxel / extents;
1735
+ return normalized;
1736
+ }
1737
+
1738
+ // Convert normalized coordinates to brick coordinates.
1739
+ // get the brick coordinate in the specified resolution based on the normalized coordinate
1740
+ // needed for pagetable calculations
1741
+ //
1742
+ // Parameters:
1743
+ // normalized - vec3: Normalized coordinates in range [0,1] for each axis
1744
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
1745
+ //
1746
+ // Returns:
1747
+ // vec3: Brick coordinates (each brick is 32x32x32 voxels)
1748
+ vec3 getBrickFromNormalized(vec3 normalized, int res) {
1749
+ vec3 voxel = getVoxelFromNormalized(normalized, res);
1750
+ vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels
1751
+ return brick;
1752
+ }
1753
+
1754
+ // Convert voxel coordinates to brick coordinates.
1755
+ // get the brick coordinate in the specified resolution based on the voxel coordinate
1756
+ //
1757
+ // Parameters:
1758
+ // voxel - vec3: Voxel coordinates in the volume space
1759
+ // res - int: Resolution level (0=highest detail, 9=lowest detail)
1760
+ //
1761
+ // Returns:
1762
+ // vec3: Brick coordinates (each brick is 32x32x32 voxels)
1763
+ vec3 getBrickFromVoxel(vec3 voxel, int res) {
1764
+ vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels
1765
+ return brick;
1766
+ }
1767
+
1768
+ // ========================================
1769
+ // CHANNEL-SPECIFIC ACCESSORS
1770
+ // ========================================
1771
+
1772
+ // Get channel offset in page table.
1773
+ // get the vector for the specified channel slot in the pagetable
1774
+ // Different channels are stored at different Z-offsets in the page table
1775
+ //
1776
+ // Parameters:
1777
+ // index - int: Channel index (0-6)
1778
+ //
1779
+ // Returns:
1780
+ // uvec3: 3D offset coordinates for the channel in the page table
1781
+ uvec3 getChannelOffset(int index) {
1782
+ if (index == 0) return uvec3(0, 0, 1);
1783
+ if (index == 1) return uvec3(0, 1, 0);
1784
+ if (index == 2) return uvec3(0, 1, 1);
1785
+ if (index == 3) return uvec3(1, 0, 0);
1786
+ if (index == 4) return uvec3(1, 0, 1);
1787
+ if (index == 5) return uvec3(1, 1, 0);
1788
+ if (index == 6) return uvec3(1, 1, 1);
1789
+ return uvec3(0, 0, 0);
1790
+ }
1791
+
1792
+ // Get color for a channel.
1793
+ // get the color per color channel
1794
+ //
1795
+ // Parameters:
1796
+ // index - int: Channel index (0-6)
1797
+ //
1798
+ // Returns:
1799
+ // vec3: RGB color values for the specified channel
1800
+ vec3 getChannelColor(int index) {
1801
+ if (index == 0) return color0.xyz;
1802
+ if (index == 1) return color1.xyz;
1803
+ if (index == 2) return color2.xyz;
1804
+ if (index == 3) return color3.xyz;
1805
+ if (index == 4) return color4.xyz;
1806
+ if (index == 5) return color5.xyz;
1807
+ if (index == 6) return color6.xyz;
1808
+ return vec3(0.0, 0.0, 0.0);
1809
+ }
1810
+
1811
+ // Get opacity for a channel
1812
+ // get the opacity (used as visibility) per color channel
1813
+ //
1814
+ // Parameters:
1815
+ // index - int: Channel index (0-6)
1816
+ //
1817
+ // Returns:
1818
+ // float: Opacity value (0.0-1.0) for the specified channel
1819
+ float getChannelOpacity(int index) {
1820
+ if (index == 0) return color0.w;
1821
+ if (index == 1) return color1.w;
1822
+ if (index == 2) return color2.w;
1823
+ if (index == 3) return color3.w;
1824
+ if (index == 4) return color4.w;
1825
+ if (index == 5) return color5.w;
1826
+ if (index == 6) return color6.w;
1827
+ return 0.0;
1828
+ }
1829
+
1830
+ // ========================================
1831
+ // PAGE TABLE DECODING
1832
+ // ========================================
1833
+
1834
+ /**
1835
+ * retrieving the brick based on:
1836
+ * location -- normalized coordinate
1837
+ * targetRes -- target resolution
1838
+ * channel -- physical channel slot
1839
+ * rnd -- random number for jittering requests
1840
+ * query -- whether to query the brick (we dont query for interblock interpolation)
1841
+ * colorIndex -- color index for querying the min max values
1842
+ *
1843
+ * returns:
1844
+ * w >= 0 -- xyz contains brick cache coordinate, w stores resolution
1845
+ * w == -1 -- not resident in any resolution, should be treated as empty
1846
+ * w == -2 -- empty (with respect to current transfer function)
1847
+ * w == -3 -- constant full (with respect to current transfer function)
1848
+ * w == -4 -- constant value within range, x stores that value
1849
+ *
1850
+ * bit layout:
1851
+ * [1] 31 | 0 — flag resident
1852
+ * [1] 30 | 1 — flag init
1853
+ * [7] 23…29 | 2…8 — min → 128
1854
+ * [7] 16…22 | 9…15 — max → 128
1855
+ * [6] 10…15 | 16…21 — x offset in brick cache → max 64
1856
+ * [6] 4…9 | 22…27 — y offset in brick cache → max 64
1857
+ * [4] 0…3 | 28…31 — z offset in brick cache → max 16, effectively 4
1858
+ */
1859
+
1860
+ /*
1861
+ Page table entry format (32 bits):
1862
+ [31] | 0 — flag resident (1=loaded in cache)
1863
+ [30] | 1 — flag init (1=initialized)
1864
+ [29:23] | 2…8 — min value (7 bits) → 128 levels
1865
+ [22:16] | 9…15 — max value (7 bits) → 128 levels
1866
+ [15:10] | 16…21 — x offset in brick cache (6 bits) → 64 bricks
1867
+ [9:4] | 22…27 — y offset in brick cache (6 bits) → 64 bricks
1868
+ [3:0] | 28…31 — z offset in brick cache (4 bits) → 16 bricks
1869
+ */
1870
+
1871
+ // Query page table to find brick location and status
1872
+ // Searches for a brick at the specified location across multiple resolution levels
1873
+ // and returns its cache coordinates and status information
1874
+ //
1875
+ // Parameters:
1876
+ // location - vec3: Normalized coordinates (0-1) within the volume
1877
+ // targetRes - int: Target resolution level to start searching from
1878
+ // channel - int: Channel index (0-6) to query
1879
+ // rnd - float: Random value (0-1) used for brick loading request selection
1880
+ // query - bool: Whether to allow brick loading requests (true) or just query (false)
1881
+ // colorIndex - int
1882
+ //
1883
+ // Returns:
1884
+ // ivec4: (x_offset, y_offset, z_offset, status) where:
1885
+ // - x_offset, y_offset, z_offset: Brick cache coordinates if found
1886
+ // - status: Resolution level (>=0) if found, or status code:
1887
+ // * -1: Not found at any resolution level
1888
+ // * -2: Empty brick (all values below threshold)
1889
+ // * -3: Constant full brick (all values above threshold)
1890
+ // * -4: Constant value brick (uniform value)
1891
+ // add maxres here
1892
+ ivec4 getBrickLocation(vec3 location, int targetRes, int channel, float rnd, bool query, int colorIndex) {
1893
+
1894
+ // min max for current color
1895
+ vec2 clim = getClim(colorIndex);
1896
+
1897
+ // resolution ranges, TODO: connect this back to color
1898
+ int channelMin = getRes(channel).x;
1899
+ int channelMax = getRes(channel).y;
1900
+
1901
+ // Clamp resolution to channel's available range
1902
+ int currentRes = clamp(targetRes, channelMin, channelMax);
1903
+ currentRes = clamp(currentRes, resGlobal.x, resGlobal.y);
1904
+ int lowestRes = clamp(resGlobal.y, channelMin, channelMax);
1905
+
1906
+ // Determine if this channel should request brick loading.
1907
+ // request the current channel based on probability
1908
+ bool requestChannel = false;
1909
+ if (int(floor(rnd * float(maxChannels))) == colorIndex) {
1910
+ requestChannel = true;
1911
+ }
1912
+
1913
+ // Try progressively lower resolutions until we find data.
1914
+ // loop through resolutions
1915
+ while (currentRes <= lowestRes) {
1916
+
1917
+ // Calculate page table coordinates for this brick
1918
+ uvec3 anchorPoint = getAnchorPoint(currentRes);
1919
+ vec3 brickLocation = getBrickFromNormalized(location, currentRes);
1920
+ uvec3 channelOffset = getChannelOffset(channel);
1921
+ vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;
1922
+
1923
+ // Special handling for resolution 0 (highest detail)
1924
+ if (currentRes == 0) {
1925
+ int zExtent = int(ceil(float(voxelExtents.z) / 32.0));
1926
+ coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;
1927
+ }
1928
+
1929
+ // Query the page table.
1930
+ // get PT entry
1931
+ uint ptEntry = texelFetch(pageTableTex, ivec3(coordinate), 0).r;
1932
+
1933
+ // Check if brick is initialized.
1934
+ // check if the PT entry is initialized
1935
+ uint isInit = (ptEntry >> 30u) & 1u;
1936
+ if (isInit == 0u) {
1937
+ currentRes++;
1938
+ // Request brick loading if needed
1939
+ if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {
1940
+ gRequest = packPTCoordToRGBA8(uvec3(coordinate));
1941
+ }
1942
+ continue;
1943
+ }
1944
+
1945
+ // Extract min/max values from page table entry.
1946
+ // get the min max values of the brick
1947
+ uint umin = ((ptEntry >> 23u) & 0x7Fu);
1948
+ uint umax = ((ptEntry >> 16u) & 0x7Fu);
1949
+ float min = float(int(umin)) / 127.0;
1950
+ float max = float(int(umax)) / 127.0;
1951
+
1952
+ // Check if brick is empty (all values below threshold).
1953
+ // exit early if brick is constant
1954
+ if (float(max) <= clim.x) {
1955
+ return ivec4(0,0,0,-2);
1956
+ // EMPTY
1957
+ } else if (float(min) >= clim.y) {
1958
+ return ivec4(0,0,0,-3); // CONSTANT FULL
1959
+ } else if ((umax - umin) < 2u) {
1960
+ return ivec4(min,0,0,-4); // CONSTANT OTHER VALUE
1961
+ }
1962
+
1963
+ // Check if brick is resident in cache.
1964
+ // return brick cache location if resident
1965
+ // continue to next resolution if not resident
1966
+ uint isResident = (ptEntry >> 31u) & 1u;
1967
+ if (isResident == 0u) {
1968
+ currentRes++;
1969
+ // Request brick loading if needed
1970
+ if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {
1971
+ gRequest = packPTCoordToRGBA8(uvec3(coordinate));
1972
+ }
1973
+ continue;
1974
+ } else {
1975
+ // Extract brick cache coordinates
1976
+ uint xBrickCache = (ptEntry >> 10u) & 0x3Fu;
1977
+ uint yBrickCache = (ptEntry >> 4u) & 0x3Fu;
1978
+ uint zBrickCache = ptEntry & 0xFu;
1979
+ uvec3 brickCacheCoord = uvec3(xBrickCache, yBrickCache, zBrickCache);
1980
+
1981
+ return ivec4(brickCacheCoord, currentRes);
1982
+ }
1983
+ }
1984
+
1985
+ // not resident in any resolution, should be treated as empty
1986
+ return ivec4(0,0,0,-1); // Not found
1987
+ }
1988
+
1989
+ // Request brick loading for a specific location and resolution.
1990
+ // Initiates a request to load a brick from disk into the brick cache.
1991
+ // set the brick request for the specified slot channel
1992
+ //
1993
+ // Parameters:
1994
+ // location - vec3: Normalized world space coordinates (0.0 to 1.0) where the brick is needed
1995
+ // targetRes - int: Target resolution level (0-9, where 0 is highest resolution)
1996
+ // channel - int: Channel index (0-6) for multi-channel datasets
1997
+ // rnd - float: Random value between 0.0 and 1.0 used for load balancing across channels
1998
+ //
1999
+ // Returns:
2000
+ // void: No return value, but sets gRequest output variable if conditions are met
2001
+ void setBrickRequest(vec3 location, int targetRes, int channel, float rnd) {
2002
+ uvec3 anchorPoint = getAnchorPoint(targetRes);
2003
+ vec3 brickLocation = getBrickFromNormalized(location, targetRes);
2004
+ uvec3 channelOffset = getChannelOffset(channel);
2005
+ vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;
2006
+
2007
+ // Special handling for resolution 0
2008
+ if (targetRes == 0) {
2009
+ int zExtent = int(ceil(float(voxelExtents.z) / 32.0));
2010
+ coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;
2011
+ }
2012
+
2013
+ // Pack coordinates and set request
2014
+ if (int(floor(rnd * float(maxChannels))) == channel) {
2015
+ gRequest = packPTCoordToRGBA8(uvec3(coordinate));
2016
+ }
2017
+ }
2018
+
2019
+ // Track brick usage for cache management.
2020
+ // Records which brick in the cache is being accessed for LRU (Least Recently Used) eviction.
2021
+ // set the usage for the specified brick
2022
+ //
2023
+ // Parameters:
2024
+ // brickCacheOffset - ivec3: 3D coordinates of the brick within the brick cache texture
2025
+ // t_hit_min_os - float: Ray entry time in object space (start of ray-volume intersection)
2026
+ // t_hit_max_os - float: Ray exit time in object space (end of ray-volume intersection)
2027
+ // t_os - float: Current sampling position along the ray in object space
2028
+ // rnd - float: Random value between 0.0 and 1.0 used for probabilistic usage tracking
2029
+ //
2030
+ // Returns:
2031
+ // void: No return value, but sets gUsage output variable to track cache access patterns
2032
+ void setUsage(ivec3 brickCacheOffset, float t_hit_min_os, float t_hit_max_os, float t_os, float rnd) {
2033
+ float normalized_t_os = (t_os - t_hit_min_os) / (t_hit_max_os - t_hit_min_os); // Normalize to 0-1
2034
+ if (normalized_t_os <= rnd || gUsage == vec4(0.0, 0.0, 0.0, 0.0)) {
2035
+ gUsage = vec4(vec3(brickCacheOffset) / 255.0, 1.0);
2036
+ }
2037
+ }
2038
+
2039
+ // ========================================
2040
+ // UTILITY FUNCTIONS
2041
+ // ========================================
2042
+
2043
+ // Get maximum component of a 3D vector.
2044
+ // get the max value of a vec3
2045
+ //
2046
+ // Parameters:
2047
+ // v - vec3: Input 3D vector
2048
+ //
2049
+ // Returns:
2050
+ // float: The maximum value among the x, y, and z components
2051
+ float vec3_max(vec3 v) {
2052
+ return max(v.x, max(v.y, v.z));
2053
+ }
2054
+
2055
+ // Get minimum component of a 3D vector.
2056
+ // get the min value of a vec3
2057
+ //
2058
+ // Parameters:
2059
+ // v - vec3: Input 3D vector
2060
+ //
2061
+ // Returns:
2062
+ // float: The minimum value among the x, y, and z components
2063
+ float vec3_min(vec3 v) {
2064
+ return min(v.x, min(v.y, v.z));
2065
+ }
2066
+
2067
+ // Calculate level-of-detail based on distance.
2068
+ // Uses logarithmic scaling to determine appropriate resolution level.
2069
+ // get the LOD based on the distance to the camera
2070
+ //
2071
+ // Parameters:
2072
+ // distance - float: Distance from camera to sampling point
2073
+ // highestRes - int: Highest available resolution level (typically 0)
2074
+ // lowestRes - int: Lowest available resolution level (typically 9)
2075
+ // lodFactor - float: Scaling factor that controls LOD sensitivity
2076
+ //
2077
+ // Returns:
2078
+ // int: Resolution level index (0-9) where 0 is highest resolution
2079
+ int getLOD(float distance, int highestRes, int lowestRes, float lodFactor) {
2080
+ int lod = int(log2(distance * lodFactor));
2081
+ return clamp(lod, highestRes, lowestRes);
2082
+ }
2083
+
2084
+ // Calculate step size for ray marching at a given resolution.
2085
+ // Determines optimal sampling step size based on voxel dimensions and ray direction.
2086
+ // get the voxel step in object space
2087
+ //
2088
+ // Parameters:
2089
+ // res - int: Resolution level index (0-9, where 0 is highest resolution)
2090
+ // osDir - vec3: Ray direction vector in object space (should be normalized)
2091
+ //
2092
+ // Returns:
2093
+ // float: Optimal step size in object space units for stable ray marching
2094
+ float voxelStepOS(int res, vec3 osDir) {
2095
+ vec3 voxelSize = getScale(res) / vec3(voxelExtents);
2096
+ vec3 dt_vec = voxelSize / abs(osDir);
2097
+ return min(dt_vec.x, min(dt_vec.y, dt_vec.z));
2098
+ }
2099
+
2100
+ // ========================================
2101
+ // INTERPOLATION FUNCTIONS
2102
+ // ========================================
2103
+
2104
+ // Linear interpolation between two values.
2105
+ // Performs smooth interpolation between two scalar values.
2106
+ //
2107
+ // Parameters:
2108
+ // v0 - float: First value to interpolate from
2109
+ // v1 - float: Second value to interpolate to
2110
+ // fx - float: Interpolation factor between 0.0 and 1.0
2111
+ //
2112
+ // Returns:
2113
+ // float: Interpolated value between v0 and v1 based on fx
2114
+ float lerp(float v0, float v1, float fx) {
2115
+ return mix(v0, v1, fx); // (1-fx)·v0 + fx·v1
2116
+ }
2117
+
2118
+ // Bilinear interpolation between four values
2119
+ // Performs 2D interpolation using four corner values arranged in a square
2120
+ //
2121
+ // Parameters:
2122
+ // v00 - float: Bottom-left corner value
2123
+ // v10 - float: Bottom-right corner value
2124
+ // v01 - float: Top-left corner value
2125
+ // v11 - float: Top-right corner value
2126
+ // f - vec2: 2D interpolation factors (x, y) between 0.0 and 1.0
2127
+ //
2128
+ // Returns:
2129
+ // float: Interpolated value from the four corner values
2130
+ float bilerp(float v00, float v10, float v01, float v11, vec2 f) {
2131
+ float c0 = mix(v00, v10, f.x); // Interpolate in X on bottom row
2132
+ float c1 = mix(v01, v11, f.x); // Interpolate in X on top row
2133
+ return mix(c0, c1, f.y); // Now interpolate those in Y
2134
+ }
2135
+
2136
+ // Trilinear interpolation between eight values
2137
+ // Performs 3D interpolation using eight corner values arranged in a cube
2138
+ //
2139
+ // Parameters:
2140
+ // v000 - float: Bottom-left-back corner value
2141
+ // v100 - float: Bottom-right-back corner value
2142
+ // v010 - float: Bottom-left-front corner value
2143
+ // v110 - float: Bottom-right-front corner value
2144
+ // v001 - float: Top-left-back corner value
2145
+ // v101 - float: Top-right-back corner value
2146
+ // v011 - float: Top-left-front corner value
2147
+ // v111 - float: Top-right-front corner value
2148
+ // f - vec3: 3D interpolation factors (x, y, z) between 0.0 and 1.0
2149
+ //
2150
+ // Returns:
2151
+ // float: Interpolated value from the eight corner values
2152
+ float trilerp(
2153
+ float v000, float v100, float v010, float v110,
2154
+ float v001, float v101, float v011, float v111,
2155
+ vec3 f) { // f = fract(coord)
2156
+ // Interpolate along X for each of the four bottom-face voxels
2157
+ float c00 = mix(v000, v100, f.x);
2158
+ float c10 = mix(v010, v110, f.x);
2159
+ float c01 = mix(v001, v101, f.x);
2160
+ float c11 = mix(v011, v111, f.x);
2161
+
2162
+ // Interpolate those along Y
2163
+ float c0 = mix(c00, c10, f.y);
2164
+ float c1 = mix(c01, c11, f.y);
2165
+
2166
+ // Final interpolation along Z
2167
+ return mix(c0, c1, f.z);
2168
+ }
2169
+
2170
+ // ========================================
2171
+ // BRICK CACHE SAMPLING
2172
+ // ========================================
2173
+
2174
+ // Sample a value from the brick cache texture.
2175
+ // Converts brick coordinates and voxel position to texture coordinates for sampling.
2176
+ // sample the brick cache based on the brick cache coordinate and the in-brick coordinate
2177
+ //
2178
+ // Parameters:
2179
+ // brickCacheCoord - vec3: 3D coordinates of the brick within the brick cache
2180
+ // voxelInBrick - vec3: 3D coordinates of the voxel within the brick (0-31 in each dimension)
2181
+ //
2182
+ // Returns:
2183
+ // float: Sampled voxel value from the brick cache texture (typically normalized 0.0-1.0)
2184
+ float sampleBrick(vec3 brickCacheCoord, vec3 voxelInBrick) {
2185
+ vec3 brickCacheCoordNormalized = vec3(
2186
+ (float(brickCacheCoord.x) * BRICK_SIZE + float(voxelInBrick.x)) / BRICK_CACHE_SIZE_X,
2187
+ (float(brickCacheCoord.y) * BRICK_SIZE + float(voxelInBrick.y)) / BRICK_CACHE_SIZE_Y,
2188
+ (float(brickCacheCoord.z) * BRICK_SIZE + float(voxelInBrick.z)) / BRICK_CACHE_SIZE_Z
2189
+ );
2190
+ return texture(brickCacheTex, brickCacheCoordNormalized).r;
2191
+ }
2192
+
2193
+ /**
2194
+ * main renderloop
2195
+ */
2196
+ void main(void) {
2197
+
2198
+ // ========================================
2199
+ // INITIALIZATION
2200
+ // ========================================
2201
+
2202
+ // Initialize all render targets (multiple output textures)
2203
+ gRequest = vec4(0,0,0,0); // Brick loading requests
2204
+ gUsage = vec4(0,0,0,0); // Brick usage tracking
2205
+ gColor = vec4(0.0, 0.0, 0.0, 0.0); // Final color output
2206
+
2207
+ // out color sums up our accumulated value before writing it into the gColor buffer
2208
+ vec4 outColor = vec4(0.0, 0.0, 0.0, 0.0); // Accumulated color
2209
+
2210
+ // Generate random number for jittered sampling (reduces artifacts)
2211
+ float rnd = random();
2212
+
2213
+ // Get the lowest available resolution level
2214
+ int lowestDataRes = getLowestRes();
2215
+
2216
+ // ========================================
2217
+ // RAY-VOLUME INTERSECTION
2218
+ // ========================================
2219
+
2220
+ // Normalize the view ray direction
2221
+ vec3 ws_rayDir = normalize(rayDirUnnorm);
2222
+
2223
+ // Calculate intersection with volume bounding box
2224
+ // Returns (entry_time, exit_time) for the ray-box intersection
2225
+ vec2 t_hit = intersect_hit(cameraCorrected, ws_rayDir);
2226
+ if (t_hit.x >= t_hit.y) { discard; } // Ray misses volume entirely
2227
+
2228
+ t_hit.x = max(t_hit.x, 0.0); // Clamp entry to 0 (no negative distances)
2229
+ float t = t_hit.x;
2230
+
2231
+ // Calculate distance from camera for LOD selection
2232
+ float distance = abs((cameraCorrected / boxSize).z + (ws_rayDir / boxSize).z * t );
2233
+
2234
+ // ========================================
2235
+ // COORDINATE SPACE CONVERSION
2236
+ // ========================================
2237
+
2238
+ // Convert from world space to object space (normalized 0-1 coordinates)
2239
+ float ws2os = length(ws_rayDir / boxSize); // Scale factor for conversion
2240
+ float t_hit_min_os = t_hit.x * ws2os; // Entry point in object space
2241
+ float t_hit_max_os = t_hit.y * ws2os; // Exit point in object space
2242
+ float t_os = t_hit_min_os; // Current position in object space
2243
+
2244
+ // Calculate effective LOD factor based on volume size.
2245
+ // voxel edge is the max extent of the volume
2246
+ float voxelEdge = float(max(voxelExtents.x, max(voxelExtents.y, voxelExtents.z)));
2247
+
2248
+ // calculate LOD factor based on the voxel edge
2249
+ float lodFactorEffective = lodFactor * voxelEdge / 256.0;
2250
+
2251
+ // ========================================
2252
+ // RESOLUTION AND SAMPLING SETUP
2253
+ // ========================================
2254
+
2255
+ // Determine target resolution based on distance (LOD)
2256
+ int targetRes = getLOD(t, 0, 9, lodFactorEffective);
2257
+
2258
+ // Set adaptive stepping resolution
2259
+ int stepResAdaptive = renderRes;
2260
+ int stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);
2261
+
2262
+ // Convert ray to object space coordinates
2263
+ vec3 os_rayDir = normalize(ws_rayDir / boxSize);
2264
+ vec3 os_rayOrigin = cameraCorrected / boxSize + vec3(0.5);
2265
+
2266
+ // Calculate step size based on current resolution
2267
+ float dt = voxelStepOS(stepResEffective, os_rayDir);
2268
+
2269
+ // ========================================
2270
+ // SAMPLING POSITION INITIALIZATION
2271
+ // ========================================
2272
+
2273
+ // Convert to normalized sampling coordinates (0-1 range)
2274
+ vec3 p = cameraCorrected + t_hit.x * ws_rayDir;
2275
+ p = p / boxSize + vec3(0.5); // Transform to 0-1 range
2276
+
2277
+ // Calculate step vector in normalized space
2278
+ vec3 dp = (os_rayDir * dt);
2279
+
2280
+ // Apply jittered sampling to reduce artifacts
2281
+ p += dp * (rnd);
2282
+ // Avoid boundary issues
2283
+ p = clamp(p, 0.0 + 0.0000028, 1.0 - 0.0000028);
2284
+
2285
+ // ========================================
2286
+ // RENDERING VARIABLES
2287
+ // ========================================
2288
+
2289
+ // Color accumulation for front-to-back compositing.
2290
+ // color accumulation variables, are calculated per 'slice'
2291
+ vec3 rgbCombo = vec3(0.0);
2292
+ float total = 0.0;
2293
+
2294
+ // For alpha blending.
2295
+ // alpha accumulation variable runs globally
2296
+ float alphaMultiplicator = 1.0;
2297
+
2298
+ // Request tracking (for brick loading).
2299
+ // if we have a request for a brick which not visible in lower
2300
+ // resolutions, we can overwrite it once
2301
+ bool overWrittenRequest = false;
2302
+
2303
+ // Current state tracking
2304
+ vec3 currentTargetResPTCoord = vec3(0,0,0);
2305
+ int currentLOD = targetRes;
2306
+
2307
+ // ========================================
2308
+ // CHANNEL-SPECIFIC CONSTANTS
2309
+ // ========================================
2310
+
2311
+ // Pre-compute channel properties for efficiency.
2312
+ // constants per color channel
2313
+ vec3 [] c_color = vec3[7](getChannelColor(0), getChannelColor(1), getChannelColor(2), getChannelColor(3), getChannelColor(4), getChannelColor(5), getChannelColor(6));
2314
+ float [] c_opacity = float[7](getChannelOpacity(0), getChannelOpacity(1), getChannelOpacity(2), getChannelOpacity(3), getChannelOpacity(4), getChannelOpacity(5), getChannelOpacity(6));
2315
+ // resolution ranges (currently) per color channel
2316
+ // TODO: figure out how to hook it up with frontend
2317
+ 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);
2318
+ 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);
2319
+ vec3 [] res_color = vec3[10](
2320
+ vec3(1.0, 0.0, 0.0),
2321
+ vec3(0.0, 0.0, 1.0),
2322
+ vec3(0.0, 1.0, 0.0),
2323
+ vec3(1.0, 0.0, 1.0),
2324
+ vec3(0.0, 1.0, 1.0),
2325
+ vec3(1.0, 1.0, 0.0),
2326
+ vec3(1.0, 0.5, 0.5),
2327
+ vec3(0.5, 0.5, 1.0),
2328
+ vec3(0.5, 1.0, 0.5),
2329
+ vec3(0.5, 0.5, 0.5)
2330
+ );
2331
+
2332
+ // ========================================
2333
+ // PER-CHANNEL STATE ARRAYS
2334
+ // ========================================
2335
+
2336
+ // Current state for each channel.
2337
+ // current state variables per color channel
2338
+
2339
+ // current resolution
2340
+ int [] c_res_current = int[7](0,0,0,0,0,0,0);
2341
+ // current value
2342
+ float [] c_val_current = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
2343
+ // current brick cache coordinate
2344
+ 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));
2345
+ // current voxel in current resolution
2346
+ 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));
2347
+ // current pagetable coordinate
2348
+ 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));
2349
+ // current render mode -- 0: empty (add 0), 1: constant (add current val), 2: voxel (query new voxel)
2350
+ // upon change of PT we re-query anyways
2351
+ int [] c_renderMode_current = int[7](-1, -1, -1, -1, -1, -1, -1);
2352
+
2353
+ // Adjacent brick caching for interpolation
2354
+ // current pagetable coordinate of the adjacent bricks in X, Y, Z or diagonal direction
2355
+ 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));
2356
+ 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));
2357
+ 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));
2358
+ 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));
2359
+ // corresponding brick coordinates of the adjacent bricks in X, Y, Z or diagonal direction
2360
+ 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));
2361
+ 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));
2362
+ 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));
2363
+ 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));
2364
+
2365
+ // Min/max tracking for MIP/MinIP rendering.
2366
+ // min and max values of the current color
2367
+ // used for minimum/maximum intensity projection
2368
+ float [] c_minVal = float[7](-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0);
2369
+ float [] c_maxVal = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
2370
+
2371
+ // Per-resolution coordinate tracking
2372
+ 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));
2373
+ 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));
2374
+ 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));
2375
+ 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));
2376
+
2377
+ // resolution changed flag
2378
+ bool resolutionChanged = false;
2379
+ // number of repetitions (for debugging purposes)
2380
+ int reps = 0;
2381
+
2382
+ // ========================================
2383
+ // MAIN RAY-MARCHING LOOP
2384
+ // ========================================
2385
+
2386
+ // while we are 'in' the volume.
2387
+ // Continue marching until we exit the volume or reach maximum opacity.
2388
+ while (t_os < t_hit_max_os && t_os >= t_hit_min_os
2389
+ && vec3_max(p) < 1.0 && vec3_min(p) >= 0.0
2390
+ ) {
2391
+
2392
+ // Reset per-sample accumulation.
2393
+ // initialize slice values
2394
+ vec3 rgbCombo = vec3(0.0);
2395
+ float total = 0.0;
2396
+
2397
+ // Update target resolution based on current distance (with jitter).
2398
+ // calculate target resolution based on distance and lod factor
2399
+ targetRes = getLOD(t, 0, 9, lodFactorEffective * (0.999 + 0.002 * rnd));
2400
+
2401
+ // ========================================
2402
+ // RESOLUTION CHANGE HANDLING
2403
+ // ========================================
2404
+
2405
+ // if target resolution changed, update the current resolution and stepsize
2406
+ if (targetRes != currentLOD) {
2407
+ currentLOD = targetRes;
2408
+ stepResAdaptive++;
2409
+ stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);
2410
+
2411
+ // Adjust sampling position for new resolution
2412
+ p -= dp * rnd;
2413
+ dt = voxelStepOS(stepResEffective, os_rayDir);
2414
+ dp = os_rayDir * dt;
2415
+ p += dp * rnd;
2416
+ resolutionChanged = true;
2417
+
2418
+ // Check bounds after resolution change
2419
+ 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) {
2420
+ break;
2421
+ }
2422
+ } else {
2423
+ resolutionChanged = false;
2424
+ }
2425
+
2426
+ // ========================================
2427
+ // UPDATE COORDINATES FOR ALL RESOLUTIONS
2428
+ // ========================================
2429
+
2430
+ // Calculate page table coordinates and voxel positions for all resolution levels
2431
+ for (int r = 0; r < 10; r++) {
2432
+ r_prevPTCoord[r] = r_ptCoord[r];
2433
+ r_prevVoxel[r] = r_voxel[r];
2434
+ r_ptCoord[r] = getBrickFromNormalized(p, r);
2435
+ r_voxel[r] = getVoxelFromNormalized(p, r);
2436
+ }
2437
+
2438
+ // ========================================
2439
+ // RENDER MODE DEFINITIONS
2440
+ // ========================================
2441
+ // 0: empty brick (no data)
2442
+ // 1: constant brick (uniform value)
2443
+ // 2: voxel brick (variable data)
2444
+
2445
+ // initialize the per channel slice values
2446
+ vec3 sliceColor = vec3(0.0);
2447
+ float sliceAlpha = 0.0;
2448
+
2449
+ // ========================================
2450
+ // MULTI-CHANNEL SAMPLING
2451
+ // ========================================
2452
+
2453
+ // Process each channel independently.
2454
+ // iterate over up to 7 channels by color
2455
+ for (int c = 0; c < 7; c++) {
2456
+ // Skip channels with zero opacity.
2457
+ // skip if opacity is 0 or if color is not mapped to a physical slot
2458
+ if (c_opacity[c] <= 0.000001) {
2459
+ continue;
2460
+ } else if (channelMapping[c] == -1) {
2461
+ continue;
2462
+ }
2463
+
2464
+ // physical slot in pagetable
2465
+ int slot = channelMapping[c];
2466
+
2467
+ // keep track of status
2468
+ bool newBrick = false;
2469
+ bool newVoxel = false;
2470
+ // best possible resolution
2471
+ int bestRes = clamp(targetRes, c_res_min[c], c_res_max[c]);
2472
+
2473
+ // Check if we need to load a new brick at a better resolution.
2474
+ // check if any new better resolution could be available, if so, we need to re-query the brick
2475
+ bool betterResChanged = false;
2476
+ for (int r = bestRes; r <= c_res_current[c]; r++ ) {
2477
+ if (r_ptCoord[r] != r_prevPTCoord[r]) {
2478
+ betterResChanged = true;
2479
+ break;
2480
+ }
2481
+ }
2482
+
2483
+ // Determine if we need to load new brick data.
2484
+ // check if we need to re-query the brick / voxel or reuse past 'val'
2485
+ if (r_ptCoord[bestRes] != r_prevPTCoord[bestRes]
2486
+ || c_renderMode_current[c] == -1
2487
+ || resolutionChanged == true
2488
+ || betterResChanged
2489
+ ) {
2490
+ newBrick = true;
2491
+ newVoxel = true;
2492
+ } else if (c_renderMode_current[c] == 2) {
2493
+ newVoxel = true;
2494
+ } else if (c_renderMode_current[c] == 0) {
2495
+ continue; // Skip empty bricks
2496
+ }
2497
+
2498
+ // ========================================
2499
+ // BRICK LOADING AND CACHING
2500
+ // ========================================
2501
+
2502
+ // check if a new brick is available in the best possible resolution
2503
+ if (newBrick) {
2504
+ // Query page table for brick location and status
2505
+ ivec4 brickCacheInfo = getBrickLocation(p, bestRes, slot, rnd, true, c);
2506
+ // check information about the newly queried brick
2507
+ // if the res is not at best res, possibly the same as previous brick
2508
+ if (brickCacheInfo.w == -1 || brickCacheInfo.w == -2) {
2509
+ // Empty brick - no data available.
2510
+ // we can skip the rest of the loop
2511
+ c_val_current[c] = 0.0;
2512
+ c_renderMode_current[c] = 0;
2513
+ c_minVal[c] = 0.0;
2514
+ continue;
2515
+ } else if (brickCacheInfo.w == -3) {
2516
+ // Solid brick - constant maximum value.
2517
+ // we set the value and do not need to query a voxel
2518
+ c_val_current[c] = 1.0;
2519
+ c_renderMode_current[c] = 1;
2520
+ c_maxVal[c] = 1.0;
2521
+ newVoxel = false;
2522
+ } else if (brickCacheInfo.w == -4) {
2523
+ // Constant brick - uniform value.
2524
+ // static value -- we set the value and do not need to query a voxel
2525
+ float val = float(brickCacheInfo.x);
2526
+ c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));
2527
+ c_renderMode_current[c] = 1;
2528
+ newVoxel = false;
2529
+ } else if (brickCacheInfo.w >= 0) {
2530
+ // Voxel brick - variable data, load from cache.
2531
+ // new brick -- we set the coordinate and resolution and need to query a voxel
2532
+ c_res_current[c] = brickCacheInfo.w;
2533
+ c_ptCoord_current[c] = r_ptCoord[c_res_current[c]];
2534
+ c_brickCacheCoord_current[c] = vec3(brickCacheInfo.xyz);
2535
+ c_renderMode_current[c] = 2;
2536
+ newVoxel = true;
2537
+
2538
+ // Track brick usage for cache management.
2539
+ // we set the usage of the brick based on the channel and the relative distance into the cube
2540
+ if (int(floor(rnd * float(maxChannels))) == c) {
2541
+ setUsage(brickCacheInfo.xyz, t_hit_min_os, t_hit_max_os, t_os, rnd);
2542
+ }
2543
+ }
2544
+ }
2545
+
2546
+ // ========================================
2547
+ // VOXEL SAMPLING WITH INTERPOLATION
2548
+ // ========================================
2549
+
2550
+ // we need to query a new voxel e.g. sample the brick cache
2551
+ if (newVoxel) {
2552
+ c_voxel_current[c] = r_voxel[c_res_current[c]];
2553
+
2554
+ // we clamp the coordinate to be inside the brick and sample the volume
2555
+ reps++;
2556
+
2557
+ // Calculate position within the brick (0-31 range)
2558
+ vec3 voxelInBrick = mod(c_voxel_current[c], 32.0);
2559
+ vec3 clampedVoxelInBrick = clamp(voxelInBrick, 0.5, 31.5);
2560
+ // Sample the brick cache texture
2561
+ float val = sampleBrick(c_brickCacheCoord_current[c].xyz, clampedVoxelInBrick);
2562
+
2563
+ // ========================================
2564
+ // HIGH-QUALITY INTERPOLATION (renderRes == 0)
2565
+ // ========================================
2566
+
2567
+ // interblock interpolation
2568
+ if (renderRes == 0) {
2569
+ // calculate what axis we need to interpolate
2570
+
2571
+ // Check if we're near brick boundaries (need interpolation)
2572
+ bvec3 clampedMin = lessThan(voxelInBrick, clampedVoxelInBrick);
2573
+ bvec3 clampedMax = greaterThan(voxelInBrick, clampedVoxelInBrick);
2574
+ bvec3 clamped = bvec3(clampedMin.x || clampedMax.x, clampedMin.y || clampedMax.y, clampedMin.z || clampedMax.z);
2575
+ vec3 diff = voxelInBrick - clampedVoxelInBrick;
2576
+
2577
+ if (any(clampedMin) || any(clampedMax)) {
2578
+ int boundaryAxes = int(clamped.x) + int(clamped.y) + int(clamped.z);
2579
+ float f = 0.0;
2580
+
2581
+ if (boundaryAxes == 1) {
2582
+ // Linear interpolation across one boundary
2583
+ vec3 otherGlobalVoxelPos = vec3(0,0,0);
2584
+ vec3 otherP = vec3(0,0,0);
2585
+ float otherVoxelVal = 0.0;
2586
+
2587
+ // Determine which axis we're interpolating across
2588
+ if (clampedMin.x) {
2589
+ otherGlobalVoxelPos = c_voxel_current[c] - vec3(1.0, 0.0, 0.0);
2590
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2591
+ f = abs(diff.x);
2592
+ } else if (clampedMax.x) {
2593
+ otherGlobalVoxelPos = c_voxel_current[c] + vec3(1.0, 0.0, 0.0);
2594
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2595
+ f = abs(diff.x);
2596
+ } else if (clampedMin.y) {
2597
+ otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 1.0, 0.0);
2598
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2599
+ f = abs(diff.y);
2600
+ } else if (clampedMax.y) {
2601
+ otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 1.0, 0.0);
2602
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2603
+ f = abs(diff.y);
2604
+ } else if (clampedMin.z) {
2605
+ otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 0.0, 1.0);
2606
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2607
+ f = abs(diff.z);
2608
+ } else if (clampedMax.z) {
2609
+ otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 0.0, 1.0);
2610
+ otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2611
+ f = abs(diff.z);
2612
+ }
2613
+
2614
+ // Sample the neighboring voxel
2615
+ vec3 otherPTcoord = getBrickFromNormalized(otherP, c_res_current[c]);
2616
+ otherPTcoord = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2617
+ vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0);
2618
+ otherVoxelInBrick -= diff;
2619
+
2620
+ // Check if neighbor is outside volume bounds
2621
+ 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) {
2622
+ otherVoxelVal = val;
2623
+ } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) {
2624
+ // Use cached adjacent brick
2625
+ otherVoxelVal = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick);
2626
+ } else {
2627
+ // Load new adjacent brick
2628
+ ivec4 otherBrickCacheInfo = ivec4(-1);
2629
+ if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz) {
2630
+ otherBrickCacheInfo = ivec4(c_brick_XYZ_adjacent[c]);
2631
+ } else {
2632
+ otherBrickCacheInfo = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c);
2633
+ }
2634
+ if (otherBrickCacheInfo.w == -1 || otherBrickCacheInfo.w == -2) {
2635
+ otherVoxelVal = val;
2636
+ } else if (otherBrickCacheInfo.w == -3) {
2637
+ otherVoxelVal = 1.0;
2638
+ } else if (otherBrickCacheInfo.w == -4) {
2639
+ otherVoxelVal = float(otherBrickCacheInfo.x);
2640
+ } else {
2641
+ // TODO: we do not recalculate the voxelInBrick based on the resolution
2642
+ otherVoxelVal = sampleBrick(vec3(otherBrickCacheInfo.xyz), otherVoxelInBrick);
2643
+ }
2644
+ c_PT_XYZ_adjacent[c] = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]);
2645
+ c_brick_XYZ_adjacent[c] = vec4(otherBrickCacheInfo);
2646
+ }
2647
+
2648
+ // Perform linear interpolation
2649
+ float originalVal = val;
2650
+ val = lerp(originalVal, otherVoxelVal, f);
2651
+ } else if (boundaryAxes == 2) {
2652
+ // Bilinear interpolation across two boundaries
2653
+ vec3 offA = vec3(0.0);
2654
+ vec3 offB = vec3(0.0);
2655
+ vec2 f = vec2(0.0);
2656
+
2657
+ // Determine which two axes we're interpolating across
2658
+ if (clamped.x && clamped.y) {
2659
+ offA.x = clampedMin.x ? -1.0 : 1.0;
2660
+ offB.y = clampedMin.y ? -1.0 : 1.0;
2661
+ f = vec2(abs(diff.x), abs(diff.y));
2662
+ } else if (clamped.x && clamped.z) {
2663
+ offA.x = clampedMin.x ? -1.0 : 1.0;
2664
+ offB.z = clampedMin.z ? -1.0 : 1.0;
2665
+ f = vec2(abs(diff.x), abs(diff.z));
2666
+ } else if (clamped.y && clamped.z) {
2667
+ offA.y = clampedMin.y ? -1.0 : 1.0;
2668
+ offB.z = clampedMin.z ? -1.0 : 1.0;
2669
+ f = vec2(abs(diff.y), abs(diff.z));
2670
+ }
2671
+
2672
+ // Macro for sampling at offset positions
2673
+ #define SAMPLE_AT_OFFSET(OFF, DEST) { vec3 otherGlobalVoxelPos = c_voxel_current[c] + (OFF); vec3 otherP = getNormalizedFromVoxel( otherGlobalVoxelPos, c_res_current[c]); if ( any(lessThan(otherP, vec3(0.0))) || any(greaterThanEqual(otherP, vec3(1.0))) ) { DEST = val; } else { vec3 otherPTcoord = getBrickFromNormalized( otherP, c_res_current[c]); vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0) - diff; bool matched = false; if (otherPTcoord == c_PT_X_adjacent[c].xyz && c_brick_X_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_X_adjacent[c].xyz, otherVoxelInBrick); matched = true; } else if (otherPTcoord == c_PT_Y_adjacent[c].xyz && c_brick_Y_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_Y_adjacent[c].xyz, otherVoxelInBrick); matched = true; } else if (otherPTcoord == c_PT_Z_adjacent[c].xyz && c_brick_Z_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_Z_adjacent[c].xyz, otherVoxelInBrick); matched = true; } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick); matched = true; } ivec4 info = ivec4(-1); if (otherPTcoord == c_PT_X_adjacent[c].xyz) { info = ivec4(c_brick_X_adjacent[c]); } else if (otherPTcoord == c_PT_Y_adjacent[c].xyz) { info = ivec4(c_brick_Y_adjacent[c]); } else if (otherPTcoord == c_PT_Z_adjacent[c].xyz) { info = ivec4(c_brick_Z_adjacent[c]); } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz) { info = ivec4(c_brick_XYZ_adjacent[c]); } else { info = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c); } if (!matched) { if (info.w == -1 || info.w == -2) { DEST = val; } else if (info.w == -3) { DEST = 1.0; } else if (info.w == -4) { DEST = float(info.x); } else { DEST = sampleBrick(vec3(info.xyz), otherVoxelInBrick); } if (abs((OFF).x) > 0.5 && abs((OFF).y) < 0.5 && abs((OFF).z) < 0.5) { c_PT_X_adjacent[c] = otherPTcoord; c_brick_X_adjacent[c] = vec4(info); } else if (abs((OFF).y) > 0.5 && abs((OFF).x) < 0.5 && abs((OFF).z) < 0.5) { c_PT_Y_adjacent[c] = otherPTcoord; c_brick_Y_adjacent[c] = vec4(info); } else if (abs((OFF).z) > 0.5 && abs((OFF).x) < 0.5 && abs((OFF).y) < 0.5) { c_PT_Z_adjacent[c] = otherPTcoord; c_brick_Z_adjacent[c] = vec4(info); } else { c_PT_XYZ_adjacent[c] = otherPTcoord; c_brick_XYZ_adjacent[c] = vec4(info); } } } }
2674
+
2675
+ // Sample the four corners for bilinear interpolation
2676
+ float v00 = val;
2677
+ float v10; float v01; float v11;
2678
+ SAMPLE_AT_OFFSET(offA, v10);
2679
+ SAMPLE_AT_OFFSET(offB, v01);
2680
+ SAMPLE_AT_OFFSET(offA + offB, v11);
2681
+
2682
+ val = bilerp(v00, v10, v01, v11, f);
2683
+
2684
+ #undef SAMPLE_AT_OFFSET
2685
+
2686
+ } else if (boundaryAxes == 3) {
2687
+ // Trilinear interpolation across all three boundaries
2688
+
2689
+ vec3 offA = vec3(0.0);
2690
+ vec3 offB = vec3(0.0);
2691
+ vec3 offC = vec3(0.0);
2692
+ vec3 f = vec3(0.0);
2693
+
2694
+ offA.x = clampedMin.x ? -1.0 : 1.0;
2695
+ offB.y = clampedMin.y ? -1.0 : 1.0;
2696
+ offC.z = clampedMin.z ? -1.0 : 1.0;
2697
+
2698
+ f = vec3(abs(diff.x), abs(diff.y), abs(diff.z));
2699
+
2700
+ // Macro for sampling at offset positions
2701
+ #define SAMPLE_AT_OFFSET(OFF, DEST) { vec3 otherGlobalVoxelPos = c_voxel_current[c] + (OFF); vec3 otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]); if (any(lessThan(otherP, vec3(0.0))) || any(greaterThanEqual(otherP, vec3(1.0)))) { DEST = val; } else { vec3 otherPTcoord = getBrickFromNormalized(otherP, c_res_current[c]); vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0) - diff; if (otherPTcoord == c_PT_X_adjacent[c] && c_brick_X_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_X_adjacent[c].xyz, otherVoxelInBrick); } else if (otherPTcoord == c_PT_Y_adjacent[c].xyz && c_brick_Y_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_Y_adjacent[c].xyz, otherVoxelInBrick); } else if (otherPTcoord == c_PT_Z_adjacent[c].xyz && c_brick_Z_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_Z_adjacent[c].xyz, otherVoxelInBrick); } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) { DEST = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick); } else { ivec4 otherBrickCacheInfo = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c); vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0) - diff; if (otherBrickCacheInfo.w == -1 || otherBrickCacheInfo.w == -2) { DEST = val; } else if (otherBrickCacheInfo.w == -3) { DEST = 1.0; } else if (otherBrickCacheInfo.w == -4) { DEST = float(otherBrickCacheInfo.x); } else { DEST = sampleBrick(vec3(otherBrickCacheInfo.xyz), otherVoxelInBrick); } } } }
2702
+
2703
+ // Sample all eight corners for trilinear interpolation
2704
+ float v000 = val;
2705
+ float v100; float v010; float v001; float v110; float v101; float v011; float v111;
2706
+ SAMPLE_AT_OFFSET(offA, v100);
2707
+ SAMPLE_AT_OFFSET(offB, v010);
2708
+ SAMPLE_AT_OFFSET(offC, v001);
2709
+ SAMPLE_AT_OFFSET(offA + offB, v110);
2710
+ SAMPLE_AT_OFFSET(offA + offC, v101);
2711
+ SAMPLE_AT_OFFSET(offB + offC, v011);
2712
+ SAMPLE_AT_OFFSET(offA + offB + offC, v111);
2713
+
2714
+ val = trilerp(v000, v100, v010, v001, v110, v101, v011, v111, f);
2715
+
2716
+ #undef SAMPLE_AT_OFFSET
2717
+
2718
+ }
2719
+
2720
+ } else {
2721
+ // No boundary interpolation needed - clear adjacent brick cache.
2722
+ // no adjacent bricks -> reset the adjacent trackers
2723
+ c_PT_X_adjacent[c] = c_PT_Y_adjacent[c] = c_PT_Z_adjacent[c] = c_PT_XYZ_adjacent[c] = vec3(-1.0);
2724
+ c_brick_X_adjacent[c] = c_brick_Y_adjacent[c] = c_brick_Z_adjacent[c] = c_brick_XYZ_adjacent[c] = vec4(-1.0);
2725
+ }
2726
+ }
2727
+
2728
+ // ========================================
2729
+ // VALUE NORMALIZATION AND TRACKING
2730
+ // ========================================
2731
+
2732
+ // Normalize value to 0-1 range using channel-specific contrast limits.
2733
+ // we normalize the (accumulated) value to the range of the color channel
2734
+ c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));
2735
+
2736
+ // Track min/max values for MIP/MinIP rendering.
2737
+ // update the min and max values for the min/max projection
2738
+ if (c_minVal[c] == -1.0) {
2739
+ c_minVal[c] = c_val_current[c];
2740
+ } else {
2741
+ c_minVal[c] = min(c_minVal[c], c_val_current[c]);
2742
+ }
2743
+ c_maxVal[c] = max(c_maxVal[c], c_val_current[c]);
2744
+
2745
+ }
2746
+
2747
+ // ========================================
2748
+ // BRICK REQUEST GENERATION
2749
+ // ========================================
2750
+
2751
+ // Request higher resolution bricks if we're using lower resolution than optimal.
2752
+ // potentially overwrite brick request
2753
+ if (!overWrittenRequest
2754
+ && c_res_current[c] != bestRes
2755
+ && c_val_current[c] > 0.0
2756
+ && c_renderMode_current[c] == 2
2757
+ && int(floor(rnd * float(maxChannels))) == c) {
2758
+ setBrickRequest(p, bestRes, slot, rnd);
2759
+ overWrittenRequest = true;
2760
+ }
2761
+
2762
+ // ========================================
2763
+ // CHANNEL COMPOSITING
2764
+ // ========================================
2765
+
2766
+ // Accumulate this channel's contribution.
2767
+ // sum up the values onto the slice values
2768
+ total += c_val_current[c];
2769
+ if (u_renderstyle == 3) {
2770
+ rgbCombo += c_val_current[c] * res_color[targetRes];
2771
+ } else if (u_renderstyle == 4) {
2772
+ rgbCombo += c_val_current[c] * res_color[c_res_current[c]];
2773
+ } else {
2774
+ rgbCombo += c_val_current[c] * c_color[c];
2775
+ }
2776
+
2777
+ }
2778
+
2779
+ // ========================================
2780
+ // FRONT-TO-BACK COMPOSITING
2781
+ // ========================================
2782
+
2783
+ // Clamp total intensity and calculate alpha.
2784
+ // add the calculated slice to the total color
2785
+ total = clamp(total, 0.0, 1.0);
2786
+ sliceAlpha = total * opacity * dt * 32.0; // Scale by step size and brick size
2787
+ sliceColor = rgbCombo;
2788
+
2789
+ // Front-to-back alpha blending
2790
+ outColor.rgb += sliceAlpha * alphaMultiplicator * sliceColor;
2791
+ outColor.a += sliceAlpha * alphaMultiplicator;
2792
+ alphaMultiplicator *= (1.0 - sliceAlpha);
2793
+
2794
+ // Early termination for opaque regions (standard rendering only).
2795
+ // check if we can exit early
2796
+ if (outColor.a > 0.99 && u_renderstyle == 0) { break; }
2797
+
2798
+ // ========================================
2799
+ // ADVANCE RAY POSITION
2800
+ // ========================================
2801
+
2802
+ // Move to next sample position
2803
+ t += dt;
2804
+ p += dp;
2805
+ t_os += dt;
2806
+ }
2807
+
2808
+ // ========================================
2809
+ // RENDERING STYLE POST-PROCESSING
2810
+ // ========================================
2811
+
2812
+ if (u_renderstyle == 1) {
2813
+ // Minimum Intensity Projection (MinIP)
2814
+ // Shows the minimum value encountered along each ray
2815
+ outColor = vec4(0.0);
2816
+ for (int c = 0; c < 7; c++) {
2817
+ if (c_color[c] != vec3(0.0, 0.0, 0.0)) {
2818
+ outColor.rgb += c_minVal[c] * c_color[c];
2819
+ outColor.a += c_minVal[c];
2820
+ }
2821
+ }
2822
+ } else if (u_renderstyle == 0) {
2823
+ // Maximum Intensity Projection (MIP)
2824
+ // Shows the maximum value encountered along each ray
2825
+ outColor = vec4(0.0);
2826
+ for (int c = 0; c < 7; c++) {
2827
+ if (c_color[c] != vec3(0.0, 0.0, 0.0)) {
2828
+ outColor.rgb += c_maxVal[c] * c_color[c];
2829
+ }
2830
+ }
2831
+ outColor.a = 1.0;
2832
+ }
2833
+
2834
+ // ========================================
2835
+ // FINAL OUTPUT
2836
+ // ========================================
2837
+
2838
+ // Convert from linear to sRGB color space and set all render targets
2839
+ gColor = vec4(linear_to_srgb(outColor.r),
2840
+ linear_to_srgb(outColor.g),
2841
+ linear_to_srgb(outColor.b),
2842
+ outColor.a);
2843
+
2844
+ }
2845
+ `;
2846
+ const VolumeShader = {
2847
+ uniforms: {
2848
+ u_size: { value: new Vector3(1, 1, 1) },
2849
+ clim0: { value: new Vector2(0.2, 0.8) },
2850
+ clim1: { value: new Vector2(0.2, 0.8) },
2851
+ clim2: { value: new Vector2(0.2, 0.8) },
2852
+ clim3: { value: new Vector2(0.2, 0.8) },
2853
+ clim4: { value: new Vector2(0.2, 0.8) },
2854
+ clim5: { value: new Vector2(0.2, 0.8) },
2855
+ clim6: { value: new Vector2(0.2, 0.8) },
2856
+ clim7: { value: new Vector2(0.2, 0.8) },
2857
+ xClip: { value: new Vector2(0, 1e6) },
2858
+ yClip: { value: new Vector2(0, 1e6) },
2859
+ zClip: { value: new Vector2(0, 1e6) },
2860
+ u_window_size: { value: new Vector2(1, 1) },
2861
+ u_vol_scale: { value: new Vector3(1, 1, 1) },
2862
+ // Set renderstyle to 3 or 4 for debugging modes
2863
+ u_renderstyle: { value: 2 },
2864
+ brickCacheTex: { type: "sampler3D", value: null },
2865
+ pageTableTex: { type: "usampler3D", value: null },
2866
+ color0: { value: new Vector4(0, 0, 0) },
2867
+ color1: { value: new Vector4(0, 0, 0) },
2868
+ color2: { value: new Vector4(0, 0, 0) },
2869
+ color3: { value: new Vector4(0, 0, 0) },
2870
+ color4: { value: new Vector4(0, 0, 0) },
2871
+ color5: { value: new Vector4(0, 0, 0) },
2872
+ color6: { value: new Vector4(0, 0, 0) },
2873
+ channelMapping: {
2874
+ value: [-1, -1, -1, -1, -1, -1, -1]
2875
+ },
2876
+ resGlobal: { value: new Vector2(0, 9) },
2877
+ res0: { value: new Vector2(0, 9) },
2878
+ res1: { value: new Vector2(0, 9) },
2879
+ res2: { value: new Vector2(0, 9) },
2880
+ res3: { value: new Vector2(0, 9) },
2881
+ res4: { value: new Vector2(0, 9) },
2882
+ res5: { value: new Vector2(0, 9) },
2883
+ res6: { value: new Vector2(0, 9) },
2884
+ maxChannels: { value: 0 },
2885
+ lodFactor: { value: 1 },
2886
+ near: { value: 0.1 },
2887
+ far: { value: 1e4 },
2888
+ opacity: { value: 1 },
2889
+ volumeCount: { value: 0 },
2890
+ boxSize: { value: new Vector3(1, 1, 1) },
2891
+ renderRes: { value: 1e3 },
2892
+ voxelExtents: { value: new Vector3(1, 1, 1) },
2893
+ anchor0: { value: new Vector3(0, 0, 0) },
2894
+ anchor1: { value: new Vector3(0, 0, 0) },
2895
+ anchor2: { value: new Vector3(0, 0, 0) },
2896
+ anchor3: { value: new Vector3(0, 0, 0) },
2897
+ anchor4: { value: new Vector3(0, 0, 0) },
2898
+ anchor5: { value: new Vector3(0, 0, 0) },
2899
+ anchor6: { value: new Vector3(0, 0, 0) },
2900
+ anchor7: { value: new Vector3(0, 0, 0) },
2901
+ anchor8: { value: new Vector3(0, 0, 0) },
2902
+ anchor9: { value: new Vector3(0, 0, 0) },
2903
+ scale0: { value: new Vector3(1, 1, 1) },
2904
+ scale1: { value: new Vector3(2, 2, 2) },
2905
+ scale2: { value: new Vector3(4, 4, 4) },
2906
+ scale3: { value: new Vector3(8, 8, 8) },
2907
+ scale4: { value: new Vector3(16, 16, 16) },
2908
+ scale5: { value: new Vector3(32, 32, 32) },
2909
+ scale6: { value: new Vector3(64, 64, 64) },
2910
+ scale7: { value: new Vector3(128, 128, 128) },
2911
+ scale8: { value: new Vector3(256, 256, 256) },
2912
+ scale9: { value: new Vector3(512, 512, 512) }
2913
+ },
2914
+ vertexShader: volumeVertexShader,
2915
+ fragmentShader: volumeFragmentShader
2916
+ };
2917
+ function logWithColor$1(message) {
2918
+ if (atLeastLogLevel(LogLevel.DEBUG)) {
2919
+ console.warn(`%cRM: ${message}`, "background: orange; color: white; padding: 2px; border-radius: 3px;");
2920
+ }
2921
+ }
2922
+ function normalizeValue(value, minMax) {
2923
+ return value / minMax[1];
2924
+ }
2925
+ class VolumeRenderManager {
2926
+ constructor() {
2927
+ logWithColor$1("Initializing VolumeRenderManager");
2928
+ this.uniforms = null;
2929
+ this.shader = null;
2930
+ this.meshScale = [1, 1, 1];
2931
+ this.geometrySize = [1, 1, 1];
2932
+ this.boxSize = [1, 1, 1];
2933
+ this.zarrInit = false;
2934
+ this.channelsVisible = [];
2935
+ this.channelTargetC = [];
2936
+ this.zarrStoreNumResolutions = null;
2937
+ this.channelMaxResolutionIndex = [];
2938
+ this.colors = [];
2939
+ this.contrastLimits = [];
2940
+ this.layerTransparency = 1;
2941
+ this.xSlice = new Vector2(-1, 1e5);
2942
+ this.ySlice = new Vector2(-1, 1e5);
2943
+ this.zSlice = new Vector2(-1, 1e5);
2944
+ this.originalScale = [1, 1, 1];
2945
+ this.physicalDimensions = [1, 1, 1];
2946
+ this.maxResolution = [1, 1, 1];
2947
+ this.maxRange = 255;
2948
+ this.maxRangeSet = false;
2949
+ this.initializeShader();
2950
+ }
2951
+ /**
2952
+ * Initialize shader and uniform objects
2953
+ */
2954
+ initializeShader() {
2955
+ logWithColor$1("Initializing shader");
2956
+ this.shader = VolumeShader;
2957
+ this.uniforms = UniformsUtils.clone(this.shader.uniforms);
2958
+ }
2959
+ /**
2960
+ * Extract rendering settings from coordination props
2961
+ * @param {Object} props - Component props
2962
+ * @returns {Object} Extracted rendering settings
2963
+ */
2964
+ extractRenderingSettingsFromProps(props) {
2965
+ const { images = {}, imageLayerScopes = [], imageLayerCoordination = [{}], imageChannelScopesByLayer = {}, imageChannelCoordination = [{}], spatialRenderingMode } = props;
2966
+ const layerScope = imageLayerScopes[0];
2967
+ if (!layerScope) {
2968
+ logWithColor$1("Extracting rendering settings from props - no layer scope");
2969
+ return {
2970
+ valid: false
2971
+ };
2972
+ }
2973
+ const channelScopes = imageChannelScopesByLayer[layerScope];
2974
+ const layerCoordination = imageLayerCoordination[0][layerScope];
2975
+ const channelCoordination = imageChannelCoordination[0][layerScope];
2976
+ const data = images[layerScope]?.image?.instance?.getData();
2977
+ if (!data) {
2978
+ logWithColor$1("Extracting rendering settings from props - no image data");
2979
+ return {
2980
+ valid: false
2981
+ };
2982
+ }
2983
+ if (!channelCoordination[channelScopes?.[0]][CoordinationType.SPATIAL_CHANNEL_WINDOW]) {
2984
+ logWithColor$1("Extracting rendering settings from props - no channel window set");
2985
+ return {
2986
+ valid: false
2987
+ };
2988
+ }
2989
+ const imageWrapperInstance = images[layerScope].image.instance;
2990
+ const is3dMode = spatialRenderingMode === "3D";
2991
+ const isRgb = layerCoordination[CoordinationType.PHOTOMETRIC_INTERPRETATION] === "RGB";
2992
+ const visible = layerCoordination[CoordinationType.SPATIAL_LAYER_VISIBLE];
2993
+ const layerTransparency = layerCoordination[CoordinationType.SPATIAL_LAYER_OPACITY];
2994
+ const colors = isRgb ? [
2995
+ [255, 0, 0],
2996
+ [0, 255, 0],
2997
+ [0, 0, 255]
2998
+ ] : channelScopes.map((cScope) => channelCoordination[cScope][CoordinationType.SPATIAL_CHANNEL_COLOR]);
2999
+ const contrastLimits = isRgb ? [
3000
+ [0, 255],
3001
+ [0, 255],
3002
+ [0, 255]
3003
+ ] : channelScopes.map((cScope) => channelCoordination[cScope][CoordinationType.SPATIAL_CHANNEL_WINDOW] || [0, 255]);
3004
+ if (!this.maxRangeSet) {
3005
+ this.maxRange = Math.max(...contrastLimits.map((limit) => limit[1]));
3006
+ this.maxRangeSet = true;
3007
+ }
3008
+ const channelsVisible = isRgb ? [
3009
+ visible && true,
3010
+ visible && true,
3011
+ visible && true
3012
+ ] : channelScopes.map((cScope) => visible && channelCoordination[cScope][CoordinationType.SPATIAL_CHANNEL_VISIBLE]);
3013
+ const channelTargetC = isRgb ? [
3014
+ visible && true,
3015
+ visible && true,
3016
+ visible && true
3017
+ ] : channelScopes.map((cScope) => visible && imageWrapperInstance.getChannelIndex(channelCoordination[cScope][CoordinationType.SPATIAL_TARGET_C]));
3018
+ const channelMaxResolutionIndex = isRgb ? [
3019
+ visible && null,
3020
+ visible && null,
3021
+ visible && null
3022
+ ] : channelScopes.map((cScope) => channelCoordination[cScope][CoordinationType.SPATIAL_MAX_RESOLUTION]);
3023
+ let xSlice = layerCoordination[CoordinationType.SPATIAL_SLICE_X];
3024
+ let ySlice = layerCoordination[CoordinationType.SPATIAL_SLICE_Y];
3025
+ let zSlice = layerCoordination[CoordinationType.SPATIAL_SLICE_Z];
3026
+ const lodFactor = layerCoordination[CoordinationType.SPATIAL_LOD_FACTOR] ?? 1;
3027
+ xSlice = xSlice !== null ? xSlice : new Vector2(-1, 1e5);
3028
+ ySlice = ySlice !== null ? ySlice : new Vector2(-1, 1e5);
3029
+ zSlice = zSlice !== null ? zSlice : new Vector2(-1, 1e5);
3030
+ const allChannels = images[layerScope].image.loaders[0].channels;
3031
+ logWithColor$1("Extracting rendering settings from props - success");
3032
+ return {
3033
+ valid: true,
3034
+ channelsVisible,
3035
+ allChannels,
3036
+ channelTargetC,
3037
+ channelMaxResolutionIndex,
3038
+ data,
3039
+ colors,
3040
+ contrastLimits,
3041
+ is3dMode,
3042
+ layerTransparency,
3043
+ xSlice,
3044
+ ySlice,
3045
+ zSlice,
3046
+ lodFactor
3047
+ };
3048
+ }
3049
+ /**
3050
+ * Update the render settings from the given props
3051
+ * @param {Object} props - Component props
3052
+ * @returns {boolean} True if settings were successfully updated
3053
+ */
3054
+ updateFromProps(props) {
3055
+ const settings = this.extractRenderingSettingsFromProps(props);
3056
+ if (!settings.valid) {
3057
+ logWithColor$1("Updating from props - invalid settings");
3058
+ return false;
3059
+ }
3060
+ this.channelsVisible = settings.channelsVisible;
3061
+ this.channelTargetC = settings.channelTargetC;
3062
+ this.channelMaxResolutionIndex = settings.channelMaxResolutionIndex;
3063
+ this.colors = settings.colors;
3064
+ this.contrastLimits = settings.contrastLimits;
3065
+ this.renderingMode = settings.renderingMode;
3066
+ this.layerTransparency = settings.layerTransparency;
3067
+ this.xSlice = settings.xSlice;
3068
+ this.ySlice = settings.ySlice;
3069
+ this.zSlice = settings.zSlice;
3070
+ this.uniforms.lodFactor.value = settings.lodFactor;
3071
+ logWithColor$1(`lodFactor ${settings.lodFactor}`);
3072
+ this.shader.uniforms.lodFactor.value = settings.lodFactor;
3073
+ logWithColor$1("Updating from props - success");
3074
+ return true;
3075
+ }
3076
+ /**
3077
+ * Update the rendering based on the current volume data from the data manager
3078
+ * @param {object} volumeDataManagerProps - Params derived from the volume data manager
3079
+ * @returns {Object|null} Updated rendering settings or null if rendering is not possible
3080
+ */
3081
+ updateRendering({ zarrStoreShapes, originalScaleXYZ, physicalDimensionsXYZ, maxResolutionXYZ, boxDimensionsXYZ, normalizedScaleXYZ, bcTHREE, ptTHREE }) {
3082
+ logWithColor$1("Updating rendering");
3083
+ this.channelTargetC.findIndex((channel, idx) => this.channelsVisible[idx]);
3084
+ if (!Array.isArray(zarrStoreShapes) || zarrStoreShapes.length === 0) {
3085
+ return null;
3086
+ }
3087
+ const shape = zarrStoreShapes[0];
3088
+ const dimensions = {
3089
+ xLength: shape[4] || 1,
3090
+ yLength: shape[3] || 1,
3091
+ zLength: shape[2] || 1
3092
+ };
3093
+ const texturesList = [];
3094
+ const colorsSave = [];
3095
+ const contrastLimitsList = [];
3096
+ this.channelTargetC.forEach((channel, id) => {
3097
+ if (this.channelsVisible[id] || true) {
3098
+ const max = this.maxRange ? this.maxRange : 255;
3099
+ const minMax = [0, max];
3100
+ colorsSave.push([
3101
+ this.colors[id][0] / 255,
3102
+ this.colors[id][1] / 255,
3103
+ this.colors[id][2] / 255,
3104
+ this.channelsVisible[id] ? 1 : 0
3105
+ ]);
3106
+ log.debug("colorsSave", colorsSave);
3107
+ if (this.contrastLimits[id][0] === 0 && this.contrastLimits[id][1] === 255) {
3108
+ contrastLimitsList.push([
3109
+ normalizeValue(minMax[0], minMax),
3110
+ normalizeValue(minMax[1], minMax)
3111
+ ]);
3112
+ } else {
3113
+ contrastLimitsList.push([
3114
+ normalizeValue(this.contrastLimits[id][0], minMax),
3115
+ normalizeValue(this.contrastLimits[id][1], minMax)
3116
+ ]);
3117
+ }
3118
+ }
3119
+ });
3120
+ if (!this.zarrInit) {
3121
+ this.originalScale = originalScaleXYZ;
3122
+ this.physicalDimensions = physicalDimensionsXYZ;
3123
+ this.maxResolution = maxResolutionXYZ;
3124
+ const scaledResolution = boxDimensionsXYZ;
3125
+ this.normalizedScale = normalizedScaleXYZ;
3126
+ this.meshScale = [
3127
+ this.originalScale[0] / this.originalScale[0],
3128
+ this.originalScale[1] / this.originalScale[0],
3129
+ this.originalScale[2] / this.originalScale[0]
3130
+ ];
3131
+ this.geometrySize = scaledResolution;
3132
+ this.boxSize = scaledResolution;
3133
+ log.debug("this.boxSize", this.boxSize);
3134
+ log.debug("this.geometrySize", this.geometrySize);
3135
+ log.debug("this.meshScale", this.meshScale);
3136
+ log.debug("this.originalScale", this.originalScale);
3137
+ log.debug("this.physicalDimensions", this.physicalDimensions);
3138
+ log.debug("this.maxResolution", this.maxResolution);
3139
+ log.debug("scaledResolution", scaledResolution);
3140
+ this.zarrInit = true;
3141
+ }
3142
+ this.updateUniforms(
3143
+ texturesList,
3144
+ dimensions,
3145
+ // Pass dimensions object instead of volume
3146
+ this.renderingMode,
3147
+ contrastLimitsList,
3148
+ colorsSave,
3149
+ this.layerTransparency,
3150
+ this.xSlice,
3151
+ this.ySlice,
3152
+ this.zSlice,
3153
+ bcTHREE,
3154
+ ptTHREE
3155
+ );
3156
+ return {
3157
+ uniforms: this.uniforms,
3158
+ shader: this.shader,
3159
+ meshScale: this.meshScale,
3160
+ geometrySize: this.geometrySize,
3161
+ boxSize: this.boxSize
3162
+ };
3163
+ }
3164
+ /**
3165
+ * Update shader uniforms with current rendering values
3166
+ * @param {Array} textures - List of 3D textures
3167
+ * @param {Object} dimensions - Dimensions object
3168
+ * @param {number} renderstyle - Rendering mode value
3169
+ * @param {Array} contrastLimits - List of contrast limits for each channel
3170
+ * @param {Array} colors - List of colors for each channel
3171
+ * @param {number} layerTransparency - Overall transparency value
3172
+ * @param {Vector2} xSlice - X clipping plane
3173
+ * @param {Vector2} ySlice - Y clipping plane
3174
+ * @param {Vector2} zSlice - Z clipping plane
3175
+ */
3176
+ updateUniforms(textures, dimensions, renderstyle, contrastLimits, colors, layerTransparency, xSlice, ySlice, zSlice, brickCacheTexture, pageTableTexture) {
3177
+ logWithColor$1("Updating uniforms");
3178
+ this.uniforms.boxSize.value.set(this.boxSize[0], this.boxSize[1], this.boxSize[2]);
3179
+ this.uniforms.brickCacheTex.value = brickCacheTexture;
3180
+ this.uniforms.pageTableTex.value = pageTableTexture;
3181
+ this.uniforms.near.value = 0.1;
3182
+ this.uniforms.far.value = 3e3;
3183
+ this.uniforms.opacity.value = layerTransparency;
3184
+ this.uniforms.volumeCount.value = textures.length;
3185
+ this.uniforms.u_size.value.set(dimensions.xLength, dimensions.yLength, dimensions.zLength);
3186
+ this.uniforms.u_window_size.value.set(0, 0);
3187
+ this.uniforms.u_vol_scale.value.set(1 / dimensions.xLength, 1 / dimensions.yLength, 1 / dimensions.zLength * 2);
3188
+ this.uniforms.clim0.value.set(contrastLimits.length > 0 ? contrastLimits[0][0] : null, contrastLimits.length > 0 ? contrastLimits[0][1] : null);
3189
+ this.uniforms.clim1.value.set(contrastLimits.length > 1 ? contrastLimits[1][0] : null, contrastLimits.length > 1 ? contrastLimits[1][1] : null);
3190
+ this.uniforms.clim2.value.set(contrastLimits.length > 2 ? contrastLimits[2][0] : null, contrastLimits.length > 2 ? contrastLimits[2][1] : null);
3191
+ this.uniforms.clim3.value.set(contrastLimits.length > 3 ? contrastLimits[3][0] : null, contrastLimits.length > 3 ? contrastLimits[3][1] : null);
3192
+ this.uniforms.clim4.value.set(contrastLimits.length > 4 ? contrastLimits[4][0] : null, contrastLimits.length > 4 ? contrastLimits[4][1] : null);
3193
+ this.uniforms.clim5.value.set(contrastLimits.length > 5 ? contrastLimits[5][0] : null, contrastLimits.length > 5 ? contrastLimits[5][1] : null);
3194
+ this.uniforms.clim6.value.set(contrastLimits.length > 6 ? contrastLimits[6][0] : null, contrastLimits.length > 6 ? contrastLimits[6][1] : null);
3195
+ this.uniforms.xClip.value.set(xSlice[0] * (1 / this.maxResolution[0]) * this.boxSize[0], xSlice[1] * (1 / this.maxResolution[0]) * this.boxSize[0]);
3196
+ this.uniforms.yClip.value.set(ySlice[0] * (1 / this.maxResolution[1]) * this.boxSize[1], ySlice[1] * (1 / this.maxResolution[1]) * this.boxSize[1]);
3197
+ this.uniforms.zClip.value.set(zSlice[0] * (1 / this.maxResolution[2]) * this.boxSize[2], zSlice[1] * (1 / this.maxResolution[2]) * this.boxSize[2]);
3198
+ this.uniforms.color0.value.set(colors.length > 0 ? colors[0][0] : null, colors.length > 0 ? colors[0][1] : null, colors.length > 0 ? colors[0][2] : null, colors.length > 0 ? colors[0][3] : null);
3199
+ this.uniforms.color1.value.set(colors.length > 1 ? colors[1][0] : null, colors.length > 1 ? colors[1][1] : null, colors.length > 1 ? colors[1][2] : null, colors.length > 1 ? colors[1][3] : null);
3200
+ this.uniforms.color2.value.set(colors.length > 2 ? colors[2][0] : null, colors.length > 2 ? colors[2][1] : null, colors.length > 2 ? colors[2][2] : null, colors.length > 2 ? colors[2][3] : null);
3201
+ this.uniforms.color3.value.set(colors.length > 3 ? colors[3][0] : null, colors.length > 3 ? colors[3][1] : null, colors.length > 3 ? colors[3][2] : null, colors.length > 3 ? colors[3][3] : null);
3202
+ this.uniforms.color4.value.set(colors.length > 4 ? colors[4][0] : null, colors.length > 4 ? colors[4][1] : null, colors.length > 4 ? colors[4][2] : null, colors.length > 4 ? colors[4][3] : null);
3203
+ this.uniforms.color5.value.set(colors.length > 5 ? colors[5][0] : null, colors.length > 5 ? colors[5][1] : null, colors.length > 5 ? colors[5][2] : null, colors.length > 5 ? colors[5][3] : null);
3204
+ this.uniforms.color6.value.set(colors.length > 6 ? colors[6][0] : null, colors.length > 6 ? colors[6][1] : null, colors.length > 6 ? colors[6][2] : null, colors.length > 6 ? colors[6][3] : null);
3205
+ for (let i = 0; i < 7; i++) {
3206
+ if (typeof this.channelMaxResolutionIndex[i] === "number") {
3207
+ this.uniforms[`res${i}`].value.set(Math.max(1, this.channelMaxResolutionIndex[i]), this.zarrStoreNumResolutions - 1);
3208
+ }
3209
+ }
3210
+ }
3211
+ /**
3212
+ * Sets the processing render target
3213
+ * @param {WebGLMultipleRenderTargets} mrt - Multiple render targets object with 3 attachments
3214
+ */
3215
+ /*
3216
+ setProcessingTargets(mrt) {
3217
+ logWithColor('setting processing targets');
3218
+ this.mrt = mrt;
3219
+ }
3220
+ */
3221
+ setChannelMapping(channelMapping) {
3222
+ logWithColor$1("setting channel mapping");
3223
+ log.debug("channelMapping", channelMapping);
3224
+ this.uniforms.channelMapping.value = channelMapping;
3225
+ }
3226
+ // Only called on initialization of the
3227
+ // VolumeDataManager and VolumeRenderManager for a particular image.
3228
+ setZarrUniforms(zarrStore, PT) {
3229
+ logWithColor$1("setting zarr uniforms");
3230
+ log.debug("zarrStore", zarrStore);
3231
+ log.debug("PT", PT);
3232
+ for (let i = 0; i <= 9; i++) {
3233
+ if (PT.anchors && PT.anchors[i]) {
3234
+ this.uniforms[`anchor${i}`].value.set(PT.anchors[i][0] || 0, PT.anchors[i][1] || 0, PT.anchors[i][2] || 0);
3235
+ } else {
3236
+ log.debug("anchor", i, "does not exist");
3237
+ this.uniforms[`anchor${i}`].value.set(0, 0, 0);
3238
+ }
3239
+ if (zarrStore.scales && zarrStore.scales[i]) {
3240
+ this.uniforms[`scale${i}`].value.set(zarrStore.scales[i][0] || 1, zarrStore.scales[i][1] || 1, zarrStore.scales[i][2] || 1);
3241
+ } else {
3242
+ log.debug("scale", i, "does not exist");
3243
+ }
3244
+ }
3245
+ log.debug("zarrStore.brickLayout", zarrStore.brickLayout);
3246
+ this.zarrStoreNumResolutions = zarrStore.brickLayout.length;
3247
+ for (let i = 0; i < 7; i++) {
3248
+ this.uniforms[`res${i}`].value.set(
3249
+ // TODO(mark) make this dependent on the per-channel maxResolution slider value.
3250
+ 1,
3251
+ zarrStore.brickLayout.length - 1
3252
+ );
3253
+ }
3254
+ this.uniforms.resGlobal.value.set(1, zarrStore.brickLayout.length - 1);
3255
+ this.uniforms.voxelExtents.value.set(zarrStore.shapes[0][4], zarrStore.shapes[0][3], zarrStore.shapes[0][2]);
3256
+ this.uniforms.maxChannels.value = Math.min(zarrStore.channelCount, 7);
3257
+ log.debug("this.channelsVisible", this.channelsVisible);
3258
+ log.debug("zarrStore.shapes[0]", zarrStore.shapes[0]);
3259
+ log.debug("PT", PT);
3260
+ log.debug("uniforms", this.uniforms);
3261
+ }
3262
+ }
3263
+ const gaussianVertexShader = `//
3264
+ varying vec2 vUv;
3265
+ void main() {
3266
+ vUv = uv;
3267
+ gl_Position = vec4(position, 1.0);
3268
+ }
3269
+ `;
3270
+ const gaussianFragmentShader = `//
3271
+ // Input texture to blur
3272
+ uniform sampler2D tDiffuse;
3273
+ // Resolution of the texture (width, height)
3274
+ uniform vec2 resolution;
3275
+ // Blur strength: 1=no blur, 2-3=3x3 kernel, 4-5=5x5 kernel, 6+=7x7 kernel
3276
+ uniform int gaussian;
3277
+ // Texture coordinates for current pixel
3278
+ varying vec2 vUv;
3279
+
3280
+ /**
3281
+ * No blur - returns the original pixel color
3282
+ */
3283
+ vec4 noGaussian() {
3284
+ vec4 color = texture2D(tDiffuse, vUv);
3285
+ return color;
3286
+ }
3287
+
3288
+ /**
3289
+ * Applies 3x3 Gaussian blur kernel
3290
+ * Samples 9 pixels in a 3x3 grid around the current pixel
3291
+ * Weights are based on 2D Gaussian distribution
3292
+ */
3293
+ vec4 gaussian3(vec2 texel) {
3294
+ vec4 color = vec4(0.0);
3295
+
3296
+ // Top row: weights [0.0625, 0.125, 0.0625]
3297
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, -1.0)) * 0.0625;
3298
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, -1.0)) * 0.125;
3299
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, -1.0)) * 0.0625;
3300
+
3301
+ // Middle row: weights [0.125, 0.25, 0.125] (center pixel gets highest weight)
3302
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 0.0)) * 0.125;
3303
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 0.0)) * 0.25;
3304
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 0.0)) * 0.125;
3305
+
3306
+ // Bottom row: weights [0.0625, 0.125, 0.0625]
3307
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 1.0)) * 0.0625;
3308
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 1.0)) * 0.125;
3309
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 1.0)) * 0.0625;
3310
+
3311
+ return color;
3312
+ }
3313
+
3314
+ /**
3315
+ * Applies 5x5 Gaussian blur kernel
3316
+ * Samples 25 pixels in a 5x5 grid around the current pixel
3317
+ * Weights sum to 1.0 (273/273) for proper normalization
3318
+ */
3319
+ vec4 gaussian5(vec2 texel) {
3320
+ vec4 color = vec4(0.0);
3321
+
3322
+ // Row 1: weights [1/273, 4/273, 7/273, 4/273, 1/273]
3323
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, -2.0)) * 1.0/273.0;
3324
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, -2.0)) * 4.0/273.0;
3325
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, -2.0)) * 7.0/273.0;
3326
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, -2.0)) * 4.0/273.0;
3327
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, -2.0)) * 1.0/273.0;
3328
+
3329
+ // Row 2: weights [4/273, 16/273, 26/273, 16/273, 4/273]
3330
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, -1.0)) * 4.0/273.0;
3331
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, -1.0)) * 16.0/273.0;
3332
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, -1.0)) * 26.0/273.0;
3333
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, -1.0)) * 16.0/273.0;
3334
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, -1.0)) * 4.0/273.0;
3335
+
3336
+ // Row 3 (center): weights [7/273, 26/273, 41/273, 26/273, 7/273]
3337
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, 0.0)) * 7.0/273.0;
3338
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 0.0)) * 26.0/273.0;
3339
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 0.0)) * 41.0/273.0; // Center pixel
3340
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 0.0)) * 26.0/273.0;
3341
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, 0.0)) * 7.0/273.0;
3342
+
3343
+ // Row 4: weights [4/273, 16/273, 26/273, 16/273, 4/273]
3344
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, 1.0)) * 4.0/273.0;
3345
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 1.0)) * 16.0/273.0;
3346
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 1.0)) * 26.0/273.0;
3347
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 1.0)) * 16.0/273.0;
3348
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, 1.0)) * 4.0/273.0;
3349
+
3350
+ // Row 5: weights [1/273, 4/273, 7/273, 4/273, 1/273]
3351
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, 2.0)) * 1.0/273.0;
3352
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 2.0)) * 4.0/273.0;
3353
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 2.0)) * 7.0/273.0;
3354
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 2.0)) * 4.0/273.0;
3355
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, 2.0)) * 1.0/273.0;
3356
+
3357
+ return color;
3358
+ }
3359
+
3360
+ /**
3361
+ * Applies 7x7 Gaussian blur kernel
3362
+ * Samples 49 pixels in a 7x7 grid around the current pixel
3363
+ * Weights sum to 1.0 (1003/1003) for proper normalization
3364
+ * Creates the strongest blur effect
3365
+ */
3366
+ vec4 gaussian7(vec2 texel) {
3367
+ vec4 color = vec4(0.0);
3368
+
3369
+ // Row 1: weights [0, 0, 1/1003, 2/1003, 1/1003, 0, 0]
3370
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, -3.0)) * 1.0/1003.0;
3371
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, -3.0)) * 2.0/1003.0;
3372
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, -3.0)) * 1.0/1003.0;
3373
+
3374
+ // Row 2: weights [0, 3/1003, 13/1003, 22/1003, 13/1003, 3/1003, 0]
3375
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, -2.0)) * 3.0/1003.0;
3376
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, -2.0)) * 13.0/1003.0;
3377
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, -2.0)) * 22.0/1003.0;
3378
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, -2.0)) * 13.0/1003.0;
3379
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, -2.0)) * 3.0/1003.0;
3380
+
3381
+ // Row 3: weights [1/1003, 13/1003, 59/1003, 97/1003, 59/1003, 13/1003, 1/1003]
3382
+ color += texture2D(tDiffuse, vUv + texel * vec2(-3.0, -1.0)) * 1.0/1003.0;
3383
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, -1.0)) * 13.0/1003.0;
3384
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, -1.0)) * 59.0/1003.0;
3385
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, -1.0)) * 97.0/1003.0;
3386
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, -1.0)) * 59.0/1003.0;
3387
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, -1.0)) * 13.0/1003.0;
3388
+ color += texture2D(tDiffuse, vUv + texel * vec2( 3.0, -1.0)) * 1.0/1003.0;
3389
+
3390
+ // Row 4 (center): weights [2/1003, 22/1003, 97/1003, 159/1003, 97/1003, 22/1003, 2/1003]
3391
+ color += texture2D(tDiffuse, vUv + texel * vec2(-3.0, 0.0)) * 2.0/1003.0;
3392
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, 0.0)) * 22.0/1003.0;
3393
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 0.0)) * 97.0/1003.0;
3394
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 0.0)) * 159.0/1003.0; // Center pixel
3395
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 0.0)) * 97.0/1003.0;
3396
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, 0.0)) * 22.0/1003.0;
3397
+ color += texture2D(tDiffuse, vUv + texel * vec2( 3.0, 0.0)) * 2.0/1003.0;
3398
+
3399
+ // Row 5: weights [1/1003, 13/1003, 59/1003, 97/1003, 59/1003, 13/1003, 1/1003]
3400
+ color += texture2D(tDiffuse, vUv + texel * vec2(-3.0, 1.0)) * 1.0/1003.0;
3401
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, 1.0)) * 13.0/1003.0;
3402
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 1.0)) * 59.0/1003.0;
3403
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 1.0)) * 97.0/1003.0;
3404
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 1.0)) * 59.0/1003.0;
3405
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, 1.0)) * 13.0/1003.0;
3406
+ color += texture2D(tDiffuse, vUv + texel * vec2( 3.0, 1.0)) * 1.0/1003.0;
3407
+
3408
+ // Row 6: weights [0, 3/1003, 13/1003, 22/1003, 13/1003, 3/1003, 0]
3409
+ color += texture2D(tDiffuse, vUv + texel * vec2(-2.0, 2.0)) * 3.0/1003.0;
3410
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 2.0)) * 13.0/1003.0;
3411
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 2.0)) * 22.0/1003.0;
3412
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 2.0)) * 13.0/1003.0;
3413
+ color += texture2D(tDiffuse, vUv + texel * vec2( 2.0, 2.0)) * 3.0/1003.0;
3414
+
3415
+ // Row 7: weights [0, 0, 1/1003, 2/1003, 1/1003, 0, 0]
3416
+ color += texture2D(tDiffuse, vUv + texel * vec2(-1.0, 3.0)) * 1.0/1003.0;
3417
+ color += texture2D(tDiffuse, vUv + texel * vec2( 0.0, 3.0)) * 2.0/1003.0;
3418
+ color += texture2D(tDiffuse, vUv + texel * vec2( 1.0, 3.0)) * 1.0/1003.0;
3419
+
3420
+ return color;
3421
+ }
3422
+
3423
+ /**
3424
+ * Main fragment shader function
3425
+ * Determines which blur kernel to apply based on the 'gaussian' uniform
3426
+ */
3427
+ void main() {
3428
+ // Calculate the size of one texel (pixel) in texture coordinates
3429
+ vec2 texel = 1.0 / resolution;
3430
+
3431
+ // Initialize output color (this line appears to be unused/debug code)
3432
+ vec4 color = vec4(1.0, 0.0, 0.0, 0.0);
3433
+
3434
+ // TODO: remove these variables since not used
3435
+ bool left = vUv.x < 0.5;
3436
+ bool top = vUv.y < 0.5;
3437
+
3438
+ if (gaussian > 1 && gaussian <= 3) {
3439
+ // Apply 3x3 Gaussian blur for values 2-3
3440
+ color = gaussian3(texel);
3441
+ } else if (gaussian > 3 && gaussian <= 5) {
3442
+ // Apply 5x5 Gaussian blur for values 4-5
3443
+ color = gaussian5(texel);
3444
+ } else if (gaussian > 5) {
3445
+ // Apply 7x7 Gaussian blur for values 6 and above
3446
+ color = gaussian7(texel);
3447
+ } else {
3448
+ // No blur for value 1 or less
3449
+ color = noGaussian();
3450
+ }
3451
+
3452
+ // Output the final blurred color
3453
+ gl_FragColor = color;
3454
+ }
3455
+ `;
3456
+ function framebufferFor(renderer, rt) {
3457
+ const p = renderer.properties?.get(rt);
3458
+ return p?.framebuffer || p?.__webglFramebuffer || rt.__webglFramebuffer;
3459
+ }
3460
+ function logWithColor(msg) {
3461
+ if (atLeastLogLevel(LogLevel.DEBUG)) {
3462
+ console.warn(`%cV: ${msg}`, "background: deeppink; color: white; padding: 2px; border-radius: 3px;");
3463
+ }
3464
+ }
3465
+ function performGeometryPass(_gl, _camera, _scene, { mrtRef }) {
3466
+ _gl.setRenderTarget(mrtRef.current);
3467
+ _gl.clear(true, true, true);
3468
+ _gl.render(_scene, _camera);
3469
+ }
3470
+ function performBlitPass(_gl, { screenSceneRef, screenCameraRef }) {
3471
+ _gl.setRenderTarget(null);
3472
+ _gl.clear(true, true, true);
3473
+ _gl.render(screenSceneRef.current, screenCameraRef.current);
3474
+ }
3475
+ function handleRequests(_gl, { frameRef, dataManager, mrtRef, bufRequest, bufUsage }) {
3476
+ const ctx = _gl.getContext();
3477
+ const f = frameRef.current;
3478
+ if (dataManager.noNewRequests === true && !dataManager.manuallyStopped && f % 100 === 0 && f < 500) {
3479
+ dataManager.noNewRequests = false;
3480
+ }
3481
+ if (dataManager.triggerRequest === true && dataManager.noNewRequests === false) {
3482
+ ctx.bindFramebuffer(ctx.READ_FRAMEBUFFER, framebufferFor(_gl, mrtRef.current));
3483
+ ctx.readBuffer(ctx.COLOR_ATTACHMENT1);
3484
+ ctx.readPixels(0, 0, mrtRef.current.width, mrtRef.current.height, ctx.RGBA, ctx.UNSIGNED_BYTE, bufRequest.current);
3485
+ dataManager.processRequestData(bufRequest.current, {
3486
+ // If width/height are not yet available,
3487
+ // processRequestData will proceed without weighting.
3488
+ width: mrtRef?.current?.width,
3489
+ height: mrtRef?.current?.height,
3490
+ // Default sigmaNormalized = 0.25; can be tuned
3491
+ sigmaNormalized: DEFAULT_SIGMA_NORMALIZED
3492
+ });
3493
+ } else if (dataManager.triggerUsage === true && dataManager.noNewRequests === false) {
3494
+ ctx.bindFramebuffer(ctx.READ_FRAMEBUFFER, framebufferFor(_gl, mrtRef.current));
3495
+ ctx.readBuffer(ctx.COLOR_ATTACHMENT2);
3496
+ ctx.readPixels(0, 0, mrtRef.current.width, mrtRef.current.height, ctx.RGBA, ctx.UNSIGNED_BYTE, bufUsage.current);
3497
+ dataManager.processUsageData(bufUsage.current);
3498
+ }
3499
+ }
3500
+ function handleAdaptiveQuality(clock, params) {
3501
+ const { invalidate, isInteracting, dataManager, spatialRenderingModeChanging, meshRef, stillRef, screenQuadRef, lastSampleRef, frameRef, lastFrameCountRef } = params;
3502
+ if (screenQuadRef.current) {
3503
+ if (!stillRef.current) {
3504
+ screenQuadRef.current.material.uniforms.gaussian.value = 7;
3505
+ } else {
3506
+ screenQuadRef.current.material.uniforms.gaussian.value = 0;
3507
+ }
3508
+ }
3509
+ if (isInteracting) {
3510
+ if (!dataManager.manuallyStopped) {
3511
+ dataManager.noNewRequests = false;
3512
+ dataManager.triggerUsage = true;
3513
+ }
3514
+ return;
3515
+ }
3516
+ if (spatialRenderingModeChanging)
3517
+ return;
3518
+ const meshRefUniforms = meshRef.current?.material?.uniforms;
3519
+ const renderSpeed = meshRefUniforms?.renderRes?.value ?? dataManager?.PT?.lowestDataRes;
3520
+ if (dataManager.noNewRequests) {
3521
+ if (renderSpeed !== 0) {
3522
+ if (meshRefUniforms) {
3523
+ meshRefUniforms.renderRes.value = 0;
3524
+ }
3525
+ log.debug("Adaptive Quality: No new requests. Setting renderSpeed to 0 (best quality).");
3526
+ stillRef.current = false;
3527
+ screenQuadRef.current.material.uniforms.gaussian.value = 7;
3528
+ invalidate();
3529
+ } else if (!stillRef.current) {
3530
+ log.debug("Adaptive Quality: No new requests and already at best quality. Setting stillRef to true.");
3531
+ stillRef.current = true;
3532
+ screenQuadRef.current.material.uniforms.gaussian.value = 0;
3533
+ }
3534
+ return;
3535
+ }
3536
+ if (stillRef.current) {
3537
+ stillRef.current = false;
3538
+ invalidate();
3539
+ }
3540
+ const t = clock.getElapsedTime();
3541
+ if (t - lastSampleRef.current < 1) {
3542
+ return;
3543
+ }
3544
+ const timeElapsedDuringSample = t - lastSampleRef.current;
3545
+ const framesRenderedDuringSample = frameRef.current - lastFrameCountRef.current;
3546
+ let fps = 0;
3547
+ if (timeElapsedDuringSample > 0) {
3548
+ fps = framesRenderedDuringSample / timeElapsedDuringSample;
3549
+ }
3550
+ lastSampleRef.current = t;
3551
+ lastFrameCountRef.current = frameRef.current;
3552
+ const upscale = fps > 100 && renderSpeed > 0;
3553
+ const downscale = fps < 30 && renderSpeed < dataManager.PT.lowestDataRes;
3554
+ if (upscale || downscale) {
3555
+ const newSpeed = renderSpeed + (downscale ? 1 : -1);
3556
+ if (meshRefUniforms) {
3557
+ meshRefUniforms.renderRes.value = newSpeed;
3558
+ }
3559
+ invalidate();
3560
+ }
3561
+ }
3562
+ function VolumeView(props) {
3563
+ const {
3564
+ images,
3565
+ imageLayerScopes,
3566
+ imageLayerCoordination,
3567
+ imageChannelScopesByLayer,
3568
+ imageChannelCoordination,
3569
+ // onInitComplete,
3570
+ spatialRenderingMode,
3571
+ spatialRenderingModeChanging,
3572
+ onVolumeLoadingUpdate
3573
+ } = props;
3574
+ const {
3575
+ gl
3576
+ // scene,
3577
+ // camera
3578
+ } = useThree();
3579
+ const invalidate = useThree((state) => state.invalidate);
3580
+ const orbitRef = useRef(null);
3581
+ const meshRef = useRef(null);
3582
+ const bufRequest = useRef(null);
3583
+ const bufUsage = useRef(null);
3584
+ const mrtRef = useRef(null);
3585
+ const [renderState, setRenderState] = useState({
3586
+ uniforms: null,
3587
+ shader: null,
3588
+ meshScale: [1, 1, 1],
3589
+ geometrySize: [1, 1, 1]
3590
+ });
3591
+ const is3D = spatialRenderingMode === "3D";
3592
+ const [dmInitComplete, setDmInitComplete] = useState(false);
3593
+ const screenSceneRef = useRef(null);
3594
+ const screenCameraRef = useRef(null);
3595
+ const screenQuadRef = useRef(null);
3596
+ const [isInteracting, _setIsInteracting] = useState(false);
3597
+ const interactionTimeoutRef = useRef(null);
3598
+ const stillRef = useRef(false);
3599
+ const frameRef = useRef(0);
3600
+ const lastSampleRef = useRef(0);
3601
+ const lastFrameCountRef = useRef(0);
3602
+ const dataManager = useMemo(() => new VolumeDataManager(gl), [gl]);
3603
+ const renderManager = useMemo(() => new VolumeRenderManager(), []);
3604
+ useEffect(() => {
3605
+ logWithColor("useEffect MRT target matching canvas");
3606
+ const { width, height } = gl.domElement;
3607
+ const mrt = new WebGLMultipleRenderTargets(width, height, 3);
3608
+ mrt.texture.forEach((tex) => {
3609
+ tex.format = THREE.RGBAFormat;
3610
+ tex.type = THREE.UnsignedByteType;
3611
+ tex.minFilter = THREE.NearestFilter;
3612
+ tex.magFilter = THREE.NearestFilter;
3613
+ tex.generateMipmaps = false;
3614
+ });
3615
+ const screenScene = new THREE.Scene();
3616
+ const screenCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
3617
+ screenCamera.position.z = 1;
3618
+ const screenMaterial = new THREE.ShaderMaterial({
3619
+ uniforms: {
3620
+ // Bind the first render target texture as the input of the gaussian blur shader.
3621
+ tDiffuse: { value: mrt.texture[0] },
3622
+ resolution: { value: new THREE.Vector2(width, height) },
3623
+ gaussian: { value: 7 }
3624
+ },
3625
+ vertexShader: gaussianVertexShader,
3626
+ fragmentShader: gaussianFragmentShader,
3627
+ transparent: true
3628
+ });
3629
+ const screenQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), screenMaterial);
3630
+ screenScene.add(screenQuad);
3631
+ screenSceneRef.current = screenScene;
3632
+ screenCameraRef.current = screenCamera;
3633
+ screenQuadRef.current = screenQuad;
3634
+ bufRequest.current = new Uint8Array(width * height * 4);
3635
+ bufUsage.current = new Uint8Array(width * height * 4);
3636
+ mrtRef.current = mrt;
3637
+ return () => {
3638
+ mrt.dispose();
3639
+ screenMaterial.dispose();
3640
+ screenQuad.geometry.dispose();
3641
+ };
3642
+ }, [gl]);
3643
+ const firstImageLayerScope = imageLayerScopes?.[0];
3644
+ const firstImage = images?.[firstImageLayerScope];
3645
+ imageChannelScopesByLayer?.[firstImageLayerScope];
3646
+ const firstImageLayerChannelCoordination = imageChannelCoordination?.[0]?.[firstImageLayerScope];
3647
+ useEffect(() => {
3648
+ logWithColor("useEffect INIT");
3649
+ let hasRerun = false;
3650
+ if (!dataManager || !renderManager) {
3651
+ log.debug("dataManager or renderManager not initialized yet");
3652
+ return;
3653
+ }
3654
+ if (!firstImage) {
3655
+ log.debug("no first image layer yet");
3656
+ return;
3657
+ }
3658
+ if (!firstImageLayerChannelCoordination) {
3659
+ log.debug("no firstImageLayerChannelCoordination yet");
3660
+ return;
3661
+ }
3662
+ const initializeDataManager = async () => {
3663
+ if (dataManager.initStatus === INIT_STATUS.COMPLETE) {
3664
+ log.debug("dataManager already initialized, skipping");
3665
+ setDmInitComplete(true);
3666
+ return;
3667
+ }
3668
+ dataManager.initImages(images, imageLayerScopes);
3669
+ await dataManager.init(firstImageLayerChannelCoordination);
3670
+ if (hasRerun) {
3671
+ log.debug("Initialization useEffect has rerun, aborting remaining initialization");
3672
+ return;
3673
+ }
3674
+ log.debug("dataManager initialized");
3675
+ renderManager.setZarrUniforms(dataManager.zarrStore, dataManager.PT);
3676
+ renderManager.setChannelMapping(dataManager.channels.colorMappings);
3677
+ log.debug("rm.uniforms", renderManager.uniforms);
3678
+ setDmInitComplete(true);
3679
+ invalidate();
3680
+ };
3681
+ initializeDataManager();
3682
+ return () => {
3683
+ hasRerun = true;
3684
+ };
3685
+ }, [dataManager, renderManager, images, imageLayerScopes]);
3686
+ useEffect(() => {
3687
+ const on3D = spatialRenderingMode === "3D";
3688
+ if (on3D && dataManager && renderManager && dmInitComplete) {
3689
+ logWithColor("useEffect spatialRenderingMode");
3690
+ const propsForRenderManager = {
3691
+ images,
3692
+ imageLayerScopes,
3693
+ imageLayerCoordination,
3694
+ imageChannelScopesByLayer,
3695
+ imageChannelCoordination,
3696
+ spatialRenderingMode
3697
+ };
3698
+ if (renderManager.updateFromProps(propsForRenderManager)) {
3699
+ const { zarrInit } = renderManager;
3700
+ if (!zarrInit) {
3701
+ dataManager.ptTHREE.needsUpdate = false;
3702
+ dataManager.bcTHREE.needsUpdate = false;
3703
+ dataManager.renderer.initTexture(dataManager.bcTHREE);
3704
+ dataManager.renderer.initTexture(dataManager.ptTHREE);
3705
+ dataManager.initTexture();
3706
+ }
3707
+ const nextRenderState = renderManager.updateRendering({
3708
+ zarrStoreShapes: dataManager.zarrStore.shapes,
3709
+ originalScaleXYZ: dataManager.getOriginalScaleXYZ(),
3710
+ physicalDimensionsXYZ: dataManager.getPhysicalDimensionsXYZ(),
3711
+ maxResolutionXYZ: dataManager.getMaxResolutionXYZ(),
3712
+ boxDimensionsXYZ: dataManager.getBoxDimensionsXYZ(),
3713
+ normalizedScaleXYZ: dataManager.getNormalizedScaleXYZ(),
3714
+ bcTHREE: dataManager.bcTHREE,
3715
+ ptTHREE: dataManager.ptTHREE
3716
+ });
3717
+ if (nextRenderState) {
3718
+ setRenderState(nextRenderState);
3719
+ }
3720
+ }
3721
+ }
3722
+ }, [dataManager, renderManager, images, imageLayerScopes, imageLayerCoordination, imageChannelScopesByLayer, imageChannelCoordination, spatialRenderingMode, dmInitComplete]);
3723
+ const RENDER_PRIORITY = 1;
3724
+ useFrame((state, delta, xrFrame) => {
3725
+ if (!mrtRef.current || !dataManager || !renderManager) {
3726
+ return;
3727
+ }
3728
+ if (!renderState.shader) {
3729
+ return;
3730
+ }
3731
+ const { gl: frameGl, camera: frameCamera, scene: frameScene, clock } = state;
3732
+ if (!stillRef.current) {
3733
+ performGeometryPass(frameGl, frameCamera, frameScene, { mrtRef });
3734
+ }
3735
+ performBlitPass(frameGl, { screenSceneRef, screenCameraRef });
3736
+ handleRequests(frameGl, { frameRef, dataManager, mrtRef, bufRequest, bufUsage });
3737
+ handleAdaptiveQuality(clock, {
3738
+ invalidate,
3739
+ isInteracting,
3740
+ dataManager,
3741
+ spatialRenderingModeChanging,
3742
+ meshRef,
3743
+ stillRef,
3744
+ screenQuadRef,
3745
+ lastSampleRef,
3746
+ frameRef,
3747
+ lastFrameCountRef
3748
+ });
3749
+ frameRef.current += 1;
3750
+ }, RENDER_PRIORITY);
3751
+ const setIsInteracting = useEventCallback((interacting) => {
3752
+ logWithColor("invalidateOnInteraction callback");
3753
+ _setIsInteracting(interacting);
3754
+ if (interacting) {
3755
+ const meshRefUniforms = meshRef.current?.material?.uniforms;
3756
+ if (meshRefUniforms) {
3757
+ meshRefUniforms.renderRes.value = dataManager.PT.lowestDataRes;
3758
+ }
3759
+ stillRef.current = false;
3760
+ invalidate();
3761
+ }
3762
+ });
3763
+ const onOrbitControlsStart = useEventCallback((e) => {
3764
+ setIsInteracting(true);
3765
+ });
3766
+ const onOrbitControlsEnd = useEventCallback((e) => {
3767
+ clearTimeout(interactionTimeoutRef.current);
3768
+ interactionTimeoutRef.current = setTimeout(() => {
3769
+ setIsInteracting(false);
3770
+ }, 300);
3771
+ });
3772
+ useEffect(() => {
3773
+ if (!dmInitComplete)
3774
+ return;
3775
+ logWithColor("useEffect firstImageLayerChannelCoordination");
3776
+ setIsInteracting(true);
3777
+ log.debug("something about channels changed");
3778
+ dataManager.updateChannels(firstImageLayerChannelCoordination);
3779
+ renderManager.setChannelMapping(dataManager.channels.colorMappings);
3780
+ clearTimeout(interactionTimeoutRef.current);
3781
+ interactionTimeoutRef.current = setTimeout(() => {
3782
+ setIsInteracting(false);
3783
+ }, 300);
3784
+ }, [dataManager, firstImageLayerChannelCoordination, renderManager, setIsInteracting, dmInitComplete]);
3785
+ const stopLoading = useEventCallback(() => {
3786
+ if (dataManager) {
3787
+ dataManager.stopLoading();
3788
+ }
3789
+ });
3790
+ const restartLoading = useEventCallback(() => {
3791
+ if (dataManager) {
3792
+ dataManager.restartLoading();
3793
+ }
3794
+ });
3795
+ const getLoadingProgress = useEventCallback(() => {
3796
+ if (dataManager) {
3797
+ return dataManager.getLoadingProgress();
3798
+ }
3799
+ return null;
3800
+ });
3801
+ useEffect(() => {
3802
+ if (!onVolumeLoadingUpdate)
3803
+ return void 0;
3804
+ const intervalId = setInterval(() => {
3805
+ const loadingProgress = getLoadingProgress();
3806
+ onVolumeLoadingUpdate({
3807
+ loadingProgress,
3808
+ stillRef,
3809
+ onStopLoading: stopLoading,
3810
+ onRestartLoading: restartLoading
3811
+ });
3812
+ }, 1e3);
3813
+ return () => clearInterval(intervalId);
3814
+ }, [onVolumeLoadingUpdate, stillRef]);
3815
+ if (!is3D || !dataManager || !renderManager)
3816
+ return null;
3817
+ if (!renderState.shader) {
3818
+ return jsxs("group", { children: [jsxs("mesh", { children: [jsx("boxGeometry", { args: [1, 1, 1] }), jsx("meshBasicMaterial", { color: "#444", wireframe: true })] }), jsx(OrbitControls, { ref: orbitRef })] });
3819
+ }
3820
+ return jsxs("group", { children: [jsx(
3821
+ OrbitControls,
3822
+ {
3823
+ // ref={mainOrbitControlsRef}
3824
+ enableDamping: false,
3825
+ onStart: onOrbitControlsStart,
3826
+ onEnd: onOrbitControlsEnd
3827
+ }
3828
+ ), jsxs("mesh", { ref: meshRef, scale: renderState.meshScale, children: [jsx("boxGeometry", { args: renderState.geometrySize }), jsx("shaderMaterial", { uniforms: renderState.uniforms, vertexShader: renderState.shader.vertexShader, fragmentShader: renderState.shader.fragmentShader, side: THREE.BackSide, transparent: false, glslVersion: THREE.GLSL3 })] })] });
3829
+ }
3830
+ function SpatialWrapper(props) {
3831
+ return jsx(Canvas, { frameloop: "always", style: {
3832
+ position: "absolute",
3833
+ top: 0,
3834
+ left: 0,
3835
+ width: "100%",
3836
+ height: "100%",
3837
+ padding: 0,
3838
+ margin: 0
3839
+ // backgroundColor: 'white',
3840
+ }, camera: {
3841
+ fov: 50,
3842
+ up: [0, 1, 0],
3843
+ position: [0, 0, 4],
3844
+ near: 0.01,
3845
+ far: 15
3846
+ }, gl: {
3847
+ antialias: true,
3848
+ logarithmicDepthBuffer: false,
3849
+ preserveDrawingBuffer: false,
3850
+ autoClear: false
3851
+ }, children: jsx(Suspense, { fallback: "Initializing Volume View...", children: jsx(VolumeView, { ...props }) }) });
3852
+ }
3853
+ export {
3854
+ SpatialWrapper
3855
+ };