Custom Shaders en Flutter para divertirse un rato

Carlos Daniel
6 min readOct 17, 2023

Aquí puedes encontrar la versión en inglés.

En Flutter, por si no lo sabías, existe una interesante forma de renderizar objetos (además de los clásicos widgets y otras más). Los Custom Shaders, otra manera de renderizar objetos sin un uso exhaustivo de los clásicos widgets que usamos en nuestro día a día.

Para entender un poco, miremos a nivel general cómo es el proceso de renderización en Flutter. El objetivo final siempre es que en la pantalla se “pinte” exactamente lo que le digamos (pixeles en la pantalla). Cada pixel tiene asociado un color/textura. Esta parte está a cargo de la GPU con los llamados Fragment Shaders. En resumen y a muy alto nivel, son procesos en paralelo para poder “colorear” la pantalla pixel a pixel. Este proceso pasa desde la más simple app en Flutter como la app Counter básica hasta la más compleja UI que le indiquemos al framework que queremos mostrar.

Para que los shaders puedan hacer su labor, el API de Canvas, una capa súper abstracta que al traducir lo requerido por la capa de shaders permite hacer gráficos declarativos, como shapes o custom paths que serán “rellenados” de color. Todo el detalle en la documentación oficial con Canvas.

Basados en lo anterior, en esta gráfica resumen se muestra cómo desde un widget se llega a la capa de Canvas API (el camino clásico en el desarrollo con Flutter) o desde un custom painter, que a su vez requiere un CustomPainter widget.

Proceso de pintado de cada pixel en la pantalla

Y aprovechando el poder del paint del CustomPainter, le podemos decir que tenemos un shader indicando qué color usar en cada pixel, por tanto, si al shader le decimos qué hacer, y este a su vez se lo indica al paint, entonces el paintlo hará también. Y recordemos como mencionamos en el párrafo anterior, que la GPU puede ejecutar todo este proceso de los cientos, miles o millones de pixels en paralelo. Esto, porque es su propósito de existir.

Así, con los custom shader podremos tanto pintar desde un fondo de un color en particular hasta un mega fractal, pasando por gradientes, modificando texturas de una imagen y muchas cosas más.

Implementando Custom Shaders en Flutter

Según el título del artículo, le diremos entonces específicamente al CustomPainter que use shaders custom que le pasaremos. Estos shaders no están escritos en Dart, están escritos GLSL (Open GL Shading Languaje), y no es nuevo, lleva años en el mundo y es ampliamente usado en el mundo de la matemática y el diseño 2d y 3D entre otros.
Para el alcance de este escrito, no haremos nada desde ceros, al contrario, aprovecharemos muchos shaders escritos y publicados por otras personas, y los visualizaremos en una app Flutter.

Ahora, para tener en cuenta, la sintaxis de un shader que podemos usar en Flutter debe seguir los siguientes lineamientos básicos:

1. En un archivo .frag, debemos incluir el archivo flutter/runtime_effects.glsl, que contiene un amplio set de helpers específicos para Flutter.

#include <flutter/runtime_effect.glsl>

2. Declaramos los parámetros de entrada que nuestro shader recibirá desde nuestra app. Para ello, usamos la palabra uniform además del tipo de dato y luego el nombre de la variable.
Entonces, si queremos incluir una variable que indique el tiempo, o un color específico a usar, usamos: uniform float iTimeo uniform vec4 uColor respectivamente. En GLSL se usan para datos numéricos float, para colores vec4 que corresponde a un vector con el RGB más el alpha. uniform indica que la variable tendrá el mismo valor siempre para cada pixel calculado por el proceso.

uniform vec2 uSize;
uniform float iTime;

out vec4 fragColor;

3. Declaramos la variable que el shader retornará. Siempre debe ser un vec4 que será un color de tipo RGB mas el alpha, y por convención suele usarse fragColor como nombre.

4. El shader debe tener un método main() y su única tarea es asignarle un valor a la variable que retornaremos.

out vec4 fragColor;

void main() {
// do something to assign fragColor a value
// ...
fragColor = vec4(finalColor, 1.0);
}

Luego de tener el archivo del shader, para que Flutter pueda reconocerlo, debe definirse bajo la sección shaders: del archivo pubspec.yaml, con la ruta definida de su ubicación. En este caso lo tenemos en:

shaders:
- shaders/art_fractal.frag

Este shader en particular, lo hemos tomado de ShaderToy, una comunidad y herramienta usada para crear y compartir shaders. Nos basaremos en shaders ya creados (si bien nosotros podemos crear lo nuestros o modificar alguno ya existente) y los ajustaremos para que puedan visualizarse desde nuestra app.

Cómo visualizamos estos shaders en nuestra app?

Para nuestro escenario, tenemos en nuestra app un widget stateful GeneralShaderPage, que recibe el path del shader a mostrar. Dicho shader, para el ejemplo en mención con el fractal, recibe como parámetro de entrada el tamaño de la pantalla (un vec2 con width y el height), una variable de tiempo (el segundo, o milisegundo) y retornará el vec4 con el color del pixel aplicado.

Inicializamos el shader con `_loadShader()`, que al final no es más que cargarlo como un asset y se lo enviamos a GeneralPainterque es nuestro CustomPainter que se encargará de pintar cada pixel de la forma en que se lo definimos en el archivo .frag
Lo más importante de nuestro CustomPainter es que en el método paintal shader le seteamos las variables que este recibe (tamaño y tiempo) y luego al Canvas que se pintará, le entregamos este objeto con dichas configuraciones:

void paint(Canvas canvas, Size size) {
final paint = Paint();
shader.setFloat(0, size.width);
shader.setFloat(1, size.height);
shader.setFloat(2, time);
paint.shader = shader;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
}

Tengamos en cuenta que la variable de entrada tamaño es un vec2 con el width y el height:

shader.setFloat(0, size.width);
shader.setFloat(1, size.height);

Y que la variable tiempo (en un momento dado, el pixel tendrá un color en particular) es un float.

Desde el main page de la app llamamos nuestro widget GeneralShaderPageseteando sus argumentos requeridos y el resultado final es esta belleza:

Ya el resto, es jugar con otros shaders diferentes que podríamos referenciar de ShaderToy, o implementando los nuestros propios. Y si por ejemplo, un shader dado requiere una imagen como argumento de entrada, a la que queramos aplicarle algún efecto nosotros directamente como un blur o pixelarla, debemos entonces hacerlo enviándola como imagen al CustomPainter, seteandola en el shader antes de ponerla en el Canvas y recibiendola en el .frag del shader:

void paint(Canvas canvas, Size size) {
for (int i = 0; i < images.length; i++) {
shader.setImageSampler(i, images[i]!);
}
shader.setFloat(0, size.width);
shader.setFloat(1, size.height);
for (int i = 0; i < uniforms.length; i++) {
shader.setFloat(i + 2, uniforms[i]);
}
final paint = Paint();
paint.shader = shader;
canvas.drawRect(Offset.zero & size, paint);
}

Y el resultado para un blur tipo water drop es algo como esto:

Para ver más detalles de esta implementación, en este repositorio, puedes encontrarlo además de los shaders de ShaderToy que usé y el resultado final.

Para cerrar

Puedes marcarlo con estrellita, modificarlo o agregar nuevos shaders interesantes, por ejemplo que reciban la posición X,Y en la pantalla del tap o click del mouse para que reciba el movimiento y pueda rotarse. Hay muchas cosas por hacer diferentes y cool.

Referencias:

Playing with paths in Flutter

Creating Custom Shaders

--

--

Carlos Daniel

Android & Flutter Developer. GDE for Android & Mobile Engineer.