How to Use Shaders as Materials in Three.js (with Uniforms)

Vic Sidious
3 min readDec 21, 2019

I’ve recently learned how to use Three.js’s ShaderMaterial to map shaders onto meshes.

If you’re not familiar with shaders, I recommend you use the first chapters of Patricio Gonzalez Vivo’s The Book of Shaders as a launchpad.

For you to follow along, I set up a basic Three.js scene here.

Materials, materials

When we create a 3D object, or mesh, in Three.js, we are combining two things—a geometry and a material. In the example scene, I’m using a MeshNormalMaterial.

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshNormalMaterial();

Three.js has a special kind of material class called ShaderMaterial.

const material = new THREE.ShaderMaterial({
vertexShader: vShader,
fragmentShader: fShader,
uniforms
});

A ShaderMaterial requires a parameter object; in it we pass essential data—our vertexShader, our fragmentShader, and uniforms.

Uniforms

A uniform, for our purposes, is a value that we can pass to our shader from our Three.js scene. In our fragment shader, these values are applied uniformly to each fragment or pixel in the screen. Common uniforms to pass are values for screen resolution, time, and mouse coordinates. Let’s define our uniforms in an object.

const uniforms = {
u_resolution: { value: { x: null, y: null } },
u_time: { value: 0.0 },
u_mouse: { value: { x: null, y: null } },
}

For the resolution, we can update it by adding the following inside our onWindowResize method:

if (uniforms.u_resolution !== undefined){
uniforms.u_resolution.value.x = window.innerWidth;
uniforms.u_resolution.value.y = window.innerHeight;

}

For the time, we can instantiate a Three.js Clock class, and tick it every frame inside our render method:

const clock = new THREE.Clock();function render() {  // rotate the cube
cube.rotation.y += 0.01;
cube.rotation.x += 0.01;
// update time uniform
uniforms.u_time.value = clock.getElapsedTime();
// animation loop
requestAnimationFrame( render );
renderer.render( scene, camera );
}

And for the mouse, we can track and update it with an event listener:

document.addEventListener('mousemove', (e) =>{
window.addEventListener( 'resize', onWindowResize, false );
uniforms.u_mouse.value.x = e.clientX;
uniforms.u_mouse.value.y = e.clientY;

})

We’re done! Time for some basic shader code.

Shade it up

We can store our GLSL shaders as strings in this manner:

Vertex shader

const vShader = `
varying vec2 v_uv;
void main() {
v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`

Fragment shader

const fShader = `
varying vec2 v_uv;
uniform vec2 u_mouse;
uniform vec2 u_resolution;
uniform vec3 u_color;
uniform float u_time;
void main() {
vec2 v = u_mouse / u_resolution;
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = vec4(1.0, 0.0, sin(u_time * 5.0) + 0.5, 1.0).rgba;
}
`

This is a simple shader that waves between a red and pink color.

In the end, our ShaderMaterial takes all of these parameters in and is ready to use.

// define geometry and material
const geometry = new THREE.BoxGeometry(1,1,1);
const material = new THREE.ShaderMaterial({
vertexShader: vShader,
fragmentShader: fShader,
uniforms
});
// mesh 'em together
const cube = new THREE.Mesh(geometry, material);
// add to scene
scene.add( cube );

Which should produce the following result:

That’s all, folks!

If you have any questions, advice or general feedback, please reach out to me at vic@sidiousvic.dev or catch me @sidiousvic on Twitter.

--

--

Vic Sidious

Triple-edged programmer, educator and musician from Mexico, based in Tokyo.