Twisted Colorful Spheres with Three.js

I love blobs and I enjoy looking for interesting ways to change basic geometries with Three.js: bending a plane, twisting a box, or exploring a torus (like in this 10-min video tutorial). So this time, my love for shaping things will be the excuse to see what we can do with a sphere, transforming it using shaders. 

This tutorial will be brief, so we’ll skip the basic render/scene setup and focus on manipulating the sphere’s shape and colors, but if you want to know more about the setup check out these steps.

We’ll go with a more rounded than irregular shape, so the premise is to deform a sphere and use that same distortion to color it.

Vertex displacement

As you’ve probably been thinking, we’ll be using noise to deform the geometry by moving each vertex along the direction of its normal. Think of it as if we were pushing each vertex from the inside out with different strengths. I could elaborate more on this, but I rather point you to this article by The Spite aka Jaume Sanchez Elias, he explains this so well! I bet some of you have stumbled upon this article already.

So in code, it looks like this:

varying vec3 vNormal;

uniform float uTime;
uniform float uSpeed;
uniform float uNoiseDensity;
uniform float uNoiseStrength;

#pragma glslify: pnoise = require(glsl-noise/periodic/3d)

void main() {
  float t = uTime * uSpeed;
  // You can also use classic perlin noise or simplex noise,
  // I'm using its periodic variant out of curiosity
  float distortion = pnoise((normal + t), vec3(10.0) * uNoiseDensity) * uNoiseStrength;

  // Disturb each vertex along the direction of its normal
  vec3 pos = position + (normal * distortion);

  vNormal = normal;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

And now we should see a blobby sphere:

See the Pen Vertex displacement by Mario (@marioecg) on CodePen.

You can experiment and change its values to see how the blob changes. I know we’re going with a more subtle and rounded distortion, but feel free to go crazy with it; there are audio visualizers out there that deform a sphere to the point that you don’t even think it’s based on a sphere.

Now, this already looks interesting, but let’s add one more touch to it next.

Noitation

…is just a word I came up with to combine noise with rotation (ba dum tss), but yes! Adding some twirl to the mix makes things more compelling.

If you’ve ever played with Play-Doh as a child, you have surely molded a big chunk of clay into a ball, grab it with each hand, and twisted in opposite directions until the clay tore apart. This is kind of what we want to do (except for the breaking part).

To twist the sphere, we are going to generate a sine wave from top to bottom of the sphere. Then, we are going to use this top-bottom wave as a rotation for the current position. Since the values increase/decrease from top to bottom, the rotation is going to oscillate as well, creating a twist:

varying vec3 vNormal;

uniform float uTime;
uniform float uSpeed;
uniform float uNoiseDensity;
uniform float uNoiseStrength;
uniform float uFrequency;
uniform float uAmplitude;

#pragma glslify: pnoise = require(glsl-noise/periodic/3d)
#pragma glslify: rotateY = require(glsl-rotate/rotateY)

void main() {
  float t = uTime * uSpeed;
  // You can also use classic perlin noise or simplex noise,
  // I'm using its periodic variant out of curiosity
  float distortion = pnoise((normal + t), vec3(10.0) * uNoiseDensity) * uNoiseStrength;

  // Disturb each vertex along the direction of its normal
  vec3 pos = position + (normal * distortion);

  // Create a sine wave from top to bottom of the sphere
  // To increase the amount of waves, we'll use uFrequency
  // To make the waves bigger we'll use uAmplitude
  float angle = sin(uv.y * uFrequency + t) * uAmplitude;
  pos = rotateY(pos, angle);    

  vNormal = normal;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

Notice how the waves emerge from the top, it’s soothing. Some of you might find this movement therapeutic, so take some time to appreciate it and think about what we’ve learned so far…

See the Pen Noitation by Mario (@marioecg) on CodePen.

Alright! Now that you’re back let’s get on to the fragment shader.

Colorific

If you take a close look at the shaders before, you see, almost at the end, that we’ve been passing the normals to the fragment shader. Remember that we want to use the distortion to color the shape, so first let’s create a varying where we pass that distortion to:

varying float vDistort;

uniform float uTime;
uniform float uSpeed;
uniform float uNoiseDensity;
uniform float uNoiseStrength;
uniform float uFrequency;
uniform float uAmplitude;

#pragma glslify: pnoise = require(glsl-noise/periodic/3d)
#pragma glslify: rotateY = require(glsl-rotate/rotateY)

void main() {
  float t = uTime * uSpeed;
  // You can also use classic perlin noise or simplex noise,
  // I'm using its periodic variant out of curiosity
  float distortion = pnoise((normal + t), vec3(10.0) * uNoiseDensity) * uNoiseStrength;

  // Disturb each vertex along the direction of its normal
  vec3 pos = position + (normal * distortion);

  // Create a sine wave from top to bottom of the sphere
  // To increase the amount of waves, we'll use uFrequency
  // To make the waves bigger we'll use uAmplitude
  float angle = sin(uv.y * uFrequency + t) * uAmplitude;
  pos = rotateY(pos, angle);    

  vDistort = distortion; // Train goes to the fragment shader! Tchu tchuuu

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

And use vDistort to color the pixels instead:

varying float vDistort;

uniform float uIntensity;

void main() {
  float distort = vDistort * uIntensity;

  vec3 color = vec3(distort);

  gl_FragColor = vec4(color, 1.0);
}

We should get a kind of twisted, smokey black and white color like so:

See the Pen Colorific by Mario (@marioecg) on CodePen.

With this basis, we’ll take it a step further and use it in conjunction with one of my favorite color functions out there.

Cospalette

Cosine palette is a very useful function to create and control color with code based on the brightness, contrast, oscillation of cosine, and phase of cosine. I encourage you to watch Char Stiles explain this further, which is soooo good. Final s/o to Inigo Quilez who wrote an article about this function some years ago; for those of you who haven’t stumbled upon his genius work, please do. I would love to write more about him, but I’ll save that for a poem.

Let’s use cospalette to input the distortion and see how it looks:

varying vec2 vUv;
varying float vDistort;

uniform float uIntensity;

vec3 cosPalette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
  return a + b * cos(6.28318 * (c * t + d));
}   

void main() {
  float distort = vDistort * uIntensity;

  // These values are my fav combination, 
  // they remind me of Zach Lieberman's work.
  // You can find more combos in the examples from IQ:
  // https://iquilezles.org/www/articles/palettes/palettes.htm
  // Experiment with these!
  vec3 brightness = vec3(0.5, 0.5, 0.5);
  vec3 contrast = vec3(0.5, 0.5, 0.5);
  vec3 oscilation = vec3(1.0, 1.0, 1.0);
  vec3 phase = vec3(0.0, 0.1, 0.2);

  // Pass the distortion as input of cospalette
  vec3 color = cosPalette(distort, brightness, contrast, oscilation, phase);

  gl_FragColor = vec4(color, 1.0);
}

¡Liiistoooooo! See how the color palette behaves similar to the distortion because we’re using it as input. Swap it for vUv.x or vUv.y to see different results of the palette, or even better, come up with your own input!

See the Pen Cospalette by Mario (@marioecg) on CodePen.

And that’s it! I hope this short tutorial gave you some ideas to apply to anything you’re creating or inspired you to make something. Next time you use noise, stop and think if you can do something extra to make it more interesting and make sure to save Cospalette in your shader toolbelt.

Explore and have fun with this! And don’t forget to share it with me on Twitter. If you got any questions or suggestions, let me know.

I hope you learned something new. Till next time! 

References and Credits

Thanks to all the amazing people that put knowledge out in the world!

The post Twisted Colorful Spheres with Three.js appeared first on Codrops.

Kinetic Typography with Three.js

Kinetic Typography may sound complicated but it’s just the elegant way to say “moving text” and, more specifically, to combine motion with text to create animations.

Imagine text on top of a 3D object, now could you see it moving along the object’s shape? Nice! That’s exactly what we’ll do in this article ? we’ll learn how to move text on a mesh using Three.js and three-bmfont-text.

We’re going to skip a lot of basics, so to get the most from this article we recommend you have some basic knowledge about Three.js, GLSL shaders, and three-bmfont-text.

Basis

The main idea for all these demos is to have a texture with text, use it on a mesh and play with it inside shaders. The simplest way of doing it is to have an image with text and then use it as a texture. But it can be a pain to figure out the correct size to try to display crisp text on the mesh, and later to change whatever text is in the image.

To avoid all these issues, we can generate that texture using code! We create a Render Target (RT) where we can have a scene that has text rendered with three-bmfont-text, and then use it as the texture of a mesh. This way we have more freedom to move, change, or color text. We’ll be taking this route following the next steps:

  1. Set up a RT with the text
  2. Create a mesh and add the RT texture
  3. Change the texture inside the fragment shader

To begin, we’ll run everything after the font file and atlas are loaded and ready to be used with three-bmfont-text. We won’t be going over this since I explained it in one of my previous articles.

The structure goes like this:

init() {
  // Create geometry of packed glyphs
  loadFont(fontFile, (err, font) => {
    this.fontGeometry = createGeometry({
      font,
      text: "ENDLESS"
    });

    // Load texture containing font glyphs
    this.loader = new THREE.TextureLoader();
    this.loader.load(fontAtlas, texture => {
      this.fontMaterial = new THREE.RawShaderMaterial(
        MSDFShader({
          map: texture,
          side: THREE.DoubleSide,
          transparent: true,
          negate: false,
          color: 0xffffff
        })
      );

      // Methods are called here
    });
  });
}

Now take a deep breath, grab your tea or coffee, chill, and let’s get started.

Render Target

A Render Target is a texture you can render to. Think of it as a canvas where you can draw whatever is inside and place it wherever you want. Having this flexibility makes the texture dynamic, so we can later add, change, or remove stuff in it.

Let’s set a RT along with a camera and a scene where we’ll place the text.

createRenderTarget() {
  // Render Target setup
  this.rt = new THREE.WebGLRenderTarget(
    window.innerWidth,
    window.innerHeight
  );

  this.rtCamera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
  this.rtCamera.position.z = 2.5;

  this.rtScene = new THREE.Scene();
  this.rtScene.background = new THREE.Color("#000000");
}

Once we have the RT scene, let’s use the font geometry and material previously created to make the text mesh.

createRenderTarget() {
  // Render Target setup
  this.rt = new THREE.WebGLRenderTarget(
    window.innerWidth,
    window.innerHeight
  );

  this.rtCamera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
  this.rtCamera.position.z = 2.5;

  this.rtScene = new THREE.Scene();
  this.rtScene.background = new THREE.Color("#000000");

  // Create text with font geometry and material
  this.text = new THREE.Mesh(this.fontGeometry, this.fontMaterial);

  // Adjust text dimensions
  this.text.position.set(-0.965, -0.275, 0);
  this.text.rotation.set(Math.PI, 0, 0);
  this.text.scale.set(0.008, 0.02, 1);

  // Add text to RT scene
  this.rtScene.add(this.text);
 
  this.scene.add(this.text); // Add to main scene
}

Note that for now, we added the text to the main scene to render it on the screen.

Cool! Let’s make it more interesting and “paste” the scene over a shape next.

Mesh and render texture

For simplicity, we’ll first use as shape a BoxGeometry together with ShaderMaterial to pass custom shaders, time and the render texture uniforms.

createMesh() {
  this.geometry = new THREE.BoxGeometry(1, 1, 1);

  this.material = new THREE.ShaderMaterial({
    vertexShader,
    fragmentShader,
    uniforms: {
      uTime: { value: 0 },
      uTexture: { value: this.rt.texture }
    }
  });

  this.mesh = new THREE.Mesh(this.geometry, this.material);

  this.scene.add(this.mesh);
}

The vertex shader won’t be doing anything interesting this time; we’ll skip it and focus on the fragment instead, which is showing the colors of the RT texture. It’s inverted for now (1. - texture) to stand out from the background.

varying vec2 vUv;

uniform sampler2D uTexture;

void main() {
  vec3 texture = texture2D(uTexture, vUv).rgb;

  gl_FragColor = vec4(1. - texture, 1.);
}

Normally, we would just render the main scene directly, but with a RT we have to first render to it before rendering to the screen.

render() {
  ...

  // Draw Render Target
  this.renderer.setRenderTarget(this.rt);
  this.renderer.render(this.rtScene, this.rtCamera);
  this.renderer.setRenderTarget(null);
  this.renderer.render(this.scene, this.camera);
}

And now a box should appear on the screen where each face has the text on it:

Looks alright so far, but what if we could cover more text around the shape?

Repeating the texture

GLSL’s built-in function fract comes handy to make repetition. We’ll use it to repeat the texture coordinates when multiplying them by a scalar so it wraps between 0 and 1.

varying vec2 vUv;

uniform sampler2D uTexture;

void main() {
  vec2 repeat = vec2(2., 6.); // 2 columns, 6 rows
  vec2 uv = fract(vUv * repeat);

  vec3 texture = texture2D(uTexture, uv).rgb;
  texture *= vec3(uv.x, uv.y, 1.);

  gl_FragColor = vec4(texture, 1.);
}

Notice that we also multiply the texture by the uv components to visualize the modified texture coordinates.

We’re getting there, right? The text should also follow the object’s shape; here’s where time comes in. We need to add it to the texture coordinates, in this case to the x component so it moves horizontally.

varying vec2 vUv;

uniform sampler2D uTexture;
uniform float uTime;

void main() {
  float time = uTime * 0.75;
  vec2 repeat = vec2(2., 6.);
  vec2 uv = fract(vUv * repeat + vec2(-time, 0.));

  vec3 texture = texture2D(uTexture, uv).rgb;

  gl_FragColor = vec4(texture, 1.);
}

And for a sweet touch, let’s blend the color with the the background.

This is basically the process! RT texture, repetition, and motion. Now that we’ve looked at the mesh for so long, using a BoxGeometry gets kind of boring, doesn’t it? Let’s change it in the next final bonus chapter.

Changing the geometry

As a kid, I loved playing and twisting these tangle toys, perhaps that’s the reason why I find satisfaction with knots and twisted shapes? Let this be an excuse to work with a torus knot geometry.

For the sake of rendering smooth text we’ll exaggerate the amount of tubular segments the knot has.

createMesh() {
  this.geometry = new THREE.TorusKnotGeometry(9, 3, 768, 3, 4, 3);

  ...
}

Inside the fragment shader, we’ll repeat any number of columns we want just to make sure to leave the same number of rows as the number of radial segments, which is 3.

varying vec2 vUv;

uniform sampler2D uTexture;
uniform float uTime;

void main() {
  vec2 repeat = vec2(12., 3.); // 12 columns, 3 rows
  vec2 uv = fract(vUv * repeat);

  vec3 texture = texture2D(uTexture, uv).rgb;
  texture *= vec3(uv.x, uv.y, 1.);

  gl_FragColor = vec4(texture, 1.);
}

And here’s our tangled torus knot:

Before adding time to the texture coordinates, I think we can make a fake shadow to give a better sense of depth. For that we’ll need to pass the position coordinates from the vertex shader using a varying.

varying vec2 vUv;
varying vec3 vPos;

void main() {
  vUv = uv;
  vPos = position;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}

We now can use the z-coordinates and clamp them between 0 and 1, so the regions of the mesh farther the screen get darker (towards 0), and the closest lighter (towards 1).

varying vec3 vPos;

void main() {
  float shadow = clamp(vPos.z / 5., 0., 1.);

  gl_FragColor = vec4(vec3(shadow), 1.);
}

See? It sort of looks like white bone:

Now the final step! Multiply the shadow to blend it with the texture, and add time again.

varying vec2 vUv;
varying vec3 vPos;

uniform sampler2D uTexture;
uniform float uTime;

void main() {
  float time = uTime * 0.5;
  vec2 repeat = -vec2(12., 3.);
  vec2 uv = fract(vUv * repeat - vec2(time, 0.));

  vec3 texture = texture2D(uTexture, uv).rgb;

  float shadow = clamp(vPos.z / 5., 0., 1.);

  gl_FragColor = vec4(texture * shadow, 1.);
}

Fresh out of the oven! Look at this sexy torus coming out of the darkness. Internet high five!


We’ve just scratched the surface making repeated tiles of text, but there are many ways to add fun to the mixture. Could you use trigonometry or noise functions? Play with color? Text position? Or even better, do something with the vertex shader. The sky’s the limit! I encourage you to explore this and have fun with it.

Oh! And don’t forget to share it with me on Twitter. If you got any questions or suggestions, let me know.

Hope you learned something new. Till next time!

References and Credits

The post Kinetic Typography with Three.js appeared first on Codrops.

Create a Wave Motion Effect on an Image with Three.js

Waves! Because who does not enjoy the visual comfort an oscillating motion has on the human eye? Well, I do and for this tutorial, I would like to explain how to make waves on a 3D plane with Three.js using simplex noise.

To keep things short, we’ll just focus on the plane effect and not on the smooth scrolling or setup required to synchronize the DOM with WebGL. For these, check out Jesper Landberg’s codepen on smooth scrolling, and Luigi De Rosa’s article on how EPIC mixed WebGL and the DOM for WeCargo.

We’ll asume you have some basic understanding of Three.js, vertex and fragment shaders, so we’ll skip things like how to set up a scene.

Now let’s begin.

Creating the mesh

First, we’ll create a mesh using a PlaneGeometry and a ShaderMaterial.

Since we want to add a texture later, we’ll give out PlaneGeometry the same proportions as our 400×600 image. So, 0.4 for the width and 0.6 for the height. Having the same proportions makes it so the texture doesn’t stretch.

this.geometry = new THREE.PlaneGeometry(0.4, 0.6, 16, 16);
this.material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: {
    uTime: { value: 0.0 }
  },
  wireframe: true,
});
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.mesh);

