光照-05.投光物(Light casters)

我們目前使用的所有光照都來自于一個單獨的光源,這是空間中的一個點。它的效果不錯,但是在真實世界,我們有多種類型的光,它們每個表現都不同。一個光源把光投射到物體上,叫做投光。這個教程里我們討論幾種不同的投光類型。學習模擬不同的光源是你未來豐富你的場景的另一個工具。

我們首先討論定向光(directional light),接著是作為之前學到知識的擴展的點光(point light),最后我們討論聚光燈(Spotlight)。下面的教程我們會把這幾種不同的光類型整合到一個場景中。

定向光(Directional Light)

當一個光源很遠的時候,來自光源的每條光線接近于平行。這看起來就像所有的光線來自于同一個方向,無論物體和觀察者在哪兒。當一個光源被設置為無限遠時,它被稱為定向光(也被成為平行光),因為所有的光線都有著同一個方向;它會獨立于光源的位置。

我們知道的定向光源的一個好例子是,太陽。太陽和我們不是無限遠,但它也足夠遠了,在計算光照的時候,我們感覺它就像無限遠。在下面的圖片里,來自于太陽的所有的光線都被定義為平行光:

因為所有的光線都是平行的,對于場景中的每個物體光的方向都保持一致,物體和光源的位置保持怎樣的關系都無所謂。由于光的方向向量保持一致,光照計算會和場景中的其他物體相似。

我們可以通過定義一個光的方向向量,來模擬這樣一個定向光,而不是使用光的位置向量。著色器計算保持大致相同的要求,這次我們直接使用光的方向向量來代替用position向量計算lightDir向量:

struct Light
{
    // vec3 position; // 現在不在需要光源位置了,因為它是無限遠的
    vec3 direction;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
    vec3 lightDir = normalize(-light.direction);
    ...
}

注意,我們首先對light.direction向量取反。目前我們使用的光照計算需要光的方向作為一個來自片段朝向的光源的方向,但是人們通常更習慣定義一個定向光作為一個全局方向,它從光源發出。所以我們必須對全局光的方向向量取反來改變它的方向;它現在是一個方向向量指向光源。同時,確保對向量進行標準化處理,因為假定輸入的向量就是一個單位向量是不明智的。

作為結果的lightDir向量被使用在diffuse和specular計算之前。

例子里我們先定義10個不同的箱子位置,為每個箱子生成不同的模型矩陣,每個模型矩陣包含相應的本地到世界變換:

// Draw the container (using container's vertex attributes)
glBindVertexArray (containerVAO);
for (GLuint i = 0; i < 10; i++)
{
    model = glm::mat4 ();
    model = glm::translate (model, cubePositions[i]);
    GLfloat angle = 20.0f * i;
    model = glm::rotate (model, angle, glm::vec3 (1.0f, 0.3f, 0.5f));
    glUniformMatrix4fv (modelLoc, 1, GL_FALSE, glm::value_ptr (model));
    glDrawArrays (GL_TRIANGLES, 0, 36);
}
glBindVertexArray (0);

同時,不要忘記定義光源的方向(注意,我們把方向定義為:從光源處發出的方向;在下面,你可以快速看到光的方向的指向):

GLint lightDirPos = glGetUniformLocation (lightingShader.Program, "light.direction");
glUniform3f (lightDirPos, -.02f, -1.0f, -0.3f);

我們已經把光的位置和方向向量傳遞為vec3,但是有些人去想更喜歡把所有的向量設置為vec4.當定義位置向量為vec4的時候,把w元素設置為1.0非常重要,這樣平移和投影才會合理的被應用。然而,當定義一個方向向量為vec4時,我們并不想讓平移發揮作用(因為它們除了代表方向,其他什么也不是)所以我們把w元素設置為0.0。
方向向量被表示為:vec4(0.2f, 1.0f, 0.3f, 0.0f)。這可以作為簡單檢查光的類型的方法:你可以檢查w元素是否等于1.0,查看我們現在所擁有的光的位置向量,w是否等于0.0,我們有一個光的方向向量,所以根據那個調整計算方法:

if (lightVector.w == 0.0) // Note: be careful for floating point errors
                // Do directional light calculations
else if (lightVector.w == 1.0)
    // Do light calculations using the light's position (like last tutorial)  

有趣的事實:這就是舊OpenGL(固定函數式)決定一個光源是一個定向光還是位置光源,根據這個修改它的光照。

如果你現在編譯應用,飛躍場景,它看起來像有一個太陽一樣的光源,把光拋到物體身上。你可以看到diffuse和specular元素都對該光源進行反射了,就像天空上有一個光源嗎?看起來就像這樣:

你可以在這里獲得應用的所有代碼

定點光(Point Light)

定向光作為全局光可以照亮整個場景,這非常棒,但是另一方面除了定向光,我們通常也需要幾個定點光,在場景里發亮。點光是一個在世界里有位置的光源,它向所有方向發光,光線隨距離增加逐漸變暗。想象燈泡和火炬作為投光物,它們可以扮演點光的角色。

之前我們定義的光源所模擬光線的強度不會因為距離變遠而衰減,這使得看起來像是光源亮度極強。在大多數3D仿真場景中,我們更希望去模擬一個僅僅能照亮靠近光源點附近場景的光源,而不是照亮整個場景的光源。

我們想讓黑暗中與光源比較近的箱子被輕微地照亮。

衰減(Attenuation)

隨著光線穿越距離的變遠使得亮度也相應地減少的現象,通常稱之為衰減(Attenuation)。一種隨著距離減少亮度的方式是使用線性等式。這樣的一個隨著距離減少亮度的線性方程,可以使遠處的物體更暗。然而,這樣的線性方程效果會有點假。在真實世界,通常光在近處時非常亮,但是一個光源的亮度,開始的時候減少的非常快,之后隨著距離的增加,減少的速度會慢下來。我們需要一種不同的方程來減少光的亮度。

在這里I是當前片段的光的亮度,d代表片段到光源的距離。為了計算衰減值,我們定義3個項:常數項Kc,一次項Kl和二次項Kq。

  • The constant term is usually kept at 1.0 which is mainly there to make sure the resulting denominator never gets smaller than 1 since it would otherwise boost the intensity with certain distances, which is not the effect we're looking for.
  • The linear term is multiplied with the distance value that reduces the intensity in a linear fashion.
  • The quadratic(二次) term is multiplied with the quadrant of the distance and sets a quadratic decrease of intensity for the light source. The quadratic term will be less significant compared to the linear term when the distance is small, but gets much larger than the linear term as the distance grows.

你可以看到當距離很近的時候光有最強的亮度,但是隨著距離增大,亮度明顯減弱,大約接近100的時候,就會慢慢接近0。這就是我們想要的。

選擇正確的值

但是,我們把這三個項設置為什么值呢?正確的值的設置由很多因素決定:環境、你希望光所覆蓋的距離范圍、光的類型等。大多數場合,這是經驗的問題,也要適度調整。下面的表格展示一些各項的值,它們模擬現實(某種類型的)光源,覆蓋特定的半徑(距離)。第一欄定義一個光的距離,它覆蓋所給定的項。這些值是大多數光的良好開始,它是來自Ogre3D's wiki

就像你所看到的,常數項Kc一直都是1.0。一次項Kl為了覆蓋更遠的距離通常很小,二次項Kq就更小了。嘗試用這些值進行實驗,看看它們在你的實現中各自的效果。我們的環境中,32到100的距離對大多數光通常就足夠了。

實現衰減

為了實現衰減,在著色器中我們會需要三個額外數值:也就是公式的常量、一次項和二次項。最好把它們儲存在之前定義的Light結構體中。要注意的是我們計算lightDir,就是在前面的教程中我們所做的,不是像之前的定向光的那部分。

struct Light            // 創建一些光的屬性來各自獨立的影響每個光照
{
    vec3 position;      
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    // 公式的常量、一次項和二次項
    float constant;
    float linear;
    float quadratic;
};

然后,我們在OpenGL中設置這些項:我們希望光覆蓋50的距離,所以我們會使用上面的表格中合適的常數項、一次項和二次項:

glUniform1f (glGetUniformLocation (lightingShader.Program, "light.constant"), 1.0f);
glUniform1f (glGetUniformLocation (lightingShader.Program, "light.linear"), 0.09f);
glUniform1f (glGetUniformLocation (lightingShader.Program, "light.quadratic"), 0.032f);

在片段著色器中實現衰減很直接:我們根據公式簡單的計算衰減值,在乘以ambient、diffuse和specular元素。

我們可以通過獲取片段和光源之間的不同向量把向量的長度結果作為距離項。我們可以使用GLSL的內建length函數做這件事:

float distance = length(light.position - FragPos);
float attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance));

然后,我們在光照計算中,通過把衰減值乘以ambient
、diffuse和specular顏色,包含這個衰減值。

我們可以可以把ambient元素留著不變,這樣amient光照就不會隨著距離減少,但是如果我們使用多余1個的光源,所有的ambient
元素會開始疊加,因此這種情況,我們希望ambient光照也衰減。簡單的調試出對于你的環境來說最好的效果。

ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;

你可以看到現在只有最近處的箱子的前面被照得最亮。后面的箱子一點都沒被照亮,因為它們距離光源太遠了。你可以在這里找到項目源碼

聚光燈(Spotlight)

聚光燈是一種位于環境中某處的光源,它不是向所有方向照射,而是只朝某個方向照射。聚光燈的好例子是路燈或手電筒。

OpenGL中的聚光燈用世界空間位置,一個方向和一個指定聚光燈半徑的切光角來表示。我們計算的每個片段,如果片段在聚光燈的切光方向之間(就是在圓錐體內),我們就會把片段照亮。下面的圖可以讓你明白聚光燈是如何工作的:

  • LightDir:從片段指向光源的向量。
  • SpotDir:聚光燈所指向的方向。
  • Phiφ:定義聚光燈半徑的切光角。每個落在這個角度之外的,聚光燈都不會照亮。
  • Thetaθ:LightDir向量和SpotDir向量之間的角度。θ值應該比φ值小,這樣才會在聚光燈內。

所以我們大致要做的是,計算LightDir向量和SpotDir向量的點乘(返回兩個單位向量的點乘,還記得嗎?),然后在和遮光角φ對比。我們下面將創建手電筒的范例。

手電筒

手電筒是一個坐落在觀察者位置的聚光燈,通常瞄準玩家透視圖的前面。基本上說,一個手電筒是一個普通的聚光燈,但是根據玩家的位置和方向持續的更新它的位置和方向。

所以我們需要為片段著色器提供的值,是聚光燈的位置向量(來計算光的方向坐標),聚光燈的方向向量和遮光角。我們可以把這些值儲存在Light結構體中:

struct Light
{
    vec3 position;
    vec3 direction;
    float cutOff;
    ...
};

下面我們把這些適當的值傳給著色器:

glUniform3f (lightPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);
glUniform3f (lightSpotdirLoc, camera.Front.x, camera.Front.y, camera.Front.z);
glUniform1f (lightSpotCutOffLoc, glm::cos (glm::radians (12.5f)));

你可以看到,我們為遮光角設置一個角度,但是我們根據一個角度計算了余弦值,把這個余弦結果傳給了片段著色器。這么做的原因是在片段著色器中,我們計算LightDir和SpotDir向量的點乘,而點乘返回一個余弦值,不是一個角度,所以我們不能直接把一個角度和余弦值對比。為了獲得這個角度,我們必須計算點乘結果的反余弦,這個操作開銷是很大的。所以為了節約一些性能,我們先計算給定切光角的余弦值,然后把結果傳遞給片段著色器。由于每個角度都被表示為余弦了,我們可以直接對比它們,而不用進行任何開銷高昂的操作。

現在剩下要做的是計算θ值,用它和φ值對比,以決定我們是否在或不在聚光燈的內部:

