12、OpenGL ES基础:管线、着色器语言、纹理映射

说实话,做音视频开发这么多年,我见过不少朋友一提到OpenGL ES就头疼。觉得它门槛高、概念抽象、代码又长。其实你想想看,OpenGL ES说白了就是一套给GPU下指令的接口。你告诉它怎么画、画什么,它帮你把像素渲染到屏幕上。今天这一章,我们就把它拆开揉碎了讲清楚。

12.1 OpenGL ES管线:数据是怎么变成画面的?

先问大家一个问题:你手机里的一张图片,或者一段视频帧,是怎么变成屏幕上那个五彩斑斓的像素点的?

答案就是——图形管线。我习惯把它想象成一条流水线。原始数据从一端进去,经过一道道工序,最后从另一端出来一张完整的画面。

核心管线阶段(简化版):

  • 顶点着色器:处理每个顶点的位置、颜色。说白了就是确定“点在哪”。
  • 图元装配:把顶点连成三角形、线段等基本图形。
  • 光栅化:把图形拆成一个个像素碎片。
  • 片段着色器:决定每个像素最终的颜色。
  • 逐片元操作:深度测试、模板测试、混合,最后写入帧缓冲区。

我在项目中遇到过一个问题:视频渲染出来画面是颠倒的。排查了半天,发现是顶点坐标的Y轴方向搞反了。嗯,这种坑踩过一次就记住了。

OpenGL ES 图形管线流程 顶点数据 (坐标/颜色/纹理) 顶点着色器 (坐标变换) 图元装配 (三角形/线段) 光栅化 (像素碎片) 片段着色器 (像素着色) 逐片元操作 (深度/模板/混合) 帧缓冲区 (最终画面) 逐片元

我的小建议:刚开始学管线,别死记硬背每个阶段。你只需要记住两个关键点:顶点着色器管“形状”,片段着色器管“颜色”。其他阶段OpenGL帮你自动处理了。

12.2 着色器语言(GLSL):写给GPU的小作文

GLSL全称是OpenGL Shading Language。我把它理解为写给GPU的“小作文”。你写一段代码,告诉GPU怎么处理顶点、怎么给像素上色。

来看一个最简单的顶点着色器:

// 顶点着色器
attribute vec4 aPosition;  // 输入:顶点坐标
void main() {
    gl_Position = aPosition;  // 直接输出坐标
}

再看对应的片段着色器:

// 片段着色器
precision mediump float;  // 设置精度
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);  // 输出红色
}

这两段代码加起来,就能在屏幕上画一个红色的三角形。是不是比想象中简单?

GLSL 核心语法速览:

类型说明示例
vec2/vec3/vec42/3/4维向量vec4(1.0, 0.0, 0.0, 1.0)
mat2/mat3/mat42x2/3x3/4x4矩阵mat4(1.0)
sampler2D2D纹理采样器uniform sampler2D uTexture;
attribute顶点属性(每个顶点不同)attribute vec4 aPosition;
uniform全局常量(所有顶点相同)uniform mat4 uMatrix;
varying从顶点着色器传到片段着色器varying vec2 vTexCoord;

我曾经犯过一个低级错误:在片段着色器里忘了写 precision mediump float;。结果在某些手机上渲染出来全是花屏。嗯,这种坑踩过一次就长记性了。

12.3 纹理映射:把图片贴到模型上

纹理映射,说白了就是把一张图片“贴”到3D模型表面。比如你做一个地球仪,总不能手动画每个国家的轮廓吧?直接把世界地图贴上去就行了。

纹理映射的核心是——纹理坐标。每个顶点对应一个纹理坐标(u, v),范围是0.0到1.0。

  • (0, 0) 对应图片左下角
  • (1, 1) 对应图片右上角
  • (0.5, 0.5) 对应图片正中心

来看一个带纹理的顶点着色器:

attribute vec4 aPosition;
attribute vec2 aTexCoord;  // 纹理坐标
varying vec2 vTexCoord;    // 传给片段着色器

void main() {
    gl_Position = aPosition;
    vTexCoord = aTexCoord;
}

片段着色器:

precision mediump float;
uniform sampler2D uTexture;  // 纹理采样器
varying vec2 vTexCoord;

void main() {
    gl_FragColor = texture2D(uTexture, vTexCoord);
}

你想想看,有了纹理映射,视频播放器里的每一帧画面,本质上就是一张不断更新的纹理。我们只需要把解码后的YUV数据上传到纹理,然后渲染到一个矩形上,视频就播放出来了。

注意:纹理坐标和屏幕坐标的Y轴是相反的。如果你发现渲染出来的画面是倒的,试试把纹理坐标的Y值取反:vTexCoord.y = 1.0 - vTexCoord.y;

12.4 实战:在Android上画一个带纹理的矩形

光说不练假把式。我们来看看在Android上怎么用OpenGL ES画一个带纹理的矩形。

核心步骤就这几步:

  1. 创建GLSurfaceView,设置OpenGL ES版本
  2. 编写顶点和片段着色器
  3. 创建纹理,加载图片数据
  4. 设置顶点坐标和纹理坐标
  5. 在onDrawFrame中绘制

关键代码片段:

// 顶点坐标(矩形四个角)
float[] vertexData = {
    -0.5f, -0.5f,  // 左下
     0.5f, -0.5f,  // 右下
    -0.5f,  0.5f,  // 左上
     0.5f,  0.5f   // 右上
};

// 纹理坐标(对应四个角)
float[] texCoordData = {
    0.0f, 0.0f,  // 左下
    1.0f, 0.0f,  // 右下
    0.0f, 1.0f,  // 左上
    1.0f, 1.0f   // 右上
};

// 绘制时使用三角形条带
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

避坑指南:我曾经在纹理加载时忘了设置 GLES20.GL_TEXTURE_MIN_FILTER,结果纹理显示出来全是锯齿。加上 GL_LINEAR_MIPMAP_LINEAR 就好了。另外,纹理尺寸最好是2的幂次方(如256x256、512x512),否则某些老设备会出问题。

12.5 本章小结

这一章我们聊了OpenGL ES的三大基础:管线、着色器、纹理映射。管线是骨架,着色器是灵魂,纹理映射是血肉。三者结合起来,你就能在Android上渲染出任何画面了。

我个人觉得,学OpenGL ES最好的方式就是动手写代码。别怕出错,出错了才能记住。下一章我们会把这些知识用到视频播放器里,真正实现视频画面的渲染。


公众号:蓝海资料掘金营,微信deep3321