From 0560ea3c4948a585e1d97a2e66c6d02dc8beab2a Mon Sep 17 00:00:00 2001 From: Aiden Date: Sun, 3 May 2026 14:51:57 +1000 Subject: [PATCH] osc multi arguments --- OSC/Test.json | 149 ++++++++++++++++-- .../OscServer.cpp | 110 ++++++++----- .../OscServer.h | 1 + shaders/composition-guides/shader.json | 63 ++++++++ shaders/composition-guides/shader.slang | 48 ++++++ shaders/video-transform/shader.json | 54 +++++++ shaders/video-transform/shader.slang | 44 ++++++ tests/OscServerTests.cpp | 15 ++ 8 files changed, 436 insertions(+), 48 deletions(-) create mode 100644 shaders/composition-guides/shader.json create mode 100644 shaders/composition-guides/shader.slang create mode 100644 shaders/video-transform/shader.json create mode 100644 shaders/video-transform/shader.slang diff --git a/OSC/Test.json b/OSC/Test.json index cb88f4d..0c376f5 100644 --- a/OSC/Test.json +++ b/OSC/Test.json @@ -68,23 +68,28 @@ "padding": "auto", "html": "", "css": "", - "design": "default", "pips": true, "snap": false, "spring": false, "rangeX": { - "min": -180, - "max": 180 + "min": -60, + "max": 60 }, "rangeY": { - "min": -120, - "max": 120 + "min": 45, + "max": -45 }, "logScaleX": false, "logScaleY": false, "sensitivity": 1, - "value": [0, 0], - "default": [0, 0], + "value": [ + 0, + 0 + ], + "default": [ + 0, + 0 + ], "linkId": "", "address": "/VideoShaderToys/fisheye-reproject/xy", "preArgs": "", @@ -93,9 +98,135 @@ "target": "127.0.0.1:9000", "ignoreDefaults": false, "bypass": true, - "split": [], "onCreate": "", - "onValue": "if (touch !== undefined) return;\nvar pan = Array.isArray(value) ? Number(value[0]) : 0;\nvar tilt = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/panDegrees', {type: 'f', value: pan});\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type: 'f', value: tilt});", + "onValue": "var pan = Array.isArray(value) ? Number(value[0]) : 0;\nvar tilt = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/panDegrees', {type: 'f', value: pan});\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type: 'f', value: tilt});", + "onTouch": "", + "pointSize": 20, + "ephemeral": false, + "label": "", + "stepsX": false, + "stepsY": false, + "clipX": "", + "clipY": "", + "axisLock": "", + "doubleTap": false + }, + { + "type": "fader", + "top": 120, + "left": 570, + "lock": false, + "id": "fader_1", + "visible": true, + "interaction": true, + "comments": "", + "width": 90, + "height": 420, + "expand": false, + "colorText": "auto", + "colorWidget": "auto", + "colorStroke": "auto", + "colorFill": "auto", + "alphaStroke": "auto", + "alphaFillOff": "auto", + "alphaFillOn": "auto", + "lineWidth": "auto", + "borderRadius": "auto", + "padding": "auto", + "html": "", + "css": "", + "design": "default", + "knobSize": "auto", + "colorKnob": "auto", + "horizontal": false, + "pips": false, + "dashed": false, + "gradient": [], + "snap": false, + "touchZone": "all", + "spring": false, + "doubleTap": false, + "range": { + "min": 100, + "max": 10 + }, + "logScale": false, + "sensitivity": 1, + "steps": "", + "origin": "auto", + "value": "", + "default": 90, + "linkId": "", + "address": "/VideoShaderToys/fisheye-reproject/virtualFovDegrees", + "preArgs": "", + "typeTags": "", + "decimals": 2, + "target": "127.0.0.1:9000", + "ignoreDefaults": false, + "bypass": false, + "onCreate": "", + "onValue": "", + "onTouch": "" + }, + { + "type": "xy", + "top": 700, + "left": 190, + "lock": false, + "id": "Pan Pad", + "visible": true, + "interaction": true, + "comments": "", + "width": "auto", + "height": "auto", + "expand": false, + "colorText": "auto", + "colorWidget": "auto", + "colorStroke": "auto", + "colorFill": "auto", + "alphaStroke": "auto", + "alphaFillOff": "auto", + "alphaFillOn": "auto", + "lineWidth": "auto", + "borderRadius": "auto", + "padding": "auto", + "html": "", + "css": "", + "pointSize": 20, + "ephemeral": false, + "pips": true, + "label": "", + "snap": false, + "spring": false, + "rangeX": { + "min": -1, + "max": 1 + }, + "rangeY": { + "min": -1, + "max": 1 + }, + "logScaleX": false, + "logScaleY": false, + "stepsX": false, + "stepsY": false, + "clipX": "", + "clipY": "", + "axisLock": "", + "doubleTap": false, + "sensitivity": 1, + "value": "", + "default": "", + "linkId": "", + "address": "/VideoShaderToys/video-transform/pan", + "preArgs": "", + "typeTags": "", + "decimals": 2, + "target": "", + "ignoreDefaults": false, + "bypass": true, + "onCreate": "", + "onValue": "var x = Array.isArray(value) ? Number(value[0]) : 0;\nvar y = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/video-transform/pan', {type: 'f', value: x}, {type: 'f', value: y});", "onTouch": "" } ], diff --git a/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp b/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp index ae0cb2b..cc38780 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp @@ -42,6 +42,7 @@ std::vector SplitAddress(const std::string& address) } return parts; } + } OscServer::OscServer() @@ -147,55 +148,35 @@ bool OscServer::DecodeMessage(const char* data, int byteCount, OscMessage& messa return false; } - const char valueType = typeTags[1]; - if (valueType == 'f') + std::vector values; + for (std::size_t index = 1; index < typeTags.size(); ++index) { - double value = 0.0; - if (!ReadFloat32(data, byteCount, offset, value)) + std::string valueJson; + if (!DecodeArgument(data, byteCount, offset, typeTags[index], valueJson)) + { + error = "Unsupported or malformed OSC value type."; return false; - std::ostringstream stream; - stream << std::setprecision(9) << value; - message.valueJson = stream.str(); - return true; + } + values.push_back(valueJson); } - if (valueType == 'd') + if (values.size() == 1) { - double value = 0.0; - if (!ReadFloat64(data, byteCount, offset, value)) - return false; - std::ostringstream stream; - stream << std::setprecision(17) << value; - message.valueJson = stream.str(); + message.valueJson = values.front(); return true; } - if (valueType == 'i') + std::ostringstream arrayJson; + arrayJson << "["; + for (std::size_t index = 0; index < values.size(); ++index) { - int value = 0; - if (!ReadInt32(data, byteCount, offset, value)) - return false; - message.valueJson = std::to_string(value); - return true; + if (index > 0) + arrayJson << ","; + arrayJson << values[index]; } - - if (valueType == 's') - { - std::string value; - if (!ReadPaddedString(data, byteCount, offset, value)) - return false; - message.valueJson = BuildJsonString(value); - return true; - } - - if (valueType == 'T' || valueType == 'F') - { - message.valueJson = valueType == 'T' ? "true" : "false"; - return true; - } - - error = "Unsupported OSC value type."; - return false; + arrayJson << "]"; + message.valueJson = arrayJson.str(); + return true; } bool OscServer::DispatchMessage(const OscMessage& message, std::string& error) const @@ -211,6 +192,57 @@ bool OscServer::DispatchMessage(const OscMessage& message, std::string& error) c mCallbacks.updateParameter(parts[1], parts[2], message.valueJson, error); } +bool OscServer::DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson) +{ + if (valueType == 'f') + { + double value = 0.0; + if (!ReadFloat32(data, byteCount, offset, value)) + return false; + std::ostringstream stream; + stream << std::setprecision(9) << value; + valueJson = stream.str(); + return true; + } + + if (valueType == 'd') + { + double value = 0.0; + if (!ReadFloat64(data, byteCount, offset, value)) + return false; + std::ostringstream stream; + stream << std::setprecision(17) << value; + valueJson = stream.str(); + return true; + } + + if (valueType == 'i') + { + int value = 0; + if (!ReadInt32(data, byteCount, offset, value)) + return false; + valueJson = std::to_string(value); + return true; + } + + if (valueType == 's') + { + std::string value; + if (!ReadPaddedString(data, byteCount, offset, value)) + return false; + valueJson = BuildJsonString(value); + return true; + } + + if (valueType == 'T' || valueType == 'F') + { + valueJson = valueType == 'T' ? "true" : "false"; + return true; + } + + return false; +} + bool OscServer::ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value) { if (offset < 0 || offset >= byteCount) diff --git a/apps/LoopThroughWithOpenGLCompositing/OscServer.h b/apps/LoopThroughWithOpenGLCompositing/OscServer.h index 5082b25..f4fb0b4 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OscServer.h +++ b/apps/LoopThroughWithOpenGLCompositing/OscServer.h @@ -37,6 +37,7 @@ private: void ServerLoop(); bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const; bool DispatchMessage(const OscMessage& message, std::string& error) const; + static bool DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson); static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value); static bool ReadInt32(const char* data, int byteCount, int& offset, int& value); static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value); diff --git a/shaders/composition-guides/shader.json b/shaders/composition-guides/shader.json new file mode 100644 index 0000000..de1be82 --- /dev/null +++ b/shaders/composition-guides/shader.json @@ -0,0 +1,63 @@ +{ + "id": "composition-guides", + "name": "Composition Guides", + "description": "Overlays rule-of-thirds guides and a center crosshair for camera alignment and framing.", + "category": "Utility", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "showThirds", + "label": "Rule of Thirds", + "type": "bool", + "default": true + }, + { + "id": "showCrosshair", + "label": "Center Crosshair", + "type": "bool", + "default": true + }, + { + "id": "lineColor", + "label": "Line Color", + "type": "color", + "default": [1.0, 1.0, 1.0, 1.0] + }, + { + "id": "lineOpacity", + "label": "Line Opacity", + "type": "float", + "default": 0.65, + "min": 0.0, + "max": 1.0, + "step": 0.01 + }, + { + "id": "lineThicknessPixels", + "label": "Line Thickness", + "type": "float", + "default": 2.0, + "min": 0.5, + "max": 12.0, + "step": 0.1 + }, + { + "id": "crosshairSizePixels", + "label": "Crosshair Size", + "type": "float", + "default": 54.0, + "min": 8.0, + "max": 240.0, + "step": 1.0 + }, + { + "id": "crosshairGapPixels", + "label": "Crosshair Gap", + "type": "float", + "default": 10.0, + "min": 0.0, + "max": 80.0, + "step": 1.0 + } + ] +} diff --git a/shaders/composition-guides/shader.slang b/shaders/composition-guides/shader.slang new file mode 100644 index 0000000..83550c5 --- /dev/null +++ b/shaders/composition-guides/shader.slang @@ -0,0 +1,48 @@ +float lineMask(float coordinate, float target, float pixelThickness, float resolution) +{ + float halfWidth = max(pixelThickness, 0.5) * 0.5 / max(resolution, 1.0); + float distanceToLine = abs(coordinate - target); + float feather = halfWidth * 0.85 + (1.0 / max(resolution, 1.0)); + return 1.0 - smoothstep(halfWidth, feather, distanceToLine); +} + +float thirdsMask(float2 uv, float2 resolution) +{ + float thickness = max(lineThicknessPixels, 0.5); + float mask = 0.0; + mask = max(mask, lineMask(uv.x, 1.0 / 3.0, thickness, resolution.x)); + mask = max(mask, lineMask(uv.x, 2.0 / 3.0, thickness, resolution.x)); + mask = max(mask, lineMask(uv.y, 1.0 / 3.0, thickness, resolution.y)); + mask = max(mask, lineMask(uv.y, 2.0 / 3.0, thickness, resolution.y)); + return mask; +} + +float crosshairMask(float2 uv, float2 resolution) +{ + float2 pixel = (uv - 0.5) * resolution; + float halfThickness = max(lineThicknessPixels, 0.5) * 0.5; + float halfSize = max(crosshairSizePixels, 1.0) * 0.5; + float halfGap = max(crosshairGapPixels, 0.0) * 0.5; + + float horizontalExtent = step(abs(pixel.x), halfSize) * (1.0 - step(abs(pixel.x), halfGap)); + float verticalExtent = step(abs(pixel.y), halfSize) * (1.0 - step(abs(pixel.y), halfGap)); + + float horizontalLine = horizontalExtent * (1.0 - smoothstep(halfThickness, halfThickness + 1.5, abs(pixel.y))); + float verticalLine = verticalExtent * (1.0 - smoothstep(halfThickness, halfThickness + 1.5, abs(pixel.x))); + return max(horizontalLine, verticalLine); +} + +float4 shadeVideo(ShaderContext context) +{ + float2 resolution = max(context.outputResolution, float2(1.0, 1.0)); + float mask = 0.0; + + if (showThirds) + mask = max(mask, thirdsMask(context.uv, resolution)); + if (showCrosshair) + mask = max(mask, crosshairMask(context.uv, resolution)); + + float opacity = saturate(lineOpacity * lineColor.a) * mask; + float3 color = lerp(context.sourceColor.rgb, lineColor.rgb, opacity); + return float4(color, context.sourceColor.a); +} diff --git a/shaders/video-transform/shader.json b/shaders/video-transform/shader.json new file mode 100644 index 0000000..67ca809 --- /dev/null +++ b/shaders/video-transform/shader.json @@ -0,0 +1,54 @@ +{ + "id": "video-transform", + "name": "Video Transform", + "description": "Zooms, pans, and rotates the video by remapping output pixels back into source UV space.", + "category": "Utility", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "zoom", + "label": "Zoom", + "type": "float", + "default": 1.0, + "min": 0.1, + "max": 8.0, + "step": 0.01 + }, + { + "id": "pan", + "label": "Pan", + "type": "vec2", + "default": [0.0, 0.0], + "min": [-1.0, -1.0], + "max": [1.0, 1.0], + "step": [0.001, 0.001] + }, + { + "id": "rotationDegrees", + "label": "Rotation", + "type": "float", + "default": 0.0, + "min": -180.0, + "max": 180.0, + "step": 0.1 + }, + { + "id": "edgeMode", + "label": "Edge Mode", + "type": "enum", + "default": "black", + "options": [ + { "value": "black", "label": "Black" }, + { "value": "clamp", "label": "Clamp" }, + { "value": "wrap", "label": "Wrap" }, + { "value": "mirror", "label": "Mirror" } + ] + }, + { + "id": "outsideColor", + "label": "Outside Color", + "type": "color", + "default": [0.0, 0.0, 0.0, 1.0] + } + ] +} diff --git a/shaders/video-transform/shader.slang b/shaders/video-transform/shader.slang new file mode 100644 index 0000000..e6f3714 --- /dev/null +++ b/shaders/video-transform/shader.slang @@ -0,0 +1,44 @@ +static const float PI = 3.14159265358979323846; + +float2 rotateAroundCenter(float2 uv, float radians) +{ + float2 centered = uv - 0.5; + float s = sin(radians); + float c = cos(radians); + return float2(c * centered.x - s * centered.y, s * centered.x + c * centered.y) + 0.5; +} + +float mirroredCoordinate(float coordinate) +{ + float wrapped = frac(coordinate * 0.5) * 2.0; + return wrapped <= 1.0 ? wrapped : 2.0 - wrapped; +} + +float2 applyEdgeMode(float2 uv, out bool inside) +{ + inside = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0; + + if (edgeMode == 1) + return clamp(uv, 0.0, 1.0); + if (edgeMode == 2) + return frac(uv); + if (edgeMode == 3) + return float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)); + + return uv; +} + +float4 shadeVideo(ShaderContext context) +{ + float safeZoom = max(zoom, 0.001); + float2 sourceUv = (context.uv - 0.5) / safeZoom + 0.5; + sourceUv -= pan; + sourceUv = rotateAroundCenter(sourceUv, -rotationDegrees * (PI / 180.0)); + + bool inside = false; + float2 sampledUv = applyEdgeMode(sourceUv, inside); + if (!inside && edgeMode == 0) + return outsideColor; + + return sampleVideo(sampledUv); +} diff --git a/tests/OscServerTests.cpp b/tests/OscServerTests.cpp index ba93908..6dc8fbd 100644 --- a/tests/OscServerTests.cpp +++ b/tests/OscServerTests.cpp @@ -110,6 +110,20 @@ void TestDecodeDoubleMessage() Expect(message.valueJson.find("51.5") == 0, "double OSC value becomes JSON number"); } +void TestDecodeVectorMessage() +{ + OscServer server; + std::vector packet = BuildOscPacket("/VideoShaderToys/video-transform/pan", ",ff"); + AppendFloat32(packet, 0.25f); + AppendFloat32(packet, -0.5f); + + OscServerTestAccess::Message message; + std::string error; + Expect(OscServerTestAccess::Decode(server, packet, message, error), "multi-float OSC message decodes"); + Expect(message.address == "/VideoShaderToys/video-transform/pan", "multi-float OSC address is preserved"); + Expect(message.valueJson.find("[0.25,-0.5") == 0, "multi-float OSC value becomes JSON array"); +} + void TestDecodeIntStringAndBoolMessages() { OscServer server; @@ -183,6 +197,7 @@ int main() { TestDecodeFloatMessage(); TestDecodeDoubleMessage(); + TestDecodeVectorMessage(); TestDecodeIntStringAndBoolMessages(); TestDispatchValidAddress(); TestRejectsUnsupportedAddress();