Authoring a shader plugin

Drop a shader file, declare it in plugin.toml, and DNA turns it into a node with sliders and colour pickers built for you.

A shader plugin is the lowest-friction way to extend DNA. You don't write any code beyond the shader itself — no compiling, no build step. You write a WGSL (or GLSL) effect, name your parameters in comments, point a small text file at it, and a new node appears in the search palette. Edit the shader and save, and DNA recompiles it live.

The shape of a shader plugin

A shader plugin is a folder under ~/.dna/plugins/ containing two things:

A minimal manifest looks like this:

[plugin]
name = "My Effects"
version = "0.1.0"
api_version = "1.0.0"

[plugin.shader]

[[shader]]
id = "custom.chromatic_aberration"
display_name = "Chromatic Aberration"
description = "Splits the colour channels outward from centre"
category = "Effects"
file = "chromatic.wgsl"

The [plugin.shader] line marks the whole plugin as a shader plugin. Each [[shader]] block becomes one node. The id must be unique (this is the node's registry name), display_name is what you see in the palette, and file points at the shader sitting beside the manifest.

A plugin is exactly one kind — a shader plugin can't also be a script or native plugin. Pick one per folder.

Declaring a shader node

Each [[shader]] entry accepts these fields:

The file path has to stay inside your plugin folder — no .. and no absolute paths.

Want a node that mixes two images? Give it inputs = ["content", "overlay"]. Each name becomes an input port on the node and a texture you can sample in the shader.

Writing the shader

DNA wraps your shader body in the boilerplate (the vertex stage, uniforms, and texture bindings) so you only write the interesting part. Your fragment entry point reads the input image and returns a colour:

// @param float brightness -1.0 1.0 0.0 "Brightness"
// @param float contrast 0.0 2.0 1.0 "Contrast"

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let color = textureSample(input_texture, tex_sampler, in.uv);
    let brightness = uniforms.params[0];
    let contrast = uniforms.params[1];

    var rgb = (color.rgb - 0.5) * contrast + 0.5 + brightness;
    return vec4<f32>(rgb, color.a);
}

A few names are always available to you:

Parameters become controls

The lines starting with // @param are what give your node its sliders, checkboxes, and colour pickers. The format is the type first, then the name, then its range and default:

// @param float strength 0.0 0.05 0.01 "Aberration strength"
// @param color tint #ff0000 "Tint colour"
// @param bool invert false "Invert output"

Each type maps to a control automatically:

You read floats by their slot in uniforms.params[N] (in declaration order) and colours from uniforms.color_0 through color_3. You get up to 32 float slots and 4 colour slots; anything past that is ignored.

Because these become normal node controls, you can keyframe them, drive them with an expression, or wire them to live input like MIDI — the same as any built-in node.

Live editing and errors

Save the shader file and DNA recompiles it on the spot — no restart. If you make a typo, the node turns red and shows the line and column of the problem in the inspector, while the last working version keeps rendering. A broken shader never crashes the app, so you can iterate freely.

Shaders run on your GPU and are checked against what your specific graphics card supports. A shader that leans on a hardware feature your GPU lacks is refused at compile time rather than misbehaving. If a node won't compile on one machine but works on another, this is usually why.

Shader plugins, like any plugin, are gated by the project's trust level. A restricted project won't load plugins that ask for extra capabilities. See Capabilities & trust.

See also