As a performance side note, if you are rendering more than one element to a scene, make sure to reuse the geometry object.

Making some noise

We’re going to displace the plane using 3D simplex noise.

Our simplex noise function snoise, outputs values between -1 and 1 based on the position values you give it.

Simplex noise is amazing because it’s random and seamless! So it’s really useful to create organic patterns. Which is exactly what we want for our wave effect.

Now inside the vertex shader. Using the vertex positions of the plane, we’re going to sample simplex noise to get a wave distortion, adjusting its frequency and amplitude to control it. The frequency will only change the x vector component to make horizontal waves on the plane and adding a time uniform to make them move. For the distortion to take place forwards and backwards, we need to add the value to the z vector component.

varying vec2 vUv;
uniform float uTime;

void main() {
  vUv = uv;

  vec3 pos = position;
  float noiseFreq = 3.5;
  float noiseAmp = 0.15; 
  vec3 noisePos = vec3(pos.x * noiseFreq + uTime, pos.y, pos.z);
  pos.z += snoise(noisePos) * noiseAmp;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.);
}

And that’s really it for the motion! Even though it’s moving, the effect doesn’t quite sell because the inside looks flat.

To give that extra flare, as it is displaced, we need to modify colors accordingly. But before we do that, let’s add some pretty image!

Adding the texture

