Porting Simple Shadertoy Shaders to Three JS

David B.
6 min readFeb 1, 2025

--

Hi there! After porting numerous ShaderToy shaders to Three.js, I want to share the key differences and conversion processes that will help you bring those amazing ShaderToy effects into your Three.js projects.

We will look at shaders that do not use channels/image textures, since they are easiest to port

Let’s start with what makes ShaderToy shaders different from Three.js:

// ShaderToy default shader
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;

// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

// Output to screen
fragColor = vec4(col,1.0);
}

See the above code live — realtimehtml.com/glsl

To use this in Three.js, we need to make several changes.

  • Redefine UV, pass it from the vertex shader
  • Define time uniform
  • Define resolution uniform
// Three.js shader
uniform float time;
uniform vec2 resolution;

varying vec2 vUv;

void main() {
// Normalized pixel coordinates (from 0 to 1)
// In Three.js we can use vUv directly instead of fragCoord/resolution
vec2 uv = vUv;

// Time varying pixel color
// Replace iTime with time uniform
vec3 col = 0.5 + 0.5*cos(time + uv.xyx + vec3(0,2,4));

// Output to screen
gl_FragColor = vec4(col, 1.0);
}

Here’s how to set it up in Three.js:

  • Define vertex and fragment shaders
  • Define scene, PlaneGeometry, RawShaderMaterial
  • Pass the above-mentioned uniforms and varyings
<!DOCTYPE html>
<html>
<head>
<title>Three.js Shader Example</title>
<!-- disable-loop-protection -->
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>

<script type="module">
import * as THREE from 'three';

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Resize handler
function onWindowResize() {
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);

// Define shaders
const vertexShader = `in vec3 position;
void main() {
gl_Position = vec4(position, 1.0);
}`;
const fragmentShader = `precision highp float;
precision highp int;

uniform vec3 iResolution; // viewport resolution (in pixels)
uniform float iTime; // shader playback time (in seconds)
uniform float iTimeDelta; // render time (in seconds)
uniform float iFrameRate; // shader frame rate
uniform int iFrame; // shader playback frame
uniform float iChannelTime[4]; // channel playback time (in seconds)
uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click
uniform vec4 iDate; // (year, month, day, time in seconds)
out vec4 fragColor;



void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;

// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

// Output to screen
fragColor = vec4(col,1.0);
}


void main() {
mainImage(fragColor, gl_FragCoord.xy);
}`;

// Shader material
const uniforms = {
iTime: { value: 0 },
iResolution: { value: new THREE.Vector3() },
iMouse: { value: new THREE.Vector4() },
iFrame: { value: 0 },
iTimeDelta: { value: 0 },
iFrameRate: { value: 60 },
iChannelTime: { value: [0, 0, 0, 0] },
iChannelResolution: { value: [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 0)
]},
iDate: { value: new THREE.Vector4() }
};

const material = new THREE.RawShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
glslVersion: THREE.GLSL3
});

// Create a plane that fills the screen
const geometry = new THREE.PlaneGeometry(2, 2);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// Mouse handling
let mousePosition = { x: 0, y: 0, z: 0, w: 0 };
let isMouseDown = false;

renderer.domElement.addEventListener('mousemove', (e) => {
mousePosition.x = e.clientX;
mousePosition.y = window.innerHeight - e.clientY;
if (isMouseDown) {
mousePosition.z = mousePosition.x;
mousePosition.w = mousePosition.y;
}
uniforms.iMouse.value.set(mousePosition.x, mousePosition.y, mousePosition.z, mousePosition.w);
});

renderer.domElement.addEventListener('mousedown', (e) => {
isMouseDown = true;
mousePosition.z = e.clientX;
mousePosition.w = window.innerHeight - e.clientY;
uniforms.iMouse.value.set(mousePosition.x, mousePosition.y, mousePosition.z, mousePosition.w);
});

renderer.domElement.addEventListener('mouseup', () => {
isMouseDown = false;
});

// Animation loop
let startTime = Date.now();
let lastTime = startTime;
function animate() {
requestAnimationFrame(animate);

const currentTime = Date.now();
const deltaTime = (currentTime - lastTime) * 0.001; // in seconds
lastTime = currentTime;

uniforms.iTime.value = (currentTime - startTime) * 0.001;
uniforms.iTimeDelta.value = deltaTime;
uniforms.iFrame.value++;
uniforms.iFrameRate.value = 1.0 / deltaTime;
uniforms.iResolution.value.set(window.innerWidth, window.innerHeight, 1);

// Update date uniform
const date = new Date();
uniforms.iDate.value.set(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours() * 60 * 60 +
date.getMinutes() * 60 +
date.getSeconds() +
date.getMilliseconds() * 0.001
);

renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>

So, to port any Shadertoy shader into Threejs we mostly need to redeclare built-in uniform shader inputs

These are shaderToy’s Built-in Uniforms, which I usually like to redeclare/reimplement in my ThreeJS apps

uniform vec3      iResolution;           // viewport resolution (in pixels)
uniform float iTime; // shader playback time (in seconds)
uniform float iTimeDelta; // render time (in seconds)
uniform float iFrameRate; // shader frame rate
uniform int iFrame; // shader playback frame
uniform float iChannelTime[4]; // channel playback time (in seconds)
uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click
uniform samplerXX iChannel0..3; // input channel. XX = 2D/Cube
uniform vec4 iDate; // (year, month, day, time in seconds)

So, the final ported working version would look something like this

Live link — realtimehtml.com

But hey, there is a simpler way of doing this!

Let’s use a GLSL editor, which is optimized for porting shaders from shader toy into ThreeJS

these are the steps:

Step 1. — COPY — Copy shader code from shader toy

note: it should not be using channel images

Step 2. — REPLACE — Go to realtimehtml.com/glsl and replace mainImage function with copied content

Step 3. — DOWNLOAD — Click the download button and that will open threejs ported version of the code

Step 4. — TEST — copy the generated code, go to realtimhtml.com (HTML, CSS, JS playground), and paste the code to make sure that it works

Examples

Here are several shader codes ported this way:

MandelBulb

Base warp fBM

Protean clouds

Star Nest

Ball of fire

Ray Marching Primitives

Finally, here is the video to explain it

I hope this was useful,

Thank you for reading

--

--

David B.
David B.

No responses yet