OpenGL學習: 光照系列3-光源類型和使用多個光源

寫在前面 
上一節光照中使用材質和lighting maps介紹了使用材質屬性和lighting maps使物體的光照效果能反映物體的材料特性,看起來更逼真。在前面的章節中使用的實際上都是一個點光源,本節將學習其他幾種光源類型,以及在場景中使用多個光源

通過本節可以瞭解到

  • 方向光源
  • 點光源
  • 聚光燈光源
  • 使用多個光源

光源類型

在前面章節中,我們通過位置和成分分量大小來指定光源屬性,這個光源實際上是一個點光源。除了點光源外,還包括方向光源,聚光燈光源等其他類型的光源,他們的特點如下圖所示(來自Simple Lighting): 
光源類型 
使用不同的光源主要是爲了模擬現實環境中不同類型的光,使場景光照效果更能滿足需求。每種類型的光源各有其特點,下面予以介紹。

方向光源

方向光源的特點就是光的方向幾乎都平行,只有一個方向,這是爲了模擬光源在無限遠處的情景,例如太陽光。方向光源一般不考慮光的衰減,它與光源具體位置無關,我們只需要爲它指定方向即可。注意一般我們指定方向光源的方向時,習慣從光源指向物體,而在計算光照時,又需要從物體指向光源的方向,因此需要做一個翻轉。指定方向光源的結構體在着色器中定義爲:

// 方向光源屬性結構體
struct DirLightAttr
{
    vec3 direction; // 方向光源
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在計算光照時,我們不必利用物體的位置和光源位置計算光源方向了,

   // 不再需要
   vec3 lightDir = normalize(light.position - FragPos);
  • 1
  • 2

直接使用這個direction即可:

   vec3 lightDir = normalize(-light.direction); // 翻轉方向光源的方向
  • 1

在着色器中計算光照的效果與上一節的光照計算是相同的:

   void main()
{   
    // 環境光成分
    vec3    ambient = light.ambient * vec3(texture(material.diffuseMap, TextCoord));

    // 漫反射光成分 此時需要光線方向爲指向光源
    vec3    lightDir = normalize(-light.direction); // 翻轉方向光源的方向
    vec3    normal = normalize(FragNormal);
    float   diffFactor = max(dot(lightDir, normal), 0.0);
    vec3    diffuse = diffFactor * light.diffuse * vec3(texture(material.diffuseMap, TextCoord));

    // 鏡面反射成分 此時需要光線方向爲由光源指出
    float   specularStrength = 0.5f;
    vec3    reflectDir = normalize(reflect(-lightDir, normal));
    vec3    viewDir = normalize(viewPos - FragPos);
    float   specFactor = pow(max(dot(reflectDir, viewDir), 0.0), material.shininess);
    vec3    specular = specFactor * light.specular * vec3(texture(material.specularMap, TextCoord));

    vec3    result = ambient + diffuse + specular;
    color   = vec4(result , 1.0f);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

另外還需要在主程序中指定光源的方向如下:

   GLint lightDirLoc = glGetUniformLocation(shader.programId, "light.direction");
   glm::vec3 lampDir(0.5f, 0.8f, 0.0f);
   glUniform3f(lightDirLoc, lampDir.x, lampDir.y, lampDir.z); // 方向光源
  • 1
  • 2
  • 3

使用方向光源的效果就是,場景中無論遠近,物體得到的光照都一樣,光從同一個平行的方向射向物體表面,效果如下圖所示:

方向光源

點光源

在前面兩節使用的光源都是一個簡單的點光源,場景中物體不管離光源位置遠近得到的光照強度都相同,這一點與實際不相符合。實際中的點光源向各個方向發射光,但是物體與光源的距離d增大時光照的強度將會減弱。我們需要模擬這個特點來是點光源更加逼真。光照強度的衰減係數Fatt與距離d之間的關係如何確定呢? 如果簡單的使用線性函數,距離稍微遠點的物體光照強度減小得太過於明顯,不符合實際情況,因此一般要考慮使用二次函數。可以定義光照強度的衰減係數Fatt與距離d之間的關係如下式:

Fatt=1.0Kc+Kld+Kqd2

其中Kc表示常係數,當d=0時,Fatt=1表示沒有衰減,這時光照強度最大;Kl表示線性衰減係數,Kq表示二次衰減係數。使用上述公式計算光照的衰減時,大致的走勢如下圖所示(來自www.learnopengl.com):

這裏寫圖片描述

可以看出當距離較近時光照強度較大,當距離超過一定範圍後光照強度就很弱了,光照強度的較小不是直線型的,而是曲線型的,這樣更符合實際情形。

調整上述衰減係數以模擬逼真的光照效果,需要仔細玩耍這些參數,是一件需要經驗的工作

在主程序中設置衰減係數如下:

 GLint attConstant = glGetUniformLocation(shader.programId, "light.constant");
GLint attLinear = glGetUniformLocation(shader.programId, "light.linear");
GLint attQuadratic = glGetUniformLocation(shader.programId, "light.quadratic");
// 設置衰減係數
glUniform1f(attConstant, 1.0f);
glUniform1f(attLinear, 0.09f);
glUniform1f(attQuadratic, 0.032f);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在着色器中對應的點光源結構體更新爲:

   // 點光源屬性結構體
struct PointLightAttr
{
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant; // 衰減常數
    float linear;   // 衰減一次係數
    float quadratic; // 衰減二次係數
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

計算光照強度後乘以使用衰減係數:

   // 計算衰減因子
float distance = length(light.position - FragPos); // 在世界座標系中計算距離
float attenuation = 1.0f / (light.constant 
        + light.linear * distance
        + light.quadratic * distance * distance);
vec3 result = (ambient + diffuse + specular) * attenuation;
color   = vec4(result , 1.0f);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

使用有衰減的點光源效果如下圖所示:

正面點光源

這裏我們看到紅色箭頭指向的物體隨着與光源距離不同光照有明暗不同的效果。

聚光燈光源

聚光燈光源的特點是光只在一個指定的範圍內發散,如下圖所示(來自www.learnopengl.com):

聚光燈

注意指定聚光燈指定了3個方面:

  • SpotDir 聚光燈的燈軸的方向
  • LightPos 聚光燈的位置
  • Cutoff 聚光燈的張角 即圖中的ϕ

在計算聚光燈的光照效果時需要計算的量包括:

  • lightDir 物體的位置和光源位置之差構成的光照射方向
  • θ是lightDir與SpotDir之間的夾角

聚光燈的特點是,當夾角θ <= ϕ時物體接受到光照,當θ > ϕ物體落在聚光燈的照明範圍外,將得不到光照。 
在着色器中定義聚光燈的結構體爲:

   // 聚光燈光源屬性結構體
struct SpotLightAttr
{
    vec3 position;  // 聚光燈的位置
    vec3 direction; // 聚光燈的spot direction
    float cutoff;   // 聚光燈張角的餘弦值
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant; // 衰減常數
    float linear;   // 衰減一次係數
    float quadratic; // 衰減二次係數
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

注意聚光燈在傳遞張角時,使用了一個技巧,即傳遞夾角的餘弦值而不是角度值。對於cos函數,在[0,π2]時函數遞減,如下圖左邊部分所示:

函數

那麼當θ <= ϕ時,有cos(θ)>=cos(ϕ),這一點在着色器中將會利用到。

在主程序中設置參數的方法同上述方向光源和點光源一樣,這裏不再贅述。在着色器中計算聚光燈效果的實現思路爲:

   void main()
{   
   // 環境光成分
   vec3 lightDir = normalize(light.position - FragPos);
   // 光線與聚光燈spotDir夾角餘弦值
   float theta = dot(lightDir,normalize(-light.direction));
    if(theta > light.cutoff)    
    {
        // 在聚光燈張角範圍內 計算漫反射光成分 鏡面反射成分 
    }
    else
    {
        // 不在張角範圍內時只有環境光成分
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

其中計算漫反射和鏡面反射同點光源的計算是一樣的,也需要考慮光照強度衰減的因素。

如果將聚光燈的位置定義爲觀察者所在位置,將聚光燈的光軸方向定義爲觀察者的觀察正向,那麼就可以實現手電筒的效果。當觀察者在場景中移動時,手電筒發出光位置也隨着改變,如下圖所示:

聚光燈

改進的手電筒實現

上述十年的手電筒效果,存在一個缺陷,就是當物體超過手電筒這個聚光燈模型的張角時,場景立馬變暗了,這個與實際情形不符合。實際拿着手電筒時,物體不再張角範圍內時是逐漸變暗的,而這裏實現的手電筒的邊緣部分帶有很明顯的變暗的感覺。可以爲聚光燈模型指定兩個張角,ϕ用於內張角餘弦值,r用於外張角餘弦值,定義光照強度爲:

  • 0.0f 當θ 落在外部張角之外時,沒有光照
  • [0.0f,1.0f]之間 當θ 落在內外張角之間時
  • 1.0f 當θ 位於內部張角之內時

我們需要定義一個函數來實現這個光照強度的計算,如下圖中右半部分所示:

函數 
並結合OpenGL提供的clamp函數來實現,clamp函數定義如下:

API genType clamp( genType x, 
genType minVal, 
genType maxVal); 
這個函數將值x截斷到minVal和maxVal之間。

這裏我們使用一個形如如下形式的函數: 

f1=θγϵ

其中: θ爲lightDir與SpotDir的夾角的餘弦,r表示聚光燈的外張角的餘弦,ϵ=ϕr表示內外張角的餘弦之差。 
可以驗證上述f1函數滿足圖中f函數的要求,則我們可以在着色器中實現改進的手電筒模型爲:

        // 計算內外張角範圍內的強度
    float theta = dot(lightDir, normalize(-light.direction));// 光線與聚光燈spotDir夾角餘弦值
    float epsilon = light.cutoff - light.outerCutoff;
    float intensity = clamp((theta - light.outerCutoff) / epsilon, 0.0, 1.0); // 引入聚光燈內張角和外張角後的強度值
    diffuse *= intensity;
    specular *= intensity;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

改進時,對漫反射光和鏡面光成分乘以通過內外張角計算出來的強度係數,其餘部分不變。通過增加的這個intensity來計算光照後,手電筒效果如下:

改進的手電筒模型

從上圖可以看出,改進之後從內張角到外張角之間這部分逐漸變暗,這樣的效果更佳符合實際手電筒的效果。

使用多個光源

上面學習了方向光源、點光源以及聚光燈光源,我們可以將他們應用到一個場景中。在着色器中,定義計算場景中各個不同類型光照效果的函數如下:

// 計算光源效果的函數聲明 包括方向、點、聚光燈光源3種實現
vec3 calculateDirLight(DirLightAttr light, vec3 fragNormal, vec3 fragPos,vec3 viewPos);
vec3 calculatePointLight(PointLightAttr light, vec3 fragNormal, vec3 fragPos,vec3 viewPos);
vec3 calculateSpotLight(SpotLightAttr light,vec3 fragNormal, vec3 fragPos, vec3 viewPos);
  • 1
  • 2
  • 3
  • 4

定義多個光源:

   uniform DirLightAttr dirLight;  // 方向光源
#define POINT_LIGHT_NUM 4
uniform PointLightAttr pointLights[POINT_LIGHT_NUM]; // 定義點光源數組
uniform SpotLightAttr spotLight;  // 聚光燈光源
  • 1
  • 2
  • 3
  • 4

則計算總的光照效果的函數實現爲:

   void main()
{   
    vec3 result = calculateDirLight(
        dirLight, FragNormal,
        FragPos, viewPos);
    for(int i = 0; i < POINT_LIGHT_NUM; ++i)
    {
        result += calculatePointLight(
            pointLights[i],FragNormal, 
            FragPos, viewPos);
    }
    result += calculateSpotLight(
        spotLight, FragNormal, 
        FragPos, viewPos);
    color   = vec4(result , 1.0f);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

實現函數calculatexxxLight的方法同上面講到的一樣,是對相關類型光源光照計算的一個函數封裝,這裏不再贅述。需要注意的是點光源包含一個數組,設置數組中每個光源時,需要在主程序中使用數組的索引方式,例如:

   GLint lightAmbientLoc = glGetUniformLocation(
   shader.programId,"pointLights[0].ambient");
   glUniform3f(lightAmbientLoc, 0.0f, 0.1f, 0.4f);
  • 1
  • 2
  • 3

根據需要調整各個多個光源的參數值後,實現的一個藍色調效果爲:

藍色調效果

最後的說明

本節學習了三種光源類型,以及如何實現光照的計算,並在最後將多個類型的光源應用到了同一個場景中。到目前爲止,我們已經學習了基本的光照,能夠實現一些理想的效果了,後面還會繼續學習光照的高級技巧。接下來,我們打算學習加載模型的方法,使場景中的物體更豐富

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