First, let’s create a new uniform that holds the texture for the ShaderMaterial.

this.material = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: {
    uTime: { value: 0.0 },
    uTexture: { value: new THREE.TextureLoader().load(img) },
  },
});

Second, let’s sample the texture in the fragment shader.

varying vec2 vUv;
uniform sampler2D uTexture;

void main() {
  vec3 texture = texture2D(uTexture, vUv).rgb;
  gl_FragColor = vec4(texture, 1.);
}

It looks solid already. Cool! Let’s go a step further and make it even more interesting by displacing the texture coordinates.

Displacing the texture coordinates

We can use the noise value from the vertex shader, pass it to the fragment shader, and add it to the texture coordinates so the image follows the motion of the displacement.

To share the distortion data from the vertex shader to the fragment shader we’ll use a varying, and for now let’s disable the vertex movement so we can appreciate more the texture displacement.

varying vec2 vUv;
varying float vWave;
uniform float uTime;

void main() {
  vUv = uv;

  vec3 pos = position;
  float noiseFreq = 3.5;
  float noiseAmp = 0.15; 
  vec3 noisePos = vec3(pos.x * noiseFreq + uTime, pos.y, pos.z);
  pos.z += snoise(noisePos) * noiseAmp;
  vWave = pos.z; // Off it goes!

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.);
}

A new variable inside the fragment shader will be needed to scale down the distortion so it’s not too sharp.

varying vec2 vUv;
varying float vWave;
uniform sampler2D uTexture;

void main() {
  float wave = vWave * 0.2;
  vec3 texture = texture2D(uTexture, vUv + wave).rgb;
  gl_FragColor = vec4(texture, 1.);
}

See how the texture moves in the same way but in two dimensions? What if instead of moving all the texture coordinates, we only did it for one color channel to give a more futuristic vibe?

Splitting the color channel

To move one color channel, we’ll need to separate each color vector of the texture, add the wave value to one of their coordinates, and combine them back together. I’ll try it on the blue channel, but you can experiment with any of them.

varying vec2 vUv;
varying float vWave;
uniform sampler2D uTexture;

void main() {
  float wave = vWave * 0.2;
  // Split each texture color vector
  float r = texture2D(uTexture, vUv).r;
  float g = texture2D(uTexture, vUv).g;
  float b = texture2D(uTexture, vUv + wave).b;
  // Put them back together
  vec3 texture = vec3(r, g, b);
  gl_FragColor = vec4(texture, 1.);
}

Now the movement is happening just on blue channel of the texture. Spooky!

Finally, we can enable again the vertex distortion to get the final result. Aaaand here it is:

