@vitessce/spatial-accelerated 3.8.10 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- export const volumeFragmentShader: "//\n// #include <packing>\nprecision highp float;\nprecision highp int;\nprecision highp sampler3D;\nprecision highp usampler3D;\n\n// ========================================\n// INPUT VARIABLES (from vertex shader)\n// ========================================\n// Unnormalized ray direction from camera\nin vec3 rayDirUnnorm;\n// Camera position in world space\nin vec3 cameraCorrected;\n\n// ========================================\n// TEXTURE SAMPLERS\n// ========================================\n// 3D texture containing cached brick data (2048x2048x128)\n// (2048*2048*128)/(32*32*32) = 16,384 bricks can be stored?\nuniform sampler3D brickCacheTex;\n// 3D texture containing page table entries (brick metadata)\nuniform usampler3D pageTableTex;\n\n// ========================================\n// RENDERING PARAMETERS/CONSTANTS\n// ========================================\n// Rendering style: 0=MIP, 1=MinIP, 2=standard volume rendering\nuniform int u_renderstyle;\n// Global opacity multiplier for volume rendering\nuniform float opacity;\n\n// ========================================\n// CONTRAST LIMITS (per channel)\n// per channel min/max values for value normalization\n// ========================================\nuniform vec2 clim0;\nuniform vec2 clim1;\nuniform vec2 clim2;\nuniform vec2 clim3;\nuniform vec2 clim4;\nuniform vec2 clim5;\nuniform vec2 clim6;\n\n// ========================================\n// CLIPPING PLANES\n// e.g., for X-axis clipping: (min_x, max_x) or (-1, -1) if disabled\n// ========================================\nuniform vec2 xClip;\nuniform vec2 yClip;\nuniform vec2 zClip;\n\n// ========================================\n// CHANNEL COLORS AND OPACITIES\n// rgb -- color values, a -- visibility (boolean)\n// ========================================\nuniform vec4 color0;\nuniform vec4 color1;\nuniform vec4 color2;\nuniform vec4 color3;\nuniform vec4 color4;\nuniform vec4 color5;\nuniform vec4 color6;\n\n// maps colors to physical spaces\nuniform int channelMapping[7];\n\n// ========================================\n// VOLUME AND RESOLUTION PARAMETERS\n// ========================================\n// Volume bounding box size in world space\nuniform highp vec3 boxSize;\n// Rendering resolution level (affects step size)\n// stepsize, correlates with resolution\nuniform int renderRes;\n// Volume dimensions in voxels (x, y, z)\n// resolution 0 voxel extents\nuniform uvec3 voxelExtents;\n// Global resolution range: (min_res, max_res)\n// global range of requested resolutions\nuniform ivec2 resGlobal;\n// Maximum number of active channels\n// max number of channels (relevant for the cache statistics)\n// between 1 and 7\nuniform int maxChannels;\n\n// ========================================\n// PER-CHANNEL RESOLUTION RANGES\n// per color channel resolution range\n// Each channel can have different available resolution levels\n// e.g., for Channel 0: (min_res, max_res)\n// ========================================\nuniform ivec2 res0;\nuniform ivec2 res1;\nuniform ivec2 res2;\nuniform ivec2 res3;\nuniform ivec2 res4;\nuniform ivec2 res5;\nuniform ivec2 res6;\n// Channel 7: unused\nuniform ivec2 res7;\n\n// ========================================\n// LEVEL-OF-DETAIL PARAMETERS\n// controls how fast we decrease the resolution\n// ========================================\n// LOD factor for distance-based resolution selection\nuniform float lodFactor;\n\n// ========================================\n// ANCHOR POINTS (per resolution level)\n// per resolution anchor point for pagetable\n// ========================================\n// Anchor points define the origin of page table for each resolution level\n// Resolution 0 anchor point (highest detail)\nuniform uvec3 anchor0;\nuniform uvec3 anchor1;\nuniform uvec3 anchor2;\nuniform uvec3 anchor3;\nuniform uvec3 anchor4;\nuniform uvec3 anchor5;\nuniform uvec3 anchor6;\nuniform uvec3 anchor7;\nuniform uvec3 anchor8;\nuniform uvec3 anchor9;\n// Resolution 9 anchor point (lowest detail)\n\n// ========================================\n// SCALE FACTORS (per resolution level)\n// per resolution downsample factor\n// ========================================\n// Scale factors determine voxel size at each resolution level\n// Resolution 0 scale factors (should be 1,1,1)\nuniform vec3 scale0;\nuniform vec3 scale1;\nuniform vec3 scale2;\nuniform vec3 scale3;\nuniform vec3 scale4;\nuniform vec3 scale5;\nuniform vec3 scale6;\nuniform vec3 scale7;\nuniform vec3 scale8;\nuniform vec3 scale9;\n// Resolution 9 scale factors\n\n// ========================================\n// VARYING VARIABLES (unused but required)\n// ========================================\n// Fragment position (unused)\nvarying vec4 glPosition;\n// World space coordinates (used for depth only)\nvarying vec3 worldSpaceCoords;\n\n// ========================================\n// OUTPUT VARIABLES (multiple render targets)\n// output buffers\n// ========================================\n// Final rendered color (sRGB)\nlayout(location = 0) out vec4 gColor;\n// Brick loading requests (packed coordinates)\nlayout(location = 1) out vec4 gRequest;\n// Brick usage tracking (for cache management)\nlayout(location = 2) out vec4 gUsage;\n\n// ========================================\n// CONSTANTS\n// ========================================\n// Size of each brick in voxels (32x32x32)\nconst float BRICK_SIZE = 32.0;\n// Brick cache texture width\nconst float BRICK_CACHE_SIZE_X = 2048.0;\n// Brick cache texture height\nconst float BRICK_CACHE_SIZE_Y = 2048.0;\n// Brick cache texture depth\nconst float BRICK_CACHE_SIZE_Z = 128.0;\n// Number of bricks in X (64)\nconst float BRICK_CACHE_BRICKS_X = BRICK_CACHE_SIZE_X / BRICK_SIZE;\n// Number of bricks in Y (64)\nconst float BRICK_CACHE_BRICKS_Y = BRICK_CACHE_SIZE_Y / BRICK_SIZE;\n// Number of bricks in Z (4)\nconst float BRICK_CACHE_BRICKS_Z = BRICK_CACHE_SIZE_Z / BRICK_SIZE;\n\n// ========================================\n// RAY-VOLUME INTERSECTION\n// calculating the intersection of the ray with the bounding box\n// ========================================\n// Calculates the intersection of a ray with the volume's bounding box\n// Returns (entry_time, exit_time) for the ray-box intersection\n// Handles clipping planes by adjusting the bounding box\n//\n// Parameters:\n// orig - vec3: Ray origin point in world space\n// dir - vec3: Ray direction vector (should be normalized)\n//\n// Returns:\n// vec2: (entry_time, exit_time) where:\n// - entry_time: Distance along ray to enter the volume\n// - exit_time: Distance along ray to exit the volume\n// - If no intersection: entry_time > exit_time\nvec2 intersect_hit(vec3 orig, vec3 dir) {\n // Start with full volume bounds\n vec3 boxMin = vec3(-0.5) * boxSize;\n vec3 boxMax = vec3(0.5) * boxSize;\n \n // Apply clipping planes if they're active (xClip.x > -1.0 means active)\n if (xClip.x > -1.0) {\n boxMin.x = xClip.x - (boxSize.x / 2.0);\n if (xClip.y < boxSize.x)\n boxMax.x = xClip.y - (boxSize.x / 2.0);\n }\n if (yClip.x > -1.0) {\n boxMin.y = yClip.x - (boxSize.y / 2.0);\n if (yClip.y < boxSize.y)\n boxMax.y = yClip.y - (boxSize.y / 2.0);\n }\n if (zClip.x > -1.0) {\n boxMin.z = zClip.x - (boxSize.z / 2.0);\n if (zClip.y < boxSize.z)\n boxMax.z = zClip.y - (boxSize.z / 2.0);\n }\n \n // Standard ray-box intersection algorithm\n vec3 invDir = 1.0 / dir;\n vec3 tmin0 = (boxMin - orig) * invDir;\n vec3 tmax0 = (boxMax - orig) * invDir;\n vec3 tmin = min(tmin0, tmax0);\n vec3 tmax = max(tmin0, tmax0);\n float t0 = max(tmin.x, max(tmin.y, tmin.z)); // Entry time\n float t1 = min(tmax.x, min(tmax.y, tmax.z)); // Exit time\n return vec2(t0, t1);\n}\n\n// ========================================\n// UTILITY FUNCTIONS\n// ========================================\n\n// Pseudo-random number generator for jittered sampling\n// random number generator based on the uv coordinate\n// Author @patriciogv - 2015\n// http://patriciogonzalezvivo.com\n//\n// Parameters:\n// None (uses gl_FragCoord.xy as input)\n//\n// Returns:\n// float: Random value between 0.0 and 1.0 based on fragment coordinates\nfloat random() {\n return fract(sin(dot(gl_FragCoord.xy, vec2(12.9898,78.233)))* 43758.5453123);\n}\n\n// Convert from linear RGB to sRGB color space\n// Implements the standard sRGB transfer function for gamma correction\n//\n// Parameters:\n// x - float: Linear RGB value between 0.0 and 1.0\n//\n// Returns:\n// float: sRGB value between 0.0 and 1.0\nfloat linear_to_srgb(float x) {\n if (x <= 0.0031308f) {\n return 12.92f * x;\n }\n return 1.055f * pow(x, 1.f / 2.4f) - 0.055f;\n}\n\n// Convert from linear RGB to sRGB color space (vector version)\n// Applies sRGB conversion to each RGB component while preserving alpha\n//\n// Parameters:\n// x - vec4: Linear RGBA color with components between 0.0 and 1.0\n//\n// Returns:\n// vec4: sRGB RGBA color with components between 0.0 and 1.0\nvec4 linear_to_srgb(vec4 x) {\n return vec4(linear_to_srgb(x.r), linear_to_srgb(x.g), linear_to_srgb(x.b), x.a);\n}\n\n// ========================================\n// PAGE TABLE COORDINATE PACKING\n// transform the pagetable coordinate into a RGBA8 value\n// ========================================\n// Packs 3D page table coordinates into RGBA8 texture format\n// Uses 10 bits for X, 10 bits for Y, 12 bits for Z\n//\n// Parameters:\n// coord - uvec3: 3D coordinates to pack (X, Y, Z components)\n// - X coordinate: 10-bit unsigned integer (0-1023)\n// - Y coordinate: 10-bit unsigned integer (0-1023) \n// - Z coordinate: 12-bit unsigned integer (0-4095)\n//\n// Returns:\n// vec4: RGBA8 encoded coordinates with components between 0.0 and 1.0\n// - R: Upper 8 bits of packed 32-bit value\n// - G: Middle-upper 8 bits of packed 32-bit value\n// - B: Middle-lower 8 bits of packed 32-bit value\n// - A: Lower 8 bits of packed 32-bit value\nvec4 packPTCoordToRGBA8(uvec3 coord) {\n\n uint x = coord.x & 0x3FFu; // 10 bits for X coordinate\n uint y = coord.y & 0x3FFu; // 10 bits for Y coordinate\n uint z = coord.z & 0xFFFu; // 12 bits for Z coordinate\n\n // Pack into 32-bit integer\n uint packed =\n (x << 22u) |\n (y << 12u) |\n (z);\n\n // Decompose into RGBA8 format\n return vec4(\n float((packed >> 24u) & 0xFFu) / 255.0,\n float((packed >> 16u) & 0xFFu) / 255.0,\n float((packed >> 8u) & 0xFFu) / 255.0,\n float(packed & 0xFFu) / 255.0\n );\n}\n\n// ========================================\n// RESOLUTION AND ANCHOR POINT ACCESSORS\n// ========================================\n\n// Get anchor point for a specific resolution level\n// Anchor points define the origin of the page table for each resolution\n//\n// Parameters:\n// index - int: Resolution level index (0-9, where 0 is highest resolution)\n//\n// Returns:\n// uvec3: 3D anchor point coordinates in the page table, or (-1, -1, -1) if invalid\nuvec3 getAnchorPoint(int index) {\n if (index == 0) return anchor0;\n if (index == 1) return anchor1;\n if (index == 2) return anchor2;\n if (index == 3) return anchor3;\n if (index == 4) return anchor4;\n if (index == 5) return anchor5;\n if (index == 6) return anchor6;\n if (index == 7) return anchor7;\n if (index == 8) return anchor8;\n if (index == 9) return anchor9;\n return uvec3(-1, -1, -1);\n}\n\n// Find the lowest available resolution level\n// Returns the highest resolution index that has valid data\n//\n// Parameters:\n// None\n//\n// Returns:\n// int: Highest resolution level index (0-9) that has valid anchor point data\n// Returns 9 if no valid resolution levels are found\nint getLowestRes() {\n for (int i = 0; i < 10; i++) {\n if (getAnchorPoint(i) == uvec3(0,0,0)) {\n return i - 1;\n }\n }\n return 9;\n}\n\n// Get the downsample factor for a resolution level\n// Scale factors determine the voxel size at each resolution\n//\n// Parameters:\n// index - int: Resolution level index (0-9, where 0 is highest resolution)\n//\n// Returns:\n// vec3: Scale factors (x, y, z) for the resolution level, or (-1, -1, -1) if invalid\n// Higher scale factors indicate larger voxels (lower resolution)\nvec3 getScale(int index) {\n if (index == 0) return scale0;\n if (index == 1) return scale1;\n if (index == 2) return scale2;\n if (index == 3) return scale3;\n if (index == 4) return scale4;\n if (index == 5) return scale5;\n if (index == 6) return scale6;\n if (index == 7) return scale7;\n if (index == 8) return scale8;\n if (index == 9) return scale9;\n return vec3(-1.0, -1.0, -1.0);\n}\n\n// Get the resolution range for a color channel\n// Returns (min_res, max_res) for the channel\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// ivec2: Resolution range as (min_resolution_level, max_resolution_level)\n// Returns (-1, -1) if channel index is invalid\nivec2 getRes(int index) {\n if (index == 0) return res0;\n if (index == 1) return res1;\n if (index == 2) return res2;\n if (index == 3) return res3;\n if (index == 4) return res4;\n if (index == 5) return res5;\n if (index == 6) return res6;\n return ivec2(-1, -1);\n}\n\n// Get the min/max values (contrast limits) for a color channel\n// Returns (min_value, max_value) for normalization\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// vec2: Contrast limits as (min_value, max_value) for data normalization\n// Returns (-1.0, -1.0) if channel index is invalid\nvec2 getClim(int index) {\n if (index == 0) return clim0;\n if (index == 1) return clim1;\n if (index == 2) return clim2;\n if (index == 3) return clim3;\n if (index == 4) return clim4;\n if (index == 5) return clim5;\n if (index == 6) return clim6;\n return vec2(-1.0, -1.0);\n}\n\n// ========================================\n// COORDINATE TRANSFORMATIONS\n// ========================================\n\n// Convert normalized coordinates (0-1) to voxel coordinates.\n// get the voxel coordinate in the specified resolution from the normalized coordinate\n//\n// Parameters:\n// normalized - vec3: Normalized coordinates in range [0,1] for each axis\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Voxel coordinates in the volume space at the specified resolution\nvec3 getVoxelFromNormalized(vec3 normalized, int res) {\n vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution\n vec3 voxel = normalized * extents;\n return voxel;\n}\n\n// Convert voxel coordinates to normalized coordinates (0-1)\n// get the normalized coordinate based on the voxel coordinate in the specified resolution\n//\n// Parameters:\n// voxel - vec3: Voxel coordinates in the volume space\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Normalized coordinates in range [0,1] for each axis\nvec3 getNormalizedFromVoxel(vec3 voxel, int res) {\n vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution\n vec3 normalized = voxel / extents;\n return normalized;\n}\n\n// Convert normalized coordinates to brick coordinates.\n// get the brick coordinate in the specified resolution based on the normalized coordinate\n// needed for pagetable calculations\n//\n// Parameters:\n// normalized - vec3: Normalized coordinates in range [0,1] for each axis\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Brick coordinates (each brick is 32x32x32 voxels)\nvec3 getBrickFromNormalized(vec3 normalized, int res) {\n vec3 voxel = getVoxelFromNormalized(normalized, res);\n vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels\n return brick;\n}\n\n// Convert voxel coordinates to brick coordinates.\n// get the brick coordinate in the specified resolution based on the voxel coordinate\n//\n// Parameters:\n// voxel - vec3: Voxel coordinates in the volume space\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Brick coordinates (each brick is 32x32x32 voxels)\nvec3 getBrickFromVoxel(vec3 voxel, int res) {\n vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels\n return brick;\n}\n\n// ========================================\n// CHANNEL-SPECIFIC ACCESSORS\n// ========================================\n\n// Get channel offset in page table.\n// get the vector for the specified channel slot in the pagetable\n// Different channels are stored at different Z-offsets in the page table\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// uvec3: 3D offset coordinates for the channel in the page table\nuvec3 getChannelOffset(int index) {\n if (index == 0) return uvec3(0, 0, 1);\n if (index == 1) return uvec3(0, 1, 0);\n if (index == 2) return uvec3(0, 1, 1);\n if (index == 3) return uvec3(1, 0, 0);\n if (index == 4) return uvec3(1, 0, 1);\n if (index == 5) return uvec3(1, 1, 0);\n if (index == 6) return uvec3(1, 1, 1);\n return uvec3(0, 0, 0);\n}\n\n// Get color for a channel.\n// get the color per color channel\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// vec3: RGB color values for the specified channel\nvec3 getChannelColor(int index) {\n if (index == 0) return color0.xyz;\n if (index == 1) return color1.xyz;\n if (index == 2) return color2.xyz;\n if (index == 3) return color3.xyz;\n if (index == 4) return color4.xyz;\n if (index == 5) return color5.xyz;\n if (index == 6) return color6.xyz;\n return vec3(0.0, 0.0, 0.0);\n}\n\n// Get opacity for a channel\n// get the opacity (used as visibility) per color channel\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// float: Opacity value (0.0-1.0) for the specified channel\nfloat getChannelOpacity(int index) {\n if (index == 0) return color0.w;\n if (index == 1) return color1.w;\n if (index == 2) return color2.w;\n if (index == 3) return color3.w;\n if (index == 4) return color4.w;\n if (index == 5) return color5.w;\n if (index == 6) return color6.w;\n return 0.0;\n}\n\n// ========================================\n// PAGE TABLE DECODING\n// ========================================\n\n/**\n * retrieving the brick based on:\n * location -- normalized coordinate\n * targetRes -- target resolution\n * channel -- physical channel slot\n * rnd -- random number for jittering requests \n * query -- whether to query the brick (we dont query for interblock interpolation)\n * colorIndex -- color index for querying the min max values\n * \n * returns:\n * w >= 0 -- xyz contains brick cache coordinate, w stores resolution\n * w == -1 -- not resident in any resolution, should be treated as empty\n * w == -2 -- empty (with respect to current transfer function)\n * w == -3 -- constant full (with respect to current transfer function)\n * w == -4 -- constant value within range, x stores that value\n *\n * bit layout:\n * [1] 31 | 0 — flag resident\n * [1] 30 | 1 — flag init\n * [7] 23…29 | 2…8 — min → 128\n * [7] 16…22 | 9…15 — max → 128\n * [6] 10…15 | 16…21 — x offset in brick cache → max 64\n * [6] 4…9 | 22…27 — y offset in brick cache → max 64\n * [4] 0…3 | 28…31 — z offset in brick cache → max 16, effectively 4\n*/\n\n/*\nPage table entry format (32 bits):\n[31] | 0 — flag resident (1=loaded in cache)\n[30] | 1 — flag init (1=initialized)\n[29:23] | 2…8 — min value (7 bits) → 128 levels\n[22:16] | 9…15 — max value (7 bits) → 128 levels \n[15:10] | 16…21 — x offset in brick cache (6 bits) → 64 bricks\n[9:4] | 22…27 — y offset in brick cache (6 bits) → 64 bricks\n[3:0] | 28…31 — z offset in brick cache (4 bits) → 16 bricks\n*/\n\n// Query page table to find brick location and status\n// Searches for a brick at the specified location across multiple resolution levels\n// and returns its cache coordinates and status information\n//\n// Parameters:\n// location - vec3: Normalized coordinates (0-1) within the volume\n// targetRes - int: Target resolution level to start searching from\n// channel - int: Channel index (0-6) to query\n// rnd - float: Random value (0-1) used for brick loading request selection\n// query - bool: Whether to allow brick loading requests (true) or just query (false)\n// colorIndex - int\n//\n// Returns:\n// ivec4: (x_offset, y_offset, z_offset, status) where:\n// - x_offset, y_offset, z_offset: Brick cache coordinates if found\n// - status: Resolution level (>=0) if found, or status code:\n// * -1: Not found at any resolution level\n// * -2: Empty brick (all values below threshold)\n// * -3: Constant full brick (all values above threshold)\n// * -4: Constant value brick (uniform value)\n// add maxres here\nivec4 getBrickLocation(vec3 location, int targetRes, int channel, float rnd, bool query, int colorIndex) {\n\n // min max for current color \n vec2 clim = getClim(colorIndex);\n\n // resolution ranges, TODO: connect this back to color\n int channelMin = getRes(channel).x;\n int channelMax = getRes(channel).y;\n\n // Clamp resolution to channel's available range\n int currentRes = clamp(targetRes, channelMin, channelMax);\n currentRes = clamp(currentRes, resGlobal.x, resGlobal.y);\n int lowestRes = clamp(resGlobal.y, channelMin, channelMax);\n\n // Determine if this channel should request brick loading.\n // request the current channel based on probability\n bool requestChannel = false;\n if (int(floor(rnd * float(maxChannels))) == colorIndex) {\n requestChannel = true;\n }\n\n // Try progressively lower resolutions until we find data.\n // loop through resolutions\n while (currentRes <= lowestRes) {\n\n // Calculate page table coordinates for this brick\n uvec3 anchorPoint = getAnchorPoint(currentRes);\n vec3 brickLocation = getBrickFromNormalized(location, currentRes);\n uvec3 channelOffset = getChannelOffset(channel);\n vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;\n \n // Special handling for resolution 0 (highest detail)\n if (currentRes == 0) {\n int zExtent = int(ceil(float(voxelExtents.z) / 32.0));\n coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;\n }\n\n // Query the page table.\n // get PT entry\n uint ptEntry = texelFetch(pageTableTex, ivec3(coordinate), 0).r;\n\n // Check if brick is initialized.\n // check if the PT entry is initialized\n uint isInit = (ptEntry >> 30u) & 1u;\n if (isInit == 0u) { \n currentRes++; \n // Request brick loading if needed\n if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {\n gRequest = packPTCoordToRGBA8(uvec3(coordinate));\n }\n continue;\n }\n \n // Extract min/max values from page table entry.\n // get the min max values of the brick\n uint umin = ((ptEntry >> 23u) & 0x7Fu);\n uint umax = ((ptEntry >> 16u) & 0x7Fu);\n float min = float(int(umin)) / 127.0;\n float max = float(int(umax)) / 127.0;\n \n // Check if brick is empty (all values below threshold).\n // exit early if brick is constant\n if (float(max) <= clim.x) {\n return ivec4(0,0,0,-2);\n // EMPTY\n } else if (float(min) >= clim.y) {\n return ivec4(0,0,0,-3); // CONSTANT FULL\n } else if ((umax - umin) < 2u) {\n return ivec4(min,0,0,-4); // CONSTANT OTHER VALUE\n }\n \n // Check if brick is resident in cache.\n // return brick cache location if resident\n // continue to next resolution if not resident\n uint isResident = (ptEntry >> 31u) & 1u;\n if (isResident == 0u) {\n currentRes++;\n // Request brick loading if needed\n if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {\n gRequest = packPTCoordToRGBA8(uvec3(coordinate));\n }\n continue;\n } else {\n // Extract brick cache coordinates\n uint xBrickCache = (ptEntry >> 10u) & 0x3Fu;\n uint yBrickCache = (ptEntry >> 4u) & 0x3Fu;\n uint zBrickCache = ptEntry & 0xFu;\n uvec3 brickCacheCoord = uvec3(xBrickCache, yBrickCache, zBrickCache);\n\n return ivec4(brickCacheCoord, currentRes);\n }\n }\n\n // not resident in any resolution, should be treated as empty\n return ivec4(0,0,0,-1); // Not found\n}\n\n// Request brick loading for a specific location and resolution.\n// Initiates a request to load a brick from disk into the brick cache.\n// set the brick request for the specified slot channel\n//\n// Parameters:\n// location - vec3: Normalized world space coordinates (0.0 to 1.0) where the brick is needed\n// targetRes - int: Target resolution level (0-9, where 0 is highest resolution)\n// channel - int: Channel index (0-6) for multi-channel datasets\n// rnd - float: Random value between 0.0 and 1.0 used for load balancing across channels\n//\n// Returns:\n// void: No return value, but sets gRequest output variable if conditions are met\nvoid setBrickRequest(vec3 location, int targetRes, int channel, float rnd) {\n uvec3 anchorPoint = getAnchorPoint(targetRes);\n vec3 brickLocation = getBrickFromNormalized(location, targetRes);\n uvec3 channelOffset = getChannelOffset(channel);\n vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;\n \n // Special handling for resolution 0\n if (targetRes == 0) {\n int zExtent = int(ceil(float(voxelExtents.z) / 32.0));\n coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;\n }\n \n // Pack coordinates and set request\n if (int(floor(rnd * float(maxChannels))) == channel) {\n gRequest = packPTCoordToRGBA8(uvec3(coordinate));\n }\n}\n\n// Track brick usage for cache management.\n// Records which brick in the cache is being accessed for LRU (Least Recently Used) eviction.\n// set the usage for the specified brick\n//\n// Parameters:\n// brickCacheOffset - ivec3: 3D coordinates of the brick within the brick cache texture\n// t_hit_min_os - float: Ray entry time in object space (start of ray-volume intersection)\n// t_hit_max_os - float: Ray exit time in object space (end of ray-volume intersection)\n// t_os - float: Current sampling position along the ray in object space\n// rnd - float: Random value between 0.0 and 1.0 used for probabilistic usage tracking\n//\n// Returns:\n// void: No return value, but sets gUsage output variable to track cache access patterns\nvoid setUsage(ivec3 brickCacheOffset, float t_hit_min_os, float t_hit_max_os, float t_os, float rnd) {\n float normalized_t_os = (t_os - t_hit_min_os) / (t_hit_max_os - t_hit_min_os); // Normalize to 0-1\n if (normalized_t_os <= rnd || gUsage == vec4(0.0, 0.0, 0.0, 0.0)) {\n gUsage = vec4(vec3(brickCacheOffset) / 255.0, 1.0);\n }\n}\n\n// ========================================\n// UTILITY FUNCTIONS\n// ========================================\n\n// Get maximum component of a 3D vector.\n// get the max value of a vec3\n//\n// Parameters:\n// v - vec3: Input 3D vector\n//\n// Returns:\n// float: The maximum value among the x, y, and z components\nfloat vec3_max(vec3 v) {\n return max(v.x, max(v.y, v.z));\n}\n\n// Get minimum component of a 3D vector.\n// get the min value of a vec3\n//\n// Parameters:\n// v - vec3: Input 3D vector\n//\n// Returns:\n// float: The minimum value among the x, y, and z components\nfloat vec3_min(vec3 v) {\n return min(v.x, min(v.y, v.z));\n}\n\n// Calculate level-of-detail based on distance.\n// Uses logarithmic scaling to determine appropriate resolution level.\n// get the LOD based on the distance to the camera\n//\n// Parameters:\n// distance - float: Distance from camera to sampling point\n// highestRes - int: Highest available resolution level (typically 0)\n// lowestRes - int: Lowest available resolution level (typically 9)\n// lodFactor - float: Scaling factor that controls LOD sensitivity\n//\n// Returns:\n// int: Resolution level index (0-9) where 0 is highest resolution\nint getLOD(float distance, int highestRes, int lowestRes, float lodFactor) {\n int lod = int(log2(distance * lodFactor));\n return clamp(lod, highestRes, lowestRes);\n}\n\n// Calculate step size for ray marching at a given resolution.\n// Determines optimal sampling step size based on voxel dimensions and ray direction.\n// get the voxel step in object space\n//\n// Parameters:\n// res - int: Resolution level index (0-9, where 0 is highest resolution)\n// osDir - vec3: Ray direction vector in object space (should be normalized)\n//\n// Returns:\n// float: Optimal step size in object space units for stable ray marching\nfloat voxelStepOS(int res, vec3 osDir) {\n vec3 voxelSize = getScale(res) / vec3(voxelExtents);\n vec3 dt_vec = voxelSize / abs(osDir);\n return min(dt_vec.x, min(dt_vec.y, dt_vec.z));\n}\n\n// ========================================\n// INTERPOLATION FUNCTIONS\n// ========================================\n\n// Linear interpolation between two values.\n// Performs smooth interpolation between two scalar values.\n//\n// Parameters:\n// v0 - float: First value to interpolate from\n// v1 - float: Second value to interpolate to\n// fx - float: Interpolation factor between 0.0 and 1.0\n//\n// Returns:\n// float: Interpolated value between v0 and v1 based on fx\nfloat lerp(float v0, float v1, float fx) {\n return mix(v0, v1, fx); // (1-fx)·v0 + fx·v1\n}\n\n// Bilinear interpolation between four values\n// Performs 2D interpolation using four corner values arranged in a square\n//\n// Parameters:\n// v00 - float: Bottom-left corner value\n// v10 - float: Bottom-right corner value\n// v01 - float: Top-left corner value\n// v11 - float: Top-right corner value\n// f - vec2: 2D interpolation factors (x, y) between 0.0 and 1.0\n//\n// Returns:\n// float: Interpolated value from the four corner values\nfloat bilerp(float v00, float v10, float v01, float v11, vec2 f) {\n float c0 = mix(v00, v10, f.x); // Interpolate in X on bottom row\n float c1 = mix(v01, v11, f.x); // Interpolate in X on top row\n return mix(c0, c1, f.y); // Now interpolate those in Y\n}\n\n// Trilinear interpolation between eight values\n// Performs 3D interpolation using eight corner values arranged in a cube\n//\n// Parameters:\n// v000 - float: Bottom-left-back corner value\n// v100 - float: Bottom-right-back corner value\n// v010 - float: Bottom-left-front corner value\n// v110 - float: Bottom-right-front corner value\n// v001 - float: Top-left-back corner value\n// v101 - float: Top-right-back corner value\n// v011 - float: Top-left-front corner value\n// v111 - float: Top-right-front corner value\n// f - vec3: 3D interpolation factors (x, y, z) between 0.0 and 1.0\n//\n// Returns:\n// float: Interpolated value from the eight corner values\nfloat trilerp(\n float v000, float v100, float v010, float v110,\n float v001, float v101, float v011, float v111,\n vec3 f) { // f = fract(coord)\n // Interpolate along X for each of the four bottom-face voxels\n float c00 = mix(v000, v100, f.x);\n float c10 = mix(v010, v110, f.x);\n float c01 = mix(v001, v101, f.x);\n float c11 = mix(v011, v111, f.x);\n\n // Interpolate those along Y\n float c0 = mix(c00, c10, f.y);\n float c1 = mix(c01, c11, f.y);\n\n // Final interpolation along Z\n return mix(c0, c1, f.z);\n}\n\n// ========================================\n// BRICK CACHE SAMPLING\n// ========================================\n\n// Sample a value from the brick cache texture.\n// Converts brick coordinates and voxel position to texture coordinates for sampling.\n// sample the brick cache based on the brick cache coordinate and the in-brick coordinate\n//\n// Parameters:\n// brickCacheCoord - vec3: 3D coordinates of the brick within the brick cache\n// voxelInBrick - vec3: 3D coordinates of the voxel within the brick (0-31 in each dimension)\n//\n// Returns:\n// float: Sampled voxel value from the brick cache texture (typically normalized 0.0-1.0)\nfloat sampleBrick(vec3 brickCacheCoord, vec3 voxelInBrick) {\n vec3 brickCacheCoordNormalized = vec3(\n (float(brickCacheCoord.x) * BRICK_SIZE + float(voxelInBrick.x)) / BRICK_CACHE_SIZE_X,\n (float(brickCacheCoord.y) * BRICK_SIZE + float(voxelInBrick.y)) / BRICK_CACHE_SIZE_Y,\n (float(brickCacheCoord.z) * BRICK_SIZE + float(voxelInBrick.z)) / BRICK_CACHE_SIZE_Z\n );\n return texture(brickCacheTex, brickCacheCoordNormalized).r;\n}\n\n/**\n * main renderloop\n*/\nvoid main(void) {\n\n // ========================================\n // INITIALIZATION\n // ========================================\n \n // Initialize all render targets (multiple output textures)\n gRequest = vec4(0,0,0,0); // Brick loading requests\n gUsage = vec4(0,0,0,0); // Brick usage tracking\n gColor = vec4(0.0, 0.0, 0.0, 0.0); // Final color output\n\n // out color sums up our accumulated value before writing it into the gColor buffer\n vec4 outColor = vec4(0.0, 0.0, 0.0, 0.0); // Accumulated color\n\n // Generate random number for jittered sampling (reduces artifacts)\n float rnd = random();\n\n // Get the lowest available resolution level\n int lowestDataRes = getLowestRes();\n\n // ========================================\n // RAY-VOLUME INTERSECTION\n // ========================================\n \n // Normalize the view ray direction\n vec3 ws_rayDir = normalize(rayDirUnnorm);\n \n // Calculate intersection with volume bounding box\n // Returns (entry_time, exit_time) for the ray-box intersection\n vec2 t_hit = intersect_hit(cameraCorrected, ws_rayDir);\n if (t_hit.x >= t_hit.y) { discard; } // Ray misses volume entirely\n \n t_hit.x = max(t_hit.x, 0.0); // Clamp entry to 0 (no negative distances)\n float t = t_hit.x;\n \n // Calculate distance from camera for LOD selection\n float distance = abs((cameraCorrected / boxSize).z + (ws_rayDir / boxSize).z * t );\n\n // ========================================\n // COORDINATE SPACE CONVERSION\n // ========================================\n \n // Convert from world space to object space (normalized 0-1 coordinates)\n float ws2os = length(ws_rayDir / boxSize); // Scale factor for conversion\n float t_hit_min_os = t_hit.x * ws2os; // Entry point in object space\n float t_hit_max_os = t_hit.y * ws2os; // Exit point in object space\n float t_os = t_hit_min_os; // Current position in object space\n\n // Calculate effective LOD factor based on volume size.\n // voxel edge is the max extent of the volume\n float voxelEdge = float(max(voxelExtents.x, max(voxelExtents.y, voxelExtents.z)));\n\n // calculate LOD factor based on the voxel edge\n float lodFactorEffective = lodFactor * voxelEdge / 256.0;\n\n // ========================================\n // RESOLUTION AND SAMPLING SETUP\n // ========================================\n \n // Determine target resolution based on distance (LOD)\n int targetRes = getLOD(t, 0, 9, lodFactorEffective);\n \n // Set adaptive stepping resolution\n int stepResAdaptive = renderRes;\n int stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);\n\n // Convert ray to object space coordinates\n vec3 os_rayDir = normalize(ws_rayDir / boxSize);\n vec3 os_rayOrigin = cameraCorrected / boxSize + vec3(0.5);\n \n // Calculate step size based on current resolution\n float dt = voxelStepOS(stepResEffective, os_rayDir);\n\n // ========================================\n // SAMPLING POSITION INITIALIZATION\n // ========================================\n \n // Convert to normalized sampling coordinates (0-1 range)\n vec3 p = cameraCorrected + t_hit.x * ws_rayDir;\n p = p / boxSize + vec3(0.5); // Transform to 0-1 range\n \n // Calculate step vector in normalized space\n vec3 dp = (os_rayDir * dt);\n \n // Apply jittered sampling to reduce artifacts\n p += dp * (rnd);\n // Avoid boundary issues\n p = clamp(p, 0.0 + 0.0000028, 1.0 - 0.0000028);\n\n // ========================================\n // RENDERING VARIABLES\n // ========================================\n \n // Color accumulation for front-to-back compositing.\n // color accumulation variables, are calculated per 'slice'\n vec3 rgbCombo = vec3(0.0);\n float total = 0.0;\n\n // For alpha blending.\n // alpha accumulation variable runs globally\n float alphaMultiplicator = 1.0;\n\n // Request tracking (for brick loading).\n // if we have a request for a brick which not visible in lower\n // resolutions, we can overwrite it once\n bool overWrittenRequest = false;\n\n // Current state tracking\n vec3 currentTargetResPTCoord = vec3(0,0,0);\n int currentLOD = targetRes;\n\n // ========================================\n // CHANNEL-SPECIFIC CONSTANTS\n // ========================================\n \n // Pre-compute channel properties for efficiency.\n // constants per color channel\n vec3 [] c_color = vec3[7](getChannelColor(0), getChannelColor(1), getChannelColor(2), getChannelColor(3), getChannelColor(4), getChannelColor(5), getChannelColor(6));\n float [] c_opacity = float[7](getChannelOpacity(0), getChannelOpacity(1), getChannelOpacity(2), getChannelOpacity(3), getChannelOpacity(4), getChannelOpacity(5), getChannelOpacity(6));\n // resolution ranges (currently) per color channel\n // TODO: figure out how to hook it up with frontend\n 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);\n 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);\n \n // ========================================\n // PER-CHANNEL STATE ARRAYS\n // ========================================\n \n // Current state for each channel.\n // current state variables per color channel\n \n // current resolution\n int [] c_res_current = int[7](0,0,0,0,0,0,0);\n // current value\n float [] c_val_current = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);\n // current brick cache coordinate\n 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));\n // current voxel in current resolution\n 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));\n // current pagetable coordinate\n 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));\n // current render mode -- 0: empty (add 0), 1: constant (add current val), 2: voxel (query new voxel)\n // upon change of PT we re-query anyways\n int [] c_renderMode_current = int[7](-1, -1, -1, -1, -1, -1, -1);\n \n // Adjacent brick caching for interpolation\n // current pagetable coordinate of the adjacent bricks in X, Y, Z or diagonal direction\n 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));\n 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));\n 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));\n 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));\n // corresponding brick coordinates of the adjacent bricks in X, Y, Z or diagonal direction\n 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));\n 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));\n 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));\n 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));\n\n // Min/max tracking for MIP/MinIP rendering.\n // min and max values of the current color\n // used for minimum/maximum intensity projection\n float [] c_minVal = float[7](-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0);\n float [] c_maxVal = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);\n\n // Per-resolution coordinate tracking\n 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));\n 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));\n 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));\n 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));\n\n // resolution changed flag\n bool resolutionChanged = false;\n // number of repetitions (for debugging purposes)\n int reps = 0;\n\n // ========================================\n // MAIN RAY-MARCHING LOOP\n // ========================================\n \n // while we are 'in' the volume.\n // Continue marching until we exit the volume or reach maximum opacity.\n while (t_os < t_hit_max_os && t_os >= t_hit_min_os\n && vec3_max(p) < 1.0 && vec3_min(p) >= 0.0\n ) {\n\n // Reset per-sample accumulation.\n // initialize slice values\n vec3 rgbCombo = vec3(0.0);\n float total = 0.0;\n\n // Update target resolution based on current distance (with jitter).\n // calculate target resolution based on distance and lod factor\n targetRes = getLOD(t, 0, 9, lodFactorEffective * (0.999 + 0.002 * rnd));\n\n // ========================================\n // RESOLUTION CHANGE HANDLING\n // ========================================\n \n // if target resolution changed, update the current resolution and stepsize\n if (targetRes != currentLOD) {\n currentLOD = targetRes;\n stepResAdaptive++;\n stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);\n \n // Adjust sampling position for new resolution\n p -= dp * rnd;\n dt = voxelStepOS(stepResEffective, os_rayDir);\n dp = os_rayDir * dt;\n p += dp * rnd;\n resolutionChanged = true;\n\n // Check bounds after resolution change\n 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) {\n break;\n }\n } else {\n resolutionChanged = false;\n }\n\n // ========================================\n // UPDATE COORDINATES FOR ALL RESOLUTIONS\n // ========================================\n \n // Calculate page table coordinates and voxel positions for all resolution levels\n for (int r = 0; r < 10; r++) {\n r_prevPTCoord[r] = r_ptCoord[r];\n r_prevVoxel[r] = r_voxel[r];\n r_ptCoord[r] = getBrickFromNormalized(p, r);\n r_voxel[r] = getVoxelFromNormalized(p, r);\n }\n\n // ========================================\n // RENDER MODE DEFINITIONS\n // ========================================\n // 0: empty brick (no data)\n // 1: constant brick (uniform value)\n // 2: voxel brick (variable data)\n\n // initialize the per channel slice values\n vec3 sliceColor = vec3(0.0);\n float sliceAlpha = 0.0;\n\n // ========================================\n // MULTI-CHANNEL SAMPLING\n // ========================================\n \n // Process each channel independently.\n // iterate over up to 7 channels by color\n for (int c = 0; c < 7; c++) {\n // Skip channels with zero opacity.\n // skip if opacity is 0 or if color is not mapped to a physical slot\n if (c_opacity[c] <= 0.000001) {\n continue;\n } else if (channelMapping[c] == -1) {\n continue;\n }\n\n // physical slot in pagetable\n int slot = channelMapping[c];\n\n // keep track of status\n bool newBrick = false;\n bool newVoxel = false;\n // best possible resolution\n int bestRes = clamp(targetRes, c_res_min[c], c_res_max[c]);\n\n // Check if we need to load a new brick at a better resolution.\n // check if any new better resolution could be available, if so, we need to re-query the brick\n bool betterResChanged = false;\n for (int r = bestRes; r <= c_res_current[c]; r++ ) {\n if (r_ptCoord[r] != r_prevPTCoord[r]) {\n betterResChanged = true;\n break;\n }\n }\n\n // Determine if we need to load new brick data.\n // check if we need to re-query the brick / voxel or reuse past 'val'\n if (r_ptCoord[bestRes] != r_prevPTCoord[bestRes]\n || c_renderMode_current[c] == -1\n || resolutionChanged == true\n || betterResChanged\n ) {\n newBrick = true;\n newVoxel = true;\n } else if (c_renderMode_current[c] == 2) {\n newVoxel = true;\n } else if (c_renderMode_current[c] == 0) {\n continue; // Skip empty bricks\n }\n\n // ========================================\n // BRICK LOADING AND CACHING\n // ========================================\n \n // check if a new brick is available in the best possible resolution\n if (newBrick) {\n // Query page table for brick location and status\n ivec4 brickCacheInfo = getBrickLocation(p, bestRes, slot, rnd, true, c);\n // check information about the newly queried brick\n // if the res is not at best res, possibly the same as previous brick\n if (brickCacheInfo.w == -1 || brickCacheInfo.w == -2) {\n // Empty brick - no data available.\n // we can skip the rest of the loop\n c_val_current[c] = 0.0;\n c_renderMode_current[c] = 0;\n c_minVal[c] = 0.0;\n continue;\n } else if (brickCacheInfo.w == -3) {\n // Solid brick - constant maximum value.\n // we set the value and do not need to query a voxel\n c_val_current[c] = 1.0;\n c_renderMode_current[c] = 1;\n c_maxVal[c] = 1.0;\n newVoxel = false;\n } else if (brickCacheInfo.w == -4) {\n // Constant brick - uniform value.\n // static value -- we set the value and do not need to query a voxel\n float val = float(brickCacheInfo.x);\n c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));\n c_renderMode_current[c] = 1;\n newVoxel = false;\n } else if (brickCacheInfo.w >= 0) {\n // Voxel brick - variable data, load from cache.\n // new brick -- we set the coordinate and resolution and need to query a voxel\n c_res_current[c] = brickCacheInfo.w;\n c_ptCoord_current[c] = r_ptCoord[c_res_current[c]];\n c_brickCacheCoord_current[c] = vec3(brickCacheInfo.xyz);\n c_renderMode_current[c] = 2;\n newVoxel = true;\n \n // Track brick usage for cache management.\n // we set the usage of the brick based on the channel and the relative distance into the cube\n if (int(floor(rnd * float(maxChannels))) == c) {\n setUsage(brickCacheInfo.xyz, t_hit_min_os, t_hit_max_os, t_os, rnd);\n }\n }\n }\n \n // ========================================\n // VOXEL SAMPLING WITH INTERPOLATION\n // ========================================\n \n // we need to query a new voxel e.g. sample the brick cache\n if (newVoxel) {\n c_voxel_current[c] = r_voxel[c_res_current[c]];\n\n // we clamp the coordinate to be inside the brick and sample the volume\n reps++;\n \n // Calculate position within the brick (0-31 range)\n vec3 voxelInBrick = mod(c_voxel_current[c], 32.0);\n vec3 clampedVoxelInBrick = clamp(voxelInBrick, 0.5, 31.5);\n // Sample the brick cache texture\n float val = sampleBrick(c_brickCacheCoord_current[c].xyz, clampedVoxelInBrick);\n\n // ========================================\n // HIGH-QUALITY INTERPOLATION (renderRes == 0)\n // ========================================\n \n // interblock interpolation\n if (renderRes == 0) {\n // calculate what axis we need to interpolate\n \n // Check if we're near brick boundaries (need interpolation)\n bvec3 clampedMin = lessThan(voxelInBrick, clampedVoxelInBrick);\n bvec3 clampedMax = greaterThan(voxelInBrick, clampedVoxelInBrick);\n bvec3 clamped = bvec3(clampedMin.x || clampedMax.x, clampedMin.y || clampedMax.y, clampedMin.z || clampedMax.z);\n vec3 diff = voxelInBrick - clampedVoxelInBrick;\n \n if (any(clampedMin) || any(clampedMax)) { \n int boundaryAxes = int(clamped.x) + int(clamped.y) + int(clamped.z);\n float f = 0.0;\n \n if (boundaryAxes == 1) {\n // Linear interpolation across one boundary\n vec3 otherGlobalVoxelPos = vec3(0,0,0);\n vec3 otherP = vec3(0,0,0);\n float otherVoxelVal = 0.0;\n\n // Determine which axis we're interpolating across\n if (clampedMin.x) {\n otherGlobalVoxelPos = c_voxel_current[c] - vec3(1.0, 0.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.x);\n } else if (clampedMax.x) {\n otherGlobalVoxelPos = c_voxel_current[c] + vec3(1.0, 0.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.x);\n } else if (clampedMin.y) {\n otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 1.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.y);\n } else if (clampedMax.y) {\n otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 1.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.y);\n } else if (clampedMin.z) {\n otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 0.0, 1.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.z);\n } else if (clampedMax.z) {\n otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 0.0, 1.0); \n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.z);\n }\n\n // Sample the neighboring voxel\n vec3 otherPTcoord = getBrickFromNormalized(otherP, c_res_current[c]);\n otherPTcoord = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0);\n otherVoxelInBrick -= diff;\n\n // Check if neighbor is outside volume bounds\n 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) {\n otherVoxelVal = val;\n } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) {\n // Use cached adjacent brick\n otherVoxelVal = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick);\n } else {\n // Load new adjacent brick\n ivec4 otherBrickCacheInfo = ivec4(-1);\n if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz) {\n otherBrickCacheInfo = ivec4(c_brick_XYZ_adjacent[c]);\n } else {\n otherBrickCacheInfo = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c); \n }\n if (otherBrickCacheInfo.w == -1 || otherBrickCacheInfo.w == -2) {\n otherVoxelVal = val;\n } else if (otherBrickCacheInfo.w == -3) {\n otherVoxelVal = 1.0;\n } else if (otherBrickCacheInfo.w == -4) {\n otherVoxelVal = float(otherBrickCacheInfo.x);\n } else {\n // TODO: we do not recalculate the voxelInBrick based on the resolution\n otherVoxelVal = sampleBrick(vec3(otherBrickCacheInfo.xyz), otherVoxelInBrick);\n }\n c_PT_XYZ_adjacent[c] = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]); \n c_brick_XYZ_adjacent[c] = vec4(otherBrickCacheInfo);\n }\n \n // Perform linear interpolation\n float originalVal = val;\n val = lerp(originalVal, otherVoxelVal, f); \n } else if (boundaryAxes == 2) {\n // Bilinear interpolation across two boundaries\n vec3 offA = vec3(0.0);\n vec3 offB = vec3(0.0);\n vec2 f = vec2(0.0);\n\n // Determine which two axes we're interpolating across\n if (clamped.x && clamped.y) {\n offA.x = clampedMin.x ? -1.0 : 1.0;\n offB.y = clampedMin.y ? -1.0 : 1.0;\n f = vec2(abs(diff.x), abs(diff.y));\n } else if (clamped.x && clamped.z) {\n offA.x = clampedMin.x ? -1.0 : 1.0;\n offB.z = clampedMin.z ? -1.0 : 1.0;\n f = vec2(abs(diff.x), abs(diff.z));\n } else if (clamped.y && clamped.z) {\n offA.y = clampedMin.y ? -1.0 : 1.0;\n offB.z = clampedMin.z ? -1.0 : 1.0;\n f = vec2(abs(diff.y), abs(diff.z));\n }\n\n // Macro for sampling at offset positions\n #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); } } } }\n \n // Sample the four corners for bilinear interpolation\n float v00 = val;\n float v10; float v01; float v11;\n SAMPLE_AT_OFFSET(offA, v10);\n SAMPLE_AT_OFFSET(offB, v01);\n SAMPLE_AT_OFFSET(offA + offB, v11);\n\n val = bilerp(v00, v10, v01, v11, f);\n\n #undef SAMPLE_AT_OFFSET\n\n } else if (boundaryAxes == 3) {\n // Trilinear interpolation across all three boundaries\n \n vec3 offA = vec3(0.0);\n vec3 offB = vec3(0.0);\n vec3 offC = vec3(0.0);\n vec3 f = vec3(0.0);\n\n offA.x = clampedMin.x ? -1.0 : 1.0;\n offB.y = clampedMin.y ? -1.0 : 1.0;\n offC.z = clampedMin.z ? -1.0 : 1.0;\n\n f = vec3(abs(diff.x), abs(diff.y), abs(diff.z));\n\n // Macro for sampling at offset positions\n #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); } } } }\n \n // Sample all eight corners for trilinear interpolation\n float v000 = val;\n float v100; float v010; float v001; float v110; float v101; float v011; float v111;\n SAMPLE_AT_OFFSET(offA, v100);\n SAMPLE_AT_OFFSET(offB, v010);\n SAMPLE_AT_OFFSET(offC, v001);\n SAMPLE_AT_OFFSET(offA + offB, v110);\n SAMPLE_AT_OFFSET(offA + offC, v101);\n SAMPLE_AT_OFFSET(offB + offC, v011);\n SAMPLE_AT_OFFSET(offA + offB + offC, v111);\n\n val = trilerp(v000, v100, v010, v001, v110, v101, v011, v111, f);\n\n #undef SAMPLE_AT_OFFSET\n \n }\n\n } else {\n // No boundary interpolation needed - clear adjacent brick cache.\n // no adjacent bricks -> reset the adjacent trackers\n c_PT_X_adjacent[c] = c_PT_Y_adjacent[c] = c_PT_Z_adjacent[c] = c_PT_XYZ_adjacent[c] = vec3(-1.0);\n c_brick_X_adjacent[c] = c_brick_Y_adjacent[c] = c_brick_Z_adjacent[c] = c_brick_XYZ_adjacent[c] = vec4(-1.0);\n }\n }\n\n // ========================================\n // VALUE NORMALIZATION AND TRACKING\n // ========================================\n \n // Normalize value to 0-1 range using channel-specific contrast limits.\n // we normalize the (accumulated) value to the range of the color channel\n c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));\n\n // Track min/max values for MIP/MinIP rendering.\n // update the min and max values for the min/max projection\n if (c_minVal[c] == -1.0) {\n c_minVal[c] = c_val_current[c];\n } else {\n c_minVal[c] = min(c_minVal[c], c_val_current[c]);\n }\n c_maxVal[c] = max(c_maxVal[c], c_val_current[c]);\n\n }\n\n // ========================================\n // BRICK REQUEST GENERATION\n // ========================================\n \n // Request higher resolution bricks if we're using lower resolution than optimal.\n // potentially overwrite brick request\n if (!overWrittenRequest \n && c_res_current[c] != bestRes\n && c_val_current[c] > 0.0\n && c_renderMode_current[c] == 2\n && int(floor(rnd * float(maxChannels))) == c) {\n setBrickRequest(p, bestRes, slot, rnd);\n overWrittenRequest = true;\n }\n\n // ========================================\n // CHANNEL COMPOSITING\n // ========================================\n \n // Accumulate this channel's contribution.\n // sum up the values onto the slice values\n total += c_val_current[c];\n rgbCombo += c_val_current[c] * c_color[c];\n\n }\n\n // ========================================\n // FRONT-TO-BACK COMPOSITING\n // ========================================\n \n // Clamp total intensity and calculate alpha.\n // add the calculated slice to the total color\n total = clamp(total, 0.0, 1.0);\n sliceAlpha = total * opacity * dt * 32.0; // Scale by step size and brick size\n sliceColor = rgbCombo;\n\n // Front-to-back alpha blending\n outColor.rgb += sliceAlpha * alphaMultiplicator * sliceColor;\n outColor.a += sliceAlpha * alphaMultiplicator;\n alphaMultiplicator *= (1.0 - sliceAlpha);\n\n // Early termination for opaque regions (standard rendering only).\n // check if we can exit early\n if (outColor.a > 0.99 && u_renderstyle == 0) { break; }\n\n // ========================================\n // ADVANCE RAY POSITION\n // ========================================\n \n // Move to next sample position\n t += dt;\n p += dp;\n t_os += dt;\n }\n\n // ========================================\n // RENDERING STYLE POST-PROCESSING\n // ========================================\n \n if (u_renderstyle == 1) { \n // Minimum Intensity Projection (MinIP)\n // Shows the minimum value encountered along each ray\n outColor = vec4(0.0);\n for (int c = 0; c < 7; c++) {\n if (c_color[c] != vec3(0.0, 0.0, 0.0)) {\n outColor.rgb += c_minVal[c] * c_color[c];\n outColor.a += c_minVal[c];\n }\n }\n } else if (u_renderstyle == 0) { \n // Maximum Intensity Projection (MIP)\n // Shows the maximum value encountered along each ray\n outColor = vec4(0.0);\n for (int c = 0; c < 7; c++) {\n if (c_color[c] != vec3(0.0, 0.0, 0.0)) {\n outColor.rgb += c_maxVal[c] * c_color[c];\n }\n }\n outColor.a = 1.0;\n }\n\n // ========================================\n // FINAL OUTPUT\n // ========================================\n \n // Convert from linear to sRGB color space and set all render targets\n gColor = vec4(linear_to_srgb(outColor.r), \n linear_to_srgb(outColor.g), \n linear_to_srgb(outColor.b), \n outColor.a);\n \n}\n";
1
+ export const volumeFragmentShader: "//\n// #include <packing>\nprecision highp float;\nprecision highp int;\nprecision highp sampler3D;\nprecision highp usampler3D;\n\n// ========================================\n// INPUT VARIABLES (from vertex shader)\n// ========================================\n// Unnormalized ray direction from camera\nin vec3 rayDirUnnorm;\n// Camera position in world space\nin vec3 cameraCorrected;\n\n// ========================================\n// TEXTURE SAMPLERS\n// ========================================\n// 3D texture containing cached brick data (2048x2048x128)\n// (2048*2048*128)/(32*32*32) = 16,384 bricks can be stored?\nuniform sampler3D brickCacheTex;\n// 3D texture containing page table entries (brick metadata)\nuniform usampler3D pageTableTex;\n\n// ========================================\n// RENDERING PARAMETERS/CONSTANTS\n// ========================================\n// Rendering style: 0=MIP, 1=MinIP, 2=standard volume rendering, 3=DEBUG\nuniform int u_renderstyle;\n// Global opacity multiplier for volume rendering\nuniform float opacity;\n\n// ========================================\n// CONTRAST LIMITS (per channel)\n// per channel min/max values for value normalization\n// ========================================\nuniform vec2 clim0;\nuniform vec2 clim1;\nuniform vec2 clim2;\nuniform vec2 clim3;\nuniform vec2 clim4;\nuniform vec2 clim5;\nuniform vec2 clim6;\n\n// ========================================\n// CLIPPING PLANES\n// e.g., for X-axis clipping: (min_x, max_x) or (-1, -1) if disabled\n// ========================================\nuniform vec2 xClip;\nuniform vec2 yClip;\nuniform vec2 zClip;\n\n// ========================================\n// CHANNEL COLORS AND OPACITIES\n// rgb -- color values, a -- visibility (boolean)\n// ========================================\nuniform vec4 color0;\nuniform vec4 color1;\nuniform vec4 color2;\nuniform vec4 color3;\nuniform vec4 color4;\nuniform vec4 color5;\nuniform vec4 color6;\n\n// maps colors to physical spaces\nuniform int channelMapping[7];\n\n// ========================================\n// VOLUME AND RESOLUTION PARAMETERS\n// ========================================\n// Volume bounding box size in world space\nuniform highp vec3 boxSize;\n// Rendering resolution level (affects step size)\n// stepsize, correlates with resolution\nuniform int renderRes;\n// Volume dimensions in voxels (x, y, z)\n// resolution 0 voxel extents\nuniform uvec3 voxelExtents;\n// Global resolution range: (min_res, max_res)\n// global range of requested resolutions\nuniform ivec2 resGlobal;\n// Maximum number of active channels\n// max number of channels (relevant for the cache statistics)\n// between 1 and 7\nuniform int maxChannels;\n\n// ========================================\n// PER-CHANNEL RESOLUTION RANGES\n// per color channel resolution range\n// Each channel can have different available resolution levels\n// e.g., for Channel 0: (min_res, max_res)\n// ========================================\nuniform ivec2 res0;\nuniform ivec2 res1;\nuniform ivec2 res2;\nuniform ivec2 res3;\nuniform ivec2 res4;\nuniform ivec2 res5;\nuniform ivec2 res6;\n// Channel 7: unused\nuniform ivec2 res7;\n\n// ========================================\n// LEVEL-OF-DETAIL PARAMETERS\n// controls how fast we decrease the resolution\n// ========================================\n// LOD factor for distance-based resolution selection\nuniform float lodFactor;\n\n// ========================================\n// ANCHOR POINTS (per resolution level)\n// per resolution anchor point for pagetable\n// ========================================\n// Anchor points define the origin of page table for each resolution level\n// Resolution 0 anchor point (highest detail)\nuniform uvec3 anchor0;\nuniform uvec3 anchor1;\nuniform uvec3 anchor2;\nuniform uvec3 anchor3;\nuniform uvec3 anchor4;\nuniform uvec3 anchor5;\nuniform uvec3 anchor6;\nuniform uvec3 anchor7;\nuniform uvec3 anchor8;\nuniform uvec3 anchor9;\n// Resolution 9 anchor point (lowest detail)\n\n// ========================================\n// SCALE FACTORS (per resolution level)\n// per resolution downsample factor\n// ========================================\n// Scale factors determine voxel size at each resolution level\n// Resolution 0 scale factors (should be 1,1,1)\nuniform vec3 scale0;\nuniform vec3 scale1;\nuniform vec3 scale2;\nuniform vec3 scale3;\nuniform vec3 scale4;\nuniform vec3 scale5;\nuniform vec3 scale6;\nuniform vec3 scale7;\nuniform vec3 scale8;\nuniform vec3 scale9;\n// Resolution 9 scale factors\n\n// ========================================\n// VARYING VARIABLES (unused but required)\n// ========================================\n// Fragment position (unused)\nvarying vec4 glPosition;\n// World space coordinates (used for depth only)\nvarying vec3 worldSpaceCoords;\n\n// ========================================\n// OUTPUT VARIABLES (multiple render targets)\n// output buffers\n// ========================================\n// Final rendered color (sRGB)\nlayout(location = 0) out vec4 gColor;\n// Brick loading requests (packed coordinates)\nlayout(location = 1) out vec4 gRequest;\n// Brick usage tracking (for cache management)\nlayout(location = 2) out vec4 gUsage;\n\n// ========================================\n// CONSTANTS\n// ========================================\n// Size of each brick in voxels (32x32x32)\nconst float BRICK_SIZE = 32.0;\n// Brick cache texture width\nconst float BRICK_CACHE_SIZE_X = 2048.0;\n// Brick cache texture height\nconst float BRICK_CACHE_SIZE_Y = 2048.0;\n// Brick cache texture depth\nconst float BRICK_CACHE_SIZE_Z = 128.0;\n// Number of bricks in X (64)\nconst float BRICK_CACHE_BRICKS_X = BRICK_CACHE_SIZE_X / BRICK_SIZE;\n// Number of bricks in Y (64)\nconst float BRICK_CACHE_BRICKS_Y = BRICK_CACHE_SIZE_Y / BRICK_SIZE;\n// Number of bricks in Z (4)\nconst float BRICK_CACHE_BRICKS_Z = BRICK_CACHE_SIZE_Z / BRICK_SIZE;\n\n// ========================================\n// RAY-VOLUME INTERSECTION\n// calculating the intersection of the ray with the bounding box\n// ========================================\n// Calculates the intersection of a ray with the volume's bounding box\n// Returns (entry_time, exit_time) for the ray-box intersection\n// Handles clipping planes by adjusting the bounding box\n//\n// Parameters:\n// orig - vec3: Ray origin point in world space\n// dir - vec3: Ray direction vector (should be normalized)\n//\n// Returns:\n// vec2: (entry_time, exit_time) where:\n// - entry_time: Distance along ray to enter the volume\n// - exit_time: Distance along ray to exit the volume\n// - If no intersection: entry_time > exit_time\nvec2 intersect_hit(vec3 orig, vec3 dir) {\n // Start with full volume bounds\n vec3 boxMin = vec3(-0.5) * boxSize;\n vec3 boxMax = vec3(0.5) * boxSize;\n \n // Apply clipping planes if they're active (xClip.x > -1.0 means active)\n if (xClip.x > -1.0) {\n boxMin.x = xClip.x - (boxSize.x / 2.0);\n if (xClip.y < boxSize.x)\n boxMax.x = xClip.y - (boxSize.x / 2.0);\n }\n if (yClip.x > -1.0) {\n boxMin.y = yClip.x - (boxSize.y / 2.0);\n if (yClip.y < boxSize.y)\n boxMax.y = yClip.y - (boxSize.y / 2.0);\n }\n if (zClip.x > -1.0) {\n boxMin.z = zClip.x - (boxSize.z / 2.0);\n if (zClip.y < boxSize.z)\n boxMax.z = zClip.y - (boxSize.z / 2.0);\n }\n \n // Standard ray-box intersection algorithm\n vec3 invDir = 1.0 / dir;\n vec3 tmin0 = (boxMin - orig) * invDir;\n vec3 tmax0 = (boxMax - orig) * invDir;\n vec3 tmin = min(tmin0, tmax0);\n vec3 tmax = max(tmin0, tmax0);\n float t0 = max(tmin.x, max(tmin.y, tmin.z)); // Entry time\n float t1 = min(tmax.x, min(tmax.y, tmax.z)); // Exit time\n return vec2(t0, t1);\n}\n\n// ========================================\n// UTILITY FUNCTIONS\n// ========================================\n\n// Pseudo-random number generator for jittered sampling\n// random number generator based on the uv coordinate\n// Author @patriciogv - 2015\n// http://patriciogonzalezvivo.com\n//\n// Parameters:\n// None (uses gl_FragCoord.xy as input)\n//\n// Returns:\n// float: Random value between 0.0 and 1.0 based on fragment coordinates\nfloat random() {\n return fract(sin(dot(gl_FragCoord.xy, vec2(12.9898,78.233)))* 43758.5453123);\n}\n\n// Convert from linear RGB to sRGB color space\n// Implements the standard sRGB transfer function for gamma correction\n//\n// Parameters:\n// x - float: Linear RGB value between 0.0 and 1.0\n//\n// Returns:\n// float: sRGB value between 0.0 and 1.0\nfloat linear_to_srgb(float x) {\n if (x <= 0.0031308f) {\n return 12.92f * x;\n }\n return 1.055f * pow(x, 1.f / 2.4f) - 0.055f;\n}\n\n// Convert from linear RGB to sRGB color space (vector version)\n// Applies sRGB conversion to each RGB component while preserving alpha\n//\n// Parameters:\n// x - vec4: Linear RGBA color with components between 0.0 and 1.0\n//\n// Returns:\n// vec4: sRGB RGBA color with components between 0.0 and 1.0\nvec4 linear_to_srgb(vec4 x) {\n return vec4(linear_to_srgb(x.r), linear_to_srgb(x.g), linear_to_srgb(x.b), x.a);\n}\n\n// ========================================\n// PAGE TABLE COORDINATE PACKING\n// transform the pagetable coordinate into a RGBA8 value\n// ========================================\n// Packs 3D page table coordinates into RGBA8 texture format\n// Uses 10 bits for X, 10 bits for Y, 12 bits for Z\n//\n// Parameters:\n// coord - uvec3: 3D coordinates to pack (X, Y, Z components)\n// - X coordinate: 10-bit unsigned integer (0-1023)\n// - Y coordinate: 10-bit unsigned integer (0-1023) \n// - Z coordinate: 12-bit unsigned integer (0-4095)\n//\n// Returns:\n// vec4: RGBA8 encoded coordinates with components between 0.0 and 1.0\n// - R: Upper 8 bits of packed 32-bit value\n// - G: Middle-upper 8 bits of packed 32-bit value\n// - B: Middle-lower 8 bits of packed 32-bit value\n// - A: Lower 8 bits of packed 32-bit value\nvec4 packPTCoordToRGBA8(uvec3 coord) {\n\n uint x = coord.x & 0x3FFu; // 10 bits for X coordinate\n uint y = coord.y & 0x3FFu; // 10 bits for Y coordinate\n uint z = coord.z & 0xFFFu; // 12 bits for Z coordinate\n\n // Pack into 32-bit integer\n uint packed =\n (x << 22u) |\n (y << 12u) |\n (z);\n\n // Decompose into RGBA8 format\n return vec4(\n float((packed >> 24u) & 0xFFu) / 255.0,\n float((packed >> 16u) & 0xFFu) / 255.0,\n float((packed >> 8u) & 0xFFu) / 255.0,\n float(packed & 0xFFu) / 255.0\n );\n}\n\n// ========================================\n// RESOLUTION AND ANCHOR POINT ACCESSORS\n// ========================================\n\n// Get anchor point for a specific resolution level\n// Anchor points define the origin of the page table for each resolution\n//\n// Parameters:\n// index - int: Resolution level index (0-9, where 0 is highest resolution)\n//\n// Returns:\n// uvec3: 3D anchor point coordinates in the page table, or (-1, -1, -1) if invalid\nuvec3 getAnchorPoint(int index) {\n if (index == 0) return anchor0;\n if (index == 1) return anchor1;\n if (index == 2) return anchor2;\n if (index == 3) return anchor3;\n if (index == 4) return anchor4;\n if (index == 5) return anchor5;\n if (index == 6) return anchor6;\n if (index == 7) return anchor7;\n if (index == 8) return anchor8;\n if (index == 9) return anchor9;\n return uvec3(-1, -1, -1);\n}\n\n// Find the lowest available resolution level\n// Returns the highest resolution index that has valid data\n//\n// Parameters:\n// None\n//\n// Returns:\n// int: Highest resolution level index (0-9) that has valid anchor point data\n// Returns 9 if no valid resolution levels are found\nint getLowestRes() {\n for (int i = 0; i < 10; i++) {\n if (getAnchorPoint(i) == uvec3(0,0,0)) {\n return i - 1;\n }\n }\n return 9;\n}\n\n// Get the downsample factor for a resolution level\n// Scale factors determine the voxel size at each resolution\n//\n// Parameters:\n// index - int: Resolution level index (0-9, where 0 is highest resolution)\n//\n// Returns:\n// vec3: Scale factors (x, y, z) for the resolution level, or (-1, -1, -1) if invalid\n// Higher scale factors indicate larger voxels (lower resolution)\nvec3 getScale(int index) {\n if (index == 0) return scale0;\n if (index == 1) return scale1;\n if (index == 2) return scale2;\n if (index == 3) return scale3;\n if (index == 4) return scale4;\n if (index == 5) return scale5;\n if (index == 6) return scale6;\n if (index == 7) return scale7;\n if (index == 8) return scale8;\n if (index == 9) return scale9;\n return vec3(-1.0, -1.0, -1.0);\n}\n\n// Get the resolution range for a color channel\n// Returns (min_res, max_res) for the channel\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// ivec2: Resolution range as (min_resolution_level, max_resolution_level)\n// Returns (-1, -1) if channel index is invalid\nivec2 getRes(int index) {\n if (index == 0) return res0;\n if (index == 1) return res1;\n if (index == 2) return res2;\n if (index == 3) return res3;\n if (index == 4) return res4;\n if (index == 5) return res5;\n if (index == 6) return res6;\n return ivec2(-1, -1);\n}\n\n// Get the min/max values (contrast limits) for a color channel\n// Returns (min_value, max_value) for normalization\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// vec2: Contrast limits as (min_value, max_value) for data normalization\n// Returns (-1.0, -1.0) if channel index is invalid\nvec2 getClim(int index) {\n if (index == 0) return clim0;\n if (index == 1) return clim1;\n if (index == 2) return clim2;\n if (index == 3) return clim3;\n if (index == 4) return clim4;\n if (index == 5) return clim5;\n if (index == 6) return clim6;\n return vec2(-1.0, -1.0);\n}\n\n// ========================================\n// COORDINATE TRANSFORMATIONS\n// ========================================\n\n// Convert normalized coordinates (0-1) to voxel coordinates.\n// get the voxel coordinate in the specified resolution from the normalized coordinate\n//\n// Parameters:\n// normalized - vec3: Normalized coordinates in range [0,1] for each axis\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Voxel coordinates in the volume space at the specified resolution\nvec3 getVoxelFromNormalized(vec3 normalized, int res) {\n vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution\n vec3 voxel = normalized * extents;\n return voxel;\n}\n\n// Convert voxel coordinates to normalized coordinates (0-1)\n// get the normalized coordinate based on the voxel coordinate in the specified resolution\n//\n// Parameters:\n// voxel - vec3: Voxel coordinates in the volume space\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Normalized coordinates in range [0,1] for each axis\nvec3 getNormalizedFromVoxel(vec3 voxel, int res) {\n vec3 extents = (vec3(voxelExtents) / getScale(res)); // Voxel extents at this resolution\n vec3 normalized = voxel / extents;\n return normalized;\n}\n\n// Convert normalized coordinates to brick coordinates.\n// get the brick coordinate in the specified resolution based on the normalized coordinate\n// needed for pagetable calculations\n//\n// Parameters:\n// normalized - vec3: Normalized coordinates in range [0,1] for each axis\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Brick coordinates (each brick is 32x32x32 voxels)\nvec3 getBrickFromNormalized(vec3 normalized, int res) {\n vec3 voxel = getVoxelFromNormalized(normalized, res);\n vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels\n return brick;\n}\n\n// Convert voxel coordinates to brick coordinates.\n// get the brick coordinate in the specified resolution based on the voxel coordinate\n//\n// Parameters:\n// voxel - vec3: Voxel coordinates in the volume space\n// res - int: Resolution level (0=highest detail, 9=lowest detail)\n//\n// Returns:\n// vec3: Brick coordinates (each brick is 32x32x32 voxels)\nvec3 getBrickFromVoxel(vec3 voxel, int res) {\n vec3 brick = floor(voxel / 32.0); // Each brick is 32x32x32 voxels\n return brick;\n}\n\n// ========================================\n// CHANNEL-SPECIFIC ACCESSORS\n// ========================================\n\n// Get channel offset in page table.\n// get the vector for the specified channel slot in the pagetable\n// Different channels are stored at different Z-offsets in the page table\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// uvec3: 3D offset coordinates for the channel in the page table\nuvec3 getChannelOffset(int index) {\n if (index == 0) return uvec3(0, 0, 1);\n if (index == 1) return uvec3(0, 1, 0);\n if (index == 2) return uvec3(0, 1, 1);\n if (index == 3) return uvec3(1, 0, 0);\n if (index == 4) return uvec3(1, 0, 1);\n if (index == 5) return uvec3(1, 1, 0);\n if (index == 6) return uvec3(1, 1, 1);\n return uvec3(0, 0, 0);\n}\n\n// Get color for a channel.\n// get the color per color channel\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// vec3: RGB color values for the specified channel\nvec3 getChannelColor(int index) {\n if (index == 0) return color0.xyz;\n if (index == 1) return color1.xyz;\n if (index == 2) return color2.xyz;\n if (index == 3) return color3.xyz;\n if (index == 4) return color4.xyz;\n if (index == 5) return color5.xyz;\n if (index == 6) return color6.xyz;\n return vec3(0.0, 0.0, 0.0);\n}\n\n// Get opacity for a channel\n// get the opacity (used as visibility) per color channel\n//\n// Parameters:\n// index - int: Channel index (0-6)\n//\n// Returns:\n// float: Opacity value (0.0-1.0) for the specified channel\nfloat getChannelOpacity(int index) {\n if (index == 0) return color0.w;\n if (index == 1) return color1.w;\n if (index == 2) return color2.w;\n if (index == 3) return color3.w;\n if (index == 4) return color4.w;\n if (index == 5) return color5.w;\n if (index == 6) return color6.w;\n return 0.0;\n}\n\n// ========================================\n// PAGE TABLE DECODING\n// ========================================\n\n/**\n * retrieving the brick based on:\n * location -- normalized coordinate\n * targetRes -- target resolution\n * channel -- physical channel slot\n * rnd -- random number for jittering requests \n * query -- whether to query the brick (we dont query for interblock interpolation)\n * colorIndex -- color index for querying the min max values\n * \n * returns:\n * w >= 0 -- xyz contains brick cache coordinate, w stores resolution\n * w == -1 -- not resident in any resolution, should be treated as empty\n * w == -2 -- empty (with respect to current transfer function)\n * w == -3 -- constant full (with respect to current transfer function)\n * w == -4 -- constant value within range, x stores that value\n *\n * bit layout:\n * [1] 31 | 0 — flag resident\n * [1] 30 | 1 — flag init\n * [7] 23…29 | 2…8 — min → 128\n * [7] 16…22 | 9…15 — max → 128\n * [6] 10…15 | 16…21 — x offset in brick cache → max 64\n * [6] 4…9 | 22…27 — y offset in brick cache → max 64\n * [4] 0…3 | 28…31 — z offset in brick cache → max 16, effectively 4\n*/\n\n/*\nPage table entry format (32 bits):\n[31] | 0 — flag resident (1=loaded in cache)\n[30] | 1 — flag init (1=initialized)\n[29:23] | 2…8 — min value (7 bits) → 128 levels\n[22:16] | 9…15 — max value (7 bits) → 128 levels \n[15:10] | 16…21 — x offset in brick cache (6 bits) → 64 bricks\n[9:4] | 22…27 — y offset in brick cache (6 bits) → 64 bricks\n[3:0] | 28…31 — z offset in brick cache (4 bits) → 16 bricks\n*/\n\n// Query page table to find brick location and status\n// Searches for a brick at the specified location across multiple resolution levels\n// and returns its cache coordinates and status information\n//\n// Parameters:\n// location - vec3: Normalized coordinates (0-1) within the volume\n// targetRes - int: Target resolution level to start searching from\n// channel - int: Channel index (0-6) to query\n// rnd - float: Random value (0-1) used for brick loading request selection\n// query - bool: Whether to allow brick loading requests (true) or just query (false)\n// colorIndex - int\n//\n// Returns:\n// ivec4: (x_offset, y_offset, z_offset, status) where:\n// - x_offset, y_offset, z_offset: Brick cache coordinates if found\n// - status: Resolution level (>=0) if found, or status code:\n// * -1: Not found at any resolution level\n// * -2: Empty brick (all values below threshold)\n// * -3: Constant full brick (all values above threshold)\n// * -4: Constant value brick (uniform value)\n// add maxres here\nivec4 getBrickLocation(vec3 location, int targetRes, int channel, float rnd, bool query, int colorIndex) {\n\n // min max for current color \n vec2 clim = getClim(colorIndex);\n\n // resolution ranges, TODO: connect this back to color\n int channelMin = getRes(channel).x;\n int channelMax = getRes(channel).y;\n\n // Clamp resolution to channel's available range\n int currentRes = clamp(targetRes, channelMin, channelMax);\n currentRes = clamp(currentRes, resGlobal.x, resGlobal.y);\n int lowestRes = clamp(resGlobal.y, channelMin, channelMax);\n\n // Determine if this channel should request brick loading.\n // request the current channel based on probability\n bool requestChannel = false;\n if (int(floor(rnd * float(maxChannels))) == colorIndex) {\n requestChannel = true;\n }\n\n // Try progressively lower resolutions until we find data.\n // loop through resolutions\n while (currentRes <= lowestRes) {\n\n // Calculate page table coordinates for this brick\n uvec3 anchorPoint = getAnchorPoint(currentRes);\n vec3 brickLocation = getBrickFromNormalized(location, currentRes);\n uvec3 channelOffset = getChannelOffset(channel);\n vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;\n \n // Special handling for resolution 0 (highest detail)\n if (currentRes == 0) {\n int zExtent = int(ceil(float(voxelExtents.z) / 32.0));\n coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;\n }\n\n // Query the page table.\n // get PT entry\n uint ptEntry = texelFetch(pageTableTex, ivec3(coordinate), 0).r;\n\n // Check if brick is initialized.\n // check if the PT entry is initialized\n uint isInit = (ptEntry >> 30u) & 1u;\n if (isInit == 0u) { \n currentRes++; \n // Request brick loading if needed\n if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {\n gRequest = packPTCoordToRGBA8(uvec3(coordinate));\n }\n continue;\n }\n \n // Extract min/max values from page table entry.\n // get the min max values of the brick\n uint umin = ((ptEntry >> 23u) & 0x7Fu);\n uint umax = ((ptEntry >> 16u) & 0x7Fu);\n float min = float(int(umin)) / 127.0;\n float max = float(int(umax)) / 127.0;\n \n // Check if brick is empty (all values below threshold).\n // exit early if brick is constant\n if (float(max) <= clim.x) {\n return ivec4(0,0,0,-2);\n // EMPTY\n } else if (float(min) >= clim.y) {\n return ivec4(0,0,0,-3); // CONSTANT FULL\n } else if ((umax - umin) < 2u) {\n return ivec4(min,0,0,-4); // CONSTANT OTHER VALUE\n }\n \n // Check if brick is resident in cache.\n // return brick cache location if resident\n // continue to next resolution if not resident\n uint isResident = (ptEntry >> 31u) & 1u;\n if (isResident == 0u) {\n currentRes++;\n // Request brick loading if needed\n if (requestChannel == true && (gRequest.a + gRequest.b + gRequest.g + gRequest.r == 0.0) && query == true) {\n gRequest = packPTCoordToRGBA8(uvec3(coordinate));\n }\n continue;\n } else {\n // Extract brick cache coordinates\n uint xBrickCache = (ptEntry >> 10u) & 0x3Fu;\n uint yBrickCache = (ptEntry >> 4u) & 0x3Fu;\n uint zBrickCache = ptEntry & 0xFu;\n uvec3 brickCacheCoord = uvec3(xBrickCache, yBrickCache, zBrickCache);\n\n return ivec4(brickCacheCoord, currentRes);\n }\n }\n\n // not resident in any resolution, should be treated as empty\n return ivec4(0,0,0,-1); // Not found\n}\n\n// Request brick loading for a specific location and resolution.\n// Initiates a request to load a brick from disk into the brick cache.\n// set the brick request for the specified slot channel\n//\n// Parameters:\n// location - vec3: Normalized world space coordinates (0.0 to 1.0) where the brick is needed\n// targetRes - int: Target resolution level (0-9, where 0 is highest resolution)\n// channel - int: Channel index (0-6) for multi-channel datasets\n// rnd - float: Random value between 0.0 and 1.0 used for load balancing across channels\n//\n// Returns:\n// void: No return value, but sets gRequest output variable if conditions are met\nvoid setBrickRequest(vec3 location, int targetRes, int channel, float rnd) {\n uvec3 anchorPoint = getAnchorPoint(targetRes);\n vec3 brickLocation = getBrickFromNormalized(location, targetRes);\n uvec3 channelOffset = getChannelOffset(channel);\n vec3 coordinate = floor(vec3(anchorPoint * channelOffset)) + brickLocation;\n \n // Special handling for resolution 0\n if (targetRes == 0) {\n int zExtent = int(ceil(float(voxelExtents.z) / 32.0));\n coordinate = vec3(anchorPoint) + vec3(0.0, 0.0, zExtent * channel) + brickLocation;\n }\n \n // Pack coordinates and set request\n if (int(floor(rnd * float(maxChannels))) == channel) {\n gRequest = packPTCoordToRGBA8(uvec3(coordinate));\n }\n}\n\n// Track brick usage for cache management.\n// Records which brick in the cache is being accessed for LRU (Least Recently Used) eviction.\n// set the usage for the specified brick\n//\n// Parameters:\n// brickCacheOffset - ivec3: 3D coordinates of the brick within the brick cache texture\n// t_hit_min_os - float: Ray entry time in object space (start of ray-volume intersection)\n// t_hit_max_os - float: Ray exit time in object space (end of ray-volume intersection)\n// t_os - float: Current sampling position along the ray in object space\n// rnd - float: Random value between 0.0 and 1.0 used for probabilistic usage tracking\n//\n// Returns:\n// void: No return value, but sets gUsage output variable to track cache access patterns\nvoid setUsage(ivec3 brickCacheOffset, float t_hit_min_os, float t_hit_max_os, float t_os, float rnd) {\n float normalized_t_os = (t_os - t_hit_min_os) / (t_hit_max_os - t_hit_min_os); // Normalize to 0-1\n if (normalized_t_os <= rnd || gUsage == vec4(0.0, 0.0, 0.0, 0.0)) {\n gUsage = vec4(vec3(brickCacheOffset) / 255.0, 1.0);\n }\n}\n\n// ========================================\n// UTILITY FUNCTIONS\n// ========================================\n\n// Get maximum component of a 3D vector.\n// get the max value of a vec3\n//\n// Parameters:\n// v - vec3: Input 3D vector\n//\n// Returns:\n// float: The maximum value among the x, y, and z components\nfloat vec3_max(vec3 v) {\n return max(v.x, max(v.y, v.z));\n}\n\n// Get minimum component of a 3D vector.\n// get the min value of a vec3\n//\n// Parameters:\n// v - vec3: Input 3D vector\n//\n// Returns:\n// float: The minimum value among the x, y, and z components\nfloat vec3_min(vec3 v) {\n return min(v.x, min(v.y, v.z));\n}\n\n// Calculate level-of-detail based on distance.\n// Uses logarithmic scaling to determine appropriate resolution level.\n// get the LOD based on the distance to the camera\n//\n// Parameters:\n// distance - float: Distance from camera to sampling point\n// highestRes - int: Highest available resolution level (typically 0)\n// lowestRes - int: Lowest available resolution level (typically 9)\n// lodFactor - float: Scaling factor that controls LOD sensitivity\n//\n// Returns:\n// int: Resolution level index (0-9) where 0 is highest resolution\nint getLOD(float distance, int highestRes, int lowestRes, float lodFactor) {\n int lod = int(log2(distance * lodFactor));\n return clamp(lod, highestRes, lowestRes);\n}\n\n// Calculate step size for ray marching at a given resolution.\n// Determines optimal sampling step size based on voxel dimensions and ray direction.\n// get the voxel step in object space\n//\n// Parameters:\n// res - int: Resolution level index (0-9, where 0 is highest resolution)\n// osDir - vec3: Ray direction vector in object space (should be normalized)\n//\n// Returns:\n// float: Optimal step size in object space units for stable ray marching\nfloat voxelStepOS(int res, vec3 osDir) {\n vec3 voxelSize = getScale(res) / vec3(voxelExtents);\n vec3 dt_vec = voxelSize / abs(osDir);\n return min(dt_vec.x, min(dt_vec.y, dt_vec.z));\n}\n\n// ========================================\n// INTERPOLATION FUNCTIONS\n// ========================================\n\n// Linear interpolation between two values.\n// Performs smooth interpolation between two scalar values.\n//\n// Parameters:\n// v0 - float: First value to interpolate from\n// v1 - float: Second value to interpolate to\n// fx - float: Interpolation factor between 0.0 and 1.0\n//\n// Returns:\n// float: Interpolated value between v0 and v1 based on fx\nfloat lerp(float v0, float v1, float fx) {\n return mix(v0, v1, fx); // (1-fx)·v0 + fx·v1\n}\n\n// Bilinear interpolation between four values\n// Performs 2D interpolation using four corner values arranged in a square\n//\n// Parameters:\n// v00 - float: Bottom-left corner value\n// v10 - float: Bottom-right corner value\n// v01 - float: Top-left corner value\n// v11 - float: Top-right corner value\n// f - vec2: 2D interpolation factors (x, y) between 0.0 and 1.0\n//\n// Returns:\n// float: Interpolated value from the four corner values\nfloat bilerp(float v00, float v10, float v01, float v11, vec2 f) {\n float c0 = mix(v00, v10, f.x); // Interpolate in X on bottom row\n float c1 = mix(v01, v11, f.x); // Interpolate in X on top row\n return mix(c0, c1, f.y); // Now interpolate those in Y\n}\n\n// Trilinear interpolation between eight values\n// Performs 3D interpolation using eight corner values arranged in a cube\n//\n// Parameters:\n// v000 - float: Bottom-left-back corner value\n// v100 - float: Bottom-right-back corner value\n// v010 - float: Bottom-left-front corner value\n// v110 - float: Bottom-right-front corner value\n// v001 - float: Top-left-back corner value\n// v101 - float: Top-right-back corner value\n// v011 - float: Top-left-front corner value\n// v111 - float: Top-right-front corner value\n// f - vec3: 3D interpolation factors (x, y, z) between 0.0 and 1.0\n//\n// Returns:\n// float: Interpolated value from the eight corner values\nfloat trilerp(\n float v000, float v100, float v010, float v110,\n float v001, float v101, float v011, float v111,\n vec3 f) { // f = fract(coord)\n // Interpolate along X for each of the four bottom-face voxels\n float c00 = mix(v000, v100, f.x);\n float c10 = mix(v010, v110, f.x);\n float c01 = mix(v001, v101, f.x);\n float c11 = mix(v011, v111, f.x);\n\n // Interpolate those along Y\n float c0 = mix(c00, c10, f.y);\n float c1 = mix(c01, c11, f.y);\n\n // Final interpolation along Z\n return mix(c0, c1, f.z);\n}\n\n// ========================================\n// BRICK CACHE SAMPLING\n// ========================================\n\n// Sample a value from the brick cache texture.\n// Converts brick coordinates and voxel position to texture coordinates for sampling.\n// sample the brick cache based on the brick cache coordinate and the in-brick coordinate\n//\n// Parameters:\n// brickCacheCoord - vec3: 3D coordinates of the brick within the brick cache\n// voxelInBrick - vec3: 3D coordinates of the voxel within the brick (0-31 in each dimension)\n//\n// Returns:\n// float: Sampled voxel value from the brick cache texture (typically normalized 0.0-1.0)\nfloat sampleBrick(vec3 brickCacheCoord, vec3 voxelInBrick) {\n vec3 brickCacheCoordNormalized = vec3(\n (float(brickCacheCoord.x) * BRICK_SIZE + float(voxelInBrick.x)) / BRICK_CACHE_SIZE_X,\n (float(brickCacheCoord.y) * BRICK_SIZE + float(voxelInBrick.y)) / BRICK_CACHE_SIZE_Y,\n (float(brickCacheCoord.z) * BRICK_SIZE + float(voxelInBrick.z)) / BRICK_CACHE_SIZE_Z\n );\n return texture(brickCacheTex, brickCacheCoordNormalized).r;\n}\n\n/**\n * main renderloop\n*/\nvoid main(void) {\n\n // ========================================\n // INITIALIZATION\n // ========================================\n \n // Initialize all render targets (multiple output textures)\n gRequest = vec4(0,0,0,0); // Brick loading requests\n gUsage = vec4(0,0,0,0); // Brick usage tracking\n gColor = vec4(0.0, 0.0, 0.0, 0.0); // Final color output\n\n // out color sums up our accumulated value before writing it into the gColor buffer\n vec4 outColor = vec4(0.0, 0.0, 0.0, 0.0); // Accumulated color\n\n // Generate random number for jittered sampling (reduces artifacts)\n float rnd = random();\n\n // Get the lowest available resolution level\n int lowestDataRes = getLowestRes();\n\n // ========================================\n // RAY-VOLUME INTERSECTION\n // ========================================\n \n // Normalize the view ray direction\n vec3 ws_rayDir = normalize(rayDirUnnorm);\n \n // Calculate intersection with volume bounding box\n // Returns (entry_time, exit_time) for the ray-box intersection\n vec2 t_hit = intersect_hit(cameraCorrected, ws_rayDir);\n if (t_hit.x >= t_hit.y) { discard; } // Ray misses volume entirely\n \n t_hit.x = max(t_hit.x, 0.0); // Clamp entry to 0 (no negative distances)\n float t = t_hit.x;\n \n // Calculate distance from camera for LOD selection\n float distance = abs((cameraCorrected / boxSize).z + (ws_rayDir / boxSize).z * t );\n\n // ========================================\n // COORDINATE SPACE CONVERSION\n // ========================================\n \n // Convert from world space to object space (normalized 0-1 coordinates)\n float ws2os = length(ws_rayDir / boxSize); // Scale factor for conversion\n float t_hit_min_os = t_hit.x * ws2os; // Entry point in object space\n float t_hit_max_os = t_hit.y * ws2os; // Exit point in object space\n float t_os = t_hit_min_os; // Current position in object space\n\n // Calculate effective LOD factor based on volume size.\n // voxel edge is the max extent of the volume\n float voxelEdge = float(max(voxelExtents.x, max(voxelExtents.y, voxelExtents.z)));\n\n // calculate LOD factor based on the voxel edge\n float lodFactorEffective = lodFactor * voxelEdge / 256.0;\n\n // ========================================\n // RESOLUTION AND SAMPLING SETUP\n // ========================================\n \n // Determine target resolution based on distance (LOD)\n int targetRes = getLOD(t, 0, 9, lodFactorEffective);\n \n // Set adaptive stepping resolution\n int stepResAdaptive = renderRes;\n int stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);\n\n // Convert ray to object space coordinates\n vec3 os_rayDir = normalize(ws_rayDir / boxSize);\n vec3 os_rayOrigin = cameraCorrected / boxSize + vec3(0.5);\n \n // Calculate step size based on current resolution\n float dt = voxelStepOS(stepResEffective, os_rayDir);\n\n // ========================================\n // SAMPLING POSITION INITIALIZATION\n // ========================================\n \n // Convert to normalized sampling coordinates (0-1 range)\n vec3 p = cameraCorrected + t_hit.x * ws_rayDir;\n p = p / boxSize + vec3(0.5); // Transform to 0-1 range\n \n // Calculate step vector in normalized space\n vec3 dp = (os_rayDir * dt);\n \n // Apply jittered sampling to reduce artifacts\n p += dp * (rnd);\n // Avoid boundary issues\n p = clamp(p, 0.0 + 0.0000028, 1.0 - 0.0000028);\n\n // ========================================\n // RENDERING VARIABLES\n // ========================================\n \n // Color accumulation for front-to-back compositing.\n // color accumulation variables, are calculated per 'slice'\n vec3 rgbCombo = vec3(0.0);\n float total = 0.0;\n\n // For alpha blending.\n // alpha accumulation variable runs globally\n float alphaMultiplicator = 1.0;\n\n // Request tracking (for brick loading).\n // if we have a request for a brick which not visible in lower\n // resolutions, we can overwrite it once\n bool overWrittenRequest = false;\n\n // Current state tracking\n vec3 currentTargetResPTCoord = vec3(0,0,0);\n int currentLOD = targetRes;\n\n // ========================================\n // CHANNEL-SPECIFIC CONSTANTS\n // ========================================\n \n // Pre-compute channel properties for efficiency.\n // constants per color channel\n vec3 [] c_color = vec3[7](getChannelColor(0), getChannelColor(1), getChannelColor(2), getChannelColor(3), getChannelColor(4), getChannelColor(5), getChannelColor(6));\n float [] c_opacity = float[7](getChannelOpacity(0), getChannelOpacity(1), getChannelOpacity(2), getChannelOpacity(3), getChannelOpacity(4), getChannelOpacity(5), getChannelOpacity(6));\n // resolution ranges (currently) per color channel\n // TODO: figure out how to hook it up with frontend\n 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);\n 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);\n vec3 [] res_color = vec3[10](\n vec3(1.0, 0.0, 0.0), \n vec3(0.0, 0.0, 1.0), \n vec3(0.0, 1.0, 0.0), \n vec3(1.0, 0.0, 1.0), \n vec3(0.0, 1.0, 1.0),\n vec3(1.0, 1.0, 0.0),\n vec3(1.0, 0.5, 0.5), \n vec3(0.5, 0.5, 1.0), \n vec3(0.5, 1.0, 0.5), \n vec3(0.5, 0.5, 0.5)\n );\n\n // ========================================\n // PER-CHANNEL STATE ARRAYS\n // ========================================\n \n // Current state for each channel.\n // current state variables per color channel\n \n // current resolution\n int [] c_res_current = int[7](0,0,0,0,0,0,0);\n // current value\n float [] c_val_current = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);\n // current brick cache coordinate\n 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));\n // current voxel in current resolution\n 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));\n // current pagetable coordinate\n 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));\n // current render mode -- 0: empty (add 0), 1: constant (add current val), 2: voxel (query new voxel)\n // upon change of PT we re-query anyways\n int [] c_renderMode_current = int[7](-1, -1, -1, -1, -1, -1, -1);\n \n // Adjacent brick caching for interpolation\n // current pagetable coordinate of the adjacent bricks in X, Y, Z or diagonal direction\n 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));\n 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));\n 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));\n 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));\n // corresponding brick coordinates of the adjacent bricks in X, Y, Z or diagonal direction\n 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));\n 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));\n 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));\n 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));\n\n // Min/max tracking for MIP/MinIP rendering.\n // min and max values of the current color\n // used for minimum/maximum intensity projection\n float [] c_minVal = float[7](-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0);\n float [] c_maxVal = float[7](0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);\n\n // Per-resolution coordinate tracking\n 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));\n 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));\n 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));\n 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));\n\n // resolution changed flag\n bool resolutionChanged = false;\n // number of repetitions (for debugging purposes)\n int reps = 0;\n\n // ========================================\n // MAIN RAY-MARCHING LOOP\n // ========================================\n \n // while we are 'in' the volume.\n // Continue marching until we exit the volume or reach maximum opacity.\n while (t_os < t_hit_max_os && t_os >= t_hit_min_os\n && vec3_max(p) < 1.0 && vec3_min(p) >= 0.0\n ) {\n\n // Reset per-sample accumulation.\n // initialize slice values\n vec3 rgbCombo = vec3(0.0);\n float total = 0.0;\n\n // Update target resolution based on current distance (with jitter).\n // calculate target resolution based on distance and lod factor\n targetRes = getLOD(t, 0, 9, lodFactorEffective * (0.999 + 0.002 * rnd));\n\n // ========================================\n // RESOLUTION CHANGE HANDLING\n // ========================================\n \n // if target resolution changed, update the current resolution and stepsize\n if (targetRes != currentLOD) {\n currentLOD = targetRes;\n stepResAdaptive++;\n stepResEffective = clamp(stepResAdaptive, 0, lowestDataRes);\n \n // Adjust sampling position for new resolution\n p -= dp * rnd;\n dt = voxelStepOS(stepResEffective, os_rayDir);\n dp = os_rayDir * dt;\n p += dp * rnd;\n resolutionChanged = true;\n\n // Check bounds after resolution change\n 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) {\n break;\n }\n } else {\n resolutionChanged = false;\n }\n\n // ========================================\n // UPDATE COORDINATES FOR ALL RESOLUTIONS\n // ========================================\n \n // Calculate page table coordinates and voxel positions for all resolution levels\n for (int r = 0; r < 10; r++) {\n r_prevPTCoord[r] = r_ptCoord[r];\n r_prevVoxel[r] = r_voxel[r];\n r_ptCoord[r] = getBrickFromNormalized(p, r);\n r_voxel[r] = getVoxelFromNormalized(p, r);\n }\n\n // ========================================\n // RENDER MODE DEFINITIONS\n // ========================================\n // 0: empty brick (no data)\n // 1: constant brick (uniform value)\n // 2: voxel brick (variable data)\n\n // initialize the per channel slice values\n vec3 sliceColor = vec3(0.0);\n float sliceAlpha = 0.0;\n\n // ========================================\n // MULTI-CHANNEL SAMPLING\n // ========================================\n \n // Process each channel independently.\n // iterate over up to 7 channels by color\n for (int c = 0; c < 7; c++) {\n // Skip channels with zero opacity.\n // skip if opacity is 0 or if color is not mapped to a physical slot\n if (c_opacity[c] <= 0.000001) {\n continue;\n } else if (channelMapping[c] == -1) {\n continue;\n }\n\n // physical slot in pagetable\n int slot = channelMapping[c];\n\n // keep track of status\n bool newBrick = false;\n bool newVoxel = false;\n // best possible resolution\n int bestRes = clamp(targetRes, c_res_min[c], c_res_max[c]);\n\n // Check if we need to load a new brick at a better resolution.\n // check if any new better resolution could be available, if so, we need to re-query the brick\n bool betterResChanged = false;\n for (int r = bestRes; r <= c_res_current[c]; r++ ) {\n if (r_ptCoord[r] != r_prevPTCoord[r]) {\n betterResChanged = true;\n break;\n }\n }\n\n // Determine if we need to load new brick data.\n // check if we need to re-query the brick / voxel or reuse past 'val'\n if (r_ptCoord[bestRes] != r_prevPTCoord[bestRes]\n || c_renderMode_current[c] == -1\n || resolutionChanged == true\n || betterResChanged\n ) {\n newBrick = true;\n newVoxel = true;\n } else if (c_renderMode_current[c] == 2) {\n newVoxel = true;\n } else if (c_renderMode_current[c] == 0) {\n continue; // Skip empty bricks\n }\n\n // ========================================\n // BRICK LOADING AND CACHING\n // ========================================\n \n // check if a new brick is available in the best possible resolution\n if (newBrick) {\n // Query page table for brick location and status\n ivec4 brickCacheInfo = getBrickLocation(p, bestRes, slot, rnd, true, c);\n // check information about the newly queried brick\n // if the res is not at best res, possibly the same as previous brick\n if (brickCacheInfo.w == -1 || brickCacheInfo.w == -2) {\n // Empty brick - no data available.\n // we can skip the rest of the loop\n c_val_current[c] = 0.0;\n c_renderMode_current[c] = 0;\n c_minVal[c] = 0.0;\n continue;\n } else if (brickCacheInfo.w == -3) {\n // Solid brick - constant maximum value.\n // we set the value and do not need to query a voxel\n c_val_current[c] = 1.0;\n c_renderMode_current[c] = 1;\n c_maxVal[c] = 1.0;\n newVoxel = false;\n } else if (brickCacheInfo.w == -4) {\n // Constant brick - uniform value.\n // static value -- we set the value and do not need to query a voxel\n float val = float(brickCacheInfo.x);\n c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));\n c_renderMode_current[c] = 1;\n newVoxel = false;\n } else if (brickCacheInfo.w >= 0) {\n // Voxel brick - variable data, load from cache.\n // new brick -- we set the coordinate and resolution and need to query a voxel\n c_res_current[c] = brickCacheInfo.w;\n c_ptCoord_current[c] = r_ptCoord[c_res_current[c]];\n c_brickCacheCoord_current[c] = vec3(brickCacheInfo.xyz);\n c_renderMode_current[c] = 2;\n newVoxel = true;\n \n // Track brick usage for cache management.\n // we set the usage of the brick based on the channel and the relative distance into the cube\n if (int(floor(rnd * float(maxChannels))) == c) {\n setUsage(brickCacheInfo.xyz, t_hit_min_os, t_hit_max_os, t_os, rnd);\n }\n }\n }\n \n // ========================================\n // VOXEL SAMPLING WITH INTERPOLATION\n // ========================================\n \n // we need to query a new voxel e.g. sample the brick cache\n if (newVoxel) {\n c_voxel_current[c] = r_voxel[c_res_current[c]];\n\n // we clamp the coordinate to be inside the brick and sample the volume\n reps++;\n \n // Calculate position within the brick (0-31 range)\n vec3 voxelInBrick = mod(c_voxel_current[c], 32.0);\n vec3 clampedVoxelInBrick = clamp(voxelInBrick, 0.5, 31.5);\n // Sample the brick cache texture\n float val = sampleBrick(c_brickCacheCoord_current[c].xyz, clampedVoxelInBrick);\n\n // ========================================\n // HIGH-QUALITY INTERPOLATION (renderRes == 0)\n // ========================================\n \n // interblock interpolation\n if (renderRes == 0) {\n // calculate what axis we need to interpolate\n \n // Check if we're near brick boundaries (need interpolation)\n bvec3 clampedMin = lessThan(voxelInBrick, clampedVoxelInBrick);\n bvec3 clampedMax = greaterThan(voxelInBrick, clampedVoxelInBrick);\n bvec3 clamped = bvec3(clampedMin.x || clampedMax.x, clampedMin.y || clampedMax.y, clampedMin.z || clampedMax.z);\n vec3 diff = voxelInBrick - clampedVoxelInBrick;\n \n if (any(clampedMin) || any(clampedMax)) { \n int boundaryAxes = int(clamped.x) + int(clamped.y) + int(clamped.z);\n float f = 0.0;\n \n if (boundaryAxes == 1) {\n // Linear interpolation across one boundary\n vec3 otherGlobalVoxelPos = vec3(0,0,0);\n vec3 otherP = vec3(0,0,0);\n float otherVoxelVal = 0.0;\n\n // Determine which axis we're interpolating across\n if (clampedMin.x) {\n otherGlobalVoxelPos = c_voxel_current[c] - vec3(1.0, 0.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.x);\n } else if (clampedMax.x) {\n otherGlobalVoxelPos = c_voxel_current[c] + vec3(1.0, 0.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.x);\n } else if (clampedMin.y) {\n otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 1.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.y);\n } else if (clampedMax.y) {\n otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 1.0, 0.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.y);\n } else if (clampedMin.z) {\n otherGlobalVoxelPos = c_voxel_current[c] - vec3(0.0, 0.0, 1.0);\n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.z);\n } else if (clampedMax.z) {\n otherGlobalVoxelPos = c_voxel_current[c] + vec3(0.0, 0.0, 1.0); \n otherP = getNormalizedFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n f = abs(diff.z);\n }\n\n // Sample the neighboring voxel\n vec3 otherPTcoord = getBrickFromNormalized(otherP, c_res_current[c]);\n otherPTcoord = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]);\n vec3 otherVoxelInBrick = mod(otherGlobalVoxelPos, 32.0);\n otherVoxelInBrick -= diff;\n\n // Check if neighbor is outside volume bounds\n 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) {\n otherVoxelVal = val;\n } else if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz && c_brick_XYZ_adjacent[c].w >= 0.0) {\n // Use cached adjacent brick\n otherVoxelVal = sampleBrick(c_brick_XYZ_adjacent[c].xyz, otherVoxelInBrick);\n } else {\n // Load new adjacent brick\n ivec4 otherBrickCacheInfo = ivec4(-1);\n if (otherPTcoord == c_PT_XYZ_adjacent[c].xyz) {\n otherBrickCacheInfo = ivec4(c_brick_XYZ_adjacent[c]);\n } else {\n otherBrickCacheInfo = getBrickLocation(otherP, c_res_current[c], slot, rnd, false, c); \n }\n if (otherBrickCacheInfo.w == -1 || otherBrickCacheInfo.w == -2) {\n otherVoxelVal = val;\n } else if (otherBrickCacheInfo.w == -3) {\n otherVoxelVal = 1.0;\n } else if (otherBrickCacheInfo.w == -4) {\n otherVoxelVal = float(otherBrickCacheInfo.x);\n } else {\n // TODO: we do not recalculate the voxelInBrick based on the resolution\n otherVoxelVal = sampleBrick(vec3(otherBrickCacheInfo.xyz), otherVoxelInBrick);\n }\n c_PT_XYZ_adjacent[c] = getBrickFromVoxel(otherGlobalVoxelPos, c_res_current[c]); \n c_brick_XYZ_adjacent[c] = vec4(otherBrickCacheInfo);\n }\n \n // Perform linear interpolation\n float originalVal = val;\n val = lerp(originalVal, otherVoxelVal, f); \n } else if (boundaryAxes == 2) {\n // Bilinear interpolation across two boundaries\n vec3 offA = vec3(0.0);\n vec3 offB = vec3(0.0);\n vec2 f = vec2(0.0);\n\n // Determine which two axes we're interpolating across\n if (clamped.x && clamped.y) {\n offA.x = clampedMin.x ? -1.0 : 1.0;\n offB.y = clampedMin.y ? -1.0 : 1.0;\n f = vec2(abs(diff.x), abs(diff.y));\n } else if (clamped.x && clamped.z) {\n offA.x = clampedMin.x ? -1.0 : 1.0;\n offB.z = clampedMin.z ? -1.0 : 1.0;\n f = vec2(abs(diff.x), abs(diff.z));\n } else if (clamped.y && clamped.z) {\n offA.y = clampedMin.y ? -1.0 : 1.0;\n offB.z = clampedMin.z ? -1.0 : 1.0;\n f = vec2(abs(diff.y), abs(diff.z));\n }\n\n // Macro for sampling at offset positions\n #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); } } } }\n \n // Sample the four corners for bilinear interpolation\n float v00 = val;\n float v10; float v01; float v11;\n SAMPLE_AT_OFFSET(offA, v10);\n SAMPLE_AT_OFFSET(offB, v01);\n SAMPLE_AT_OFFSET(offA + offB, v11);\n\n val = bilerp(v00, v10, v01, v11, f);\n\n #undef SAMPLE_AT_OFFSET\n\n } else if (boundaryAxes == 3) {\n // Trilinear interpolation across all three boundaries\n \n vec3 offA = vec3(0.0);\n vec3 offB = vec3(0.0);\n vec3 offC = vec3(0.0);\n vec3 f = vec3(0.0);\n\n offA.x = clampedMin.x ? -1.0 : 1.0;\n offB.y = clampedMin.y ? -1.0 : 1.0;\n offC.z = clampedMin.z ? -1.0 : 1.0;\n\n f = vec3(abs(diff.x), abs(diff.y), abs(diff.z));\n\n // Macro for sampling at offset positions\n #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); } } } }\n \n // Sample all eight corners for trilinear interpolation\n float v000 = val;\n float v100; float v010; float v001; float v110; float v101; float v011; float v111;\n SAMPLE_AT_OFFSET(offA, v100);\n SAMPLE_AT_OFFSET(offB, v010);\n SAMPLE_AT_OFFSET(offC, v001);\n SAMPLE_AT_OFFSET(offA + offB, v110);\n SAMPLE_AT_OFFSET(offA + offC, v101);\n SAMPLE_AT_OFFSET(offB + offC, v011);\n SAMPLE_AT_OFFSET(offA + offB + offC, v111);\n\n val = trilerp(v000, v100, v010, v001, v110, v101, v011, v111, f);\n\n #undef SAMPLE_AT_OFFSET\n \n }\n\n } else {\n // No boundary interpolation needed - clear adjacent brick cache.\n // no adjacent bricks -> reset the adjacent trackers\n c_PT_X_adjacent[c] = c_PT_Y_adjacent[c] = c_PT_Z_adjacent[c] = c_PT_XYZ_adjacent[c] = vec3(-1.0);\n c_brick_X_adjacent[c] = c_brick_Y_adjacent[c] = c_brick_Z_adjacent[c] = c_brick_XYZ_adjacent[c] = vec4(-1.0);\n }\n }\n\n // ========================================\n // VALUE NORMALIZATION AND TRACKING\n // ========================================\n \n // Normalize value to 0-1 range using channel-specific contrast limits.\n // we normalize the (accumulated) value to the range of the color channel\n c_val_current[c] = max(0.0, (val - getClim(c).x) / (getClim(c).y - getClim(c).x));\n\n // Track min/max values for MIP/MinIP rendering.\n // update the min and max values for the min/max projection\n if (c_minVal[c] == -1.0) {\n c_minVal[c] = c_val_current[c];\n } else {\n c_minVal[c] = min(c_minVal[c], c_val_current[c]);\n }\n c_maxVal[c] = max(c_maxVal[c], c_val_current[c]);\n\n }\n\n // ========================================\n // BRICK REQUEST GENERATION\n // ========================================\n \n // Request higher resolution bricks if we're using lower resolution than optimal.\n // potentially overwrite brick request\n if (!overWrittenRequest \n && c_res_current[c] != bestRes\n && c_val_current[c] > 0.0\n && c_renderMode_current[c] == 2\n && int(floor(rnd * float(maxChannels))) == c) {\n setBrickRequest(p, bestRes, slot, rnd);\n overWrittenRequest = true;\n }\n\n // ========================================\n // CHANNEL COMPOSITING\n // ========================================\n \n // Accumulate this channel's contribution.\n // sum up the values onto the slice values\n total += c_val_current[c];\n if (u_renderstyle == 3) {\n rgbCombo += c_val_current[c] * res_color[targetRes];\n } else if (u_renderstyle == 4) {\n rgbCombo += c_val_current[c] * res_color[c_res_current[c]];\n } else {\n rgbCombo += c_val_current[c] * c_color[c];\n }\n\n }\n\n // ========================================\n // FRONT-TO-BACK COMPOSITING\n // ========================================\n \n // Clamp total intensity and calculate alpha.\n // add the calculated slice to the total color\n total = clamp(total, 0.0, 1.0);\n sliceAlpha = total * opacity * dt * 32.0; // Scale by step size and brick size\n sliceColor = rgbCombo;\n\n // Front-to-back alpha blending\n outColor.rgb += sliceAlpha * alphaMultiplicator * sliceColor;\n outColor.a += sliceAlpha * alphaMultiplicator;\n alphaMultiplicator *= (1.0 - sliceAlpha);\n\n // Early termination for opaque regions (standard rendering only).\n // check if we can exit early\n if (outColor.a > 0.99 && u_renderstyle == 0) { break; }\n\n // ========================================\n // ADVANCE RAY POSITION\n // ========================================\n \n // Move to next sample position\n t += dt;\n p += dp;\n t_os += dt;\n }\n\n // ========================================\n // RENDERING STYLE POST-PROCESSING\n // ========================================\n \n if (u_renderstyle == 1) { \n // Minimum Intensity Projection (MinIP)\n // Shows the minimum value encountered along each ray\n outColor = vec4(0.0);\n for (int c = 0; c < 7; c++) {\n if (c_color[c] != vec3(0.0, 0.0, 0.0)) {\n outColor.rgb += c_minVal[c] * c_color[c];\n outColor.a += c_minVal[c];\n }\n }\n } else if (u_renderstyle == 0) { \n // Maximum Intensity Projection (MIP)\n // Shows the maximum value encountered along each ray\n outColor = vec4(0.0);\n for (int c = 0; c < 7; c++) {\n if (c_color[c] != vec3(0.0, 0.0, 0.0)) {\n outColor.rgb += c_maxVal[c] * c_color[c];\n }\n }\n outColor.a = 1.0;\n }\n\n // ========================================\n // FINAL OUTPUT\n // ========================================\n \n // Convert from linear to sRGB color space and set all render targets\n gColor = vec4(linear_to_srgb(outColor.r), \n linear_to_srgb(outColor.g), \n linear_to_srgb(outColor.b), \n outColor.a);\n \n}\n";
2
2
  //# sourceMappingURL=VolumeFragmentShader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeFragmentShader.d.ts","sourceRoot":"","sources":["../../src/shaders/VolumeFragmentShader.js"],"names":[],"mappings":"AAmLA,o4xEAsmDE"}
1
+ {"version":3,"file":"VolumeFragmentShader.d.ts","sourceRoot":"","sources":["../../src/shaders/VolumeFragmentShader.js"],"names":[],"mappings":"AAmLA,u/yEAwnDE"}
@@ -6,7 +6,7 @@
6
6
  * - Brick-based caching system for large datasets
7
7
  * - Multi-channel rendering with independent color/opacity
8
8
  * - Adaptive sampling based on distance and resolution
9
- * - Support for different rendering styles (MIP, MinIP, standard)
9
+ * - Support for different rendering styles (MIP, MinIP, standard, DEBUG)
10
10
  *
11
11
  * ========================================
12
12
  * BRICK CACHE AND PAGE TABLE EXPLANATION
@@ -203,7 +203,7 @@ uniform usampler3D pageTableTex;
203
203
  // ========================================
204
204
  // RENDERING PARAMETERS/CONSTANTS
205
205
  // ========================================
206
- // Rendering style: 0=MIP, 1=MinIP, 2=standard volume rendering
206
+ // Rendering style: 0=MIP, 1=MinIP, 2=standard volume rendering, 3=DEBUG
207
207
  uniform int u_renderstyle;
208
208
  // Global opacity multiplier for volume rendering
209
209
  uniform float opacity;
@@ -1215,7 +1215,19 @@ void main(void) {
1215
1215
  // TODO: figure out how to hook it up with frontend
1216
1216
  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);
