From ff0d59a9a4eceb552700ceb5b6a4df4a5294c57c Mon Sep 17 00:00:00 2001 From: RoboDoig Date: Fri, 4 Sep 2026 16:37:40 +0100 Subject: [PATCH 1/4] Implementation of shader image rotation --- src/Extensions/SaturationShaderPass.cs | 61 ++++++++++++++++-- src/test_visualizer.bonsai | 88 ++++++++++++-------------- 2 files changed, 96 insertions(+), 53 deletions(-) diff --git a/src/Extensions/SaturationShaderPass.cs b/src/Extensions/SaturationShaderPass.cs index 770896c..04591bf 100644 --- a/src/Extensions/SaturationShaderPass.cs +++ b/src/Extensions/SaturationShaderPass.cs @@ -15,6 +15,9 @@ public class SaturationShaderPass : Combinator public float MinSaturation {get; set;} public float MaxSaturation {get; set;} + [Description("Clockwise rotation applied to the image, in radians.")] + public float Rotation { get; set; } + const string VertexShaderSource = @" #version 330 core layout(location = 0) in vec2 vertexPosition; @@ -36,12 +39,37 @@ void main() uniform float minSaturation = 0.1; uniform float maxSaturation = 0.9; uniform float alpha = 0.5; + uniform float rotation = 0.0; + uniform vec2 sourceSize; + uniform vec2 outputSize; in vec2 texCoord; out vec4 fragColor; + // Maps a coordinate in the rotated output back into the source texture by + // applying the inverse rotation about the centre of the image. The offsets are + // in pixels rather than normalised units, so a non-square image is rotated + // rigidly instead of being skewed by its aspect ratio. + vec2 sourceCoord(vec2 uv) + { + vec2 offset = (uv - 0.5) * outputSize; + float c = cos(rotation); + float s = sin(rotation); + vec2 rotated = vec2(c * offset.x + s * offset.y, c * offset.y - s * offset.x); + return rotated / sourceSize + 0.5; + } + void main() { - vec4 texColor = texture(tex0, texCoord); + vec2 sourceUv = sourceCoord(texCoord); + + // The rotated image does not cover the whole output; leave the rest clear. + if (any(lessThan(sourceUv, vec2(0.0))) || any(greaterThan(sourceUv, vec2(1.0)))) + { + fragColor = vec4(0,0,0,0); + return; + } + + vec4 texColor = texture(tex0, sourceUv); vec4 overlay = vec4(0,0,0,0); if (texColor.r >= maxSaturation) overlay = vec4(1,0,0,alpha); @@ -67,6 +95,9 @@ public override IObservable Process(IObservable sour int alphaLocation = 0; int minSaturation = 0; int maxSaturation = 0; + int rotationLocation = 0; + int sourceSizeLocation = 0; + int outputSizeLocation = 0; return source.Select(texture => { var currentContext = ImGui.GetCurrentContext(); @@ -81,19 +112,29 @@ public override IObservable Process(IObservable sour alphaLocation = GL.GetUniformLocation(shaderProgram, "alpha"); maxSaturation = GL.GetUniformLocation(shaderProgram, "maxSaturation"); minSaturation = GL.GetUniformLocation(shaderProgram, "minSaturation"); + rotationLocation = GL.GetUniformLocation(shaderProgram, "rotation"); + sourceSizeLocation = GL.GetUniformLocation(shaderProgram, "sourceSize"); + outputSizeLocation = GL.GetUniformLocation(shaderProgram, "outputSize"); } - // The pass renders into a texture of the same size as the incoming one. + // The pass renders into a texture large enough to hold the bounding box + // of the rotated image, so no part of the source is cropped away. int width, height; GL.BindTexture(TextureTarget.Texture2D, sourceTexture); GL.GetTexLevelParameter(TextureTarget.Texture2D, 0, GetTextureParameter.TextureWidth, out width); GL.GetTexLevelParameter(TextureTarget.Texture2D, 0, GetTextureParameter.TextureHeight, out height); - if (width != targetWidth || height != targetHeight) + + var rotation = Rotation; + var cos = Math.Abs(Math.Cos(rotation)); + var sin = Math.Abs(Math.Sin(rotation)); + var outputWidth = Math.Max(1, (int)Math.Round(width * cos + height * sin)); + var outputHeight = Math.Max(1, (int)Math.Round(width * sin + height * cos)); + if (outputWidth != targetWidth || outputHeight != targetHeight) { - targetTexture = CreateTarget(targetTexture, width, height); + targetTexture = CreateTarget(targetTexture, outputWidth, outputHeight); targetRef = CreateTextureRef(targetTexture); - targetWidth = width; - targetHeight = height; + targetWidth = outputWidth; + targetHeight = outputHeight; GL.BindFramebuffer(FramebufferTarget.Framebuffer, framebuffer); GL.FramebufferTexture2D( FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, @@ -103,8 +144,10 @@ public override IObservable Process(IObservable sour var viewport = new int[4]; GL.GetInteger(GetPName.Viewport, viewport); + var blendEnabled = GL.IsEnabled(EnableCap.Blend); + GL.Disable(EnableCap.Blend); GL.BindFramebuffer(FramebufferTarget.Framebuffer, framebuffer); - GL.Viewport(0, 0, width, height); + GL.Viewport(0, 0, outputWidth, outputHeight); GL.UseProgram(shaderProgram); GL.ActiveTexture(TextureUnit.Texture0); @@ -112,6 +155,9 @@ public override IObservable Process(IObservable sour GL.Uniform1(alphaLocation, Alpha); GL.Uniform1(maxSaturation, MaxSaturation); GL.Uniform1(minSaturation, MinSaturation); + GL.Uniform1(rotationLocation, rotation); + GL.Uniform2(sourceSizeLocation, (float)width, (float)height); + GL.Uniform2(outputSizeLocation, (float)outputWidth, (float)outputHeight); GL.BindVertexArray(vertexArray); GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4); @@ -120,6 +166,7 @@ public override IObservable Process(IObservable sour GL.UseProgram(0); GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); GL.Viewport(viewport[0], viewport[1], viewport[2], viewport[3]); + if (blendEnabled) GL.Enable(EnableCap.Blend); return targetRef; }); diff --git a/src/test_visualizer.bonsai b/src/test_visualizer.bonsai index 80d9447..eee517d 100644 --- a/src/test_visualizer.bonsai +++ b/src/test_visualizer.bonsai @@ -1,4 +1,4 @@ - + - true + false @@ -2318,9 +2318,6 @@ it.Index as TrialIndex - - - @@ -2378,12 +2375,14 @@ it.Index as TrialIndex + 0.371 0.1 0.3 + 0 @@ -2473,7 +2472,7 @@ it.Index as TrialIndex - + @@ -2491,35 +2490,34 @@ it.Index as TrialIndex - + - - - - + + + + - - - - - - - - + + + + + + + + + - - - + + - + - @@ -2739,9 +2737,6 @@ it.Index as TrialIndex - - - @@ -2799,12 +2794,14 @@ it.Index as TrialIndex + 0.5 0.1 0.3 + 0 @@ -2894,7 +2891,7 @@ it.Index as TrialIndex - + @@ -2912,35 +2909,34 @@ it.Index as TrialIndex - + - - - - + + + + - - - - - - - - + + + + + + + + + - - - + + - + - From 427ba7a97475cc20764d047f06023128aa6e2c0f Mon Sep 17 00:00:00 2001 From: RoboDoig Date: Fri, 4 Sep 2026 20:22:02 +0100 Subject: [PATCH 2/4] Implement and test image flip in shader pass --- src/Extensions/SaturationShaderPass.cs | 23 +- src/test_visualizer.bonsai | 278 +++---------------------- 2 files changed, 51 insertions(+), 250 deletions(-) diff --git a/src/Extensions/SaturationShaderPass.cs b/src/Extensions/SaturationShaderPass.cs index 04591bf..b9899f2 100644 --- a/src/Extensions/SaturationShaderPass.cs +++ b/src/Extensions/SaturationShaderPass.cs @@ -18,6 +18,12 @@ public class SaturationShaderPass : Combinator [Description("Clockwise rotation applied to the image, in radians.")] public float Rotation { get; set; } + [Description("Mirrors the image left to right, about its vertical axis.")] + public bool FlipHorizontal { get; set; } + + [Description("Mirrors the image top to bottom, about its horizontal axis.")] + public bool FlipVertical { get; set; } + const string VertexShaderSource = @" #version 330 core layout(location = 0) in vec2 vertexPosition; @@ -42,20 +48,26 @@ void main() uniform float rotation = 0.0; uniform vec2 sourceSize; uniform vec2 outputSize; + uniform vec2 flip = vec2(0.0, 0.0); in vec2 texCoord; out vec4 fragColor; // Maps a coordinate in the rotated output back into the source texture by - // applying the inverse rotation about the centre of the image. The offsets are - // in pixels rather than normalised units, so a non-square image is rotated - // rigidly instead of being skewed by its aspect ratio. + // applying the inverse transform about the centre of the image. The rotation + // offsets are in pixels rather than normalised units, so a non-square image is + // rotated rigidly instead of being skewed by its aspect ratio. The forward + // transform mirrors the image before rotating it, so the inverse undoes the + // rotation first and the mirroring second. Each component of `flip` is 1 to + // mirror that axis and 0 to leave it alone; mirroring maps the unit square onto + // itself, so it does not disturb the coverage test in main(). vec2 sourceCoord(vec2 uv) { vec2 offset = (uv - 0.5) * outputSize; float c = cos(rotation); float s = sin(rotation); vec2 rotated = vec2(c * offset.x + s * offset.y, c * offset.y - s * offset.x); - return rotated / sourceSize + 0.5; + vec2 sourceUv = rotated / sourceSize + 0.5; + return mix(sourceUv, 1.0 - sourceUv, flip); } void main() @@ -98,6 +110,7 @@ public override IObservable Process(IObservable sour int rotationLocation = 0; int sourceSizeLocation = 0; int outputSizeLocation = 0; + int flipLocation = 0; return source.Select(texture => { var currentContext = ImGui.GetCurrentContext(); @@ -115,6 +128,7 @@ public override IObservable Process(IObservable sour rotationLocation = GL.GetUniformLocation(shaderProgram, "rotation"); sourceSizeLocation = GL.GetUniformLocation(shaderProgram, "sourceSize"); outputSizeLocation = GL.GetUniformLocation(shaderProgram, "outputSize"); + flipLocation = GL.GetUniformLocation(shaderProgram, "flip"); } // The pass renders into a texture large enough to hold the bounding box @@ -158,6 +172,7 @@ public override IObservable Process(IObservable sour GL.Uniform1(rotationLocation, rotation); GL.Uniform2(sourceSizeLocation, (float)width, (float)height); GL.Uniform2(outputSizeLocation, (float)outputWidth, (float)outputHeight); + GL.Uniform2(flipLocation, FlipHorizontal ? 1f : 0f, FlipVertical ? 1f : 0f); GL.BindVertexArray(vertexArray); GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4); diff --git a/src/test_visualizer.bonsai b/src/test_visualizer.bonsai index eee517d..2c1e7bc 100644 --- a/src/test_visualizer.bonsai +++ b/src/test_visualizer.bonsai @@ -1,4 +1,4 @@ - + - PT1.756S + PT0.79S @@ -422,7 +422,7 @@ - PT1.826S + PT0.306S @@ -493,7 +493,7 @@ - PT0.735S + PT2.147S @@ -899,7 +899,7 @@ it.Index as TrialIndex - false + true @@ -1039,7 +1039,7 @@ it.Index as TrialIndex - PT0.377S + PT0.907S @@ -1731,9 +1731,9 @@ it.Index as TrialIndex true - 0.22 - 196 - 0.21708835946260363 + -0.33 + 32 + -0.32753804625363003 0 -10 @@ -2007,66 +2007,6 @@ it.Index as TrialIndex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Break 200 @@ -2125,9 +2065,10 @@ it.Index as TrialIndex - + + CameraPreview @@ -2212,146 +2153,6 @@ it.Index as TrialIndex Value - - - - - - 2 - - - - - - - Flipped - - - - Source1 - - - Item2 - - - - 2 - - - - - - - - - - - - - Item1 - - - Item2 - - - - - - - - - Vertical - - - - NotFlipped - - - - Source1 - - - Item2 - - - - 2 - - - - - - - - - - - - - - - Item1 - - - - - - new ( - it.Size.Width / 2 as X, - it.Size.Height / 2 as Y -) - - - - - - - - - - 320 - 240 - - - - - - - - - - - 320 - 240 - - - 0 - 0 - - 0 - - 1 - 1 - - - - - - - - - - - 1,0,0;0,1,0 - Linear - - 0 - 0 - 0 - 0 - - - Table @@ -2376,13 +2177,17 @@ it.Index as TrialIndex + + - 0.371 + 0.5 0.1 0.3 - 0 + 0.1 + false + false @@ -2472,52 +2277,31 @@ it.Index as TrialIndex - + - - + - + - - - - + + - + - - + + + - + - - - - - - - - - - - - - - - - - - - - - - + + + @@ -2802,6 +2586,8 @@ it.Index as TrialIndex 0.1 0.3 0 + false + false From ad94c1c9b355944b6e5ebe1a16e25a7c5afa3055 Mon Sep 17 00:00:00 2001 From: RoboDoig Date: Fri, 4 Sep 2026 20:29:42 +0100 Subject: [PATCH 3/4] Save camera preview as IncludeWorkflow --- src/Extensions/CameraPreviewVisualizer.bonsai | 158 ++---------------- 1 file changed, 13 insertions(+), 145 deletions(-) diff --git a/src/Extensions/CameraPreviewVisualizer.bonsai b/src/Extensions/CameraPreviewVisualizer.bonsai index c8d3fc5..01086e6 100644 --- a/src/Extensions/CameraPreviewVisualizer.bonsai +++ b/src/Extensions/CameraPreviewVisualizer.bonsai @@ -1,10 +1,8 @@ - + @@ -18,9 +16,10 @@ - + + CameraPreview @@ -68,7 +67,7 @@ - BodyCamera + Camera1 @@ -105,148 +104,11 @@ Value.Image - - - - - - 2 - - - - - - Flipped - - - - Source1 - - - Item2 - - - - 2 - - - - - - - - - - - - - Item1 - - - Item2 - - - - - - - - - Vertical - - - - NotFlipped - - - - Source1 - - - Item2 - - - - 2 - - - - - - - - - - - - + - Item1 - - - - - - new ( - it.Size.Width / 2 as X, - it.Size.Height / 2 as Y -) - - - - - - - - - - 320 - 240 - - - - - - - - - - - - - - 320 - 240 - - - 0 - 0 - - 0 - - 1 - 1 - - - - - - - - - - - 1,0,0;0,1,0 - Linear - - 0 - 0 - 0 - 0 - - + Value.Image Table @@ -271,12 +133,18 @@ + + + 0.5 0.1 - 0.9 + 0.3 + 0.1 + false + false From 9ca725e46d6fa474e6d461eec3022b0b536b2d76 Mon Sep 17 00:00:00 2001 From: RoboDoig Date: Fri, 4 Sep 2026 20:35:15 +0100 Subject: [PATCH 4/4] fix xml issue from rebase --- src/Extensions/CameraPreviewVisualizer.bonsai | 54 +++++-------------- src/test_visualizer.bonsai | 26 +++------ 2 files changed, 21 insertions(+), 59 deletions(-) diff --git a/src/Extensions/CameraPreviewVisualizer.bonsai b/src/Extensions/CameraPreviewVisualizer.bonsai index 01086e6..fb53817 100644 --- a/src/Extensions/CameraPreviewVisualizer.bonsai +++ b/src/Extensions/CameraPreviewVisualizer.bonsai @@ -1,4 +1,4 @@ - + Value.Image - - - - - Value.Image - Table @@ -234,53 +228,31 @@ - + - - + - + - - - - + + - + - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/test_visualizer.bonsai b/src/test_visualizer.bonsai index 2c1e7bc..41f6d21 100644 --- a/src/test_visualizer.bonsai +++ b/src/test_visualizer.bonsai @@ -370,7 +370,7 @@ - PT0.79S + PT1.537S @@ -422,7 +422,7 @@ - PT0.306S + PT0.079S @@ -493,7 +493,7 @@ - PT2.147S + PT2.62S @@ -1013,7 +1013,7 @@ it.Index as TrialIndex true - false + true @@ -1039,7 +1039,7 @@ it.Index as TrialIndex - PT0.907S + PT0.974S @@ -1731,9 +1731,9 @@ it.Index as TrialIndex true - -0.33 - 32 - -0.32753804625363003 + -0.04 + 3 + -0.037661941925837626 0 -10 @@ -1997,16 +1997,6 @@ it.Index as TrialIndex - - - - - - - - - - Break 200