diff --git a/packages/core/examples/volume_rendering/main.ts b/packages/core/examples/volume_rendering/main.ts index ba1af108..32d23d3a 100644 --- a/packages/core/examples/volume_rendering/main.ts +++ b/packages/core/examples/volume_rendering/main.ts @@ -167,6 +167,57 @@ volumeFolder .add(volumeLayer, "earlyTerminationAlpha", 0.8, 1.0, 0.01) .name("Early termination threshold"); +const volumeMaxBounds = { x: 82.55, y: 82.26, z: 8.5 }; +const boundsControls = { + minX: 0, + minY: 0, + minZ: 0, + maxX: volumeMaxBounds.x, + maxY: volumeMaxBounds.y, + maxZ: volumeMaxBounds.z, +}; + +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, volumeMaxBounds.x, 0.01) + .name("Min X") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "minY", 0, volumeMaxBounds.y, 0.01) + .name("Min Y") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "minZ", 0, volumeMaxBounds.z, 0.01) + .name("Min Z") + .onChange(updateVolumeBounds); + +boundsFolder + .add(boundsControls, "maxX", 0, volumeMaxBounds.x, 0.01) + .name("Max X") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "maxY", 0, volumeMaxBounds.y, 0.01) + .name("Max Y") + .onChange(updateVolumeBounds); +boundsFolder + .add(boundsControls, "maxZ", 0, volumeMaxBounds.z, 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..d2a01154 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; + protected visible_ = true; private readonly textures_: Texture[] = []; private staleTextures_: Texture[] = []; private readonly transform_ = new TrsTransform(); @@ -33,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/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index b942278e..6e7b4ff2 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -8,6 +8,7 @@ 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"; export type VolumeLayerProps = { source: ChunkSource; @@ -32,6 +33,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { private sourcePolicy_: ImageSourcePolicy; private chunkStoreView_?: ChunkStoreView; private channelProps_?: ChannelProps[]; + private clipBounds_?: Box3; private lastLoadedTime_: number | undefined = undefined; private lastNumRenderedChannelChunks_: number | undefined = undefined; @@ -68,6 +70,28 @@ 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); + const maxBound = max + ? vec3.clone(max) + : vec3.fromValues(Infinity, Infinity, Infinity); + if (!this.clipBounds_) { + this.clipBounds_ = new Box3(minBound, maxBound); + } else { + this.clipBounds_.min = minBound; + this.clipBounds_.max = maxBound; + } + for (const volume of this.currentVolumes_.values()) { + volume.clipToBounds(this.clipBounds_); + } + } + public setChannelProps(channelProps: ChannelProps[]) { this.channelProps_ = channelProps; for (const volume of this.currentVolumes_.values()) { @@ -114,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; } @@ -123,7 +148,7 @@ 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]); + volume.updateWorldScaleAndBoundsFromChunk(chunks[0]); return volume; } @@ -168,6 +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.clipToBounds(this.clipBounds_); volume.wireframeEnabled = this.debugShowWireframes; this.currentVolumes_.set(key, volume); this.addObject(volume); @@ -177,26 +203,6 @@ 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 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 6bf5086d..5564248e 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -11,9 +11,15 @@ import { import { Texture3D } from "../textures/texture_3d"; import { vec3 } from "gl-matrix"; import type { Chunk } from "../../data/chunk"; +import { Box3 } from "@/math/box3"; export class VolumeRenderable extends RenderableObject { public voxelScale: vec3 = vec3.fromValues(1, 1, 1); + 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(); @@ -44,6 +50,85 @@ export class VolumeRenderable extends RenderableObject { this.loadedChannels_.add(channelIndex); } + 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, + chunk.offset.y, + chunk.offset.z + ); + 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 clipToBounds(clipBounds: Box3) { + this.visible_ = Box3.intersects(clipBounds, this.fullVolumeWorldBounds_); + if (!this.visible_) return; + + const clippedMin = vec3.max( + vec3.create(), + this.fullVolumeWorldBounds_.min, + clipBounds.min + ); + const clippedMax = vec3.min( + vec3.create(), + this.fullVolumeWorldBounds_.max, + 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, + proxySize, + 0.5 + ); + this.transform.setScale(proxySize); + this.transform.setTranslation(proxyCenter); + + 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], + (clippedMin[2] - this.fullVolumeWorldBounds_.min[2]) / volumeSize[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] + ); + } + private addChannelTexture(channelIndex: number, chunk: Chunk): void { const texture = Texture3D.createWithChunk(chunk); const textureIndex = this.textures.length; @@ -129,6 +214,16 @@ export class VolumeRenderable extends RenderableObject { this.voxelScale[1], this.voxelScale[2], ], + BoxMinUVW: [ + this.clippedVolumeUVWBounds_.min[0], + this.clippedVolumeUVWBounds_.min[1], + this.clippedVolumeUVWBounds_.min[2], + ], + 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 15a48122..e4324495 100644 --- a/packages/core/src/renderers/shaders/volume_frag.glsl +++ b/packages/core/src/renderers/shaders/volume_frag.glsl @@ -44,6 +44,8 @@ uniform vec4 Visible; uniform vec4 ValueOffset; uniform vec4 ValueScale; uniform vec3 Color[4]; +uniform vec3 BoxMinUVW; +uniform vec3 BoxMaxUVW; vec2 findBoxIntersectionsAlongRay(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) { vec3 reciprocalRayDir = 1.0 / rayDir; @@ -87,6 +89,7 @@ void main() { // The ray in model space goes from the camera to the point on the back face vec3 RayDirModel = normalize(PositionModel - CameraPositionModel); + // 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; @@ -96,10 +99,12 @@ 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 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; diff --git a/packages/core/src/renderers/webgl_renderer.ts b/packages/core/src/renderers/webgl_renderer.ts index 18c8a16b..cd7e502a 100644 --- a/packages/core/src/renderers/webgl_renderer.ts +++ b/packages/core/src/renderers/webgl_renderer.ts @@ -150,7 +150,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);