Unity内置的雾效需要在每个shader中分别编写,造成了极大的不便。这里利用屏幕后处理产生可单独控制且自由度更高的雾效。
屏幕后雾效的本质在于,通过深度纹理重构出每个像素在世界空间中的位置,根据得到的世界坐标计算出雾效系数,最后利用雾效系数与雾的颜色相乘并与原始颜色进行插值运算得出最终效果。
float3 afterFog=f*fogColor+(1-f)*origColor;
上面的插值运算中f代表雾效系数,
它有多种计算方法:
1.线性运算:
f=(dmax-Abs(z))/dmax-dmin;
其中dmax和dmin分别代表受雾影响的最大和最小距离,z为给定的距离位置(像素位置)
2.指数运算:
f=pow(e,-d*Abs(z));
其中d控制雾的浓度,e为数学常量
3.二次指数:
f=pow(e,-pow(d*z,2));
为了更方便的对参数进行控制,需要重构每个像素在世界空间中的位置,常规实现方法如下:
1.构建像素的NDC坐标然后用VP矩阵的逆矩阵反向推导
2.通过向量的基本运算求得
方法1需要在片元着色器中进行矩阵乘法,若想得到性能更优的实现方式,考虑使用方法2。
向量的基本运算方式如下:
float4 worldPos=_WorldSpaceCameraPos+linearDepth*interpolatedRay;
_WorldSpaceCameraPos表示摄像机在世界空间中的位置,linearDepth*interpolatedRay是为了求得世界空间下的像素相对于摄像机的偏移量。根据向量的加法,就可以求出该像素在世界空间中的位置。
linearDepth线性深度值可以利用摄像机的深度纹理来求,关键在于求一个插值射线interpolatedRay。
分析interpolatedRay的含义可以知道,它主要表示该像素到摄像机的方向向量,可以由顶点着色器的各个顶点输出并插值得到。
基于这一点,可以直接在C#脚本中计算出屏幕四个顶点(左上,左下,右上,右下)的向量,传值给顶点着色器即可,这样避免在Shader中进行繁杂的数学运算。
参数控制脚本,同时计算顶点相对于摄像机的方向向量。此脚本挂载在摄像机上:
1 using UnityEngine;
2
3 public class FogWithDepthTexCtrl : ScreenEffectBase
4 {
5 private const string _FrustumCornersRay = "_FrustumCornersRay";
6
7 private const string _FogDensity = "_FogDensity";
8 private const string _FogColor = "_FogColor";
9 private const string _FogUnderStart = "_FogUnderStart";
10 private const string _FogTopEnd = "_FogTopEnd";
11
12 private Camera myCamera;
13 public Camera MyCamera
14 {
15 get
16 {
17 if (myCamera == null)
18 myCamera = GetComponent<Camera>();
19 return myCamera;
20 }
21 }
22
23 private Transform myCameraTran;
24 public Transform MyCameraTran
25 {
26 get
27 {
28 if (myCameraTran == null)
29 myCameraTran = MyCamera.transform;
30 return myCameraTran;
31 }
32 }
33
34 [Range(0, 3)]
35 public float fogDensity = 1.0f;//控制雾的浓度
36 public Color fogColor = Color.white;
37 public float fogUnderStart = 0.0f;//雾起始高度
38 public float fogTopEnd = 2.0f;//雾结束高度
39
40 private void OnEnable()
41 {
42 MyCamera.depthTextureMode |= DepthTextureMode.Depth;
43 }
44
45 private void OnDisable()
46 {
47 MyCamera.depthTextureMode &= ~DepthTextureMode.Depth;
48 }
49
50 private void OnRenderImage(RenderTexture source, RenderTexture destination)
51 {
52 if (Material != null)
53 {
54 //需要传递的四个角相对于摄像机的方向向量,这里用矩阵的每一行来表示
55 Matrix4x4 frustumCorners = Matrix4x4.identity;
56
57 float fov = MyCamera.fieldOfView;
58 float near = MyCamera.nearClipPlane;
59 float aspect = MyCamera.aspect;
60
61 //计算近裁剪平面三个标准方向
62 float halfHeight = near * Mathf.Tan(fov * .5f * Mathf.Deg2Rad);
63 Vector3 toTop = halfHeight * MyCameraTran.up;
64 Vector3 toRight = halfHeight * MyCameraTran.right * aspect;
65 Vector3 toForward = near * MyCameraTran.forward;
66
67 //用三个标准方向重构四个顶点关于摄像机的向量
68 Vector3 topRight = toForward + toRight + toTop;
69 topRight /= near;
70
71 Vector3 topLeft = toForward - toRight + toTop;
72 topLeft /= near;
73
74 Vector3 bottomRight = toForward + toRight - toTop;
75 bottomRight /= near;
76
77 Vector3 bottomLeft = toForward - toRight - toTop;
78 bottomLeft /= near;
79
80 //用矩阵的每一行来存储这些向量,这里的顺序要与之后解析的顺序对应
81 frustumCorners.SetRow(0, topLeft);
82 frustumCorners.SetRow(1, topRight);
83 frustumCorners.SetRow(2, bottomLeft);
84 frustumCorners.SetRow(3, bottomRight);
85
86 //传递向量矩阵和对应的参数
87 Material.SetMatrix(_FrustumCornersRay, frustumCorners);
88
89 Material.SetFloat(_FogDensity, fogDensity);
90 Material.SetColor(_FogColor, fogColor);
91 Material.SetFloat(_FogUnderStart, fogUnderStart);
92 Material.SetFloat(_FogTopEnd, fogTopEnd);
93
94 Graphics.Blit(source, destination, Material);
95 }
96 else
97 Graphics.Blit(source, destination);
98 }
99 }
1 Shader "MyUnlit/FogWithDepthTex"
2 {
3 Properties
4 {
5 _MainTex ("Texture", 2D) = "white" {}
6 }
7 SubShader
8 {
9 Pass
10 {
11 ZTest Always Cull Off ZWrite Off
12
13 CGPROGRAM
14 #pragma vertex vert
15 #pragma fragment frag
16
17 #include "UnityCG.cginc"
18
19 //对应四个顶点的射线矩阵
20 float4x4 _FrustumCornersRay;
21
22 sampler2D _MainTex;
23 half4 _MainTex_TexelSize;
24 sampler2D _CameraDepthTexture;
25 half _FogDensity;
26 fixed4 _FogColor;
27 float _FogUnderStart;
28 float _FogTopEnd;
29
30 struct appdata
31 {
32 float4 vertex : POSITION;
33 float2 uv : TEXCOORD0;
34 };
35
36 struct v2f
37 {
38 half4 uv : TEXCOORD0;
39 float4 vertex : SV_POSITION;
40 //顶点着色器输出的插值射线
41 float4 interpolatedRay:TEXCOORD1;
42 };
43
44 v2f vert (appdata v)
45 {
46 v2f o;
47 o.vertex = UnityObjectToClipPos(v.vertex);
48 o.uv.xy = v.uv;
49 o.uv.zw=v.uv;//zw存深度纹理
50
51 //对插值射线的索引进行解析,判定该顶点是四个角中的哪一个
52 int idx=0;
53 if(v.uv.x>.5f&&v.uv.y>.5f)
54 idx=1;
55 else if(v.uv.x<.5f&&v.uv.y<.5f)
56 idx=2;
57 else if(v.uv.x>.5f&&v.uv.y<.5f)
58 idx=3;
59
60 //主纹理外的纹理要进行平台差异化处理,同时对顶点的索引也需要进行处理(左上对左下,右上对右下)
61 #if UNITY_UV_STARTS_AT_TOP
62 if(_MainTex_TexelSize.y<0){
63 o.uv.w=1-o.uv.w;
64 idx=idx<2?idx+2:idx-2;
65 }
66 #endif
67
68 //按照解析的索引值得到需要传递的插值射线
69 o.interpolatedRay=_FrustumCornersRay[idx];
70
71 return o;
72 }
73
74 fixed4 frag (v2f i) : SV_Target
75 {
76 //计算像素在世界空间中的位置
77 float linearDepth=LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv.zw));
78 float3 worldPos=_WorldSpaceCameraPos+linearDepth*i.interpolatedRay.xyz;
79
80 //计算雾效系数,这里主要用的关于世界空间高度的线性雾计算
81 float fogDensity=(_FogTopEnd-worldPos.y)/(_FogTopEnd-_FogUnderStart);
82 fogDensity=saturate(fogDensity*_FogDensity);
83
84 //插值得到最终雾效
85 fixed4 col = tex2D(_MainTex, i.uv);
86 col.rgb=lerp(col.rgb,_FogColor.rgb,fogDensity);
87
88 return col;
89 }
90 ENDCG
91 }
92 }
93 }
效果如下: