Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/user-guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ Topics
to segmentation layers.
- :doc:`annotation_shaders` — writing custom GLSL shader code to drive
the visual appearance of annotations from their properties.
- :doc:`segmentation_shaders` — writing custom GLSL shader code to color
segmentation layers from segment ids and segment properties.

.. toctree::
:hidden:

navigation
annotations
annotation_shaders
segmentation_shaders
171 changes: 171 additions & 0 deletions docs/user-guide/segmentation_shaders.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
Segmentation Shaders
--------------------

Segmentation layers can use custom GLSL code to change how segments are colored.
The technical reference for the shader API is in
`the segmentation rendering guide <https://github.com/google/neuroglancer/blob/master/src/layer/segmentation/rendering.md>`_.
This page is a gentler introduction to the same ideas.

The Segment Color Function
~~~~~~~~~~~~~~~~~~~~~~~~~~

A segmentation shader defines a ``segmentColor`` function. The default shader
keeps Neuroglancer's existing segment color unchanged:

.. code-block:: text

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
return color;
}

The ``color`` argument is the color Neuroglancer would normally use for the
segment. It may come from the segment color hash, the layer's default segment
color, or an explicit color assigned to that segment. The ``isStated`` argument
is ``true`` when ``color`` came from an explicit segment color. The
``hasProperties`` argument is ``true`` when Neuroglancer found segment property
data for the current segment.

To make every visible segment red, return a red ``vec3``:

.. code-block:: text

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
return vec3(1.0, 0.0, 0.0);
}

If you want to preserve explicit per-segment colors but recolor everything
else, use ``isStated``:

.. code-block:: text

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
if (isStated) {
return color;
}
return vec3(0.0, 0.6, 1.0);
}

Opacity
~~~~~~~

Use a ``vec4`` return type to set opacity. The alpha channel is the fourth
component. Returning a negative alpha leaves the layer opacity unchanged.

.. code-block:: text

vec4 segmentColor(vec4 color, bool hasProperties, bool isStated) {
if (isStated) {
return color;
}
return vec4(color.rgb, 0.35);
}

Segment Properties
~~~~~~~~~~~~~~~~~~

Segment properties can drive color choices. If a segment property map is
available, a shader can read tags and properties directly by name:

.. code-block:: text

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
if (!hasProperties) {
return vec3(0.5, 0.5, 0.5);
}
if (tag("axon")) {
return vec3(1.0, 0.4, 0.0);
}
if (prop("size") > 100u) {
return vec3(1.0, 1.0, 0.0);
}
return color;
}

``tag("axon")`` returns ``true`` when the current segment has that tag.
``prop("size")`` reads a numerical property named ``size``. For string
properties, compare the value with a string literal:

.. code-block:: text

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
if (!hasProperties) {
return vec3(0.5, 0.5, 0.5);
}
if (prop("class") == "interneuron") {
return vec3(0.0, 1.0, 0.6);
}
return color;
}

Property Controls
~~~~~~~~~~~~~~~~~

A ``property`` UI control lets the user choose which segment property a shader
uses. The control can be filtered to tags, numerical properties, or string
properties.

.. code-block:: glsl

#uicontrol property selectedTag(type="tag")
#uicontrol property selectedSize(type="number")

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
if (!hasProperties) {
return vec3(0.5, 0.5, 0.5);
}
if (selectedTag) {
return vec3(1.0, 0.0, 0.0);
}
if (selectedSize > 100u) {
return vec3(1.0, 1.0, 0.0);
}
return color;
}

Use ``type="string"`` for a string property picker. ``type="number"`` and
``type="numerical"`` both select numerical properties.

Data Mapping
~~~~~~~~~~~~

For continuous numerical properties, an ``invlerp`` control maps a property
range to ``0-1``. The user can adjust the selected property and range from the
shader UI.

.. code-block:: glsl

#uicontrol float intensity invlerp(property="size", range=[0, 1000])

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
if (!hasProperties) {
return vec3(0.5, 0.5, 0.5);
}
return intensity() * vec3(1.0, 0.2, 0.0);
}

If ``property`` is omitted, Neuroglancer selects the first available numerical
segment property as the default. ``range`` controls the data values mapped to
``0`` and ``1``. ``window`` can be added to control the range shown by the UI
widget.

Colormaps
~~~~~~~~~

The remapped value from an ``invlerp`` control can be passed to a colormap:

.. code-block:: glsl

#uicontrol float intensity invlerp(property="size", range=[0, 1000])

vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {
if (!hasProperties) {
return vec3(0.5, 0.5, 0.5);
}
if (isStated) {
return color;
}
return colormapJet(intensity());
}

