Rotating Twisted 3D Typography with Three.js and Shaders

A brief exploration into how to twist and rotate text in 3D using Three.js and Shader magic.

Everybody likes circles. I personally like anything rounded. And it is impossible to love circles without loving SINes and COSines! Trust me. Because it is the same thing all around (pun intended!)

Sine and cosine are intimately connected to the concept of a circle, as they describe its oscillations and rotations. However, let’s shift our focus to the fascinating topic of manipulating space. Trigonometry can seem intimidating, but with small, manageable steps, we can gain mastery over the 3D universe.

Creating some text

Three.js has a built-in module to create text geometries. They even called this module TextGeometry, so you won’t confuse it with TextAlgebra and TextCalculus 👀.

You can’t really use a .ttf or .woff file directly, instead, you will have to convert it to a so called typeface.json format first.

new FontLoader().load('font.json', (font) => {
    let textGeo = new TextGeometry(this.settings.text, {
        font: font,
        size: 1, // fontsize
        height: 1, // extrusion
        curveSegments: 10, // how smooth the text is
        bevelEnabled: false,

And then just like this you can generate geometrical text! 

 Usually it will not be centered. So you could just add:

And now the text will be in the center! That’s important because it simplifies any further transformations.

Now, let’s twist!

There are a bunch of ways to rotate something in Three.js. You could, for example, distort the geometry object itself. But the most performant way is of course to use vertex shaders!

And what’s a twist anyway? It’s just a rotation of vertices around one axis, but with different rotation angles across that axis. So we can rotate the whole mesh around that axis with the same angle:

But we can also rotate depending on UV or any other parameter:

There are already a lot of snippets out there to rotate things. Interestingly, we don’t even need a 3D rotation for twisting things in 3D, as long as we rotate around one of the X, Y or Z axes.

Usually, at the core of any rotation is this two by two matrix:

mat2 rotate2d(in float radians){
    float c = cos(radians);
    float s = sin(radians);
    return mat2(c, -s, s, c);

See, just a bunch of sines and cosines! This one is in GLSL, which is for shaders. But it is actually universal, because it’s a mathematical formula. So if you multiply this matrix with a vector(point), you will get a rotation of that vector.

Now that we have a centered geometry with our text, we can use a shader to twist it around the X axis:

pos.yz = rotate2d(ANGLE)*pos.yz;

To get the amount of twist we need, we can calculate the bounds of the mesh:

material.uniforms.uMin.value = geo.boundingBox.min;
material.uniforms.uMax.value = geo.boundingBox.min;

Then in shader we do the following:

float theta = mapRange(position.x, uMin.x, uMax.x, -PI, PI); // basically number of pivots we want
pos.yz = rotate2d(ANGLE)*pos.yz; // twist

So with just a little bit of trigonometry, we got ourselves a twisted text already. Beautiful!

Warping space to form a circular shape

Now let’s go further. We have this `theta` parameter, between -PI and PI. That means we can map every horizontal point of our text to a circle, by using sin+cos! 

So for every point on the X axis, we gonna have the Theta angle [0,2PI], and we can calculate the corresponding virtual circle point:

vec3 circlePoint = vec3(sin(theta), cos(theta),0.);  // because circles!
// z is 0, because i want my circle to be in X-Y plane, in front of my camera which is at (0,0,2)

But this, of course, will make us 0 width on a 3D object, it’s just a circle. Because THETA only depends on X. We need to account for the y,z coordinates of the original mesh. We will do that in two steps, first we move to our “circle point” with some radius, and then we add an y-z offset of the original positions:

vec3 newPosition = circlePoint*RADIUS + circlePoint*pos.y + vec3(0.,0.,pos.z);

To break it down once again: first we map our x axis with `circlePoint*RADIUS`, then we add our y axis offset with `circlePoint*pos.y`. Basically, I want to move my Y in the same direction as the point on a circle, but with the original Y offset of the vertex.

And then we just add the original z, which is not really changing at all during this transformation. It’s important, that it’s all based on geometry along the X axis, and we want our circle to be in X-Y plane.

That might have been a looot of X-Y-Zs in one paragraph, but the best way to get a grasp of it, is to try it yourself, and to play with parameters in this formula. Just randomly multiply some of them with, let’s say 3 or 42, and see what’s gonna happen. The whole code with twisting and making it a circle will look now like this:

vec3 pos = position; // copy original
float theta = mapRange(position.x, uMin.x, uMax.x, -PI, PI);
// twist
pos.yz = rotate2D(theta)*pos.yz;
// bend into circle
vec3 circlePoint = vec3(sin(theta), cos(theta),0.);
pos = circlePoint*RADIUS + circlePoint*pos.y + vec3(0.,0.,pos.z);

To make everything look beautiful we would also need to replicate all the transformation steps for NORMALs of the object. That’s because, while we play with geometry vertices, normals are staying the same. And we need them for correct lighting. But that’s 99% the same code as for vertices, except it’s transforming normal.

And here you go, a nice example in React Three Fiber you can go and play with. Just locate the shader part, and start changing numbers to learn how it works:

The end

I hope you enjoyed this brief exploration of manipulating space. This is just the tip of the iceberg when it comes to the capabilities you can achieve. I encourage you to experiment with the files and discover the power of shaping space to your desires. With a mastery of trigonometry, even the impossible can become possible. Have a good day and stay safe!

Addition: Have a look at this video coding session about the impossible Mobius animation with Three.js:

Tagged with:

Yuri Artiukh

Yuriy is a developer from Kyiv, Ukraine. Leading a small frontend agency, also speaking at conferences, and open for freelance projects. Curious about CSS and shaders. Loves to learn every day.

Stay in the loop: Get your dose of frontend twice a week

👾 Hey! Looking for the latest in frontend? Twice a week, we'll deliver the freshest frontend news, website inspo, cool code demos, videos and UI animations right to your inbox.

Zero fluff, all quality, to make your Mondays and Thursdays more creative!