一、简介
本文介绍了如何使用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 Data
vector<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 mesh
void Draw(Shader &shader)
{
// draw mesh
...
}
void DrawToTexture(Shader &shader, GLuint &renderedTexture)
{
// 1. 设置 帧缓存
// 2. 设置 纹理 (renderedTexture,由于存储渲染结果)
// 3. 设置 深度缓存
// 4. 开始渲染
// 1. 设置 帧缓存
// framebuffer
GLuint 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);
// 绑定渲染缓冲对象,指定后续的 操作(设置) 目标为 depthrederbuffer
glBindRenderbuffer(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_ATTACHMENT0
glFramebufferTexture(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 ok
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
std::cout << "Error";
return;
}
// Render to our framebuffer
// 绑定 FramebufferName,接下来的渲染将写入到 FramebufferName 帧缓存中
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
// 4. 开始渲染
// 开始渲染,将渲染结果存储到 renderedTexture
// draw mesh
glActiveTexture(GL_TEXTURE0); // 激活 纹理单元0
glBindTexture(GL_TEXTURE_2D, texture.Id); // 绑定纹理,将纹理texture.id 绑定到 纹理单元0 上
glUniform1i(glGetUniformLocation(shader.ID, "texture1"), 0); // 将 shader 中的 texture1 绑定到 纹理单元0
glBindVertexArray(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);
}
...
};
4. 场景渲染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 * La
float ambientStrenth = k[0];
vec3 ambient = ambientStrenth * lightColor;
// Diffuse
// Id = kd * max(0, normal dot light) * Ld
float 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 * Ls
float 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 color
vec3 objectColor = texture(texture1, textureCoord).xyz;
// Color = Ambient + Diffuse + Specular
// I = Ia + Id + Is
FragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f);
}
5. 屏幕渲染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 core
in vec2 textureCoord;
out vec3 FragColor;
uniform sampler2D texture1;
void main() {
// 800, 600 分别为窗口的 width 和 height
vec2 blurredTextureCoord =
textureCoord +
0.005 * vec2(sin(800.0 * textureCoord.x), cos(600.0 * textureCoord.y));
FragColor = texture(texture1, blurredTextureCoord).xyz;
}
6. main.cpp 代码
6.1). 代码整体流程
- 初始化glfw,glad,窗口
- 编译 shader 程序
- 加载obj模型、纹理图片
- 设置光源和相机位置,Phong(Blinn-Phong)模型参数
- 开始渲染
5.1 使用场景渲染shader, 渲染到纹理
5.2 使用屏幕渲染shader, 渲染到屏幕 - 释放资源
6.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 程序 ******/
// 场景渲染shader
Shader sceneShader("../resources/sceneVertexShader.vert", "../resources/sceneFragmentShader.frag");
// 屏幕渲染shader
Shader 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"); //
// backpack
Mesh 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 shader
vector<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, ks
float 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);
}
7. 编译运行及结果
编译运行:
cd ./build
cmake ..
make
./OpenGL_Blur_Effect
渲染结果:
四、全部代码及模型文件
全部代码以及模型文件可以在[OpenGL]使用OpenGL实现模糊效果(Blur Effect)中下载。