GLSL 程式設計/Unity/螢幕疊加

本教程涵蓋螢幕疊加,在 Unity 中也被稱為“GUI 紋理”。
這是關於非標準頂點變換的教程系列的第一個教程,這些教程偏離了在“頂點變換”部分中描述的標準頂點變換。本教程使用在“紋理球體”部分中描述的紋理以及在“透明度”部分中描述的混合。
螢幕疊加(即 Unity 術語中的 GUI 紋理)有很多應用,例如左側影像中的標題,還有其他 GUI(圖形使用者介面)元素,如按鈕或狀態資訊。這些元素的共同特徵是它們應該始終出現在場景的頂部,並且永遠不會被任何其他物件遮擋。這些元素也不應該受到任何相機移動的影響。因此,頂點變換應該直接從物體空間到螢幕空間。Unity 的 GUI 紋理允許我們透過在螢幕上指定位置渲染紋理影像來渲染這種元素。本教程嘗試藉助著色器來重現 GUI 紋理的功能。通常,你仍然會使用 GUI 紋理而不是這種著色器;但是,著色器允許更大的靈活性,因為你可以根據自己的需要對其進行任何方式的調整,而 GUI 紋理只提供有限的可能性。(例如,你可以更改著色器,使 GPU 在對被不透明 GUI 紋理遮擋的三角形進行光柵化時花費更少的時間。)
Unity 的 GUI 紋理的位置由渲染矩形的左下角的X 和Y 座標(以畫素為單位)指定,其中 在螢幕中心,以及渲染矩形的寬度 和高度(以畫素為單位)。為了模擬 GUI 紋理,我們使用類似的著色器屬性
Properties {
_MainTex ("Texture", Rect) = "white" {}
_Color ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_X ("X", Float) = 0.0
_Y ("Y", Float) = 0.0
_Width ("Width", Float) = 128
_Height ("Height", Float) = 128
}
以及相應的 uniforms
uniform sampler2D _MainTex;
uniform vec4 _Color;
uniform float _X;
uniform float _Y;
uniform float _Width;
uniform float _Height;
對於實際物件,我們可以使用一個僅包含兩個三角形的網格來形成一個矩形。但是,我們也可以只使用預設的立方體物件,因為背面剔除(以及退化為邊的三角形的剔除)允許我們確保僅對立方體的兩個三角形進行光柵化。預設立方體物件的角在物體空間中的座標為 和 ,即矩形的左下角在,右上角在。為了將這些座標變換為螢幕空間中使用者指定的座標,我們首先將它們變換為畫素中的光柵位置,其中 在螢幕的左下角
uniform vec4 _ScreenParams; // x = width; y = height;
// z = 1 + 1.0/width; w = 1 + 1.0/height
...
#ifdef VERTEX
void main()
{
vec2 rasterPosition = vec2(
_X + _ScreenParams.x / 2.0
+ _Width * (gl_Vertex.x + 0.5),
_Y + _ScreenParams.y / 2.0
+ _Height * (gl_Vertex.y + 0.5));
...
這種變換將我們立方體正面左下角從物體空間中的 變換為光柵位置vec2(_X + _ScreenParams.x / 2.0, _Y + _ScreenParams.y / 2.0),其中_ScreenParams.x 是螢幕寬度(以畫素為單位),_ScreenParams.y 是高度(以畫素為單位)。右上角從 變換為vec2(_X + _ScreenParams.x / 2.0 + _Width, _Y + _ScreenParams.y / 2.0 + _Height)。光柵位置很方便,實際上它們經常在 OpenGL 中使用;但是,它們並不是我們這裡真正需要的。
頂點著色器在gl_Position中的輸出是在所謂的“裁剪空間”中,如“頂點變換”部分所述。GPU 透過將這些座標除以透視除法的第四個座標gl_Position.w來將它們變換為 和 之間的歸一化裝置座標。如果我們將此第四個座標設定為 ,則此除法不會改變任何內容;因此,我們可以將gl_Position的前三個座標視為歸一化裝置座標中的座標,其中 指定了近平面上的螢幕左下角,而 指定了近平面上的螢幕右上角。(我們應該使用近平面以確保矩形位於其他所有內容的前面。)為了在gl_Position中指定任何螢幕位置,我們必須在此座標系中指定它。幸運的是,將光柵位置轉換為歸一化裝置座標並不太難。
gl_Position = vec4(
2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
2.0 * rasterPosition.y / _ScreenParams.y - 1.0,
-1.0, // near plane
1.0 // all coordinates are divided by this coordinate
);
正如你很容易驗證的那樣,這將光柵位置vec2(0,0)轉換為歸一化裝置座標 ,並將光柵位置vec2(_ScreenParams.x, _ScreenParams.y)轉換為 ,這正是我們需要的。
這對於從物件空間到螢幕空間的頂點變換來說已經足夠了。但是,我們仍然需要計算適當的紋理座標,以便在正確的位置查詢紋理影像。紋理座標應該介於 和 之間,這實際上很容易從物件空間中介於 和 之間的頂點座標中計算出來。
textureCoords =
vec4(gl_Vertex.x + 0.5, gl_Vertex.y + 0.5, 0.0, 0.0);
// for a cube, gl_Vertex.x and gl_Vertex.y
// are -0.5 or 0.5
使用變化的變數textureCoords,我們就可以使用一個簡單的片段程式在紋理影像中查詢顏色,並用使用者指定的顏色_Color進行調製。
#ifdef FRAGMENT
void main()
{
gl_FragColor =
_Color * texture2D (_MainTex, vec2(textureCoords));
}
#endif
就是這樣。
完整的著色器程式碼
[edit | edit source]如果我們將所有部分放在一起,我們將得到以下著色器,它使用Overlay佇列在所有其他內容之後渲染物件,並使用 alpha 混合(見“透明度”部分)以允許透明紋理。它還停用深度測試以確保紋理永遠不會被遮擋。
Shader "GLSL shader for screen overlays" {
Properties {
_MainTex ("Texture", Rect) = "white" {}
_Color ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_X ("X", Float) = 0.0
_Y ("Y", Float) = 0.0
_Width ("Width", Float) = 128
_Height ("Height", Float) = 128
}
SubShader {
Tags { "Queue" = "Overlay" } // render after everything else
Pass {
Blend SrcAlpha OneMinusSrcAlpha // use alpha blending
ZTest Always // deactivate depth test
GLSLPROGRAM
// User-specified uniforms
uniform sampler2D _MainTex;
uniform vec4 _Color;
uniform float _X;
uniform float _Y;
uniform float _Width;
uniform float _Height;
// The following built-in uniforms
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec4 _ScreenParams; // x = width; y = height;
// z = 1 + 1.0/width; w = 1 + 1.0/height
// Varyings
varying vec4 textureCoords;
#ifdef VERTEX
void main()
{
vec2 rasterPosition = vec2(
_X + _ScreenParams.x / 2.0
+ _Width * (gl_Vertex.x + 0.5),
_Y + _ScreenParams.y / 2.0
+ _Height * (gl_Vertex.y + 0.5));
gl_Position = vec4(
2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
2.0 * rasterPosition.y / _ScreenParams.y - 1.0,
-1.0, // near plane is -1.0
1.0);
textureCoords =
vec4(gl_Vertex.x + 0.5, gl_Vertex.y + 0.5, 0.0, 0.0);
// for a cube, gl_Vertex.x and gl_Vertex.y
// are -0.5 or 0.5
}
#endif
#ifdef FRAGMENT
void main()
{
gl_FragColor =
_Color * texture2D (_MainTex, vec2(textureCoords));
}
#endif
ENDGLSL
}
}
}
當你將此著色器用於立方體物件時,紋理影像可能會根據相機的方向而出現和消失。這是由於 Unity 的裁剪造成的,Unity 不會渲染完全位於場景中相機可見區域(視錐體)之外的物件。此裁剪基於遊戲物件的傳統變換,這對我們的著色器來說沒有意義。為了停用此裁剪,我們可以簡單地使立方體物件成為相機的子物件(透過將它拖到層次結構檢視中的相機上)。如果立方體物件隨後被放置在相機的前面,它將始終保持在相同的相對位置,因此它不會被 Unity 裁剪。(至少在遊戲檢視中不會。)
不透明螢幕疊加的更改
[edit | edit source]對著色器可以進行許多更改,例如不同的混合模式或不同的深度,以使 3D 場景中的幾個物件位於疊加層前面。在這裡,我們只關注不透明疊加層。
不透明螢幕疊加層會遮擋場景中的三角形。如果 GPU 瞭解此遮擋,它就不需要對這些被遮擋的三角形進行光柵化(例如,透過使用延遲渲染或早期深度測試)。為了確保 GPU 有機會應用這些最佳化,我們必須首先渲染螢幕疊加層,方法是設定
Tags { "Queue" = "Background" }
此外,我們應該避免混合,方法是移除Blend指令。透過這些更改,不透明螢幕疊加層可能會提高效能,而不是造成光柵化效能損失。
總結
[edit | edit source]恭喜您完成了另一個教程。我們已經看到了
- 如何使用 GLSL 著色器模擬 GUI 紋理。
- 如何修改著色器以實現不透明螢幕疊加。
如果您還想了解更多