KINTO Tech Blog
Android

AGSLでAndroid UIを変換するCustom Shaders を簡単に

Cover Image for AGSLでAndroid UIを変換するCustom Shaders を簡単に

はじめに

こんにちは!Yao Xieです。 KINTO Technologies のモバイルアプリ開発グループで、KINTO
かんたん申し込みアプリ
のAndroidアプリを開発しています。本記事では、AGSL(Android Graphics Shading Language)を活用してカスタムUIコンポーネントを向上したり、Androidアプリで高度な画像処理をする方法を紹介します。

AGSLとは

AGSL (Android Graphics Shading Language)は、Android 向けに設計されたGPUベースのシェーディング言語のことです。Skia Shading Language (SKSL)をベースにしたAGSLは、高度なグラフィックエフェクトを生みだせるAndroid特有の最適性を備えています。AGSLはAndroidのレンダリングパイプラインと完全に統合しているので、複雑なビジュアルエフェクトを効率良くスムーズに実装できます。

GLSLからSKSLへ、そしてAGSLへ

グラフィックスシェーディング言語は、現代のアプリで求められている、高品質なグラフィックへの需要に応えるために大きく進化してきました。簡単にまとめると:

  • GLSL(OpenGLシェーディング言語):
    • リジナルのシェーディング言語で、2D・3DグラフィックのレンダリングにOpenGLと併用されます。GLSLのおかげで、GPU上で動作するカスタムシェーダーを書き出すことができます。
  • SKSL(Skiaシェーディング言語):
    • Skiaグラフィックスライブラリの一部として導入されています。SKSL は 2DグラフィックをレンダリングするためにAndroidなどいろいろなプラットフォームで使われています。
  • AGSL(Android Graphics Shading Language):
    • Android用に特化してデザインしてあるシェーディング言語です。SKSLの機能をベースに、Androidのレンダリングパイプラインとスムーズに統合できるように調整してあります。

GLSL、SKSL、AGSLの主な違い

AGSLはモバイルデバイス向けに最適化されていて、GLSLよりもパフォーマンスが高く、消費電力が低いです。Androidレンダリングパイプラインと統合していることで、より効率良くグラフィックをレンダリングすることができます。

  • GLSL:
    • OpenGL用の、C言語に似た構文です。
    • クロスプラットフォーム対応ではあるものの、OpenGL ESのバリエーションの影響でAndroidでは制限があります。
  • SKSL:
    • GLSLに似ていますが、Skiaの2Dグラフィック向けに最適化してあります。
    • 主にSkia内部で使用されていて、Androidの直接的な開発ではあまり利用できません。
  • AGSL:
    • SKSLをベースにしつつ、 Android特有の向上性を備えています。
    • Androidのグラフィックパイプラインと完全に統合していて、最適なパフォーマンスを発揮します。

AGSLの仕組みは?

下の図は、AGSLシェーダー文字列がAndroidのグラフィックレンダリングシステムやデータフロープロセスの中でどの位置にあるかを示す階層図(上から下への順序)です。(概念的な図なので、正確なシステムアーキテクチャではありません。)

agsl_get_started

はじめに

Step 1:グラデーションシェーダを定義する

AGSLを使用して、テキストにのみ滑らかなグラデーションエフェクトを加えるシェーダーファイルを作成します。コンポーザブルインプットによって、グラデーションがテキストのアルファマスクにしっかりと適切に反映できます。

@Language("AGSL")
val gradientTextShader = """
  uniform float2 resolution; // Text size
  uniform float time;        // Time for animation
  uniform shader composable; // Input composable (text mask)

  half4 main(float2 coord) {
      // Normalize coordinates to [0, 1]
      float2 uv = coord / resolution;

      // Hardcoded gradient colors
      half4 startColor = half4(1.0, 0.65, 0.15, 1.0); // Orange
      half4 endColor = half4(0.26, 0.65, 0.96, 1.0);  // Blue

      // Linear gradient from startColor to endColor
      half4 gradientColor = mix(startColor, endColor, uv.x);

      // Optional: Add a subtle animation (gradient shifting)
      float shift = 0.5 + 0.5 * sin(time * 2.0);
      gradientColor = mix(startColor, endColor, uv.x + shift * 0.1);

      // Use the alpha from the input composable mask
      half4 textAlpha = composable.eval(coord);

      // Combine the gradient color with the composable alpha
      return gradientColor * textAlpha.a;
  }
""".trimIndent()

