wandres.dev
TEXTO EN 3D · Geometría, sprites y SDF

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.

⏱ 18 min

TextGeometry es la única de las cuatro técnicas de texto en 3D que produce un objeto físico: una malla con volumen, con bisel, que proyecta sombra, que se puede cortar con un plano de recorte y en la que un reflejo especular recorre el canto de una letra. También es, con diferencia, la más cara. Saber cuánto cuesta exactamente y de dónde sale el coste convierte una decisión de fe en una decisión de ingeniería.

🎯 Al terminar esta lección sabrás
  • Cargar una fuente con FontLoader y construir una TextGeometry con los parámetros correctos de r184.
  • Medir el número real de vértices y triángulos que produce una cadena.
  • Identificar los tres parámetros que multiplican el coste y ajustarlos con criterio.
  • Reconocer las limitaciones tipográficas del formato y decidir si son aceptables.

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.

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 origen

const 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.

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.

Cada cambio de texto es una geometria nueva, y la anterior sigue en la VRAM

El error que convierte una demo bonita en una fuga de memoria: TextGeometry no se puede modificar. Cambiar el texto significa construir una geometría nueva, y la vieja no se libera sola. Mesh.geometry = nuevaGeometria sustituye la referencia en JavaScript, pero el buffer de la GPU sigue vivo hasta que llames explícitamente a dispose(). Un contador que actualice el texto una vez por segundo filtra una geometría por segundo; a los cinco minutos son trescientas geometrías no indexadas en memoria de vídeo y el navegador empieza a arrastrarse sin ningún mensaje de error. El patrón correcto es siempre const anterior = mesh.geometry; mesh.geometry = nueva; anterior.dispose();, en ese orden, porque dispose() dispara un evento que el renderer usa para soltar el buffer y hacerlo antes de asignar la nueva deja un cuadro con la geometría destruida asignada. Y el corolario de diseño: si tu texto cambia más de una vez cada pocos segundos, TextGeometry es la herramienta equivocada, por mucho que el bisel quede bien. Reconstruir y subir a la GPU decenas de miles de vértices es un coste que ninguna de las otras tres técnicas paga.

⚔️ Reto práctico

Genera la misma cadena con curveSegments de 2, 4, 8 y 12, mide los triángulos con la función de arriba y anota los cuatro números. Después mira las cuatro mallas a tamaño de pantalla completa y decide en cuál empiezas a distinguir las facetas. Ese es tu presupuesto real, y casi seguro está por debajo del valor por defecto.