DRAFT 0.1 · OPEN STANDARD · UNSTABLE

WISPFX.ORG

PORTABLE VIDEO EFFECTS & TRANSITIONS

A WispFX plugin is a JSON manifest plus WGSL shaders, and it renders identically in native wgpu hosts and in the browser through WebGPU. Write an effect or a transition once, run it in any host that speaks the standard, and test it headlessly with the wispfx CLI.

01 / FORMAT

Manifest + WGSL

A plugin is a folder: manifest.json plus one or more WGSL files. Typed params, ordered passes, and nothing a host has to guess.

02 / KINDS

Effect and transition

An effect takes one input, source. A transition takes from and to plus a progress the host drives from 0 to 1 across the cut.

03 / ABI

One host prelude

The host prepends a prelude that declares every binding and the vertex stage. A plugin writes fs_main and never its own samplers or textures.

04 / SIGNALS

Audio and MIDI aware

A plugin declares MIDI 2.0 controllers and notes it reacts to. The host feeds them from an audio follower, a hardware device or OSC, and the shader reads plain 0..1 floats. A grade can follow the bass.

05 / PORTABLE

wgpu and WebGPU

Linear premultiplied rgba16float in and out, WGSL both naga and the browser accept, no extensions. A native host and a browser host draw the same frame.

EXAMPLE · CROSS DISSOLVE

manifest.json declares what the plugin is, its params and its passes. shader.wgsl is the fragment code. The host prepends the prelude and compiles.

manifest.json
{
  "wisp": "0.1",
  "id": "org.wispfx.cross-dissolve",
  "name": "Cross Dissolve",
  "kind": "transition",
  "category": "dissolve",
  "version": "0.1.0",
  "license": "MIT OR Apache-2.0",
  "params": [
    { "id": "ease", "label": "Ease", "type": "float",
      "default": 0.0, "min": 0.0, "max": 1.0, "animatable": true }
  ],
  "passes": [
    { "shader": "shader.wgsl", "entry": "fs_main",
      "inputs": ["from", "to"], "output": "out" }
  ]
}
shader.wgsl
struct Params {
    ease: f32,
}
@group(1) @binding(0) var<uniform> params: Params;

@fragment
fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let a = textureSampleLevel(wisp_input0, wisp_sampler, uv, 0.0);
    let b = textureSampleLevel(wisp_input1, wisp_sampler, uv, 0.0);
    let p = clamp(wisp_globals.progress, 0.0, 1.0);
    let t = mix(p, smoothstep(0.0, 1.0, p), params.ease);
    return mix(a, b, t); // premultiplied: a plain mix is correct
}
HOST BINDINGS
  • 0 / 0wisp_sampler · sampler, linear filter, clamp to edge
  • 0 / 1wisp_globals · uniform WispGlobals: resolution, texel, time, progress
  • 0 / 2wisp_input0 · texture_2d<f32>, first pass input
  • 0 / 3wisp_input1 · texture_2d<f32>, second pass input, or 1×1 transparent
  • 1 / 0params · uniform Params, declared by the plugin to match the manifest
  • 1 / 1wisp_signals · uniform Signals, one f32 per MIDI 2.0 controller or note the plugin declares
  • ENTRY@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f
  • COLOURlinear, premultiplied RGBA, rgba16float, Rec. 709 primaries, HDR allowed
STACK
  • SHADERSWGSL · naga and browser WebGPU, no extensions
  • NATIVEwgpu
  • BROWSERWebGPU
  • REFERENCERust crate: parse, validate, assemble, render · wispfx check and wispfx render
  • CATALOGreference list of effects and transitions to cover · CATALOG.md
  • LICENSEMIT OR Apache-2.0 · DCO sign-off