Create stunning 3D text with custom fonts in Three.js

8 min read

Create stunning __3D text with custom fonts__ in Three.js

When doing anything 3D on the web, the first library that comes up in a developer’s mind is Three.js. Three.js provides high-level abstractions to draw 3D graphics using WebGL in the browser. I have been playing around with it quite a bit recently for 3D text rendering and realized that using custom font faces is a pain, especially when you want a simple way to render custom fonts from CDNs like Google Fonts, Fontsource. There’s very little documentation on this topic online and I hope this article helps in filling that gap.

Elements of 3D text rendering

Let’s take a look at the different pieces involved at high level for rendering Text in Three.js.

TextGeometry

Three.js exposes a TextGeometry class which is specialized for creating the underlying geometry for a text. Think of “geometry” as a set of vertices, edges, faces – basically the structure of the object. TextGeometry has various options to customize font face, size, thickness, bevel, etc. The example usage of this class in the documentation shows the following code snippet.

js
1
const loader = new FontLoader();
2
3
loader.load('fonts/helvetiker_regular.typeface.json', function (font) {
4
const geometry = new TextGeometry('Hello three.js!', {
5
font: font,
6
size: 80,
7
height: 5,
8
// other options...
9
});
10
});

FontLoader

Notice the FontLoader class in the above snippet? It is responsible for creating a FontLoader instance that deals with loading custom fonts that can be used by TextGeometry to create the text in that font face.

The load() method of the FontLoader instance takes a file path / URL to the font file and a callback function which gets called with the loaded font data. This font data can then be passed to TextGeometry’s constructor for creating a text geometry in that font.

Mesh and Material

The text geometry cannot be rendered as is, it has to first be converted to a Mesh object. A Mesh holds the geometry definition and the materials applied to it which is used by the renderer to render that object. In the following example, I assign a MeshStandardMaterial to the Text object. You can read more about the options that can be set in the material at the documentation linked with it.

Seems straightforward until you notice something strange in that font file path – 'fonts/helvetiker_regular.typeface.json'. See that .json extension? Seems weird right? Most of us are familiar with standard font file formats like TTF, WOFF, WOFF2, etc. but the FontLoader only understands font files in .json format! My next question was – how do I convert my existing font files in one of the standard font file formats mentioned above to the JSON format supported by FontLoader?

Facetype.js

The documentation of FontLoader class talks about a tool called facetype.js which can be used for converting font files to the JSON format. The tool asks you to choose a font file, and it converts it to a JSON or a JS file. The JSON file can then be included in the source code and loaded in Three.js.

Screenshot of Facetype.js website – https://gero3.github.io/facetype.js/

The GitHub repo of the project indicates that it is not published as a library that can be used in other projects. This kinda leaves us with two options if we want to load fonts dynamically from something like Google Fonts:

  1. Download all the font source files(in TTF format) from Google Fonts, convert them to static JSON format using the facetype.js tool, then include them in the source code of the project and load them in Three.js. This is not scalable as the Google Fonts library is ever-expanding and each font has various font weights(normal, semi-bold, bold, etc.) and font styles(normal, italicized). Converting each of them to JSON format and serving them from our servers is a waste of resources – time, bandwidth, and even money 💸!
  2. Use the JavaScript files from the source code of facetype.js. The JS files can be included in the source code of the project and Google Fonts CDN and APIs can be used to fetch the required TTF font files. Once the file is fetched, the relevant functions can be called(from the JS files that were included from facetype.js) to convert the TTF data to JSON format on-the-fly. Finally, this JSON data can then be loaded in Three.js. This approach is far better than the previous method but the downside to it is that it includes external JS scripts without a package manager. We won’t get any future updates if the scripts change(unless the changes are manually included in our project), and it may be tricky to use it with ES6-imports and TypeScript.

Three.js example files to the rescue!