I hope you made it until here! This is just one of countless possibilities you can do with shaders and Three.js. For example, in the second demo, I took a different approach making transitions using displacement maps — if you’re interested in it, perhaps we can leave that for another time.

Make sure to tweak the values to get different results, mess with the noise, or use another kind of noise (psst psst Worley), create something and have fun with it! Oh, and don’t forget to share it with me on Twitter. If you got any questions or suggestions, please let me know, too.

Hope you learned something new. Until next time!

References and Credits

Create a Wave Motion Effect on an Image with Three.js was written by Mario Carrillo and published on Codrops.

Create Text in Three.js with Three-bmfont-text

There are many ways of displaying text inside a Three.js application: drawing text to a canvas and use it as a texture, importing a 3D model of a text, creating text geometry, and using bitmap fonts — or BMFonts. This last one has a bunch of helpful properties on how to render text into a scene.

Text in WebGL opens many possibilities to create amazing things on the web. A great example is Sorry, Not Sorry by awesome folks at Resn or this refraction experiment by Jesper Vos. Let’s use Three.js with three-bmfont-text to create text in 3D and give it a nice look using shaders.

Three-bmfont-text is a tool created by Matt DesLauriers and Jam3 that renders BMFont files in Three.js, allowing to batch glyphs into a single geometry. It also supports things like word-wrapping, kerning, and msdf — please watch Zach Tellman’s talk on distance fields, he explains it very good.

