目录
知乎看见了大佬的帖子,想尝试在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之间的夹角θ)
查看其在球体上的表现:
此时我们可以根据球体的输出结果再次验证一下法线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(深度写入)
在染色层中,仅实现颜色的输出即可
2、反射层
- 新建标准的ASE,除颜色外,考虑直接光高光等光照,命名为Outer
- 由于需要进行色彩的混合——Additive(叠加),我们在侧边栏进行设置
- 由于是半透明效果,需要关闭ZWrite(深度写入)
这里与染色层的连线仅仅是有以下不同:
+++ 添加了直接光高光反射(布林-冯)+++
+++ 对颜色使用了gamma空间-线性空间的转化(仅为优化视觉效果)+++
+++ 添加了金属/光滑度控制 +++
3、ASE中同时使用两层Pass
网络上在ASE中使用多层Pass的相关资料不多,自己摸索出来了一些方式,不知道是否正确,欢迎讨论交流!
对于内层【染色层】:
- 重新设置ASE属性并给Pass命名
对于外层【反射层】:
- 在Additional Use Passes部分添加其余Pass,Below/Above
三、代码实现
由于自己复现的代码存在一定的问题,这里先贴出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)