- En Unity, las formas más comunes de dibujar contornos alrededor de objetos son el efecto rim, la expansión de vértices, el buffer de blur, el algoritmo Jump Flood y la detección de bordes; cada una tiene distintos costos de calidad, rendimiento y configuración
- El efecto rim aproxima Fresnel con el producto punto entre la normal y la dirección de la vista para resaltar el interior de los bordes, pero los resultados varían mucho entre modelos redondeados y modelos con aristas marcadas
- La expansión de vértices crea el contorno agrandando una malla duplicada por detrás; si se procesa en espacio de clip, permite mantener un ancho más constante en pantalla
- El buffer de blur y el algoritmo Jump Flood dibujan la silueta en un buffer y luego la expanden; son especialmente útiles para líneas suaves o contornos muy anchos, respectivamente
- La detección de bordes encuentra discontinuidades en los buffers de profundidad, normales y color en toda la pantalla para generar líneas, pero requiere ajustar con detalle umbrales y modulaciones para reducir líneas no deseadas
Por qué usar renderizado de contornos
- Los contornos se usan en juegos para crear un estilo visual o como ayuda de gameplay, por ejemplo para resaltar o seleccionar objetos
- Sable usa contornos para lograr un estilo parecido al de un cómic
- The Last of Us usa contornos para resaltar enemigos cuando el jugador entra en modo sigilo
- Las cinco técnicas tienen distintos compromisos entre rendimiento, calidad visual y cantidad de configuración manual
Efecto rim: resaltar los bordes internos con una aproximación de Fresnel
- El efecto rim aproxima el efecto Fresnel para renderizar una línea en el rim/borde del objeto
- El cálculo se basa en el producto punto entre el vector normal normalizado
Ny la dirección de vista normalizadaV- El valor
1.0 - saturate(dot(N, V))se eleva a una potenciaPpara obtener la intensidad del efecto - Es más una aproximación para contornos que un Fresnel físicamente correcto
- El valor
- En una implementación en Unity, un shader personalizado controla el ancho, potencia, suavidad y color del contorno
- La implementación de ejemplo usa
smoothstep,lerp,_OutlineWidth,_OutlineSoftness,_OutlinePower,_OutlineColor
- La implementación de ejemplo usa
- Este método siempre aparece como una línea interna y no se ve en el contorno exterior del objeto
- Funciona bien en objetos redondeados y suaves, pero en cubos o modelos complejos el ancho de la línea puede no ser uniforme o puede no parecer un contorno
Expansión de vértices: agrandar una malla duplicada para crear una línea exterior
- La expansión de vértices vuelve a renderizar una copia del objeto o de la malla original, expande los vértices por detrás del original para crear una forma más grande y la renderiza con un color sólido
-
Elegir la dirección de expansión
- La calidad del contorno depende mucho de la dirección en la que se muevan los vértices
- Si se mueven en la dirección de la posición del vértice, la malla toma una forma inflada
- En espacio local, la posición del vértice puede usarse como un vector desde el centro del objeto hasta el vértice
- Si no se normaliza, los vértices más alejados del centro se desplazan más
- Usar
normalize(positionOS) * widthpermite un desplazamiento más uniforme en espacio de objeto - Si se mueven en la dirección del vector normal, el resultado es bueno en objetos con bordes suaves, como esferas o cápsulas
- En objetos con aristas marcadas, como cubos, pueden aparecer huecos en el contorno
- También se pueden usar colores de vértice como dirección de expansión
- Es un método que genera normales personalizadas y las guarda en el canal de color de vértices de la malla
- Si se hornean normales suaves esféricas como colores de vértice en un cubo, se puede obtener un mejor contorno
- La desventaja es la configuración manual de tener que crear normales personalizadas para cada malla, aunque se puede automatizar con scripts
-
Elegir el espacio de expansión
- En la etapa de vértices del shader, las coordenadas de los vértices empiezan en espacio de objeto y se transforman a espacio de clip mediante la matriz MVP
- El flujo es espacio de objeto/modelo/local → espacio de mundo → espacio de cámara/vista → espacio de clip → espacio de pantalla → espacio de viewport/ventana
- Si se expande en espacio de objeto, la transformación MVP todavía no se aplicó, por lo que el contorno puede deformarse
- Al aplicar la matriz de modelo pueden aparecer problemas de escalado
- Al pasar de espacio de clip a espacio de pantalla, la división de perspectiva provoca reducción por perspectiva
- Parte del desplazamiento en espacio 3D ocurre en dirección de la cámara y no contribuye al ancho del contorno en pantalla
- Expandir en espacio de clip permite crear un contorno limpio, con un ancho casi uniforme en pantalla
- Después de transformar la posición y la normal del vértice a espacio de clip, solo se modifican las coordenadas
xey - Se corrige la relación de aspecto dividiendo por el ancho y alto de la pantalla
- Se multiplica por
wen espacio de clip para que la división de perspectiva posterior no tenga un efecto neto sobre el ancho del contorno - Se multiplica por
width * 2para que una unidad de ancho corresponda a 1 píxel de la pantalla - El método en espacio de clip también depende de las normales de la malla
- Si no se usan normales personalizadas, pueden aparecer huecos en aristas marcadas
- Si las normales están mal configuradas y apuntan en la dirección opuesta, los vértices del contorno también se desplazan en sentido contrario y generan huecos
- Puede verse más explicación relacionada en creating an outline in clip space
-
Enmascaramiento
- En la malla duplicada, solo debe verse la parte del contorno que sobresale
- El enfoque común es hacer culling de la geometría front-facing de la malla duplicada y crear el contorno con las backfaces
- La prueba de profundidad usa less than or equal to para que las backfaces solo se vean en la posición del contorno
- Otro método es usar una máscara de stencil para impedir que la malla duplicada se vea delante del original
- En ese caso no hace falta culling
- No se generan líneas internas del objeto
- Si dos objetos se superponen, el contorno solo se ve alrededor de ambos objetos
Buffer de blur: expandir una silueta difuminándola
- El método de buffer de blur renderiza la silueta del objeto en un buffer, luego la expande con blur y usa ese resultado para renderizar el contorno
-
Buffer de silueta
- El primer paso es crear el buffer de silueta
- Cada objeto se renderiza en una textura con un shader de salida de color sólido
- Si todas las siluetas se renderizan en blanco, al final se puede multiplicar por el color de contorno deseado para obtener un único color
- Si cada objeto necesita un contorno de color distinto, cada silueta puede renderizarse con un color específico
-
Pasada de blur
- La pasada de blur se usa para expandir el buffer de silueta
- Normalmente se usa box blur o Gaussian blur
- Para mejorar el rendimiento, se puede reducir la resolución del buffer de silueta antes del blur
- La pasada de blur es costosa porque calcula el promedio o promedio ponderado de varios píxeles alrededor de cada píxel
- Conviene realizar el blur en 2 pasadas
- En filtros separables como box blur y Gaussian blur, la complejidad algorítmica se puede reducir de
O(N²)aO(2N) - Primero se aplica blur en dirección vertical y luego se aplica blur en dirección horizontal al resultado para obtener el resultado final
- El ancho del contorno se controla con el parámetro
_KernelSizedel shader de blur
-
Pasada de contorno y enmascaramiento
- Después de la pasada de blur, la silueta difuminada se compone con la escena original para crear el contorno
- El buffer de blur es adecuado para contornos suaves o luminosos
- Si se aplica un step al resultado del blur, también se pueden renderizar contornos duros
- Al igual que con la expansión de vértices, se puede usar una máscara de stencil para que el contorno se renderice solo detrás de la geometría
- Puede tener un impacto de rendimiento mayor que otros métodos
Algoritmo Jump Flood: manejar contornos muy anchos
- El cuarto método renderiza contornos con el algoritmo Jump Flood
- Su ventaja es que puede renderizar contornos muy anchos con un costo de rendimiento razonable
- La explicación detallada continúa en The Quest for Very Wide Outlines de Ben Golus
Detección de bordes: encontrar discontinuidades en toda la pantalla
- El método de detección de bordes dibuja líneas buscando discontinuidades en la escena durante una pasada de pantalla completa
- Las discontinuidades pueden detectarse en los valores del buffer de profundidad, los vectores normales, el color de albedo u otros datos disponibles durante el proceso de renderizado
-
Roberts cross
- El Roberts cross operator es un operador diferencial que calcula la suma de cuadrados de las diferencias entre píxeles diagonales
- En una implementación real, detecta bordes convolucionando un kernel sobre la imagen original
- Usa dos kernels: uno en dirección
xy otro en direccióny - El tamaño del kernel es
2 x 2 - Solo necesita 4 muestras alrededor de un píxel
- Es un operador simple, pero puede producir buenos resultados
-
Sobel operator
- El Sobel operator también usa dos kernels, uno en dirección
xy otro en direccióny - El tamaño del kernel Sobel es
3 x 3y usa 9 muestras alrededor de un píxel - Puede verse más sobre cómo funciona el filtro Sobel en este blog post on Sobel filters
- El Sobel operator también usa dos kernels, uno en dirección
-
Fuentes de discontinuidad
- El enfoque habitual es buscar discontinuidades en la textura de profundidad, textura de normales y textura de color que genera el pipeline de renderizado para la escena
- La pasada de detección de bordes samplea estas texturas y detecta discontinuidades con los operadores mencionados
- Los bordes generados así pueden dibujarse debido a una discontinuidad encontrada en cualquiera de los tres buffers
- Este método aplica contornos a todos los objetos que escriben en esos buffers, por lo que ofrece poco control por objeto
- Permitir varias fuentes de discontinuidad ayuda a crear un sistema de contornos más robusto
- Algunos bordes se detectan en las tres fuentes
- Muchos bordes se detectan solo por la contribución de una fuente específica
- Se pueden asignar distintos pesos y umbrales a cada fuente para controlar el resultado visual del contorno
-
Modulación de la detección de bordes
- Si solo se aplica un operador de detección de bordes al buffer de discontinuidades, puede ser difícil obtener resultados sin artefactos
- En muchos pipelines de renderizado, el buffer de profundidad se implementa de forma no lineal
- La diferencia de profundidad entre dos objetos cercanos a la cámara separados por 1 m es mayor que la de dos objetos lejanos separados por 1 m
- Para corregirlo, el umbral de detección de discontinuidades de profundidad puede modularse con el propio buffer de profundidad
- La geometría cercana requiere una discontinuidad de valores de profundidad más grande antes de ser detectada como borde
- También pueden aparecer bordes no deseados en ángulos rasantes pequeños
- Se puede modular usando una máscara Fresnel creada con el producto punto entre el vector normal
Ny la dirección de vistaV - Esta máscara es del mismo tipo que la usada en el método de efecto rim
- También pueden usarse otras técnicas de modulación, pero la elección depende del efecto visual buscado
-
Fuentes de discontinuidad personalizadas
- También se pueden proporcionar fuentes de discontinuidad personalizadas al shader de contornos
- Son render textures creadas directamente durante el proceso de renderizado y contienen datos personalizados para generar el contorno
- Como se puede controlar directamente qué objetos escriben en el buffer personalizado, también se puede controlar qué objetos reciben contorno
- Por ejemplo, se pueden renderizar los colores de vértice de una malla en una textura para crear una fuente de discontinuidad
- Otras alternativas son colorear las caras según la posición en el mundo, o crear un buffer personalizado que combine información del buffer de profundidad y del buffer de normales
- Puede encontrarse más información en Linework section map
1 comentarios
Comentarios de Hacker News
El artículo enlazado sobre Jump Flood Algorithm me pareció realmente bueno: https://bgolus.medium.com/the-quest-for-very-wide-outlines-b...
Es interesante pensar en varios enfoques que se pueden usar a nivel de píxel/texel, y aquí también los campos de distancia con signo (SDF) se usan de una forma ingeniosa que resuelve gran parte del trabajo
El resultado de crear contornos del ancho deseado en tiempo lineal es impresionante comparado con un enfoque de fuerza bruta para anchos grandes
Recomiendo mucho los SDF, ya sean basados en vectores, basados en funciones como el trabajo de Inigo Quilez, o un método ráster basado en texels/voxels como en el artículo
Houdini también tiene buen soporte para SDF ráster y un conjunto maduro de herramientas SDF, así que vale la pena verlo aunque sea con la versión gratuita
La desventaja de ejecutarlo en GPU es que requiere escrituras de acceso aleatorio, es decir, compute shaders
Si te sirve hacerlo en CPU, hay varias implementaciones
JavaScript: https://parmanoir.com/distance/
C: https://github.com/983/df
C++: https://github.com/opencv/opencv/blob/4.x/modules/imgproc/sr...
Python: https://github.com/pymatting/pymatting/blob/afd2dec073cb08b8...
Porque tienen buena calidad y permiten renderizar efectos basados en distancia, como contornos pulsantes
Esta herramienta para dibujar líneas 3D también guarda el SDF en una textura pequeña y lo muestrea en tiempo de ejecución: https://x.com/alexanderameye/status/1663523972485357569
Los SDF son realmente potentes
Si se extiende el mismo método hasta radiance cascades, puede volverse aún más rápido, así que es bastante interesante
Algún día me gustaría profundizar en gráficos 3D estilizados como proyecto de investigación y desarrollo.
Últimamente hubo bastante avance, pero todavía parece quedar mucha fruta al alcance de la mano.
Me pregunto cómo reducir el detalle de modelos 3D con toon rendering cuando la cámara se aleja, y cómo hacer transiciones naturales entre aspectos más estilizados y menos estilizados.
También está el problema de si se pueden renderizar de forma convincente como escenas 3D los fondos en acuarela de la animación 2D dibujada a mano, y cómo animar suavemente en espacio de pantalla las pinceladas y la textura del papel.
Sigue abierto cómo representar elementos como humo, fuego, árboles, pasto, barro, lluvia, pelaje y agua en juegos 3D estilizados.
También me intriga si, en juegos con cámara libre, se puede automatizar la deformación sutil de modelos para que se vean mejor desde el ángulo actual de la cámara, como en la animación dibujada a mano.
Otra gran pregunta es cómo serían el editor de mallas y el editor de fondos ideales para un renderer estilizado, y si realmente hacen falta superficies 3D y rigs físicamente correctos o si podrían definirse de forma más abstracta.
También es interesante si se podría renderizar pixel art retro a partir de modelos 3D simples para crear juegos 2D procedurales, y quizá la estilización podría usarse para que, cuando dos mallas se intersectan accidentalmente, esa intersección se note menos.
Solo con las preguntas se podrían llenar las carreras de diez personas, pero creo que eso más bien es algo bueno.
El filtro Kuwahara también resulta suficientemente convincente para la mayoría.
Para herramientas de edición orientadas a renderers estilizados, Blender + Rigify + shape keys + un poco de magia con drivers fue suficiente para mis necesidades.
Texturizar en Blender es fastidioso, pero a nivel hobby se puede tolerar; si necesitas más control de renderizado no fotorrealista, quizá convenga más el fork de DillonGoo Studio: https://www.dillongoostudios.com/gooengine
Probé crear pixel art a partir de modelos 3D renderizando animaciones/modelos a baja resolución, y el resultado fue aceptable, aunque requirió prueba y error.
Recuerdo que también hubo casos con postprocesado más sofisticado para eliminar cosas como el parpadeo de píxeles.
Baja resolución: https://x.com/Navy_Green/status/1525564342975995904
Estabilización: https://x.com/Navy_Green/status/1693820282245431540
Empecé mi carrera con apps de VR, pero pronto me pasé al desarrollo web porque el mercado era mejor.
Leer artículos como este me hace extrañar ese campo.
Trabajar con gráficos 3D, colisiones y shaders tenía una sensación mágica difícil de encontrar en otras áreas.
En la práctica estás creando mundos y reproduciendo física, y las matemáticas son mucho más prácticas y aparecen con mucha más frecuencia que en otros campos de la programación.
La satisfacción laboral es mucho mayor, y la profundidad y el techo del tema también son más altos.
Llevo 4 años en esto y nunca me topé con ese muro de aburrimiento que sentía en desarrollo web.
No hay nada como llegar el lunes al trabajo, abrir el editor del motor, ver renderizarse el pequeño mundo en el que estás trabajando y pensar qué función genial vas a agregar después.
En cambio, en el arte técnico, si absorben los resultados y alimentan redes neuronales con ellos, desaparecen las partes interesantes y gratificantes, y para el trabajo profesional solo quedan las tareas aburridas de siempre, o incluso aumentan.
También baja el valor de todo el conjunto de habilidades, y la gente de la industria tech actúa como si supiera más del tema que quienes lo hacen, mientras acusa de no alegrarse a quienes no celebran que empresas de Silicon Valley arruinen el mercado ofreciendo resultados de C- a precio de F, sin compensar a las personas que crearon los datos originales.
Sopesando pros y contras, creo que la decisión de pasarme al desarrollo web fue la correcta.
En mi juego Astral Divide, hice una técnica que no aparece en el artículo.
Es parecida a Blurred Buffer, pero sin hacer una pasada de blur; usa los bordes que genera el antialiasing.
Se dibuja el objeto en blanco opaco sobre un fondo negro transparente, y en el fragment shader se filtran, con un umbral hardcodeado, solo los píxeles cuyo canal alfa no sea completamente opaco ni completamente transparente.
El resultado es bastante bueno, el costo de rendimiento es bajo y la implementación es muy simple.
Hace tiempo, al implementar una herramienta de recorte en un software de edición de video, necesitaba desenfocar temporalmente el área fuera del recorte durante la edición.
Como no quería agregar otra pasada de blur costosa, solo cambié el sesgo de mipmap para que se renderizara una textura de menor resolución, y el filtrado de texturas hizo todo el trabajo gratis.
Al compararlo con un efecto de blur de recorte similar de PowerPoint, se veía casi igual; de hecho, PowerPoint tenía un poco de banding de color y mi implementación no.
De forma similar, en una situación en la que no había antialiasing a resolución completa, la mayor parte del lienzo eran rectángulos 2D rotables, así que se veían bordes con efecto escalonado.
En lugar de activar antialiasing de pantalla completa, agrandé un poco todos los rectángulos y reduje proporcionalmente las coordenadas UV, de modo que los bordes visibles quedaran dentro del rectángulo 3D real, y una vez más el filtrado de texturas lo resolvió gratis.
¿O es un enfoque en el que mantienes el alfa y solo cambias el color, de modo que el contorno queda tenue pero sin escalones?
Las notas son excelentes.
Hace poco estuve buscando enfoques de detección de bordes y encontré un buen método del desarrollador de Mars First Logistics: https://www.reddit.com/r/Unity3D/comments/taq2ou/improving_e...
El artículo es excelente y el resultado se ve muy bien: https://ameye.dev/notes/rendering-outlines/edge-detection/co...
Parece una escena del cómic neerlandés Franka.
Es un artículo realmente sobresaliente y la experiencia de lectura también es excelente.
Explica conceptos difíciles con palabras que cualquiera puede entender, los diagramas y ejemplos son buenos, y la legibilidad es de primer nivel gracias al espaciado y la tipografía.
Me da curiosidad qué lo llevó a crear el tema actual, y si alguna vez consideró crear una plataforma de publicación centrada en ingenieros.
En software, el arte técnico es, sin duda, mi primer amor.
Ojalá en Godot fuera más fácil la canalización de compute shaders para efectos de posprocesamiento.
La configuración actual de plugins de compositor tiene bastante código repetitivo.
Este repositorio es un buen ejemplo de posprocesamiento en Godot: https://github.com/sphynx-owner/JFA_driven_motion_blur_demo
Creo que la primera vez que vi este efecto fue en Wacky Races de Dreamcast.
En esa época había mucha publicidad diciendo que era el primer juego en introducir ese efecto y que los desarrolladores prácticamente lo habían inventado.
No sé si eso era cierto o solo exageración de marketing, pero como gamer, sin duda fue la primera vez que lo experimenté.
Honestamente, la Dreamcast fue el primer hardware capaz de procesar este efecto en tiempo real con alta calidad.
Desarrollé el efecto de cel shading para el juego de Dreamcast Looney Tunes: Space Race, y lo hice en la primera semana después de recibir el kit de desarrollo de Dreamcast.
Infogrames Sheffield, que hizo Wacky Racers, vio una versión temprana de nuestra implementación y agregó un efecto similar a su juego.
Se veía bien, pero llegó tarde en la producción y no estaba optimizado alrededor de ese efecto como nuestro juego.
El equipo de Jet Grind Radio también creó el mismo efecto de forma independiente y lanzó antes que nosotros.
El algoritmo era exactamente el mismo, pero lo usaban de otra manera: ellos aceptaban activamente los contornos irregulares, anchos y dentados, mientras que Sheffield y nosotros luchábamos contra esa característica para intentar ajustarlo a un estilo artístico más uniforme y tradicional.
Aproximadamente un año después, en Dragons Lair 3D para Xbox, parece que alguien descubrió cómo ejecutar cel shading con detección de bordes en tiempo real.
Probé implementar ese enfoque en Dreamcast, pero el rendimiento no alcanzaba para aplicarlo a varios personajes al mismo tiempo mientras el juego corría.
No sé si fue porque la Xbox era más potente o porque el algoritmo era más inteligente, pero el resultado es innegable.
Si quieres hacer un juego que parezca una caricatura realmente dibujada a mano, personalmente creo que ese método sigue siendo el de mayor calidad.
Algún día me gustaría encontrar una excusa para volver a implementarlo, y ahora creo que el rendimiento ya no sería un problema.
Una coincidencia bastante increíble.
Definitivamente tengo que empezar con programación de shaders y renderizado 3D.
Este tipo de artículos es genial, y me gustaría poder escribir shaders de verdad.