
最近在尝试做一些比较复杂的效果,其中用到了像是沙漠、雪地等这种具有一定颗粒感的质感渲染。这次的文章就作为一个简单的铺垫,记录一下其中用到的一个小效果的思考历程。
其实单就地形本身的效果来说,算法上也并没有什么特殊之处,无非就是基础色+法线图+光照模型等常规的套路组合拳。但对于沙漠或雪地来说,经常能看到其伴随着一个特殊效果,就是扑了一层闪粉似的,会随着玩家视角的变化而变化的星光闪闪的效果。(下面惯例用原神里的效果来演示- -为凸显闪光点,图像经处理拉高了对比)

接下来是本文实现的效果。

这次的实现从原理上来说还是一如既往的弄虚作假。从物理上来说,这种闪烁的效果可能是因为沙地中一些比较光滑的矿物颗粒发生镜面反射产生的,但它只是一个锦上添花的细节,个人认为没必要为了这个再去做一张砂砾的法线图然后去算光照模型(而且我尝试过后也觉得效果并不好)。
本人曾做过一个类似的星空材质的效果,当时是用两层遮罩图去实现“闪烁”的动态效果。固定一层不动,另一层UV随时间平移,然后取两张图的乘积作为输出结果。

可以看到,在平铺参数合适的情况下效果还是比较自然的。而对于沙地的闪烁效果,视觉上与这个星光是相似的,只不过第二层的UV平移并不是每时每刻都在发生。
参考原神中的效果,通过观察可以总结出下面两点主要特征:
1、距离越远,闪点越少
2、只有在视角发生变化时才会闪烁
对于第一点,程序中可以拿场景深度(模型上一点到相机的距离)算一个衰减值乘上去;对于第二点,结合之前星空材质的实现思路,可以顺理成章的想到“用视向量控制UV偏移”的方案。
在实现过程中,本人尝试了3种不同的方案来实现效果,下面依次说明并比较优劣。

红色层级按模型UV正常采样,绿色层级按屏幕UV采样。优点是在完全俯视视角也有明显的闪烁效果;缺点是不论在什么视角下,闪烁的变化都过于敏感。
虽然标题可能有些唬人,但实际上又双叒叕是射线平面求交点,即前不久的冰层效果中刚用过的方法。

绿色层级在红色的基础上,于切空间内向下做了一定的偏移。优点是重复感低,闪烁随视角变化的敏感度适中;缺点是在完全俯视视角下的变化会偏弱。
其实就是在上个方案的基础上,计算交点用的向量不再取每个点的视向量,而是统一取相机的朝向向量来计算。

