欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

LearnOpenGL

程序员文章站 2022-07-04 11:07:35
...
  • 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