Android Compose中简单使用AGSL

本文探讨下如何使用AGSL (Android Graphics Shading Language)。

着色器(Shader)的概念

简单来说,着色器是可以插入到图形管道(graphics pipeline)的不同阶段的代码片段,它们由GPU执行。根据插入位置的不同,它们的名称和函数可能略有不同。有几种类型,包括片段着色器(fragment shaders)、顶点着色器(vertex shaders)、几何着色器(geometry shaders)和细分着色器(tessellation shaders)等。

本文仅探讨第一种类型:片段着色器(也称为像素着色器pixel shaders)。有一点非常重要,从Android 13(api>=33)才支持AGSL。

编程语言: AGSL

OpenGL ES开发者来肯定对GLSL(OpenGL Shading Language)很熟悉。AGSL和GLSL很像,有很多共同点。AGSL和GLSL的最大区别是两者的坐标系:在AGSL,原点在左上方;在GLSL,原点在左下方。这个说法来自原文链接,原文这样说的:

the most significant difference between AGSL and GLSL lies in their coordinate systems. In GLSL, the origin is typically located in the bottom-left corner of the screen, whereas in AGSL, it is positioned in the top-left corner.

这跟我理解的GLSL坐标系不太一样,我所理解的GLSL坐标系,原点是在最中心,纵坐标范围自下而上是-1到1,横坐标范围从左到右是-1到1. 哪位大神帮忙解释下GLSL坐标系到底是什么样的?作者的意思是不是是说相对于x、y都为正的区间来说,原点的位置处于左下角?如果这样解释的话,我所理解的GLSL坐标系和原作者的就应该是一致的。

AGSL和C语言很像,可以看下面一段非常简单的fragment shader代码片段:

half4 main(vec2 fragCoord) { 
    return half4(1.0, 0.0, 0.0, 1.0);
}

函数的输入类型是vec2,二维向量,表示像素的坐标,可以用fragCoord.x和fragCoord.y访问横纵坐标值;参数名字叫做fragCoord,当然可以随便取别的任意名字。
fragment shader的输出类型是half4,字面意思是4的一半,就是2字节,即16-bit的浮点类型。这个返回值存储了要绘制的像素的颜色值————上面的代码是红色(r=1,g=0,b=0,a=1)。最终的绘制效果就是一张纯红色的图片。

纯红色有点单调,我们来点新鲜的:颜色根据x坐标进行线性渐变的效果,也就是我们要动态调整像素的颜色值。我们需要创建一个单独的数据缓冲区,这个数据在Shader的所有的并行执行中共享,这就要借助uniform数据类型,哦,不对,uniform不是数据类型,只是一个关键字:

uniform float2 resolution;
const float3 colour = float3(1, 0, 0);
half4 main(vec2 fragCoord) { 
    return half4(colour * fragCoord.x / resolution.x, 1.);
}

可以看到,新代码多了个uniform修饰的float2类型的变量,colour表示像素点的三原色值即三个float值(实例中是纯红色),fragCoord.x表示像素点的x坐标,resolution.x表示屏幕宽度,这样我们就实现了根据横坐标红色进行渐变的颜色效果。

渐变色比纯色感觉好多了,不过我们可以再往前进一步,不仅仅是红色的渐变,我们可以动态设置color的三原色的值,代码如下:

uniform float2 resolution;
uniform float4 colour;
half4 main(vec2 fragCoord) {
    return half4(colour.rgb * fragCoord.x / resolution.x, 1.);
}

可以看到,我们把color变量也用uniform修饰了,类型从float3改为了float4。怎么修改color的值呢?kotlin伪代码如下:

fun uniform(color: android.graphics.Color) {
    val colorArray = floatArrayOf(color.red(), color.green(), color.blue(), color.alpha())
    android.graphics.RuntimeShader.setFloatUniform("colour", colorArray)
}

需要注意的是setFloatUniform函数的第一个参数的字符常量一定要和AGSL代码里的uniform float4 colour变量名字保持一致。

完整的代码包括MainActivity.kt、LinearGradientScreen.kt、LinearGradientViewModel.kt、LinearGradientModifier.kt、ShaderModifier.kt、ShaderModifier.android.kt,源码如下:

// MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                LinearGradientScreen(android.graphics.Color.valueOf(1f, 0f, 0f, 1f)) // 红色渐变效果
            }
        }
    }
}

// LinearGradientScreen.kt
@Composable
fun LinearGradientScreen(
    color: Color,
    modifier: Modifier = Modifier,
    viewModel: LinearGradientViewModel = androidx.lifecycle.viewmodel.compose.viewModel { LinearGradientViewModel(color) },
) {
    val colorState by viewModel.color.collectAsState()
    var green: MutableState<Float> = remember { mutableStateOf<Float>(0f) }
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .height(600.dp)
    ) {
        Text(
            text = "",
            modifier = Modifier
                .fillMaxWidth()
                .height(400.dp)
                .linearGradualShader(colorState)
        )
        Slider(
            value = green.value,
            onValueChange = {
                green.value = it
                viewModel.onColorChanged(Color.valueOf(1f - green.value, green.value, 0f, 0f))
            },
            valueRange = 0f..1f,
            modifier = Modifier.fillMaxWidth()
        )
    }
}

// LinearGradientViewModel.kt
class LinearGradientViewModel(color: Color) : ViewModel() {
    private val _color: MutableStateFlow<Color> = MutableStateFlow(color)
    val color: StateFlow<Color> = _color.asStateFlow()

