跳轉到內容

GLSL 程式設計/Unity/輪廓增強

來自華夏公益教科書,開放世界開放書籍
一隻半透明的水母。注意輪廓處的透明度增加。

本教程涵蓋了表面法線向量變換。它假設您熟悉“透明度”一節中討論的 Alpha 混合,以及“世界空間中的著色”一節中討論的著色器屬性。

本教程的目標是實現左邊照片中可見的效果:半透明物體的輪廓往往比物體其他部分更不透明。這即使沒有光照,也會增加對三維形狀的印象。事實證明,變換後的法線對於獲得這種效果至關重要。

表面法線向量(簡稱法線)在表面片上。

光滑表面的輪廓

[編輯 | 編輯原始碼]

在光滑表面的情況下,輪廓上表面的點以平行於視平面的法線向量為特徵,因此與觀察者的方向正交。在左邊的圖中,圖頂部的輪廓處的藍色法線向量平行於視平面,而其他法線向量更多地指向觀察者(或攝像機)方向。透過計算觀察者方向和法線向量,並測試它們是否(幾乎)相互正交,我們可以因此測試一個點是否(幾乎)在輪廓上。

更具體地說,如果V是歸一化(即長度為 1)的觀察者方向,而N是歸一化的表面法線向量,那麼如果點積為 0,這兩個向量就正交:V·N = 0。在實踐中,這種情況很少發生。但是,如果點積V·N接近 0,我們可以假設該點接近輪廓。

增加輪廓處的透明度

[編輯 | 編輯原始碼]

因此,對於我們的效果,我們應該增加透明度,如果點積V·N接近 0。對於觀察者方向和法線向量之間的較小點積,有幾種方法可以增加透明度。以下是一種方法(它實際上背後有一個物理模型,在這篇出版物的第 5.1 節中進行了描述),用於從材料的常規透明度 計算增加的透明度

檢查像這樣的公式的極端情況總是很有意義的。考慮接近輪廓的點的情況:V·N ≈ 0。在這種情況下,常規透明度 將被一個小的正數除。(請注意,GLSL 保證以優雅的方式處理除以零的情況;因此,我們不必擔心它。)因此,無論 是什麼, 和一個小的正數的比率將更大。 函式將確保得到的透明度 永遠不會大於 1。

另一方面,對於遠離輪廓的點,我們有V·N ≈ 1。在這種情況下,α' ≈ min(1, α) ≈ α;也就是說,這些點的透明度不會發生太大變化。這正是我們想要的。因此,我們剛剛驗證了該公式至少是合理的。

在著色器中實現公式

[編輯 | 編輯原始碼]

為了在著色器中實現類似於的方程,第一個問題應該是:應該在頂點著色器還是片段著色器中實現它?在某些情況下,答案很明確,因為實現需要紋理對映,而紋理對映通常只在片段著色器中可用。但是,在許多情況下,沒有普遍的答案。在頂點著色器中實現往往更快(因為通常頂點的數量少於片段的數量),但影像質量較低(因為法線向量和其他頂點屬性在頂點之間可能會發生突然變化)。因此,如果您最關心效能,那麼在頂點著色器中實現可能是一個更好的選擇。另一方面,如果您最關心影像質量,那麼在畫素著色器中實現可能是一個更好的選擇。在每個頂點照明(即 Gouraud 著色,將在“鏡面高光”部分中討論)和每個片段照明(即 Phong 著色,將在“平滑鏡面高光”部分中討論)之間存在著同樣的權衡。

下一個問題是:應該在哪個座標系中實現這個方程?(有關標準座標系的描述,請參閱“頂點變換”部分。)同樣,也沒有普遍的答案。但是,在 Unity 中,在世界座標系中實現通常是一個不錯的選擇,因為許多統一變數是在世界座標系中指定的。(在其他環境中,在檢視座標系中實現非常常見。)

在實現方程之前,最後一個問題是:我們從哪裡獲得方程的引數?常規的不透明度是由著色器屬性指定的(在 RGBA 顏色中,請參閱“在世界空間中著色”部分)。法線向量 gl_Normal 是標準頂點屬性(請參閱“著色器的除錯”部分)。指向觀察者的方向可以在頂點著色器中計算為從世界空間中的頂點位置到世界空間中的相機位置 _WorldSpaceCameraPos 的向量,它由 Unity 提供。

因此,我們只需要將頂點位置和法線向量變換到世界空間,然後才能實現方程。從物體空間到世界空間的變換矩陣 _Object2World 及其逆矩陣 _World2Object 由 Unity 提供,如“在世界空間中著色”部分中所述。有關將變換矩陣應用於點和法線向量的詳細討論,請參閱“應用矩陣變換”部分。基本結果是點和方向只需乘以變換矩陣即可進行變換,例如

uniform mat4 _Object2World;
...
vec4 positionInWorldSpace = _Object2World * gl_Vertex;
vec3 viewDirection = _WorldSpaceCameraPos - vec3(positionInWorldSpace);

另一方面,法線向量透過乘以轉置的逆變換矩陣進行變換。由於 Unity 為我們提供了逆變換矩陣(除了右下角元素之外,它等於 _World2Object * unity_Scale.w),因此更好的選擇是從左側將法線向量乘以逆矩陣,這等效於從右側將其乘以轉置的逆矩陣,如“應用矩陣變換”部分中所述。

uniform mat4 _World2Object; // the inverse of _Object2World 
   // (after multiplication with unity_Scale.w)
uniform vec4 unity_Scale;
...
vec3 normalInWorldSpace = vec3(vec4(gl_Normal, 0.0) * _World2Object 
   * unity_Scale.w); // corresponds to a multiplication of the 
   // transposed inverse of _Object2World with gl_Normal

請注意,不正確的右下角矩陣元素沒有問題,因為它始終乘以 0。此外,如果縮放不重要,則乘以 unity_Scale.w 是不必要的;例如,如果我們對所有變換後的向量進行歸一化。

現在,我們擁有了編寫著色器所需的所有部分。

著色器程式碼

[edit | edit source]
Shader "GLSL silhouette enhancement" {
   Properties {
      _Color ("Color", Color) = (1, 1, 1, 0.5) 
         // user-specified RGBA color including opacity
   }
   SubShader {
      Tags { "Queue" = "Transparent" } 
         // draw after all opaque geometry has been drawn
      Pass { 
         ZWrite Off // don't occlude other objects
         Blend SrcAlpha OneMinusSrcAlpha // standard alpha blending

         GLSLPROGRAM

         uniform vec4 _Color; // define shader property for shaders

         // The following built-in uniforms are also defined in 
         // "UnityCG.glslinc", which could be #included 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix 
            // (apart from the factor unity_Scale.w)
                  
         varying vec3 varyingNormalDirection; 
            // normalized surface normal vector
         varying vec3 varyingViewDirection; 
            // normalized view direction 
                  
         #ifdef VERTEX
         
         void main()
         {				
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; 
               // multiplication with unity_Scale.w is unnecessary 
               // because we normalize transformed vectors

            varyingNormalDirection = normalize(
               vec3(vec4(gl_Normal, 0.0) * modelMatrixInverse));
            varyingViewDirection = normalize(_WorldSpaceCameraPos 
               - vec3(modelMatrix * gl_Vertex));

            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
         
         #endif

         #ifdef FRAGMENT
         
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
            vec3 viewDirection = normalize(varyingViewDirection);
            
            float newOpacity = min(1.0, _Color.a 
               / abs(dot(viewDirection, normalDirection)));
            gl_FragColor = vec4(vec3(_Color), newOpacity);
         }
         
         #endif

         ENDGLSL
      }
   }
}

newOpacity 的賦值幾乎是方程的字面翻譯

請注意,我們在頂點著色器中對 varyings varyingNormalDirectionvaryingViewDirection 進行歸一化(因為我們希望在方向之間進行插值,而不會對任何方向賦予更大的權重或更小的權重),並在片段著色器的開頭進行歸一化(因為插值可能會在一定程度上扭曲我們的歸一化)。但是,在許多情況下,頂點著色器中的 varyingNormalDirection 的歸一化是不必要的。類似地,在大多數情況下,片段著色器中的 varyingViewDirection 的歸一化是不必要的。

更多藝術控制

[edit | edit source]

雖然描述的輪廓增強基於物理模型,但它缺乏藝術控制;也就是說,CG 藝術家不能輕鬆地建立比物理模型建議的更薄或更厚的輪廓。為了允許更多藝術控制,您可以引入另一個(正)浮點數屬性,並將點積 |V·N| 提高到這個數字的冪(使用內建的 GLSL 函式 pow(float x, float y)),然後再在上述方程中使用它。這將允許 CG 藝術家獨立於基本顏色的不透明度建立更薄或更厚的輪廓。

總結

[edit | edit source]

恭喜您完成了本教程。我們已經討論了

  • 如何找到平滑表面的輪廓(使用法線向量和視線方向的點積)。
  • 如何增強這些輪廓處的透明度。
  • 如何在著色器中實現方程。
  • 如何將點和法線向量從物體空間變換到世界空間(對法線向量使用轉置的逆模型矩陣)。
  • 如何計算視線方向(作為從相機位置到頂點位置的差)。
  • 如何對歸一化的方向進行插值(即進行兩次歸一化:在頂點著色器和片段著色器中)。
  • 如何提供對輪廓厚度進行更多藝術控制。

進一步閱讀

[edit | edit source]

如果您還想了解更多資訊

  • 關於物體空間和世界空間,您應該閱讀“頂點變換”部分中的描述。
  • 關於如何將變換矩陣應用於點、方向和法線向量,您應該閱讀“應用矩陣變換”部分
  • 關於渲染透明物體的基本知識,您應該閱讀“透明度”部分
  • 關於 Unity 提供的統一變數和著色器屬性,您應該閱讀“在世界空間中著色”部分
  • 關於輪廓增強的數學原理,您可以閱讀 Martin Kraus 在 2005 年 IEEE 視覺化大會上發表的論文“尺度不變體積渲染”的第 5.1 節,該論文可在 網上 獲取。


< GLSL 程式設計/Unity

除非另有說明,本頁所有示例原始碼均已進入公有領域。
華夏公益教科書