1217
1217
  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);
1218
-
1218
+ vec3 [] res_color = vec3[10](
1219
+ vec3(1.0, 0.0, 0.0),
1220
+ vec3(0.0, 0.0, 1.0),
1221
+ vec3(0.0, 1.0, 0.0),
1222
+ vec3(1.0, 0.0, 1.0),
1223
+ vec3(0.0, 1.0, 1.0),
1224
+ vec3(1.0, 1.0, 0.0),
1225
+ vec3(1.0, 0.5, 0.5),
1226
+ vec3(0.5, 0.5, 1.0),
1227
+ vec3(0.5, 1.0, 0.5),
1228
+ vec3(0.5, 0.5, 0.5)
1229
+ );
1230
+
1219
1231
  // ========================================
1220
1232
  // PER-CHANNEL STATE ARRAYS
1221
1233
  // ========================================
@@ -1744,7 +1756,13 @@ void main(void) {
1744
1756
  // Accumulate this channel's contribution.
1745
1757
  // sum up the values onto the slice values
1746
1758
  total += c_val_current[c];
1747
- rgbCombo += c_val_current[c] * c_color[c];
1759
+ if (u_renderstyle == 3) {
1760
+ rgbCombo += c_val_current[c] * res_color[targetRes];
1761
+ } else if (u_renderstyle == 4) {
1762
+ rgbCombo += c_val_current[c] * res_color[c_res_current[c]];
1763
+ } else {
1764
+ rgbCombo += c_val_current[c] * c_color[c];
1765
+ }
1748
1766
 
1749
1767
  }
1750
1768
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/spatial-accelerated",
3
- "version": "3.8.10",
3
+ "version": "3.9.0",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -28,16 +28,16 @@
28
28
  "short-number": "^1.0.6",
29
29
  "three": "^0.154.0",
30
30
  "zarrita": "0.5.4",
31
- "@vitessce/constants-internal": "3.8.10",
32
- "@vitessce/gl": "3.8.10",
33
- "@vitessce/legend": "3.8.10",
34
- "@vitessce/sets-utils": "3.8.10",
35
- "@vitessce/spatial-utils": "3.8.10",
36
- "@vitessce/tooltip": "3.8.10",
37
- "@vitessce/utils": "3.8.10",
38
- "@vitessce/vit-s": "3.8.10",
39
- "@vitessce/styles": "3.8.10",
40
- "@vitessce/globals": "3.8.10"
31
+ "@vitessce/constants-internal": "3.9.0",
32
+ "@vitessce/gl": "3.9.0",
33
+ "@vitessce/legend": "3.9.0",
34
+ "@vitessce/sets-utils": "3.9.0",
35
+ "@vitessce/spatial-utils": "3.9.0",
36
+ "@vitessce/tooltip": "3.9.0",
37
+ "@vitessce/utils": "3.9.0",
38
+ "@vitessce/styles": "3.9.0",
39
+ "@vitessce/vit-s": "3.9.0",
40
+ "@vitessce/globals": "3.9.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/three": "^0.154.0",
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { Suspense } from 'react';
2
2
  // Note: need to be careful about versions of RTF, Three, React, and React DOM in package.json
3
3
  // to avoid multiple copies of RTF, since the Canvas depends on a React Context.
4
4
  // Otherwise you may see errors such as "useThree can only be used inside the Canvas component!".
@@ -50,11 +50,11 @@ export function SpatialWrapper(props) {
50
50
  autoClear: false,
51
51
  }}
52
52
  >
53
- <VolumeView
54
- {...props}
55
- // forwardRef={volumeViewRef}
56
- // onInitComplete={handleInitComplete}
57
- />
53
+ <Suspense fallback="Initializing Volume View...">
54
+ <VolumeView
55
+ {...props}
56
+ />
57
+ </Suspense>
58
58
  </Canvas>
59
59
  );
60
60
  }