在学习光照之前,我们先要学习一下基础知识。
示例代码在本系列文章中第一篇里的github仓库。
颜色
在现实世界中,我们看到一个物体的颜色,是由于它能反射这种颜色的光,比如我们看见一个红色的苹果,是因为光照经过苹果表面时,苹果表面吸收了除了红色之外的光,而红光反射后进入人眼,就能看见红色。
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
- 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
环境光照
下面我们来模拟一下环境光照,环境光照非常简单,把环境光照加入场景很简单,就是一个光照强度常量因子乘以光的颜色,得到的结果再乘以物体的颜色,就是片段的颜色了。
下面我们接着用上一篇文章中的代码来实现为立方体加入环境光照。这里我们在片段着色器中加入一个LightColor 三分量,这里表示颜色的光,我们传入白色的环境光,就是(1.0,1.0,1.0);
#version 300 es
precision mediump float;
uniform vec3 lightColor;
in vec3 fColor;
out vec4 gColor;
void main() {
//计算环境光照
float ambientStrength = 0.2;
vec3 ambient = ambientStrength * lightColor;
//最终颜色等于环境光照乘以物体颜色
vec3 result = ambient * fColor;
gColor = vec4(result,1.0);
}
环境光照强度0.2 效果图:
环境光照强度0.5效果图:
环境光照强度1.0效果图:
从上面几种效果可以大致感受到环境光照强度对物体表面最终成色的影响。可以体会到,当环境光照强度越小的情况,物体表面最终成色越趋近于黑色,这个是比较符合现实
漫反射
环境光照本身不能提供最有趣的结果,但是漫反射光照就能开始对物体产生显著的视觉影响了,对于漫反射我们可以这样理解:漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图:
图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化。当θ接近于90度时,这束光对物体的影响最小。我们用法向量和代表光照的向量点乘的结果表示这个影响的值,正正好负责上述的结论:当光纤垂直于物体表面时,对物体的影响最大。
所以我们如果想要计算漫反射光照,需要两个条件:1.垂直于顶点的法向量(单位向量)2.定向的光线:光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
注意: 这里片段的位置还必须是世界坐标中的位置!
下面我们来计算一下漫反射
先看顶点着色器:
#version 300 es
precision mediump float;
in vec4 vPosition;
//垂直于顶点的法向量
in vec3 N;
out vec3 fColor;
out vec3 Normal;
//片段的位置,转化为世界坐标系下的
out vec3 fragPos;
uniform mat4 modelTransform;
uniform mat4 viewTransform;
uniform mat4 projectTransform;
//法线变换矩阵,把法线向量转化到世界空间中,需要运用到法线变换矩阵
uniform mat4 invertTransposeMatrix;
void main(){
mat4 mvpTransform = projectTransform * viewTransform * modelTransform;
gl_Position = mvpTransform * vPosition;
//将顶点位置转换为世界空间坐标
fragPos = (modelTransform * vPosition).xyz;
//法线向量转换为世界空间坐标
Normal = (invertTransposeMatrix * vec4(N,0.0)).xyz;
}
这里需要注意的有三点:
1.我们取的顶点位置需要转换到世界空间坐标下。
2.法线向量的转换:这里需要用法线矩阵来乘以法线向量,将法线向量也转换到世界空间坐标下。法线矩阵被定义为「模型矩阵左上角的逆矩阵的转置矩阵」。至于法线矩阵为什么这样取值,感兴趣的可以去百度一下,这里不做深入探讨了。
3.法线向量我们通过属性Normal传递进来,因为我们这里绘制的是立方体,它每个面的法向量的取值非常简单,比如+X轴的面的法线向量就是(1.0, 0.0, 0.0)。
代码中是这样获取逆转置矩阵:
//获取法线变换矩阵
BOOL canConvert = YES;
GLKMatrix4 invertTransposeMatrix = GLKMatrix4InvertAndTranspose(modelMatrix, &canConvert);
//如果能获取到法线矩阵,就加载
if (canConvert) {
glUniformMatrix4fv(invertTransposeIndex, 1, GL_FALSE, invertTransposeMatrix.m);
}
GLKit提供了很多矩阵操作的API,可以提高开发效率。
接下来看一下顶点着色器有哪些改变:
#version 300 es
precision mediump float;
uniform vec3 lightColor;
//光源位置
uniform vec3 lightPos;
in vec3 Normal;
in vec3 fragPos;
out vec4 gColor;
void main() {
//计算环境光照
float ambientStrength = 0.2;
vec3 ambient = ambientStrength * lightColor;
//计算漫反射
//计算光线位置
vec3 norm = normalize(Normal);
vec3 lightDirection = normalize(lightPos - fragPos);
float diffu = dot(norm,lightDirection);
vec3 diffuse = diffu * lightColor;
//最终颜色(这里指定物体颜色为红色)
vec3 result = (ambient + diffuse) * vec3(1.0,0.0,0.0);
gColor = vec4(result,1.0);
}
这里需要注意:
我们传入的光源位置也是基于世界空间坐标系,光源位置减去顶点位置就是光线的方向向量,然后我们对法线向量和光线方向向量取单位向量,因为我们当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误。
最后我们看一下加入了环境光照和漫反射的效果:
这里是不是看起来更像一个立方体了,不要着急,后面还有一个很关键的因素:镜面反射:
镜面反射
在生活中你也许会有过这种经历,窗外的阳光照射到镜子的时候,你挪动位置看向镜子,会发现在一个位置,光线最为刺眼,镜面反射就是模拟这种效果,如上图所示。
光线照在物体表面,反射后形成反射光线(我们计为R),人看向物体表面被照射的点形成一个观察向量,当这两条向量重合的时候(θ = 0°时),人眼看到的光线强度最大,当两条向量夹角θ 越大时,看到的光线强度最小,我们就需要计算这个镜面分量。
下面我们看一下顶点着色器的变化:
version 300 es
precision mediump float;
uniform vec3 lightColor;
//光源位置
uniform vec3 lightPos;
//观察点的位置
uniform vec3 viewPos;
in vec3 Normal;
in vec3 fragPos;
out vec4 gColor;
void main() {
//计算环境光照
float ambientStrength = 0.3;
vec3 ambient = ambientStrength * lightColor;
//计算漫反射
//计算光线位置
vec3 norm = normalize(Normal);
vec3 lightDirection = normalize(lightPos - fragPos);
float diffu = dot(norm,lightDirection);
vec3 diffuse = diffu * lightColor;
//计算镜面光照
//定义一个镜面强度
float specularStrength = 0.5;
//观察向量
vec3 viewDirection = normalize(viewPos - fragPos);
//反射向量
vec3 reflectDirection = reflect(-lightDirection,norm);
float spec = pow(max(dot(viewDirection,reflectDirection), 0.0) , 32.0);
vec3 specular = specularStrength * spec * lightColor;
//最终颜色(这里指定物体颜色为红色)
vec3 result = (ambient + diffuse + specular) * vec3(1.0,0.0,0.0);
gColor = vec4(result,1.0);
}
这里我们要注意以下几点:
1.计算反射向量的时候,我们第一个传入的光线向量取值是反的,因为这里lightDirection是顶点到光源的向量,而reflect函数要求第一个传入的参数是光源到顶点的向量,所以要取反。
2.计算镜面分量:float spec = pow(max(dot(viewDirection,reflectDirection), 0.0) , 32.0);
这里我们取观察向量和反射向量的点乘结果,只取正值,这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。改变这个反光度,你会看到不同反光度的视觉效果影响。
在加入镜面光照后,我们改变了光源的位置,这样是为了使光线向量和观察向量
的夹角处于一个合适的范围,以便于我们观察到添加镜面光照后效果。
//设置环境光颜色为白色
static float light[3] = {1.0f, 1.0f, 1.0f};
//设置光源位置(世界空间坐标系)
static float lightPos[3] = {0.0,1.5,-1.5};
//这里设置观察点为摄像机的位置(cameraMatrix里eye的位置)
static float viewPos[3] = {0.0,0.0,2.0};
最终效果如下图: