MilkDrop Preset Authoring Guide
Author: Ryan Geiss
Original: geisswerks.com/milkdrop/milkdrop_preset_authoring.html
This guide is reproduced here with credit to Ryan Geiss and for the convenience of the MilkDrop community. For the most up-to-date version, please refer to the original document.
Table of Contents
1. About Presets
When you watch MilkDrop, you are watching a series of presets. Each one has its own look and feel, draws the sound waves in a particular way, and has certain motions to it. After some time, you will see a short blend transition, and then you will be watching a new preset.
A single preset is a collection of parameters that tell MilkDrop how to draw the wave, how to warp the image around, and so on. MilkDrop ships with over 100 built-in presets, each one having a distinct look and feel to it.
Using MilkDrop’s built-in “preset-editing menu” (the M key), you can edit presets on the fly, on-screen, from within the program. You can make slight adjustments to existing presets, then save over them; or you can change lots of things so the preset doesn’t look anything like the original, and then save it under a new name. You can even write insane new mathematical equations, of your own imagination, into your preset files and come up with things that MilkDrop has never done before!
Each preset is saved as a file with the .milk extension, so you can easily send them to your friends or post them on the web.
2. Preset Authoring — Basic
You can edit the properties of the current preset by hitting M, which brings up the “preset-editing menu”. From this menu you can use the up and down arrow keys to select an item. Press the RIGHT arrow key to move forward through the menu and select the item (you can also hit SPACE or RETURN); press the LEFT arrow key to go back to the previous menu.
Pressing M while the menu is already showing will hide the menu; pressing ESCAPE will do the same thing. Press M again to bring the menu back.
Once you’ve reached an item on the menu whose value can be edited, use the UP and DOWN arrow keys to increase or decrease its value, respectively. Changes will register immediately. Use PAGE UP and PAGE DOWN to increase the value more quickly. Hold down SHIFT and use the UP/DOWN arrow keys to change the value very slowly. Hit RETURN to keep the new value, or ESC to abort the change.
If the item you’re editing is a text string, you can use the arrow keys to move around. The Insert key can be used to toggle between insert and overtype modes. You can hold shift and use the arrow keys (home, end, left, right) to make a selection, which will be identified by brackets []. You can then use CTRL-C or CTRL-X to copy or cut text. CTRL-P pastes. When finished editing, hit RETURN to keep the new string, or ESC to abort the change.
You’ll want to get into the habit of using SCROLL LOCK whenever you’re making changes to a preset that you intend to save; otherwise, MilkDrop is sure to move you along to a new (random) preset, over time.
Hotkeys
Motion:
| Key | Action |
|---|---|
i / I | zoom in / out |
[ / ] | push motion left / right (dx) |
{ / } | push motion up / down (dy) |
< / > | rotate left / right (rot) |
o / O | shrink / grow warp amplitude |
Waveform:
| Key | Action |
|---|---|
W | cycle through waveforms |
j / J | scale waveform down / up |
e / E | make waveform more transparent / more solid |
Brightness (MilkDrop 1 presets only):
| Key | Action |
|---|---|
g / G | decrease / increase gamma |
Video Echo (MilkDrop 1 presets only):
| Key | Action |
|---|---|
q / Q | scale 2nd layer down / up |
F | flip 2nd layer (cycles 4 orientations) |
3. Preset Authoring — Advanced
This section describes how to use the per-frame and per-vertex equations to develop unique new presets.
a. Per-Frame Equations
When you hit M to show the preset-editing menu, all of the properties that make up the preset are there. The values you can specify here (zoom amount, rotation amount, wave color, etc.) are all static values — they don’t change in time.
However, presets get far more interesting if you can animate these parameters. For example, if you could make the zoom amount oscillate between 0.9 and 1.1 over time, the image would cyclically zoom in and out.
You can do this by writing per-frame equations. These are executed once per frame:
zoom = zoom + 0.1*sin(time);This would make the zoom amount oscillate between 0.9 and 1.1 over time. The equation says: “take the static value of zoom, then replace it with that value, plus some variation.” This equation cycles every 6.28 seconds (since sin()‘s period is 2π). To cycle every 2 seconds:
zoom = zoom + 0.1*sin(time*3.14);To make waveform color vary through time:
wave_r = wave_r + 0.5*sin(time*1.13);
wave_g = wave_g + 0.5*sin(time*1.23);
wave_b = wave_b + 0.5*sin(time*1.33);Stagger the frequencies so the R, G, and B components cycle at different rates, to avoid a greyscale wave.
Per-Frame Variable Reference
| Name | Writable? | Range | Description |
|---|---|---|---|
zoom | yes | >0 | inward/outward motion. 0.9=zoom out 10%, 1.0=none, 1.1=zoom in 10% |
zoomexp | yes | >0 | curvature of the zoom; 1=normal |
rot | yes | any | rotation. 0=none, 0.1=CCW, -0.1=CW |
warp | yes | >0 | warping magnitude; 0=none, 1=normal |
cx | yes | 0..1 | center of rotation/stretching, horizontal. 0=left, 0.5=center, 1=right |
cy | yes | 0..1 | center of rotation/stretching, vertical. 0=top, 0.5=center, 1=bottom |
dx | yes | any | horizontal motion. -0.01=left, 0=none, 0.01=right |
dy | yes | any | vertical motion. -0.01=up, 0=none, 0.01=down |
sx | yes | >0 | horizontal stretching; 0.99=shrink 1%, 1=normal |
sy | yes | >0 | vertical stretching; 0.99=shrink 1%, 1=normal |
wave_mode | yes | 0-7 | waveform type |
wave_x | yes | 0..1 | waveform position X. 0=left, 0.5=center, 1=right |
wave_y | yes | 0..1 | waveform position Y. 0=bottom, 0.5=center, 1=top |
wave_r | yes | 0..1 | red color in wave |
wave_g | yes | 0..1 | green color in wave |
wave_b | yes | 0..1 | blue color in wave |
wave_a | yes | 0..1 | opacity of wave. 0=transparent, 1=opaque |
wave_mystery | yes | -1..1 | does different things per waveform type |
wave_usedots | yes | 0/1 | draw wave as dots instead of lines |
wave_thick | yes | 0/1 | double thickness |
wave_additive | yes | 0/1 | additive drawing, saturating toward white |
wave_brighten | yes | 0/1 | scale R/G/B until at least one reaches 1.0 |
ob_size | yes | 0..0.5 | outer border thickness |
ob_r, ob_g, ob_b | yes | 0..1 | outer border color |
ob_a | yes | 0..1 | outer border opacity |
ib_size | yes | 0..0.5 | inner border thickness |
ib_r, ib_g, ib_b | yes | 0..1 | inner border color |
ib_a | yes | 0..1 | inner border opacity |
mv_r, mv_g, mv_b | yes | 0..1 | motion vector color |
mv_a | yes | 0..1 | motion vector opacity |
mv_x | yes | 0..64 | number of motion vectors in X |
mv_y | yes | 0..48 | number of motion vectors in Y |
mv_l | yes | 0..5 | motion vector trail length |
mv_dx, mv_dy | yes | -1..1 | motion vector placement offset |
decay | yes | 0..1 | fade to black. 1=no fade, 0.9=strong, 0.98=recommended |
gamma | yes | >0 | brightness. 1=normal, 2=double |
echo_zoom | yes | >0 | size of 2nd graphics layer |
echo_alpha | yes | >0 | opacity of 2nd layer. 0=off, 0.5=half, 1=opaque |
echo_orient | yes | 0-3 | orientation of 2nd layer. 0=normal, 1=flip X, 2=flip Y, 3=both |
darken_center | yes | 0/1 | dims center to prevent overbright |
wrap | yes | 0/1 | screen element wrapping |
invert | yes | 0/1 | invert colors |
brighten | yes | 0/1 | brighten dark parts (square root filter) |
darken | yes | 0/1 | darken bright parts (squaring filter) |
solarize | yes | 0/1 | emphasize mid-range colors |
monitor | yes | any | debug value shown with N key |
time | NO | >0 | seconds since MilkDrop started |
fps | NO | >0 | current framerate |
frame | NO | any | frame count since start |
progress | NO | 0..1 | progress through preset (0=just loaded, 1=about to end) |
bass | NO | >0 | current bass level. 1=normal |
mid | NO | >0 | current mid level |
treb | NO | >0 | current treble level |
bass_att | NO | >0 | attenuated bass (damped in time) |
mid_att | NO | >0 | attenuated mid |
treb_att | NO | >0 | attenuated treble |
meshx | NO | 8-128 | mesh size X |
meshy | NO | 6-96 | mesh size Y |
pixelsx | NO | 16-4096 | window width in pixels |
pixelsy | NO | 16-4096 | window height in pixels |
aspectx | NO | >0 | aspect ratio multiplier for X |
aspecty | NO | >0 | aspect ratio multiplier for Y |
q1–q32 | yes | any | pass values between variable pools (diagram) |
You can also make up to 30 of your own variables:
my_volume = (bass + mid + treb)/3;
zoom = zoom + 0.1*(my_volume - 1);Note: Custom variables do NOT carry over from per-frame to per-vertex equations. Use q1–q32 to bridge the gap.
b. Per-Vertex Equations
What if you wanted to vary a parameter differently for different locations on the screen? For example, making pixels far from center zoom more for a perspective effect:
zoom = zoom + rad*0.1;Where rad is the distance of the pixel from the center of the screen (0 at center, 1 at corners).
Per-Vertex Exclusive Variables
| Name | Writable? | Range | Description |
|---|---|---|---|
x | NO | 0..1 | X position. 0=left, 0.5=center, 1=right |
y | NO | 0..1 | Y position. 0=top, 0.5=center, 1=bottom |
rad | NO | 0..1 | distance from center. 0=center, 1=corners |
ang | NO | 0..6.28 | angle from center. 0=right, π/2=above, π=left, 3π/2=below |
All per-frame writable variables (zoom, rot, warp, cx, cy, dx, dy, sx, sy) plus all read-only variables (time, fps, bass, etc.) and q1–q32 are also available.
Performance maxim: “If a per-vertex equation doesn’t use at least one of the variables { x, y, rad, ang }, then it should actually be a per-frame equation.”
The per-vertex equations are actually computed only at mesh vertices, then interpolated across the space between them. The “mesh size” option defines how many evaluation points there are.
c. Variable Pools, Declaring Your Own Variables, Persistence
Declaring your own variables is simple:
billy = 5.3;There are three variable pools in MilkDrop:
- Preset init code + preset per-frame code
- Custom wave init + custom wave per-frame code
- Custom shape init + custom shape per-frame code
Within a pool, user-defined variables persist from frame to frame. But you can’t read a variable from another pool — use q1–q32 to bridge between pools.
Per-vertex code and custom wave per-point code use scratch variables only — they don’t persist meaningfully from vertex to vertex or point to point.
d. Preset Init Code; Q Variables
The preset initialization code runs once when a preset is loaded. It does two things:
- Sets initial values for user-defined variables
- Sets default (“sticky”) values for
q1–q32
Whatever values q1–q32 have after the init code become the starting values for each frame. Per-frame code can modify them, and those modifications propagate to per-vertex code and shaders — but are reset each frame.

