OpenGL ES 深度剖析
一、OpenGL ES 概述
1.1 技术定位与应用场景
OpenGL ES(OpenGL for Embedded Systems)是专为嵌入式设备(如移动电话、PDA、游戏主机等)设计的图形API,它是OpenGL的子集,针对资源有限的设备进行了优化。OpenGL ES提供了一套跨平台的图形编程接口,允许开发者在不同的硬件平台上实现高效的2D和3D图形渲染。
从应用场景来看,OpenGL ES广泛应用于移动游戏开发、虚拟现实(VR)、增强现实(AR)、图形编辑器、医学图像处理、地理信息系统等领域。例如,在移动游戏中,OpenGL ES用于实现游戏场景的渲染、角色动画、特效等;在VR应用中,它用于实现沉浸式的3D环境渲染和交互。
架构图:
调用流程图:
类的关系图:
1.2 发展历程与版本演进
OpenGL ES的发展历程可以追溯到2003年,当时Khronos Group发布了OpenGL ES 1.0版本。此后,随着移动设备性能的不断提升和图形技术的不断发展,OpenGL ES也经历了多个版本的演进:
- OpenGL ES 1.x:基于固定功能管线,主要用于简单的2D和3D图形渲染,不支持可编程着色器。
- OpenGL ES 2.0:引入了可编程着色器模型,支持顶点着色器和片段着色器,大大增强了图形渲染的灵活性和表现力。
- OpenGL ES 3.0:在2.0的基础上增加了许多新特性,如纹理压缩、多渲染目标、实例化渲染等,提升了图形渲染的性能和效率。
- OpenGL ES 3.1:进一步扩展了功能,支持计算着色器、纹理存储、顶点数组对象等,为通用计算和高级图形效果提供了支持。
- OpenGL ES 3.2:增加了对曲面细分着色器、几何着色器等高级特性的支持,提升了图形渲染的质量和复杂度。
每个版本的更新都带来了新的功能和性能提升,同时也保持了向后兼容性,使得开发者可以根据目标设备的支持情况选择合适的版本进行开发。
架构图:
调用流程图:
类的关系图:
1.3 与其他图形API的对比
与其他图形API(如Direct3D、Vulkan、WebGL等)相比,OpenGL ES具有以下特点:
- 跨平台性:OpenGL ES是一种跨平台的图形API,支持多种操作系统(如iOS、Android、Windows、Linux等)和硬件平台,具有良好的兼容性。
- 轻量级设计:针对嵌入式设备的资源限制进行了优化,API设计简洁,内存占用少,运行效率高。
- 广泛的硬件支持:几乎所有的移动设备和嵌入式系统都支持OpenGL ES,开发者可以放心地使用它进行跨设备开发。
- 可编程着色器:从OpenGL ES 2.0开始支持可编程着色器,允许开发者通过编写自定义的着色器代码来实现复杂的图形效果。
- 成熟的生态系统:OpenGL ES已经存在了很长时间,拥有丰富的开发工具、文档和社区资源,开发者可以很容易地找到所需的帮助和支持。
然而,OpenGL ES也有一些不足之处,例如:
- API设计复杂:OpenGL ES的API设计相对复杂,学习曲线较陡,尤其是对于初学者来说可能会有一定的难度。
- 缺乏现代特性:与最新的图形API(如Vulkan、DirectX 12)相比,OpenGL ES在性能优化和底层硬件控制方面可能存在一定的局限性。
- 驱动兼容性问题:在某些设备上,OpenGL ES的驱动可能存在兼容性问题,导致应用程序出现渲染错误或性能下降。
架构图:
对比表:
特性 | OpenGL ES | Direct3D | Vulkan | WebGL |
---|---|---|---|---|
平台支持 | 跨平台(移动、嵌入式) | Windows | 跨平台(桌面、移动) | 网页浏览器 |
API类型 | 状态机模型 | 面向对象 | 显式控制 | 基于OpenGL ES |
着色器模型 | GLSL ES | HLSL | SPIR-V | GLSL ES |
性能 | 中等 | 高 | 极高 | 中等 |
学习曲线 | 较陡 | 较陡 | 极陡 | 中等 |
多线程支持 | 有限 | 良好 | 优秀 | 有限 |
底层硬件控制 | 有限 | 中等 | 优秀 | 有限 |
生态系统 | 成熟 | 成熟 | 发展中 | 成熟 |
类的关系图:
1.4 架构设计与核心组件
OpenGL ES的架构设计采用了分层的模型,主要由以下几个核心组件组成:
- 客户端(Client):客户端是应用程序运行的环境,包括CPU、内存和应用程序代码。客户端负责生成图形数据(如顶点坐标、纹理数据等),并通过OpenGL ES API将这些数据发送到服务器。
- 服务器(Server):服务器是图形硬件(GPU)及其驱动程序的抽象。服务器负责接收客户端发送的命令和数据,并执行实际的图形渲染操作。
- 命令队列(Command Queue):命令队列是客户端和服务器之间的通信桥梁。客户端将OpenGL ES命令放入命令队列中,服务器从命令队列中取出命令并执行。
- 状态机(State Machine):OpenGL ES使用状态机模型来管理渲染状态。应用程序可以设置各种状态参数(如着色器程序、纹理、变换矩阵等),这些状态参数会影响后续的渲染操作。
OpenGL ES的核心功能可以分为以下几个模块:
- 上下文管理:负责创建、管理和销毁OpenGL ES上下文,以及在上下文中设置和查询状态。
- 渲染管线:定义了从顶点数据到最终像素的处理流程,包括顶点处理、图元装配、光栅化、片段处理等阶段。
- 着色器系统:支持可编程着色器,允许开发者通过编写顶点着色器和片段着色器来控制渲染过程。
- 纹理管理:负责加载、创建和管理纹理数据,以及在渲染过程中使用纹理。
- 帧缓冲对象:提供了对渲染目标的灵活控制,允许将渲染结果输出到纹理或其他缓冲区。
- 同步机制:提供了各种同步机制,确保渲染操作按正确的顺序执行,并在需要时进行CPU和GPU之间的同步。
架构图:
调用流程图:
类的关系图:
二、OpenGL ES 上下文管理
2.1 上下文的基本概念
在OpenGL ES中,上下文(Context)是一个核心概念,它代表了一个OpenGL ES的运行环境。每个上下文都包含了OpenGL ES的所有状态信息,如当前的着色器程序、纹理、变换矩阵、渲染状态等。应用程序必须在一个上下文中执行OpenGL ES命令,才能进行图形渲染。
上下文的主要作用包括:
- 存储OpenGL ES的状态:上下文维护了OpenGL ES的所有状态信息,应用程序可以通过设置这些状态来控制渲染行为。
- 管理资源:上下文中管理了所有的OpenGL ES资源,如着色器程序、纹理、缓冲区对象等。这些资源只能在创建它们的上下文中使用。
- 执行命令:OpenGL ES命令必须在一个上下文中执行。每个命令都会影响当前上下文中的状态或资源。
在多线程环境中,每个线程可以有自己的上下文,或者多个线程可以共享同一个上下文。上下文之间的资源共享需要特别注意,因为某些资源(如纹理和缓冲区对象)可能不是线程安全的。
架构图:
调用流程图:
类的关系图:
2.2 上下文的创建与初始化
在不同的平台上,创建OpenGL ES上下文的方式可能有所不同。下面以Android平台为例,介绍OpenGL ES上下文的创建过程。
在Android平台上,通常使用EGL(Embedded-System Graphics Library)来创建和管理OpenGL ES上下文。EGL是一个底层的图形库,它提供了OpenGL ES与本地窗口系统之间的接口。
下面是一个在Android平台上创建OpenGL ES 3.0上下文的示例代码:
java
// 获取EGL显示
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (display == EGL14.EGL_NO_DISPLAY) {
// 处理错误
return;
}
// 初始化EGL
int[] version = new int[2];
if (!EGL14.eglInitialize(display, version, 0, version, 1)) {
// 处理错误
return;
}
// 配置EGL上下文属性
int[] configAttribs = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 16,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES3_BIT_KHR,
EGL14.EGL_NONE
};
// 选择EGL配置
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
if (!EGL14.eglChooseConfig(display, configAttribs, 0, configs, 0, 1, numConfigs, 0)) {
// 处理错误
return;
}
EGLConfig config = configs[0];
// 创建EGL上下文
int[] contextAttribs = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, // OpenGL ES 3.0
EGL14.EGL_NONE
};
EGLContext context = EGL14.eglCreateContext(display, config, EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
if (context == EGL14.EGL_NO_CONTEXT) {
// 处理错误
return;
}
// 创建EGL表面
EGLSurface surface = EGL14.eglCreateWindowSurface(display, config, surfaceHolder.getSurface(), null, 0);
if (surface == EGL14.EGL_NO_SURFACE) {
// 处理错误
return;
}
// 绑定上下文
if (!EGL14.eglMakeCurrent(display, surface, surface, context)) {
// 处理错误
return;
}
上述代码的主要步骤包括:
- 获取EGL显示 :通过
eglGetDisplay
函数获取与本地窗口系统的连接。 - 初始化EGL :调用
eglInitialize
函数初始化EGL库。 - 配置EGL上下文属性:设置所需的颜色缓冲区大小、深度缓冲区大小、渲染类型等属性。
- 选择EGL配置 :使用
eglChooseConfig
函数选择符合要求的EGL配置。 - 创建EGL上下文 :调用
eglCreateContext
函数创建OpenGL ES上下文。 - 创建EGL表面 :使用
eglCreateWindowSurface
函数创建与本地窗口关联的EGL表面。 - 绑定上下文 :通过
eglMakeCurrent
函数将上下文和表面绑定,使上下文成为当前上下文。
架构图:
调用流程图:
类的关系图:
2.3 上下文的状态管理
OpenGL ES上下文维护了大量的状态信息,这些状态信息控制着渲染过程的各个方面。应用程序可以通过各种OpenGL ES API来查询和修改这些状态。
上下文的状态可以分为以下几类:
- 着色器状态:包括当前使用的着色器程序、顶点属性、统一变量等。
- 纹理状态:包括当前绑定的纹理对象、纹理参数、纹理单元等。
- 变换状态:包括模型视图矩阵、投影矩阵、纹理矩阵等。
- 渲染状态:包括深度测试、模板测试、混合、裁剪等状态。
- 帧缓冲状态:包括当前绑定的帧缓冲对象、颜色附件、深度附件等。
下面是一些常见的状态查询和修改操作的示例:
java
// 查询当前上下文是否支持某个扩展
boolean isExtensionSupported = GLES30.glGetString(GLES30.GL_EXTENSIONS).contains("GL_OES_texture_compression_ETC1");
// 设置深度测试状态
GLES30.glEnable(GLES30.GL_DEPTH_TEST); // 启用深度测试
GLES30.glDepthFunc(GLES30.GL_LESS); // 设置深度比较函数
// 设置混合状态
GLES30.glEnable(GLES30.GL_BLEND); // 启用混合
GLES30.glBlendFunc(GLES30.GL_SRC_ALPHA, GLES30.GL_ONE_MINUS_SRC_ALPHA); // 设置混合函数
// 查询当前的视口
int[] viewport = new int[4];
GLES30.glGetIntegerv(GLES30.GL_VIEWPORT, viewport, 0);
// 设置视口
GLES30.glViewport(0, 0, width, height);
// 查询当前的着色器程序
int[] currentProgram = new int[1];
GLES30.glGetIntegerv(GLES30.GL_CURRENT_PROGRAM, currentProgram, 0);
// 使用着色器程序
GLES30.glUseProgram(shaderProgram);
在OpenGL ES中,状态的修改是即时生效的,并且会一直保持直到被再次修改。这种状态机模型使得OpenGL ES的API设计相对简洁,但也要求开发者在编程时要特别注意状态的管理,避免不必要的状态切换。
架构图:
调用流程图:
类的关系图:
2.4 上下文的共享机制
在多线程环境中,有时候需要多个线程共享同一个OpenGL ES上下文,或者在不同的上下文之间共享资源。OpenGL ES提供了上下文共享机制来支持这种需求。
上下文共享的基本原理是:多个上下文可以共享同一个资源池,这样一个上下文中创建的资源(如纹理、缓冲区对象等)可以在其他共享的上下文中使用。
在Android平台上,可以通过EGL来实现上下文共享。下面是一个创建共享上下文的示例:
java
// 创建主上下文(与前面的示例相同)
EGLContext mainContext = createMainContext(display, config);
// 创建共享上下文
int[] shareContextAttribs = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
};
EGLContext sharedContext = EGL14.eglCreateContext(display, config, mainContext, shareContextAttribs, 0);
if (sharedContext == EGL14.EGL_NO_CONTEXT) {
// 处理错误
return;
}
在上面的示例中,sharedContext
与mainContext
共享资源。这意味着在mainContext
中创建的资源可以在sharedContext
中使用,反之亦然。
需要注意的是,虽然资源可以在共享的上下文中使用,但同一时间只能有一个上下文是当前上下文。也就是说,一个线程在使用某个上下文时,其他线程不能同时使用同一个上下文。因此,在多线程环境中使用共享上下文时,需要适当的同步机制来避免竞争条件。
另外,不是所有的OpenGL ES资源都可以共享。通常,纹理对象、缓冲区对象、着色器程序等可以共享,但帧缓冲对象、渲染缓冲对象等通常不能共享。具体的共享规则取决于平台和驱动实现。
架构图:
调用流程图:
类的关系图:
2.5 上下文的销毁与资源释放
当不再需要OpenGL ES上下文时,应该正确地销毁上下文并释放相关的资源,以避免内存泄漏和资源浪费。
在Android平台上,销毁OpenGL ES上下文的基本步骤如下:
java
// 释放EGL资源
EGL14.eglMakeCurrent(display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroySurface(display, surface);
EGL14.eglDestroyContext(display, context);
EGL14.eglTerminate(display);
上述代码的主要步骤包括:
- 解除当前上下文绑定 :调用
eglMakeCurrent
函数,将当前上下文设置为EGL_NO_CONTEXT
,解除上下文与表面的绑定。 - 销毁EGL表面 :使用
eglDestroySurface
函数销毁之前创建的EGL表面。 - 销毁EGL上下文 :调用
eglDestroyContext
函数销毁OpenGL ES上下文。 - 终止EGL :使用
eglTerminate
函数终止EGL库的使用,释放相关资源。
除了销毁上下文本身,还需要释放上下文中创建的所有资源,如纹理、缓冲区对象、着色器程序等。这些资源的释放通常需要调用相应的OpenGL ES API:
java
// 释放纹理
int[] textures = new int[1];
GLES30.glGenTextures(1, textures, 0);
// 使用纹理...
GLES30.glDeleteTextures(1, textures, 0); // 释放纹理
// 释放缓冲区对象
int[] buffers = new int[1];
GLES30.glGenBuffers(1, buffers, 0);
// 使用缓冲区对象...
GLES30.glDeleteBuffers(1, buffers, 0); // 释放缓冲区对象
// 释放着色器程序
int shaderProgram = createShaderProgram();
// 使用着色器程序...
GLES30.glDeleteProgram(shaderProgram); // 释放着色器程序
在释放资源时,需要确保资源不再被使用。如果在上下文销毁之前没有释放资源,可能会导致内存泄漏。
架构图:
调用流程图:
类的关系图:
三、OpenGL ES 渲染管线
3.1 渲染管线概述
OpenGL ES渲染管线是一个图形处理流程,定义了从顶点数据到最终像素的转换过程。渲染管线将复杂的图形渲染任务分解为多个阶段,每个阶段都有特定的功能和处理逻辑。
渲染管线的主要阶段包括:
- 顶点处理:处理顶点数据,包括顶点坐标变换、光照计算等。
- 图元装配:将顶点组合成图元(如点、线、三角形等)。
- 光栅化:将图元转换为片段(像素)。
- 片段处理:处理每个片段,包括纹理采样、颜色计算等。
- 逐片段操作:执行深度测试、模板测试、混合等操作,最终确定每个像素的颜色。
架构图:
调用流程图:
类的关系图:
3.2 顶点处理阶段
顶点处理阶段是渲染管线的第一个阶段,负责处理输入的顶点数据。这个阶段的主要任务是对顶点进行变换和计算,将顶点从模型空间转换到裁剪空间。
顶点处理阶段的核心组件是顶点着色器,它是一段可编程的代码,用于对每个顶点进行处理。顶点着色器可以执行各种操作,如顶点变换、光照计算、纹理坐标生成等。
顶点处理阶段的主要步骤包括:
- 顶点数据输入:从顶点缓冲区读取顶点数据,包括顶点坐标、法线、纹理坐标等。
- 顶点着色器执行:对每个顶点执行顶点着色器程序,计算顶点的最终位置和其他属性。
- 顶点变换:将顶点从模型空间转换到世界空间,再到视图空间,最后到裁剪空间。
- 可选的固定功能处理:在某些情况下,可能会执行一些固定功能的处理,如光照计算。
下面是一个简单的顶点着色器示例:
glsl
// 顶点着色器
#version 300 es
// 输入属性
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
// 输出变量
out vec3 vNormal;
out vec2 vTexCoord;
// 统一变量
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
void main() {
// 计算顶点位置
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
// 传递法线和纹理坐标到片段着色器
vNormal = uNormalMatrix * aNormal;
vTexCoord = aTexCoord;
}
在这个顶点着色器中,我们接收顶点位置、法线和纹理坐标作为输入,计算顶点的最终位置,并将法线和纹理坐标传递给片段着色器。
架构图:
调用流程图:
类的关系图:
3.3 图元装配阶段
图元装配阶段是渲染管线的第二个阶段,负责将处理后的顶点组合成图元(如点、线、三角形等),并进行裁剪和透视除法等操作。
图元装配阶段的主要步骤包括:
- 图元类型确定:根据API调用指定的图元类型(如GL_TRIANGLES、GL_LINES等),将顶点组合成相应的图元。
- 图元构建:根据图元类型,将顶点数据组织成完整的图元。
- 裁剪:将超出裁剪空间的图元进行裁剪,确保只有可见的部分进入下一阶段。
- 透视除法:将裁剪空间的坐标除以w分量,转换为标准化设备坐标(NDC)。
- 视口变换:将标准化设备坐标转换为窗口坐标。
下面是图元装配阶段的一些关键代码示例:
cpp
// 图元装配类
class PrimitiveAssembly {
public:
// 根据图元类型装配图元
void AssemblePrimitives(PrimitiveType type, const std::vector<Vertex>& vertices, std::vector<Primitive>& primitives) {
switch (type) {
case POINTS:
AssemblePoints(vertices, primitives);
break;
case LINES:
AssembleLines(vertices, primitives);
break;
case LINE_STRIP:
AssembleLineStrip(vertices, primitives);
break;
case LINE_LOOP:
AssembleLineLoop(vertices, primitives);
break;
case TRIANGLES:
AssembleTriangles(vertices, primitives);
break;
case TRIANGLE_STRIP:
AssembleTriangleStrip(vertices, primitives);
break;
case TRIANGLE_FAN:
AssembleTriangleFan(vertices, primitives);
break;
}
}
// 裁剪图元
void ClipPrimitives(const std::vector<Primitive>& input, std::vector<Primitive>& output) {
// 实现裁剪算法
// ...
}
// 透视除法
void PerspectiveDivide(std::vector<Vertex>& vertices) {
for (auto& vertex : vertices) {
vertex.position /= vertex.position.w; // 透视除法
}
}
// 视口变换
void ViewportTransform(std::vector<Vertex>& vertices, const Viewport& viewport) {
for (auto& vertex : vertices) {
// 将NDC坐标转换为窗口坐标
vertex.position.x = viewport.x + (vertex.position.x + 1.0f) * viewport.width / 2.0f;
vertex.position.y = viewport.y + (1.0f - vertex.position.y) * viewport.height / 2.0f;
vertex.position.z = viewport.zNear + vertex.position.z * (viewport.zFar - viewport.zNear) / 2.0f;
}
}
private:
// 各种图元装配方法的实现
void AssemblePoints(const std::vector<Vertex>& vertices, std::vector<Primitive>& primitives);
void AssembleLines(const std::vector<Vertex>& vertices, std::vector<Primitive>& primitives);
// ...
};
架构图:
调用流程图:
类的关系图:
3.4 光栅化阶段
光栅化阶段是渲染管线的第三个阶段,负责将图元(如三角形)转换为片段(像素)。这个阶段决定了哪些像素会受到图元的影响,并为每个像素计算相应的属性值。
光栅化阶段的主要步骤包括:
- 三角形设置:计算三角形边缘的参数方程,为光栅化做准备。
- 三角形遍历:遍历三角形覆盖的像素区域,确定哪些像素被三角形覆盖。
- 片段生成:为每个被覆盖的像素生成一个片段。
- 属性插值:为每个片段插值计算顶点属性(如颜色、纹理坐标、法线等)。
下面是光栅化阶段的一些关键代码示例:
cpp
// 光栅化器类
class Rasterizer {
public:
// 光栅化三角形
void RasterizeTriangle(const Vertex& v0, const Vertex& v1, const Vertex& v2, Framebuffer& framebuffer) {
// 三角形设置:计算边界框
int minX = std::min({v0.position.x, v1.position.x, v2.position.x});
int maxX = std::max({v0.position.x, v1.position.x, v2.position.x});
int minY = std::min({v0.position.y, v1.position.y, v2.position.y});
int maxY = std::max({v0.position.y, v1.position.y, v2.position.y});
// 裁剪到视口
minX = std::max(minX, 0);
maxX = std::min(maxX, framebuffer.GetWidth() - 1);
minY = std::max(minY, 0);
maxY = std::min(maxY, framebuffer.GetHeight() - 1);
// 遍历边界框内的所有像素
for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) {
// 计算重心坐标
float w0 = EdgeFunction(v1.position, v2.position, {x, y});
float w1 = EdgeFunction(v2.position, v0.position, {x, y});
float w2 = EdgeFunction(v0.position, v1.position, {x, y});
// 判断像素是否在三角形内部
if (w0 >= 0 && w1 >= 0 && w2 >= 0) {
// 归一化重心坐标
float area = EdgeFunction(v0.position, v1.position, v2.position);
w0 /= area;
w1 /= area;
w2 /= area;
// 深度插值
float depth = w0 * v0.position.z + w1 * v1.position.z + w2 * v2.position.z;
// 属性插值
Vertex interpolatedVertex = InterpolateVertex(v0, v1, v2, w0, w1, w2);
// 创建片段
Fragment fragment(x, y, depth, interpolatedVertex);
// 处理片段
ProcessFragment(fragment, framebuffer);
}
}
}
}
// 处理片段
void ProcessFragment(const Fragment& fragment, Framebuffer& framebuffer) {
// 深度测试
if (fragment.depth < framebuffer.GetDepth(fragment.x, fragment.y)) {
// 更新深度缓冲
framebuffer.SetDepth(fragment.x, fragment.y, fragment.depth);
// 调用片段着色器
Color color = FragmentShader(fragment);
// 应用混合
ApplyBlending(fragment.x, fragment.y, color, framebuffer);
}
}
private:
// 计算边缘函数值
float EdgeFunction(const Vec2& a, const Vec2& b, const Vec2& c) {
return (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x);
}
// 插值顶点属性
Vertex InterpolateVertex(const Vertex& v0, const Vertex& v1, const Vertex& v2, float w0, float w1, float w2) {
Vertex result;
// 插值位置
result.position = w0 * v0.position + w1 * v1.position + w2 * v2.position;
// 插值法线
result.normal = w0 * v0.normal + w1 * v1.normal + w2 * v2.normal;
// 插值纹理坐标
result.texCoord = w0 * v0.texCoord + w1 * v1.texCoord + w2 * v2.texCoord;
// 其他属性插值...
return result;
}
};
架构图:
调用流程图:
类的关系图:
3.5 片段处理阶段
片段处理阶段是渲染管线的第四个阶段,负责处理光栅化阶段生成的每个片段。这个阶段的核心是片段着色器,它是一段可编程的代码,用于计算每个片段的最终颜色。
片段处理阶段的主要步骤包括:
- 片段着色器执行:对每个片段执行片段着色器程序,计算片段的颜色。
- 纹理采样:从纹理中获取颜色值,通常使用插值后的纹理坐标。
- 颜色计算:结合纹理颜色、光照信息、材质属性等,计算片段的最终颜色。
- 可选的输出处理:可以对片段着色器的输出进行一些后处理,如雾效计算。
下面是一个简单的片段着色器示例:
glsl
// 片段着色器
#version 300 es
precision mediump float;
// 输入变量(从顶点着色器插值而来)
in vec3 vNormal;
in vec2 vTexCoord;
// 输出变量
out vec4 fragColor;
// 统一变量
uniform sampler2D uTexture;
uniform vec3 uLightDir;
uniform vec3 uLightColor;
uniform vec3 uAmbientColor;
void main() {
// 采样纹理
vec4 texColor = texture(uTexture, vTexCoord);
// 计算法线(归一化)
vec3 normal = normalize(vNormal);
// 计算光照方向(归一化)
vec3 lightDir = normalize(uLightDir);
// 计算漫反射光照
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * uLightColor;
// 计算最终颜色
vec3 finalColor = (uAmbientColor + diffuse) * texColor.rgb;
// 设置片段颜色
fragColor = vec4(finalColor, texColor.a);
}
在这个片段着色器中,我们接收插值后的法线和纹理坐标,从纹理中采样颜色,计算漫反射光照,并最终确定片段的颜色。
架构图:
调用流程图:
类的关系图:
3.6 逐片段操作阶段
逐片段操作阶段是渲染管线的最后一个阶段,负责对每个片段执行一系列测试和操作,最终确定该片段是否会被绘制到帧缓冲中。
逐片段操作阶段的主要步骤包括:
- 裁剪测试:检查片段是否在视口范围内。
- Alpha测试:根据片段的Alpha值决定是否丢弃该片段。
- 模板测试:根据模板缓冲的值决定是否丢弃该片段。
- 深度测试:根据深度缓冲的值决定是否丢弃该片段。
- 模板操作:如果启用了模板测试,更新模板缓冲的值。
- 深度操作:如果深度测试通过,更新深度缓冲的值。
- 混合:将片段的颜色与帧缓冲中已有的颜色进行混合。
- 抖动:对最终颜色进行抖动处理,增加颜色精度。
- 帧缓冲写入:将最终颜色写入帧缓冲。
下面是逐片段操作阶段的一些关键代码示例:
cpp
// 逐片段操作类
class PerFragmentOperations {
public:
// 执行逐片段操作
bool ProcessFragment(Fragment& fragment, Framebuffer& framebuffer, StencilBuffer& stencilBuffer, DepthBuffer& depthBuffer) {
// 裁剪测试
if (!ScissorTest(fragment)) {
return false;
}
// Alpha测试
if (!AlphaTest(fragment)) {
return false;
}
// 模板测试
if (!StencilTest(fragment, stencilBuffer)) {
return false;
}
// 深度测试
if (!DepthTest(fragment, depthBuffer)) {
return false;
}
// 执行模板操作
StencilOperation(fragment, stencilBuffer);
// 执行深度操作
DepthOperation(fragment, depthBuffer);
// 混合
Blend(fragment, framebuffer);
// 抖动(简化处理)
Dither(fragment);
// 写入帧缓冲
WriteToFramebuffer(fragment, framebuffer);
return true;
}
private:
// 裁剪测试
bool ScissorTest(const Fragment& fragment) {
// 检查片段是否在裁剪矩形内
return (fragment.x >= scissorRect.x &&
fragment.x < scissorRect.x + scissorRect.width &&
fragment.y >= scissorRect.y &&
fragment.y < scissorRect.y + scissorRect.height);
}
// Alpha测试
bool AlphaTest(const Fragment& fragment) {
if (!alphaTestEnabled) {
return true;
}
// 执行Alpha测试
switch (alphaFunc) {
case GL_NEVER: return false;
case GL_LESS: return fragment.color.a < alphaRef;
case GL_EQUAL: return fragment.color.a == alphaRef;
case GL_LEQUAL: return fragment.color.a <= alphaRef;
case GL_GREATER: return fragment.color.a > alphaRef;
case GL_NOTEQUAL: return fragment.color.a != alphaRef;
case GL_GEQUAL: return fragment.color.a >= alphaRef;
case GL_ALWAYS: return true;
default: return true;
}
}
// 模板测试
bool StencilTest(const Fragment& fragment, StencilBuffer& stencilBuffer) {
if (!stencilTestEnabled) {
return true;
}
// 获取当前模板值
unsigned char stencilValue = stencilBuffer.GetValue(fragment.x, fragment.y);
// 执行模板测试
return ((stencilValue & stencilMask) compareFunc (stencilRef & stencilMask));
}
// 深度测试
bool DepthTest(const Fragment& fragment, DepthBuffer& depthBuffer) {
if (!depthTestEnabled) {
return true;
}
// 获取当前深度值
float depthValue = depthBuffer.GetValue(fragment.x, fragment.y);
// 执行深度测试
switch (depthFunc) {
case GL_NEVER: return false;
case GL_LESS: return fragment.depth < depthValue;
case GL_EQUAL: return fragment.depth == depthValue;
case GL_LEQUAL: return fragment.depth <= depthValue;
case GL_GREATER: return fragment.depth > depthValue;
case GL_NOTEQUAL: return fragment.depth != depthValue;
case GL_GEQUAL: return fragment.depth >= depthValue;
case GL_ALWAYS: return true;
default: return true;
}
}
// 其他操作的实现...
};
架构图:
调用流程图:
四、OpenGL ES 着色器系统
4.1 着色器概述
着色器是OpenGL ES中实现图形渲染可编程部分的关键组件。它们是运行在GPU上的小程序,用于控制渲染管线中的特定阶段,如顶点处理和片段处理。
OpenGL ES着色器使用专门的着色器语言编写,主要有两种类型的着色器:
- 顶点着色器:处理每个顶点,执行坐标变换、光照计算等操作。
- 片段着色器:处理每个片段(像素),执行纹理采样、颜色计算等操作。
从OpenGL ES 3.1开始,还引入了其他类型的着色器,如计算着色器,用于通用计算任务。
着色器的主要优势在于它们能够充分利用GPU的并行计算能力,实现高效的图形渲染和复杂的视觉效果。
架构图:
调用流程图:
4.2 顶点着色器
顶点着色器是处理顶点数据的可编程阶段,它对每个输入顶点执行一次。顶点着色器的主要任务是将顶点从模型空间转换到裁剪空间,并为后续阶段提供必要的顶点属性。
顶点着色器的基本结构包括:
- 输入变量:接收顶点属性数据,如位置、法线、纹理坐标等。
- 统一变量:全局变量,对所有顶点相同,如变换矩阵、光照参数等。
- 输出变量:传递数据到后续阶段,通常是经过插值后传递给片段着色器。
- 主函数 :顶点着色器的入口点,必须定义
gl_Position
变量作为顶点的最终位置。
下面是一个顶点着色器的示例:
glsl
#version 300 es
// 输入属性
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
// 输出变量
out vec3 vNormal;
out vec2 vTexCoord;
// 统一变量
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
void main() {
// 计算顶点位置
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
// 传递法线和纹理坐标到片段着色器
vNormal = uNormalMatrix * aNormal;
vTexCoord = aTexCoord;
}
这个顶点着色器接收顶点位置、法线和纹理坐标,计算顶点的最终位置,并将法线和纹理坐标传递给片段着色器。
顶点着色器的主要功能包括:
- 坐标变换:将顶点从模型空间转换到裁剪空间。
- 光照计算:计算顶点的光照效果,如漫反射、镜面反射等。
- 顶点属性传递:将顶点属性(如法线、纹理坐标)传递给片段着色器。
- 骨骼动画:实现基于骨骼的顶点动画。
- 顶点变形:实现各种顶点变形效果,如波浪、扭曲等。
架构图:
调用流程图:
类的关系图:
4.3 片段着色器
片段着色器是处理片段(像素)的可编程阶段,它对光栅化阶段生成的每个片段执行一次。片段着色器的主要任务是计算每个片段的最终颜色。
片段着色器的基本结构包括:
- 输入变量:接收从顶点着色器插值而来的数据,如法线、纹理坐标等。
- 统一变量:全局变量,对所有片段相同,如纹理、光照参数等。
- 输出变量:定义片段的最终颜色。
- 主函数:片段着色器的入口点,必须定义输出颜色变量。
下面是一个片段着色器的示例:
glsl
#version 300 es
precision mediump float;
// 输入变量(从顶点着色器插值而来)
in vec3 vNormal;
in vec2 vTexCoord;
// 输出变量
out vec4 fragColor;
// 统一变量
uniform sampler2D uTexture;
uniform vec3 uLightDir;
uniform vec3 uLightColor;
uniform vec3 uAmbientColor;
void main() {
// 采样纹理
vec4 texColor = texture(uTexture, vTexCoord);
// 计算法线(归一化)
vec3 normal = normalize(vNormal);
// 计算光照方向(归一化)
vec3 lightDir = normalize(uLightDir);
// 计算漫反射光照
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * uLightColor;
// 计算最终颜色
vec3 finalColor = (uAmbientColor + diffuse) * texColor.rgb;
// 设置片段颜色
fragColor = vec4(finalColor, texColor.a);
}
这个片段着色器接收插值后的法线和纹理坐标,从纹理中采样颜色,计算漫反射光照,并最终确定片段的颜色。
片段着色器的主要功能包括:
- 纹理采样:从纹理中获取颜色值。
- 光照计算:实现复杂的光照模型,如Phong模型、Blinn-Phong模型等。
- 颜色混合:将多种颜色源混合,如纹理颜色、光照颜色等。
- 特效实现:实现各种特效,如阴影、反射、折射等。
- 后处理效果:实现图像后处理效果,如模糊、锐化、色调调整等。
架构图:
调用流程图:
类的关系图:
4.4 着色器语言
OpenGL ES着色器使用专门的着色器语言编写,称为OpenGL ES着色器语言(GLSL ES)。GLSL ES是一种C风格的编程语言,专为图形渲染而设计。
GLSL ES的主要特点包括:
- 强类型语言:所有变量和表达式都必须有明确的类型。
- 内置数据类型:包括标量类型(float、int、bool)、向量类型(vec2、vec3、vec4)、矩阵类型(mat2、mat3、mat4)等。
- 内置函数:提供了丰富的数学函数、几何函数、纹理采样函数等。
- 没有指针:不支持指针操作,避免了内存管理的复杂性。
- 并行执行:着色器程序在GPU上并行执行,每个顶点或片段独立处理。
下面是GLSL ES中一些重要的概念和语法:
变量类型:
glsl
// 标量类型
float a = 1.0;
int b = 2;
bool c = true;
// 向量类型
vec2 v2 = vec2(1.0, 2.0);
vec3 v3 = vec3(1.0, 2.0, 3.0);
vec4 v4 = vec4(v3, 1.0);
// 矩阵类型
mat4 m4 = mat4(1.0); // 单位矩阵
内置函数:
glsl
// 数学函数
float sinVal = sin(angle);
float cosVal = cos(angle);
float lengthVal = length(vec3(1.0, 2.0, 3.0));
// 几何函数
float dotProduct = dot(normal, lightDir);
vec3 crossProduct = cross(vec3(1.0), vec3(0.0, 1.0, 0.0));
vec3 normalizedVec = normalize(vector);
// 纹理采样函数
vec4 texColor = texture(sampler2D, texCoord);
控制结构:
glsl
// 条件语句
if (condition) {
// 执行代码
} else {
// 执行代码
}
// 循环语句
for (int i = 0; i < 10; i++) {
// 执行代码
}
着色器变量限定符:
glsl
// 输入变量(顶点着色器)
in vec3 aPosition;
// 输出变量(顶点着色器)
out vec3 vPosition;
// 输入变量(片段着色器)
in vec3 vPosition;
// 输出变量(片段着色器)
out vec4 fragColor;
// 统一变量
uniform mat4 uModelViewProjectionMatrix;
// 常量
const float PI = 3.1415926;
架构图:
4.5 着色器编译与链接
着色器程序在使用前需要进行编译和链接。这个过程涉及将着色器源代码编译成GPU可执行的形式,并将多个着色器组合成一个完整的着色器程序。
着色器编译和链接的主要步骤包括:
- 创建着色器对象:为顶点着色器和片段着色器创建OpenGL ES对象。
- 指定着色器源代码:将GLSL ES源代码加载到着色器对象中。
- 编译着色器:编译着色器源代码。
- 检查编译状态:检查编译是否成功,如果失败,获取错误信息。
- 创建着色器程序对象:创建一个OpenGL ES程序对象。
- 附加着色器:将编译好的顶点着色器和片段着色器附加到程序对象上。
- 链接着色器程序:链接着色器程序,生成可执行代码。
- 检查链接状态:检查链接是否成功,如果失败,获取错误信息。
- 使用着色器程序:激活着色器程序,使其成为当前渲染状态的一部分。
下面是着色器编译和链接的代码示例:
cpp
// 着色器编译和链接着色器程序的函数
GLuint CreateShaderProgram(const char* vertexShaderSource, const char* fragmentShaderSource) {
// 创建顶点着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
if (vertexShader == 0) {
std::cerr << "无法创建顶点着色器!" << std::endl;
return 0;
}
// 指定顶点着色器源代码
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
// 编译顶点着色器
glCompileShader(vertexShader);
// 检查编译状态
GLint compiled;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv(vertexShader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = new char[infoLen];
glGetShaderInfoLog(vertexShader, infoLen, NULL, infoLog);
std::cerr << "顶点着色器编译错误: " << infoLog << std::endl;
delete[] infoLog;
}
glDeleteShader(vertexShader);
return 0;
}
// 创建片段着色器
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
if (fragmentShader == 0) {
std::cerr << "无法创建片段着色器!" << std::endl;
glDeleteShader(vertexShader);
return 0;
}
// 指定片段着色器源代码
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
// 编译片段着色器
glCompileShader(fragmentShader);
// 检查编译状态
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv(fragmentShader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = new char[infoLen];
glGetShaderInfoLog(fragmentShader, infoLen, NULL, infoLog);
std::cerr << "片段着色器编译错误: " << infoLog << std::endl;
delete[] infoLog;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return 0;
}
// 创建着色器程序
GLuint shaderProgram = glCreateProgram();
if (shaderProgram == 0) {
std::cerr << "无法创建着色器程序!" << std::endl;
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return 0;
}
// 附加着色器到程序
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
// 链接着色器程序
glLinkProgram(shaderProgram);
// 检查链接状态
GLint linked;
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &linked);
if (!linked) {
GLint infoLen = 0;
glGetProgramiv(shaderProgram, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
char* infoLog = new char[infoLen];
glGetProgramInfoLog(shaderProgram, infoLen, NULL, infoLog);
std::cerr << "着色器程序链接错误: " << infoLog << std::endl;
delete[] infoLog;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glDeleteProgram(shaderProgram);
return 0;
}
// 着色器已链接到程序,现在可以删除
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
架构图:
调用流程图:
五、OpenGL ES 纹理系统
5.1 纹理概述
纹理是OpenGL ES中用于向图形表面添加细节的一种方式。它们是存储在GPU内存中的图像数据,可以在渲染过程中被采样和应用到几何体表面。
纹理的主要用途包括:
- 表面细节:为物体表面添加颜色、图案和细节。
- 环境映射:模拟反射和折射效果。
- 光照贴图:存储预计算的光照信息。
- 高度图:用于地形渲染和位移映射。
- 法线贴图:模拟表面细节而不增加几何复杂度。
- 立方体贴图:用于天空盒、环境反射等。
OpenGL ES支持多种纹理类型,包括:
- 2D纹理:最常见的纹理类型,是一个二维图像。
- 立方体贴图:由六个2D纹理组成,代表立方体的六个面,用于环境映射。
- 3D纹理:类似于体积数据,有三个维度。
- 2D纹理数组:一组2D纹理,共享相同的尺寸。
架构图:
调用流程图:
5.2 2D纹理
2D纹理是OpenGL ES中最常见的纹理类型,它是一个二维图像,由宽度和高度定义。2D纹理可以用来表示物体的表面颜色、法线、高度等信息。
创建和使用2D纹理的主要步骤包括:
- 创建纹理对象 :使用
glGenTextures
创建一个纹理对象。 - 绑定纹理 :使用
glBindTexture
将纹理对象绑定到纹理单元。 - 设置纹理参数 :使用
glTexParameteri
设置纹理参数,如环绕模式和过滤模式。 - 加载纹理数据 :使用
glTexImage2D
或glCompressedTexImage2D
加载纹理数据。 - 生成Mipmap (可选):使用
glGenerateMipmap
生成多级渐远纹理。 - 在着色器中使用纹理 :在着色器中使用
texture
函数采样纹理。
下面是一个创建和使用2D纹理的代码示例:
cpp
// 创建2D纹理
GLuint CreateTexture2D(int width, int height, GLenum format, const unsigned char* data) {
GLuint textureId;
// 生成纹理对象
glGenTextures(1, &textureId);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId);
// 设置纹理参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
// 生成Mipmap
glGenerateMipmap(GL_TEXTURE_2D);
// 解除绑定
glBindTexture(GL_TEXTURE_2D, 0);
return textureId;
}
// 在着色器中使用纹理
void UseTextureInShader(GLuint textureId, GLuint shaderProgram, const char* uniformName) {
// 激活纹理单元0
glActiveTexture(GL_TEXTURE0);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId);
// 获取统一变量位置
GLint textureUniform = glGetUniformLocation(shaderProgram, uniformName);
// 设置统一变量,指定使用纹理单元0
glUniform1i(textureUniform, 0);
}
在片段着色器中,可以这样采样2D纹理:
glsl
#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D uTexture;
void main() {
fragColor = texture(uTexture, vTexCoord);
}
架构图:
调用流程图:
5.3 立方体贴图
立方体贴图由六个2D纹理组成,每个纹理代表立方体的一个面。立方体贴图通常用于环境映射、天空盒和反射效果。
立方体贴图的六个面分别是:
- GL_TEXTURE_CUBE_MAP_POSITIVE_X:右
- GL_TEXTURE_CUBE_MAP_NEGATIVE_X:左
- GL_TEXTURE_CUBE_MAP_POSITIVE_Y:上
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:下
- GL_TEXTURE_CUBE_MAP_POSITIVE_Z:后
- GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:前
创建和使用立方体贴图的主要步骤包括:
- 创建纹理对象 :使用
glGenTextures
创建一个纹理对象。 - 绑定纹理 :使用
glBindTexture(GL_TEXTURE_CUBE_MAP, textureId)
将纹理对象绑定到立方体贴图目标。 - 设置纹理参数:设置环绕模式和过滤模式。
- 加载六个面的纹理数据 :使用
glTexImage2D
为每个面加载纹理数据。 - 生成Mipmap (可选):使用
glGenerateMipmap(GL_TEXTURE_CUBE_MAP)
生成多级渐远纹理。 - 在着色器中使用立方体贴图:在着色器中使用三维方向向量采样立方体贴图。
下面是一个创建和使用立方体贴图的代码示例:
cpp
// 创建立方体贴图
GLuint CreateCubeMapTexture(const unsigned char* faces[6], int width, int height, GLenum format) {
GLuint textureId;
// 生成纹理对象
glGenTextures(1, &textureId);
// 绑定纹理
glBindTexture(GL_TEXTURE_CUBE_MAP, textureId);
// 设置纹理参数
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
// 加载六个面的纹理数据
for (GLuint i = 0; i < 6; i++) {
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, faces[i]);
}
// 生成Mipmap
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
// 解除绑定
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
return textureId;
}
// 在着色器中使用立方体贴图
void UseCubeMapTextureInShader(GLuint textureId, GLuint shaderProgram, const char* uniformName) {
// 激活纹理单元0
glActiveTexture(GL_TEXTURE0);
// 绑定立方体贴图
glBindTexture(GL_TEXTURE_CUBE_MAP, textureId);
// 获取统一变量位置
GLint textureUniform = glGetUniformLocation(shaderProgram, uniformName);
// 设置统一变量,指定使用纹理单元0
glUniform1i(textureUniform, 0);
}
在片段着色器中,可以这样采样立方体贴图:
glsl
#version 300 es
precision mediump float;
in vec3 vNormal;
out vec4 fragColor;
uniform samplerCube uCubeMap;
void main() {
// 使用法线向量作为方向向量采样立方体贴图
fragColor = texture(uCubeMap, normalize(vNormal));
}
架构图:
调用流程图:
5.4 纹理采样与过滤
纹理采样是从纹理中获取颜色值的过程。在渲染过程中,纹理坐标通常是浮点数,而纹理数据是离散的,因此需要一种方法来确定如何从离散的纹理数据中获取连续的颜色值。这就是纹理过滤的作用。
OpenGL ES提供了多种纹理过滤模式,主要分为两类:
- 放大过滤:当纹理被放大时使用(纹理像素比屏幕像素少)。
- 缩小过滤:当纹理被缩小时使用(纹理像素比屏幕像素多)。
常见的纹理过滤模式包括:
- GL_NEAREST:最近邻过滤,使用最接近纹理坐标的纹理元素。
- GL_LINEAR:线性过滤,使用周围纹理元素的加权平均值。
- Mipmap过滤:使用多级渐远纹理,根据距离选择合适的纹理级别。
下面是各种过滤模式的效果比较:
过滤模式 | 描述 | 质量 | 性能 |
---|---|---|---|
GL_NEAREST | 最近邻过滤,没有Mipmap | 低 | 高 |
GL_LINEAR | 线性过滤,没有Mipmap | 中等 | 中等 |
GL_NEAREST_MIPMAP_NEAREST | 最近邻Mipmap选择,最近邻过滤 | 中等 | 高 |
GL_LINEAR_MIPMAP_NEAREST | 最近邻Mipmap选择,线性过滤 | 中等偏高 | 中等 |
GL_NEAREST_MIPMAP_LINEAR | 线性Mipmap选择,最近邻过滤 | 中等偏高 | 中等 |
GL_LINEAR_MIPMAP_LINEAR | 线性Mipmap选择,线性过滤(三线性过滤) | 高 | 低 |
纹理环绕模式控制当纹理坐标超出[0,1]范围时的行为。常见的环绕模式包括:
- GL_REPEAT:重复纹理。
- GL_MIRRORED_REPEAT:镜像重复纹理。
- GL_CLAMP_TO_EDGE:夹紧到边缘。
- GL_CLAMP_TO_BORDER:夹紧到边界。
下面是纹理采样和过滤的代码示例:
cpp
// 设置纹理过滤和环绕模式
void SetTextureParameters(GLuint textureId, GLenum minFilter, GLenum magFilter, GLenum wrapS, GLenum wrapT) {
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId);
// 设置缩小过滤模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
// 设置放大过滤模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
// 设置S方向环绕模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS);
// 设置T方向环绕模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapT);
// 解除绑定
glBindTexture(GL_TEXTURE_2D, 0);
}
在着色器中,可以使用内置的texture
函数进行纹理采样:
glsl
#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D uTexture;
void main() {
// 采样纹理
fragColor = texture(uTexture, vTexCoord);
}
调用流程图:
5.5 纹理压缩
纹理压缩是一种减少纹理内存占用和提高渲染性能的技术。通过压缩纹理数据,可以减少GPU内存的使用,降低带宽需求,并提高缓存效率。
OpenGL ES支持多种纹理压缩格式,包括:
- ETC (Ericsson Texture Compression):一种专为移动设备设计的压缩格式,支持RGB和RGBA。
- ASTC (Adaptive Scalable Texture Compression):一种现代的纹理压缩格式,支持各种压缩率和纹理尺寸。
- DXT/BC (Block Compression):一种广泛使用的压缩格式,也称为S3TC。
- PVRTC (PowerVR Texture Compression):Imagination Technologies开发的压缩格式,主要用于PowerVR GPU。
使用纹理压缩的主要步骤包括:
- 检查压缩格式支持 :使用
glGetString(GL_EXTENSIONS)
检查设备支持哪些压缩格式。 - 加载压缩纹理数据 :使用
glCompressedTexImage2D
或类似函数加载压缩纹理数据。 - 设置纹理参数:设置过滤模式、环绕模式等。
- 在着色器中使用压缩纹理:与未压缩纹理相同。
下面是一个使用ETC2压缩格式的代码示例:
cpp
// 检查ETC2压缩格式支持
bool IsETC2Supported() {
const char* extensions = (const char*)glGetString(GL_EXTENSIONS);
return (strstr(extensions, "GL_ETC2_RGBA8") != nullptr);
}
// 加载ETC2压缩纹理
GLuint LoadETC2Texture(const unsigned char* compressedData, int width, int height, int dataSize) {
GLuint textureId;
// 生成纹理对象
glGenTextures(1, &textureId);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureId);
// 设置纹理参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// 加载压缩纹理数据
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_ETC2_RGBA8, width, height, 0, dataSize, compressedData);
// 生成Mipmap
glGenerateMipmap(GL_TEXTURE_2D);
// 解除绑定
glBindTexture(GL_TEXTURE_2D, 0);
return textureId;
}
架构图:
调用流程图: