13. 3D Text 3D text
introduce
We've covered enough of the basics to create some nice looking effects. For our first serious project, we're going to fork a developer ilithya's work ( https://www.ilithya.rocks/ ), which has a large 3D text in the middle of the scene, with lots of geometry floating around the text.
This work is a great example of what is possible in the early days of learning Three.js. It's simple, efficient, and the effects look great.
Three.js already supports 3D text geometry through the TextGeometry class. The problem is that you have to specify a font first, and that font has to be in a specific json format called typeface.
We will not involve issues related to copyright authorization of digital fonts. After you use the downloaded fonts, you must guarantee the right to use the fonts when using the fonts, or the copyright of the fonts is free for developers to use.
how to get the font
There are many ways to get fonts in typeface format. First, you can convert your fonts using a converter like this: https://gero3.github.io/facetype.js/ . You have to provide a file and click the convert button.
You can also node_modules
find the fonts in the Three.js library examples in the folder. /node_modules/three/examples/fonts/
You can put these fonts in /static/
a folder, or you can import them directly in your javascript files, since they are json and .json
files in Vite .js
are supported like :
import typefaceFont from 'three/examples/fonts/helvetiker_regular.typeface.json'
We mix the two techniques by opening /node_modules/three/examples/fonts/
, fetching helvetiker_regular.typeface.json
files and LICENSE
files and putting them into /static/fonts/
folders (you need to create fonts
folders).
Now just write at the end of the base URL to access the /fonts/helvetiker_regular.typeface.json
font.
load font
To load fonts we have to use a new loader class called FontLoader .
This class THREE
is not available in variables. OrbitControls
Import it as we did in the previous lessons :
import {
FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
This loader works like TextureLoader. Add the following code after that section textureLoader
(don't forget to change the path if you're using a different font):
/**
* Fonts
*/
const fontLoader = new FontLoader()
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) =>
{
console.log('loaded')
}
)
Enter your console and find that it is printed 'loaded'
. If not, check the previous steps and search the console for potential errors. We can now access the font
by using the variable inside the function . Unlike TextureLoader , we have to write the rest of the code in the success callback of this function.font
create geometry
As we said before, we will use TextGeometry to create geometry.
Just like FontLoader , we need to import it:
import {
TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'
Note the example code on the documentation page; the values are much larger than in our scenario.
Make sure to write the code inside the success callback function:
fontLoader.load(
'/fonts/helvetiker_regular.typeface.json',
(font) =>
{
const textGeometry = new TextGeometry(
'Hello Three.js',
{
font: font,
size: 0.5,
height: 0.2,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5
}
)
const textMaterial = new THREE.MeshBasicMaterial()
const text = new THREE.Mesh(textGeometry, textMaterial)
scene.add(text)
}
)
You should get a white 3D text that needs improvement.
First, comment out the code for the cube. Its purpose is to make sure everything works.
If you want to see some cool meshes to add wireframe: true
to your materials.
const textMaterial = new THREE.MeshBasicMaterial({
wireframe: true })
You can now see that the geometry was generated with many triangles. Creating text geometry is long and difficult for computers. curveSegments
Avoid doing this too many times and bevelSegments
keep the geometry as low as possible by reducing polys and attributes.
Remove once you are satisfied with the geometry rendering detail level wireframe
.
center text
There are several ways to center text. One way is to use boundaries. Bounds are information associated with geometry that tells what space that geometry occupies. It can be a box or a sphere.
You can't actually see these borders, but it helps Three.js easily calculate whether an object is on screen or not, and if it's not, the object won't even be rendered. This is called frustum culling, but it's not the topic of this lesson.
What we want is to use this bounds to know the size of the geometry and re-center it. By default, Three.js uses sphere bounds. What we want is a box boundary, to be more precise. To do this, we can ask Three.js computeBoundingBox()
to calculate this box bounds by calling geometry:
textGeometry.computeBoundingBox()
We can boundingBox
check this box using geometry properties.
console.log(textGeometry.boundingBox)
The result is an object named Box3 that has one min
property and one max
property. The min
0 is not what we expected. This is due bevelThicknessand bevelSize
, but we can ignore it for now.
Now that we have measures, we can move objects. We don't move the mesh, but the whole geometry. This way, the mesh will still be centered in the scene, and the text geometry will also be centered within our mesh.
To do this, we can translate(...)
use the method on the geometry immediately after the method computeBoundingBox()
:
textGeometry.translate(
- textGeometry.boundingBox.max.x * 0.5,
- textGeometry.boundingBox.max.y * 0.5,
- textGeometry.boundingBox.max.z * 0.5
)
The text is centered, but if you want to be very precise, you should also subtract bevelSizeis 0.02
:
textGeometry.translate(
- (textGeometry.boundingBox.max.x - 0.02) * 0.5, // Subtract bevel size
- (textGeometry.boundingBox.max.y - 0.02) * 0.5, // Subtract bevel size
- (textGeometry.boundingBox.max.z - 0.03) * 0.5 // Subtract bevel thickness
)
What we're doing here can actually center()
be done faster by calling a method on the geometry:
textGeometry.center()
Much easier, no? The purpose of our handwritten centering is to understand boundary and frustum culling .
Add matcap material
Time to add a cool material to our text. We'll MeshMatcapMaterial
replace MeshBasicMaterial with because it looks cooler and performs better.
First, let's choose a matcap
Texture. We'll use the ones located /static/textures/matcaps/
in the folder matcaps
, but feel free to use your own matcaps
.
You can also download one from this repository https://github.com/nidorx/matcaps . Don't spend too much time choosing it! If not for personal use, make sure you have the copyright to use it. You don't need high resolution textures, 256x256
it should be more than enough.
We can now load textures using the TextureLoader we already have in our code:
const matcapTexture = textureLoader.load('/textures/matcaps/1.png')
We can now replace the ugly MeshBasicMaterial with the beautiful MeshMatcapMaterial and use our matcapTexture
variables with properties:matcap
const textMaterial = new THREE.MeshMatcapMaterial({
matcap: matcapTexture })
You should be able to render a lovely text with a cool material on it.
add object
Let's add floating objects. To do this, we will create a donut inside the loop function.
In the success function, right after that text
section, add the loop function:
for(let i = 0; i < 100; i++)
{
}
We could do this outside of the success function, but we need to create the text and the object together for good reasons, as you'll see later.
In this loop, create a TorusGeometry (like the technical name for a donut) with the same material as the text and the Mesh :
for(let i = 0; i < 100; i++)
{
const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({
matcap: matcapTexture })
const donut = new THREE.Mesh(donutGeometry, donutMaterial)
scene.add(donut)
}
You should get 100 donuts in one place.
Let's add some randomness to their positions:
donut.position.x = (Math.random() - 0.5) * 10
donut.position.y = (Math.random() - 0.5) * 10
donut.position.z = (Math.random() - 0.5) * 10
You should scatter 100 donuts on the field.
Add randomness to rotation. There is no need to rotate all 3 axes, and since the donut is symmetrical, a half turn is enough:
donut.rotation.x = Math.random() * Math.PI
donut.rotation.y = Math.random() * Math.PI
The donut should rotate in all directions.
Finally, we can add randomness to the scale. x, y, z
Be careful though; we need to use the same value for all 3 axes ( ):
const scale = Math.random()
donut.scale.set(scale, scale, scale)
optimization
Our code is not optimal. As we saw in the previous lesson, we can use the same material on multiple meshes , but we can also use the same geometry to save performance.
Move thedonutGeometry
the thedonutMaterial
sum out of the loop:
const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({
matcap: matcapTexture })
for(let i = 0; i < 100; i++)
{
// ...
}
You should get the same result, but we can go further. text
The material of donut
is the same as that of .
Let's delete donutMaterial
, rename textMaterialbymaterial
and use it for thetext
and donut
:
const material = new THREE.MeshMatcapMaterial({
matcap: matcapTexture })
// ...
const text = new THREE.Mesh(textGeometry, material)
// ...
for(let i = 0; i < 100; i++)
{
const donut = new THREE.Mesh(donutGeometry, material)
// ...
}
We could go ahead and optimize, but there's a whole class on optimization, so hold that for now.
more optimization
You can add more shapes, animate them, or even experiment with others if you want matcaps
.