Add in.dt, slider warnings in --check, and two agent-written effects

Spec fixes come from a test where pi + deepseek-v4.1-flash wrote effects
from EFFECTS.md alone: glitch-blocks and ghost-stamps both compiled on the
first try. The agent's feedback led to in.dt (frame time), documented
limits of in.prev as memory, and a --check warning for ignored slider lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rasyidan Akbar F. 2026-10-05 20:50:31 +07:00
commit 169c5f9b63
6 changed files with 111 additions and 9 deletions

View file

@ -13,7 +13,7 @@ float4 effect(float2 uv, Inputs in) {
```
- `uv` is the pixel's position: `(0,0)` is top-left, `(1,1)` is bottom-right.
- Return `float4(r, g, b, 1.0)` with components in 0..1.
- Return `float4(r, g, b, 1.0)` with components in 0..1. Alpha is not shown on screen; return 1.0 unless you use it as memory (see below).
- Do not write `#include`, `using namespace`, a vertex function or a fragment function. The app adds those.
## What `in` gives you
@ -24,10 +24,11 @@ float4 effect(float2 uv, Inputs in) {
| `in.camera` | texture | The untouched camera frame. |
| `in.last` | texture | The camera frame before this one. `length(camera - last)` at a pixel tells you how much it changed. |
| `in.prev` | texture | This effect's own output from the previous frame. Read it to build trails and feedback. |
| `in.flow` | texture | `.xy` = which way and how far the picture moved at this spot since the last frame, in `uv` units. Typically tiny (0.001 to 0.02). |
| `in.flow` | texture | `.xy` = which way and how far the picture moved at this spot since the last frame, in `uv` units. Typically tiny (0.001 to 0.02): `length(flow) * 50.0` is a rough 0..1 "how fast" value. |
| `in.particles` | texture | `.r` = brightness of particles that spawn where something moves and drift along with the motion. |
| `in.smp` | sampler | Pass it to every `.sample()` call. |
| `in.time` | float | Seconds since the app started. |
| `in.dt` | float | Seconds since the previous frame. Effects run once per camera frame, so this is about 1/30. Use it to make fades and timers last the same in real time whatever the frame rate: `value *= exp(-in.dt / seconds)`. |
| `in.resolution` | float2 | Output size in pixels. One pixel is `1.0 / in.resolution` in `uv` units. |
| `in.p.<name>` | float | Your sliders (see below). |
@ -44,7 +45,7 @@ Put these at the top of the file. Spacing matters: one space between the parts,
```
- `// slider glow 0.0 4.0 1.5` adds a slider; read its value as `in.p.glow`. Up to 16 per effect.
- `// order 60` sets the position in the chain. Lower runs first; the default is 50. Existing effects use 10 to 90.
- `// order 60` sets the position in the chain. Any whole number; only the relative order matters, lower runs first, and the default is 50. Existing effects use 10 to 90. The motion inputs (`flow`, `particles`) are computed from the camera before any effect runs.
- `// on` makes the effect enabled when the app starts. Leave it out for new effects.
Slider names must be plain identifiers and must not be Metal built-ins or keywords (`mix`, `length`, `in`, `float`, `step`, `sample` and so on). `amount`, `speed`, `glow`, `hue` are fine.
@ -61,7 +62,8 @@ Slider names must be plain identifiers and must not be Metal built-ins or keywor
- Write float numbers with a decimal point: `2.0`, not `2`. Convert loop counters with `float(i)`.
- Helper functions go above `effect`, must be marked `static`, and take `Inputs in` as a parameter if they read textures.
- There is no global state. To remember something between frames, write it into your output and read it back from `in.prev`. The alpha channel (`.a`) of your output is free for this: it is not shown on screen.
- There is no global state. To remember something between frames, write it into your output and read it back from `in.prev`. Its `.rgb` is the picture you showed last frame; its alpha channel (`.a`) is free for your own use because it is not shown on screen. That gives one private number per pixel, stored with 8-bit precision (256 steps between 0 and 1), which is enough for a glow or heat level but not for a timestamp or a full-colour image.
- To do something every N seconds, test where `in.time` is inside the current period: `fract(in.time / N) < in.dt / N` is true for exactly one frame per period.
## Two complete examples
@ -112,12 +114,12 @@ swift build -c release
.build/release/MotionLab --only <name> --snapshot /tmp/<name>.png
```
`--check` also lists the sliders it found for each effect. If one of yours is missing, its `// slider` line is malformed.
`--check` also lists the sliders it found for each effect, and warns when a `// slider` line was ignored because it is malformed or over the limit of 16.
## Checklist
- The file is `effects/<name>.metal` and defines `float4 effect(float2 uv, Inputs in)`.
- It reads its input picture from `in.image`, not `in.camera`, so it works in a chain.
- Every number a user might want to tune is a slider with a sensible range and default.
- The few numbers a user would most want to tune are sliders with a sensible range and default. Internal constants can stay in the code.
- The first line is a comment saying what the effect does.
- `.build/release/MotionLab --check` prints `ok` for it.