【Shader案例】鏡面反射

目錄

一、鏡面反射效果圖 

二、製作Plane鏡面

三、箱子Shader(被鏡面照射的物體)

四、製作將planeNormal和planePos傳遞給Shader的C#腳本


轉載:https://blog.csdn.net/linjf520/article/details/99647624

一、鏡面反射效果圖 

 

二、製作Plane鏡面

不透明物體且先於箱子渲染,模板測試寫入1,是一個表面着色器Lambert漫反射+顏色

Shader "Unlit/PlaneShader"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_ColorTint("_Color Tint", Color) = (1,1,1,1)
	}
	SubShader
	{
		//Queue是Geometry+1,它會在MirrorShader之前渲染。即先於鏡面反射的物體渲染,爲了寫入模板值1
		Tags { "RenderType" = "Opaque" "Queue" = "Geometry+1" }
		LOD 300
		//模板測試,寫入模板值爲1,目的:只讓鏡面部分產生鏡面效果,利用了模板測試進行的操作
		Stencil
		{
			Ref 1
			Comp Always
			Pass Replace
		}
		CGPROGRAM
		
		#pragma surface surf Lambert finalcolor:mycolor
		#pragma target 3.0
		sampler2D _MainTex;
		sampler2D _BumpTex;
		fixed4 _ColorTint;
		struct Input {
			float2 uv_MainTex;			
		};
		void surf(Input IN, inout SurfaceOutput o) {
			fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
			o.Albedo = tex.rgb;
			o.Alpha = tex.a;
		}
		void mycolor(Input IN, SurfaceOutput o, inout fixed4 color) {
			color = color * _ColorTint;
		}
		ENDCG
	}
	FallBack "Legacy Shaders/Diffuse"
}

三、箱子Shader(被鏡面照射的物體)

原理:2個Pass,第一個Pass渲染出Plane下的鏡面物體,第二個Pass渲染出原本的物體;

第一個Pass主要進行如下步驟,具體也可直接看代碼理解。(可選看)
① Tags可看出是一個不透明隊列,且在Plane之後渲染,目的是爲了進行模板測試,只渲染出模板值爲1的部分,也就是位於Plane區域的部分。

②Cull Front 裁剪正面,因爲鏡面物體渲染的是背面才正常,默認是裁剪背面,即渲染正面,你可以註釋掉這句代碼進行查看不正常的情況。

③ZTest Always 深度測試總是通過,目的是爲了將位於Plane物體下的鏡面物體渲染出來,否則鏡面物體無法通過深度測試而無法渲染出來,具體可註釋掉查看,默認是深度測試LEqual

④Blend 開啓透明混合(這就不解釋了吧)

⑤Stencil 深度測試, Ref指明模板值爲1,Comp Equal指明是隻有等於1的模板值區域能夠渲染出這個鏡面物體,若不瞭解什麼是模板測試,可百度或查看我這篇文章 https://blog.csdn.net/qq_39574690/article/details/106151722

⑥第一個Pass的頂點着色器計算出鏡面物體的頂點位置worldPos 和 物體和Plane鏡面的垂直距離distance,計算方式爲
distance = dot(planeNormal, 物體頂點位置-planePos);
dot(A,B) = |A||B|cos(A與B向量最小夾角),此時的A和B看成是單位向量,那麼dot(A,B)=cos(夾角)=垂直距離/|B|,此時還相差一個|B|距離(即(物體頂點位置-planePos)向量長度)的倍數,理論上要乘上這個向量長度纔是真正的垂直距離。所以你可以這麼做,對我代碼上的distance乘以這個向量長度。
計算出distance垂直距離後,通過planeNormal和distance計算出鏡面物體的頂點位置(世界空間),然後用這個新的頂點位置轉爲裁剪空間傳入v2f的vertex(作爲物體裁剪座標),所以此時已經完成了最關鍵的鏡面效果!下面是介紹透明、噪聲偏移效果。

