Skip to content

Commit a9939d6

Browse files
authored
Volume rendering compositing changes and wireframe rendering to vis chunks in volume rendering mode (#503)
* feat: VR front-to-back compositing and chunk vis * chore: add comment about opacity correction source * refactor: clearer naming for compositing * chore: fix formatting * feat: add gain parameter to VR and python binds The gain float can be used to scale the VR intensity. Also bind that gain to Python controls and bind VR bool to Python. * refactor: pull OIT emit function out as standalone * feat: perform OIT along rays during VR * test: ensure that color and revealage are independent of sampling rate Refactored volume rendering shader to accomodate * feat: add depth culling to VR rays during marching * chore: format file * fix: break composite loop if VR ray goes behind opaque * fix: remove console.log during testing * feat: volume rendering chunk vis in wireframe mode * refactor: rename gain to volumeRenderingGain Also update relevant Python bindings * feat: change gain scale from linear to exponential * refactor: rename to depthBufferTexture (remove ID) * format: run python formatting * fix: default volume rendering slider gain of 0 (e^0 passed to shader)
1 parent 9524a72 commit a9939d6

File tree

7 files changed

+221
-20
lines changed

7 files changed

+221
-20
lines changed

python/neuroglancer/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
PlaceEllipsoidTool, # noqa: F401
4949
BlendTool, # noqa: F401
5050
OpacityTool, # noqa: F401
51+
VolumeRenderingTool, # noqa: F401
52+
VolumeRenderingGainTool, # noqa: F401
5153
VolumeRenderingDepthSamplesTool, # noqa: F401
5254
CrossSectionRenderScaleTool, # noqa: F401
5355
SelectedAlphaTool, # noqa: F401

python/neuroglancer/viewer_state.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@ class OpacityTool(Tool):
164164
TOOL_TYPE = "opacity"
165165

166166

167+
@export_tool
168+
class VolumeRenderingTool(Tool):
169+
__slots__ = ()
170+
TOOL_TYPE = "volumeRendering"
171+
172+
173+
@export_tool
174+
class VolumeRenderingGainTool(Tool):
175+
__slots__ = ()
176+
TOOL_TYPE = "volumeRenderingGain"
177+
178+
167179
@export_tool
168180
class VolumeRenderingDepthSamplesTool(Tool):
169181
__slots__ = ()
@@ -543,6 +555,12 @@ def __init__(self, *args, **kwargs):
543555
)
544556
opacity = wrapped_property("opacity", optional(float, 0.5))
545557
blend = wrapped_property("blend", optional(str))
558+
volume_rendering = volumeRendering = wrapped_property(
559+
"volumeRendering", optional(bool, False)
560+
)
561+
volume_rendering_gain = volumeRenderingGain = wrapped_property(
562+
"volumeRenderingGain", optional(float, 1)
563+
)
546564
volume_rendering_depth_samples = volumeRenderingDepthSamples = wrapped_property(
547565
"volumeRenderingDepthSamples", optional(float, 64)
548566
)

src/image_user_layer.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import {
9898
ShaderControls,
9999
} from "#/widget/shader_controls";
100100
import { Tab } from "#/widget/tab_view";
101+
import { trackableFiniteFloat } from "#/trackable_finite_float";
101102

102103
const OPACITY_JSON_KEY = "opacity";
103104
const BLEND_JSON_KEY = "blend";
@@ -106,6 +107,7 @@ const SHADER_CONTROLS_JSON_KEY = "shaderControls";
106107
const CROSS_SECTION_RENDER_SCALE_JSON_KEY = "crossSectionRenderScale";
107108
const CHANNEL_DIMENSIONS_JSON_KEY = "channelDimensions";
108109
const VOLUME_RENDERING_JSON_KEY = "volumeRendering";
110+
const VOLUME_RENDERING_GAIN_JSON_KEY = "volumeRenderingGain";
109111
const VOLUME_RENDERING_DEPTH_SAMPLES_JSON_KEY = "volumeRenderingDepthSamples";
110112

