From our sponsor: Meco is a distraction-free space for reading and discovering newsletters, separate from the inbox.
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:
textGeo.center()
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:
geo.computeBoundingBox();
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: