RhinoFreak 发表于 2021-5-10 11:19

【浅入浅出】Unity 雾效

大家好,我是Shawn。如何实现“雾效”?”在Unity中,是有自带的雾效的,在Lighting窗口下,在other Settings就可以找到fog选项了,启用fog就能使用Unity自带的雾效了,但是,需要注意的是,只有在向前渲染下才能实现Unity自带的雾效,Unity有三种模式:Linear,Exp 和 Exp2 分别对应,线性、指数、和指数的平方的增长模式。
雾效因子分别对应:

https://www.zhihu.com/equation?tex=f%3DE-c%2FE-S 、 https://www.zhihu.com/equation?tex=f%3D%5Cfrac%7B1%7D%7B2%5E%7Bcd%7D%7D%3D2%5E%7B-%28cd%29%7D 、 https://www.zhihu.com/equation?tex=f%3D%5Cfrac%7B1%7D%7B2%5E%7Bcd%5E2%7D%7D%3D2%5E%7B-%28cd%29%5E2%7D
E是end、S是start、c是coordinate、d就是density,通过上述式子我们不难发现这是一个关于c的减函数,即距离越远,雾效因子越小。
我们已经知道Unity是如何实现雾效的了,其实就是计算雾效因子,然后进行差值就行了
现在我们试着创建一个自己的雾效。
首先需要添加#pragma multi_compile_fog让Unity为我们生成正确的变体。然后我们还需要
设置一个宏控制雾效的开启。
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
                                #define APPLY_FOG 1            
                        #endif在顶点着色器之前,编写一个自定义的函数:
        float4 ApplyFog(float4 color , v2f i){

                float viewDistance = length(_WorldSpaceCameraPos - i.worldPos);
                UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
                color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
                return color ;
                       
        }这个函数计算了顶点到摄像机的距离,并且我们用Unity自带的宏为我们计算雾效因子,由于雾效因子的范围不在0-1之间所以我们用需要限定一下,这个宏可以在UnityCG.cginc里面查看到:
#if defined(FOG_LINEAR)
    // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start))
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w
#elif defined(FOG_EXP)
    // factor = exp(-density*z)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)
#elif defined(FOG_EXP2)
    // factor = exp(-(density*z)^2)
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor)
#else
    #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0
#endif其实就是Unity判断我们选择的模式,传入c就算雾效因子,unity_FogParams可以在UnityShaderVariables里查看,这里我们直接给出:
unity_FogParams:(density / sqrt(ln(2)), density / ln(2), –1/(end-start), end/(end-start))


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


在上一个方法中我们使用的c是物体到摄像机的距离,下面我们将基于深度来实现雾效。
我们需要用一个变量来储存深度值,这里我们把顶点的世界坐标改成float4类型,用w储存深度值:
float4 worldPos : TEXCOORD2;在顶点着色器中添加一段代码:
#if APPLY_FOG
    o.worldPos.w = o.vertex.z;
#endif我们记录了clip space下的深度值,由于平台的差异我们取到的z值的范围也不同,我们使用
宏UNITY_Z_0_FAR_FROM_CLIPSPACE来针对不同平台做不同处理,把深度转换为从近切面到远切面线性增大的值。
float4 ApplyFog(float4 color , v2f i){

                //float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
                float viewDistance =UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w);
                UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
                color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
                return color ;
                       
                        }其他代码不变,我们得到的效果几乎和上面的一样。
但其实还是差别的,当我们基于深度来实现雾效,我们不需要开根号,运行速度更快了,但是,我们还会发现一些问题,当我们的摄像机旋转时,我们的深度变了,从而影响了雾效,尽管他在逻辑上是不会变化的
源自catlike coding
我们可以查看做一个简单的测试,查看旋转时摄像机的深度图:


我们可以发现虽然是同一个位置,长方体的颜色却有细微的不同
下面是完整代码:(同时包含了基于距离和深度的方法)
Shader "Unlit/TestFog"
{
    Properties
    {
      _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
      Tags { "RenderType"="Opaque" }
      LOD 100

      Pass
      {
          //Tags{"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

          #define FOG_DISTANCE

          #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
                 #if !defined(FOG_DISTANCE)
                        #define FOG_DEPTH 1
                #endif
                #define APPLY_FOG 1
                               
          #endif

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                                float4 worldPos : TEXCOORD2;
                               
                               
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;


                        float4 ApplyFog(float4 color , v2f i){
                               
                               
                                float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
                                #if FOG_DEPTH
                                       viewDistance =UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w);
                                #endif
                                UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
                                color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
                                return color ;
                       
                        }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                                o.worldPos.xyz = mul(unity_ObjectToWorld , v.vertex).xyz;

                                #if APPLY_FOG
                                o.worldPos.w = o.vertex.z;
                                #endif
               
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                                #if APPLY_FOG
               col = col*ApplyFog(col ,i);
                           #endif
                return col;
            }
            ENDCG
      }
    }
}前文提到:这种方法只能用在向前渲染中,在延迟渲染中我们就要使用屏幕后处理。
我们先写一个最基本的后处理脚本:

public class PostScreenEffect : MonoBehaviour
{
    public Shader shader;

    Material material;

   
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
      if (material == null)
      {
            material= new Material(shader);
      }
      Graphics.Blit(source, destination, material);
    }然后创建一个Imageeffect的shader文件,思路也很简单,就是通过采样深度纹理来获取深度值,从而计算距离。我们要声明_CameraDepthTexture来采样深度纹理,然后再采样他,核心代码如下:
fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv);
                                depth = Linear01Depth(depth);
                                if(depth<1){
                                        float distance = depth * _ProjectionParams.z-_ProjectionParams.y;
                                        UNITY_CALC_FOG_FACTOR_RAW(distance);
                                        unityFogFactor = saturate(unityFogFactor);
                               
                                        col.rgb = lerp(unity_FogColor.rgb,col.rgb,unityFogFactor);
                                }

                return col;
            }我们用Linear01Depth函数是为了能够得到一个0-1范围的线性的深度值,你可能已经不止一次听到类似“MVP矩阵的P导致了非线性”,所以我再唠叨一遍,哈哈。然后转换为距离,同样你也可以在UnityShaderVariables里查看_ProjectionParams. 这里z就代表远裁面,y代表近裁面。
我们把深度值限定在小于1的范围内是因为我们不想渲染天空盒。
返回Unity查看效果:


最后,我们再用真正的距离实现屏幕后处理实现雾效:
这里的距离我们可以从摄像机发射一道光线。如果没有没有物体的话,这道光线可以一直延伸最后达到远裁面,如果有物体,我们我们只要记录下它的深度值与远裁面的比,然后乘这个光线向量得到一个新的向量,就是一个简单的相似三角形的知识~最后计算这个向量的模,我们就得到了距离了。
理论上来说要为每个像素发射一次射线,但是,我们这里我们只计算远裁面的四个角的射线,剩下的就交给差值去做吧。
幸运的是,我们不用自己手动计算这四个射线,Unity有给定函数方便了我们的计算。
点击这里了解这个函数。
我们在camera的脚本中添加一些代码:
Camera cam = null;
   
Vector3[] frustumCorners;
Vector4[] vectorArray;
/****下面代码添加至OnRenderImage函数中****/
if (material == null)
      {
            material= new Material(shader);
             cam = GetComponent<Camera>();
            frustumCorners = new Vector3;
      }


cam.CalculateFrustumCorners(new Rect(0, 0, 1, 1), cam.farClipPlane, cam.stereoActiveEye, frustumCorners);

      vectorArray = frustumCorners;
      vectorArray = frustumCorners;
      vectorArray = frustumCorners;
      vectorArray = frustumCorners;
      material.SetVectorArray("_FrustumCorners", vectorArray);在calculateFrustumCorners函数里我们传入的是Vector3,而我们SetVectorArray需要传入Vector4 ,所以我们变化一下,这样我们的数值就能正确被传入了 ;
在我们的shader文件下我们声明一个宏,来控制是否启用distance的算法。
#define FOG_DISTANCE 接着要在顶点结构体声明ray向量:
#ifdef FOG_DISTANCE
        float3 ray : TEXCOORD1;
#endif最后我们修改片元着色器中的核心语句:
if(depth<1){
                float distance = depth * _ProjectionParams.z-_ProjectionParams.y;
                #ifdef FOG_DISTANCE
                distance = length(depth*i.ray);
                #endif
                UNITY_CALC_FOG_FACTOR_RAW(distance);
                unityFogFactor = saturate(unityFogFactor);

                col.rgb = lerp(unity_FogColor.rgb,col.rgb,unityFogFactor);
                 }效果:
以上就是雾效的所有内容了


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


代码如下:
Shader "Custom/GradientColor"
{
    Properties
    {
      _MainTex ("Texture", 2D) = "white" {}
      _FogAmount("Fog amount", float) = 1
      _ColorRamp("Color ramp", 2D) = "white" {}
      _FogIntensity("Fog intensity", float) = 1
    }
    SubShader
    {
       
      // No culling or depth
      Cull Off ZWrite Off ZTest Always

      Pass
      {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            #include "UnityCG.cginc"

                       

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 scrPos : TEXCOORD1;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.scrPos = ComputeScreenPos(o.vertex);
                return o;
            }
            
            sampler2D _MainTex;
            sampler2D _CameraDepthTexture;
            sampler2D _ColorRamp;
            float _FogAmount;
            float _FogIntensity;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 orCol = tex2D(_MainTex, i.uv);
                float depthValue = Linear01Depth (tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos)));
                                //float depthValue = Linear01Depth (SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv));

                                float distance = depthValue*_ProjectionParams.z-_ProjectionParams.y;
                                UNITY_CALC_FOG_FACTOR_RAW(distance);
                                unityFogFactor = saturate(unityFogFactor);
                               
                float depthValueMul = depthValue * _FogAmount;
                fixed4 fogCol = tex2D(_ColorRamp, (float2(depthValueMul, 0)));
                                if(depthValue<1){
                                        #if !defined(FOG_LINEAR) && !defined(FOG_EXP) && !defined(FOG_EXP2)
                                       
                                        orCol =lerp(orCol, fogCol, fogCol.a * _FogIntensity) ;
                                        #else
                                        unityFogFactor =1-unityFogFactor;
                                        orCol = lerp(orCol,fogCol,unityFogFactor*_FogIntensity);
                                        #endif
                                       
                                }

               
                                return orCol;
                               
            }
            ENDCG
      }
    }
}我们通过深度对一张梯度图采样,这样不同的深度就能显示不同的颜色,如果你觉的这样很不自然的话,当然也可以使用雾效因子来对他进行采样。
如果你的渲染路径是向前渲染的话,你就需要在脚本中加入Camera.main.depthTextureMode = DepthTextureMode.Depth;
这样你就可以访问深度图了
我们找一张梯度图看看效果:
也可以这样:
当然如果你找的颜色是彩虹色的话:
Enjoy!

七彩极 发表于 2021-5-10 11:26

我遇到了个奇怪的问题,现在有a,b两个项目,用到了同一个粒子特效,在a项目底下无论雾效开不开那个粒子特效都会被渲染,而在b项目中那个粒子特效会因为雾效的打开而在game里看不见,scene里面可见,会是什么unity设置导致出现这个问题么?

七彩极 发表于 2021-5-10 11:34

嘻嘻嘻 我似乎和大佬混到一个群里了哈哈
[欢呼]

mastertravels77 发表于 2021-5-10 11:43

太深入了
[害羞]
页: [1]
查看完整版本: 【浅入浅出】Unity 雾效