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

 

若有任何问题,可留言或评论.

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值