From f2074d7d604f656074acf0301fb5c2ee41002d36 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Mon, 24 Feb 2025 18:08:21 +0100 Subject: [PATCH] feat: option to hide cross section background in 3D slice views (#663) --- docs/json_schema/viewer_state.yml | 4 ++ python/neuroglancer/viewer_state.py | 3 + src/data_panel_layout.ts | 2 + src/layer_group_viewer.ts | 4 ++ src/layer_groups_layout.ts | 1 + src/perspective_view/panel.ts | 20 +++++-- src/sliceview/frontend.ts | 92 +++++++++++++++++++++++------ src/sliceview/panel.ts | 16 ++++- src/ui/viewer_settings.ts | 4 ++ src/viewer.ts | 5 ++ 10 files changed, 127 insertions(+), 24 deletions(-) diff --git a/docs/json_schema/viewer_state.yml b/docs/json_schema/viewer_state.yml index 31f925b9cd..0b927c6c57 100644 --- a/docs/json_schema/viewer_state.yml +++ b/docs/json_schema/viewer_state.yml @@ -57,6 +57,10 @@ properties: type: boolean title: "Indicates whether to show cross sections in the 3-d view of `~DataPanelLayoutType.4panel` and `~DataPanelLayoutType.4panel-alt` layouts." default: true + hideCrossSectionBackground3D: + type: boolean + title: "Indicates whether to hide the background of cross-section views in the 3-d view of `~DataPanelLayoutType.4panel` and `~DataPanelLayoutType.4panel-alt` layouts." + default: false gpuMemoryLimit: type: integer title: "GPU memory limit, in bytes." diff --git a/python/neuroglancer/viewer_state.py b/python/neuroglancer/viewer_state.py index cc596d7492..bd8502ac9c 100644 --- a/python/neuroglancer/viewer_state.py +++ b/python/neuroglancer/viewer_state.py @@ -1797,6 +1797,9 @@ class ViewerState(JsonObjectWrapper): "projectionOrientation", optional(array_wrapper(np.float32, 4)) ) show_slices = showSlices = wrapped_property("showSlices", optional(bool, True)) + hide_cross_section_background_3d = hideCrossSectionBackground3D = wrapped_property( + "hideCrossSectionBackground3D", optional(bool, False) + ) show_axis_lines = showAxisLines = wrapped_property( "showAxisLines", optional(bool, True) ) diff --git a/src/data_panel_layout.ts b/src/data_panel_layout.ts index fda6f00101..79ce2ec570 100644 --- a/src/data_panel_layout.ts +++ b/src/data_panel_layout.ts @@ -100,6 +100,7 @@ export interface ViewerUIState inputEventBindings: InputEventBindings; crossSectionBackgroundColor: TrackableRGB; perspectiveViewBackgroundColor: TrackableRGB; + hideCrossSectionBackground3D: TrackableBoolean; } export interface DataDisplayLayout extends RefCounted { @@ -181,6 +182,7 @@ export function getCommonViewerState(viewer: ViewerUIState) { selectedLayer: viewer.selectedLayer, visibility: viewer.visibility, scaleBarOptions: viewer.scaleBarOptions, + hideCrossSectionBackground3D: viewer.hideCrossSectionBackground3D, }; } diff --git a/src/layer_group_viewer.ts b/src/layer_group_viewer.ts index afae67a2c5..007661a16f 100644 --- a/src/layer_group_viewer.ts +++ b/src/layer_group_viewer.ts @@ -102,6 +102,7 @@ export interface LayerGroupViewerState { visibleLayerRoles: WatchableSet; crossSectionBackgroundColor: TrackableRGB; perspectiveViewBackgroundColor: TrackableRGB; + hideCrossSectionBackground3D: TrackableBoolean; } export interface LayerGroupViewerOptions { @@ -357,6 +358,9 @@ export class LayerGroupViewer extends RefCounted { get enableAdaptiveDownsampling() { return this.viewerState.enableAdaptiveDownsampling; } + get hideCrossSectionBackground3D() { + return this.viewerState.hideCrossSectionBackground3D; + } get showScaleBar() { return this.viewerState.showScaleBar; } diff --git a/src/layer_groups_layout.ts b/src/layer_groups_layout.ts index bfcd190e89..4944ef1c3a 100644 --- a/src/layer_groups_layout.ts +++ b/src/layer_groups_layout.ts @@ -420,6 +420,7 @@ function getCommonViewerState(viewer: Viewer) { velocity: viewer.velocity.addRef(), crossSectionBackgroundColor: viewer.crossSectionBackgroundColor, perspectiveViewBackgroundColor: viewer.perspectiveViewBackgroundColor, + hideCrossSectionBackground3D: viewer.hideCrossSectionBackground3D, }; } diff --git a/src/perspective_view/panel.ts b/src/perspective_view/panel.ts index 20b1d82e7a..03c8797405 100644 --- a/src/perspective_view/panel.ts +++ b/src/perspective_view/panel.ts @@ -95,6 +95,7 @@ export interface PerspectiveViewerState extends RenderedDataViewerState { showSliceViewsCheckbox?: boolean; crossSectionBackgroundColor: TrackableRGB; perspectiveViewBackgroundColor: TrackableRGB; + hideCrossSectionBackground3D: TrackableBoolean; rpc: RPC; } @@ -270,6 +271,7 @@ class PerspectiveViewState extends PerspectiveViewStateBase { export class PerspectivePanel extends RenderedDataPanel { declare viewer: PerspectiveViewerState; + sliceViewRenderHelper: SliceViewRenderHelper; projectionParameters: Owned; @@ -321,10 +323,6 @@ export class PerspectivePanel extends RenderedDataPanel { ); private axesLineHelper = this.registerDisposer(AxesLineHelper.get(this.gl)); - sliceViewRenderHelper = this.registerDisposer( - SliceViewRenderHelper.get(this.gl, perspectivePanelEmit), - ); - protected offscreenFramebuffer = this.registerDisposer( new FramebufferConfiguration(this.gl, { colorBuffers: [ @@ -406,6 +404,15 @@ export class PerspectivePanel extends RenderedDataPanel { viewer: PerspectiveViewerState, ) { super(context, element, viewer); + this.sliceViewRenderHelper = this.registerDisposer( + SliceViewRenderHelper.get( + this.gl, + perspectivePanelEmit, + this.viewer, + true /*perspectivePanel*/, + ), + ); + this.projectionParameters = this.registerDisposer( new DerivedProjectionParameters({ navigationState: this.navigationState, @@ -580,6 +587,11 @@ export class PerspectivePanel extends RenderedDataPanel { this.registerDisposer( viewer.wireFrame.changed.add(() => this.scheduleRedraw()), ); + this.registerDisposer( + viewer.hideCrossSectionBackground3D.changed.add(() => + this.scheduleRedraw(), + ), + ); this.sliceViews.changed.add(() => this.scheduleRedraw()); } diff --git a/src/sliceview/frontend.ts b/src/sliceview/frontend.ts index 4cd15754fd..b87475efb6 100644 --- a/src/sliceview/frontend.ts +++ b/src/sliceview/frontend.ts @@ -27,6 +27,7 @@ import type { DisplayDimensionRenderInfo, NavigationState, } from "#src/navigation_state.js"; +import type { PerspectiveViewerState } from "#src/perspective_view/panel.js"; import { updateProjectionParametersFromInverseViewAndProjection } from "#src/projection_parameters.js"; import type { ChunkDisplayTransformParameters, @@ -60,6 +61,7 @@ import { SliceViewProjectionParameters, } from "#src/sliceview/base.js"; import { ChunkLayout } from "#src/sliceview/chunk_layout.js"; +import type { SliceViewerState } from "#src/sliceview/panel.js"; import { SliceViewRenderLayer } from "#src/sliceview/renderlayer.js"; import type { WatchableValueInterface } from "#src/trackable_value.js"; import type { Borrowed, Disposer, Owned } from "#src/util/disposable.js"; @@ -72,6 +74,8 @@ import type { ProgressOptions } from "#src/util/progress_listener.js"; import { NullarySignal } from "#src/util/signal.js"; import { withSharedVisibility } from "#src/visibility_priority/frontend.js"; import type { GL } from "#src/webgl/context.js"; +import type { ParameterizedContextDependentShaderGetter } from "#src/webgl/dynamic_shader.js"; +import { parameterizedContextDependentShaderGetter } from "#src/webgl/dynamic_shader.js"; import type { HistogramSpecifications } from "#src/webgl/empirical_cdf.js"; import { TextureHistogramGenerator } from "#src/webgl/empirical_cdf.js"; import type { TextureBuffer } from "#src/webgl/offscreen.js"; @@ -80,8 +84,7 @@ import { FramebufferConfiguration, makeTextureBuffers, } from "#src/webgl/offscreen.js"; -import type { ShaderModule, ShaderProgram } from "#src/webgl/shader.js"; -import { ShaderBuilder } from "#src/webgl/shader.js"; +import type { ShaderModule, ShaderBuilder } from "#src/webgl/shader.js"; import { getSquareCornersBuffer } from "#src/webgl/square_corners_buffer.js"; import type { RPC } from "#src/worker_rpc.js"; import { registerSharedObjectOwner } from "#src/worker_rpc.js"; @@ -721,41 +724,83 @@ export class SliceViewChunk extends Chunk { */ export class SliceViewRenderHelper extends RefCounted { private copyVertexPositionsBuffer; - private shader: ShaderProgram; private textureCoordinateAdjustment = new Float32Array(4); + private shaderGetter: ParameterizedContextDependentShaderGetter< + { emitter: ShaderModule; isProjection: boolean }, + boolean + >; - constructor( - public gl: GL, + defineShader( + builder: ShaderBuilder, + hideTransparent: boolean, + isProjection: boolean, emitter: ShaderModule, ) { - super(); - this.copyVertexPositionsBuffer = getSquareCornersBuffer(this.gl); - - const builder = new ShaderBuilder(gl); builder.addVarying("vec2", "vTexCoord"); builder.addUniform("sampler2D", "uSampler"); builder.addInitializer((shader) => { - gl.uniform1i(shader.uniform("uSampler"), 0); + this.gl.uniform1i(shader.uniform("uSampler"), 0); }); builder.addUniform("vec4", "uColorFactor"); builder.addUniform("vec4", "uBackgroundColor"); builder.addUniform("mat4", "uProjectionMatrix"); builder.addUniform("vec4", "uTextureCoordinateAdjustment"); builder.require(emitter); - builder.setFragmentMain(` + const glsl_fragmentMainStart = ` vec4 sampledColor = texture(uSampler, vTexCoord); -if (sampledColor.a == 0.0) { +if (sampledColor.a == 0.0) {`; + let glsl_fragmentMainEnd: string; + if (hideTransparent && isProjection) { + glsl_fragmentMainEnd = ` + discard; +} +else { + emit(sampledColor * uColorFactor, 0u); +} +`; + } else { + glsl_fragmentMainEnd = ` sampledColor = uBackgroundColor; } emit(sampledColor * uColorFactor, 0u); -`); +`; + } + builder.setFragmentMain(`${glsl_fragmentMainStart}${glsl_fragmentMainEnd}`); builder.addAttribute("vec4", "aVertexPosition"); builder.setVertexMain(` vTexCoord = uTextureCoordinateAdjustment.xy + 0.5 * (aVertexPosition.xy + 1.0) * uTextureCoordinateAdjustment.zw; gl_Position = uProjectionMatrix * aVertexPosition; `); - this.shader = this.registerDisposer(builder.build()); + } + + constructor( + public gl: GL, + private emitter: ShaderModule, + private viewer: SliceViewerState | PerspectiveViewerState, + private isProjection: boolean, + ) { + super(); + + this.copyVertexPositionsBuffer = getSquareCornersBuffer(this.gl); + this.shaderGetter = parameterizedContextDependentShaderGetter( + this, + this.gl, + { + memoizeKey: "sliceview/SliceViewRenderHelper", + parameters: this.viewer.hideCrossSectionBackground3D, + getContextKey: ({ emitter, isProjection }) => + `${getObjectId(emitter)}${isProjection}`, + defineShader: (builder, context, hideTransparent) => { + this.defineShader( + builder, + hideTransparent, + context.isProjection, + context.emitter, + ); + }, + }, + ); } draw( @@ -768,11 +813,19 @@ gl_Position = uProjectionMatrix * aVertexPosition; xEnd: number, yEnd: number, ) { - const { gl, shader, textureCoordinateAdjustment } = this; + const { gl, textureCoordinateAdjustment } = this; textureCoordinateAdjustment[0] = xStart; textureCoordinateAdjustment[1] = yStart; textureCoordinateAdjustment[2] = xEnd - xStart; textureCoordinateAdjustment[3] = yEnd - yStart; + const shaderResult = this.shaderGetter({ + emitter: this.emitter, + isProjection: this.isProjection, + }); + const shader = shaderResult.shader; + if (shader === null) { + throw new Error("Shader compilation failed in SliceViewRenderHelper."); + } shader.bind(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); @@ -801,10 +854,15 @@ gl_Position = uProjectionMatrix * aVertexPosition; gl.bindTexture(gl.TEXTURE_2D, null); } - static get(gl: GL, emitter: ShaderModule) { + static get( + gl: GL, + emitter: ShaderModule, + viewer: SliceViewerState | PerspectiveViewerState, + isProjection: boolean, + ) { return gl.memoize.get( `sliceview/SliceViewRenderHelper:${getObjectId(emitter)}`, - () => new SliceViewRenderHelper(gl, emitter), + () => new SliceViewRenderHelper(gl, emitter, viewer, isProjection), ); } } diff --git a/src/sliceview/panel.ts b/src/sliceview/panel.ts index bea3bf506c..adb9c8cc41 100644 --- a/src/sliceview/panel.ts +++ b/src/sliceview/panel.ts @@ -66,6 +66,7 @@ export interface SliceViewerState extends RenderedDataViewerState { wireFrame: TrackableBoolean; scaleBarOptions: TrackableScaleBarOptions; crossSectionBackgroundColor: TrackableRGB; + hideCrossSectionBackground3D: TrackableBoolean; } export enum OffscreenTextures { @@ -101,11 +102,10 @@ const tempVec4 = vec4.create(); export class SliceViewPanel extends RenderedDataPanel { declare viewer: SliceViewerState; + private sliceViewRenderHelper; private axesLineHelper = this.registerDisposer(AxesLineHelper.get(this.gl)); - private sliceViewRenderHelper = this.registerDisposer( - SliceViewRenderHelper.get(this.gl, sliceViewPanelEmitColor), - ); + private colorFactor = vec4.fromValues(1, 1, 1, 1); private pickIDs = new PickIDManager(); @@ -167,6 +167,16 @@ export class SliceViewPanel extends RenderedDataPanel { viewer: SliceViewerState, ) { super(context, element, viewer); + + this.sliceViewRenderHelper = this.registerDisposer( + SliceViewRenderHelper.get( + this.gl, + sliceViewPanelEmitColor, + this.viewer, + false /*sliceViewPanel*/, + ), + ); + viewer.wireFrame.changed.add(() => this.scheduleRedraw()); registerActionListener( element, diff --git a/src/ui/viewer_settings.ts b/src/ui/viewer_settings.ts index a0c936f94c..913cb39749 100644 --- a/src/ui/viewer_settings.ts +++ b/src/ui/viewer_settings.ts @@ -118,6 +118,10 @@ export class ViewerSettingsPanel extends SidePanel { addCheckbox("Show axis lines", viewer.showAxisLines); addCheckbox("Show scale bar", viewer.showScaleBar); addCheckbox("Show cross sections in 3-d", viewer.showPerspectiveSliceViews); + addCheckbox( + "Hide sections background 3-d", + viewer.hideCrossSectionBackground3D, + ); addCheckbox("Show default annotations", viewer.showDefaultAnnotations); addCheckbox( "Show chunk statistics", diff --git a/src/viewer.ts b/src/viewer.ts index a5a7519cd9..3b41053d77 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -253,6 +253,10 @@ class TrackableViewerState extends CompoundTrackable { this.add("showDefaultAnnotations", viewer.showDefaultAnnotations); this.add("showSlices", viewer.showPerspectiveSliceViews); + this.add( + "hideCrossSectionBackground3D", + viewer.hideCrossSectionBackground3D, + ); this.add( "gpuMemoryLimit", viewer.dataContext.chunkQueueManager.capacities.gpuMemory.sizeLimit, @@ -416,6 +420,7 @@ export class Viewer extends RefCounted implements ViewerState { enableAdaptiveDownsampling = new TrackableBoolean(true, true); showScaleBar = new TrackableBoolean(true, true); showPerspectiveSliceViews = new TrackableBoolean(true, true); + hideCrossSectionBackground3D = new TrackableBoolean(false, false); visibleLayerRoles = allRenderLayerRoles(); showDefaultAnnotations = new TrackableBoolean(true, true); crossSectionBackgroundColor = new TrackableRGB(