Post Processing in ThreeJs | Make your Scene Pop

Post Processing in ThreeJs | Make your Scene Pop

Chapter 5: Setting up the Effects Composer and Enhancing our Renders with awesome Filters

ยท

9 min read

Introduction ๐Ÿซฑ๐Ÿฝโ€๐Ÿซฒ๐Ÿฟ

This chapter will be all about how to enhance the renders you get from ThreeJs and give them some flair โœจ

This can all be done with the resources ThreeJs provides as well : )

We're going to learn about Post-Processing, as the name suggests, it's processing that happens after the scene has been rendered.

Essentially, we're taking the rendered scene and applying a filter to it, like you would in your native phone camera, Instagram, Snapchat etc.

Instagram filter illustration

Prerequisite โœ”๏ธ

We covered how to make a scattering of cubes and how to make them light up on click in the last chapter, we won't go over that again, here's the boilerplate we're using,

<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;" class="threeD"></div>
    <script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
    <script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three/build/three.module.js"
        }
    }
    </script>
    <script type="module">
        import * as THREE from "three";
        import { OrbitControls } from "https://unpkg.com/three/examples/jsm/controls/OrbitControls.js";

        const init = (
            targetClass = "threeD"
        ) => {
            const target = document.getElementsByClassName(targetClass);
            if (target) {
                // Renderer and Scene Setup
                const renderer = new THREE.WebGLRenderer({ alpha: true });
                const Cwidth = () => {
                    return document.getElementsByClassName(targetClass)[0].clientWidth;
                };
                const Cheight = () => {
                    return document.getElementsByClassName(targetClass)[0].clientHeight;
                };
                renderer.shadowMap.enabled = false;
                renderer.setPixelRatio(window.devicePixelRatio);
                renderer.setSize(Cwidth(), Cheight());
                renderer.physicallyCorrectLights = true;
                renderer.domElement.setAttribute("id", "three-canvas");

                target[0].appendChild(renderer.domElement);
                const scene = new THREE.Scene();
                const ambientLight = new THREE.AmbientLight(0xffffff, 2);
                scene.add(ambientLight);

                // Camera Setup
                const aspect = Cwidth() / Cheight();
                const fov = 60;
                const near = 1.0;
                const far = 200.0;
                const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
                camera.position.set(0, 30, 0);
                camera.lookAt(0, 0, 0);

                // Setting Up Orbital Controls 
                const controls = new OrbitControls(camera, renderer.domElement);
                controls.update();

                // Setting up Store for Raycasting
                let raycastStore = [];
                const addToRCStore = (obj) => raycastStore.push(obj);
                const removeFromRCStore = (obj) => {
                    for (let i = 0; i < raycastStore.length; i++) {
                        if (raycastStore[i] === obj) raycastStore.splice(i, 1);
                    }
                }

                // Setting up RayCaster
                let ndcMouseX = -1;
                let ndcMouseY = -1;
                let rc = new THREE.Raycaster();

                document.addEventListener("mousemove", (e) => {
                    ndcMouseX = (e.clientX / window.innerWidth) * 2 - 1;
                    ndcMouseY = -(e.clientY / window.innerHeight) * 2 + 1;
                });

                const CheckRC = (camera, onIntersection, onEmpty = () => { }) => {
                    rc.setFromCamera(new THREE.Vector3(ndcMouseX, ndcMouseY, 0), camera);
                    let intersects = rc.intersectObjects(raycastStore);
                    if (intersects.length > 0) {
                        onIntersection(intersects);
                    } else {
                        onEmpty();
                    }
                }

                const RCJob = setInterval(() => {
                    CheckRC(
                        camera,
                        () => { document.body.style.cursor = "pointer"; },
                        () => { document.body.style.cursor = "auto"; }
                    );
                }, 100); // Start an Hover Job

                document.addEventListener("mousedown", () => {
                    CheckRC(
                        camera,
                        (intersects) => {
                            const col = new THREE.Color("#b62886");
                            if (!intersects[0].object.material.color.equals(col)) {
                                intersects[0].object.material.color = col;
                                intersects[0].object.material.emissive = new THREE.Color("#AAAAAA");
                            } else {
                                intersects[0].object.material.color = new THREE.Color("#639af6");;
                                intersects[0].object.material.emissive = new THREE.Color("#000000");
                            }
                        }
                    );
                }); // Start a Click Job

                // Adding Stuff to Our World  
                // Add Point Light
                const light = new THREE.PointLight(
                    0xffffff /* Color */,
                    80 /* Intensity */,
                    100 /* Maximum Range of Light */
                );
                light.position.set(30, 30, 30);
                scene.add(light);

                // Add Primitive Shapes
                const randomIntFromInterval = (min, max) => { // min and max included 
                    return Math.floor(Math.random() * (max - min + 1) + min)
                }

                const spawnRandomCube = () => {
                    // Make Shape
                    const size = randomIntFromInterval(2, 7);
                    const geometry = new THREE.BoxBufferGeometry(
                        size /** height */,
                        size /** width */,
                        size /** length */);
                    // Make Texture
                    const material = new THREE.MeshStandardMaterial({ color: 0x639af6 });
                    // Make Cube
                    const cube = new THREE.Mesh(geometry, material);
                    // Random Position
                    cube.position.set(
                        randomIntFromInterval(-80, 80),
                        randomIntFromInterval(-80, 80),
                        randomIntFromInterval(-80, 80)
                    );
                    // Random Angle
                    cube.rotation.set(
                        THREE.Math.degToRad(randomIntFromInterval(0, 90)),
                        THREE.Math.degToRad(randomIntFromInterval(0, 90)),
                        THREE.Math.degToRad(randomIntFromInterval(0, 90))
                    )
                    // Add to Scene
                    scene.add(cube);
                    addToRCStore(cube);
                }

                for (let i = 0; i < 200; i++) spawnRandomCube();

                // Play Animation
                const RAF = () => {
                    requestAnimationFrame(() => {
                        // Animations

                        // Recursively Render
                        renderer.render(scene, camera);
                        RAF();
                    });
                };

                RAF();

                // Responsiveness
                window.addEventListener(
                    "resize",
                    () => {
                        camera.aspect = Cwidth() / Cheight();
                        renderer.setSize(Cwidth(), Cheight());
                        camera.updateProjectionMatrix();
                    },
                    false
                );
            }
        };

        init();
    </script>

Concept ๐Ÿค”

We'll have to setup an effects Composer which we can basically pass effects to, "effects" here are called Passes ( eg: Glitch Effect is called a Glitch Pass )

Render Effect or Render Pass is our base render, so we definitely need that, and then all the effects that we add will be applied in the order that we mention them in our code.

So if we do something like this in our code,

composer.addPass( renderPass );
/** some code */
composer.addPass( bloomPass );
/** some more code */
composer.addPass( glitchPass );

We'll get the following effect,

Order Of application of Effects, illustration

Also, now that our Composer is actually giving us the final result, Composer should be one that handles the final rendering, not the WebGLRenderer.

Setting Up ๐Ÿ—๏ธ

We'll need EffectComposer and RenderPass to use any post-processing effect, so let's add them to our imports,

import { EffectComposer } from "https://unpkg.com/three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "https://unpkg.com/three/examples/jsm/postprocessing/RenderPass.js";

And now just above // Play Animation, we setup our post processing,

Let's make our composer and hand it the Render Pass,

// Post Processing
const composer = new EffectComposer(renderer); // make composer
const renderPass = new RenderPass(scene, camera); // make render pass
composer.addPass(renderPass); // add render pass

And now, since our Composer will actually give us the real final result, it should be the one that handles the final rendering (painting on the HTML Canvas),

So let's replace

renderer.render(scene, camera);

in our // Play Animation section with

composer.render(scene, camera);

Now our Setup is complete!!

Right now things should look exactly as they did before, but now we can easily add more effects, so let's try it :D

Adding Bloom Effect ๐ŸŒŸ

Alright, let's first import the Bloom effect constructor from ThreeJs,

import { UnrealBloomPass } from "https://unpkg.com/three/examples/jsm/postprocessing/UnrealBloomPass.js";

Now in our Post Processing section, we can configure and add our Bloom : )

Let's make our Bloom effect with some config,

const bloomPass = new UnrealBloomPass(new THREE.Vector2(Cwidth(), Cheight()));
/** You can tweak the settings below to get best effect you desire */
bloomPass.threshold = 0;
bloomPass.strength = 0.8;
bloomPass.radius = 0.5;
bloomPass.exposure = 1;

Now that we have our Bloom Effect prepared, let's add it to the composer,

composer.addPass(bloomPass);

And we're done! that's it :D

Here's how it looks,

Bloom Effect Demo

The bloom effect essentially works on the emission / emissive property of the material, if we didn't give our cubes any emission there wouldn't be this glow-y effect.

Here's the ThreeJs demo on Bloom where you can play with all the settings and see what does what. (Click the image below to be redirected)

ofblum.gif

Summary ๐Ÿ“

So in this chapter we saw how to setup an Effects Composer and apply cool filters on our renders to make them really pop out :D

Here's the collection of all the effects ThreeJs offers built in, use these for now, somewhere down the line we'll learn how to make our own : )

Here's all the code for this chapter,

<div style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;" class="threeD"></div>
    <script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
    <script type="importmap">
    {
        "imports": {
            "three": "https://unpkg.com/three/build/three.module.js"
        }
    }
    </script>
    <script type="module">
        import * as THREE from "three";
        import { OrbitControls } from "https://unpkg.com/three/examples/jsm/controls/OrbitControls.js";
        import { UnrealBloomPass } from "https://unpkg.com/three/examples/jsm/postprocessing/UnrealBloomPass.js";
        import { EffectComposer } from "https://unpkg.com/three/examples/jsm/postprocessing/EffectComposer.js";
        import { RenderPass } from "https://unpkg.com/three/examples/jsm/postprocessing/RenderPass.js";
        const init = (
            targetClass = "threeD"
        ) => {
            const target = document.getElementsByClassName(targetClass);
            if (target) {
                // Renderer and Scene Setup
                const renderer = new THREE.WebGLRenderer({ alpha: true });
                const Cwidth = () => {
                    return document.getElementsByClassName(targetClass)[0].clientWidth;
                };
                const Cheight = () => {
                    return document.getElementsByClassName(targetClass)[0].clientHeight;
                };
                renderer.shadowMap.enabled = false;
                renderer.setPixelRatio(window.devicePixelRatio);
                renderer.setSize(Cwidth(), Cheight());
                renderer.physicallyCorrectLights = true;
                renderer.domElement.setAttribute("id", "three-canvas");

                target[0].appendChild(renderer.domElement);
                const scene = new THREE.Scene();
                const ambientLight = new THREE.AmbientLight(0xffffff, 2);
                scene.add(ambientLight);

                // Camera Setup
                const aspect = Cwidth() / Cheight();
                const fov = 60;
                const near = 1.0;
                const far = 200.0;
                const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
                camera.position.set(0, 30, 0);
                camera.lookAt(0, 0, 0);

                // Setting Up Orbital Controls 
                const controls = new OrbitControls(camera, renderer.domElement);
                controls.update();

                // Setting up Store for Raycasting
                let raycastStore = [];
                const addToRCStore = (obj) => raycastStore.push(obj);
                const removeFromRCStore = (obj) => {
                    for (let i = 0; i < raycastStore.length; i++) {
                        if (raycastStore[i] === obj) raycastStore.splice(i, 1);
                    }
                }

                // Setting up RayCaster
                let ndcMouseX = -1;
                let ndcMouseY = -1;
                let rc = new THREE.Raycaster();

                document.addEventListener("mousemove", (e) => {
                    ndcMouseX = (e.clientX / window.innerWidth) * 2 - 1;
                    ndcMouseY = -(e.clientY / window.innerHeight) * 2 + 1;
                });

                const CheckRC = (camera, onIntersection, onEmpty = () => { }) => {
                    rc.setFromCamera(new THREE.Vector3(ndcMouseX, ndcMouseY, 0), camera);
                    let intersects = rc.intersectObjects(raycastStore);
                    if (intersects.length > 0) {
                        onIntersection(intersects);
                    } else {
                        onEmpty();
                    }
                }

                const RCJob = setInterval(() => {
                    CheckRC(
                        camera,
                        () => { document.body.style.cursor = "pointer"; },
                        () => { document.body.style.cursor = "auto"; }
                    );
                }, 100); // Start an Hover Job

                document.addEventListener("mousedown", () => {
                    CheckRC(
                        camera,
                        (intersects) => {
                            const col = new THREE.Color("#b62886");
                            if (!intersects[0].object.material.color.equals(col)) {
                                intersects[0].object.material.color = col;
                                intersects[0].object.material.emissive = new THREE.Color("#AAAAAA");
                            } else {
                                intersects[0].object.material.color = new THREE.Color("#639af6");;
                                intersects[0].object.material.emissive = new THREE.Color("#000000");
                            }
                        }
                    );
                }); // Start a Click Job

                // Adding Stuff to Our World  
                // Add Point Light
                const light = new THREE.PointLight(
                    0xffffff /* Color */,
                    80 /* Intensity */,
                    100 /* Maximum Range of Light */
                );
                light.position.set(30, 30, 30);
                scene.add(light);

                // Add Primitive Shapes
                const randomIntFromInterval = (min, max) => { // min and max included 
                    return Math.floor(Math.random() * (max - min + 1) + min)
                }

                const spawnRandomCube = () => {
                    // Make Shape
                    const size = randomIntFromInterval(2, 7);
                    const geometry = new THREE.BoxBufferGeometry(
                        size /** height */,
                        size /** width */,
                        size /** length */);
                    // Make Texture
                    const material = new THREE.MeshStandardMaterial({ color: 0x639af6 });
                    // Make Cube
                    const cube = new THREE.Mesh(geometry, material);
                    // Random Position
                    cube.position.set(
                        randomIntFromInterval(-80, 80),
                        randomIntFromInterval(-80, 80),
                        randomIntFromInterval(-80, 80)
                    );
                    // Random Angle
                    cube.rotation.set(
                        THREE.Math.degToRad(randomIntFromInterval(0, 90)),
                        THREE.Math.degToRad(randomIntFromInterval(0, 90)),
                        THREE.Math.degToRad(randomIntFromInterval(0, 90))
                    )
                    // Add to Scene
                    scene.add(cube);
                    addToRCStore(cube);
                }

                for (let i = 0; i < 200; i++) spawnRandomCube();

                // Post Processing
                const composer = new EffectComposer(renderer); // make composer
                const renderPass = new RenderPass(scene, camera); // make render pass
                composer.addPass(renderPass); // add render pass

                const bloomPass = new UnrealBloomPass(new THREE.Vector2(Cwidth(), Cheight()));
                bloomPass.threshold = 0;
                bloomPass.strength = 0.8;
                bloomPass.radius = 0.5;
                bloomPass.exposure = 1;

                composer.addPass(bloomPass);



                // Play Animation
                const RAF = () => {
                    requestAnimationFrame(() => {
                        // Animations

                        // Recursively Render
                        composer.render(scene, camera);
                        RAF();
                    });
                };

                RAF();

                // Responsiveness
                window.addEventListener(
                    "resize",
                    () => {
                        camera.aspect = Cwidth() / Cheight();
                        renderer.setSize(Cwidth(), Cheight());
                        camera.updateProjectionMatrix();
                    },
                    false
                );
            }
        };

        init();
    </script>

Conclusion ๐Ÿ‚

So now the objects in our scene look great!, but does the background always have to be this dark depressing void though?

dark depressing void

Sure we can always do {alpha: true} in our WebGLRenderer initialization (and we have) and add a background image... but static background images are boring, we're doing 3D here! we won't accept anything less for any of our creations : )

So we'll learn how to easily import a 3D google-street-view-like environment into our scene to give some substance to it.

So do follow up on the chapter ThreeJs: Skyboxes and HDRIs!

Thank you so much for reading and as always, I hope you learned something : )

ย