本文探讨下如何使用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):
更多效果和算法
渐变效果涉及的算法比较简单,中学生都能看懂,如果感兴趣可以研究下其它的算法,比如下面几个:
- Vignetting。
- Smooth pixelation,跟pixelation类似,但是用正弦波来调制。
- 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