18、OpenGL ES纹理渲染:纹理概念、纹理坐标、加载纹理、纹理映射到SurfaceView

纹理渲染,说白了就是把一张图片贴到3D模型上。你想想看,如果所有物体都是纯色的,那画面得多单调?纹理就是给图形世界穿上衣服的过程。

我个人习惯把纹理理解成「GPU能读懂的图片」。它不像我们平时看到的JPG或PNG,而是经过特殊处理后的像素数据。嗯,这里要注意,纹理在GPU里是以二维数组的形式存在的,每个元素就是一个像素的颜色值。

纹理概念:从图片到纹理的转变

纹理本质上就是一张图像数据。但为什么我们不直接叫它「图片」呢?因为纹理在图形管线中有特殊的用途和限制。

我在项目中遇到过一个问题:加载了一张高清照片,结果渲染出来是花的。后来才发现,纹理的宽高必须是2的幂次方,比如256、512、1024。虽然现在的OpenGL ES 3.0已经放宽了这个限制,但为了兼容性,我建议你还是尽量遵守这个规则。

纹理的核心属性:
  • 纹素:纹理中的最小单位,相当于像素
  • 纹理层级:Mipmap链,从大到小的一组纹理
  • 纹理格式:RGB、RGBA、Luminance等
  • 纹理环绕方式:重复、镜像、截取等

纹理坐标:贴图的位置信息

纹理坐标,也叫UV坐标。它决定了纹理的哪一部分被贴到模型的哪个位置。

纹理坐标的范围是0.0到1.0。为什么不是像素坐标?因为这样设计可以跟纹理的分辨率解耦。你换一张更高清的图片,坐标不用改,效果自动变好。

坐标轴 范围 说明
S (U) 0.0 - 1.0 水平方向,从左到右
T (V) 0.0 - 1.0 垂直方向,从下到上

注意,纹理坐标的Y轴方向跟屏幕坐标是相反的。屏幕坐标Y轴向下,纹理坐标Y轴向上。我曾经因为这个搞反了,结果贴图上下颠倒,排查了半天才发现是坐标写反了。

加载纹理:把图片数据送入GPU

加载纹理,说白了就是把Bitmap数据传给OpenGL ES。这个过程有几个关键步骤:

  1. 生成纹理对象:glGenTextures()
  2. 绑定纹理:glBindTexture()
  3. 设置纹理参数:环绕方式、过滤方式
  4. 上传纹理数据:glTexImage2D()
  5. 生成Mipmap:glGenerateMipmap()
// 加载纹理的核心代码
int[] textureIds = new int[1];
GLES20.glGenTextures(1, textureIds, 0);
int textureId = textureIds[0];

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

// 设置纹理参数
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, 
    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, 
    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, 
    GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, 
    GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);

// 上传纹理数据
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
加载纹理时,记得把Bitmap的配置设为ARGB_8888。我见过有人用RGB_565,结果纹理颜色失真严重。另外,纹理加载最好放在后台线程,避免阻塞UI。

纹理映射:把纹理贴到SurfaceView上

纹理映射,就是把纹理坐标跟顶点坐标对应起来。每个顶点都有一个纹理坐标,GPU会根据这些坐标从纹理中采样颜色。

举个例子,一个矩形有4个顶点,对应的纹理坐标就是矩形的四个角:

// 顶点坐标
float[] vertices = {
    -0.5f,  0.5f,  // 左上
    -0.5f, -0.5f,  // 左下
     0.5f, -0.5f,  // 右下
     0.5f,  0.5f   // 右上
};

// 纹理坐标
float[] texCoords = {
    0.0f, 0.0f,  // 左上
    0.0f, 1.0f,  // 左下
    1.0f, 1.0f,  // 右下
    1.0f, 0.0f   // 右上
};

在片段着色器里,我们通过纹理采样器来获取颜色:

// 片段着色器
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;

void main() {
    gl_FragColor = texture2D(uTexture, vTexCoord);
}
纹理坐标跟顶点坐标的对应关系一定要正确。我曾经犯过一个错误:顶点顺序是顺时针,纹理坐标却是逆时针,结果纹理显示不出来。检查了半天才发现是坐标对应错了。

纹理渲染的完整流程

把纹理渲染到SurfaceView上,整个流程是这样的:

  1. 创建SurfaceView,获取SurfaceHolder
  2. 创建EGL环境,绑定到Surface
  3. 编译着色器程序
  4. 加载纹理数据
  5. 设置顶点和纹理坐标
  6. 绘制纹理
  7. 交换缓冲区

嗯,这里要注意,纹理渲染是在单独的渲染线程中完成的,不能在主线程操作OpenGL ES。

纹理渲染完整流程 创建SurfaceView 获取SurfaceHolder 创建EGL环境 绑定到Surface 编译着色器 顶点+片段着色器 加载纹理 Bitmap → GPU 设置坐标 顶点+纹理坐标 绘制纹理 glDrawArrays 交换缓冲区 循环渲染

纹理渲染是视频播放、图像处理的基础。掌握了纹理的概念和映射方式,你就能在SurfaceView上展示各种视觉效果了。

纹理渲染的性能优化:
  • 使用纹理图集,减少纹理切换
  • 合理设置纹理过滤方式,平衡质量和性能
  • 避免频繁更新纹理数据
  • 使用VBO减少CPU-GPU数据传输

好了,纹理渲染的核心内容就这些。记住,纹理就是GPU能读懂的图片,纹理坐标就是贴图的位置,加载纹理就是把数据送入GPU,纹理映射就是把纹理贴到模型上。这四个概念搞清楚了,纹理渲染你就掌握了。


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