Porting a Shadertoy shader to Unity (GLSL to HLSL): a conversion guide
Shadertoy is the best place to find inspiring shaders, and Unity is where many people want to use them. The languages look different at first glance, but Shadertoy code is GLSL and Unity's is HLSL, and they are close relatives. Most of the work in a port is renaming. A few differences really can break the result, so this guide covers both.
Step 1: the straight renames
These are the same operation under a different name:
| GLSL (Shadertoy) | HLSL (Unity) |
|---|---|
vec2, vec3, vec4 | float2, float3, float4 |
mat2, mat3, mat4 | float2x2, float3x3, float4x4 |
mix(a, b, t) | lerp(a, b, t) |
fract(x) | frac(x) |
atan(y, x) | atan2(y, x) |
clamp(x, 0.0, 1.0) | saturate(x) (or keep clamp, both work) |
inversesqrt(x) | rsqrt(x) |
dFdx(x), dFdy(x) | ddx(x), ddy(x) |
Everything else you commonly see (sin, cos, pow, dot, cross, normalize, length, smoothstep, step, min, max, abs, floor) has the same name and meaning.
Step 2: the Shadertoy inputs
Shadertoy gives your code special variables. In Unity you supply the equivalents yourself:
- iTime becomes
_Time.y, a built-in that holds the time in seconds (the four values of_Timeare t/20, t, 2t and 3t). - iResolution.xy becomes
_ScreenParams.xyfor the screen, or the size of the texture you are drawing into. - fragCoord (the pixel position) is easiest to replace with a UV from the mesh: multiply it by the size to get pixels, or divide pixels by the size to get UV.
- iChannel0...3 (textures) become
sampler2Dproperties that you assign in the material.
The entry point changes too. Shadertoy writes void mainImage(out vec4 fragColor, in vec2 fragCoord) and assigns to fragColor. In Unity your fragment function returns the color.
A worked example
Here is a small Shadertoy shader, a color-cycling gradient:
// Shadertoy (GLSL)
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));
fragColor = vec4(col, 1.0);
}
and the Unity version of the fragment function:
// Unity (HLSL), inside a shader pass
float4 frag(float2 uv : TEXCOORD0) : SV_Target
{
float3 col = 0.5 + 0.5 * cos(_Time.y + uv.xyx + float3(0, 2, 4));
return float4(col, 1.0);
}
Only three things changed: the entry point, the types, and the names of the inputs. The maths in the middle is identical. If you would rather not wire up the surrounding shader file by hand, ShaderBattle's export button writes a complete Unity file for you: write the effect in the editor, and it wraps your function in the boilerplate.
Step 3: the differences that actually bite
mod is not fmod for negative numbers
GLSL mod(x, y) returns x - y * floor(x / y), which is always positive when y is positive. HLSL fmod(x, y) keeps the sign of x, like the % operator in C. For positive numbers they agree; for negative numbers they do not, and tiling patterns break in just the places where coordinates go below zero. Write the GLSL definition by hand:
float mod(float x, float y) { return x - y * floor(x / y); }
Matrix multiplication reads the other way
In GLSL you write M * v. In HLSL you call mul(M, v), and the order of the arguments matters. If a rotation spins the wrong way after porting, try swapping the arguments to mul. This site's matrices lesson explains how the two conventions relate.
Integers and floats
Strict GLSL (including WebGL 2) refuses to mix 1 and 1.0 in many places, which is why Shadertoy code is full of 0. and 2.0. HLSL is more forgiving, so code that was already correct in GLSL will compile. Going the other way, from HLSL to GLSL, is where you meet the errors.
Which way is up?
The vertical direction of UV, and of render textures, can differ between graphics APIs and platforms. If the picture appears upside down, flip the y coordinate. Test with a plain gradient first so you know what you are seeing.
Loops and arrays
Many Shadertoy shaders use large loops (ray marching, fractals) that are fine on a desktop GPU but slow on a phone. When you port for mobile, lower the iteration counts and test on real hardware. See the performance guide for what to look for.
A porting checklist
- Rename the types and functions using the table.
- Replace
mainImagewith a function that returns the color. - Replace
iTime,iResolutionandfragCoordwith Unity's equivalents. - Replace GLSL
modwith your own version that handles negative numbers. - Check any matrix multiplication and flip the order if the result is mirrored or spun the wrong way.
- Look at a plain gradient to confirm which way is up, then compare against Shadertoy.
Learn the pieces
If a ported shader does something you do not understand, the lessons explain the building blocks one at a time, in your choice of HLSL or GLSL: start at Every pixel, one function, and see Taking it into Unity for the Unity side.
Keep reading
- What is a fragment shader? A plain explanation with code
- HLSL vs GLSL vs WGSL vs Metal: shader languages compared
- How to learn shader programming: a step-by-step roadmap
- What is ray marching? Distance fields explained simply
- Noise in shaders: value noise, fBm and what they are for
- How to make an ocean shader: waves, Fresnel and foam
- Why is my circle an oval? UV coordinates and aspect ratio in shaders
- step, lerp and smoothstep: the three functions behind most shader effects
- Signed distance functions explained: drawing shapes with math
- How to debug a shader when you cannot print anything
- Why is my shader slow? A practical guide to shader performance