环境配置
1.配置opengl环境:下载visual studio2022,下载GLFW(https://www.glfw.org/download.html),下载安装cMake(https://cmake.org/download/),使用cMake为GLFW生成工程文件。
在GLFW工程文件中找到GLFW.sln用vs打开,生成解决方案,得到glfw3.lib。
配置GLAD库,打开在线服务(https://glad.dav1d.de/),将Language设置为C/C++,在API选项中,选择3.3以上的OpenGL(gl)版本。之后将Profile设置为Core,选中Generate a loader选项。最后点击生成(Generate)按钮来生成库文件。
2.创建窗口。
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL) 创建一个窗口对象,800为窗口宽度,600为窗口高度,"LearnOpenGL"为窗口名字。
glViewport(0, 0, 800, 600);//设置渲染窗口大小,前两个参数为左下角位置,后两个为右上角位置。
glfwSwapBuffers(window);//利用双缓冲机制,交换前后缓冲,渲染好的颜色缓冲交换到窗口上保证画面流畅。
glfwPollEvents();检测输入事件。
glfwTerminate();:释放资源。
创建输入事件。
按下esc退出窗口。
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
while (!glfwWindowShouldClose(window))
{
processInput(window);
glfwSwapBuffers(window);
glfwPollEvents();
}
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);清屏并给窗口填充颜色。
glClear(GL_COLOR_BUFFER_BIT);清空颜色缓冲。
渲染函数
// 渲染循环
while(!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染指令
...
// 检查并调用事件,交换缓冲
glfwPollEvents();
glfwSwapBuffers(window);
}
3.OpenGL有很多缓冲对象类型,OpenGL允许我们同时绑定多个缓冲对象,只要它们是不同的缓冲类型。
顶点数组缓冲对象:Vertex Array Object,VAO
可以储存操作的顶点缓冲对象信息,方便多个不同的顶点数据切换。
创建一个VAO对象
unsigned int VAO;
glGenVertexArrays(1, &VAO);//1创建的VAO对象个数,VAO对象。
绑定VAO,当绑定VAO后,再操作VBO,所有的信息将会被记录再该VAO对象中,当需要使用该顶点数据时再绑定对应的VAO对象即可。
glBindVertexArray(VAO);
顶点缓冲对象:Vertex Buffer Object,VBO
管理GPU中顶点数据缓冲内存的对象,可以存储大量顶点,便于顶点着色器处理。
创建一个VBO
unsigned int VBO;
glGenBuffers(1, &VBO);//1 生成缓冲对象的数量,VBO id
顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
glBindBuffer(GL_ARRAY_BUFFER, VBO);
绑定好后,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//缓冲类型,数据大小,数据,储存方式(GL_STATIC_DRAW :数据不会或几乎不会改变,GL_DYNAMIC_DRAW:数据会被经常改变,GL_STREAM_DRAW :数据每次绘制时都会改变。)
元素缓冲对象:Element Buffer Object,EBO; 索引缓冲对象 Index Buffer Object,IBO。 这两个是同一个东西。
存储一系列三角形顶点索引信息。
创建索引缓冲对象
unsigned int EBO;
glGenBUffers(1,&EBO);
绑定缓冲对象
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//GL_ELEMENT_ARRAY_BUFFER 缓冲类型
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);//将索引信息复制到缓冲里
当绘制物体时先绑定EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
绘制物体
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//GL_TRIANGLES绘制模式,绘制的顶点个数,GL_UNSIGNED_INT索引数据类型,0偏移量
注意:在绑定VAO时,绑定的最后一个元素缓冲区对象(EBO)会存储为该VAO对应的EBO,所以只需要绑定VAO,EBO也自动绑定了。VAO会存储glBindBuffer调用行为,这也意味着它也会储存解绑调用,所以应该EBO解绑之前先解绑VAO。
4.图形渲染管线:顶点着色器,图元装配,几何着色器,光栅化,片段着色器,(深度测试,模板测试等)测试和混合。
顶点着色器:把单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
创建着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);//GL_VERTEX_SHADER (顶点着色器)着色器类型
将着色器代码附加到着色器对象上,编译着色器
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//着色器对象,字符串数量,代码字符串
glCompileShader(vertexShader);//着色器对象
图元装配:将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状。
几何着色器:把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
光栅化:把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段。在片段着色器运行之前会执行裁切。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
片段着色器:主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
测试预混合:检测片段的对应的深度值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值并对物体进行混合。
标准化设备坐标(Normalized Device Coordinates, NDC):一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。
5.GLSL:着色器语言
6.着色器程序对象
着色器程序对象是多个着色器合并之后并最终链接完成的版本。在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
创建一个着色器程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
将各个着色器对象附加到着色器程序对象,链接他们。
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
激活着色器程序对象
glUseProgram(shaderProgram);
删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
7.设置顶点数据读取格式。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);//顶点属性位置值(对应顶点着色器中layout(location = 0)),顶点属性大小,数据类型,属性数据大小,起始偏移量
激活顶点属性
glEnableVertexAttribArray(0);//0 顶点属性位置值
绘制图像
glDrawArrays(GL_TRIANGLES, 0, 3);//GL_TRIANGLES图元样式,0定点数组的起始索引,3需要绘制的顶点个数。
8.纹理
纹理可以映射到坐标上给物体设置颜色(类似于皮肤),有些可以用来存储数据信息。
设置当纹理坐标超出范围时采样方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);//纹理目标(绑定到GL_TEXTURE_2D上的纹理),x方向(S,T,R分别对应X,Y,Z),采样方式(GL_MIRRORED_REPEAT重复纹理,GL_MIRRORED_REPEAT镜像重复纹理,GL_CLAMP_TO_EDGE拉伸边缘)
//纹理坐标超出纹理大小范围时用户也可以指定填充颜色
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);//纹理目标,用户指定填充色,颜色向量
纹理过滤:当纹理和对应物体不一样大时需要进行纹理过滤以便纹理能适应物体。
当纹理小于物体时,需要对纹理进行上采样(过滤)。opengl有两种常见的过滤方式:1.GL_NEAREST邻近过滤(opengl默认方式),选中中心点最接近该坐标的像素作为该点的颜色值。
2.GL_LINEAR线性过滤,通过对该点临近四个像素值进行双线性插值得到该点的颜色值。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:当纹理大于物体时,会产生走样.需要建立一系列纹理图像,它们的尺寸逐渐减半。在进行纹理映射时只需找到mipmap最接近的两张纹理,对他们进行三线性插值得到最终的纹理。而只需要内存占用比原纹理多了1/3。
生成mipmap
glGenerateMipmap(GL_TEXTURE_2D);//纹理目标
mipmap纹理过滤方式和普通纹理一样。
生成纹理对象
unsigned int texture;
glGenTextures(1, &texture);//生成数量,纹理对象
绑定纹理
glBindTexture(GL_TEXTURE_2D, texture);//绑定纹理到纹理目标
使用数据生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//纹理目标,多级纹理级别,纹理存储格式,纹理宽,纹理高,总为0,源图的格式,图像数据类型,图像数据。
生成mipmap
glGenerateMipmap(GL_TEXTURE_2D);
在绑定纹理之前先激活纹理单元
glActiveTexture(GL_TEXTURE0);//GL_TEXTURE0纹理单元(默认为GL_TEXTURE0激活,其他的为GL_TEXTURE1,GL_TEXTURE2...)
将纹理绑定到激活的纹理单元上。
glBindTexture(GL_TEXTURE_2D, texture);
设置着色器对应的纹理单元
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);//ourShader.ID着色器id,该着色器内定义的采样器名,0纹理单元。
9.stb_image.h
stb_image.h是一个图片加载库,可以将图片转换成数据信息。
加载图片信息
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);//图片路径,储存图片宽度,储存图片高度,储存图片颜色通道个数。
释放图像数据
stbi_image_free(data);
加载图片时翻转y轴
stbi_set_flip_vertically_on_load(true);
10.GLM
GLM是opengl的一个数学运算库。
注意矩阵运算不具有交换性,应该从右往左看。
常用方法:
对矩阵trans添加一个位移能力并生成一个新的矩阵
Trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));//变换矩阵,位移向量
对矩阵trans添加一个绕vec3(0.0, 0.0, 1.0)旋转radians(90.0f)能力并生成一个新的矩阵
Trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));//变换矩阵,旋转弧度,旋转轴
对矩阵trans添加一个缩放功能
Trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));//变换矩阵,缩放向量
11.图形学一些术语
局部空间:物体的起始坐标空间。在通过model矩阵作用后变换为世界坐标空间。
世界空间:该空间是整个游戏世界内的空间,各个物体会以同一个世界坐标原点摆放。在通过view矩阵作用之后变换为观察空间。
观察空间:该空间是以观察者(摄像机)为坐标原点。在通过projection矩阵作用之后变换为裁剪空间。
裁剪空间:该空间会把特定范围外的画面剔除,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。在通过Viewport Transform(视口变换)后变换到最终的屏幕空间。
屏幕空间:将裁剪空间坐标投影到屏幕空间上,对应为用户的窗口屏幕。最后再将坐标送到光栅器。
模型矩阵(model):模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子,你需要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵;我们将箱子的局部坐标变换到场景/世界中的不同位置。
观察矩阵(view):将世界坐标转化到观察者视野中的矩阵,被用来将世界坐标变换到观察空间。
投影矩阵(projection):将顶点坐标从观察变换到裁剪空间。指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。
透视除法(Perspective Division):在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。
平截头体:由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。
正射投影:正射投影定义了两个相同大小的平面板,两个平面板与他们包裹的空间构成一个长方体,两个平面板内的物体会被相同大小比例映射到屏幕,会产生不真实的效果,忽略了透视效果。
创建一个正射投影矩阵
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);//平面板左边界,后平面板右边界,平面板下边界,平面板上边界,前平面板位置,后平面板位置
透视:离自己越远的物体看起来越小,即为透视效果。
透视投影:构建了两个大小不一样的平面板,小的平面板在前(离摄像机更近),大的平面板在后。透视投影矩阵会根据坐标点离近平面板的距离修改w分量(距离越远的w分量越大),最后坐标投影到三维坐标时x,y,z分量都会除以w分量,来实现近大远小的效果。
创建透视投影矩阵
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);//视野角度,展示窗口宽高比,近平面板,远平面板
v(裁剪坐标) = projection * view * model * v(本地坐标)
本地坐标经过模型矩阵,观察矩阵,投影矩阵变换后得到裁剪坐标。
将(模型矩阵)矩阵传入着色器
int modelLoc = glGetUniformLocation(ourShader.ID, "model"));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
深度缓冲:
深度缓冲是用来进行深度测试的缓冲。它存储了当前要展示在屏幕上所有像素点的深度信息。进行深度测试时,将片段的深度值与深度缓冲中对应位置的深度值比较,选择深度值更小(深度大的被挡住)的片段展示,同时更新深度缓冲。
启动深度缓冲
glEnable(GL_DEPTH_TEST);
清除深度缓冲
glClear(GL_DEPTH_BUFFER_BIT);
摄像机
设置一个位置为摄像头位置pos1,pos1减去原点位置pos0即为摄像头方向向量direct。定义一个上向量up1(平行y轴),用up1与direct叉乘得到右向量right。再用direct与right叉乘得到上轴up。direct,righr,up构成以摄像机为原点的坐标空间。
lookAt矩阵:将世界坐标空间物体变换到摄像机坐标空间。(可以视为观察矩阵)
创建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));//摄像机位置,原点(目标)位置,上向量
光照
冯氏光照模型(Phong Lighting Model):该模型光照包括环境光(自然环境中各种的反射光简化为环境光),漫反射光(物体漫反射的光),镜面反射光(物体镜面反射的光)。
漫反射贴图:用于给物体添加漫反射光照效果的贴图,是一种特殊的纹理。需要传入贴图纹理和纹理坐标,并对纹理采样。
镜面光贴图:用于给物体添加镜面反射光照效果的贴图,是一种特殊的纹理。需要传入贴图纹理和纹理坐标,并对纹理采样。
平行光:当点光源从无穷远处射来的强度不会衰减的光,可看做是平行的光(如阳光)。可以直接用光照向量来计算。
点光源:从一个点向周围散发可衰减的光(如灯泡)。需要用点光源位置与照射位置的计算出光照向量,还需要让光照强度随着点光源位置与照射位置之间的距离变化。
光照强度随距离变化的公式,i = 1.0/(c + ldis + qdis^2)
light.constant通常为1,保证分母不小于1,后两项是距离对光照强度的线性和非线性影响的组合。
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
聚光:朝某个范围发散可衰减的光(如手电筒)。初步,需要设置光线的角度(切光角)控制光照范围,我们比较(聚光到需要照射点的向量与聚光方向构成的夹角@)与切光角b的大小,当@小于b时,该照射点在聚光照射范围内,反之则不在。更进一步,我们还需要光照方向上的内外圆锥来制造一个平滑的边缘效果(光线强度在从内圆锥边缘开始减弱,直至外圆锥边缘光线强度为0)。设内圆锥光切角a,(通过余弦值计算角度开销很大,通常会用他们的余弦值比较)
内圆锥向外圆锥衰减公式 i = (cos@ - cosb)/(cosa - cosb)
float theta = dot(lightDir, normalize(-light.direction)); //光线照射方向向量
float epsilon = (light.cutOff - light.outerCutOff);//内外圆锥光切角余弦值差值
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);//光照强度随角度变化公式,clamp限制了结果在0-1之间
切光角:聚光的方向与聚光照射边缘的夹角。
渲染缓冲区:存储渲染结果的地方。
帧缓冲区:最终显示在屏幕上的区域。通过指定渲染通道,渲染结果可以从渲染缓冲区输出到帧缓冲区,从而实现与帧缓冲区的通信。
渲染通道:渲染通道是一个特殊的附件,用于指定渲染缓冲区的位置和大小。文章来源:https://uudwc.com/A/xWePk
参考
LearnOpenGL CN文章来源地址https://uudwc.com/A/xWePk