Frontend Development 12 min read

Converting TTF Fonts to Three.js JSON and Rendering 3D Text with Holes, Positioning, and Dynamic Input

This tutorial explains how to convert TTF/WOFF font files into Three.js‑compatible JSON using a ttf_to_json tool, load the resulting font with FontLoader, generate shapes (including holes), center the geometry, adjust the camera, and add dynamic text input for interactive 3D rendering.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Converting TTF Fonts to Three.js JSON and Rendering 3D Text with Holes, Positioning, and Dynamic Input

Introduction

The article demonstrates how to transform TTF/WOFF font files into JSON files usable by Three.js, load them with FontLoader , and create various 3D text effects such as solid letters, neon outlines, and more.

Font to JSON

Downloaded fonts are usually in ttf or woff format, which Three.js cannot consume directly. The ttf_to_json utility converts these files into the JSON format required by FontLoader . After uploading a font, click the convert button; for large fonts the file can exceed 18 MB, so you may restrict the character set with the “Restrict character set” option to reduce size.

Loading the Font

Once the JSON is generated, it can be loaded into a Three.js scene:

import { Font, FontLoader } from 'three/addons/loaders/FontLoader.js';
const loader = new FontLoader();
const loadFont = (url) => {
  return new Promise((res) => {
    loader.load(`${import.meta.env.VITE_ASSETS_URL}assets/font/${url}.json`, (response) => {
      res(response);
    });
  });
};

The load method accepts four callbacks: url , onLoad , onProgress , and onError .

Font File Structure

The loaded Font instance provides a generateShapes method that returns an array of Shape objects representing the glyph outlines.

generateShapes(text: string, size: number): Shape[];

Generating Shapes

Example:

const shapes = font.generateShapes('t', 4);
console.log('shapes', shapes);

Each Shape contains its outer contour and, if present, a holes array for internal cut‑outs.

Processing Holes

To render characters with internal holes (e.g., the Chinese character “猫”), iterate over shape.holes and merge them into the main shapes array:

for (let i = 0; i < shapes.length; i++) {
  const shape = shapes[i];
  if (shape.holes && shape.holes.length > 0) {
    for (let j = 0; j < shape.holes.length; j++) {
      const hole = shape.holes[j];
      holeShapes.push(hole);
    }
  }
}
shapes.push.apply(shapes, holeShapes);

Creating Line Geometry

For each shape, extract its points and build a line geometry:

const color = 0x006699;
const matDark = new THREE.LineBasicMaterial({ color, side: THREE.DoubleSide });
for (let i = 0; i < shapes.length; i++) {
  const shape = shapes[i];
  const points = shape.getPoints();
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const lineMesh = new THREE.Line(geometry, matDark);
  lineText.add(lineMesh);
}

Centering the Text

Compute the bounding box of the combined geometry and translate it so the text is centered:

const geometry = new THREE.ShapeGeometry(shapes);
geometry.computeBoundingBox();
const xMid = -0.5 * ((geometry.boundingBox.max.x) - (geometry.boundingBox.min.x));
const yMid = -0.5 * ((geometry.boundingBox.max.y) - (geometry.boundingBox.min.y));
// Inside the point‑loop
geometry.translate(xMid, yMid, 0);

Adjusting the Camera

To keep all characters visible after scaling, dynamically update the camera’s field of view based on the text width:

camera.aspect = window.innerWidth / window.innerHeight;
camera.fov = (360 / Math.PI) * Math.atan(tanFOV * ((xMid * 2) / lastHeight));
camera.updateProjectionMatrix();

Here lastHeight is a reference height measured when the font size is 4 and the field of view is 45°.

Dynamic Text Input

An HTML form captures user input and triggers the createText function to render new text:

<div class="input-btn">
  <form action="#">
    <input type="text" id="text" /><br />
    <input type="submit" value="提交" />
  </form>
</div>

var form = document.querySelector('form');
if (form) {
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var text = (document.getElementById('text')).value;
    createText(text);
  });
}

Before adding new text, remove the previous group with lineText.removeFromParent() to avoid overlapping.

Fly Line Effect

The fly line implementation resides in src/utils/fly.ts . Instantiate with let Fly = new TFly() , then call Fly.update() each render frame and use Fly.setFly() to create animated lines:

const createFly = (points) => {
  const flyGroup = Fly.setFly({
    index: 0,
    num: Math.floor(points.length * 0.2),
    points,
    spaced: 1000,
    starColor: new THREE.Color(color),
    endColor: new THREE.Color(color),
    size: 0.2
  });
  flyLineGroup.add(flyGroup);
};

Glow Objects

For advanced visual effects such as glowing bodies, refer to the related article “Three.js Rendering Advanced Visual Turbine Model” for detailed shader setups.

JSONThree.jsWebGL3D textfont conversionTTF
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.