This pattern is useful when a numerical segment property should control color
continuously while still respecting explicitly assigned segment colors.
107 changes: 107 additions & 0 deletions src/annotation/type_handler.browser_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* @license
* Copyright 2026 Google Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import "#src/annotation/bounding_box.js";
import "#src/annotation/ellipsoid.js";
import "#src/annotation/line.js";
import "#src/annotation/point.js";
import "#src/annotation/polyline.js";

import { describe, expect, it } from "vitest";
import type { AnnotationPropertySpec } from "#src/annotation/index.js";
import { AnnotationType } from "#src/annotation/index.js";
import {
type AnnotationShaderGetter,
getAnnotationTypeRenderHandler,
} from "#src/annotation/type_handler.js";
import { WatchableValue } from "#src/trackable_value.js";
import { initializeWebGL } from "#src/webgl/context.js";
import {
makeTrackableFragmentMain,
makeWatchableShaderError,
} from "#src/webgl/dynamic_shader.js";
import {
getFallbackBuilderState,
parseShaderUiControls,
ShaderControlState,
} from "#src/webgl/shader_ui_controls.js";

describe("annotation property shaders", () => {
it("compiles a shader that colors by boolean property", () => {
const canvas = document.createElement("canvas");
const gl = initializeWebGL(canvas);
const fragmentMain = makeTrackableFragmentMain(`
void main() {
if (prop_highlight() != 0u) {
setColor(vec3(1.0, 0.0, 0.0));
} else {
setColor(vec3(0.0, 0.0, 1.0));
}
}
`);
const shaderControlState = new ShaderControlState(fragmentMain);
const fallbackShaderParameters = new WatchableValue(
getFallbackBuilderState(parseShaderUiControls(fragmentMain.value)),
);
const shaderError = makeWatchableShaderError();
const properties: AnnotationPropertySpec[] = [
{
identifier: "highlight",
description: undefined,
type: "bool",
default: 0,
},
];
const renderHandler = getAnnotationTypeRenderHandler(AnnotationType.POINT);
const renderHelper = new renderHandler.perspectiveViewRenderHelper(
gl,
AnnotationType.POINT,
/*rank=*/ 3,
properties,
shaderControlState,
fallbackShaderParameters,
shaderError,
);
renderHelper.targetIsSliceView = false;
renderHelper.pickIdsPerInstance = renderHandler.pickIdsPerInstance;

const shaderGetter = (
renderHelper as typeof renderHelper & {
shaderGetter3d: AnnotationShaderGetter;
}
).shaderGetter3d;
const shaderResult = shaderGetter((builder) => {
builder.addOutputBuffer("vec4", "out_color", 0);
builder.addFragmentCode(`
void emit(vec4 color, highp uint pickId) {
out_color = color;
}
`);
});

expect(shaderError.value).toBeNull();
expect(shaderResult.shader).not.toBeNull();
expect(shaderResult.shader!.vertexSource).toContain(
"highp uint prop_highlight()",
);
expect(shaderResult.shader!.vertexSource).toContain(
"if (prop_highlight() != 0u)",
);

renderHelper.dispose();
shaderControlState.dispose();
});
});
22 changes: 7 additions & 15 deletions src/annotation/type_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
parameterizedEmitterDependentShaderGetter,
shaderCodeWithLineDirective,
} from "#src/webgl/dynamic_shader.js";
import { copyHistogramToCPU } from "#src/webgl/empirical_cdf.js";
import {
defineInvlerpShaderFunction,
enableLerpShaderFunction,
Expand Down Expand Up @@ -444,7 +445,7 @@ float getMaxSubspaceClipCoefficient(float modelPointA[${this.rank}], float mode
}

`);
addControlsToBuilder(parameters, builder);
addControlsToBuilder(parameters, builder, /*fragment=*/ false);
builder.addVertexCode(`
const bool PROJECTION_VIEW = ${!this.targetIsSliceView};
bool ng_discardValue;
Expand Down Expand Up @@ -776,21 +777,12 @@ gl_PointSize = 1.0;
}
gl.drawArrays(WebGL2RenderingContext.POINTS, 0, context.count);
if (DEBUG_HISTOGRAMS) {
const tempBuffer = new Float32Array(256 * 4);
gl.readPixels(
0,
0,
256,
1,
WebGL2RenderingContext.RGBA,
WebGL2RenderingContext.FLOAT,
tempBuffer,
const histogram = copyHistogramToCPU(gl);
console.log(
"histogram property:",
propertyIdentifier,
histogram.join(" "),
);
const tempBuffer2 = new Float32Array(256);
for (let j = 0; j < 256; ++j) {
tempBuffer2[j] = tempBuffer[j * 4];
}
console.log("histogram", tempBuffer2.join(" "));
}
binder.disable();
break;
Expand Down
Loading
Loading