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

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

保护原创,请勿转载!

相关推荐
alexhilton5 小时前
面向开发者的系统设计:像建筑师一样思考
android·kotlin·android jetpack
CYRUS_STUDIO14 小时前
用 Frida 控制 Android 线程:kill 命令、挂起与恢复全解析
android·linux·逆向
CYRUS_STUDIO14 小时前
Frida 实战:Android JNI 数组 (jobjectArray) 操作全流程解析
android·逆向
用户0917 小时前
Gradle Cache Entries 深度探索
android·java·kotlin
循环不息优化不止17 小时前
安卓 View 绘制机制深度解析
android
叽哥17 小时前
Kotlin学习第 9 课:Kotlin 实战应用:从案例到项目
android·java·kotlin
雨白1 天前
Java 线程通信基础:interrupt、wait 和 notifyAll 详解
android·java
Lei活在当下1 天前
【业务场景架构实战】4. 支付状态分层流转的设计和实现
架构·android jetpack·响应式设计
诺诺Okami1 天前
Android Framework-Launcher-UI和组件
android
潘潘潘1 天前
Android线程间通信机制Handler介绍
android