Saltar al contenido

TextGeometry: texto que es geometría de verdad

Cómo FontLoader y TextGeometry convierten una cadena en triángulos, cuántos triángulos exactamente, y en qué casos ese precio merece la pena.

De una cadena a un montón de triángulos

El camino tiene cuatro pasos y conviene tenerlos separados en la cabeza porque cada uno tiene su coste.

Primero, FontLoader descarga un JSON en formato typeface, generado con facetype.js a partir de un TTF o un OTF. Ese JSON contiene, por cada glifo, la lista de comandos de contorno: rectas, cuadráticas y cúbicas de Bézier. No es una fuente: es una versión de la fuente reducida a contornos, sin tabla de kerning completa, sin sustituciones y sin instrucciones de hinting.

Segundo, font.generateShapes( texto, tamaño ) recorre la cadena y produce un array de objetos Shape, uno por glifo, con sus agujeros (la contraforma de una “o”, el hueco de una “a”) como holes.

Tercero, ExtrudeGeometry —de la que TextGeometry hereda directamente— convierte cada Shape en puntos discretizando cada curva en curveSegments segmentos, triangula la cara frontal con un algoritmo de recorte de orejas, duplica esa triangulación para la cara trasera, y genera las paredes laterales uniendo contorno frontal con contorno trasero.

Cuarto, si hay bisel, todo el proceso de las paredes se repite bevelSegments veces con el contorno desplazado hacia dentro y hacia fuera.

JS
import * as THREE from 'three';import { FontLoader } from 'three/addons/loaders/FontLoader.js';import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';const font = await new FontLoader().loadAsync(  '/fonts/helvetiker_regular.typeface.json');const geometry = new TextGeometry( 'NIVEL DIOS', {  font,  size: 1,             // altura de la caja de mayusculas, en unidades de mundo  depth: 0.2,          // extrusion; se llamaba height antes de r163  curveSegments: 4,    // el parametro que mas manda en el coste  bevelEnabled: true,  bevelThickness: 0.02,  bevelSize: 0.015,  bevelOffset: 0,  bevelSegments: 2,} );geometry.center();   // TextGeometry nace con la esquina inferior izquierda en el origenconst mesh = new THREE.Mesh(  geometry,  new THREE.MeshStandardMaterial( { roughness: 0.35, metalness: 0.6 } ));scene.add( mesh );

El parámetro depth se llamaba height en versiones anteriores a r163. Si copias código antiguo y el texto sale plano, es eso.

Cuánto cuesta, con números

No hay que estimarlo: se mide.

JS
function medir( geometry, etiqueta ) {  const v = geometry.attributes.position.count;  const indexada = geometry.index !== null;  const triangulos = indexada ? geometry.index.count / 3 : v / 3;  const bytes = Object.values( geometry.attributes )    .reduce( ( t, a ) => t + a.array.byteLength, 0 );  console.log( etiqueta, {    vertices: v,    triangulos,    indexada,    kilobytes: ( bytes / 1024 ).toFixed( 1 ),  } );}medir( geometry, 'NIVEL DIOS' );

Lo primero que verás es que indexada es false. ExtrudeGeometry no genera índice, así que cada triángulo lleva sus tres vértices propios y no se comparte ni uno. Eso es tres veces más vértices de los que necesitaría una geometría indexada equivalente, y los atributos position, normal y uv se pagan tres veces.

Lo segundo es la sensibilidad a curveSegments. Ese parámetro controla en cuántos tramos se discretiza cada curva de cada contorno. Una letra como la “S” tiene unas veinte curvas; la “l” tiene cero. Subir de 4 a 12 multiplica por tres los puntos de contorno de las letras curvas, y como las paredes laterales se generan por contorno, el coste de la extrusión sube en la misma proporción.

Lo tercero es que el bisel es acumulativo. Con bevelSegments: 3 tienes las paredes de la extrusión más seis anillos adicionales de paredes de bisel, tres delante y tres detrás. Un texto con bisel de tres segmentos cuesta aproximadamente el triple que el mismo texto sin bisel.

Ajuste Efecto en el coste
curveSegments de 12 a 4 reduce los triángulos de las letras curvas a un tercio
bevelSegments de 3 a 1 reduce las paredes laterales a la mitad
bevelEnabled: false elimina todas las paredes de bisel
Cadena el doble de larga el doble de todo, exactamente

Un consejo que no aparece en ninguna documentación: curveSegments: 4 es indistinguible de 12 a cualquier tamaño en pantalla por debajo de unos trescientos píxeles de altura de letra. Por encima de eso empiezan a verse las facetas en las curvas exteriores de la “O” y de la “S”.

Lo que este formato no sabe hacer

El JSON typeface es una simplificación agresiva de una fuente, y las cosas que se dejó por el camino son exactamente las que hacen que la tipografía se vea profesional.

No hay shaping: los glifos se colocan uno detrás de otro según su anchura de avance. Eso basta para el alfabeto latino, pero el árabe necesita formas contextuales distintas según la posición de la letra en la palabra, y el devanagari necesita reordenación y conjuntos. Con TextGeometry ambos salen mal, no ligeramente mal.

No hay ligaduras ni sustituciones OpenType. La “fi” con la f y la i pegadas, los números en versalitas, las alternativas estilísticas: nada de eso viaja en el typeface.

El kerning existe pero es limitado: el formato guarda una tabla de pares y generateShapes la aplica si está presente, pero facetype.js no siempre la exporta completa. Las combinaciones difíciles como “AV” o “To” pueden quedar sueltas.

El parámetro direction acepta 'ltr', 'rtl' y 'tb', y lo que hace es cambiar el orden y el eje en que se colocan los glifos. Es una reordenación, no un algoritmo bidireccional: una cadena mixta con hebreo y latín no se resuelve.

Y el peso del asset no es trivial: un typeface con el juego latino completo ronda los 70 KB, y si necesitas acentos, símbolos y puntuación extendida se va a más. Para tres palabras en pantalla eso es mucho JSON.