LearnOpenGL
- OpenGL: 一个定义了函数布局和输出的图形API的正式规范。
- GLAD: 一个拓展加载库,用来为我们加载并设定所有OpenGL函数指针,从而让我们能够使用所有(现代)OpenGL函数。
- 视口(Viewport): 我们需要渲染的窗口。
- 图形管线(Graphics Pipeline): 一个顶点在呈现为像素之前经过的全部过程。
- 着色器(Shader): 一个运行在显卡上的小型程序。很多阶段的图形管道都可以使用自定义的着色器来代替原有的功能。
- 标准化设备坐标(Normalized Device Coordinates, NDC): 顶点在通过在剪裁坐标系中剪裁与透视除法后最终呈现在的坐标系。所有位置在NDC下-1.0到1.0的顶点将不会被丢弃并且可见。
- 顶点缓冲对象(Vertex Buffer Object): 一个调用显存并存储所有顶点数据供显卡使用的缓冲对象。
- 顶点数组对象(Vertex Array Object): 存储缓冲区和顶点属性状态。
- 索引缓冲对象(Element Buffer Object): 一个存储索引供索引化绘制使用的缓冲对象。
- Uniform: 一个特殊类型的GLSL变量。它是全局的(在一个着色器程序中每一个着色器都能够访问uniform变量),并且只需要被设定一次。
- 纹理(Texture): 一种包裹着物体的特殊类型图像,给物体精细的视觉效果。
- 纹理缠绕(Texture Wrapping): 定义了一种当纹理顶点超出范围(0, 1)时指定OpenGL如何采样纹理的模式。
- 纹理过滤(Texture Filtering): 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式。这通常在纹理被放大情况下发生。
- 多级渐远纹理(Mipmaps): 被存储的材质的一些缩小版本,根据距观察者的距离会使用材质的合适大小。
- stb_image.h: 图像加载库。
- 纹理单元(Texture Units): 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染。
- 向量(Vector): 一个定义了在空间中方向和/或位置的数学实体。
- 矩阵(Matrix): 一个矩形阵列的数学表达式。
- GLM: 一个为OpenGL打造的数学库。
- 局部空间(Local Space): 一个物体的初始空间。所有的坐标都是相对于物体的原点的。
- 世界空间(World Space): 所有的坐标都相对于全局原点。
- 观察空间(View Space): 所有的坐标都是从摄像机的视角观察的。
- 裁剪空间(Clip Space): 所有的坐标都是从摄像机视角观察的,但是该空间应用了投影。这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出。OpenGL负责处理剩下的事情(裁剪/透视除法)。
- 屏幕空间(Screen Space): 所有的坐标都由屏幕视角来观察。坐标的范围是从0到屏幕的宽/高。
- LookAt****矩阵: 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。
- 欧拉角(Euler Angles): 被定义为偏航角(Yaw),俯仰角(Pitch),和滚转角(Roll)从而允许我们通过这三个值构造任何3D方向。
你好三角形
图形渲染管线的每个阶段的抽象展示。要注意蓝色部分代表的是我们可以注入自定义的着色器的部分。
着色器练习
\1. 修改顶点着色器让三角形上下颠倒
gl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0);
\2. 使用uniform定义一个水平偏移量,在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧
// In your CPP file:
float offset = 0.5f;
ourShader.setFloat(“xOffset”, offset);
// In your vertex shader:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
uniform float xOffset;
void main()
{
gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0);
ourColor = aColor;
}
\3. 使用out
关键字把顶点位置输出到片段着色器,并将片段的颜色设置为与顶点位置相等(来看看连顶点位置值都在三角形中被插值的结果)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?
// Vertex shader:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
// out vec3 ourColor;
out vec3 ourPosition;
void main()
{
gl_Position = vec4(aPos, 1.0);
// ourColor = aColor;
ourPosition = aPos;
}
// Fragment shader:
#version 330 core
out vec4 FragColor;
// in vec3 ourColor;
in vec3 ourPosition;
void main()
{
FragColor = vec4(ourPosition, 1.0);
}
仔细考虑一下:片段颜色的输出等于三角形的(插值)坐标。 三角形左下角的坐标是什么? 这是(-0.5f,-0.5f,0.0f)。 由于xy值为负,因此将它们限制为0.0f。 这一直发生到三角形的中心,因为从该点开始,值将再次被正插值。 0.0f的值当然是黑色的,这说明了三角形的黑色面。
左下角的坐标为(-0.5,-0.5,0) 而颜色的范围是(0,1)小于0黑色 大于1白色
纹理练习
1、 修改片段着色器,仅让笑脸图案朝另一个方向看,
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;
void main()
{
FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, vec2(1.0 - TexCoord.x, TexCoord.y)), 0.2);
}
2、 尝试用不同的纹理环绕方式,设定一个从0.0f到2.0f范围内的(而不是原来的0.0f到1.0f)纹理坐标。试试看能不能在箱子的角落放置4个笑脸:参考解答,结果。记得一定要试试其它的环绕方式。
改变纹理坐标和环绕方式
3、 使用一个uniform变量作为mix函数的第三个参数来改变两个纹理可见度,使用上和下键来改变箱子或笑脸的可见度:参考解答。
在片段着色器加入一个uniform类型的变量factor 控制两个纹理可见度
在设置一个全局变量factor 键盘控制它的大小 从而改变两个纹理可见度的值
在输入监听GLFWwindow中:
If(glfwGetKey(window,GLFW_KEY_UP) == GLFW_PRESS)
{
Factor +=0.01f;
}
变换
如何把矩阵传递给着色器?
我们在前面简单提到过GLSL里也有一个mat4类型。所以我们将修改顶点着色器让其接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:
//获取uniform变量地址
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, “transform”);
//传递到着色器
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
变换练习
尝试再次调用glDrawElements画出第二个箱子,只使用变换将其摆放在不同的位置。让这个箱子被摆放在窗口的左上角,并且会不断的缩放(而不是旋转)。(sin函数在这里会很有用,不过注意使用sin函数时应用负值会导致物体被翻转):
**这个代码是变换练习3.MP4
// create transformations
glm::mat4 transform = glm::mat4(1.0f); // make sure to initialize matrix to identity matrix first
transform = glm::translate(transform, glm::vec3(0.5f, -0.5f, 0.0f));
transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f,1.0f));
transform = glm::scale(transform, glm::vec3(0.5, 0.5, 0.5));//缩放
// get matrix’s uniform location and set matrix
ourShader.use();
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, “transform”); //获取uniform变量地址
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));//传递到着色器
//ourShader.setFloat(“xOffset”, offset);
glBindVertexArray(VAO); // seeing as we only have a single VAO there’s no need to bind it every time, but we’ll do so to keep things a bit more organized
//glDrawArrays(GL_TRIANGLES, 0, 3);
// glBindVertexArray(0); // no need to unbind it every time
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// second transformation
// ---------------------
transform = glm::mat4(1.0f); //它重置为单位矩阵
transform = glm::translate(transform, glm::vec3(-0.5f, 0.5f, 0.0f));
transform = glm::rotate(transform, (float)glm::cos(glfwGetTime()), glm::vec3(0.0f, 0.0f, 1.0f));
float scaleAmount = sin(glfwGetTime());
transform = glm::scale(transform, glm::vec3(scaleAmount, scaleAmount, scaleAmount));
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, &transform[0][0]); // 这次将矩阵值数组的第一个元素作为它的内存指针值
//现在用新的变换替换了统一矩阵,再画一遍
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
坐标系统
1、局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
2、下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
3、接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
4、坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
5、最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
//创建模型矩阵
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));//通过将顶点坐标乘以这个模型矩阵,我们将该顶点坐标变换到世界坐标。
//创建视图矩阵
glm::mat4 view=glm::mat4(1.0f);;
// 注意,我们将矩阵向我们要进行移动场景的反方向移动。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f)); //将世界坐标变换为观察空间坐标
//创建投影矩阵
glm::mat4 projection= glm::mat4(1.0f);;
projection = glm::perspective(glm::radians(45.0f), float(SCR_WIDTH) / float(SCR_HEIGHT), 0.1f, 100.0f);//将投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
//传递着色器(在循环渲染中)
glUniformMatrix4fv(modelLocation, 1, GL_FALSE, glm::value_ptr(model));//传递到着色器
glUniformMatrix4fv(viewLocation, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, glm::value_ptr(projection));
正方体随着时间旋转
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
这里出现一个问题就是旋转速度特别特别快?
这是因为我们没有在循环渲染里 初始化model矩阵
坐标系统练习
对GLM的projection函数中的FoV和aspect-ratio参数进行实验。看能否搞懂它们是如何影响透视平截头体的
Fov代表视角 视角大看得远 东西小 视角小 看得近 东西大
FOV 45
FOV25
2、将观察矩阵在各个方向上进行位移,来看看场景是如何改变的。注意把观察矩阵当成摄像机对象。
View //将世界坐标变换为观察空间坐标 // 注意,我们将矩阵向我们要进行移动场景的反方向移动。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, 0.0f));
感觉摄像机在箱子的内心 中心
★view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
感觉摄像机在所有箱子的远处 视野比较清楚
view = glm::translate(view, glm::vec3(0.0f, 0.0f, 3.0f));
感觉摄像机贴在了箱子的脸上
控制相机定义全局变量 offsteX offsteY offsteZ
在循环渲染中先初始化一个矩阵
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(model, glm::vec3(offsetX, offsteY, offsteZ));
ourShader.setMat4(“projection”, projection*trans);
然后通过键盘输入控制offset值的大小来控制照相机的位置
3、使用模型矩阵只让是3倍数的箱子旋转(以及第1个箱子),而让剩下的箱子保持静止
摄像机
摄像机位置
获取摄像机位置很简单。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。我们把摄像机位置设置为上一节中的那个相同的位置:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
不要忘记正z轴是从屏幕指向你的,如果我们希望摄像机向后移动,我们就沿着z轴的正方向移动。
摄像机方向
下一个需要的向量是摄像机的方向,这里指的是摄像机指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。还记得如果将两个矢量相减,我们就能得到这两个矢量的差吗?用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于我们知道摄像机指向z轴负方向,但我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序,我们就会获得一个指向摄像机正z轴方向的向量:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f); glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
方向向量(Direction Vector)并不是最好的名字,因为它实际上指向从它到目标向量的相反方向(译注:注意看前面的那个图,蓝色的方向向量大概指向z轴的正方向,与摄像机实际指向的方向是正好相反的)。
摄像机观察物体 以一个圆周运动观察
在循环渲染中的代码:
glm::mat4 view = glm::mat4(1.0f);
float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
view = glm::lookAt(glm::vec3(camX, 1.0f, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
ourShader.setMat4(“view”, view);
光照
基础光照
计算漫反射光照需要什么?
- 法向量:一个垂直于顶点表面的向量。
- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
计算漫反射光照的步骤:
1、 需要光源的位置向量和片段的位置向量。由于光源的位置是一个静态变量,我们可以简单地在片段着色器中把它声明为uniform:uniform vec3 lightPos;
然后在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform。我们使用在前面声明的lightPos向量作为光源位置:
lightingShader.setVec3(“lightPos”, lightPos);
2、 需要片段的位置。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以我们声明一个输出变量,并计算它的世界空间坐标:
out vec3 FragPos; out vec3 Normal;
void main()
{ gl_Position = projection * view * model * vec4(aPos, 1.0); FragPos = vec3(model * vec4(aPos, 1.0));
Normal = aNormal; }
光照贴图
漫反射贴图 + 镜面光贴图
像在[之前](https://learnopengl-cn.github.io/01 Getting started/06 Textures/)教程中详细讨论过的纹理,而这基本就是这样:一个纹理。我们仅仅是对同样的原理使用了不同的名字:其实都是使用一张覆盖物体的图像,让我们能够逐片段索引其独立的颜色值。在光照场景中,它通常叫做一个漫反射贴图(Diffuse Map)(3D艺术家通常都这么叫它),它是一个表现了物体所有的漫反射颜色的纹理图像。
注意我们将在片段着色器中再次需要纹理坐标,所以我们声明一个额外的输入变量。接下来我们只需要从纹理中采样片段的漫反射颜色值即可:
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
不要忘记将环境光得材质颜色设置为漫反射材质颜色同样的值。
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
镜面高光的强度可以通过图像每个像素的亮度来获取。镜面光贴图上的每个像素都可以由一个颜色向量来表示,比如说黑色代表颜色向量vec3(0.0)
,灰色代表颜色向量vec3(0.5)
。在片段着色器中,我们接下来会取样对应的颜色值并将它乘以光源的镜面强度。一个像素越「白」,乘积就会越大,物体的镜面光分量就会越亮。
由于箱子大部分都由木头所组成,而且木头材质应该没有镜面高光,所以漫反射纹理的整个木头部分全部都转换成了黑色。箱子钢制边框的镜面光强度是有细微变化的,钢铁本身会比较容易受到镜面高光的影响,而裂缝则不会。
采样镜面光贴图
镜面光贴图和其它的纹理非常类似,所以代码也和漫反射贴图的代码很类似。记得要保证正确地加载图像并生成一个纹理对象。由于我们正在同一个片段着色器中使用另一个纹理采样器,我们必须要对镜面光贴图使用一个不同的纹理单元(见[纹理](https://learnopengl-cn.github.io/01 Getting started/06 Textures/)),所以我们在渲染之前先把它绑定到合适的纹理单元上:
lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
接下来更新片段着色器的材质属性,让其接受一个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);
通过使用镜面光贴图我们可以可以对物体设置大量的细节,比如物体的哪些部分需要有闪闪发光的属性,我们甚至可以设置它们对应的强度。镜面光贴图能够在漫反射贴图之上给予我们更高一层的控制。
学习使用 工具imgui
首先GitHub下载imgui 之后导入相关的包
生成解决方案会出现 无法解析外部符号_gl3wViewport (库)
Example_glfw_opengll3案例的默认使用#include <GLFW/glfw3.h>
我们默认使用的是#include <glad.h>
在imconfig.h(配置文件)头文件 加入一句声明 #define IMGUI_IMPL_OPENGL_LOADER_GLAD(定义加载器)
上一篇: Compute Shaders
下一篇: LearnOpenGL | GLSL