Shadertoy Exporter Workflow: Preparing Shaders for Unity, Three.js, and MoreShadertoy is a vibrant playground for shader developers — a place to prototype visual effects quickly using GLSL fragment shaders and see results in real time. But demos on Shadertoy aren’t automatically ready for game engines or WebGL frameworks like Unity and Three.js. This article walks through a practical, step-by-step workflow for exporting Shadertoy shaders, adapting them for different runtimes, and optimizing them for performance and portability.
Why export Shadertoy shaders?
Shadertoy simplifies shader experimentation by providing a shared environment with common inputs (time, resolution, mouse, channel textures) and helpful visual tools. However:
- Shadertoy uses its own input conventions and helper functions.
- It assumes a single-fragment-shader environment with no built-in vertex shader.
- Resource loading, texture channels, and coordinate spaces differ between platforms.
Exporting translates a Shadertoy demo into runnable GLSL or platform-specific shader code, integrating utilities (uniforms, samplers, buffers) and resolving differences in coordinate systems, texture formats, and precision. A careful export ensures visual parity, performance, and maintainability.
Overview of the export workflow
- Audit the Shadertoy shader (inputs, buffers, channels)
- Extract and refactor core shader code
- Replace Shadertoy-specific helpers and uniforms
- Map channel textures and buffers to platform resources
- Handle coordinate systems and UV conventions
- Provide a vertex shader (if needed) and define geometry
- Integrate shader into the target (Unity, Three.js, etc.)
- Test, profile, and optimize
- Package and document
Each step contains pitfalls and platform-specific adjustments; below we expand on them with examples and practical tips.
1) Audit the Shadertoy shader
Start by understanding the shader’s structure and dependencies.
- Identify uniforms and inputs:
- iResolution, iTime, iMouse
- iChannel0..iChannel3 (textures or buffers)
- iDate, iFrame, etc.
- Identify buffers (multiple-pass setups). Buffers on Shadertoy can act as intermediate render targets used by the main pass.
- Note any helper functions or macros (e.g., noise, hashing, palette functions).
- Observe texture sampling patterns (do they use textureLod? textureSize?).
Make a simple checklist:
- Which channels are images vs. buffers vs. iChannelVideo?
- Do buffers rely on previous-frame feedback (iFrame or buffer read/write)?
- Are there assumptions about floating-point textures or filtering modes?
2) Extract and refactor core shader code
Shadertoy shaders often mix utility code, multiple pass logic, and display code. For portability:
- Separate utility functions into a shared include file (noise, math utilities).
- Isolate the main shading function (e.g., vec4 getColor(vec2 uv)).
- If the demo uses multiple passes, identify the data flow between buffers and main pass.
Refactor tips:
- Remove or replace global static variables that assume single-run semantics.
- Wrap reusable code in functions with explicit inputs (avoid relying on global iResolution inside nested helpers — pass it as a parameter if convenient).
- Comment assumptions (e.g., “expects normalized UV in [0,1]”).
3) Replace Shadertoy-specific helpers and uniforms
Shadertoy provides conveniences that don’t exist in engines. Replace them with equivalent uniforms and functions.
Common replacements:
- iResolution -> uniform vec2 u_resolution
- iTime -> uniform float u_time
- iMouse -> uniform vec4 u_mouse (or split into u_mouse and u_mouseDown)
- iFrame -> uniform int u_frame
- iDate -> struct or separate uniforms for year/month/day/time if needed
Shadertoy’s functions:
- texture(iChannelX, uv) -> texture(sampler2D iChannelX, uv) (same syntax in GLSL, but ensure sampler bindings)
- textureLod requires WebGL2 or extensions; for WebGL1 use manual mip LOD workarounds or prefiltered textures.
Provide a small uniform block example for GLSL:
uniform vec2 u_resolution; uniform float u_time; uniform vec4 u_mouse; uniform sampler2D u_channel0;
4) Map channel textures and buffers to platform resources
Channels in Shadertoy can be:
- Static images (e.g., iChannel0 = an uploaded PNG)
- Buffers (render targets from previous passes)
- Videos or webcams
Mapping guidelines:
-
Three.js
- Use THREE.Texture or THREE.VideoTexture for channels.
- For buffers, use THREE.WebGLRenderTarget and ping-pong between two render targets for feedback loops.
- Bind textures to the ShaderMaterial uniforms: { u_channel0: { value: texture } }.
-
Unity (URP/HDRP or Built-in)
- Use Material.SetTexture / Shader.SetGlobalTexture.
- For buffer passes, use RenderTextures and Blit/Graphics.Blit or CommandBuffers.
- For compute-like buffer operations in Unity, consider using a compute shader or Graphics.Blit with a pass that writes into a RenderTexture.
-
Raw WebGL
- Create textures and framebuffers for each channel.
- Bind them to texture units and set uniform samplers to the corresponding units.
Important: ensure correct texture formats and filtering. If the shader expects floating-point precision (common for buffering operations), enable floating-point textures (OES_texture_float in WebGL1 or EXT_color_buffer_float / EXT_color_buffer_half_float in WebGL2) and ensure render target support.
5) Handle coordinate systems and UV conventions
Shadertoy’s UV origin and coordinate conventions:
- fragCoord is in pixels with origin at bottom-left (gl_FragCoord convention varies).
- Many Shadertoy shaders compute uv = fragCoord.xy / iResolution.xy.
Platform differences:
- WebGL/Three.js fragment coordinates typically have origin at bottom-left for gl_FragCoord, but texture V-direction differences and HTML canvas pixel ratio can affect mapping.
- Unity’s UVs have origin at bottom-left in the shader for OpenGL, but Direct3D flips the Y on render textures in some pipelines — Unity sometimes requires flipping the Y when sampling RenderTextures.
Fixes:
- Standardize inputs: compute normalized UV using the platform’s resolution and, if necessary, flip Y with uv.y = 1.0 – uv.y when sampling textures coming from different origins.
- When using RenderTextures or framebuffers, check whether the texture is vertically flipped and correct by flipping uv.y or using appropriate texture parameters.
Example adjustment:
vec2 uv = gl_FragCoord.xy / u_resolution; #ifdef FLIP_Y uv.y = 1.0 - uv.y; #endif
6) Provide a vertex shader and define geometry
Shadertoy runs only fragment shaders, but most platforms require a vertex shader and geometry:
- Fullscreen quad: simplest approach — two triangles covering the screen.
- Triangle trick: a single large triangle can cover the screen and has fewer vertices.
Vertex shader example (GLSL):
#version 300 es in vec3 position; out vec2 v_uv; void main() { v_uv = (position.xy + 1.0) * 0.5; gl_Position = vec4(position, 1.0); }
For WebGL1, use attribute qualifiers and varyings instead of in/out.
Pass v_uv to the fragment shader and use it in place of fragCoord when appropriate:
vec2 uv = v_uv * u_resolution;
Or compute pixel coords: vec2 fragCoord = v_uv * u_resolution;
7) Integrate into target environments
Three.js (WebGL)
- Create ShaderMaterial with vertex and fragment shaders.
- Assign textures and uniforms via the material’s uniforms object.
- Use WebGLRenderTarget for buffers; implement ping-pong rendering in your render loop.
Minimal Three.js snippet:
const material = new THREE.ShaderMaterial({ uniforms: { u_time: { value: 0 }, u_resolution: { value: new THREE.Vector2(width, height) }, u_channel0: { value: texture0 } }, vertexShader, fragmentShader });
Unity
- Convert GLSL to HLSL-like syntax if using ShaderLab, or use Unity’s ShaderGraph / URP custom passes.
- For simple porting, consider using Unity’s GLSL-to-HLSL translation (note: Unity’s surface shaders and pipeline differences may require rewriting).
- Bind textures and render targets in scripts, update u_time, and call Graphics.Blit for full-screen passes.
Raw WebGL
- Set up programs, compile shaders, bind attribute buffers for a full-screen quad, set uniforms, and draw.
- For multiple-pass shaders, render to framebuffers and swap textures per pass.
8) Test, profile, and optimize
Visual parity checks:
- Compare frames side-by-side with Shadertoy at multiple resolutions and times.
- Check edge cases: mouse interactions, looping animations, buffer feedbacks.
Performance profiling:
- Use browser DevTools (Three.js/WebGL) or Unity Profiler.
- Identify expensive operations: high-cost noise, loops, high texture lookups, heavy derivatives (dFdx/dFdy), dynamic branching.
Optimization techniques:
- Lower shader precision where acceptable (mediump vs highp) for WebGL.
- Precompute constants on CPU and pass as uniforms.
- Replace iterative loops with analytic approximations if possible.
- Reduce texture lookups or use lower-resolution buffers for intermediate passes.
- MIP mapping and bilinear filtering: choose appropriate filtering for performance/quality.
- For feedback loops, consider half-resolution buffers or packing data into fewer channels.
9) Packaging and documentation
- Bundle shared utility functions as includes or modules, with clear instructions for each target platform.
- Document required graphics capabilities (WebGL2, float textures, extensions).
- Provide a mapping table of Shadertoy inputs to your uniforms and texture slots.
- Include sample code snippets and a demo scene for Unity or a web page for Three.js so other developers can test quickly.
Example mapping table:
Shadertoy name | GLSL uniform | Three.js uniform | Unity binding |
---|---|---|---|
iResolution | u_resolution (vec2) | uniforms.u_resolution | shader _Resolution |
iTime | u_time (float) | uniforms.u_time | shader _Time |
iChannel0 | u_channel0 (sampler2D) | uniforms.u_channel0 | _Channel0 |
Common pitfalls and how to avoid them
- Missing extensions: Ensure floating-point textures and rendering to float targets are supported before relying on them.
- Y-flip issues: Always verify texture coordinate orientation, especially when sampling from RenderTextures or framebuffers.
- Precision mismatches: Visual artifacts may appear if highp is downgraded unexpectedly; test on target devices.
- Cross-origin textures: For web demos, ensure textures are served with CORS headers when used in WebGL.
Example: Porting a simple procedural Shadertoy shader to Three.js
- Extract fragment main and utilities.
- Replace iResolution -> u_resolution, iTime -> u_time.
- Create a ShaderMaterial in Three.js, assign uniforms.
- Create a plane mesh covering the screen (or use full-screen triangle).
- In the render loop update u_time and render.
Three.js render-loop pseudocode:
function animate(t) { material.uniforms.u_time.value = t * 0.001; renderer.setRenderTarget(null); renderer.render(scene, camera); requestAnimationFrame(animate); }
Conclusion
Exporting Shadertoy shaders for Unity, Three.js, or raw WebGL is a manageable process when approached methodically: audit the source, refactor code, translate Shadertoy-specific inputs to platform uniforms, manage textures and buffers, handle coordinate differences, supply a vertex shader, and optimize for the target environment. With careful testing and documentation, you can preserve the creative output of Shadertoy demos and integrate them into production projects across multiple platforms.
If you want, I can convert a specific Shadertoy shader for Three.js or Unity — paste the shader and tell me which target you prefer.
Leave a Reply