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光照模型弄混)。