TextGeometry y el presupuesto de triángulos
Cómo cargar una fuente y generar texto en 3D, la fórmula que explica por qué el texto es la geometría más cara del catálogo, y cómo repartir un presupuesto de triángulos en una escena real.
TextGeometry es un ExtrudeGeometry disfrazado, y hereda todos sus costes multiplicados por el número de letras. Es la única geometría del catálogo capaz de convertir una palabra de diez caracteres en veinte mil triángulos sin que aparezca ningún número sospechoso en el código. Entender la fórmula que lo produce es lo que permite bajar ese número en un factor de cinco sin que se note, y cerrar así el recorrido por el catálogo con lo único que de verdad importa: cuánto cuesta cada elección.
- Cargar una fuente y generar texto en 3D con las opciones correctas.
- Derivar el número de triángulos de un texto a partir de sus puntos de contorno.
- Repartir un presupuesto de triángulos entre los elementos de una escena.
- Reconocer cuándo
TextGeometryes la herramienta equivocada.
Cargar una fuente y generar el texto
TextGeometry vive en los addons y necesita una fuente en formato typeface.json, que es un volcado de los contornos de cada glifo. No sirve un archivo de fuente normal: hay que convertirlo antes.
import * as THREE from 'three';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
const fuente = await new FontLoader().loadAsync( '/fonts/helvetiker_regular.typeface.json' );
const geometria = new TextGeometry( 'Nivel dios', {
font: fuente,
size: 1,
depth: 0.2,
curveSegments: 6,
bevelEnabled: true,
bevelThickness: 0.02,
bevelSize: 0.015,
bevelSegments: 1
} );
geometria.center(); // el texto se genera desde el origen hacia la derecha
const texto = new THREE.Mesh( geometria, new THREE.MeshStandardMaterial( { color: 0xf9e2af } ) );
scene.add( texto );
Las opciones y sus valores por defecto reales, que son de otra escala de la que casi nadie espera:
| Opción | Por defecto | Nota |
|---|---|---|
font |
ninguno | Sin ella, la geometría sale vacía |
size |
100 |
Altura de la mayúscula, en unidades de mundo |
depth |
50 |
Profundidad de la extrusión |
curveSegments |
12 |
Heredado de ExtrudeGeometry |
bevelEnabled |
false |
Aquí sí está desactivado por defecto |
bevelThickness |
10 |
Solo si activas el chaflán |
bevelSize |
8 |
Solo si activas el chaflán |
bevelSegments |
3 |
Heredado |
bevelOffset |
0 |
Heredado |
direction |
'ltr' |
También 'rtl' y 'tb' |
size a cien y depth a cincuenta significan que un texto creado sin opciones mide cien unidades de alto y cincuenta de fondo. Si tu escena está en metros, eso es un cartel de cien metros. Es la causa número uno de “he añadido el texto y no veo nada”: está ahí, y la cámara está dentro de la letra N.
La otra diferencia respecto a ExtrudeGeometry es que aquí bevelEnabled sí es false por defecto, así que los valores de chaflán se ponen a cero salvo que lo actives explícitamente. Es la decisión correcta y la contraria a la de su clase padre, lo cual no ayuda a recordarlo.
Si la opción font no está definida, TextGeometry llama al constructor de ExtrudeGeometry sin argumentos y obtienes un cuadrado extruido. No lanza ningún error.
Por qué el texto es caro
La estructura del coste es derivable. Cada glifo es un conjunto de contornos cerrados de curvas de Bézier. curveSegments decide en cuántos segmentos rectos se convierte cada curva, y de ahí sale P, el número total de puntos de contorno del glifo. A partir de P, ExtrudeGeometry genera tres cosas:
- Las dos tapas. Un polígono simple de
Pvértices se triangula enP − 2triángulos, y hay dos tapas:2·(P − 2). - Las paredes laterales. Dos triángulos por cada segmento de contorno y por cada paso de extrusión:
2·P·steps. - El chaflán. Dos triángulos por segmento de contorno, por cada división del chaflán, en los dos extremos:
4·P·bevelSegments.
triangulos_por_glifo ≈ 2·(P − 2) + 2·P·steps + 4·P·bevelSegments
Ahora los números. Una letra latina típica con curveSegments a doce tiene del orden de cien puntos de contorno. Con steps a uno:
| Configuración | Triángulos por letra | Diez letras |
|---|---|---|
curveSegments 12, sin chaflán |
~400 | ~4 000 |
curveSegments 12, bevelSegments 1 |
~800 | ~8 000 |
curveSegments 12, bevelSegments 3 |
~1 600 | ~16 000 |
curveSegments 4, bevelSegments 1 |
~320 | ~3 200 |
curveSegments 4, sin chaflán |
~160 | ~1 600 |
Diez veces de diferencia entre las dos configuraciones extremas, y las dos producen texto que a tamaño moderado se ve prácticamente igual. El chaflán con tres divisiones es el sospechoso principal: duplica los triángulos él solo, y con una división el efecto visual es casi el mismo, porque lo que aporta un chaflán es una línea de luz en la arista y para eso basta con un anillo.
Y recuerda que TextGeometry hereda de ExtrudeGeometry la ausencia de índice: los vértices son tres por triángulo. Diez letras con la configuración media son cuarenta y ocho mil vértices, más de ochenta veces una esfera.
Medirlo es una línea y hay que hacerlo siempre:
console.log( geometria.attributes.position.count / 3, 'triángulos' );
El presupuesto de una escena
Con el catálogo recorrido, ya se puede hacer la operación que de verdad importa: repartir un presupuesto. La fuente de verdad es el renderer, después del culling y contando las pasadas de sombra:
renderer.info.autoReset = false; // si renderizas varias veces por frame
function medir() {
renderer.info.reset();
renderer.render( scene, camera );
console.log( renderer.info.render.calls, 'llamadas',
renderer.info.render.triangles, 'triángulos' );
}
Los órdenes de magnitud que conviene tener en la cabeza, con la advertencia de que son puntos de partida para medir y no garantías:
| Objetivo | Triángulos por frame | Llamadas de dibujo |
|---|---|---|
| Móvil de gama media, 60 fps | 200 000 a 500 000 | menos de 100 |
| Portátil con gráficos integrados | 1 a 2 millones | menos de 300 |
| Escritorio con GPU dedicada | 5 a 10 millones | menos de 1000 |
Y dos advertencias sobre cómo leer esa tabla. La primera es que las sombras duplican: cada luz que proyecta sombra renderiza la escena otra vez desde su punto de vista, así que un millón de triángulos con dos luces con sombra son tres millones. La segunda, y más importante, es que el número de triángulos casi nunca es el cuello de botella real. Lo son las llamadas de dibujo, el sobredibujado de transparencias y el ancho de banda de texturas. Un millón de triángulos en un objeto va bien; mil objetos de mil triángulos van mal. Esa observación es la que motiva todo lo que viene después sobre instancing.
Con eso, el criterio para repartir es de proporcionalidad al tamaño en pantalla. Un objeto que ocupa el diez por ciento del área merece aproximadamente el diez por ciento del presupuesto. Los objetos de fondo, que ocupan pocos píxeles, merecen las geometrías más baratas del catálogo aunque sean visualmente pobres: a veinte píxeles, un icosaedro de nivel cero y una esfera de treinta y dos por dieciséis se ven exactamente igual y cuestan 20 triángulos frente a 960.
Cuándo no usar TextGeometry
Vale la pena decirlo claramente: TextGeometry es la herramienta correcta para texto tridimensional que forma parte de la escena —un rótulo modelado, un título con relieve, letras que se manipulan como objetos— y la herramienta equivocada para casi todo lo demás.
Si el texto es plano y siempre mira a la cámara, un Sprite con una textura generada en un lienzo 2D cuesta dos triángulos y se ve más nítido, porque el texto rasterizado por el navegador tiene sugerencias de renderizado que ninguna extrusión iguala. Si el texto cambia con frecuencia, regenerar una TextGeometry implica reconstruir contornos, triangular y subir buffers, y eso son milisegundos por cada cambio. Si el texto tiene que ser seleccionable, accesible o buscable, la respuesta es HTML superpuesto, y ninguna cantidad de geometría lo suple.
Y si necesitas texto nítido a cualquier tamaño dentro de la escena, con oclusión correcta y sin regenerar geometría, la técnica es un campo de distancias con signo, que es lo que implementa la librería de terceros más usada del ecosistema. Ninguna de esas alternativas está en el núcleo de Three.js, y la razón es que ninguna es geometría: son texturas, y eso las saca del catálogo que hemos recorrido.
El error más común al empezar con Three.js es tratar el catálogo de geometrías como una lista de piezas de construcción y montar escenas encajando primitivas con sus valores por defecto. Funciona, y es la razón de que Three.js sea tan accesible, pero deja sobre la mesa un factor de cinco o diez en rendimiento que se recupera con una tarde de trabajo. La forma productiva de ver el catálogo es otra: es un conjunto de generadores paramétricos con coste conocido y derivable, y su valor principal es que te da una referencia contra la que medir cualquier decisión. Cuando sabes que una esfera por defecto son 960 triángulos, sabes que ponerle un chaflán de tres divisiones a una palabra de diez letras cuesta lo mismo que dieciséis esferas, y esa comparación es la que te hace bajarlo a uno. Cuando sabes que un cilindro con tapas cuesta el doble que sin ellas, miras los postes de tu escena y encuentras cincuenta tapas que nadie ve. Cuando sabes que la fórmula es siempre un producto de dos segmentos, entiendes que subir la calidad es cuadrático y bajarla también, y que por tanto la mitad de tu presupuesto suele estar en tres o cuatro objetos concretos. Ninguno de esos razonamientos requiere perfilar nada ni abrir ninguna herramienta: son aritmética sobre fórmulas que caben en una tarjeta. Y esa es la diferencia entre optimizar por intuición —que casi siempre ataca lo que se ve grande— y optimizar por cuenta, que ataca lo que de verdad cuesta.
- Genera un texto sin opciones y averigua por qué no lo ves.
- Mide los triángulos de la misma palabra con las cinco configuraciones de la tabla.
- Comprueba que la fórmula por glifo predice el resultado dentro de un margen razonable.
- Suma el presupuesto de una escena completa con
renderer.infoy localiza los tres objetos que se llevan la mitad. - Sustituye un rótulo plano por un
Spritecon textura de lienzo y compara triángulos y nitidez.