跳轉到內容

Cg 程式設計/Unity/透明度

來自 Wikibooks,開放世界開放書籍
皮埃爾·奧古斯特·科特 (Pierre Auguste Cot) 的《春天》(Le Printemps),1873 年。請注意透明的服裝。

本教程介紹了使用 Unity 中的 Cg 著色器對片段進行混合(即對其進行合成)。它假設您熟悉“切片”一節中討論的前後面的概念。

更具體地說,本教程是關於渲染透明物體的,例如透明玻璃、塑膠、織物等。(更嚴格地說,這些實際上是半透明物體,因為它們不需要完全透明。)透明物體允許我們透過它們看到;因此,它們的顏色會與它們背後的物體的顏色“混合”。

正如“可程式設計圖形管道”一節中所述,片段著色器為每個片段計算一個 RGBA 顏色(即片段輸出引數中的紅色、綠色、藍色和 alpha 分量,語義為 COLOR)(除非片段被丟棄)。然後,按照“每個片段操作”一節中所述處理這些片段。其中一項操作是混合階段,它將片段的顏色(如片段輸出引數中指定的那樣)——稱為“源顏色”——與幀緩衝區中已有的對應畫素的顏色——稱為“目標顏色”(因為混合後的顏色的“目標”是幀緩衝區)——結合起來。

混合是一個固定功能階段,即您可以配置它,但不能對其進行程式設計。它的配置方式是指定一個混合方程。您可以將混合方程視為對結果 RGBA 顏色的以下定義

float4 result = SrcFactor * fragment_output + DstFactor * pixel_color;

其中 fragment_output 是片段著色器計算的 RGBA 顏色,pixel_color 是當前在幀緩衝區中的 RGBA 顏色,result 是混合後的結果,即混合階段的輸出。SrcFactorDstFactor 是可配置的 RGBA 顏色(型別為 float4),它們與片段輸出顏色和畫素顏色按分量進行相乘。SrcFactorDstFactor 的值在 Unity 的 ShaderLab 語法中用以下程式碼行指定

Blend {SrcFactor 的程式碼} {DstFactor 的程式碼}

兩種因子最常見的程式碼總結在下表中(更多程式碼在Unity 的 ShaderLab 關於混合的參考中提到)

程式碼 結果因子 (SrcFactorDstFactor)
One float4(1.0, 1.0, 1.0, 1.0)
Zero float4(0.0, 0.0, 0.0, 0.0)
SrcColor fragment_output
SrcAlpha fragment_output.aaaa
DstColor pixel_color
DstAlpha pixel_color.aaaa
OneMinusSrcColor float4(1.0, 1.0, 1.0, 1.0) - fragment_output
OneMinusSrcAlpha float4(1.0, 1.0, 1.0, 1.0) - fragment_output.aaaa
OneMinusDstColor float4(1.0, 1.0, 1.0, 1.0) - pixel_color
OneMinusDstAlpha float4(1.0, 1.0, 1.0, 1.0) - pixel_color.aaaa

正如“向量和矩陣運算”一節中所述,pixel_color.aaaa 只是 float4(pixel_color.a, pixel_color.a, pixel_color.a, pixel_color.a) 的簡寫。還要注意,混合方程中所有顏色和因子的所有分量都在 0 到 1 之間進行鉗位。

Alpha 混合

[編輯 | 編輯原始碼]

混合方程的一個具體示例稱為“Alpha 混合”。在 Unity 中,它以這種方式指定

Blend SrcAlpha OneMinusSrcAlpha

這對應於

float4 result = fragment_output.aaaa * fragment_output + (float4(1.0, 1.0, 1.0, 1.0) - fragment_output.aaaa) * pixel_color;

這使用 fragment_output 的 alpha 分量作為不透明度。也就是說,片段輸出顏色越不透明,其不透明度和 alpha 分量就越大,因此結果中混合的片段輸出顏色越多,幀緩衝區中混合的畫素顏色越少。完全不透明的片段輸出顏色(即 alpha 分量為 1)將完全替換畫素顏色。

此混合方程有時被稱為“覆蓋”操作,即“fragment_output 覆蓋 pixel_color”,因為它對應於將一層具有特定不透明度的片段輸出顏色疊加在畫素顏色之上。(想象一下,一層有色玻璃或有色半透明塑膠放在另一個顏色的東西上面。)

由於 Alpha 混合的流行,顏色的 alpha 分量通常被稱為不透明度,即使沒有使用 Alpha 混合。此外,請注意,在計算機圖形學中,透明度的常用正式定義是1 − 不透明度

預乘 Alpha 混合

[編輯 | 編輯原始碼]

Alpha 混合有一個重要的變體:有時片段輸出顏色已將 alpha 分量預乘到顏色分量中。(您可以將它視為已包含增值稅的價格。)在這種情況下,alpha 不應再次乘以顏色(對應於增值稅不得多次新增的規則),並且正確的混合是

Blend One OneMinusSrcAlpha

這對應於

float4 result = float4(1.0, 1.0, 1.0, 1.0) * fragment_output + (float4(1.0, 1.0, 1.0, 1.0) - fragment_output.aaaa) * pixel_color;

計算機圖形學中的兩個常見錯誤是 1) 在預乘顏色上使用標準 Alpha 混合的混合方程,以及 2) 在未預乘的顏色上使用預乘 Alpha 混合的混合方程。由這些錯誤引起的視覺錯誤可能非常微妙;通常它們表現為混合物體的變暗或變亮的輪廓。因此,在程式設計混合方程時,請確保您知道自己是否正在處理預乘顏色。

疊加混合

[編輯 | 編輯原始碼]

混合方程的另一個示例是

Blend One One

這對應於

float4 result = float4(1.0, 1.0, 1.0, 1.0) * fragment_output + float4(1.0, 1.0, 1.0, 1.0) * pixel_color;

它只是將片段輸出顏色新增到幀緩衝區中的顏色。請注意,alpha 分量根本沒有使用;儘管如此,此混合方程對於許多型別的透明效果非常有用;例如,它通常用於粒子系統,當它們代表火焰或其他透明併發出光的東西時。在“獨立於順序的透明度”一節中將更詳細地討論疊加混合。

更多混合方程示例在Unity 的 ShaderLab 關於混合的參考中給出。

著色器程式碼

[編輯 | 編輯原始碼]

這裡是一個簡單的著色器,它使用 Alpha 混合渲染具有 0.3 不透明度的綠色。

Shader "Cg shader using blending" {
   SubShader {
      Tags { "Queue" = "Transparent" } 
         // draw after all opaque geometry has been drawn
      Pass {
         ZWrite Off // don't write to depth buffer 
            // in order not to occlude other objects

         Blend SrcAlpha OneMinusSrcAlpha // use alpha blending

         CGPROGRAM 
 
         #pragma vertex vert 
         #pragma fragment frag
 
         float4 vert(float4 vertexPos : POSITION) : SV_POSITION 
         {
            return UnityObjectToClipPos(vertexPos);
         }
 
         float4 frag(void) : COLOR 
         {
            return float4(0.0, 1.0, 0.0, 0.3); 
               // the fourth component (alpha) is important: 
               // this is semitransparent green
         }
 
         ENDCG  
      }
   }
}

除了上面討論過的混合方程外,只有兩行需要更多解釋:Tags { "Queue" = "Transparent" }ZWrite Off

ZWrite Off 停用寫入深度緩衝區。正如“每個片段操作”一節中所解釋的那樣,深度緩衝區儲存最近片段的深度,並丟棄任何深度更大的片段。然而,在透明片段的情況下,這不是我們想要的,因為我們可以(至少有可能)透過透明片段看到。因此,透明片段不應遮擋其他片段,因此停用寫入深度緩衝區。另請參見Unity 的 ShaderLab 關於剔除和深度測試的參考.

程式碼行 Tags { "Queue" = "Transparent" } 指定使用此子著色器的網格將在所有不透明網格渲染完成後進行渲染。部分原因是我們停用了寫入深度緩衝區:一個後果是,即使不透明網格更遠,透明片段也可能被不透明網格遮擋。為了解決這個問題,我們首先繪製所有不透明網格(在 Unity 的“不透明佇列”中),然後繪製所有透明網格(在 Unity 的“透明佇列”中)。網格是否被視為不透明或透明取決於其子著色器的標籤,如程式碼行 Tags { "Queue" = "Transparent" } 中指定的那樣。有關子著色器標籤的更多詳細資訊,請參閱Unity 的 ShaderLab 關於子著色器標籤的參考.

應該提到,這種使用停用的寫入深度緩衝區來渲染透明網格的策略並不總是能解決所有問題。如果片段混合的順序無關緊要,它可以完美地工作;例如,如果片段顏色只是新增到幀緩衝區中的畫素顏色,則片段混合的順序並不重要;請參見“獨立於順序的透明度”一節。但是,對於其他混合方程,例如 Alpha 混合,結果將根據片段混合的順序而不同。(如果您透過幾乎不透明的綠色玻璃看幾乎不透明的紅色玻璃,您將主要看到綠色,而如果您透過幾乎不透明的紅色玻璃看幾乎不透明的綠色玻璃,您將主要看到紅色。同樣,將幾乎不透明的綠色混合在幾乎不透明的紅色之上與將幾乎不透明的紅色混合在幾乎不透明的綠色之上將有所不同。)為了避免出現偽影,建議使用疊加混合或使用小不透明度的(預乘)Alpha 混合(在這種情況下,目標因子 DstFactor 接近 1,因此 Alpha 混合接近疊加混合)。

包含背面

[編輯 | 編輯原始碼]

之前的著色器與其他物體配合得很好,但實際上它沒有渲染物體的“內部”。但是,由於我們可以透過透明物體的外部,我們也應該渲染內部。如“切口”部分所述,內部可以透過使用Cull Off停用剔除來渲染。但是,如果我們只是停用剔除,我們可能會遇到麻煩:如上所述,透明片段的渲染順序通常很重要,但在沒有剔除的情況下,來自內部和外部的重疊三角形可能以隨機順序渲染,這會導致令人討厭的渲染偽像。因此,我們希望確保內部(通常更遠)在外部渲染之前先渲染。在 Unity 的 ShaderLab 中,這是透過指定兩個通道來實現的,這兩個通道按定義的順序為同一個網格執行

Shader "Cg shader using blending" {
   SubShader {
      Tags { "Queue" = "Transparent" } 
         // draw after all opaque geometry has been drawn
      Pass {
         Cull Front // first pass renders only back faces 
             // (the "inside")
         ZWrite Off // don't write to depth buffer 
            // in order not to occlude other objects
         Blend SrcAlpha OneMinusSrcAlpha // use alpha blending

         CGPROGRAM 
 
         #pragma vertex vert 
         #pragma fragment frag
 
         float4 vert(float4 vertexPos : POSITION) : SV_POSITION 
         {
            return UnityObjectToClipPos(vertexPos);
         }
 
         float4 frag(void) : COLOR 
         {
            return float4(1.0, 0.0, 0.0, 0.3);
               // the fourth component (alpha) is important: 
               // this is semitransparent red
         }
 
         ENDCG  
      }

      Pass {
         Cull Back // second pass renders only front faces 
             // (the "outside")
         ZWrite Off // don't write to depth buffer 
            // in order not to occlude other objects
         Blend SrcAlpha OneMinusSrcAlpha // use alpha blending

         CGPROGRAM 
 
         #pragma vertex vert 
         #pragma fragment frag
 
         float4 vert(float4 vertexPos : POSITION) : SV_POSITION 
         {
            return UnityObjectToClipPos(vertexPos);
         }
 
         float4 frag(void) : COLOR 
         {
            return float4(0.0, 1.0, 0.0, 0.3);
               // the fourth component (alpha) is important: 
               // this is semitransparent green
         }
 
         ENDCG  
      }
   }
}

在這個著色器中,第一個通道使用正面剔除(使用Cull Front)來優先渲染背面(內部)。之後,第二個通道使用背面剔除(使用Cull Back)來渲染正面(外部)。這對於凸面網格(沒有凹陷的封閉網格;例如球體或立方體)非常有效,並且通常是其他網格的良好近似值。

恭喜你,你完成了本教程!渲染透明物體的一個有趣之處在於它不僅僅是關於混合,還需要了解剔除和深度緩衝區。具體來說,我們已經研究了

  • 什麼是混合以及如何在 Unity 中指定它。
  • 包含透明和不透明物體的場景是如何渲染的,以及如何在 Unity 中將物體分類為透明或不透明。
  • 如何渲染透明物體的內部和外部,特別是如何在 Unity 中指定兩個通道。

進一步閱讀

[編輯 | 編輯原始碼]

如果你還想了解更多

< Cg 程式設計/Unity

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