在之前的章節學習的光源都是一個點,雖然效果不錯,和現實世界的光源還有一定的差距。將光投射(Cast)到物體的光源叫做投光物(Light Caster),這裏我們學習幾種比較常見的光源:定向光(Directional Light),點光源(Point Light)還有聚光(Spotlight)。
定向光
當光源非常遠時,來自光源的每條光線就會近似於互相平行,而且光源強度相同。當我們使用一個假設光源處於無限遠處的模型時,它就被稱爲定向光。定向光非常好的一個例子就是太陽。太陽距離我們並不是無限遠,但它已經遠到在光照計算中可以把它視爲無限遠了。所以來自太陽的所有光線將被模擬爲平行光線,我們可以在下圖看到:
因爲所有的光線都是平行的,所以物體與光源的相對位置是不重要的,因爲對場景中每一個物體光的方向都是一致的。由於光的位置向量保持一致,場景中每個物體的光照計算將會是類似的。
我們可以定義一個光線方向向量而不是位置向量來模擬一個定向光。着色器的計算基本保持不變,但這次我們將直接使用光的direction向量而不是通過position來計算lightDir向量。我們修改着色器代碼,
struct Light {
// vec3 position; // 使用定向光就不再需要位置了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-(aLightMatrix *light.direction));
...
}
首先我們對光的向量在View空間計算,得到最終的lightDir向量,隨後再用它來計算環境光和鏡面光。這裏我們創建多個箱子並對它們進行不同的縮放和位移,來展示光照的效果。直接來看效果圖
點光源
在前面的章節中,我們用的光源都是點光源,但是我們看到被光照的地方亮度都是相同的,而實際上光是會隨着距離變大而變弱的,而且超過最大距離後就沒有光照效果了,這通常叫做衰減。我們需要一個公式來減少光的強度,不用擔心已經有前輩解決了這個問題,
在這裏d代表了片段距光源的距離。接下來爲了計算衰減值,我們定義3個(可配置的)項:常數項、一次項和二次項。
- 常數項通常保持爲1.0,它的主要作用是保證分母永遠不會比1小,否則的話在某些距離上它反而會增加強度,這肯定不是我們想要的效果。
- 一次項會與距離值相乘,以線性的方式減少強度。
- 二次項會與距離的平方相乘,讓光源以二次遞減的方式減少強度。二次項在距離比較小的時候影響會比一次項小很多,但當距離值比較大的時候它就會比一次項更大了。
由於二次項的存在,光線會在大部分時候以線性的方式衰退,直到距離變得足夠大,讓二次項超過一次項,光的強度會以更快的速度下降。這樣的結果就是,光在近距離時亮度很高,但隨着距離變遠亮度迅速降低,最後會以更慢的速度減少亮度。下面這張圖顯示了在100的距離內衰減的效果:
怎麼選擇合適的值呢,可以參考文檔http://wiki.ogre3d.org/tiki-index.php?page=-Point+Light+Attenuation,我們選擇32到100距離之內的值都可以,這裏我們選擇50距離的值:1.0, 0.09, 0.032,我們給着色器代碼的光源結構體添加這三個常數項參數,並計算衰減,計算衰減後再與三個光照效果相乘,最後得到結果,關鍵代碼如下,
……
struct Light {
……
float constant;
float linear;
float quadratic;
};
……
void main() {
……
// 衰減
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
// 結果
vec3 result = ambient + diffuse + specular;
gl_FragColor = vec4(result, 1.0);
}
我們查看效果圖,可以看到離光源遠的地方光照效果更弱
聚光
聚光也很常見,例如手電筒,有燈罩的路燈等等,這種光之後朝一個特定方向照射,在光照範圍外就沒有光照效果。先看下圖聚光燈的工作,
我們先要計算得到頂點上與光源的向量:LightDir,然後 計算其與光源方向(SoptDir)的夾角θ,如果這個角度大於聚光的角度ϕ則該頂點可以被照射到,反之則不可以。所以我們要定義這三個參數,光源位置,光源照射方向,還有光源的照射範圍
// 定義光源結構體
struct Light {
vec3 position;
vec3 direction;
float cutOff;
};
其中cutOff爲切光角,是用角度值計算得到的餘弦值,我們需要計算LightDir和SpotDir向量的點積,然後和cutOff對比。主要代碼如下
……
void main() {
vec3 lightDir = normalize(light.position - fragPos);
// 檢查當前點與光源的連線是否在聚光燈內
float theta = dot(lightDir, normalize(-(aLightMatrix * light.direction)));
if(theta > light.cutOff){
// 被照射到
……
} else {
// 沒有被照射到
gl_FragColor = vec4(light.ambient * texture2D(material.diffuse, TextCoord).rgb, 1.0);
}
}
我們傳入各項值後查看效果,
我們看到聚光燈內的光照比較亮,所以我們需要一個逐漸變化的效果。我們定義一個內圓錐和一個外圓錐:大於外圓錐不顯示光照效果;小於內圓錐顯示最大亮度;而在外圓錐和內圓錐內的區域光照效果逐漸變化。具體公式如下,
這裏ϵ(Epsilon)是內(ϕ)和外圓錐(γ)之間的餘弦值差(ϵ=ϕ−γ)。最終的I值就是在當前片段聚光的強度。值若小於0時置爲0,這時我們就不用再使用if else了,主要代碼如下,
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
……
diffuse *= intensity;
specular *= intensity;
……
此時我們看到聚光的效果就比較平滑了
此章對應文檔https://learnopengl-cn.github.io/02%20Lighting/05%20Light%20casters/,可以查看文檔更加詳細的瞭解。