前言: 『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 语法
 - ✅ 掌握了顶点数据的创建和传递
 - ✅ 编写了第一个着色器程序
 - ✅ 成功渲染了一个三角形