Phong光照模型
现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的,所以我们使用数学和物理工具来简化和描述现实世界的光照,使得最终呈现的效果尽可能的接近现实。
其中,我们最常用的一种称为Phong(冯氏)光照模型。
Phong光照模型使用环境光(ambient),漫反射光(diffuse),高光反射(specular)三个分量来描述一个被光照射的物体的光照颜色。
- 环境光 (ambient) : 我们现实生活中的某个环境中可能有多个光源,经由一系列其他物体的反射等形成了某种颜色。我们可以通过全局光照逐一计算各个光源、各种反射等,但是那样太消耗性能,于是给定某种颜色常量作为环境光来模拟上述情况。一般来说,环境光都是比较暗的。
- 漫反射光(difffuse): 可以理解成由于物体表面比较粗糙,光在各个方向反射的现象。一般来说,某一部分越正对着光就越亮。
- 高光反射(specular) : 就是镜面反射,是模拟非常光滑的物体对光的反射。
所以,最终的光照的输出颜色是:
color ambient = calcAmbient()
color diffuse = calcAmbient()
color specular = calcSpecular()
color result = ambient + diffuse + specular
(ambient是常数)下面介绍不同颜色分类的计算方法。
Lambert和HalfLambert计算漫反射光照
Lambert是一个模拟现实光照的经验模型,即漫反射光的光强仅与入射光的方向和反射点处表面法向夹角的余弦成正比。
余弦我们可以很方便的用Dot来计算获得,显然背对我们的光线的地方是得不到光照的,所以要确保Dot的结果大于0.
float diffuseFactor = max(, dot(normal,lightDir));
color result = lightColor * diffuseFactor;
Lambert的计算效果有点瑕疵,那是因为在所有背对着光源的部分我们最后得到的结果都是0,那么会让背对光源的地方看起来有点像一个平面,所以有一个改进的HalfLambert模型。
HalfLambert改进了公式:
float diffuseFactor = A * dot(normal,lightDir) + B;
color = lightColor * diffuseFactor;
其中A和B是两个系数,我们一般都取A = B = 0.5,通过这种方式,我们把dot的结果从[-1, 1]映射到了[0, 1],而且背对着光源的部分也不全都是0,是一些位于[0, 0.5]的值,有了明暗变化,不再是像一个平面。
Phong和BlinnPhong计算高光反射
根据实际生活可知,高光反射(镜面反射)会因为观察者的观察角度不同略有区别:
Phong就根据上图构建了高光反射的计算模型:
float specularFactor = pow(max(dot(viewDir,reflectDir),),shininess);
color result = lightColor * specularFactor;
I是光的入射向量,R是光的反射向量,V是观察者的视线向量(一般是摄像机的视线向量),α是反射向量和视线向量的夹角。
很显然,这个夹角越大我们能看到的东西越小。
在计算specularFactor的算式中,shininess代表了光泽度。
显然,这里有个难点是如何计算出反射光向量。
做一下辅助线我们可以很方便的求出:
显然:
() I + R = * P
=> R = * P - I
() I + S = P
() S是-I在Normal方向上的投影
=> S = Dot(-I,N) / Length(N) * normalize(N)
为了简化计算,我们把S和I都标准化,那么
S = Dot(-I,N) * N = -Dot(I,N) * N
() 根据()()(),得 R = * (I + S) - I = * (I - Dot(I,N) * N) - I = I - Dot(I,N) * N
所以,Phong高光计算可表示为:
normal = normlize(normal);
lightDir = normlize(lightDir);
vec3 reflectDir = lightDir - Dot(lightDir,normal) * normal;
float specularFactor = pow(max(dot(viewDir,reflectDir),),shininess);
color result = lightColor * specularFactor;
由于这种方法需要计算反射向量,所以性能开销不算小,改进的BlinnPhong模型不再使用反射向量,而是一引入了一个新的向量h,最终计算的效果和Phong差不多,且有更小的开销。
vec3 half = normalize(viewDir + lightDir);
float specularFactor = pow(max(dot(viewDir,half),),shininess);
color result = lightColor * specularFactor;
光源类型
在上述的讨论中,我们只是很抽象的用lightColor和lightDir来描述了一个光源,实际上显示生活中有多种光源。
像太阳(平行光),点灯泡(点光源),手电筒(聚光),我们也有特定的模型来描述这些光源。
(Directional Light)平行光
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
定向光非常好的一个例子就是太阳。太阳距离我们并不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线。
struct DirectLight {
vec3 direction;//光照方向,就是上述的lightDir,定值
color ambient;//环境光分量
color diffuse;//漫反射光lightColor
color specular;//镜面反射光lightColor
};
(Point Light)点光源
定向光对于照亮整个场景的全局光源是很不错的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源。
和平行光不同的是,我们不直接给定它的光照方向,而是给出点光源的位置,然后根据物体的位置来计算光照向量。同时,我们需要给出一个衰减公式,来模拟随着距离的变化光照的衰减。
Fatt=1.0Kc+Kl∗d+Kq∗d2
其中 KC 是常数衰减因子, Kl 是线性衰减因子, Kq 是二次项衰减因子。
struct PointLight {
vec3 pos;//点光源的坐标,一般是世界坐标
float constant;
float linera;
float quadratic;
color ambient;//环境光分量
color diffuse;//漫反射光lightColor
color specular;//镜面反射光lightColor
};
所以,我们的计算方式为:
color CalcPointLight(PointLight light, vec3 normal, vec3 objPos, vec3 viewDir)
{
vec3 lightDir = normalize(objPos - light.pos);//光是入射向量
float diff = 0.5 * dot(normal, lightDir) + ;
// specular shading
vec3 half = normalize(viewDir + lightDir);
float spec = pow(max(dot(viewDir, half), ), shininess);
// attenuation
float distance = length(lightDir);//物体和发光位置的距离
float attenuation = / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// combine results
color ambient = light.ambient;
color diffuse = light.diffuse * diff;
color specular = light.specular * spec;
//衰减
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
(Spot Light)聚光
我们要讨论的最后一种类型的光是聚光(Spotlight)。
聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
- lightDir : 光照的入射向量
- spotDir : 聚光灯指示的方向
- ϕ : 指定了聚光半径的角。落在这个角度之外的物体都不会被这个聚光所照亮。
- θ : lightDir向量和spotDir向量之间的夹角,如果在聚光灯范围内那么 θ < ϕ
struct SpotLight{
vec3 pos;//聚光灯的位置
vec3 spotDir;//聚光灯的指示方向
flaot phi;
//聚光灯也要考虑衰减
float constant;
float linera;
float quadratic;
color ambient;//环境光分量
color diffuse;//漫反射光lightColor
color specular;//镜面反射光lightColor
}
color CalcSpotLight(SpotLight light, vec3 normal, vec3 objPos, vec3 viewDir)
{
vec3 lightDir = normalize(objPos - light.pos);
float theta = dot(lightDir, normalize(light.spotDir));
if(theta >= light.phi){
//ambient
color ambient = light.ambient;
//diffuse
float diff = 0.5 * dot(normal, lightDir) + ;
color diffuse = light.diffuse * diff;
//specular shading
vec3 half = normalize(viewDir + lightDir);
float spec = pow(max(dot(viewDir, half), ), shininess);
color specular = light.specular * spec;
//衰减
float distance = length(lightDir);//物体和发光位置的距离
float attenuation = / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return ambient + diffuse + specular;
}
else{
// 光照范围之外,仅显示环境光
return light.ambient;
}
}
这样的计算方式最终得出的结果实际上也是有瑕疵的,你可以很清楚的看到明显的边界。
我们想让它的边界部分柔和一点,所以模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。
我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,在内圆锥到外圆锥的这个范围内对光照进行衰减。
在内圆锥的边界光照强度是1,外圆锥的光照强度是0,在这两个圆锥之间的光照强度,我们通过插值来确定。
我们可以用下述公式进行计算:
Intensity=θ−innerouter−inner
(因为这里的inner和outer都是角度,所以这里其实是一个取巧的方法,实际上应该利用三角插值)
struct SpotLight{
vec3 pos;//聚光灯的位置
vec3 spotDir;//聚光灯的指示方向
float inner;//内圆锥的角度
float outer;//外圆锥的角度
//聚光灯也要考虑衰减
float constant;
float linera;
float quadratic;
color ambient;//环境光分量
color diffuse;//漫反射光lightColor
color specular;//镜面反射光lightColor
}
color CalcSpotLight(SpotLight light, vec3 normal, vec3 objPos, vec3 viewDir)
{
vec3 lightDir = normalize(objPos - light.pos);
float theta = dot(lightDir, normalize(light.spotDir));
float intensity = theta - light.inner / light.outer - light.inner;
if(theta >= light.inner){
//ambient
color ambient = light.ambient;
//diffuse
float diff = 0.5 * dot(normal, lightDir) + ;
color diffuse = light.diffuse * diff;
diffuse *= intensity;
//specular shading
vec3 half = normalize(viewDir + lightDir);
float spec = pow(max(dot(viewDir, half), ), shininess);
color specular = light.specular * spec;
specular *= intensity;
//衰减
float distance = length(lightDir);//物体和发光位置的距离
float attenuation = / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return ambient + diffuse + specular;
}
else{
// 光照范围之外,仅显示环境光
return light.ambient;
}
}
由于if-else可能会引起流水线断流,所以可以对intensity进行clamp,去掉if-else
color CalcSpotLight(SpotLight light, vec3 normal, vec3 objPos, vec3 viewDir)
{
vec3 lightDir = normalize(objPos - light.pos);
float theta = dot(lightDir, normalize(light.spotDir));
float intensity = theta - light.inner / light.outer - light.inner;
intensity = clamp(intensity, , );
//ambient
color ambient = light.ambient;
//diffuse
float diff = 0.5 * dot(normal, lightDir) + ;
color diffuse = light.diffuse * diff;
diffuse *= intensity;
//specular shading
vec3 half = normalize(viewDir + lightDir);
float spec = pow(max(dot(viewDir, half), ), shininess);
color specular = light.specular * spec;
specular *= intensity;
//衰减
float distance = length(lightDir);//物体和发光位置的距离
float attenuation = / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return ambient + diffuse + specular;
}
光照计算阶段
计算顶点光照然后通过插值确定像素光照的算法称为Gouraud Shading(高德洛着色)
直接在FragmentShader中计算光照的算法称为Phong Shading(冯氏着色,注意不要和Phong光照模型弄混)。