Found this useful? Love this post

Experimenting with 3D web graphics to build a logo using three.js

With a bit of spare time the other month I decided to learn how to create some awesome 3D graphics in a web browser. I’d seen examples in the past of some cool stuff at Chrome Experiments website. So with a bit of research I decided to pick three.js as my Javascript library. I was surprised to find that partial support for WebGL sits at around 75% according to CanIUse.com and full support sitting at 49% (as of January 2015). I thought it would be much lower as I haven’t seen it around the web much.

I decided to make Mezzanine-media‘s logo, since thats where I work and initially started looking into three.js.

Building the logo

You can see the final product in it’s fullscreen glory here.

Building the final product can be split up into four sections:

  1. The Three.js initialisation and parameters
  2. The actual object(s) (The Mezzanine logo in this case)
  3. The render composer passes and gradient to apply on the final scene
  4. The final touches

1. The Three.js initialisation and parameters

To save you time, you can download all of the files (120kb, ZIP) used to create the Mezzanine logo so you can get a jump start!

Create a new HTML file with the basic structure below:

<!DOCTYPE html>
<html>
<head>
 <title>Using Three.js</title>
 <script src="scripts/three.min.js"></script>
 <script src="scripts/BloomPass.js"></script>
 <script src="scripts/CopyShader.js"></script>
 <script src="scripts/ConvolutionShader.js"></script>
 <script src="scripts/Detector.js"></script>
 <script src="scripts/dragpancontrols.js"></script>
 <script src="scripts/EffectComposer.js"></script>
 <script src="scripts/FilmPass.js"></script>
 <script src="scripts/FilmShader.js"></script>
 <script src="scripts/FocusShader.js"></script>
 <script src="scripts/FXAAShader.js"></script>
 <script src="scripts/MaskPass.js"></script>
 <script src="scripts/RenderPass.js"></script>
 <script src="scripts/ShaderPass.js"></script>
 <script src="scripts/VignetteShader.js"></script>
 <script src="scripts/mezzanine-three.js"></script>
 <style>
    html, body {background-color: #1a1a1a;height: 100%; overflow: hidden;padding: 0;margin: 0;width: 100%;}
    p {color: #FFFFFF; text-align: center;}
 </style>
</head>
<body>
    <script>
    // Where your Three.js initialisation code will go
    </script>
</body>
</html>

We now are going to start constructing our environment with JavaScript. Let’s begin by declaring a bunch of variables:

var container, stats;
var camera, scene, renderer;

var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;

var attributes, uniforms, shaderMaterial, vc1;
var mesh, parent;
var renderPass, copyPass, effectFocus, composer;

var cameraControls;

Next up is to create our init() function:

function init() {
    container = document.createElement('div');
    document.body.appendChild(container);

    //Renderer setup. Test to see if WebGL is supported
    if (Detector.webgl) {
        renderer = new THREE.WebGLRenderer({
            antialias: true
        });
    } else {
        container.innerHTML = '<p>It appears your browser doesn\'t support WebGL :(</p>';
        return false;
    }
    renderer.autoClear = false;
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    // Create a new scene
    scene = new THREE.Scene();
}

In the section above, we basically create a new div, test to see if the browser supports WebGL using Detector, initialise the THREE.WebGLRenderer and add the renderer to the new div we created. We finally need to declare a new scene, we will be adding all our components to this scene.

Adding the Camera

    camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 10000 );
    camera.position.set(0, 0, 20);
    scene.fog = new THREE.Fog(0x000000, 1, 1000);

Declaring PerspectiveCamera, we set the following frustum (the three-dimensional region that is visible) parameters:

  • fov – Camera frustum vertical field of view (75)
  • aspect – Camera frustum aspect ratio (window.innerWidth / window.innerHeight)
  • near – Camera frustum near plane (1)
  • far – Camera frustum far plane (10000)

Adding some lights

    // Light 1
    var light = new THREE.DirectionalLight(0xffffff);
    light.position.set(200, 200, 200);
    scene.add(light);

    // Light 2
    var light2 = new THREE.DirectionalLight(0xffffff);
    light2.position.set(-200, 200, 200);
    light2.intensity = 0.5;
    scene.add(light2);

We have added two lights to the scene, set the positioning and in the second instance, set the intensity of the light before adding it to the scene. Relatively straight forward. Refer to the DirctionalLight documentation on threejs.org for a full set of options.

Creating the grid

With a little bit of maths, we’re going to create a nice simple little grid to give some perspective to our environment.

    // Create a simple grid to give some visual depth
    var line_material = new THREE.LineBasicMaterial({
            color: 0x84B7EF,
            linewidth: .5,
            transparent: true,
            opacity: .3
        }),
        geometry = new THREE.Geometry(),
        floor = -1,
        i = 0,
        step = 1,
        size = 14;

    for (i; i <= size / step * 2; i += 1) {

            geometry.vertices.push(new THREE.Vector3(-size, floor, i * step - size));
            geometry.vertices.push(new THREE.Vector3(size, floor, i * step - size));

            geometry.vertices.push(new THREE.Vector3(i * step - size, floor, -size));
            geometry.vertices.push(new THREE.Vector3(i * step - size, floor, size));

    }

    var line = new THREE.Line(geometry, line_material, THREE.LinePieces);
    line.scale.multiplyScalar(50);
    line.position.set(0, -50, -100);
    scene.add(line);

In the code above we:

  • Declare our line_material and specify it’s parameters,
  • Declare the Geometry object which we then conduct a for loop to push new vertices that will determine our grid,
  • Create a Line object and pass in our geometry, line_material and the type (LinePieces, in this case),
  • Apply multiply scalar to the line,
  • Set the position and then add it to the scene.

Adding the camera pan control

We also need to initialise the Camera’s drag-pan controls. This allows the end user to move around the scene from a fixed point. We will use the cameraControls variable later on in our tutorial to update the scene on window resizing events and on the render() function.

//Add the camera pan control
cameraControls = new THREEx.DragPanControls(camera);

Load the Mezzanine logo

We haven’t build it yet, but lets place the following line of code so when we start constructing our logo, so we can see our progress.

    var mezzanine = new threeMezzanine();

Your page so far

<!DOCTYPE html>
<html>
<head>
    <title>Using Three.js</title>
    <script src="scripts/three.min.js"></script>
    <script src="scripts/BloomPass.js"></script>
    <script src="scripts/CopyShader.js"></script>
    <script src="scripts/ConvolutionShader.js"></script>
    <script src="scripts/Detector.js"></script>
    <script src="scripts/dragpancontrols.js"></script>
    <script src="scripts/EffectComposer.js"></script>
    <script src="scripts/FilmPass.js"></script>
    <script src="scripts/FilmShader.js"></script>
    <script src="scripts/FocusShader.js"></script>
    <script src="scripts/FXAAShader.js"></script>
    <script src="scripts/MaskPass.js"></script>
    <script src="scripts/RenderPass.js"></script>
    <script src="scripts/ShaderPass.js"></script>
    <script src="scripts/VignetteShader.js"></script>
    <script src="scripts/mezzanine-three.js"></script>
    <style>
        html, body {background-color: #1a1a1a;height: 100%; overflow: hidden;padding: 0;margin: 0;width: 100%;}
        p {color: #FFFFFF; text-align: center;}
    </style>
</head>
<body>
<script>
    var container, stats;
    var camera, scene, renderer;

    var windowHalfX = window.innerWidth / 2;
    var windowHalfY = window.innerHeight / 2;

    var attributes, uniforms, shaderMaterial, vc1;
    var mesh, parent;
    var renderPass, copyPass, effectFocus, composer;

    var cameraControls;

    function init() {
        container = document.createElement('div');
        document.body.appendChild(container);

        //Renderer setup. Test to see if WebGL is supported
        if (Detector.webgl) {
            renderer = new THREE.WebGLRenderer({
                antialias: true
            });
        } else {
            container.innerHTML = '<p>It appears your browser doesn\'t support WebGL :(</p>';
            return false;
        }
        renderer.autoClear = false;
        renderer.setSize(window.innerWidth, window.innerHeight);
        container.appendChild(renderer.domElement);

        // Create a new scene
        scene = new THREE.Scene();

        // Adding the camera
        camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 10000 );
        camera.position.set(0, 0, 20);
        scene.fog = new THREE.Fog(0x000000, 1, 1000);

        // Light 1
        var light = new THREE.DirectionalLight(0xffffff);
        light.position.set(200, 200, 200);
        scene.add(light);

        // Light 2
        var light2 = new THREE.DirectionalLight(0xffffff);
        light2.position.set(-200, 200, 200);
        light2.intensity = 0.5;
        scene.add(light2);

        // Create a simple grid to give some visual depth
        var line_material = new THREE.LineBasicMaterial({
                    color: 0x84B7EF,
                    linewidth: .5,
                    transparent: true,
                    opacity: .3
                }),
                geometry = new THREE.Geometry(),
                floor = -1,
                i = 0,
                step = 1,
                size = 14;

        for (i; i <= size / step * 2; i += 1) {

            geometry.vertices.push(new THREE.Vertex(new THREE.Vector3(-size, floor, i * step - size)));
            geometry.vertices.push(new THREE.Vertex(new THREE.Vector3(size, floor, i * step - size)));

            geometry.vertices.push(new THREE.Vertex(new THREE.Vector3(i * step - size, floor, -size)));
            geometry.vertices.push(new THREE.Vertex(new THREE.Vector3(i * step - size, floor, size)));

        }

        var line = new THREE.Line(geometry, line_material, THREE.LinePieces);
        line.scale.multiplyScalar(50);
        line.position.set(0, -50, -100);
        scene.add(line);

        //Add the camera pan control
        cameraControls = new THREEx.DragPanControls(camera);

        var mezzanine = new threeMezzanine();
    }
</script>
</body>
</html>

2. The actual object (Mezzanine logo)

Our logo was created in Adobe Illustrator. This makes it relatively painless to get an object up and running. You’ll need to download and install an awesome Illustrator plugin called Ai -> Canvas plugin created by Mike Swanson. Once you’ve done that, you’ll need to open your AI file and remove any artboards / content that you don’t want to appear in your final render. (My first pass resulted in about 8 different versions of the Mezzanine logo getting output!). Once you’ve exported the file using Ai->Canvas, create a new JavaScript file and add a function called yourLogo(). Then go and open the HTML file you exported from Adobe Illustrator and view the source of the page and copy everything inside function draw(ctx) and paste into the yourLogo() function. In the new .js file, go through and comment out all of the following occurrences:

  • ctx.beginPath()
  • ctx.closePath()
  • ctx.fill()
  • cta.restore()

Now you’ll need to go through and create variables for each path in your file. You can work out when a new path occurs when you see ctx.beginPath(). You can refer to the mezzanine-three.js file as to how I set it up (it’s by no means perfect but gives you a rough idea). You may have noticed in the mezzanine-three.js file that there were a couple of references to ctxXX.holes.push(ctxXXSubtract). This basically, as the function reads, allows you to punch holes in your paths. Now you probably want to see what it will look like as you’re converting your paths. Create a function with the following:

    function addShape(shape, extrudeSettings, color, x, y, z, rx, ry, rz, s) {

        var geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
        var mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({
            color: color,
            shading: THREE.FlatShading
        }));

        mesh.position.set(x, y, z);
        mesh.rotation.set(rx, ry, rz);
        mesh.scale.set(s, s, s);

        parent.add(mesh);
    }

Then add the following function:

    function init() {
        scene.add(parent);
        addShape(ctx, extrudeSettings3, 0xadef, -173.45, 66.45, -10, 0, 3.14, 3.14, 1);
        parent.scale.multiplyScalar(0.05); // Sets the scale for the object
    }

For each new ctx variable you have, you’ll need to add another addShape function. You’ll also need to create some extrude settings to make your elements appear in 3D. You’ll notice at the top of the mezzanine-three.js file I have a couple of variables called extrudeSettingsX which contain the following parameters with various values:

    var extrudeSettings3 = {
        bevelEnabled: false,
        bevelThickness: 10.0,
        bevelSize: 0,
        bevelSegments: 50,
        curveSegments : 50,
        steps: 50,
        amount: 50
    };

You’ll finally need to call the init() function at the bottom of yourLogo function.

3. The render composer passes and gradient to apply on the final scene

At this point we have the logo and our stage set. Now to bring a little class.

Adding a gradient

We first need to insert two script blocks just after the opening of the <body> tag.

<script type="x-shader/x-vertex" id="vertexShader">

    varying vec3 vWorldPosition;

    void main() {

    vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
    vWorldPosition = worldPosition.xyz;

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

    }

</script>
<script type="x-shader/x-fragment" id="fragmentShader">

    uniform vec3 topColor;
    uniform vec3 bottomColor;
    uniform float offset;
    uniform float exponent;

    varying vec3 vWorldPosition;

    void main() {

    float h = normalize( vWorldPosition + offset ).y;
    gl_FragColor = vec4( mix( bottomColor, topColor, max( pow( h, exponent ), 0.0 ) ), 1.0 );

    }

</script>

These two script tags will be used to create our gradient. We will now need to place the following script after the last piece of script we wrote at the end of part 1. This will initialise the gradient.

  // Gradient Back
 var vertexShader = document.getElementById('vertexShader').textContent;
 var fragmentShader = document.getElementById('fragmentShader').textContent;
 var uniforms = {
     topColor: {
         type: "c",
         value : new THREE.Color(0x666666)
 },
     bottomColor: {
         type: "c",
         value : new THREE.Color(0x000000)
 },
     offset: {
         type: "f",
         value: 50
 },
     exponent: {
         type: "f",
         value: 1
     }
 };

 var skyGeo = new THREE.SphereGeometry(1500, 32, 15);
 var skyMat = new THREE.ShaderMaterial({
     vertexShader: vertexShader,
     fragmentShader: fragmentShader,
     uniforms: uniforms,
     side: THREE.BackSide
 });
 var sky = new THREE.Mesh(skyGeo, skyMat);
 sky.position.set(0, 0, 0);
 scene.add(sky);

Walking through the script above, we get the two script elements that we placed at the top of the document. We then initialise the uniforms variable object. Within this object we declare the topColor with a type of “c” for THREE.Color and the value of the gradient being “new THREE.Color(0x666666)”. We then repeat this for bottomColor changing the value to a different colour. We then set the offset and exponent values with a type of “f” to indicate a float value.

We then declare skyGeo and create a new THREE.SphereGeometry passing in the radius, widthSegments (number of horizontal segments. Minimum value is 3, and default is 8) and heightSegments (number of vertical segments. Minimum value is 2, and the default is 6).

We now need to create the THREE.ShaderMaterial and pass in the variables we have declared previously:

  • vertexShader
  • fragmentShader
  • uniforms
  • THREE.BackSide (Defines which faces will be rendered – options are front, back or both.

After this we declare sky and initialise a new THREE.Mesh(skyGeo, skyMat) passing in the previous two variables declared. We then need to set the position of the sky and finally add the sky to our scene.

Adding the composer passes

Please note the following is not part of the officially supported three.js library. However, it is used in many of their examples.

We now can add the composer passes which will provide a cleaner finish to our scene.

After adding the sky to our scene we need to call our function that has the composer elements.

        
// Call the composer passes
initPostProcessing();

We now need to create two functions:

function initPostProcessing() {

   //Create Shader Passes
   var renderModel = new THREE.RenderPass(scene, camera);
   copyPass = new THREE.ShaderPass(THREE.CopyShader);
   composer = new THREE.EffectComposer(renderer);
   composer.addPass(renderModel);
   composer.addPass(copyPass);
   copyPass.renderToScreen = true;
   toggleEffects();
}


function toggleEffects() {

    composer = new THREE.EffectComposer(renderer);
    composer.addPass(new THREE.RenderPass(scene, camera));

    var effectFXAA = new THREE.ShaderPass(THREE.FXAAShader);
    var width = window.innerWidth || 2;
    var height = window.innerHeight || 2;
    effectFXAA.uniforms['resolution'].value.set(1 / width, 1 / height);
    composer.addPass(effectFXAA);

    var effectFilm = new THREE.FilmPass(0.1, 0.125, 2048, false);
    composer.addPass(effectFilm);

    var effectBloom = new THREE.BloomPass(.45);
    composer.addPass(effectBloom);

    var vignettePass = new THREE.ShaderPass(THREE.VignetteShader);
    vignettePass.uniforms["darkness"].value = 1.1;
    vignettePass.uniforms["offset"].value = 1;
    vignettePass.renderToScreen = true;
    composer.addPass(vignettePass);

}

Looking at the initPostProcessing function, we create a renderModel = new THREE.RenderPass, followed by a copyPass = new THREE.ShaderPass and composer = new THREE.EffectComposer. We then addPass the renderModel and copyPass variables to composer.  We then set the copyPass.renderToScreen variable to true, this ensures that our passes are actually rendered to the screen. We finally trigger the toggleEffects function. We could put the contents of this function inside the initPostProcessing function, however, for testing purposes I’ve split them.

Moving onto the toggleEffects function, we initialise a new THREE.EffectComposer and add the scene and camera to the new pass. We are then going to add four passes:

  • FXAAShader – makes the rendered product softer, smoothing out hard edges
  • FilmPass – adds a film-like look to your scene. You can set the noise intensity, scan lines intensity, number of scan lines and if the scene should appear in greyscale or colour.
  • BloomPass – adds a glow to your scene (similar effect to under/over exposing an image)
  • VignetteShader – adds a vignette to the scene

Resizing the window

While not required, we should add a function that updates the scene when the browser window is resized. It’s a relatively simple function. We need to first add an event listener and pass it a function:

// Bind the resize event to ensure it fills the window
window.addEventListener('resize', onWindowResize, false);

We then need to create the function:

    function onWindowResize() {

        windowHalfX = window.innerWidth / 2;
        windowHalfY = window.innerHeight / 2;

        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);

    }

The function declares a windowHalfX and windowHalfY which are then used to set the camera.aspect property. We then update the camera.updateProjectionMatrix(). Finally we set the size of the renderer viewport to the current width and height.

4. The final touches

We’re so close to the finish line. We just need to create two more tiny functions and then call them!

function animate() {
    requestAnimationFrame(animate);
    render();
}

function render() {

    cameraControls.update();
    composer.render(scene, camera);

}

init();animate();

The animate function is called at the bottom of our script. The first line, requestAnimationFrame(animate); is a browser-based API which was pass in the animate variable as a callback. We then call the render() function on the second line.

The render function consists of just two lines, the first, cameraControls.update() to update the position of the camera relative to user input. The second line to update the scene and camera. Note we have used the composer variable (with all our effect passes) as opposed to the renderer variable.

We finally init() our three.js animation and call the animate() function to trigger the drawing of the scene and camera.

Conclusion

You should now have a fully functional three.js animation running in your browser. Please feel free to post any suggestions or comments below.

Subscribe

You'll only get 1 email per month containing new posts (I hate spam as much as you!). You can opt out at anytime.

Categories

Leave a Reply

Your email address will not be published. Required fields are marked *

Preview Comment

4 Comments

  1. Daniel
    https://www.domsammut.com/?p=1134#comment-2028

    Daniel

    Hi,

    Finally I found a good tutorial about this, I have been looking a lot to be able to build a logo on this way. I have some questions if you can help me.

    1. How can I set the size of the logo? I created mine in Illustrator but it’s quite big, is it possible to manage the size on the code?
    2. I think I managed resizing the logo where it says: camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 10000 ), I changed the 75 for 100 and now it looks smaller.
    3. Now that I resized the logo, it is not centered, it’s a little to the right side. How is this possible to center it?
    4. How can I change the black background to a different one?

    Thanks so much and thanks for sharing your knowledges

    • Dom Sammut
      https://www.domsammut.com/?p=1134#comment-2030

      Dom Sammut

      Hey Daniel,

      Glad you’ve found this useful! In regards to your questions:

      1. If you take a look at the mezzanine-three.js file, inside the init function (line 305), there is a line of code present that controls the scale of the object:
        parent.scale.multiplyScalar(0.05);
        
      2. Cool, maybe try my solution in point 1.
      3. Try my suggestion in point one, I did a quick test, and it remains in the centre focal point. Alternatively you can play around with camera position.
      4. The black background is set as part of the add gradient step. You’ll see our black 0x000000 and 0x666666. Feel free to change the gradient or remove altogether and set a solid colour.

      Hope that answers your questions.

      Cheers
      Dom

  2. Noobie
    https://www.domsammut.com/?p=1134#comment-1926

    Noobie

    Hi,

    I want the logo to appear in the header section of my website’s index.html file. How would I do this?

    Also, how would I change the words from “Mezzanine Media” to ABC Corp?

    • Dom Sammut
      https://www.domsammut.com/?p=1134#comment-1927

      Dom Sammut

      Hey Noobie,

      In my example, I created the logo in Adobe Illustrator. You’ll need to follow Part 2 to convert it into the required format. I have not supplied the logo illustrator file as this is specific to a particular business. You can use pretty much any illustrator file provided you follow the guidelines :)

      Cheers
      Dom

css.php