Step 2:シェーダ用Modifierを作成する

テキストにグラデーションシェーダを適用するカスタム Modifierを定義します。このシェーダは、ダイナミックなタイムパラメータを活用して、グラデーションをアニメーション化します。

fun Modifier.gradientTextEffect(): Modifier = composed {
    val shader = remember { RuntimeShader(gradientTextShader) }
    var time by remember { mutableStateOf(0f) }

    // Increment animation time
    LaunchedEffect(Unit) {
        while (true) {
            time += 0.016f // Simulate 60 FPS
            delay(16)
        }
    }

    this.graphicsLayer {
        shader.setFloatUniform("resolution", size.width, size.height)
        shader.setFloatUniform("time", time)

        renderEffect = RenderEffect
            .createRuntimeShaderEffect(shader, "composable")
            .asComposeRenderEffect()
    }
}

Step 3:シェーダをテキストコンポーネントに適用する

UIでModifier.gradientTextEffectを使用して、グラデーションエフェクトを適用します。

@Composable
fun GradientTextDemo() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "Gradient Text",
            fontSize = 36.sp,
            fontWeight = FontWeight.Bold,
            color = Color.White,
            modifier = Modifier.gradientTextEffect()
        )
    }
}

結果

agsl_get_started

AGSLで他にできることってこれだけ?

AGSLの機能は基本をはるかに超えて、ダイナミックで魅力的で、高パフォーマンスなアプリ体験を作り上げるのをサポートしてくれます。ここからは、AGSLでアプリをさらにレベルアップする方法を、実例をもとに探ってみましょう。

1.UIコンポーネントを強化

AGSLを使えば、ユーザーの心を捉えて、アプリの目的を際立たせる魅力的なUI要素を作成することができます。

  • アニメーションボーダー: カード、ボタン、または画像の周囲にマーキーエフェクトや点滅エフェクトを作成します。
  • カスタムグラデーション: ダイナミックに流れる、アニメーション化したGPUアクセラレーショングラデーションを実装すします。
  • Dynamic Glow エフェクト: ボタンやスライダーに、光るハイライト後光を追加します。

例:運転スキルのトレーニングアプリ

運転スキルのトレーニングアプリを開発していると想像してみてください。目標は、インターフェイスを視覚的に惹きつけるものにして、ユーザーが「トレーニング開始」ボタンのように大切な要素を操作できるようにすることです。AGSLがDynamic Glowエフェクトをどのように実現するのかをご紹介します。

AGSL シェーダコード:
@Language("AGSL")
val glowButtonShader = """
  // Shader for a glowing rounded rectangle button

  uniform shader button;              // Input texture or color for the button
  uniform float2 size;                // Button size
  uniform float cornerRadius;         // Corner radius of the button
  uniform float glowRadius;           // Radius of the glow effect
  uniform float glowIntensity;        // Intensity of the glow
  layout(color) uniform half4 glowColor; // Color of the glow

  // Signed Distance Function (SDF) for a rounded rectangle
  float calculateRoundedRectSDF(vec2 position, vec2 rectSize, float radius) {
      vec2 adjustedPosition = abs(position) - rectSize + radius; // Adjust for rounded corners
      return min(max(adjustedPosition.x, adjustedPosition.y), 0.0) 
             + length(max(adjustedPosition, 0.0)) - radius;
  }

  // Function to calculate glow intensity based on distance
  float calculateGlow(float distance, float radius, float intensity) {
      return pow(radius / distance, intensity); // Glow falls off as distance increases
  }

  half4 main(float2 coord) {
      // Normalize coordinates and aspect ratio
      float aspectRatio = size.y / size.x;
      float2 normalizedPosition = coord.xy / size;
      normalizedPosition.y *= aspectRatio;

      // Define normalized rectangle size and center
      float2 normalizedRect = float2(1.0, aspectRatio);
      float2 normalizedRectCenter = normalizedRect / 2.0;
      normalizedPosition -= normalizedRectCenter;

      // Calculate normalized corner radius and distance
      float normalizedRadius = aspectRatio / 2.0;
      float distanceToRect = calculateRoundedRectSDF(normalizedPosition, normalizedRectCenter, normalizedRadius);

      // Get the button's color
      half4 buttonColor = button.eval(coord);

      // Inside the rounded rectangle, return the button's original color
      if (distanceToRect < 0.0) {
        return buttonColor;
      }

      // Outside the rectangle, calculate glow effect
      float glow = calculateGlow(distanceToRect, glowRadius, glowIntensity);
      half4 glowEffect = glow * glowColor;

      // Apply tone mapping to the glow for a natural look
      glowEffect = 1.0 - exp(-glowEffect);

      return glowEffect;
  }
""".trimIndent()

