找回密码
 立即注册
查看: 703|回复: 3

[笔记] 【浅入浅出】Unity 雾效

[复制链接]
发表于 2021-5-10 11:19 | 显示全部楼层 |阅读模式
大家好,我是Shawn。如何实现“雾效”?”在Unity中,是有自带的雾效的,在Lighting窗口下,在other Settings就可以找到fog选项了,启用fog就能使用Unity自带的雾效了,但是,需要注意的是,只有在向前渲染下才能实现Unity自带的雾效,Unity有三种模式:Linear,Exp 和 Exp2 分别对应,线性、指数、和指数的平方的增长模式。
雾效因子分别对应:


E是end、S是start、c是coordinate、d就是density,通过上述式子我们不难发现这是一个关于c的减函数,即距离越远,雾效因子越小。
我们已经知道Unity是如何实现雾效的了,其实就是计算雾效因子,然后进行差值就行了
现在我们试着创建一个自己的雾效。
首先需要添加#pragma multi_compile_fog让Unity为我们生成正确的变体。然后我们还需要
设置一个宏控制雾效的开启。
  1. #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
  2.                                 #define APPLY_FOG 1            
  3.                         #endif
复制代码
在顶点着色器之前,编写一个自定义的函数:
  1.         float4 ApplyFog(float4 color , v2f i){
  2.                 float viewDistance = length(_WorldSpaceCameraPos - i.worldPos);
  3.                 UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
  4.                 color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
  5.                 return color ;
  6.                        
  7.         }
复制代码
这个函数计算了顶点到摄像机的距离,并且我们用Unity自带的宏为我们计算雾效因子,由于雾效因子的范围不在0-1之间所以我们用需要限定一下,这个宏可以在UnityCG.cginc里面查看到:
  1. #if defined(FOG_LINEAR)
  2.     // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start))
  3.     #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w
  4. #elif defined(FOG_EXP)
  5.     // factor = exp(-density*z)
  6.     #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)
  7. #elif defined(FOG_EXP2)
  8.     // factor = exp(-(density*z)^2)
  9.     #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor)
  10. #else
  11.     #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0
  12. #endif
复制代码
其实就是Unity判断我们选择的模式,传入c就算雾效因子,unity_FogParams可以在UnityShaderVariables里查看,这里我们直接给出:
unity_FogParams:(density / sqrt(ln(2)), density / ln(2), –1/(end-start), end/(end-start))


在片元着色器中代码就更简单了,只需要调用即可:
  1. fixed4 frag (v2f i) : SV_Target
  2.             {
  3.                 // sample the texture
  4.                 fixed4 col = tex2D(_MainTex, i.uv);
  5.                 // apply fog
  6.                 #if APPLY_FOG
  7.                col = col*ApplyFog(col ,i);
  8.                  #endif
  9.                 return col;
  10.             }
复制代码
返回Unity查看效果,我们没有用光照模型,只输出颜色:
其中一个球是标准材质,用来做对比


在上一个方法中我们使用的c是物体到摄像机的距离,下面我们将基于深度来实现雾效。
我们需要用一个变量来储存深度值,这里我们把顶点的世界坐标改成float4类型,用w储存深度值:
  1. float4 worldPos : TEXCOORD2;
复制代码
在顶点着色器中添加一段代码:
  1. #if APPLY_FOG
  2.     o.worldPos.w = o.vertex.z;
  3. #endif
复制代码
我们记录了clip space下的深度值,由于平台的差异我们取到的z值的范围也不同,我们使用
宏UNITY_Z_0_FAR_FROM_CLIPSPACE来针对不同平台做不同处理,把深度转换为从近切面到远切面线性增大的值。
  1. float4 ApplyFog(float4 color , v2f i){
  2.                 //float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
  3.                 float viewDistance =UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w);
  4.                 UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
  5.                 color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
  6.                 return color ;
  7.                        
  8.                         }
复制代码
其他代码不变,我们得到的效果几乎和上面的一样。
但其实还是差别的,当我们基于深度来实现雾效,我们不需要开根号,运行速度更快了,但是,我们还会发现一些问题,当我们的摄像机旋转时,我们的深度变了,从而影响了雾效,尽管他在逻辑上是不会变化的
源自catlike coding
我们可以查看做一个简单的测试,查看旋转时摄像机的深度图:


我们可以发现虽然是同一个位置,长方体的颜色却有细微的不同
下面是完整代码:(同时包含了基于距离和深度的方法)
  1. Shader "Unlit/TestFog"
  2. {
  3.     Properties
  4.     {
  5.         _MainTex ("Texture", 2D) = "white" {}
  6.     }
  7.     SubShader
  8.     {
  9.         Tags { "RenderType"="Opaque" }
  10.         LOD 100
  11.         Pass
  12.         {
  13.             //Tags{"LightMode" = "ForwardBase"}
  14.             CGPROGRAM
  15.             #pragma vertex vert
  16.             #pragma fragment frag
  17.             // make fog work
  18.             #pragma multi_compile_fog
  19.             #include "UnityCG.cginc"
  20.             #define FOG_DISTANCE
  21.             #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
  22.                  #if !defined(FOG_DISTANCE)
  23.                         #define FOG_DEPTH 1
  24.                 #endif
  25.                 #define APPLY_FOG 1  
  26.                                
  27.             #endif
  28.             struct appdata
  29.             {
  30.                 float4 vertex : POSITION;
  31.                 float2 uv : TEXCOORD0;
  32.             };
  33.             struct v2f
  34.             {
  35.                 float2 uv : TEXCOORD0;
  36.                 float4 vertex : SV_POSITION;
  37.                                 float4 worldPos : TEXCOORD2;
  38.                                
  39.                                
  40.             };
  41.             sampler2D _MainTex;
  42.             float4 _MainTex_ST;
  43.                         float4 ApplyFog(float4 color , v2f i){
  44.                                
  45.                                
  46.                                 float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
  47.                                 #if FOG_DEPTH
  48.                                          viewDistance =UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w);
  49.                                 #endif
  50.                                 UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
  51.                                 color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
  52.                                 return color ;
  53.                        
  54.                         }
  55.             v2f vert (appdata v)
  56.             {
  57.                 v2f o;
  58.                 o.vertex = UnityObjectToClipPos(v.vertex);
  59.                 o.uv = TRANSFORM_TEX(v.uv, _MainTex);
  60.                                 o.worldPos.xyz = mul(unity_ObjectToWorld , v.vertex).xyz;
  61.                                 #if APPLY_FOG
  62.                                 o.worldPos.w = o.vertex.z;
  63.                                 #endif
  64.                
  65.                 return o;
  66.             }
  67.             fixed4 frag (v2f i) : SV_Target
  68.             {
  69.                 // sample the texture
  70.                 fixed4 col = tex2D(_MainTex, i.uv);
  71.                 // apply fog
  72.                                 #if APPLY_FOG
  73.                col = col*ApplyFog(col ,i);
  74.                            #endif
  75.                 return col;
  76.             }
  77.             ENDCG
  78.         }
  79.     }
  80. }
复制代码
前文提到:这种方法只能用在向前渲染中,在延迟渲染中我们就要使用屏幕后处理。
我们先写一个最基本的后处理脚本:
  1. [ExecuteInEditMode]
  2. public class PostScreenEffect : MonoBehaviour
  3. {
  4.     public Shader shader;
  5.     Material material;
  6.     [ImageEffectOpaque]
  7.     private void OnRenderImage(RenderTexture source, RenderTexture destination)
  8.     {
  9.         if (material == null)
  10.         {
  11.             material= new Material(shader);
  12.         }
  13.         Graphics.Blit(source, destination, material);
  14.     }
复制代码
然后创建一个Imageeffect的shader文件,思路也很简单,就是通过采样深度纹理来获取深度值,从而计算距离。我们要声明_CameraDepthTexture来采样深度纹理,然后再采样他,核心代码如下:
  1. fixed4 frag (v2f i) : SV_Target
  2.             {
  3.                 fixed4 col = tex2D(_MainTex, i.uv);
  4.                                 float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
  5.                                 depth = Linear01Depth(depth);
  6.                                 if(depth<1){
  7.                                         float distance = depth * _ProjectionParams.z-_ProjectionParams.y;
  8.                                         UNITY_CALC_FOG_FACTOR_RAW(distance);
  9.                                         unityFogFactor = saturate(unityFogFactor);
  10.                                
  11.                                         col.rgb = lerp(unity_FogColor.rgb,col.rgb,unityFogFactor);
  12.                                 }
  13.                 return col;
  14.             }
复制代码
我们用Linear01Depth函数是为了能够得到一个0-1范围的线性的深度值,你可能已经不止一次听到类似“MVP矩阵的P导致了非线性”,所以我再唠叨一遍,哈哈。然后转换为距离,同样你也可以在UnityShaderVariables里查看_ProjectionParams. 这里z就代表远裁面,y代表近裁面。
我们把深度值限定在小于1的范围内是因为我们不想渲染天空盒。
返回Unity查看效果:


最后,我们再用真正的距离实现屏幕后处理实现雾效:
这里的距离我们可以从摄像机发射一道光线。如果没有没有物体的话,这道光线可以一直延伸最后达到远裁面,如果有物体,我们我们只要记录下它的深度值与远裁面的比,然后乘这个光线向量得到一个新的向量,就是一个简单的相似三角形的知识~最后计算这个向量的模,我们就得到了距离了。
理论上来说要为每个像素发射一次射线,但是,我们这里我们只计算远裁面的四个角的射线,剩下的就交给差值去做吧。
幸运的是,我们不用自己手动计算这四个射线,Unity有给定函数方便了我们的计算。
点击这里了解这个函数。
我们在camera的脚本中添加一些代码:
  1. Camera cam = null;
  2.    
  3. Vector3[] frustumCorners;
  4. Vector4[] vectorArray;
  5. /****下面代码添加至OnRenderImage函数中****/
  6. if (material == null)
  7.         {
  8.             material= new Material(shader);
  9.              cam = GetComponent<Camera>();
  10.             frustumCorners = new Vector3[4];
  11.         }
  12. cam.CalculateFrustumCorners(new Rect(0, 0, 1, 1), cam.farClipPlane, cam.stereoActiveEye, frustumCorners);
  13.         vectorArray[0] = frustumCorners[0];
  14.         vectorArray[1] = frustumCorners[3];
  15.         vectorArray[2] = frustumCorners[1];
  16.         vectorArray[3] = frustumCorners[2];
  17.         material.SetVectorArray("_FrustumCorners", vectorArray);
复制代码
在calculateFrustumCorners函数里我们传入的是Vector3,而我们SetVectorArray需要传入Vector4 ,所以我们变化一下,这样我们的数值就能正确被传入了 ;
在我们的shader文件下我们声明一个宏,来控制是否启用distance的算法。
  1. #define FOG_DISTANCE
复制代码
接着要在顶点结构体声明ray向量:
  1. #ifdef FOG_DISTANCE
  2.         float3 ray : TEXCOORD1;
  3. #endif
复制代码
最后我们修改片元着色器中的核心语句:
  1. if(depth<1){
  2.                 float distance = depth * _ProjectionParams.z-_ProjectionParams.y;
  3.                 #ifdef FOG_DISTANCE
  4.                 distance = length(depth*i.ray);
  5.                 #endif
  6.                 UNITY_CALC_FOG_FACTOR_RAW(distance);
  7.                 unityFogFactor = saturate(unityFogFactor);
  8.                 col.rgb = lerp(unity_FogColor.rgb,col.rgb,unityFogFactor);
  9.                  }
复制代码
效果:
以上就是雾效的所有内容了


最后的最后(真的是最后了)我们来实现一个类似firewatch的效果:


代码如下:
  1. Shader "Custom/GradientColor"
  2. {
  3.     Properties
  4.     {
  5.         _MainTex ("Texture", 2D) = "white" {}
  6.         _FogAmount("Fog amount", float) = 1
  7.         _ColorRamp("Color ramp", 2D) = "white" {}
  8.         _FogIntensity("Fog intensity", float) = 1
  9.     }
  10.     SubShader
  11.     {
  12.        
  13.         // No culling or depth
  14.         Cull Off ZWrite Off ZTest Always
  15.         Pass
  16.         {
  17.             CGPROGRAM
  18.             #pragma vertex vert
  19.             #pragma fragment frag
  20.             #pragma multi_compile_fog
  21.             #include "UnityCG.cginc"
  22.                        
  23.             struct appdata
  24.             {
  25.                 float4 vertex : POSITION;
  26.                 float2 uv : TEXCOORD0;
  27.             };
  28.             
  29.             struct v2f
  30.             {
  31.                 float2 uv : TEXCOORD0;
  32.                 float4 vertex : SV_POSITION;
  33.                 float4 scrPos : TEXCOORD1;
  34.             };
  35.             v2f vert (appdata v)
  36.             {
  37.                 v2f o;
  38.                 o.vertex = UnityObjectToClipPos(v.vertex);
  39.                 o.uv = v.uv;
  40.                 o.scrPos = ComputeScreenPos(o.vertex);
  41.                 return o;
  42.             }
  43.             
  44.             sampler2D _MainTex;
  45.             sampler2D _CameraDepthTexture;
  46.             sampler2D _ColorRamp;
  47.             float _FogAmount;
  48.             float _FogIntensity;
  49.             fixed4 frag (v2f i) : SV_Target
  50.             {
  51.                 fixed4 orCol = tex2D(_MainTex, i.uv);
  52.                 float depthValue = Linear01Depth (tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos)));
  53.                                 //float depthValue = Linear01Depth (SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv));
  54.                                 float distance = depthValue*_ProjectionParams.z-_ProjectionParams.y;
  55.                                 UNITY_CALC_FOG_FACTOR_RAW(distance);
  56.                                 unityFogFactor = saturate(unityFogFactor);
  57.                                
  58.                 float depthValueMul = depthValue * _FogAmount;
  59.                 fixed4 fogCol = tex2D(_ColorRamp, (float2(depthValueMul, 0)));
  60.                                 if(depthValue<1){
  61.                                         #if !defined(FOG_LINEAR) && !defined(FOG_EXP) && !defined(FOG_EXP2)
  62.                                        
  63.                                         orCol =  lerp(orCol, fogCol, fogCol.a * _FogIntensity) ;
  64.                                         #else
  65.                                         unityFogFactor =1-unityFogFactor;
  66.                                         orCol = lerp(orCol,fogCol,unityFogFactor*_FogIntensity);
  67.                                         #endif
  68.                                        
  69.                                 }
  70.                
  71.                                 return orCol;
  72.                                
  73.             }
  74.             ENDCG
  75.         }
  76.     }
  77. }
复制代码
我们通过深度对一张梯度图采样,这样不同的深度就能显示不同的颜色,如果你觉的这样很不自然的话,当然也可以使用雾效因子来对他进行采样。
如果你的渲染路径是向前渲染的话,你就需要在脚本中加入Camera.main.depthTextureMode = DepthTextureMode.Depth;
这样你就可以访问深度图了
我们找一张梯度图看看效果:
也可以这样:
当然如果你找的颜色是彩虹色的话:
Enjoy!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-5-10 11:26 | 显示全部楼层
我遇到了个奇怪的问题,现在有a,b两个项目,用到了同一个粒子特效,在a项目底下无论雾效开不开那个粒子特效都会被渲染,而在b项目中那个粒子特效会因为雾效的打开而在game里看不见,scene里面可见,会是什么unity设置导致出现这个问题么?
发表于 2021-5-10 11:34 | 显示全部楼层
嘻嘻嘻 我似乎和大佬混到一个群里了哈哈
[欢呼]
发表于 2021-5-10 11:43 | 显示全部楼层
太深入了
[害羞]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-18 21:15 , Processed in 0.102915 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表