From 1dbbaedffd2786896436a0cbc27ea3932093901f Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 23 Mar 2026 21:20:17 +0100 Subject: [PATCH 01/12] wip: working clipping --- .../core/examples/volume_rendering/main.ts | 52 ++++++++ packages/core/src/core/renderable_object.ts | 1 + packages/core/src/layers/volume_layer.ts | 121 +++++++++++++++--- .../objects/renderable/volume_renderable.ts | 18 +++ .../src/renderers/shaders/volume_frag.glsl | 8 +- packages/core/src/renderers/webgl_renderer.ts | 10 +- 6 files changed, 185 insertions(+), 25 deletions(-) diff --git a/packages/core/examples/volume_rendering/main.ts b/packages/core/examples/volume_rendering/main.ts index ba1af108..43a38829 100644 --- a/packages/core/examples/volume_rendering/main.ts +++ b/packages/core/examples/volume_rendering/main.ts @@ -167,6 +167,58 @@ volumeFolder .add(volumeLayer, "earlyTerminationAlpha", 0.8, 1.0, 0.01) .name("Early termination threshold"); +// Volume Bounds Controls +const boundsControls = { + minX: 0, + minY: 0, + minZ: 0, + maxX: 82.55, + maxY: 82.26, + maxZ: 8.5, +}; + +function updateVolumeBounds() { + const min = vec3.fromValues( + boundsControls.minX, + boundsControls.minY, + boundsControls.minZ + ); + const max = vec3.fromValues( + boundsControls.maxX, + boundsControls.maxY, + boundsControls.maxZ + ); + volumeLayer.setClipBounds(min, max); +} + +const boundsFolder = volumeFolder.addFolder("Clip Bounds"); + +boundsFolder + .add(boundsControls, "minX", 0, 82.55, 0.01) + .name("Min X") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "minY", 0, 82.26, 0.01) + .name("Min Y") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "minZ", 0, 8.5, 0.01) + .name("Min Z") + .onChange(updateVolumeBounds); + +boundsFolder + .add(boundsControls, "maxX", 0, 82.55, 0.01) + .name("Max X") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "maxY", 0, 82.26, 0.01) + .name("Max Y") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "maxZ", 0, 8.5, 0.01) + .name("Max Z") + .onChange(updateVolumeBounds); + const channelsFolder = gui.addFolder("Channels"); channelProps.forEach((config, index) => { createChannelControls(channelsFolder, config, index); diff --git a/packages/core/src/core/renderable_object.ts b/packages/core/src/core/renderable_object.ts index 666d40dc..c18f9be3 100644 --- a/packages/core/src/core/renderable_object.ts +++ b/packages/core/src/core/renderable_object.ts @@ -11,6 +11,7 @@ export abstract class RenderableObject extends Node { public wireframeEnabled = false; public wireframeColor = Color.WHITE; public depthTest = true; + public visible = true; private readonly textures_: Texture[] = []; private staleTextures_: Texture[] = []; private readonly transform_ = new TrsTransform(); diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index b942278e..9369aebe 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -8,6 +8,8 @@ import { RenderablePool } from "../utilities/renderable_pool"; import { vec3 } from "gl-matrix"; import { sortFrontToBack } from "../math/sort_by_distance"; import { ChannelProps, ChannelsEnabled } from "../objects/textures/channel"; +import { Box3 } from "@/math/box3"; +import { Logger } from "@/utilities/logger"; export type VolumeLayerProps = { source: ChunkSource; @@ -32,6 +34,10 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { private sourcePolicy_: ImageSourcePolicy; private chunkStoreView_?: ChunkStoreView; private channelProps_?: ChannelProps[]; + private clipBounds_ = new Box3( + vec3.fromValues(-Infinity, -Infinity, -Infinity), + vec3.fromValues(Infinity, Infinity, Infinity) + ); private lastLoadedTime_: number | undefined = undefined; private lastNumRenderedChannelChunks_: number | undefined = undefined; @@ -68,6 +74,18 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { } } + public setClipBounds(min?: vec3, max?: vec3) { + this.clipBounds_.min = min + ? vec3.clone(min) + : vec3.fromValues(-Infinity, -Infinity, -Infinity); + this.clipBounds_.max = max + ? vec3.clone(max) + : vec3.fromValues(Infinity, Infinity, Infinity); + for (const volume of this.currentVolumes_.values()) { + this.updateVolumeTransform(volume); + } + } + public setChannelProps(channelProps: ChannelProps[]) { this.channelProps_ = channelProps; for (const volume of this.currentVolumes_.values()) { @@ -177,24 +195,91 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { if (this.state !== "ready") this.setState("ready"); } - private updateVolumeTransform(volume: VolumeRenderable, chunk: Chunk) { - const worldSize = { - x: chunk.shape.x * chunk.scale.x, - y: chunk.shape.y * chunk.scale.y, - z: chunk.shape.z * chunk.scale.z, - }; - volume.transform.setScale([worldSize.x, worldSize.y, worldSize.z]); - vec3.set(volume.voxelScale, chunk.scale.x, chunk.scale.y, chunk.scale.z); - const originOffset = { - x: (chunk.shape.x * chunk.scale.x) / 2, - y: (chunk.shape.y * chunk.scale.y) / 2, - z: (chunk.shape.z * chunk.scale.z) / 2, - }; - volume.transform.setTranslation([ - chunk.offset.x + originOffset.x, - chunk.offset.y + originOffset.y, - chunk.offset.z + originOffset.z, - ]); + private updateVolumeTransform(volume: VolumeRenderable, chunk?: Chunk) { + if (chunk) { + volume.worldSize = vec3.fromValues( + chunk.shape.x * chunk.scale.x, + chunk.shape.y * chunk.scale.y, + chunk.shape.z * chunk.scale.z + ); + vec3.set(volume.voxelScale, chunk.scale.x, chunk.scale.y, chunk.scale.z); + const originOffset = { + x: volume.worldSize[0] / 2, + y: volume.worldSize[1] / 2, + z: volume.worldSize[2] / 2, + }; + volume.worldOrigin = vec3.fromValues( + chunk.offset.x + originOffset.x, + chunk.offset.y + originOffset.y, + chunk.offset.z + originOffset.z + ); + volume.boxMinWorld = vec3.fromValues( + chunk.offset.x, + chunk.offset.y, + chunk.offset.z + ); + volume.boxMaxWorld = vec3.fromValues( + chunk.offset.x + volume.worldSize[0], + chunk.offset.y + volume.worldSize[1], + chunk.offset.z + volume.worldSize[2] + ); + } + + const clippedVolumeMin = vec3.max( + vec3.create(), + volume.boxMinWorld, + this.clipBounds_.min + ); + const clippedVolumeMax = vec3.min( + vec3.create(), + volume.boxMaxWorld, + this.clipBounds_.max + ); + volume.visible = Box3.intersects(volume.boundingBox, this.clipBounds_); + if (!volume.visible) return; + + const boxWorldMinSize = vec3.subtract( + vec3.create(), + clippedVolumeMin, + volume.worldOrigin + ); + const boxWorldMaxSize = vec3.subtract( + vec3.create(), + clippedVolumeMax, + volume.worldOrigin + ); + volume.boxMinModel = vec3.fromValues( + boxWorldMinSize[0] / volume.worldSize[0], + boxWorldMinSize[1] / volume.worldSize[1], + boxWorldMinSize[2] / volume.worldSize[2] + ); + volume.boxMaxModel = vec3.fromValues( + boxWorldMaxSize[0] / volume.worldSize[0], + boxWorldMaxSize[1] / volume.worldSize[1], + boxWorldMaxSize[2] / volume.worldSize[2] + ); + + // TODO this is ending up slightly off + // const proxyScale = vec3.fromValues( + // clippedVolumeMax[0] - clippedVolumeMin[0], + // clippedVolumeMax[1] - clippedVolumeMin[1], + // clippedVolumeMax[2] - clippedVolumeMin[2] + // ); + // const proxyTranslation = vec3.fromValues( + // (clippedVolumeMax[0] + clippedVolumeMin[0]) / 2, + // (clippedVolumeMax[1] + clippedVolumeMin[1]) / 2, + // (clippedVolumeMax[2] + clippedVolumeMin[2]) / 2 + // ); + const scale = volume.worldSize; + const translation = volume.worldOrigin; + volume.transform.setScale(scale); + volume.transform.setTranslation(translation); + volume.realTransform.setScale(scale); + volume.realTransform.setTranslation(translation); + Logger.debug( + "VolumeLayer", + `Updated volume transform. World origin: ${volume.worldOrigin}, world size: ${volume.worldSize}, boxMinModel: ${volume.boxMinModel}, boxMaxModel: ${volume.boxMaxModel}, visible: ${volume.visible}` + ); } private releaseAndRemoveVolumes(volumes: Iterable) { diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index 6bf5086d..b4f2b393 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -11,9 +11,17 @@ import { import { Texture3D } from "../textures/texture_3d"; import { vec3 } from "gl-matrix"; import type { Chunk } from "../../data/chunk"; +import { TrsTransform } from "../../core/transforms"; export class VolumeRenderable extends RenderableObject { public voxelScale: vec3 = vec3.fromValues(1, 1, 1); + public boxMinModel = vec3.fromValues(0, 0, 0); + public boxMaxModel = vec3.fromValues(1, 1, 1); + public boxMinWorld = vec3.fromValues(0, 0, 0); + public boxMaxWorld = vec3.fromValues(1, 1, 1); + public worldSize = vec3.fromValues(1, 1, 1); + public worldOrigin = vec3.fromValues(0, 0, 0); + public realTransform = new TrsTransform(); private channels_: Required[]; private channelToTextureIndex_: Map = new Map(); @@ -129,6 +137,16 @@ export class VolumeRenderable extends RenderableObject { this.voxelScale[1], this.voxelScale[2], ], + BoxMinModel: [ + this.boxMinModel[0], + this.boxMinModel[1], + this.boxMinModel[2], + ], + BoxMaxModel: [ + this.boxMaxModel[0], + this.boxMaxModel[1], + this.boxMaxModel[2], + ], // TODO could cache and store transform } ); } diff --git a/packages/core/src/renderers/shaders/volume_frag.glsl b/packages/core/src/renderers/shaders/volume_frag.glsl index 15a48122..888f2e4d 100644 --- a/packages/core/src/renderers/shaders/volume_frag.glsl +++ b/packages/core/src/renderers/shaders/volume_frag.glsl @@ -29,10 +29,6 @@ uniform highp vec3 CameraPositionModel; uniform vec3 VoxelScale; in highp vec3 PositionModel; -// The bounding box in model space is normalized to -0.5 to 0.5 -vec3 boundingboxMin = vec3(-0.50); -vec3 boundingboxMax = vec3(0.50); - // Volume rendering parameters uniform bool DebugShowDegenerateRays; uniform float RelativeStepSize; @@ -44,6 +40,8 @@ uniform vec4 Visible; uniform vec4 ValueOffset; uniform vec4 ValueScale; uniform vec3 Color[4]; +uniform vec3 BoxMinModel; +uniform vec3 BoxMaxModel; vec2 findBoxIntersectionsAlongRay(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) { vec3 reciprocalRayDir = 1.0 / rayDir; @@ -87,7 +85,7 @@ void main() { // The ray in model space goes from the camera to the point on the back face vec3 RayDirModel = normalize(PositionModel - CameraPositionModel); - vec2 rayIntersections = findBoxIntersectionsAlongRay(CameraPositionModel, RayDirModel, boundingboxMin, boundingboxMax); + vec2 rayIntersections = findBoxIntersectionsAlongRay(CameraPositionModel, RayDirModel, BoxMinModel, BoxMaxModel); float tEnter = rayIntersections.x; float tExit = rayIntersections.y; diff --git a/packages/core/src/renderers/webgl_renderer.ts b/packages/core/src/renderers/webgl_renderer.ts index 18c8a16b..ff7b65b4 100644 --- a/packages/core/src/renderers/webgl_renderer.ts +++ b/packages/core/src/renderers/webgl_renderer.ts @@ -16,6 +16,7 @@ import { Camera } from "../objects/cameras/camera"; import { mat4, vec2, vec3, vec4 } from "gl-matrix"; import { Frustum } from "../math/frustum"; +import { VolumeRenderable } from "@/objects/renderable/volume_renderable"; // Idetik defines screen-space with +Y pointing downward. // With the default camera, the basis vectors are: @@ -150,7 +151,7 @@ export class WebGLRenderer extends Renderer { this.textures_.disposeTexture(texture); }); - if (!object.programName) return; + if (!object.programName || !object.visible) return; this.state_.setCullFaceMode(object.cullFaceMode); this.state_.setDepthTesting(object.depthTest); this.state_.setDepthMask(object.depthTest); @@ -217,7 +218,12 @@ export class WebGLRenderer extends Renderer { program.setUniform(uniformName, layer.opacity); break; case "CameraPositionModel": { - const inverseModelView = mat4.invert(mat4.create(), modelView); + const realModelView = mat4.multiply( + mat4.create(), + camera.viewMatrix, + (object as VolumeRenderable).realTransform.matrix + ); + const inverseModelView = mat4.invert(mat4.create(), realModelView); const cameraPositionView = vec4.fromValues(0, 0, 0, 1); const cameraPositionModel = vec4.transformMat4( vec4.create(), From 4ac564cc87424a138aef48072818f6d7466af044 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 23 Mar 2026 21:27:57 +0100 Subject: [PATCH 02/12] feat: proxy geom and shader clipping refactor: clean up solution fix: add update volume check refactor: move bulk of logic to volume renderable --- packages/core/src/layers/volume_layer.ts | 92 +--------------- .../objects/renderable/volume_renderable.ts | 103 +++++++++++++++--- .../src/renderers/shaders/volume_frag.glsl | 22 ++-- packages/core/src/renderers/webgl_renderer.ts | 8 +- 4 files changed, 103 insertions(+), 122 deletions(-) diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index 9369aebe..adc3afe9 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -9,7 +9,6 @@ import { vec3 } from "gl-matrix"; import { sortFrontToBack } from "../math/sort_by_distance"; import { ChannelProps, ChannelsEnabled } from "../objects/textures/channel"; import { Box3 } from "@/math/box3"; -import { Logger } from "@/utilities/logger"; export type VolumeLayerProps = { source: ChunkSource; @@ -82,7 +81,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { ? vec3.clone(max) : vec3.fromValues(Infinity, Infinity, Infinity); for (const volume of this.currentVolumes_.values()) { - this.updateVolumeTransform(volume); + volume.applyBoxClip(this.clipBounds_); } } @@ -141,7 +140,6 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { this.volumeToPoolKey_.set(volume, poolKey); for (const chunk of chunks) volume.updateVolumeWithChunk(chunk); - this.updateVolumeTransform(volume, chunks[0]); return volume; } @@ -186,6 +184,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { for (const [key, chunks] of groupedChunks) { const volume = this.getOrCreateVolume(key, chunks); + volume.applyBoxClip(this.clipBounds_); volume.wireframeEnabled = this.debugShowWireframes; this.currentVolumes_.set(key, volume); this.addObject(volume); @@ -195,93 +194,6 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { if (this.state !== "ready") this.setState("ready"); } - private updateVolumeTransform(volume: VolumeRenderable, chunk?: Chunk) { - if (chunk) { - volume.worldSize = vec3.fromValues( - chunk.shape.x * chunk.scale.x, - chunk.shape.y * chunk.scale.y, - chunk.shape.z * chunk.scale.z - ); - vec3.set(volume.voxelScale, chunk.scale.x, chunk.scale.y, chunk.scale.z); - const originOffset = { - x: volume.worldSize[0] / 2, - y: volume.worldSize[1] / 2, - z: volume.worldSize[2] / 2, - }; - volume.worldOrigin = vec3.fromValues( - chunk.offset.x + originOffset.x, - chunk.offset.y + originOffset.y, - chunk.offset.z + originOffset.z - ); - volume.boxMinWorld = vec3.fromValues( - chunk.offset.x, - chunk.offset.y, - chunk.offset.z - ); - volume.boxMaxWorld = vec3.fromValues( - chunk.offset.x + volume.worldSize[0], - chunk.offset.y + volume.worldSize[1], - chunk.offset.z + volume.worldSize[2] - ); - } - - const clippedVolumeMin = vec3.max( - vec3.create(), - volume.boxMinWorld, - this.clipBounds_.min - ); - const clippedVolumeMax = vec3.min( - vec3.create(), - volume.boxMaxWorld, - this.clipBounds_.max - ); - volume.visible = Box3.intersects(volume.boundingBox, this.clipBounds_); - if (!volume.visible) return; - - const boxWorldMinSize = vec3.subtract( - vec3.create(), - clippedVolumeMin, - volume.worldOrigin - ); - const boxWorldMaxSize = vec3.subtract( - vec3.create(), - clippedVolumeMax, - volume.worldOrigin - ); - volume.boxMinModel = vec3.fromValues( - boxWorldMinSize[0] / volume.worldSize[0], - boxWorldMinSize[1] / volume.worldSize[1], - boxWorldMinSize[2] / volume.worldSize[2] - ); - volume.boxMaxModel = vec3.fromValues( - boxWorldMaxSize[0] / volume.worldSize[0], - boxWorldMaxSize[1] / volume.worldSize[1], - boxWorldMaxSize[2] / volume.worldSize[2] - ); - - // TODO this is ending up slightly off - // const proxyScale = vec3.fromValues( - // clippedVolumeMax[0] - clippedVolumeMin[0], - // clippedVolumeMax[1] - clippedVolumeMin[1], - // clippedVolumeMax[2] - clippedVolumeMin[2] - // ); - // const proxyTranslation = vec3.fromValues( - // (clippedVolumeMax[0] + clippedVolumeMin[0]) / 2, - // (clippedVolumeMax[1] + clippedVolumeMin[1]) / 2, - // (clippedVolumeMax[2] + clippedVolumeMin[2]) / 2 - // ); - const scale = volume.worldSize; - const translation = volume.worldOrigin; - volume.transform.setScale(scale); - volume.transform.setTranslation(translation); - volume.realTransform.setScale(scale); - volume.realTransform.setTranslation(translation); - Logger.debug( - "VolumeLayer", - `Updated volume transform. World origin: ${volume.worldOrigin}, world size: ${volume.worldSize}, boxMinModel: ${volume.boxMinModel}, boxMaxModel: ${volume.boxMaxModel}, visible: ${volume.visible}` - ); - } - private releaseAndRemoveVolumes(volumes: Iterable) { for (const volume of volumes) { volume.clearLoadedChannels(); diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index b4f2b393..43a41464 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -11,17 +11,16 @@ import { import { Texture3D } from "../textures/texture_3d"; import { vec3 } from "gl-matrix"; import type { Chunk } from "../../data/chunk"; -import { TrsTransform } from "../../core/transforms"; +import { Box3 } from "@/math/box3"; export class VolumeRenderable extends RenderableObject { public voxelScale: vec3 = vec3.fromValues(1, 1, 1); - public boxMinModel = vec3.fromValues(0, 0, 0); - public boxMaxModel = vec3.fromValues(1, 1, 1); - public boxMinWorld = vec3.fromValues(0, 0, 0); - public boxMaxWorld = vec3.fromValues(1, 1, 1); - public worldSize = vec3.fromValues(1, 1, 1); - public worldOrigin = vec3.fromValues(0, 0, 0); - public realTransform = new TrsTransform(); + public boxMinUV = vec3.fromValues(0, 0, 0); + public boxSizeUV = vec3.fromValues(1, 1, 1); + private boxMinWorld_ = vec3.fromValues(0, 0, 0); + private boxMaxWorld_ = vec3.fromValues(1, 1, 1); + private worldSize_ = vec3.fromValues(1, 1, 1); + private worldOrigin_ = vec3.fromValues(0, 0, 0); private channels_: Required[]; private channelToTextureIndex_: Map = new Map(); @@ -49,9 +48,85 @@ export class VolumeRenderable extends RenderableObject { this.addChannelTexture(channelIndex, chunk); } + this.setSizeFromChunk(chunk); this.loadedChannels_.add(channelIndex); } + private setSizeFromChunk(chunk: Chunk) { + this.worldSize_ = vec3.fromValues( + chunk.shape.x * chunk.scale.x, + chunk.shape.y * chunk.scale.y, + chunk.shape.z * chunk.scale.z + ); + + vec3.set(this.voxelScale, chunk.scale.x, chunk.scale.y, chunk.scale.z); + + this.worldOrigin_ = vec3.fromValues( + chunk.offset.x + this.worldSize_[0] / 2, + chunk.offset.y + this.worldSize_[1] / 2, + chunk.offset.z + this.worldSize_[2] / 2 + ); + + this.boxMinWorld_ = vec3.fromValues( + chunk.offset.x, + chunk.offset.y, + chunk.offset.z + ); + + this.boxMaxWorld_ = vec3.fromValues( + chunk.offset.x + this.worldSize_[0], + chunk.offset.y + this.worldSize_[1], + chunk.offset.z + this.worldSize_[2] + ); + } + + public applyBoxClip(clipBounds: Box3) { + const clippedVolumeMin = vec3.max( + vec3.create(), + this.boxMinWorld_, + clipBounds.min + ); + const clippedVolumeMax = vec3.min( + vec3.create(), + this.boxMaxWorld_, + clipBounds.max + ); + const proxyGeometryScale = vec3.subtract( + vec3.create(), + clippedVolumeMax, + clippedVolumeMin + ); + const proxyGeometryCenter = vec3.add( + vec3.create(), + clippedVolumeMin, + vec3.scale(vec3.create(), proxyGeometryScale, 0.5) + ); + this.transform.setScale(proxyGeometryScale); + this.transform.setTranslation(proxyGeometryCenter); + this.visible = Box3.intersects(clipBounds, this.boundingBox); + + const boxWorldMinSize = vec3.subtract( + vec3.create(), + clippedVolumeMin, + this.worldOrigin_ + ); + const boxWorldMaxSize = vec3.subtract( + vec3.create(), + clippedVolumeMax, + this.worldOrigin_ + ); + this.boxMinUV = vec3.fromValues( + boxWorldMinSize[0] / this.worldSize_[0] + 0.5, + boxWorldMinSize[1] / this.worldSize_[1] + 0.5, + boxWorldMinSize[2] / this.worldSize_[2] + 0.5 + ); + this.boxSizeUV = vec3.fromValues( + (boxWorldMaxSize[0] - boxWorldMinSize[0]) / this.worldSize_[0], + (boxWorldMaxSize[1] - boxWorldMinSize[1]) / this.worldSize_[1], + (boxWorldMaxSize[2] - boxWorldMinSize[2]) / this.worldSize_[2] + ); + } + private addChannelTexture(channelIndex: number, chunk: Chunk): void { const texture = Texture3D.createWithChunk(chunk); const textureIndex = this.textures.length; @@ -137,16 +212,8 @@ export class VolumeRenderable extends RenderableObject { this.voxelScale[1], this.voxelScale[2], ], - BoxMinModel: [ - this.boxMinModel[0], - this.boxMinModel[1], - this.boxMinModel[2], - ], - BoxMaxModel: [ - this.boxMaxModel[0], - this.boxMaxModel[1], - this.boxMaxModel[2], - ], // TODO could cache and store transform + BoxMinUV: [this.boxMinUV[0], this.boxMinUV[1], this.boxMinUV[2]], + BoxSizeUV: [this.boxSizeUV[0], this.boxSizeUV[1], this.boxSizeUV[2]], } ); } diff --git a/packages/core/src/renderers/shaders/volume_frag.glsl b/packages/core/src/renderers/shaders/volume_frag.glsl index 888f2e4d..25df3aa2 100644 --- a/packages/core/src/renderers/shaders/volume_frag.glsl +++ b/packages/core/src/renderers/shaders/volume_frag.glsl @@ -29,6 +29,10 @@ uniform highp vec3 CameraPositionModel; uniform vec3 VoxelScale; in highp vec3 PositionModel; +// The bounding box in model space is normalized to -0.5 to 0.5 +vec3 boundingboxMin = vec3(-0.50); +vec3 boundingboxMax = vec3(0.50); + // Volume rendering parameters uniform bool DebugShowDegenerateRays; uniform float RelativeStepSize; @@ -40,8 +44,8 @@ uniform vec4 Visible; uniform vec4 ValueOffset; uniform vec4 ValueScale; uniform vec3 Color[4]; -uniform vec3 BoxMinModel; -uniform vec3 BoxMaxModel; +uniform vec3 BoxMinUV; +uniform vec3 BoxSizeUV; vec2 findBoxIntersectionsAlongRay(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) { vec3 reciprocalRayDir = 1.0 / rayDir; @@ -85,7 +89,8 @@ void main() { // The ray in model space goes from the camera to the point on the back face vec3 RayDirModel = normalize(PositionModel - CameraPositionModel); - vec2 rayIntersections = findBoxIntersectionsAlongRay(CameraPositionModel, RayDirModel, BoxMinModel, BoxMaxModel); + // Compute ray intersections in normalized model space of clipped proxy + vec2 rayIntersections = findBoxIntersectionsAlongRay(CameraPositionModel, RayDirModel, boundingboxMin, boundingboxMax); float tEnter = rayIntersections.x; float tExit = rayIntersections.y; @@ -94,10 +99,13 @@ void main() { return; } - vec3 entryPoint = CameraPositionModel + RayDirModel * tEnter; - entryPoint = clamp(entryPoint + 0.5, 0.0, 1.0); - vec3 exitPoint = CameraPositionModel + RayDirModel * tExit; - exitPoint = clamp(exitPoint + 0.5, 0.0, 1.0); + vec3 entryPoint = CameraPositionModel + RayDirModel * tEnter + 0.5; + vec3 exitPoint = CameraPositionModel + RayDirModel * tExit + 0.5; + + // Map from clipped proxy model space to full volume model space + // This allows supporting axis aligned clipping in volume rendering + entryPoint = clamp(BoxMinUV + (entryPoint * BoxSizeUV), 0.0, 1.0); + exitPoint = clamp(BoxMinUV + (exitPoint * BoxSizeUV), 0.0, 1.0); // Step 2 - calculate the number of samples based on the length of the ray vec3 rayWithinModel = exitPoint - entryPoint; diff --git a/packages/core/src/renderers/webgl_renderer.ts b/packages/core/src/renderers/webgl_renderer.ts index ff7b65b4..cd7e502a 100644 --- a/packages/core/src/renderers/webgl_renderer.ts +++ b/packages/core/src/renderers/webgl_renderer.ts @@ -16,7 +16,6 @@ import { Camera } from "../objects/cameras/camera"; import { mat4, vec2, vec3, vec4 } from "gl-matrix"; import { Frustum } from "../math/frustum"; -import { VolumeRenderable } from "@/objects/renderable/volume_renderable"; // Idetik defines screen-space with +Y pointing downward. // With the default camera, the basis vectors are: @@ -218,12 +217,7 @@ export class WebGLRenderer extends Renderer { program.setUniform(uniformName, layer.opacity); break; case "CameraPositionModel": { - const realModelView = mat4.multiply( - mat4.create(), - camera.viewMatrix, - (object as VolumeRenderable).realTransform.matrix - ); - const inverseModelView = mat4.invert(mat4.create(), realModelView); + const inverseModelView = mat4.invert(mat4.create(), modelView); const cameraPositionView = vec4.fromValues(0, 0, 0, 1); const cameraPositionModel = vec4.transformMat4( vec4.create(), From 3204f6a89d9231b0f541a17d7129187b1255d497 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 10:44:13 +0100 Subject: [PATCH 03/12] refactor: simplify to use box3 --- packages/core/src/layers/volume_layer.ts | 25 ++-- .../objects/renderable/volume_renderable.ts | 132 ++++++++++-------- .../src/renderers/shaders/volume_frag.glsl | 6 +- 3 files changed, 87 insertions(+), 76 deletions(-) diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index adc3afe9..8e106ea5 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -33,10 +33,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { private sourcePolicy_: ImageSourcePolicy; private chunkStoreView_?: ChunkStoreView; private channelProps_?: ChannelProps[]; - private clipBounds_ = new Box3( - vec3.fromValues(-Infinity, -Infinity, -Infinity), - vec3.fromValues(Infinity, Infinity, Infinity) - ); + private clipBounds_?: Box3; private lastLoadedTime_: number | undefined = undefined; private lastNumRenderedChannelChunks_: number | undefined = undefined; @@ -74,14 +71,18 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { } public setClipBounds(min?: vec3, max?: vec3) { - this.clipBounds_.min = min - ? vec3.clone(min) - : vec3.fromValues(-Infinity, -Infinity, -Infinity); - this.clipBounds_.max = max - ? vec3.clone(max) - : vec3.fromValues(Infinity, Infinity, Infinity); + if (!this.clipBounds_) { + this.clipBounds_ = new Box3(min, max); + } else { + this.clipBounds_.min = min + ? vec3.clone(min) + : vec3.fromValues(-Infinity, -Infinity, -Infinity); + this.clipBounds_.max = max + ? vec3.clone(max) + : vec3.fromValues(Infinity, Infinity, Infinity); + } for (const volume of this.currentVolumes_.values()) { - volume.applyBoxClip(this.clipBounds_); + volume.clipVolumeToBounds(this.clipBounds_); } } @@ -184,7 +185,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { for (const [key, chunks] of groupedChunks) { const volume = this.getOrCreateVolume(key, chunks); - volume.applyBoxClip(this.clipBounds_); + if (this.clipBounds_) volume.clipVolumeToBounds(this.clipBounds_); volume.wireframeEnabled = this.debugShowWireframes; this.currentVolumes_.set(key, volume); this.addObject(volume); diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index 43a41464..cbe90183 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -15,12 +15,11 @@ import { Box3 } from "@/math/box3"; export class VolumeRenderable extends RenderableObject { public voxelScale: vec3 = vec3.fromValues(1, 1, 1); - public boxMinUV = vec3.fromValues(0, 0, 0); - public boxSizeUV = vec3.fromValues(1, 1, 1); - private boxMinWorld_ = vec3.fromValues(0, 0, 0); - private boxMaxWorld_ = vec3.fromValues(1, 1, 1); - private worldSize_ = vec3.fromValues(1, 1, 1); - private worldOrigin_ = vec3.fromValues(0, 0, 0); + private fullVolumeWorldBounds_: Box3 = new Box3(); + private clippedVolumeUVWBounds_: Box3 = new Box3( + vec3.fromValues(0, 0, 0), + vec3.fromValues(1, 1, 1) + ); private channels_: Required[]; private channelToTextureIndex_: Map = new Map(); @@ -48,82 +47,85 @@ export class VolumeRenderable extends RenderableObject { this.addChannelTexture(channelIndex, chunk); } - this.setSizeFromChunk(chunk); + this.updateWorldScaleAndBoundsFromChunk(chunk); this.loadedChannels_.add(channelIndex); } - private setSizeFromChunk(chunk: Chunk) { - this.worldSize_ = vec3.fromValues( - chunk.shape.x * chunk.scale.x, - chunk.shape.y * chunk.scale.y, - chunk.shape.z * chunk.scale.z - ); - + private updateWorldScaleAndBoundsFromChunk(chunk: Chunk) { vec3.set(this.voxelScale, chunk.scale.x, chunk.scale.y, chunk.scale.z); - - this.worldOrigin_ = vec3.fromValues( - chunk.offset.x + this.worldSize_[0] / 2, - chunk.offset.y + this.worldSize_[1] / 2, - chunk.offset.z + this.worldSize_[2] / 2 - ); - - this.boxMinWorld_ = vec3.fromValues( + this.fullVolumeWorldBounds_.min = vec3.fromValues( chunk.offset.x, chunk.offset.y, chunk.offset.z ); - - this.boxMaxWorld_ = vec3.fromValues( - chunk.offset.x + this.worldSize_[0], - chunk.offset.y + this.worldSize_[1], - chunk.offset.z + this.worldSize_[2] + this.fullVolumeWorldBounds_.max = vec3.fromValues( + chunk.offset.x + chunk.shape.x * chunk.scale.x, + chunk.offset.y + chunk.shape.y * chunk.scale.y, + chunk.offset.z + chunk.shape.z * chunk.scale.z + ); + this.transform.setScale( + vec3.subtract( + vec3.create(), + this.fullVolumeWorldBounds_.max, + this.fullVolumeWorldBounds_.min + ) + ); + this.transform.setTranslation( + vec3.scaleAndAdd( + vec3.create(), + this.fullVolumeWorldBounds_.min, + vec3.subtract( + vec3.create(), + this.fullVolumeWorldBounds_.max, + this.fullVolumeWorldBounds_.min + ), + 0.5 + ) ); } - public applyBoxClip(clipBounds: Box3) { - const clippedVolumeMin = vec3.max( + public clipVolumeToBounds(clipBounds: Box3) { + // Set proxy geometry transform to match the clipped region + const clippedMin = vec3.max( vec3.create(), - this.boxMinWorld_, + this.fullVolumeWorldBounds_.min, clipBounds.min ); - const clippedVolumeMax = vec3.min( + const clippedMax = vec3.min( vec3.create(), - this.boxMaxWorld_, + this.fullVolumeWorldBounds_.max, clipBounds.max ); - const proxyGeometryScale = vec3.subtract( + const proxySize = vec3.subtract(vec3.create(), clippedMax, clippedMin); + const proxyCenter = vec3.scaleAndAdd( vec3.create(), - clippedVolumeMax, - clippedVolumeMin + clippedMin, + proxySize, + 0.5 ); - const proxyGeometryCenter = vec3.add( + this.transform.setScale(proxySize); + this.transform.setTranslation(proxyCenter); + this.visible = Box3.intersects(clipBounds, this.fullVolumeWorldBounds_); + if (!this.visible) return; + + // Compute UVW bounds for the clipped region + // Transform clipped world bounds to normalized volume space [0,1] + const volumeSize = vec3.subtract( vec3.create(), - clippedVolumeMin, - vec3.scale(vec3.create(), proxyGeometryScale, 0.5) + this.fullVolumeWorldBounds_.max, + this.fullVolumeWorldBounds_.min ); - this.transform.setScale(proxyGeometryScale); - this.transform.setTranslation(proxyGeometryCenter); - this.visible = Box3.intersects(clipBounds, this.boundingBox); - const boxWorldMinSize = vec3.subtract( - vec3.create(), - clippedVolumeMin, - this.worldOrigin_ - ); - const boxWorldMaxSize = vec3.subtract( - vec3.create(), - clippedVolumeMax, - this.worldOrigin_ - ); - this.boxMinUV = vec3.fromValues( - boxWorldMinSize[0] / this.worldSize_[0] + 0.5, - boxWorldMinSize[1] / this.worldSize_[1] + 0.5, - boxWorldMinSize[2] / this.worldSize_[2] + 0.5 + this.clippedVolumeUVWBounds_.min = vec3.fromValues( + (clippedMin[0] - this.fullVolumeWorldBounds_.min[0]) / volumeSize[0], + (clippedMin[1] - this.fullVolumeWorldBounds_.min[1]) / volumeSize[1], + (clippedMin[2] - this.fullVolumeWorldBounds_.min[2]) / volumeSize[2] ); - this.boxSizeUV = vec3.fromValues( - (boxWorldMaxSize[0] - boxWorldMinSize[0]) / this.worldSize_[0], - (boxWorldMaxSize[1] - boxWorldMinSize[1]) / this.worldSize_[1], - (boxWorldMaxSize[2] - boxWorldMinSize[2]) / this.worldSize_[2] + + this.clippedVolumeUVWBounds_.max = vec3.fromValues( + (clippedMax[0] - this.fullVolumeWorldBounds_.min[0]) / volumeSize[0], + (clippedMax[1] - this.fullVolumeWorldBounds_.min[1]) / volumeSize[1], + (clippedMax[2] - this.fullVolumeWorldBounds_.min[2]) / volumeSize[2] ); } @@ -212,8 +214,16 @@ export class VolumeRenderable extends RenderableObject { this.voxelScale[1], this.voxelScale[2], ], - BoxMinUV: [this.boxMinUV[0], this.boxMinUV[1], this.boxMinUV[2]], - BoxSizeUV: [this.boxSizeUV[0], this.boxSizeUV[1], this.boxSizeUV[2]], + BoxMinUV: [ + this.clippedVolumeUVWBounds_.min[0], + this.clippedVolumeUVWBounds_.min[1], + this.clippedVolumeUVWBounds_.min[2], + ], + BoxMaxUV: [ + this.clippedVolumeUVWBounds_.max[0], + this.clippedVolumeUVWBounds_.max[1], + this.clippedVolumeUVWBounds_.max[2], + ], } ); } diff --git a/packages/core/src/renderers/shaders/volume_frag.glsl b/packages/core/src/renderers/shaders/volume_frag.glsl index 25df3aa2..2860462e 100644 --- a/packages/core/src/renderers/shaders/volume_frag.glsl +++ b/packages/core/src/renderers/shaders/volume_frag.glsl @@ -45,7 +45,7 @@ uniform vec4 ValueOffset; uniform vec4 ValueScale; uniform vec3 Color[4]; uniform vec3 BoxMinUV; -uniform vec3 BoxSizeUV; +uniform vec3 BoxMaxUV; vec2 findBoxIntersectionsAlongRay(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) { vec3 reciprocalRayDir = 1.0 / rayDir; @@ -104,8 +104,8 @@ void main() { // Map from clipped proxy model space to full volume model space // This allows supporting axis aligned clipping in volume rendering - entryPoint = clamp(BoxMinUV + (entryPoint * BoxSizeUV), 0.0, 1.0); - exitPoint = clamp(BoxMinUV + (exitPoint * BoxSizeUV), 0.0, 1.0); + entryPoint = clamp(BoxMinUV + entryPoint * (BoxMaxUV - BoxMinUV), 0.0, 1.0); + exitPoint = clamp(BoxMinUV + exitPoint * (BoxMaxUV - BoxMinUV), 0.0, 1.0); // Step 2 - calculate the number of samples based on the length of the ray vec3 rayWithinModel = exitPoint - entryPoint; From 3a4c18c525b756458afd0c488d4f12edfa27f5cd Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 11:52:47 +0100 Subject: [PATCH 04/12] fix: protect against incorrect bounds or equal bounds --- packages/core/src/objects/renderable/volume_renderable.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index cbe90183..e8b84833 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -97,6 +97,10 @@ export class VolumeRenderable extends RenderableObject { clipBounds.max ); const proxySize = vec3.subtract(vec3.create(), clippedMax, clippedMin); + if (proxySize[0] <= 0 || proxySize[1] <= 0 || proxySize[2] <= 0) { + this.visible = false; + return; + } const proxyCenter = vec3.scaleAndAdd( vec3.create(), clippedMin, From 8dc3283009a0a9b851ebab7acd73a65175ad6e4b Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 11:59:17 +0100 Subject: [PATCH 05/12] refactor: remove magic num in example --- .../core/examples/volume_rendering/main.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/core/examples/volume_rendering/main.ts b/packages/core/examples/volume_rendering/main.ts index 43a38829..32d23d3a 100644 --- a/packages/core/examples/volume_rendering/main.ts +++ b/packages/core/examples/volume_rendering/main.ts @@ -167,14 +167,14 @@ volumeFolder .add(volumeLayer, "earlyTerminationAlpha", 0.8, 1.0, 0.01) .name("Early termination threshold"); -// Volume Bounds Controls +const volumeMaxBounds = { x: 82.55, y: 82.26, z: 8.5 }; const boundsControls = { minX: 0, minY: 0, minZ: 0, - maxX: 82.55, - maxY: 82.26, - maxZ: 8.5, + maxX: volumeMaxBounds.x, + maxY: volumeMaxBounds.y, + maxZ: volumeMaxBounds.z, }; function updateVolumeBounds() { @@ -192,30 +192,29 @@ function updateVolumeBounds() { } const boundsFolder = volumeFolder.addFolder("Clip Bounds"); - boundsFolder - .add(boundsControls, "minX", 0, 82.55, 0.01) + .add(boundsControls, "minX", 0, volumeMaxBounds.x, 0.01) .name("Min X") .onChange(updateVolumeBounds); boundsFolder - .add(boundsControls, "minY", 0, 82.26, 0.01) + .add(boundsControls, "minY", 0, volumeMaxBounds.y, 0.01) .name("Min Y") .onChange(updateVolumeBounds); boundsFolder - .add(boundsControls, "minZ", 0, 8.5, 0.01) + .add(boundsControls, "minZ", 0, volumeMaxBounds.z, 0.01) .name("Min Z") .onChange(updateVolumeBounds); boundsFolder - .add(boundsControls, "maxX", 0, 82.55, 0.01) + .add(boundsControls, "maxX", 0, volumeMaxBounds.x, 0.01) .name("Max X") .onChange(updateVolumeBounds); boundsFolder - .add(boundsControls, "maxY", 0, 82.26, 0.01) + .add(boundsControls, "maxY", 0, volumeMaxBounds.y, 0.01) .name("Max Y") .onChange(updateVolumeBounds); boundsFolder - .add(boundsControls, "maxZ", 0, 8.5, 0.01) + .add(boundsControls, "maxZ", 0, volumeMaxBounds.z, 0.01) .name("Max Z") .onChange(updateVolumeBounds); From 0617ddf36e08c2f8a6ab5e3942594c8749a2f8e7 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 11:59:34 +0100 Subject: [PATCH 06/12] fix: correct bounds setting on partial bounds passed --- packages/core/src/layers/volume_layer.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index 8e106ea5..e075c7ec 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -71,15 +71,17 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { } public setClipBounds(min?: vec3, max?: vec3) { + const minBound = min + ? vec3.clone(min) + : vec3.fromValues(-Infinity, -Infinity, -Infinity); + const maxBound = max + ? vec3.clone(max) + : vec3.fromValues(Infinity, Infinity, Infinity); if (!this.clipBounds_) { - this.clipBounds_ = new Box3(min, max); + this.clipBounds_ = new Box3(minBound, maxBound); } else { - this.clipBounds_.min = min - ? vec3.clone(min) - : vec3.fromValues(-Infinity, -Infinity, -Infinity); - this.clipBounds_.max = max - ? vec3.clone(max) - : vec3.fromValues(Infinity, Infinity, Infinity); + this.clipBounds_.min = minBound; + this.clipBounds_.max = maxBound; } for (const volume of this.currentVolumes_.values()) { volume.clipVolumeToBounds(this.clipBounds_); From 077e0d7790ccfdff93e1e5f941f27a77d8583449 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 12:06:33 +0100 Subject: [PATCH 07/12] feat: change api to allow undefined bounds reset --- packages/core/src/layers/volume_layer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index e075c7ec..814bbc75 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -71,6 +71,10 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { } public setClipBounds(min?: vec3, max?: vec3) { + if (min === undefined && max === undefined) { + this.clipBounds_ = undefined; + return; + } const minBound = min ? vec3.clone(min) : vec3.fromValues(-Infinity, -Infinity, -Infinity); From 7f31a2f081ab34b0ccff77fc8f22da02c0401211 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 12:12:41 +0100 Subject: [PATCH 08/12] perf: avoid multiple vec computations per volume --- packages/core/src/layers/volume_layer.ts | 2 ++ packages/core/src/objects/renderable/volume_renderable.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index 814bbc75..14d44272 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -138,6 +138,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { const existing = this.currentVolumes_.get(key); if (existing) { for (const chunk of chunks) existing.updateVolumeWithChunk(chunk); + existing.updateWorldScaleAndBoundsFromChunk(chunks[0]); return existing; } @@ -147,6 +148,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { this.volumeToPoolKey_.set(volume, poolKey); for (const chunk of chunks) volume.updateVolumeWithChunk(chunk); + volume.updateWorldScaleAndBoundsFromChunk(chunks[0]); return volume; } diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index e8b84833..3c064153 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -47,11 +47,10 @@ export class VolumeRenderable extends RenderableObject { this.addChannelTexture(channelIndex, chunk); } - this.updateWorldScaleAndBoundsFromChunk(chunk); this.loadedChannels_.add(channelIndex); } - private updateWorldScaleAndBoundsFromChunk(chunk: Chunk) { + public updateWorldScaleAndBoundsFromChunk(chunk: Chunk) { vec3.set(this.voxelScale, chunk.scale.x, chunk.scale.y, chunk.scale.z); this.fullVolumeWorldBounds_.min = vec3.fromValues( chunk.offset.x, From 2d65308395baf6b4e29ff8e8d8aca2e0ffffa111 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 12:18:38 +0100 Subject: [PATCH 09/12] perf: early return if non visible --- .../core/src/objects/renderable/volume_renderable.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index 3c064153..1335fd01 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -84,7 +84,9 @@ export class VolumeRenderable extends RenderableObject { } public clipVolumeToBounds(clipBounds: Box3) { - // Set proxy geometry transform to match the clipped region + this.visible = Box3.intersects(clipBounds, this.fullVolumeWorldBounds_); + if (!this.visible) return; + const clippedMin = vec3.max( vec3.create(), this.fullVolumeWorldBounds_.min, @@ -108,17 +110,12 @@ export class VolumeRenderable extends RenderableObject { ); this.transform.setScale(proxySize); this.transform.setTranslation(proxyCenter); - this.visible = Box3.intersects(clipBounds, this.fullVolumeWorldBounds_); - if (!this.visible) return; - // Compute UVW bounds for the clipped region - // Transform clipped world bounds to normalized volume space [0,1] const volumeSize = vec3.subtract( vec3.create(), this.fullVolumeWorldBounds_.max, this.fullVolumeWorldBounds_.min ); - this.clippedVolumeUVWBounds_.min = vec3.fromValues( (clippedMin[0] - this.fullVolumeWorldBounds_.min[0]) / volumeSize[0], (clippedMin[1] - this.fullVolumeWorldBounds_.min[1]) / volumeSize[1], From 3fc398e9c50b3773cd3481d444cfc64ac99dbb91 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 12:21:30 +0100 Subject: [PATCH 10/12] refactor: change 2d tex coord uv name to 3d name uvw --- .../core/src/objects/renderable/volume_renderable.ts | 4 ++-- packages/core/src/renderers/shaders/volume_frag.glsl | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index 1335fd01..174a610e 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -214,12 +214,12 @@ export class VolumeRenderable extends RenderableObject { this.voxelScale[1], this.voxelScale[2], ], - BoxMinUV: [ + BoxMinUVW: [ this.clippedVolumeUVWBounds_.min[0], this.clippedVolumeUVWBounds_.min[1], this.clippedVolumeUVWBounds_.min[2], ], - BoxMaxUV: [ + BoxMaxUVW: [ this.clippedVolumeUVWBounds_.max[0], this.clippedVolumeUVWBounds_.max[1], this.clippedVolumeUVWBounds_.max[2], diff --git a/packages/core/src/renderers/shaders/volume_frag.glsl b/packages/core/src/renderers/shaders/volume_frag.glsl index 2860462e..e4324495 100644 --- a/packages/core/src/renderers/shaders/volume_frag.glsl +++ b/packages/core/src/renderers/shaders/volume_frag.glsl @@ -44,8 +44,8 @@ uniform vec4 Visible; uniform vec4 ValueOffset; uniform vec4 ValueScale; uniform vec3 Color[4]; -uniform vec3 BoxMinUV; -uniform vec3 BoxMaxUV; +uniform vec3 BoxMinUVW; +uniform vec3 BoxMaxUVW; vec2 findBoxIntersectionsAlongRay(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) { vec3 reciprocalRayDir = 1.0 / rayDir; @@ -102,10 +102,9 @@ void main() { vec3 entryPoint = CameraPositionModel + RayDirModel * tEnter + 0.5; vec3 exitPoint = CameraPositionModel + RayDirModel * tExit + 0.5; - // Map from clipped proxy model space to full volume model space - // This allows supporting axis aligned clipping in volume rendering - entryPoint = clamp(BoxMinUV + entryPoint * (BoxMaxUV - BoxMinUV), 0.0, 1.0); - exitPoint = clamp(BoxMinUV + exitPoint * (BoxMaxUV - BoxMinUV), 0.0, 1.0); + // Map from clipped proxy model space to texture UVW space + entryPoint = clamp(BoxMinUVW + entryPoint * (BoxMaxUVW - BoxMinUVW), 0.0, 1.0); + exitPoint = clamp(BoxMinUVW + exitPoint * (BoxMaxUVW - BoxMinUVW), 0.0, 1.0); // Step 2 - calculate the number of samples based on the length of the ray vec3 rayWithinModel = exitPoint - entryPoint; From 5c33f437b058893a4fe1bae877796562e0e185f9 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 12:32:54 +0100 Subject: [PATCH 11/12] refactor: remove uneeded name "volume" on volume function --- packages/core/src/layers/volume_layer.ts | 4 ++-- packages/core/src/objects/renderable/volume_renderable.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index 14d44272..6e7b4ff2 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -88,7 +88,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { this.clipBounds_.max = maxBound; } for (const volume of this.currentVolumes_.values()) { - volume.clipVolumeToBounds(this.clipBounds_); + volume.clipToBounds(this.clipBounds_); } } @@ -193,7 +193,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { for (const [key, chunks] of groupedChunks) { const volume = this.getOrCreateVolume(key, chunks); - if (this.clipBounds_) volume.clipVolumeToBounds(this.clipBounds_); + if (this.clipBounds_) volume.clipToBounds(this.clipBounds_); volume.wireframeEnabled = this.debugShowWireframes; this.currentVolumes_.set(key, volume); this.addObject(volume); diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index 174a610e..552eaf9b 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -83,7 +83,7 @@ export class VolumeRenderable extends RenderableObject { ); } - public clipVolumeToBounds(clipBounds: Box3) { + public clipToBounds(clipBounds: Box3) { this.visible = Box3.intersects(clipBounds, this.fullVolumeWorldBounds_); if (!this.visible) return; From da39c73934b7ab5face6112be2e33c5ce7ef5f82 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Tue, 24 Mar 2026 14:38:06 +0100 Subject: [PATCH 12/12] refactor: restrict object visibility to protected --- packages/core/src/core/renderable_object.ts | 6 +++++- packages/core/src/objects/renderable/volume_renderable.ts | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/core/renderable_object.ts b/packages/core/src/core/renderable_object.ts index c18f9be3..d2a01154 100644 --- a/packages/core/src/core/renderable_object.ts +++ b/packages/core/src/core/renderable_object.ts @@ -11,7 +11,7 @@ export abstract class RenderableObject extends Node { public wireframeEnabled = false; public wireframeColor = Color.WHITE; public depthTest = true; - public visible = true; + protected visible_ = true; private readonly textures_: Texture[] = []; private staleTextures_: Texture[] = []; private readonly transform_ = new TrsTransform(); @@ -34,6 +34,10 @@ export abstract class RenderableObject extends Node { return stale; } + public get visible() { + return this.visible_; + } + public get geometry() { return this.geometry_; } diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index 552eaf9b..5564248e 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -84,8 +84,8 @@ export class VolumeRenderable extends RenderableObject { } public clipToBounds(clipBounds: Box3) { - this.visible = Box3.intersects(clipBounds, this.fullVolumeWorldBounds_); - if (!this.visible) return; + this.visible_ = Box3.intersects(clipBounds, this.fullVolumeWorldBounds_); + if (!this.visible_) return; const clippedMin = vec3.max( vec3.create(), @@ -99,7 +99,7 @@ export class VolumeRenderable extends RenderableObject { ); const proxySize = vec3.subtract(vec3.create(), clippedMax, clippedMin); if (proxySize[0] <= 0 || proxySize[1] <= 0 || proxySize[2] <= 0) { - this.visible = false; + this.visible_ = false; return; } const proxyCenter = vec3.scaleAndAdd(