With all that said, let’s begin.

Attention: This tutorial assumes you have some understanding of Three.js, GLSL shaders and glslify, so we’ll skip things like how to set up a scene and import shaders.

Getting started

Before everything, we need to load a font file to create a geometry three-bmfont-text provides packed with bitmap glyphs. Then, we load a texture atlas of the font which is a collection of all characters inside a single image. After loading is done, we’ll pass the geometry and material to a function that will initialize a Three.js setup. To generate these files check out this repository.

const createGeometry = require('three-bmfont-text');
const loadFont = require('load-bmfont');

loadFont('fonts/Lato.fnt', (err, font) => {
  // Create a geometry of packed bitmap glyphs
  const geometry = createGeometry({
    font,
    text: 'OCEAN'
  });
  
  // Load texture containing font glyphs
  const loader = new THREE.TextureLoader();
  loader.load('fonts/Lato.png', (texture) => {
    // Start and animate renderer
    init(geometry, texture);
    animate();
  });
});

Creating the text mesh

It’s time to create the mesh with the msdf shader three-bmfont-text comes with. This module has a default vertex and fragment shader that forms sharp text. We’ll change them later to produce a wavy effect.

const MSDFShader = require('three-bmfont-text/shaders/msdf');

function init(geometry, texture) {
  // Create material with msdf shader from three-bmfont-text
  const material = new THREE.RawShaderMaterial(MSDFShader({
    map: texture,
    color: 0x000000, // We'll remove it later when defining the fragment shader
    side: THREE.DoubleSide,
    transparent: true,
    negate: false,
  }));

  // Create mesh of text       
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(-80, 0, 0); // Move according to text size
  mesh.rotation.set(Math.PI, 0, 0); // Spin to face correctly
  scene.add(mesh);
}

