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のグラフィックレンダリングシステムやデータフロープロセスの中でどの位置にあるかを示す階層図(上から下への順序)です。(概念的な図なので、正確なシステムアーキテクチャではありません。)
はじめに
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で他にできることってこれだけ?
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()
結果
ボタンが明滅する後光を出して、関心を引き付けながら車のライトのような雰囲気を作り出せます。
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()
結果
このシェーダーがあれば、最小限のパフォーマンスコストでアプリの画像に深みやエレガントさを加えることができます。
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()
結果
このシェーダーでアプリのローディング画面が未来的でスタイリッシュになり、待ち時間が短く感じられてより楽しめるものになります。
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つのコードはここでは省略します)、ローエンドのデバイスでもスムーズに動作します。
さいごに
見た目が素晴らしく、高いインタラクティブ性と最適なパフォーマンスを備えたエフェクトがAndroid アプリで作成できる。AGSLはそんな多機能なツールです。UIコンポーネントの強化、高度な画像処理、プロシージャルグラフィックスの生成、アニメーションが多い場面でのパフォーマンス向上など、AGSLを使えばアプリが一段と際立ちます。
AGSLがあれば、可能性は創造力次第です。さっそく試して、アプリに命を吹き込みましょう!
関連記事 | Related Posts

Implementing Screenshot Testing in the Unlimited Android App Was Tougher Than Expected

When We Put Compose on Top of BottomSheetDialogFragment, Anchoring a Button to the Bottom Proved Harder Than Expected

Apollo Kotlinをv4にバージョンアップしました

Android Compose Object-Oriented Navigation

Transform Android UI with AGSL: Custom Shaders Made Easy

Unlimited Android Appにスクリーンショットテストを導入しようとしたら意外と大変だった話