在Unity中实现简易的镭射效果

目录

一、前置知识

         1、菲涅尔效应

2、为何选择菲涅尔效应?

3、Shading Model 着色模型

4、Blend 叠加模式

二、ASE实现

1、染色层

 2、反射层

3、ASE中同时使用两层Pass

三、代码实现

四、存在的问题


知乎看见了大佬的帖子,想尝试在Unity中实现一下镭射材质的shader,记录一下学习过程~


一、前置知识

1、菲涅尔效应

本次实现涉及到的原理仅有此条:菲涅尔效应(Fresnel Effect)


我们可以通过日常现象来理解菲涅尔效应:

当我们走在湖边时看向湖面——>看到部分湖底的样子,部分岸边景色的倒影

当我们垂直看向湖面的时候——>几乎完全看到湖底,几乎完全看不到岸边景色的倒影

由此得出一个粗暴的结论

物体法线N视线V之间的夹角越大,看到其他物体反射过来的光线越(眼中看到其他折射的景色)

物体法线N视线V之间的夹角越小,看到其他物体反射过来的光线越,看到物体本身比较多(看到湖底)


我们也可以通过数学方程来理解菲涅尔效应:

R(θ) = R(0)+(1- R(0))*(1-cos(θ))^5
R(0) = (n1 - n2)²/(n1 + n2)²

R(θ) 表示反射光线强度,其中θ指半角向量视线(摄像机)方向的夹角

R(0)表示0角度下的菲涅尔反射,n1是光源通常为空气或真空下的折射率,n2是表面材料的折射率,折射率根据材料的不同有所差异。大部分开发者在描述菲涅尔效应的着色效果编写中都会忽略折射率的影响,即n1 = n2

带入n1 = n2后可以得出公式:

R(θ) =(1-cos(θ))^5

通过该公式可以看出:

θ越大,cos(θ)越小,1-cos(θ)越大,(1-cos(θ))^5越大

θ越小,cos(θ)越大,1-cos(θ)越小,(1-cos(θ))^5越小

因此我们可以得出结论:半角向量视线(摄像机)V方向的夹角越大,反射强度越大,由于半角向量通常用于替代法线N方向

我们可以进一步推导出:物体法线N视线V之间的夹角越大,看到其他物体反射过来的光线越

注:半角向量=光源向量+视线(摄像机)方向


总结:菲涅尔效应映射到实际操作中,核心就是求出法线方向N与视线方向V之间的夹角


2、为何选择菲涅尔效应?

由上文可知,实操中关键在于求出法线方向N与视线方向V之间的夹角

(Shader中我们通常使用点乘dot()来计算夹角)

我们可以先使用ASE插件简单输出一下,菲涅尔的数学公式:1-cos(N与V之间的夹角θ)

查看其在球体上的表现:

ASE中连线
 
输出结果
 

 此时我们可以根据球体的输出结果再次验证一下法线N与视线V之间的夹角θ与反射强度的关系

以此时的球面中心点为例,颜色最黑——>该点法线N与视线V之间的夹角θ几乎为0,所以差不多没有反射光线投射出来


回归正题,我们可以惊喜地发现该公式在球体上的表现特点:

从内到外,从黑到白,连续均匀地将(0,1)映射在球体上

那么谁还有这样的特点呢!镭射材质的颜色!将色相上的颜色连续均匀地映射在物体表面!

并且在覆盖黑/白纯色时表现出的色相刚好是相反的!

覆盖白/黑底色恰好表现出相反的色相(图源网络)
 

所以我们选择菲涅尔效应核心公式R(θ) =(1-cos(θ))^5来控制表现镭射材质的颜色!


3、Shading Model 着色模型

简而言之就是研究物体材质由多少层构成,每一层受到了何种光照/等,层与层之间的混合关系

在本次镭射shader的实现过程中,主要采用了两层的光照模型

(划分成两层的原因请见参考文章)

内层【染色层】:不受光

外层【反射层】:受到直接光高光/间接光漫反射镜面反射等


4、Blend 叠加模式

在半透明的前提下开启叠加模式~

内层【染色层】:开启blend混合,采用multiply(正片叠底)的混合模式

正片叠底

外层【反射层】:开启blend混合,采用additive(叠加)的混合模式

柔性叠加

二、ASE实现

1、染色层

- 在染色层的ASE实现中,我们需要新建一个Unlit(无光模式)的ASE,命名为Inner

- 由于需要进行色彩的混合——Multiply(正片叠底),我们在侧边栏进行设置

- 由于是半透明效果,需要关闭ZWrite(深度写入)

在染色层中,仅实现颜色的输出即可

染色层ASE属性设置
 
染色层ASE连线
 
染色层效果

 2、反射层

- 新建标准的ASE,除颜色外,考虑直接光高光等光照,命名为Outer

- 由于需要进行色彩的混合——Additive(叠加),我们在侧边栏进行设置

- 由于是半透明效果,需要关闭ZWrite(深度写入)

这里与染色层的连线仅仅是有以下不同

+++ 添加了直接光高光反射(布林-冯)+++

+++ 对颜色使用了gamma空间-线性空间的转化(仅为优化视觉效果)+++

+++ 添加了金属/光滑度控制 +++

反射层ASE属性设置
 
反射层ASE连线
 
反射层效果
 

3、ASE中同时使用两层Pass

网络上在ASE中使用多层Pass的相关资料不多,自己摸索出来了一些方式,不知道是否正确,欢迎讨论交流!

对于内层【染色层】:

- 重新设置ASE属性并给Pass命名

重新设置染色层属性

对于外层【反射层】:

- 在Additional Use Passes部分添加其余Pass,Below/Above

设置多层Pass
 

两层Pass
 
镭射材质在其他物体上的表现效果
 

三、代码实现

由于自己复现的代码存在一定的问题,这里先贴出ASE自动生成的代码部分,可读性比较差些

Shader "04BM/Combine"
{
	Properties
	{
		_RimPower("RimPower", Float) = 0
		_RimBias("RimBias", Range( 0 , 1)) = 0
		_RimScale("RimScale", Float) = 0
		_DirectSpec("DirectSpec", Float) = 1
		_Metallic("Metallic", Float) = 1
		_Smoothness("Smoothness", Float) = 1
		_Saturation("Saturation", Range( 0 , 1)) = 0
		_Value("Value", Range( 0 , 1)) = 0
		[HideInInspector] __dirty( "", Int ) = 1
	}

	SubShader
	{
		Pass
		{
			ColorMask 0
			ZWrite On
		}

		UsePass "04BM/Inner/"
		Tags{ "RenderType" = "Custom"  "Queue" = "Transparent+0" "IsEmissive" = "true"  }
		Cull Back
		ZWrite Off
		Blend OneMinusDstColor One
		
		CGINCLUDE
		#include "UnityCG.cginc"
		#include "UnityPBSLighting.cginc"
		#include "Lighting.cginc"
		#pragma target 4.6
		struct Input
		{
			float3 worldNormal;
			float3 viewDir;
			float3 worldPos;
		};

		uniform float _RimPower;
		uniform float _RimBias;
		uniform float _RimScale;
		uniform float _Saturation;
		uniform float _Value;
		uniform float _DirectSpec;
		uniform float _Metallic;
		uniform float _Smoothness;


		float3 HSVToRGB( float3 c )
		{
			float4 K = float4( 1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0 );
			float3 p = abs( frac( c.xxx + K.xyz ) * 6.0 - K.www );
			return c.z * lerp( K.xxx, saturate( p - K.xxx ), c.y );
		}


		void surf( Input i , inout SurfaceOutputStandard o )
		{
			float4 color50 = IsGammaSpace() ? float4(0.5377358,0.5208259,0.5208259,0) : float4(0.2506461,0.2338261,0.2338261,0);
			o.Albedo = ( float4( 0,0,0,0 ) * color50 ).rgb;
			float3 ase_worldNormal = i.worldNormal;
			float dotResult7 = dot( ase_worldNormal , i.viewDir );
			float clampResult17 = clamp( dotResult7 , 0.0 , 1.0 );
			float3 hsvTorgb19 = HSVToRGB( float3(( ( pow( ( 1.0 - clampResult17 ) , _RimPower ) + _RimBias ) * _RimScale ),_Saturation,_Value) );
			float3 temp_cast_1 = (2.2).xxx;
			float3 ase_worldPos = i.worldPos;
			#if defined(LIGHTMAP_ON) && UNITY_VERSION < 560 //aseld
			float3 ase_worldlightDir = 0;
			#else //aseld
			float3 ase_worldlightDir = normalize( UnityWorldSpaceLightDir( ase_worldPos ) );
			#endif //aseld
			float3 normalizeResult37 = normalize( ( ase_worldlightDir + i.viewDir ) );
			float dotResult39 = dot( normalizeResult37 , ase_worldNormal );
			float clampResult54 = clamp( dotResult39 , 0.0 , 1.0 );
			float3 temp_cast_2 = (0.4545455).xxx;
			o.Emission = pow( ( pow( hsvTorgb19 , temp_cast_1 ) + pow( clampResult54 , _DirectSpec ) ) , temp_cast_2 );
			o.Metallic = _Metallic;
			o.Smoothness = _Smoothness;
			o.Alpha = 1;
		}

		ENDCG
		CGPROGRAM
		#pragma surface surf Standard keepalpha fullforwardshadows 

		ENDCG
		Pass
		{
			Name "ShadowCaster"
			Tags{ "LightMode" = "ShadowCaster" }
			ZWrite On
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 4.6
			#pragma multi_compile_shadowcaster
			#pragma multi_compile UNITY_PASS_SHADOWCASTER
			#pragma skip_variants FOG_LINEAR FOG_EXP FOG_EXP2
			#include "HLSLSupport.cginc"
			#if ( SHADER_API_D3D11 || SHADER_API_GLCORE || SHADER_API_GLES || SHADER_API_GLES3 || SHADER_API_METAL || SHADER_API_VULKAN )
				#define CAN_SKIP_VPOS
			#endif
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			#include "UnityPBSLighting.cginc"
			struct v2f
			{
				V2F_SHADOW_CASTER;
				float3 worldPos : TEXCOORD1;
				float3 worldNormal : TEXCOORD2;
				UNITY_VERTEX_INPUT_INSTANCE_ID
				UNITY_VERTEX_OUTPUT_STEREO
			};
			v2f vert( appdata_full v )
			{
				v2f o;
				UNITY_SETUP_INSTANCE_ID( v );
				UNITY_INITIALIZE_OUTPUT( v2f, o );
				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO( o );
				UNITY_TRANSFER_INSTANCE_ID( v, o );
				float3 worldPos = mul( unity_ObjectToWorld, v.vertex ).xyz;
				half3 worldNormal = UnityObjectToWorldNormal( v.normal );
				o.worldNormal = worldNormal;
				o.worldPos = worldPos;
				TRANSFER_SHADOW_CASTER_NORMALOFFSET( o )
				return o;
			}
			half4 frag( v2f IN
			#if !defined( CAN_SKIP_VPOS )
			, UNITY_VPOS_TYPE vpos : VPOS
			#endif
			) : SV_Target
			{
				UNITY_SETUP_INSTANCE_ID( IN );
				Input surfIN;
				UNITY_INITIALIZE_OUTPUT( Input, surfIN );
				float3 worldPos = IN.worldPos;
				half3 worldViewDir = normalize( UnityWorldSpaceViewDir( worldPos ) );
				surfIN.viewDir = worldViewDir;
				surfIN.worldPos = worldPos;
				surfIN.worldNormal = IN.worldNormal;
				SurfaceOutputStandard o;
				UNITY_INITIALIZE_OUTPUT( SurfaceOutputStandard, o )
				surf( surfIN, o );
				#if defined( CAN_SKIP_VPOS )
				float2 vpos = IN.pos;
				#endif
				SHADOW_CASTER_FRAGMENT( IN )
			}
			ENDCG
		}
	}
	Fallback "Diffuse"
	CustomEditor "ASEMaterialInspector"
}

四、存在的问题

1、暂时还没学习到如何(手动)在shader代码中实现smoothness和metallic,所以在博客的代码部分实现效果会打折扣,未来会继续补充

2、在使用代码复现的过程中,NdotV得到的结果显示到材质球上后,黑色区域呈现椭圆形,是由于在顶点Shader中计算世界空间下的坐标pos时,使用的向量不当,这里正确的代码应该是:

o.pos_world = normalize(mul(unity_ObjectToWorld, v.vertex).xyz);

刚入门shader,如有错误,欢迎交流讨论~

参考文章:

万物皆可镭射,个性吸睛的材质渲染技术 - 知乎 (zhihu.com)

Shader实验室:菲涅尔效应 - 知乎 (zhihu.com)

(17条消息) 全息投影特效制作详解_0卡卡0的博客-CSDN博客_全息投影制作

(21条消息) Shader Blend 混合效果_lsw5530的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值