⑦Alpha透明值的計算,利用distance/最大可視範圍得到一個係數,用此係數進行lerp(maxAlpha, minAlpha, 係數)得到最終透明值,效果是離鏡面越遠的圖像越透明。

⑧噪聲偏移值offsetXY,它是在片元着色器進行計算的,從噪聲圖採樣得出一個噪聲偏移值,然後針對這個值先後進行了整體偏移offset,縮放scale,擾動衰減atten,其中擾動衰減在_NoiseAttenWeight值爲1時,模擬水平效果,離鏡面越遠的圖像受到噪聲偏移值的影響越大,不過在效果圖上表現的不是很明顯,所以你想要表達的很誇張的話,可以給noiseAtten乘以一個倍數即可。

⑨在片元着色器對三種情況進行了剔除discard操作:
⑴ 鏡面物體離Plane(鏡面)大於_MirrorVisableDistance時進行剔除
⑵透明度小於等於0時進行剔除
⑶鏡面物體超出Plane(鏡面)部分被剔除

以上就是第一個Pass的介紹,而第二個Pass則是渲染正常物體情況,在片元着色器進行了對潛入鏡面的部分剔除discard

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unlit/MirrorShader"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_NoiseTex("噪聲紋理", 2D) = "white" {}
		[Space(10)]
		_MirrorVisableDistance("鏡像可視最大深度", Range(0,4)) = 4
		[Space(10)]
		_MirrorMinAlpha("鏡面物體最小透明度", Range(0,1)) = 0
		_MirrorMaxAlpha("鏡面物體最大透明度", Range(0,1)) = 1
		[Space(10)]
		_NoiseUVOffsetSpeedX("噪音UV X軸偏移速度", Range(0,10)) = 1
		_NoiseUVOffsetSpeedY("噪音UV Y軸偏移速度", Range(0,10)) = 1
		[Space(10)]
		_NoiseOffset("噪音偏移值的整體偏移值", Range(0,0.9)) = 0.25
		[Space(10)]
		_NoiseScaleX("噪音縮放值X", Range(0,1)) = 1
		_NoiseScaleY("噪音縮放值Y", Range(0,1)) = 1
		[Space(10)]
		//注意無擾動衰減,並不是無噪音偏移值的意思!衰減是針對噪音偏移值進行的
		_NoiseAttenWeight("噪音偏移衰減權重(0時完全無擾動衰減,1時有擾動衰減)", Range(0,1)) = 1
		[Space(10)]
		_AlphaFScale("透明度衰減敏感度", Range(0, 5)) = 1
	}
	SubShader
	{
		//位於Plane之後渲染,注意看Queue是Geometry+2,目的是爲了進行模板測試
		Tags { "RenderType" = "Transparent" "Queue" = "Geometry+2" }
		
		Pass
		{
			Cull front //裁剪正面 因爲鏡面的物體是倒立的物體,背面是優先渲染的 可註釋掉這句代碼來看問題
			ZTest Always //總是通過深度測試
			ZWrite Off	 //禁止深度寫入
			Blend SrcAlpha OneMinusSrcAlpha //開啓混合
			//鏡面物體渲染只有通過模板測試,即模板值爲1的部分渲染
			Stencil{
				Ref 1
				Comp Equal
			}
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag			

			#include "UnityCG.cginc"

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

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

			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _NoiseTex;
			float3 planeNormal; //平面法線(世界空間的歸一化平面Y軸向量)
			float3 planePos;	//平面位置(世界座標)
			float _MirrorVisableDistance;
			float _MirrorMinAlpha;
			float _MirrorMaxAlpha;
			float _NoiseUVOffsetSpeedX;
			float _NoiseUVOffsetSpeedY;
			float _NoiseOffset;
			fixed _NoiseScaleX;
			fixed _NoiseScaleY;
			fixed _NoiseAttenWeight;
			float _AlphaFScale;
			v2f vert(appdata v)
			{				
				v2f o;
				//1. 計算物體和平面距離distance 和 鏡面下的頂點位置worldPos(世界空間)
				float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
				float3 p = worldPos.xyz - planePos.xyz;
				float distance = dot(planeNormal, p); //dot(planeNormal, normalize(p)) * length(p); //Plane平面與物體頂點的垂直距離
				worldPos.xyz = worldPos.xyz + (-planeNormal) * (distance * 2); //反向偏移2倍距離得到新的頂點位置(世界空間)

				o.worldPos.xyz = worldPos.xyz;
				o.worldPos.w = distance;  //w值存儲distance

				float alpha = lerp(_MirrorMaxAlpha, _MirrorMinAlpha, _AlphaFScale * (distance / _MirrorVisableDistance));

				o.worldNormal.xyz = UnityObjectToWorldNormal(v.normal).xyz;
				o.worldNormal.w = alpha; //w值存儲alpha

				o.vertex = mul(UNITY_MATRIX_VP, worldPos); //世界轉裁剪空間
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				if (i.worldPos.w > _MirrorVisableDistance) discard; //距離大於可視距離拋棄
				if (i.worldNormal.w <= 0) discard; //透明度小於等於0拋棄

				float3 dir = i.worldPos.xyz - planePos; //注意此時worldPos是原物體頂點位置的反向位置(它是從頂點着色器計算出來的)
				half d = dot(dir, planeNormal);
				if (d > 0) discard; //超出鏡面拋棄,在鏡面下方d<=0

				//獲取噪聲值
				float2 offsetXY = float2(tex2D(_NoiseTex, i.uv + fixed2(_Time.x * _NoiseUVOffsetSpeedX, 0)).r,
					tex2D(_NoiseTex, i.uv + fixed2(0, _Time.x * _NoiseUVOffsetSpeedY)).r);
				offsetXY -= _NoiseOffset;                       //噪音偏移向量 整體偏移
				offsetXY *= fixed2(_NoiseScaleX, _NoiseScaleY); //噪音偏移向量 縮放

				//當_NoiseAttenWeight=1時,模擬水面-越深的圖像偏移越大,越淺的圖像偏移越小
				//當_NoiseAttenWeight=0時,水面下的圖像都進行噪音偏移(影響程度一樣)
				float noiseAtten = i.worldPos.w / _MirrorVisableDistance; //擾動衰減值(0~1) 越近鏡面的噪音偏移值越小(完全貼近的爲0),否則反之
				noiseAtten = lerp(1, noiseAtten, _NoiseAttenWeight); //_NoiseAttenWeight爲0時,無擾動 此時噪音偏移值影響最大, 反之,越遠平面的噪音偏移越大
				offsetXY *= noiseAtten;  //噪音偏移向量 衰減

				fixed4 col = tex2D(_MainTex, i.uv + offsetXY);
				return fixed4(col.rgb, i.worldNormal.w);
			}
			ENDCG
		}
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag			

			#include "UnityCG.cginc"

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

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

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float3 planeNormal;
			float3 planePos;
			v2f vert(appdata v)
			{
				v2f o;
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				float3 dir = i.worldPos - planePos;//注意此時的worldPos是正常的物體頂點世界位置
				half d = dot(dir, planeNormal);
				if (d < 0) discard; //正常物體渲染時,低於鏡面的拋棄
				fixed4 col = tex2D(_MainTex, i.uv);
				return col;
			}
			ENDCG
		}
	}
}

四、製作將planeNormal和planePos傳遞給Shader的C#腳本

可掛載到任意一個物體上,啓動遊戲時會將Plane物體的法線和位置傳遞給Mat材質的Shader上。

 

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

public class MirrorHelper : MonoBehaviour
{
    public GameObject plane;
    public Material mat;

    void Start()
    {
        mat.SetVector("planeNormal", plane.transform.up);
        mat.SetVector("planePos", plane.transform.position);
    }

    void Update()
    {
        mat.SetVector("planeNormal", plane.transform.up);
        mat.SetVector("planePos", plane.transform.position);
    }
}

 

若有任何問題,可留言或評論.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章