前言: 『OpenGL学习』 从零打造 Android 滤镜相机
上一篇: # 『OpenGL学习滤镜相机』- Day1: OpenGL ES 入门与环境搭建
Github: OpenGLTest
📚 今日目标
- 理解 OpenGL 渲染管线的工作流程
- 掌握顶点数据的创建和传递
- 学习着色器(Shader)的基本概念
- 编写第一个顶点着色器和片段着色器
- 成功渲染一个彩色三角形
运行效果:

🎯 学习内容
1. OpenGL 渲染管线
渲染管线流程图
markdown
┌────────────────┐
│ 顶点数据 │ ← 定义形状的顶点坐标
└───────┬────────┘
↓
┌────────────────┐
│ 顶点着色器 │ ← 处理每个顶点(位置变换等)
└───────┬────────┘
↓
┌────────────────┐
│ 图元装配 │ ← 将顶点组装成三角形
└───────┬────────┘
↓
┌────────────────┐
│ 光栅化 │ ← 将三角形转换为片段(像素)
└───────┬────────┘
↓
┌────────────────┐
│ 片段着色器 │ ← 计算每个片段(像素)的颜色
└───────┬────────┘
↓
┌────────────────┐
│ 帧缓冲 │ ← 最终显示到屏幕
└────────────────┘
核心概念:
- 顶点(Vertex):定义形状的点,包含位置、颜色等信息
- 图元(Primitive):基本图形单元,如点、线、三角形
- 片段(Fragment):光栅化后的像素候选,包含位置、颜色等信息
- 着色器(Shader):运行在 GPU 上的小程序
2. 着色器(Shader)
什么是着色器?
着色器是用 GLSL(OpenGL Shading Language) 编写的程序,运行在 GPU 上,用于控制渲染管线的某些阶段。
OpenGL ES 2.0 的两种着色器
| 着色器类型 | 作用 | 输入 | 输出 |
|---|---|---|---|
| 顶点着色器 (Vertex Shader) | 处理每个顶点 | 顶点属性 | 顶点位置、varying 变量 |
| 片段着色器 (Fragment Shader) | 计算每个片段的颜色 | varying 变量 | 片段颜色 |
顶点着色器示例
glsl
// 最简单的顶点着色器
attribute vec4 aPosition; // 输入:顶点位置
void main() {
gl_Position = aPosition; // 输出:顶点位置
}
片段着色器示例
glsl
// 最简单的片段着色器
precision mediump float; // 精度声明
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 输出:红色
}
3. GLSL 基础语法
变量类型
| 修饰符 | 说明 | 使用场景 |
|---|---|---|
attribute |
顶点属性,每个顶点不同 | 仅顶点着色器,接收顶点数据 |
uniform |
统一变量,所有顶点/片段相同 | 两种着色器都可用,传递常量 |
varying |
易变变量,从顶点传到片段 | 顶点着色器输出→片段着色器输入 |
数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
float |
浮点数 | float a = 1.0; |
vec2 |
2D 向量 | vec2 pos = vec2(0.0, 1.0); |
vec3 |
3D 向量 | vec3 color = vec3(1.0, 0.0, 0.0); |
vec4 |
4D 向量 | vec4 rgba = vec4(1.0, 0.0, 0.0, 1.0); |
mat4 |
4x4 矩阵 | mat4 matrix; |
4. 顶点数据
定义三角形的顶点
在 OpenGL 的标准化坐标系中(NDC),我们定义一个三角形:
kotlin
val vertices = floatArrayOf(
// x, y, z
0.0f, 0.5f, 0.0f, // 顶点 0:上方
-0.5f, -0.5f, 0.0f, // 顶点 1:左下
0.5f, -0.5f, 0.0f // 顶点 2:右下
)
顶点缓冲对象(VBO)
OpenGL 不能直接使用 Java/Kotlin 数组,需要转换为 ByteBuffer:
kotlin
val vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
.order(ByteOrder.nativeOrder()) // 使用本地字节序
.asFloatBuffer() // 转换为 Float 缓冲
.put(vertices) // 填充数据
.position(0) // 重置位置
为什么要这样做?
- Java 数组在堆内存,可能被 GC 移动
- ByteBuffer 使用直接内存,OpenGL 可以直接访问
- 字节序问题:不同平台可能不同
5. 着色器程序的编译和链接
完整流程
markdown
┌──────────────┐ ┌──────────────┐
│ 顶点着色器 │ │ 片段着色器 │
│ 源代码 │ │ 源代码 │
└──────┬───────┘ └──────┬───────┘
│ │
↓ 编译 ↓ 编译
┌──────────────┐ ┌──────────────┐
│ 顶点着色器 │ │ 片段着色器 │
│ 对象 │ │ 对象 │
└──────┬───────┘ └──────┬───────┘
│ │
└──────────┬──────────┘
↓ 链接
┌─────────────┐
│ 着色器程序 │
└─────────────┘
代码步骤
kotlin
// 1. 创建着色器对象
val vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
// 2. 上传着色器源代码
GLES20.glShaderSource(vertexShader, vertexShaderCode)
// 3. 编译着色器
GLES20.glCompileShader(vertexShader)
// 4. 检查编译状态
val status = IntArray(1)
GLES20.glGetShaderiv(vertexShader, GLES20.GL_COMPILE_STATUS, status, 0)
if (status[0] == 0) {
// 编译失败,获取错误信息
val log = GLES20.glGetShaderInfoLog(vertexShader)
throw RuntimeException("Shader compilation failed: $log")
}
// 片段着色器同理...
// 5. 创建程序对象
val program = GLES20.glCreateProgram()
// 6. 附加着色器
GLES20.glAttachShader(program, vertexShader)
GLES20.glAttachShader(program, fragmentShader)
// 7. 链接程序
GLES20.glLinkProgram(program)
// 8. 检查链接状态(类似编译检查)
💻 代码实践
Day02Renderer 核心代码
kotlin
class Day02Renderer : GLSurfaceView.Renderer {
// 着色器源代码
private val vertexShaderCode = """
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
}
""".trimIndent()
private val fragmentShaderCode = """
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
""".trimIndent()
// 三角形顶点
private val vertices = floatArrayOf(
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
)
private lateinit var vertexBuffer: FloatBuffer
private var program: Int = 0
private var aPositionLocation: Int = 0
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 设置背景色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
// 创建顶点缓冲
vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertices)
vertexBuffer.position(0)
// 编译着色器
val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// 创建程序
program = GLES20.glCreateProgram()
GLES20.glAttachShader(program, vertexShader)
GLES20.glAttachShader(program, fragmentShader)
GLES20.glLinkProgram(program)
// 获取 attribute 位置
aPositionLocation = GLES20.glGetAttribLocation(program, "aPosition")
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
// 清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// 使用程序
GLES20.glUseProgram(program)
// 启用顶点属性
GLES20.glEnableVertexAttribArray(aPositionLocation)
// 传递顶点数据
GLES20.glVertexAttribPointer(
aPositionLocation, // 属性位置
3, // 每个顶点的分量数(x, y, z)
GLES20.GL_FLOAT, // 数据类型
false, // 是否归一化
0, // 步长(0 表示紧密排列)
vertexBuffer // 数据缓冲
)
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)
// 禁用顶点属性
GLES20.glDisableVertexAttribArray(aPositionLocation)
}
private fun loadShader(type: Int, shaderCode: String): Int {
val shader = GLES20.glCreateShader(type)
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
}
🎨 练习任务
基础任务
-
运行代码,看到橙色三角形
- 在 MainActivity 点击"Day 02"
- 确认能看到一个橙色三角形
-
修改三角形颜色
- 在片段着色器中修改
gl_FragColor的值 - 尝试:红色 (1, 0, 0, 1)、绿色 (0, 1, 0, 1)、蓝色 (0, 0, 1, 1)
- 在片段着色器中修改
-
修改三角形形状
- 修改顶点坐标,改变三角形的大小和位置
- 尝试创建不同形状的三角形
进阶任务
-
渲染多个三角形
- 增加顶点数据,绘制两个三角形
- 提示:6 个顶点,每 3 个组成一个三角形
-
渐变色三角形
- 为每个顶点添加颜色属性
- 使用 varying 变量传递颜色
- 实现顶点颜色插值
-
绘制正方形
- 使用两个三角形拼接成正方形
- 思考:为什么需要 6 个顶点?
📖 重要概念总结
OpenGL ES 核心 API(新学的)
| API | 说明 |
|---|---|
glCreateShader(type) |
创建着色器对象 |
glShaderSource(shader, source) |
上传着色器源代码 |
glCompileShader(shader) |
编译着色器 |
glCreateProgram() |
创建程序对象 |
glAttachShader(program, shader) |
附加着色器到程序 |
glLinkProgram(program) |
链接程序 |
glUseProgram(program) |
使用程序 |
glGetAttribLocation(program, name) |
获取 attribute 位置 |
glEnableVertexAttribArray(location) |
启用顶点属性数组 |
glVertexAttribPointer(...) |
指定顶点属性数据 |
glDrawArrays(mode, first, count) |
绘制图元 |
关键术语
- Vertex Shader:顶点着色器
- Fragment Shader:片段着色器
- GLSL:OpenGL 着色语言
- Attribute:顶点属性
- Uniform:统一变量
- Varying:易变变量
- VBO:顶点缓冲对象
❓ 常见问题
Q1: 为什么看不到三角形?
检查清单:
- 着色器编译链接成功?
- 调用了
glUseProgram(program)? - 顶点坐标在 [-1, 1] 范围内?
- 调用了
glEnableVertexAttribArray? - Alpha 值设置为 1.0?
Q2: 三角形方向反了怎么办?
OpenGL 默认使用逆时针顺序定义正面。调整顶点顺序即可。
Q3: ByteBuffer 为什么要 allocateDirect?
allocate():在 JVM 堆上分配,会被 GC 管理allocateDirect():在本地内存分配,OpenGL 可直接访问
Q4: glVertexAttribPointer 的参数是什么意思?
kotlin
glVertexAttribPointer(
location, // attribute 的位置
size, // 每个顶点几个分量(2/3/4)
type, // 数据类型(GL_FLOAT等)
normalized, // 是否归一化
stride, // 步长(字节)
buffer // 数据缓冲
)
🔗 扩展阅读
✅ 今日小结
今天我们:
- ✅ 理解了 OpenGL 渲染管线的流程
- ✅ 学习了着色器的基本概念和 GLSL 语法
- ✅ 掌握了顶点数据的创建和传递
- ✅ 编写了第一个着色器程序
- ✅ 成功渲染了一个三角形