本文探討下如何使用AGSL (Android Graphics Shading Language)帝洪。
著色器(Shader)的概念
簡(jiǎn)單來(lái)說(shuō),著色器是可以插入到圖形管道(graphics pipeline)的不同階段的代碼片段脚猾,它們由GPU執(zhí)行葱峡。根據(jù)插入位置的不同,它們的名稱和函數(shù)可能略有不同龙助。有幾種類型砰奕,包括片段著色器(fragment shaders)、頂點(diǎn)著色器(vertex shaders)、幾何著色器(geometry shaders)和細(xì)分著色器(tessellation shaders)等军援。
本文僅探討第一種類型:片段著色器(也稱為像素著色器pixel shaders)仅淑。有一點(diǎn)非常重要,從Android 13(api>=33)才支持AGSL胸哥。
編程語(yǔ)言: AGSL
OpenGL ES開(kāi)發(fā)者來(lái)肯定對(duì)GLSL(OpenGL Shading Language)很熟悉涯竟。AGSL和GLSL很像,有很多共同點(diǎn)空厌。AGSL和GLSL的最大區(qū)別是兩者的坐標(biāo)系:在AGSL昆禽,原點(diǎn)在左上方;在GLSL蝇庭,原點(diǎn)在左下方醉鳖。這個(gè)說(shuō)法來(lái)自原文鏈接,原文這樣說(shuō)的:
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坐標(biāo)系不太一樣哮内,我所理解的GLSL坐標(biāo)系盗棵,原點(diǎn)是在最中心,縱坐標(biāo)范圍自下而上是-1到1北发,橫坐標(biāo)范圍從左到右是-1到1. 哪位大神幫忙解釋下GLSL坐標(biāo)系到底是什么樣的纹因?作者的意思是不是是說(shuō)相對(duì)于x、y都為正的區(qū)間來(lái)說(shuō)琳拨,原點(diǎn)的位置處于左下角瞭恰?如果這樣解釋的話,我所理解的GLSL坐標(biāo)系和原作者的就應(yīng)該是一致的狱庇。
AGSL和C語(yǔ)言很像惊畏,可以看下面一段非常簡(jiǎn)單的fragment shader代碼片段:
half4 main(vec2 fragCoord) {
return half4(1.0, 0.0, 0.0, 1.0);
}
函數(shù)的輸入類型是vec2,二維向量密任,表示像素的坐標(biāo)颜启,可以用fragCoord.x和fragCoord.y訪問(wèn)橫縱坐標(biāo)值;參數(shù)名字叫做fragCoord浪讳,當(dāng)然可以隨便取別的任意名字缰盏。
fragment shader的輸出類型是half4,字面意思是4的一半淹遵,就是2字節(jié)口猜,即16-bit的浮點(diǎn)類型。這個(gè)返回值存儲(chǔ)了要繪制的像素的顏色值————上面的代碼是紅色(r=1,g=0,b=0,a=1)透揣。最終的繪制效果就是一張純紅色的圖片济炎。
純紅色有點(diǎn)單調(diào),我們來(lái)點(diǎn)新鮮的:顏色根據(jù)x坐標(biāo)進(jìn)行線性漸變的效果淌实,也就是我們要?jiǎng)討B(tài)調(diào)整像素的顏色值冻辩。我們需要?jiǎng)?chuàng)建一個(gè)單獨(dú)的數(shù)據(jù)緩沖區(qū)猖腕,這個(gè)數(shù)據(jù)在Shader的所有的并行執(zhí)行中共享,這就要借助uniform數(shù)據(jù)類型恨闪,哦倘感,不對(duì),uniform不是數(shù)據(jù)類型咙咽,只是一個(gè)關(guān)鍵字:
uniform float2 resolution;
const float3 colour = float3(1, 0, 0);
half4 main(vec2 fragCoord) {
return half4(colour * fragCoord.x / resolution.x, 1.);
}
可以看到老玛,新代碼多了個(gè)uniform
修飾的float2類型的變量,colour表示像素點(diǎn)的三原色值即三個(gè)float值(實(shí)例中是純紅色)钧敞,fragCoord.x表示像素點(diǎn)的x坐標(biāo)蜡豹,resolution.x表示屏幕寬度,這樣我們就實(shí)現(xiàn)了根據(jù)橫坐標(biāo)紅色進(jìn)行漸變的顏色效果溉苛。
漸變色比純色感覺(jué)好多了镜廉,不過(guò)我們可以再往前進(jìn)一步,不僅僅是紅色的漸變愚战,我們可以動(dòng)態(tài)設(shè)置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函數(shù)的第一個(gè)參數(shù)的字符常量一定要和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)
}
}
最終的漸變效果如圖所示(拖動(dòng)條最小時(shí)捧颅,是純紅色的漸變效果r=1,g=0,b=0,a=1;拖動(dòng)條最大時(shí)是純綠色的漸變效果r=0,g=1,b=0,a=1):
更多效果和算法
漸變效果涉及的算法比較簡(jiǎn)單较雕,中學(xué)生都能看懂,如果感興趣可以研究下其它的算法挚币,比如下面幾個(gè):
- Vignetting亮蒋。
- Smooth pixelation,跟pixelation類似妆毕,但是用正弦波來(lái)調(diào)制慎玖。
- Chromatic aberration
這三個(gè)效果的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