1. 程式人生 > 其它 >第4篇 Unity中各種shader - Unity 3D ShaderLab開發實踐

第4篇 Unity中各種shader - Unity 3D ShaderLab開發實踐

第4篇 Unity中各種shader - Unity 3D ShaderLab開發實踐

15 Pass的通用指令開關

15.1 LOD

Unity LOD-Level of Detail(多層次細節)用法教程 - Chinar - 部落格園 (cnblogs.com)

shader中的實現方式:

指令碼中的控制:

using UnityEngine;

public class_SetShaderLOD : MonoBehaviour {
    public Shader myShader;
    // ...
    private float val = 6;
    void Update() {
        myShader.maximumLOD = (int)val * 100; // 設定shader的最大LOD數值
    }
}

內建的LOD示例:

15.2 渲染佇列

RenderQueue會改變物體渲染的先後順序,但並不會改變物體的空間位置。對於透明物體,混合材質,RenderQueue的數值對最終的渲染輸出都比較敏感。

15.3 透明的產生

Alpha檢測用於在fragment函式完成最終的計算之後,在即將寫入到幀中之前,通過和一個固定的數值比較,來決定當前fragment函式的計算結果到底要不要寫入到幀中,從而輸出到螢幕。通過Alpha test可以實現半透效果繪製。

15.4 混合操作

與OpenGL的blend類似。還支援BlendOp選項,可以在通常的加操作之外執行Max,Min,Sub以及RevSub這4種操作。

15.5 Color Mask

colormask的作用是指定渲染結果的輸出通道,而不是通常情況下的RGBA這四個通道都會被寫入。

15.6 ZTest 深度測試

ZTest:深度測試,開啟後測試結果決定片元是否被捨棄,可配置

ZTest可設定的測試規則:

  • ZTest Less:深度小於當前快取則通過
  • ZTest Greater:深度大於當前快取則通過
  • ZTest LEqual:深度小於等於當前快取則通過
  • ZTest GEqual:深度大於等於當前快取則通過
  • ZTest Equal:深度等於當前快取則通過
  • ZTest NotEqual:深度不等於當前快取則通過
  • ZTest Always:不論如何都通過

15.7 對Z深度的偏移

Unity Shader - Offset 的測試,解決簡單的z-fighting情況_Jave.Lin 的學習筆記-CSDN部落格

z-fighting(直譯,就是z值的競爭)

原因是因為我們的不同的多邊形共面時,在光柵階段生成的fragment的螢幕xy座標一樣,但depth值又不一樣的浮點誤差引起的問題,然後浮點round-off的四捨五入,導致z-fighting

glPolygonOffset(GLfloat factor, GLfloat units);

當啟用Offset的時候,每個片段的深度都需要加上一個offset值,offset操作需要在進行深度測試和深度寫入之前執行。offset的值的計算公式如下:

offset = m * factor + r * units

其中,m是多邊形最大的深度斜率,r是視窗座標系下深度可識別的最小解析度,r是特定實現的常量。

當offset > 0的時候,好比將物體推動遠離你;offset < 0是,將物體拉近你。遍歷多邊形時,depth slope是z值的改變數除以x或y軸的改變數,depth是視窗座標系下[0,1]的範圍,

15.8 面的剔除操作

【Unity Shader】 Cull(表面剔除)_讚美月亮的專欄-CSDN部落格

15.9 自動貼圖座標的生成

15.10 抓屏操作

15.11 Fog

17 SurfaceShader

可以通過exclude_path排除特定的renderpass,如:

lighting model見:Custom Lighting models in Surface Shaders_Passion 的部落格-CSDN部落格

17.3 Forward渲染路徑下的SurfaceShader

常規的程式碼實現如下:

Shader "forwardSurf" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BumpMap ("Bumpmap", 2D) = "bump" {}
		_ColorTint ("Tint", COlor) = (1.0, 0.6, 0.6, 1.0)
		_FogColor ("Fog Color", Color) = (0.3, 0.4, 0.7, 1.0)
	}
	SubShader {
		Tags {"RenderType"="Opaque"}
		LOD 200
		CGPROGRAM
		#pragma surface surf LitModel exclude_path:prepass vertex:vert finalcolor:mycolor
		#pragma debug
		
		sampler2D _MainTex;
		sampler2D _BumpMap;
		fixed4 _ColorTint;
		fixed4 _FogColor;
		
		struct Input {
			float3 viewDir;
			float4 cc:COLOR;
			float4 screenPos;
			float3 worldPos;
			float3 worldRef1;
			float3 worldNormal;
			float2 uv_MainTex;
			float2 uv_BumpMap;
			half fog;
			INTERNAL_DATA
		};
		
		// 此函式會在Vertex函式中呼叫
		void vert(inout appdata_full v, out Input o)
		{
			float4 hpos = mul(UNITY_MATRIX_MVP, v.vertex);
			o.fog = min(1, dot(hpos.xy, hpos.xy)*0.1);
		}
		// Fragment中呼叫
		void surf(Input IN, inout SurfaceOutput o) {
			o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
			half4 c = tex2D(_MainTex, IN.uv_MainTex);
			o.Albedo = c.rgb;
			o.Alpha = c.a;
		}
		half4 LightingLitModel(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
		{
			#ifndef USING_DIRECTIONAL_LIGHT
			lightDir = normalize(lightDir);
			#endif
			viewDir = normalize(viewDir);
			half3 h = normalize(lightDir + viewDir);
			half diff = max(0, dot(s.Normal, lightDir));
			float nh = max(0, dot(s.Normal, h));
			float3 spec = pow(nh, s.Specular*128.0) * s.Gloss;
			half4 c;
			c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
			c.a = s.Alpha + _LightColor0.a * Luminace(spec) * atten; //????
			return c;
		}
		
		void mycolor(Input IN, SurfaceOutput o, inout fixed4 color)
		{
			color *= _ColorTint;
			fixed3 fogColor = _FogColor.rgb;
			#ifdef UNITY_PASS_FORWARDADD
			fogColor = 0;
			#endif
			color.rgb = lerp(color.rgb, fogColor, IN.fog);
		}
		
		ENDCG
	}
	Fallback Off
}

上面自定義的幾個函式,vert在vertex shader中最先呼叫,生成的Input結果傳遞給vertex shader,得到v2f_surf結果傳遞給fragment shader,fragment shader中,SurfaceOutput傳遞給出了surf的計算結果,LightingLitModel計算出一個顏色,最後被mycolor進行修改,成為最終fragment shader的輸出。

上面的實現是沒有光照貼圖部分的。

其他略。後面主要講了surfaceshader編譯後得到的vertex,fragment結果,TODO

18 凹凸材質

18.1 切空間

以你自己為中心所觀察到的世界就是切空間。所謂切空間就是法線Normal(從腳到頭的方向,Z),Tangent切方向(雙眼直視正前方,X),Binormal(水平抬起你的右胳膊,Y),這三個向量構造的空間。

18.2 凹凸貼圖

法線貼圖的資訊是表示在切空間中,因此光照計算需要在切空間中進行。

18.2.1 計算到切空間的矩陣

常見的實現方式如下:

Shader "Bump_1" {
	Properties {
		_BumpMap ("BumpMap", 2D) = "white" {}
	}
	SubShader {
		Pass {
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			float4 _LightColor0;
			sampler2D _BumpMap;
			struct v2f {
				float4 pos:SV_POSITION;
				float2 uv:TEXCOORD0;
				float3 lightDir:TEXCOORD1; // 傳遞到Fragment Shader中的一個切空間光源方向
			}
			v2f vert (appdata_full v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.texcoord.xy;
				float3 binormal = cross(v.normal,v.tangent)*v.tangent.w;
				float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal); // 切空間矩陣
				o.lightDir = mul(_World2Object, _WorldSpaceLightPos0).xyz; // 平行光
				o.lightDir = mul(rotation, o.lightDir); // 光源轉換到切向量空間
				return o;
			}
			float4 frag(v2f i):COLOR
			{
				float4 c=1;
				float4 packedN = tex2D(_BumpMap, i.uv);
				// 解DXT5nm壓縮格式
				float3 N = float3(2.0*packedN.wy-1,1.0);
				N.z = sqrt(1-N.x*N.x-N.y*N.y);
				float diff = max(0, dot(N, i.lightDir)); //計算漫反射
				c = _LightColor0*diff;
				return c*2; // 乘以2是為了和預設的Unity光照傳統保持一致,這個是unity早期為了更容易的產生過度曝光人為加上去的
			}
			ENDCG
		}
	}
}

unity對切空間計算的支援:

