OpenGL学习笔记(颜色、基础光照)
目录
- 颜色
- 创建一个光照场景
- 基础光照
- 环境光
- 漫反射
- 法向量如何处理?
- 镜面光照
GitHub主页:https://github.com/sdpyy
OpenGL学习仓库:https://github.com/sdpyy1/CppLearn/tree/main/OpenGLtree/main/OpenGL):https://github.com/sdpyy1/CppLearn/tree/main/OpenGL
颜色
颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成,它们通常被缩写为RGB。仅仅用这三个值就可以组合出任意一种颜色。我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的(Reflected)颜色。换句话说,那些不能被物体所吸收(Absorb)的颜色(被拒绝的颜色)就是我们能够感知到的物体的颜色。例如,太阳光能被看见的白光其实是由许多不同的颜色组合而成的(如下图所示)。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有子颜色,不被吸收的蓝色光被反射到我们的眼中,让这个玩具看起来是蓝色的。下图显示的是一个珊瑚红的玩具,它以不同强度反射了多个颜色。
你可以看到,白色的阳光实际上是所有可见颜色的集合,物体吸收了其中的大部分颜色。它仅反射了代表物体颜色的部分,被反射颜色的组合就是我们所感知到的颜色(此例中为珊瑚红)。将光源设置为白色。当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)
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);
创建一个光照场景
这个就需要把入门的东西融会贯通了
首先VBO、VAO可以这样设置。把VBO绑定好之后,分别对cube和光照都绑定VAO,因为现在VBO被绑定,所以两个VAO绑定时都自动挂载了该VBO,也就是说两个物体用的同一套顶点数据,VBO实现了公用
// 创建Object的ID
GLuint VBO;
GL_CALL(glGenVertexArrays(1, &cubeVAO));
GL_CALL(glGenVertexArrays(1,&lightVAO));
GL_CALL(glGenBuffers(1, &VBO));
// VBO写入显存
GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, VBO));
GL_CALL(glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW));
// 绑定VAO到正方形
GL_CALL(glBindVertexArray(cubeVAO));
GL_CALL(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *) 0));
GL_CALL(glEnableVertexAttribArray(0));
// 绑定VAO到灯光
GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, VBO));
GL_CALL(glBindVertexArray(lightVAO));
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
GL_CALL(glEnableVertexAttribArray(0));
// 解绑VAO,在渲染循环中,需要哪一个再绑定哪一个
glBindVertexArray(0);
// VBO已经被设置到了VAO,不需要再绑定VBO了
glBindBuffer(GL_ARRAY_BUFFER, 0);
下来分别需要创建两个shader用于cube和光照方块
shader cubeShader("./shader/shader.vs", "./shader/shader.fs");
shader lightingShader("./shader/shader.vs", "./shader/light_shader.fs");
在画两个方块时对两个shader设置不同的Model变换矩阵,实际上就是设置uniform变量
// 画正方形
cubeShader.use();
glBindVertexArray(cubeVAO);
cubeShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
cubeShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
auto modelTrans = glm::mat4(1.0f);
setMVP(cubeShader,modelTrans);
GL_CALL(glDrawArrays(GL_TRIANGLES, 0, 36));
// 画光源
lightingShader.use();
glBindVertexArray(lightVAO);
auto lightModelTrans = glm::mat4(1.0f);
lightModelTrans = glm::translate(lightModelTrans, lightPos);
lightModelTrans = glm::scale(lightModelTrans, glm::vec3(0.2f));
setMVP(lightingShader,lightModelTrans);
GL_CALL(glDrawArrays(GL_TRIANGLES, 0, 36));
调整摄像机的位置就可以得到下图
基础光照
其中一个模型被称为风氏光照模型(Phong Lighting Model)。风氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:
环境光
光能够在其它的表面上反射,对一个物体产生间接的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。简单做法就是给物体表面最终的效果加上一个很小的常量,这个可以写在片段着色器
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
捎带往后移动一下摄像机,太近了
Camera camera(glm::vec3(0.0f, 0.0f, 5.0f));
写到这里我其实不是很理解为什么颜色要相乘。返回去看上一章得到解释是:把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色),所以ambient相当于对光照颜色的稀释(0.1),之后与物体颜色相乘得到就是物体应该反射的光,展示效果如下
漫反射
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化。需要法向量来计算,在顶点数据中添加法线数据
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
};
相应的VAO和顶点着色器同步修改,并将顶点法线out到片段着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec3 Normal;
void main()
{
gl_Position = projection*view*model*vec4(aPos, 1.0);
Normal = aNormal;
}
另外在片段着色器中需要光照位置信息,用uniform传递即可,最后还需要每个像素对应在空间中的位置,这个直接可以用顶点着色器的顶点数据左乘模型变换,就得到了空间坐标,直接把他传递给片段着色器即可。
这些都做完的效果就是片段着色器拿到了每个像素在空间中的位置以及对应的法线。
法向量点乘光照方向得到两个方向的夹角cos(注意钝角处理和向量单位化)
void main()
{
// 环境光
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm,lightDir),0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse)* objectColor;
FragColor = vec4(result, 1.0);
}
最终效果,可以看出与光源夹角小的地方确实更亮了
法向量如何处理?
如果我们只是简单的进行平移,并不会影响原本的法线,但是旋转和变换会影响。如果一个平面通过Model变换进行了旋转,很显然法线确实就不一样了。另外缩放可以看下边这张图
这种不等比的缩放,会破会法线的。
修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。
法线矩阵被定义为「模型矩阵左上角3x3部分的逆矩阵的转置矩阵」
Normal = mat3(transpose(inverse(model))) * aNormal;
在转移到片段着色器之前,这样处理法线,才能让法线正常使用。
矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。以学习为目的的话这样做还好,但是对于一个高效的应用来说,你最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)
镜面光照
和漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,但是它也决定于观察方向,例如玩家是从什么方向看向这个片段的。镜面光照决定于表面的反射特性。如果我们把物体表面设想为一面镜子,那么镜面光照最强的地方就是我们看到表面上反射光的地方。只有高光反射与观察方向有关
// 高光反射
flaot 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用来定义高光的反光度(Shininess)
到这里我意识到Games101的作业三没有lightColor是因为它默认光源是白色的也就是(1,1,1),这里考虑到了
也可以在顶点着色器中对每个顶点做光照处理,这种处理就叫做Gouraud着色(Gouraud Shading),计算量更少