Skip to content

feat(segmentation_user_layer): add user shader for segmentation layers with support for segment properties - #1074

Open
chrisj wants to merge 37 commits into
google:masterfrom
AllenInstitute:cj-segment-color-user-shader
Open

chrisj wants to merge 37 commits into
google:masterfrom
AllenInstitute:cj-segment-color-user-shader

Conversation

@chrisj

@chrisj chrisj commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

continuation of #862

@chrisj
chrisj force-pushed the cj-segment-color-user-shader branch 8 times, most recently from 07f7c7c to 7adf3fc Compare July 15, 2026 16:43
@chrisj
chrisj marked this pull request as ready for review July 15, 2026 16:43
@chrisj
chrisj force-pushed the cj-segment-color-user-shader branch from 7adf3fc to 53cfe9b Compare July 15, 2026 16:45

@seankmartin seankmartin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks nice, thanks for all the changes! In general the API in the shader seems pretty reasonable to me. I've not gotten to go through it all, but some small comments from what I have looked through. There are also some TODO markers and the likes, not sure if those are still relevant or you settled on the approach in those cases

colorGroupState.segmentColorHash.compute(color, objectId);
return color;
const { getShaderSegmentColor } = displayState;
return getShaderSegmentColor(objectId) ?? color;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we detect if the shader is the default one, and in that case use the old non-shader color path to display the colors in the UI for performance. I noticed that the segmentation menu can sometimes feel a bit more sluggish to go through, which seems pretty fair when you've chosen to use the segment shader and to me it seems fine. But if possible to avoid this slight hit if you didn't change your shader I think that would be good

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is what I should do for now but maybe I should give a quick try at adding batching. I don't notice sluggishness but I do notice that the color doesn't load immediately if scrolling very fast on my machine. I can't replicate that on the main branch.

It's easy to add support to calculate many segment colors in a single draw call, the issue is that the segment list is designed so that each row is processed independently. But if I can get it working, it should be far more performant.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot of sense, thanks Chris

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seankmartin can you check if this deployment has noticeably better performance for you?

demo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, that's so much better yes thanks @chrisj

const hasStringProperty = inlineProperties.properties.some(
(p) => p.type === "string",
);
if (!hasStringProperty) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is leftover but it does raise what we should do here. Shader error comes to mind, but then in theory the underlying data could change and lose a property. Maybe that's fine though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that was for demonstration as string properties are not common. But do you mean if a shader references a property that was later removed from the segment properties file? If so I think the current error "property does not exist" is correct.

});
}

function makeSkeletonShaderCodeWidget(layer: SegmentationUserLayer) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Minor but the style of the element layout seems different on this new shader controls in the UI compared to the skeleton ones

Comment thread src/layer/segmentation/index.ts Outdated
}

export const DEFAULT_FRAGMENT_SEGMENT_COLOR = `
vec3 segmentColor(vec3 color, bool hasProperties, bool isStated) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh also minor but leading newline in the resulting shader here that probably is better left out

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also I should rename it from FRAGMENT becomes it is sometimes used in the vertex shader

@chrisj

chrisj commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@seankmartin thanks for the review! I have some changes I'll push shortly that I noticed yesterday

@seankmartin

Copy link
Copy Markdown
Contributor

@seankmartin thanks for the review! I have some changes I'll push shortly that I noticed yesterday

sounds great, thanks!

@chrisj
chrisj force-pushed the cj-segment-color-user-shader branch 2 times, most recently from 00444f6 to 9f4e76c Compare July 20, 2026 15:28
@chrisj
chrisj force-pushed the cj-segment-color-user-shader branch 2 times, most recently from 72bcce9 to b3fd516 Compare August 13, 2026 16:56
@chrisj

chrisj commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@seankmartin I simplified the performance optimizations and added it to this PR. I added a temporary setting that you can toggle with "q" to easily observe the difference. Can you give it a test to see if you are still see the big performance improvement? I can see the a difference when I press "q" on and off while dragging the scroll bar. If it looks good I'll revert those last few commits.

example

@seankmartin

Copy link
Copy Markdown
Contributor

@seankmartin I simplified the performance optimizations and added it to this PR. I added a temporary setting that you can toggle with "q" to easily observe the difference. Can you give it a test to see if you are still see the big performance improvement? I can see the a difference when I press "q" on and off while dragging the scroll bar. If it looks good I'll revert those last few commits.

example

Thanks @chrisj, that's great! And yes still much better for me when batched

@chrisj
chrisj force-pushed the cj-segment-color-user-shader branch 4 times, most recently from 775e56d to c4c7d9d Compare August 19, 2026 14:55
toMerged: Uint32Array,
): InlineSegmentNumericalProperty {
const values = new Float32Array(numMerged);
const values = property.values.slice(0, numMerged);

@chrisj chrisj Aug 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been diving into this change and need some thoughts.

This function is called when merging multiple segment property maps together. It was casting every numerical property values array to Float32Array. If the maps had differing sets of segments, each segment received a value of NaN for the numerical property of each map it was not in.

The easiest solution would be to just use a zero value and keep each values as it's original numerical type. Another option is to do that but add a validity mask. This is a rough idea of what that would look like: AllenInstitute@4e7a970

if (hasProp("myProperty")) {
  uint value = prop("myProperty");
  // use value
}

I'm leaning towards using 0 instead of NaN for non-float and skipping the validity mask as I imagine it won't have much use.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me to do the simpler version, I also can't imagine it's used too often

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be fine to use 0 for non-float properties --- using NaN for float properties would still be good I think.

@chrisj
chrisj force-pushed the cj-segment-color-user-shader branch from 7dac20e to 1784f69 Compare August 25, 2026 12:16
): InlineSegmentNumericalProperty {
const values = new Float32Array(numMerged);
values.fill(Number.NaN);
const values = new (property.values

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbms are you good with this change?

759a4d6

@fcollman

Copy link
Copy Markdown
Contributor

@jbms @seankmartin I think we think this PR is now in a state that it is ready for review again and chris thinks he has addressed all the issues. There is the one spot he would like feedback on that is in the most recent comment.

@fcollman

Copy link
Copy Markdown
Contributor

@jbms @seankmartin recently we have been trying to specify colors across a large number of meshes (for an atlas, ~1300 regions) where we have some semantic coloring available. Right now that requires a custom information in the state in a segmentColors, rather than having those colors available as segmentProperties that would lift them out of the state.

return color;
}

Use ``type="string"`` for a string property picker. ``type="number"`` and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor but not sure the "tag" type shown in the code snippet is clear from this line as "tag" isn't mentioned

Comment thread src/mesh/frontend.ts
float absCosAngle = abs(dot(normal, uLightDirection.xyz));
float lightingFactor = absCosAngle + uLightDirection.w;
vColor = vec4(lightingFactor * uColor.rgb, uColor.a);
vColor = segmentColorUserShader(uint64_t(uID));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was experimenting with this a little more, here is an example

I think this may have made the same mistake I made in the new cylinder and ball rendering mode for skeletons where it missed that the old color coming in from getObjectColor premultiplied by the user setting of opacity in the layer, and that seems to be missing here and also in the skeleton/frontend code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a similar problem with equivalences, equivalence colors are no longer used, for example looks like

Image but should be like

Image Where around the center of the screen at least two meshes of segments should be the same color as they were merged and the skeletons in 2d seem to not use the equivalence color while the segments do. And I think the hover highlight isn't behaving quite right with equivalences

Comment thread src/layer/segmentation/index.ts Outdated
this.layer.manager.chunkManager.chunkQueueManager.gl,
);

this.offscreenGL = initializeWebGL(new OffscreenCanvas(1, 1));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not overly sure about the new canvas and GL context being made inside of the user layer display state, I think it would be better if we could use something at a higher level than this. For example at the viewer context level. If I remember correctly from when I was working on transfer functions, the empirical CDF code in webgl/empirical_cdf used a pattern for this for working with the viewer context which we could maybe reuse here. I believe it made some new framebuffers instead of a full context. What do you think?

Comment thread src/layer/segmentation/rendering.md Outdated
### Examples

```glsl
#uicontrol float property1 invlerp(property="myNumericalProperty", window=[1, 10])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work for me, the type has to be invlerp not float. Is this intended to work? Or are the docs written here and in rst incorrect?

Comment thread src/layer/segmentation/index.ts Outdated

// 2d/3d share GL context
// the offscreen canvas is a separate context that doesn't share textures
this.segmentationColorUserShader = new SegmentColorUserShaderManager(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this and the other shader color user manager should be wrapped in a registerDisposer call

Comment thread src/segment_color.ts Outdated
this.hashMapManager.enable(
gl,
shader,
GPUHashTable.get(this.gl, this.segmentPropertyIndexMap),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This I believe should instead be like on L983 because otherwise we will add many refs to the same hash table

Comment thread src/widget/invlerp.ts Outdated
gl.deleteTexture(previousTextureFromValues);
if (!values || !window) return;
const { dataType } = this;
const texture = createHistogramTextureFromValues(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be a bit cleaner and less likely to leak if it is more like the ScaleBarTexture in scale_bar.ts or the DirectLookupTableTexture or BaseLookupTexture in transfer_function.ts, so you can make a HistogramTexture class which is ref counted

chrisj added 29 commits October 29, 2026 16:13
Previously, merged numerical properties were initialized with NaN, which
coerced integer typed arrays to zero while leaving float properties as NaN.
Allocate the merged values with the source typed-array constructor so missing
entries consistently default to zero, preserve the property datatype, and
expand the bounds to include zero.
…r shader generates 6 or less colors for more than 6 visible segments.

perf(ui/layer_bar): limit automaticLayerBarColors to first 10,0000 visible segments
…rols and add detection for property shader controls
…ng a type that only changes the visbility of numerical properties
refactor(segmentation): remove usage of negative alpha as it is unnecessary
…estored controls against the merged property set
…n safe, otherwise rename the properties with a numerical suffix, similar to the layer renaming strategy
…when constrained vertically

fix(segmentation): segmentation user shader editors now hide separately and correctly restore the value from the state
@chrisj
chrisj force-pushed the cj-segment-color-user-shader branch from a2b2cf5 to edb4c9b Compare October 29, 2026 20:51

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants