/shader-convert — GLSL to HLSL Shader Conversion
Convert Shadertoy GLSL shaders to the Alt-Tabby HLSL pixel shader format.
Invocation
/shader-convert — Scan src/shaders/ (and subdirs mouse/, selection/) for any .glsl without matching .hlsl, convert all
/shader-convert <Shadertoy URL> — Fetch shader from Shadertoy via Playwright, then convert
/shader-convert <pasted GLSL + metadata> — Convert manually pasted shader source
Not supported: Multi-buffer shaders (Buffer A/B/C/D tabs). Our pipeline is single-pass only. Skip shaders that require inter-frame feedback or multi-pass rendering.
Three Modes
Mode A — Scan Directory (no args)
- Scan
src/shaders/ and subdirs src/shaders/mouse/, src/shaders/selection/ for any name.glsl that has no matching name.hlsl
- For each unconverted shader: convert, create
.hlsl (and .json if missing)
- Mouse shaders go in
src/shaders/mouse/, selection shaders in src/shaders/selection/, background shaders in src/shaders/
Mode B — Shadertoy URL (arg matches shadertoy.com/view/)
Requires the Playwright MCP server. Extracts shader source, metadata, and iChannel textures automatically.
Step 0: Check for Playwright MCP
The Playwright MCP is not loaded by default — it's toggled on-demand to avoid context bloat.
Check if you have access to mcp__playwright__browser_navigate. If NOT available:
- Tell the user: "Playwright MCP is not enabled. Run this to enable it, then restart Claude Code:"
powershell -File tools/toggle-playwright-mcp.ps1 on
- STOP — do not proceed until the user restarts with Playwright available.
Step 1: Navigate and Wait
browser_navigate → https://www.shadertoy.com/view/{id}
If Cloudflare challenge appears, wait ~10s for auto-pass. Verify the page title changes from "Shader - Shadertoy BETA" to the shader name.
Step 2: Extract All Data (single evaluate call)
() => {
const st = window.gShaderToy;
if (!st) return { error: 'gShaderToy not found' };
// Metadata
const info = st.mInfo;
// Code from each pass via CodeMirror Doc
const passes = st.mPass.map((p, i) => {
const code = p.mDocs && typeof p.mDocs.getValue === 'function'
? p.mDocs.getValue() : null;
return { index: i, code, charCount: code ? code.length : 0 };
});
// Tab names → map pass index to role
const tabNames = {};
for (let i = 0; i < 10; i++) {
const tab = document.getElementById('tab' + i);
if (tab) tabNames['tab' + i] = tab.textContent.trim();
}
// Pass types from effect renderer (image, common, buffer, sound, cubemap)
const passTypes = st.mEffect ? st.mEffect.mPasses.map((p, i) => ({
index: i, type: p.mType
})) : [];
// iChannel inputs for the Image pass
let iChannels = [];
if (st.mEffect && st.mEffect.mPasses) {
const imgPass = st.mEffect.mPasses.find(p => p.mType === 'image') || st.mEffect.mPasses[0];
if (imgPass && imgPass.mInputs) {
iChannels = imgPass.mInputs.map((inp, ch) => {
if (!inp) return null;
return JSON.parse(JSON.stringify(inp.mInfo));
});
}
}
return {
info: { name: info.name, username: info.username, description: info.description, tags: info.tags },
passes, tabNames, passTypes, iChannels
};
}
Step 3: Validate Compatibility
Check passTypes — if any pass has type of "buffer", "sound", or "cubemap":
"buffer" → STOP: Multi-buffer shader, not supported. Tell user.
"sound" → STOP: Audio-output shader, not supported.
"cubemap" → STOP: Cubemap pass, not supported.
- Only
"image" and "common" are valid.
Check iChannels for audio inputs:
- If any channel has
mType !== "texture" (e.g., "music", "musicstream", "webcam", "video", "keyboard"), note it. Audio channels will need synthetic beat replacement (see §7). Webcam/video/keyboard → skip the shader.
Step 4: Save GLSL Source
Combine passes into a single .glsl file:
- If a
"common" tab exists: put Common code first, then // --- Image --- separator, then Image code
- If only Image: save directly
- Derive
name from info.name → snake_case (e.g., "Power (Chainsaw Man)" → power_chain_saw_man)
Step 5: Download iChannel Textures
For each non-null iChannel with mType === "texture":
// In browser_run_code (needs Playwright page object for download API)
// NOTE: page.evaluate only accepts one arg — wrap multiple values in an object
async (page) => {
const textures = [
{ url: 'https://www.shadertoy.com' + mSrc0, file: 'name_i0.png' },
{ url: 'https://www.shadertoy.com' + mSrc1, file: 'name_i1.png' }
];
const results = [];
for (const tex of textures) {
const downloadPromise = page.waitForEvent('download', { timeout: 15000 });
await page.evaluate(({url, filename}) => {
return fetch(url).then(r => r.blob()).then(blob => {
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
});
}, {url: tex.url, filename: tex.file});
const download = await downloadPromise;
await download.saveAs('src/shaders/' + tex.file);
results.push(tex.file);
}
return results;
}
- Full URL:
https://www.shadertoy.com + mInfo.mSrc (e.g., /media/a/...png)
- Save as:
src/shaders/{name}_i{channel}.png
- curl won't work — Shadertoy returns 403 for direct requests (requires cookies/origin)
Step 6: Create .json Metadata
Populate from extracted info:
name → info.name
shadertoyId → the ID from the URL
author → info.username
license → "CC BY-NC-SA 3.0" (Shadertoy default)
iChannels → from downloaded textures, include filter/wrap from mSampler
Step 7: Close Browser & Convert
Close the browser (browser_close) immediately — before writing any files or starting HLSL conversion. The Playwright MCP server is a shared resource; holding it open blocks other agents. Extract all data into local variables in Steps 2-5, then close the browser as the very first action in this step.
Proceed to HLSL conversion (same as Mode C).
Step 8: Remind User to Disable Playwright MCP
After Mode B is fully complete (conversion, bundle, compile, tests), remind the user:
"Playwright MCP is still enabled and consuming context. To disable it for future sessions, run:"
powershell -File tools/toggle-playwright-mcp.ps1 off
Mode C — Paste GLSL (with non-URL args)
- Ask for a shader name if not obvious from the source
- Write
src/shaders/name.glsl with the pasted source
- Create
src/shaders/name.json with metadata (prompt for Shadertoy URL/author if not provided)
- Convert to
src/shaders/name.hlsl
Conversion Steps (all modes)
1. Mechanical Type Conversions
| GLSL |
HLSL |
vec2, vec3, vec4 |
float2, float3, float4 |
ivec2, ivec3, ivec4 |
int2, int3, int4 |
mat2, mat3, mat4 |
float2x2, float3x3, float4x4 |
fract() |
frac() |
mod(a, b) |
fmod(a, b) |
mix(a, b, t) |
lerp(a, b, t) |
texture(sampler, uv) |
tex.Sample(samplerState, uv) |
texelFetch(sampler, coord, lod) |
tex.Load(int3(coord, lod)) |
atan(y, x) |
atan2(y, x) |
dFdx(), dFdy() |
ddx(), ddy() |
2. Constructor Broadcasts
GLSL allows vec3(x) as shorthand for vec3(x, x, x). HLSL does too with float3(x, x, x) or (float3)x.
3. Replace Shadertoy Uniforms
| Shadertoy |
HLSL cbuffer |
iTime |
time |
iResolution.xy |
resolution |
iResolution (vec3) |
float3(resolution, 1.0) |
iTimeDelta |
timeDelta |
iFrame |
frame |
fragCoord |
input.pos.xy (from SV_Position) |
fragColor |
return value of PSMain |
4. Entry Point Wrapper
Replace void mainImage(out vec4 fragColor, in vec2 fragCoord) with:
// PSInput and cbuffer are provided by alt_tabby_common.hlsl (prepended automatically)
float4 PSMain(PSInput input) : SV_Target {
float2 fragCoord = input.pos.xy;
// ... converted body ...
return AT_PostProcess(color); // instead of manual post-processing
}
Y-axis flip: Shadertoy's fragCoord.y = 0 is at the bottom of the screen; SV_Position.y = 0 is at the top. For shaders with gravity, falling particles, directional motion, or any up/down asymmetry, flip Y at the start:
float2 fragCoord = float2(input.pos.x, resolution.y - input.pos.y);
Symmetric shaders (noise fields, clouds, fractals) usually don't need the flip.
5. Constant Buffer Header
The cbuffer and PSInput struct are provided by alt_tabby_common.hlsl, which is prepended automatically before compilation. Do NOT include them in the .hlsl file. The common header provides:
cbuffer Constants : register(b0) with all uniforms (144 bytes, 9 × 16-byte rows):
- Core:
time (float), resolution (float2), timeDelta (float), frame (uint), darken (float), desaturate (float), opacity (float)
- Mouse:
iMouse (float2, cursor px), iMouseVel (float2, velocity px/sec), iMouseSpeed (float, magnitude of velocity)
- Grid/Compute:
gridW (uint, grid width, 0 = no grid), gridH (uint, grid height, 0 = no grid), maxParticles (uint, particle slots excluding grid cells), reactivity (float, cursor force multiplier)
- Selection:
selRect (float4, x/y/w/h), selColor (float4, premul RGBA), borderColor (float4, premul RGBA), borderWidth (float), isHovered (float, intensity: 1.0 = full selected, <1.0 = dimmed for hover), entranceT (float), selGlow (float, outer glow radius multiplier), selIntensity (float, effect blend strength), rowRadius (float, user's RowRadius in pixels, 0 = shader decides)
struct PSInput with SV_Position and TEXCOORD0
AT_PostProcess(float3 col) and AT_PostProcess(float3 col, float customAlpha) functions
All fields are populated every frame for all shader categories. Background shaders typically use only core fields. Mouse shaders use core + mouse fields. Selection shaders use core + selection fields.
Compute-enabled mouse shaders access the same cbuffer via register(b0) in both CSMain and PSMain. The common header is prepended for both entry points during compilation.
iChannel Texture2D/SamplerState declarations still go in the individual .hlsl file (they vary per shader and are NOT in the common header).
6. Alpha Handling
Standard shaders (alpha from brightness): End PSMain with return AT_PostProcess(color);
Shaders with custom alpha (transparency masks, particle alpha, etc.): End PSMain with return AT_PostProcess(color, customAlpha);
AT_PostProcess handles darken, desaturate, opacity multiplication, and premultiplied alpha output. Do NOT write these manually.
7. Audio Channels
Shadertoy shaders may use iChannel0..3 for audio input (spectrum/waveform) or produce audio output via a "Sound" tab.
Audio input (e.g., texture(iChannel0, vec2(freq, 0.0)).r for spectrum): Replace with a gentle time-based pulse so the shader retains dynamic variation without requiring audio hardware:
float getBeat() {
return smoothstep(0.6, 0.9, pow(sin(time * 1.5) * 0.5 + 0.5, 4.0)) * 0.3;
}
Adjust frequency/amplitude to match how the original used the audio data (subtle background pulse vs heavy bass reactivity).
Audio output ("Sound" tab shaders): Remove entirely. Alt-Tabby is visual only.
8. Mouse Input (iMouse)
Shadertoy provides iMouse (pixel coordinates, click state). Alt-Tabby provides mouse data via cbuffer:
iMouse (float2): cursor position in physical pixels
iMouseVel (float2): cursor velocity in pixels/second (smoothed, CPU-computed)
iMouseSpeed (float): magnitude of iMouseVel (convenience scalar)
For background shaders (no mouse interaction): Zero out or ignore iMouse. Set any derived mouse variables to (float2)0 and simplify away dead code. If mouse is the sole camera control, replace with a time-based sweep:
float2 fakeMouse = float2(
sin(time * 0.1) * 0.3,
cos(time * 0.07) * 0.2
);
For mouse-category shaders: Use iMouse for cursor position, iMouseVel for direction-dependent effects (particles trailing behind cursor), and iMouseSpeed for motion gating (e.g., if (iMouseSpeed < threshold) return zero). Speed-gated effects should use smoothstep() for gradual activation rather than hard cutoffs.
9. iChannel Textures
If the shader uses iChannel0..3:
- Save texture PNGs as
src/shaders/name_i0.png, name_i1.png, etc.
- Add entries to the
.json metadata:"iChannels": [{"index": 0, "file": "name_i0.png", "filter": "linear", "wrap": "repeat"}]
- In HLSL, declare textures:
Texture2D iChannel0 : register(t0);
SamplerState samp0 : register(s0);
- Replace
texture(iChannelN, uv) with iChannelN.Sample(sampN, uv)
Final Steps (always, after all conversions)
Run the bundle script:
powershell -File tools/shader_bundle.ps1
Compile shaders to DXBC:
powershell -File tools/shader_compile.ps1
Run tests:
.\tests\test.ps1
.json Metadata Format
{
"name": "Display Name",
"shadertoyId": "XXXXXX",
"author": "Author Name",
"license": "CC BY-NC-SA 3.0",
"opacity": 0.50,
"iChannels": [],
"timeOffsetMin": 40,
"timeOffsetMax": 120,
"timeAccumulate": true
}
opacity: Default layer opacity when compositing (0.0-1.0)
iChannels: Array of texture references (empty if no textures needed)
timeOffsetMin: (optional) Minimum random time offset in seconds. Skips the shader's warmup period so it looks interesting immediately. Falls back to config ShaderTimeOffsetMin (default 30) if omitted.
timeOffsetMax: (optional) Maximum random time offset in seconds. Falls back to config ShaderTimeOffsetMax (default 90) if omitted. Set higher for shaders with long warmup (e.g., volumetric fog needs 40-120s).
timeAccumulate: (optional) When true, shader time persists across overlay show/hide so it picks up where it left off. Falls back to config ShaderTimeAccumulate (default true) if omitted. Set false for shaders with a deliberate intro animation you want to see each time.
category: (optional) "mouse" or "selection" for shaders in subdirectories. Background shaders (root dir) omit this field.
compute: (optional) { "maxParticles": N, "particleStride": 32, "baseParticles": M }. When present, the shader is compiled as a compute+pixel pair. CS entry point is CSMain (compiled with cs_5_0), PS entry point is PSMain (compiled with ps_5_0), both in the same .hlsl file. The compute shader writes to RWStructuredBuffer via UAV; the pixel shader reads the same buffer as StructuredBuffer at register(t4). Only used for mouse-category shaders that need persistent GPU-side state (particles, waves).
baseParticles is the shader's intrinsic particle count (0 for pure-grid fluids, 128/384 for particle+grid). maxParticles in the JSON is the default total (= baseParticles + gridW * gridH at high quality). At runtime, actual buffer size is computed from baseParticles * ParticleDensity + gridW * gridH where grid dimensions come from GridQuality config.
Add time fields when the shader has a notable warmup period or deliberate intro. Omit them for shaders that look good immediately at any time value.
Note on shader models: Compute shaders require cs_5_0 profile (DX11 feature level 11_0). Compute-paired pixel shaders use ps_5_0. Background and selection shaders remain ps_4_0 for maximum compatibility.
Compute Shader Architectures
Compute-enabled mouse shaders follow one of three buffer layout patterns:
- Particle + Grid (ember_trail, ember_trail_long, campfire_embers, smoke_trail, fireflies, scatter, gravity_well, neon_trail): Buffer =
[particles 0..N-1] [grid cells N..N+W*H-1]. CS particle threads do physics, CS grid threads accumulate nearby particles. PS bilinear-samples the grid. Grid cells reuse the Particle struct: pos.xy = accumulated RG, vel.xy = accumulated BA.
- Pure Grid / Fluid (fluid_aquarium, fluid_calm, fluid_emit, water_surface): Buffer =
[grid cells 0..W*H-1]. CS runs wave/fluid equations. PS samples the grid.
- No Grid (ripple): Buffer =
[slots 0..N-1]. CS manages slot lifecycle. PS loops over all slots.
Shader Categories
Shaders are organized into three categories by directory:
- Background shaders (
src/shaders/): Composited as stackable layers behind the window list. Up to 4 layers.
- Mouse shaders (
src/shaders/mouse/): Single-slot effect receiving cursor data: iMouse (position), iMouseVel (velocity px/sec), iMouseSpeed (speed magnitude). Add "category": "mouse" to JSON.
- Selection shaders (
src/shaders/selection/): Single-slot effect for row selection highlight. Receives selRect, selColor, borderColor, borderWidth, isHovered, entranceT, selGlow, selIntensity, rowRadius via cbuffer. isHovered is an intensity multiplier (1.0 = full selected, <1.0 = dimmed for hover reuse) — use it directly as float intensity = isHovered;. Do NOT hardcode hover dimming (e.g., lerp(1.0, 0.45, isHovered) is wrong). For corner radius, use rowRadius > 0.0 ? rowRadius : min(hs.x, hs.y) * 0.15 so the shader respects the user's RowRadius setting while falling back to 15% of the smaller half-dimension. Add "category": "selection" to JSON.
1---2name: shader-convert3description: Convert Shadertoy GLSL shaders to HLSL for Alt-Tabby's D3D11 pipeline4---5
6# /shader-convert — GLSL to HLSL Shader Conversion
7
8Convert Shadertoy GLSL shaders to the Alt-Tabby HLSL pixel shader format.
9
10## Invocation
11
12- `/shader-convert` — Scan `src/shaders/` (and subdirs `mouse/`, `selection/`) for any `.glsl` without matching `.hlsl`, convert all
13- `/shader-convert <Shadertoy URL>` — Fetch shader from Shadertoy via Playwright, then convert
14- `/shader-convert <pasted GLSL + metadata>` — Convert manually pasted shader source
15
16**Not supported:** Multi-buffer shaders (Buffer A/B/C/D tabs). Our pipeline is single-pass only. Skip shaders that require inter-frame feedback or multi-pass rendering.
17
18## Three Modes
19
20### Mode A — Scan Directory (no args)
21
221. Scan `src/shaders/` and subdirs `src/shaders/mouse/`, `src/shaders/selection/` for any `name.glsl` that has no matching `name.hlsl`
232. For each unconverted shader: convert, create `.hlsl` (and `.json` if missing)
243. Mouse shaders go in `src/shaders/mouse/`, selection shaders in `src/shaders/selection/`, background shaders in `src/shaders/`
25
26### Mode B — Shadertoy URL (arg matches `shadertoy.com/view/`)
27
28Requires the Playwright MCP server. Extracts shader source, metadata, and iChannel textures automatically.
29
30#### Step 0: Check for Playwright MCP
31
32The Playwright MCP is **not loaded by default** — it's toggled on-demand to avoid context bloat.
33
34Check if you have access to `mcp__playwright__browser_navigate`. If NOT available:
351. Tell the user: "Playwright MCP is not enabled. Run this to enable it, then restart Claude Code:"
36 ```
37 powershell -File tools/toggle-playwright-mcp.ps1 on
38 ```
392. **STOP** — do not proceed until the user restarts with Playwright available.
40
41#### Step 1: Navigate and Wait
42
43```
44browser_navigate → https://www.shadertoy.com/view/{id}
45```
46
47If Cloudflare challenge appears, wait ~10s for auto-pass. Verify the page title changes from "Shader - Shadertoy BETA" to the shader name.
48
49#### Step 2: Extract All Data (single evaluate call)
50
51```javascript
52() => {
53 const st = window.gShaderToy;
54 if (!st) return { error: 'gShaderToy not found' };
55
56 // Metadata
57 const info = st.mInfo;
58
59 // Code from each pass via CodeMirror Doc
60 const passes = st.mPass.map((p, i) => {
61 const code = p.mDocs && typeof p.mDocs.getValue === 'function'
62 ? p.mDocs.getValue() : null;
63 return { index: i, code, charCount: code ? code.length : 0 };
64 });
65
66 // Tab names → map pass index to role
67 const tabNames = {};
68 for (let i = 0; i < 10; i++) {
69 const tab = document.getElementById('tab' + i);
70 if (tab) tabNames['tab' + i] = tab.textContent.trim();
71 }
72
73 // Pass types from effect renderer (image, common, buffer, sound, cubemap)
74 const passTypes = st.mEffect ? st.mEffect.mPasses.map((p, i) => ({
75 index: i, type: p.mType
76 })) : [];
77
78 // iChannel inputs for the Image pass
79 let iChannels = [];
80 if (st.mEffect && st.mEffect.mPasses) {
81 const imgPass = st.mEffect.mPasses.find(p => p.mType === 'image') || st.mEffect.mPasses[0];
82 if (imgPass && imgPass.mInputs) {
83 iChannels = imgPass.mInputs.map((inp, ch) => {
84 if (!inp) return null;
85 return JSON.parse(JSON.stringify(inp.mInfo));
86 });
87 }
88 }
89
90 return {
91 info: { name: info.name, username: info.username, description: info.description, tags: info.tags },
92 passes, tabNames, passTypes, iChannels
93 };
94}
95```
96
97#### Step 3: Validate Compatibility
98
99Check `passTypes` — if any pass has `type` of `"buffer"`, `"sound"`, or `"cubemap"`:
100- `"buffer"` → **STOP**: Multi-buffer shader, not supported. Tell user.
101- `"sound"` → **STOP**: Audio-output shader, not supported.
102- `"cubemap"` → **STOP**: Cubemap pass, not supported.
103- Only `"image"` and `"common"` are valid.
104
105Check `iChannels` for audio inputs:
106- If any channel has `mType` !== `"texture"` (e.g., `"music"`, `"musicstream"`, `"webcam"`, `"video"`, `"keyboard"`), note it. Audio channels will need synthetic beat replacement (see §7). Webcam/video/keyboard → skip the shader.
107
108#### Step 4: Save GLSL Source
109
110Combine passes into a single `.glsl` file:
111- If a `"common"` tab exists: put Common code first, then `// --- Image ---` separator, then Image code
112- If only Image: save directly
113- Derive `name` from `info.name` → snake_case (e.g., "Power (Chainsaw Man)" → `power_chain_saw_man`)
114
115#### Step 5: Download iChannel Textures
116
117For each non-null iChannel with `mType === "texture"`:
118
119```javascript
120// In browser_run_code (needs Playwright page object for download API)
121// NOTE: page.evaluate only accepts one arg — wrap multiple values in an object
122async (page) => {
123 const textures = [
124 { url: 'https://www.shadertoy.com' + mSrc0, file: 'name_i0.png' },
125 { url: 'https://www.shadertoy.com' + mSrc1, file: 'name_i1.png' }
126 ];
127 const results = [];
128 for (const tex of textures) {
129 const downloadPromise = page.waitForEvent('download', { timeout: 15000 });
130 await page.evaluate(({url, filename}) => {
131 return fetch(url).then(r => r.blob()).then(blob => {
132 const blobUrl = URL.createObjectURL(blob);
133 const a = document.createElement('a');
134 a.href = blobUrl;
135 a.download = filename;
136 document.body.appendChild(a);
137 a.click();
138 document.body.removeChild(a);
139 URL.revokeObjectURL(blobUrl);
140 });
141 }, {url: tex.url, filename: tex.file});
142 const download = await downloadPromise;
143 await download.saveAs('src/shaders/' + tex.file);
144 results.push(tex.file);
145 }
146 return results;
147}
148```
149
150- Full URL: `https://www.shadertoy.com` + `mInfo.mSrc` (e.g., `/media/a/...png`)
151- Save as: `src/shaders/{name}_i{channel}.png`
152- **curl won't work** — Shadertoy returns 403 for direct requests (requires cookies/origin)
153
154#### Step 6: Create .json Metadata
155
156Populate from extracted `info`:
157- `name` → `info.name`
158- `shadertoyId` → the ID from the URL
159- `author` → `info.username`
160- `license` → `"CC BY-NC-SA 3.0"` (Shadertoy default)
161- `iChannels` → from downloaded textures, include `filter`/`wrap` from `mSampler`
162
163#### Step 7: Close Browser & Convert
164
165**Close the browser (`browser_close`) immediately** — before writing any files or starting HLSL conversion. The Playwright MCP server is a shared resource; holding it open blocks other agents. Extract all data into local variables in Steps 2-5, then close the browser as the very first action in this step.
166
167Proceed to HLSL conversion (same as Mode C).
168
169#### Step 8: Remind User to Disable Playwright MCP
170
171After Mode B is fully complete (conversion, bundle, compile, tests), remind the user:
172
173"Playwright MCP is still enabled and consuming context. To disable it for future sessions, run:"
174```
175powershell -File tools/toggle-playwright-mcp.ps1 off
176```
177
178### Mode C — Paste GLSL (with non-URL args)
179
1801. Ask for a shader name if not obvious from the source
1812. Write `src/shaders/name.glsl` with the pasted source
1823. Create `src/shaders/name.json` with metadata (prompt for Shadertoy URL/author if not provided)
1834. Convert to `src/shaders/name.hlsl`
184
185## Conversion Steps (all modes)
186
187### 1. Mechanical Type Conversions
188
189| GLSL | HLSL |
190|------|------|
191| `vec2`, `vec3`, `vec4` | `float2`, `float3`, `float4` |
192| `ivec2`, `ivec3`, `ivec4` | `int2`, `int3`, `int4` |
193| `mat2`, `mat3`, `mat4` | `float2x2`, `float3x3`, `float4x4` |
194| `fract()` | `frac()` |
195| `mod(a, b)` | `fmod(a, b)` |
196| `mix(a, b, t)` | `lerp(a, b, t)` |
197| `texture(sampler, uv)` | `tex.Sample(samplerState, uv)` |
198| `texelFetch(sampler, coord, lod)` | `tex.Load(int3(coord, lod))` |
199| `atan(y, x)` | `atan2(y, x)` |
200| `dFdx()`, `dFdy()` | `ddx()`, `ddy()` |
201
202### 2. Constructor Broadcasts
203
204GLSL allows `vec3(x)` as shorthand for `vec3(x, x, x)`. HLSL does too with `float3(x, x, x)` or `(float3)x`.
205
206### 3. Replace Shadertoy Uniforms
207
208| Shadertoy | HLSL cbuffer |
209|-----------|-------------|
210| `iTime` | `time` |
211| `iResolution.xy` | `resolution` |
212| `iResolution` (vec3) | `float3(resolution, 1.0)` |
213| `iTimeDelta` | `timeDelta` |
214| `iFrame` | `frame` |
215| `fragCoord` | `input.pos.xy` (from SV_Position) |
216| `fragColor` | return value of PSMain |
217
218### 4. Entry Point Wrapper
219
220Replace `void mainImage(out vec4 fragColor, in vec2 fragCoord)` with:
221
222```hlsl
223// PSInput and cbuffer are provided by alt_tabby_common.hlsl (prepended automatically)
224
225float4 PSMain(PSInput input) : SV_Target {
226 float2 fragCoord = input.pos.xy;
227 // ... converted body ...
228 return AT_PostProcess(color); // instead of manual post-processing
229}
230```
231
232**Y-axis flip:** Shadertoy's `fragCoord.y = 0` is at the **bottom** of the screen; `SV_Position.y = 0` is at the **top**. For shaders with gravity, falling particles, directional motion, or any up/down asymmetry, flip Y at the start:
233
234```hlsl
235float2 fragCoord = float2(input.pos.x, resolution.y - input.pos.y);
236```
237
238Symmetric shaders (noise fields, clouds, fractals) usually don't need the flip.
239
240### 5. Constant Buffer Header
241
242The cbuffer and PSInput struct are provided by `alt_tabby_common.hlsl`, which is prepended automatically before compilation. **Do NOT include them in the `.hlsl` file.** The common header provides:
243- `cbuffer Constants : register(b0)` with all uniforms (144 bytes, 9 × 16-byte rows):
244 - **Core**: `time` (float), `resolution` (float2), `timeDelta` (float), `frame` (uint), `darken` (float), `desaturate` (float), `opacity` (float)
245 - **Mouse**: `iMouse` (float2, cursor px), `iMouseVel` (float2, velocity px/sec), `iMouseSpeed` (float, magnitude of velocity)
246 - **Grid/Compute**: `gridW` (uint, grid width, 0 = no grid), `gridH` (uint, grid height, 0 = no grid), `maxParticles` (uint, particle slots excluding grid cells), `reactivity` (float, cursor force multiplier)
247 - **Selection**: `selRect` (float4, x/y/w/h), `selColor` (float4, premul RGBA), `borderColor` (float4, premul RGBA), `borderWidth` (float), `isHovered` (float, intensity: 1.0 = full selected, <1.0 = dimmed for hover), `entranceT` (float), `selGlow` (float, outer glow radius multiplier), `selIntensity` (float, effect blend strength), `rowRadius` (float, user's RowRadius in pixels, 0 = shader decides)
248- `struct PSInput` with `SV_Position` and `TEXCOORD0`
249- `AT_PostProcess(float3 col)` and `AT_PostProcess(float3 col, float customAlpha)` functions
250
251All fields are populated every frame for all shader categories. Background shaders typically use only core fields. Mouse shaders use core + mouse fields. Selection shaders use core + selection fields.
252
253Compute-enabled mouse shaders access the same cbuffer via `register(b0)` in both `CSMain` and `PSMain`. The common header is prepended for both entry points during compilation.
254
255iChannel `Texture2D`/`SamplerState` declarations still go in the individual `.hlsl` file (they vary per shader and are NOT in the common header).
256
257### 6. Alpha Handling
258
259**Standard shaders** (alpha from brightness): End PSMain with `return AT_PostProcess(color);`
260
261**Shaders with custom alpha** (transparency masks, particle alpha, etc.): End PSMain with `return AT_PostProcess(color, customAlpha);`
262
263`AT_PostProcess` handles darken, desaturate, opacity multiplication, and premultiplied alpha output. Do NOT write these manually.
264
265### 7. Audio Channels
266
267Shadertoy shaders may use `iChannel0..3` for audio input (spectrum/waveform) or produce audio output via a "Sound" tab.
268
269- **Audio input** (e.g., `texture(iChannel0, vec2(freq, 0.0)).r` for spectrum): Replace with a gentle time-based pulse so the shader retains dynamic variation without requiring audio hardware:
270 ```hlsl
271 float getBeat() {
272 return smoothstep(0.6, 0.9, pow(sin(time * 1.5) * 0.5 + 0.5, 4.0)) * 0.3;
273 }
274 ```
275 Adjust frequency/amplitude to match how the original used the audio data (subtle background pulse vs heavy bass reactivity).
276
277- **Audio output** ("Sound" tab shaders): Remove entirely. Alt-Tabby is visual only.
278
279### 8. Mouse Input (iMouse)
280
281Shadertoy provides `iMouse` (pixel coordinates, click state). Alt-Tabby provides mouse data via cbuffer:
282- `iMouse` (float2): cursor position in physical pixels
283- `iMouseVel` (float2): cursor velocity in pixels/second (smoothed, CPU-computed)
284- `iMouseSpeed` (float): magnitude of `iMouseVel` (convenience scalar)
285
286**For background shaders** (no mouse interaction): Zero out or ignore `iMouse`. Set any derived mouse variables to `(float2)0` and simplify away dead code. If mouse is the sole camera control, replace with a time-based sweep:
287 ```hlsl
288 float2 fakeMouse = float2(
289 sin(time * 0.1) * 0.3,
290 cos(time * 0.07) * 0.2
291 );
292 ```
293
294**For mouse-category shaders**: Use `iMouse` for cursor position, `iMouseVel` for direction-dependent effects (particles trailing behind cursor), and `iMouseSpeed` for motion gating (e.g., `if (iMouseSpeed < threshold) return zero`). Speed-gated effects should use `smoothstep()` for gradual activation rather than hard cutoffs.
295
296### 9. iChannel Textures
297
298If the shader uses `iChannel0..3`:
299
3001. Save texture PNGs as `src/shaders/name_i0.png`, `name_i1.png`, etc.
3012. Add entries to the `.json` metadata:
302 ```json
303 "iChannels": [{"index": 0, "file": "name_i0.png", "filter": "linear", "wrap": "repeat"}]
304 ```
3053. In HLSL, declare textures:
306 ```hlsl
307 Texture2D iChannel0 : register(t0);
308 SamplerState samp0 : register(s0);
309 ```
3104. Replace `texture(iChannelN, uv)` with `iChannelN.Sample(sampN, uv)`
311
312## Final Steps (always, after all conversions)
313
31410. Run the bundle script:
315 ```
316 powershell -File tools/shader_bundle.ps1
317 ```
318
31911. Compile shaders to DXBC:
320 ```
321 powershell -File tools/shader_compile.ps1
322 ```
323
32412. Run tests:
325 ```
326 .\tests\test.ps1
327 ```
328
329## .json Metadata Format
330
331```json
332{
333 "name": "Display Name",
334 "shadertoyId": "XXXXXX",
335 "author": "Author Name",
336 "license": "CC BY-NC-SA 3.0",
337 "opacity": 0.50,
338 "iChannels": [],
339 "timeOffsetMin": 40,
340 "timeOffsetMax": 120,
341 "timeAccumulate": true
342}
343```
344
345- `opacity`: Default layer opacity when compositing (0.0-1.0)
346- `iChannels`: Array of texture references (empty if no textures needed)
347- `timeOffsetMin`: (optional) Minimum random time offset in seconds. Skips the shader's warmup period so it looks interesting immediately. Falls back to config `ShaderTimeOffsetMin` (default 30) if omitted.
348- `timeOffsetMax`: (optional) Maximum random time offset in seconds. Falls back to config `ShaderTimeOffsetMax` (default 90) if omitted. Set higher for shaders with long warmup (e.g., volumetric fog needs 40-120s).
349- `timeAccumulate`: (optional) When true, shader time persists across overlay show/hide so it picks up where it left off. Falls back to config `ShaderTimeAccumulate` (default true) if omitted. Set false for shaders with a deliberate intro animation you want to see each time.
350- `category`: (optional) `"mouse"` or `"selection"` for shaders in subdirectories. Background shaders (root dir) omit this field.
351- `compute`: (optional) `{ "maxParticles": N, "particleStride": 32, "baseParticles": M }`. When present, the shader is compiled as a compute+pixel pair. CS entry point is `CSMain` (compiled with `cs_5_0`), PS entry point is `PSMain` (compiled with `ps_5_0`), both in the same `.hlsl` file. The compute shader writes to `RWStructuredBuffer` via UAV; the pixel shader reads the same buffer as `StructuredBuffer` at `register(t4)`. Only used for mouse-category shaders that need persistent GPU-side state (particles, waves).
352 - `baseParticles` is the shader's intrinsic particle count (0 for pure-grid fluids, 128/384 for particle+grid). `maxParticles` in the JSON is the default total (= baseParticles + gridW * gridH at high quality). At runtime, actual buffer size is computed from `baseParticles * ParticleDensity + gridW * gridH` where grid dimensions come from `GridQuality` config.
353
354Add time fields when the shader has a notable warmup period or deliberate intro. Omit them for shaders that look good immediately at any time value.
355
356**Note on shader models:** Compute shaders require `cs_5_0` profile (DX11 feature level 11_0). Compute-paired pixel shaders use `ps_5_0`. Background and selection shaders remain `ps_4_0` for maximum compatibility.
357
358### Compute Shader Architectures
359
360Compute-enabled mouse shaders follow one of three buffer layout patterns:
361
362- **Particle + Grid** (ember_trail, ember_trail_long, campfire_embers, smoke_trail, fireflies, scatter, gravity_well, neon_trail): Buffer = `[particles 0..N-1] [grid cells N..N+W*H-1]`. CS particle threads do physics, CS grid threads accumulate nearby particles. PS bilinear-samples the grid. Grid cells reuse the Particle struct: `pos.xy` = accumulated RG, `vel.xy` = accumulated BA.
363- **Pure Grid / Fluid** (fluid_aquarium, fluid_calm, fluid_emit, water_surface): Buffer = `[grid cells 0..W*H-1]`. CS runs wave/fluid equations. PS samples the grid.
364- **No Grid** (ripple): Buffer = `[slots 0..N-1]`. CS manages slot lifecycle. PS loops over all slots.
365
366### Shader Categories
367
368Shaders are organized into three categories by directory:
369- **Background shaders** (`src/shaders/`): Composited as stackable layers behind the window list. Up to 4 layers.
370- **Mouse shaders** (`src/shaders/mouse/`): Single-slot effect receiving cursor data: `iMouse` (position), `iMouseVel` (velocity px/sec), `iMouseSpeed` (speed magnitude). Add `"category": "mouse"` to JSON.
371- **Selection shaders** (`src/shaders/selection/`): Single-slot effect for row selection highlight. Receives `selRect`, `selColor`, `borderColor`, `borderWidth`, `isHovered`, `entranceT`, `selGlow`, `selIntensity`, `rowRadius` via cbuffer. `isHovered` is an intensity multiplier (1.0 = full selected, <1.0 = dimmed for hover reuse) — use it directly as `float intensity = isHovered;`. Do NOT hardcode hover dimming (e.g., `lerp(1.0, 0.45, isHovered)` is wrong). For corner radius, use `rowRadius > 0.0 ? rowRadius : min(hs.x, hs.y) * 0.15` so the shader respects the user's RowRadius setting while falling back to 15% of the smaller half-dimension. Add `"category": "selection"` to JSON.