初探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 世界。期待与你相见!

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

保护原创,请勿转载!

相关推荐
CYRUS_STUDIO2 小时前
手把手教你改造 AAR:解包、注入逻辑、重打包,一条龙玩转第三方 SDK!
android·逆向
CYRUS_STUDIO3 小时前
Android 源码如何导入 Android Studio?踩坑与解决方案详解
android·android studio·源码阅读
前端赵哈哈4 小时前
初学者入门:Android 实现 Tab 点击切换(TabLayout + ViewPager2)
android·java·android studio
一条上岸小咸鱼7 小时前
Kotlin 控制流(二):返回和跳转
android·kotlin
Jasonakeke7 小时前
【重学 MySQL】九十二、 MySQL8 密码强度评估与配置指南
android·数据库·mysql
Mertrix_ITCH7 小时前
在 Android Studio 中修改 APK 启动图标(2025826)
android·ide·android studio
荏苒追寻7 小时前
Android OpenGL基础1——常用概念及方法解释
android
人生游戏牛马NPC1号7 小时前
学习 Android (十七) 学习 OpenCV (二)
android·opencv·学习
恋猫de小郭8 小时前
谷歌开启 Android 开发者身份验证,明年可能开始禁止“未经验证”应用的侧载,要求所有开发者向谷歌表明身份
android·前端·flutter
用户098 小时前
Gradle声明式构建总结
android