學習OpenGL ES for Android(十三)— 投光物

在之前的章節學習的光源都是一個點,雖然效果不錯,和現實世界的光源還有一定的差距。將光投射(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向量,隨後再用它來計算環境光和鏡面光。這裏我們創建多個箱子並對它們進行不同的縮放和位移,來展示光照的效果。直接來看效果圖

源碼地址https://github.com/jklwan/OpenGLProject/blob/master/sample/src/main/java/com/chends/opengl/renderer/light/LightCastersDirectionalRenderer.java

點光源

在前面的章節中,我們用的光源都是點光源,但是我們看到被光照的地方亮度都是相同的,而實際上光是會隨着距離變大而變弱的,而且超過最大距離後就沒有光照效果了,這通常叫做衰減。我們需要一個公式來減少光的強度,不用擔心已經有前輩解決了這個問題,

 

在這裏d代表了片段距光源的距離。接下來爲了計算衰減值,我們定義3個(可配置的)項:常數項Kc_、一次項K_l和二次項Kq_

  • 常數項通常保持爲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);
}

我們查看效果圖,可以看到離光源遠的地方光照效果更弱

源碼地址https://github.com/jklwan/OpenGLProject/blob/master/sample/src/main/java/com/chends/opengl/renderer/light/LightCastersPointRenderer.java

聚光

聚光也很常見,例如手電筒,有燈罩的路燈等等,這種光之後朝一個特定方向照射,在光照範圍外就沒有光照效果。先看下圖聚光燈的工作,

我們先要計算得到頂點上與光源的向量: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://github.com/jklwan/OpenGLProject/blob/master/sample/src/main/java/com/chends/opengl/renderer/light/LightCastersSpotLightRenderer.java

此章對應文檔https://learnopengl-cn.github.io/02%20Lighting/05%20Light%20casters/,可以查看文檔更加詳細的瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章