初探Compose中的着色器RuntimeShader

本文译自「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 世界。期待与你相见!

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
fatiaozhang95272 小时前
创维智能融合终端SK-M424_S905L3芯片_2+8G_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
来来走走3 小时前
Flutter开发 了解Scaffold
android·开发语言·flutter
哆啦A梦的口袋呀4 小时前
Android 底层实现基础
android
闻道且行之5 小时前
Android Studio下载及安装配置
android·ide·android studio
小墙程序员5 小时前
kotlin元编程(二)使用 Kotlin 来生成源代码
android·kotlin·android studio
小墙程序员5 小时前
kotlin元编程(一)一文理解 Kotlin 反射
android·kotlin·android studio
fatiaozhang95276 小时前
创维智能融合终端DT741_移动版_S905L3芯片_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
KotlinKUG贵州8 小时前
贪心算法:从“瞎蒙”到稳赚
算法·kotlin
小林学Android8 小时前
Android四大组件之Activity详解
android