大师级 Compose 图形编程—AGSL 入门

随着 Android 12L (API 32) 的发布,Google 引入了一项强大的图形技术 ------ AGSL (Android Graphics Shading Language)。

AGSL 为使用 Jetpack Compose 构建动态、高性能、视觉震撼的 UI 效果打开了全新的大门。如果你曾经对 GLSLHLSL 或其他着色语言有所耳闻,那么 AGSL 将是你在 Android 原生开发中实现复杂视觉效果的瑞士军刀,不,是屠龙宝刀!

本篇博客将带你由浅入深、层层递进地掌握 AGSL 的基本概念,并结合 Compose 实现一个简单案例,助你拿到这张大师级图形编程的入场券。

为什么需要 AGSL?

AGSL 出现之前,Android 开发者若想实现复杂的图形效果(如模糊、渐变动画、像素化、斜体文字等),通常有以下几种选择:

  1. Bitmap 操作:在主线程或后台线程中操作 Bitmap。该操作性能差,如果在主线程还容易导致卡顿。因为操作实际上是在 CPU 上进行的,很多小伙伴可能认为图形相关的操作都是在 GPU 上运行的,其实不是的,只要是通过代码实现的逻辑,就是在 CPU 上运行的。
  2. OpenGL / Vulkan :功能强大,但学习曲线陡峭,代码复杂,与 Compose 集成困难。很多游戏引擎都是采用的这种方法,几乎是纯 OpenGL 方案。
  3. 第三方库:如 RenderScript(已废弃)、Lottie (矢量动画库,结合设计师做矢量动画,非常好用)或 SVGA。但灵活性有限,适用的场景也比较局限。

那么,AGSL 的出现解决了哪些痛点:

  • 原生集成:专为 Android 设计,与 CanvasShaderBrush 深度集成。
  • 高性能:运行在 GPU 上,效率远超 CPU 计算。
  • 简单易用:语法类似 GLSL,但更轻量,无需管理上下文或缓冲区。
  • Compose 友好:可直接在 CanvasModifier.graphicsLayer 中使用。

你的第一个 Shader

AGSL 是一种类 C 语言,用于编写运行在 GPU 上的小程序(Shader )。它的核心是 main 函数,接收输入并返回颜色:

c 复制代码
half4 main(float2 fragCoord) {
    return half4(1.0, 0.0, 0.0, 1.0); // 红色
}

half4 main(float2 fragCoord) 是一个 main 函数,也就是该 Shader 的入口函数,每一次像素的计算,都是从这个函数开始的。

该函数接受一个输入,类型是 float2float2 表示两个浮点数,也就是一个包含两个浮点数的数组。同理 float3 表示三个浮点数,float4 表示四个浮点数,float5 表示......没有 float5,哈哈!一般情况下,数组个数只能支持到 4

官方称法是矢量,不过这里我们理解称数组,逻辑是一样的。

如果你只需要一个 float,那就是 float 类型,没有 float1

该函数会返回一个 half4,也就是 4half 的数组。

half 表示中等精度的浮点数,通常是 16 位,也就是 2 个字节。而 float 是高精度浮点数,32 位,也就是 4 个字节。

同理,还有一些其他类型:int/int2/int3/int4short/short2/short3/short4等。

Shader 的作用是:返回 fragCoord 对应的像素坐标的颜色。此时此刻,我们返回的是红色。

half4(1.0, 0.0, 0.0, 1.0) 表示颜色的 rgba 分量,也就是颜色的红色,绿色,蓝色,透明度分量,取值范围是 [0-1]

使用 uniform

uniform 是在 Shader 外部设置、在 Shader 内部保持不变的变量。

c 复制代码
uniform half4 uColor; // uniform 颜色

half4 main(float2 fragCoord) {
    return uColor;
}

此时我们可以暂时理解为,uColor 针对该函数是静态变量,也就是在运行期间 uColor 是全局可访问的。

因为 main 函数是无法更改其输入输出的。它只能接收一个像素坐标,然后返回该坐标的颜色。如果我们想告知 Shader 其他信息,例如时间、尺寸等,我们就需要使用 uniform 传递进去。


好的,Shader 的入门就到这里了,接下来,我们举几个例子实战一下。

Compose 实战

AGSL 本身无法直接绘制内容,它需要通过 RuntimeShaderShaderBrush 集成到 Compose 中。

定义 AGSL 代码

我们就用上面用到的 AGSL 代码:

