Efectos de renderizado basados en sombreado en Compose Desktop con Skia

Efectos de renderizado basados en sombreado en Compose Desktop con Skia

Hoy, vamos a ver la reciente adición en Skia - filtros de imagen basados en shaders, que están disponibles como efectos de renderizado en Compose Desktop. Estos filtros operan sobre el contenido del nodo de renderizado específico en la jerarquía de Compose, permitiendo implementar efectos como este con un único shader compuesto.


Empecemos con una ventana esqueleto. Para los propósitos de esta demostración, es una simple ventana de composición sin decoración. No se puede mover, no se puede cambiar el tamaño, no se puede hacer clic alrededor, y sirve como telón de fondo para nuestro contenido principal. En primer lugar, los visuales de la ventana:


Y el código que hay detrás:


Ahora añadimos nuestros tres círculos. Uno es un relleno sólido, y los otros dos utilizan gradientes, uno horizontal y el otro diagonal:


Y el código, utilizando las APIs Canvas.drawCircle y Brush.linearGradient:


Ahora comenzamos con el núcleo de la implementación, nuestro shader. El sombreador se "ejecuta" en cada píxel en el contenido de renderizado de este nodo Canvas. Para los píxeles dentro del rectángulo redondeado de la "tarjeta", queremos aplicar un efecto de blur, y para los píxeles fuera de ese rectángulo redondeado, no queremos aplicar nada (un ajuste en esta parte en un momento). Para distinguir entre los píxeles dentro y fuera de la tarjeta, nosotros:


  • Pasar la configuración del rectángulo redondeado - caja delimitadora y radio de la esquina - al shader como uniformes (aka parámetros).
  • Utiliza una función auxiliar de distancia con signo 2D (SDF) para determinar si el píxel actual está dentro o fuera de nuestro rectángulo redondeado.

En primer lugar, las imágenes que conseguimos:


Y el código:


Veamos con más detalle las distintas partes:


  • El código del sombreador en sí mismo es una única cadena de varias líneas. Esa cadena es compilada por Skia en tiempo de ejecución, y por ahora no hay resaltado de sintaxis incorporado o comprobación de errores en tiempo de edición o construcción dentro de IDEA. Para conseguirlo, instala este plugin y marca tu cadena de sombreado como @Language("GLSL").
  • El shader recibe dos parámetros. El primero es el contenido disponible "implícitamente" - que es el content del nodo de renderizado subyacente, que es en nuestro caso el canvas con tres círculos. El segundo es nuestro shader de blur que se aplicará en los píxeles dentro del área del rectángulo redondeado.
  • La configuración del rectángulo redondeado se pasa como dos uniformes. El primero es la caja delimitadora como vec4 (una 4-tupla de floats). El segundo es el radio de la esquina como un float.
  • Creamos un efecto en tiempo de ejecución con RuntimeEffect.makeForShader.
  • Que luego envolvemos en un RuntimeShaderBuilder.
  • Que luego se utiliza para crear un efecto de renderizado que se establece en Modifier.graphicsLayer en nuestro nodo Canvas:
  • Pasar content y blur como nombres de sombreadores hijos - tenga en cuenta que estos deben coincidir con los uniform shaders en el propio sombreador.
  • — null para la entrada de content - indicando a Skia que la entrada real será el contenido del nodo de renderizado subyacente.
  • E ImageFilter.createBlur como entrada de blur - un filtro de imagen de blur que se aplicará en los píxeles dentro del rectángulo redondeado
  • Por último, ten en cuenta que el resultado de ImageFilter.makeRuntimeShader, que es un objeto de la API Skia, debe convertirse en un objeto de la API Compose con ImageFilter.asComposeRenderEffect.

En este punto, es un poco difícil ver si el desenfoque se aplica sólo en los píxeles dentro del rectángulo redondeado, ya que todas esas esquinas redondeadas son efectivamente "invisibles" en lo que respecta al desenfoque (desenfocar un rectángulo negro sólido deja todos los píxeles del mismo color negro). Lo que vamos a hacer ahora es añadir otro lienzo hijo a nuestra caja de nivel superior, que será el borde de la tarjeta (más los textos como último paso):


Código:


Observe que todavía no podemos ver si el blur se aplica correctamente alrededor de las esquinas redondeadas. Para ver eso, tendríamos que aplicar gradientes de color más "nítidos" en nuestros círculos, y un gradiente en nuestra caja de nivel superior en sí. Sin embargo, para el propósito de esta demostración en particular, ese paso no es estrictamente necesario, y lo vamos a omitir.


Nuestro siguiente paso es volver a nuestro rectángulo redondeado y aplicar una sombra paralela a su alrededor para ayudar a compensar un poco el fondo con los círculos, creando una ligera separación visual entre estas dos capas conceptuales:


Desde el punto de vista de la implementación, el primer intento fue mirar las APIs de Skia ImageFilter.makeDropShadow e ImageFilter.makeDropShadowOnly, pero parece que:


  • El primero funciona como "se espera" sólo en formas rellenas totalmente opacas. En nuestro caso, nuestra "tarjeta" es un degradado blanco translúcido, por lo que la sombra paralela se nota mucho menos ya que el propio relleno no es totalmente opaco. Y además, la sombra paralela se aplica como un "fantasma" del contorno de la forma, no sólo fuera de la forma, sino también dentro de ella. En el caso de un relleno totalmente opaco, no importa, ya que la sombra interior queda oculta. Pero en el caso de un relleno translúcido se nota bastante.
  • Esta segunda parte (las partes exterior e interior de la sombra de la gota) es la razón por la que la segunda API no es un buen ajuste también.

Mientras que este problema podría abordarse aplicando un recorte negativo (recortando la parte interior del rectángulo redondeado), o utilizando uno de los modos de mezcla, aquí estamos optando por emular la sombra de caída como parte del propio sombreado:


La función de distancia con signo devuelve un float - negativo si el punto está dentro de la forma, y positivo si está fuera. Lo que hacemos aquí es mirar el caso positivo - píxeles fuera de la forma, y para aquellos dentro de la distancia de 30 píxeles del contorno del rectángulo redondeado, aplicamos un oscurecimiento decadente, cambiando efectivamente el color del píxel actual dado a nosotros desde la llamada content.eval(coord) hacia el color negro. El decaimiento en sí mismo es exponencial para emular mejor la apariencia de la sombra - vea la llamada pow(darkenFactor, 1.6).


Esta parte, por cierto, es la razón por la que el sombreado se aplica en todo el nodo del lienzo sin recortes. Mientras que el desenfoque (y el subsiguiente relleno interior + noise) se aplica al interior del rectángulo redondeado, el efecto de la sombra se aplica a los píxeles fuera de esa área.


Ahora hacemos un relleno interior de gradiente blanco translúcido:


El relleno es más fuerte alrededor de la esquina superior izquierda de nuestra área interior, y decae hacia la esquina inferior derecha. En primer lugar, determinamos la distancia a la que se encuentra nuestro píxel de la esquina superior izquierda utilizando la función de length incorporada y los swizzles xyzw incorporados, y luego cambiamos el color del píxel desenfocado obtenido de la llamada blur.eval(coord) hacia el color blanco - que es vec4(1.0) en este fragmento de shader:


Si se observa con suficiente atención, se verá una banda notable de este gradiente radial. Para solucionar esta imperfección visual, también aplicamos una textura de noise sobre el relleno blanco de gradiente translúcido:


En primer lugar, tenemos que configurar la fuente del propio noise. Para ello, añadimos un uniform shader más a nuestro sombreador:


Y utilizar Shader.makeFractalNoise y RuntimeShaderBuilder.child para pasar ese noise shader:


Con esta última adición a nuestro shader principal, podemos incorporar el noise encima de nuestro gradiente blanco translúcido:


Observe que la salida de Shader.makeFractalNoise para cada píxel no es una escala de grises, y utilizamos la máscara rgb swizzle para convertir los componentes rojo-verde-azul del píxel de ruido a su valor de luminancia, y luego aplicamos esa luminancia sobre el relleno de gradiente alfa.


Por último, estamos listos para añadir nuestros tres textos en el interior del rectángulo redondeado que ahora combina el desenfoque, el gradiente blanco translúcido y la textura de noise:


Aunque no es estrictamente necesario en este caso particular, aquí se está utilizando la función de ayuda introducida anteriormente Canvas.drawTextOnPath que dibuja una cadena a lo largo del camino especificado - en nuestro caso, cada camino es un simple segmento horizontal:


Un par de notas más antes de terminar.


Casi siempre hay múltiples formas de utilizar las APIs gráficas existentes para conseguir los efectos visuales deseados. Aquí se está utilizando un único sombreador compuesto con dos sombreadores hijos (blur y noise) para resaltar las capacidades de las APIs de sombreado existentes en Skia. También decidí omitir la exploración de las APIs de filtros de imagen de sombra y emular esa parte en este shader. Todo esto se puede dividir en múltiples sombreadores, múltiples lienzos o para utilizar diferentes APIs de Skia.


La razón para usar dos Canvas componibles es separar:


  • El "fondo" de la tarjeta -el blur, el gradiente blanco translúcido, el noise- que se aplica a los círculos de color subyacentes.
  • Del "primer plano" de la tarjeta -el borde translúcido y los textos- que se dibujan sobre ese fondo compuesto.

Esto es todo por esta entrega de skia en Compose. ¡Gracias por llegar hasta aquí!


Reactions

0

0

0

0

Access hereTo be able to comment

TheWhiteCode.com is not the creator or owner of the images shown, references are: