-
安装
By 陈帅
(补充):
由于我比较习惯使用VSCode,所以从一开始我就按照VScode的教程,用Cmake去编译后面的Assimp模型加载库,然后发现,里面的坑比较多,最后看了国外论坛的一则提问(编译加载不成功怎么办,然后只有作者很久之后的一个回答:已经转VS。),才意识到用VS编译一步成功,所以还是先用VS比较好。
1.1 VsCode环境配置
- 配置编译器MinGW,将将MinGW文件下的bin文件添加到系统环境变量中。
- 打开vscode,按键Ctrl+Shift+P,输入Create C++ project创建一个C++文件工程(需安装好C/C++ Project Generator插件)。
-
在vscode下终端输入“make run”编译测试成功输出例程。
1.1.1 配置OpenGL环境
1.glfw配置:
-
是一个辅助OpenGL surface窗口显示以及键鼠输入输出辅助库。官网https://www.glfw.org/download.html下载预编译的32位二进制文件。(此处32位不是电脑系统的位数,是我们要运行OpenGL的GLFW库的位数,当然32位电脑也别用64位的GLFW库文件)
- 下载好之后,将压缩包中include目录下的GLFW文件复制到vscode工程下的include目录;
-
同样将压缩包中lib-mingw目录下的glfw3.dll复制到vscode工程下的output目录。
(应用程序发出OpenGL命令,由动态库glfw3.dll接收,发送到服务器端的WINSRV.DLL,然后发送给视频显示驱动程序)
-
Glad配置:
GLAD: 一个拓展加载库,用来为我们加载并设定所有OpenGL函数指针,从而让我们能够使用所有(现代)OpenGL函数。
-
打开官网(https://glad.dav1d.de)按照下图配置:
-
然后下载生成的压缩包并解压;然后在CMD终端进入该目录,执行以下两条命令生成
gcc ./src/glad.c -c -I ./include/ // 生成 .o文件
ar -rc libglad.a glad.o // 生成我们所需要的 .a文件glad.o与libglad.a文件。
将生成的libglad.a复制到vscode创建的C++工程下的lib件夹下。
-
将解压glad\include路径下的glad、KHR两个文件夹,复制到vscode创建的C++工程下的include文件夹下;
3.OpenGL测试
1. 修改makefile
#
‘make’ build executable file ‘main’
‘make clean’ removes all .o and executable files
#
define the Cpp compiler to use
CXX = g++
define any compile-time flags
CXXFLAGS := -std=c++17 -Wall -Wextra -g
define library paths in addition to /usr/lib
if I wanted to include libraries not in /usr/lib I’d specify
their path using -Lpath, something like:
LFLAGS =
define output directory
OUTPUT := output
define source directory
SRC := src
define include directory
INCLUDE := include
define lib directory
LIB := lib
LIBRARIES := -lglad -lglfw3dll
ifeq ($(OS),Windows_NT)
MAIN := main.exe
SOURCEDIRS := $(SRC)
INCLUDEDIRS := $(INCLUDE)
LIBDIRS := $(LIB)
FIXPATH = $(subst /,\,$1)
RM := del /q /f
MD := mkdir
else
MAIN := main
SOURCEDIRS := $(shell find $(SRC) -type d)
INCLUDEDIRS := $(shell find $(INCLUDE) -type d)
LIBDIRS := $(shell find $(LIB) -type d)
FIXPATH = $1
RM = rm -f
MD := mkdir -p
endif
define any directories containing header files other than /usr/include
INCLUDES := $(patsubst %,-I%, $(INCLUDEDIRS:%/=%))
define the C libs
LIBS := $(patsubst %,-L%, $(LIBDIRS:%/=%))
define the C source files
SOURCES := $(wildcard $(patsubst %,%/*.cpp, $(SOURCEDIRS)))
define the C object files
OBJECTS := $(SOURCES:.cpp=.o)
#
The following part of the makefile is generic; it can be used to
build any executable just by changing the definitions above and by
deleting dependencies appended to the file from ‘make depend’
#
OUTPUTMAIN := $(call FIXPATH,$(OUTPUT)/$(MAIN))
all: $(OUTPUT) $(MAIN)
` `@echo Executing ‘all’ complete!
$(OUTPUT):
` `$(MD) $(OUTPUT)
$(MAIN): $(OBJECTS)
` `$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(OUTPUTMAIN) $(OBJECTS) $(LFLAGS) $(LIBS) $(LIBRARIES)
this is a suffix replacement rule for building .o’s from .c’s
it uses automatic variables $<: the name of the prerequisite of
the rule(a .c file) and $@: the name of the target of the rule (a .o file)
(see the gnu make manual section about automatic variables)
.cpp.o:
` `$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@
.PHONY: clean
clean:
` `$(RM) $(OUTPUTMAIN)
` `$(RM) $(call FIXPATH,$(OBJECTS))
` `@echo Cleanup complete!
run: all
1
./$(OUTPUTMAIN)
` `@echo Executing ‘run: all’ complete!
2.添加main文件
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
` `// glfw: initialize and configure
` `// ——————————
` `glfwInit();
` `glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
` `glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
` `glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
` `glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
` `// glfw window creation
` `// ——————–
` `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;
` `}
` `glfwMakeContextCurrent(window);
` `glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
` `// glad: load all OpenGL function pointers
` `// —————————————
` `if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
` `{
` `std::cout « “Failed to initialize GLAD” « std::endl;
` `return -1;
` `}
` `// render loop
` `// ———–
` `while (!glfwWindowShouldClose(window))
` `{
` `// input
` `// —–
` `processInput(window);
` `// render
` `// ——
` `glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
` `glClear(GL_COLOR_BUFFER_BIT);
` `// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
` `// ——————————————————————————-
` `glfwSwapBuffers(window);
` `glfwPollEvents();
` `}
` `// glfw: terminate, clearing all previously allocated GLFW resources.
` `// ——————————————————————
` `glfwTerminate();
` `return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ———————————————————————————————————
void processInput(GLFWwindow *window)
{
` `if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
` `glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ———————————————————————————————
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
` `// make sure the viewport matches the new window dimensions; note that width and
` `// height will be significantly larger than specified on retina displays.
` `glViewport(0, 0, width, height);
}
3.Make测试
在vscode中打开终端,并执行make run 命令。
##
1.2 Visual Studio环境配置
1.2.1 配置OpenGL环境
1.GLFW配置:
先到官网https://www.glfw.org/download.html下源码包文件。
然后用Cmake编译(Cmake下载地址:https://cmake.org/download/)。
安装之后,将Cmake安装目录的Bin路径添加到windows系统环境环境变量Path中。
然后打开Cmak-GUI.exe,编译源码。需要手动创建一个编译后保存二进制文件的目录(build)。依次进行如下步骤:
在点击“Configure”后,出现以下窗口,来选取编译所用的生成器。我安装的是VS 2019,电脑是64位操作系统。
然后点击“Finish”。然后会出现红色字体提示,根据提示看看有没有ERROR,自行百度。然后点击“Generate”生成所需二进制文件。此时,可以之间点击“Open Project”按钮,直接用VS2019打开生成的工程(也可以在刚刚创建的build文件夹中打开GLFW.sln)。
配置好VS:
点击”“生成”->“生成解决方案”。
于是,在工程的src/Debug文件夹,便得到了我们所需的库文件glfw.lib。
最后,就是在VS里链接GLFW的include和lib库文件。
建议最好是将GLFW源代码解压后的文件夹include中的GLFW,直接复制到我们自己工程的include文件夹中。同样,将刚刚生成的glfw3.lib库文件复制到我们自己工程的libs目录中。最后在vs里如下图设置好。
最好使用动态的路径$(SolutionDir)\include
“链接器”->“输入”:添加glfw3.lib和opengl32.lib(opengl32.lib是系统自带的)。
GLFW配置完成(若想用VSCode,则需要用MinGW编译。但是在VS中编译方便很多,出现的bug少)。如果想用VSCode,可以在VSCode里编辑,在VS中运行。
2.Glad配置:
GLAD: 一个拓展加载库,用来为我们加载并设定所有OpenGL函数指针,从而让我们能够使用所有(现代)OpenGL函数。
打开官网(https://glad.dav1d.de)按照下图配置:
将解压glad\include路径下的glad、KHR两个文件夹,复制到vs创建的C++工程下的include文件夹下;
然后复制src/glad.c文件,添加到刚刚你创建的工程中,添加到项目工程中:
1.2.1 配置Asssimp模型加载库环境
过程和配置GLFW很像,可以参照下面博客:
(10条消息) 使用CMake编译Assimp库,然后添加到vs项目中_Waves___的博客-CSDN博客
(正是因为这个库用MinGW编译过程中出现的各种奇葩问题,我才决定从VSCode转到VS)
2.OpenGL简介
是一种图形与硬件的接口(与平台无关):
` `一个定义了函数布局和输出的图形API的正式规范。
是一个API,不是一个框架、平台。OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。
由于opengl是跨平台的,并且用于窗口界面中的绘制。但窗口与系统有关心,那么将无法统一,所以opengl是核心库,它只负责绘制。窗口操作交给其他程序接口去负责。(应用程序发出OpenGL命令,由动态库glfw3.dll接收,发送到服务器端的WINSRV.DLL,然后发送给视频显示驱动程序)
不需要把三维模型数据写成固定的格式,所以开发者可以直接使用不同格式的模型数据。
OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者(实际的OpenGL库的开发者通常是显卡的生产商)自行决定。
核心模式(core可编程)与立即渲染模式(固定管线)
OpenGL自身是一个巨大的状态机,状态通常被称为OpenGL上下文(Context)。
优点:
优点如下:首先,OpenGL是跨平台的技术,其图形应用程序可以运行在Windows、Linux、Android等多个系统上;其次,OpenGL是开放式的技术平台,支持C/C++/Python多种开发语言,API均可免费获取;然后,OpenGL是底层的图形API,它具有充分的可扩展性和灵活性;最后,OpenGL文档丰富,开发者社区比较活跃,有大量的源代码可以参考。
2.1入门
2.1.1创建窗口(hello window)
OpenGL有意将这些操作抽象(Abstract)出去。这意味着我们不得不自己处理创建窗口,好在有第三方库提供了支持,常用的库有GLFW、GLUT、SDL、SFML,我使用GLFW。
GLFW它允许用户创建OpenGL上下文、定义窗口参数以及处理用户输入。
由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。
打开GLAD的在线服务,将语言(Language)设置为C/C++,在API选项中,选择3.3以上的OpenGL(gl)版本(我们的教程中将使用3.3版本,但更新的版本也能用)。之后将模式(Profile)设置为Core核心模式,并且保证选中了生成加载器(Generate a loader)选项。
- 创建流程:你好,窗口 - LearnOpenGL CN (learnopengl-cn.github.io)
- 代码:D:\OpenGlProject\OpenGL_All\learnCN\src\LearnOpenGL.cpp
我的电脑显卡的OenGL版本是4.6.
双缓冲(Double Buffer)机制:
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
2.1.2三角形(hello Triangle)
在学习此节之前,建议将这三个单词先记下来:
- 顶点数组对象:Vertex Array Object,VAO,一个调用显存并存储所有顶点数据供显卡使用的缓冲对象。
- 顶点缓冲对象:Vertex Buffer Object,VBO, 一个存储元素索引供索引化绘制使用的缓冲对象。
- 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO
着色器(Shader):具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。
图形渲染管线的每个阶段:
我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线? GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
OpenGL仅当3D坐标在3个轴(x、y和z)上-1.0到1.0的范围内时才处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates)。
注意:ES的纹理坐标上下镜像。
创建过程:(类似C语言编译、链接)
1.创建编译顶点、片段着色器:
glCreateShader -> glShaderSource -> glCompileShader
2.着色器程序连接、链接(生成可执行文件):
glCreateProgram -> glAttachShader -> glLinkProgram
3.删除着色器对象:
glDeleteShader
2.1.3 着色器
着色器(Shader)是运行在GPU上的小程序。
着色器语言(GLSL),是类C语言写成的,是为图形计算量身定制的,它包含一些针对向量和矩阵操作。
GLSL语言结构:
#version version_number
in type in_variable_name; (顶点属性,个数由硬件来决定通常情况下它至少会返回16个)
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
int main()
{
` `// 处理输入并进行一些图形操作
…
` `// 输出处理过的结果到输出变量
` `out_variable_name = weird_stuff_we_processed;
}
GLSL数据类型:
int、float、double、uint和bool
两种容器:
向量Vector、矩阵Matrix
大多数时候我们使用vecn,因为float足够满足大多数要求了。
一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量。
向量重组:
vec2 someVec; //两个
vec4 differentVec = someVec.xyxx; //2个重组为4个
vec3 anotherVec = differentVec.zyw; //2个重组为3个
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;//向量相加
我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
输入与输出:
每个着色器使用输入和输出,输出变量与下一个着色器的输入匹配,它就会传递下去。
顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。顶点着色器需要为它的输入提供一个额外的layout标识layout (location = 0),这样我们才能把它链接到顶点数据。
片段着色器,它需要一个vec4颜色输出变量。
顶点着色器为片段着色器决定颜色:
顶点着色器GLSL:
#version 330 core //版本,core模式
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
` `gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
` `vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}
片段着色器GLSL:
#version 330 core //版本,core模式
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
out vec4 FragColor;
Void main()
{
FragColor = vertexColor;
}
Uniform:
一个特殊类型的GLSL变量。在一个着色器程序中每一个着色器都能够访问uniform变量,并且只需要被设定一次。
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式。(全局量)
此时,我们只需在着色器片段GLSL中声明一个uniform类型数据,然后在main函数中更新它的值:
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量ourColor
void main()
{
` `FragColor = ourColor;
}
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, “ourColor”); //查询uniform
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
双缓冲区:
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
2.1.4 纹理
贴图。
纹理(Texture)是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;这样就可以让物体非常精细而不用指定额外的顶点(避免大量额外的开销)。
需要指定三角形的每个顶点各自对应纹理的哪个部分。
使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),终始于(1, 1)。
对应的纹理坐标为(0,0),(1, 0),(0.5, 1) ES左上角是(0,0)
纹理坐标定义:
float texCoords[] = {
` `0.0f, 0.0f, // 左下角
` `1.0f, 0.0f, // 右下角
` `0.5f, 1.0f // 上中
};
纹理环绕方式:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
纹理过滤(插值):
纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值。
GL_NEAREST(临近插值过滤,缩小时使用)和GL_LINEAR(线性过滤,放大时使用)。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大
多级渐远纹理(Mipmap):
OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。
在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大时,不用插值
加载与创建纹理:
使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说我们要用的stb_image.h库。
stb_image.h库:stb/stb_image.h at master · nothings/stb (github.com)
#define STB_IMAGE_IMPLEMENTATION
#include “stb_image.h”
int width, height, nrChannels;
unsigned char *data = stbi_load(“container.jpg”, &width, &height, &nrChannels, 0);
生成纹理:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load(“container.jpg”, &width, &height, &nrChannels, 0);
if (data)
{
` `glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
` `glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
` `std::cout « “Failed to load texture” « std::endl;
}
stbi_image_free(data); ###
2.1.5 变换(矩阵)
在齐次坐标系(增加一个分量,其他分量除以该分量),不是笛卡尔坐标系。
向量:
向量有一个方向(Direction)和大小(Magnitude)。
原点为(0, 0, 0),指向x,y,z的向量。
矩阵的加减乘除。
向量缩放:
(增加一个额外的维度:齐次坐标)
向量位移:
(增加一个额外的维度:齐次坐标)
旋转矩阵:
避免万向节死锁的真正解决方案是使用四元数。
矩阵组合:
可以把多个变换组合到一个矩阵中。
(缩放2倍,并向移动(1,2,3)个单位)
GLM:
GLM是OpenGL Mathematics的缩写,
GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)。如果你使用的是0.9.9或0.9.9以上的版本,你需要将所有的矩阵初始化改为 glm::mat4 mat = glm::mat4(1.0f)。
我选择下载低版本:Release GLM 0.9.8.5 · g-truc/glm (github.com)
vertex.vs文件修改:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
` `gl_Position = transform * vec4(aPos, 1.0f);
` `TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
旋转、缩放、平移:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f)); //平移
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0)); //旋转(x,y,z轴)
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5)); //缩放
//获取矩阵uniform并设置矩阵(需要把变换矩阵传递给着色器)
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, “transform”);
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
2.1.6 坐标系统
每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。
变换为标准化设备坐标,接着再转化为屏幕坐标。
OpenGL世界坐标 、纹理坐标 、android屏幕纹理坐标:
物体的坐标 几个过渡坐标系:
坐标变换过程:
// 注意乘法要从右向左读
gl_Position = projection * view * model * vec4(aPos, 1.0);
- 局部空间(Local Space,或者称为物体空间(Object Space)): 比如画一个立方体时。
- 世界空间(World Space):把所有物体导入程序中时。
- 观察空间(View Space,或者称为视觉空间(Eye Space)):摄像机空间,从摄像机视角观察。
- 裁剪空间(Clip Space):投影到-1.0至1.0坐标范围以外的被裁剪掉。
- 屏幕空间(Screen Space):通过正射投影投到屏幕空间。
投影过程
正射投影:
要创建一个正射投影矩阵,我们可以使用GLM的内置函数glm::ortho:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
(参数: 平截头体的左、右坐标、底部、顶部、近平面、远平面距离)
透视投影:
近大远小的效果,顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
参数(视野角、宽高比、平截头体近平面1.0f、远平面100.0f)
2.1.7 摄像机(观察空间)
实现场景移动。
为了实现我们的摄像机视角在移动的感觉,可以把场景中的物体反方向移动。
要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。
摄像机位置:
空间中的一个向量(指向了摄像机)
glm::vec3(0.0f, 0.0f, 3.0f);
摄像机方向:
摄像机的指向(可以参考向量相减,指向被减的向量),由于摄像机指向Z轴的负方向,但是我们需要的是摄像机指向Z轴的负方向,所以交换一下相减的顺序。
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
右轴(x轴):
摄像机方向向量 叉乘X 上向量(z轴,UP)。
(叉乘得到垂直的向量)
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
上轴(y轴):
右向量(x轴) 叉乘X 方向向量。
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
Look At:
LookAt矩阵作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间。
其中R是右向量,U是上向量,D是方向向量P是摄像机位置向量。注意,位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向。
我们要做的只是定义一个摄像机位置,一个目标位置和一个表示世界空间中的上向量的向量(我们计算右向量使用的那个上向量)。接着GLM就会创建一个LookAt矩阵,我们可以把它当作我们的观察矩阵:
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), //摄像机位置
` `glm::vec3(0.0f, 0.0f, 0.0f), //目标位置
` `glm::vec3(0.0f, 1.0f, 0.0f)); //表示世界空间中的上向量(UP)
相机视角自由移动:
首先我们必须设置一个摄像机系统:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
然后窗口获取键盘输入:
void processInput(GLFWwindow *window)
{
…
` `float cameraSpeed = 0.05f; // adjust accordingly
` `if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
` `cameraPos += cameraSpeed * cameraFront;
` `if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
` `cameraPos -= cameraSpeed * cameraFront;
` `if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
` `cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
` `if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
` `cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
最后用LookAt实现:
` `一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
移动速度:
` `float velocity = MovementSpeed * deltaTime;
视角移动(欧拉角):
俯仰角和偏航角转换为方向向量的处理需要一些三角学知识:
俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll)。
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 译注:direction代表摄像机的前轴(Front),这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
鼠标输入:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
在调用这个函数之后,无论我们怎么去移动鼠标,光标都不会显示了,它也不会离开窗口。对于FPS摄像机系统来说非常完美。
监听鼠标移动事件,(和键盘输入相似)我们会用一个回调函数来完成,函数的原型如下:
glfwSetCursorPosCallback(window, mouse_callback);
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
` `if(firstMouse)
` `{
` `lastX = xpos;
` `lastY = ypos;
` `firstMouse = false;
` `}
` `float xoffset = xpos - lastX;
` `float yoffset = lastY - ypos;
` `lastX = xpos;
` `lastY = ypos;
` `float sensitivity = 0.05;
` `xoffset *= sensitivity;
` `yoffset *= sensitivity;
` `yaw += xoffset;
` `pitch += yoffset;
` `if(pitch > 89.0f)
` `pitch = 89.0f;
` `if(pitch < -89.0f)
` `pitch = -89.0f;
` `glm::vec3 front;
` `front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
` `front.y = sin(glm::radians(pitch));
` `front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
` `cameraFront = glm::normalize(front);
}
缩放:
需要一个鼠标滚轮的回调函数:
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
` `if(fov >= 1.0f && fov <= 45.0f)
` `fov -= yoffset;
` `if(fov <= 1.0f)
` `fov = 1.0f;
` `if(fov >= 45.0f)
` `fov = 45.0f;
}
把透视投影矩阵上传到GPU:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
注册鼠标滚轮的回调函数:
glfwSetScrollCallback(window, scroll_callback);
2.2光照
2.2.1颜色
仅仅用这RGB三个值就可以组合出任意一种颜色。例如,要获取一个珊瑚红(Coral)色的话,我们可以定义这样的一个颜色向量:
Glm::vec3 color(1.0f, 0.5f, 0.31f);
是物体所反射的(Reflected)颜色。
白色的阳光实际上是所有可见颜色的集合,物体吸收了其中的大部分颜色。它仅反射了代表物体颜色的部分,被反射颜色的组合就是我们所感知到的颜色(此例中为珊瑚红)。
当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。
反射的颜色=光源颜色×物体颜色
计算反射颜色分量大小,即为两个颜色向量相乘:
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);//光源颜色
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);//物体颜色
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);//反射的颜色
创建一个光照场景:
还要创建一个表示灯(光源)的立方体,所以我们还要为这个灯创建一个专门的lightVAO。(接下来的教程中我们会频繁地对顶点数据和属性指针做出修改,我们并不想让这些修改影响到灯(我们只关心灯的顶点位置),因此我们有必要为灯创建一个新的VAO。):
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
创建反射颜色的片段着色器:
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
` `FragColor = vec4(lightColor * objectColor, 1.0);
}
我们把物体的颜色设置为之前提到的珊瑚红色,并把光源设置为白色:
// 在此之前不要忘记首先 use 对应的着色器程序(来设定uniform)
lightingShader.use();
lightingShader.setVec3(“objectColor”, 1.0f, 0.5f, 0.31f);
lightingShader.setVec3(“lightColor”, 1.0f, 1.0f, 1.0f);
我们希望灯一直保持明亮,不受其它颜色变化的影响,我们需要为灯的绘制创建另外的一套着色器:
#version 330 core
out vec4 FragColor;
void main()
{
` `FragColor = vec4(1.0); // 将向量的四个分量全部设置为1.0
}
光源立方体的生成:
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
model = glm::mat4();
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));
lampShader.use();
// 设置模型、视图和投影矩阵uniform
…
// 绘制灯立方体对象
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
2.2.2基础光照
现实世界的光照是极其复杂的。
冯氏光照模型(Phong Lighting Model)的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
- 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
环境光照:
光通常来自于我们周围分散的很多光源,光能够在其它的表面上反射,对一个物体产生间接的影响。全局照明(Global Illumination)算法(开销大)。
所以先从简单的环境光照开始。
使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中,这样子的话即便场景中没有直接的光源也能看起来存在有一些发散的光。
float ambientStrength = 0.3; //环境光照系数
glm::vec3 lightColor = ambientStrength * glm::vec3(1.0f, 1.0f, 1.0f); //灯光颜色设置为(红色+绿色+蓝色)* 环境光照系数
ourShader.setVec3(“lightColor”, lightColor); 具体计算公式在fragment.fs文件中定义。
(环境光照系数分别为1.0f,0.3f)
漫反射光照:
漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。
我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光最亮。
为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西。
计算漫反射需要:
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
法向量:
法向量是一个垂直于顶点表面的(单位)向量。
计算漫反射光照:
每个顶点都有了法向量,但是我们仍然需要光源的位置向量和片段的位置向量。
uniform vec3 lightPos;
lightingShader.setVec3(“lightPos”, lightPos);
光的方向向量是光源位置向量与片段位置向量之间的向量差:
in vec3 FragPos;
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量:
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
有了环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色:
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
镜面光照:
把镜面高光(Specular Highlight)加进来。
它也决定于观察方向,例如玩家是从什么方向看向这个片段的。
我们通过根据法向量翻折入射光的方向来计算反射向量。然后我们计算反射向量与观察方向的角度差,它们之间夹角越小,镜面光的作用就越大。
观察者的世界空间坐标:
uniform vec3 viewPos;
lightingShader.setVec3(“viewPos”, camera.Position);
定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响:
float specularStrength = 0.5;
视线方向向量:
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
计算镜面分量:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。
将环境光、漫反射、镜面反射加权到物体颜色上:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
2.2.3材质
如果我们想要在OpenGL中模拟多种类型的物体,我们必须针对每种表面定义不同的材质(Material)属性。
材质:
材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)、反光度。
定义为结构体:
#version 330 core
struct Material {
` `vec3 ambient; //环境光照
` `vec3 diffuse; //漫反射
` `vec3 specular; //镜面反射高光
` `float shininess; //反光度(影响高光的的散射半径)
};
uniform Material material;
设置材质:
设置每个单独的uniform:
lightingShader.setVec3(“material.ambient”, 1.0f, 0.5f, 0.31f);
lightingShader.setVec3(“material.diffuse”, 1.0f, 0.5f, 0.31f);
lightingShader.setVec3(“material.specular”, 0.5f, 0.5f, 0.5f);
lightingShader.setFloat(“material.shininess”, 32.0f);
计算环境光+漫反射+镜面反射:
void main()
{
` `// 环境光
` `vec3 ambient = lightColor * material.ambient;
` `// 漫反射
` `vec3 norm = normalize(Normal);
` `vec3 lightDir = normalize(lightPos - FragPos);
` `float diff = max(dot(norm, lightDir), 0.0);
` `vec3 diffuse = lightColor * (diff * material.diffuse);
` `// 镜面光
` `vec3 viewDir = normalize(viewPos - FragPos);
` `vec3 reflectDir = reflect(-lightDir, norm);
` `float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = lightColor * (spec * material.specular);
` `vec3 result = ambient + diffuse + specular;
` `FragColor = vec4(result, 1.0);
}
光的属性:
光源对环境光、漫反射和镜面光分量也分别具有不同的强度。
这次是要为每个光照分量分别指定一个强度向量,假设lightColor是vec3(1.0),代码会看起来像这样:
vec3 ambient = vec3(1.0) * material.ambient;
vec3 diffuse = vec3(1.0) * (diff * material.diffuse);
vec3 specular = vec3(1.0) * (spec * material.specular);
(环境光强度设置为一个小一点的值:vec3 ambient = vec3(0.1) * material.ambient;)
光照属性创建类似材质结构体的东西:
struct Light {
` `vec3 position;
` `vec3 ambient;
` `vec3 diffuse;
` `vec3 specular;
};
uniform Light light;
更新片段着色器:
vec3 ambient = light.ambient * material.ambient;
vec3 diffuse = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);
设置光照强度结构体:
lightingShader.setVec3(“light.ambient”, 0.2f, 0.2f, 0.2f);
lightingShader.setVec3(“light.diffuse”, 0.5f, 0.5f, 0.5f); // 将光照调暗了一些以搭配场景
lightingShader.setVec3(“light.specular”, 1.0f, 1.0f, 1.0f);
不同的光源颜色:
我们可以随着时间改变它们的颜色,从而获得一些非常有意思的效果。由于所有的东西都在片段着色器中配置好了,修改光源的颜色非常简单:
glm::vec3 lightColor;
lightColor.x = sin(glfwGetTime() * 2.0f);
lightColor.y = sin(glfwGetTime() * 0.7f);
lightColor.z = sin(glfwGetTime() * 1.3f);
glm::vec3 diffuseColor = lightColor * glm::vec3(0.5f); // 降低影响
glm::vec3 ambientColor = diffuseColor * glm::vec3(0.2f); // 很低的影响
lightingShader.setVec3(“light.ambient”, ambientColor);
lightingShader.setVec3(“light.diffuse”, diffuseColor);
2.2.4光照贴图
物体通常并不只包含有一种材质,而是由多种材质所组成。
引入漫反射和镜面光贴图(Map)。这允许我们对物体的漫反射分量(以及间接地对环境光分量,它们几乎总是一样的)和镜面光分量有着更精确的控制。
漫反射贴图:
与纹理的实现一样,都是贴图。
这次我们会将纹理储存为Material结构体中的一个sampler2D。我们将之前定义的vec3漫反射颜色向量替换为漫反射贴图:
struct Material {
` `sampler2D diffuse; //sampler2D是不透明类型,只能通过uniform来定义它。
` `vec3 specular;
` `float shininess;
};
…
in vec2 TexCoords;
镜面贴图:
木头不应该有这么强的镜面高光,钢铁应该是有一些镜面高光的。
也就意味着我们需要生成一个黑白的(如果你想得话也可以是彩色的)纹理,来定义物体每部分的镜面光强度。
(使用Photoshop或Gimp之类的工具,将漫反射纹理转换为镜面光纹理还是比较容易的,只需要剪切掉一些部分,将图像转换为黑白的,并增加亮度/对比度就好了。)
采样镜面贴图:
首先加载纹理:
Texture texture1(“src/old/container2.jpg”);
Texture texture2(“src/old/container2_specular.jpg”);
use绑定到模型:
ourShader.use();//set uniform数据前,要先use激活
ourShader.setInt(“material.diffuse”, 0); // Use Uniform texture
ourShader.setInt(“material.specular”, 1);
激活光照贴图纹理:
texture1.use(GL_TEXTURE0);
texture2.use(GL_TEXTURE1);
更新片段着色器的材质属性,让其接受一个sampler2D而不是vec3作为镜面光分量:
struct Material {
` `sampler2D diffuse;
` `sampler2D specular;
` `float shininess;
};
最后我们希望采样镜面光贴图,来获取片段所对应的镜面光强度:
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
FragColor = vec4(ambient + diffuse + specular, 1.0);
2.2.5投光物
将光投射(Cast)到物体的光源叫做投光物(Light Caster)。
平行光(定向光):
(齐次坐标系的w分量设置为0)
光源处于很远的地方时,来自光源的每条光线就会近似于互相平行(类似于太阳)。
不像之前通过位置来计算光的方向,我们将直接使用光的direction向量。
struct Light {
` `// vec3 position; // 使用定向光就不再需要了
` `vec3 direction;
` `vec3 ambient;
` `vec3 diffuse;
` `vec3 specular;
};
…
void main()
{
` `vec3 lightDir = normalize(-light.direction); // 物体的片段至光源的方向(因为OpenGL原理是物体发光)
…
}
定义光源方向:
lightingShader.setVec3(“light.direction”, -0.2f, -1.0f, -0.3f); //光源发出的方向
可以根据齐次坐标系的W分量判断是否为平行光:
if(lightVector.w == 0.0) // 注意浮点数据类型的误差
` `// 执行定向光照计算
else if(lightVector.w == 1.0)
` `// 根据光源的位置做光照计算(与上一节一样)
点光源:
就是之前一直默认的做法。
定义为物体指向光源的方向(OpenGL原理为物体发光):
vec3 lightDir = normalize(light.position-FragPos);
衰减(此时用点光源的light.position):
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。
一般简单的方法是线性衰减,但是现实世界并不是这样。所以需要用一个非线性公式来计算,幸运的是,已经有前人解决了这个问题,用以下光强衰减公式乘以光强系数:
Fatt=1.0Kq∗d2+Kl∗d1+Kc
分母部分 分别表示三部分的叠加:
- 二次项:非线性衰减,让光源以二次递减的方式减少强度。
- 一次项:线性衰减。
- 常数项:保持为1.0,保证分母大于分子,避免衰减系数大于0。
光所能覆盖的距离对应的系数参数如下表:
实现衰减,衰减系数保存在Light结构体中:
struct Light {
` `vec3 position;
` `vec3 ambient;
` `vec3 diffuse;
` `vec3 specular;
` `float constant;
` `float linear;
` `float quadratic;
};
根据距离设置三个系数(如:可见距离为50):
lightingShader.setFloat(“light.constant”, 1.0f);
lightingShader.setFloat(“light.linear”, 0.09f);
lightingShader.setFloat(“light.quadratic”, 0.032f);
GLSL的计算公式:
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
` `light.quadratic * (distance * distance));
聚光(手电筒):
聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。
只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。
用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径。
LightDir:从片段指向光源的向量。
SpotDir:聚光所指向的方向。
Phi:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
Theta:LightDir向量和SpotDir向量之间的夹角。
我们将聚光的位置向量(来计算光的方向向量)、聚光的方向向量和一个切光角。我们可以将它们储存在Light结构体中:
struct Light {
` `vec3 position;
` `vec3 direction;
` `float cutOff;
…
};
将值传入着色器:
lightingShader.setVec3(“light.position”, camera.Position);
lightingShader.setVec3(“light.direction”, camera.Front);
lightingShader.setFloat(“light.cutOff”, glm::cos(glm::radians(12.5f)));//计算余弦是因为会计算LightDir和SpotDir向量的点积,这个点积返回的将是一个余弦值而不是角度值,所以我们不能直接使用角度值和余弦值进行比较。
平滑/软化边缘:
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
我们现在有了一个在聚光外是负的,在内圆锥内大于1.0的,在边缘处于两者之间的强度值了。如果我们正确地约束(Clamp)这个值,在片段着色器中就不再需要if-else了,我们能够使用计算出来的强度值直接乘以光照分量:
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
…
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
…
2.2.6多光源
模拟一个类似太阳的定向光(Directional Light)光源,四个分散在场景中的点光源(Point Light),以及一个手电筒(Flashlight)。
定向光:
Shader ourShader(“src/old/vertex.vs”, “src/old/fragment.fs”); // you can name your shader files however you like
` `// ourShader.use();
` `Shader lightCubeShader(“src/old/light_cube.vs”, “src/old/light_cube.fs”);
定向光源结构体:
struct DirLight {
` `vec3 direction;//定向光源方向
` `vec3 ambient;
` `vec3 diffuse;
` `vec3 specular;
};
uniform DirLight dirLight;
在计算冯氏光照的三光中,主要在于光的方向是固定的(定向光可以不衰减):
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
` `vec3 lightDir = normalize(-light.direction);
` `// 漫反射着色
` `float diff = max(dot(normal, lightDir), 0.0);
` `// 镜面光着色
` `vec3 reflectDir = reflect(-lightDir, normal);
` `float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
` `// 合并结果
` `vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
` `vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
` `vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
` `return (ambient + diffuse + specular);
}
点光源:
定义4个点光源的结构体:
struct PointLight {
` `vec3 position;//每个点光源的位置
` `float constant;//点光源的三个衰减系数
` `float linear;
` `float quadratic;
` `vec3 ambient;点光源的三光
` `vec3 diffuse;
` `vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
计算点光源对片段的颜色贡献(与定向光不同,此时需要计算光源相对物体片段的位置):
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
` `vec3 lightDir = normalize(light.position - fragPos);
` `// 漫反射着色
` `float diff = max(dot(normal, lightDir), 0.0);
` `// 镜面光着色
` `vec3 reflectDir = reflect(-lightDir, normal);
` `float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
` `// 衰减
` `float distance = length(light.position - fragPos);
` `float attenuation = 1.0 / (light.constant + light.linear * distance +
` `light.quadratic * (distance * distance));
` `// 合并结果
` `vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
` `vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
` `vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
` `ambient *= attenuation;
` `diffuse *= attenuation;
` `specular *= attenuation;
` `return (ambient + diffuse + specular);
}
合并结果:
在main函数中,将计算的三种光源对片段颜色的贡献进行叠加合并:
void main()
{
` `// 属性
` `vec3 norm = normalize(Normal);
` `vec3 viewDir = normalize(viewPos - FragPos);
` `// 第一阶段:定向光照
` `vec3 result = CalcDirLight(dirLight, norm, viewDir);
` `// 第二阶段:点光源
` `for(int i = 0; i < NR_POINT_LIGHTS; i++)
` `result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
` `// 第三阶段:聚光
` `//result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
` `FragColor = vec4(result, 1.0);
}
(右图为手电筒光效果)
2.3模型加载
2.3.1 Assimp
和箱子对象不同,我们不太能够对像是房子、汽车或者人形角色这样的复杂形状手工定义所有的顶点、法线和纹理坐标。
目前,模型通常都由3D建模师在Blender、3DS Max或者Maya这样的工具中精心制作。
这些所谓的3D建模工具(3D Modeling Tool)可以创建复杂的形状,并使用一种叫做UV映射(uv-mapping)的手段来应用贴图。这些工具将会在导出到模型文件的时候自动生成所有的顶点坐标、顶点法线以及纹理坐标。
所以,我们的工作就是解析这些导出的模型文件以及提取所有有用的信息,将它们储存为OpenGL能够理解的格式。
模型的文件格式有很多种,每一种都会以它们自己的方式来导出模型数据。
Wavefront的.obj格式 :
这样的模型格式,只包含了模型数据以及材质信息,像是模型颜色和漫反射/镜面光贴图。
而以XML为基础的Collada文件格式 :
则非常的丰富,包含模型、光照、多种材质、动画数据、摄像机、完整的场景信息等等。
不同格式的模型之间通常并没有一个通用的结构,所以必须自己对每一种需要导入的文件格式写一个导入器。很幸运的是,正好有一个库(Assimp)专门处理这个问题。
模型加载库:
Assimp:Open Asset Import Library(开放的资产导入库):
它会将所有的模型数据加载至Assimp的通用数据结构中
Assimp通常会将整个模型加载进一个场景(Scene)对象,将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下:
- 和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
- 场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh(网格)对象,节点中的mMeshes(网格)数组保存的只是场景中网格数组的索引。
- 一个Mesh(网格)对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
- 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的(见你好,三角形)。
- 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
所以,我们需要做的第一件事是:
- 将一个物体加载到Scene对象中,
- 遍历节点,获取对应的Mesh对象(我们需要递归搜索每个节点的子节点),
- 并处理每个Mesh对象来获取顶点数据、索引以及它的材质属性。
- 最终的结果是一系列的网格数据,我们会将它们包含在一个Model对象中。
定义一个网格顶点基本包含:
struct Vertex {
` `glm::vec3 Position;
` `glm::vec3 Normal;
` `glm::vec2 TexCoords;
};
另外,一个纹理包含纹理ID,纹理类型:
struct Texture {
` `unsigned int id;
` `string type;
};
定义网格Mesh类:
class Mesh {
` `public:
` `/* 网格数据 */
` `vector
` `vector
` `vector
` `/*构造函数调用 setupMesh()(生Gen 、绑Bind 、 二者BufferDate 、 指定Attrib) */
` `Mesh(vector
` `void Draw(Shader shader);
` `private:
` `/* 渲染数据 */
` `unsigned int VAO, VBO, EBO;
` `/* 函数 */
` `void setupMesh();
};
2.3.2模型
导入3D模型到OpenGL:
使用Assimp来加载模型,并将它转换至多个在上一节中创建的Mesh对象。
class Model
{
` `public:
` `/* 函数 */
` `Model(char *path)
` `{
` `loadModel(path);
` `}
` `void Draw(Shader shader);
` `private:
` `/* 模型数据 */
` `vector
` `string directory;
` `/* 函数 */
` `void loadModel(string path);
` `void processNode(aiNode *node, const aiScene *scene);
` `Mesh processMesh(aiMesh *mesh, const aiScene *scene);
` `vector
` `string typeName);
};
导入3D模型:
包含头文件:
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
assimp库加载支持格式的模型:
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace); |
处理每个网格,对每个网格的所有父子节点进行递归调用:
void processNode(aiNode *node, const aiScene *scene)
{
` `// 处理节点所有的网格(如果有的话)
` `for(unsigned int i = 0; i < node->mNumMeshes; i++)
` `{
` `aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
` `meshes.push_back(processMesh(mesh, scene));
` `}
` `// 接下来对它的子节点重复这一过程
` `for(unsigned int i = 0; i < node->mNumChildren; i++)
` `{
` `processNode(node->mChildren[i], scene);
` `}
}
索引indices:
Assimp的接口定义了每个网格都有一个面(Face)数组,一个面包含了多个索引,所以通过面(face)计算索引数:
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]);
}
材质:
如果想要获取网格真正的材质,我们还需要索引场景的mMaterials数组。网格材质索引位于它的mMaterialIndex属性中:
aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
// 1. 漫反射贴图
vector
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
// 2. 镜面反射贴图
vector
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
// 3. normal贴图
std::vector
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
// 4. height贴图
std::vector
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
重大优化:
加载纹理是一个开销较大的操作,所以我们会对模型的代码进行调整,将所有加载过的纹理全局储存,每当我们想加载一个纹理的时候,首先去检查它有没有被加载过。如果有的话,我们会直接使用那个纹理,并跳过整个加载流程,来为我们省下很多处理能力。
为了能够比较纹理,我们还需要储存它们的路径:
struct Texture {
` `unsigned int id;
` `string type;
` `aiString path; // 我们储存纹理的路径用于与其它纹理进行比较
};
将所有加载过的纹理储存在另一个vector中:
vector
加载判断纹理函数:
vector
` `{
` `vector
` `for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
` `{
` `aiString str;
` `mat->GetTexture(type, i, &str);
` `// check if texture was loaded before and if so, continue to next iteration: skip loading a new texture
` `bool skip = false;
` `for (unsigned int j = 0; j < textures_loaded.size(); j++)
` `{
` `if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
` `{
` `textures.push_back(textures_loaded[j]);
` `skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)
` `break;
` `}
` `}
` `if (!skip)
` `{ // *如果纹理还没有被加载,则加载它*
` `Texture texture;
` `texture.id = TextureFromFile(str.C_Str(), this->directory);
` `texture.type = typeName;
` `texture.path = str.C_Str();
` `textures.push_back(texture);
` `textures_loaded.push_back(texture); // *如果纹理还没有被加载,则加载它*
` `}
` `}
` `return textures;
` `} #
2.4高级OpenGL
2.4.1 深度测试
在前面,运用深度缓冲(z buffer)防止被遮挡的面渲染出来。
深度缓冲就像颜色缓冲,有着宽(width)、高(high)。
是有窗口自动创建的,以float形式存储深度值。
当深度测试glEnable(GL_DEPTH_TEST);启用时,OpenGL会自动将每个片段的深度值与深度缓冲进行对比的深度测试,测试通过的话,深度缓冲就更新为新的深度值;如果测试失败,则片段被丢弃。
深度测试是在片段着色器运行之后,在屏幕空间运行的。
开启深度测试:
glEnable(GL_DEPTH_TEST)
由于每次会在深度缓冲区存储当前片段的Z值,需要每次清除深度缓冲,否则会保存上一次渲染的深度值。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //*每帧开始时,清除颜色缓冲、Z深度缓冲* |
某些情况下你会需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲:
glDepthMask(GL_FALSE);
深度测试函数:
OpenGL允许我们修改深度测试中使用的比较运算符。这允许我们来控制OpenGL什么时候该通过或丢弃一个片段,什么时候去更新深度缓冲。我们可以调用glDepthFunc函数来设置比较运算符:
glDepthFunc(GL_LESS);
函数接受的运算符如下(默认为GL_LESS):
GL_ALWAYS: GL_LESS:
深度值精度:
深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。观察空间的z值可能是投影平截头体的近平面(Near)和远平面(Far)之间的任何值。将观察空间的z值归一化到0-1:
其中,near为平截头体的近平面;far为平截头体的远平面。如下的0.1f和1000.0f。
glm::mat4projection=glm::perspective(glm::radians(camera.Zoom),(float)SCR_WIDTH/ (float)SCR_HEIGHT, 0.1f, 1000.0f);
然而,在实践中是几乎永远不会使用这样的线性深度缓冲(Linear Depth Buffer)的。要想有正确的投影性质,需要使用一个非线性的深度方程,需要距离近时精度高:
深度冲突:
例如,两个平面或者三角形非常紧密地平行排列在一起时,深度缓冲没有足够的精度决定谁在前面,像是这两个形状在争夺(Fight)谁该处于前端。会出现奇怪的条纹。
防止深度冲突:
不要把多个物体摆的太近。
将近平面设置远一点。
牺牲一定的性能,使用更高精度的深度缓冲。(大部分是24位,但是大部分显卡支持32位)
##
2.4.2模板测试
模板测试:
和深度测试一样,它也可能会丢弃片段。模板测试是根据又一个缓冲来进行的,它叫做模板缓冲(Stencil Buffer)。
启用模板缓冲:
glEnable(GL_STENCIL_TEST);
和颜色和深度缓冲一样,你也需要在每次迭代之前清除模板缓冲:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); |
模板函数:
glStencilFunc(GLenum func, GLint ref, GLuint mask)
glStencilFunc(GL_EQUAL, 1, 0xFF) //只要一个片段的模板值等于(GL_EQUAL)参考值1,片段将会通过测试并被绘制,否则会被丢弃。
物体轮廓:
将会为每个(或者一个)物体在它的周围创建一个很小的有色边框。
创建轮廓步骤:
- 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
- 渲染物体。
- 禁用模板写入以及深度测试。
- 将每个物体缩放一点点。
- 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
- 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
- 再次启用模板写入和深度测试。
##
2.4.3混合
丢弃片段:
教程上最先讲述的丢弃片段,但是该方法可以被后面的混合所替代,而且用混合适用性会更好。
先讲丢弃片段(就是在片段着色器.fs中把alpha通道设定一个小阈值,丢弃alpha透明的部分贴图):
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
` `vec4 texColor = texture(texture1, TexCoords);
` `if(texColor.a < 0.1)
` `discard;
` `FragColor = texColor;
}
当采样纹理的边缘的时候,图片边缘进行插值处理时,会采用下一个重复的值进行插值。所以在处理透明图片时,需要让透明图片的边缘插值取消,设置环绕方式为GL_CLAMP_TO_EDGE:
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,format==GL_RGBA?GL_CLAMP_TO_EDGE:GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,format==GL_RGBA?GL_CLAMP_TO_EDGE:GL_REPEAT);
但是,如果使用混合,则不需要在片段着色器中设定阈值。
混合:
混合就是渲染有透明度的图片纹理(就可以替代丢弃片段的操作)。
首先,启用混合:
glEnable(GL_BLEND);
选择混合的计算方式:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
不要打乱顺序:
我们需要最先绘制最远的物体,最后绘制最近的物体(不需要排序不透明的物体):
- 先绘制所有不透明的物体。
- 对所有透明的物体排序。
- 按由远到近的顺序绘制所有透明的物体。
C++标准库的map数据结构,会自动根据键(Key)对它的值(Value)进行排序。所以只需要把距离(distance)设置为键,则可以自动排序了:
std::map<float, glm::vec3> sorted;
` `for (unsigned int i = 0; i < windowPos.size(); i++) // *排序(先画远的window)*
` `{
` `float distance = glm::length(camera.Position - windowPos[i]);
` `sorted[distance] = windowPos[i]; //会自动排序
` `}
由远至近,画每个透明的物体(需要将递增的map反序一下,变为递减):
for (std::map<float, glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
` `{
` `glm::mat4 modelwindow = glm::mat4(1.0f);
` `modelwindow = glm::translate(modelwindow, it->second);
` `modelwindow = glm::rotate(modelwindow, glm::radians(180.0f), glm::vec3(0.0, 0.0, 1.0));
` `windowShader.setMat4(“model”, modelwindow);
` `glDrawArrays(GL_TRIANGLES, 0, 6);
` `}
##
2.4.4面剔除
当观察一个立方体时,最多只能观察到3个面,所以干脆丢弃其余看不见的其他面。
(此方法只适用于封闭立体形;当只用草的平面贴图时要保证正背面都可观察,则不能使用面剔除)
OpenGL能够检查所有面向(Front Facing)观察者的面,并渲染它们,而丢弃那些背向(Back Facing)的面,节省我们很多的片段着色器调用。
如何判断正面和背面,OpenGL使用了一个很聪明的技巧,分析顶点数据的环绕顺序(Winding Order)。
环绕顺序:
每个三角形由3个顶点所组成,我们会从三角形中间来看,为这3个顶点设定一个环绕顺序。
将三角形顶点顺序以逆时针定义的话,正面看起来环绕顺序为逆时针,此时看背面就是顺时针。
面剔除:
OpenGL的面剔除选项了,它默认是禁用状态的。
首先确保在每个三角形中,都以逆时针定义顶点。
启用面剔除:
glEnable(GL_CULL_FACE);
选择剔除哪个面:
glCullFace(GL_FRONT);(不调用时,默认背面)
- GL_BACK:只剔除背向面。
- GL_FRONT:只剔除正向面。
- GL_FRONT_AND_BACK:剔除正向面和背向面。
选择顺时针、逆时针作为正面:
glFrontFace(GL_CCW);
- GL_CCW:默认环绕方向为逆时针。
- GL_FRONT:顺时针。
##
2.4.5帧缓冲(FBO)
前面所涉及的颜色缓冲、深度缓冲、模板缓冲都是帧缓冲,储存在内存中。
OpenGL允许我们定义自己的帧缓冲。
完整的帧缓冲:
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数。
FBO的纹理附件:
在帧缓冲中添加纹理,那么就可以将渲染指令写入这个纹理图像,可以很方便的调用。
先创建一个纹理(glTexImage2D NULL即可):
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
创建帧缓冲:
unsigned int fbo;
glGenFramebuffers(1, &fbo);
绑定帧缓冲:
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
检查帧缓冲是否完成:
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
然后将创建的2D纹理texture附加到帧缓冲:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
……具体看网上教程
后期处理:
反相/颜色反转(有alpha通道的图像需要先用阈值丢弃透明的片段,最后将输出颜色片段的alpha通道设置为不透明1.0f):
void main()
{
` `if(texColor.a < 0.1) // 阈值丢弃片段
` `discard;
` `FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
转灰度图(有alpha通道的图像需要先用阈值丢弃透明的片段,最后将输出颜色片段的alpha通道设置为不透明1.0f):
void main()
{
` `vec4 texColor = texture(texture1, TexCoords);
` `if(texColor.a < 0.1) // 阈值丢弃片段
` `discard;
` `float average = (texColor.r + texColor.g + texColor.b) / 3.0; //转灰度图(平均值法)
` `FragColor = vec4(average,average,average,1.0);
}
可以对R、G、B颜色通道设置不同的权重,因为人眼对绿色更加敏感一些:
void main()
{
` `FragColor = texture(screenTexture, TexCoords);
` `float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b;(不同权重法)
` `FragColor = vec4(average, average, average, 1.0);
}
卷积核效果有alpha通道的图像需要先用阈值丢弃透明的片段,最后将输出颜色片段的alpha通道设置为不透明1.0f):
卷积核可以用来提取图像上的纹理信息。
假设用3X3卷积核:
const float offset = 1.0/300; //计算卷积的偏移数组
vec2 offset[9] = vec2[](
` `vec2(-offset, offset), // 左上
` `vec2( 0.0f, offset), // 正上
` `vec2( offset, offset), // 右上
` `vec2(-offset, 0.0f), // 左
` `vec2( 0.0f, 0.0f), // 中
` `vec2( offset, 0.0f), // 右
` `vec2(-offset, -offset), // 左下
` `vec2( 0.0f, -offset), // 正下
` `vec2( offset, -offset) // 右下
` `);
` `float kernel[9] = float[]( //锐化的卷积核
` `-1,-1,-1,
` `-1, 9,-1,
` `-1,-1,-1
` `);
` `vec4 sampleTex[9];
` `for(int i=0;i<9;i++)
` `{
` `sampleTex[i] = vec4(texture(texture1, TexCoords.st+offset[i])); //获取像素点周围共9个像素
` `}
` `vec4 col = vec4(0.0f);
` `for(int i=0;i<9;i++)
` `{
` `col += sampleTex[i]*kernel[i];
` `}
` `FragColor = col;
−1−1−1−19−1−1−1−1卷积核效果:
2222−152222卷积核效果:
##
2.4.6立方体贴图
立方体纹理就是一个包含6个2D纹理面的立体纹理。
这样,可以通过方向向量来索引。方向向量原点在立方体纹理的中心。(根据方向向量所指向的纹理元素得到纹理值)
创建立方体贴图:
先生成绑定纹理:
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
6个面都要调用纹理目标:
int width, height, nrChannels;
unsigned char *data;
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
` `data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
` `glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
}
设定环绕方式和过滤方式:
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);
在片段着色器中,我们使用了一个不同类型的采样器,samplerCube,使用立方体贴图的片段着色器:
in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器
void main()
{
` `FragColor = texture(cubemap, textureDir);
}
这些的用途主要用在天空盒的渲染中。
天空盒:
天空盒就是一个立方体贴图,包含6个面,每个面都需要一个纹理:
可以在网站上下载天空盒资源http://www.custommapmakers.org/
或者下图的天空盒:https://learnopengl-cn.github.io/data/skybox.rar
加载天空盒纹理(分别加载6个面的纹理):
unsigned int loadCubemap(vector<std::string> faces)
{
` `unsigned int textureID;
` `glGenTextures(1, &textureID);
` `glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
` `int width, height, nrChannels;
` `for (unsigned int i = 0; i < faces.size(); i++)
` `{
` `unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
` `if (data)
` `{
` `glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
` `stbi_image_free(data);
` `}
` `else
` `{
` `std::cout « “Cubemap texture failed to load at path: “ « faces[i] « std::endl;
` `stbi_image_free(data);
` `}
` `}
` `glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
` `glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
` `glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
` `glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
` `glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); // *R轴设定环绕方式,对应纹理的第三个维度*
` `return textureID;
}
天空盒立方体贴图的6个面存放在序列容器中:
vector<std::string> faces
{
` `“right.jpg”,
` `“left.jpg”,
` `“top.jpg”,
` `“bottom.jpg”,
` `“front.jpg”,
` `“back.jpg”
};
unsigned int cubemapTexture = loadCubemap(faces);
天空盒的VAO、VBO:
float skyboxVertices[] = {
` `// positions
` `-1.0f, 1.0f, -1.0f,
` `-1.0f, -1.0f, -1.0f,
` `1.0f, -1.0f, -1.0f,
` `1.0f, -1.0f, -1.0f,
` `1.0f, 1.0f, -1.0f,
` `-1.0f, 1.0f, -1.0f,
` `-1.0f, -1.0f, 1.0f,
` `-1.0f, -1.0f, -1.0f,
` `-1.0f, 1.0f, -1.0f,
` `-1.0f, 1.0f, -1.0f,
` `-1.0f, 1.0f, 1.0f,
` `-1.0f, -1.0f, 1.0f,
` `1.0f, -1.0f, -1.0f,
` `1.0f, -1.0f, 1.0f,
` `1.0f, 1.0f, 1.0f,
` `1.0f, 1.0f, 1.0f,
` `1.0f, 1.0f, -1.0f,
` `1.0f, -1.0f, -1.0f,
` `-1.0f, -1.0f, 1.0f,
` `-1.0f, 1.0f, 1.0f,
` `1.0f, 1.0f, 1.0f,
` `1.0f, 1.0f, 1.0f,
` `1.0f, -1.0f, 1.0f,
` `-1.0f, -1.0f, 1.0f,
` `-1.0f, 1.0f, -1.0f,
` `1.0f, 1.0f, -1.0f,
` `1.0f, 1.0f, 1.0f,
` `1.0f, 1.0f, 1.0f,
` `-1.0f, 1.0f, 1.0f,
` `-1.0f, 1.0f, -1.0f,
` `-1.0f, -1.0f, -1.0f,
` `-1.0f, -1.0f, 1.0f,
` `1.0f, -1.0f, -1.0f,
` `1.0f, -1.0f, -1.0f,
` `-1.0f, -1.0f, 1.0f,
` `1.0f, -1.0f, 1.0f
};
天空盒的顶点着色器、片段着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
` `TexCoords = aPos;
` `gl_Position = projection * view * vec4(aPos, 1.0);
}
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
` `FragColor = texture(skybox, TexCoords);
}
绘制天空盒时,我们需要将它变为场景中的第一个渲染的物体,并且禁用深度写入。这样子天空盒就会永远被绘制在其它物体的背后了:
glDepthMask(GL_FALSE);
skyboxShader.use();
// … 设置观察和投影矩阵
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// … 绘制剩下的场景
优化:
如果先渲染天空盒,再渲染其他物体,虽然能够保证天空在后面不会遮挡其他物体,但是太过占用资源。
可以使用深度测试,只要保证天空盒的深度一直是最大值1.0即可。其x、y、z的分量是通过除以第四个分量w获得的,所以让z分量等于为w分量,那么z/w=1.0一直在最大深度。
void main()
{
` `TexCoords = aPos;
` `vec4 pos = projection * view * vec4(aPos, 1.0);
` `gl_Position = pos.xyww; //z分量给为w,保证透视中z/w=1
}
环境映射:
通过环境的立方体贴图,给物体反射和折射的属性。
反射:
镜子就是一个反射性物体:它会根据观察者的视角反射它周围的环境。
主要是计算反射向量。可以用GLSL语言中的reflect(入射,法向量)函数计算反射向量。
最后,反射向量作为采样立方体贴图的方向向量,返回环境颜色。
着色器片段中GLSL语言实现:
vec3 CalcReflectCubemaps(vec3 normal, vec3 fragPos, vec3 viewDir, samplerCube skyb)
{
` `vec3 I = normalize(fragPos - viewDir); //片段位置-人眼的相机位置
` `vec3 R = reflect(I, normal); //直接调用reflect函数
` `return vec3(texture(skyb, R).rgb);
}
折射:
主要是利用折射定律(斯涅尔定律)。主要涉及到折射率ratio。
不同介质的折射率如下:
当光线从空气进入玻璃时,此时取ratio=1.00/1.52.
着色器片段中GLSL语言实现:
vec3 CalcRefractCubemaps(vec3 normal, vec3 fragPos, vec3 viewDir, samplerCube skyb)
{
` `float ratio = 1.00/1.52; // 折射率
` `vec3 I = normalize(fragPos - viewDir); //片段位置-人眼的相机位置
` `vec3 R = refract(I, normal, ratio); //直接调用refract函数
` `return vec3(texture(skyb, R).rgb);
}
2.4.7高级数据
高级数据 - LearnOpenGL CN (learnopengl-cn.github.io)
##
2.4.8 高级GLSL
GLSL的内建变量:
GLSL提供了一些以gl_开头的变量,例如之前接触到的着色器裁剪空间输出向量:gl_Position,片段着色器的gl_FragCoord.
顶点着色器变量:
如果想要在屏幕上显示渲染物体,必须使用着色器裁剪空间输出向量gl_Position。
gl_PointSize: 设置像素点的宽高(大小)。需要启用修改点大小功能glEnable(GL_PROGRAM_POINT_SIZE);
例如,在顶点着色器上设置顶点大小随着距离增加而变大:
void main()
{
` `gl_Position = projection * view * model * vec4(aPos, 1.0);
` `gl_PointSize = gl_Position.z;
}
gl_Position和gl_PointSize都是输出变量,可以对其进行写入修改。
gl_VertexID是输入变量,只能进行读取。其存储的是当前正在绘制顶点的ID。
- 当使用gl_DrawElements()索引渲染时,这个变量存储顶点ID;
- 当使用gl_DrawArrays()时,这个变量存储已经处理的顶点个数。
虽然没啥用,但是可以访问一下顶点信息。
片段着色器变量:
在片段着色器中有两个输入变量:gl_FragCoord、gl_FrontFacing、gl_FragDepth.
gl_FragCoord:
其z分量对应片段的深度值,其x、y分量是窗口空间坐标,其原点对应窗口的左下角。
其可以根据窗口坐标,计算出不同的颜色。
例如,在窗口不同坐标范围设置不同的颜色:
void main()
{
` `if(gl_FragCoord.x < 400)
` `FragColor = vec4(1.0, 0.0, 0.0, 1.0);
` `else
` `FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
一般应用在演示场景中。
gl_FrontFacing:
在前面的面剔除内容中,通过顶点的环绕方向判断面是正面还是背面。
如果不使用面剔除,可以通过gl_FrontFacing来获取当前片段是正面还是背面。
- 如果当前片段为正面,返回true;
- 如果当前片段为背面,返回false;
例如,在正面和背面设置不同的纹理:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D frontTexture;
uniform sampler2D backTexture;
void main()
{
` `if(gl_FrontFacing)
` `FragColor = texture(frontTexture, TexCoords);
` `else
` `FragColor = texture(backTexture, TexCoords);
}
gl_FragDepth:
gl_FragCoord读取当前片段窗口坐标以及深度值,但是如果要修改深度值可用gl_FragDepth。
从OpenGL4.2版本开始,在使用gl_FragDepth时,需要在片段着色器顶部声明gl_FragDepth变量:
layout (depth_
其中条件可选:
例如,对片段深度增加0.1:
#version 420 core // 注意GLSL的版本!
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;
void main()
{
` `FragColor = vec4(1.0);
` `gl_FragDepth = gl_FragCoord.z + 0.1;
}
接口块:
从顶点着色器向片段着色器发送数据时,希望发送多个变量。
接口块就像数组和结构体,不同的是,根据in、 out来确定它是一个输出块还是输出块。
例如,顶点着色器的输出:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out VS_OUT
{
` `vec2 TexCoords;
} vs_out;
void main()
{
` `gl_Position = projection * view * model * vec4(aPos, 1.0);
` `vs_out.TexCoords = aTexCoords;
}
对应片段着色器的输入:
#version 330 core
out vec4 FragColor;
in VS_OUT
{
` `vec2 TexCoords;
} fs_in;
uniform sampler2D texture;
void main()
{
` `FragColor = texture(texture, fs_in.TexCoords);
}
输出块与输入块的名字要一样才能匹配。
Uniform缓冲对象:
尽管我们单独使用uniform设置projection、view、model矩阵很方便。但是我们发现,我们引入的多个对象的projection、view的矩阵是一样的,只是model矩阵设置位移与旋转角度不一样。
所以,我们利用uniform缓冲对象,只需设置projection、view一次即可。需要将projection、view矩阵存储到uniform块中:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (std140) uniform Matrices
{
` `mat4 projection;
` `mat4 view;
};
uniform mat4 model;
void main()
{
` `gl_Position = projection * view * model * vec4(aPos, 1.0);
}
其中,std140是一种特殊的布局方式。
std140布局声明了每个变量的偏移量都是由一系列规则所决定的,这显式地声明了每个变量类型的内存布局。我们可以手动计算每个变量的偏移量。
Uniform块布局:
其中,std140是一种特殊的布局方式。
std140布局声明了每个变量的偏移量都是由一系列规则所决定的,这显式地声明了每个变量类型的内存布局。我们可以手动计算每个变量的偏移量。
每个变量都有一个基准对齐量,再计算对齐偏移量。
GLSL中的每个变量,比如说int、float和bool,都被定义为4字节量。每4个字节将会用一个N来表示。
Uniform块采用std140布局时,各个成员的对齐偏移量:
使用Uniform缓冲:
首先,将不同的uniform缓冲绑定的不同的绑定点(binding points)上,将着色器A、B的uniform块链接到相同的绑定点,即可共享相同的uniform数据(前提是,两个着色器定义的uniform块名称相同)。
例如,将多个着色器绑定到相同的uniform块(Matrices),并设定绑定点为0:
// *首先,将顶点着色器的Uniform块设置为绑定点0*
unsigned int uniformBlockIndexOurShader = glGetUniformBlockIndex(ourShader.ID, “Matrices”);
` `unsigned int uniformBlockIndexLightCubeShader= glGetUniformBlockIndex(lightCubeShader.ID, “Matrices”);
unsigned int uniformBlockIndexFloorShader = glGetUniformBlockIndex(floorShader.ID, “Matrices”);
unsigned int uniformBlockIndexGrassShader= glGetUniformBlockIndex(grassShader.ID, “Matrices”);
unsigned int uniformBlockIndexWindowShader=glGetUniformBlockIndex(windowShader.ID, “Matrices”);
unsigned int uniformBlockIndexSkyboxShader=glGetUniformBlockIndex(skyboxShader.ID, “Matrices”);
glUniformBlockBinding(ourShader.ID, uniformBlockIndexOurShader, 0);
glUniformBlockBinding(lightCubeShader.ID, uniformBlockIndexLightCubeShader, 0);
glUniformBlockBinding(floorShader.ID, uniformBlockIndexFloorShader, 0);
glUniformBlockBinding(grassShader.ID, uniformBlockIndexGrassShader, 0);
glUniformBlockBinding(windowShader.ID, uniformBlockIndexWindowShader, 0);
glUniformBlockBinding(skyboxShader.ID, uniformBlockIndexSkyboxShader, 0);
然后,创建uniform缓冲对象本身,并绑定到绑定点0: unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// define the range of the buffer that links to a uniform binding point
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
其中,分配的内存大小为两个Mat4矩阵的大小。
使用glBufferSubData在进入渲染循环之前存储投影矩阵:
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
每次循环中,将view矩阵更新到uniform块的第二个矩阵中:
glm::mat4 view = camera.GetViewMatrix();
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
在添加很多物体时,比较省事,只需设置一次projection、view矩阵。
2.4.9 几何着色器
在顶点着色器与片段着色器之间还有一个几何着色器:
顶点着色器vs -> 几何着色器gs -> 片段着色器fs
在几何着色器顶部,要先声明输入的图元类型:
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;
第一行括号内的布局修饰符可以是以下几种:
第二行括号内的输出修饰符的line_strip,表示线条。max_vertices 表示最大顶点数。
GLSL提供了一个内建函数:
in gl_Vertex
{
` `vec4 gl_Position;
` `float gl_PointSize;
` `float gl_ClipDistance[];
} gl_in[];
我们可以使用2个几何着色器函数,EmitVertex和EndPrimitive,来生成新的数据:
void GenerateLine(int index)
{
` `gl_Position = projection * gl_in[index].gl_Position;
` `EmitVertex(); //添加图元
` `gl_Position = projection * (gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE);
` `EmitVertex(); //添加图元
` `EndPrimitive(); //添加完,合成图元
}
爆破物体效果:
希望将每个三角形沿着法向量的方向移动一小段距离。
首先要计算每个三角形的法向量。利用三角形的两条边来叉乘,从而获得垂直的法向量。以下使用三个顶点(0,1,2)来计算法向量:
vec3 GetNormal()
{
` `vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
` `vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
` `return normalize(cross(a, b));
}
接下来,就是沿着法向量位移一段距离(随时间变化):
vec4 explode(vec4 position, vec3 normal)
{
` `float magnitude = 2.0;
` `vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
` `return position + vec4(direction, 0.0);
}
主函数实现:
void main() {
` `vec3 normal = GetNormal();
` `gl_Position = explode(gl_in[0].gl_Position, normal);
` `TexCoords = gs_in[0].texCoords;
` `EmitVertex();
` `gl_Position = explode(gl_in[1].gl_Position, normal);
` `TexCoords = gs_in[1].texCoords;
` `EmitVertex();
` `gl_Position = explode(gl_in[2].gl_Position, normal);
` `TexCoords = gs_in[2].texCoords;
` `EmitVertex();
` `EndPrimitive();
}
而且别忘了在OpenGL代码中设置time变量:
shader.setFloat(“time”, glfwGetTime());
法向量可视化:
顶点着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out VS_OUT {
` `vec3 normal;
} vs_out;
uniform mat4 view;
uniform mat4 model;
void main()
{
` `vs_out.normal = mat3(transpose(inverse(model))) * aNormal;
` `gl_Position = view * model * vec4(aPos, 1.0);
}
几何着色器:
#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;
in VS_OUT {
` `vec3 normal;
} gs_in[];
const float MAGNITUDE = 0.2;
uniform mat4 projection;
void GenerateLine(int index)
{
` `gl_Position = projection * gl_in[index].gl_Position;
` `EmitVertex();
` `gl_Position = projection * (gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE);
` `EmitVertex();
` `EndPrimitive();
}
void main()
{
` `GenerateLine(0); // first vertex normal
` `GenerateLine(1); // second vertex normal
` `GenerateLine(2); // third vertex normal
}
片段着色器:
#version 330 core
out vec4 FragColor;
void main()
{
` `FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
2.4.10 实例化
绘制大量的物体时,如果一直调用glDrawArrays或glDrawElements函数则会消耗大量的性能。
所以利用实例化,将数据一次性发给GPU,不用不停地与CPU进行通信,然后使用一个绘制函数来绘制很多个物体,会好很多。即使用一个渲染来绘制多个物体。
使用实例化时,只需将glDrawArrays和glDrawElements的渲染调用分别改为glDrawArraysInstanced和glDrawElementsInstanced就可以,额外加一个实例数量,设置需要渲染的实例次数(个数),内建函数gl_InstanceID会从0随着递增。
实例化时,顶点着色器中,model用保存的所有model的数组instanceMatrix代替(不再使用uniform model):
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;
out vec2 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
` `gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0);
` `TexCoords = aTexCoords;
}
因为mat4实质上是4个vec4,所以应该设置4个顶点。此时将位置设置为3了,所以每个顶点的position应该为3,4,5,6。
所以绑定顶点数组如下:
unsigned int buffer;
` `glGenBuffers(1, &buffer);
` `glBindBuffer(GL_ARRAY_BUFFER, buffer);
` `glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
` `for (unsigned int i = 0; i < ourModel.meshes.size(); i++)
` `{
` `unsigned int VAO = ourModel.meshes[i].VAO;
` `glBindVertexArray(VAO);
` `glEnableVertexAttribArray(3);
` `glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void *)0);
` `glEnableVertexAttribArray(4);
` `glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void *)(sizeof(glm::vec4)));
` `glEnableVertexAttribArray(5);
` `glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void *)(2 * sizeof(glm::vec4)));
` `glEnableVertexAttribArray(6);
` `glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void *)(3 * sizeof(glm::vec4)));
` `glVertexAttribDivisor(3, 1);
` `glVertexAttribDivisor(4, 1);
` `glVertexAttribDivisor(5, 1);
` `glVertexAttribDivisor(6, 1);
` `glBindVertexArray(0);
` `}
首先在小行星带的model位置矩阵,要求行星带环绕小行星分布,将每个小行星的model添加到一维矩阵数组中保存,具体定义如下:
unsigned int amount = 100000; // *设置100000个实例*
` `glm::mat4 *modelMatrices; // *定义指针数组*
` `modelMatrices = new glm::mat4[amount];
` `srand(static_cast
` `float radius = 5.0f; // *半径*
` `float offset = 2.0f; // *位置偏移*
` `for (unsigned int i = 0; i < amount; i++)
` `{
` `glm::mat4 model = glm::mat4(1.0f);
` `// *1. 位移:分布在半径为 ‘radius’ 的圆形上,偏移的范围是 [-offset, offset]*
` `float angle = (float)i / (float)amount * 360.0f;
` `float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
` `float x = sin(angle) * radius + displacement;
` `displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
` `float y = displacement * 0.4f; // *让行星带的高度比x和z的宽度要小*
` `displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
` `float z = cos(angle) * radius + displacement;
` `model = glm::translate(model, glm::vec3(x, y, z));
` `// *2. 缩放:在 0.05 和 0.25f 之间缩放*
` `float scale = static_cast
` `model = glm::scale(model, glm::vec3(scale));
` `// *3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转*
` `float rotAngle = static_cast
` `model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
` `// *4. 添加到矩阵的数组中*
` `modelMatrices[i] = model;
` `}
最后,绑定顶点数组VAO画元素(此时使用glDrawElementsInstanced函数画):
instanceShader.use();
for(unsigned int i = 0; i < ourModel.meshes.size(); i++)
{
` `glBindVertexArray(rock.meshes[i].VAO);
` `glDrawElementsInstanced(
` `GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
` `);
}
(100000个小行星带) (20辆太空车)
2.4.11 抗锯齿
我们发现,在渲染出来的模型物体边缘存在着很多的锯齿(Aliasing)。现在通常有以下两种方法来解决这个问题:
- 超采样抗锯齿:
使用比正常分辨率更高的分辨率,性能开销大,只有短暂的辉煌。
- 多重采样抗锯齿(MSAA):
目前主要常用的抗锯齿方法。
多重采样:
问题主要出现在光栅化的过程中。理论上顶点坐标可以定义为任意值,但是片段则不行,因为它们受限于你窗口的分辨率。顶点坐标与片段之间几乎永远也不会有一对一的映射,所以光栅器必须以某种方式来决定每个顶点最终所在的片段/屏幕坐标。
例如下面光栅化中,每个像素有个中心采样点,三个顶点连线组成的三角形所覆盖的像素就会生成为片段:
结果就是得到的有锯齿的边缘。
多重采样就是在每个像素上,增加多个采样点,而不是仅仅一个。那么根据三角形所覆盖的采样点数,来决定该像素片段的颜色值,从而使得物体边缘看起来更加平滑。
如果使用的采样点非常多,启用多重采样会显著降低程序的性能。在本节写作时,通常采用的是4采样点的MSAA。
OpenGL中的多重采样抗锯齿(MSAA):
在OpenGL中,我们要在每个像素中存储4个采样点的缓冲,此时就需要一个新的数据类型“多重采样缓冲”。我们只需在创建窗口时,提示(Hint)GLFW,在创建窗口之前调用:
glfwWindowHint(GLFW_SAMPLES, 4);
OpenGL默认已经启用多重采样:
glEnable(GL_MULTISAMPLE);
离屏多重采样抗锯齿(MSAA):
从上面操作可知,MSAA通过GLFW创建非常简单。
然而如果用我们自己的帧缓冲创建离屏渲染,必须要自己写多重采样缓冲。
一般有两种方法创建多重采样缓冲:
- 多重采样纹理附件
- 多重采样渲染缓冲附件
多重采样纹理附件,创建支持多个采样点的纹理,使用glTexImage2DMultisample来替代glTexImage2D:
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples(样本数), GL_RGB, width, height, GL_TRUE);
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
自定义抗锯齿算法:
2.5高级光照
2.5.1 高级光照
之前的冯氏光照模型,主要是通过环境光+漫反射光+镜面反射光组成。
在镜面反射光计算中,视线方向与反射光线方向分别存在<90°与 >90°的情况:
其中反射光分量计算公式为:
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
可知,当视线方向与反射光线方向>90°时,点乘为负数,最后max取最大值为0。所以会损失掉大于90°部分的镜面反射光。
1977年,James F. Blinn在冯氏着色模型上加以拓展,引入了Blinn-Phong着色模型。Blinn-Phong模型与冯氏模型非常相似,但是它对镜面光模型的处理上有一些不同,让我们能够解决之前提到的问题。Blinn-Phong模型不再依赖于反射向量,而是采用了所谓的半程向量(Halfway Vector),即光线与视线夹角一半方向上的一个单位向量。当半程向量与法线向量越接近时,镜面光分量就越大。
主要改变在片段着色器内,由冯氏模型方法 -> Blinn-Phong方法。即用半程向量代替反射向量:
vec3 reflectDir = reflect(-lightDir, normal);
spec = pow(max(dot(viewDir, reflectDir), 0.0), 8.0);
变为:
vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(normal, halfwayDir), 0.0), 8.0);
##
2.5.2 gamma校正
Gamma也叫灰度系数,每种显示设备都有自己的Gamma值。
- 第一行:人眼感知的正常灰阶。亮度翻倍,感受到一倍的颜色变化。
- 第二行:物理世界真实亮度。物理的亮度基于光子的数量,光子数量翻倍时,人眼只对暗的颜色变化更敏感。
所以当监视器显示亮度时,我们看到的就像第二行那样。
大多数监视器是阴极射线管显示器(CRT)。它有一个物理特性就是两倍的输入电压产生的不是两倍的亮度,CRT亮度是是电压的2.2次幂而得(这也叫监视器Gamma)。
监视器Gamma、Gamma校正(监视器Gamma的倒数)、理想状态的曲线图如下:
Gamma校正:
- 使用OpenGL内建的sRGB帧缓冲:glEnable(GL_FRAMEBUFFER_SRGB);
- 每个片段着色器内计算:float gamma = 2.2;
` `fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
(Gamma 校正前(左)、后对比(右))
额外需要注意的一点就是,在使用gamma校正后,光源衰减可以选用线性衰减。
##
2.5.3(1) 阴影映射
阴影映射:
阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。
当前实时渲染领域还没找到一种完美的阴影算法。目前有几种近似阴影技术,但它们都有自己的弱点和不足。
深度贴图:
渲染阴影:
改进阴影贴图:
PCF:
##
2.5.3(2) 点阴影
##
2.5.3(3) CSM
2.6超大数组给着色器
2.6.1 加载到2D纹理(texelFetch)
相较于texture方法加载纹理,会进行归一化、插值等操作,比较耗时。
texelFetch则使用未归一化的坐标直接访问纹理数据,没有进行归一化与插值,此时纹理坐标就为原始图像的宽、高。
vec4 texelFetch(sampler2D sampler, ivec2 P, int lod);
vec4 texelFetch(sampler3D sampler, ivec3 P, int lod);
vec4 texelFetch(samplerBuffer sampler, int P);
下面两行代码可以直接互换:
gl_FragColor = texture(s_Texture, v_texCoord);
gl_FragColor = texelFetch(s_Texture, ivec2(int(v_texCoord.x * imgWidth), int(v_texCoord.y * imgHeight)), 0);
但是会有轻微差异。
##
2.6.2 Uniform(UBO,Uniform Buffer Object)
多个program对象着色器内具有相同的uniform块时适用,可以一下子把所有着色器内部的uniform块内进行传入。
OpenGL ES 会对Uniform数量进行限制,一般支持1024个uniform。
查看uniform可用数量:
int maxVertexUniform, maxFragmentUniform;
glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS, &maxVertexUniform);
glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, &maxFragmentUniform);
使用很大的数组会突破这个限制,并且uniform变量很难管理,
所以使用UBO方法进行存储uniform,此时不会占用着色器program自身的uniform数量空间。
是一种新的内存->显存的存储方式。一般与uniform块配合使用。
顶点着色器内定义Uniform块:
#version 310 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
layout (std140) uniform MVPMatrix
{
` `mat4 projection;
` `mat4 view;
` `mat4 model;
};
out vec2 v_texCoord;
void main()
{
` `gl_Position = projection * view * model * a_position;
` `v_texCoord = a_texCoord;
}
绑定生成UBO:
// 获取与绑定uniform块
GLuint uniformBlockIndex = glGetUniformBlockIndex(m_ProgramObj, “MVPMatrix”);
glUniformBlockBinding(m_ProgramObj, uniformBlockIndex, 0);
//生成与绑定
glGenBuffers(1, &m_UboId);
glBindBuffer(GL_UNIFORM_BUFFER, m_UboId);
glBufferData(GL_UNIFORM_BUFFER, 3 * sizeof(glm::mat4), nullptr, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
//定义绑定点为 0 buffer 的范围
glBindBufferRange(GL_UNIFORM_BUFFER, 0, m_UboId, 0, 3 * sizeof(glm::mat4));
2.6.3纹理缓冲区对象(TBO,Texture Buffer Object)
OpenGL ES 3.2 引入的概念,Android需要保证 API >= 24。
//生成一个 Buffer Texture
glGenTextures(1, &m_TboTexId);
float *bigData = new float[BIG_DATA_SIZE];
for (int i = 0; i < BIG_DATA_SIZE; ++i) {
` `bigData[i] = i * 1.0f;
}
//生成一个 TBO ,并将一个大的数组上传至 TBO
glGenBuffers(1, &m_TboId);
glBindBuffer(GL_TEXTURE_BUFFER, m_TboId);
glBufferData(GL_TEXTURE_BUFFER, sizeof(float) * BIG_DATA_SIZE, bigData, GL_STATIC_DRAW);
delete [] bigData;
//使用TBO
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_BUFFER, m_TboTexId);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_TboId);
GLUtils::setInt(m_ProgramObj, “u_buffer_tex”, 0);
片段着色器,需要引入扩展 texture buffer ,注意版本声明为 #version 320 es :
#version 320 es
#extension GL_EXT_texture_buffer : require
in mediump vec2 v_texCoord;
layout(location = 0) out mediump vec4 outColor;
uniform mediump samplerBuffer u_buffer_tex; // TBO
uniform mediump sampler2D u_2d_texture; //普通2D纹理
uniform mediump int u_BufferSize;
void main()
{
` `mediump int index = int((v_texCoord.x +v_texCoord.y) /2.0 * float(u_BufferSize - 1));
` `mediump float value = texelFetch(u_buffer_tex, index).x;
` `mediump vec4 lightColor = vec4(vec3(vec2(value / float(u_BufferSize - 1)), 0.0), 1.0);
` `outColor = texture(u_2d_texture, v_texCoord) * lightColor;
}
2.6.4 PBO(像素缓冲对象)
与 PBO 绑定相关的 Target 标签有 2 个:GL_PIXEL_UNPACK_BUFFER 和 GL_PIXEL_PACK_BUFFER。
PBO 可以在 GPU 的缓存间快速传递像素数据,不影响 CPU 时钟周期,除此之外,PBO 还支持异步传输。
` `—– >
所以,绑定 PBO 后,执行 glTexImage2D (将图像数据从 PBO 传输到纹理对象) 操作,CPU 无需等待,可以立即返回。
int imgByteSize = m_Image.width * m_Image.height * 4;//RGBA
glGenBuffers(1, &uploadPboId);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboId); //program -> OpenGL
glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, 0, GL_STREAM_DRAW);
glGenBuffers(1, &downloadPboId);
glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId); //OpenGL-> program
glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, 0, GL_STREAM_DRAW);
两个 PBO 加载图像数据到纹理对象:
利用 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<size_t>(dataSize));
` `//update image data
` `int randomRow = rand() % (m_RenderImage.height - 5);
` `memset(bufPtr + randomRow * m_RenderImage.width * 4, 188,
` `static_cast<size_t>(m_RenderImage.width * 4 * 5));
` `glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
}
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
END_TIME(“PBOSample::UploadPixels Update Image data”)
使用 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<GLubyte *>(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”)
GLuint Buffer;
GLuint Texture;
void init()
{
` `// 创建1个缓冲区
` `glGenBuffers(1, &Buffer);
` `// 缓冲区刚创建出来的时候还没有分配内存,所以我们要初始化一下它
` `// 先绑定.. glBindBuffer(GL_ARRAY_BUFFER, Buffer);
` `// 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
` `// 然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
` `// 数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个NULL就行了
` `// GL内部会给缓冲区分配内存,然后什么都不干,第4个参数可以优化显存效率,指定
` `// 缓冲区中的数据读写频繁程度,如果缓冲区中的数据不经常读写,可以传入 GL_STATIC_****
` `// 这样GL会把缓冲区放在内存数据不经常变动的区域,如果要经常读写缓冲区中的数据,可以传
` `// 别的值,具体参考 @https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/ 吧
` `// 注意这里的 BUFFER_SIZE 我们假设要复制整个屏幕的像素数据,格式为RGB就行了,那么
// 大小就是 屏幕宽度×屏幕高度×3,每个像素3字节
glBufferData(GL_ARRAY_BUFFER, BUFFER_SIZE, NULL, GL_STREAM_COPY);
` `// 这样我们的缓冲区就已经初始化好了,它现在已经有一块可用的内存
` `// 随时可以用 glMapBuffer 来访问
` `// 初始化完了那么解绑吧
` `glBindBuffer(GL_ARRAY_BUFFER, 0);
` `// 创建1个纹理,等会把屏幕复制到这个纹理
` `glGenTextures(1, &Texture);
` `// 初始化纹理,不多解释了 glBindTexture(GL_TEXTURE_2D, Texture);
` `// 这里data参数传NULL和上面缓冲区一样,GL仅仅给纹理分配内存而已
` `// ScreenWide和ScreenTall是屏幕的宽度和高度
` `// 格式用RGB,因为屏幕不需要透明通道,所以纹理的像素数据大小是和上面的缓冲区大小一样的
` `glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, ScreenWide, ScreenTall, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
` `glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
` `glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
` `// 初始化完了解绑
` `glBindTexture(GL_TEXTURE_2D, 0);
}
void draw()
{
` `// 假装这里画了游戏场景
` `// 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方 glBindBuffer(GL_PIXEL_PACK_BUFFER, Buffer);
` `// 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
` `// 前4个参数就是要读取的屏幕区域,不多解释
` `// 格式是RGB,类型是BYTE,每个像素3字节
` `// 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,这里不啰嗦没用的东西了。
` `// 传NULL就行
glReadPixels(0, 0, ScreenWide, ScreenTall, GL_RGB, GL_UNSIGNED_BYTE, NULL);
` `// 好了我们已经成功把屏幕的像素数据复制到了缓冲区里
` `// 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
` `// 完成截图
` `/******
` `// 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
` `// 然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
` `void *data = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_WRITE);
` `if (data)
` `{
` `WriteTGA(“screenshot.tga”, ScreenWide, ScreenTall, data);
` `glUnmapBuffer(GL_PIXEL_PACK_BUFFER); // 不要忘了解除Map
` `}
` `******/
` `// 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
qglBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
` `// 接着我们演示一下把缓冲区中的像素数据传给纹理
` `// 首先我们把缓冲区绑定到 GL_PIXEL_UNPACK_BUFFER 这个地方。这里注意啊!GL_PIXEL_PACK_BUFFER 和 GL_PIXEL_UNPACK_BUFFER 是不同的! glBindBuffer(GL_PIXEL_UNPACK_BUFFER, Buffer);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, Texture);
` `// 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
` `// 如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
` `// 前面参数很简单就不解释了,最后一个参数和上面glReadPixels同理,传NULL就行
` `// 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
` `// 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据
// 这就提高了速度,并且优化了显存的利用率
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, ScreenWide, ScreenTall, GL_RGB, GL_UNSIGNED_BYTE, NULL);
` `// 完事了把GL_PIXEL_UNPACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
` `// 这时候我们已经更新了纹理,我们可以把纹理画出来看看
` `// 假装这里有绘制纹理的代码
}
分别对图像降噪滤波、边缘检测、形态学处理、霍夫变换、仿射变换、漫水填充、膨胀腐蚀等一些传统处理方法进行实现与效果对比,总结他们适用的场景。
优化图像处理速度。相较于在GLSL内进行进行纹理与坐标的计算处理,在OpenCV中实现图像灰度变化、边缘检测等算法会使得处理时间大大增加。但在机器学习模型推理前的数据预处理阶段,一些依靠OpenCV必要的处理环节还是无法避免。
速度对比:
|纹理大小:7680x4160||| | :- | :- | :- | |普通2D纹理|50ms|| |双PBO|UNPACK : 10ms|PACK : 1ms| ##
2.6.5 FBO(帧缓冲对象)-离屏渲染
可以为其添加纹理或渲染缓冲区对象(RBO)。
利用 GPU 在后台完成一些图像转换、缩放等耗时操作,这个时候利用 FBO 可以方便实现类似需求。
PBO添加了纹理、渲染缓冲区之后,才能作为渲染目标。只提供3中attachment(颜色、深度、模板)。
FBO渲染时,先添加连接对象:
然后可以使用 glReadPixels 或者 HardwareBuffer 将渲染后的图像数据读出来,从而实现在后台利用 GPU 完成对图像的处理。
使用步骤如下:
1.初始化FBO:
(1)主要是创建一个普通的2D纹理;
(2)glGenFramebuffers、glBindFramebuffer也创建一个FBO缓冲;
(3)glFramebufferTexture2D然后将普通2D纹理绑定到FBO缓冲;
(4)glTexImage2D分配内存。
1.初始化的具体代码:
// 创建一个 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);
2.使用FBO的步骤:
(1)glBindFramebuffer先绑定FBO;
(2)glUseProgram、glBindVertexArray、glActiveTexture然后激活program,绑定顶点数组VAO,激活对应纹理;
(3)glBindTexture、glUniform1i、glDrawElements、绑定纹理,FBO纹理位置传0,绘制;
(4)glBindVertexArray(0)、glBindTexture(GL_TEXTURE_2D, 0)、glBindFramebuffer(GL_FRAMEBUFFER, 0);绘制完之后绑定0解绑。
(5)glBindTexture、glUniform1i、glDrawElements然后拿到FBO渲染结果,再进行一次普通2D渲染即可。
2.具体使用的代码:
// 绑定 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);