warp_mesh — the past, resampled through a per-vertex grid
One transform per vertex, where trails gives you one for the whole frame.
system = "warp_mesh" covers the frame with a grid of cells and resamples the
previous frame through it, giving each grid vertex its own zoom, rotation,
stretch and drift. The rasterizer interpolates between them, so the past can
spiral in one corner and drift in another — which no single affine can express
and which is why this exists beside the fb_* vocabulary rather than inside it
(ADR-0113,
generalizing ADR-0048).
It is also the only scene here with nothing to draw of its own when you author it by hand. Every other system paints a figure; this one paints light into a feedback field and then moves the field. Turn the deposit off and the frame goes black in about a second.
A converted MilkDrop preset is the exception, and it is not one you can
write: it brings its own draw layer — a waveform, custom waves and shapes,
borders, a motion grid — and turns the deposit off. See
[milk] below.
system = "warp_mesh"
[mesh]x = 32 # grid cells across; clamped to the tier (see below)y = 24
[per_vertex] # evaluated ONCE PER VERTEX, with x/y/rad/ang boundzoom = "1.9 + rad * 0.9" # the centre is pulled harder than the rimrot = "0.35 + sin(ang) * 0.30"
[params]decay = "0.06" # fraction of the past surviving per SECONDdeposit = "7 + clamp(mid * 4, 0, 3.5)"deposit_radius = "0.30" # a ring; 0 is a blob at the centredeposit_width = "0.03"deposit_arms = "6"deposit_twist = "2.4"deposit_spin = "0.5"The [per_vertex] table
Its bindings are evaluated once per mesh vertex per frame, with four extra
variables in scope that read 0 everywhere else:
| variable | is |
|---|---|
x / y | the vertex’s position in 0..1, y = 0 at the top |
rad | its distance from the centre, 1.0 at the middle of the top and bottom edges on any display |
ang | its angle there, 0..tau, counter-clockwise from the +x axis as you look at the screen |
rad and ang are aspect-corrected against the render target, never against
the mesh: zoom = 1 + rad * 0.2 draws the same circular gradient on a 16:9
monitor and on a 5:4 one. Only [per_vertex] binds them — a [params] binding
naming rad gets a load warning and reads zero.
The table accepts exactly the nine names below, and each is also an ordinary
scalar param. That is the composition rule: a scalar sets the output for the
whole mesh, and a [per_vertex] binding of the same name replaces it vertex by
vertex. Bind none of them and you have the single shared transform; bind one and
only that one varies.
| output | does (all rates are per second) |
|---|---|
zoom | scale factor. Above 1 the past expands outward — the classic tunnel. Below 1 it collapses inward. 1 is still |
rot | radians per second the past turns about (cx, cy) |
cx / cy | the fixed point everything else turns about, in uv. 0.5, 0.5 is the middle |
dx / dy | drift, in frame-heights per second, so the vocabulary is isotropic |
sx / sy | axis stretch factors, per second, applied before the rotation |
warp | strength of the procedural wobble — four sinusoids whose own frequencies drift, so it never settles into a visible standing pattern. Inert at 0 |
Two more scalars shape that wobble and are not per-vertex, because they are properties of the pattern rather than of a vertex:
| param | does |
|---|---|
warp_scale | the wobble’s spatial scale. Larger is coarser. Default 1 |
warp_speed | how fast its frequencies drift. Default 1 |
warp_speed integrates a phase (ADR-0132),
the same shape fragment_field’s
field_speed / fold_speed
take — so it is safe to bind to audio. A rate that instead multiplied the shared
clock would move the warp’s phase by fifty seconds in one frame on a swing from
1 to 1.5 a hundred seconds into a set: a teleport, not an acceleration.
A [smoothing] entry naming a [per_vertex] binding is ignored, with a
warning — the same rule a per-element index binding takes, and for the same
reason: a series has no single value for the smoother to hold. Ease the scalar
instead, or ease something the expression reads.
The field, and what feeds it
| param | does |
|---|---|
decay | fraction of the past surviving per second. 0.05 leaves a quarter-second tail; 0.5 leaves a second and a half; clamped just under 1, because at exactly 1 the field is a perfect integrator and any deposit whitens the frame without bound. Default 0.72 |
deposit | light laid down per second. 0 and the frame goes black. Default 1.6 |
deposit_x / deposit_y | where, in uv. Default 0.5, 0.5 |
deposit_radius | the ring’s radius in frame-heights. 0 degenerates to a soft blob at the centre. Default 0.45 |
deposit_width | the ring’s gaussian sigma, in frame-heights. Default 0.11 |
deposit_arms | angular lobes around the ring. Below 0.5 the modulation is off entirely rather than a degenerate single lobe. Default 0 |
deposit_twist | how much the arms spiral with radius. Default 0 |
deposit_spin | how fast they turn, in radians a second. Integrates a phase (ADR-0132), like warp_speed above and for the same reason, so binding it to audio bends the arms’ rotation instead of teleporting it. Default 0 |
The deposit is coloured by angle through the shared palette, so the shared
colour vocabulary applies as it does everywhere else: hue, color_span,
color_center, saturation, palette_mix, palette_steps, palette_contour,
and brightness scaling the whole present.
MilkDrop’s composite roster
Ten more, and they are here because they are what most of the MilkDrop library
is made of rather than because the idiom needed them. Each is a flag in the source
format and stays one here — the four remaps are one select apiece in the present
shader, so a preset that binds none of them pays nothing.
| param | does |
|---|---|
wrap | past 0.5, the past is toroidal: content that a zoom pulls off one edge comes back on the other. This is what most of the classic tunnels are made of. Off — the default — an off-field read contributes nothing, which is the transparent-border policy trails takes |
gamma | multiplies the light on the way out. Default 1 |
darken_center | darkens a soft disc at the middle, in frame-heights so it is round on any display. What stops a zooming feedback loop saturating there. A fraction gives a proportional amount, which the source format cannot express |
brighten | past 0.5, sqrt of the light — lifts the shadows |
darken | past 0.5, the light squared |
solarize | past 0.5, c * (1 - c) * 4 — the classic inversion around mid-grey |
invert | past 0.5, 1 - c |
echo_alpha | how far the present blends toward a second sampled copy of the finished frame. 0 — the default — is the exact identity and costs one uniform branch; 1 is the copy alone, with the base frame gone |
echo_zoom | how far that copy is zoomed, about the frame centre. 1 is the same size. Default 1 |
echo_orient | how it is flipped: 0 none, 1 left-right, 2 top-bottom, 3 both. Rounded to the nearest of the four, so a smoothed or computed value never lands between them. Out of range wraps, so a preset that animates the orientation by counting gets a cycle. Default 0 |
The four remaps operate on linear light here, where MilkDrop applied them to 8-bit display-referred pixels. They are the same gesture, not the same arithmetic:
brighten’s square root lifts the shadows either way, by a different amount. That difference is the whole reason ADR-0046’s pipeline is interesting rather than merely compatible, and it is stated here so a converted preset that looks different is not mistaken for one that looks wrong.
The echo is the one member of that roster that is not a remap, and it is the one that reaches for a second sample rather than reshaping the first. It blends toward that copy rather than adding it (ADR-0119), so it can never brighten the frame past what the frame already holds: at
echo_alpha = 1,echo_orient = 1,echo_zoom = 1you get the picture mirrored left-right, not the picture plus its mirror. Summing was tried first and washed Songflower out — a big soft magnified duplicate laid over a crisp lattice is a contrast destroyer.It reads the field as the present pass finds it — after the warp, the deposit and the draw layer — and writes nothing back, so it does not accumulate: what the next frame warps is untouched by it. That is also what makes the mirror above exact rather than approximate, since both arms evolve the same field.
The deposit is laid down after the warp, deliberately. Its light is “now”, so it is crisp on the frame it appears and warped from the next frame onward. Deposit before warping and every stroke would be smeared on arrival.
The [mesh] table, and why the grid is capped
x and y are the number of cells, 2..128 and 2..96 — the ceiling
is the .milk format’s own, so a converted MilkDrop preset’s grid is always
representable. Absent keys mean 32 x 24.
The grid is a resolution, not a shape
(ADR-0037):
raising it refines how finely the per-vertex program is sampled and changes
nothing about what the scene draws, and no coordinate anywhere in the scene takes
its aspect from it. It is also a tier capacity, because every vertex costs one
evaluation of every [per_vertex] binding on the render thread — 64 x 48 on the
floor tier and 88 x 66 on rich, measured rather than chosen (the ladder and the
numbers are on TierConfig::mesh_grid). A preset asking for more gets the tier’s
ceiling; it is not an error, and the request survives so the same file renders
finer on a better machine.
A coarse grid shows itself where the transform varies fast. The interpolation between vertices is linear, so a program with a sharp feature in it —
zoom = 1 + (rad > 0.5) * 2— renders that feature as a polygon of the grid rather than as a circle. Either raise[mesh], or write the feature as something smooth:smoothstepoverradcosts nothing extra and has no vertices to show.
[milk] — the table you do not write
A converted MilkDrop preset carries a fourth table, and it is the one part of this file’s vocabulary that is not for you:
[milk]per_frame = """.regs q1 zoom bass time.codeconst 1.024..."""
[[milk.shapes]] # up to four, and as many [[milk.waves]]count = 7 # sides (a shape) or points (a wave)instances = 3per_frame = """..."""That is compiled EEL2 bytecode, emitted ahead of time by milkconv and executed
by a small stack VM in the engine
(ADR-0113).
Three things about it are worth knowing even though you will not author one:
-
A bundle is authoritative about the transform. Its programs run after
[params]and[per_vertex], so bindingzoomalongside a[milk]table is inert rather than a conflict. What a converted preset does still take from[params]is everything the bundle has no opinion about — the palette and the compositing stages. -
The rates inside it are MilkDrop’s, per frame. The runtime converts them to this engine’s per-second vocabulary at a nominal 30 fps, which is what makes a converted preset move at the speed its author saw on any display. So a number you read in a bundle is not on the same scale as the same-named
[params]binding beside it. -
A bundle brings its own light, so the deposit stays off. That is the one place where reading this section as “the warp mesh has nothing to draw” would mislead you.
-
A MilkDrop 2 bundle may also carry
warp_shader/comp_shader— the preset’s own pixel shaders, translated from HLSL to WGSL ahead of time, plus ablur_levelnaming how deep a blur chain they sample. The engine validates each through naga at load; a shader that does not compile rejects that one preset by name and the rest of the library loads. When awarp_shaderis present it replaces the built-in decay fragment, and acomp_shaderreplaces the built-in present remaps — the bundle is then authoritative about the compositing too, not just the transform. -
A bundle’s feedback field quantizes, and a hand-authored one’s does not. MilkDrop’s feedback target is 8-bit, so
decaytimes a dim pixel truncates to zero and a classic preset’s background stays black; this engine’s field isRgba16Float, where the same residual survives and integrates into a wash (ADR-0118). So the presence of a[milk]table turns the emulation on, at 255 steps in the sRGB-encoded domain, and a nativewarp_meshpreset — which has no bundle — keeps the full float range it was designed for. The one key here you might ever touch is the override:[milk]quantize_steps = 255 # the default for a bundle. 0 turns it off entirely;# -255 floors dim pixels to zero without stepping the# levels betweenA negative value still takes its floor from the magnitude, which is the one trap here: the shader kills a pixel whose encoded value is below
1/|steps|, so-255floors at one 8-bit step — the thing Alternative D means — and-1floors at encoded1.0and renders a near-black screen. The sign picks the mode; the number still picks the level. Write-255unless you know why you want another floor.It is a runtime uniform rather than something baked into the converted shader, which is what makes an A/B a preset edit rather than a re-conversion.
The draw layer a converted preset brings
Between the warp and the composite, a bundle draws what MilkDrop draws. None of it is authorable from a hand-written preset — it is listed so a converted file is readable, and so the vocabulary is somewhere other than the source:
| what | from |
|---|---|
| the waveform | eight wave_mode figures over the audio trace — a circle, a pair of rings, a horizontal and a vertical scope, a Lissajous, a mirrored pair, an angled line and its double — placed by wave_x/wave_y, coloured by wave_r/_g/_b/_a, shaped by wave_scale, wave_smoothing and wave_mystery |
| up to four custom waves | each a polyline or a scatter from its own per-point program |
| up to four custom shapes | each up to 1024 filled polygons from its own per-frame program, with an optional outline |
| the two borders | ob_* outside, ib_* inside |
| the motion grid | mv_*: a lattice of short strokes |
Two blend modes, and the difference is visible. MilkDrop picks per element between adding light and compositing it over what is already there (
bAdditiveWaves, and a custom element’s ownadditive). This engine honours both, because reading them all as additive is not a small error: an additive seam sums where alpha-over replaces, so ten overlapping producers land at ten rather than at one. On the 28.5 % of the MilkDrop library that setsfDecay = 1— where the field never fades — that is the difference between a preset and a white frame.
The waveform’s own trace is the mono signal this engine analyses, where MilkDrop had two channels. Where the reference tells two of its wave modes apart by drawing the left channel against the right, this draws the one trace at the separation the reference’s own parameters name. It is the same figure with the channel difference removed.
Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.