THREE.JS Advanced Tips : Shadow
14

I have been using THREE.JS on and off for almost 3 years now. I found it very user-friendly for beginner because there are tons of tutorials and examples online.

You can easily learn all of the basic things like to set up the scene, adding objects and lighting etc. However, after you have learned a bit more, there are not many tutorials to show you how to do a bit more advanced with THREE.JS. That's why I would like to write some posts from my experiences about some advanced tips for the mid level THREE.JS developers here.

In this series of posts, I assume all of the readers understand how to write their own simple shaders. If you don't you should check out the An Introduction to Shaders part 1 and part 2 by Paul Lewis. It will probably take you an hour or two to digest and you will love it ^^

THREE.JS Advanced Tips : Shadow

In this post, I would like to talk about the shadow. In THREE.JS, it has an internal shadow system. It is very powerful and easy to use. But I noticed that so many developers have issue with using shadow with their custom shaders. The most common issue is that after we moved the vertex position in the vertex shader, the shadow casting doesn't seem to be automatically reflected from the changes.

Therefore, I am going to step by step going through the shadow casting and shadow receiving on a regular mesh here. First of all, let's create some custom shaders:

// vertex shader

varying vec3 vNormal;
varying vec3 vWorldPosition;

uniform float time;

void main() {

    // adding some displacement based on the vertex position
    vec3 offset = vec3(
        sin(position.x * 10.0 + time) * 15.0,
        sin(position.y * 10.0 + time + 31.512) * 15.0,
        sin(position.z * 10.0 + time + 112.512) * 15.0
    );

    vec3 pos = position + offset;

    // just add some noise to the normal
    vNormal = normalMatrix * vec3(normal + normalize(offset) * 0.2);

    vec4 worldPosition = modelMatrix * vec4(pos, 1.0);

    // store the world position as varying for lighting
    vWorldPosition = worldPosition.xyz;

    gl_Position = projectionMatrix * viewMatrix * worldPosition;

}
// fragment shader

varying vec3 vNormal;
varying vec3 vWorldPosition;

uniform vec3 lightPosition;

void main(void) {

    vec3 lightDirection = normalize(lightPosition - vWorldPosition);

    // simpliest hardcoded lighting ^^
    float c = 0.35 + max(0.0, dot(vNormal, lightDirection)) * 0.4;

    gl_FragColor = vec4(c, c, c, 1.0);
}

If you had experience using the ShaderMaterial in THREE.JS, the codes above are pretty straight forward to you. It basically calculates the hardcoded(and ugly) light in the fragment shader. Nothing fancy here in the tutorial.

For the javascript part, we would do something like the following and try to add the shadows like we used those PhongMateral meshes by using the castShadow and receiveShadow properties:


function _initMesh() {
    var geometry = new THREE.IcosahedronGeometry( 200, 3 );
    var material = new THREE.ShaderMaterial({
        vertexShader: vs,
        fragmentShader: fs,
        uniforms: {
            lightPosition: {type: 'v3', value: new THREE.Vector3(700, 700, 700)},
            time: {type: 'f', value: 0}
        }
    });

    var mesh = new THREE.Mesh( geometry, material );
    mesh.castShadow = true;
    mesh.receiveShadow = true;

    scene.add(mesh);
}

Here is the live result from the codes above:

Sorry, this snippet requires webgl feature.
Please update your browser!
Loading...

As you can see from the live result above, there are 2 mistakes:

  1. The icosahedron does not receive the shadow of the spinning box.
  2. The shadow of the icosahedron that casted on the ground doesn't include the offset we applied in the vertex shader.

1. Fix the shadow receiving

To fix the shadow receiving, you will need to include the snippets in the internal THREE.ShaderChunk into your shaders. As it requires run-time string interpolation, even if you use tools like glslify, I will recommend you to create a simple snippet for convenience. Here is what I created for this example:

function replaceThreeChunkFn(a, b) {
    return THREE.ShaderChunk[b] + '\n';
}

function shaderParse(glsl) {
    return glsl.replace(/\/\/\s?chunk\(\s?(\w+)\s?\);/g, replaceThreeChunkFn);
}

Then whenever we want to use the the snippets from the THREE.ShaderChunk, we can just add a commented line like this in our shaders:

// chunk(shadowmap_fragment);

Now, we need to insert shadow snippets from the THREE.ShaderChunk into our shaders:

// vertex shader

varying vec3 vNormal;
varying vec3 vWorldPosition;

uniform float time;

// chunk(shadowmap_pars_vertex);

void main() {

    // adding some displacement based on the vertex position
    vec3 offset = vec3(
        sin(position.x * 10.0 + time) * 15.0,
        sin(position.y * 10.0 + time + 31.512) * 15.0,
        sin(position.z * 10.0 + time + 112.512) * 15.0
    );

    vec3 pos = position + offset;

    // just add some noise to the normal
    vNormal = normalMatrix * vec3(normal + normalize(offset) * 0.2);

    vec4 worldPosition = modelMatrix * vec4(pos, 1.0);
    // chunk(shadowmap_vertex);

    // store the world position as varying for lighting
    vWorldPosition = worldPosition.xyz;

    gl_Position = projectionMatrix * viewMatrix * worldPosition;

}

All we added in the vertex shader are just those commented // chunk(); codes which will be replaced with the THREE.ShaderChunk during the run-time.

Bare in mind that, in the shadermap_vertex snippet, it requires the vec4 worldPosition which you can obtains by using modelMatrix * vec4(position, 1.0). We need to make sure the vec4 worldPosition is available before that // chunk(shadowmap_vertex); line.

// fragment shader

varying vec3 vNormal;
varying vec3 vWorldPosition;

uniform vec3 lightPosition;

// chunk(shadowmap_pars_fragment);

void main(void) {

    vec3 lightDirection = normalize(lightPosition - vWorldPosition);

    vec3 outgoingLight = vec3(1.0);

    // chunk(shadowmap_fragment);

    // simpliest hardcoded lighting ^^
    float c = 0.35 + max(0.0, dot(vNormal, lightDirection)) * 0.4 * shadowMask.x;

    gl_FragColor = vec4(c, c, c, 1.0);
}

In the fragment shader, other than the commented // chunk(); codes, we also added the outgoingLight as it is a required variable from the shadowmap_fragment. After that we can use the shaderMask variable to determinate how strong the shadow is and output the color.

As we used the shadow map shader chunk, we will need to add the corresponded uniforms we used to the ShaderMaterial in the javascript:


function _initMesh() {
    var geometry = new THREE.IcosahedronGeometry( 200, 3 );
    var material = new THREE.ShaderMaterial({

        // apply the shaderParse to the shaders
        vertexShader: shaderParse(vs),
        fragmentShader: shaderParse(fs),

        // merge the shadow map uniforms to the uniforms we had
        uniforms: THREE.UniformsUtils.merge([
            THREE.UniformsLib.shadowmap,
            {
                lightPosition: {type: 'v3', value: new THREE.Vector3(700, 700, 700)},
                time: {type: 'f', value: 0}
            }
        ])
    });

    var mesh = new THREE.Mesh( geometry, material );
    mesh.castShadow = true;
    mesh.receiveShadow = true;

    scene.add(mesh);
}

Sorry, this snippet requires webgl feature.
Please update your browser!
Loading...

2. Fix the shadow casting

The reason behind this issue is quite obvious, in our custom vertex shader we changed the vertex positions so the final positions are no long the same as the values in the original position attribute. By default, the internal shadow system will use the DepthMaterial which will assume whatever you have in the position attribute will be the final vertex position and that is causing this issue.

To fix it, it is quite simple. There are 2 properties - customDepthMaterial and customDistanceMaterial in the renderable objects like Mesh, Lines and Points that you can assign to override the default DepthMaterial. These 2 properties are not mentioned anywhere in the document(at least by the time I wrote this post) and you can only find one example use one of these properties here.

  • customDepthMaterial - It is for the shadow casted by most of the Light object except PointLight
  • customDistanceMaterial - It is only for the shadow casted by the PointLight

In the example, we used a spot light. So, let's create our depth vertex shader for that:

// depth vertex shader

uniform float time;

void main() {

    vec3 offset = vec3(
        sin(position.x * 10.0 + time) * 15.0,
        sin(position.y * 10.0 + time + 31.512) * 15.0,
        sin(position.z * 10.0 + time + 112.512) * 15.0
    );

    vec3 pos = position + offset;

    vec4 worldPosition = modelMatrix * vec4(pos, 1.0);

    gl_Position = projectionMatrix * viewMatrix * worldPosition;

}

In the depth vertex shader codes, we basically just cloned the position adjustment we had from the vertex shader. After that we can assign the customDepthMaterial to our ShaderMaterial in the javascript like this:


