管线已经通了,现在把三种数据源接上桥:数值(uniform)、图像(纹理)、交互(触摸)。接完这一章,本书 Phase 0~2 的每一个 Demo——包括那些带滑块和照片的——你都能在 Android 上完整复刻。
1Uniform:类型对照与缓存
GLSL 每种 uniform 类型都有一个对应的 Kotlin 传值函数,规律是 glUniform + 分量数 + 类型:
| GLSL 声明 | Kotlin API | 本书里的角色 |
|---|---|---|
| uniform float | glUniform1f(loc, v) | uTime、uProgress |
| uniform vec2 | glUniform2f(loc, x, y) | uResolution、uMouse |
| uniform vec3 / vec4 | glUniform3f / 4f(loc, …) | 颜色参数 |
| uniform int / bool | glUniform1i(loc, v) | 开关、模式切换 |
| uniform mat4 | glUniformMatrix4fv(loc, 1, false, m, 0) | 3D 变换(本书没用到) |
| uniform sampler2D | glUniform1i(loc, 纹理单元号) | uTex(注意:传的是单元号!) |
loc 是 uniform 在 Program 里的「门牌号」,用名字查询。查询有成本,别每帧查——初始化时查一次存成员变量:
private var uTimeLoc = -1
private var uResLoc = -1
private var uTexLoc = -1
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
program = ShaderUtil.createProgram(VERT_SRC, FRAG_SRC)
uTimeLoc = GLES30.glGetUniformLocation(program, "uTime")
uResLoc = GLES30.glGetUniformLocation(program, "uResolution")
uTexLoc = GLES30.glGetUniformLocation(program, "uTex")
}
override fun onDrawFrame(gl: GL10?) {
// ……glUseProgram 之后:
GLES30.glUniform1f(uTimeLoc, time)
GLES30.glUniform2f(uResLoc, width.toFloat(), height.toFloat())
}
0.3 章坑清单的复习:如果某个 uniform 在 Shader 里声明了但没参与最终颜色计算,编译器会把它优化掉,查询返回 -1。给 -1 传值会被静默忽略,不崩溃。所以「调了半天 uniform 没反应」时,先确认它真的被用到了。
2纹理:把 Bitmap 送进 GPU
Phase 2 里所有吃图的效果,在 Android 上的图像来源就是 Bitmap。上传流程四步:建纹理对象 → 设采样参数 → 上传像素 → 释放 Bitmap:
fun loadTexture(context: Context, resId: Int): Int {
val ids = IntArray(1)
GLES30.glGenTextures(1, ids, 0)
val texId = ids[0]
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId)
// 采样参数:决定 texture() 在放大/缩小/越界时的行为
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR) // 缩小:线性
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR) // 放大:线性
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE) // 越界:延展边缘
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,
GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE)
// 解码 Bitmap 并上传(inScaled=false 防止按密度缩放)
val opts = BitmapFactory.Options().apply { inScaled = false }
val bitmap = BitmapFactory.decodeResource(context.resources, resId, opts)
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0)
bitmap.recycle() // 像素已进显存,CPU 侧的可以扔了
return texId
}
两组参数值得懂,因为它们直接改变画面观感:
- FILTER(过滤):
GL_LINEAR双线性插值,放大平滑;GL_NEAREST取最近像素,放大出马赛克锯齿——像素风游戏(以及 2.2 章的像素化,如果你想在采样端偷懒)反而要选它。 - WRAP(越界):UV 超出 0~1 时,
CLAMP_TO_EDGE重复边缘像素(特效常用,防止扭曲时采到「对面」);GL_REPEAT平铺重复(无缝纹理背景用)。2.3 章毛玻璃的随机偏移能安全越界,靠的就是 CLAMP。
0.2 章预告过:Bitmap 第一行在顶部,GL 纹理坐标原点在底部,所以直接上传的图是上下颠倒的。三种修法任选:① Shader 里 uv.y = 1.0 - uv.y(最简单);② 上传前 Matrix().apply { postScale(1f, -1f) } 翻转 Bitmap;③ 顶点数据里把纹理坐标翻过来。团队项目里选一种全项目统一,不然翻两次等于没翻,查起来怀疑人生。
3纹理单元:多图协作
Shader 里可以声明多个 sampler2D(3.5 章转场就要两张图)。GPU 通过「纹理单元」这个中转站管理:每个单元插一张纹理,uniform 里存的是单元号。绑定是三步走:
// 单元 0 ← 第一张图
GLES30.glActiveTexture(GLES30.GL_TEXTURE0) // ① 选中单元 0
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texFrom) // ② 往里插纹理
GLES30.glUniform1i(uTexFromLoc, 0) // ③ 告诉 sampler:去 0 号找
// 单元 1 ← 第二张图
GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texTo)
GLES30.glUniform1i(uTexToLoc, 1)
新手最容易犯的错是给 sampler2D 传纹理 ID(texId)——记住口诀:「纹理插单元,sampler 记单元号」,ID 只在 glBindTexture 里出现。
4触摸:线程与坐标翻转
最后一座桥是交互。它牵扯 3.1 章的 GL 线程军规:触摸事件在主线程到达,GL 在渲染线程消费,标准解法是 @Volatile 字段做单向交接:
class ShaderRenderer : GLSurfaceView.Renderer {
@Volatile var touchX = 0f // 主线程写,GL 线程读
@Volatile var touchY = 0f
override fun onDrawFrame(gl: GL10?) {
// ……
// y 翻转:Android 触摸 y 向下,GL 坐标 y 向上(0.2 章的老朋友)
GLES30.glUniform2f(uMouseLoc, touchX, height - touchY)
}
}
// View 侧(主线程):
glSurfaceView.setOnTouchListener { v, event ->
renderer.touchX = event.x
renderer.touchY = event.y
if (event.action == MotionEvent.ACTION_DOWN) v.performClick()
true
}
更复杂的数据(比如一次性替换整张纹理)用 glSurfaceView.queueEvent { … } 把闭包投递到 GL 线程执行——它保证运行在正确线程、正确时机(帧间隙)。
到这里,本书 Demo 运行时的每个能力你都有 Kotlin 版了:uTime/uResolution(3.2)、uTex(本章)、uMouse(本章)、uProgress(就是一个普通 float uniform,接 Slider/动画值)。也就是说:Phase 0~2 的四十多个 Demo,每一个你都能原样搬上手机了。
本章小结
- Uniform 传值 API 按「分量数+类型」命名;location 初始化时查一次缓存,-1 表示被优化掉而非出错。
- 纹理上传四步:genTextures → 参数 → texImage2D → recycle;FILTER 决定缩放观感,WRAP 决定越界行为。
- Bitmap 与 GL 的 y 轴相反,图是倒的就翻 uv.y,全项目统一一种翻法。
- 多纹理靠单元中转:纹理插单元,sampler 记单元号,别把 texId 传给 sampler。
- 主线程写 @Volatile、GL 线程读;重操作用 queueEvent;触摸 y 要翻转。
动手练习
- 把 2.2 章乐高 Demo 搬上手机:loadTexture 加载一张你的相册照片,uProgress 接一个 Compose Slider(通过 @Volatile 交接)。
- 实现「触摸涟漪」:2.1 章水波扭曲的中心点改为 uMouse,手指按哪波纹从哪扩散。
- 故意把 sampler2D 的 uniform 传成 texId 而不是单元号,观察画面症状(通常是黑或取错图)——见过一次,终生免疫。