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
- Open Filament.
- Drag the
.filament-animationfile onto the app window. - 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
| Platform | Path |
|---|---|
| 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 bundleWhen 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
| Field | Type | Required | Notes |
|---|---|---|---|
schemaVersion | integer | yes | Use 1. This exists so future Filament versions can evolve the format without breaking older bundles. |
id | string | yes | A 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. |
label | string | yes | The human-readable name shown in the UI. Plain text, max 80 characters. |
group | string | yes | Which section of the picker the animation appears in. See "Group values" below. |
version | string | yes | Semantic version like 1.0.0. Must be three dot-separated numbers. |
author | string | yes | Free-form attribution. Plain text, max 80 characters. |
description | string | no | Longer description shown in tooltips and the library. Plain text, max 1000 characters. |
usesPalette | boolean | no | Defaults to false. Set to true if your shader reads the active palette. Must agree with @requires palette in the WGSL header. |
thumbnail | string | no | Relative path to a thumbnail image inside the bundle. Typically thumbnail.png. |
samples | string array | no | Relative 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
idis 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, notOceanRippleorocean_ripple.labelis what users see. Capitalize it nicely:"Ocean Ripple".versionfollows semantic versioning. Bump the third number for fixes (1.0.0→1.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:
- The header — a block of
//comments at the top, in a fixed format, that tells Filament about the shader's parameters and requirements. - 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 valueRecognized keys
| Key | Cardinality | Purpose |
|---|---|---|
@animation | exactly 1 | Declares the animation's id. Must match manifest.json exactly. |
@param | any number | Declares a knob the user can adjust. See parameters below. |
@uniform | any number | Reserved for future use. Parse, ignore at runtime. |
@requires | 0 or 1 | Comma-separated feature flags. Currently palette is the only one. |
Minimal valid header
// @animation myAnimationThat 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 paletteParameters: 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
| Type | UI control | Default attribute format |
|---|---|---|
float | Slider | default=0.5, requires min and max |
int | Slider with integer steps | default=3, requires min and max |
bool | Toggle | default=true or default=false |
color | Color picker | default=#rrggbb |
enum:a|b|c | Dropdown | default=a, choices encoded in the type |
Attributes
| Attribute | Required for | Notes |
|---|---|---|
type | all | One of the type strings above. |
min | float, int | Lower bound, inclusive. |
max | float, int | Upper bound, inclusive. Must be greater than min. |
default | all | Initial value. Must be valid for the type and within bounds. |
step | float, int (optional) | Slider step. Defaults to 0.01 for float, 1 for int. |
label | optional | Human-readable name on the slider. Defaults to the id with each word capitalized. |
unit | optional | Short suffix like Hz, °, %. Max 16 characters. |
valueFormat | optional | One 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
Paramsstruct 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 type | WGSL field type |
|---|---|
float | f32 |
int | i32 |
bool | u32 (write 0 for false, 1 for true) |
color | vec3<f32> (RGB in 0..1) — followed by an f32 padding field |
enum:a|b|c | u32 (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
timeand 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:
- 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.
- 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.
| Function | What 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:
- Make sure your folder has at least
animation.wgslandmanifest.json. - Optionally add
thumbnail.png(256×144 recommended, max 1024×576). - Optionally add a
samples/subfolder with up to 8 example renders (max 1280×720 each). - Optionally add
LICENSE.txtwith your terms. - Zip the contents of the folder (not the folder itself — the
manifest.jsonshould be at the root of the zip). - Rename the resulting
.zipto.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-animationIterating 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:
- Drop your bundle into the user animations directory once.
- Open it in Filament and select it.
- Edit
animation.wgslin your editor. - Save.
- 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, andLICENSE.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
| Item | Limit |
|---|---|
| Total uncompressed contents | 5 MB |
animation.wgsl | 256 KB |
manifest.json | 64 KB |
thumbnail.png | 1 MB, max 1024×576 |
Each samples/*.png | 1 MB, max 1280×720 |
| Total sample images | 8 |
| Compression ratio | Any 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, parameterlabel: 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
#includeor 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 acompile-error.txtand is not loaded on next launch until you fix it and move it back.
Identity rules
- Your
idcannot match a built-in animation's id. If you try, the import is rejected with a clear message — rename your bundle inmanifest.jsonand try again. - If you import a bundle whose
idmatches 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-animationfile.
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:
colorparameters take avec3<f32>plus a trailingf32padding field in the struct.boolparameters areu32in the struct (0for false,1for true).enumparameters areu32(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
timeis wrong, many shaders that animate produce a black frame attime = 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 type | WGSL field |
|---|---|
float | f32 |
int | i32 |
bool | u32 |
color | vec3<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 useswrap01(t).
Failure modes
| Condition | Result |
|---|---|
| Manifest invalid / required field missing | Import rejected, toast. |
id collides with a built-in | Import rejected, toast. |
id collides with an existing user bundle | UI prompts for replace/cancel. |
| Any security rule violation | Import rejected, no files written. |
| WGSL fails to compile | Bundle moved to disabled/<id>/ with compile-error.txt. Previous animation stays active. |
| Manifest disagrees with WGSL (id mismatch, palette mismatch) | Import rejected. |
Filesystem locations
| Platform | User 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.