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:
plugin.toml— the manifest that declares your shader node(s).One or more shader files (
.wgslor.glsl) sitting next to it.
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:
id(required) — unique node id, e.g.custom.vignette.display_name(required) — the name shown in the palette.description— a short line about what it does.category— which palette group it lands in.keywords— comma-separated search terms so people can find it.file(required) — relative path to the shader, ending in.wgslor.glsl.language—WGSL(default) orGLSL.shader_type—Fragment(default) orCompute.inputs— named image inputs (default is a singlecontentinput). Up to four.
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:
in.uv— the pixel position, 0 to 1 across the image.input_texture— your image input (orinput_<name>when you have several), sampled withtex_sampler.uniforms.resolution— the output size in pixels,uniforms.timein seconds, anduniforms.frameas a frame count. Great for animation with no keyframing at all.
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:
float→ a slider (min max default).vec2/vec3/vec4→ per-component sliders.int→ an integer slider.bool→ a checkbox.color→ a colour picker (#rrggbb).
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.