『OpenGL学习滤镜相机』- Day2: 渲染第一个三角形

前言: 『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
    }
}

🎨 练习任务

基础任务

  1. 运行代码,看到橙色三角形

    • 在 MainActivity 点击"Day 02"
    • 确认能看到一个橙色三角形
  2. 修改三角形颜色

    • 在片段着色器中修改 gl_FragColor 的值
    • 尝试:红色 (1, 0, 0, 1)、绿色 (0, 1, 0, 1)、蓝色 (0, 0, 1, 1)
  3. 修改三角形形状

    • 修改顶点坐标,改变三角形的大小和位置
    • 尝试创建不同形状的三角形

进阶任务

  1. 渲染多个三角形

    • 增加顶点数据,绘制两个三角形
    • 提示:6 个顶点,每 3 个组成一个三角形
  2. 渐变色三角形

    • 为每个顶点添加颜色属性
    • 使用 varying 变量传递颜色
    • 实现顶点颜色插值
  3. 绘制正方形

    • 使用两个三角形拼接成正方形
    • 思考:为什么需要 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         // 数据缓冲
)

🔗 扩展阅读

✅ 今日小结

今天我们:

  1. ✅ 理解了 OpenGL 渲染管线的流程
  2. ✅ 学习了着色器的基本概念和 GLSL 语法
  3. ✅ 掌握了顶点数据的创建和传递
  4. ✅ 编写了第一个着色器程序
  5. ✅ 成功渲染了一个三角形

下一篇

相关推荐
风语者日志7 小时前
[LitCTF 2023]这是什么?SQL !注一下 !
android·数据库·sql
2501_915921438 小时前
iOS 26 CPU 使用率监控策略 多工具协同构建性能探索体系
android·ios·小程序·https·uni-app·iphone·webview
狂团商城小师妹8 小时前
JAVA国际版同城打车源码同城服务线下结账系统源码适配PAD支持Android+IOS+H5
android·java·ios·小程序·交友
游戏开发爱好者88 小时前
iOS 应用逆向对抗手段,多工具组合实战(iOS 逆向防护/IPA 混淆/无源码加固/Ipa Guard CLI 实操)
android·ios·小程序·https·uni-app·iphone·webview
虚伪的空想家8 小时前
ip网段扫描机器shell脚本
android·linux·网络协议·tcp/ip·shell·脚本·network
generallizhong8 小时前
android TAB切换
android·gitee
00后程序员张9 小时前
iOS 文件管理与导出实战,多工具协同打造高效数据访问与调试体系
android·macos·ios·小程序·uni-app·cocoa·iphone
Boop_wu9 小时前
[MySQL] JDBC
android
qq_7174100110 小时前
FAQ09075:6572平台相机拍照,拍下来的照片无法查看,图库查看时提示“无缩略图”
android