Agent skill
shader-convert
Convert Shadertoy GLSL shaders to HLSL for Alt-Tabby's D3D11 pipeline
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/shader-convert
SKILL.md
/shader-convert — GLSL to HLSL Shader Conversion
Convert Shadertoy GLSL shaders to the Alt-Tabby HLSL pixel shader format.
Invocation
/shader-convert— Scansrc/shaders/(and subdirsmouse/,selection/) for any.glslwithout 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 subdirssrc/shaders/mouse/,src/shaders/selection/for anyname.glslthat has no matchingname.hlsl - For each unconverted shader: convert, create
.hlsl(and.jsonif missing) - Mouse shaders go in
src/shaders/mouse/, selection shaders insrc/shaders/selection/, background shaders insrc/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
namefrominfo.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.nameshadertoyId→ the ID from the URLauthor→info.usernamelicense→"CC BY-NC-SA 3.0"(Shadertoy default)iChannels→ from downloaded textures, includefilter/wrapfrommSampler
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.glslwith the pasted source - Create
src/shaders/name.jsonwith 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)
- Core:
struct PSInputwithSV_PositionandTEXCOORD0AT_PostProcess(float3 col)andAT_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)).rfor spectrum): Replace with a gentle time-based pulse so the shader retains dynamic variation without requiring audio hardware:hlslfloat 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 pixelsiMouseVel(float2): cursor velocity in pixels/second (smoothed, CPU-computed)iMouseSpeed(float): magnitude ofiMouseVel(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
.jsonmetadata:json"iChannels": [{"index": 0, "file": "name_i0.png", "filter": "linear", "wrap": "repeat"}] - In HLSL, declare textures:
hlsl
Texture2D iChannel0 : register(t0); SamplerState samp0 : register(s0); - Replace
texture(iChannelN, uv)withiChannelN.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 configShaderTimeOffsetMin(default 30) if omitted.timeOffsetMax: (optional) Maximum random time offset in seconds. Falls back to configShaderTimeOffsetMax(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 configShaderTimeAccumulate(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 isCSMain(compiled withcs_5_0), PS entry point isPSMain(compiled withps_5_0), both in the same.hlslfile. The compute shader writes toRWStructuredBuffervia UAV; the pixel shader reads the same buffer asStructuredBufferatregister(t4). Only used for mouse-category shaders that need persistent GPU-side state (particles, waves).baseParticlesis the shader's intrinsic particle count (0 for pure-grid fluids, 128/384 for particle+grid).maxParticlesin the JSON is the default total (= baseParticles + gridW * gridH at high quality). At runtime, actual buffer size is computed frombaseParticles * ParticleDensity + gridW * gridHwhere grid dimensions come fromGridQualityconfig.
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. ReceivesselRect,selColor,borderColor,borderWidth,isHovered,entranceT,selGlow,selIntensity,rowRadiusvia cbuffer.isHoveredis an intensity multiplier (1.0 = full selected, <1.0 = dimmed for hover reuse) — use it directly asfloat intensity = isHovered;. Do NOT hardcode hover dimming (e.g.,lerp(1.0, 0.45, isHovered)is wrong). For corner radius, userowRadius > 0.0 ? rowRadius : min(hs.x, hs.y) * 0.15so the shader respects the user's RowRadius setting while falling back to 15% of the smaller half-dimension. Add"category": "selection"to JSON.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?