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.
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.
- Cargar una fuente con
FontLoadery construir unaTextGeometrycon 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.
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.
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.