Używanie AGSL w aplikacji na Androida

Na tej stronie omawiamy podstawy AGSL i różne sposoby ich używania w aplikacji na Androida.

Prosty program do cieniowania AGSL

Kod programu do cieniowania jest wywoływany dla każdego narysowanego piksela i zwraca kolor, którym piksel powinien być pomalowany. Bardzo prosty cienier to taki, który zawsze zwraca jeden kolor. W tym przykładzie użyto czerwonego. Program do cieniowania jest zdefiniowany w elemencie 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" +
   "}";

Następnym krokiem jest utworzenie obiektu RuntimeShader zainicjowanego przez ciąg znaków cieniowania. Spowoduje to również skompilowanie programu do cieniowania.

Kotlin

val fixedColorShader = RuntimeShader(COLOR_SHADER_SRC)

Java

RuntimeShader fixedColorShader = new RuntimeShader(COLOR_SHADER_SRC);

Elementu RuntimeShader można używać wszędzie tam, gdzie można korzystać ze standardowego narzędzia do cieniowania Androida. Możesz na przykład użyć jej do rysowania w niestandardowym elemencie View przy użyciu 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
   }
}

Generuje się czerwony znak View. Możesz użyć metody uniform, aby przekazać do cieniowania parametr koloru, który ma zostać narysowany. Najpierw dodaj kolor uniform do cieniowania:

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" +
   "}";

Następnie wywołaj setColorUniform z niestandardowego elementu View, aby przekazać wybrany kolor do cieniowania AGSL.

Kotlin

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

Java

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

Teraz uzyskujesz zielony kolor View. Kolorem View steruje się za pomocą parametru z kodu w niestandardowym elemencie View, zamiast być osadzony w cieniowaniu.

Zamiast tego możesz utworzyć efekt gradientu kolorów. Najpierw musisz zmienić cieniowanie, aby akceptować rozdzielczość View jako dane wejściowe:

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" +
   "}";

Rysowanie gradientu

Ten program do cieniowania jest nieco fantazyjny. Dla każdego piksela tworzy się wektor float2 zawierający współrzędne x i y podzielone przez rozdzielczość, co da wartość z zakresu od 0 do 1. Następnie używa przeskalowanego wektora do utworzenia czerwonego i zielonego składu zwracanego koloru.

Przekazujesz rozdzielczość View do cieniowania AGSL uniform, wywołując metodę 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);
   }
}
Czerwony i zielony gradient
Czerwony i zielony gradient

Animowanie programu do cieniowania

Możesz użyć podobnej metody do animowania cieniowania, modyfikując go, aby uzyskać stroje iTime i iDuration. cieniowanie użyje tych wartości, aby utworzyć trójkątną falę dla kolorów, co spowoduje, że będą się one zmieniać w obu wartościach gradientu.

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"+
   "}";

Na podstawie kodu źródłowego widoku niestandardowego ValueAnimator aktualizuje uniform 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());
   }
});
Animowany gradient czerwony i zielony
Animowany gradient czerwony i zielony

Malowanie złożonych obiektów

Nie musisz rysować programu do cieniowania, aby wypełnić tło. Możesz go używać w dowolnym miejscu, które akceptuje obiekt Paint, np. 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);
Czerwono-zielony animowany tekst gradientu
Animowany tekst gradientu w kolorze czerwonym i zielonym

Przekształcenia cieniowania i obszaru roboczego

Do zacienionego tekstu możesz zastosować dodatkowe przekształcenia Canvas, np. obrót. W ValueAnimator możesz zaktualizować macierz dla obrotów 3D za pomocą wbudowanej klasy android.graphics.Camera.

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);

Chcesz obracać tekst od osi środkowej, a nie od rogu, ustaw progi tekstu, a następnie użyj preTranslate i postTranslate, aby zmienić macierz tak, aby przetłumaczył tekst, tak aby 0,0 był środkiem obrotu, ale nie zmieniono pozycji jego rysowania na ekranie.

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();
Czerwono-zielony obracający się animowany tekst gradientu
Czerwony i zielony obracający się animowany tekst gradientu

Używanie RuntimeShadera z Jetpack Compose

Używanie RuntimeShader jest jeszcze łatwiejsze, jeśli renderujesz interfejs za pomocą Jetpack Compose. Zaczynam od tego samego cienia gradientowego co wcześniej:

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

Możesz zastosować go do ShaderBrush. Możesz następnie używać ShaderBrush jako parametru poleceń rysowania w zakresie rysowania dostępnym w 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)
}
Utwórz koło gradientowe AGSL
Czerwono-zielone kółko gradientu

Używanie RuntimeShader z funkcją RenderEffect

Za pomocą RenderEffect możesz zastosować RuntimeShader do nadrzędnych View i wszystkich widoków podrzędnych. To droższe niż rysowanie niestandardowego elementu View. ale pozwala łatwo utworzyć efekt obejmujący to, co zostało wcześniej narysowane za pomocą createRuntimeShaderEffect.

Kotlin

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

Java

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

Drugi parametr to nazwa jednolitego programu do cieniowania, który możesz eval z parametrem współrzędnych (takim jak przekazywany we fragCoord), aby uzyskać pierwotny kolor obiektu RenderNode (widoku danych i jego widoków podrzędnych), co umożliwia wykonywanie różnego rodzaju efektów.

uniform shader background;       // Root node of View tree to be altered
return mix(returnColor, background.eval(fragCoord), 0.5);
Przycisk przenikania siatki nad przyciskiem
Zmieszczona siatka AGSL nad przyciskiem

Efekt siatki zmieszany nad przyciskiem, ale pod pływającym przyciskiem polecenia (ponieważ znajduje się on w innej hierarchii View).