PHASE 3 · ANDROID 实战

3.5实战:转场动画

从 Shadertoy 到你的 App

Phase 3 的毕业项目:一个完整的 Shader 转场系统——两张画面,一个 0~1 的进度,像素级别的华丽切换。这也是开源世界研究最充分的 Shader 场景(GL Transitions 项目收录了上百种),学会框架后,那上百种全是你的素材库。

1转场的通用框架

一切 Shader 转场都是同一个函数:

GLSL · 转场的本质
uniform sampler2D uTexFrom;   // 旧画面
uniform sampler2D uTexTo;     // 新画面
uniform float uProgress;      // 0.0 = 全旧,1.0 = 全新

// 每个转场唯一的差别:mask 怎么随 progress 和坐标变化
float mask = transitionMask(uv, uProgress);
fragColor = mix(texture(uTexFrom, uv), texture(uTexTo, uv), mask);

1.1 章的老朋友:造 mask,然后 mix。最笨的 mask 是 mask = uProgress(整屏淡入淡出);而所有炫酷转场,不过是让 mask 在空间上不同步——圆形揭示是「离中心近的先变」,百叶窗是「每条先后变」,故障转场是「随机块乱序变」。

2三个转场,一个滑块

Demo 里「旧画面」是照片,「新画面」用 1.4 章调色板现场生成(省一张纹理,还复习了知识)。拖滑块就是在手动播放转场:

注意②里的细节:擦除时旧画面还额外做了一个小位移(拖影)——被替换的一方也参与运动,转场立刻从「换图」升级成「有物理感的推挤」。这类小心思是 GL Transitions 里最值得偷师的部分。

3Kotlin 端:双纹理 Renderer

3.2/3.3 的积木直接拼:多一张纹理、多一个 uProgress,没有任何新 API:

Kotlin · TransitionRenderer(节选,骨架同 3.2)
class TransitionRenderer(private val context: Context) : GLSurfaceView.Renderer {
    private var program = 0
    private var texFrom = 0
    private var texTo = 0
    @Volatile var progress = 0f          // 主线程写(动画),GL 线程读

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        program = ShaderUtil.createProgram(VERT_SRC, TRANSITION_FRAG)
        texFrom = loadTexture(context, R.drawable.page_from)   // 3.3 章的函数
        texTo   = loadTexture(context, R.drawable.page_to)
        // …VAO/VBO 与 location 缓存,同 3.2/3.3
    }

    override fun onDrawFrame(gl: GL10?) {
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        GLES30.glUseProgram(program)

        // 双纹理绑定(3.3 章口诀:纹理插单元,sampler 记单元号)
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texFrom)
        GLES30.glUniform1i(uTexFromLoc, 0)
        GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texTo)
        GLES30.glUniform1i(uTexToLoc, 1)

        GLES30.glUniform1f(uProgressLoc, progress)
        GLES30.glUniform2f(uResLoc, width.toFloat(), height.toFloat())

        GLES30.glBindVertexArray(vao)
        GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
    }
}

实战中「两张画面」从哪来?静态素材用 drawable;真实页面转场则在切换前把两个页面分别 View.drawToBitmap()(或 Compose 的 rememberGraphicsLayer().toImageBitmap())截成 Bitmap 再上传——转场期间显示 GLSurfaceView,结束后无缝换回真实页面。

4Compose 驱动进度

1.4 章说过的分工在这里落地:缓动交给动画框架,像素交给 Shader:

Kotlin · Compose 侧
@Composable
fun TransitionScreen() {
    var showNext by remember { mutableStateOf(false) }
    val progress by animateFloatAsState(
        targetValue = if (showNext) 1f else 0f,
        animationSpec = tween(700, easing = FastOutSlowInEasing),  // 缓动在这
        label = "transition"
    )
    var renderer by remember { mutableStateOf(null) }

    // progress 变化 → 写进 renderer(@Volatile 字段,跨线程安全)
    LaunchedEffect(progress) { renderer?.progress = progress }

    Box(Modifier.fillMaxSize()) {
        AndroidView(
            modifier = Modifier.fillMaxSize(),
            factory = { ctx ->
                GLSurfaceView(ctx).apply {
                    setEGLContextClientVersion(3)
                    val r = TransitionRenderer(ctx)
                    renderer = r
                    setRenderer(r)
                    renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
                }
            }
        )
        Button(
            onClick = { showNext = !showNext },
            modifier = Modifier.align(Alignment.BottomCenter).padding(32.dp)
        ) { Text(if (showNext) "返回" else "切换") }
    }
}
Android 视角 AGSL 同构版

API 33+ 可以整个换成 AGSL:两张 Bitmap 各包成 BitmapShader,用 runtimeShader.setInputShader("from", …) / setInputShader("to", …) 喂进去,进度照样 setFloatUniform——连 GLSurfaceView 都省了,直接画在 Compose Canvas 上。框架不变,壳更薄。

5移植方法论:从 Shadertoy 到 App

毕业前把「抄效果」这件事升格成方法论。以后在 Shadertoy / GL Transitions 看到心动的效果:

  1. 读懂输入:它用了哪些 uniform?(iChannel0 = 你的纹理,iTime = uTime,iResolution = uResolution,iMouse = uMouse——Shadertoy 的命名对照就这四个);
  2. 浏览器里拆解:贴进本书任意 Demo 的编辑器,按 0.2 章调试三板斧逐段注释/输出中间值,搞清每段贡献了什么;
  3. 裁剪:删掉手机跑不动的部分(超多 octave、几百次循环采样),用 1.5/2.1 章的降本技巧替换;
  4. 换头:GLSL 300 es 直接进路线 A;按 3.4 章清单转 AGSL 进路线 B;
  5. 接线:效果参数 → uniform → Compose 状态/动画,收工。

从此「很多 GLSL 代码都是抄的,根本不知道为什么」这句话,对你失效了。

本章小结

  • 转场 = mix(from, to, mask(uv, progress));炫酷程度取决于 mask 在空间上怎么不同步。
  • 被替换的一方也要参与运动(拖影、缩放),质感立升一级。
  • Kotlin 端零新知识:双纹理绑定 + uProgress,全是 3.2/3.3 的积木。
  • 分工铁律:动画框架管进度和缓动,Shader 管像素;@Volatile 或 setFloatUniform 交接。
  • 移植五步法:读输入 → 浏览器拆解 → 裁剪 → 换头(GLSL/AGSL)→ 接线。

动手练习

  1. 在 Demo 里加第四种转场「百叶窗」:uv.x 切 12 条,每条按 fract 内坐标先后翻转(提示:mask 用条内坐标与进度比较)。
  2. 给圆形揭示加「起点参数」:uniform vec2 uCenter,从点击位置扩散(结合 3.3 章触摸桥)——这就是 Material 的涟漪转场。
  3. 去 gl-transitions.com 挑一个你喜欢的转场,按第 5 节五步法完整移植到 Demo 编辑器里跑通。