Cómo usar AGSL en tu app para Android

En esta página, se abordan los conceptos básicos de AGSL y las diferentes maneras de usarlo en tu app para Android.

Un sombreador AGSL simple

Se llama a tu código de sombreador para cada píxel dibujado y muestra el color con el que se debería pintar el píxel. Un sombreador extremadamente simple es aquel que siempre muestra un solo color; en este ejemplo, se usa el rojo. El sombreador se define dentro de un String.

Kotlin

private const val COLOR_SHADER_SRC =
   """half4 main(float2 fragCoord) {
      return half4(1,0,0,1);
   }"""

Java

private static final String COLOR_SHADER_SRC =
   "half4 main(float2 fragCoord) {\n" +
      "return half4(1,0,0,1);\n" +
   "}";

El siguiente paso es crear un objeto RuntimeShader inicializado con tu string de sombreador. Esto también compila el sombreador.

Kotlin

val fixedColorShader = RuntimeShader(COLOR_SHADER_SRC)

Java

RuntimeShader fixedColorShader = new RuntimeShader(COLOR_SHADER_SRC);

Se puede usar RuntimeShader en cualquier lugar donde haya un sombreador estándar de Android. Por ejemplo, puedes usarlo para dibujar en un View personalizado usando un Canvas.

Kotlin

val paint = Paint()
paint.shader = fixedColorShader
override fun onDrawForeground(canvas: Canvas?) {
   canvas?.let {
      canvas.drawPaint(paint) // fill the Canvas with the shader
   }
}

Java

Paint paint = new Paint();
paint.setShader(fixedColorShader);
public void onDrawForeground(@Nullable Canvas canvas) {
   if (canvas != null) {
      canvas.drawPaint(paint); // fill the Canvas with the shader
   }
}

Esto dibuja un View rojo. Puedes usar un uniform para pasar un parámetro de color al sombreador que se dibujará. Primero, agrega el color uniform al sombreador:

Kotlin

private const val COLOR_SHADER_SRC =
"""layout(color) uniform half4 iColor;
   half4 main(float2 fragCoord) {
      return iColor;
   }"""

Java

private static final String COLOR_SHADER_SRC =
   "layout(color) uniform half4 iColor;\n"+
      "half4 main(float2 fragCoord) {\n" +
      "return iColor;\n" +
   "}";

Luego, llama a setColorUniform desde tu View personalizado para pasar el color deseado al sombreador AGSL.

Kotlin

fixedColorShader.setColorUniform("iColor", Color.GREEN )

Java

fixedColorShader.setColorUniform("iColor", Color.GREEN );

Ahora, obtienes un View verde; el color View se controla con un parámetro del código en tu View personalizado, en lugar de incorporarse en el sombreador.

En su lugar, puedes crear un efecto de gradiente de color. Primero deberás cambiar el sombreador para que acepte la resolución View como entrada:

Kotlin

private const val COLOR_SHADER_SRC =
"""uniform float2 iResolution;
   half4 main(float2 fragCoord) {
      float2 scaled = fragCoord/iResolution.xy;
      return half4(scaled, 0, 1);
   }"""

Java

private static final String COLOR_SHADER_SRC =
   "uniform float2 iResolution;\n" +
      "half4 main(float2 fragCoord) {\n" +
      "float2 scaled = fragCoord/iResolution.xy;\n" +
      "return half4(scaled, 0, 1);\n" +
   "}";

Cómo dibujar el gradiente

Este sombreador hace algo un poco elaborado. Para cada píxel, crea un vector float2 que contiene las coordenadas x e y divididas por la resolución, lo que creará un valor entre cero y uno. Luego, usa ese vector ajustado para construir los componentes rojo y verde del color que se muestra.

Para pasar la resolución del View a un sombreador de AGSL uniform, llama a setFloatUniform.

Kotlin

val paint = Paint()
paint.shader = fixedColorShader
override fun onDrawForeground(canvas: Canvas?) {
   canvas?.let {
      fixedColorShader.setFloatUniform("iResolution", width.toFloat(), height.toFloat())
      canvas.drawPaint(paint)
   }
}

Java

Paint paint = new Paint();
paint.setShader(fixedColorShader);
public void onDrawForeground(@Nullable Canvas canvas) {
   if (canvas != null) {
      fixedColorShader.setFloatUniform("iResolution", (float)getWidth(), (float()getHeight()));
      canvas.drawPaint(paint);
   }
}
Gradiente de rojo y verde
gradiente de rojo y verde

Cómo animar el sombreador

Puedes usar una técnica similar para animar el sombreador modificándolo para recibir uniformes iTime y iDuration. El sombreador usará estos valores a fin de crear una onda triangular para los colores, lo que hará que fluyan entre los valores de sus gradientes.

Kotlin

private const val DURATION = 4000f
private const val COLOR_SHADER_SRC = """
   uniform float2 iResolution;
   uniform float iTime;
   uniform float iDuration;
   half4 main(in float2 fragCoord) {
      float2 scaled = abs(1.0-mod(fragCoord/iResolution.xy+iTime/(iDuration/2.0),2.0));
      return half4(scaled, 0, 1.0);
   }
"""

Java

private static final float DURATION = 4000f;
private static final String COLOR_SHADER_SRC =
   "uniform float2 iResolution;\n"+
   "uniform float iTime;\n"+
   "uniform float iDuration;\n"+
   "half4 main(in float2 fragCoord) {\n"+
      "float2 scaled = abs(1.0-mod(fragCoord/iResolution.xy+iTime/(iDuration/2.0),2.0));\n"+
      "return half4(scaled, 0, 1.0);\n"+
   "}";

Desde el código fuente de la vista personalizada, un ValueAnimator actualiza el uniforme de iTime.

Kotlin

// declare the ValueAnimator
private val shaderAnimator = ValueAnimator.ofFloat(0f, DURATION)

// use it to animate the time uniform
shaderAnimator.duration = DURATION.toLong()
shaderAnimator.repeatCount = ValueAnimator.INFINITE
shaderAnimator.repeatMode = ValueAnimator.RESTART
shaderAnimator.interpolator = LinearInterpolator()

animatedShader.setFloatUniform("iDuration", DURATION )
shaderAnimator.addUpdateListener { animation ->
    animatedShader.setFloatUniform("iTime", animation.animatedValue as Float )
}
shaderAnimator.start()

Java

// declare the ValueAnimator
private final ValueAnimator shaderAnimator = ValueAnimator.ofFloat(0f, DURATION);

// use it to animate the time uniform
shaderAnimator.setDuration((long)DURATION);
shaderAnimator.setRepeatCount(ValueAnimator.INFINITE);
shaderAnimator.setRepeatMode(ValueAnimator.RESTART);
shaderAnimator.setInterpolator(new LinearInterpolator());

animatedShader.setFloatUniform("iDuration", DURATION );
shaderAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
   public final void onAnimationUpdate(ValueAnimator animation) {
      animatedShader.setFloatUniform("iTime", (float)animation.getAnimatedValue());
   }
});
Gradiente animado de rojo y verde
gradiente animado de rojo y verde

Pinta objetos complejos

No es necesario que dibujes el sombreador para rellenar el fondo; puedes usarlo en cualquier lugar que acepte un objeto Paint, como drawText.

Kotlin

canvas.drawText(ANIMATED_TEXT, TEXT_MARGIN_DP, TEXT_MARGIN_DP + bounds.height(),
   paint)

Java

canvas.drawText(ANIMATED_TEXT, TEXT_MARGIN_DP, TEXT_MARGIN_DP + bounds.height(),
   paint);
Texto con gradiente animado en rojo y verde
Texto gradiente animado de rojo y verde

Transformaciones de sombreado y lienzo

Puedes aplicar transformaciones Canvas adicionales en tu texto sombreado, como la rotación. En ValueAnimator, puedes actualizar una matriz para rotaciones 3D mediante la clase android.graphics.Camera integrada.

Kotlin

// in the ValueAnimator
camera.rotate(0.0f, animation.animatedValue as Float / DURATION * 360f, 0.0f)

Java

// in the ValueAnimator
camera.rotate(0.0f, (Float)animation.getAnimatedValue() / DURATION * 360f, 0.0f);

Dado que deseas rotar el texto desde el eje central en lugar de la esquina, obtén los límites del texto y, luego, usa preTranslate y postTranslate para modificar la matriz a fin de traducirlo de modo que 0,0 sea el centro de la rotación sin cambiar la posición en la que se dibuja el texto en la pantalla.

Kotlin

linearColorPaint.getTextBounds(ANIMATED_TEXT, 0, ANIMATED_TEXT.length, bounds)
camera.getMatrix(rotationMatrix)
val centerX = (bounds.width().toFloat())/2
val centerY = (bounds.height().toFloat())/2
rotationMatrix.preTranslate(-centerX, -centerY)
rotationMatrix.postTranslate(centerX, centerY)
canvas.save()
canvas.concat(rotationMatrix)
canvas.drawText(ANIMATED_TEXT, 0f, 0f + bounds.height(), paint)
canvas.restore()

Java

linearColorPaint.getTextBounds(ANIMATED_TEXT, 0, ANIMATED_TEXT.length(), bounds);
camera.getMatrix(rotationMatrix);
float centerX = (float)bounds.width()/2.0f;
float centerY = (float)bounds.height()/2.0f;
rotationMatrix.preTranslate(-centerX, -centerY);
rotationMatrix.postTranslate(centerX, centerY);
canvas.save();
canvas.concat(rotationMatrix);
canvas.drawText(ANIMATED_TEXT, 0f, 0f + bounds.height(), paint);
canvas.restore();
Texto con gradiente animado rotativo de rojo y verde
Texto con gradiente animado rotativo de rojo y verde

Cómo usar RuntimeShader con Jetpack Compose

Es aún más fácil usar RuntimeShader si renderizas tu IU con Jetpack Compose. Comienza con el mismo sombreador de gradientes de antes:

private const val COLOR_SHADER_SRC =
    """uniform float2 iResolution;
   half4 main(float2 fragCoord) {
   float2 scaled = fragCoord/iResolution.xy;
   return half4(scaled, 0, 1);
}"""

Puedes aplicar ese sombreador a un ShaderBrush. Luego, usa ShaderBrush como parámetro para los comandos de dibujo dentro del alcance de dibujo de tu Canvas.

// created as top level constants
val colorShader = RuntimeShader(COLOR_SHADER_SRC)
val shaderBrush = ShaderBrush(colorShader)

Canvas(
   modifier = Modifier.fillMaxSize()
) {
   colorShader.setFloatUniform("iResolution",
   size.width, size.height)
   drawCircle(brush = shaderBrush)
}
Círculo gradiente de AGSL Compose
Círculo gradiente rojo y verde

Cómo usar RuntimeShader con RenderEffect

Puedes usar RenderEffect para aplicar un RuntimeShader a un View superior y a todas las vistas secundarias. Esto es más costoso que dibujar un View personalizado. pero te permite crear fácilmente un efecto que incorpora lo que se habría dibujado originalmente con createRuntimeShaderEffect.

Kotlin

view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(myShader, "background"))

Java

view.setRenderEffect(RenderEffect.createRuntimeShaderEffect(myShader, "background"));

El segundo parámetro es el nombre de un sombreador uniforme que puedes usar en eval con un parámetro de coordenadas (como el que se pasó en fragCoord) para obtener el color original de RenderNode (la vista y sus vistas secundarias), lo que te permite realizar todo tipo de efectos.

uniform shader background;       // Root node of View tree to be altered
return mix(returnColor, background.eval(fragCoord), 0.5);
Botón de cuadrícula combinada
Botón de cuadrícula de AGSL mezclado

Efecto de cuadrícula mezclado sobre un botón, pero debajo de un botón de acción flotante (ya que está en una jerarquía View diferente).