想实现一个被绿苔覆盖的岩石材质,绿苔只存在法线朝上的面上,先看unity中的效果。分析一下技术路线,需要采样两种纹理,绿苔纹理用三平面映射方式采样,岩石材质正常采样。采样后对两种纹理进行混合,为使两纹理过渡平滑自然,用噪声纹理创建过渡效果。接下来是具体的实现过程:
前期准备
准备一个岩石材质、一个绿苔材质、一张噪声纹理和一个岩石模型。
先写好一个标准的表面着色器,在Shader中声明属性和参数。因为噪声混合的区域我们需要透明裁剪效果,渲染模式用透明裁剪(TransparentCutout)。采用基于物理的光照模型函数Standard:
Shader"Shader Learn"{
Properties{
_MainTex("Albedo", 2D) = "white"{}
[NoScaleOffset]_Normal("Normal", 2D) = "bump"{}
[NoScaleOffset]_OcclusionMap("Occlusion", 2D) = "white"{}
_OcclusionStrength("Occlusion Strength", Range(0, 1)) = 1.0
[NoScaleOffset]_MetallicRough("MetallicRough", 2D) = "white"{}
_Metallic("Metallic", Range(0, 1) = 0
_Smoothness("Smoothness", Range(0, 1) = 0
_TopAlbedo("Top Albedo", 2D) = "white"{}
[NoScaleOffset]_TopNormal("Top Normal", 2D) = "bump" {}
[NoScaleOffset]_TopMetallicRough ("Metallic/Roughness (RGBA)", 2D) = "white" {}
_TopMetallic("Metallic", Range(0, 1)) = 0
_TopSmoothness("Smoothness", Range(0, 1)) = 0
[NoScaleOffset]_Noise ("Noise", 2D) = "white" {}
_noiseScale("NoiseScale", Range(0.0, 1.0)) = 1.0
}
SubShader{
Tags{"RenderType" = "TransparentCutout"
"Queue" = "AlphaTest"}
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma target 5.0
#include"UnityStandardUtils.cginc"
#include "UnityCG.cginc"
#include "AutoLight.cginc"
sampler2D _MainTex, _Normal, _OcclusionMap, _MetallicRough, _TopAlbedo, _TopNormal, _TopMetallicRough, _Noise;
float4 _Top_ST;
float4 _UVOffset;
half _OcclusionStrength, _Metallic, _Smoothness, _TopMetallic, _TopSmoothness, _NoiseScale;
struct Input{
float4 screenPos;
float3 worldPos;
float3 viewDir;
float3 worldNormal;
float2 uv_MainTex;
INTERNAL_DATA
};
void surf(Input IN, inout SurfaceOutputStandard o){
}
ENDCG
}
FallBack"Diffuse"
}
采样纹理
因为绿苔只存在与法线向上的面上,用三平面映射方式采样绿苔纹理,在unity中法线向上的轴是Y轴,所以以XZ的坐标作为采样的UV。在表面着色器中加入以下代码:
//采样Top纹理
float2 UVTop = IN.worldPos.xz * _TopScale;
fixed4 colTop = tex2D(_TopAlbedo, UVTop);
half3 normalTop = Unpack(tex2D(_TopNormal, UVTop));
half4 metallicTop = tex2D(_TopMetallic, UVTop);
//采样岩石纹理
fixed4 color = tex2D(_MainTex, IN.uv_MainTex);
half3 normal = Unpack(tex2D(_Normal, IN.uv_MainTex));
half4 metallic = tex2D(_MetallicRough, IN.uv_MainTex);
half occlusion = tex2D(_OcclusionMap, IN.uv_MainTex);
//采样噪声
fixed4 noise = tex2D(_Noise, UVTop);
纹理混合
真实的绿苔应该是长在岩石纹理之上的,是纹理之上再叠了一层纹理,而不是简单的纹理替换,所以就需要用到纹理混合技术。而对于两个法线的混合,不能像颜色贴图那样简单使用一个lerp函数。因为大多数法线贴图是存储在切线空间中的,为了保存两个法线的细节,我们用重定向法线映射(RNM)技术来混合法线。
而在顶面与底面的过渡时,为使过渡更加自然细腻。采样一张噪声图作为混合因子。可以从下图看到,用噪声后过渡处更加随机,可以通过调节噪声图的大小调节过渡效果。
混合因子
根据法线朝向Y轴的角度,得到它的混合因子。因为法线是一个单位向量,法线y分量的大小就可以代表法线与Y轴的角度,y分量越接近1,说明法线越朝向与Y轴。但是过渡应该存在于一个比较小的范围,可以用smoothstep函数把平滑效果限制在特定区域:
IN.worldNormal = WorldNormalVector(IN, float3(0, 0, 1));
float blend = clamp(IN.worldNormal.y, 0, 1);
blend = smoothstep(noise, 1, blend);
noiseBlend = smoothstep(0.1, 0.5, blend);
混合法线
RNM技术是通过重新调整法线向量的方法来处理法线的混合,根据基础法线纹理的x,y轴进行旋转得到重定向后的矩阵,再应用该矩阵对另一个法线贴图进行变换得到混合后的结果。
具体可看:Blending in Detail
将它转为函数的形式方便调用:
half3 blend_rnm(half3 n1, half3 n2)
{
n1.z += 1;
n2.xy = -n2.xy;
return n1 * dot(n1, n2) / n1.z - n2;
}
混合顶部纹理,因为normal和normalTop都是采样法线贴图得出的,他们是基于切线空间的法线,直接混合它们,得到的结果只是切线空间中两个细节法线的组合,它无法正确反映物体的整体几何朝向,我们需采用世界空间的法线进行混合:
half3 absVertNormal = abs(IN.worldNormal);
normalBlend = blend_rnm(half3(IN.worldNormal.xz, absVertNormal.y), normalTop);
half3 normalBlend = normalize(normalBlend.xzy);
float3 normalBlend = WorldToTangentNormalVector(IN, worldNormal);
o.Normla = lerp(normal, normalBlend, noiseBlend);
混合其他属性:
o.Albedo = lerp(color, colTop, noiseBlend);
o.Occlusion = lerp(1, occlusion, (float)_OcclusionStrength);
o.Metallic = lerp(metallic.r * _Metallic, noise.r * _TopMetallic, noiseBlend);
o.Smoothness = lerp(metallic.a * _Glossiness, _TopMetallic.a * _TopGlossiness, noiseBlend);
最后就可以得到开头的效果啦。