Saltar al contenido

troika-three-text y los campos de distancia con signo

Por qué un SDF mantiene el borde nítido a cualquier escala, cómo troika genera el atlas en un worker desde la fuente real, y la API completa que necesitas.

Qué se guarda en un campo de distancia

En una textura de glifo convencional, cada téxel guarda cuánta tinta hay: cerca de uno dentro de la letra, cerca de cero fuera, y valores intermedios en el borde. Al ampliar, el filtrado bilineal interpola cobertura, y una rampa de cobertura ampliada es una rampa más ancha. Eso es exactamente lo que percibimos como desenfoque.

En un campo de distancia con signo, cada téxel guarda la distancia al contorno más cercano, negativa dentro y positiva fuera, normalizada a un rango. El contorno de la letra es el conjunto de puntos donde ese campo vale cero. Al ampliar, el filtrado bilineal interpola distancias, y la interpolación de un campo de distancias sigue siendo aproximadamente un campo de distancias: la superficie de nivel cero sigue estando exactamente en el mismo sitio geométrico. El borde no se ensancha, se reconstruye.

Y como el campo es continuo, el antialiasing sale gratis y con la anchura correcta: basta con hacer un smoothstep de un píxel de ancho alrededor del cero. La técnica es de Chris Green, en un artículo de Valve de 2007, y desde entonces es como se dibuja el texto en prácticamente todos los motores de videojuego.

GLSL
// La idea, en tres lineasfloat d = texture2D( uSdf, vUv ).r - 0.5;   // 0.5 es el cero del campo codificadofloat w = fwidth( d );                       // el ancho de un pixel en unidades de campofloat alfa = smoothstep( -w, w, -d );        // dentro es alfa 1

Sin fwidth la anchura del suavizado sería fija en unidades de textura y por tanto variable en píxeles, con lo que el texto lejano quedaría duro y el cercano blando. Con fwidth la transición mide siempre lo mismo en pantalla, que es lo que hace que la técnica funcione a cualquier escala.

El defecto del canal único, y por qué troika lo tolera

Un SDF de un solo canal no puede representar dos bordes que se cruzan dentro de un mismo téxel. En una esquina viva —el vértice de una “A”, la punta de una “W”— el téxel de la esquina almacena una única distancia y la reconstrucción redondea el pico. A tamaños normales no se ve; a tamaños muy grandes, sí.

La solución conocida es el MSDF, campo de distancia multicanal: se guardan tres distancias en R, G y B, correspondientes a distintos subconjuntos de aristas, y la mediana de las tres reconstruye las esquinas exactas. Es lo que hace msdfgen y lo que usan librerías como three-msdf-text.

troika-three-text usa SDF de canal único y compensa por otra vía: en vez de una única textura ampliada uniformemente, genera el campo por glifo con una resolución configurable y aplica el antialiasing con derivadas en el fragment shader. El resultado es excelente hasta tamaños grandes y solo se queda corto en tipografía de titular gigante con serifas finas. A cambio, el atlas ocupa un tercio y la generación es mucho más rápida, lo que importa porque troika genera el atlas en tiempo de ejecución.

Ese es el punto que separa a troika del resto: no consume un atlas precocinado, sino que lee el fichero de fuente real —WOFF, TTF u OTF— con un parser propio, extrae los contornos de los glifos que tu texto necesita, y calcula sus campos de distancia en un web worker. Nunca subes al servidor una imagen de atlas, ni tienes que decidir de antemano qué caracteres vas a usar, ni te quedas sin poder escribir un carácter que no previste.

La API que necesitas

JS
import { Text, preloadFont } from 'troika-three-text';// 1. Precarga opcional pero muy recomendable: evita el parpadeo del primer syncawait new Promise( ( resolver ) => {  preloadFont(    {      font: '/fonts/Inter-SemiBold.woff',      characters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 0123456789.,',      sdfGlyphSize: 64,    },    resolver  );} );// 2. Crear y configurarconst rotulo = new Text();rotulo.text = 'Campos de distancia';rotulo.font = '/fonts/Inter-SemiBold.woff';rotulo.fontSize = 0.35;rotulo.color = 0xcdd6f4;rotulo.anchorX = 'center';rotulo.anchorY = 'middle';rotulo.maxWidth = 4;rotulo.textAlign = 'center';rotulo.lineHeight = 1.25;rotulo.letterSpacing = 0.01;rotulo.outlineWidth = '3%';rotulo.outlineColor = 0x11111b;rotulo.sdfGlyphSize = 64;scene.add( rotulo );// 3. sync() aplica los cambios. Es asincrono.rotulo.sync( () => {  // Aqui, y solo aqui, la geometria y el boundingSphere ya existen  rotulo.geometry.computeBoundingBox();} );

Propiedades que merece la pena conocer más allá de las obvias. curveRadius curva el texto sobre un cilindro virtual, ideal para etiquetas alrededor de un objeto. clipRect recorta el texto a un rectángulo en el espacio local, útil para listas con scroll. fillOpacity, strokeWidth y strokeColor dan trazo interior sin tocar el contorno exterior. depthOffset desplaza la profundidad para resolver el z-fighting cuando el texto está pegado a una superficie.

Y la que casi nadie usa: el material es reemplazable. Troika parchea cualquier material de Three.js para aplicar el recorte por SDF, así que puedes darle iluminación PBR completa.

JS
rotulo.material = new THREE.MeshStandardMaterial( {  roughness: 0.4,  metalness: 0.8,} );rotulo.sync();

El coste de dibujado es sobresaliente: cada instancia de Text es una sola draw call, con la geometría instanciada —un cuadrado por glifo— y el atlas compartido entre todas las instancias que usen la misma fuente. Cien etiquetas con la misma tipografía son cien draw calls pero una sola textura, frente a las cien texturas de los sprites de canvas.

La limpieza también es una sola llamada:

JS
rotulo.removeFromParent();rotulo.dispose();

Dónde troika no llega

No renderiza escrituras que requieran reordenación compleja con la misma fidelidad que el motor del navegador; para latín, cirílico, griego y la mayoría de escrituras alfabéticas va sobrado, pero si tu producto tiene que servir devanagari o thai correctamente, el canvas 2D del navegador sigue siendo la única opción que garantiza el resultado.

No es texto real para el usuario: no se puede seleccionar con el ratón ni lo lee un lector de pantalla. Troika ofrece getSelectionRects y getCaretAtPoint para construir selección propia, pero eso es implementar un editor de texto, no obtener uno.

Y el primer sync de una fuente nueva tiene un coste medible: parsear el WOFF y generar los SDF de los glifos usados lleva entre decenas y cientos de milisegundos. Ocurre en un worker, así que no bloquea el hilo principal, pero el texto no aparece hasta que termina. De ahí la importancia de preloadFont.