osc multi arguments
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 7s
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled

This commit is contained in:
2026-05-03 14:51:57 +10:00
parent 7dc4b552a5
commit 0560ea3c49
8 changed files with 436 additions and 48 deletions

View File

@@ -68,23 +68,28 @@
"padding": "auto", "padding": "auto",
"html": "", "html": "",
"css": "", "css": "",
"design": "default",
"pips": true, "pips": true,
"snap": false, "snap": false,
"spring": false, "spring": false,
"rangeX": { "rangeX": {
"min": -180, "min": -60,
"max": 180 "max": 60
}, },
"rangeY": { "rangeY": {
"min": -120, "min": 45,
"max": 120 "max": -45
}, },
"logScaleX": false, "logScaleX": false,
"logScaleY": false, "logScaleY": false,
"sensitivity": 1, "sensitivity": 1,
"value": [0, 0], "value": [
"default": [0, 0], 0,
0
],
"default": [
0,
0
],
"linkId": "", "linkId": "",
"address": "/VideoShaderToys/fisheye-reproject/xy", "address": "/VideoShaderToys/fisheye-reproject/xy",
"preArgs": "", "preArgs": "",
@@ -93,9 +98,135 @@
"target": "127.0.0.1:9000", "target": "127.0.0.1:9000",
"ignoreDefaults": false, "ignoreDefaults": false,
"bypass": true, "bypass": true,
"split": [],
"onCreate": "", "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": "" "onTouch": ""
} }
], ],

View File

@@ -42,6 +42,7 @@ std::vector<std::string> SplitAddress(const std::string& address)
} }
return parts; return parts;
} }
} }
OscServer::OscServer() OscServer::OscServer()
@@ -147,55 +148,35 @@ bool OscServer::DecodeMessage(const char* data, int byteCount, OscMessage& messa
return false; return false;
} }
const char valueType = typeTags[1]; std::vector<std::string> values;
if (valueType == 'f') for (std::size_t index = 1; index < typeTags.size(); ++index)
{ {
double value = 0.0; std::string valueJson;
if (!ReadFloat32(data, byteCount, offset, value)) if (!DecodeArgument(data, byteCount, offset, typeTags[index], valueJson))
{
error = "Unsupported or malformed OSC value type.";
return false; return false;
std::ostringstream stream; }
stream << std::setprecision(9) << value; values.push_back(valueJson);
message.valueJson = stream.str();
return true;
} }
if (valueType == 'd') if (values.size() == 1)
{ {
double value = 0.0; message.valueJson = values.front();
if (!ReadFloat64(data, byteCount, offset, value))
return false;
std::ostringstream stream;
stream << std::setprecision(17) << value;
message.valueJson = stream.str();
return true; return true;
} }
if (valueType == 'i') std::ostringstream arrayJson;
arrayJson << "[";
for (std::size_t index = 0; index < values.size(); ++index)
{ {
int value = 0; if (index > 0)
if (!ReadInt32(data, byteCount, offset, value)) arrayJson << ",";
return false; arrayJson << values[index];
message.valueJson = std::to_string(value);
return true;
} }
arrayJson << "]";
if (valueType == 's') message.valueJson = arrayJson.str();
{ return true;
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;
} }
bool OscServer::DispatchMessage(const OscMessage& message, std::string& error) const 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); 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) bool OscServer::ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value)
{ {
if (offset < 0 || offset >= byteCount) if (offset < 0 || offset >= byteCount)

View File

@@ -37,6 +37,7 @@ private:
void ServerLoop(); void ServerLoop();
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const; bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
bool DispatchMessage(const 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 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 ReadInt32(const char* data, int byteCount, int& offset, int& value);
static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value); static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value);

View File

@@ -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
}
]
}

View File

@@ -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);
}

View File

@@ -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]
}
]
}

View File

@@ -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);
}

View File

@@ -110,6 +110,20 @@ void TestDecodeDoubleMessage()
Expect(message.valueJson.find("51.5") == 0, "double OSC value becomes JSON number"); Expect(message.valueJson.find("51.5") == 0, "double OSC value becomes JSON number");
} }
void TestDecodeVectorMessage()
{
OscServer server;
std::vector<char> 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() void TestDecodeIntStringAndBoolMessages()
{ {
OscServer server; OscServer server;
@@ -183,6 +197,7 @@ int main()
{ {
TestDecodeFloatMessage(); TestDecodeFloatMessage();
TestDecodeDoubleMessage(); TestDecodeDoubleMessage();
TestDecodeVectorMessage();
TestDecodeIntStringAndBoolMessages(); TestDecodeIntStringAndBoolMessages();
TestDispatchValidAddress(); TestDispatchValidAddress();
TestRejectsUnsupportedAddress(); TestRejectsUnsupportedAddress();