float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
    // 執行光照計算
}
else // 否則使用環境光,使得場景不至于完全黑暗
color = vec4(light.ambient*vec3(texture(material.diffuse,TexCoords)

我們首先計算lightDir和取反的direction向量的點乘。確保對所有相關向量進行了標準化處理。

你可能奇怪為什么if條件中使用>符號而不是<符號。為了在聚光燈以內,θ不是應該比光的遮光值更小嗎?這沒錯,但是不要忘了,角度值是以余弦值來表示的,一個0度的角表示為1.0的余弦值,當一個角是90度的時候被表示為0.0的余弦值,你可以在這里看到:



現在你可以看到,余弦越是接近1.0,角度就越小。這就解釋了為什么θ需要比切光值更大了。切光值當前被設置為12.5的余弦,它等于0.9978,所以θ的余弦值在0.9979和1.0之間,片段會在聚光燈內,被照亮。

運行應用,在聚光燈內的片段才會被照亮。這看起來像這樣:

你可以在這里獲得項目代碼

它看起來仍然有點假,原因是聚光燈有了一個硬邊。片段著色器一旦到達了聚光燈的圓錐邊緣,它就立刻黑了下來,卻沒有任何平滑減弱的過度。一個真實的聚光燈的光會在它的邊界處平滑減弱的。

平滑/軟化邊緣

為創建聚光燈的平滑邊,我們希望去模擬的聚光燈有一個內圓錐和外圓錐。我們可以把內圓錐設置為前面部分定義的圓錐,我們希望外圓錐從內邊到外邊逐步的變暗。

為創建外圓錐,我們簡單定義另一個余弦值,它代表聚光燈的方向向量和外圓錐的向量(等于它的半徑)的角度。然后,如果片段在內圓錐和外圓錐之間,就會給它計算出一個0.0到1.0之間的亮度。如果片段在內圓錐以內這個亮度就等于1.0,如果在外面就是0.0。

我們可以使用下面的公式計算這樣的值:

I=(θ?γ) / ?

這里?是內部圓錐和外部圓錐余弦值的差。結果I的值是聚光燈在當前片段的亮度。

我們使用一個例子來解釋這個公式的工作原理:

就像你看到的那樣我們基本是根據θ在外余弦和內余弦之間插值。

由于我們現在有了一個亮度值,當在聚光燈外的時候是個負的,當在內部圓錐以內大于1。如果我們適當地把這個值固定,我們在片段著色器中就再不需要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);    
...
// We'll leave ambient unaffected so we always have a little light.
diffuse  *= intensity;
specular *= intensity;
...

注意,我們使用了clamp函數,它把第一個參數固定在0.0和1.0之間。這保證了亮度值不會超出[0, 1]以外。

確定你把outerCutOff值添加到了Light結構體,并在應用中設置了它的uniform值。對于下面的圖片,內部遮光角12.5f,外部遮光角是17.5f:

可以在這里找到項目源碼

這樣的一個手電筒/聚光燈類型的燈光非常適合恐怖游戲,結合定向和點光,環境會真的開始被照亮了。

練習

  • 試著修改上面的各種不同種類的光源及其片段著色器。試著將部分矢量進行反向并嘗試使用 < 來代替 > 。試著解釋這些修改導致不同顯示效果的原因。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 一個光源把光投射到物體上,叫做投光(Light casters)。 定向光 當一個光源很遠的時候,來自光源的每條光...
    龍遁流閱讀 378評論 0 1
  • 一、四大光照類型1.環境光(Ambient Light) 一個物體即使沒有直接被光源照射,但是只要有光線通過其他物...
    CarlDonitz閱讀 1,503評論 0 0
  • 一、如果你想閱讀整個世界,全世界就會來幫助你 手機有一種超乎尋常的力量帶你走入放松的狀態,并且使你走進其他人的思維...
    幻想家Melon閱讀 272評論 0 0
  • 今天的三種感受:焦急、開心、溫馨 今天的小進步:今天早早起床帶小寶打完預防針后,去了醫院檢查身體。因為產后身體一直...
    小嬪_1e27閱讀 162評論 0 0
  • 第四重天:散發愛能量 (自2015-11-17開啟本階段) 某某人,你覺得自己的人生中,過去或現在,曾經或正在,有...
    李英花閱讀 117評論 0 0