結果

ボタンが明滅する後光を出して、関心を引き付けながら車のライトのような雰囲気を作り出せます。

https://youtube.com/shorts/CW1yBgJyDo4?rel=0

2.高度な画像処理を実行する

AGSLはリアルタイムの画像操作に優れていて、ダイナミックでインタラクティブなエフェクトを作ることができます。AGSL を使用すれば、GPUアクセラレーターを活かした高速な画像処理エフェクトを作成できます。

  • カスタムフィルタ: セピア、ピクセル化、ビネットなどアート風なエフェクトを追加します。
  • ダイナミックブラー: モーションブラーや被写界深度エフェクトなど、リアルタイムでぼかしを適用します。
  • カラー調整: UI上で明るさ、コントラスト、彩度をダイナミックに調整します。

例:画像の波紋エフェクト

アプリに月の画像があると想像してみてください。月が水面に映るような波紋エフェクトを追加して、もっとインタラクティブで関心を引くインターフェースにしたいなと思っているとします。

AGSL シェーダコード:
@Language("AGSL")
val rippleShader = """
    // Uniform variables: inputs provided from the outside
    uniform float2 size;       // The size of the canvas in pixels (width, height)
    uniform float time;        // The elapsed time for animating the ripple effect
    uniform shader composable; // The shader applied to the composable content being rendered
    
    // Main function: calculates the final color at a given fragment (pixel) coordinate
    half4 main(float2 fragCoord) {
        // Scale factor based on the canvas width for normalization
        float scale = 1 / size.x;
        
        // Normalize fragment coordinates
        float2 scaledCoord = fragCoord * scale;
        
        // Calculate the center of the canvas in normalized coordinates
        float2 center = size * 0.5 * scale;
        
        // Calculate the distance from the current fragment to the center
        float dist = distance(scaledCoord, center);
        
        // Calculate the direction vector from the center to the fragment
        float2 dir = scaledCoord - center;
        
        // Apply a sinusoidal wave based on the distance and time
        float sin = sin(dist * 70 - time * 6.28);
        
        // Offset coordinates by applying the wave effect in the direction of the fragment
        float2 offset = dir * sin;
        
        // Calculate the texture coordinates with the ripple effect applied
        float2 textCoord = scaledCoord + offset / 30;
        
        // Sample the composable shader using the adjusted texture coordinates
        return composable.eval(textCoord / scale);
    }
""".trimIndent()
結果

このシェーダーがあれば、最小限のパフォーマンスコストでアプリの画像に深みやエレガントさを加えることができます。

https://www.youtube.com/shorts/80QOTzNUHLg?rel=0

3. プロシージャルグラフィックを有効にする

プロシージャルグラフィックスは、視覚に訴えるようなインターフェースを静的なアセットに頼ることなく作成するのにピッタリです。

  • パターンの生成: ストライプ、グリッド、ノイズなどのプロシージャルテクスチャを作成します。
  • シェイプアニメーション: モーフィングシェイプや移動パターンをデザインします。
  • 3D風エフェクト: 奥行きや遠近感を、実際の3Dレンダリングなしで表現でき ます。

例:アニメーションローディング画面

ローディング画面は単調になりがちですが、AGSLを使うとダイナミックな芸術作品のように変身します。例えば、アプリの読み込み中にきらきらなアニメーション球体を表示して、ユーザーの目を引く演出が作成できます。

