寫shader小細節——這個會不斷更新


這個是因為自己被自己蠢哭了動筆的,里面大概記錄自己所犯的錯,和一些小知識。

1.有一個錯誤我經常犯:內部定義的字段沒對應開放到編輯器的字段。這個是由於我有點依賴ide寫代碼的習慣導致,而shader的ide只提供了基本的關鍵字高亮。

  糾正方法:對一個字段,一直使用拷貝粘貼的方式進行書寫。

  cg教程給了一個寫cg代碼的tip:

  

2.法線沒歸一化正確,導致效果奇奇怪怪,這個我是在寫matcap那里犯的錯:漏了這句:

o.twoUv.zw = o.twoUv.zw * 0.5 + 0.5;//(-1,1)->(0,1)

3.Tiling和Offset:

  Tiling:縮放模型UV紋理采樣坐標,比如Tiling = 2,表示把模型UV坐標的U擴大2倍,U范圍變成了0-2

    [此時模型U=[0,1]就采用了整個紋理,即模型的左半使用整個紋理;模型U = (1,2]的采樣值使用紋理溢出填充值,看下面]

  Offset:偏移模型UV紋理采用坐標;比如Offset x=0.1表示把UV紋理坐標往左偏移,然后再采樣,
  至於超出UV紋理的UV坐標采樣的顏色值(紋理溢出填充值),則依賴UV紋理的WrapMode
  具體實現:
  float4 _MainTex_ST;//紋理縮放偏移向量(Unity默認此變量賦值,變量命名規則:紋理名_ST)
  vert()
  {
  ...
  //第一種方式:
  o.uv = v.vertex.xy * _MainTex_ST.xy + _MainTex_ST.zw;
  //第二種方式:使用內建宏,這只是對第一種的封裝
  o.uv = TRANSFORM_TEX(v.vertex,_MainTex);
  }

 4.shader的幾個優化tips:

  1.gpu是並行運算,即運算一個float和運算float4是一樣代價的,所以:

    float4 color;

    color.rgb = color.rgb*a;

    color.a = color.a*b;

    可以改成:float4 color; color = color*float4(a,a,a,b)

  2.少用if else,用step+乘法代替:

    

     step的定義圖

     如:

    if (a >= b)
    {
        c = 1;
    }
    else
    {  
        c = 2;
    }
    //可以用下面的代替
      tmp = step(b,a)
      c = tmp + 2 *(1-tmp);

    這里講一下步驟:

    第一步,參照step的定義圖,我們需要先想方設法把if內的內容搞成 a >= b或者a <= b,然后分析一下if else里面所求內容,寫成可以根據step得到的0或1進行計算的表達式,因為shader的判斷邏輯一般比較簡單,所以這一步不會特別難,然后就ok了,下面讓我們來做一下:

    改寫:

    if(a && b)

     {   c = 1;  }

    else{c = 2;}

    a&&b可以用乘法來代替,即if (a*b <=0)[這里也不嚴謹,如果a,b是負數就不對了,但很少會是負數],所以我們對比step的定義圖可以得到:

     tmp = step(a*b,0); 

     c = tmp + 2 * (1-tmp);

  3.如果能用fixed(-2,2)就用fixed,不然用half,最后才考慮用float,是幾倍的性能之差。

  4.使用紋理來編碼函數,即控制貼圖,可以用很小的貼圖然后通過插值獲得大范圍的數據,這些都是GPU硬件支持的,節省了GPU處理周期。

  5.只渲染必須着色的片段。比如可以預先打開深度測試,然后再對經過測試的片段進行fragment shader指令。

 5.

  • float4 _Time : Time (t/20, t, t*2, t*3), use to animate things inside the shaders

    

 6.記一次svn從主干merge .unity場景文件到分支的坑:

    1.必須文件級別merge,不能文件夾級別merge,也就是說merget from的目錄要詳細到該文件

    2.merge后必須在分支打開該.unity文件,看是否和主干一致,我發現修改一些obj的static屬性沒有同步過去

   總結:.unity文件不知具有何特殊性,svn merge時需要謹慎對待,這次問題出乎我意料,嚴重影響了出包時間。

 

7.GrabPass捕捉屏幕紋理

Shader "Custom/GrabVF" {
 Properties {
  //_MainTex ("Base (RGB)", 2D) = "white" {}
 }
 SubShader {
  // 在所有不透明對象之后繪制自己,更加靠近屏幕
  Tags{"Queue"="Transparent"}
  // 通道1:捕捉對象之后的屏幕內容放到_GrabTexture紋理中
  GrabPass{} 
  // 通道2:設置材質
  Pass{
   Name "pass2"
   CGPROGRAM
   #pragma vertex vert
   #pragma fragment frag
   #include "UnityCG.cginc"
   sampler2D _GrabTexture;
   float4 _GrabTexture_ST;
   struct v2f 
  {
      float4  pos : POSITION; // 輸入的模型空間中,頂點坐標信息
      float4  uv : TEXCOORD0; // 材質信息也包含了xyzw,通常只用xy,但是這里由頂點生成
    };
   v2f vert (appdata_base v)
    {
     v2f o;
     // 從模型坐標-世界坐標-視坐標-(視覺平截體乘以投影矩陣並進行透視除法)-剪裁坐標
     o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
     //o.uv = TRANSFORM_TEX(v.texcoord, _GrabTexture);// UV紋理坐標集信息來自屏幕樣本對象,如果用這個uv采樣,就把全屏的紋理顯示到當前物體上了。
     float4 screenUV = ComputeGrabScreenPos(o.pos);//計算該模型頂點在屏幕坐標的紋理信息,,_GrabTexture得到的是全屏紋理,要根據當前模型所在位置進行采樣,顯示的是物體背后的屏幕紋理而不是全屏,這個函數輸入的是在[-w,w]立方體中的坐標,輸出的是[0,w]立方體中的坐標,所以下面還要/w
     o.uv = screenUV.xy/screenUV.w;
     return o;
    }
   float4 frag (v2f i) : COLOR
   {
    // 對_GrabTexture紋理進行取樣,進行2D紋理映射查找
    half4 texCol = tex2D(_GrabTexture, i.uv);
    // 顏色反相,便於觀察效果
    return 1 - texCol;
   }
   ENDCG
  }
 }
 FallBack "Diffuse"
}

  當然也可以把相機rtt到一個tex中,然后把tex設給物體的shader,這樣也能獲得屏幕紋理了。

8.語義可以在結構里修飾變量,也可以直接修飾變量

struct app_data
{
    float4 pos: POSITION;
}
void vert(app_data v){}
void vert(float4 pos:POSITION){}

 語義是一種黏合劑,它把流水線各個階段的數據連接起來,它指明了數據對應的硬件資源,只有入口函數(頂點函數/片段函數)才使用語義,內部函數(庫or自定義的)不能使用。

但同一個語義在不同階段(輸入、輸出、頂點、片段)不是一樣的,它只是連接“不同階段”,比如應用程序的輸出和頂點函數的輸入,頂點函數的輸出和片段函數的輸入,使得后者可以去相應硬件取得前者的值並生產自己的值,比如片段函數的輸入的POSITION其實是頂點函數輸出的POSITION插值后的數值,並不一致。

 

9.swizzle重組操作符是一個圓點.:

float4 a;

a.z;a.wz;等,這個操作符效率很高,被硬件支持。

 矩陣重組:

  float a; float3 b;

  float4x4 matrix;

  a = matrix._m32;b = matrix._m32_m00_m11;也可以數組提取b = matrix[0].xyz;

寫入掩碼:float4 a = (0,0,0,00;float2 b = (1,1); a.xz = b;這里不能寫成a.zz,即寫入掩碼不能重復。

 

10.可以用使用“out”、“inout”、“in”等進行輸入輸出標識,無限制是默認是in, out可以用來輸出更多變量,但感覺這個用處不大,畢竟使用結構體就能夠輸出多個。

 

11.,即較老的機器可能出乎意料。

12.基本的片段profile(即早期)只能用一個給定的紋理坐標集存取它對應的紋理,即一個紋理一次只能讀取采樣一次,下面是不支持的:

  float4 col1 = tex2D(_tex, uv1);

  float4 col2 = tex2D(_tex, uv2);

  需要搞成:tex2D(_tex, uv1);tex2D(_tex1, uv2);其中_tex和_tex1綁定同一紋理

 

13.diffuse = Kd * Max(dot(N, L), 0);

  specular = Ks * (Max(dot(N, H), 0)^shininess;其中H 是規范化V和L的半角向量:H = normalize(V+L);

 

14.頂點shader 訪問的光照向量,視線向量,是物體空間嗎?應該是了,畢竟Unity的光照函數里,直接用之和模型的法線進行計算了,而那個法線是物體空間的。

 

15.有人在知乎問如何評測一個shader的性能,我摘取了葉勁峰老師的答案片段:

  游戲過程有很多變數,例如渲染1個接近鏡頭的NPC和渲染10個遠離鏡頭的NPC,其性能分別難以預測。瓶頸經常會改變。

  有一些shader是較容易評測的,例如,全屏后期處理的瓶頸在於pixel shader、紋理采樣和帶寬,其運行復雜度與屏幕分辨率成正比。

  對於其他shader,最簡單的評測方式是,觀察shader源代碼編譯成匯編之后的一般指令及紋理采樣指令數目。這是一個非常粗糙的評測方式,但可以用於作一些簡單的統計,找出那些可能有問題的shader。這適合像UE給美術隨意創建shader的情況。

  但在游戲開發中,我看到一般的做法是,以執行游戲來作整體評測及優化,而不是單獨評測各個部分。一方面是因為游戲有很多變數,另一方面是因為人力成本。所以通常會做一些自動評測整體性能的測試,例如讓Bot在場景中行走,記錄整體的幀率、CPU/GPU時間、draw-call等。如自動測試程序發現超出預期的數值,就發電郵通知團隊。這種測試每天自動執行,可畫出按天數的性能圖表,知道開發及優化的整體情況。

  這種做法大概也可以推廣到其他方面的自動化監控。

 

16.把對各個頂點數值不同的計算放shader里,把對各個頂點數值相同的變量放cpu里,然后傳給shader

 

17.許多實現可以在頂程序點實現,也可以在片段程序實現,一般地,在后者實現能提高效果,但前者實現能提高性能,要斟酌。另外,如果該變量在頂點間是線性變化的,應該在頂點shader里計算,或者變化不快(如漫反射系數),可以在頂點shader里計算,如果變化很快(如高亮系數),應該在片段shader里計算,實踐見真知。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM