Custom shaders with Three.JS: Uniforms, textures and lighting

If you’re familiar to WebGL and GLSL programming and have started using three.js, you’ll eventually run up to a situation where you want to code your own shader, but at the same time use the resources that this library provides you with. In this post, I’ll show you how to setup a custom shader with a three.js geometry, pass it your own uniforms, bind a texture to a particular uniform and receive all lights that you’ve added to the scene.

Starting with the basics

We start off by creating a very simple application that only renders a plane on screen:

<html>
<head>
<script src="three.min.js"></script>
<script src="render.js"></script>
<script id="vertShader" type="shader">
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = projectionMatrix *
                  modelViewMatrix * vec4(position, 1.0 );
}
</script>
<script id="fragShader" type="shader">
precision highp float;
varying vec2 vUv;
uniform float color;
void main(void) {
    gl_FragColor = vec4(vec3(color), 1.0);
}
</script>
</head>
<body style="margin: 0px;" onload="init()"></body>
</html>

And the render.js file looks like this:

	// standard global variables
	var scene, camera, renderer;

	// Character 3d object
	var character = null;

	// FUNCTIONS
	function init() {
		// SCENE
		scene = new THREE.Scene();

		// CAMERA
		var SCREEN_WIDTH = window.innerWidth,
                    SCREEN_HEIGHT = window.innerHeight;
		var VIEW_ANGLE = 45,
                    ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT,
                    NEAR = 0.1, FAR = 1000;
		camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT,
                                                     NEAR, FAR);
		scene.add(camera);
		camera.position.set(0,0,5);
		camera.lookAt(scene.position);

		// RENDERER
		renderer = new THREE.WebGLRenderer({
            antialias:true,
            alpha: true
        });
		renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
		var container = document.body;
		container.appendChild( renderer.domElement );

		// Main polygon
		character = buildCharacter();
		scene.add(character);

		// Start animation
		animate();
	}

	var buildCharacter = (function() {
		var _geo = null;

		// Share the same geometry across all planar objects
		function getPlaneGeometry() {
			if(_geo == null) {
				_geo = new THREE.PlaneGeometry(1.0, 1.0);
			}

			return _geo;
		};

		return function() {
			var g = getPlaneGeometry();
			var mat = new THREE.ShaderMaterial({
				uniforms: {
					color: {type: 'f', value: 0.0}
				},
				vertexShader: document.
                              getElementById('vertShader').text,
				fragmentShader: document.
                              getElementById('fragShader').text
			});

			var obj = new THREE.Mesh(g, mat);
			return obj;
		}
	})();

	function animate() {
		// Update uniform
		var c = 0.5+0.5*Math.cos(
                new Date().getTime()/1000.0 * Math.PI);
		character.material.uniforms.color.value = c;
		// Render scene
		renderer.render( scene, camera );
		requestAnimationFrame( animate );
	}

So far, there’s nothing special about this code: Up until line 33 we setup the scene, camera, and the renderer. We also create this object called character, which so far only represents a plane whose color changes between black and white periodically. The function getPlaneGeometry just instantiates a THREE.PlaneGeometry, which contains vertices, normals, and texture coordinates. The anonymous function on line 48 will create our mesh object. In three.js, a mesh is a “renderable” type formed by combining a geometry (from which three.js will create a vertex buffer object) and a material (which is basically a shader with metadata, such as uniforms). Here, we create a ShaderMaterial, which allows us to specify our custom vertex and fragment shaders. The “uniforms” parameter takes an object with the format:

uniformName: {type: TYPE, value: VALUE}

For a guide on accepted formats, consult here. For demonstration purposes, I created a float uniform named color.

Finally, on function animate I update the color uniform to a simple senoidal function. Three.JS will send the current value of each uniform to the GPU every time you call render. As far as I know, there’s no dirty flag to tell it when to update an uniform.

Vertex shader and implicit attributes and uniforms

If you paid close attention to the vertex shader code, you’ve probably noticed that the declaration for the “position” attribute seems to be missing. The same also applies to the projectionMatrix and modelViewMatrix. In fact, three.js modifies the vertex shader given to it by appending the declaration of several attributes and uniforms. The following variables are the ones I am currently aware of:

  • attribute position: vec3, the vertex itself
  • attribute normal: vec3, the normal at the current vertex
  • attribute uv: vec2, the texture coord
  • uniform projectionMatrix: mat4, self explanatory
  • uniform modelMatrix: mat4, object-to-world matrix
  • uniform viewMatrix: mat4, world-to-camera matrix
  • uniform modelViewMatrix: mat4, same as viewMatrix*modelMatrix, or object-to-camera matrix

