OpenGL学习历程
说明
- OpenGL学习中文网
- 学习OpenGL3.3核心模式(因为早期的立即渲染模式,也称固定渲染管线,这个模式虽然绘图方便,但灵活性不强,OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,为什么要使用3.3版本,因为所有OpenGL的更高的版本都是在3.3的基础上,引入了额外的功能,并没有改动核心架构)
- 开发环境Win10,VS2017
OpenGL开发环境搭建
GLFW配置
- GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文,定义窗口参数以及处理用户输入
- GLFW源码包下载,请下载32位的版本,64位版本大部分人反映有BUG。
- CMake生成项目,并使用VistualStudio编译之后在src/Debug文件夹内可找到glfw3.lib
- 创建文件夹ThirdParty/glfw3存放include和lib
- ThirdParty
- opengl
- include
- GLFW
- glfw3.h
- glfw3native.h
- GLFW
- lib
- glfw3.lib
- include
- opengl
- 在项目引用头文件的方法
#include <GLFW\glfw3.h>
GLAD配置
- GLAD是一个开源的库,GLAD使用了一个在线服务。
- 选择对应选项后,点击生成(Generate)按钮来生成库文件。
- GLAD现在应该提供给你了一个zip压缩文件,包含两个头文件目录,和一个glad.c文件。将两个头文件目录(glad和KHR)复制到你的include文件夹中,并添加glad.c文件到你的工程中。
- 在项目引用头文件的方法
#include <glad/glad.h>
VS项目引入第三方库
- 创建VC++控制台项目
- 拷贝glad.c文件到项目中
- 项目设置中添加包含目录:ThirdParty/opengl/include
- 添加库目录:ThirdParty/opengl/lib
- 添加库:opengl32.lib;glfw3.lib;
知识理论
图形渲染管线(Graphics Pipeline)
- 图形渲染管线,指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。
- 图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
- 图形渲染管线阶段
1. 顶点着色器(Vertex Shader)
它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
2. 图元装配(Primitive Assembly)
顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状。
3. 几何着色器(Geometry Shader)
几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
4. 光栅化阶段(Rasterization Stage)
这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。 下图是图形渲染管线的每个阶段的抽象展示。 要注意蓝色部分代表的是我们可以注入自定义的着色器的部分
关键词
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
GLSL(OpenGL着色器语言)
- 着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
- 典型的着色器结构
#version version_number in type in_variable_name; in type in_variable_name; out type out_variable_name; uniform type uniform_name; int main() { // 处理输入并进行一些图形操作 ... // 输出处理过的结果到输出变量 out_variable_name = weird_stuff_we_processed; }
- 默认基础数据类型
GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix) - 着色器的输入与输出(数据传递)
- in 和 out
- uniform
纹理(Texture)
- 纹理是一个2D图片(甚至也有1D和3D的纹理)
- 纹理坐标(Texture Coordinate),为了能够把纹理映射到三角形上,三角形每个顶点关联着一个纹理坐标,用来标明该从纹理图像的哪个部分采样(采集片段颜色),纹理坐标在x和y轴上,范围为0到1之间(2D纹理图像),使用纹理坐标获取纹理颜色叫做采样(Sampling)。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
float texCoords[] = { 0.0f, 0.0f, // 左下角 1.0f, 0.0f, // 右下角 0.5f, 1.0f // 上中 };
- 纹理环绕方式
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
```
// 可以使用glTexParameter*函数对单独的一个坐标轴设置(纹理坐标轴s、t、r等价于x、y、z)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
//选择GL_CLAMP_TO_BORDER选项,我们还需要指定一个边缘的颜色。
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
```
- 纹理过滤(Texture Filtering)
纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。
- GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
- GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
那么这两种纹理过滤方式有怎样的视觉效果呢?
//我们需要使用glTexParameter*函数为放大和缩小指定过滤方式。 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- 多级渐远纹理(Mipmap)
简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。
手工为每个纹理图像创建一系列多级渐远纹理很麻烦,幸好OpenGL有一个glGenerateMipmaps函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。 - 加载与创建纹理
纹理图像可能被储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?自己写一个图像加载器,把图像转化为字节序列很麻烦,用另一种解决方案stb_image.h库,stb_image下载。
下载这一个头文件,将它以stb_image.h的名字加入你的工程,并另创建一个新的C++文件,输入以下代码:#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h"
变换
- 向量
- 向量与标量运算
- 向量取反
- 向量加减
- 长度
- 向量相乘
- 点乘(Dot Product),记作v¯⋅k¯
两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。
公式:v¯⋅k¯=||v¯||⋅||k¯||⋅cosθ
乘是通过将对应分量逐个相乘,然后再把所得积相加来计算的。两个单位向量点乘会像是这样:
要计算两个单位向量间的夹角,我们可以使用反余弦函数cos−1 ,可得结果是143.1度。 - 另一个是叉乘(Cross Product),记作v¯×k¯。
叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。下面的图片展示了3D空间中叉乘的样子:
两个正交向量A和B叉积:
- 点乘(Dot Product),记作v¯⋅k¯
- 矩阵
- 矩阵加减
- 矩阵数乘
- 矩阵相乘
- 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。
- 矩阵相乘不遵守交换律(Commutative),也就是说A⋅B≠B⋅A。
- 单位矩阵
单位矩阵是一个除了对角线以外都是0的N×N矩阵。
- 缩放
如果我们把缩放变量表示为(S1,S2,S3)我们可以为任意向量(x,y,z)定义一个缩放矩阵:
- 位移
如果我们把位移向量表示为(Tx,Ty,Tz),我们就能把位移矩阵定义为:
- 旋转
2D或3D空间中的旋转用角(Angle)来表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。
旋转矩阵在3D空间中每个单位轴都有不同定义,旋转角度用θ表示://大多数旋转函数需要用弧度制的角,但幸运的是角度制的角也可以很容易地转化为弧度制的: 弧度转角度:角度 = 弧度 * (180.0f / PI) 角度转弧度:弧度 = 角度 * (PI / 180.0f)
沿x轴旋转:
沿y轴旋转:
沿z轴旋转:
- 矩阵的组合
假设我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后位移(1, 2, 3)个单位。我们需要一个位移和缩放矩阵来完成这些变换。
建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移。比如,如果你先位移再缩放,位移的向量也会同样被缩放。
最终的变换矩阵左乘我们的向量会得到以下结果:
不错!向量先缩放2倍,然后位移了(1, 2, 3)个单位。
- GLM
GLM是OpenGL Mathematics的缩写,它是一个只有头文件的库,不用链接和编译。
GLM下载//我们需要的GLM的大多数功能都可以从下面这3个头文件中找到: #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp>
项目案例
创建窗口
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
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);
// 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);
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();
}
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);
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}