一、简介
本文介绍了如何使用OpenGL实现模糊效果(Blur Effect),并在最后给出了全部的代码。
本文在[OpenGL]使用OpenGL实现Phong、Blinn-Phong模型的基础上实现模糊效果
。在实现模糊效果时,对场景进行两趟渲染。
- 第一趟渲染,使用
场景渲染shader(sceneShader)
先对场景进行渲染,将渲染结果存储的自定义的FBO对应的纹理中(renderedTexture
)。 - 第二趟渲染,使用
屏幕渲染shader(screenShader)
渲染一个和窗口大小相同的矩形,同时将renderedTexture
当作该矩形的纹理,并在fragment shader中对纹理进行模糊处理,最后将结果渲染到屏幕。
具体流程如下:
按照本文流程实现完成后,理论上可以得到如下结果:
二、帧缓冲FBO介绍
1. 什么是FBO, Frame Buffer Object
在 OpenGL 中,帧缓冲对象(Framebuffer Object,简称 FBO)是一种允许用户自定义渲染目标的工具。通过 FBO,可以将渲染的结果直接输出到纹理或缓冲区,而不是直接显示到屏幕上。
默认的帧缓冲是在创建窗口的时候生成和配置的(GLFW帮我们做了这些)。通过创建自定义的帧缓冲,我们可以获得额外的渲染目标(target),例如将渲染结果存储在纹理当中,而不是简单的将渲染结果显示在屏幕窗口中。
2. 使用FBO可以实现什么功能
使用FBO可以实现离屏渲染、后处理效果和动态纹理。
- 离屏渲染:将渲染结果保存在纹理中,用于生成实时的阴影图、镜面反射或动态环境贴图等效果。
- 后处理效果:生成效果如模糊、景深、边缘检测等,通常先将场景渲染到 FBO,再在屏幕上渲染时应用这些效果。本文就是基于FBO实现后处理效果(模糊效果)。
- 动态纹理:实时渲染场景到纹理中,比如显示在一个虚拟的电视屏幕或反射表面上。
三、基于FBO实现模糊效果
0. 环境需要
- Linux,或者 windos下使用wsl2。
- 安装GLFW和GLAD。请参考[OpenGL] wsl2上安装使用cmake+OpenGL教程。
- 安装glm。glm是个可以只使用头文件的库,因此可以直接下载release的压缩文件,然后解压到
include
目录下。例如,假设下载的release版本的压缩文件为glm-1.0.1-light.zip
。将glm-1.0.1-light.zip
复制include
目录下,然后执行以下命令即可解压glm源代码:unzip glm-1.0.1-light.zip
- 需要下载 stb_image.h 作为加载
.png
图像的库。将 stb_image.h 下载后放入include/
目录下。
1. 项目目录
其中:
Mesh.hpp
包含了自定义的 Vertex, Texture, 和 Mesh 类,用于加载 obj 模型、加载图片生成纹理。Shader.hpp
用于创建 shader 程序。sceneVertexShader.vert
和sceneFragmentShader.frag
是用于编译场景渲染shader
程序的 顶点着色器 和 片段着色器 代码。screenVertexShader.vert
和screenFragmentShader.frag
是用于编译屏幕渲染shader
程序的 顶点着色器 和 片段着色器 代码。
下面介绍各部分的代码:
2. CMakeLists.txt代码
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 14)project(OpenGL_Blur_Effect)include_directories(include)find_package(glfw3 REQUIRED)
file(GLOB project_file main.cpp glad.c)
add_executable(${PROJECT_NAME} ${project_file})
target_link_libraries(${PROJECT_NAME} glfw)
3. Mesh.hpp 代码
Mesh.hpp 代码与[OpenGL]使用OpenGL实现Phong、Blinn-Phong模型中的Mesh.hpp基本相同,主要区别是增加了一个构造函数,和一个用于将渲染结果写入纹理的void DrawToTexture(Shader &shader, GLuint &renderedTexture)
函数,Mesh.hpp的主要代码如下:
class Mesh
{public:// mesh Datavector<Vertex> vertices; // vertex 数据,一个顶点包括 position, normal 和 texture coord 三个信息vector<unsigned int> indices; // index 数据,用于拷贝到 EBO 中Texture texture;unsigned int VAO;Mesh(vector<Vertex> vertices_, vector<unsigned int> indices_, Texture texture_): vertices(vertices_), indices(indices_), texture(texture_){setupMesh();}Mesh(string obj_path, string texture_path = ""){// load obj...}// render the meshvoid Draw(Shader &shader){// draw mesh...}void DrawToTexture(Shader &shader, GLuint &renderedTexture){// 1. 设置 帧缓存// 2. 设置 纹理 (renderedTexture,由于存储渲染结果)// 3. 设置 深度缓存// 4. 开始渲染// 1. 设置 帧缓存// framebufferGLuint FramebufferName = 0;glGenFramebuffers(1, &FramebufferName);glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);// 2. 设置 纹理 (renderedTexture,由于存储渲染结果)// texture// GLuint renderedTexture;if (glIsTexture(renderedTexture) == false){glGenTextures(1, &renderedTexture);}// "Bind" the newly created texture : all future texture functions will modify this texture// 将 renderedTexture 绑定到 GL_TEXTURE_2D 上,接下来所有对 TEXTURE_2D 的操作都会应用于 renderedTexture 上glBindTexture(GL_TEXTURE_2D, renderedTexture);// Give an empty image to OpenGL ( the last "0" )// glTexImage2d() 用于创建并初始化二维纹理数据的函数, 参数含义如下:// 1. 目标纹理类型, GL_TEXTURE_2D 为 2D 类型纹理// 2. 详细级别(mipmap级别),基础图像级别通常设置为0// 3. 存储格式,GL_RGBA 表示四通道// 4,5. 纹理宽,高,设为800, 600(与窗口同宽、高)// 6. 边框宽度,设为0// 7. 传入数据的纹理格式,此处选择 GL_RGBA (由于我们使用 null// 指针处地数据初始化纹理,不管此处选择什么对结果都无影响)// 8. 数据类型,每个颜色通道内的数据类型,设为 GL_UNSIGNED_BYTE,数值范围在 [0,255]// 9. 指向纹理图像数据(初始数据)的指针,设为0(null),使用空置初始化纹理glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 800, 600, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);// Poor filtering// 设置 GL_TEXTURE_2D 纹理的过滤方式glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);// 设置 GL_TEXTURE_2D 纹理的边缘处理方式glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);// 3. 设置 深度缓存// The depth buffer// 为上面的 framebuffer 申请一个 depth buffer (用于正确绘制)// 手动申请的 framebuffer 不会自动带有 depth buffer or template buffer or color buffer,必须手动设置// 此处收到设置一个 depth buffer// 由于正确地渲染结果(主要根据渲染场景的深度信息确定哪些部分需要渲染,哪些部分可以丢弃,跟正常渲染流程一样)GLuint depthrenderbuffer;glGenRenderbuffers(1, &depthrenderbuffer);// 绑定渲染缓冲对象,指定后续的 操作(设置) 目标为 depthrederbufferglBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer);// 指定渲染缓冲的内部格式为深度格式,意味着这个缓冲区将用于存储深度信息glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 800, 600);// 将渲染缓冲对象附加到当前绑定的帧缓冲对象glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer);// Set "renderedTexture" as our colour attachement #0// 设置 renderedTexture 附加到 帧缓冲对象上, 并设置 颜色缓冲槽位 为 GL_COLOR_ATTACHMENT0glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderedTexture, 0);// 申请生成 depth buffer 后尽量(必须)手动 clear 一下glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Set the list of draw buffers.// 设置 layout (location = 0) 的输出到 GL_COLOR_ATTCHMENT 上GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0};// 设置决定片段着色器的输出会写入哪些颜色缓冲(此时只写入 GL_COLOR_ATTACHMENT0 缓冲)glDrawBuffers(1, DrawBuffers); // "1" is the size of DrawBuffers// Always check that our framebuffer is okif (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE){std::cout << "Error";return;}// Render to our framebuffer// 绑定 FramebufferName,接下来的渲染将写入到 FramebufferName 帧缓存中glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);// 4. 开始渲染// 开始渲染,将渲染结果存储到 renderedTexture// draw meshglActiveTexture(GL_TEXTURE0); // 激活 纹理单元0glBindTexture(GL_TEXTURE_2D, texture.Id); // 绑定纹理,将纹理texture.id 绑定到 纹理单元0 上glUniform1i(glGetUniformLocation(shader.ID, "texture1"), 0); // 将 shader 中的 texture1 绑定到 纹理单元0glBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);// glBindTexture(GL_TEXTURE_2D, 0);glBindVertexArray(0);/****************/// 解绑 FramebufferName,接下来的渲染将写入默认的帧缓冲(屏幕) 中glBindFramebuffer(GL_FRAMEBUFFER, 0);}...
};
3. 场景渲染shader代码
场景渲染shader依旧使用Phong
(或者Blinn-Phong
)模型渲染场景,顶点着色器和片段着色器代码如下:
sceneVertextShader.vert
:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNor;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;out vec3 vertexPos;
out vec3 vertexNor;
out vec2 textureCoord;
void main()
{textureCoord = aTexCoord;// 裁剪空间坐标系 (clip space) 中 点的位置gl_Position = projection * view * model * vec4(aPos, 1.0f);// 世界坐标系 (world space) 中 点的位置vertexPos = (model * vec4(aPos,1.0f)).xyz;// 世界坐标系 (world space) 中 点的法向vertexNor = mat3(transpose(inverse(model))) * aNor;
}
sceneFragmentShader.frag
:
#version 330 core
out vec4 FragColor;in vec3 vertexPos;
in vec3 vertexNor;
in vec2 textureCoord;
uniform vec3 cameraPos;
uniform vec3 lightPos;
uniform vec3 k;
uniform sampler2D texture1;
void main() {vec3 lightColor = vec3(1.0f, 1.0f, 1.0f);// Ambient// Ia = ka * Lafloat ambientStrenth = k[0];vec3 ambient = ambientStrenth * lightColor;// Diffuse// Id = kd * max(0, normal dot light) * Ldfloat diffuseStrenth = k[1];vec3 normalDir = normalize(vertexNor);vec3 lightDir = normalize(lightPos - vertexPos);vec3 diffuse =diffuseStrenth * max(dot(normalDir, lightDir), 0.0) * lightColor;// Specular (Phong)// Is = ks * (view dot reflect)^s * Lsfloat specularStrenth = k[2];vec3 viewDir = normalize(cameraPos - vertexPos);vec3 reflectDir = reflect(-lightDir, normalDir);vec3 specular = specularStrenth *pow(max(dot(viewDir, reflectDir), 0.0f), 2) * lightColor;// Specular (Blinn-Phong)// Is = ks * (normal dot halfway)^s Ls// float specularStrenth = k[2];// vec3 viewDir = normalize(cameraPos - vertexPos);// vec3 halfwayDir = normalize(lightDir + viewDir);// vec3 specular = specularStrenth *// pow(max(dot(normalDir, halfwayDir), 0.0f), 2) *// lightColor;// Obejct colorvec3 objectColor = texture(texture1, textureCoord).xyz;// Color = Ambient + Diffuse + Specular// I = Ia + Id + IsFragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f);
}
4. 屏幕渲染shader代码
屏幕渲染shader的目标是对渲染对象的纹理进行模糊处理,一个简单的方案是在片段着色器中对于目标片段(像素),令其颜色与相邻像素进行一个偏移。下面是本文使用的屏幕渲染shader
顶点着色器和片段着色器代码:
screenVertextShader.vert
:
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNor;
layout(location = 2) in vec2 aTexCoord;out vec2 textureCoord;
void main() {textureCoord = aTexCoord;// 裁剪空间坐标系 (clip space) 中 点的位置gl_Position = vec4(aPos, 1.0f);
}
screenFragmentShader.frag
:
#version 330 corein vec2 textureCoord;out vec3 FragColor;uniform sampler2D texture1;void main() {// 800, 600 分别为窗口的 width 和 heightvec2 blurredTextureCoord =textureCoord +0.005 * vec2(sin(800.0 * textureCoord.x), cos(600.0 * textureCoord.y));FragColor = texture(texture1, blurredTextureCoord).xyz;
}
5. main.cpp 代码
5.1). 代码整体流程
- 初始化glfw,glad,窗口
- 编译 shader 程序
- 加载obj模型、纹理图片
- 设置光源和相机位置,Phong(Blinn-Phong)模型参数
- 开始渲染
5.1 使用场景渲染shader, 渲染到纹理
5.2 使用屏幕渲染shader, 渲染到屏幕 - 释放资源
5.2). main.cpp代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "Shader.hpp"
#include "Mesh.hpp"#include "glm/ext.hpp"
#include "glm/mat4x4.hpp"#include <random>
#include <iostream>
// 用于处理窗口大小改变的回调函数
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
// 用于处理用户输入的函数
void processInput(GLFWwindow *window);// 指定窗口默认width和height像素大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;/************************************/int main()
{/****** 1.初始化glfw, glad, 窗口 *******/// glfw 初始化 + 配置 glfw 参数glfwInit();glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// 在创建窗口之前glfwWindowHint(GLFW_SAMPLES, 4); // 设置多重采样级别为4// glfw 生成窗口GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);if (window == NULL){// 检查是否成功生成窗口,如果没有成功打印出错信息并且退出std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return -1;}// 设置窗口window的上下文glfwMakeContextCurrent(window);// 配置window变化时的回调函数glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// 使用 glad 加载 OpenGL 中的各种函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}// 启用 深度测试glEnable(GL_DEPTH_TEST);// 启用 多重采样抗锯齿glEnable(GL_MULTISAMPLE);// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 三角形 的轮廓glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 使用填充模式,绘制时对 三角形 内部进行填充/************************************//****** 2.编译 shader 程序 ******/// 场景渲染shaderShader sceneShader("../resources/sceneVertexShader.vert", "../resources/sceneFragmentShader.frag");// 屏幕渲染shaderShader screenShader("../resources/screenVertexShader.vert", "../resources/screenFragmentShader.frag");/************************************//****** 3.加载obj模型、纹理图片、Phong模型参数 ******/// 3.1 scene mesh// Mesh ourModel("../resources/models/backpack/backpack.obj", "../resources/models/backpack/backpack.jpg"); //// backpackMesh ourModel("../resources/models/spot/spot.obj", "../resources/models/spot/spot.png"); // dairy cow// Mesh ourModel("../resources/models/rock/rock.obj", "../resources/models/rock/rock.png"); // rock// 3.2 screen mesh// scene shader, screen shadervector<Vertex> vertices;vertices.push_back({{-1, 1, 0}, {0, 1, 0}, {0, 1}});vertices.push_back({{-1, -1, 0}, {0, 1, 0}, {0, 0}});vertices.push_back({{1, -1, 0}, {0, 1, 0}, {1, 0}});vertices.push_back({{1, 1, 0}, {0, 1, 0}, {1, 1}});vector<unsigned int> indices = {0, 1, 2, 0, 2, 3};Texture renderedTexture = {"", 0}; // 初始化为 空纹理Mesh screenMesh(vertices, indices, renderedTexture);/************************************//****** 4.设置光源和相机位置,Phong(Blinn-phong)模型参数 ******/// I = Ia + Id + Is// Ia = ka * La// Id = kd * (normal dot light) * Ld// Is = ks * (reflect dot view)^s * Ls// 模型参数 ka, kd, ksfloat k[] = {0.1f, 0.7f, 0.2f}; // ka, kd, ks// 光源位置glm::vec3 light_pos = glm::vec3(-2.0f, 2.0f, 0.0f);// 相机位置glm::vec3 camera_pos = glm::vec3(0.0f, 0.0f, 1.5f);/************************************//****** 5.开始渲染 ******/float rotate = 90.0f;while (!glfwWindowShouldClose(window)){// 5.1 使用场景渲染shader, 渲染到纹理rotate += 0.5f;// input// -----processInput(window);// render// ------glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 清除颜色缓冲区 并且 清楚深度缓冲区glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// 使用我们之前编译连接好的 shader 程序// 必须先使用 shaderProgram 然后才能操作 shaderProgram 中的 uniform 变量sceneShader.use();// 设置 MVP 矩阵// model 矩阵glm::mat4 model = glm::mat4(1.0f);model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));model = glm::rotate(model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f));model = glm::rotate(model, glm::radians(rotate), glm::vec3(0.0f, 1.0f, 0.0f));model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0f, 0.0f, 1.0f));model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f));// view 矩阵glm::mat4 view = glm::mat4(1.0f);view = glm::lookAt(camera_pos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));// projection 矩阵glm::mat4 projection = glm::mat4(1.0f);projection = glm::perspective(glm::radians(60.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);/************************************/sceneShader.setMat4("model", model);sceneShader.setMat4("view", view);sceneShader.setMat4("projection", projection);sceneShader.setVec3("k", k[0], k[1], k[2]);sceneShader.setVec3("cameraPos", camera_pos);sceneShader.setVec3("lightPos", light_pos);// 使用 场景渲染shader,将渲染结果存储到 renderedTexture 中ourModel.DrawToTexture(sceneShader, renderedTexture.Id);// 5.2 使用屏幕渲染shader, 渲染到屏幕screenMesh.setTexture(renderedTexture);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);screenShader.use();// 使用 屏幕渲染shader,将渲染结果显示到屏幕screenMesh.Draw(screenShader);glfwSwapBuffers(window); // 在gfw中启用双缓冲,确保绘制的平滑和无缝切换glfwPollEvents(); // 用于处理所有挂起的事件,例如键盘输入、鼠标移动、窗口大小变化等事件}/************************************//****** 6.释放资源 ******/// glfw 释放 glfw使用的所有资源glfwTerminate();/************************************/return 0;
}// 用于处理用户输入的函数
void processInput(GLFWwindow *window)
{// 当按下 Esc 按键时调用 glfwSetWindowShouldClose() 函数,关闭窗口if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS){glfwSetWindowShouldClose(window, true);}
}// 在使用 OpenGL 和 GLFW 库时,处理窗口大小改变的回调函数
// 当窗口大小发生变化时,确保 OpenGL 渲染的内容能够适应新的窗口大小,避免图像被拉伸、压缩或出现其他比例失真的问题
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{glViewport(0, 0, width, height);
}
6. 编译运行及结果
编译运行:
cd ./build
cmake ..
make
./OpenGL_Blur_Effect
渲染结果:
四、全部代码及模型文件
全部代码以及模型文件可以在[OpenGL]使用OpenGL实现模糊效果(Blur Effect)中下载。
五、参考
[1].opengl-tutorial-教程14:渲染到纹理
[2].高级OpenGL-帧缓冲