GLSL 程式設計/Unity/透明度

本教程介紹了使用 Unity 中的 GLSL 著色器對片段進行混合(即合成它們)。它假定您熟悉前面和後面的概念,如“剖檢視”部分中所述。
更具體地說,本教程是關於渲染透明物件,例如透明玻璃、塑膠、織物等(更嚴格地說,這些實際上是半透明物體,因為它們不必完全透明)。透明物體允許我們透過它們看到;因此,它們的顏色與它們後面的任何顏色“混合”。
如“OpenGL ES 2.0 管道”部分所述,片段著色器為每個片段計算一個 RGBA 顏色(即在gl_FragColor中為紅色、綠色、藍色和 alpha 分量)(除非片段被丟棄)。然後,如“每個片段的操作”部分中所述,對這些片段進行處理。其中一個操作是混合階段,它將片段的顏色(如gl_FragColor中指定的)與已在幀緩衝區中的相應畫素的顏色組合起來,稱為“源顏色”(因為混合後的顏色的“目標”是幀緩衝區),稱為“目標顏色”。
混合是一個固定功能階段,即您可以配置它,但不能程式設計它。配置方式是指定一個混合方程式。您可以將混合方程式視為對結果 RGBA 顏色的定義
vec4 result = SrcFactor * gl_FragColor + DstFactor * pixel_color;
其中pixel_color是當前在幀緩衝區中的 RGBA 顏色,result是混合後的結果,即混合階段的輸出。SrcFactor和DstFactor是可配置的 RGBA 顏色(型別為vec4),它們與片段顏色和畫素顏色按分量相乘。SrcFactor和DstFactor的值在 Unity 的 ShaderLab 語法中使用此行指定
Blend {SrcFactor的程式碼} {DstFactor的程式碼}
這兩個因素的最常見程式碼總結在下表中(更多程式碼在Unity 的 ShaderLab 關於混合的參考文件中提到)
| 程式碼 | 結果因子 (SrcFactor或DstFactor) |
|---|---|
One |
vec4(1.0)
|
Zero |
vec4(0.0)
|
SrcColor |
gl_FragColor
|
SrcAlpha |
vec4(gl_FragColor.a)
|
DstColor |
pixel_color
|
DstAlpha |
vec4(pixel_color.a)
|
OneMinusSrcColor |
vec4(1.0) - gl_FragColor
|
OneMinusSrcAlpha |
vec4(1.0 - gl_FragColor.a)
|
OneMinusDstColor |
vec4(1.0) - pixel_color
|
OneMinusDstAlpha |
vec4(1.0 - pixel_color.a)
|
如“向量和矩陣操作”部分中所述,vec4(1.0)只是vec4(1.0, 1.0, 1.0, 1.0)的簡寫。另外請注意,混合方程式中所有顏色和因子的所有分量都在 0 到 1 之間。
混合方程式的具體示例稱為“alpha 混合”。在 Unity 中,它以這種方式指定
Blend SrcAlpha OneMinusSrcAlpha
對應於
vec4 result = vec4(gl_FragColor.a) * gl_FragColor + vec4(1.0 - gl_FragColor.a) * pixel_color;
這使用gl_FragColor的 alpha 分量作為不透明度。也就是說,片段顏色越不透明,其不透明度及其 alpha 分量就越大,因此混合到結果中的片段顏色越多,混合到幀緩衝區中的畫素顏色就越少。一個完全不透明的片段顏色(即 alpha 分量為 1 的顏色)將完全替換畫素顏色。
此混合方程式有時被稱為“over”操作,即“gl_FragColor over pixel_color”,因為它對應於在畫素顏色之上放置具有特定不透明度的片段顏色層。(想想一層彩色玻璃或彩色半透明塑膠放在另一顏色的東西之上。)
由於 alpha 混合的普及,即使沒有使用 alpha 混合,顏色的 alpha 分量通常也稱為不透明度。此外,請注意,在計算機圖形學中,透明度的常見正式定義是1 - 不透明度。
alpha 混合有一個重要的變體:有時片段顏色已將其 alpha 分量預乘到顏色分量中。(您可能會將其視為已包含增值稅的價格。)在這種情況下,alpha 不應再次相乘(增值稅不應再次新增),正確的混合是
Blend One OneMinusSrcAlpha
對應於
vec4 result = vec4(1.0) * gl_FragColor + vec4(1.0 - gl_FragColor.a) * pixel_color;
混合方程式的另一個示例是
Blend One One
這對應於
vec4 result = vec4(1.0) * gl_FragColor + vec4(1.0) * pixel_color;
這只是將片段顏色新增到幀緩衝區中的顏色。請注意,alpha 分量根本沒有使用;儘管如此,此混合方程式對於許多型別的透明效果非常有用;例如,它通常用於粒子系統,當它們表示火或其他透明且發光的物體時。在“與順序無關的透明度”部分中更詳細地討論了疊加混合。
更多混合方程式的示例在Unity 的 ShaderLab 關於混合的參考文件中給出。
這是一個簡單的著色器,它使用 alpha 混合來渲染不透明度為 0.3 的綠色。
Shader "GLSL 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
GLSLPROGRAM
#ifdef VERTEX
void main()
{
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = vec4(0.0, 1.0, 0.0, 0.3);
// the fourth component (alpha) is important:
// this is semitransparent green
}
#endif
ENDGLSL
}
}
}
除了上面討論的混合方程式之外,只有兩行需要更多解釋: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 "GLSL shader using blending (including back faces)" {
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
GLSLPROGRAM
#ifdef VERTEX
void main()
{
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 0.3);
// the fourth component (alpha) is important:
// this is semitransparent red
}
#endif
ENDGLSL
}
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
// standard blend equation "source over destination"
GLSLPROGRAM
#ifdef VERTEX
void main()
{
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor = vec4(0.0, 1.0, 0.0, 0.3);
// fourth component (alpha) is important:
// this is semitransparent green
}
#endif
ENDGLSL
}
}
}
在這個著色器中,第一個傳遞使用正面剔除(使用 `Cull Front`)來先渲染背面(內部)。 之後,第二個傳遞使用背面剔除(使用 `Cull Back`)來渲染正面(外部)。 這對於凸面網格(沒有凹陷的封閉網格;例如球體或立方體)非常有效,並且通常是其他網格的良好近似。
恭喜你完成了本教程! 關於渲染透明物體的一件有趣的事情是,它不僅僅與混合有關,還需要了解剔除和深度緩衝區。 具體來說,我們已經瞭解了
- 什麼是混合以及如何在 Unity 中指定它。
- 帶有透明和不透明物件的場景是如何渲染的,以及如何在 Unity 中將物體分類為透明或不透明。
- 如何在 Unity 中渲染透明物體的內部和外部,特別是如何指定兩個傳遞。
如果你還想了解更多
- 關於 OpenGL 管道,你應該閱讀“OpenGL ES 2.0 管道”部分。
- 關於 OpenGL 管道中的每個片段操作(例如混合和深度測試),你應該閱讀“每個片段操作”部分。
- 關於正面剔除和背面剔除,你應該閱讀“剖面圖”部分。
- 關於如何在 Unity 中指定剔除和深度緩衝區功能,你應該閱讀 Unity 的 ShaderLab 關於剔除和深度測試的參考。
- 關於如何在 Unity 中指定混合,你應該閱讀 Unity 的 ShaderLab 關於混合的參考。
- 關於 Unity 中的渲染佇列,你應該閱讀 Unity 的 ShaderLab 關於子著色器標籤的參考。