Adding a texture

Now let’s add a texture to our polygon. In three.js, this is done by creating an uniform of type “t” and assigning to it a THREE.Texture object. If your texture is an image, there’s a helper function that simplifies this task:

// It accepts other formats too
THREE.ImageUtils.loadTexture("path/to/image.png")
We will draw this guy inside our polygon. Kudos for Stephen "Redshrike" Challener and William.Thompsonj
We will draw this guy inside our polygon. Kudos for Stephen “Redshrike” Challener and William.Thompsonj

After I’ve chosen the texture image (remember to use an image whose dimensions are powers of two), our shader material is initialized as follows:

var creatureImage = THREE.ImageUtils
                    .loadTexture('texture.png');
creatureImage.magFilter = THREE.NearestFilter;
var mat = new THREE.ShaderMaterial({
    uniforms: {
        color: {type: 'f', value: 0.0},
        evilCreature: {type: 't', value: creatureImage}
    },
    vertexShader: document.
                  getElementById('vertShader').text,
    fragmentShader: document.
                    getElementById('fragShader').text,
    transparent: true
});

Here I’ve loaded the image file and created a THREE.Texture object. I also set the magnification filter to nearest (I want my texture to be pixelated as I scale it up). More information on the filter constants used by three.js can be found on the official documentation, and here you can see the configuration fields.

Another important property introduced is the transparent flag, since my texture has some regions with alpha=0. By setting the transparent flag to true, three.js will automatically call gl.enable(GL_BLEND) when this object is about to be drawn. It also defers the rendering of transparent objects to after opaque objects are drawn. Also, three.js draws these objects ordered from the farthest to the closest ones.

Now that the texture uniform is set up, the fragment shader can be modified to process it:

precision highp float;

varying vec2 vUv;
uniform float color;
// Name it whatever it was named in the uniforms object
uniform sampler2D evilCreature;

void main(void) {
	// Standard sampling procedure. Just make sure
	// you've passed the uv coords varying.
	gl_FragColor = texture2D(evilCreature, vUv);
}

Dealing with lights

Lighting is a little bit more complicated. Ideally, you’d just have to set a “usesLight” flag and three.js would take care to send all lights that you have created to your shader as an implicit uniform. Unfortunately, that’s not how it works (at least not at the time of this writing). The good news is that it’s still relatively easy: You only have to manually setup the uniforms (both on the material and on your shader code).

Setting up the material

The first thing you have to do is to enable the lights flag on the material:

var creatureImage = THREE.ImageUtils.
                    loadTexture('texture.png');
creatureImage.magFilter = THREE.NearestFilter;
var mat = new THREE.ShaderMaterial({
    uniforms: {
        color: {type: 'f', value: 0.0},
        evilCreature: {type: 't', value: creatureImage}
    },
    vertexShader: document.
                  getElementById('vertShader').text,
    fragmentShader: document.
                    getElementById('fragShader').text,
    transparent: true,
    lights: true
});

Now you have to add to the uniforms object a list with all light uniforms. Since three.js already has a public field with all light uniforms (and there’s a lot of them) in THREE.UniformsLib[‘lights’], you can merge them to your uniform object with the function THREE.UniformUtils.merge:

var mat = new THREE.ShaderMaterial({
    uniforms: THREE.UniformsUtils.merge([
        THREE.UniformsLib['lights'],
        {
            color: {type: 'f', value: 0.0},
            evilCreature: {type: 't', value: null}
        }
    ]),
    vertexShader: document.
                  getElementById('vertShader').text,
    fragmentShader: document.
                    getElementById('fragShader').text,
    transparent: true,
    lights: true
});
// THREE.UniformsUtils.merge() call THREE.clone() on each
// uniform. We don't want our texture to be duplicated, so
// I assign it to the uniform value right here.
mat.uniforms.evilCreature.value = creatureImage;

Create some lights

In order to test our demo, let’s create a point light right in front of our object. This comes right after we create the character creation, in the init function:

    // Create light
	var light = new THREE.PointLight(0xffffff, 1.0);
    // We want it to be very close to our character
	light.position.set(0.0,0.0,0.1);
	scene.add(light);

Setting up the shader

Now three.js is automatically sending all your lights to your light uniforms, but we are not using them in our shader program. Since I’m using a point light, I’ll need to create the following uniforms:

  • vec3[MAX_POINT_LIGHTS] pointLightColor
  • vec3[MAX_POINT_LIGHTS] pointLightPosition: light position, in world coordinates
  • float[MAX_POINT_LIGHTS] pointLightDistance: used for attenuation purposes. Since you’re writing your own shader, it can really be anything you want (as long as you assign it to your light in its “distance” field)

In the official documentation you can see a list of all uniforms provided by the THREE.UniformsLib. Notice that the constant MAX_POINT_LIGHTS is automatically created by three.js, so no need to worry about it. Now my vertex shader now looks like this:

varying vec2 vUv;
varying vec3 vecPos;
varying vec3 vecNormal;

void main() {
    vUv = uv;
    // Since the light is on world coordinates,
    // I'll need the vertex position in world coords too
    // (or I could transform the light position to view
    // coordinates, but that would be more expensive)
    vecPos = (modelMatrix * vec4(position, 1.0 )).xyz;
    // That's NOT exacly how you should transform your
    // normals but this will work fine, since my model
    // matrix is pretty basic
    vecNormal = (modelMatrix * vec4(normal, 0.0)).xyz;
    gl_Position = projectionMatrix * viewMatrix *
                  vec4(vecPos, 1.0);
}

And this is the fragment shader:

precision highp float;

varying vec2 vUv;
varying vec3 vecPos;
varying vec3 vecNormal;

uniform float color;
uniform sampler2D evilCreature;

uniform vec3 pointLightColor[MAX_POINT_LIGHTS];
uniform vec3 pointLightPosition[MAX_POINT_LIGHTS];
uniform float pointLightDistance[MAX_POINT_LIGHTS];

void main(void) {
    // Pretty basic lambertian lighting...
    vec4 addedLights = vec4(0.0,0.0,0.0, 1.0);
    for(int l = 0; l < MAX_POINT_LIGHTS; l++) {
        vec3 lightDirection = normalize(vecPos
                              -pointLightPosition[l]);
        addedLights.rgb += clamp(dot(-lightDirection,
                                 vecNormal), 0.0, 1.0)
                           * pointLightColor[l];
    }
	gl_FragColor = texture2D(evilCreature, vUv)
                       * addedLights;
}

And that’s it! This is how the final character looks like:

Whoa! Now you look like a badass, sir!
Whoa! Now you look like a badass, sir!

Adding lights at any moment (aka adding lights at runtime)

So you want to add a light after your shader programs were compiled and are running (for instance, you may have a candle being lit in the middle of the gameplay). Two steps are required. First, add the light to the scene, as explained previously. Then, for each material (I hope you have access to them), set its needsUpdate flag to true. That should get your shader recompiled to take into account the new number of lights.

var light = new THREE.PointLight(0xffffff, 1.0);
light.position.set(0.0,0.0,0.1);                
scene.add(light);                               
material.needsUpdate = true;

If you have access to your character (and any objects that contain a material), you can access its material in the “material” field:

character.material.needsUpdate = true;

Notice that you will have to do that to all materials in your scene (or at least those you think that will be affected). Also, you only have to perform shader recompilation if the number of similar lights is changed, since you would be changing the NUMBER_OF_LIGHTS constant. Keep in mind that, if you add a point light and remove a spotlight, you will still need to update your material, as both the number of spotlights and point lights have changed.

Final thoughts

This is a pretty basic example that shows how to setup a custom shader with textures and three.js lights. Although the final effect could have been easily achieved without touching any shader, I hope this will prove useful to someone willing to write a shader that’s not available in the standard three.js material list.

Considering the fact that most cool effects are achievable by multipass rendering, I’ll eventually extend this post in the future to cover that functionality as well.

Advertisements

7 thoughts on “Custom shaders with Three.JS: Uniforms, textures and lighting

  1. In the step where you merge THREE.UniformsLib[‘lights’] with your uniforms, you have “evilCreature: {type: ‘t’, value: null}”

  2. In order to get this working on Three.js version 82, you need to declare a struct for the point light, instead of the three uniforms mentioned in the post. Also notice the change from MAX_POINT_LIGHTS to NUM_POINT_LIGHTS as pointed out by 2pha.

    struct PointLight {
    vec3 position;
    vec3 color;
    float distance;
    };

    uniform PointLight pointLights[ NUM_POINT_LIGHTS ];

    // Pretty basic lambertian lighting…
    vec4 addedLights = vec4(0.0, 0.0, 0.0, 1.0);
    for(int l = 0; l < NUM_POINT_LIGHTS; l++) {
    vec3 lightDirection = normalize(vPos – pointLights[l].position);
    addedLights.rgb += clamp(dot(-lightDirection, vNormal), 0.0, 1.0) * pointLights[l].color;
    }

    The rest is the same.

    1. brunoimbrizi’s post is the correct solution for r83 too.

      But lights:true has to be declared in the shadermaterial! The hint from 2pha is wrong and leads to an exception.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s