Android 13 RuntimeShader 使用介绍

Posted on
Updated on

最近 Android 13 beta 版本正式发布了。作为一个开发者,当然比较关心的是新版本中的一些行为变更,和新功能 api 等,这里我就不一一列举了,具体可以参看官网介绍。其中有一个新功能是 Android 13 添加了对可编程 RuntimeShader 对象的支持,我觉得这个 api 十分有趣,所以我把手头上的 Pixel 4a 升级到了最新的 beta 版本(当然也可以使用模拟器,不过我还是喜欢用真机进行开发),测试和试用一下这个新功能,看看效果怎么样。

这里我默认你是使用过或者了解 Shader 编程的,如果不知道 Shader 编程是什么的,建议你先学习过 Shader 编程知识后再阅读本文,体验会更佳。

什么是 RuntimeShader?

这里引用官方对 RuntimeShader api 的介绍:

A RuntimeShader calculates a per-pixel color based on the output of a user defined Android Graphics Shading Language (AGSL) function.

简单来说就是 RuntimeShader 对开发者提供了可编程的方式来改变输出的每一个像素数值,如果之前有过 OpenGL 编程经验的人,对此一定十分熟悉,这不就是渲染管线中 FragmentShader 嘛。总体使用上来讲两者是差不多的,区别是 OpenGL 中使用的是 GLSL 语法,而 RuntimeShader 中使用的是 AGSL(Android Graphics Shading Language),这个语法应该是 Google 自己定义的,做了一些修改和扩展。具体区别可以翻看官方 api 的介绍。

本文主要介绍一下我对 RuntimeShader 这个 api 的一些简单应用,完整的 Demo 代码我会放到 Github 上,链接会放在末尾,想要直接看代码可以自取。好了,话不多说,让我们开始吧。

简单颜色的绘制

首先我们要确认一下坐标系,也就是 x 和 y 轴的方向以及原点的位置。通过阅读官方文档我们可以知道,RuntimeShaderx 轴的方向是水平从左向右的,y 轴的方向是竖直从上向下的,坐标系的原点是在屏幕的左上角,这和 Android UI 界面开发的坐标系是一致的。这里我们须要注意的是,这和 OpenGL 中 y 轴的方向正好是相反的,原点位置也不相同。为了验证这个结论,我们来写一个 Demo 来验证一下。

首先我们要新建一个 RuntimeShader 对象,构造函数的入参是一个 Shader 字符串作为,代码如下:

val shader = RuntimeShader(
    """
    uniform vec2 uResolution;

    vec4 main(vec2 coords)
    {
    vec2 uv = coords / uResolution;
    vec3 col = vec3(0.);
    col.rg = uv;
    return vec4(col, 1.0);
    }
    """.trimIndent()
)

shader.setFloatUniform("uResolution", resolution)

这 Shader 中我们声明了一个变量 uResolution,代表这个 Shader 的作用范围的分辨率大小。Shader 中有一个主方法,入参是一个二维向量,代表当前我们操作像素的坐标,这个坐标是像素在 Android Canvas 中实际位置。这里我们用分辨率除入参的方式对坐标进行了归一化处理,将这个归一化坐标值 uv 赋值给输出颜色的 rg 分量,也就是红色和绿色,后面我们会从这两个颜色的变化中来判断 xy 轴的方向。最后将这个 col 拼成的 rgba 4 元向量作为方法的返回值。Shader 中的全局变量 uResolution 赋值也很简单,调用 Shader 对象的 setFloatUniform 方法进行赋值即可。需要注意的是,调用方法入参的变量名和数值是需要和 Shader 中定义的变量名和数据类型是要一一对应的。

在得到 Shader 对象示例后,我们只需要将它渲染出来就可以了,这里我们使用自定义 View 和 Paint 渲染的方式。代码如下:

class BaseShaderView(context: Context) : View(context, null, 0) {

  private val shader: RuntimeShader

  init() {
    // init shader here
  }

  private val paint = Paint().apply {
    style = Paint.Style.FILL
    color = Color.WHITE
  }

  override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    val width = width.toFloat()
    val height = height.toFloat()
    paint.shader = shader
    canvas?.drawRect(0.0f, 0.0f, width, height, paint)
  }
}

来看下最终显示效果:

Basic Unage Result

从图片中可以看到,颜色从左向右的渐变过程中红色会越来越多,也就是代表 x 轴的方向是从左向右的,同时颜色从上向下的渐变过程中绿色会越来越多,也就是代表 x 轴的方向是从上向下的,左上角的颜色是黑色 rgb(0, 0, 0),也就代表了坐标原点在屏幕左上角,这与我们的预期是相符的。

Bitmap 的绘制

在 Shader GLSL 编程中有一个十分常用的方法 texture2D,用于图片纹理的采样取值。在 RuntimeShader 中,纹理采样有一些使用上的差异,下面代码会演示使用方法。

val shader = RuntimeShader(
"""
uniform shader uBitmap;
uniform vec2 uBitmapSize;
uniform vec2 uResolution;

vec4 main(vec2 coords)
{
  vec2 uv = coords / uResolution;
  vec3 col = vec3(0.);
  col += uBitmap.eval(uv * uBitmapSize).rgb;
  return vec4(col, 1.0);
}
""".trimIndent()
)

val bitmap = BitmapFactory.decodeResource(resources, R.raw.sample)
val bitmapShader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
shader.setInputShader("uBitmap", bitmapShader)
shader.setFloatUniform("uBitmapSize", floatArrayOf(bitmap.width.toFloat(), bitmap.height.toFloat()))

在 GLSL 中纹理的类型一般使用的是 sampler2D,可以看到这里将图片纹理声明的类型是 shader。采样方法也不同,调用了 shadereval 方法,入参只有一个,是采样位置的坐标,数值大小为实际像素大小,这与 GLSL 中纹理的归一化坐标不同,需要特别注意一下。

BitmapShader 的创建没什么特别注意的,在 Android 13 之前的 api 中就存在了,这里需要做的就是将这个 Shader 对象实例通过 setInputShader 方法传入 RuntimeShader 中,变量名的规则和之前讲过的一样与 Shader 中的保持一致就行。

这里实现就是将图片填充撑满整个 Canvas 的渲染效果。

在 RenderEffect 中 RuntimeShader 的应用

除了使用 Paint 进行渲染的方式,还可以使用 api RenderEffectRenderEffect 是在 Android 12 中引入的一个 api,用于模糊效果 BlurEffect 的使用(本质上 BlurEffect 也是通过 RuntimeShader 实现的),使用方式是可以将 Effect 直接应用在 View 上。通过这个 api,我们可以轻松的对 View 应用 RuntimeShader 效果,实现 Android 13 之前实现不了的功能。 先来看一下代码:

private fun applyRuntimeShader(view: View, resolution: FloatArray) {
  val shader = RuntimeShader(
    """
    uniform shader uContent;

    vec4 main(vec2 coords)
    {
      vec3 col = vec3(0.);
      vec4 origin = uContent.eval(coords);
      float l = 0.299*origin.r+0.578*origin.g+0.144*origin.b;
      col += l;
      return vec4(col, origin.a);
    }
  """.trimIndent()
  )
  val effect = RenderEffect.createRuntimeShaderEffect(shader, "uContent")
  view.setRenderEffect(effect)
}

这段代码实现的效果是将 View 灰度化,这里关键就是 RenderEffect 实例创建的代码 RenderEffect.createRuntimeShaderEffect(shader, "uContent"),方法的第一个参数是我们需要使用的 Shader 实例,第二个参数是 Shader 中 inputBuffer shader 变量名,类似于之前的 Bitmap,可以采样对应 View 任意位置的像素进行处理。最终效果如下:

使用 RenderEffect 将整个 View 灰度化

尾巴

RuntimeShader 的出现,得益于可以直接作用于 View,对 View 进行像素级别的处理,从而可以实现很多以前无法实现的效果。同时在 Android 中编写 Shader 页变得更加方便了,不用再自己创建一个 OpenGL 上下文,写一堆胶水代码,只是为了实现一个简单的滤镜功能。我使用 RuntimeShader 移植了一个 Lut 滤镜,基本没有遇到什么困难,整个过程只须要关注 Shader 代码编写就可以了,我把代码也放在 Demo 里面了,感兴趣的可以自己查看。

缺点也十分明显,就是最低只支持到 Android 13,目前还无法大规模使用。

对于这个 api 的使用暂时先介绍到这里了,总体来看这是一个功能很强大的 api,个人十分看好。Demo 我放在下面的连接中了,需要的自取。

GitHub 项目地址


Back Home

评论区