本文译自「First look at RuntimeShaders in Compose」,原文链接medium.com/@off.mind.b...,由Alex Volkov发布于2024年4月12日。

自从我们有机会在 Compose 中使用 RuntimeShaders 自定义Shader(着色器)以来,已经过去了一年多的时间。说实话,我原本以为会有大量关于这个主题的文章。我以为现在 Android 上应该已经充斥着无数令人惊叹的示例、意想不到的效果,以及听到"着色器"这个词时脑海中浮现的其他令人着迷的东西。但事实并非如此。在 RuntimeShaders 可用之后,几乎立刻就出现了几篇文章,之后就再也没有了。一片寂静。
我想答案很简单:Android 开发者并不熟悉着色器,而着色器程序员通常不会直接为 Android 编写代码;他们通常会使用某种游戏引擎。事实上,如果我的假设正确,我希望用这篇文章来弥合这两个世界之间的差距。我真心希望着色器编写能够渗透到 Android 开发领域。因此,在本文的剩余部分,我将介绍一些必要的基础知识,以便你可以坐下来编写着色器并享受其成果。
因此,我假设你已经对 Compose 有了一定的了解。本文不会过多讨论着色器。我的主要重点是连接这两个世界。我们将在未来讨论具体的技术。假设我们有两个项目:一个带有背景图像,顶部有一个框,我们暂时将其设置为黑色。以下是代码的简化版本:
Kotlin
VerySimpleShaderTheme {
Box(Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) {
Image(
modifier = Modifier.fillMaxSize(),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
painter = painterResource(id = R.drawable.background_pattern),
contentDescription = null
)
Box(modifier = Modifier
.size(200.dp)
.clipToBounds(true)
){
}
}
}
现在,要将着色器添加到框中,你需要获取着色器文本本身,该文本以常规字符串形式传递。使用此文本创建一个 RuntimeShader
对象。然后,将其传递给 graphicsLayer
方法。这是一个简单的例子:
Kotlin
val runtimeShader = """
uniform shader image;half4 main(float2 fragCoord) {
return half4(0., 0.0, 0.0, .5);
}
""".trimIndent()
val shader = remember { RuntimeShader(runtimeShader) }
Box(modifier = Modifier.size(200.dp)
.clipToBounds()
.graphicsLayer {
this.renderEffect = RenderEffect
.createRuntimeShaderEffect(
shader, "image"
)
.asComposeRenderEffect()
}
.background(Color.White)
)
这是一个非常基础的着色器示例,它输出黑色,透明度为 50%。它看起来像下面的屏幕截图所示(背景只是来自资源库的图片)。它可能还不够令人印象深刻,但我想在这里强调两个重要的细节。首先,我们如何将着色器应用到盒子上。在我们的着色器代码中,我们需要一个着色器对象,以便在创建 renderEffect 时传递它。第二个重要的细节是,我们需要为盒子添加一个背景,并且背景不能完全透明;只有这样,我们的着色器才能应用到盒子上,否则它将不可见。

在使用着色器时,通常需要对坐标进行归一化,使其从 0 变为 1。理想情况下,从 -0.5 到 0.5,这样坐标中心就位于画布的正中央。这有助于使用各种数学公式。然而,要做到这一点,不仅需要知道当前像素坐标,还需要知道画布的总尺寸。为了演示如何做到这一点,我将向你展示下一个重点:将参数从代码传递给着色器。我们将传递盒子的尺寸并修改着色器,使其绘制一个圆圈:
Kotlin
val runtimeShader = """
uniform shader image;
uniform float2 resolution;half4 main(float2 fragCoord) {
vec2 uv = fragCoord/resolution.xy - .5;
uv.x *= resolution.x/resolution.y;
return half4(step(length(uv),0.5));
}
""".trimIndent()
Box(modifier = Modifier
.size(200.dp)
.clipToBounds()
.onSizeChanged { size ->
shader.setFloatUniform(
"resolution", size.width.toFloat(), size.height.toFloat()
)
}
.graphicsLayer {
this.renderEffect = RenderEffect
.createRuntimeShaderEffect(
shader, "image"
)
.asComposeRenderEffect()
}
.background(Color.White)
) {
}

现在我们已经学习了如何创建自己的着色器并为其传递参数,重要的是要理解你可以传递任何你需要的参数,无论是时间、颜色还是着色器所需的其他参数。为了巩固这些知识,我将向你展示一个我为 Android 制作的第一个着色器的示例------一个用于加载的发光圆圈。我们将传递当前时间来为其添加动画效果;下面是一个此类效果的简单示例:
Kotlin
val runtimeShader = """
uniform shader image;
uniform float2 resolution;
uniform float radius;
uniform float time;half4 main(float2 fragCoord) {
vec2 uv = fragCoord/resolution.xy - .5;
uv.x *= resolution.x/resolution.y;
float radiusWithTime = (1+sin(time))*0.1 + radius;
float glowingCircle = smoothstep(radiusWithTime, radiusWithTime-radiusWithTime*0.3, length(uv));
return half4(glowingCircle-step(length(uv),radius*0.7));
}
""".trimIndent()
val shader = remember { RuntimeShader(runtimeShader) }
var time by remember { mutableStateOf(0f) }
shader.setFloatUniform("radius", 0.6f)
LaunchedEffect(null) {
while (true) {
delay(10)
time+=0.01f
}
}
shader.setFloatUniform("time", time)

我已经演示了最基础的部分。我的目标是提供一个切入点,并展示它是多么的简单。关于着色器,我还有很多想讨论的,但我们留到下次再说。
感谢你的关注,祝你使用 Android、Compose 和Shader(着色器)顺利进入非凡的 UI 世界。期待与你相见!
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!