跳轉到內容

Cg 程式設計/Unity/螢幕覆蓋

來自華夏公益教科書,開放世界開放書籍
1934 年電影的片頭字幕。

本教程介紹 **螢幕覆蓋**。

它是關於非標準頂點變換的一系列教程中的第一個教程,這些教程偏離了在 “頂點變換”部分 中描述的標準頂點變換。本教程使用在 “紋理球體”部分 中描述的紋理,以及在 “透明度”部分 中描述的混合。

螢幕覆蓋

[編輯 | 編輯原始碼]

螢幕覆蓋有很多應用,例如左側圖片中的標題,以及其他 GUI(圖形使用者介面)元素,如按鈕或狀態資訊。這些元素的共同特點是它們應該始終顯示在場景的頂部,並且永遠不會被任何其他物體遮擋。這些元素也不應該受到任何相機移動的影響。因此,頂點變換應該直接從物體空間到螢幕空間。Unity 有多種方法可以在螢幕上的指定位置渲染紋理影像。本教程嘗試使用一個簡單的著色器來實現此目的。

使用 Cg 著色器將紋理渲染到螢幕

[編輯 | 編輯原始碼]

讓我們使用 XY 座標來指定紋理在螢幕上的位置,這兩個座標表示渲染矩形的左下角的畫素座標,其中 在螢幕中心,以及渲染矩形的畫素 WidthHeight。(相對於中心指定座標通常允許我們在不進行進一步調整的情況下支援各種螢幕尺寸和縱橫比。)我們使用這些著色器屬性

   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
   }

以及相應的制服

         uniform sampler2D _MainTex;
         uniform float4 _Color;
         uniform float _X;
         uniform float _Y;
         uniform float _Width;
         uniform float _Height;

對於實際的物體,我們可以使用一個網格,它只包含兩個三角形來形成一個矩形。但是,我們也可以直接使用預設的立方體物體,因為背面剔除(以及將三角形退化為邊的剔除)可以確保只有立方體的兩個三角形被光柵化。預設立方體物體的角點在物體空間中的座標為 ,即矩形的左下角位於 ,而右上角位於 。為了將這些座標變換為使用者指定的螢幕空間座標,我們首先將它們變換為畫素的光柵位置,其中 位於螢幕的左下角

         uniform float4 _ScreenParams; // x = width; y = height; 
            // z = 1 + 1.0/width; w = 1 + 1.0/height
         ...
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float2 rasterPosition = float2(
               _X + _ScreenParams.x / 2.0 
               + _Width * (input.vertex.x + 0.5),
               _Y + _ScreenParams.y / 2.0 
               + _Height * (input.vertex.y + 0.5));
            ...

此變換將我們立方體正面面的左下角從物體空間中的 變換為光柵位置 float2(_X + _ScreenParams.x / 2.0, _Y + _ScreenParams.y / 2.0),其中 _ScreenParams.x 是螢幕寬度(以畫素為單位),_ScreenParams.y 是螢幕高度(以畫素為單位)。右上角從 變換為 float2(_X + _ScreenParams.x / 2.0 + _Width, _Y + _ScreenParams.y / 2.0 + _Height)。光柵位置很方便,事實上它們經常在 OpenGL 中使用;但是,它們並不是我們這裡真正需要的。

頂點著色器的輸出引數位於所謂的“裁剪空間”中,如“頂點變換”部分所述。GPU 透過將這些座標除以透視除法中的第四個座標,將這些座標轉換為 之間的標準化裝置座標。如果我們將此第四個座標設定為,則此除法不會改變任何內容;因此,我們可以將前三個座標視為標準化裝置座標中的座標,其中 指定了近平面上的螢幕左下角,而 指定了近平面上的螢幕右上角。為了將任何螢幕位置指定為頂點輸出引數,我們必須在此座標系中指定它。幸運的是,將光柵位置的 座標轉換為標準化裝置座標並不太難。對於 座標,我們希望使用近裁剪平面的座標。在 Unity 中,這取決於平臺;因此,我們使用 Unity 的內建統一變數 _ProjectionParams.y,它指定了近裁剪平面的 座標。

            output.pos = float4(
               2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
               2.0 * rasterPosition.y / _ScreenParams.y - 1.0,
               _ProjectionParams.y, // near plane is at -1.0 or at 0.0
               1.0);

正如您很容易檢查到的那樣,這將光柵位置 float2(0,0) 轉換為標準化裝置座標,並將光柵位置 float2(_ScreenParams.x, _ScreenParams.y) 轉換為,這正是我們需要的。

還有一個複雜之處:有時 Unity 使用翻轉的投影矩陣,其中 軸指向相反的方向。在這種情況下,我們必須將 座標乘以 -1。我們可以透過將它乘以 _ProjectionParams.x 來實現這一點。

            output.pos = float4(
               2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
               _ProjectionParams.x * (2.0 * rasterPosition.y / _ScreenParams.y - 1.0),
               _ProjectionParams.y, // near plane is at -1.0 or at 0.0
               1.0);

這就是從物件空間到螢幕空間的頂點變換所需的一切。但是,我們仍然需要計算合適的紋理座標,以便在正確的位置查詢紋理影像。紋理座標應介於 之間,這實際上很容易從物件空間中 之間的頂點座標中計算出來。

            output.tex = float4(input.vertex.x + 0.5, 
               input.vertex.y + 0.5, 0.0, 0.0);
               // for a cube, vertex.x and vertex.y 
               // are -0.5 or 0.5

有了頂點輸出引數 tex,我們就可以使用簡單的片段程式在紋理影像中查詢顏色,並將其與使用者指定的顏色 _Color 相乘。

         float4 frag(vertexOutput input) : COLOR
         {
            return _Color * tex2D(_MainTex, input.tex.xy);   
         }

就是這樣。

完整的著色器程式碼

[edit | edit source]

如果將所有部分放在一起,我們將得到以下著色器,它使用 Overlay 佇列在所有其他內容之後渲染物件,並使用 alpha 混合(參見“透明度”部分)以允許使用透明紋理。它還停用深度測試,以確保紋理永遠不會被遮擋。

Shader "Cg 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
 
         CGPROGRAM
 
         #pragma vertex vert  
         #pragma fragment frag

         #include "UnityCG.cginc" 
           // defines float4 _ScreenParams with x = width;  
           // y = height; z = 1 + 1.0/width; w = 1 + 1.0/height
           // and defines float4 _ProjectionParams 
           // with x = 1 or x = -1 for flipped projection matrix;
           // y = near clipping plane; z = far clipping plane; and
           // w = 1 / far clipping plane
 
         // User-specified uniforms
         uniform sampler2D _MainTex;
         uniform float4 _Color;
         uniform float _X;
         uniform float _Y;
         uniform float _Width;
         uniform float _Height;
 
         struct vertexInput {
            float4 vertex : POSITION;
            float4 texcoord : TEXCOORD0;
         };
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 tex : TEXCOORD0;
         };
 
         vertexOutput vert(vertexInput input) 
         {
            vertexOutput output;
 
            float2 rasterPosition = float2(
               _X + _ScreenParams.x / 2.0 
               + _Width * (input.vertex.x + 0.5),
               _Y + _ScreenParams.y / 2.0 
               + _Height * (input.vertex.y + 0.5));
            output.pos = float4(
               2.0 * rasterPosition.x / _ScreenParams.x - 1.0,
               _ProjectionParams.x * (2.0 * rasterPosition.y / _ScreenParams.y - 1.0),
               _ProjectionParams.y, // near plane is at -1.0 or at 0.0
               1.0);
 
            output.tex = float4(input.vertex.x + 0.5, 
               input.vertex.y + 0.5, 0.0, 0.0);
               // for a cube, vertex.x and vertex.y 
               // are -0.5 or 0.5
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR
         {
            return _Color * tex2D(_MainTex, input.tex.xy);   
         }
 
         ENDCG
      }
   }
}

當您將此著色器用於立方體物件時,紋理影像可能會根據攝像機的方向出現和消失。這是由於 Unity 的裁剪造成的,Unity 不會渲染完全位於攝像機可見場景區域(視錐體)之外的物件。這種裁剪是基於對遊戲物件的常規變換,這對於我們的著色器來說沒有意義。為了停用此裁剪,我們可以簡單地將立方體物件設定為攝像機的子物件(透過將其拖到層次結構視窗中的攝像機上)。如果立方體物件隨後放置在攝像機前面,它將始終保持在相同的相對位置,因此不會被 Unity 裁剪。(至少在遊戲檢視中不會。)

不透明螢幕覆蓋的更改

[edit | edit source]

著色器的許多更改都是可以想象的,例如不同的混合模式或不同的深度,以便在覆蓋層前面擁有 3D 場景的幾個物件。在這裡,我們只關注不透明覆蓋層。

不透明的螢幕覆蓋將遮擋場景中的三角形。如果GPU能夠識別這種遮擋,它就不需要光柵化這些被遮擋的三角形(例如,透過使用延遲渲染或早期深度測試)。為了確保GPU有機會應用這些最佳化,我們必須先渲染螢幕覆蓋,方法是設定

標籤 { "佇列" = "背景" }

此外,我們應該避免混合,方法是刪除 `混合` 指令。透過這些更改,不透明的螢幕覆蓋更有可能提高效能,而不是降低光柵化效能。

恭喜你完成了另一個教程。我們已經看到了

  • 如何使用Cg著色器渲染螢幕覆蓋。
  • 如何修改不透明螢幕覆蓋的著色器。

進一步閱讀

[編輯 | 編輯原始碼]

如果你還想了解更多關於

< Cg程式設計/Unity

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