雾效是游戏里经常使用的一种效果。Unity内置的雾效可以产生基于距离的线性或指数雾效。然而,要想在自己编写的顶点/片元着色器中实现这些雾效,我们需要在Shader中添加#pragma_multi_compile_fog指令,同事还需要使用相关的内置宏,例如UNITY_FOG_COORDS、UNITY_TRANSFER_FOG和UNITY_APPLY_FOG等。这种方法的缺点在于,不仅需要为场景中所有物体添加相关的渲染代码,而且能够实现的效果也非常有限。本节,将实用一种基于屏幕后处理的全局雾效。使用这种方法,我们不需要更改场景内渲染的物体所使用的Shader代码,而仅仅依靠一次屏幕后处理即可。这种方法的自由性很高,我们可以方便地模拟各种雾效。
基于屏幕后处理的全局雾效的关键是,根据深度纹理来重建每个像素在世界空间下的位置。从深度纹理中重建世界坐标的方法,这种方法首先对图像空间下的视椎体射线(从摄像机出发,指向图像上某点的射线)进行插值,这条射线存储了该像素在世界空间下的摄像机的方向信息。然后,我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。当我们得到世界坐标后,就可以使用公式来墨迹雾效了。
1.重建世界坐标
坐标系中的一个顶点坐标可以通过它相对于另一个顶点坐标的偏移量来求得。重建像素的世界坐标也是基于这样的思想。我们只需要知道摄像机在世界空间下的位置,以及世界空间下该像素相对于摄像机的偏移量,他们相加就可以得到该像素的世界坐标。如下:float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;其中,_WorldSpaceCameraPos是摄像机在世界空间下的位置,这可以由Unity的内置变量直接访问得到。而linearDepth * interpolatedRay则可以计算得到该像素相对于摄像机的偏移量,linearDepth是由深度纹理得到的线性深度值,interpolatedRay是由顶点着色器输出并插值后得到的射线,他不仅包含了该像素得到的摄像机方向,也包含了距离信息。
interpolatedRay来源于对金裁剪平面的4个角的某个特定向量的插值,这4个向量包含了它们到摄像机的方向和距离信息,我们可以利用摄像机的近裁剪平面距离、FOV、纵横比计算而得。为了方便计算,我们可以先计算两个向量——toTop和toRight,他们是起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量。他们的计算公式如下:
2.雾的计算
在简单的雾效实现中,我们需要计算一个雾效系数f,作为混合原始颜色和雾的颜色的混合系数:
float3 afterFog = f * fogColor + (1-f) * origColor;
C#:
using UnityEngine;
using System.Collections;
public class FogWithDepthTexture : PostEffectsBase {
public Shader fogShader;
private Material fogMaterial = null;
public Material material {
get {
fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
return fogMaterial;
}
}
//我们需要获取摄像机的相关参数,如近裁剪平面的距离、FOV等,同时还需要获取摄像机在世界空间下的前方、上方和右方等方向,因此使用两个变量存储摄像机的Camera和Transform
private Camera myCamera;
public Camera camera {
get {
if (myCamera == null) {
myCamera = GetComponent<Camera>();
}
return myCamera;
}
}
private Transform myCameraTransform;
public Transform cameraTransform {
get {
if (myCameraTransform == null) {
myCameraTransform = camera.transform;
}
return myCameraTransform;
}
}
//fogDensity用于控制雾的浓度,fogColor用于控制雾的颜色。我们使用的雾效模拟函数是基于高度的,因此参数fogStart用于控制雾效的起始高度,fogEnd用于控制雾效的终止高度。
[Range(0.0f, 3.0f)]
public float fogDensity = 1.0f;
public Color fogColor = Color.white;
public float fogStart = 0.0f;
public float fogEnd = 2.0f;
//由于要获取摄像机的深度纹理,在onEnable()函数中设置摄像机的响应状态。
void OnEnable() {
camera.depthTextureMode |= DepthTextureMode.Depth;
}
//OnRenderImage首先计算了近裁剪平面的四个角对应的向量,并把他们存储在一个矩阵类型的变量中,按照一定的顺序把四个方向存储到了frustumCorners不同的行中,这个顺序是非常重要的,因为这决定了我们在顶点着色器中使用哪一行作为改点的待插值向量。随后,我们把结果和其他参数传递给材质,并调用Graphis.Blit(src,dest,material)把渲染结果显示在屏幕上。
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
Matrix4x4 frustumCorners = Matrix4x4.identity;
float fov = camera.fieldOfView;
float near = camera.nearClipPlane;
float aspect = camera.aspect;
float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = cameraTransform.right * halfHeight * aspect;
Vector3 toTop = cameraTransform.up * halfHeight;
Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;
topLeft.Normalize();
topLeft *= scale;
Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
topRight.Normalize();
topRight *= scale;
Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;
Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
bottomRight.Normalize();
bottomRight *= scale;
frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);
material.SetMatrix("_FrustumCornersRay", frustumCorners);
material.SetFloat("_FogDensity", fogDensity);
material.SetColor("_FogColor", fogColor);
material.SetFloat("_FogStart", fogStart);
material.SetFloat("_FogEnd", fogEnd);
Graphics.Blit (src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader:
Shader "Hidden/Fog With Depth Texture"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_FogDensity ("Fog Density", Float) = 1.0
_FogColor ("Fog Color", Color) = (1, 1, 1, 1)
_FogStart ("Fog Start", Float) = 0.0
_FogEnd ("Fog End", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
float4x4 _FrustumCornersRay;
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
half _FogDensity;
fixed4 _FogColor;
float _FogStart;
float _FogEnd;
//在v2f结构体中,我们除了定义顶点位置、屏幕图像和深度纹理坐标外,还定义了interpolatedRay变量存储插值后的像素向量。在顶点着色器中,我们对深度纹理的采样坐标进行了平台差异化处理。更重要的是,我们要决定对应了4个角中的哪个角。我们采用的仿佛是判断它的纹理坐标。我们知道,在Unity中,纹理坐标的(0,0)点对应了左下角,而点(1,1)点对应了右上角。我们据此来判断该顶点对应的索引,这个对应关系和我们在脚本中对frustumCorners的赋值顺序是一致的。实际上,不同平台的纹理坐标不一定是满足上面的条件的。
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half2 uv_depth : TEXCOORD1;
float4 interpolatedRay : TEXCOORD2;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
o.uv_depth = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
int index = 0;
if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
index = 0;
} else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
index = 1;
} else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
index = 2;
} else {
index = 3;
}
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
index = 3 - index;
#endif
o.interpolatedRay = _FrustumCornersRay[index];
return o;
}
//首先,我们需要重建该像素子在世界空间中的位置。为此,我们首先使用SAMPLE_DEPTH_TEXTURE对深度纹理进行采样,再使用LinearEyeDepth得到视角空间下的线性深度值。之后,与interpolatedRay相乘后再和世界空间下的摄像机位置相加,即可得到世界空间下的位置。然后根据材质属性_FogEnd和_FogStart计算当前像素高度worldPos.y对应的雾效系数fogDensity,再和参数_FogDensity相乘后,利用saturate函数截取[0,1]范围内,作为最后的雾效系数。最后,我们使用该系数将雾的颜色和原始颜色进行混合后返回。
fixed4 frag(v2f i) : SV_Target {
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity);
fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
return finalColor;
}
ENDCG
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack Off
}