function _initMesh() {
    var geometry = new THREE.IcosahedronGeometry( 200, 3 );
    var material = new THREE.ShaderMaterial({

        // apply the shaderParse to the shaders
        vertexShader: shaderParse(vs),
        fragmentShader: shaderParse(fs),

        // merge the shadow map uniforms to the uniforms we had
        uniforms: THREE.UniformsUtils.merge([
            THREE.UniformsLib.shadowmap,
            {
                lightPosition: {type: 'v3', value: new THREE.Vector3(700, 700, 700)},
                time: {type: 'f', value: 0}
            }
        })
    });

    var mesh = new THREE.Mesh( geometry, material );
    mesh.castShadow = true;
    mesh.receiveShadow = true;

    // magic here
    mesh.customDepthMaterial = new THREE.ShaderMaterial({
        vertexShader: shaderParse(vs_depth),
        fragmentShader: THREE.ShaderLib.depthRGBA.fragmentShader,
        uniforms: material.uniforms
    });

    scene.add(mesh);
}

As we are kind of using the same uniforms we used in our mesh ShaderMaterial, we can lazily assign the material.uniforms to the uniforms of customDepthMaterial. In that case, we only need to update the time uniform once.

And finally, here is the live result:

Sorry, this snippet requires webgl feature.
Please update your browser!
Loading...

Tips for particle shadowing

  1. Remember to pass the gl_PointSize as well in the depth vertex shader.
  2. You will need to create a depth fragment shader for particles. You can copy what you need from depthRGBA in the ShaderLib. And then to adjust the depth a bit by doing something like pack_depth( gl_FragCoord.z - 1.0 ).
  3. If you want your particle shape to be circle or something other than square, you can use discard in your depth fragment shader.
  4. If you want something semi-transparent to create some smokey alike particles, there is no solution at the moment using the internal shadow system and you will probably need to do this kind of volumetric shadowing yourself.

Happy coding!

Share:
Comments
Luis Henrique Bizarro
2015-12-07 13:29:00
Reply to
Hello, thank you so much for sharing awesome content like this, keep it up!
Adam
2016-01-20 19:39:37
Reply to
Hi! Great tutorial. Thanks for publishing it. I have one question. Why does the sphere always have a shadow on its left - it stays there when I move the camera which seem unnatural. Thanks.
João da Fonseca
2016-03-07 10:30:44
Reply to
Great toturial, really helpfull to understand how to conbine shaderChunks in three.js.

Will it be possible to share the sources?

Thanks
Niklas Knaack
2016-03-08 14:33:29
Reply to
Awesome tutorial. Thx for sharing!
Davide Prati
2016-05-24 19:03:30
Reply to
Nice tutorial, thank you for sharing.
Buster
2016-06-01 07:05:55
Reply to
Hey Ed thanks for these tutorials and your awesome examples on github I am learning a great deal from them. It seems that recently there have been some changes to Three.js (currently r77) that have outdated this tutorial and
I am struggling to get shadows to work on custom materials, is there any chance you might get a moment to update this tutorial?
Bluu
2016-06-30 06:26:10
Reply to
Hey! Can't make it work on THREE r78, could you update your tutorial for this latest release ? :)
Karol
2016-08-29 08:17:01
Reply to
hi Edan, thanks a lot for this amazing tutorial. It opened my eyes on shaderchunk features in Three.js. It would be great if you could update it with recent changes in Three.js (rev 80) as it seems new approach to shadows is implemented and the tutorial is out of date :( . Perhaps could you write tutorial on overall ShaderChunk / ShaderLibs usage ? Many Thanks!
Gunderson
2016-11-04 16:30:09
Reply to
Looks like r74 broke this technique with the shadow system rewrite :(
Alessandro
2016-11-25 10:15:53
Reply to
Hi! I'm trying to implement the feature, but I don't know how. Is it possible to see the final source code? Many thanks
Alessandro
2016-11-25 11:05:41
Reply to
I found out that 'shadowmap_fragment' causes my problem. What is the version of three you are using?
alessandro
2016-11-26 08:02:47
Reply to
Hi Edan, after wondering around and smashing my head against the console log, I found out that your code works only on Three.js 74dev release or previous ones. As it won't work with other ones I think it's good for everyone to know that beforehand.
Baptiste
2017-01-26 14:24:53
Reply to
Super well tutorial for a critical problem. Thanks.
Yuesheng
2017-01-27 02:06:19
Reply to
Thank you for sharing your skills, and it works for me~Nice man!
Just a little advice, some codes of the post depends on the version of Three.js. For instance, the code of this post is based on Three.js R74, and in the lastest version, Three.js remove the [THREE.ShaderLib.depthRGBA] and [THREE.UniformsLib.shadowmap] for some reasons.
Whatever, nice and helpful post! Keep up.
Cancel
What do you think?




All comments require my approval in order to be public. So... Spammers, please get the fuck out of my blog. Cunt!