OPENGL学习笔记之十一

#OPENGL学习笔记之十一
2019/3/4

阅读材料来自learnopengl.com以及learnopengl-cn.github.io

现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。
我们使用OpenGL构造的一个模型被称为冯氏光照模型(Phong Lighting Model),利于入门。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:
冯氏光照模型

  • 环境光照(Ambient Lighting):因为物体几乎永远不会是完全黑暗的,为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。

环境光的模拟

很复杂,光源很多,还能在物体表面上反射对其他物体造成间接影响,考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。
我们采用一个简单的全局照明模型,即
环境光照
模型,
在片段着色器里(因为涉及到面片上的颜色)glsl里这么写:

void main()
{
    float ambientStrength = 0.1;//环境强度因子
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}	

漫反射的模拟

漫反射
从图上我们能看到一个垂直物体表面的法向量,一个源自光照的光线方向向量。这两个向量的夹角很容易算出来,同时我们利用这两个向量的点乘结果(即夹角余弦值)作为一个计算光照的因子。

注意,为了(只)得到两个向量夹角的余弦值,我们使用的是单位向量(长度为1的向量),所以我们需要确保所有的向量都是标准化的,否则点乘返回的就不仅仅是余弦值了。

为了计算漫反射,我们需要获得法向量和光线方向向量,注意:
法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。
某一面片的法向量我们需要对同一平面的顶点进行一系列几何运算(列如叉乘)才能获得。
某一顶点(多个面片的交点)的法向量我们需要对该点周围的面片的法向量求平均值而获得。
如果法向量数据不多可以作为数据直接传入,不需要计算。

如果传值传入法向量,因为增加了数据,所以顶点glsl和顶点属性指针需要重新设置

//顶点着色器中
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

//主程序中
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);//步长设置为顶点数据(3)+法向量(3)=6
glEnableVertexAttribArray(0);
...

我们的简单程序所有的光照都是在片段着色器里进行,所以我们需要将法向量由顶点着色器传递到片段着色器。我们这么做:

//顶点着色器中
out vec3 Normal;
void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    Normal = aNormal;
}

//片段着色器中增加
in vec3 Normal;

关于计算漫反射

//片段着色器中
uniform vec3 lightPos;//光源的位置是一个静态变量
//主程序中
lightingShader.setVec3("lightPos", lightPos);

最后,我们还需要片段的位置。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标

//顶点着色器中
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;
}
//片段着色器中
in vec3 FragPos;

现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了。
我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光源位置向量与片段位置向量之间的向量差。我们还需要把法线和最终的方向向量都进行标准化:

//片段着色器中
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);

下一步,我们对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量。两个向量之间的角度越大,漫反射分量就会越小:

float diff = max(dot(norm, lightDir), 0.0);//max函数返回两个参数之间较大的参数,从而保证漫反射分量不会变成负数
vec3 diffuse = diff * lightColor;

vec3 result = (ambient + diffuse) * objectColor;//相加乘以物体颜色
FragColor = vec4(result, 1.0);

可以在这里对比一下完整的源代码

还没结束!

首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分,只选用模型矩阵左上角3×3的矩阵(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;这同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。

其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:
不等比缩放
每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。如果你想知道这个矩阵是如何计算出来的,建议去阅读这个文章。

法线矩阵被定义为「模型矩阵左上角的逆矩阵的转置矩阵」。注意,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵(Model-view Matrix)上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间),我们只使用模型矩阵。在顶点着色器中,我们可以使用inverse和transpose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。注意我们还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以vec3的法向量。

//顶点着色器中
Normal = mat3(transpose(inverse(model))) * aNormal;

即使是对于着色器来说,逆矩阵也是一个开销比较大的运算,因此,只要可能就应该避免在着色器中进行逆矩阵运算,它们必须为你场景中的每个顶点都进行这样的处理。用作学习目这样做是可以的,但是对于一个对效率有要求的应用来说,在绘制之前你最好用CPU计算出法线矩阵,然后通过uniform把值传递给着色器(像模型矩阵一样)。

镜面光照的模拟

我们把镜面高光(Specular Highlight)加进来
镜面光照
镜面光照需要依据光的方向向量和物体的法向量来决定,同时它也依赖于观察方向
我们通过反射法向量周围光的方向来计算反射向量(R向量)。然后我们计算反射向量和视线方向的角度差(θ角),如果夹角越小,那么镜面光的影响就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。
观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。之后,我们计算镜面光强度,用它乘以光源的颜色,再将它加上环境光和漫反射分量。

我们选择在世界空间进行光照计算,但是大多数人趋向于在观察空间进行光照计算。在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置。可是我发现在学习的时候在世界空间中计算光照更符合直觉。如果你仍然希望在观察空间计算光照的话,你需要将所有相关的向量都用观察矩阵进行变换(记得也要改变法线矩阵)。

//主程序中
uniform vec3 viewPos;
···
lightingShader.setVec3("viewPos", camera.Position);//摄像机当然就是观察者

//片段着色器中
float specularStrength = 0.5;//镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);//lightDir向量进行了取反。reflect函数要求第一个向量是从光源指向片段位置的向量.

float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;

//最后的计算
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

我们先计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。如下图所示:
在这里插入图片描述

在光照着色器的早期,开发者曾经在顶点着色器中实现冯氏光照模型。在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。然而,顶点着色器中的最终颜色值是仅仅只是那个顶点的颜色值,片段的颜色值是由插值光照颜色所得来的。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。
在这里插入图片描述
在顶点着色器中实现的冯氏光照模型叫做Gouraud着色(Gouraud Shading),而不是冯氏着色(Phong Shading)。记住,由于插值,这种光照看起来有点逊色。冯氏着色能产生更平滑的光照效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值