Annotation Shaders¶
As with other elements of neuroglancer, one can write custom GLSL code to control the way annotations are rendered on the screen. Technical documentation about how to set control different aspects of the annotations Can be found here.
This guide is a more gentle introduction to GLSL that will guide you through developing your first annotation shader code, so that properties of annotations can adjust their visual appearance.
Many of the lessons applied here also are relevant to shaders on image layers or segmentation layers, but this will stay focused on annotation shaders.
Color¶
The first and most obvious thing to change about an annotation is its
color. Colors can either be defined by red, green, blue (RGB) values,
or red, green, blue, alpha (RGBA) values, if you want annotations to be
transparent. In GLSL, RGB colors are defined by a vec3 variable type
and RGBA colors are vec4. Calling setColor with either a vec3
or vec4 will set the color.
You can see this in the default shader that comes with any new annotation layer
void main() {
setColor(defaultColor());
}
Here, defaultColor() is a function which returns the vec3 RGB color
that is the Annotation color UI control just below the shader code.
Clicking on the UI control will bring up an RGB picker that will allow you
to change the color returned by defaultColor().
You can add more color pickers to the set of UI controls with a #uicontrol
directive, optionally give it a default value, and pass it to set color.
#uicontrol vec3 mycolor color(default="red")
void main() {
setColor(mycolor);
}
There are more #uicontrol types, such as slider. Let’s add a slider to
control the red channel of a color. To create a new vec3 value,
we declare it with the type, name, and an initializer.
#uicontrol float red slider(min=0.0, max=1.0, step=0.05, default=1.0)
void main() {
vec3 mycolor = vec3(red, 0.0, 0.0);
setColor(mycolor);
}
Data mapping¶
More powerfully, we might want to take some data from an annotation
property and use it to drive the red channel. However to do so,
we often want to remap the values of that property to a 0-1 range.
Say our annotation has a property called temperature and it ranges
from 0 to 1000 across the annotations, but most annotations are
around 10-30. The invlerp control (short for inverse linear
interpolation) will help us do that.
#uicontrol float red invlerp(range=[10,30], window=[0,1000])
void main() {
vec3 mycolor = vec3(red(), 0.0, 0.0);
setColor(mycolor);
}
Now in the UI you will see a widget appear that has a dropdown menu
to select which property you want to remap. The range parameter
sets the min and max data values of the linear remap: values above the
max are clamped to 1, and values below the min are clamped to 0.
window sets what range of values the widget shows you when adjusting
range. You can omit window, which defaults to the same interval
as range, and you can omit range, in which case neuroglancer
will pick a reasonable default from the current distribution of the
selected property. Both can be adjusted from the widget at any time.
These basic elements can be combined to create many kinds of coloring behaviors. For example, let’s say we want a single-color colormap that ramps from black to a user-selectable color as an annotation property goes from low to high.
#uicontrol float intensity invlerp(range=[10,30], window=[0,1000])
#uicontrol vec3 mycolor color(default="red")
void main() {
setColor(intensity()*mycolor);
}
I can simply scale the mycolor vec3 by my remapped intensity
invlerp control that will be between 0 and 1. Users can now
change the colormap and set what ‘bright’ looks like.
What about discrete variables, like categories, where continuous changes don’t make sense? We can set up a lookup table that maps each discrete code to a specific color. The natural way to express this in GLSL is a constant array indexed by the code.
Let’s say we had a categorical property called category whose
values are codes 0, 1, or 2. (Categorical / dictionary-encoded
annotation properties surface in the shader as a uint code — the index
into the property’s categories array — so we use uint for the
lookup index.) Declare a const vec3 array with one row per code,
index it by the code, and fall back to defaultColor() for
out-of-range values:
const vec3 categoryColors[3] = vec3[3](
vec3(1.0, 0.0, 0.0), // 0: red
vec3(0.0, 1.0, 0.0), // 1: green
vec3(0.0, 0.0, 1.0) // 2: blue
);
void main() {
uint code = prop_category();
vec3 rgb = (code < 3u) ? categoryColors[code] : defaultColor();
setColor(rgb);
}
A few things to notice:
const vec3 array[N] = vec3[N](...)is the GLSL syntax for a compile-time-constant array. The sizeNappears in both the declaration and the constructor and must match.Array indices must be non-negative integers, so
codeis auintand the bounds check uses3u(auintliteral). Comparinguinttointis not allowed.The fallback to
defaultColor()handles unexpected codes safely — without it, reading past the array end is undefined.This pattern scales naturally to dozens or hundreds of categories (e.g. cell-type taxonomies). Lining up the row comment with the corresponding entry in the property’s on-disk
categoriesarray makes the mapping easy to audit.
Lines and Polylines have colors for their lines, points and endpoints.
setLineColor can be called with 1 color, or with 2, if you want the
color to vary across the line. setEndPointMarkerColor similarly can
be called with 1 color to mark both ends the same, or 2 colors to
make the start and endpoint colors different. You can read more
about the details of these in the
rendering guide
Size¶
The next obvious thing to modulate beside color is the size of annotations.
Annotations have different things to size. Points have setPointMarkerSize
which will dynamically scale annotations. All the same things that we
learned about #uicontrol apply here. So if we wanted our point
annotations to scale between 1 and 20 pixels based on a property,
we might use the invlerp control again.
#uicontrol float intensity invlerp(range=[10,30], window=[0,1000])
void main() {
setColor(defaultColor());
setPointMarkerSize(1.0 + 19.0*intensity());
}
We could control the maximum size with a slider to give the user even more control.
#uicontrol float intensity invlerp(range=[10,30], window=[0,1000])
#uicontrol float maxsize slider(min=1.0, max=50.0, step=1.0)
void main() {
setColor(defaultColor());
setPointMarkerSize(1.0 + (maxsize-1.0)*intensity());
}
The 1.0 + and (maxsize - 1.0) here mean that our points never
disappear, no matter how small the slider goes.
Toggles¶
To switch a visual feature on and off, use a checkbox uicontrol.
The name of the UI control will be available to the shader as a boolean binary variable. We can use this to toggle some aspect of our visualization on and off, such as making the bordrs of point annotations visible or invisible in the example below.
#uicontrol bool showBorders checkbox(default=true)
void main() {
setColor(defaultColor());
setPointMarkerSize(8.0);
if (showBorders) {
setPointMarkerBorderColor(vec4(0.0, 0.0, 0.0, 1.0));
setPointMarkerBorderWidth(1.0);
}
else {
setPointMarkerBorderWidth(0.0);
}
}
Discard¶
As the number of annotation points you want to render gets higher and higher, the performance of rendering will go down. The controls will start to feel sluggish, and your page might crash if the GPU gets overwhelmed.
One way to address this is to use the data itself to render only the
subset of points you are interested in. Inside main(), the
discard keyword drops the current annotation entirely — no
geometry is drawn for it.
void main() {
if (prop_quality() < 0.5) {
discard;
} else {
setColor(defaultColor());
}
}
To let the user adjust the cutoff interactively, drive it from a slider:
#uicontrol float minQuality slider(min=0.0, max=1.0, step=0.01, default=0.5)
void main() {
if (prop_quality() < minQuality) discard;
setColor(defaultColor());
}
For a categorical property, compare against the code directly. To show
only annotations belonging to a single category, pair a uint
slider with an equality check:
#uicontrol uint onlyCategory slider(min=0, max=2, default=0)
void main() {
if (prop_category() != onlyCategory) discard;
setColor(defaultColor());
}
Combine a checkbox with a discard to make a filter optional —
when the checkbox is off, all the annotations of a particular
category are dropped.
#uicontrol bool hideCategoryTwo checkbox(default=false)
void main() {
if (hideCategoryTwo && prop_category() == 2u) discard;
setColor(defaultColor());
}
Multiple discards can be stacked — the annotation is dropped if any condition matches:
#uicontrol float minQuality slider(min=0.0, max=1.0, default=0.5)
void main() {
if (prop_quality() < minQuality) discard;
if (prop_category() == 2u) discard;
setColor(defaultColor());
}