After spending some time trying to figure all this out, I had almost given up and accepted the second approach mentioned above. I wanted some kind of abstraction that would deal with loading font files in standard formats directly in Three.js without me having to worry about the intermediate steps of converting it to the JSON format.

One of the underrated features of Three.js is the number of official examples the library has. You can experience all these examples at https://threejs.org/examples/. After a bit of digging into the Three.js examples, this particular example caught my eye. The example is titled “TTFLoader using opentype”, which got me excited as this is exactly what I’ve been trying out to figure out!

The source code of this example has a line that I’d like to highlight

js
1
import * as THREE from 'three';
2
import { TTFLoader } from 'three/addons/loaders/TTFLoader.js';
3
import { Font } from 'three/addons/loaders/FontLoader.js';
4
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';

Three.js already has a TTFLoader class that can be used to load TTF font files and use them as font faces for TextGeometry! I did not know that!! It’s not mentioned anywhere in the documentation for some reason but it seems to be officially supported.

Final code

With TTFLoader in our toolbox, the final implementation in a vanilla Three.js project looks far simpler than I had imagined it to be at the start.

In React

To use Three.js in React, I recommend installing @react-three/fiber and @react-three/drei packages along with three package. They make working with Three.js in React a breeze. The drei package exports a Text3D class which is a wrapper around TextGeometry to render 3D texts. It has to be wrapped with React’s <Suspense /> component as the Text3D component suspends when loading the font. By default, the Text3D component does not support TTF font files. It exposes a font prop that expects the JSON file path / URL as we discussed earlier.

To add support for TTF files, let’s add a wrapper component around the base Text3D component from drei. This new component takes a url prop which is the URL to the TTF font file we’d like to use. While the font is being fetched and loaded using TTFLoader, let’s suspend the component. I’ll be using suspend-react package for this. It would automatically suspend async functions if any of the dependencies change and return with the resolved value of the promise.

Note: If importing three/addons/loaders/TTFLoader.js does not work, try importing three/examples/jsm/loaders/TTFLoader.js

Bonus: Implementing a custom font picker drop-down 🙌

While I’m at it, let me also show you how to implement one of the most common UI elements that are synonymous with text editing – A font family picker drop-down.

I’ll be using the Fontsource API for this because of its ease of use, but you can also use the Google Fonts API for the same. These are the steps:

  • Call the https://api.fontsource.org/v1/fonts?subsets=latin&weights=400 endpoint to fetch all the font faces. This endpoint queries only those fonts which have 400 (normal) font-weight and latin subset.
  • Render a dropdown with all the fonts returned from the above request as options.
  • When the user selects an option, call the https://api.fontsource.org/v1/fonts/:fontId endpoint with the relevant fontId. The result contains a link to the TTF font file that can be loaded with TTFLoader and rendered in the browser.

Some font families don’t render correctly. What’s happening?

You may come across certain font families that don’t render correctly. Either there are extra faces, or the direction of the geometry is reversed. This is perhaps a limitation of how 2D fonts work in a 3D environment. Facetype.js tool has a checkbox to ‘Reverse font direction’ when this happens, but apparently it cannot be automatically detected and applied dynamically. In these cases, it’s better to use alternate font families that don’t suffer from these issues or write text using characters that are immune to this problem. If you know the solution, do let me know.

Conclusion

In this article, I went over how to do 3D text rendering with custom font faces in Three.js. It started as loading fonts in JSON format, to using fonts from popular CDNs like Fontsource and Google Fonts.

Without a doubt, Three.js is an amazing library for doing anything 3D on the browser. While some very specific and niche use cases might not be documented, it has a gold mine full of examples that can act as a starting point for what you are building. Combined with @react-three/fiber and @react-three/drei that 10x the ergonomics of using Three.js with React, 3D has finally become easy for web developers!

If you found this article helpful, you will love these too.

Building an expressive API for custom confirm dialogs in React
Color Pop effect using BodyPix and TensorFlow.js

Building highlighted input field in React

12 min read
Building highlighted input field in React