My App

Custom Animations

Install community animations or write your own — Filament's animation bundle format, WGSL contract, and live hot-reload workflow.

Custom Animations

Filament ships with a built-in animation catalog, but it is also designed so anyone can write a new animation, drop it into the app, and have it appear next to the built-ins. This page explains how.

You do not need to be a programmer to read the first few sections — installing a shared animation is drag-and-drop. The later sections get more technical for people who want to write their own shaders, and the very last section is a compact reference an AI coding assistant can follow if you ask it to generate an animation for you.

[GRAPHIC: Screenshot of the animation library panel showing the built-in animations alongside a few user-installed animations, with the drop-zone visible at the top]

What an animation is

A Filament animation is a tiny program that draws one frame of colour. Filament runs that program many times per second, once per frame, and the result is what you see in the preview and what gets sent to your LEDs.

That program is written in WGSL (WebGPU Shading Language). You do not have to learn WGSL to use animations someone else made — drop their file into the app and pick it from the library. But if you want to make your own, WGSL is the language you write them in, and it looks roughly like a stripped-down C with a few math conveniences for graphics.

Each animation is packaged with a small description file (a "manifest") that tells Filament its name, its author, what knobs it exposes, and which group it belongs to in the picker. The animation file plus the manifest are wrapped up in a single shareable file with the extension .filament-animation.

That is the whole idea: a .filament-animation file is one animation. You can post one in a forum, attach it to an email, or download one from a friend. When you drag it onto Filament, it appears in your library and works exactly like the built-ins.

Installing a shared animation

  1. Open Filament.
  2. Drag the .filament-animation file onto the app window.
  3. The animation appears in the animation library, ready to use.

The animation stays installed across app restarts. To remove an animation, open the Library panel, find the tile, and choose Remove from its menu.

Where they live on disk

PlatformPath
macOS~/Library/Application Support/Filament/animations/user/<id>/
Windows%APPDATA%\Filament\animations\user\<id>\
Linux~/.local/share/Filament/animations/user/<id>/

If an animation failed to compile when you imported it (for example because it had a typo in the shader), it gets moved to a sibling folder called disabled/<id>/ and a plain-text file called compile-error.txt is dropped next to it with the exact error.

The shape of an animation bundle

An animation bundle is a directory with this layout:

my-animation/
├── animation.wgsl       (required) the shader program
├── manifest.json        (required) the description
├── thumbnail.png        (optional) 256×144 image shown in the picker
├── samples/             (optional) up to 8 example renders
│   ├── default.png
│   └── ...
└── LICENSE.txt          (optional) plain text license for the bundle

When you want to share it, you zip that directory and rename the result so it ends in .filament-animation instead of .zip. That is the file format.

The two required files are the only things Filament really needs. Everything else is optional polish.

Quick start — your first animation in 30 lines

The fastest way to feel how this works is to make one. We'll make a simple "breathing palette" animation that smoothly cycles through the user's selected palette, with one knob that controls how fast it breathes.

1. Make a folder

Pick a place on your disk and create a folder called breathingPalette. Inside it, make two files.

2. Write manifest.json

{
  "schemaVersion": 1,
  "id": "breathingPalette",
  "label": "Breathing Palette",
  "group": "User",
  "version": "1.0.0",
  "author": "Your Name",
  "description": "Slowly cycles the palette with a soft breathing pulse.",
  "usesPalette": true
}

3. Write animation.wgsl

// @animation breathingPalette
// @param speed type=float min=0 max=4 default=1 label="Speed" unit="Hz"
// @requires palette

struct Params {
  speed: f32,
  time: f32,
}

@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var palette_tex: texture_1d<f32>;
@group(0) @binding(2) var palette_sampler: sampler;

struct VertexOut {
  @builtin(position) position: vec4<f32>,
  @location(0) uv: vec2<f32>,
}

@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOut {
  let x = f32((vi << 1u) & 2u);
  let y = f32(vi & 2u);
  var out: VertexOut;
  out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
  out.uv = vec2<f32>(x, y);
  return out;
}

@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
  let phase = fract(params.time * params.speed * 0.1);
  let pulse = 0.5 + 0.5 * sin(params.time * params.speed);
  let color = palette_sample(phase) * pulse;
  return vec4<f32>(color, 1.0);
}

4. Try it

Drop the folder into the user animations directory listed above, then restart Filament — or use hot reload (covered below). Once it appears in your library, select it. You should see the entire canvas gently pulse through your active palette.

If you want to share it, zip the breathingPalette folder and rename the zip to breathingPalette.filament-animation.

That is the whole loop. Everything else on this page is reference detail.

manifest.json reference

The manifest is a regular JSON file. Filament will refuse to load an animation if the manifest is missing required fields, contains invalid values, or disagrees with the shader.

Fields

FieldTypeRequiredNotes
schemaVersionintegeryesUse 1. This exists so future Filament versions can evolve the format without breaking older bundles.
idstringyesA stable identifier. Camel-case, must match ^[a-z][a-zA-Z0-9]*$, no spaces or punctuation. Must match the @animation line in your .wgsl exactly.
labelstringyesThe human-readable name shown in the UI. Plain text, max 80 characters.
groupstringyesWhich section of the picker the animation appears in. See "Group values" below.
versionstringyesSemantic version like 1.0.0. Must be three dot-separated numbers.
authorstringyesFree-form attribution. Plain text, max 80 characters.
descriptionstringnoLonger description shown in tooltips and the library. Plain text, max 1000 characters.
usesPalettebooleannoDefaults to false. Set to true if your shader reads the active palette. Must agree with @requires palette in the WGSL header.
thumbnailstringnoRelative path to a thumbnail image inside the bundle. Typically thumbnail.png.
samplesstring arraynoRelative paths to up to 8 sample renders inside the bundle.

Group values

group must be exactly one of these strings:

  • "Color + Cycle" — palette-driven cycles, gradients, washes
  • "Sweeps" — moving bands and travelling waves
  • "Textures" — generative textures, noise fields, organic shapes
  • "Test Visuals" — calibration and diagnostic patterns
  • "User" — your own animations and ones imported from the community

If you are not sure, use "User". It is the default home for community animations.

Rules about text fields

label, author, description, and any parameter labels are rendered as plain text. HTML, Markdown, and special control characters are stripped or rejected. Use plain language.

Naming things well

  • id is internal and permanent — once people start using your animation, do not change it, because their saved projects refer to it. Pick something descriptive and camel-case it: oceanRipple, not OceanRipple or ocean_ripple.
  • label is what users see. Capitalize it nicely: "Ocean Ripple".
  • version follows semantic versioning. Bump the third number for fixes (1.0.01.0.1), the middle for new parameters or features, the first for changes that change the look so much that an existing user's project would look wrong.

animation.wgsl reference

The shader file has two layers:

  1. The header — a block of // comments at the top, in a fixed format, that tells Filament about the shader's parameters and requirements.
  2. The shader body — actual WGSL code that draws a frame.

The header

The header is the first contiguous run of //-prefixed lines at the top of the file. Filament stops reading the header at the first non-comment line, or at the first comment line that does not start with // @. Every header line looks like:

// @key value

Recognized keys

KeyCardinalityPurpose
@animationexactly 1Declares the animation's id. Must match manifest.json exactly.
@paramany numberDeclares a knob the user can adjust. See parameters below.
@uniformany numberReserved for future use. Parse, ignore at runtime.
@requires0 or 1Comma-separated feature flags. Currently palette is the only one.

Minimal valid header

// @animation myAnimation

That is enough. You can have zero parameters and no palette use. Most interesting animations will at least take a parameter or two and use the palette.

Full example header

// @animation oceanRipple
// @param waveSpeed     type=float min=0    max=4   default=1.0  label="Wave Speed"  unit="Hz"
// @param waveScale     type=float min=0.1  max=8   default=2.0  label="Wave Scale"
// @param brightness    type=float min=0    max=2   default=1.0  label="Brightness"  valueFormat=multiplier
// @param paletteMix    type=float min=0    max=1   default=0.5  label="Palette Mix" valueFormat=percent
// @param invert        type=bool                   default=false label="Invert"
// @param iterations    type=int   min=1    max=8   default=3    label="Iterations"
// @param accent        type=color                  default=#00aaff label="Accent"
// @param style         type=enum:warm|cool|wild    default=warm  label="Style"
// @requires palette

