OPENGL學習筆記之十四

#OPENGL學習筆記之十三
2019/3/12

閱讀材料來自learnopengl.com以及learnopengl-cn.github.io

這節的主要內容是增加光源的屬性及相應的效果變化。
現實世界中有很多種類的光照,我們把將光投射(Cast)到物體的光源叫做投光物(Light Caster)。投光物我們分爲以下三種:

  • 定向光(Directional Light)
  • 點光源(Point Light)
  • 聚光(Spotlight)

馬上我們還會討論如何將這些不同種類的光照類型整合到一個場景之中。

平行光(Directional Light)

平行光
如圖所示,一組平行的光束投射到物體表面。當我們使用一個假設光源處於無限遠處的模型時,它就被稱爲定向光,因爲它的所有光線都有着相同的方向,它與光源的位置是沒有關係的。定向光非常好的一個例子就是太陽,由於光的位置向量保持一致,場景中每個物體的光照計算將會是類似的。
我們在片段着色器中的light結構體基main函數中添加如下代碼:

//片段着色器中
struct Light {
    // vec3 position; // 使用定向光就不再需要了
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
  //我們目前使用的光照計算需求一個從片段至光源的光線方向,但人們更習慣定義定向光爲一個從光源出發的全局方向。
  //所以我們需要對全局光照方向向量取反來改變它的方向,它現在是一個指向光源的方向向量了。
  //而且,記得對向量進行標準化,假設輸入向量爲一個單位向量是很不明智的。
  vec3 lightDir = normalize(-light.direction);//對light.direction向量取反
  ...
}

同時,爲了方便觀察光照的效果,我們增加立方體的個數,在主程序中添加:

//主程序中
for(unsigned int i = 0; i < 10; i++)
{
    glm::mat4 model;
    model = glm::translate(model, cubePositions[i]);
    float angle = 20.0f * i;
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
    lightingShader.setMat4("model", model);

    glDrawArrays(GL_TRIANGLES, 0, 36);
}

//注意提前設置好光的方向,(注意我們將方向定義爲從光源出發的方向)。
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);

定向光的參考代碼在這裏

點光源(Point Light)

點光源
類似於燈泡,與太陽不同的是,點光源是處於世界中某一個位置的光源,它會朝着所有方向發光,但光線會隨着距離逐漸衰減

衰減

隨着光線傳播距離的增長逐漸削減光的強度通常叫做衰減(Attenuation)。隨距離減少光強度的一種方式是使用一個線性方程,雖然線性方程方程能夠隨着距離的增長線性地減少光的強度,從而讓遠處的物體更暗。然而,這樣的線性方程通常會看起來比較假。
爲了逼真的效果,我們需要一個隨着距離的增加光源的亮度一開始會下降非常快,但在遠處時剩餘的光強度就會下降的非常緩慢的規律,有人總結了如下公式。
衰減公式
d代表了片段距光源的距離。接下來爲了計算衰減值,我們定義3個(可配置的)項:常數項Kc、一次項Kl和二次項Kq。

  • 常數項Kc通常保持爲1.0,它的主要作用是保證分母永遠不會比1小
  • 一次項Kl會與距離值相乘,以線性的方式減少強度。
  • 二次項Kq會與距離的平方相乘,讓光源以二次遞減的方式減少強度。二次項在距離比較小的時候影響會比一次項小很多,但當距離值比較大的時候它就會比一次項更大了。

三個數值之間的數學關係及結果我們查閱一下網站,來選擇效果較好的值。

實現衰減

//在片段着色器中我們還需要三個額外的值
struct Light {
    vec3 position;  

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;//常數項
    float linear;//一次項
    float quadratic;//二次項
};

注意我們現在不採用剛剛學的定向光部分的vec3 direction了

//主程序我們還要設置一下屬性值
lightingShader.setFloat("light.constant",  1.0f);
lightingShader.setFloat("light.linear",    0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);

//片段着色器中我們可以使用GLSL內建的length函數來完成計算物距離體光源的距離
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;

我們可以將環境光分量保持不變,讓環境光照不會隨着距離減少,但是如果我們使用多於一個的光源,所有的環境光分量將會開始疊加,所以在這種情況下我們也希望衰減環境光照。簡單實驗一下,看看什麼才能在你的環境中效果最好。

點光源的參考代碼在這裏

聚光(Spotlight)

聚光

  • LightDir:從片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • Phiϕ:指定了聚光半徑的切光角。落在這個角度之外的物體都不會被這個聚光所照亮。
  • Thetaθ:LightDir向量和SpotDir向量之間的夾角。在聚光內部的話θ值應該比ϕ值小。

聚光是位於環境中某個位置的光源,它只朝一個特定方向而不是所有方向照射光線。這樣的結果就是只有在聚光方向的特定半徑內的物體纔會被照亮,其它的物體都會保持黑暗。聚光很好的例子就是路燈或手電筒。
OpenGL中聚光是用一個世界空間位置、一個方向和一個切光角(Cutoff Angle)來表示的,切光角指定了聚光的半徑(譯註:是圓錐的半徑不是距光源距離那個半徑)。對於每個片段,我們會計算片段是否位於聚光的切光方向之間(也就是在錐形內),如果是的話,我們就會相應地照亮片段。

手電筒

//片段着色器中,我們給light結構體添加屬性
struct Light {
    vec3  position;//聚光的位置向量(來計算光的方向向量
    vec3  direction;//聚光的方向向量
    float cutOff;//切光角
    ...
};

//主程序中傳入屬性值到着色器
lightingShader.setVec3("light.position",  camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
//用餘弦值簡化角的計算
lightingShader.setFloat("light.cutOff",   glm::cos(glm::radians(12.5f)));

//片段着色器main函數中
float theta = dot(lightDir, normalize(-light.direction));//取反的是因爲我們想讓向量指向光源而不是從光源出發

if(theta > light.cutOff) //想想爲什麼是大於號不是小於號?(餘弦值有關)
{       
  // 執行光照計算
}
else  // 否則,使用環境光,讓場景在聚光之外時不至於完全黑暗
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

聚光的參考代碼在這裏

平滑/軟化邊緣

上面的結果出來後,我們發現一個問題,聚光的邊緣效果太銳利了,與現實效果不符。
爲了創建一種看起來邊緣平滑的聚光,我們需要模擬聚光有一個內圓錐(Inner Cone)和一個外圓錐(Outer Cone)。我們可以將內圓錐設置爲上一部分中的那個圓錐,但我們也需要一個外圓錐,來讓光從內圓錐逐漸減暗,直到外圓錐的邊界。
爲了創建一個外圓錐,我們只需要再定義一個餘弦值來代表聚光方向向量和外圓錐向量(等於它的半徑)的夾角。然後,如果一個片段處於內外圓錐之間,將會給它計算出一個0.0到1.0之間的強度值。如果片段在內圓錐之內,它的強度就是1.0,如果在外圓錐之外強度值就是0.0。
平滑公式

  • ϵ(Epsilon)是內(ϕ)和外圓錐(γ)之間的餘弦值差(ϵ=ϕ−γ)
  • I值就是在當前片段聚光的強度。

我們現在有了一個在聚光外是負的,在內圓錐內大於1.0的,在邊緣處於兩者之間的強度值了。如果我們正確地約束(Clamp)這個值,在片段着色器中就不再需要if-else了,我們能夠使用計算出來的強度值直接乘以光照分量:

//片段着色器中
float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
//clamp函數,注意它的參數值
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0); 
...
// 將不對環境光做出影響,讓它總是能有一點光
diffuse  *= intensity;
specular *= intensity;
...

//主程序中設置它的uniform值,別忘了在light0結構體中也要定義
lightShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));

平滑邊緣的參考代碼在這裏

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