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.
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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.