    fun onColorChanged(config: Color) {
        _color.update { config }
    }
}

// LinearGradientModifier.kt
private val shader = """
    uniform float2 resolution;
    uniform float4 colour;
    half4 main(vec2 fragCoord) {
        return half4(colour.rgb * fragCoord.x / resolution.x, 1.);
    }
""".trimIndent()

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun Modifier.linearGradualShader(
    color: Color
): Modifier = this then shader(shader) {
    uniform("colour", color)
}

// ShaderModifier.kt
interface ShaderUniformProvider {
    fun uniform(name: String, value: Int)
    fun uniform(name: String, value: Float)
    fun uniform(name: String, value1: Float, value2: Float)
    fun uniform(name: String, color: Color)
}

// ShaderModifier.android.kt
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 fun Modifier.shader(
    shader: String,
    uniformsBlock: (ShaderUniformProvider.() -> Unit)?,
): Modifier = this then composed {
    val runtimeShader = remember { RuntimeShader(shader) }
    val shaderUniformProvider = remember { ShaderUniformProviderImpl(runtimeShader) }
    graphicsLayer {
        clip = true
        renderEffect = RenderEffect
            .createShaderEffect(
                runtimeShader.apply {
                    uniformsBlock?.invoke(shaderUniformProvider)
                    shaderUniformProvider.updateResolution(size)
                },
            ).asComposeRenderEffect()
    }
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 fun Modifier.runtimeShader(
    shader: String,
    uniformName: String = "content",
    uniformsBlock: (ShaderUniformProvider.() -> Unit)?,
): Modifier = this then composed {
    val runtimeShader = remember { RuntimeShader(shader) }
    val shaderUniformProvider = remember { ShaderUniformProviderImpl(runtimeShader) }
    graphicsLayer {
        clip = true
        renderEffect = RenderEffect
            .createRuntimeShaderEffect(
                runtimeShader.apply {
                    uniformsBlock?.invoke(shaderUniformProvider)
                    shaderUniformProvider.updateResolution(size)
                },
                uniformName,
            ).asComposeRenderEffect()
    }
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private class ShaderUniformProviderImpl(
    private val runtimeShader: RuntimeShader,
) : ShaderUniformProvider {

    fun updateResolution(size: Size) {
        uniform("resolution", size.width, size.height)
    }

    override fun uniform(name: String, value: Int) {
        runtimeShader.setIntUniform(name, value)
    }

    override fun uniform(name: String, value: Float) {
        runtimeShader.setFloatUniform(name, value)
    }

    override fun uniform(name: String, value1: Float, value2: Float) {
        runtimeShader.setFloatUniform(name, value1, value2)
    }

    override fun uniform(name: String, color: Color) {
        val colorArray = floatArrayOf(color.red(), color.green(), color.blue(), color.alpha())
        val colorArray3 = floatArrayOf(color.red(), color.green(), color.blue())
        runtimeShader.setFloatUniform(name, colorArray)
        // runtimeShader.setColorUniform(name, color.toArgb())
        // runtimeShader.setFloatUniform(name, colorArray3)
    }
}

最终的渐变效果如图所示(拖动条最小时,是纯红色的渐变效果r=1,g=0,b=0,a=1;拖动条最大时是纯绿色的渐变效果r=0,g=1,b=0,a=1):

更多效果和算法

渐变效果涉及的算法比较简单,中学生都能看懂,如果感兴趣可以研究下其它的算法,比如下面几个:

  1. Vignetting
  2. Smooth pixelation,跟pixelation类似,但是用正弦波来调制。
  3. Chromatic aberration

这三个效果的AGSL源码分别如下:

// Vignetting
uniform float2 resolution;
uniform shader content; 
uniform float intensity;
uniform float decayFactor;
half4 main(vec2 fragCoord) {
    vec2 uv = fragCoord.xy / resolution.xy;
    half4 color = content.eval(fragCoord);
    uv *=  1.0 - uv.yx;
    float vig = clamp(uv.x*uv.y * intensity, 0., 1.);
    vig = pow(vig, decayFactor);
    return half4(vig * color.rgb, color.a);
}

// Smooth pixelation
uniform float2 resolution;
uniform shader content; 
uniform float pixelSize;

vec4 main(vec2 fragCoord) {
    vec2 uv = fragCoord.xy / resolution.xy;
    float factor = (abs(sin( resolution.y * (uv.y - 0.5) / pixelSize)) + abs(sin( resolution.x * (uv.x - 0.5) / pixelSize))) / 2.0;
    half4 color = content.eval(fragCoord);
    return half4(factor * color.rgb, color.a); 
}

// Chromatic aberration
uniform float2 resolution;
uniform float intensity;
uniform shader content; 

half4 main(vec2 fragCoord) {
    vec2 uv = fragCoord.xy / resolution.xy;
    half4 color = content.eval(fragCoord);
    vec2 offset = intensity / resolution.xy;
    color.r = content.eval(resolution.xy * ((uv - 0.5) * (1.0 + offset) + 0.5)).r;
    color.b = content.eval(resolution.xy * ((uv - 0.5) * (1.0 - offset) + 0.5)).b;
    return color; 
}






原文链接:

Pushing the Boundaries of Compose Multiplatform with AGSL Shaders

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容