struct v2f {
    float4 pos:SV_POSITION;
    float2 uv:TEXCOORD0;
    float3 lightDir:TEXCOORD1; // 傳遞到Fragment Shader中的一個切空間光源方向
}
v2f vert (appdata_full v) {
    v2f o;
    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    o.uv = v.texcoord.xy;
    TANGENT_SPACE_ROTATION;
    o.lightDir = mul(_World2Object, _WorldSpaceLightPos0).xyz; // 平行光
    o.lightDir = mul(rotation, o.lightDir); // 光源轉換到切向量空間
    return o;
}
float4 frag(v2f i):COLOR
{
    float4 c=1;
    float3 N = UnpackNormal(tex2D(_BumpMap, i.uv));
    float diff = max(0, dot(N, i.lightDir)); //計算漫反射
    c = _LightColor0*diff;
    return c*2; // 乘以2是為了和預設的Unity光照傳統保持一致,這個是unity早期為了更容易的產生過度曝光人為加上去的
}

在SurfaceShader中,surf函式中使用的lightDir和viewDir已經是切空間的值了。Unity中,無高光和有高光surface lightmode分別是Lambert和BlinnPhong。

18.3 Parallax Mapping 視差對映

UV偏移。

用來欺騙眼睛,模擬出高度的交換。

視角來決定UV偏差,通過與平面垂直的Z分量的大小對UV基於xy分量的偏移大小做出約束,使得視角與切平面角度較小時,UV的偏差更大,如下:

inline float2 ParallaxOffset(half h, half height, half3 viewDir)
{
	h = h * height  - height /2.0;
	float3 v = normalize(viewDir);
	v.z += 0.42;
	return h * (v.xy/v.z);
}

18.4 Relief Mapping (地勢對映)

能夠提供小視角下,更深的效果。效果對比如下:

TODO: 文中對於原理介紹的還是比較清楚的。

19 卡通材質

19.1 描邊

19.1.1 沿法線擠出輪廓

程式碼實現:

Shader "Outline_1" {
	Properties {
		_Outline("Outline", range(0,0.2)) = 0.02
	}
	SubShader {
		Pass{//對物體進行描邊
			Tags{"LightMode"="Always"} // 總會執行,不會有光照處理
			Cull Off
			ZWrite Off
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			float _Outline;
			struct v2f {
				float4 pos:SV_POSITION;
			};
			v2f vert(appdata_full v) {
				v2f o;
				v.vertex.xyz += v.normal * _Outline; // 沿著法向量向外擴
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				return o;
			}
			float4 frag(v2f i) : COLOR
			{
				float4 c=0;
				return c;
			}
			ENDCG
		}
		Pass {//對物體進行光照計算
			Tags{"LightMode"="ForwardBase"}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			float4 _LightColor0;
			struct v2f {
				float4 pos : SV_POSITION;
				float3 lightDir : TEXCOORD0;
				float3 viewDir : TEXCOORD1;
				float3 normal : TEXCOORD2;
			};
			v2f vert(appdata_full v) {
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.normal = v.normal;
				o.lightDir = ObjSpaceLightDir(v.vertex);
				o.viewDir = ObjSpaceViewDir(v.vertex);
				return o;
			}
			float4 frag(v2f i) : COLOR {
				float4 c = 1;
				float3 N = normalize(i.normal);
				float3 viewDir = normalize(i.viewDir);
				float diff = dot(N, i.lightDir);
				diff = (diff + 1)/2;
				diff = smoothstep(diff/12, 1, diff); // 用來調顯示效果的
				c = _LightColor0 * diff;
				return c;
			}
			ENDCG
		}
	}
}

該方法對應如下問題:

  1. 兩個物體的重疊區域,描邊沒有出現(因為我們在繪製的時候關閉了Z的寫入);
  2. 物體輪廓的粗線並不是常量,而是和距離相機遠近相關的值,距離越遠越細;
  3. 我們是沿著法線方向擠出的輪廓,如果相鄰面的法線彼此分離,那麼最後的效果會是間斷的;
  4. 正方位的時候,邊界出現如下問題;

19.1.3 在相機空間中擠出

相關程式碼如下:

Pass {
	Tags {"LightMode"="Always"}
	Cull Front
	ZWrite On
	CGPROGRAM
	#pragma vertex vert
	#pragma fragment frag
	#include "UnityCG.cginc"
	float _Outline;
	struct v2f {
    	float4 pos:SV_POSITION;
    };
    v2f vert(appdata_full v) {
    	v2f o;
    	o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    	float3 norm = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
    	float2 offset = TransformViewToProjection(norm.xy);
    	o.pos.xy += offset * o.pos.z * _Outline;
    	return o;
    }
    float4 frag(v2f i) : COLOR
    {
    	float4 c=0;
    	return c;
    }
    ENDCG
}

這種方式繪製的結果還是會有2,3,4的問題。

使用UNITY_MATRIX_IT_MV的原因以及推導見:

[坑]UNITY_MATRIX_IT_MV – Matrix 64 Blog,其中關鍵內容見下圖:

上述描邊的方法,是將頂點往外擠出一定距離實現的。還可以通過Z偏移來描邊,也可以在繪製結果的基礎上進行描邊。

20 鏡面材質

暫時沒有這樣的需求,TODO

21 半透明材質

光在半透明物體中沿著各個方向進行折射,反射,所以就產生一個必然結果,那就是光的方向性消失了,但是光在物體內部的所能前進的深度依然和物體的密度、物體到光源的距離密切相關。

Shader中提供了兩個變數,一個用於控制物體到光源距離偏差,用來表達從哪個距離開始,光進入到了物體內部並開始衰減;另一個用來控制光在物體中的衰減速度,表示物體密度和透明度的控制。

22 體積霧

23 Wrap Model新解

24 面積光

25 體積光

26 材質替代渲染

Shader Replacement的存在目的是為了能夠讓我們以完全不同的另一套材質來渲染當前場景,從而達到特定的渲染效果,可以通過Camera的RenderWithShader和SetReplacementShader來做這件事情。

26.1 相機和渲染訊息

渲染週期:

26.3 如何使用RenderWithShader方法

Unity提供了使用Camera實現場景中物體替換shader渲染的方法,來滿足一些特殊需求。

public void RenderWithShader(Shader shader, string replacementTag);
public void SetReplacementShader(Shader shader, string replacementTag);

兩種方法都在掛載在Camera上的指令碼上呼叫,其中RenderWithShader 方法呼叫一次之後只渲染場景一次,SetReplacementShader方法呼叫之後會一直使用,直到呼叫ResetReplacementShader方法,則恢復所有物體的正常渲染。

通過介面中的replacementTag,來選擇呼叫Shader中的那個SubShader。

RenderWithShader通常使用方式是,你需要一個新的相機,並且disable掉掛載的Camera元件,這個相機不會再發送OnPreCull, OnPreRender or OnPostRender 訊息,最好結合RenderTexture使用。所以我使用它的方法基本就是將它作為一種觸發性質的渲染方式,在你需要的時候呼叫一次渲染方法,渲染到RenderTexture並進行使用。

27 後期效果

Render To Texture渲染到紋理,利用這種特性, 可以實現各種各樣難以在普通渲染過程中實現的華麗效果。

27.1 Graphics的兩個方法

想要做後期螢幕效果,就必須使用Graphics的Blit和BlitMultiTap方法。和相機Render、RenderWithShader方法的不同之處在於,Graphics的這兩個方法是在螢幕上又做了一個和螢幕大小一樣的平面,對此平面使用第三個引數提供的材質,然後把第一個引數作為渲染替代材質的_MainTex,第二個Texture引數作為輸出。Graphics的兩個方法是渲染一個平面,相機的Render和RenderWithShader方法渲染的仍然是場景中的物體。

27.1.2 Blit方法的簡單示例

_GraphicsFuncs/Lab_1資料夾下的場景,右邊相機顯示場景的原始狀態,還有一個螢幕,用來輸出左邊相機的內容。左邊相機有一個Blit_3.cs指令碼:

最後執行效果如下:

Blit_3指令碼對應的內容為:

using UnityEngine;
using System.Collections;

public class Bilt_3 : MonoBehaviour {
    public Material mat;
    public Material displayMat;
    public RenderTexture dstRT;
	void Start () {
        dstRT = new RenderTexture(Screen.width, Screen.height, 16);
        displayMat.mainTexture = dstRT;
	}
    void OnRenderImage(RenderTexture src,RenderTexture dst)
    {
        src.wrapMode = TextureWrapMode.Repeat;
        Graphics.Blit(src,dstRT,mat);
        Graphics.Blit(dstRT, dst);
    }
}

該指令碼的作用是先通過mat,將src渲染到右半部平面需要的texture(此處為displayMat.mainTextureGraphics.Blit(src,dstRT,mat)),然後將紋理繪製到螢幕中(Graphics.Blit(dstRT, dst))。

有半部分的矩形內,用Blit_1.shader進行繪製。

27.1.3 使用BlitMultiTap方法進行多重取樣

Graphics-BlitMultiTap - Unity 指令碼 API

可以用來做影象模糊的效果。

版權說明

作者: grassofsky

出處: http://www.cnblogs.com/grass-and-moon

本文版權歸作者,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出, 原文連結 如有問題, 可郵件([email protected])諮詢.