AGSL シェーダコード:
@Language("AGSL")
val lightBallShader = """
    uniform float2 size;       // The size of the canvas in pixels (width, height)
    uniform float time;        // The elapsed time for animating the light effect
    uniform shader composable; // Shader for the composable content
    
    half4 main(float2 fragCoord){
        // Initialize output color
        float4 o = float4(0.0);
        
        // Normalize coordinates relative to the canvas center
        float2 u = fragCoord.xy * 2.0 - size.xy;
        float2 s = u / size.y;

        //ライトボールエフェクトを計算するループ
        for (float i = 0.0; i < 180.0; i++) {
            float a = i / 90.0 - 1.0;                       // Calculate a normalized angle
            float sqrtTerm = sqrt(1.0 - a * a);            // Circular boundary constraint
            float2 p = cos(i * 2.4 + time + float2(0.0, 11.0)) * sqrtTerm; // Oscillation term
            
            // Compute position and adjust with distortion
            float2 c = s + float2(p.x, a) / (p.y + 2.0);
            
            // Calculate the distance factor (denominator)
            float denom = dot(c, c);
            
            // Add light intensity with color variation
            float4 cosTerm = cos(i + float4(0.0, 2.0, 4.0, 0.0)) + 1.0;
            o += cosTerm / denom * (1.0 - p.y) / 30000.0;
        }

        // Return final color with an alpha of 1.0
        return half4(o.rgb, 1.0);
    }
""".trimIndent()
結果

このシェーダーでアプリのローディング画面が未来的でスタイリッシュになり、待ち時間が短く感じられてより楽しめるものになります。

https://youtube.com/shorts/pUTU0KRmFek?rel=0

4. アプリのパフォーマンスを向上させる

AGSLはパフォーマンス重視な場面で力を発揮し、レンダリングタスクをGPUに任せることで、スムーズで効率的なアニメーションを実現します。

  • 効率的なアニメーション: 複雑なリアルタイムエフェクトをスムーズに処理します。
  • バッテリー最適化 消費電力を最小限に抑えつつ、目を見張るようなエフェクトを実現します。

例:マップビューでの天気アニメーション

プロダクトマネージャーから、マップビューに天気アニメーションのオーバーレイを追加するように頼まれたとします。従来の方法はパフォーマンス集約型ですが、GSLはCPUオーバーヘッドを最小限にしつつ、Androidの最適化したレンダリングパイプラインを活かして、効率よくGPUレンダリングができます。

雨のAGSLシェーダーコード:
@Language("AGSL")
val rainShader = """
    uniform float time;        // The elapsed time for animating the rain
    uniform float2 size;       // The size of the canvas in pixels (width, height)
    uniform shader composable; // Shader for the composable content

    // Generate a pseudo-random number based on input
    float random(float st) {
        return fract(sin(st * 12.9898) * 43758.5453123);
    }

    half4 main(float2 fragCoord) {
        // Normalize fragment coordinates to the [0, 1] range
        float2 uv = fragCoord / size;

        // Rain parameters
        float speed = 1.0;             // Speed of raindrops
        float t = time * speed;        // Time-adjusted factor for animation
        float density = 200.0;         // Number of rain "drops" per unit area
        float length = 0.1;            // Length of a raindrop
        float angle = radians(30.0);   // Angle of the rain (in degrees)
        float slope = tan(angle);      // Slope of the rain's trajectory

        // Compute grid position and animated raindrop position
        float gridPosX = floor(uv.x * density);
        float2 pos = -float2(uv.x * density + t * slope, fract(uv.y - t));

        // Calculate the raindrop visibility at this fragment
        float drop = smoothstep(length, 0.0, fract(pos.y + random(gridPosX)));

        // Background and rain colors
        half4 bgColor = half4(0.0, 0.0, 0.0, 0.0);  // Black transparent background
        half4 rainColor = half4(0.8, 0.8, 1.0, 1.0); // Light blue raindrop color

        // Blend the background and raindrop color based on drop visibility
        half4 color = mix(bgColor, rainColor, drop);

        return color; // Output the final color for the fragment
    }
""".trimIndent()
結果

このシェーダーは雨をリアルに再現できる上に、雲や雪にも対応でき(後者2つのコードはここでは省略します)、ローエンドのデバイスでもスムーズに動作します。

https://youtube.com/shorts/l63i3mQ_n2Y?rel=0

さいごに

見た目が素晴らしく、高いインタラクティブ性と最適なパフォーマンスを備えたエフェクトがAndroid アプリで作成できる。AGSLはそんな多機能なツールです。UIコンポーネントの強化、高度な画像処理、プロシージャルグラフィックスの生成、アニメーションが多い場面でのパフォーマンス向上など、AGSLを使えばアプリが一段と際立ちます。

AGSLがあれば、可能性は創造力次第です。さっそく試して、アプリに命を吹き込みましょう!

Facebook

関連記事 | Related Posts

イベント情報

P3NFEST Bug Bounty 2025 Winter 【KINTOテクノロジーズ協賛】