diff --git a/python/tests/skeleton_options_test.py b/python/tests/skeleton_options_test.py index 2d01eae19d..7765c3d535 100644 --- a/python/tests/skeleton_options_test.py +++ b/python/tests/skeleton_options_test.py @@ -89,9 +89,20 @@ def test_skeleton_options(webdriver): @pytest.mark.parametrize("layout", ["xy", "3d"]) @pytest.mark.parametrize("primitive", ["edge", "node"]) @pytest.mark.parametrize( - "shader_alpha,layer_alpha", [(1, 1), (0.5, 1), (1, 0.5), (0.5, 0.5)] + "shader_output,shader_alpha", + [ + ("rgba", 1), + ("rgba", 0.5), + ("rgb", 1), + ("default", 1), + ("segment-rgb", 1), + ("segment-rgba", 1), + ], ) -def test_skeleton_rgba_opacity(webdriver, layout, primitive, shader_alpha, layer_alpha): +@pytest.mark.parametrize("layer_alpha", [1, 0.5]) +def test_skeleton_opacity( + webdriver, layout, primitive, shader_output, shader_alpha, layer_alpha +): if primitive == "edge": source = SkeletonSource( vertex_positions=[[-100, 0, 0], [100, 0, 0]], @@ -117,6 +128,7 @@ def test_skeleton_rgba_opacity(webdriver, layout, primitive, shader_alpha, layer source=source, segments=[1], object_alpha=layer_alpha, + segment_colors={1: "#ff8040"}, ), ) rendering = s.layers[0].skeleton_rendering @@ -124,13 +136,27 @@ def test_skeleton_rgba_opacity(webdriver, layout, primitive, shader_alpha, layer rendering.mode3d = "lines" rendering.line_width2d = 16 rendering.line_width3d = 16 - rendering.shader = f""" + if shader_output == "rgba": + rendering.shader = f""" void main() {{ emitRGBA(vec4(1.0, 0.5, 0.25, {shader_alpha:.1f})); }} """ + elif shader_output == "rgb": + rendering.shader = "void main() { emitRGB(vec3(1.0, 0.5, 0.25)); }" + elif shader_output == "default": + rendering.shader = "void main() { emitDefault(); }" + elif shader_output == "segment-rgb": + rendering.shader = "void main() { emitRGB(segmentColor().rgb); }" + else: + rendering.shader = "void main() { emitRGBA(segmentColor()); }" pixels = webdriver.viewer.screenshot(size=[32, 32]).screenshot.image_pixels - expected_rgb = np.array([1.0, 0.5, 0.25]) * shader_alpha * layer_alpha * 255 + color = ( + np.array([255, 128, 64]) + if shader_output in ("default", "segment-rgb", "segment-rgba") + else np.array([255, 127.5, 63.75]) + ) + expected_rgb = color * shader_alpha * layer_alpha expected = np.tile(np.append(expected_rgb, 255), (4, 4, 1)) np.testing.assert_allclose(pixels[14:18, 14:18], expected, atol=2, rtol=0) diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index cb3d2540c5..de379750dc 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -45,7 +45,7 @@ import { TrackableBoolean } from "#src/trackable_boolean.js"; import { TrackableValue, WatchableValue } from "#src/trackable_value.js"; import { DataType } from "#src/util/data_type.js"; import { RefCounted } from "#src/util/disposable.js"; -import type { vec3 } from "#src/util/geom.js"; +import type { vec4 } from "#src/util/geom.js"; import { mat4 } from "#src/util/geom.js"; import { verifyFinitePositiveFloat } from "#src/util/json.js"; import { NullarySignal } from "#src/util/signal.js"; @@ -127,9 +127,19 @@ class RenderHelper extends RefCounted { defineCommonShader(builder: ShaderBuilder) { defineVertexId(builder); + // Skeleton shaders use straight-alpha colors until the final emission. builder.addUniform("highp vec4", "uColor"); builder.addUniform("highp mat4", "uProjection"); builder.addUniform("highp uint", "uPickID"); + builder.addFragmentCode(` +// Layer opacity is applied by emitRGBA. +vec4 segmentColor() { + return vec4(uColor.rgb, 1.0); +} +vec4 premultiplyAlpha(vec4 color) { + return ${this.targetIsSliceView ? "color" : "vec4(color.rgb * color.a, color.a)"}; +} +`); } edgeShaderGetter; @@ -179,19 +189,15 @@ highp uint vertexIndex = aVertexIndex.x * (1u - lineEndpointIndex) + aVertexInde `; builder.addFragmentCode(` -vec4 segmentColor() { - return uColor; -} void emitRGBA(vec4 color) { - color.a *= uColor.a; - color.rgb *= ${this.getPremultiplyAlphaFactor()}; - emit(vec4(color.rgb, color.a * getLineAlpha() * ${this.getCrossSectionFadeFactor()}), uPickID); + color.a *= uColor.a * getLineAlpha() * ${this.getCrossSectionFadeFactor()}; + emit(premultiplyAlpha(color), uPickID); } void emitRGB(vec3 color) { - emit(vec4(color * uColor.a, uColor.a * getLineAlpha() * ${this.getCrossSectionFadeFactor()}), uPickID); + emitRGBA(vec4(color, 1.0)); } void emitDefault() { - emit(vec4(uColor.rgb, uColor.a * getLineAlpha() * ${this.getCrossSectionFadeFactor()}), uPickID); + emitRGB(uColor.rgb); } `); builder.addFragmentCode(glsl_COLORMAPS); @@ -250,22 +256,17 @@ emitCircle(uProjection * vec4(vertexPosition, 1.0), uNodeDiameter, 0.0); `; builder.addFragmentCode(` -vec4 segmentColor() { - return uColor; -} void emitRGBA(vec4 color) { color.a *= uColor.a; - color.rgb *= ${this.getPremultiplyAlphaFactor()}; vec4 borderColor = color; vec4 circleColor = getCircleColor(color, borderColor); - emit(circleColor, uPickID); + emit(premultiplyAlpha(circleColor), uPickID); } void emitRGB(vec3 color) { emitRGBA(vec4(color, 1.0)); } void emitDefault() { - vec4 circleColor = getCircleColor(uColor, uColor); - emit(circleColor, uPickID); + emitRGB(uColor.rgb); } `); builder.addFragmentCode(glsl_COLORMAPS); @@ -319,10 +320,6 @@ void emitDefault() { }); } - getPremultiplyAlphaFactor() { - return this.targetIsSliceView ? "1.0" : "color.a"; - } - getCrossSectionFadeFactor() { if (this.targetIsSliceView) { return "(clamp(1.0 - 2.0 * abs(0.5 - gl_FragCoord.z), 0.0, 1.0))"; @@ -342,8 +339,17 @@ void emitDefault() { this.vertexIdHelper.enable(); } - setColor(gl: GL, shader: ShaderProgram, color: vec3) { - gl.uniform4fv(shader.uniform("uColor"), color); + setColor(gl: GL, shader: ShaderProgram, color: vec4) { + // The shared segmentation color is already premultiplied by layer alpha. + const alpha = color[3]; + const scale = alpha > 0 ? 1 / alpha : 0; + gl.uniform4f( + shader.uniform("uColor"), + color[0] * scale, + color[1] * scale, + color[2] * scale, + alpha, + ); } setPickID(gl: GL, shader: ShaderProgram, pickID: number) { @@ -641,9 +647,9 @@ export class SkeletonLayer extends RefCounted { } if (color !== undefined) { edgeShader.bind(); - renderHelper.setColor(gl, edgeShader, (color)); + renderHelper.setColor(gl, edgeShader, color); nodeShader.bind(); - renderHelper.setColor(gl, nodeShader, (color)); + renderHelper.setColor(gl, nodeShader, color); } if (pickIndex !== undefined) { edgeShader.bind();