【浅入浅出】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(&#34;_FrustumCorners&#34;, 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 &#34;Custom/GradientColor&#34;
{
Properties
{
_MainTex (&#34;Texture&#34;, 2D) = &#34;white&#34; {}
_FogAmount(&#34;Fog amount&#34;, float) = 1
_ColorRamp(&#34;Color ramp&#34;, 2D) = &#34;white&#34; {}
_FogIntensity(&#34;Fog intensity&#34;, 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 &#34;UnityCG.cginc&#34;
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! 我遇到了个奇怪的问题,现在有a,b两个项目,用到了同一个粒子特效,在a项目底下无论雾效开不开那个粒子特效都会被渲染,而在b项目中那个粒子特效会因为雾效的打开而在game里看不见,scene里面可见,会是什么unity设置导致出现这个问题么? 嘻嘻嘻 我似乎和大佬混到一个群里了哈哈
[欢呼] 太深入了
[害羞]
页:
[1]