And now the text should appear on screen. Cool, right? You can zoom and rotate with the mouse to see how crisp the text is.

Let’s make it more interesting in the next step.

GLSL

Vertex shader

To oscillate the text, trigonometry is our best friend. We want to make a sinusoidal movement along the Y and Z axis — up and down, inside and outside the screen. A vertex shader fits the bill for this since it handles the position of the vertices of the mesh. But before this, let’s add the shaders to the material and create a time uniform that will fuel them.

function init(geometry, texture) {
  // Create material with msdf shader from three-bmfont-text
  const material = new THREE.RawShaderMaterial(MSDFShader({
    vertexShader,
    fragmentShader,
    map: texture,
    side: THREE.DoubleSide,
    transparent: true,
    negate: false,
  }));

  // Create time uniform from default uniforms object
  material.uniforms.time = { type: 'f', value: 0.0 };
}

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

function render() {
  // Update time uniform each frame
  mesh.material.uniforms.time.value = this.clock.getElapsedTime();
  mesh.material.uniformsNeedUpdate = true;

  renderer.render(scene, camera);
}

Then we’ll pass it to the vertex shader:

// Variable qualifiers that come with the msdf shader
attribute vec2 uv;
attribute vec4 position;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
varying vec2 vUv;
// We passed this one
uniform float time;

void main() {
  vUv = uv;

  vec3 p = vec3(position.x, position.y, position.z);

  float frequency1 = 0.035;
  float amplitude1 = 20.0;
  float frequency2 = 0.025;
  float amplitude2 = 70.0;

  // Oscillate vertices up/down
  p.y += (sin(p.x * frequency1 + time) * 0.5 + 0.5) * amplitude1;

  // Oscillate vertices inside/outside
  p.z += (sin(p.x * frequency2 + time) * 0.5 + 0.5) * amplitude2;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

Frequency and amplitude are properties of a wave that determine their quantity and their “height”. Because we are using a sine wave to move the vertices, these properties can help control the behavior of the wave. I encourage you to tweak the values to observe different results.

Okay, so here is the tidal movement:

Fragment shader

For the fragment shader, I thought about just interpolating between two shades of blue – a light and a dark one. Simple as that.

The built-in GLSL function mix helps interpolating between two values. We can use it along with a cosine function mapped from 1 to 0, so it can go back and forth these values and change the color of the text — a value of 1 will give a dark blue and 0 a light blue, interpolating the colors between.

#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives : enable
#endif

// Variable qualifiers that come with the shader
precision highp float;
uniform float opacity;
uniform vec3 color;
uniform sampler2D map;
varying vec2 vUv;
// We passed this one
uniform float time;

// HSL to RGB color conversion module
#pragma glslify: hsl2rgb = require(glsl-hsl2rgb)

float median(float r, float g, float b) {
  return max(min(r, g), min(max(r, g), b));
}

void main() {
  // This is the code that comes to produce msdf
  vec3 sample = texture2D(map, vUv).rgb;
  float sigDist = median(sample.r, sample.g, sample.b) - 0.5;
  float alpha = clamp(sigDist/fwidth(sigDist) + 0.5, 0.0, 1.0);

  // Colors
  vec3 lightBlue = hsl2rgb(202.0 / 360.0, 1.0, 0.5);
  vec3 navyBlue = hsl2rgb(238.0 / 360.0, 0.47, 0.31);

  // Goes from 1.0 to 0.0 and vice versa
  float t = cos(time) * 0.5 + 0.5;

  // Interpolate from light to navy blue
  vec3 newColor = mix(lightBlue, navyBlue, t);

  gl_FragColor = vec4(newColor, alpha * opacity);
  if (gl_FragColor.a < 0.0001) discard;
}

And here it is! The final result:

Other examples

There is plenty of stuff one can do with three-bmfont-text. You can make words fall:

Enter and leave:

Distortion:

Water blend:

Or mess with noise:

I encourage you to explore more to create something that gets you excited, and please share it with me via twitter or email. You can reach me there, too if you got any questions, or comment below.

Hope you learned something new. Cheers!

References and Credits

Create Text in Three.js with Three-bmfont-text was written by Mario Carrillo and published on Codrops.