Write one GLSL function and Grapple turns it into an effect.

A shader effect is a GLSL function that takes a position in the frame and returns a colour. Grapple supplies the rest of the program and exposes your parameters as controls. This reference covers the function, the built-in values, parameters, inputs, examples and the request that adds an effect to a project.

What you write

Write helper functions if you need them, plus one entry point that takes a vec2 and returns a vec4:

vec4 shade(vec2 uv) {
  vec4 color = gp_sample_input_frames(uv);
  return vec4(color.rgb * gp_amount, color.a);
}

Grapple wraps your source in a complete GLSL 4.50 program: the version line, the texture bindings, the parameter block, the output and main. uv runs from 0 to 1 across the frame being rendered, and the colour you return is the effect's output for that pixel.

Name the entry point
Any identifier that isn't reserved, such as shade. main, GLSL keywords, and names starting with gl_ or gp_ are reserved.
Leave out declarations
Don't write #version, main, layout, uniforms, samplers or in/out variables.
Line numbers
Compiler messages use the line numbers of your own source.

Built-in values

NameTypeMeaning
gp_sample_<input>(uv)vec4Samples a declared input at uv, returning colour and alpha. With an input called input_frames, call gp_sample_input_frames(uv).
gp_timefloatThe current time in seconds.
gp_resolutionvec2The size in pixels of the frame being rendered. It is smaller in a reduced-resolution preview.
gp_composition_resolutionvec2The composition's frame size in pixels, the same in preview and export.
gp_pixelvec2The size of one rendered pixel in uv units, for sampling neighbouring pixels.
gp_<name>Depends on the parameterEach declared parameter, such as gp_amount.

For sizes that should look the same in preview and in the export, such as an offset of 12 pixels, divide by gp_composition_resolution rather than using gp_pixel.

Parameters

Declared parameters appear in Properties and can be keyed. In the shader they have these types:

ControlGLSL type
Slider or anglefloat
Togglebool
Point or vectorvec2
Colourvec3

Parameters share a small block of memory with the time and resolution: there is room for 28 numbers in total, where a colour counts as three and a point as two. Parameter and input names must be valid GLSL identifiers and unique.

Inputs, output and placement

A shader effect has one output, named frames, and up to three inputs, each read through its own gp_sample_ function. Where the effect sits decides what its main input receives:

On a track
The contents of one visual track.
On an adjustment layer
Everything beneath the adjustment layer.
On the composition
The finished composition, as the last step before output.

Shader effects run on Grapple's Vulkan renderer and need a GPU with Vulkan support.

Examples

Channel split

vec4 shade(vec2 uv) {
  vec2 offset = vec2(gp_split / gp_composition_resolution.x, 0.0);
  float r = gp_sample_input_frames(uv + offset).r;
  vec4 c = gp_sample_input_frames(uv);
  float b = gp_sample_input_frames(uv - offset).b;
  return vec4(r, c.g, b, c.a);
}

Parameter: split, a slider in composition pixels. Key it from 0 to 12 and back over a few frames for a hit on a cut.

Vignette

vec4 shade(vec2 uv) {
  vec4 c = gp_sample_input_frames(uv);
  vec2 aspect = vec2(gp_composition_resolution.x / gp_composition_resolution.y, 1.0);
  float d = length((uv - 0.5) * aspect);
  float edge = smoothstep(gp_radius - gp_softness, gp_radius, d);
  return vec4(c.rgb * (1.0 - gp_amount * edge), c.a);
}

Parameters: radius, softness and amount. The aspect correction keeps the vignette round on a wide frame.

Heat-haze wave

vec4 shade(vec2 uv) {
  float wave = sin(uv.y * gp_frequency + gp_time * gp_speed);
  float shift = wave * gp_amplitude / gp_composition_resolution.x;
  return gp_sample_input_frames(vec2(uv.x + shift, uv.y));
}

Parameters: frequency, speed and amplitude in composition pixels. Because it reads gp_time, the haze moves on its own; key amplitude to fade it in and out.

Add a shader to a project

Ape adds shader effects with the effect.author_from_source operation. Paste your function into the Ape panel with the parameters and where the effect should go, and Ape builds the request and looks up the track for you. This is the request for the channel split on one track:

{
  "displayName": "Channel split",
  "implementationKind": "shader",
  "language": "grapple-shader-0",
  "entrypoint": "shade",
  "source": "vec4 shade(vec2 uv) { ... }",
  "params": [{
    "name": "split",
    "label": "Split",
    "description": "Distance between the red and blue channels, in composition pixels.",
    "value": 0,
    "editor": { "family": "scalar", "min": 0, "max": 40, "step": 0.5 }
  }],
  "inputPorts": [{ "name": "input_frames", "type": "frame_stream", "frameStream": { "space": "composition_space" } }],
  "outputPorts": [{ "name": "frames", "type": "frame_stream", "frameStream": { "space": "composition_space" } }],
  "activeRange": { "start": 0, "end": 30 },
  "placement": { "kind": "track", "trackNodeId": "<track id>", "inputPort": "input_frames", "outputPort": "frames" }
}

For the finished composition, use "placement": { "kind": "composition_finish", "compositionNodeId": "<composition id>", "inputPort": "input_frames", "outputPort": "frames" }. The active range is in seconds. To send the request from the command line:

grapple-cli --open-package "C:\Users\you\Documents\Grapple\Night walk" --invoke-tool effect.author_from_source --arguments-file split.json

Run --invoke-tool project.inspect with an arguments file containing {} to find track and composition IDs.

Check the result

Preview a short, representative range before extending the effect across a sequence, and look at it at the size it will be delivered. If the effect doesn't compile, the message names the line in your source. A parameter of an unsupported type, or more parameters than fit, is reported by name, and a machine without a usable Vulkan GPU reports that shaders can't run.