Documentation/Source example
Example source
samples/vis/Shader - MilkDrop 0101.js
Referenced by:vis.CreateAsync
include('docs/Flags.js');
// MilkDrop 0101 — smoothed PCM spiral with feathered feedback
//
// The preset uses no custom waves or shapes. Its visible layers are:
// shader - MilkDrop's built-in mode-1 waveform;
// feedback - the 48x36 mesh transform and three-sample feather trail;
// post - a shifted, channel-swapped copy combined with the history.
// History advances on every Draw; frameStep follows the measured draw interval.
// Transforms, feathering, decay and source opacity use the original 30-Hz cadence.
const TARGET_FPS = 60; // Recommended values: 30, 60 or 120.
// Requires the source b0 frameStep constant supported by current JSplitter builds.
const FRAME_INTERVAL_MS = 1000 / TARGET_FPS;
window.DrawMode = 1;
vis.UpdateIntervalMs = 17; // Audio analysis interval, independent of render FPS.
const source = {
leftChannel: 0,
rightChannel: 1,
durationMs: 40,
points: 1024,
relativeBandAnalysis: true
};
const shader = `
Texture2D<float> LeftPCM : register(t0);
Texture2D<float> RightPCM : register(t1);
Texture2D<float> RelativeBands : register(t2);
cbuffer VisConstants : register(b0) {
float2 viewportSize;
float time;
float pointCount;
float frameStep; // 60-Hz units; the same measured draw interval as feedback.
};
struct VS_OUTPUT {
float4 clipSpacePos : SV_POSITION;
float4 sceneSpacePos : SCENE_POS;
};
float DistanceToSegment(float2 p, float2 a, float2 b) {
float2 ab = b - a;
float t = saturate(dot(p - a, ab) / max(dot(ab, ab), 1e-5));
return length(p - a - ab * t);
}
float2 WavePoint(float left, float right, float2 size) {
// nWaveMode=1. BuildWaves has already applied fWaveScale and the preset's
// forward smoothing before these values reach the mode-specific formula.
float radius = 0.53 + 0.43 * right - 0.28;
float angle = left * 1.57 + time * 2.3;
float radiusPixels = 0.5 * min(size.x, size.y) * radius;
return 0.5 * size + radiusPixels * float2(cos(angle), sin(angle));
}
float2 ReadWavePoint(int vertex, int sourceStart, inout float leftSmooth,
inout float rightSmooth, float2 size) {
const float waveScale = 3.586525;
const float smoothing = 0.9;
float leftRaw = clamp(LeftPCM.Load(int3(sourceStart + vertex + 32, 0, 0)).r,
-1.0, 1.0);
float rightRaw = clamp(RightPCM.Load(int3(sourceStart + vertex, 0, 0)).r,
-1.0, 1.0);
leftSmooth = leftRaw * waveScale * (1.0 - smoothing) +
leftSmooth * smoothing;
rightSmooth = vertex == 0
? rightRaw * waveScale
: rightRaw * waveScale * (1.0 - smoothing) +
rightSmooth * smoothing;
return WavePoint(leftSmooth, rightSmooth, size);
}
float4 main(VS_OUTPUT input) : SV_Target {
float2 size = max(viewportSize, float2(1.0, 1.0));
int count = (int)pointCount;
int usable = min(count, 576);
int vertexCount = min(240, usable - 32);
if (vertexCount < 3) return float4(0.0, 0.0, 0.0, 0.0);
float2 p = input.sceneSpacePos.xy;
if (length(p - 0.5 * size) > 0.96 * min(size.x, size.y))
return float4(0.0, 0.0, 0.0, 0.0);
int sourceStart = max(count - usable, 0);
const float waveScale = 3.586525;
const float smoothing = 0.9;
// BuildWaves smooths the complete left channel from sample zero. Mode 1
// reads it 32 samples ahead of the right channel, so prime that recurrence
// before walking the visible vertices.
float leftRaw = clamp(LeftPCM.Load(int3(sourceStart, 0, 0)).r, -1.0, 1.0);
float leftSmooth = leftRaw * waveScale;
[loop]
for (int primeIndex = 1; primeIndex < 32; ++primeIndex) {
leftRaw = clamp(LeftPCM.Load(int3(sourceStart + primeIndex, 0, 0)).r,
-1.0, 1.0);
leftSmooth = leftRaw * waveScale * (1.0 - smoothing) +
leftSmooth * smoothing;
}
float rightSmooth = 0.0;
float2 below = ReadWavePoint(0, sourceStart, leftSmooth, rightSmooth, size);
float2 a = below;
float2 b = ReadWavePoint(1, sourceStart, leftSmooth, rightSmooth, size);
float nearest = 1e6;
// MilkDrop's SmoothWave inserts one four-point interpolated vertex between
// every pair, expanding 240 raw points to 479 line-strip vertices.
[loop]
for (int vertex = 2; vertex < 240; ++vertex) {
if (vertex >= vertexCount) break;
float2 c = ReadWavePoint(vertex, sourceStart, leftSmooth, rightSmooth,
size);
float2 midpoint = (-0.15 * below + 1.15 * a +
1.15 * b - 0.15 * c) * 0.5;
nearest = min(nearest, DistanceToSegment(p, a, midpoint));
nearest = min(nearest, DistanceToSegment(p, midpoint, b));
below = a;
a = b;
b = c;
}
float2 lastMidpoint = (-0.15 * below + 1.15 * a +
1.15 * b - 0.15 * b) * 0.5;
nearest = min(nearest, DistanceToSegment(p, a, lastMidpoint));
nearest = min(nearest, DistanceToSegment(p, lastMidpoint, b));
float coverage = 1.0 - smoothstep(0.15, 1.25, nearest);
float3 presetColor = float3(
0.7 + 0.2 * (0.9 * sin(2.753 * time) +
0.4 * sin(2.315 * time + 1.0)),
0.65 + 0.1 * (0.9 * sin(3.183 * time + 3.0) +
0.4 * sin(2.006 * time + 4.0)),
0.7 + 0.1 * (0.9 * sin(2.393 * time + 5.0) +
0.4 * sin(2.733 * time + 2.0)));
presetColor = saturate(presetColor);
presetColor /= max(max(presetColor.r, presetColor.g),
max(presetColor.b, 1e-4));
// Modern MilkDrop 0.10.0 DrawWave stores RGB in normal order and rounds
// each vertex component to the nearest 8-bit normalized value.
// t2: bass/mid/treb followed by bass_att/mid_att/treb_att.
float3 visibleColor = floor(saturate(presetColor) * 255.0 + 0.5) / 255.0;
float relativeVolume = (
RelativeBands.Load(int3(0, 0, 0)).r +
RelativeBands.Load(int3(1, 0, 0)).r +
RelativeBands.Load(int3(2, 0, 0)).r) * 0.333;
float waveAlpha = saturate(
1.200006 * 1.25 * (relativeVolume - 0.97) / (1.67 - 0.97));
// The native visibility test precedes UNORM8 quantization.
if (waveAlpha < 0.004) waveAlpha = 0.0;
waveAlpha = floor(waveAlpha * 255.0 + 0.5) / 255.0;
// This source image is accumulated once per feedback step. Convert its
// 30-Hz opacity so 60/120-Hz operation does not inject extra brightness.
float alpha30 = coverage * waveAlpha;
float alpha = alpha30; // Fast path at 30 FPS (frameStep == 2).
[branch]
if (frameStep < 1.5)
alpha = 1.0 - pow(saturate(1.0 - alpha30), frameStep * 0.5);
return float4(visibleColor * alpha, alpha);
}`;
const warpShader = `
Texture2D Previous : register(t0);
SamplerState PreviousSampler : register(s0);
cbuffer WarpConstants : register(b0) {
float2 viewportSize;
float time;
float frameStep;
};
struct VS_OUTPUT {
float4 clipSpacePos : SV_POSITION;
float4 sceneSpacePos : SCENE_POS;
float4 texelSpacePos : TEXEL_POS;
};
float3 HistoryWrapped(float2 uv) {
return Previous.SampleLevel(PreviousSampler, frac(uv), 0.0).rgb;
}
float2 WarpVertexUV(float2 uvOriginal, float2 size) {
float2 aspect = min(float2(1.0, 1.0),
float2(size.x / size.y, size.y / size.x));
float2 vertexPos = float2(2.0 * uvOriginal.x - 1.0,
1.0 - 2.0 * uvOriginal.y);
// frameStep is expressed in 60-Hz frames. Convert it to the amount of one
// original 30-Hz preset step represented by this feedback update.
float step30 = max(frameStep * 0.5, 0.0005);
float zoom = 1.065 + 0.013 * (
0.6 * sin(0.339 * time) + 0.4 * sin(0.276 * time));
float rotation = 0.020 * (
0.6 * sin(0.381 * time) + 0.4 * sin(0.579 * time));
rotation += 0.020 * (sin(2.134 * time) +
sin(1.7134 * time) + sin(2.834 * time));
float cx = 0.5 + 0.003 * (
0.6 * sin(0.471 * time) + 0.4 * sin(0.297 * time));
float cy = 0.5 + 0.003 * (
0.6 * sin(0.379 * time) + 0.4 * sin(0.351 * time));
float dx = 0.002 * (
0.6 * sin(0.234 * time) + 0.4 * sin(0.277 * time));
dx += 0.0025 * (sin(1.134 * time) +
sin(0.7134 * time) + sin(2.334 * time));
float dy = 0.0025 * (sin(1.8834 * time) +
sin(1.0144 * time) + sin(1.334 * time));
float zoomStep = pow(max(zoom, 0.0001), step30);
float rotationStep = rotation * step30;
float2 uv = float2(
0.5 + vertexPos.x * aspect.x * 0.5 / zoomStep,
0.5 - vertexPos.y * aspect.y * 0.5 / zoomStep);
float warpTime = time;
float4 factors = float4(
11.68 + 4.0 * cos(warpTime * 1.413 + 10.0),
8.77 + 3.0 * cos(warpTime * 1.113 + 7.0),
10.54 + 3.0 * cos(warpTime * 1.233 + 3.0),
11.49 + 4.0 * cos(warpTime * 0.933 + 5.0));
float warpAmount = 0.014587 * 0.0035;
float uDelta = sin(warpTime * 0.333 +
vertexPos.x * factors.x - vertexPos.y * factors.w);
uDelta += cos(warpTime * 0.753 -
vertexPos.x * factors.y + vertexPos.y * factors.z);
float vDelta = cos(warpTime * 0.375 -
vertexPos.x * factors.z - vertexPos.y * factors.y);
vDelta += sin(warpTime * 0.825 +
vertexPos.x * factors.x + vertexPos.y * factors.w);
uv += warpAmount * step30 * float2(uDelta, vDelta);
float sineRotation = sin(rotationStep);
float cosineRotation = cos(rotationStep);
float2 centered = uv - float2(cx, cy);
uv = float2(cosineRotation * centered.x - sineRotation * centered.y,
sineRotation * centered.x + cosineRotation * centered.y) +
float2(cx, cy);
uv -= float2(dx, dy) * step30;
return 0.5 + (uv - 0.5) / aspect;
}
float2 MeshUV(float2 uvOriginal, float2 size) {
const float2 grid = float2(48.0, 36.0);
float2 scaled = saturate(uvOriginal) * grid;
float2 cell = min(floor(scaled), grid - 1.0);
float2 f = scaled - cell;
float2 corner00 = cell / grid;
float2 corner10 = (cell + float2(1.0, 0.0)) / grid;
float2 corner01 = (cell + float2(0.0, 1.0)) / grid;
float2 corner11 = (cell + float2(1.0, 1.0)) / grid;
float2 result = float2(0.0, 0.0);
if (f.x + f.y <= 1.0) {
float2 a = WarpVertexUV(corner00, size);
float2 b = WarpVertexUV(corner10, size);
float2 c = WarpVertexUV(corner01, size);
result = (1.0 - f.x - f.y) * a + f.x * b + f.y * c;
} else {
float2 a = WarpVertexUV(corner10, size);
float2 b = WarpVertexUV(corner01, size);
float2 c = WarpVertexUV(corner11, size);
result = (1.0 - f.y) * a + (1.0 - f.x) * b +
(f.x + f.y - 1.0) * c;
}
return result;
}
float4 main(VS_OUTPUT input) : SV_Target {
float2 size = max(viewportSize, float2(1.0, 1.0));
float2 uvOriginal = input.sceneSpacePos.xy / size;
float2 warped = MeshUV(uvOriginal, size);
float2 uv = input.texelSpacePos.xy + (warped - uvOriginal);
// Custom warp: keep the brightest of the main sample and two nearby zoomed
// samples. Scale their displacement because this dilation accumulates in
// history just like the geometric warp. sampler_main is bilinear-wrap.
float step30 = max(frameStep * 0.5, 0.0005);
float feather = 8.0 * length(1.0 / size) * step30;
float3 result = HistoryWrapped(uv);
result = max(result, HistoryWrapped((uv - 0.5) * (1.0 - feather) + 0.5));
result = max(result, HistoryWrapped((uv - 0.5) * (1.0 + feather) + 0.5));
result -= 0.034 * step30;
return float4(saturate(result), 1.0);
}`;
const compShader = `
Texture2D Frame : register(t0);
SamplerState Filter : register(s0);
cbuffer ImageConstants : register(b0) {
float2 viewportSize;
float time;
float frameStep;
};
struct PSInput {
float4 clipSpacePos : SV_POSITION;
float4 sceneSpacePos : SCENE_POS;
float4 texelSpacePos : TEXEL_POS;
};
float4 main(PSInput input) : SV_Target {
float2 uv = input.texelSpacePos.xy;
float3 result = Frame.SampleLevel(Filter, uv, 0.0).rgb;
const float scale = 0.9;
float2 shift = 0.05 * scale * float2(
cos(uv.y / scale * 27.0 * 0.5),
sin(uv.x / scale * 39.0 * 0.5));
// sampler_fc_main is bilinear-clamp; the custom comp reverses RGB on only
// this shifted copy before selecting the brighter channel values.
float3 shifted = Frame.SampleLevel(Filter, saturate(uv + shift), 0.0).zyx;
return float4(max(result, shifted), 1.0);
}`;
let visual = null;
vis.CreateAsync(VisSourceType.Vectorscope, source, {
shader,
feedback: warpShader,
post: compShader
})
.then(value => { visual = value; window.Repaint(); })
.catch(error => console.log("MilkDrop 0101: " + error));
function on_paint(gr) {
const w = window.Width, h = window.Height;
gr.FillSolidRect(0, 0, w, h, 0xff000000);
if (visual && w > 0 && h > 0) visual.Draw(gr, 0, 0, w, h);
}
setInterval(() => window.Repaint(), FRAME_INTERVAL_MS);