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:
Finally, here is the video to explain it
I hope this was useful,
Thank you for reading