Side note: When you edit init code and apply with CTRL+ENTER, it re-executes immediately. But editing per-frame/per-vertex code does NOT re-execute the init code — you’ll have to save and reload the preset.
e. Custom Shapes & Waves
A preset can have up to 4 custom shapes and 4 custom waves.
Custom Shapes draw an n-sided polygon (3–100 sides) anywhere on the screen, at any angle, size, color, and opacity. You can map the previous frame’s image onto the shape for effects like realtime hardware fractals. Each shape can be instanced up to 1024 times per frame.
Custom Waves draw the waveform or spectrum however you want, with per-frame and per-point control over placement and color.
T Variables (t1–t8): Similar to Q variables, but only for custom waves and shapes — they bridge between wave/shape init code and per-frame code. See the T variable diagram.
Custom Shape Per-Frame Variables
| Name | Writable? | Range | Description |
|---|---|---|---|
num_inst | no | 1-1024 | total instances to draw |
instance | no | 0..num_inst-1 | current instance number |
sides | yes | 3-100 | number of sides |
thick | yes | 0/1 | bold border |
additive | yes | 0/1 | additive blending |
x | yes | 0..1 | X position |
y | yes | 0..1 | Y position (0=bottom, 1=top) |
rad | yes | 0+ | radius |
ang | yes | 0..6.28 | rotation angle |
textured | yes | 0/1 | texture with previous frame’s image |
tex_zoom | yes | >0 | texture zoom |
tex_ang | yes | 0..6.28 | texture rotation angle |
r, g, b, a | yes | 0..1 | center color & opacity |
r2, g2, b2, a2 | yes | 0..1 | edge color & opacity |
border_r, border_g, border_b, border_a | yes | 0..1 | border color & opacity |
Custom Wave Per-Frame Variables
| Name | Writable? | Range | Description |
|---|---|---|---|
r, g, b, a | yes | 0..1 | base color & opacity |
samples | yes | 0-512 | number of samples |
Custom Wave Per-Point Variables
| Name | Writable? | Range | Description |
|---|---|---|---|
x | yes | 0..1 | X position (0=left, 1=right) |
y | yes | 0..1 | Y position (0=bottom, 1=top) |
sample | no | 0..1 | progress through waveform. 0=first, 1=last |
value1 | no | any | Left audio channel sample |
value2 | no | any | Right audio channel sample |
r, g, b, a | yes | 0..1 | color & opacity at this point |
All pools also have access to time, fps, frame, progress, audio variables, q1–q32, and t1–t8.
f. Pixel Shaders
MilkDrop 2 supports programmable pixel shaders. Each preset has two shaders:
- Warp shader — warps the image from frame to frame (effects get “baked in” and persist)
- Composite shader — draws the frame to screen (display only, doesn’t affect next frame)
Edit them via the M menu, or press F9 while editing for the onscreen quick reference.
Shader Models
- MilkDrop 1: Fixed-function graphics only (no shaders)
- Shader Model 2.0: 64 instruction limit per shader
- Shader Model 3.0: Virtually unlimited instructions (not all GPUs)
Warp Shader Example
shader_body
{
// sample the previous frame (UV is warped by per-vertex equations)
ret = tex2D( sampler_main, uv ).xyz;
// darken over time
ret *= 0.97;
}The UV coordinates drive the motion — they’re computed from per-vertex equations and interpolated across the screen.
Composite Shader Example
shader_body
{
// sample the internal canvas (uv is undistorted here)
ret = tex2D(sampler_main, uv).xyz;
// make it a little "overbright"
ret *= 1.8;
}Data Types
| Type | Description |
|---|---|
float, float2, float3, float4 | Full-precision floating point |
half, half2, half3, half4 | Half-precision (faster on some hardware) |
float2x2, float3x3, float4x3 | Transformation matrices |
Swizzle Operators
You can reorder components using .xyzw:
float4 delta = float4(5,6,7,8);
delta.wywy // -> float4(8,6,8,6)Intrinsic Instructions
Math:
| Function | Description |
|---|---|
abs(a) | Absolute value |
frac(a) | Fractional part |
floor(a) | Integer part (single floats only) |
saturate(a) | Clamp to [0..1] — often FREE |
max(a,b) | Greater of each component |
min(a,b) | Lesser of each component |
sqrt(a) | Square root |
pow(a,b) | a^b |
exp(a) | 2^a |
log(a) | log2(a) |
lerp(a,b,c) | Linear interpolate: a + c*(b-a) |
dot(a,b) | Dot product (returns single float) |
lum(a) | Convert float3 color to luminance |
length(a) | Vector length |
normalize(a) | Normalize to unit length |
Texture:
| Function | Description |
|---|---|
tex2D(sampler, uv) | Sample 2D texture at float2 UV → float4 |
tex3D(sampler, uvw) | Sample 3D texture at float3 → float4 |
GetBlur1(uv) | Slightly blurred main texture → float3 |
GetBlur2(uv) | More blurred → float3 |
GetBlur3(uv) | Very blurred → float3 |
Slow operations (use sparingly):
| Function | Description |
|---|---|
sin(a) | ~8 instructions |
cos(a) | ~8 instructions |
atan2(y,x) | Arctangent of y/x — very slow |
mul(a,b) | Matrix × vector multiply |
cross(a,b) | Cross product (float3 only) |
Per-Vertex Shader Inputs
Warp shader:
| Input | Description |
|---|---|
float2 uv | Warped UV coords ~[0..1] |
float2 uv_orig | Original (un-warped) UV coords [0..1] |
float rad | Radius from center [0..1] |
float ang | Angle from center [0..2π] |
Composite shader:
| Input | Description |
|---|---|
float2 uv | Un-warped UV coords |
float rad | Radius from center |
float ang | Angle from center |
float3 hue_shader | Color varying across screen (legacy hue shader) |
Per-Frame Shader Inputs
float4 rand_preset; // 4 random floats [0..1], per preset
float4 rand_frame; // 4 random floats [0..1], per frame
float time; // seconds since preset start (wraps at 10,000)
float fps; // framerate
float frame; // frame number
float progress; // progress through preset [0..1]
float bass, mid, treb, vol; // immediate audio
float bass_att, mid_att, treb_att, vol_att; // attenuated audio
float4 aspect; // .xy aspect multiplier, .zw inverse
float4 texsize; // .xy = (w,h); .zw = (1/w, 1/h)
float4 slow_roam_cos, slow_roam_sin; // slow-varying [0..1]
float4 roam_cos, roam_sin; // faster-varying [0..1]
float q1 ... q32; // per-frame equation outputs
float4 _qa ... _qh; // q1-q32 grouped into float4sPlus rotation matrices: rot_s1–rot_s4 (static), rot_d1–rot_d4 (slow), rot_f1–rot_f4 (fast), rot_vf1–rot_vf4 (very fast), rot_uf1–rot_uf4 (ultra fast), rot_rand1–rot_rand4 (random every frame).
Texture Sampling
Main texture samplers:
| Sampler | Filtering | Outside [0..1] |
|---|---|---|
sampler_main (or sampler_fw_main) | bilinear | wrap |
sampler_fc_main | bilinear | clamp |
sampler_pw_main | point | wrap |
sampler_pc_main | point | clamp |
Noise Textures
| Name | Dims | Pixels | Quality |
|---|---|---|---|
noise_lq | 2D | 256×256 | low |
noise_lq_lite | 2D | 32×32 | low |
noise_mq | 2D | 64×64 | medium |
noise_hq | 2D | 32×32 | high |
noisevol_lq | 3D | 32³ | low |
noisevol_hq | 3D | 8³ | high |
Use tex2D for 2D noise, tex3D for 3D noise. For 1:1 pixel mapping:
float4 texsize_noise_lq; // declare above shader_body
// In shader_body:
float4 noiseVal = tex2D(sampler_noise_lq,
uv_orig * texsize.xy * texsize_noise_lq.zw + rand_frame.xy);Reading Textures from Disk
Save textures to milkdrop2\textures\. Then in your shader:
sampler sampler_billy; // loads billy.jpg (or .dds, .png, .tga, .bmp)
float4 texsize_billy; // .xy = (w,h); .zw = (1/w, 1/h)
shader_body
{
float3 mypixel = tex2D(sampler_billy, uv).xyz;
}Supported formats: jpg, dds, png, tga, bmp, dib.
For random textures, use rand00 through rand15 as the filename. For random subsets, append a prefix: sampler sampler_rand02_smalltiled.
Cool Shader Tricks
Auto center darkening:
ret *= 0.97 + 0.03*saturate( length(uv - uv_orig)*200 );Soft max (smoother than max(a,b), inputs must be [0..1]):
a + b - a*bError diffusion dithering:
float2 uv_noise = uv_orig*texsize.xy*texsize_noise_lq.zw + rand_frame.xy;
half4 noiseVal = tex2D(sampler_noise_lq, uv_noise);
ret += (noiseVal.xyz*2-1) * 0.01;Best darkening approach:
ret = (ret - 0.002)*0.99;g. Quality Assurance
-
Keep presets fast. Division is 11× slower than multiplication — pre-divide common values. Move per-vertex computations that don’t use
{x, y, rad, ang}into per-frame code. -
Design at default mesh size. Check that presets look correct at the default setting before distributing.
-
Design in 32-bit color mode for standard brightness levels.
-
Don’t underestimate
dxanddy. Some of the best presets are based purely on manual dx/dy control — all other effects (zoom, warp, rot) are abstractions that can be simulated with{x, y, rad, ang}and{dx, dy}. -
Test
progressusage with various “Time Between Auto Preset Changes” settings. -
Shader guidelines:
- Use small (≤256×256) textures, saved as JPG 95%
- Avoid
ifstatements - Avoid massive zoom-outs of textures (thrashes texture cache)
- Minimize
sin()/cos()— precompute in per-frame equations and pass viaq1–q32 - Offload per-pixel-constant calculations to per-frame equations
- Test in both square and widescreen windows
h. Debugging
Use the variable monitoring feature:
- Press N to show the monitor value
- In per-frame equations, add:
monitor = x;(where x is any variable or expression) - Press CTRL+ENTER — the value appears in the upper-right corner
Note: Monitoring only works for per-frame equations, not per-vertex.
i. Function Reference
For preset init, per-frame, and per-vertex equations (NOT for pixel shaders):
Use semicolons (;) to delimit statements. Use parentheses for precedence.
Operators: = assign, + - * / arithmetic, | bitwise or, & bitwise and, % modulo
Functions:
| Function | Description |
|---|---|
int(x) | Integer value (rounds toward zero) |
abs(x) | Absolute value |
sin(x) | Sine (radians) |
cos(x) | Cosine |
tan(x) | Tangent |
asin(x) | Arcsine |
acos(x) | Arccosine |
atan(x) | Arctangent |
sqr(x) | Square |
sqrt(x) | Square root |
pow(x,y) | x to the power of y |
log(x) | Natural log (base e) |
log10(x) | Log base 10 |
sign(x) | Sign of x, or 0 |
min(x,y) | Smallest value |
max(x,y) | Greatest value |
sigmoid(x,y) | Sigmoid function (y=constraint) |
rand(x) | Random integer modulo x |
bor(x,y) | Boolean OR (1 if either ≠ 0) |
bnot(x) | Boolean NOT (1 if x=0) |
if(cond,a,b) | If cond ≠ 0 returns a, else b |
equal(x,y) | 1 if x=y, else 0 |
above(x,y) | 1 if x>y, else 0 |
below(x,y) | 1 if x<y, else 0 |
This guide was originally written by Ryan Geiss. Visit the original document for the most current version.
