# CameraOpenGL **Repository Path**: hovoh/camera-open-glc ## Basic Information - **Project Name**: CameraOpenGL - **Description**: 通过CPP 实现 OpenGL 例子 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2024-05-28 - **Last Updated**: 2025-03-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Demo learn from 《字节流动》 # CameraOpenGL 实战 # 搭建C++ 环境 # 1.实现绘制 三角形 TriangleSample.cpp # 2.实现绘制纹理 TextureMapSample #(2) 纹理映射(纹理贴图) 纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理 纹理映射就是通过为图元的顶点坐标指定恰当的纹理坐标,通过纹理坐标在纹理图中选定特定的纹理区域,最后通过纹理坐标与顶点的映射关系,将选定的纹理区域映射到指定图元上 ##[纹理坐标系](img/纹理坐标系.png) 4 个纹理坐标分别为 T0(0,0),T1(0,1),T2(1,1),T3(1,0) ##[纹理对应顶点坐标](img/纹理对应顶点坐标系.png) 4 个纹理坐标对于的顶点坐标分别为 V0(-1,0.5),V1(-1, -0.5),V2(1,-0.5),V3(1,0.5) ## 纹理映射步骤 1.生成纹理,编译链接着色器程序 2.确定纹理坐标及对应的顶点坐标 3.加载图像数据到纹理,加载纹理坐标和顶点坐标到着色器程序 4.绘制 ## 生成纹理并加载图像数据到纹理 //生成一个纹理,将纹理 id 赋值给 m_TextureId glGenTextures(1, &m_TextureId); //将纹理 m_TextureId 绑定到类型 GL_TEXTURE_2D 纹理 glBindTexture(GL_TEXTURE_2D, m_TextureId); //设置纹理 S 轴(横轴)的拉伸方式为截取 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); //设置纹理 T 轴(纵轴)的拉伸方式为截取 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); //设置纹理采样方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //加载 RGBA 格式的图像数据 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]); 对纹理采样的片元着色器脚本 ## #version 300 es precision mediump float; //设置浮点精度 in vec2 v_texCoord; // 从顶点着色器接收纹理坐标 v_texCoord layout(location = 0) out vec4 outColor; //声明一个输出变量 outColor,位于位置 0 uniform sampler2D s_TextureMap; //声明一个 2D 纹理采样器 s_TextureMap void main() { // texture() 为内置的采样函数,v_texCoord 为顶点着色器传进来的纹理坐标 // 根据纹理坐标对纹理进行采样,输出采样的 rgba 值(4维向量) outColor = texture(s_TextureMap, v_texCoord); } #(3).YUV 渲染 YUV 图不能直接用于显示,需要转换为 RGB 格式,而 YUV 转 RGB 是一个逐像素处理的耗时操作 在 CPU 端进行转换效率过低,这时正好可以利用 GPU 强大的并行处理能力来实现 YUV 到 RGB 的转换 YUV 与 RGB 之间的转换公式 ![YUV与RGB之间的转换公式](img/YUV与RGB之间的转换公式.png) OpenGLES 实现 YUV 渲染需要用到 GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的纹理, 其中 GL_LUMINANCE 纹理用来加载 NV21 Y Plane 的数据,GL_LUMINANCE_ALPHA 纹理用来加载 UV Plane 的数据 ![常用纹理的格式类型](img/常用纹理的格式类型.png) GL_LUMINANCE 纹理在着色器中采样的纹理像素格式是(L,L,L,1),L 表示亮度。 GL_LUMINANCE 纹理在着色器中采样的纹理像素格式是(L,L,L,A),A 表示透明度。 YUV 渲染步骤: 1.生成 2 个纹理,编译链接着色器程序; 2.确定纹理坐标及对应的顶点坐标; 3.分别加载 NV21 的两个 Plane 数据到 2 个纹理,加载纹理坐标和顶点坐标数据到着色器程序; 4.绘制。 片段着色器脚本 #version 300 es precision mediump float; in vec2 v_texCoord; layout(location = 0) out vec4 outColor; uniform sampler2D y_texture; uniform sampler2D uv_texture; void main() { vec3 yuv; yuv.x = texture(y_texture, v_texCoord).r; yuv.y = texture(uv_texture, v_texCoord).a-0.5; yuv.z = texture(uv_texture, v_texCoord).r-0.5; vec3 rgb =mat3( 1.0, 1.0, 1.0, 0.0, -0.344, 1.770, 1.403, -0.714, 0.0) * yuv; outColor = vec4(rgb, 1); } y_texture 和 uv_texture 分别是 NV21 Y Plane 和 UV Plane 纹理的采样器 对两个纹理采样之后组成一个(y,u,v)三维向量 之后左乘变换矩阵转换为(r,g,b)三维向量。 # 4.VBO 和 EBO VBO (Vertex Buffer Object) 顶点缓冲区对象 EBO (Element Buffer Object) 图元索引缓冲区对象 VAO 和 EBO 实际上是对同一类 Buffer 按照用途的不同称呼。 OpenGLES2.0 编程中,用于绘制的顶点数组数据首先保存在 CPU 内存, 在调用 glDrawArrays 或者 glDrawElements 等进行绘制时,需要将顶点数组数据从 CPU 内存拷贝到显存。 但是很多时候我们没必要每次绘制的时候都去进行内存拷贝,如果可以在显存中缓存这些数据,就可以在很大程度上降低内存拷贝带来的开销。 VBO 和 EBO 的作用是在显存中提前开辟好一块内存,用于缓存顶点数据或者图元索引数据, 从而避免每次绘制时的 CPU 与 GPU 之间的内存拷贝,可以改进渲染性能,降低内存带宽和功耗。 OpenGLES3.0 支持两类缓冲区对象:顶点数组缓冲区对象、图元索引缓冲区对象。 GL_ARRAY_BUFFER 标志指定的缓冲区对象用于保存顶点数组 GL_ELEMENT_ARRAY_BUFFER 标志指定的缓存区对象用于保存图元索引。 VBO(EBO)的创建和更新。 // 创建 2 个 VBO(EBO 实际上跟 VBO 一样,只是按照用途的另一种称呼) glGenBuffers(2, m_VboIds); // 绑定第一个 VBO,拷贝顶点数组到显存 glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 绑定第二个 VBO(EBO),拷贝图元索引数据到显存 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_VboIds[1]); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); GL_STATIC_DRAW 标志标识缓冲区对象数据被修改一次,使用多次,用于绘制。 本例中顶点着色器和片段着色器增加 color 属性: //顶点着色器 #version 300 es layout(location = 0) in vec4 a_position; // 位置变量的属性位置值为 0 layout(location = 1) in vec3 a_color; // 颜色变量的属性位置值为 1 out vec3 v_color; // 向片段着色器输出一个颜色 void main() { v_color = a_color; gl_Position = a_position; }; //片段着色器 #version 300 es precision mediump float; in vec3 v_color; out vec4 o_fragColor; void main() { o_fragColor = vec4(v_color, 1.0); } 顶点数组数据和图元索引数据: 顶点数组数据 GLfloat vertices[] = { -0.5f, 0.5f, 0.0f, // 顶点 0 1.0f, 0.0f, 0.0f, // 颜色 0 -0.5f, -0.5f, 0.0f, // vertice 1 0.0f, 1.0f, 0.0f, // color 1 0.5f, -0.5f, 0.0f, // v2 0.0f, 0.0f, 1.0f, // c2 0.5f, 0.5f, 0.0f, // v3 0.5f, 1.0f, 1.0f, // c3 }; ![数组顶点+颜色](img/数组顶点+颜色.png) 由于顶点位置和颜色数据在同一个数组里,一起更新到 VBO 里面,所以需要知道 2 个属性的步长和偏移量。 为获得数据队列中下一个属性值(比如位置向量的下个 3 维分量) 我们必须向右移动 6 个 float ,其中 3 个是位置值,另外 3 个是颜色值,那么步长就是 6 乘以 float 的字节数(= 24 字节)。 同样,也需要指定顶点位置属性和颜色属性在 VBO 内存中的偏移量。 对于每个顶点来说,位置顶点属性在前,所以它的偏移量是 0 。 而颜色属性紧随位置数据之后,所以偏移量就是 3 * sizeof(GLfloat) ,用字节来计算就是 12 字节。 图元索引数据 GLushort indices[6] = { 0, 1, 2, 0, 2, 3}; # 使用 VBO 和 EBO 进行绘制。 glUseProgram(m_ProgramObj); //不使用 VBO 的绘制 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, (3+3)*sizeof(GLfloat), vertices); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, (3+3)*sizeof(GLfloat), (vertices + 3)); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices); //使用 VBO 的绘制 glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, (3+3)*sizeof(GLfloat), (const void *)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, (3+3)*sizeof(GLfloat), (const void *)(3 *sizeof(GLfloat))); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_VboIds[1]); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0); # VAO VAO(Vertex Array Object)是指顶点数组对象 VAO 的主要作用是用于管理 VBO 或 EBO , 减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。 ![顶点数组对象](img/VAO.png) 创建 VAO // 创建并绑定 VAO glGenVertexArrays(1, &m_VaoId); glBindVertexArray(m_VaoId); // 在绑定 VAO 之后,操作 VBO ,当前 VAO 会记录 VBO 的操作 glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, (3+3)*sizeof(GLfloat), (const void *)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, (3+3)*sizeof(GLfloat), (const void *)(3 *sizeof(GLfloat))); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_VboIds[1]); glBindVertexArray(GL_NONE); 使用 VAO 进行绘制: // 是不是精简了很多? glUseProgram(m_ProgramObj); glBindVertexArray(m_VaoId); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0); # FBO FBO(Frame Buffer Object)即帧缓冲区对象,实际上是一个可添加缓冲区的容器,可以为其添加纹理或渲染缓冲区对象(RBO) FBO 本身不能用于渲染,只有添加了纹理或者渲染缓冲区之后才能作为渲染目标,它仅且提供了 3 个附着(Attachment),分别是颜色附着、深度附着和模板附着 RBO(Render Buffer Object)即渲染缓冲区对象,是一个由应用程序分配的 2D 图像缓冲区。 渲染缓冲区可以用于分配和存储颜色、深度或者模板值,可以用作 FBO 中的颜色、深度或者模板附着。 使用 FBO 作为渲染目标时,首先需要为 FBO 的附着添加连接对象,如颜色附着需要连接纹理或者渲染缓冲区对象的颜色缓冲区。 ![FBO](img/FBO.png) 为什么用 FBO 默认情况下,OpenGL ES 通过绘制到窗口系统提供的帧缓冲区,然后将帧缓冲区的对应区域复制到纹理来实现渲染到纹理,但是此方法只有在纹理尺寸小于或等于帧缓冲区尺寸才有效。 另一种方式是通过使用连接到纹理的 pbuffer 来实现渲染到纹理,但是与上下文和窗口系统提供的可绘制表面切换开销也很大。因此,引入了帧缓冲区对象 FBO 来解决这个问题 在NDK OpenGLES 开发中,一般使用 GLSurfaceView 将绘制结果显示到屏幕上, 然而在实际应用中,也有许多场景不需要渲染到屏幕上,如利用 GPU 在后台完成一些图像转换、缩放等耗时操作,这个时候利用 FBO 可以方便实现类似需求。 使用 FBO 可以让渲染操作不用再渲染到屏幕上,而是渲染到离屏 Buffer 中,然后可以使用 glReadPixels 或者 HardwareBuffer 将渲染后的图像数据读出来, 从而实现在后台利用 GPU 完成对图像的处理 怎么用 FBO #创建并初始化 FBO 的步骤: // 创建一个 2D 纹理用于连接 FBO 的颜色附着 glGenTextures(1, &m_FboTextureId); glBindTexture(GL_TEXTURE_2D, m_FboTextureId); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glBindTexture(GL_TEXTURE_2D, GL_NONE); // 创建 FBO glGenFramebuffers(1, &m_FboId); // 绑定 FBO glBindFramebuffer(GL_FRAMEBUFFER, m_FboId); // 绑定 FBO 纹理 glBindTexture(GL_TEXTURE_2D, m_FboTextureId); // 将纹理连接到 FBO 附着 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_FboTextureId, 0); // 分配内存大小 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); // 检查 FBO 的完整性状态 if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE) { LOGCATE("FBOSample::CreateFrameBufferObj glCheckFramebufferStatus status != GL_FRAMEBUFFER_COMPLETE"); return false; } // 解绑纹理 glBindTexture(GL_TEXTURE_2D, GL_NONE); // 解绑 FBO glBindFramebuffer(GL_FRAMEBUFFER, GL_NONE); # 使用 FBO 的一般步骤 // 绑定 FBO glBindFramebuffer(GL_FRAMEBUFFER, m_FboId); // 选定离屏渲染的 Program,绑定 VAO 和图像纹理,进行绘制(离屏渲染) // m_ImageTextureId 为另外一个用于纹理映射的图片纹理 glUseProgram(m_FboProgramObj); glBindVertexArray(m_VaoIds[1]); glActiveTexture(GL_TEXTURE0); // 绑定图像纹理 glBindTexture(GL_TEXTURE_2D, m_ImageTextureId); glUniform1i(m_FboSamplerLoc, 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0); glBindVertexArray(0); glBindTexture(GL_TEXTURE_2D, 0); // 解绑 FBO glBindFramebuffer(GL_FRAMEBUFFER, 0); // 完成离屏渲染后,结果图数据便保存在我们之前连接到 FBO 的纹理 m_FboTextureId 。 // 我们再拿 FBO 纹理 m_FboTextureId 做一次普通渲染便可将之前离屏渲染的结果绘制到屏幕上。 // 这里我们编译连接了 2 个 program ,一个用作离屏渲染的 m_FboProgramObj,一个用于普通渲染的 m_ProgramObj //选定另外一个着色器程序,以 m_FboTextureId 纹理作为输入进行普通渲染 glUseProgram(m_ProgramObj); glBindVertexArray(m_VaoIds[0]); glActiveTexture(GL_TEXTURE0); //绑定 FBO 纹理 glBindTexture(GL_TEXTURE_2D, m_FboTextureId); glUniform1i(m_SamplerLoc, 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0); glBindTexture(GL_TEXTURE_2D, GL_NONE); glBindVertexArray(GL_NONE); # EGL 什么是 EGL EGL 是 OpenGL ES 和本地窗口系统(Native Window System)之间的通信接口,它的主要作用 1.与设备的原生窗口系统通信; 2.查询绘图表面的可用类型和配置; 3.创建绘图表面; 4.在OpenGL ES 和其他图形渲染API之间同步渲染; 5.管理纹理贴图等渲染资源。 OpenGL ES 的平台无关性正是借助 EGL 实现的 本地窗口相关的 API 提供了访问本地窗口系统的接口,而 EGL 可以创建渲染表面 EGLSurface, 同时提供了图形渲染上下文 EGLContext,用来进行状态管理,接下来 OpenGL ES 就可以在这个渲染表面上绘制。 ![EGL](img/EGL.png) Display(EGLDisplay) 是对实际显示设备的抽象; Surface(EGLSurface)是对用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer(颜色缓冲区), Stencil Buffer(模板缓冲区) ,Depth Buffer(深度缓冲区) Context (EGLContext) 存储 OpenGL ES 绘图的一些状态信息 在 Android 平台上开发 OpenGL ES 应用时,类 GLSurfaceView 已经为我们提供了对 Display , Surface , Context 的管理, 即 GLSurfaceView 内部实现了对 EGL 的封装,可以很方便地利用接口 GLSurfaceView.Renderer 的实现, 使用 OpenGL ES API 进行渲染绘制,很大程度上提升了 OpenGLES 开发的便利性。 #使用 EGL 渲染的一般步骤: 1.获取 EGLDisplay 对象,建立与本地窗口系统的连接 调用 eglGetDisplay 方法得到 EGLDisplay。 2.初始化 EGL 方法 打开连接之后,调用 eglInitialize 方法初始化。 3.获取 EGLConfig 对象,确定渲染表面的配置信息 调用 eglChooseConfig 方法得到 EGLConfig。 4.创建渲染表面 EGLSurface 通过 EGLDisplay 和 EGLConfig ,调用 eglCreateWindowSurface 或 eglCreatePbufferSurface 方法创建渲染表面,得到 EGLSurface,其中 eglCreateWindowSurface 用于创建屏幕上渲染区域,eglCreatePbufferSurface 用于创建屏幕外渲染区域。 5.创建渲染上下文 EGLContext, 通过 EGLDisplay 和 EGLConfig ,调用 eglCreateContext 方法创建渲染上下文,得到 EGLContext。 6.绑定上下文 通过 eglMakeCurrent 方法将 EGLSurface、EGLContext、EGLDisplay 三者绑定,绑定成功之后 OpenGLES 环境就创建好了,接下来便可以进行渲染 7.交换缓冲 OpenGLES 绘制结束后,使用 eglSwapBuffers 方法交换前后缓冲,将绘制内容显示到屏幕上,而屏幕外的渲染不需要调用此方法。 8.释放 EGL 环境 绘制结束后,不再需要使用 EGL 时,需要取消 eglMakeCurrent 的绑定,销毁 EGLDisplay、EGLSurface、EGLContext 三个对象。 类 EGLRender 还存在一个崩溃问题 后续研究处理 #OpenGL 坐标系统 五个坐标系统 局部空间(Local Space,或者物体空间(Object Space)) 世界空间(World Space) 观察空间(View Space, 裁剪空间(Clip Space) 屏幕空间(Screen Space) 局部空间 (Local Space) 是指对象所在的坐标空间,坐标原点由你自己指定,模型的所有顶点相对于你的对象来说都是局部的。 世界空间 在世界空间(World Space)主要实现对象的平移、缩放、旋转变换,将它们放在我们指定的位置,这些变换是通过**模型矩阵(Model Matrix)**实现的。 在 C/C++ 中可以利用 GLM 构建模型矩阵: glm::mat4 Model = glm::mat4(1.0f); //单位矩阵 Model = glm::scale(Model, glm::vec3(2.0f, 2.0f, 2.0f)); //缩放 Model = glm::rotate(Model, MATH_PI/2, glm::vec3(1.0f, 0.0f, 0.0f)); //沿 x 轴旋转 90 度 Model = glm::translate(Model, glm::vec3(0.0f, 1.0f, 0.0f)); //沿 y 轴正方向平移一个单位 GLM 是 OpenGL Mathematics 的缩写,它是一个只有头文件的库,也就是说我们只需包含对应的头文件就行了, 不用链接和编译。GLM 可以在 Github 上下载,把头文件的根目录复制到你的includes文件夹,然后你就可以使用这个库了。 观察空间(View Space)也被称为 OpenGL 相机空间,即从摄像机的角度观察到的空间,它将对象的世界空间的坐标转换为观察者视野前面的坐标, 这通常是由一系列的平移和旋转的组合来平移和旋转场景从而使得特定的对象被转换到摄像机前面,这些组合在一起的转换通常存储在一个**观察矩阵(View Matrix)**里。 在 C/C++ 中可以利用 GLM 构建观察矩阵: // View matrix glm::mat4 View = glm::lookAt( glm::vec3(0, 0, 3), // Camera is at (0,0,1), in World Space 相机位置 glm::vec3(0, 0, 0), // and looks at the origin 观察点坐标 glm::vec3(0, 1, 0) // Head is up (set to 0,-1,0 to look upside-down) 相机 up 方向,即相机头部朝向 ); 裁剪空间 裁剪空间(Clip Space)是用来裁剪观察对象的空间,在一个顶点着色器运行的最后,OpenGL 期望所有的坐标都能落在一个给定的范围内, 且任何在这个范围之外的点都应该被裁剪掉。**投影矩阵(Projection Matrix)**用来将顶点坐标从观察空间转换到裁剪空间。 投影矩阵一般分为两种:正交投影(Orthographic Projection)和透视投影(Perspective Projection)。 ![正交投影和透视投影](img/正交投影和透视投影.png) ![正交投影](img/正交投影.png) C/C++ 中可以利用 GLM 构建正交投影矩阵: glm::mat4 Projection = glm::ortho(-ratio, ratio, -1.0f, 1.0f, 0.0f, 100.0f); //ratio 一般表示视口的宽高比,width/height 前两个参数指定了平截头体的左右坐标 第三和第四参数指定了平截头体的底部和上部。 通过这四个参数我们定义了近平面和远平面的大小, 然后第五和第六个参数则定义了近平面和远平面的距离。 这个指定的投影矩阵将处于这些 x,y,z 范围之间的坐标转换到标准化设备坐标系中。 C/C++ 中可以利用 GLM 构建透视投影矩阵: glm::mat4 Projection = glm::perspective(45.0f, ratio, 0.1f, 100.f); //ratio 一般表示视口的宽高比,width/height, 它的第一个参数定义了 fov 的值,它表示的是视野(Field of View),并且设置了观察空间的大小 对于一个真实的观察效果,它的值经常设置为 45.0,但想要看到更多结果你可以设置一个更大的值。第二个参数设置了宽高比,由视口的高除以宽 第三和第四个参数设置了平截头体的近和远平面。我们经常设置近距离为 0.1 而远距离设为 100.0 。所有在近平面和远平面的顶点且处于平截头体内的顶点都会被渲染。 最后整个坐标系统的变换矩阵可以用一个矩阵表示 MVPMatrix = Projection * View * Model; OpenGL 3D 变换实现 实现 OpenGL 3D 效果最简单的方式是在顶点着色器中将顶点坐标与 MVP 变换矩阵相乘 #version 300 es layout(location = 0) in vec4 a_position; layout(location = 1) in vec2 a_texCoord; uniform mat4 u_MVPMatrix; out vec2 v_texCoord; void main() { gl_Position = u_MVPMatrix * a_position; //顶点坐标与 MVP 变换矩阵相乘 v_texCoord = a_texCoord; } 在绘制之前构建变换矩阵: /** * @param angleX 绕X轴旋转度数 * @param angleY 绕Y轴旋转度数 * @param ratio 宽高比 * */ void CoordSystemSample::UpdateMVPMatrix(glm::mat4 &mvpMatrix, int angleX, int angleY, float ratio) { LOGCATE("CoordSystemSample::UpdateMVPMatrix angleX = %d, angleY = %d, ratio = %f", angleX, angleY, ratio); angleX = angleX % 360; angleY = angleY % 360; //转化为弧度角 float radiansX = static_cast(MATH_PI / 180.0f * angleX); float radiansY = static_cast(MATH_PI / 180.0f * angleY); // Projection matrix //glm::mat4 Projection = glm::ortho(-ratio, ratio, -1.0f, 1.0f, 0.1f, 100.0f); //glm::mat4 Projection = glm::frustum(-ratio, ratio, -1.0f, 1.0f, 4.0f, 100.0f); glm::mat4 Projection = glm::perspective(45.0f,ratio, 0.1f,100.f); // View matrix glm::mat4 View = glm::lookAt( glm::vec3(0, 0, 4), // Camera is at (0,0,1), in World Space glm::vec3(0, 0, 0), // and looks at the origin glm::vec3(0, 1, 0) // Head is up (set to 0,-1,0 to look upside-down) ); // Model matrix glm::mat4 Model = glm::mat4(1.0f); Model = glm::scale(Model, glm::vec3(1.0f, 1.0f, 1.0f)); Model = glm::rotate(Model, radiansX, glm::vec3(1.0f, 0.0f, 0.0f)); Model = glm::rotate(Model, radiansY, glm::vec3(0.0f, 1.0f, 0.0f)); Model = glm::translate(Model, glm::vec3(0.0f, 0.0f, 0.0f)); mvpMatrix = Projection * View * Model; } 绘制时传入变换矩阵 void CoordSystemSample::Draw(int screenW, int screenH) { LOGCATE("CoordSystemSample::Draw()"); if(m_ProgramObj == GL_NONE || m_TextureId == GL_NONE) return; // 旋转角度变换,更新变换矩阵 UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH); //upload RGBA image data glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_TextureId); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]); glBindTexture(GL_TEXTURE_2D, GL_NONE); // Use the program object glUseProgram (m_ProgramObj); glBindVertexArray(m_VaoId); // 将总变换矩阵传入着色器程序 glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]); // Bind the RGBA map glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_TextureId); glUniform1i(m_SamplerLoc, 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0); } # 光照基础 OpenGLES 目前还无法模拟现实世界的复杂关照效果,为了在效果要求和实现难度之间做一个平衡,往往采用一些简化的模型来模拟光照效果 冯氏光照模型(Phong Lighting Model)便是其中常用的一个光照模型, 它由三种元素光组成,分别是环境光(Ambient Lighting)、散射光(Diffuse Lighting)及镜面光(Specular Lighting)。 ![光照.png](img/光照.png) 环境光 环境光表示从四面八方照射到物体上且各个方向都均匀的光,不依赖于光源位置,没有方向性。 要把环境光照添加到场景里,只需用光的颜色乘以一个(数值)很小常量环境因子,再乘以物体的颜色,然后使用它作为片段的颜色: void main() { float ambientStrength = 0.1f; //常量环境因子 vec3 ambient = ambientStrength * lightColor; //环境光强度 vec3 result = ambient * objectColor; color = vec4(result, 1.0f); } 散射光 散射光表示从物体表面向各个方向均匀反射的光。散射光的强度与入射光的强度及入射角密切相关,所以当光源位置发生变化,散射光效果也会发生明显变化。 ![散射光.png](img/散射光.png) 散射光最终强度 = 材质反射系数 × 散射光强度 × max(cos(入射角),0) 其中入射角表示:当前片段光源照射方向与法向量之间的夹角。 实现散射光的片段着色器脚本: out vec3 fragPos;//当前片段坐标 out vec3 normal; //当前片段法向量 uniform vec3 lightPos;//光源位置 void main() { float diffuseStrength = 0.5f; //材质反射系数 vec3 norm = normalize(normal); // 归一化 vec3 lightDir = normalize(lightPos - fragPos);//当前片段光源照射方向向量 float diff = max(dot(norm, lightDir), 0.0);// dot 表示两个向量的点乘 vec3 diffuse = diffuseStrength * diff * lightColor; //散射光最终强度 vec3 result = diffuse * objectColor; color = vec4(result, 1.0f); } 镜面光 镜面光是由光滑物体表面反射的方向比较集中的光,镜面光强度不仅依赖于入射光与法向量的夹角,也依赖于观察者的位置。 ![镜面光.png](img/镜面光.png) 镜面光最终强度 = 材质镜面亮度因子 × 镜面光强度 × max(cos(反射光向量与视线方向向量夹角),0) 修正后的模型也可表示为: 镜面光最终强度 = 材质镜面亮度因子 × 镜面光强度 × max(cos(半向量与法向量夹角),0) 其中半向量为镜面反射光向量与视线方向向量(从片段到观察者)的半向量。 实现镜面光的片段着色器脚本: out vec3 fragPos;//当前片段坐标 out vec3 normal; //当前片段法向量 uniform vec3 lightPos;//光源位置 void main() { float specularStrength = 0.5f; vec3 norm = normalize(normal); // 归一化 vec3 viewDir = normalize(viewPos - FragPos); //视线方向向量 vec3 reflectDir = reflect(-lightDir, norm); //镜面反射光向量 float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); //视线方向向量与镜面反射光向量点乘的32次幂,这个32是高光的发光值(Shininess)。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。 vec3 specular = specularStrength * spec * lightColor; //镜面光最终强度 vec3 result = specular * objectColor; color = vec4(result, 1.0f); } # 基础光照模型实现 实现基础光照模型的顶点着色器: #version 300 es precision mediump float; layout(location = 0) in vec4 a_position; layout(location = 1) in vec2 a_texCoord; layout(location = 2) in vec3 a_normal; uniform mat4 u_MVPMatrix; uniform mat4 u_ModelMatrix; uniform vec3 lightPos; uniform vec3 lightColor; uniform vec3 viewPos; out vec2 v_texCoord; out vec3 ambient; out vec3 diffuse; out vec3 specular; void main() { gl_Position = u_MVPMatrix * a_position; vec3 fragPos = vec3(u_ModelMatrix * a_position); // Ambient float ambientStrength = 0.1; ambient = ambientStrength * lightColor; // Diffuse float diffuseStrength = 0.5; vec3 unitNormal = normalize(vec3(u_ModelMatrix * vec4(a_normal, 1.0))); vec3 lightDir = normalize(lightPos - fragPos); float diff = max(dot(unitNormal, lightDir), 0.0); diffuse = diffuseStrength * diff * lightColor; // Specular float specularStrength = 0.9; vec3 viewDir = normalize(viewPos - fragPos); vec3 reflectDir = reflect(-lightDir, unitNormal); float spec = pow(max(dot(unitNormal, reflectDir), 0.0), 16.0); specular = specularStrength * spec * lightColor; v_texCoord = a_texCoord; } 对应的片段着色器: #version 300 es precision mediump float; in vec2 v_texCoord; in vec3 ambient; in vec3 diffuse; in vec3 specular; layout(location = 0) out vec4 outColor; uniform sampler2D s_TextureMap; void main() { vec4 objectColor = texture(s_TextureMap, v_texCoord); vec3 finalColor = (ambient + diffuse + specular) * vec3(objectColor); outColor = vec4(finalColor, 1.0); } # Transform Feedback Transform Feedback(变换反馈)是在 OpenGLES3.0 渲染管线中,顶点处理阶段结束之后,图元装配和光栅化之前的一个步骤 Transform Feedback 可以重新捕获即将装配为图元(点,线段,三角形)的顶点,然后你将它们的部分或者全部属性传递到缓存对象 Transform Feedback 的主要作用是可以将顶点着色器的处理结果输出,并且可以有多个输出, 这样可以将大量的向量或矩阵运算交给 GPU 并行处理,这是 OpenGLES 3.0 的新特性 ![transformback](img/transformback.png) 每个顶点在传递到图元装配阶段时,将所有需要捕获的属性数据记录到一个或者多个缓存对象中,程序可以通过这些缓存读出这些数据,可以将他们用于后续的渲染操作 #Transform Feedback 对象 Transform Feedback 所有状态通过一个 Transform Feedback 对象管理,主要包括以下状态: 用于记录顶点数据的缓存对象; 用于标识缓存对象的计数器; 用于标识 Transform Feedback 当前是否启用的状态量。 Transform Feedback 对象的创建绑定过程和一般的 OpenGLES 对象类似,如 VAO 。 生成和绑定 Transform Feedback 对象: glGenTransformFeedbacks(1, &m_TransFeedbackObjId); glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_TransFeedbackObjId); #Transform Feedback 缓存 Transform Feedback 主要用来管理将顶点捕捉到缓存对象的相关状态。 这个状态中包含当前连接到的 Transform Feedback 缓存绑定点的缓存对象。可以同时给 Transform Feedback 绑定多个缓存 也可以绑定缓存对象的多个子块,甚至可以将同一个缓存对象不用子块绑定到不同的 Transform Feedback 缓存绑定点上。 创建 Transform Feedback 缓存类似于创建 VBO 。 glGenBuffers(1, &m_TransFeedbackBufId); glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_TransFeedbackBufId); // 设置缓存的大小,输出是一个 3 维向量和一个 2 维向量,一共 6 个顶点,大小为 (3 + 2) * 6 * sizeof(GLfloat) glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, (3 + 2) * 6 * sizeof(GLfloat), NULL, GL_STATIC_READ); 接口 glBindBufferBase 将缓存绑定到当前 Transform Feedback 对象。 void glBindBufferBase(GLenum target, GLuint index, Gluint buffer); arget 参数须设置为 GL_TRANSFORM_FEEDBACK_BUFFER; index 必须是当前绑定的 transform feedback 对象的缓存绑定点索引; buffer 表示被绑定的缓存对象的 ID 。 为 Transform Feedback 对象绑定缓冲区对象。 glGenTransformFeedbacks(1, &m_TransFeedbackObjId); glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_TransFeedbackObjId); glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_TransFeedbackBufId); // Specify the index of the binding point within the array specified by target. glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0); #Transform Feedback 变量 glTransformFeedbackVaryings 用于指定变换反馈的变量,也就是顶点着色器需要输出的变量。 声明了 2 个变换反馈变量的顶点着色器: #version 300 es layout(location = 0) in vec4 a_position; layout(location = 1) in vec2 a_texCoord; out vec2 v_texCoord; out vec3 outPos; out vec2 outTex; void main() { gl_Position = a_position; v_texCoord = a_texCoord; outPos = vec3(a_position)*3.0; //将位置向量做一个简单运算后输出 outTex = a_texCoord * 3.0; //将纹理坐标向量做一个简单运算后输出 } 设置变换反馈变量,需要注意的是 glTransformFeedbackVaryings 需要在 glLinkProgram 之前调用。 glAttachShader(program, vertexShaderHandle); glAttachShader(program, fragShaderHandle); GLchar const * varyings[] = {"outPos", "outTex"}; glTransformFeedbackVaryings(m_ProgramObj, sizeof(varyings)/ sizeof(varyings[0]), varyings, GL_INTERLEAVED_ATTRIBS); glLinkProgram(program); # Transform Feedback 捕获启动和停止 Transform Feedback 可以随时启动、暂停和停止。 glBeginTransformFeedback 用于开始 Transform Feedback ,它的参数是用来设置将要记录的图元类型,如:GL_POINTS、GL_LINES 和 GL_TRIANGLES 。 glPuaseTransformFeedback 暂停 Transform Feedback 对变量的记录,但 Transform Feedback 还是处于启动状态。如果 Transform Feedback 没有启动则 OpenGLES 产生错误。 glResumeTransformFeedback 重新开启一个之前通过 glPuaseTransformFeedback 暂停的变换反馈过程,如果 Transform Feedback 没有启动,或者没有被处于活动状态,则产生OpenGL错误。 glEndTransformFeedback 用来结束 Transform Feedback 过程。 # Transform Feedback 缓冲区读取 Transform Feedback 过程结束后,通过 glMapBufferRange 读取缓冲区数据。 //绑定要读取的缓冲区对象 glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_TransFeedbackBufId); //读取缓冲区数据 void* rawData = glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, (3 + 2) * 6 * sizeof(GLfloat), GL_MAP_READ_BIT); float *p = (float*)rawData; for(int i= 0; i< 6; i++) { LOGCATE("TransformFeedbackSample::Draw() read feedback buffer outPos[%d] = [%f, %f, %f], outTex[%d] = [%f, %f]", i, p[i * 5], p[i * 5 + 1], p[i * 5 + 2], i, p[i * 5 + 3], p[i * 5 + 4]); } //解绑 glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER); glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0); # Transform Feedback 的使用 设置变换反馈变量; 创建 Transform Feedback 缓冲区; 创建 Transform Feedback 对象,并绑定缓冲区; 启动变换反馈,在绘制结束后停止变换反馈; 读取 Transform Feedback 缓冲区数据 # 总体实现代码: //1. 设置变换反馈变量; glAttachShader(program, vertexShaderHandle); glAttachShader(program, fragShaderHandle); GLchar const * varyings[] = {"outPos", "outTex"}; // 用于指定变换反馈的变量,也就是顶点着色器需要输出的变量。 glTransformFeedbackVaryings(m_ProgramObj, sizeof(varyings)/ sizeof(varyings[0]), varyings, GL_INTERLEAVED_ATTRIBS); glLinkProgram(program); //2. 创建 Transform Feedback 缓冲区; glGenBuffers(1, &m_TransFeedbackBufId); glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_TransFeedbackBufId); // 设置缓存的大小,输出是一个 3 维向量和一个 2 维向量,一共 6 个顶点,大小为 (3 + 2) * 6 * sizeof(GLfloat) glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, (3 + 2) * 6 * sizeof(GLfloat), NULL, GL_STATIC_READ); // 接口 glBindBufferBase 将缓存绑定到当前 Transform Feedback 对象 glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0); //3. 创建 Transform Feedback 对象,并绑定缓冲区; glGenTransformFeedbacks(1, &m_TransFeedbackObjId); glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_TransFeedbackObjId); glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_TransFeedbackBufId); glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0); //4. 启动变换反馈,在绘制结束后停止变换反馈; glViewport(0, 0, screenW, screenH); glUseProgram(m_ProgramObj); glBindVertexArray(m_VaoId); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_ImageTextureId); glUniform1i(m_SamplerLoc, 0); glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_TransFeedbackObjId); glBeginTransformFeedback(GL_TRIANGLES); glDrawArrays(GL_TRIANGLES, 0, 6); glEndTransformFeedback(); glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); glBindTexture(GL_TEXTURE_2D, GL_NONE); glBindVertexArray(GL_NONE); //5. 读取 Transform Feedback 缓冲区数据。 // Read feedback buffer glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, m_TransFeedbackBufId); void* rawData = glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, (3 + 2) * 6 * sizeof(GLfloat), GL_MAP_READ_BIT); float *p = (float*)rawData; for(int i= 0; i< 6; i++) { LOGCATE("TransformFeedbackSample::Draw() read feedback buffer outPos[%d] = [%f, %f, %f], outTex[%d] = [%f, %f]", i, p[i * 5], p[i * 5 + 1], p[i * 5 + 2], i, p[i * 5 + 3], p[i * 5 + 4]); } glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER); glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0); # 深度测试 OpenGL 深度测试是指在片段着色器执行之后,利用深度缓冲所保存的深度值决定当前片段是否被丢弃的过程。 深度缓冲区通常和颜色缓冲区有着相同的宽度和高度,一般由窗口系统自动创建并将其深度值存储为 16、 24 或 32 位浮点数。 当深度测试开启的时候, OpenGL 才会测试深度缓冲区中的深度值。如果此测试通过,深度缓冲内的值可以被设为新的深度值;如果深度测试失败,则丢弃该片段。 深度测试是在片段着色器运行之后(并且在模板测试运行之后)在屏幕空间中执行的。 与屏幕空间坐标相关的视区是由 OpenGL 的视口设置函数 glViewport 函数给定,并且可以通过片段着色器中内置的 gl_FragCoord 变量访问。 gl_FragCoord 的 X 和 y 表示该片段的屏幕空间坐标 ((0,0) 在左下角),其取值范围由 glViewport 函数决定,屏幕空间坐标原点位于左下角。 gl_FragCoord 还包含一个 z 坐标,它包含了片段的实际深度值,此 z 坐标值是与深度缓冲区的内容进行比较的值。 深度缓冲区中包含深度值介于 0.0 和 1.0 之间,物体接近近平面的时候,深度值接近 0.0 ,物体接近远平面时,深度接近 1.0 (深度缓冲区的可视化)在片段着色器中将深度值转换为物体颜色显示: #version 300 es precision mediump float; in vec2 v_texCoord; layout(location = 0) out vec4 outColor; uniform sampler2D s_TextureMap; void main() { // vec4(vec3(gl_FragCoord.z), 1.0f); 通过gl_FragCoord.z 实现 深度转颜色 vec4 objectColor = vec4(vec3(gl_FragCoord.z), 1.0f); outColor = objectColor; } ![深度转颜色](img/深度转颜色.png) 开启深度测试后,如果片段通过深度测试,OpenGL 自动在深度缓冲区存储片段的 gl_FragCoord.z 值,如果深度测试失败,那么相应地丢弃该片段。 如果启用深度测试,那么需要在渲染之前使用 glClear(GL_DEPTH_BUFFER_BIT); 清除深度缓冲区,否则深度缓冲区将保留上一次进行深度测试时所写的深度值。 另外在一些场景中,我们需要进行深度测试并相应地丢弃片段,但我们不希望更新深度缓冲区,那么可以设置深度掩码glDepthMask(GL_FALSE);实现禁用深度缓冲区的写入(只有在深度测试开启时才有效)。 OpenGL 深度测试是通过深度测试函数 glDepthFunc 控制深度测试是否通过和如何更新深度缓冲区 深度测试函数接收的比较运算符: ![深度测试比较运算符](img/深度测试运算符.png) 深度测试启用后,默认情况下深度测试函数使用 GL_LESS,这将丢弃深度值高于或等于当前深度缓冲区的值的片段 深度测试中,深度冲突现象需要值得注意。深度冲突(Z-fighting)是指两个平面(或三角形)相互平行且靠近的过于紧密, 模板缓冲区不具有足够的精度确定哪一个平面靠前,导致这两个平面的内容不断交替显示,看上去像平面内容争夺顶靠前的位置。 防止深度冲突的方法: 不要让物体之间靠得过近,以免它们的三角形面片发生重叠; 把近平面设置得远一些(越靠近近平面的位置精度越高); 牺牲一些性能,使用更高精度的深度值。 # 模板测试 模板测试与深度测试类似,主要作用是利用模板缓冲区(Stencil Buffer)所保存的模板值决定当前片段是否被丢弃,且发生于深度测试之前。 ![模板测试](img/模板测试.png) 模板测试步骤 启用模板测试,开启模板缓冲写入glStencilMask(0xFF) 执行渲染操作,更新模板缓冲区 关闭模板缓冲写入glStencilMask(0x00) 执行渲染操作,利用模板缓冲区所保存的模板值确定是否丢弃特定片段 启用模板测试 glEnable(GL_STENCIL_TEST);, 清空模板缓冲区 glClear( GL_STENCIL_BUFFER_BIT); 控制模板缓冲区是否可以进行写入: // 0xFF == 0b11111111 // 模板值与它进行按位与运算结果是模板值,模板缓冲可写 glStencilMask(0xFF); // 0x00 == 0b00000000 == 0 // 模板值与它进行按位与运算结果是0,模板缓冲不可写 glStencilMask(0x00); 模板测试的配置函数 glStencilFunc 和 glStencilOp void glStencilFunc(GLenum func, GLint ref, GLuint mask); func:设置模板测试操作。这个测试操作应用到已经储存的模板值和 glStencilFunc的 ref 值上 ,可用的选项是: GL_NEVER、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL、GL_ALWAYS ; ref:指定模板测试的引用值。模板缓冲区中的模板值会与这个值对比; mask:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为 1 。 代码实现 glStencilFunc(GL_EQUAL, 1, 0xFF); // 表示当一个片段模板值等于(GL_EQUAL)引用值1,片段就能通过测试被绘制了,否则就会被丢弃。 glStencilFunc(GL_ALWAYS, 1, 0xFF); // 表示所有片段模板测试总是通过。 glStencilOp 主要用于控制更新模板缓冲区的方式。 void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass); sfail: 如果模板测试失败将如何更新模板值; dpfail: 如果模板测试通过,但是深度测试失败时将如何更新模板值; dppass: 如果深度测试和模板测试都通过,将如何更新模板值。 可选操作 GL_KEEP 保持现有的模板值 GL_ZERO 将模板值置为 0 GL_REPLACE 将模板值设置为用 glStencilFunc 函数设置的ref值 GL_INCR 如果模板值不是最大值就将模板值 +1 GL_INCR_WRAP 与 GL_INCR 一样将模板值 +1 ,如果模板值已经是最大值则设为 0 GL_DECR 如果模板值不是最小值就将模板值 -1 GL_DECR_WRAP 与 GL_DECR 一样将模板值 -1 ,如果模板值已经是最小值则设为最大值 GL_INVERT 按位反转当前模板缓冲区的值 绘制物体轮廓是模板测试的常见应用,其步骤一般如下: 启动深度测试和模板测试,清空模板缓冲和深度缓冲; 在绘制物体前,设置 glStencilFunc(GL_ALWAYS, 1, 0xFF);,用 1 更新物体将被渲染的片段对应的模板值; 渲染物体,写入模板缓冲区; 关闭模板写入和深度测试; 将物体放大一定比例; 使用一个不同的片段着色器用来输出一个纯颜色(物体轮廓颜色); 再次绘制物体,设置 glStencilFunc(GL_NOTEQUAL, 1, 0xFF) 当片段的模板值不为 1 时,片段通过测试进行渲染; 开启模板写入和深度测试。 代码实现 //启动深度测试和模板测试,清空模板和深度缓冲 glClear(GL_STENCIL_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glEnable(GL_STENCIL_TEST); glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要写入模板缓冲 glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);//若模板测试和深度测试都通过了,将片段对应的模板值替换为1 glStencilMask(0xFF); //绘制物体 glBindVertexArray(m_VaoId); glUseProgram(m_ProgramObj); glUniform3f(m_ViewPosLoc, 0.0f, 0.0f, 3.0f); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_TextureId); glUniform1i(m_SamplerLoc, 0); UpdateMatrix(m_MVPMatrix, m_ModelMatrix, m_AngleX, m_AngleY , 1.0, glm::vec3(0.0f, 0.0f, 0.0f), ratio); glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]); glUniformMatrix4fv(m_ModelMatrixLoc, 1, GL_FALSE, &m_ModelMatrix[0][0]); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glStencilFunc(GL_NOTEQUAL, 1, 0xFF);//当片段的模板值不为 1 时,片段通过测试进行渲染 //禁用模板写入和深度测试 glStencilMask(0x00); glDisable(GL_DEPTH_TEST); //绘制物体轮廓 glBindVertexArray(m_VaoId); glUseProgram(m_OutlineProgramObj); //放大 1.05 倍 UpdateMatrix(m_MVPMatrix, m_ModelMatrix, m_AngleX, m_AngleY, 1.05, glm::vec3(0.0f, 0.0f, 0.0f), ratio); glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]); glUniformMatrix4fv(m_ModelMatrixLoc, 1, GL_FALSE, &m_ModelMatrix[0][0]); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); //开启模板写入和深度测试 glStencilMask(0xFF); glEnable(GL_DEPTH_TEST); glDisable(GL_STENCIL_TEST); 另外需要注意,在使用 GLSurfaceView 时,新的 API 默认没有配置模板缓冲区,需要使用 setEGLConfigChooser 配置模板缓冲区 public MyGLSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); this.setEGLContextClientVersion(2); mGLRender = new MyGLRender(); /*If no setEGLConfigChooser method is called, then by default the view will choose an RGB_888 surface with a depth buffer depth of at least 16 bits.*/ setEGLConfigChooser(8, 8, 8, 8, 16, 16);//最后 2 个参数表示分别配置 16 位的深度缓冲区和模板缓冲区 setRenderer(mGLRender); setRenderMode(RENDERMODE_WHEN_DIRTY); } # 实例化 OpenGL ES 实例化(Instancing) OpenGL ES 实例化(Instancing)是一种只调用一次渲染函数就能绘制出很多物体的技术, 可以实现将数据一次性发送给 GPU ,告诉 OpenGL ES 使用一个绘制函数,将这些数据绘制成多个物体。 实例化(Instancing)避免了 CPU 多次向 GPU 下达渲染命令(避免多次调用 glDrawArrays 或 glDrawElements 等绘制函数), 节省了绘制多个物体时 CPU 与 GPU 之间的通信时间,提升了渲染性能。 使用实例化渲染需要使用的绘制接口: //普通渲染 glDrawArrays (GLenum mode, GLint first, GLsizei count); glDrawElements (GLenum mode, GLsizei count, GLenum type, const void *indices); //实例化渲染 glDrawArraysInstanced (GLenum mode, GLint first, GLsizei count, GLsizei instancecount); glDrawElementsInstanced (GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount); # 立方体贴图 OpenGL ES 立方体贴图本质上还是纹理映射,是一种 3D 纹理映射。立方体贴图所使的纹理称为立方图纹理,它是由 6 个单独的 2D 纹理组成,每个 2D 纹理是立方图的一个面 立方图纹理的采样通过一个 3D 向量(s, t, r)作为纹理坐标 这个 3D 向量只作为方向向量使用,OpenGL ES 获取方向向量触碰到立方图表面上的纹理像素作为采样结果。 方向向量触碰到立方图表面对应的纹理位置作为采样点,要求立方图的中心必须位于原点。 立方图纹理的使用与 2D 纹理基本一致,首先生成一个纹理,激活相应纹理单元,然后绑定到 GL_TEXTURE_CUBE_MAP类型纹理 GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); 由于立方图包含 6 个纹理,每个面对应一个纹理,需要调用glTexImage2D函数 6 次, OpenGL ES 为立方图提供了 6 个不同的纹理目标,对应立方图的 6 个面,且 6 个纹理目标按顺序依次增 1。 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 前 将立方图的 6 个面对应的图像数据加载到纹理,其中m_pSkyBoxRenderImg为图像数据的数组: glGenTextures(1, &m_TextureId); glBindTexture(GL_TEXTURE_CUBE_MAP, m_TextureId); for (int i = 0; i < sizeof(m_pSkyBoxRenderImg) / sizeof(NativeImage); ++i) { glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA, m_pSkyBoxRenderImg[i].width, m_pSkyBoxRenderImg[i].height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_pSkyBoxRenderImg[i].ppPlane[0] ); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_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); glBindTexture(GL_TEXTURE_CUBE_MAP, 0); 类似于普通的 2D 纹理,在使用立方图纹理绘制物体之前,需要激活相应的纹理单元并绑定到立方图上。 不同的是,对应的片段着色器中,采样器变成了 samplerCube,并且纹理坐标变成了三维方向向量。 #version 300 es precision mediump float; in vec3 v_texCoord; layout(location = 0) out vec4 outColor; uniform samplerCube s_SkyBox; void main() { outColor = texture(s_SkyBox, v_texCoord); } 天空盒的绘制: // draw SkyBox glUseProgram(m_ProgramObj); glBindVertexArray(m_SkyBoxVaoId); glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, m_TextureId); glUniform1i(m_SamplerLoc, 0); glDrawArrays(GL_TRIANGLES, 0, 36); 接下来我们想在天空盒内绘制一个立方体,并让立方体的表面反射它周围环境的属性。 天空盒内物体反射的原理图: ![天空盒物体反射](img/天空盒物体反射.png) 其中 I 表示观察方向向量,通过当前顶点坐标减去相机位置(观察者)坐标计算得出; N 表示物体的法线向量,R 为反射向量,通过使用 GLSL 的内建函数 reflect 计算得出反射向量 R。 最后,以反射向量 R 作为方向向量对立方图进行索采样,返回采样结果(一个对应反射环境的颜色值)。最后的效果看起来就像物体反射了天空盒。 天空盒内绘制物体(反射周围环境颜色)使用的顶点着色器 #version 300 es precision mediump float; layout(location = 0) in vec3 a_position; layout(location = 1) in vec3 a_normal; uniform mat4 u_MVPMatrix; uniform mat4 u_ModelMatrix; out vec3 v_texCoord; out vec3 v_normal; void main() { gl_Position = u_MVPMatrix * vec4(a_position, 1.0); v_normal = mat3(transpose(inverse(u_ModelMatrix))) * a_normal; v_texCoord = vec3(u_ModelMatrix * vec4(a_position, 1.0)); } 天空盒内绘制物体(反射周围环境颜色)使用的片段着色器: #version 300 es precision mediump float; in vec3 v_texCoord; in vec3 v_normal; layout(location = 0) out vec4 outColor; uniform samplerCube s_SkyBox; uniform vec3 u_cameraPos; void main() { float ratio = 1.00 / 1.52; vec3 I = normalize(v_texCoord - u_cameraPos); //反射 vec3 R = reflect(I, normalize(v_normal)); //折射 //vec3 R = refract(I, normalize(v_normal), ratio); outColor = texture(s_SkyBox, R); } 绘制天空盒和盒内立方体: UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, 1.0, (float) screenW / screenH); // draw SkyBox glUseProgram(m_ProgramObj); glBindVertexArray(m_SkyBoxVaoId); glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, m_TextureId); glUniform1i(m_SamplerLoc, 0); glDrawArrays(GL_TRIANGLES, 0, 36); UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, 0.4f, (float) screenW / screenH); // draw Cube glUseProgram(m_CubeProgramObj); glBindVertexArray(m_CubeVaoId); glUniformMatrix4fv(m_CubeMVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]); glUniformMatrix4fv(m_CubeModelMatLoc, 1, GL_FALSE, &m_ModelMatrix[0][0]); glUniform3f(m_ViewPosLoc, 0.0f, 0.0f, 1.8f); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, m_TextureId); glUniform1i(m_CubeSamplerLoc, 0); glDrawArrays(GL_TRIANGLES, 0, 36); # 混合 OpenGL ES 混合本质上是将 2 个片元的颜色进行调和,产生一个新的颜色。 OpenGL ES 混合发生在片元通过各项测试之后,准备进入帧缓冲区的片元和原有的片元按照特定比例加权计算出最终片元的颜色值,不再是新(源)片元直接覆盖缓冲区中的(目标)片元。 ![混合公式](img/混合公式.png) C_{source}:源颜色向量,来自纹理的颜色向量; C_{destination}:目标颜色向量,储存在颜色缓冲中当前位置的颜色向量; F_{source}:源因子,设置了对源颜色加权; F_{destination}:目标因子,设置了对目标颜色加权; 操作符可以是加(+)、减(-)、Min、Max 等。 启用 OpenGL ES 混合使用 glEnable(GL_BLEND);。 然后通过 void glBlendFunc(GLenum sfactor, GLenum dfactor); 设置混合的方式,其中 sfactor 表示源因子,dfactor 表示目标因子。 glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // GL_SRC_ALPHA 表示源因子取值为源颜色的 alpha // GL_ONE_MINUS_SRC_ALPHA 表示目标因子取值为 1- alpha(源颜色的 alpha) // 操作符默认为 GL_FUNC_ADD ,即加权相加。 // 混合公式变成了 源颜色向量 × alpha + 目标颜色向量 × (1- alpha) GL_SRC_ALPHA 表示源因子取值为源颜色 alpha (透明度)通道值 GL_ONE_MINUS_SRC_ALPHA 表示目标因子取值为 1- alpha(源颜色的 alpha) 由于操作符默认为 GL_FUNC_ADD,即元素相加,所以混合公式变成了源颜色向量 × alpha + 目标颜色向量 × (1- alpha)。 ![混合因子表](img/混合因子表.png) 我们也可以通过 void glBlendEquation(GLenum mode) 自定义操作符 ![混合因子自定义操作符](img/混合因子自定义操作符.png) 我们可以为 RGB 和 alpha 通道各自设置不同的混合因子,使用 glBlendFuncSeperate: //对 RGB 和 Alpha 分别设置 BLEND 函数 //void glBlendFuncSeparate(GLenum srcRGB,GLenum dstRGB,GLenum srcAlpha,GLenum dstAlpha); glBlendFuncSeperate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,GL_ONE, GL_ZERO); 代码对应的混合公式为 混合结果颜色 RGB 向量 = 源颜色 RGB 向量 × alpha + 目标颜色 RGB 向量 × (1- alpha); 混合结果颜色 alpha = 源颜色 alpha × 1 + 目标颜色 alpha × 0; 当然我们也可以为 RGB 和 alpha 通道各自设置不同操作符 void glBlendEquationSeparate(GLenum modeRGB,GLenum modeAlpha); 另外需要格外注意的是,开启混合和深度测试绘制透明物体时,需要遵循物体距观察者(Camera)的距离,由远到近开始绘制, 这样可以避免由于深度测试开启后(在透明物体后面)丢弃片元造成的奇怪现象。 ![](img/深度测试先近后远.png) ![](img/深度测试先远后近.png) 可以看出未按由远到近顺序绘制的结果,出现了透明物体遮挡了其他物体的奇怪现象,这是由深度测试造成的。 #相机开发 相机预览实现 1. 基于 Android 原生 SurfaceTexture 的纯 GPU 实现方式 使用 SurfaceTexture 作为预览载体 SurfaceTexture 可来自于 GLSurfaceView、TextureView 或 SurfaceView 这些独立拥有 Surface 的封装类,也可以自定义实现。 作为预览载体的 SurfaceTexture 绑定的纹理需要是 OES 纹理,即 GLES11Ext.GL_TEXTURE_EXTERNAL_OES 纹理, 来自于 GLES 的扩展 #extension GL_OES_EGL_image_external 中,使用 OES 纹理后, 我们不需要在片段着色器中自己做 YUV to RGBA 的转换,因为 OES 纹理可以直接接收 YUV 数据或者直接输出 YUV 数据。 OES 纹理创建实现 private int createOESTexture(){ int[] texture = new int[1]; GLES20.glGenTextures(1, texture, 0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_MIN_FILTER,GL10.GL_LINEAR); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); return texture[0]; } 使用 OES 纹理需要修改片段着色器,在着色器脚本的头部增加扩展纹理的声明 #extension GL_OES_EGL_image_external : require , 并且纹理采样器不再使用 sample2D ,需要换成 samplerExternalOES 作为纹理采样器。 #version 300 es #extension GL_OES_EGL_image_external : require precision mediump float; in vec2 v_texCoord; uniform samplerExternalOES s_TexSampler; void main() { gl_FragColor = texture(s_TexSampler, v_texCoord); } 2.通过相机的预览回调接口获取帧的 YUV 数据,利用 CPU 算法处理完成之后,传入显存,再利用 GPU 实现 YUV 转 RGBA 进行渲染,即 CPU + GPU 的实现方式 相机预览数据的常见格式是 YUV420P 或者 YUV420SP(NV21) 1.相机预览数据获取 以 Camera2 为例,主要是通过 ImageReader 实现,该类封装了 Surface : private ImageReader.OnImageAvailableListener mOnPreviewImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); if (image != null) { if (mCamera2FrameCallback != null) { mCamera2FrameCallback.onPreviewFrame(CameraUtil.YUV_420_888_data(image), image.getWidth(), image.getHeight()); } image.close(); } } }; mPreviewImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2); mPreviewImageReader.setOnImageAvailableListener(mOnPreviewImageAvailableListener, mBackgroundHandler); CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); builder.addTarget(mPreviewImageReader.getSurface()); session.setRepeatingRequest(mPreviewRequest, null, mBackgroundHandler); //在自定义接口中获取预览数据,通过 JNI 传入到 C++ 层 public void onPreviewFrame(byte[] data, int width, int height) { Log.d(TAG, "onPreviewFrame() called with: data = [" + data + "], width = [" + width + "], height = [" + height + "]"); mByteFlowRender.setRenderFrame(IMAGE_FORMAT_I420, data, width, height); //每次传入新数据,请求重新渲染 mByteFlowRender.requestRender(); } 用于YUV 数据用到的着色器脚本 主要是将 3 个纹理对应的 YUV 分量,分别采样后转成 RGBA : //顶点着色器 #version 100 varying vec2 v_texcoord; attribute vec4 position; attribute vec2 texcoord; uniform mat4 MVP; void main() { v_texcoord = texcoord; gl_Position = MVP*position; } //片段着色器 #version 100 precision highp float; varying vec2 v_texcoord; uniform lowp sampler2D s_textureY; uniform lowp sampler2D s_textureU; uniform lowp sampler2D s_textureV; void main() { float y, u, v, r, g, b; y = texture2D(s_textureY, v_texcoord).r; u = texture2D(s_textureU, v_texcoord).r; v = texture2D(s_textureV, v_texcoord).r; u = u - 0.5; v = v - 0.5; r = y + 1.403 * v; g = y - 0.344 * u - 0.714 * v; b = y + 1.770 * u; gl_FragColor = vec4(r, g, b, 1.0); } C++ 层代码实现 //编译链接着色器 int GLByteFlowRender::CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource) { m_Program = GLUtils::CreateProgram(pVertexShaderSource, pFragShaderSource, m_VertexShader, m_FragShader); if (!m_Program) { GLUtils::CheckGLError("Create Program"); LOGCATE("GLByteFlowRender::CreateProgram Could not create program."); return 0; } m_YTextureHandle = glGetUniformLocation(m_Program, "s_textureY"); m_UTextureHandle = glGetUniformLocation(m_Program, "s_textureU"); m_VTextureHandle = glGetUniformLocation(m_Program, "s_textureV"); m_VertexCoorHandle = (GLuint) glGetAttribLocation(m_Program, "position"); m_TextureCoorHandle = (GLuint) glGetAttribLocation(m_Program, "texcoord"); m_MVPHandle = glGetUniformLocation(m_Program, "MVP"); return m_Program; } //创建 YUV 分量对应的 3 个纹理 bool GLByteFlowRender::CreateTextures() { LOGCATE("GLByteFlowRender::CreateTextures"); GLsizei yWidth = static_cast(m_RenderFrame.width); GLsizei yHeight = static_cast(m_RenderFrame.height); glActiveTexture(GL_TEXTURE0); glGenTextures(1, &m_YTextureId); glBindTexture(GL_TEXTURE_2D, m_YTextureId); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, yWidth, yHeight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); if (!m_YTextureId) { GLUtils::CheckGLError("Create Y texture"); return false; } GLsizei uWidth = static_cast(m_RenderFrame.width / 2); GLsizei uHeight = yHeight / 2; glActiveTexture(GL_TEXTURE1); glGenTextures(1, &m_UTextureId); glBindTexture(GL_TEXTURE_2D, m_UTextureId); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, uWidth, uHeight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); if (!m_UTextureId) { GLUtils::CheckGLError("Create U texture"); return false; } GLsizei vWidth = static_cast(m_RenderFrame.width / 2); GLsizei vHeight = (GLsizei) yHeight / 2; glActiveTexture(GL_TEXTURE2); glGenTextures(1, &m_VTextureId); glBindTexture(GL_TEXTURE_2D, m_VTextureId); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, vWidth, vHeight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL); if (!m_VTextureId) { GLUtils::CheckGLError("Create V texture"); return false; } return true; } //每传入一帧新数据后,更新纹理 bool GLByteFlowRender::UpdateTextures() { LOGCATE("GLByteFlowRender::UpdateTextures"); if (m_RenderFrame.ppPlane[0] == NULL) { return false; } if (!m_YTextureId && !m_UTextureId && !m_VTextureId && !CreateTextures()) { return false; } glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_YTextureId); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, (GLsizei) m_RenderFrame.width, (GLsizei) m_RenderFrame.height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_RenderFrame.ppPlane[0]); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, m_UTextureId); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, (GLsizei) m_RenderFrame.width >> 1, (GLsizei) m_RenderFrame.height >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_RenderFrame.ppPlane[1]); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, m_VTextureId); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, (GLsizei) m_RenderFrame.width >> 1, (GLsizei) m_RenderFrame.height >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_RenderFrame.ppPlane[2]); return true; } //绑定纹理到着色器,传入顶点和纹理坐标数据 GLuint GLByteFlowRender::UseProgram() { LOGCATE("GLByteFlowRender::UseProgram"); ByteFlowLock lock(&m_ShaderBufLock); if (m_IsShaderChanged) { GLUtils::DeleteProgram(m_Program); CreateProgram(kVertexShader, m_pFragShaderBuf); m_IsShaderChanged = false; m_IsProgramChanged = true; } if (!m_Program) { LOGCATE("GLByteFlowRender::UseProgram Could not use program."); return 0; } if (m_IsProgramChanged) { glUseProgram(m_Program); GLUtils::CheckGLError("GLByteFlowRender::UseProgram"); glVertexAttribPointer(m_VertexCoorHandle, 2, GL_FLOAT, GL_FALSE, 2 * 4, VERTICES_COORS); glEnableVertexAttribArray(m_VertexCoorHandle); glUniform1i(m_YTextureHandle, 0); glUniform1i(m_UTextureHandle, 1); glUniform1i(m_VTextureHandle, 2); glVertexAttribPointer(m_TextureCoorHandle, 2, GL_FLOAT, GL_FALSE, 2 * 4, TEXTURE_COORS); glEnableVertexAttribArray(m_TextureCoorHandle); m_IsProgramChanged = false; } return m_Program; } //渲染预览图像 void GLByteFlowRender::OnDrawFrame() { LOGCATE("GLByteFlowRender::OnDrawFrame"); glViewport(0, 0, m_ViewportWidth, m_ViewportHeight); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glDisable(GL_CULL_FACE); if (!UpdateTextures() || !UseProgram()) { LOGCATE("GLByteFlowRender::OnDrawFrame skip frame"); return; } glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } # LUT 滤镜 什么是 LUT ? LUT 是 Look Up Table 的简称,称作颜色查找表,是一种针对色彩空间的管理和转换技术。 它可以分为一维 LUT(1D LUT) 和 三维 LUT(3D LUT),其中三维 LUT 比较常用。 简单来讲,LUT 就是一个 RGB 组合到另一个 RGB 组合的映射关系表。 LUT(R, G, B) = (R1, G1, B1) LUT 滤镜是一种比较经典的滤镜,本质上属于独立像素点替换,即根据 OpenGL 采样器对纹理进行采样得到的像素点,再基于像素点的(R,G,B)分量查表,获得 LUT 映射的(R1,G1,B1),替换原来的输出。 一般 RGB 像素占用 3 个字节,包含 3 个分量,每个分量有 256 种取值,那么三维 LUT 模板就可以包含 256 X 256 X 256 种情况,占用 48MB 内存空间。 这样一个 LUT 模板内存占用过大同时也降低了查找的效率,通常会采取下采样方式来降低数据量。 例如可以对三维 LUT 模板每个分量分别进行 64 次采样,这样就获得一个 64 X 64 X 64 大小的映射关系表,对于不在表内的颜色值可以进行插值获得其相似结果。 三维 LUT 模板,即64 X 64 X 64 大小的映射关系表,通常是用一张分辨率为 512 X 512 的二维图片表示,称为 LUT 图(模板图)。 ![LUT 图](img/LUT 图.png) LUT 图在横竖方向上被分成了 8 X 8 一共 64 个小方格,每一个小方格内的 B(Blue)分量为一个定值,64 个小方格一共表示了 B 分量的 64 种取值。 对于每一个小方格,横竖方向又各自分为 64 个小格,以左下角为原点,横向小格的 R(Red)分量依次增加,纵向小格的 G(Green)分量依次增加。 至此,我们可以根据原始采样像素 RGB 中的 B 分量值,确定我们要选用 LUT 图中的第几个小格,然后再根据(R,G)分量值为纵横坐标,确定映射的 RGB 组合 // Lut 滤镜 #version 100 precision highp float; varying vec2 v_texcoord; //Lut 纹理 uniform sampler2D s_LutTexture; uniform lowp sampler2D s_textureY; uniform lowp sampler2D s_textureU; uniform lowp sampler2D s_textureV; vec4 YuvToRgb(vec2 uv) { float y, u, v, r, g, b; y = texture2D(s_textureY, uv).r; u = texture2D(s_textureU, uv).r; v = texture2D(s_textureV, uv).r; u = u - 0.5; v = v - 0.5; r = y + 1.403 * v; g = y - 0.344 * u - 0.714 * v; b = y + 1.770 * u; return vec4(r, g, b, 1.0); } void main() { //原始采样像素的 RGBA 值 vec4 textureColor = YuvToRgb(v_texcoord); //获取 B 分量值,确定 LUT 小方格的 index, 取值范围转为 0~63 float blueColor = textureColor.b * 63.0; //取与 B 分量值最接近的 2 个小方格的坐标 vec2 quad1; quad1.y = floor(floor(blueColor) / 8.0); quad1.x = floor(blueColor) - (quad1.y * 8.0); vec2 quad2; quad2.y = floor(ceil(blueColor) / 7.9999); quad2.x = ceil(blueColor) - (quad2.y * 8.0); //通过 R 和 G 分量的值确定小方格内目标映射的 RGB 组合的坐标,然后归一化,转化为纹理坐标。 vec2 texPos1; texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); vec2 texPos2; texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); //取目标映射对应的像素值 vec4 newColor1 = texture2D(s_LutTexture, texPos1); vec4 newColor2 = texture2D(s_LutTexture, texPos2); //使用 Mix 方法对 2 个边界像素值进行混合 vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), 1.0); } #3D 模型 OBJ 文件数据结构 # 开头的行表示注释行; mtllib 表示指定该 OBJ 文件所使用的 mtl 文件(材质文件); v 开头的行表示存放的是顶点坐标,后面三个数分别表示一个顶点的(x,y,z)坐标值; vn 开头的行表示存放的是顶点法向量,后面三个数分别表示一个顶点法向量的三维(x,y,z)分量值; vt 开头的行表示存放的是纹理坐标,后面三个数分别表示一个纹理坐标的(s,t,p)分量值,其中 p 分量一般用于 3D 纹理; usemtl 01___Default 表示使用指定 mtl 文件中名为 01___Default的材质; s 1 表示开启平滑渲染; f 开头的行表示存放的是一个三角面的信息,后面有三组数据分别表示组成三角面的三个顶点的信息,每个顶点信息的格式为:顶点位置索引/纹理坐标索引/法向量索引。 mtl 文件结构 newmtl 01___Default表示定义一个名为 01___Default 的材质; Ns 表示材质的反射指数,反射指数越高则高光越密集,取值范围在一般为 [0,1000]; Ni 表示材质的折射值(折射率),一般取值范围是 [0.001,10] ,取值为 1.0,表示光在通过物体的时候不发生弯曲,玻璃的折射率为 1.5 ; d 表示材质的渐隐指数(通透指数),取值为 1.0 表示完全不透明,取值为 0.0 时表示完全透明; Tr 表示材质的透明度(与 d 的取值相反),默认值为0.0(完全不透明); Tf 表示材质的滤光折射率,三维向量表示; illum 表示材质的光照模型; Ka 表示材质的环境光(Ambient Color)(r,g,b); Kd 表示材质的散射光(Diffuse Color)(r,g,b); Ks 表示材质的镜面光(Apecular Color)(r,g,b); Ke 表示材质的发射光,它与环境光,散射光和镜面光并存,代表材质发出的光量; map_Ka 表示为材质的环境反射指定纹理文件(纹理采样值与环境光相乘作为输出颜色的一部分加权); map_Kd 表示为材质的漫反射指定纹理文件; map_Ke 表示为材质的发射光指定纹理文件; map_d 表示为材质的透明度指定纹理文件; bump 表示指定材质的凹凸纹理文件,凹凸纹理修改表面法线,用于凹凸纹理的图像表示相对于平均表面的表面拓扑或高度(没用过)。 3D 模型的设计一般是由许多小模型拼接组合成一个完整的大模型,一个小模型作为一个独立的渲染单元,我们称这些小模型为网格(Mesh)。 网格作为独立的渲染单元至少需要包含一组顶点数据,每个顶点数据包含一个位置向量,一个法向量和一个纹理坐标,有了纹理坐标也需要为网格指定纹理对应的材质,还有绘制时顶点的索引。 这样我们可以为 Mesh 定义一个顶点: struct Vertex { // 位置向量 glm::vec3 Position; // 法向量 glm::vec3 Normal; // 纹理坐标 glm::vec2 TexCoords; }; 还需要一个描述纹理信息的结构体: struct Texture { GLuint id;//纹理 id ,OpenGL 环境下创建 String type; //纹理类型(diffuse纹理或者specular纹理) }; 网格作为独立的渲染单元至少需要包含一组顶点数据以及顶点的索引和纹理,可以定义如下: class Mesh { Public: vector vertices;//一组顶点 vector indices;//顶点对应的索引 vector textures;//纹理 Mesh(vector vertices, vector indices, vector texture); Void Draw(Shader shader); private: GLuint VAO, VBO, EBO; void initMesh(); void Destroy(); } 我们通过 initMesh 方法创建相应的 VAO、VBO、EBO,初始化缓冲,设置着色器程序的 uniform 变量 Mesh(vector vertices, vector indices, vector textures) { this->vertices = vertices; this->indices = indices; this->textures = textures; this->initMesh(); } void initMesh() { //生成 VAO、VBO、EBO glGenVertexArrays(1, &this->VAO); glGenBuffers(1, &this->VBO); glGenBuffers(1, &this->EBO); //初始化缓冲区 glBindVertexArray(this->VAO); glBindBuffer(GL_ARRAY_BUFFER, this->VBO); glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex), &this->vertices[0], GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint), &this->indices[0], GL_STATIC_DRAW); // 设置顶点坐标指针 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)0); // 设置法线指针 glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, Normal)); // 设置顶点的纹理坐标 glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, TexCoords)); glBindVertexArray(0); } //销毁纹理和缓冲区对象 void Destroy() { for (int i = 0; i < textures.size(); ++i) { glDeleteTextures(1, &textures[i].id); } glDeleteBuffers(1, &EBO); glDeleteBuffers(1, &VBO); glDeleteVertexArrays(1, &VAO); VAO = EBO = VBO = GL_NONE; } 预处理指令 offsetof 用于计算结构体属性的偏移量,把结构体作为它的第一个参数,第二个参数是这个结构体名字的变量,函数返回这个变量从结构体开始的字节偏移量(offset)。 如:offsetof(Vertex, Normal) 返回 12 个字节,即 3 * sizeof(float) 。 我们用到的顶点着色器(简化后): #version 300 es layout (location = 0) in vec3 a_position; layout (location = 1) in vec3 a_normal; layout (location = 2) in vec2 a_texCoord; out vec2 v_texCoord; uniform mat4 u_MVPMatrix; void main() { v_texCoord = a_texCoord; vec4 position = vec4(a_position, 1.0); gl_Position = u_MVPMatrix * position; } 而使用的片段着色器需要根据使用到的纹理数量和类型的不同做不同的调整。如只有一个 diffuse 纹理的片段着色器如下 #version 300 es out vec4 outColor; in vec2 v_texCoord; uniform sampler2D texture_diffuse1; void main() { outColor = texture(texture_diffuse1, v_texCoord); } 假如在一个网格中我们有 3 个 diffuse 纹理和 3 个 specular 纹理,那么对应的片段着色器中采样器的声明如下: uniform sampler2D texture_diffuse1; uniform sampler2D texture_diffuse2; uniform sampler2D texture_diffuse3; uniform sampler2D texture_specular1; uniform sampler2D texture_specular2; uniform sampler2D texture_specular3; 总结起来就是我们需要根据 Mesh 中纹理的数量和类型以及模型光照需求来使用不同的片段着色器和顶点着色器。 Mesh 的渲染的逻辑: //渲染网格 void Draw(Shader shader) { unsigned int diffuseNr = 1; unsigned int specularNr = 1; //遍历各个纹理,根据纹理的数量和类型确定采样器变量名 for(unsigned int i = 0; i < textures.size(); i++) { glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding string number; string name = textures[i].type; if(name == "texture_diffuse") number = std::to_string(diffuseNr++); else if(name == "texture_specular") number = std::to_string(specularNr++); // transfer unsigned int to stream glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i); // and finally bind the texture glBindTexture(GL_TEXTURE_2D, textures[i].id); } //绘制网格 glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); glActiveTexture(GL_TEXTURE0); } Shader 类的逻辑简单包含了着色器程序的创建销毁和 uniform 类型变量的设置。 class Shader { public: unsigned int ID;//着色器程序的 ID Shader(const char* vertexStr, const char* fragmentStr) { //创建着色器程序 ID = GLUtils::CreateProgram(vertexStr, fragmentStr); } void Destroy() { //销毁着色器程序 GLUtils::DeleteProgram(ID); } void use() { glUseProgram(ID); } void setFloat(const std::string &name, float value) const { glUniform1f(glGetUniformLocation(ID, name.c_str()), value); } void setVec3(const std::string &name, float x, float y, float z) const { glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z); } void setMat4(const std::string &name, const glm::mat4 &mat) const { glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); } }; 前面我们知道了一个模型(Model)包含许多个网格(Mesh),各个 Mesh 独立渲染共同组成整个 Model。Model 类可定义如下 class Model { public: Model(GLchar* path) { loadModel(path); } //渲染模型,即依次渲染各个网格 void Draw(Shader shader); //销毁模型的所有网格 void Destroy(); private: //模型所包含的网格 vector meshes; //模型文件所在目录 string directory; //加载模型 void loadModel(string path); //处理 aiScene 对象包含的节点和子节点 void processNode(aiNode* node, const aiScene* scene); //生成网格 Mesh processMesh(aiMesh* mesh, const aiScene* scene); //创建纹理并加载图像数据 vector loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName); }; 使用 Assimp 加载 3D 模型比较简单,最终模型被加载到一个 Assimp 中定义的 aiScene 对象中,aiScene 对象除了包含网格和材质,还包含一个 aiNode 对象(根节点),然后我们还需要遍历各个子节点的网格。 #include "assimp/Importer.hpp" #include "assimp/scene.h" #include "assimp/postprocess.h" Assimp::Importer importer; const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs); 参数 aiProcess_Triangulate 表示如果模型不是(全部)由三角形组成,应该转换所有的模型的原始几何形状为三角形;aiProcess_FlipUVs 表示基于 y 轴翻转纹理坐标。 Model 类中加载模型的函数 void loadModel(string const &path) { Assimp::Importer importer; const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs); if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero { LOGCATE("Model::loadModel path=%s, assimpError=%s", path, importer.GetErrorString()); return; } directory = path.substr(0, path.find_last_of('/')); //处理节点 processNode(scene->mRootNode, scene); } //递归处理所有节点 void processNode(aiNode *node, const aiScene *scene) { for(unsigned int i = 0; i < node->mNumMeshes; i++) { aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; if(mesh != nullptr) meshes.push_back(processMesh(mesh, scene)); } for(unsigned int i = 0; i < node->mNumChildren; i++) { processNode(node->mChildren[i], scene); } } //生成网格 Mesh Mesh processMesh(aiMesh* mesh, const aiScene* scene) { vector vertices; vector indices; vector textures; for(GLuint i = 0; i < mesh->mNumVertices; i++) { Vertex vertex; // 处理顶点坐标、法线和纹理坐标 ... vertices.push_back(vertex); } // 处理顶点索引 for(unsigned int i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; for(unsigned int j = 0; j < face.mNumIndices; j++) indices.push_back(face.mIndices[j]); } // 处理材质 if(mesh->mMaterialIndex >= 0) { aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); ... } return Mesh(vertices, indices, textures); } 在 native 层加载纹理的时候,我们使用 OpenCV 对图片进行解码,然后生成纹理对象: unsigned int TextureFromFile(const char *path, const string &directory) { string filename = string(path); filename = directory + '/' + filename; unsigned int textureID; glGenTextures(1, &textureID); unsigned char *data = nullptr; LOGCATE("TextureFromFile Loading texture %s", filename.c_str()); //使用 OpenCV 对图片进行解码 cv::Mat textureImage = cv::imread(filename); if (!textureImage.empty()) { // OpenCV 默认解码成 BGR 格式,这里转换为 RGB cv::cvtColor(textureImage, textureImage, CV_BGR2RGB); glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureImage.cols, textureImage.rows, 0, GL_RGB, GL_UNSIGNED_BYTE, textureImage.data); glGenerateMipmap(GL_TEXTURE_2D); 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); GO_CHECK_GL_ERROR(); } else { LOGCATE("TextureFromFile Texture failed to load at path: %s", path); } return textureID; } 绘制模型就是遍历每个 Mesh 进行绘制: void Draw(Shader shader) { for(unsigned int i = 0; i < meshes.size(); i++) meshes[i].Draw(shader); } 最后就是这个 Model 类的使用示例: //初始化,加载模型 m_pModel = new Model("/sdcard/model/poly/Apricot_02_hi_poly.obj"); m_pShader = new Shader(vShaderStr, fShaderStr); //绘制模型 glClearColor(0.5f, 0.5f, 0.5f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH); m_pShader->use(); m_pShader->setMat4("u_MVPMatrix", m_MVPMatrix); m_pModel->Draw((*m_pShader)); //销毁对象 if (m_pModel != nullptr) { m_pModel->Destroy(); delete m_pModel; m_pModel = nullptr; } if (m_pShader != nullptr) { m_pShader->Destroy(); delete m_pShader; m_pShader = nullptr; } #PBO OpenGL PBO(Pixel Buffer Object),被称为像素缓冲区对象,主要被用于异步像素传输操作。 PBO 仅用于执行像素传输,不连接到纹理,且与 FBO (帧缓冲区对象)无关。 OpenGL PBO(像素缓冲区对象) 类似于 VBO(顶点缓冲区对象),PBO 开辟的也是 GPU 缓存,而存储的是图像数据。 ![PBO](img/PBO.png) 与 PBO 绑定相关的 Target 标签有 2 个:GL_PIXEL_UNPACK_BUFFER 和 GL_PIXEL_PACK_BUFFER 。 其中将 PBO 绑定为 GL_PIXEL_UNPACK_BUFFER 时,glTexImage2D() 和 glTexSubImage2D() 表示从 PBO 中解包(unpack)像素数据并复制到帧缓冲区 。 将 PBO 绑定为 GL_PIXEL_PACK_BUFFER 时,glReadPixels() 表示从帧缓冲区中读取像素数据并打包进(pack) PBO 。 为什么要用 PBO? 在 OpenGL 开发中,特别是在低端平台上处理高分辨率的图像时,图像数据在内存和显存之前拷贝往往会造成性能瓶颈,而利用 PBO 可以在一定程度上解决这个问题。 使用 PBO 可以在 GPU 的缓存间快速传递像素数据,不影响 CPU 时钟周期,除此之外,PBO 还支持异步传输。 ![](img/常规从文件中加载纹理.png) 上图从文件中加载纹理,图像数据首先被加载到 CPU 内存中,然后通过 glTexImage2D 函数将图像数据从 CPU 内存复制到 OpenGL 纹理对象中 (GPU 内存),两次数据传输(加载和复制)完全由 CPU 执行和控制。 ![](img/PBO文件中加载纹理.png) 如上图所示,文件中的图像数据可以直接加载到 PBO 中,这个操作是由 CPU 控制。我们可以通过 glMapBufferRange 获取 PBO 对应 GPU 缓冲区的内存地址 将图像数据加载到 PBO 后,再将图像数据从 PBO 传输到纹理对象中完全是由 GPU 控制,不会占用 CPU 时钟周期。 所以,绑定 PBO 后,执行 glTexImage2D (将图像数据从 PBO 传输到纹理对象) 操作,CPU 无需等待,可以立即返回。 通过对比这两种(将图像数据传送到纹理对象中)方式,可以看出,利用 PBO 传输图像数据,省掉了一步 CPU 耗时操作(将图像数据从 CPU 内存复制到 纹理对象中)。 怎么用 PBO? int imgByteSize = m_Image.width * m_Image.height * 4;//RGBA glGenBuffers(1, &uploadPboId); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboId); glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, 0, GL_STREAM_DRAW); glGenBuffers(1, &downloadPboId); glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId); glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, 0, GL_STREAM_DRAW); PBO 的创建和初始化类似于 VBO ,以上示例表示创建 PBO ,并申请大小为 imgByteSize 的缓冲区。 绑定为 GL_PIXEL_UNPACK_BUFFER 表示该 PBO 用于将像素数据从程序传送到 OpenGL 中; 绑定为 GL_PIXEL_PACK_BUFFER 表示该 PBO 用于从 OpenGL 中读回像素数据. 从上面内容我们知道,加载图像数据到纹理对象时,CPU 负责将图像数据拷贝到 PBO ,而 GPU 负责将图像数据从 PBO 传送到纹理对象。 所以,当我们使用多个 PBO 时,通过交换 PBO 的方式进行拷贝和传送,可以实现这两步操作同时进行 使用两个 PBO 加载图像数据到纹理对象 ![](img/使用两个PBO加载数据到纹理对象.png) 如图示,利用 2 个 PBO 加载图像数据到纹理对象,使用 glTexSubImage2D 通知 GPU 将图像数据从 PBO1 传送到纹理对象,同时 CPU 将新的图像数据复制到 PBO2 中 int dataSize = m_RenderImage.width * m_RenderImage.height * 4; //使用 `glTexSubImage2D` 将图像数据从 PBO1 传送到纹理对象 int index = m_FrameIndex % 2; int nextIndex = (index + 1) % 2; BEGIN_TIME("PBOSample::UploadPixels Copy Pixels from PBO to Textrure Obj") glBindTexture(GL_TEXTURE_2D, m_ImageTextureId); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m_UploadPboIds[index]); //调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_RenderImage.width, m_RenderImage.height, GL_RGBA, GL_UNSIGNED_BYTE, 0); END_TIME("PBOSample::UploadPixels Copy Pixels from PBO to Textrure Obj") //更新图像数据,复制到 PBO 中 BEGIN_TIME("PBOSample::UploadPixels Update Image data") glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m_UploadPboIds[nextIndex]); glBufferData(GL_PIXEL_UNPACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW); GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, dataSize, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT); LOGCATE("PBOSample::UploadPixels bufPtr=%p",bufPtr); if(bufPtr) { memcpy(bufPtr, m_RenderImage.ppPlane[0], static_cast(dataSize)); //update image data int randomRow = rand() % (m_RenderImage.height - 5); memset(bufPtr + randomRow * m_RenderImage.width * 4, 188, static_cast(m_RenderImage.width * 4 * 5)); glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); } glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); END_TIME("PBOSample::UploadPixels Update Image data") ![](img/不使用PBO耗时.png) ![](img/使用PBO耗时.png) 使用两个 PBO 从帧缓冲区读回图像数据 ![](img/用两个PBO从帧缓冲区读回图像数据.png) 如上图所示,利用 2 个 PBO 从帧缓冲区读回图像数据,使用 glReadPixels 通知 GPU 将图像数据从帧缓冲区读回到 PBO1 中,同时 CPU 可以直接处理 PBO2 中的图像数据。 //交换 PBO int index = m_FrameIndex % 2; int nextIndex = (index + 1) % 2; //将图像数据从帧缓冲区读回到 PBO 中 BEGIN_TIME("DownloadPixels glReadPixels with PBO") glBindBuffer(GL_PIXEL_PACK_BUFFER, m_DownloadPboIds[index]); glReadPixels(0, 0, m_RenderImage.width, m_RenderImage.height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); END_TIME("DownloadPixels glReadPixels with PBO") // glMapBufferRange 获取 PBO 缓冲区指针 BEGIN_TIME("DownloadPixels PBO glMapBufferRange") glBindBuffer(GL_PIXEL_PACK_BUFFER, m_DownloadPboIds[nextIndex]); GLubyte *bufPtr = static_cast(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize, GL_MAP_READ_BIT)); if (bufPtr) { nativeImage.ppPlane[0] = bufPtr; //NativeImageUtil::DumpNativeImage(&nativeImage, "/sdcard/DCIM", "PBO"); glUnmapBuffer(GL_PIXEL_PACK_BUFFER); } glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); END_TIME("DownloadPixels PBO glMapBufferRange") 我们对比下从帧缓冲区读回图像数据,使用 PBO 和不使用 PBO 两种情况的耗时差别: ![](img/不使用PBO读取图像数据.png) ![](img/使用PBO读取图像数据.png) # FFMPEG FFmpeg 有六个常用的功能模块: libavformat:多媒体文件或协议的封装和解封装库,如 Mp4、Flv 等文件封装格式,RTMP、RTSP 等网络协议封装格式; libavcodec:音视频编解码库; libavfilter:音视频、字幕滤镜库; libswscale:图像格式转换库; libswresample:音频重采样库; libavutil:工具库; #视频解码流程: //1.创建封装格式上下文 m_AVFormatContext = avformat_alloc_context(); //2.打开输入文件,解封装 if(avformat_open_input(&m_AVFormatContext, m_Url, NULL, NULL) != 0) { LOGCATE("DecoderBase::InitFFDecoder avformat_open_input fail."); break; } //3.获取音视频流信息 if(avformat_find_stream_info(m_AVFormatContext, NULL) < 0) { LOGCATE("DecoderBase::InitFFDecoder avformat_find_stream_info fail."); break; } //4.获取音视频流索引 for(int i=0; i < m_AVFormatContext->nb_streams; i++) { if(m_AVFormatContext->streams[i]->codecpar->codec_type == m_MediaType) { m_StreamIndex = i; break; } } f(m_StreamIndex == -1) { LOGCATE("DecoderBase::InitFFDecoder Fail to find stream index."); break; } //5.获取解码器参数 AVCodecParameters *codecParameters = m_AVFormatContext->streams[m_StreamIndex]->codecpar; //6.根据 codec_id 获取解码器 m_AVCodec = avcodec_find_decoder(codecParameters->codec_id); if(m_AVCodec == nullptr) { LOGCATE("DecoderBase::InitFFDecoder avcodec_find_decoder fail."); break; } //7.创建解码器上下文 m_AVCodecContext = avcodec_alloc_context3(m_AVCodec); if(avcodec_parameters_to_context(m_AVCodecContext, codecParameters) != 0) { LOGCATE("DecoderBase::InitFFDecoder avcodec_parameters_to_context fail."); break; } //8.打开解码器 result = avcodec_open2(m_AVCodecContext, m_AVCodec, NULL); if(result < 0) { LOGCATE("DecoderBase::InitFFDecoder avcodec_open2 fail. result=%d", result); break; } //9.创建存储编码数据和解码数据的结构体 m_Packet = av_packet_alloc(); //创建 AVPacket 存放编码数据 m_Frame = av_frame_alloc(); //创建 AVFrame 存放解码后的数据 //10.解码循环 while (av_read_frame(m_AVFormatContext, m_Packet) >= 0) { //读取帧 if (m_Packet->stream_index == m_StreamIndex) { if (avcodec_send_packet(m_AVCodecContext, m_Packet) != 0) { //视频解码 return -1; } while (avcodec_receive_frame(m_AVCodecContext, m_Frame) == 0) { //获取到 m_Frame 解码数据,在这里进行格式转换,然后进行渲染,下一节介绍 ANativeWindow 渲染过程 } } av_packet_unref(m_Packet); //释放 m_Packet 引用,防止内存泄漏 } //11.释放资源,解码完成 if(m_Frame != nullptr) { av_frame_free(&m_Frame); m_Frame = nullptr; } if(m_Packet != nullptr) { av_packet_free(&m_Packet); m_Packet = nullptr; } if(m_AVCodecContext != nullptr) { avcodec_close(m_AVCodecContext); avcodec_free_context(&m_AVCodecContext); m_AVCodecContext = nullptr; m_AVCodec = nullptr; } if(m_AVFormatContext != nullptr) { avformat_close_input(&m_AVFormatContext); avformat_free_context(m_AVFormatContext); m_AVFormatContext = nullptr; } ANativeWindow 渲染解码帧 每一种操作系统都定义了自己的窗口系统,而 ANativeWindow 就是 Android 的本地窗口,在 Android Java 层,Surface 又继承于 ANativeWindow 实际上 Surface 是 ANativeWindow 的具体实现,所以一个 ANativeWindow 表示的就是一块屏幕缓冲区。 我们要渲染一帧图像,只需要将图像数据刷进 ANativeWindow 所表示的屏幕缓冲区即可。 enum { // NOTE: these values must match the values from graphics/common/x.x/types.hal /** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Alpha: 8 bits. **/ WINDOW_FORMAT_RGBA_8888 = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, /** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Unused: 8 bits. **/ WINDOW_FORMAT_RGBX_8888 = AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM, /** Red: 5 bits, Green: 6 bits, Blue: 5 bits. **/ WINDOW_FORMAT_RGB_565 = AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM, }; 需要注意的是 ANativeWindow 仅支持 RGB 类型的图像数据,所以我们还需要利用 libswscale 库将解码后的 YUV 数据转成 RGB 。 利用 libswscale 库将对图像进行格式转换,有如下几个步骤: //1. 分配存储 RGB 图像的 buffer m_VideoWidth = m_AVCodecContext->width; m_VideoHeight = m_AVCodecContext->height; m_RGBAFrame = av_frame_alloc(); //计算 Buffer 的大小 int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_VideoWidth, m_VideoHeight, 1); //为 m_RGBAFrame 分配空间 m_FrameBuffer = (uint8_t *) av_malloc(bufferSize * sizeof(uint8_t)); av_image_fill_arrays(m_RGBAFrame->data, m_RGBAFrame->linesize, m_FrameBuffer, AV_PIX_FMT_RGBA, m_VideoWidth, m_VideoHeight, 1); //2. 获取转换的上下文 m_SwsContext = sws_getContext(m_VideoWidth, m_VideoHeight, m_AVCodecContext->pix_fmt, m_RenderWidth, m_RenderHeight, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, NULL, NULL, NULL); //3. 格式转换 sws_scale(m_SwsContext, frame->data, frame->linesize, 0, m_VideoHeight, m_RGBAFrame->data, m_RGBAFrame->linesize); //4. 释放资源 if(m_RGBAFrame != nullptr) { av_frame_free(&m_RGBAFrame); m_RGBAFrame = nullptr; } if(m_FrameBuffer != nullptr) { free(m_FrameBuffer); m_FrameBuffer = nullptr; } if(m_SwsContext != nullptr) { sws_freeContext(m_SwsContext); m_SwsContext = nullptr; } 我们拿到了 RGBA 格式的图像,可以利用 ANativeWindow 进行渲染了 //1. 利用 Java 层 SurfaceView 传下来的 Surface 对象,获取 ANativeWindow m_NativeWindow = ANativeWindow_fromSurface(env, surface); //2. 设置渲染区域和输入格式 ANativeWindow_setBuffersGeometry(m_NativeWindow, m_VideoWidth, m_VideoHeight, WINDOW_FORMAT_RGBA_8888); //3. 渲染 ANativeWindow_Buffer m_NativeWindowBuffer; //锁定当前 Window ,获取屏幕缓冲区 Buffer 的指针 ANativeWindow_lock(m_NativeWindow, &m_NativeWindowBuffer, nullptr); uint8_t *dstBuffer = static_cast(m_NativeWindowBuffer.bits); int srcLineSize = m_RGBAFrame->linesize[0];//输入图的步长(一行像素有多少字节) int dstLineSize = m_NativeWindowBuffer.stride * 4;//RGBA 缓冲区步长 for (int i = 0; i < m_VideoHeight; ++i) { //一行一行地拷贝图像数据 memcpy(dstBuffer + i * dstLineSize, m_FrameBuffer + i * srcLineSize, srcLineSize); } //解锁当前 Window ,渲染缓冲区数据 ANativeWindow_unlockAndPost(m_NativeWindow); //4. 释放 ANativeWindow if(m_NativeWindow) ANativeWindow_release(m_NativeWindow); #音频解码流程: 1.FFmpeg Android so 编译 https://juejin.cn/post/6845166891841880072