OpenGL ES 纹理以及纹理的映射
文章目录
- 开启纹理
- 创建纹理
- 绑定纹理
- 生成纹理
- 纹理坐标
- 图像配置
- 线性插值
- 重复效果
- 限制拉伸
- 完整代码
在 Android OpenGL ES 中使用纹理(Texture)可以显著提升图形渲染的质量和效率。以下是使用纹理的主要好处:
-
增强视觉真实感
纹理可以将复杂的图像细节(如皮肤、木纹、砖石等)映射到简单的几何模型上,避免为每个细节创建复杂的几何体,从而用较低的计算成本实现高度真实的视觉效果。 -
减少内存占用
通过纹理复用,可以避免为相似的物体重复定义顶点数据。
使用同一张纹理图渲染多个相同的物体(如树木、建筑)
利用纹理压缩技术(如 ETC、ASTC)进一步减少内存占用 -
提高渲染性能
纹理在 GPU 中以高度优化的方式存储和处理,比动态生成的图形更高效。例如:
使用预渲染的纹理代替实时计算(如阴影、光照效果)
利用 Mipmapping 技术优化远处物体的纹理采样 -
实现复杂特效
纹理不仅可以用于静态图像,还能实现各种高级特效:
环境映射(Environment Mapping):模拟反射效果(如镜面、金属表面)
法线贴图(Normal Mapping):通过纹理模拟表面细节,增加立体感
程序纹理(Procedural Textures):动态生成纹理(如火焰、烟雾)
多重纹理(Multi-texturing):叠加多个纹理创建复合效果(如地形 + 植被) -
简化模型复杂度
使用纹理可以将细节从几何模型转移到纹理图像中,从而:
减少顶点数量,降低 GPU 处理负担
简化模型设计流程,提高开发效率
支持更复杂的视觉效果,而不增加渲染负担 -
跨平台兼容性
OpenGL ES 纹理机制在不同设备上具有良好的一致性,使得:
纹理资源可以在不同 Android 设备间复用
便于将应用移植到其他支持 OpenGL ES 的平台 -
灵活的纹理控制
通过调整纹理参数,可以实现丰富的视觉变化:
过滤模式(Filtering):控制纹理缩放时的质量(如线性插值、最近邻采样)
环绕模式(Wrapping):定义纹理坐标超出 [0,1] 范围时的行为(重复、镜像等)
纹理混合(Blending):将纹理与颜色或其他纹理组合
开启纹理
为了使用纹理,需要开启一些开关启动一些功能。
- 打开2D贴图
激活 OpenGL ES 上下文的 2D 纹理功能
允许后续的纹理操作(如绑定纹理、设置纹理参数)生效
确保在绘制时使用纹理坐标对纹理进行采样gl.glEnable(GL10.GL_TEXTURE_2D)
- 打开混色
在 Android OpenGL ES 中,gl.glEnable(GL10.GL_BLEND)
用于启用颜色混合功能,这是实现半透明效果、粒子系统、纹理叠加等视觉效果的关键技术。gl.glEnable(GL10.GL_BLEND)
- 设置混色
下面方法是 Android OpenGL ES 中用于设置标准透明度混合(Alpha Blending)的核心方法。
这个设置允许你实现半透明效果,让新绘制的物体能够以透明方式与背景融合。gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA)
- OpenGL 支持的混色方案如下
因子 | 描述 |
---|---|
GL_ZERO | 因子 = (0, 0, 0, 0) |
GL_ONE | 因子 = (1, 1, 1, 1) |
GL_SRC_COLOR | 因子 = (Rs, Gs, Bs, As) (源颜色) |
GL_ONE_MINUS_SRC_COLOR | 因子 = (1-Rs, 1-Gs, 1-Bs, 1-As) (1 - 源颜色) |
GL_DST_COLOR | 因子 = (Rd, Gd, Bd, Ad) (目标颜色) |
GL_ONE_MINUS_DST_COLOR | 因子 = (1-Rd, 1-Gd, 1-Bd, 1-Ad) (1 - 目标颜色) |
GL_SRC_ALPHA | 因子 = (As, As, As, As) (源 Alpha 值) |
GL_ONE_MINUS_SRC_ALPHA | 因子 = (1-As, 1-As, 1-As, 1-As) (1 - 源 Alpha 值) |
GL_DST_ALPHA | 因子 = (Ad, Ad, Ad, Ad) (目标 Alpha 值) |
GL_ONE_MINUS_DST_ALPHA | 因子 = (1-Ad, 1-Ad, 1-Ad, 1-Ad) (1 - 目标 Alpha 值) |
通过合理选择混色方案,你可以在 Android OpenGL ES 应用中实现从简单半透明到复杂特效的各种视觉效果。
创建纹理
// 存储纹理IDprivate val texture = IntArray(1)// 用于生成纹理ID是临时存储val textureArray = IntArray(1)// 生成纹理ID,存储到textureArray 中。gl.glGenTextures(1, textureArray, 0)// 保存ID到成员变量,后续使用texture[0] = textureArray[0]
生成纹理:glGenTextures 是 OpenGL 的核心函数,用于创建纹理对象
第一个参数 1:表示生成 1 个纹理
第二个参数 textureArray:存储生成的纹理 ID
第三个参数 0:表示从数组的第 0 个位置开始存储
绑定纹理
-
纹理绑定的基本概念
纹理绑定是指将一个纹理对象(通过 glGenTextures 生成)关联到当前 OpenGL 上下文的过程。绑定后,所有对纹理的操作(如设置参数、上传数据)都将作用于这个纹理对象。OpenGL ES 使用纹理单元(Texture Unit)机制来支持同时使用多个纹理。每个纹理单元可以绑定一个纹理对象,通过 glActiveTexture 切换当前使用的纹理单元。
纹理绑定本质
- 将一个纹理对象(由 textureId 标识)关联到当前的纹理目标(如 GL_TEXTURE_2D)
- 状态覆盖:每次调用 glBindTexture 都会覆盖之前的绑定状态
- 最终生效:在绘制时,OpenGL 使用的是最后一次绑定的纹理
- 绑定纹理函数
gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])
第一个参数:纹理目标:指定纹理的类型,常见的有:
GL_TEXTURE_2D:二维纹理(最常用)
GL_TEXTURE_CUBE_MAP:立方体贴图(用于天空盒、反射等)
GL_TEXTURE_3D:三维纹理(OpenGL ES 3.0+ 支持)
第二个参数:纹理 ID:通过 glGenTextures 生成的唯一标识符,代表一个纹理对象
- 为什么需要绑定纹理?
OpenGL 使用状态机模型,所有操作都作用于当前绑定的对象。
绑定纹理的主要目的是:
- 设置纹理参数:在绑定时设置过滤模式、环绕模式等
- 上传纹理数据:将图像数据(如 Bitmap)加载到纹理中
- 在绘制时使用纹理:确保正确的纹理被应用到几何体上
生成纹理
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmapTexture, 0)
该方法参数如下:
public static void texImage2D(int target, // 纹理目标类型int level, // Mipmap 级别Bitmap bitmap, // 源 Bitmap 对象int border // 边框宽度(必须为 0)
)
- 对bitmap的尺寸要求
2 的幂尺寸:传统 OpenGL ES 1.x 和部分设备要求纹理尺寸必须是 2 的幂(如 64×64、256×512)。若使用非 2 的幂(NPOT)纹理,可能需要额外设置纹理参数.
OpenGL ES 2.0+ 支持:现代设备通常支持任意尺寸(需检查 GL_MAX_TEXTURE_SIZE),但为兼容性建议使用 2 的幂尺寸。
纹理坐标
在 OpenGL ES 中,纹理坐标(Texture Coordinates)用于将 2D 图像(纹理)映射到 3D 模型的表面。理解纹理坐标的工作原理对实现正确的纹理映射至关重要。
-
坐标系范围:纹理坐标使用标准化的 (s, t) 坐标系统,范围从 (0, 0) 到 (1, 1)。
- s:水平方向(类似屏幕的 X 轴)。
- t:垂直方向(类似屏幕的 Y 轴)。
-
原点位置:(0, 0) 位于纹理的左下角,(1, 1) 位于右上角。
-
与屏幕坐标的差异:屏幕坐标通常以左上角为原点,而 OpenGL 纹理坐标以左下角为原点,这可能导致纹理显示时上下颠倒(需特别处理)。
-
纹理坐标与顶点坐标的映射
- 纹理坐标需要与顶点坐标一一对应,以确定纹理如何被拉伸或扭曲到模型表面。以下是常见形状的映射示例:
图中是一个正方形,对应原点为中心点,长度为1,对应的纹理坐标,可见左下角顶点坐标(-1,-1,0) 对应的纹理坐标的(0,0)
- 纹理坐标需要与顶点坐标一一对应,以确定纹理如何被拉伸或扭曲到模型表面。以下是常见形状的映射示例:
顶点坐标 纹理坐标
(-1, 1, 0) ------------ (0, 1)| || || |
(-1,-1, 0) ------------ (0, 0)------------(1,-1, 0) (1, 0)|||------------(1, 1, 0) (1, 1)
对应代码如下:
// 顶点坐标(x, y, z)
float[] vertices = {-1f, 1f, 0f, // 左上-1f, -1f, 0f, // 左下1f, -1f, 0f, // 右下1f, 1f, 0f // 右上
};// 纹理坐标(s, t)
float[] texCoords = {0f, 1f, // 左上0f, 0f, // 左下1f, 0f, // 右下1f, 1f // 右上
};
图像配置
线性插值
在 OpenGL ES 中,纹理的线性插值(Linear Interpolation) 是一种纹理过滤技术,用于在纹理被缩放时生成平滑的视觉效果。与最近邻采样(GL_NEAREST)相比,线性插值通过计算相邻纹理像素(纹素)的加权平均值来减少锯齿和马赛克现象,尤其适用于纹理被缩小或旋转的场景。
-
最近邻采样(Nearest Neighbor):
- 直接选择离采样点最近的单个纹素值。
- 优点:性能高,适合像素艺术风格。
- 缺点:纹理缩放时会产生明显锯齿或马赛克。
-
线性插值(Bilinear Filtering):
- 在水平和垂直方向各取 2×2 个纹素,计算采样点周围纹素的加权平均值。
- 优点:平滑过渡,减少锯齿。
- 缺点:模糊化(尤其在大幅缩小时),性能略低于最近邻。
-
启用线性插值
- 通过 glTexParameteri() 设置纹理过滤模式:
// 绑定纹理后设置过滤模式
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);// 设置缩小过滤为线性插值(纹理被缩小时)
gl.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);// 设置放大过滤为线性插值(纹理被放大时)
gl.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
**GL_LINEAR 代表OpenGL采用简单的线性插值方式调整图像。**
为了将图标贴到正方体的一个面,绘制时根据面来设置纹理,最后会贴出全部代码。增加纹理部分如下:
private fun setTexture(gl: GL10) {gl.glEnable(GL10.GL_TEXTURE_2D)gl.glEnable(GL10.GL_BLEND)gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA)val textureArray = IntArray(1)gl.glGenTextures(1, textureArray, 0)texture[0] = textureArray[0]// 绑定纹理gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])// 当需要缩小时采用线性插值方式gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR)// 当需要放大时采用线性插值的方式gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR)GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmapTexture, 0)}
绘制时采用纹理的代码如下(只针对其中一个面):
// 目标面启用纹理gl.glEnable(GL10.GL_TEXTURE_2D)gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY)gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textCoordsSquare)
public void glTexCoordPointer(int size, // 每个纹理坐标的分量数int type, // 数据类型int stride, // 相邻两个纹理坐标之间的字节偏移量java.nio.Buffer pointer // 数据缓冲区
)
-
size(每个纹理坐标的分量数)取值:必须为 2、3 或 4。
- 2表示 (s, t) 坐标(最常用)。
- 3表示 (s, t, r) 坐标(用于 3D 纹理,OpenGL ES 中较少使用)。
- 4表示 (s, t, r, q) 坐标(用于齐次坐标,OpenGL ES 中极少使用)。
-
pointer(数据缓冲区)
- 类型:java.nio.Buffer(通常为 FloatBuffer)。
- 含义:存储纹理坐标数据的缓冲区,必须为直接缓冲区(Direct Buffer)。
绘制效果如下:Android的Logo。
备注:如果一个四边形的图片通过纹理的方式贴到三角形上,则选取正方形的三个角的纹理点,对应三角形坐标点。选取的三角形部分会绘制到对应三角形上。
重复效果
先将纹理坐标超过 1.0
// 纹理坐标(左下角为(0,0),右上角为(1,1))private val textureCoords = floatArrayOf(0.0f, 2f, // 左下 V02f, 2f, // 右下 V12f, 0.0f, // 右上 V20.0f, 0.0f // 左上 V3)
然后增加设置重复设置
// 设置纹理环绕模式(重复)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT)
效果如下:
GL_REPEAT:纹理会在超出部分重复平铺。例如,纹理坐标为 1.5 时,会使用 0.5 处的纹理数据,从而实现纹理在整个表面上重复铺设的效果,常用于制作无缝纹理背景。
限制拉伸
OpenGLES可以设置简单地将纹理坐标超过1.0的值限制为1.0,任何低于0.0的值限制为0.0。这实际会引起边沿像素重复。
将重复设置修改为限制拉伸设置
gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE)
效果如下:只显示了左上角一个图标。
完整代码
class SquareWithTextureRenderer(private val context: Context) : GLSurfaceView.Renderer {private val vertexBuffer: FloatBufferprivate val indexBuffer: ShortBufferprivate val textureCoordsBuffer: FloatBufferprivate val normalsBuffer: FloatBufferprivate val texture: IntArrayprivate val mBitmapTexture: Bitmapprivate var angle = 0f// 正方形的4个顶点坐标(中心在原点,边长为1)private val vertices = floatArrayOf(-0.5f, -0.5f, 0.0f, // 左下 V00.5f, -0.5f, 0.0f, // 右下 V10.5f, 0.5f, 0.0f, // 右上 V2-0.5f, 0.5f, 0.0f // 左上 V3)// 两个三角形组成正方形的顶点索引private val indices = shortArrayOf(0, 1, 2, // 第一个三角形2, 3, 0 // 第二个三角形)// 正方形顶点的法线(全部朝向屏幕外,即Z轴正方向)private val normals = floatArrayOf(0.0f, 0.0f, 1.0f, // 所有顶点法线相同0.0f, 0.0f, 1.0f,0.0f, 0.0f, 1.0f,0.0f, 0.0f, 1.0f)// 纹理坐标(左下角为(0,0),右上角为(1,1))private val textureCoords = floatArrayOf(0.0f, 2f, // 左下 V02f, 2f, // 右下 V12f, 0.0f, // 右上 V20.0f, 0.0f // 左上 V3)init {// 初始化顶点缓冲区val vbb = ByteBuffer.allocateDirect(vertices.size * 4)vbb.order(ByteOrder.nativeOrder())vertexBuffer = vbb.asFloatBuffer()vertexBuffer.put(vertices)vertexBuffer.position(0)// 初始化索引缓冲区val ibb = ByteBuffer.allocateDirect(indices.size * 2)ibb.order(ByteOrder.nativeOrder())indexBuffer = ibb.asShortBuffer()indexBuffer.put(indices)indexBuffer.position(0)// 初始化纹理坐标缓冲区val tcb = ByteBuffer.allocateDirect(textureCoords.size * 4)tcb.order(ByteOrder.nativeOrder())textureCoordsBuffer = tcb.asFloatBuffer()textureCoordsBuffer.put(textureCoords)textureCoordsBuffer.position(0)// 初始化法线缓冲区val nb = ByteBuffer.allocateDirect(normals.size * 4)nb.order(ByteOrder.nativeOrder())normalsBuffer = nb.asFloatBuffer()normalsBuffer.put(normals)normalsBuffer.position(0)// 加载纹理图片texture = IntArray(1)mBitmapTexture = BitmapFactory.decodeResource(context.resources, R.drawable.ic_l)}override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f)gl.glEnable(GL10.GL_DEPTH_TEST)gl.glEnableClientState(GL10.GL_VERTEX_ARRAY)gl.glEnableClientState(GL10.GL_NORMAL_ARRAY)setupLight(gl)setMaterial(gl)setTexture(gl)}private fun setTexture(gl: GL10) {gl.glEnable(GL10.GL_TEXTURE_2D)gl.glEnable(GL10.GL_BLEND)gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA)val textureArray = IntArray(1)gl.glGenTextures(1, textureArray, 0)texture[0] = textureArray[0]// 绑定纹理gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])// 设置纹理过滤gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR)// 设置纹理环绕模式
// gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT)
// gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE)// 加载纹理GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmapTexture, 0)mBitmapTexture.recycle() // 纹理加载后回收Bitmap}override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {gl.glViewport(0, 0, width, height)gl.glMatrixMode(GL10.GL_PROJECTION)gl.glLoadIdentity()val aspectRatio = width.toFloat() / heightGLU.gluPerspective(gl, 45.0f, aspectRatio, 0.1f, 1000.0f)gl.glMatrixMode(GL10.GL_MODELVIEW)gl.glLoadIdentity()}override fun onDrawFrame(gl: GL10) {gl.glClear(GL10.GL_COLOR_BUFFER_BIT or GL10.GL_DEPTH_BUFFER_BIT)gl.glLoadIdentity()gl.glShadeModel(GL10.GL_SMOOTH)gl.glTranslatef(0.0f, 0f, -3.0f) // 拉近正方形,使其更清晰
// gl.glRotatef(angle, 0.0f, 0.0f, 1.0f) // 绕Z轴旋转// angle += 1.0f // 旋转角度递增// 设置顶点、法线和纹理坐标gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer)gl.glNormalPointer(GL10.GL_FLOAT, 0, normalsBuffer)gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY)gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureCoordsBuffer)// 启用纹理gl.glEnable(GL10.GL_TEXTURE_2D)gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])// 绘制正方形(两个三角形)gl.glDrawElements(GL10.GL_TRIANGLES, indices.size,GL10.GL_UNSIGNED_SHORT, indexBuffer)// 禁用纹理和缓冲区gl.glDisable(GL10.GL_TEXTURE_2D)gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY)}/*** 设置光效*/private fun setupLight(gl: GL10) {// 启用光照gl.glEnable(GL10.GL_LIGHTING)gl.glEnable(GL10.GL_LIGHT0)gl.glEnable(GL10.GL_SPECULAR)// 设置环境光val ambientLight = floatArrayOf(0.2f, 0.2f, 0.2f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientLight, 0)// 设置漫反射光val diffuseLight = floatArrayOf(0.8f, 0.8f, 0.8f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseLight, 0)// 设置高光val specularLight = floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularLight, 0)// 设置光源位置(位于正方形前方)val lightPosition = floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightPosition, 0)}private fun setMaterial(gl: GL10) {// 环境元素val ambientLight = floatArrayOf(0f, 0.1f, 0.9f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientLight, 0)// 散射元素val diffuseLight = floatArrayOf(0.9f, 0f, 0.1f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffuseLight, 0)// 高光元素val specularLight = floatArrayOf(0.9f, 0.9f, 0f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularLight, 0)// 反光度gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 25F)// 自发光val emissionLight = floatArrayOf(0.0f, 0.4f, 0f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, emissionLight, 0)}
}