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:
- The Three.js initialisation and parameters
- The actual object(s) (The Mezzanine logo in this case)
- The render composer passes and gradient to apply on the final scene
- 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.
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.
Thanks so much and thanks for sharing your knowledges
https://www.domsammut.com/?p=1134#comment-2030
#Dom Sammut
Hey Daniel,
Glad you’ve found this useful! In regards to your questions:
init
function (line 305), there is a line of code present that controls the scale of the object:0x000000
and0x666666
. Feel free to change the gradient or remove altogether and set a solid colour.Hope that answers your questions.
Cheers
Dom
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?
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