lunes, 25 de noviembre de 2019

Renderizado de Texto en el Editor de Diagramas de Flujo

Encontré casi por casualidad una idea para renderizar fuentes con OpenGL aparentemente mucho mejor que la que venía usando en el editor de diagramas de flujo de PSeInt (psdraw). Mi sorpresa fue grande. Conocía el concepto, pero no implementado de esta forma. Para esa aplicación (texto), resulta muuuy simple y los resultados son muy buenos.

Método original


Respecto al texto en psdraw, hasta ahora hacía lo que hacen muchos: tener la fuente (todos los caracteres) en una textura, y dibujar quads, uno por cada letra de una cadena, cada uno con una partecita de la textura. Esto es relativamente rápido y eficiente, pero tiene un problema: en el caso del texto se nota mucho el filtrado de la textura. ¿A qué me refiero? A que cuando la letra se muestra de un tamaño tal que no coincide la resolución de la textura con la de la ventana (no coincide pixel con téxel), a la textura se le aplica algún escalado rápido que la blurea o pixela.

La solución sería tener varias texturas, distintos tamaños, pero eso es más memoria, más trabajo. Y además en psdraw el nivel de zoom es continuo, no salta de un tamaño a otro sino que barre todos los intermedios, así que los tamaños posibles son infinitos. Sí, podría tener unos cuantos e interpolar, o algo tipo mipmaps, pero parece demasiada memoria (aunque nunca se note) para una tarea tan simple, y además cada interpolación usualmente implica más bluring artificial.

Hasta ahora tenía dos versiones de la fuente (de los 256 caracteres que uso), una en una textura de 1024x1024, otra de 2048x2048. En general con eso sobra, nadie mira los diagramas tan de cerca.


Método nuevo

El problema del 1er método se da cuando queremos muestrear la textura en un punto intermedio entre 2 téxeles. Allí se promedian los colores de los dos. Si estábamos en el borde entre interior y exterior de la fuente (supongamos blanco y negro), nos quedamos con un gris que no es ni una cosa ni otra, que no existiría en la fuente a mayor resolución. Ahora, supongamos que en lugar de color, lo que hay en cada posición de la textura es un indicador de cuan cerca o lejos está el borde (con un signo que depende de estar adentro/afuera). Ahora el promediado sí tiene sentido como aproximación de esa distancia. Y entonces diremos que donde la distancia sea 0 está el borde, así que si el signo del promedio es positivo estamos adentro, y si no afuera, no más grises.

 A la izquierda, capturas del método original; a la derecha, del nuevo.
La nueva textura tiene 1/4 de la resolución de la original.

Esto se llama SDF, que no es Super-Dimensional Fortress, sino Signed Distance Fields. La implementación es muuuy simple hasta en OpenGL 1. Los valores de distancia se mapean al intervalo [0;1], dejando la distancia 0 original ahora en el .5, y activando el alpha-test (literalmente tres lineas de código) la gpu hará la inteporlación y el corte por nosotros. Respecto a la textura, usé la misma que antes, reducida a solo 512x512, y con un desenfoque gaussiano aplicado con gimp. Ese no es el verdadero campo de distancias, pero el desenfoque da un efecto parecido.

El problema de este método es el antialiasing, que no existe. Los métodos que ofrece OpenGL sin tener que programar extra solo funcionan en los bordes reales de los polígonos, y no en los bordes artificiales que genera el alpha-test en el interior. Así que para solucionar eso (más o menos) tuve que programar un pequeño fragment-shader que haga un muestreo sub-pixel del alpha-test para determinar opacidades intermedias donde corresponda (y como "daño" colateral, ahora tengo una dependencia más: glew).

Izquierda: solo alpha-test; Derecha: antialiasing mediante shader

El resultado es bastante bueno. No es el mejor texto del mundo ni mucho menos. Por un lado, no representa fielmente la forma de la fuente (se redondean los vértices). Para resolver eso hay técnicas, pero involucran alguito más de trabajo en el shader y en la generación de la textura, y cómo los detalles de la tipografía a mi no me hacen mucha diferencia, lo dejé así por ahora. Por otro lado el antialiasing tampoco es tan bueno, esto se nota más en textos pequeños en pantallas no HiDPI. Pero no quería complicarlo ni encarecerlo tanto, y ya con esto se ve mucho mejor que antes, así que en cualquier caso es un avance.


En conclusión, me sorprendió la relación costo/beneficio de esta técnica, que además sirve para cualquier forma (son como máscaras binarias, más detalles aquí). Es de esas cosas que no sé cómo no encontré antes. Y aunque no tenía previsto trabajar en las fuentes de psdraw, pareció el lugar más simple para probarla. Así que en la próxima versión de PSeInt, el editor de diagramas se verá un poquito mejor.

1 comentario: