Animating a million letters using Three.js My goal in this article is to draw a million animated letters on the screen at a smooth frame rate. This task should be quite possible with modern GPUs. Each letter consists of two textured triangles, so we're only talking about two million triangles per frame. If you're coming from a traditional JavaScript animation background, this all sounds like madness. Two million triangles updated every frame is definitely not something you would like to do with JavaScript today. But thankfully we have WebGL, which lets us tap into the awesome power of modern GPUs. And two million animated triangles is quite doable with a modern GPU and some shader magic. Writing efficient WebGL code Writing efficient WebGL code requires a certain mindset. The usual way to draw using WebGL is to set up your uniforms, buffers and shaders for each object, followed by a call to draw the object. This way of drawing works when drawing a small number of objects. To draw a large number of objects, you should minimize the amount of WebGL state changes. To start with, draw all objects using the same shader after each other, so that you don't have to change shaders between objects. For simple objects like particles, you could bundle several objects into a single buffer and edit it using JavaScript. That way you'd only have to reupload the vertex buffer instead of changing shader uniforms for every single particle. But to go really fast, you need to push most of your computation to the shaders. That's what I'm trying to do here. Animate a million letters using shaders. The article's code uses the Three.js library, which abstracts away all the tedious boilerplate from writing WebGL code. Instead of having to write hundreds of lines of WebGL state setup and error handling, with Three.js you only need to write a couple lines of code. It's also easy to tap into the WebGL shader system from Three.js. Drawing multiple objects using a single draw call Here's a small pseudo-code example of how you might draw multiple objects using a single draw call. The traditional way is to draw one object at a time like this: for (var i=0; i lettersPerSide * lettersPerSide) { code = 0; // Clamp character codes to letter map size. } var cx = code % lettersPerSide; // Cx is the x-index of the letter in the map. var cy = Math.floor(code / lettersPerSide); // Cy is the y-index of the letter in the map. // Add letter vertices to the geometry. var v,t; geo.vertices.push( new THREE.Vector3( x*1.1+0.05, line*1.1+0.05, 0 ), new THREE.Vector3( x*1.1+1.05, line*1.1+0.05, 0 ), new THREE.Vector3( x*1.1+1.05, line*1.1+1.05, 0 ), new THREE.Vector3( x*1.1+0.05, line*1.1+1.05, 0 ) ); // Create faces for the letter. var face = new THREE.Face3(i*4+0, i*4+1, i*4+2); geo.faces.push(face); face = new THREE.Face3(i*4+0, i*4+2, i*4+3); geo.faces.push(face); // Compute texture coordinates for the letters. var tx = cx/lettersPerSide, ty = cy/lettersPerSide, off = 1/lettersPerSide; var sz = lettersPerSide*fontSize; geo.faceVertexUvs[0].push([ new THREE.Vector2( tx, ty+off ), new THREE.Vector2( tx+off, ty+off ), new THREE.Vector2( tx+off, ty ) ]); geo.faceVertexUvs[0].push([ new THREE.Vector2( tx, ty+off ), new THREE.Vector2( tx+off, ty ), new THREE.Vector2( tx, ty ) ]); // On newline, move to the line below and move the cursor to the start of the line. // Otherwise move the cursor to the right. if (code == 10) { line--; x=0; } else { x++; } } Vertex shader for animating the letters With a simple vertex shader, I get a flat view of the text. Nothing fancy. Runs well, but if I want to animate it, I need to do the animation in JavaScript. And JavaScript is kinda slow for animating the six million vertices involved, especially if you want to do it on every frame. Maybe there is there a faster way. Why yes, we can do procedural animation. What that means is that we do all our position and rotation math in the vertex shader. Now I don't need to run any JavaScript to update the positions of the vertices. The vertex shader runs very fast and I get a smooth frame rate even with a million triangles being individually animated every frame. To address the individual triangles, I round down the vertex coordinates so that all four points of a letter quad map to a single unique coordinate. Now I can use this coordinate to set the animation parameters for the letter in question. To be able to successfully round down coordinates, coordinates from two different letters can't overlap. The easiest way to do this is by using square letter quads with a small offset separating the letter from the one on its right side and the line above it. For example, you could use a width and height of 0.5 for the letters and align the letters on integer coordinates. Now when you round down the coordinate of any letter vertex, you get the bottom-left coordinate of the letter. Rounding down vertex coordinates to find the top-left corner of a letter. Rounding down vertex coordinates to find the top-left corner of a letter. To better understand the animated vertex shader, I'm going to go through a simple run-of-the-mill vertex shader first. This is what normally happens when you draw a 3D model to the screen. The vertices of the model are transformed by a couple transformation matrices to project each 3D vertex onto the 2D screen. Whenever a triangle defined by three of these vertices lands inside the viewport, the pixels it covers are processed by the fragment shader to color them. Anyway, here's the simple vertex shader: varying float vUv; void main() { // modelViewMatrix, position and projectionMatrix are magical // attributes that Three.js defines for us. // Transform current vertex by the modelViewMatrix // (bundled model world position & camera world position matrix). vec4 mvPosition = modelViewMatrix * position; // Project camera-space vertex to screen coordinates // using the camera's projection matrix. vec4 p = projectionMatrix * mvPosition; // uv is another magical attribute from Three.js. // We're passing it to the fragment shader unchanged. vUv = uv; gl_Position = p; } And now, the animated vertex shader. Basically, it does the same thing as the simple vertex shader, but with a small twist. Instead of transforming each vertex by just the transformation matrices, it applies a time-dependent animated transformation as well. To make each letter animate a bit differently, the animated vertex shader also modifies the animation based on the coordinates of the letter. It's going to look a good deal more complicated than the simple vertex shader because, well, it is more complicated. uniform float uTime; uniform float uEffectAmount; varying float vZ; varying vec2 vUv; // rotateAngleAxisMatrix returns the mat3 rotation matrix // for given angle and axis. mat3 rotateAngleAxisMatrix(float angle, vec3 axis) { float c = cos(angle); float s = sin(angle); float t = 1.0 - c; axis = normalize(axis); float x = axis.x, y = axis.y, z = axis.z; return mat3( t*x*x + c, t*x*y + s*z, t*x*z - s*y, t*x*y - s*z, t*y*y + c, t*y*z + s*x, t*x*z + s*y, t*y*z - s*x, t*z*z + c ); } // rotateAngleAxis rotates a vec3 over the given axis by the given angle and // returns the rotated vector. vec3 rotateAngleAxis(float angle, vec3 axis, vec3 v) { return rotateAngleAxisMatrix(angle, axis) * v; } void main() { // Compute the index of the letter (assuming 80-character max line length). float idx = floor(position.y/1.1)*80.0 + floor(position.x/1.1); // Round down the vertex coords to find the bottom-left corner point of the letter. vec3 corner = vec3(floor(position.x/1.1)*1.1, floor(position.y/1.1)*1.1, 0.0); // Find the midpoint of the letter. vec3 mid = corner + vec3(0.5, 0.5, 0.0); // Rotate the letter around its midpoint by an angle and axis dependent on // the letter's index and the current time. vec3 rpos = rotateAngleAxis(idx+uTime, vec3(mod(idx,16.0), -8.0+mod(idx,15.0), 1.0), position - mid) + mid; // uEffectAmount controls the amount of animation applied to the letter. // uEffectAmount ranges from 0.0 to 1.0. float effectAmount = uEffectAmount; vec4 fpos = vec4( mix(position,rpos,effectAmount), 1.0 ); fpos.x += -35.0; // Apply spinning motion to individual letters. fpos.z += ((sin(idx+uTime*2.0)))*4.2*effectAmount; fpos.y += ((cos(idx+uTime*2.0)))*4.2*effectAmount; vec4 mvPosition = modelViewMatrix * fpos; // Apply wavy motion to the entire text. mvPosition.y += 10.0*sin(uTime*0.5+mvPosition.x/25.0)*effectAmount; mvPosition.x -= 10.0*cos(uTime*0.5+mvPosition.y/25.0)*effectAmount; vec4 p = projectionMatrix * mvPosition; // Pass texture coordinates and the vertex z-coordinate to the fragment shader. vUv = uv; vZ = p.z; // Send the final vertex position to WebGL. gl_Position = p; } To use the vertex shader, I use a THREE.ShaderMaterial, a material type that lets you use custom shaders and specify uniforms for them. Here's how I'm using THREE.ShaderMaterial in the demo: // First, set up uniforms for the shader. var uniforms = { // map contains the letter map texture. map: { type: "t", value: 1, texture: tex }, // uTime is the urrent time. uTime: { type: "f", value: 1.0 }, // uEffectAmount controls the amount of animation applied to the letters. uEffectAmount: { type: "f", value: 0.0 } }; // Next, set up the THREE.ShaderMaterial. var shaderMaterial = new THREE.ShaderMaterial({ uniforms: uniforms, // I have my shaders inside HTML elements like //