DEV Community

Maniflames
Maniflames

Posted on

Creating a custom shader in Three.js

3D stuff in the browser is awesome. After playing around with threejs for some time and making a mini-game at school I started to like it a lot. A classmate that is really into graphics programming told me a little bit about WebGL and shaders. It seemed really cool and I promised myself I would make my own shader. Of course some other shiny thing caught my attention and I forgot about it but, from today on I can finally say that I have created a shader and used it within threejs.

Three JS

Before going all in on shaders it is probably a good idea to explain what three js is. Threejs is a javascript library to ease the process of creating 3D scenes on a canvas. Other popular solutions like a-frame and whitestorm js are build on top of it. If you have ever played around with those but want even more control definitely try it out! (If you are a TypeScript lover, three js has type definitions 😉).

The most popular intro to this library is creating a cube and making it spin. There is a written tutorial in the threejs documentation and a brilliant youtube tutorial by CJ Gammon that is part of his 'diving in: three js' series.

Creating this cube is a basically preparing a film set and placing it inside of that set. You create a scene and a camera and pass these to a renderer to say: "hey this is my movie set". Then you can place mesh, which is basically an object, within the scene. This mesh consists of a geometry (the shape of the object) and a material (the color, behavior towards light and more). Based on the material you have chosen, you might want to add different kinds of lights to the scene. In order to animate the object and actually display everything you create a loop. Within this loop you tell the renderer to show the scene. Your code might look like this:


window.addEventListener('load', init)
let scene
let camera
let renderer
let sceneObjects = []

function init() {
  scene = new THREE.Scene()

  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
  camera.position.z = 5

  renderer = new THREE.WebGLRenderer()
  renderer.setSize(window.innerWidth, window.innerHeight)

  document.body.appendChild(renderer.domElement)
  adjustLighting()
  addBasicCube()
  animationLoop()
}

function adjustLighting() {
    let pointLight = new THREE.PointLight(0xdddddd)
    pointLight.position.set(-5, -3, 3)
    scene.add(pointLight)

    let ambientLight = new THREE.AmbientLight(0x505050)
    scene.add(ambientLight)
}

function addBasicCube() {
  let geometry = new THREE.BoxGeometry(1, 1, 1)
  let material = new THREE.MeshLambertMaterial()  

  let mesh = new THREE.Mesh(geometry, material)
  mesh.position.x = -2
  scene.add(mesh)
  sceneObjects.push(mesh)
}

function animationLoop() {
  renderer.render(scene, camera)

  for(let object of sceneObjects) {
    object.rotation.x += 0.01
    object.rotation.y += 0.03
  }

  requestAnimationFrame(animationLoop)
}

Shaders

Shaders are basically functions or small scripts that are executed by the GPU. This is where WebGL and GLSL (OpenGL Shading Language) come into play. WebGL is a browser API that allows javascript to run code on the GPU. This can increase the performance of certain scripts because your GPU is optimized for doing graphics related calculations. WebGL even allows us to write code that will be executed directly by the GPU in the GLSL language. These pieces of GLSL code are our shaders and since threejs has a WebGL renderer we can write shaders to modify our mesh. In threejs you can create custom material by using the 'shader material'. This material accepts two shaders, a vertex shader and a fragment shader. Let's try to make 'gradient material'.

Vertex Shader

A vertex shader is a function that is applied on every vertex (point) of a mesh. It is usually used to distort or animate the shape of a mesh. Within our script it looks something like this:

function vertexShader() {
  return `
    varying vec3 vUv; 

    void main() {
      vUv = position; 

      vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
      gl_Position = projectionMatrix * modelViewPosition; 
    }
  `
}

The first thing that you probably notice is that all our GLSL code is in a string. We do this because WebGL will pass this piece of code to our GPU and we have to pass the code to WebGL within javascript. The second thing you might notice is that we are using variables that we did not create. This is because threejs passes those variables to the GPU for us.

Within this piece of code we calculate where the points of our mesh should be placed. We do this by calculating where the points are in the scene by multiplying the position of the mesh in the scene (modelViewMatrix) and the position of the point. After that we multiply this value with the camera's relation to the scene (projectionMatrix) so the camera settings within threejs are respected by our shader. The gl_Position is the value that the GPU takes to draw our points.

Right now this vertex shader doesn't change anything about our shape. So why even bother creating this at all? We will need the positions of parts of our mesh to create a nice gradient. By creating a 'varying' variable we can pass the position to another shader.

Fragment shader

A fragment shader is a function that is applied on every fragment of our mesh. A fragment is a result of a process called rasterization which turns the entire mesh into a collection of triangles. For every pixel that is covered by our mesh there will be at least one fragment. The fragment shader is usually used to do color transformations on pixels. Our fragment shader looks like this:

  return `
      uniform vec3 colorA; 
      uniform vec3 colorB; 
      varying vec3 vUv;

      void main() {
        gl_FragColor = vec4(mix(colorA, colorB, vUv.z), 1.0);
      }
  `
}

As you can see we take the value of the position that was passed by the vertex shader. We want to apply a mix of the colors A and B based on the position of the fragment on the z axis of our mesh. But where do the colors A and B come from? These are 'uniform' variables which means they are passed into the shader from the outside. The mix function will calculate the RGB value we want to draw for this fragment. This color and an additional value for the opacity are passed to gl_FragColor. Our GPU will set the color of a fragment to this color.

Creating the material

Now that we've created the shaders we can finally build our threejs mesh with a custom material.

function addExperimentalCube() {
  let uniforms = {
        colorB: {type: 'vec3', value: new THREE.Color(0xACB6E5)},
        colorA: {type: 'vec3', value: new THREE.Color(0x74ebd5)}
    }

  let geometry = new THREE.BoxGeometry(1, 1, 1)
  let material =  new THREE.ShaderMaterial({
    uniforms: uniforms,
    fragmentShader: fragmentShader(),
    vertexShader: vertexShader(),
  })

  let mesh = new THREE.Mesh(geometry, material)
  mesh.position.x = 2
  scene.add(mesh)
  sceneObjects.push(mesh)
}

This is where everything comes together. Our 'uniforms' colorA and colorB are created and passed along with the vertex shader and fragment shader into the shader material. The material and geometry are used to create a mesh and the mesh is added to the scene.



I build this in glitch. A friend recommended it and it is great! Some add blockers block you loading the embed though, so here is a direct link just in case.

The left cube is a cube using mesh lambert material, the right cube uses our own 'gradient material'. As you can see our material looks pretty sweet but ignores the light settings in the scene. This is because we didn't do the math in our fragment shader to take the light into account. This is hopefully something I figure out soon 😝.

Resources

It took some time to figure this out and if you liked this you should really check out the sources I have used to learn and understand this:

Top comments (11)

Collapse
 
avasconcelos114 profile image
Andre Vasconcelos

The things you can do in three.js are just mindblowing

I've never gotten as far as writing my own shaders but I dabbled with it when creating my company's website (wanderer.studio) and the sheer possibility of things you can create with it amaze me every time (just look at their example page in their site!)

Thank you for sharing your experiences with three.js :)

Collapse
 
maniflames profile image
Maniflames

Woah, the company website is super nice 👀

Collapse
 
avasconcelos114 profile image
Andre Vasconcelos • Edited

Thanks!

You can make some pretty cool experiences just using the right models. I got mine from turbosquid.com/, they have a bunch of free models you can use to experiment with

Thread Thread
 
maniflames profile image
Maniflames

Woah, this is a site I definitely need. I always tried to find something sepecific in Google Poly but could never really find it. I found a ton after just one search on turbosquid!

Collapse
 
adambernath profile image
adambernath

Have you played with the webgl shader editor yet? this - victhorlopez.github.io/editor/
I'm trying to use the output code as a vertex/fragment shader, but I cant make it work, I don't really know what I'm doing wrong, but mixing the two together would create a really awesome shader editor for three js

Collapse
 
maniflames profile image
Maniflames

No I haven't but it looks cool! The code generated by the environment contains a lot of uniforms, it might be that some of them are defined with a different name in threejs' environment. There are a lot of things that could go wrong though, I've only glanced a bit over the code generated by the example.

Collapse
 
dechamp profile image
DeChamp

This is the first post that finally made me realize thank using 3d on the web, is really not that scary. Thanks for the great post! Also your little game is pretty awesome.

Collapse
 
maniflames profile image
Maniflames

Thanks! Of all people, you saying this actually means a lot to me 😁. I'm glad that nowadays there are a lot of libraries available that make the use of things like 3D this easy. I hope you find a chance to try it out one day!

Collapse
 
anuragsahu profile image
Anurag Sahu

Loved the Effort you put in to,

my learning

the argument you pass in uniform are recoverable by the shaders
Thanks for Helping me fix my code

Collapse
 
mantiq profile image
A.Z.

Great article, finally understand some variables which in some other guides are just put out there with no proper explanation what certain variables are :)

Collapse
 
sujisucoder profile image
Sujith Surendran

did someone tried angular three (NGT)? , we could use three.js more simply there.