111113
export interface ImageLayerSelectionState extends UserLayerSelectionState {
@@ -125,6 +127,7 @@ export class ImageUserLayer extends Base {
125127
dataType = new WatchableValue<DataType | undefined>(undefined);
126128
sliceViewRenderScaleHistogram = new RenderScaleHistogram();
127129
sliceViewRenderScaleTarget = trackableRenderScaleTarget(1);
130+
volumeRenderingGain = trackableFiniteFloat(0);
128131
volumeRenderingChunkResolutionHistogram = new RenderScaleHistogram(
129132
volumeRenderingDepthSamplesOriginLogScale,
130133
);
@@ -199,6 +202,7 @@ export class ImageUserLayer extends Base {
199202
isLocalDimension;
200203
this.blendMode.changed.add(this.specificationChanged.dispatch);
201204
this.opacity.changed.add(this.specificationChanged.dispatch);
205+
this.volumeRenderingGain.changed.add(this.specificationChanged.dispatch);
202206
this.fragmentMain.changed.add(this.specificationChanged.dispatch);
203207
this.shaderControlState.changed.add(this.specificationChanged.dispatch);
204208
this.sliceViewRenderScaleTarget.changed.add(
@@ -252,6 +256,7 @@ export class ImageUserLayer extends Base {
252256
);
253257
const volumeRenderLayer = context.registerDisposer(
254258
new VolumeRenderingRenderLayer({
259+
gain: this.volumeRenderingGain,
255260
multiscaleSource: volume,
256261
shaderControlState: this.shaderControlState,
257262
shaderError: this.shaderError,
@@ -299,6 +304,9 @@ export class ImageUserLayer extends Base {
299304
specification[CHANNEL_DIMENSIONS_JSON_KEY],
300305
);
301306
this.volumeRendering.restoreState(specification[VOLUME_RENDERING_JSON_KEY]);
307+
this.volumeRenderingGain.restoreState(
308+
specification[VOLUME_RENDERING_GAIN_JSON_KEY],
309+
);
302310
this.volumeRenderingDepthSamplesTarget.restoreState(
303311
specification[VOLUME_RENDERING_DEPTH_SAMPLES_JSON_KEY],
304312
);
@@ -313,6 +321,7 @@ export class ImageUserLayer extends Base {
313321
this.sliceViewRenderScaleTarget.toJSON();
314322
x[CHANNEL_DIMENSIONS_JSON_KEY] = this.channelCoordinateSpace.toJSON();
315323
x[VOLUME_RENDERING_JSON_KEY] = this.volumeRendering.toJSON();
324+
x[VOLUME_RENDERING_GAIN_JSON_KEY] = this.volumeRenderingGain.toJSON();
316325
x[VOLUME_RENDERING_DEPTH_SAMPLES_JSON_KEY] =
317326
this.volumeRenderingDepthSamplesTarget.toJSON();
318327
return x;
@@ -451,6 +460,15 @@ const LAYER_CONTROLS: LayerControlDefinition<ImageUserLayer>[] = [
451460
toolJson: VOLUME_RENDERING_JSON_KEY,
452461
...checkboxLayerControl((layer) => layer.volumeRendering),
453462
},
463+
{
464+
label: "Gain (3D)",
465+
toolJson: VOLUME_RENDERING_GAIN_JSON_KEY,
466+
isValid: (layer) => layer.volumeRendering,
467+
...rangeLayerControl((layer) => ({
468+
value: layer.volumeRenderingGain,
469+
options: { min: -10.0, max: 10.0, step: 0.1 },
470+
})),
471+
},
454472
{
455473
label: "Resolution (3D)",
456474
toolJson: VOLUME_RENDERING_DEPTH_SAMPLES_JSON_KEY,

src/perspective_view/panel.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,22 +109,26 @@ void emit(vec4 color, highp uint pickId) {
109109
* http://casual-effects.blogspot.com/2015/03/implemented-weighted-blended-order.html
110110
*/
111111
export const glsl_computeOITWeight = `
112-
float computeOITWeight(float alpha) {
112+
float computeOITWeight(float alpha, float depth) {
113113
float a = min(1.0, alpha) * 8.0 + 0.01;
114-
float b = -gl_FragCoord.z * 0.95 + 1.0;
114+
float b = -depth * 0.95 + 1.0;
115115
return a * a * a * b * b * b;
116116
}
117117
`;
118118

119119
// Color must be premultiplied by alpha.
120+
// Can use emitAccumAndRevealage() to emit a pre-weighted OIT result.
120121
export const glsl_perspectivePanelEmitOIT = [
121122
glsl_computeOITWeight,
122123
`
124+
void emitAccumAndRevealage(vec4 accum, float revealage, highp uint pickId) {
125+
v4f_fragData0 = vec4(accum.rgb, revealage);
126+
v4f_fragData1 = vec4(accum.a, 0.0, 0.0, 0.0);
127+
}
123128
void emit(vec4 color, highp uint pickId) {
124-
float weight = computeOITWeight(color.a);
129+
float weight = computeOITWeight(color.a, gl_FragCoord.z);
125130
vec4 accum = color * weight;
126-
v4f_fragData0 = vec4(accum.rgb, color.a);
127-
v4f_fragData1 = vec4(accum.a, 0.0, 0.0, 0.0);
131+
emitAccumAndRevealage(accum, color.a, pickId);
128132
}
129133
`,
130134
];
@@ -846,6 +850,8 @@ export class PerspectivePanel extends RenderedDataPanel {
846850
renderContext.emitPickID = false;
847851
for (const [renderLayer, attachment] of visibleLayers) {
848852
if (renderLayer.isTransparent) {
853+
renderContext.depthBufferTexture =
854+
this.offscreenFramebuffer.colorBuffers[OffscreenTextures.Z].texture;
849855
renderLayer.draw(renderContext, attachment);
850856
}
851857
}

src/perspective_view/render_layer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export interface PerspectiveViewRenderContext
5050
* Specifies whether there was a previous pick ID pass.
5151
*/
5252
alreadyEmittedPickID: boolean;
53+
54+
/**
55+
* Specifies the ID of the depth frame buffer texture to query during rendering.
56+
*/
57+
depthBufferTexture?: WebGLTexture | null;
5358
}
5459

5560
export class PerspectiveViewRenderLayer<
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { fragmentShaderTest } from "#/webgl/shader_testing";
2+
import { glsl_computeOITWeight } from "#/perspective_view/panel";
3+
import { glsl_emitRGBAVolumeRendering } from "#/volume_rendering/volume_render_layer";
4+
import { vec3 } from "gl-matrix";
5+
6+
describe("volume rendering compositing", () => {
7+
const steps = [16, 22, 32, 37, 64, 100, 128, 256, 512, 551, 1024, 2048];
8+
const revealages = new Float32Array(steps.length);
9+
it("combines uniform colors the same regardless of sampling rate", () => {
10+
fragmentShaderTest(
11+
{
12+
inputSteps: "float",
13+
},
14+
{
15+
outputValue1: "float",
16+
outputValue2: "float",
17+
outputValue3: "float",
18+
outputValue4: "float",
19+
revealage: "float",
20+
},
21+
(tester) => {
22+
const { builder } = tester;
23+
builder.addFragmentCode(glsl_computeOITWeight);
24+
builder.addFragmentCode(`
25+
vec4 color = vec4(0.1, 0.3, 0.5, 0.1);
26+
float idealSamplingRate = 512.0;
27+
float uGain = 0.01;
28+
float uBrightnessFactor;
29+
vec4 outputColor;
30+
float depthAtRayPosition;
31+
`);
32+
builder.addFragmentCode(glsl_emitRGBAVolumeRendering);
33+
builder.setFragmentMain(`
34+
outputColor = vec4(0.0);
35+
revealage = 1.0;
36+
uBrightnessFactor = idealSamplingRate / inputSteps;
37+
for (int i = 0; i < int(inputSteps); ++i) {
38+
depthAtRayPosition = mix(0.0, 1.0, float(i) / (inputSteps - 1.0));
39+
emitRGBA(color);
40+
}
41+
outputValue1 = outputColor.r;
42+
outputValue2 = outputColor.g;
43+
outputValue3 = outputColor.b;
44+
outputValue4 = outputColor.a;
45+
`);
46+
for (let i = 0; i < steps.length; ++i) {
47+
const inputSteps = steps[i];
48+
tester.execute({ inputSteps });
49+
const values = tester.values;
50+
const {
51+
revealage,
52+
outputValue1,
53+
outputValue2,
54+
outputValue3,
55+
outputValue4,
56+
} = values;
57+
const color = vec3.fromValues(
58+
outputValue1 / outputValue4,
59+
outputValue2 / outputValue4,
60+
outputValue3 / outputValue4,
61+
);
62+
expect(color[0]).toBeCloseTo(0.1, 5);
63+
expect(color[1]).toBeCloseTo(0.3, 5);
64+
expect(color[2]).toBeCloseTo(0.5, 5);
65+
revealages[i] = revealage;
66+
}
67+
for (let i = 1; i < revealages.length; ++i) {
68+
expect(revealages[i]).toBeCloseTo(revealages[i - 1], 2);
69+
}
70+
},
71+
);
72+
});
73+
});

0 commit comments

Comments
 (0)