🌗 Expo + ThreeJS Shaders

thomascoldwell profile image Thomas Coldwell ãƒģ4 min read

Recently, I started having a good play around with Expo and ThreeJS which allows for some impressive 3D experiences across iOS, Android and Web - all from the same codebase! As I dug down I discovered that ThreeJS supports shaders and started looking at how I could build this into my Expo app. Almost every example I came across was purely HTML / JS based so I decided to write a post to help get shaders into your Expo app!

For those of you who don't know what a shaders do, they can be broken down into two basic types - fragment (pixel) shaders and vertex shaders. Fragment shaders enable a colour transform of all of the pixels on the screen according to some function with various input parameters such as time or a colour we want to mix in. Vertex shaders enable geometric transformation of the vertices that build up our mesh - think stretching or squishing.

Shaders are required to be written in some fairly low level syntax as their code gets directly executed on the GPU (which also makes them blazing fast). For example OpenGL and WebGL (which powers ThreeJS) uses GLSL which is syntactically similar to C.

Shaders can be implemented in ThreeJS at a high level using THREE.ShaderMaterial() and then passed as the material to our mesh. Let's create a basic ThreeJS shader below:

this.shader = new THREE.ShaderMaterial({
  uniforms: {
    time: {
      type: 'f',   // Indicates float32
      value: 0.0   // Give the time uniform a starting value
  fragmentShader: fragmentShader,  // Pass in the fragment shader we defined
  side: THREE.DoubleSide   // Make the shader work on both the inner and outer faces of the geom.

There's a few things going on here. Firstly, we are passing what is known as a uniform in GLSL - these can be thought of as variables we can pass into the shader from the outside code (in this case JavaScript). To define a uniform it needs to be given a type in our case here we have said that it will be a float (important that it is 32 bit for GLSL) and a value that defines some initial value (or constant if we don't want it to change). Secondly, we pass in the fragment shader which we haven't defined yet but is just a string of some GLSL code that will define the behaviour of this shader. Similarly, the same can be done for vertex shaders (see the snack below to see how), but we'll just focus on the one fragment shader for now. Finally, we tell the material class to apply this shader on both sides of each face of our geometry by setting side to THREE.DoubleSide - this will help things look less weird when we try to make things transparent.

Okay, so now we get to the fun bit - writing some GLSL code! Don't worry if you've never seen it or it looks too low-level for you - there is plenty of pre-written shaders available out there to do anything you could think of! Here is some code that will simply turn all of the pixels mid grey and fade their opacity in and out:

uniform float time;

void main() {
  vec3 color = vec3(0.5, 0.5, 0.5);
  float opacity = abs(sin(time));
  gl_FragColor = vec4(color, opacity);

P.s. If you want to just play around with GLSL checkout - https://thebookofshaders.com/edit.php

Let's breakdown what's going on here. First, the uniform that we defined in the shader material is declared - this will let us access it as a variable in our shader. Then we define the main function which is the entry point to the shader. We then define a vec3 which is a three channel vector that will hold our RGB colour float values - here we've set it to middle grey. The next line defines a float for the opacity of each pixel subject to the shader. This opacity is calculated using the time variable and a sine function normalised to wiggle between 0.0 and 1.0. Finally, gl_FragColor is a GLSL keyword which allows us to return the RGBA vector for that fragment (pixel) - which we set to the colour concatenated with the opacity we just worked out. And that's the shader fully defined - this can be put in some quotation marks and passed as a string to the fragment shader in the shader material. Let's get back to the JavaScript!

Currently, the shader will just read the time variable as 0.0 as that's what its value was set to. To change this value we can access it as follows anywhere in our component:

this.shader.uniforms.time.value += 1.0;

If you apply this as the material to your mesh you'll see it turn grey and fade in and out! The best way to learn shaders is to play around with them so have a go with the snack of this example included below!

Happy shader-ing!


Posted on Jan 28 by:

thomascoldwell profile

Thomas Coldwell


Expo, React Native, ThreeJs, Computer Vision, AI / ML, Shaders, Edge AI


markdown guide