跳轉到內容

GLSL 程式設計/Blender/漫反射

來自 Wikibooks,開放世界中的開放書籍
月球表面的光反射(在很好的近似中)只是漫反射。

本教程涵蓋每個頂點的漫反射

這是關於 Blender 中基本光照的一系列教程中的第一個。在本教程中,我們從單個方向光源的漫反射開始,然後包括點光源和聚光燈。後面的教程將涵蓋其擴充套件,特別是鏡面反射、每個畫素的光照、雙面光照和多個光源。

漫反射可以使用表面法向量 N 和光向量 L 計算,即指向光源的向量。

漫反射

[編輯 | 編輯原始碼]

月球幾乎完全表現出漫反射(也稱為朗伯反射),即光線被反射到所有方向而沒有鏡面高光。其他此類材料的例子包括粉筆和啞光紙;實際上,任何看起來暗淡和啞光的表面都是如此。

在完美的漫反射情況下,觀察到的反射光的強度取決於表面法向量和入射光線之間的角度的餘弦值。如左側圖所示,通常考慮從表面某一點開始的歸一化向量,在那裡應該計算光照:歸一化表面法向量N與表面正交,歸一化光方向L指向光源。

對於觀察到的漫反射光,我們需要歸一化表面法向量N和指向光源的歸一化方向L之間的角度的餘弦值,即點積N·L,因為任何兩個向量ab的點積a·b

.

對於歸一化向量,長度 |a| 和 |b| 都是 1。

如果點積N·L為負,光源位於表面的“錯誤”一側,我們應該將反射設定為 0。這可以透過使用 max(0, N·L) 來實現,它確保點積的值對於負點積被鉗制到 0。此外,反射光取決於入射光線的強度和一個材料常數 用於漫反射:對於黑色表面,材料常數 為 0,對於白色表面,它是 1。那麼漫反射強度的方程為

對於彩色光,此方程適用於每個顏色分量(例如紅色、綠色和藍色)。因此,如果變數 表示顏色向量,乘法是按分量執行的(它們在 GLSL 中是針對向量的),此方程也適用於彩色光。這實際上是我們將在著色器程式碼中使用的。

一個方向光源的著色器程式碼

[編輯 | 編輯原始碼]

如果我們只有一個方向光源(即 Blender 中的“太陽”光),那麼實現方程的著色器程式碼相對較小。為了實現該方程,我們遵循關於實現方程的問題,這些問題在輪廓增強教程中進行了討論。

  • 應該在頂點著色器還是片段著色器中實現該方程?我們在這裡嘗試頂點著色器。在平滑鏡面高光教程中,我們將檢視片段著色器中的實現。
  • 應該在哪個座標系中實現該方程?我們預設嘗試 Blender 中的檢視空間。(事實證明這是一個很好的選擇,因為 Blender 在檢視空間中提供了光方向。)
  • 我們從哪裡獲取引數?這個問題的答案比較長。

正如視空間著色教程中所述,gl_FrontMaterial.diffuse 通常為黑色;因此,我們使用 gl_FrontMaterial.emission 來表示漫反射材質顏色 。(請記住在材質選項卡中將著色 > 發射設定為 1。)視空間中光源L的方向可以在 gl_LightSource[0].position 中獲取,光線顏色 可以從 gl_LightSource[0].diffuse 中獲取。(如果只有一個光源,它始終是陣列 gl_LightSource[] 中的第 0 個。)我們從屬性 gl_Normal 獲取物體座標系中的表面法向量。由於我們在視空間中實現方程,因此我們必須將表面法向量從物體空間轉換為視空間,如輪廓增強教程中所述。

然後,頂點著色器如下所示

         varying vec4 color; 

         void main()
         {                              
            vec3 normalDirection = 
               normalize(gl_NormalMatrix * gl_Normal);
            vec3 lightDirection = 
               normalize(vec3(gl_LightSource[0].position));
 
            vec3 diffuseReflection = 
               vec3(gl_LightSource[0].diffuse) 
               * vec3(gl_FrontMaterial.emission)
               * max(0.0, dot(normalDirection, lightDirection));

            color = vec4(diffuseReflection, 1.0); 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }

片段著色器為

         varying vec4 color;

         void main()
         {            
            gl_FragColor = color;
         }

在嘗試此著色器時,請確保場景中只有一個光源,該光源必須是方向光,即型別為“太陽”。如果沒有光源,可以透過在資訊視窗選單中選擇新增 > 燈光 > 太陽來建立方向光源。

點光源的更改

[編輯 | 編輯原始碼]

對於方向光源,gl_LightSource[0].position 指定光線照射的方向。然而,對於點光源(或聚光燈),gl_LightSource[0].position 指定視空間中光源的位置,我們必須計算光源方向,即視空間中頂點位置到光源位置的差向量。由於點的第 4 個座標為 1,而方向的第 4 個座標為 0,因此我們可以輕鬆區分這兩種情況

            vec3 lightDirection;

            if (0.0 == gl_LightSource[0].position.w) 
               // directional light?
            {
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position));
            } 
            else // point or spot light
            {
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position 
                  - gl_ModelViewMatrix * gl_Vertex));
            }

雖然方向光源沒有光線衰減,但我們應該為點光源和聚光燈新增一些隨距離變化的衰減。由於光線從三維空間中的一個點向外擴散,因此它在更遠的距離處覆蓋著越來越大的虛擬球體。由於這些球體的表面積隨著半徑的增大而二次增長,而每個球體上的總光量相同,因此每單位面積的光量隨著距離點光源的距離的增大而二次減小。因此,我們應該將光源強度除以頂點到光源的距離的平方。

由於二次衰減速度很快,因此我們使用線性衰減,即用距離而不是距離的平方來除以強度。程式碼可能是

            vec3 lightDirection;
            float attenuation;

            if (0.0 == gl_LightSource[0].position.w) 
               // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(gl_LightSource[0].position 
                  - gl_ModelViewMatrix * gl_Vertex);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }

通常,我們會將線性衰減函式 1.0 / distance 乘以統一變數 gl_LightSource[0].linearAttenuation;但是,Blender 似乎沒有正確設定此統一變數。事實上,如果 Blender 正確設定了統一變數,我們應該用這種方式計算衰減

               attenuation = 
                  1.0 / (gl_LightSource[0].constantAttenuation 
                  + gl_LightSource[0].linearAttenuation * distance
                  + gl_LightSource[0].quadraticAttenuation 
                  * distance * distance);

無論如何,因子 attenuation 應該乘以 gl_LightSource[0].diffuse 來計算入射光;請參見下面的完整著色器程式碼。請注意,聚光燈具有其他功能,將在下一節中討論。

還要注意,此程式碼可能無法提供最佳效能,因為任何 if 通常都很昂貴。由於 gl_LightSource[0].position.w 為 0 或 1,因此實際上並不難重寫程式碼以避免使用 if 並進一步最佳化

            vec3 vertexToLightSource = vec3(gl_LightSource[0].position 
               - gl_ModelViewMatrix * gl_Vertex 
               * gl_LightSource[0].position.w);
            float one_over_distance = 
               1.0 / length(vertexToLightSource);
            float attenuation = mix(1.0, one_over_distance, 
               gl_LightSource[0].position.w); 
            vec3 lightDirection = 
               vertexToLightSource * one_over_distance;

但是,為了清晰起見,我們將使用包含 if 的版本。(“保持簡單,笨蛋!”)

聚光燈的更改

[編輯 | 編輯原始碼]

如果 gl_LightSource[0].spotCutoff 小於或等於 90.0,則第 0 個光源是聚光燈(否則應為 180.0)。因此,測試可以是

            if (gl_LightSource[0].spotCutoff <= 90.0) // spotlight?

聚光燈的形狀由內建統一變數 gl_LightSource[0].spotDirectiongl_LightSource[0].spotExponentgl_LightSource[0].spotCutoff 描述。具體來說,如果 -lightDirectiongl_LightSource[0].spotDirection 之間的角度的餘弦小於 gl_LightSource[0].spotCutoff 的餘弦,即如果陰影點位於聚光燈方向周圍的光錐之外,則衰減因子設定為 。我們可以透過兩個歸一化向量的點積來計算兩個向量之間角度的餘弦。gl_LightSource[0].spotCutoff 的餘弦實際上是在 gl_LightSource[0].spotCosCutoff 中提供的。如果我們將餘弦鉗位在 0 以忽略聚光燈後面的點,則測試為

                  float clampedCosine = max(0.0, dot(-lightDirection, 
                     gl_LightSource[0].spotDirection));
                  if (clampedCosine < gl_LightSource[0].spotCosCutoff) 
                     // outside of spotlight cone?
                  {
                     attenuation = 0.0;
                  }

否則,如果一個點位於光錐內,則 OpenGL 中的衰減應該用這種方式計算

                     attenuation = attenuation * pow(clampedCosine, 
                        gl_LightSource[0].spotExponent);

這允許更寬(較小的 spotExponent)和更窄(較大的 spotExponent)的聚光燈。

但是,Blender 的內建著色器似乎更類似於

                     attenuation = attenuation * pow(clampedCosine
                        - gl_LightSource[0].spotCosCutoff, 
                        gl_LightSource[0].spotExponent / 128.0);

我們將堅持使用 OpenGL 版本。

完整著色器程式碼

[編輯 | 編輯原始碼]

總而言之,我們新的頂點著色器,用於單個方向光、點光或具有線性衰減的聚光燈,變為

         varying vec4 color; 
 
         void main()
         {                              
            vec3 normalDirection = 
               normalize(gl_NormalMatrix * gl_Normal);
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == gl_LightSource[0].position.w) 
               // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = 
                  normalize(vec3(gl_LightSource[0].position));
            } 
            else // point light or spotlight (or other kind of light) 
            {
               vec3 vertexToLightSource = 
                  vec3(gl_LightSource[0].position 
                  - gl_ModelViewMatrix * gl_Vertex);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
 
               if (gl_LightSource[0].spotCutoff <= 90.0) // spotlight?
               {
                  float clampedCosine = max(0.0, dot(-lightDirection, 
                     gl_LightSource[0].spotDirection));
                  if (clampedCosine < gl_LightSource[0].spotCosCutoff) 
                     // outside of spotlight cone?
                  {
                     attenuation = 0.0;
                  }
                  else
                  {
                     attenuation = attenuation * pow(clampedCosine, 
                        gl_LightSource[0].spotExponent);
                  }
               }
            }
            vec3 diffuseReflection = attenuation 
               * vec3(gl_LightSource[0].diffuse) 
               * vec3(gl_FrontMaterial.emission)
               * max(0.0, dot(normalDirection, lightDirection));
 
            color = vec4(diffuseReflection, 1.0);
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }

片段著色器仍然是

         varying vec4 color;

         void main()
         {            
            gl_FragColor = color;
         }

恭喜!您已經瞭解了很多關於 OpenGL 光源的知識。這對以下關於更高階照明的教程至關重要。具體來說,我們已經看到了

  • 什麼是漫反射以及如何用數學方法描述它。
  • 如何在著色器中為單個方向光源實現漫反射。
  • 如何擴充套件著色器以處理具有線性衰減的點光源。
  • 如何進一步擴充套件著色器以處理聚光燈。

進一步閱讀

[編輯 | 編輯原始碼]

如果您還想了解更多


< GLSL 程式設計/Blender

除非另有說明,否則本頁上的所有示例原始碼都授予公共領域。
華夏公益教科書