PHASE 3 · ANDROID 实战

3.1Android 渲染版图

GLSurfaceView 与 AGSL 两条路线怎么选

你的 Shader 内功已经练成,现在要把它装进 App。很多教程在这里只教一条路(而且往往是老路),导致你分不清「必须的」和「历史包袱」。这一章先把 Android 上运行 Shader 的两条路线看清楚,后面四章分头深入——磨刀不误砍柴工,这十分钟的全景值回票价。

1两条路线,一张地图

在 Android 上让一段 Fragment Shader 跑起来,今天有两条正规路线:

你的 GLSL
路线 A · OpenGL ES
GLSurfaceView 自管画布
你的 AGSL
路线 B · RuntimeShader
画进任意 View / Compose
  • 路线 A(经典):OpenGL ES + GLSurfaceView。你直接操作 GPU API:自己编译 Shader、自己管顶点、自己开渲染循环。语言就是本书用的 GLSL 300 es,Demo 代码原封不动可用。功能无上限(3D、多 pass、相机流),代价是样板代码多、与 UI 系统隔离(它是一块独立的 Surface)。
  • 路线 B(现代):AGSL + RuntimeShader。Android 13(API 33)引入。你写一段 AGSL(和 GLSL 语法九成相同的方言),包成 RuntimeShader,像普通 Shader/RenderEffect 一样交给系统渲染管线(Skia)。它能直接作用于你现有的 View 和 Compose 内容——把整个 UI 当作输入纹理,这是路线 A 做不到的。代价是 API 门槛(13+)和一些功能边界。
它们的共同底座

两条路线最终都跑在同一块 GPU、同一种渲染管线上(0.1 章)。Skia(路线 B 的引擎)底层也是 GPU API。所以你在 Phase 0~2 学的一切——坐标、SDF、噪声、三把刀——两条路线通吃,差别只在「装配方式」。

2路线 A:OpenGL ES 与 GLSurfaceView

路线 A 的核心角色只有三个,先认脸,3.2/3.3 章逐个上手:

🖼️

GLSurfaceView

专门用于 GL 渲染的 View。它替你管好两件麻烦事:EGL 环境(GPU 上下文与窗口的婚介所)和独立的渲染线程

🔄

Renderer 接口

你实现三个回调:onSurfaceCreated(初始化)、onSurfaceChanged(尺寸变化)、onDrawFrame(每帧绘制)。

GLES30 类

OpenGL ES 3.0 的静态方法集,Kotlin 里所有 gl* 调用的入口,一比一对应 C API。

Kotlin · 路线 A 的最小骨架
class ShaderSurfaceView(context: Context) : GLSurfaceView(context) {
    init {
        setEGLContextClientVersion(3)          // 要求 OpenGL ES 3.0
        setRenderer(ShaderRenderer())
        renderMode = RENDERMODE_CONTINUOUSLY   // 每帧重绘(动画用)
        // 静态效果改用 RENDERMODE_WHEN_DIRTY + requestRender(),省电
    }
}

class ShaderRenderer : GLSurfaceView.Renderer {
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        // 一次性:编译链接 Shader、准备顶点(3.2 章)
    }
    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES30.glViewport(0, 0, width, height)   // 告诉 GL 画布多大
    }
    override fun onDrawFrame(gl: GL10?) {
        // 每帧:清屏 → 传 uniform → 画全屏两个三角形(3.2/3.3 章)
    }
}
GL 线程:路线 A 的第一军规

Renderer 的三个回调运行在 GLSurfaceView 私有的 GL 线程,不是主线程。所有 GLES30.* 调用只能在这个线程发生;主线程想传数据,要么用 @Volatile 字段让 GL 线程下一帧读取,要么 queueEvent { } 把代码块投递过去。在主线程调 GL 函数不会崩,只会静默无效——这是路线 A 最阴的坑。

3路线 B:AGSL 与 RuntimeShader

路线 B 没有 Surface、没有渲染线程、没有 gl* 调用。你写一段 AGSL 字符串,三行 Kotlin 就能把它糊到任何 Compose 元素上:

Kotlin + AGSL · 路线 B 的最小可用示例(API 33+)
val SHADER = """
    uniform float2 uResolution;
    uniform float uTime;

    half4 main(float2 fragCoord) {          // AGSL:main 直接收坐标、返回颜色
        float2 uv = fragCoord / uResolution;
        half3 col = half3(uv.x, uv.y, abs(sin(uTime)));
        return half4(col, 1.0);
    }
"""

@Composable
fun ShaderBox() {
    val shader = remember { RuntimeShader(SHADER) }
    val time by produceState(0f) {          // 简易时间源
        while (true) { withInfiniteAnimationFrameMillis { value = it / 1000f } }
    }
    Box(Modifier.fillMaxSize().drawWithCache {
        shader.setFloatUniform("uResolution", size.width, size.height)
        shader.setFloatUniform("uTime", time)
        val brush = ShaderBrush(shader)
        onDrawBehind { drawRect(brush) }    // Shader 当画笔,想画哪画哪
    })
}

注意 AGSL 和 GLSL 的几处表面差异(3.4 章有完整对照表):没有 #versionprecision 声明、vec2 写作 float2main 的签名是「收坐标返颜色」。肌肉记忆三分钟迁移,思维模型零迁移

4怎么选:决策表

你的需求选择为什么
给 Compose UI / 图片加特效滤镜路线 BRenderEffect 直接吃 UI 内容当纹理,十行代码的事
动态壁纸、全屏艺术背景路线 B(13+)ShaderBrush 铺满即可;要兼容老设备则 A
minSdk < 33 且必须有 Shader 特效路线 AGLSurfaceView 上古就有;或接受低版本降级无特效
相机 / 视频帧实时处理路线 ASurfaceTexture 外部纹理是 GL 的地盘
3D、多 pass 渲染、粒子系统路线 A(或游戏引擎)AGSL 单 pass 2D 为主,复杂管线要完整 GL
学习、原型、给设计师演示浏览器本书 Demo / Shadertoy 迭代最快,调完再移植
本书的学习顺序是「先 A 后 B」,理由是坦白的

路线 A 的样板代码(编译、链接、顶点、uniform)恰好就是渲染管线概念的一比一落地——写一遍,0.1 章的每个名词都长出了实体。路线 B 帮你把这些全藏掉了,先学 B 你会「能用但不知其所以然」,又回到抄代码的老路。3.2/3.3 苦一点,3.4 你会觉得 AGSL 简单得像作弊。

本章小结

  • 两条路线:A = OpenGL ES + GLSurfaceView(全功能、全版本、样板多);B = AGSL + RuntimeShader(API 33+,能吃 UI 当纹理,门槛低)。
  • Shader 知识两条路线通吃,差别只在装配方式;AGSL 是 GLSL 的近亲方言。
  • 路线 A 三角色:GLSurfaceView(管环境和线程)、Renderer(三回调)、GLES30(API 入口);GL 调用只能在 GL 线程。
  • 选型速记:UI 特效选 B,相机/3D/老设备选 A,写效果本身永远先在浏览器调好。

动手练习

  1. 建一个空 Android 项目,把「路线 A 最小骨架」跑起来:在 onDrawFrame 里只写 glClearColor(1f, 0.4f, 0.2f, 1f) + glClear,看到一块橙色就算通关(Shader 下一章才上)。
  2. 如果你的测试机是 Android 13+:把「路线 B 最小示例」贴进 Compose 项目跑通,对照本书 0.2 章的 UV 渐变 Demo,确认画面一致。
  3. 盘点你手头的真实需求(或想做的效果),用决策表给它选路线,想清楚理由。