可以发现相对于上面的方案敏感度过高,同时又多了贴图重复感较强的缺点,俯视不变的问题也没有解决,基本可以pass了。
综合比较,我最终选择的是方案2。如果硬要完全俯视下还有闪烁变化,简单想想可以尝试利用视向量或相机朝向的Y值做一个分支,即Y超过一定大小时(说明接近俯视视角),原本的方案2过渡到方案1。不过如此一来就需要多一份采样的开销,可以权衡一下- -
最后附上方案2的完整代码(其中方案1,3保留注释)。
Shader "Scene/FlashGround"
{
Properties
{
[Header(Texture)]
_MainTex ("主贴图", 2D) = "gray" {}
[NoScaleOffset]_NormalMap ("法线图", 2D) = "bump" {}
_FlashTex ("闪烁遮罩", 2D) = "black" {}
[Space(20)]
[Header(Color)]
[HDR]_BrightCol ("亮部色", color) = (1.0, 1.0, 1.0, 1.0)
_DarkCol ("暗部色", color) = (0.1, 0.1, 0.1, 1.0)
[HDR]_SpecularCol ("高光色", color) = (1.0, 1.0, 1.0, 1.0)
_AmbCol ("环境色", color) = (1.0, 1.0, 1.0, 1.0)
[Space(20)]
[Header(Material)]
_NormalInt ("法线强度", range(0, 10)) = 1.0
_Rough ("粗糙度", range(0.001, 1)) = 0.5
_FresnelPow ("菲涅尔次幂", range(1, 10)) = 5.0
[Space(20)]
[Header(Flash)]
_FlashInt ("闪烁强度", float) = 10
_FlashOffset ("闪烁偏移", float) = -0.1
_FlashRange_Min ("闪烁最小衰减半径", float) = 5.0
_FlashRange_Max ("闪烁最大衰减半径", float) = 10.0
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalPipeline"
"RenderType" = "Opaque"
"Queue" = "Geometry"
}
Pass
{
Cull back
ZWrite on
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
CBUFFER_START(UnityPerMaterial)
//贴图
sampler2D _MainTex; float4 _MainTex_ST;
sampler2D _NormalMap;
sampler2D _FlashTex; float4 _FlashTex_ST;
//颜色
float3 _BrightCol;
float3 _DarkCol;
float3 _SpecularCol;
float3 _AmbCol;
//质感
float _NormalInt;
float _Rough;
float _FresnelPow;
//闪烁
float _FlashInt;
float _FlashOffset;
float _FlashRange_Min;
float _FlashRange_Max;
CBUFFER_END
//重映射01
float Remap(float min, float max, float input)
{
float k = 1.0 / (max - min);
float b = -min * k;
return saturate(k * input + b);
}
//计算任意平面交点
float3 GetPosAnyPlaneCrossDir(float3 posPlane, float3 posRay, float3 nDirPlane, float3 nDirRay)
{
float3 deltaPos = posPlane - posRay;
float temp = dot(nDirPlane, deltaPos) / dot(nDirPlane, nDirRay);
return temp * nDirRay + posRay;
}
struct a2v
{
float4 posOS : POSITION;
float3 nDirOS : NORMAL;
float4 tDirOS : TANGENT;
float2 uv0 : TEXCOORD0;
};
struct v2f
{
float4 posCS : SV_POSITION;
float3 posWS : TEXCOORD6;
float3 nDirWS : TEXCOORD0;
float3 tDirWS : TEXCOORD1;
float3 bDirWS : TEXCOORD2;
float3 vDirWS : TEXCOORD3;
float2 uv_Main : TEXCOORD4;
float4 uv_Flash : TEXCOORD5;
// float4 uv_Screen : TEXCOORD7;
};
v2f vert(a2v i)
{
v2f o;
//坐标
o.posWS = TransformObjectToWorld(i.posOS.xyz);
o.posCS = TransformWorldToHClip(o.posWS);
//向量
o.nDirWS = TransformObjectToWorldNormal(i.nDirOS);
o.tDirWS = TransformObjectToWorldDir(i.tDirOS.xyz);
o.bDirWS = cross(o.nDirWS, o.tDirWS) * i.tDirOS.w;
o.vDirWS = GetCameraPositionWS() - o.posWS;
//UV
o.uv_Main = TRANSFORM_TEX(i.uv0, _MainTex);
o.uv_Flash.xy = TRANSFORM_TEX(i.uv0, _FlashTex);
// o.uv_Screen = ComputeScreenPos(o.posCS);
//闪烁内层UV偏移
float3x3 TBN = float3x3(normalize(o.tDirWS), normalize(o.bDirWS), normalize(o.nDirWS));
float3 vDirTS = TransformWorldToTangent(o.vDirWS, TBN);
// vDirTS = TransformWorldToTangent(UNITY_MATRIX_V[2].xyz, TBN);//统一采用相机朝向
o.uv_Flash.zw = GetPosAnyPlaneCrossDir(float3(0, 0, _FlashOffset), float3(o.uv_Flash.xy, 0), float3(0,0,1), vDirTS).xy;
return o;
}
float4 frag(v2f i) : SV_Target
{
//法线转换
float3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv_Main));
nDirTS.xy *= _NormalInt;
float3x3 TBN = float3x3(normalize(i.tDirWS), normalize(i.bDirWS), normalize(i.nDirWS));
//向量
Light light = GetMainLight(TransformWorldToShadowCoord(i.posWS));
float3 nDirWS = normalize(mul(nDirTS, TBN));
float3 lDirWS = light.direction;
float3 vDirWS = normalize(i.vDirWS);
float3 hDirWS = normalize(lDirWS + vDirWS);
//光照
float lambert = saturate(dot(nDirWS, lDirWS));
float blinn = lambert * pow(saturate(dot(nDirWS, hDirWS)), 1.0 / (_Rough*_Rough));
float3 baseCol = tex2D(_MainTex, i.uv_Main).rgb;
float3 diffuseCol = baseCol * lerp(_DarkCol, _BrightCol, lambert);
float3 specularCol = _SpecularCol * blinn;
//环境光
float nv = saturate(dot(nDirWS, vDirWS));
float fresnel = pow(1.0 - nv, _FresnelPow);
float3 ambCol = fresnel * _AmbCol;
//闪烁
float flashMask = Remap(_FlashRange_Max, _FlashRange_Min, length(i.vDirWS));
float mask0 = tex2D(_FlashTex, i.uv_Flash.xy).r;
float mask1 = tex2D(_FlashTex, i.uv_Flash.zw).r;
// mask1 = tex2D(_FlashTex, _FlashTex_ST.zw * i.uv_Screen.xy / i.uv_Screen.w).r;//屏幕坐标
float flashCol = _FlashInt * flashMask * mask0 * mask1;
//混合
float3 finalCol = (diffuseCol + specularCol + flashCol) * light.shadowAttenuation + ambCol * diffuseCol;
return float4(finalCol, 1.0);
}
ENDHLSL
}
}
}