UnityShader入门精要——全局雾效

 基于屏幕后处理的全局雾效的关键是,根据深度纹理来重建每个像素在世界空间下的位置。我们在模拟运动模糊时已经实现了这个要求,即构建出当前像素的NDC坐标,再通过当前摄像机的视角*投影矩阵的逆矩阵来得到世界空间下的像素坐标,但是,这样的实现需要在片元着色器中进行矩阵乘法的操作,而这通常会影响游戏性能。在本节中,我们将会学习一个快速从深度纹理中重建世界坐标的方法。这种方法首先对图像空间下的视锥体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后,我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。当我们得到世界坐标后,就可以轻松地使用各个公式来模拟全局雾效了。

1 重建世界坐标

坐标系中的一个顶点坐标可以通过它相对于另-一个顶点坐标的偏移量来求得。重建像素的世界坐标也是基于这样的思想。我们只需要知道摄像机在世界空间下的位置,以及世界空间下该像素相对于摄像机的偏移量,把它们相加就可以得到该像素的世界坐标。整个过程可以使用下面的代码来表示:

float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;

_WorldSpaceCameraPos :摄像机在世界空间下的位置,这可以由Unity的内置变量直接访问得到。

linearDepth * interpolatedRay :计算得到该像素相对于摄像机的偏移量

linearDepth:由深度纹理得到的线性深度值 将深度值从非线性空间转换到线性空间

interpolatedRay :由顶点着色器输出并插值后得到的射线,它不仅包含了该像素到摄像机的方向,也包含了距离信息。

interpolatedRay来源于对近裁剪平面的4个角的某个特定向量的插值,这4个向量包含了它们到摄像机的方向和距离信息,我们可以利用摄像机的近裁剪平面距离、FOV、横纵比计算而得。下图显示了计算时使用的一些辅助向量。

为了方便计算,我们可以先计算两个向量——toTop 和toRight,它们是起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量。它们的计算公式如下:

其中,Near是近裁剪平面的距离,FOV是竖直方向的视角范围,camera.up、 camera.right 分别对应了摄像机的正上方和正右方。

当得到这两个辅助向量后,我们就可以计算4个角相对于摄像机的方向了。我们以左上角为例,它的计算公式如下:


 

注意,上面求得的4个向量不仅包含了方向信息,它们的模对应了4个点到摄像机的空间距离。由于我们得到的线性深度值并非是点到摄像机的欧式距离,而是在z方向上的距离(如上图),因此,我们不能直接使用深度值和4个角的单位方向的乘积来计算它们到摄像机的偏移量,想要把深度值转换成到摄像机的欧式距离也很简单,我们以TL点为例,根据相似三角形原理,TL所在的射线上,像素的深度值和它到摄像机的实际距离的比等于近裁剪平面的距离和TL向量的模的比,即:

由此可得,我们需要的TL距离摄像机的欧氏距离dist:
 

由于4个点相互对称,因此其他3个向量的模和TL相等,即我们可以使用同一个因子和单位向量相乘,得到它们对应的向量值:

 

屏幕后处理的原理是使用特定的材质去渲染-一个刚好填充整个屏幕的四边形面片。这个四边形面片的4个顶点就对应了近裁剪平面的4个角。因此,我们可以把上面的计算结果传递给顶点着色器,顶点着色器根据当前的位置选择它所对应的向量,然后再将其输出,经插值后传递给片元着色器得到interpolatedRay, 我们就可以直接利用本节一开始提 到的公式重建该像素在世界空间下的位置了。

2        雾的计算

在简单的雾效实现中,我们需要计算一一个雾效系数 f,作为混合原始颜色和雾的颜色的混合系数:

float3 afterFog = f * fogColor + (1 - f) * origColor;

在Unity内置的雾效实现中支持三种计算方式,在给定距离z后,f的计算公式分别如下:

线性:f = \frac{d_{max}-|z|}{d_{max}-d_{min}}           dmin和dmax分别表示受雾影响的最小距离和最大距离。

指数:f = e^{-d|z|}                d是控制雾的浓度的参数

指数的平方:f = e^{-(d-|z|)^{2}}        d是控制雾的浓度的参数

在本节中,我们将使用类似线性雾的计算方式,计算基于高度的雾效。具体方法是,当给定一点在世界空间下的高度y后,f的计算公式为:

f = \frac{H_{end}-y}{H_{end}-H_{start}}

 Hstart和Hend分别表示受雾影响的起始高度和终止高度。

3        实现

后处理代码:

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;
		}  
	}

    //需要获取摄像机的相关参数,所以用两个变量存储摄像机的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;
		}
	}

	[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;    //控制雾的终止高度

    //设置获取摄像机的深度纹理时摄像机的相应状态
	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;
	}
	
	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);
		}
	}
}

OnRenderlmage首先计算了近裁剪平面的四个角对应的向量,并把它们存储在一一个矩阵类型的变量(frustumCormners)中。我们按一定顺序把这四个方向存储到了frustumCormers 不同的行中,这个顺序是非常重要的,因为这决定了我们在顶点着色器中使用哪一行作为该点的待插值向量。随后,我们把结果和其他参数传递给材质,并调用Graphics.Blit (src, dest, material)把渲染结果显示在屏幕上。

Shader代码:

Shader "Unity Shaders Book/Chapter 13/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;
		
		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 = UnityObjectToClipPos(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;
		}
		
		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
}

顶点着色器:我们对深度纹理的采样坐标进行了平台差异化处理。更重要的是,我们要决定该点对应了4个角中的哪个角。我们采用的方法是判断它的纹理坐标。在Unity中,纹理坐标的(0, 0)点对应了左下角,而(1, 1)点对应了右上角。我们据此来判断该顶点对应的索引,这个对应关系和我们在脚本中对frustumCorners 的赋值顺序是一致的。实际上,不同平台的纹理坐标不一定是满足上面的条件的,例如DirectX和Metal这样的平台,左上角对应了(0, 0)点,但大多数情况下Unity会把这些平台下的屏幕图像进行翻转,因此我们仍然可以利用这个条件。但如果在类似DirectX的平台,上开启了抗锯齿,Unity就不会进行这个翻转。为了此时仍然可以得到相应顶点位置的索引值,我们对索引值也进行了平台差异化处理,以便在必要时也对索引值进行翻转。最后,我们使用索引值来获取_FrustumCornersRay 中对应的行作为该顶点的interpolatedRay值。

片元着色器:首先,我们需要重建该像素在世界空间中的位置。为此,我们首先使用SAMPLE_DEPTH_TEXTURE对深度纹理进行采样,再使用LinearEyeDepth得到视角空间下的线性深度值。之后,与interpolatedRay相乘后再和世界空间下的摄像机位置相加,即可得到世界空间下的位置。得到世界坐标后,模拟雾效就变得非常容易。在本例中,我们选择实现基于高度的雾效模拟。我们根据材质属性_FogEnd 和_FogStart 计算当前的像素高度worldPos.y对应的雾效系数fogDensity, 再和参数_FogDensity 相乘后,利用saturate 函数截取到[0, 1]范围内,作为最后的雾效系数。然后,我们使用该系数将雾的颜色和原始颜色进行混合后返回。

猜你喜欢

转载自blog.csdn.net/weixin_51327051/article/details/123147773