1. 程式人生 > >全域性霧效 【Unity Shader入門精要13.3】

全域性霧效 【Unity Shader入門精要13.3】

霧效(fog)

unity 內建的霧效可以產生基於距離的線性或指數霧效。

然而,想要在自己編寫的頂點/片元著色器中實現這些霧效,需要在shader中新增【#pragma multi_compile_fog】指令,同時還需要使用相關的內建巨集,例如【UNITY_FOG_COORDS】【UNITY_TRANSFER_FOG】【UNITY_APPLY_FOG】等,缺點在於不僅需要為場景中所有物體新增相關的渲染程式碼,而且能夠實現的效果也非常有限,當需要對霧效進行一些個性化操作時,例如使用基於高度的霧效等,僅僅使用Unity內建的霧效就變得不再可行

 

基於屏幕後處理的全域性霧效的關鍵是:根據深度紋理來重建每個畫素在世界空間下的位置

即構建出當前畫素的NDC座標,再通過當前攝像機的視角*投影矩陣的逆矩陣來得到世界空間下的畫素座標,但是,這樣的實現需要在片元著色器中進行矩陣乘法的操作,而這通常會影響遊戲效能。

 

本節會學習一個快速從深度紋理中重建世界座標的方法:

這種方法首先對影象空間下的視椎體射線(從攝像機出發,指向影象上的某點的射線)進行插值,這條射線儲存了該影象在世界空間下到攝像機 的方向資訊,

然後我們把該射線和線性化後的視角空間下的深度值相乘,再加上攝像機的世界位置,就可以得到該畫素在世界空間下的位置,當得到世界座標後,就可以輕鬆使用各個公式來模擬全域性霧效了。


重建世界座標

如何從深度紋理中重建世界座標:

座標系中的一個頂點座標可以通過它相對於另一個頂點座標的偏移量來求得,重建畫素的世界座標也是基於這個想法。只需要知道攝像機在世界空間下的位置,以及世界空間下該畫素相對於攝像機的偏移量,把他們相加就可以得到該畫素到攝像機的世界座標。整個過程可以用下面的程式碼來表示

float4 _WorldPos=_WorldSpaceCameraPos+linearDepth*interpolatedRay;

_WorldSpaceCameraPos:攝像機在世界空間下的位置,這個可以由unity內建的變數直接訪問得到

linearDepth:由深度紋理得到的線性深度值(13.1.2講過獲取原理)

interpolatedRay:由定點著色器輸出並插值後得到的射線,他不僅包含了該畫素到攝像機的方向,也包含了距離資訊

linearDepth*interpolatedRay:得到相對於攝像機 的偏移量

P278


 

//全域性霧效 13.3

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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;
        }
    }
    //定義模擬霧效時使用的各個引數
    [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函式中設定攝像機的相應狀態*/
    private void OnEnable()
    {
        camera.depthTextureMode |= DepthTextureMode.Depth;
    }

    //實現OnRenderImage函式
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {/*首先計算了近似裁剪平面的四個對應角對應的向量,並把他們儲存在一個矩陣型別的變數(frustumCorners)
            按一定順序把這四個方向儲存到frustumConers不同的行中,這個順序非常重要,因為決定了我們在頂點著色器中使用哪一行作為該點的待插值向量。
            隨後,把結果和其他引數傳遞給材質,並呼叫Graphics.Blit把渲染結果顯示在螢幕上
             */
            
            Matrix4x4 frustumCorners = Matrix4x4.identity;//矩陣型別變數
            float fov = camera.fieldOfView;//FOV
            float near = camera.nearClipPlane;
            float far = camera.farClipPlane;
            float aspect = camera.aspect;//方向

            /*計算輔助向量
             toTop:起點位於裁平面中心,指向攝像機正上方
             toRight:起點位於裁平面中心,指向攝像機正右方
             Near:近裁剪平面的距離
             FOV:是豎直方向的視角範圍
             camera.up:攝像機的正上方
             camera.right:攝像機的正右方

            halfHeight=Near*tan[Fov/2]
            toTop=camera.up*halfHeight
            toRight=camera.right*halfHeight*aspect
             */
            float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);//
            Vector3 toRight = cameraTransform.right * halfHeight * aspect;
            Vector3 toTop = cameraTransform.up * halfHeight;

            /*
             topLeft【3】---------------topRight【2】
             |                               |
             |                               |
            bottomLeft【0】----------bottomRight【1】      */
            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.SetMatrix("_ViewProjectionInverseMatrix", (camera.projectionMatrix * camera.worldToCameraMatrix).inverse);

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

}

 

//全域性霧效 13.3

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unlit/Chapter13-FogWithDepthTexture"
{
	Properties
	{
		_MainTex ("Base(RGB)", 2D) = "white" {}
		_FogDensity("Fog Density",Float) = 1.0
		_FogStart("Fog Start",Color) = (1,1,1,1)
		_FogEnd("Fog End",Float) = 1.0
	}
		SubShader
		{
				CGINCLUDE

#include "UnityCG.cginc"


		float4x4 _FrustumCornersRay;//雖然沒有在Properies中宣告,但仍可以由指令碼傳遞給shader

		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		sampler2D _CameraDepthTexture;//深度紋理  unity會在背後把深度紋理傳遞給該值
		half _FogDensity;//霧的濃度
		fixed4 _FogColor;//霧的顏色
		float _FogStart;//霧起始位置
		float _FogEnd;//霧結束位置


		/*我們定義頂點位置,螢幕影象和深度紋理的紋理座標,InterpolatedRay變數儲存插值之後的畫素向量*/
		struct v2f {
			float4 pos:SV_POSITION;//頂點位置
			half2 uv:TEXCOORD0;//螢幕影象
			half2 uv_depth :TEXCOORD1;//深度紋理的紋理座標
			float4 interpolatedRay:TEXCOORD2;//插值之後的畫素向量
		};

		/*對深度紋理的取樣座標進行平臺差異化處理,
		更重要的是,要決定該點對應了4個角中的哪個角,
		採用的方法 是判斷他的紋理座標,
		unity    (0.1)--------(1.1)
		           |            |    =>這個對應關係和指令碼中的frustumCorners的賦值順序一致
				(0,0)-------(1.0)

		DirectX  (0.0)--------(1.0)=>但大多數情況下
		Metal     |             |
		         (0.1)--------(1.1)
		但大多數情況下unity會把這些平臺下的螢幕影象進行翻轉,因此我們仍可以利用這個條件,
		但如果在類似DirectX的平臺上開啟了抗鋸齒,unity就不會進行翻轉,
		為了此時仍然可以得到相應頂點位置索引值,我們對索引值也進行了平臺差異化處理,以便在必要時也對索引值進行翻轉
		最後,我們使用索引值來獲取_FrustumCornersRay中對應的行為為該頂點的interPolatedRay值

		儘管使用了很多判斷語句,但由於屏幕後處理所用的模型是一個四邊形網格,只包含4個頂點,因此這些操作不會對效能造成很大影響
		*/
		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{

		/*首先,需要重建該畫素在世界空間中的位置
		首先使用SAMPLE_DEPTH_TEXTURE對深度紋理進行取樣,再用LinearEyeDepth得到視角空間下的線性深度值
		之後,與interpolatedRay相乘後和世界空間下的攝像機位置相加,即可得到世界空間下的位置*/
		float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
		float3 worldPos = _WorldSpaceCameraPos + linearDepth * i. interpolatedRay.xyz;

		/*得到世界座標後,根據材質屬性_FogEnd和_FogStart計算當前的畫素高度worldPOS.y對應的霧效係數fogDensity
		再和引數_FogDensity相乘後,利用saturate函式擷取到[0,1]範圍內,作為最後的霧效係數*/
		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
}

本節介紹的使用深度紋理重建畫素的世界座標方法是非常有用的額,但需要注意的是這裡的實現是基於攝像機的投影型別是透視的前提下,如果需要在正交投影的情況下重建世界座標需要使用不同的公式