OpenGL
glInvalidateFramebuffer vs glClear
glInvalidateFramebuffer 和 glClear 是OpenGL中两个不同的函数,它们的作用和用法有所不同:
- glInvalidateFramebuffer:
- 功能:glInvalidateFramebuffer 用于显式地标记帧缓冲区的某些部分为无效。这样,GPU就知道这些部分的内容不再需要,可以避免不必要的数据交换。
- 应用场景:通常在使用帧缓冲区对象(FBO)时,当我们切换到不同的FBO或者不再需要某些颜色、深度或模板缓冲区的内容时,可以调用该函数。
- 性能影响:尽管在RenderDoc等工具中可能显示glInvalidateFramebuffer的耗时较高,但实际上,只有少数几次调用不会对渲染性能产生影响。
- glClear:
- 功能:glClear 用于清除当前帧缓冲区的内容,包括颜色缓冲区、深度缓冲区和模板缓冲区。
- 应用场景:在每一帧开始时,我们通常会调用glClear来准备帧缓冲区,以便进行新的绘制。
- 性能影响:glClear的性能开销通常较小,但在某些情况下,如果频繁调用,可能会影响性能。
总结:
- glInvalidateFramebuffer 用于标记帧缓冲区的部分内容为无效,以减少不必要的数据交换。
- glClear 用于清除整个帧缓冲区的内容,以准备进行新的绘制。
- 在使用RenderDoc等工具时,可以忽略glInvalidateFramebuffer的耗时,但需要关注片上高速缓存回写内存的消耗 。
glDiscardFramebufferEXT
void glDiscardFramebufferEXT(enum target, sizei numAttachments, const enum *attachments);
这个扩展提供了一个新的命令,glDiscardFramebufferEXT,它会使得指定帧缓冲附件的内容变为未定义状态。在未来的操作修改内容之前,这些指定缓冲区的内容是未定义的,只有被修改的区域保证包含有效内容。有效地使用此命令可以为实现提供新的优化机会。 一些 OpenGL ES 实现会将帧缓冲图像缓存到一个小的快速内存池中。在渲染之前,这些实现必须将逻辑缓冲区(如颜色、深度、模板等)的现有内容加载到该内存中。渲染后,这些缓冲区中的一部分或全部也会被存储回外部内存,以便将来再次使用其内容。在许多应用程序中,逻辑缓冲区在渲染开始时被清除。如果是这样,加载或存储这些缓冲区的工作就是浪费的。 即使没有这个扩展,如果渲染的一帧从全屏清除开始,OpenGL ES 实现也可以优化掉在渲染帧之前加载帧缓冲区内容的步骤。有了这个扩展,应用程序可以使用 DiscardFramebufferEXT 来表示帧缓冲区的内容将不再需要。在这种情况下,OpenGL ES 实现也可以优化掉在渲染帧后存储帧缓冲区内容的步骤。
glDiscardFramebufferEXT的工作是告知驱动程序你不关心framebuffer的内容。什么驱动程序(或GPU)决定用它做什么 - 这不取决于你。驱动程序可以将所有内容重置为0,或者它可以保持原样,或者当您下次调用glClear时它将使用此信息并且将更有效地执行它(例如通过为内容分配新内存,而不是执行memset与0值)。不要担心它会做什么
Texture obj与RBO的区别
FBO(Frame Buffer Object)即帧缓冲区对象,是一个可添加缓冲区的容器,可以为其添加纹理或渲染缓冲区对象(RBO),它们的区别如下图
VAO,VBO,VEO
在OpenGL中,VAO(Vertex Array Object)、VBO(Vertex Buffer Object)和EBO(Element Buffer Object)是用于管理和组织顶点数据的重要工具。它们在现代OpenGL编程中扮演着关键角色。以下是它们的区别和联系:
Vertex Array Object (VAO)
VAO是顶点数组对象,用于存储关于顶点属性配置的状态。一个VAO可以绑定多个VBO和EBO,并记录这些缓冲区的绑定状态和顶点属性配置。使用VAO可以简化顶点属性的管理和绑定,便于切换不同的顶点数据配置。
一个VAO可以关联多组记录顶点不同属性的VBO,在实际使用时只绑定该VAO就完成所有VBO的关联。见下文smaple code
主要功能
- 记录顶点属性指针的配置。
- 记录与之关联的VBO和EBO的绑定状态。
- 简化绘制调用时的状态切换
Vertex Buffer Object (VBO)
VBO是顶点缓冲对象,用于在GPU内存中存储顶点数据(如顶点位置、法线、纹理坐标等)。VBO的主要优点是能够显著提高顶点数据的传输效率,因为数据存储在GPU内存中而非CPU内存中。
主要功能
- 存储顶点数据。
- 提供高效的数据传输和存取方式。
Element Buffer Object (EBO)
EBO是元素缓冲对象(也称为索引缓冲对象,IBO),用于在GPU内存中存储顶点索引。EBO允许我们通过索引数组来绘制图形,从而避免顶点数据的重复,提高存储效率和渲染性能。
主要功能
- 存储顶点索引。
- 通过索引数组绘制顶点,从而减少重复顶点数据。
smaple code
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
// 顶点位置数据
GLfloat positions[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.5f, 0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};
// 顶点颜色数据
GLfloat colors[] = {
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f
};
// 索引数据
GLuint indices[] = {
0, 1, 2,
2, 3, 0
};
int main() {
// 初始化GLFW
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW" << std::endl;
return -1;
}
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "VAO with Multiple VBOs", nullptr, nullptr);
if (!window) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 初始化GLEW
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) {
std::cerr << "Failed to initialize GLEW" << std::endl;
return -1;
}
// 生成并绑定VAO
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 生成并绑定位置VBO
GLuint vboPositions;
glGenBuffers(1, &vboPositions);
glBindBuffer(GL_ARRAY_BUFFER, vboPositions);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 生成并绑定颜色VBO
GLuint vboColors;
glGenBuffers(1, &vboColors);
glBindBuffer(GL_ARRAY_BUFFER, vboColors);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
// 生成并绑定EBO
GLuint ebo;
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 解绑VAO
glBindVertexArray(0);
// 渲染循环
while (!glfwWindowShouldClose(window)) {
// 清屏
glClear(GL_COLOR_BUFFER_BIT);
// 绘制, 在使用时只要绑定vao即可
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// 交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
// 释放资源
glDeleteVertexArrays(1, &vao);
glDeleteBuffers(1, &vboPositions);
glDeleteBuffers(1, &vboColors);
glDeleteBuffers(1, &ebo);
// 终止GLFW
glfwTerminate();
return 0;
}
shader
初始化
变量
uniform变量
graph LR;
App-->Vertext_Shader;
Vertext_Shader-->Frag_Shader;
uniform变量是外部application程序传递给(vertex和fragment)shader的变量。因此它是application通过函数glUniform**()函数赋值的。在(vertex和fragment)shader程序内部,uniform变量就像是C语言里面的常量(const ),它不能被shader程序修改。
attribute变量
graph LR;
App-->Vertext_Shader;
attribute变量是只能在vertex shader中使用的变量。它不能在fragment shader中声明attribute变量,也不能被fragment shader中使用。一般用attribute变量来表示一些顶点的数据,如:顶点坐标,法线,纹理坐标,顶点颜色等。在application中,一般用函数glBindAttribLocation()来绑定每个attribute变量的位置,然后用函数glVertexAttribPointer()为每个attribute变量赋值。
varying变量
graph LR;
Vertext_Shader-->Frag_Shader;
varying变量是vertex和fragment shader之间做数据传递用的。一般vertex shader修改varying变量的值,然后fragment shader使用该varying变量的值。因此varying变量在vertex和fragment shader二者之间的声明必须是一致的。application不能使用此变量。
资源释放
dmabuf资源释放主要考虑texture,eglImage 和dmabuf, FBO不需要考虑。
-
texture、 eglImage和 dmabuf是一体的,释放时需要调用glDeleteTextures, eglDestroyImageKHR, close(dmabuf), 三个都完成后 dmabuf才真正释放。
-
如果texture附加到一个FBO上,glDeleteTextrues()后会使得该FBO的附加纹理为0.
-
如果前面已经attach 一个纹理到FBO, 在attach 新纹理到另一个FBO时,要先把前一个FBO的纹理设置为0, 否则释放不掉。
int main(int argc, char const *argv[]){
init_dev();
egl_Init();
dmabuf_fd = allocate_dmabuf(1920*1080*4);
textureid = create_texture_for_dmabuf(dmabuf_fd, 1920, 1080, &eglImg);
glGenFramebuffers(1, &FBO2);
glBindFramebuffer(GL_FRAMEBUFFER, FBO2);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureid);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, textureid, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
assert(0);
}
// 得到当前附加纹理ID为 1
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &text);
free_dmabuf(dmabuf_fd);
glDeleteTextures(1, &textureid);
eglDestroyImageKHR(g_EGLDisplay, eglImg);
// 完成后 /sys/kernel/debug/dmabuf/bufferinfo 中dmabuf已经释放
// 得到当前附加纹理ID为 0
glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &text);
glDeleteFramebuffers(1, &FBO2);
eglDestroyContext(g_EGLDisplay, g_EGLContext);
return 0;
int main(int argc, char const *argv[]){
init_dev();
egl_Init();
dmabuf_fd1 = allocate_dmabuf(1920*1080*4);
textureid1 = create_texture_for_dmabuf(dmabuf_fd1, 1920, 1080, &eglImg1);
dmabuf_fd2 = allocate_dmabuf(1920*1080*4);
textureid2 = create_texture_for_dmabuf(dmabuf_fd2, 1920, 1080, &eglImg2);
glGenFramebuffers(1, &FBO1);
glBindFramebuffer(GL_FRAMEBUFFER, FBO1);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureid1);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, textureid1, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
assert(0);
}
// 注意要设置FBO 的纹理为0, 否则后面不能真正释放
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, 0, 0);
glGenFramebuffers(1, &FBO2);
glBindFramebuffer(GL_FRAMEBUFFER, FBO2);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureid2);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_EXTERNAL_OES, textureid2, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
assert(0);
}
free_dmabuf(dmabuf_fd1);
glDeleteTextures(1, &textureid1);
eglDestroyImageKHR(g_EGLDisplay, eglImg1);
free_dmabuf(dmabuf_fd2);
glDeleteTextures(1, &textureid2);
eglDestroyImageKHR(g_EGLDisplay, eglImg2);
// 完成后 /sys/kernel/debug/dmabuf/bufferinfo 中dmabuf已经释放
glDeleteFramebuffers(1, &FBO1);
glDeleteFramebuffers(1, &FBO2);
eglDestroyContext(g_EGLDisplay, g_EGLContext);
return 0;
glWaitSync VS glClientWaitSync
硬件层面的指令执行
glWaitSync是在GPU上等待fence,
glClientWaitSync是在CPU上等待fence
纹理过滤
一个纹理通常是由很多的像素点组成的,那么通过纹理坐标如何得到对应点应该的颜色,这就是纹理采样方式或纹理过滤。
下面是两种重要的纹理采样(纹理过滤):
- GL_NEAREST:邻近过滤,这个是默认纹理过滤方式,OpenGL会选择中心点最接近纹理坐标的那个像素. 这种方式放大有颗粒感
- GL_LINEAR:线性过滤,它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色,一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大, 放大后较平滑。
二者放大后的效果比较
当纹理被放大或缩小贴到一个平面时,我们可以设置不同的纹理采样(纹理过滤)方式,达到不同的效果。它所涉及到的OpenGL 函数如下:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // 缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大
小纹理贴到大目标上
让我们比较一下 在一个很大的物体上应用一张低分辨率的纹理的不同效果
大纹理贴到小目标上
在这种情况下, 会产生所谓的摩尔波纹
Mipmap
如何解决上面摩尔波纹的问题,GPU 引入了Mipmap(多级渐远纹理)概念。它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。
使用Mipmap的渲染过程:
如果一个2020的像素区域需要映射400400的纹理像素区域时,首先检测到一颗像素需要映射到纹理像素为2020,然后在Mipmap纹理中里寻找最接近2020纹理像素的多级渐远纹理,并使用此多级渐远纹理进行采样
此时过滤方式的设置
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
使用方法
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 加载原始纹理图像数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
// 自动生成 Mipmap
glGenerateMipmap(GL_TEXTURE_2D);
// 设置 Mipmap 过滤模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
纹理环绕
纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像。
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色 |
相关函数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
如果我们选择GL_CLAMP_TO_BORDER选项,我们还需要指定一个边缘的颜色。这需要使用glTexParameter函数的fv后缀形式,用GL_TEXTURE_BORDER_COLOR作为它的选项,并且传递一个float数组作为边缘的颜色值:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
glMapBufferRange
glMapBufferRange
是一个 OpenGL 函数,用于将指定缓冲对象的一部分数据映射到客户端的地址空间中,可以减少一次CPU 内存数据的读写。 以下是关于 glMapBufferRange
的详细说明:
- 作用:
glMapBufferRange
允许你直接访问缓冲对象的数据,以便在客户端代码中进行读取或写入操作。- 通过映射缓冲区的一部分,你可以有效地操作其中的数据。
- 参数:
target
:目标缓冲对象类型,通常为GL_ARRAY_BUFFER
或GL_ELEMENT_ARRAY_BUFFER
。offset
:要映射的数据在缓冲对象中的偏移量。length
:要映射的数据的长度。access
:访问权限,可以是GL_READ_ONLY
、GL_WRITE_ONLY
或GL_READ_WRITE
。返回值
:映射后的指针,用于访问缓冲区数据。
- 使用场景:
- 在顶点缓冲对象(VBO)中,你可以使用
glMapBufferRange
来更新顶点数据。 - 在像素缓冲对象(PBO)中,你可以使用它来处理像素数据。
- 在顶点缓冲对象(VBO)中,你可以使用
总之,glMapBufferRange
允许你映射缓冲对象的一部分数据,以便在客户端代码中直接访问和操作。
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
int main() {
// Initialize GLFW and create a window
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW" << std::endl;
return -1;
}
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Sample", nullptr, nullptr);
if (!window) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// Initialize GLEW
if (glewInit() != GLEW_OK) {
std::cerr << "Failed to initialize GLEW" << std::endl;
return -1;
}
// Create a buffer object (VBO)
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// Allocate storage for the buffer
glBufferData(GL_ARRAY_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
// Map a portion of the buffer
GLintptr offset = 0;
GLsizeiptr size = 512;
GLvoid* mappedData = glMapBufferRange(GL_ARRAY_BUFFER, offset, size, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
if (mappedData) {
// Modify the data in the mapped region
// ...
// Unmap the buffer
glUnmapBuffer(GL_ARRAY_BUFFER);
} else {
std::cerr << "Failed to map buffer" << std::endl;
}
// Main loop
while (!glfwWindowShouldClose(window)) {
// Render your scene using the modified buffer data
// ...
glfwSwapBuffers(window);
glfwPollEvents();
}
// Cleanup
glDeleteBuffers(1, &vbo);
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
static void DumpTexture(GLuint TextureId, GLuint x0, GLuint y0, GLuint x1, GLuint y1){
int fbo0;
glGetIntegerv( GL_FRAMEBUFFER_BINDING, &fbo0);
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, TextureId, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
printf("glCheckFramebufferStatus() failed\n");
return;
}
GLuint size = (x1-x0)* (y1-y0)*4;
GLuint pbo_down;
glGenBuffers(1, &pbo_down);
glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_down);
glBufferData(GL_PIXEL_PACK_BUFFER, size, NULL, GL_STREAM_READ);
glReadPixels(x0,y0, x1, y1, GL_RGBA, GL_UNSIGNED_BYTE, 0);
GLubyte *src = (GLubyte*)glMapBufferRange(GL_PIXEL_PACK_BUFFER,0, size, GL_MAP_READ_BIT);
GLubyte *buffer = malloc(size);
memcpy(buffer, src, size);
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
stbi_write_png("/tmp/texture_img.png", (int)(x1-x0), (int)(y1-y0), 4, buffer, (int)((x1-x0)*4));
glBindFramebuffer(GL_FRAMEBUFFER, fbo0);
}
freetype
下图是freetype调用的基本框架。
参看上图,在绘制字符时以baseline为水平基准,则所有的字符就会对齐。
#include <ft2build.h>
#include FT_FREETYPE_H
#include <stdio.h>
int main() {
FT_Library library;
FT_Face face;
FT_Error error;
// 初始化 FreeType 库
error = FT_Init_FreeType(&library);
if (error) {
printf("Could not initialize FreeType library\n");
return -1;
}
// 加载字体文件
error = FT_New_Face(library, "path/to/your/font.ttf", 0, &face);
if (error) {
printf("Could not load font\n");
FT_Done_FreeType(library);
return -1;
}
// 设置字体大小
FT_Set_Pixel_Sizes(face, 0, 48);
// 加载一个字符的字形
// freetype 默认使用了灰度抗锯齿, 也可以在加载字形时设定不同的标志(如 FT_LOAD_TARGET_NORMAL 或 FT_LOAD_TARGET_LCD) 来指定不同的抗锯齿方式
error = FT_Load_Char(face, 'A', FT_LOAD_RENDER);
if (error) {
printf("Could not load character\n");
FT_Done_Face(face);
FT_Done_FreeType(library);
return -1;
}
// 获取字形位图
FT_Bitmap *bitmap = &face->glyph->bitmap;
// 输出位图信息
for (int y = 0; y < bitmap->rows; y++) {
for (int x = 0; x < bitmap->width; x++) {
putchar(bitmap->buffer[y * bitmap->pitch + x] ? '*' : ' ');
}
putchar('\n');
}
// 释放资源
FT_Done_Face(face);
FT_Done_FreeType(library);
return 0;
}
抗锯齿
line
一般情况下,OpenGL抗锯齿的效果是有的,但是如果你的程序中没有正确设置抗锯齿参数,或者你的显卡不支持抗锯齿,那么抗锯齿就不会起作用。
正确设置抗锯齿参数的方法是:
-
在程序中调用glEnable(GL_LINE_SMOOTH)函数,开启抗锯齿功能;
-
调用glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)函数,设置抗锯齿的精度;
-
调用glLineWidth(1.5f)函数,设置线条的宽度;
-
调用glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)函数,设置混合模式;
-
调用glEnable(GL_BLEND)函数,开启混合功能。
如果你的显卡不支持抗锯齿,那么你可以尝试更换显卡,或者使用其他抗锯齿技术,比如多重采样抗锯齿(MSAA)
MSAA
原理: 无论三角形遮盖了多少个子采样点,(每个图元中)每个像素只运行一次片段着色器。片段着色器所使用的顶点数据会插值到每个像素的中心,所得到的结果颜色会被储存在每个被遮盖住的子采样点中。当颜色缓冲的子样本被图元的所有颜色填满时,所有的这些颜色将会在每个像素内部平均化。因为上图的4个采样点中只有2个被遮盖住了,这个像素的颜色将会是三角形颜色与其他两个采样点的颜色(在这里是无色)的平均值,最终形成一种淡蓝色。
通过EGL设置多重采样
可以通过eglChooseConfig来设置一个 attrib_list,在这个 attrib_list中设置多重采样的信息
const EGLint attribsMSAA[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_ALPHA_SIZE, 8, // if you need the alpha channel
EGL_DEPTH_SIZE, 16,// if you need the depth buffer
EGL_STENCIL_SIZE,8,
EGL_SAMPLE_BUFFERS, 1,//打开多采样抗锯齿
EGL_SAMPLES, 4, //设置每个片段的采样点数
EGL_NONE
};
EGL_SAMPLES, 用来指定每个片段的样本数,样本数越多抗锯齿效果越好,一般推荐设置 2、4、8 。
但是采样数不能随便设置,我们可以通过 GL_MAX_SAMPLES 查询设备最大支持的采样数。
int maxSamples = 0;
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
离屏渲染 MSAA
- 构建多重采样纹理:
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex); glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE); glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
- 附加到帧缓冲
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
- 渲染到多重采样帧缓冲
渲染到多重采样帧缓冲对象的过程都是自动的。只要我们在帧缓冲绑定时绘制任何东西,光栅器就会负责所有的多重采样运算。我们最终会得到一个多重采样颜色缓冲以及/或深度和模板缓冲。因为多重采样缓冲有一点特别,我们不能直接将它们的缓冲图像用于其他运算,比如在着色器中对它们进行采样。
换句话说, 多重采样的渲染结果无法直接上屏渲染,需要 Blit 到另外一个普通的帧缓冲区或者再进行一次普通的离屏渲染
一个多重采样的图像包含比普通图像更多的信息,我们所要做的是缩小或者还原(Resolve)图像。多重采样帧缓冲的还原通常是通过glBlitFramebuffer来完成,它能够将一个帧缓冲中的某个区域复制到另一个帧缓冲中,并且将多重采样缓冲还原。
glBlitFramebuffer会将一个用4个屏幕空间坐标所定义的源区域复制到一个同样用4个屏幕空间坐标所定义的目标区域中。你可能记得在帧缓冲教程中,当我们绑定到GL_FRAMEBUFFER时,我们是同时绑定了读取和绘制的帧缓冲目标。我们也可以将帧缓冲分开绑定至GL_READ_FRAMEBUFFER与GL_DRAW_FRAMEBUFFER。glBlitFramebuffer函数会根据这两个目标,决定哪个是源帧缓冲,哪个是目标帧缓冲。接下来,我们可以将图像位块传送(Blit)到默认的帧缓冲中,将多重采样的帧缓冲传送到屏幕上。
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
or
glBindFramebuffer(GL_FRAMEBUFFER, m_FboId);
glViewport(0, 0, screenW, screenH);
glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);
GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
GLUtils::setInt(m_ProgramObj, "s_Texture", 1);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
//多重采样缓冲区无法直接上屏(渲染),先搞到另外一个缓冲区,然后再上屏
glBindFramebuffer(GL_FRAMEBUFFER, m_FboId2);
glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_READ_FRAMEBUFFER, m_FboId); // 设置GL_READ_FRAMEBUFFER 为m_FboId
glReadBuffer(GL_COLOR_ATTACHMENT0);
glBlitFramebuffer(0, 0, screenW, screenH,
0, 0, screenW, screenH,
GL_COLOR_BUFFER_BIT, GL_LINEAR);
文字渲染中的抗锯齿
文字渲染中的抗锯齿技术主要是为了减少由于像素网格和字体轮廓之间的不匹配而产生的锯齿状边缘。以下是几种常见的抗锯齿手段:
- 灰度抗锯齿(Grayscale Anti-Aliasing):
- 通过在文字边缘使用不同灰度的像素来平滑过渡,使边缘看起来更平滑。
- 优点是可以显著减少锯齿,缺点是对比度会有所降低,尤其在小字体上效果不明显。
- freetype 默认使用了灰度抗锯齿, 也可以在加载字形时设定不同的标志(如 FT_LOAD_TARGET_NORMAL 或 FT_LOAD_TARGET_LCD) 来指定不同的抗锯齿方式。
- 次像素抗锯齿(Subpixel Anti-Aliasing):
- 利用显示屏的次像素结构(通常是RGB子像素)来平滑边缘。
- 通过分别控制RGB子像素的亮度,可以达到比灰度抗锯齿更高的水平。
- 常见的技术有ClearType(微软)和FreeType(开源)。
- LCD渲染(LCD Rendering):
- 是次像素抗锯齿的一种实现方式,特别针对LCD显示器。
- 通过利用LCD显示器的物理特性(RGB子像素排列),提高文字的清晰度和对比度。
- 向量绘图(Vector Drawing):
- 采用矢量图形渲染文字,使得文字在任意放大或缩小时都保持平滑。
- 这种方法依赖于计算机的高计算能力,对于复杂的字体和大文本量可能会影响性能。
- 混合抗锯齿(Hybrid Anti-Aliasing):
- 结合灰度抗锯齿和次像素抗锯齿,取两者优点来实现更平滑的效果。
- 这种方法需要较高的计算资源,但可以得到最佳的抗锯齿效果。
每种抗锯齿技术都有其优缺点,具体选择需要根据显示设备、字体大小、渲染性能要求等因素来决定。
图像处理
对比度调节
对比度调整的影响
- 增加对比度:高亮部分变得更亮,暗部变得更暗,图像更具视觉冲击力。
- 减少对比度:图像更平坦,亮部和暗部的差异减少。
通过把颜色调整到[-0.5,0.5]之间,然后乘以一个系数,再调回(0, 1.0)来实现对比度的调节
static const char* blit_frag_src =""
"varying highp vec2 varTexCoord; \n"
"uniform sampler2D dfbSampler; \n"
"void main (void) \n"
"{ \n"
" highp vec4 color = texture2D(dfbSampler, varTexCoord); \n"
" color.rgb = (color.rgb-0.5)*1.5 + 0.5; \n"
" gl_FragColor = color; \n"
"} \n";
详解
color.rgb - 0.5
- 将颜色值从原始范围 [0, 1] 转换到范围 [-0.5, 0.5]。
- 这样做的目的是为了将颜色值居中,以便更容易进行对比度调整。
(color.rgb - 0.5) * contrast
- 将转换后的颜色值乘以对比度因子
contrast
。 - 增大
contrast
会增加图像的对比度,使亮的部分更亮,暗的部分更暗。 - 减小
contrast
会减少图像的对比度,使图像变得更平坦。
- 将转换后的颜色值乘以对比度因子
(color.rgb - 0.5) * contrast + 0.5
- 将调整对比度后的颜色值从范围 [-0.5, 0.5] 再次转换回原始范围 [0, 1]。
- 通过加回 0.5,确保颜色值回到 [0, 1] 范围,适合在标准的RGB颜色空间中表示。
gamma校正
Gamma校正是一种调整图像亮度的非线性操作,用于矫正显示设备的非线性响应,从而使图像显示更加自然。
Gamma校正使用一个指数函数来调整图像的亮度。
static const char* blit_frag_src =""
"varying highp vec2 varTexCoord; \n"
"uniform sampler2D dfbSampler; \n"
"void main (void) \n"
"{ \n"
" highp vec4 color = texture2D(dfbSampler, varTexCoord); \n"
" color.rgb = pow(color.rgb, vec3(1.0/2.2)); \n"
" gl_FragColor = color; \n"
"} \n";
color.rgb = pow(color.rgb, vec3(1.0 / gamma));
这个公式的作用是对颜色值进行Gamma校正。vec3(1.0/gamma) 确保了每个颜色分量都被提升到1.0/gamma次方,以达到Gamma校正的效果。 通过调整gamma的值,你可以控制图像的亮度。通常情况下,gamma=2.2 被认为是标准的Gamma值。
阈值分隔
阈值分割:就是根据像素值进行简单的二值化操作
这段片段着色器代码实现了一个简单的图像分割算法,即阈值分割。通过比较每个像素的亮度值与给定的阈值,可以将图像分割为前景(亮度大于阈值的部分)和背景(亮度小于或等于阈值的部分)。这在一些简单的图像处理任务中非常有用,例如二值化处理。
static const char* blit_frag_src =""
"varying highp vec2 varTexCoord; "
"uniform sampler2D dfbSampler; "
"void main (void) "
"{ "
" highp vec4 color = texture2D(dfbSampler, varTexCoord); "
" highp float brightness = dot(color.rgb, vec3(0.299, 0.587, 0.114)); "
" if(brightness > 0.5) "
" gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); "
" else "
" gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); "
"} ";
颜色的亮度公式
highp float brightness = dot(color.rgb, vec3(0.299, 0.587, 0.114));
计算采样颜色的亮度。使用 dot 函数计算颜色的亮度值。亮度计算公式为 brightness = 0.299 * R + 0.587 * G + 0.114 * B
。这是因为人眼对不同颜色的敏感度不同,所以使用这种加权平均方法来计算亮度更符合人眼感知.
白平衡
白平衡是摄影和图像处理中的一个重要概念,用于调整图像的颜色,以便使图像中的白色和灰色区域看起来在不同光源下仍然是自然的白色。不同光源会产生不同的色温,导致图像的颜色偏移。通过白平衡调整,可以校正这些色偏,使得图像的颜色更加真实和自然。
片段着色器
#version 330 core
in vec2 TexCoord;
out vec4 color;
uniform sampler2D texture1;
uniform vec3 whiteBalanceGain; // vec3(r_gain, g_gain, b_gain)
void main() {
vec4 texColor = texture(texture1, TexCoord);
texColor.r *= whiteBalanceGain.r;
texColor.g *= whiteBalanceGain.g;
texColor.b *= whiteBalanceGain.b;
color = texColor;
}
基于灰度世界假设,计算白平衡增益系数
void calculateWhiteBalanceGains(const std::vector<unsigned char>& imageData, int width, int height, float& r_gain, float& g_gain, float& b_gain) {
unsigned long long r_sum = 0, g_sum = 0, b_sum = 0;
int pixelCount = width * height;
// 计算图像中红、绿、蓝通道的平均值。
for (int i = 0; i < pixelCount * 4; i += 4) {
r_sum += imageData[i];
g_sum += imageData[i + 1];
b_sum += imageData[i + 2];
}
float r_avg = r_sum / pixelCount;
float g_avg = g_sum / pixelCount;
float b_avg = b_sum / pixelCount;
// 计算平均灰度值。
float avg = (r_avg + g_avg + b_avg) / 3.0f;
// 计算每个通道的增益系数,使其平均值接近于灰度值。
r_gain = avg / r_avg;
g_gain = avg / g_avg;
b_gain = avg / b_avg;
}
降噪
均值滤波对图像进行降噪
取坐标点周围9个点的颜色计算平均值
#version 330 core
in vec2 TexCoord;
out vec4 color;
uniform sampler2D texture1;
uniform vec2 texOffset[9];
void main() {
vec3 result = vec3(0.0);
for (int i = 0; i < 9; i++) {
result += texture(texture1, TexCoord + texOffset[i]).rgb;
}
result /= 9.0;
color = vec4(result, 1.0);
}
纹理偏移量的计算
float offset = 1.0f / std::max(width, height);
GLfloat texOffset = {
-offset, offset,
0.0f, offset,
offset, offset,
-offset, 0.0f,
0.0f, 0.0f,
offset, 0.0f,
-offset, -offset,
0.0f, -offset,
offset, -offset
};