Parameters: types, ranges, and UI controls

Every @param declares one knob in Filament's UI for your animation. The declaration both validates the value the user picks and tells the UI how to draw the control.

Parameter syntax

// @param <id> type=<type> [type-specific attributes] default=<default> [common attributes]

Parameter types

TypeUI controlDefault attribute format
floatSliderdefault=0.5, requires min and max
intSlider with integer stepsdefault=3, requires min and max
boolToggledefault=true or default=false
colorColor pickerdefault=#rrggbb
enum:a|b|cDropdowndefault=a, choices encoded in the type

Attributes

AttributeRequired forNotes
typeallOne of the type strings above.
minfloat, intLower bound, inclusive.
maxfloat, intUpper bound, inclusive. Must be greater than min.
defaultallInitial value. Must be valid for the type and within bounds.
stepfloat, int (optional)Slider step. Defaults to 0.01 for float, 1 for int.
labeloptionalHuman-readable name on the slider. Defaults to the id with each word capitalized.
unitoptionalShort suffix like Hz, °, %. Max 16 characters.
valueFormatoptionalOne of fixed, percent, degrees, hertz, multiplier, integer. Controls how the number is displayed next to the slider. Defaults are inferred from unit.

Examples by type

// Float slider, 0.0 to 1.0, default 0.5, fancier number format
// @param mix         type=float min=0 max=1 default=0.5 label="Mix" valueFormat=percent

// Integer slider, 1 to 8, default 3
// @param iterations  type=int   min=1 max=8 default=3   label="Iterations"

// Boolean toggle
// @param invert      type=bool                  default=false label="Invert"

// Color picker
// @param accent      type=color                 default=#ff8800 label="Accent"

// Enum dropdown
// @param style       type=enum:warm|cool|wild   default=warm  label="Style"

Parameter id rules

  • Lowercase-camel, must match ^[a-z][a-zA-Z0-9]*$.
  • Unique within the file.
  • Will appear in your shader's Params struct under exactly this name.

The runtime contract

Filament's shader runtime guarantees a fixed set of bindings and conventions. Your shader must match them.

Bind group layout

Every animation shader uses bind group 0 with these three bindings:

@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var palette_tex: texture_1d<f32>;
@group(0) @binding(2) var palette_sampler: sampler;

You write these lines verbatim. The runtime fills them in. If usesPalette is false in your manifest, the runtime still provides the palette bindings; it just binds a neutral 1×1 palette.

The Params struct

Your Params struct must list every @param you declared, in the same order they appear in the header, then a final time: f32 field. WGSL maps types this way:

@param typeWGSL field type
floatf32
inti32
boolu32 (write 0 for false, 1 for true)
colorvec3<f32> (RGB in 0..1) — followed by an f32 padding field
enum:a|b|cu32 (index of the chosen tag, starting at 0)

The runtime appends a time: f32 field at the end, after all your parameters. The order matters. If you reorder fields the bytes will not line up and your shader will display nonsense.

Minimal Params

struct Params {
  speed: f32,
  time: f32,
}

Realistic Params

For a header with speed: float, iterations: int, invert: bool, and accent: color:

struct Params {
  speed: f32,
  iterations: i32,
  invert: u32,
  accent: vec3<f32>,
  accent_pad: f32,
  time: f32,
}

The vertex shader

Filament does not provide a vertex shader for you. You write your own, but it is the same fullscreen triangle in every animation. The canonical version is:

struct VertexOut {
  @builtin(position) position: vec4<f32>,
  @location(0) uv: vec2<f32>,
}

@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOut {
  let x = f32((vi << 1u) & 2u);
  let y = f32(vi & 2u);
  var out: VertexOut;
  out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
  out.uv = vec2<f32>(x, y);
  return out;
}

uv ranges from (0,0) at the top-left to (1,1) at the bottom-right of the canvas. Most animations work in this space.

The fragment shader

The fragment shader must be named fs_main, take a VertexOut, and return a vec4<f32> colour at @location(0):

@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
  // ...your code...
  return vec4<f32>(r, g, b, 1.0);
}

The output format is Rgba16Float, so you can write values outside 0..1 without clipping. The downsample-and-output stage clamps to display range, but high-dynamic-range intermediate values are fine and sometimes useful for bloom-like effects.

What you cannot do

  • No file includes or imports
  • No external network access
  • No reading or writing files
  • No persistent state across frames (use time and re-derive)
  • No reading from outside your bind group

These are not just policy — Filament refuses to compile shaders that try.

Using the global palette

The user picks a palette in Filament's main UI. If your shader sets @requires palette and the manifest has "usesPalette": true, your shader gets access to the currently-active palette as a 1-D texture, plus a built-in helper:

let color = palette_sample(t);

t is a float; palette_sample wraps it to [0, 1) and returns an RGB vec3. Use it any time you want to colour something with the user's chosen scheme:

let hue_position = uv.x + params.time * 0.2;
let color = palette_sample(hue_position);
return vec4<f32>(color, 1.0);

The user can change the palette at any time and your animation picks it up on the next frame. This is the right way to make your animation feel like a member of Filament rather than a stranger glued on — let the user dress it.

Opting out of the palette

If you want fixed colours and do not want the user's palette to affect your animation, omit @requires palette and set "usesPalette": false (or omit the field — it defaults to false). You can still declare palette_tex and palette_sampler if you like; they will be bound to a neutral 1×1 texture and ignored.

Master effects: what happens after your shader runs

Filament has master effects that run on top of every animation — trails, hue rotation, and the global brightness/crossfade controls described on the Live Player page.

These are controlled by global knobs in the player UI, not by individual animations. You do not implement them. Your shader writes a single frame of colour, and the master-effects pass takes care of trails and hue afterwards.

Two practical consequences:

  1. Do not bake trails or hue rotation into your shader. The user already has them as global knobs; doing your own will fight with theirs.
  2. Your shader's output is in linear RGB. The master-effects pass operates on that linear value before it is sent to the LEDs or shown in the preview.

Helper functions (the prelude)

Filament automatically prepends a small set of helper functions to every animation. You can call them without declaring anything. They make common patterns (noise, rotation, hue conversion) painless.

FunctionWhat it does
wrap01(value)Wraps a float to [0, 1)
smoothstep_unit(value)Smooth Hermite interpolation in [0, 1]
hash2d(p)Cheap deterministic hash of a vec2<f32>
value_noise(p)Smooth 2D value noise
fbm_noise(p)Fractional Brownian motion, multi-octave noise
rotate_centered(uv, degrees)Rotates a UV around (0.5, 0.5)
glow_band(distance, radius, softness)Smooth glow ring around a radius
wrapped_band(position, center, width, fuzziness)A soft band that wraps around [0, 1)
hsv_to_rgb(h, s, v)HSV to RGB conversion
distance_to_segment(p, a, b)2D point-to-line-segment distance
add_color(a, b)Saturating colour add
palette_sample(t)Reads the active palette (only useful with usesPalette: true)

Both snake_case and camelCase versions of each helper exist, so call whichever feels natural. The constant TAU (= 2π) is also available.

You can read the prelude in full at src-tauri/resources/animations/_prelude.wgsl in the source tree, or by opening any built-in animation that uses these helpers.

Sharing your animation

When you are ready to share:

  1. Make sure your folder has at least animation.wgsl and manifest.json.
  2. Optionally add thumbnail.png (256×144 recommended, max 1024×576).
  3. Optionally add a samples/ subfolder with up to 8 example renders (max 1280×720 each).
  4. Optionally add LICENSE.txt with your terms.
  5. Zip the contents of the folder (not the folder itself — the manifest.json should be at the root of the zip).
  6. Rename the resulting .zip to .filament-animation.

On macOS:

cd path/to/my-animation
zip -r ../my-animation.filament-animation .

On Windows PowerShell:

Compress-Archive -Path .\my-animation\* -DestinationPath .\my-animation.zip
Rename-Item .\my-animation.zip .\my-animation.filament-animation

Iterating quickly with hot reload

Filament watches the user animation directory for changes. While Filament is running, if you edit animation.wgsl or manifest.json of an installed user animation and save the file, Filament re-parses and re-compiles the bundle within a fraction of a second. The preview switches over without dropping any master-effects history (trails keep their decay state across the swap).

The natural development loop:

  1. Drop your bundle into the user animations directory once.
  2. Open it in Filament and select it.
  3. Edit animation.wgsl in your editor.
  4. Save.
  5. Watch the preview update.

If you introduce a compile error, Filament keeps the previous version active and shows the error in a toast. Fix the error, save again, and the new version takes over.

Manifest-only edits (renaming the label, tweaking a parameter range) reload without rebuilding the shader pipeline, so they are essentially instant. WGSL edits and parameter-shape changes recompile the shader; expect ~10–100 ms.

Limits and safety rules

Filament treats every .filament-animation file as untrusted input, even if you wrote it yourself. Filament validates a bundle before it is installed and refuses ones that violate any of these:

Structural rules

  • The only allowed entries are animation.wgsl, manifest.json, thumbnail.png, samples/*.png, and LICENSE.txt.
  • No path traversal (.., absolute paths, backslashes, Windows drive prefixes), no symlinks, no hardlinks, no encrypted entries, no nested archives.
  • No more than 32 entries in the archive.
  • No duplicate paths (including case-insensitive duplicates).

Size limits

ItemLimit
Total uncompressed contents5 MB
animation.wgsl256 KB
manifest.json64 KB
thumbnail.png1 MB, max 1024×576
Each samples/*.png1 MB, max 1280×720
Total sample images8
Compression ratioAny entry whose uncompressed size is more than 100× its compressed size is rejected.

Content rules

  • Plain text everywhere: no HTML, no Markdown, no control characters other than normal whitespace.
  • label, author, parameter label: max 80 characters each.
  • description: max 1000 characters.
  • Parameter unit: max 16 characters.
  • Enum tag: max 32 characters.
  • All images must be valid, non-interlaced, non-animated PNGs.

Shader rules

  • WGSL only (no GLSL, no SPIR-V).
  • No #include or external file references.
  • Must compile with naga (the WGSL frontend used by both WebGPU and the Rust runtime).
  • Pathological shaders that run too slowly are not blocked, but if compilation fails the bundle is moved to a disabled/ folder with a compile-error.txt and is not loaded on next launch until you fix it and move it back.

Identity rules

  • Your id cannot match a built-in animation's id. If you try, the import is rejected with a clear message — rename your bundle in manifest.json and try again.
  • If you import a bundle whose id matches an existing user animation, Filament asks "replace?" and waits for your confirmation.

These limits are deliberately strict. If a real community bundle ever needs more, the limits will be raised.

Troubleshooting

"An animation named X already ships with Filament"

You picked an id that collides with a built-in. Open manifest.json, change id to something unique, and update the matching @animation line in the .wgsl. The two must always agree.

"Compile failed"

Your WGSL has a syntax or semantic error. Filament moves the bundle to disabled/<id>/ and writes compile-error.txt next to it with the exact WGSL compiler message and offending line. Fix the file, then either:

  • Move the bundle back to animations/user/<id>/ (Filament picks it up on next launch), or
  • Re-import the corrected .filament-animation file.

If you are editing in place and using hot reload, just save the fixed file — the next save attempt is treated independently of the disabled state.

Colours look completely wrong / nothing draws

Most likely your Params struct does not match your @param declarations. The order must be identical and time: f32 must come last, after all your parameters. Even a single field out of place will shift every other field's bytes.

Check also:

  • color parameters take a vec3<f32> plus a trailing f32 padding field in the struct.
  • bool parameters are u32 in the struct (0 for false, 1 for true).
  • enum parameters are u32 (the zero-based index of the chosen tag).

"Manifest and shader disagree on palette"

Your manifest says "usesPalette": true but your shader does not declare // @requires palette (or vice versa). Make the two agree.

Preview is black

  • If time is wrong, many shaders that animate produce a black frame at time = 0. Try moving the seek bar.
  • If you read the palette but your active Filament palette is all black, the output will be black. Switch palettes.
  • If you accidentally returned vec4<f32>(0.0, 0.0, 0.0, 1.0), fix the math.

"The animation lags / stutters"

The most common cause is doing too much work per pixel in a long loop. WGSL loops are unrolled by the GPU only when their bounds are constant; loops with high iterations parameter values run a lot of work per pixel. Cap the iteration count, or do the heavy work less often.

Worked example: a second animation

For reference, here is a complete, working bundle that uses several parameter types, the palette, and a couple of prelude helpers. It draws a noisy field that warps and tints over time.

manifest.json:

{
  "schemaVersion": 1,
  "id": "warpField",
  "label": "Warp Field",
  "group": "Textures",
  "version": "1.0.0",
  "author": "Example",
  "description": "Warping noise field tinted by the active palette.",
  "usesPalette": true,
  "thumbnail": "thumbnail.png"
}

animation.wgsl:

// @animation warpField
// @param speed       type=float min=0   max=4 default=1.0 label="Speed"     unit="Hz"
// @param scale       type=float min=0.5 max=8 default=2.5 label="Scale"
// @param warpAmount  type=float min=0   max=2 default=0.6 label="Warp"
// @param brightness  type=float min=0   max=2 default=1.0 label="Brightness" valueFormat=multiplier
// @param invert      type=bool                 default=false label="Invert"
// @requires palette

struct Params {
  speed: f32,
  scale: f32,
  warpAmount: f32,
  brightness: f32,
  invert: u32,
  time: f32,
}

@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var palette_tex: texture_1d<f32>;
@group(0) @binding(2) var palette_sampler: sampler;

struct VertexOut {
  @builtin(position) position: vec4<f32>,
  @location(0) uv: vec2<f32>,
}

@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOut {
  let x = f32((vi << 1u) & 2u);
  let y = f32(vi & 2u);
  var out: VertexOut;
  out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
  out.uv = vec2<f32>(x, y);
  return out;
}

@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
  let t = params.time * params.speed;
  let p = in.uv * params.scale;

  let warp = vec2<f32>(
    fbm_noise(p + vec2<f32>(t * 0.1, 0.0)) - 0.5,
    fbm_noise(p + vec2<f32>(0.0, t * 0.1)) - 0.5,
  ) * params.warpAmount;

  let field = fbm_noise(p + warp + vec2<f32>(t * 0.05, -t * 0.07));
  var sample_t = field;
  if (params.invert == 1u) {
    sample_t = 1.0 - field;
  }

  let color = palette_sample(sample_t) * params.brightness;
  return vec4<f32>(color, 1.0);
}

Save the two files into a folder called warpField, zip the contents, rename to warpField.filament-animation, and drop it on Filament.


Reference card for AI agents and frameworks

This section is a compact spec for tools generating animation bundles programmatically. Everything in it is also covered above in prose form.

Bundle requirements

  • Directory or zip with extension .filament-animation.
  • Required files at the root: animation.wgsl, manifest.json.
  • Optional: thumbnail.png, samples/*.png (≤ 8), LICENSE.txt.
  • No other files, no subdirectories beyond samples/, no traversal, no symlinks.
  • Hard limits: 32 entries, 5 MB total uncompressed, 256 KB WGSL, 64 KB manifest, 1 MB per image, compression ratio ≤ 100:1.

Manifest schema (manifest.json)

{
  "schemaVersion": 1,
  "id": "string, ^[a-z][a-zA-Z0-9]*$, must equal @animation",
  "label": "string, ≤80 chars, plain text",
  "group": "Color + Cycle | Sweeps | Textures | Test Visuals | User",
  "version": "semver string, e.g. 1.0.0",
  "author": "string, ≤80 chars",
  "description": "string, optional, ≤1000 chars",
  "usesPalette": false,
  "thumbnail": "thumbnail.png",
  "samples": ["samples/example.png"]
}

WGSL header grammar

The first contiguous run of //-prefixed lines at the top of the file is the header. One annotation per line. Parsing stops at the first non-@ comment or non-comment line.

// @animation <id>
// @param <id> type=<type> [min=<n> max=<n>] default=<value> [step=<n>] [label="..."] [unit="..."] [valueFormat=<fmt>]
// @uniform <id> type=<type>           (reserved; round-trip only)
// @requires palette                   (allowlist; only "palette" today)

<type>float | int | bool | color | enum:<tag>|<tag>....

<fmt>fixed | percent | degrees | hertz | multiplier | integer.

Parameter id pattern: ^[a-z][a-zA-Z0-9]*$. Unique within the file. min < max for numeric types. default must satisfy the type and bounds.

Params struct contract

The shader must declare a Params struct whose fields appear in the exact declaration order of @param entries, then a final time: f32. Type mapping:

@param typeWGSL field
floatf32
inti32
boolu32
colorvec3<f32> followed by f32 padding
enum:...u32 (zero-based tag index)

Time is always appended as time: f32 last. Struct is uniform-buffer aligned per WGSL std140-like rules.

Bind group layout

@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var palette_tex: texture_1d<f32>;
@group(0) @binding(2) var palette_sampler: sampler;

Always group 0, always these three bindings. The palette bindings are present even if usesPalette is false (they get a neutral fallback).

Vertex / fragment contract

  • Vertex entry point name: free, but the canonical name is vs_main. Must emit positions from @builtin(vertex_index) (no vertex buffer is bound).
  • Fragment entry point name: fs_main.
  • Fragment returns vec4<f32> at @location(0).
  • Render target format is Rgba16Float; HDR intermediate values are allowed.
  • The runtime draws three vertices as a single fullscreen triangle.

Prelude (auto-prepended; both built-in and user shaders)

Available without declaration:

  • Constant: TAU (= 2π).
  • Functions (snake_case and camelCase aliases for each): wrap01, smoothstep_unit, hash2d, value_noise, fbm_noise, rotate_centered, glow_band, wrapped_band, hsv_to_rgb, checkerboard_secondary, distance_to_segment, add_color.
  • Palette helpers (always present): palette_sample(t: f32) -> vec3<f32>, paletteSample(t: f32) -> vec3<f32>. Sampling uses wrap01(t).

Failure modes

ConditionResult
Manifest invalid / required field missingImport rejected, toast.
id collides with a built-inImport rejected, toast.
id collides with an existing user bundleUI prompts for replace/cancel.
Any security rule violationImport rejected, no files written.
WGSL fails to compileBundle moved to disabled/<id>/ with compile-error.txt. Previous animation stays active.
Manifest disagrees with WGSL (id mismatch, palette mismatch)Import rejected.

Filesystem locations

PlatformUser animations
macOS~/Library/Application Support/Filament/animations/user/
Windows%APPDATA%\Filament\animations\user\
Linux~/.local/share/Filament/animations/user/

Each bundle lives under <root>/<id>/. Failed bundles live under <root>/disabled/<id>/.

Minimal valid bundle

manifest.json:

{
  "schemaVersion": 1,
  "id": "minimal",
  "label": "Minimal",
  "group": "User",
  "version": "1.0.0",
  "author": "Author"
}

animation.wgsl:

// @animation minimal

struct Params {
  time: f32,
}

@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var palette_tex: texture_1d<f32>;
@group(0) @binding(2) var palette_sampler: sampler;

struct VertexOut {
  @builtin(position) position: vec4<f32>,
  @location(0) uv: vec2<f32>,
}

@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOut {
  let x = f32((vi << 1u) & 2u);
  let y = f32(vi & 2u);
  var out: VertexOut;
  out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
  out.uv = vec2<f32>(x, y);
  return out;
}

@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
  let r = 0.5 + 0.5 * sin(params.time);
  return vec4<f32>(r, in.uv.x, in.uv.y, 1.0);
}

That bundle compiles, imports, and runs. Use it as a starting point for any generated animation.

On this page