Kotlin 复制代码
val simpleShader = """
    half4 main(float2 fragCoord) {
        return half4(1.0, 0.0, 0.0, 1.0); // 红色
    }
""".trimIndent()

第一步我们需要将 AGSL 存入到 Kotlin 的 String 中。

创建 RuntimeShader

kotlin 复制代码
val shader = remember { RuntimeShader(simpleShader) }

这一步非常简单,就是将这段字符串传递给 RuntimeShader 构造函数即可。考虑到 Compose 的重组问题,所以这里我们需要 remember,防止 Compose 重组的时候创建新的 RuntimeShader

使用 Canvas 绘制

Kotlin 复制代码
val shaderBrush = remember {
    ShaderBrush(shader)
}

Canvas(
    modifier = Modifier.size(200.dp)
) {
    drawRect(shaderBrush)
}

使用 Canvas 绘制的时候,需要使用 ShaderBrush 来应用 RuntimeShader

稍安勿躁,这里概念突然变多了,我们花点时间解释一下这里的工作原理。

Canvas 实际上只是一张画布,他可以绘制任何东西,与 Shader 没有什么关系。

ShaderBrush 是一种笔刷,这个笔刷的如何绘制内容是通过 RuntimeShader 去定义的。

RuntimeShader 如何绘制像素,是通过 AGSL 去定义的。

现在,我们要在 Canvas 上绘制一个正方形,就需要使用 drawRectdrawRect 方法需要一个笔刷 ------ Brush。因为此处笔刷的实现我们需要使用 AGSL 去定义绘制逻辑,所以使用了 Brush 的一个实现之一 ShaderBrush,而ShaderBrush,需要 RuntimeShader,于是顺理成章。

完整代码如下,其实非常简单:

Kotlin 复制代码
// simpleShader 是一个全局变量,不存在于 Composable 函数中。
val simpleShader = """
    half4 main(float2 fragCoord) {
        return half4(1.0, 0.0, 0.0, 1.0); // 红色
    }
""".trimIndent()

//...

val shader = remember { RuntimeShader(simpleShader) } 

val shaderBrush = remember {
    ShaderBrush(shader)
}

Canvas(
    modifier = Modifier.size(200.dp)
) {
    drawRect(shaderBrush)
}
//...

一睹为快,看看效果:

Nothing less, Nothing more.

一个简单的红色正方形。

渐变

好的,我们已经知道如何使用 AGSL 了,接下来,我们给这个纯色加一点不一样的效果,渐变。

我们修改一下 AGSL 代码:

c 复制代码
uniform float2 iResolution; // 多了一个 float2 uniform 类型,这里用来表示画布大小
half4 main(float2 fragCoord) {
    float gradient = fragCoord.x / iResolution.x;
    return half4(gradient, gradient, gradient, 1.0); // 红色
}

这里多加了一个 uniform 类型,用于存储画布的大小,fragCoord.x / iResolution.x 会计算像素在 x 轴上的比率,靠左的像素,会接近 0,靠右的像素,会接近 1

先说一下 AGSL 的坐标系:

AGSLView 的坐标系保持一致,左上角是原点,x 轴和 y 轴的正方向分别向右、向下。

那么上述代码,会形成从左往右,从 01 的渐变,也就是颜色上从黑色到白色。

Kotlin 复制代码
val shader = remember { RuntimeShader(simpleShader) }

val shaderBrush = remember {
    ShaderBrush(shader)
}

Canvas(
    modifier = Modifier.size(200.dp)
) {
    shader.setFloatUniform("iResolution", size.width, size.height)
    drawRect(shaderBrush)
}

仔细看是如何给 iResolution 赋值的,RuntimeShader 有多个设置 Uniform 的方法,这里 iResolutionfloat2 类型,所以使用 setFloatUniform,并传递两个 float 值,分别是画布的宽高。

我们看下效果:


关于元素的访问,我们这里使用了 xyzw------矢量方式进行访问,实际上 AGSL 也支持 rgba 的颜色访问方式。

以下表达式是等价的:

  • fragCoord.x / iResolution.x
  • fragCoord.r / iResolution.x
  • fragCoord.r / iResolution.r
  • fragCoord.x / iResolution.r

如果你想同时访问多个元素,就是:fragCoord.xyfragCoord.yzfragCoord.xyz

你甚至可以调换顺序:fragCoord.zxyfragCoord.wzy

这里我们可以用到的时候再说,大家先有个概念即可。

与 graphicsLayer 共舞

我们最后一个案例,将与 Modifier.graphicsLayer 进行结合。

我们先使用一个普通的组件 Image 显示一张普通的图片:

Kotlin 复制代码
Image(
    painter = painterResource(R.drawable.bkg_naruto),
    contentDescription = null,
)

我们现在希望实现一个效果,就是将这个图片灰度化,也就是只有黑白之间的过渡,没有其他颜色的情况。

灰度化其实就是颜色的色值 rgb 三个分量是一样的。

我们先编写 AGSL

c 复制代码
uniform shader image; // 图片
half4 main(float2 fragCoord) {
    half4 sample = image.eval(fragCoord); // 对图片进行采样
    float gray = (sample.r + sample.g + sample.b) / 3.0; // 计算平均分量
    return half4(gray, gray, gray, 1.0); // 灰度化
}

uniform shader image 定义了一个 shader 类型的全局变量。shader 类型就是一张纹理,这里我们直接理解为当前的显示效果。如果组件是 Image,显示效果就是当前的图片,如果组件是 Text,显示效果就是文字。

image.eval(fragCoord) 会对纹理进行采样,也就是获取 image 在坐标 fragCoord 的颜色色值,返回一个 half4 类型。

当我们拿到这个颜色色值的时候,就可以直接进行运算,返回平均分量了,在最后,将每个颜色分量,都设置为这个平均分量,我们的图片灰度化,就成功了!

注意看代码中,因为我们把 sample 当做颜色来对待,所以是用了 rgba 的访问方式,实际上如果用 xyzw 是一样的,但是 rgba 会更加直观,可读性高!

接下来,就是结合 graphicsLayer 使用了:

Kotlin 复制代码
val shader = remember {
    RuntimeShader(grayShader)
}

Image(
    painter = painterResource(R.drawable.bkg_naruto),
    contentDescription = null,
    modifier = Modifier.graphicsLayer {
        renderEffect = RenderEffect.createRuntimeShaderEffect(
            shader,"image"
        ).asComposeRenderEffect() // 注意这里的使用方法,与 Canvas 不一样
    },
)

我们依然是先创建一个 RuntimeShader,之后在 Modifier.graphicsLayer 中,需要设置 renderEffect 来使用该效果。

RenderEffect.createRuntimeShaderEffect 会通过 RuntimeShader 创建一个 RenderEffect,但是 RenderEffect 并不能直接在 Compose 中使用,不过转换也很简单,asComposeRenderEffect 即可。

注意 createRuntimeShaderEffect 这个方法,第一个参数是我们创建的 RuntimeShader,第二个参数我们给入了 image,这个与 AGSLuniform shader imageimage 是一致的,表示将当前的纹理设置给 image

官方文档对于 createRuntimeShaderEffect 第二个参数的注释:the uniform name defined in the RuntimeShader's program to which the contents of the RenderNode will be bound.

好,所有工作完毕,我们运行,看看效果:

完美,收工!

总结

AGSL 是 Android 图形编程的一次重大进化。它让原本复杂的 GPU 编程变得触手可及。通过本篇博客的学习,你已经掌握了从基础语法到实战应用的完整链条。

后续我会继续给大家分享关于 AGSL 与 Compose 结合的神奇用法,点个关注,不迷路!

AGSL 参考:developer.android.com/develop/ui/...


欢迎各位关注公众号,文章同步更新:

相关推荐
用户2018792831674 小时前
ANR之RenderThread不可中断睡眠state=D
android
煤球王子4 小时前
简单学:Android14中的Bluetooth—PBAP下载
android
phoneixsky4 小时前
Kotlin的各种上下文Receiver,到底怎么个事
kotlin
小趴菜82274 小时前
安卓接入Max广告源
android
齊家治國平天下4 小时前
Android 14 系统 ANR (Application Not Responding) 深度分析与解决指南
android·anr
ZHANG13HAO4 小时前
Android 13.0 Framework 实现应用通知使用权默认开启的技术指南
android
heeheeai4 小时前
okhttp使用指南
okhttp·kotlin·教程
【ql君】qlexcel4 小时前
Android 安卓RIL介绍
android·安卓·ril
写点啥呢4 小时前
android12解决非CarProperty接口深色模式设置后开机无法保持
android·车机·aosp·深色模式·座舱
IT酷盖4 小时前
Android解决隐藏依赖冲突
android·前端·vue.js