Added trigger
This commit is contained in:
@@ -252,6 +252,7 @@ If neither variable is set, the workflow falls back to the repo-local defaults u
|
||||
- Find a better UI library for react.
|
||||
- Logs.
|
||||
- Continue source cleanup/refactoring. Pass 1 done
|
||||
- Support a separate sound shader `.slang` file in shader packages.
|
||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||
- Add WebView2
|
||||
- move to MSDF, typography rasterisation
|
||||
- abritary function/triggers for the shader
|
||||
@@ -187,6 +187,7 @@ Supported types:
|
||||
| `bool` | `bool` | `true` or `false` |
|
||||
| `enum` | `int` | selected option index |
|
||||
| `text` | generated texture/helper | string |
|
||||
| `trigger` | `int <id>`, `float <id>Time` | pulse/count |
|
||||
|
||||
Float example:
|
||||
|
||||
@@ -314,6 +315,26 @@ float4 premultipliedText = drawTitleText(textUv, float4(1.0, 1.0, 1.0, 1.0));
|
||||
|
||||
Text is currently limited to printable ASCII. `maxLength` defaults to `64` and is clamped to `1..256`. The optional `font` field references a packaged font declared in `fonts`; if no font is specified, the runtime uses its fallback sans-serif renderer.
|
||||
|
||||
Trigger example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "flash",
|
||||
"label": "Flash",
|
||||
"type": "trigger"
|
||||
}
|
||||
```
|
||||
|
||||
A trigger appears as a button in the control UI. Pressing it increments the shader-visible integer `flash` and records the runtime time in `flashTime`:
|
||||
|
||||
```slang
|
||||
float age = context.time - flashTime;
|
||||
float intensity = flash > 0 ? exp(-age * 5.0) : 0.0;
|
||||
color.rgb += intensity;
|
||||
```
|
||||
|
||||
Triggers are useful for one-shot shader reactions such as flashes, ripples, cuts, or randomized looks. They do not execute arbitrary CPU code; they only update uniforms consumed by the shader.
|
||||
|
||||
Parameter validation:
|
||||
|
||||
- Float values are clamped to `min`/`max` if provided.
|
||||
@@ -321,6 +342,7 @@ Parameter validation:
|
||||
- `color` must have exactly 4 numbers.
|
||||
- Enum defaults must match one of the declared option values.
|
||||
- Text defaults must be strings. Non-printable characters are dropped and values are clamped to `maxLength`.
|
||||
- Trigger values are incremented by the host when triggered. The shader sees the trigger count and last trigger time.
|
||||
- Non-finite numeric values are rejected.
|
||||
|
||||
## Texture Assets
|
||||
|
||||
@@ -75,6 +75,10 @@ bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availa
|
||||
}
|
||||
case ShaderParameterType::Text:
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
||||
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ std::string ShaderParameterTypeToString(ShaderParameterType type)
|
||||
case ShaderParameterType::Boolean: return "bool";
|
||||
case ShaderParameterType::Enum: return "enum";
|
||||
case ShaderParameterType::Text: return "text";
|
||||
case ShaderParameterType::Trigger: return "trigger";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
@@ -187,6 +188,11 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
||||
type = ShaderParameterType::Text;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "trigger")
|
||||
{
|
||||
type = ShaderParameterType::Trigger;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1011,6 +1017,15 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parameterIt->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
ShaderParameterValue& value = layer->parameterValues[parameterId];
|
||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||
return true;
|
||||
}
|
||||
|
||||
ShaderParameterValue normalized;
|
||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||
return false;
|
||||
@@ -1057,6 +1072,15 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parameterIt->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
ShaderParameterValue& value = matchedLayer->parameterValues[parameterIt->id];
|
||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||
return true;
|
||||
}
|
||||
|
||||
ShaderParameterValue normalized;
|
||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||
return false;
|
||||
@@ -1714,6 +1738,12 @@ void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, co
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ShaderParameterType::Trigger:
|
||||
if (valueIt->second.numberValues.empty())
|
||||
valueJson = JsonValue(0.0);
|
||||
else
|
||||
valueJson = JsonValue(std::max(0.0, std::floor(valueIt->second.numberValues.front())));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!shouldNormalize)
|
||||
@@ -2103,6 +2133,8 @@ JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition&
|
||||
return JsonValue(value.enumValue);
|
||||
case ShaderParameterType::Text:
|
||||
return JsonValue(value.textValue);
|
||||
case ShaderParameterType::Trigger:
|
||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
case ShaderParameterType::Float:
|
||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
case ShaderParameterType::Vec2:
|
||||
|
||||
@@ -100,6 +100,9 @@ ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition&
|
||||
case ShaderParameterType::Text:
|
||||
value.textValue = NormalizeTextValue(definition.defaultTextValue, definition.maxLength);
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
value.numberValues = { 0.0, -1000000.0 };
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -190,6 +193,14 @@ bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definit
|
||||
}
|
||||
normalizedValue.textValue = NormalizeTextValue(value.asString(), definition.maxLength);
|
||||
return true;
|
||||
case ShaderParameterType::Trigger:
|
||||
if (!value.isNumber() && !value.isBoolean())
|
||||
{
|
||||
error = "Expected numeric or boolean value for trigger parameter '" + definition.id + "'.";
|
||||
return false;
|
||||
}
|
||||
normalizedValue.numberValues = { value.isNumber() ? std::max(0.0, std::floor(value.asNumber())) : 0.0, -1000000.0 };
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -32,6 +32,7 @@ std::string SlangCBufferTypeForParameter(ShaderParameterType type)
|
||||
case ShaderParameterType::Boolean: return "bool";
|
||||
case ShaderParameterType::Enum: return "int";
|
||||
case ShaderParameterType::Text: return "";
|
||||
case ShaderParameterType::Trigger: return "int";
|
||||
}
|
||||
return "float";
|
||||
}
|
||||
@@ -52,6 +53,12 @@ std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>&
|
||||
{
|
||||
if (definition.type == ShaderParameterType::Text)
|
||||
continue;
|
||||
if (definition.type == ShaderParameterType::Trigger)
|
||||
{
|
||||
source << "\tint " << definition.id << ";\n";
|
||||
source << "\tfloat " << definition.id << "Time;\n";
|
||||
continue;
|
||||
}
|
||||
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
|
||||
}
|
||||
return source.str();
|
||||
|
||||
@@ -72,6 +72,11 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
||||
type = ShaderParameterType::Text;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "trigger")
|
||||
{
|
||||
type = ShaderParameterType::Trigger;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ enum class ShaderParameterType
|
||||
Color,
|
||||
Boolean,
|
||||
Enum,
|
||||
Text
|
||||
Text,
|
||||
Trigger
|
||||
};
|
||||
|
||||
struct ShaderParameterOption
|
||||
|
||||
@@ -70,6 +70,12 @@ Examples:
|
||||
|
||||
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.
|
||||
|
||||
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
|
||||
|
||||
```text
|
||||
/VideoShaderToys/trigger-flash/flash 1
|
||||
```
|
||||
|
||||
## Open Stage Control
|
||||
|
||||
For simple scalar controls, set the widget address and target directly:
|
||||
|
||||
@@ -474,7 +474,7 @@ components:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [float, vec2, color, bool, enum]
|
||||
enum: [float, vec2, color, bool, enum, text, trigger]
|
||||
min:
|
||||
type: array
|
||||
items:
|
||||
|
||||
59
shaders/trigger-ripple/shader.json
Normal file
59
shaders/trigger-ripple/shader.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"id": "trigger-ripple",
|
||||
"name": "Trigger Ripple",
|
||||
"description": "A water-drop style ripple that expands across the video whenever the trigger is pressed.",
|
||||
"category": "Utility",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "drop",
|
||||
"label": "Drop",
|
||||
"type": "trigger"
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Center",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.01, 0.01]
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"min": 0.0,
|
||||
"max": 0.3,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "speed",
|
||||
"label": "Duration",
|
||||
"type": "float",
|
||||
"default": 0.3,
|
||||
"min": 0.3,
|
||||
"max": 5.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "width",
|
||||
"label": "Wave Width",
|
||||
"type": "float",
|
||||
"default": 0.09,
|
||||
"min": 0.01,
|
||||
"max": 0.25,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "damping",
|
||||
"label": "Damping",
|
||||
"type": "float",
|
||||
"default": 0.25,
|
||||
"min": 0.05,
|
||||
"max": 3.0,
|
||||
"step": 0.05
|
||||
}
|
||||
]
|
||||
}
|
||||
34
shaders/trigger-ripple/shader.slang
Normal file
34
shaders/trigger-ripple/shader.slang
Normal file
@@ -0,0 +1,34 @@
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
if (drop <= 0)
|
||||
return context.sourceColor;
|
||||
|
||||
float age = max(context.time - dropTime, 0.0);
|
||||
float2 aspect = float2(context.outputResolution.x / max(context.outputResolution.y, 1.0), 1.0);
|
||||
float2 fromDrop = (context.uv - center) * aspect;
|
||||
float radius = length(fromDrop);
|
||||
|
||||
float2 c0 = (float2(0.0, 0.0) - center) * aspect;
|
||||
float2 c1 = (float2(1.0, 0.0) - center) * aspect;
|
||||
float2 c2 = (float2(0.0, 1.0) - center) * aspect;
|
||||
float2 c3 = (float2(1.0, 1.0) - center) * aspect;
|
||||
float maxRadius = max(max(length(c0), length(c1)), max(length(c2), length(c3)));
|
||||
float progress = saturate(age / max(speed, 0.001));
|
||||
float waveRadius = progress * maxRadius;
|
||||
float distanceToWave = radius - waveRadius;
|
||||
float waveWidth = max(width * maxRadius, 0.0001);
|
||||
float ring = (1.0 - smoothstep(waveWidth, waveWidth * 1.65, abs(distanceToWave))) * exp(-progress * damping * 0.35);
|
||||
float crest = 1.0 - smoothstep(0.0, waveWidth * 0.35, abs(distanceToWave));
|
||||
float ripple = ring * (distanceToWave >= 0.0 ? 1.0 : -0.55);
|
||||
|
||||
float2 direction = radius > 0.0001 ? fromDrop / radius : float2(0.0, 0.0);
|
||||
float2 uvOffset = direction * (ripple * strength);
|
||||
uvOffset.x /= aspect.x;
|
||||
|
||||
float2 refractedUv = clamp(context.uv - uvOffset, float2(0.0, 0.0), float2(1.0, 1.0));
|
||||
float4 refracted = sampleVideo(refractedUv);
|
||||
|
||||
float highlight = saturate(ring * 0.75 + crest * 0.55);
|
||||
refracted.rgb += highlight * 0.075;
|
||||
return saturate(refracted);
|
||||
}
|
||||
@@ -167,6 +167,28 @@ void TestTextNormalization()
|
||||
error.clear();
|
||||
Expect(!NormalizeAndValidateParameterValue(definition, JsonValue(12.0), value, error), "text rejects non-string values");
|
||||
}
|
||||
|
||||
void TestTriggerNormalization()
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = "burst";
|
||||
definition.type = ShaderParameterType::Trigger;
|
||||
|
||||
ShaderParameterValue defaultValue = DefaultValueForDefinition(definition);
|
||||
Expect(defaultValue.numberValues.size() == 2 && defaultValue.numberValues[0] == 0.0, "trigger defaults to an unfired count");
|
||||
Expect(defaultValue.numberValues[1] < 0.0, "trigger default time is safely in the past");
|
||||
|
||||
ShaderParameterValue value;
|
||||
std::string error;
|
||||
Expect(NormalizeAndValidateParameterValue(definition, JsonValue(4.8), value, error), "trigger accepts numeric counts");
|
||||
Expect(value.numberValues.size() == 2 && value.numberValues[0] == 4.0, "trigger count is floored to an integer");
|
||||
|
||||
error.clear();
|
||||
Expect(NormalizeAndValidateParameterValue(definition, JsonValue(true), value, error), "trigger accepts boolean pulse values");
|
||||
|
||||
error.clear();
|
||||
Expect(!NormalizeAndValidateParameterValue(definition, JsonValue("fire"), value, error), "trigger rejects string values");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -177,6 +199,7 @@ int main()
|
||||
TestColorAndBooleanNormalization();
|
||||
TestEnumAndDefaults();
|
||||
TestTextNormalization();
|
||||
TestTriggerNormalization();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
@@ -64,7 +64,8 @@ void TestValidManifest()
|
||||
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
|
||||
{ "value": "soft", "label": "Soft" },
|
||||
{ "value": "hard", "label": "Hard" }
|
||||
] }
|
||||
] },
|
||||
{ "id": "flash", "label": "Flash", "type": "trigger" }
|
||||
]
|
||||
})");
|
||||
|
||||
@@ -76,8 +77,9 @@ void TestValidManifest()
|
||||
Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
|
||||
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
|
||||
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
|
||||
Expect(package.parameters.size() == 3, "parameters parse");
|
||||
Expect(package.parameters.size() == 4, "parameters parse");
|
||||
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
|
||||
Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
@@ -99,9 +99,9 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
} = useThrottledParameterValue(parameter, onParameterChange);
|
||||
|
||||
const defaultValue = parameter.defaultValue;
|
||||
const resetDisabled = defaultValue === undefined || valuesMatch(draftValue, defaultValue);
|
||||
const resetDisabled = parameter.type === "trigger" || defaultValue === undefined || valuesMatch(draftValue, defaultValue);
|
||||
const resetParameter = () => {
|
||||
if (defaultValue !== undefined) {
|
||||
if (parameter.type !== "trigger" && defaultValue !== undefined) {
|
||||
sendValue(defaultValue);
|
||||
}
|
||||
};
|
||||
@@ -318,5 +318,22 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (parameter.type === "trigger") {
|
||||
const triggerCount = Number(draftValue ?? 0);
|
||||
return (
|
||||
<section className="parameter">
|
||||
{header}
|
||||
<button
|
||||
type="button"
|
||||
className="parameter__trigger"
|
||||
onClick={() => sendValue(triggerCount + 1)}
|
||||
>
|
||||
Trigger
|
||||
</button>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ export function formatParameterValue(parameterType, value) {
|
||||
if (parameterType === "bool") {
|
||||
return value ? "Enabled" : "Disabled";
|
||||
}
|
||||
if (parameterType === "trigger") {
|
||||
return `Triggered ${Number(value ?? 0)} time${Number(value ?? 0) === 1 ? "" : "s"}`;
|
||||
}
|
||||
return `${value ?? ""}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user