最近做一个效果涉及到各向异性着色,顺便记录一下。
各向异性,如果刚听过这个词的人,可能觉得绕口难懂,它的英文名:Anisotropy。它是各同向性(Isotropy)的反义词,各向同性这个词在第一次接触的时候就稍微好理解一点,就是各个方向矢量朝向相同嘛。那么前面带个AN的各向异性就是各个方向矢量朝向不同。
先贴上wiki百科吧:wike各向异性-中文
非均向性(anisotropy),或作各向异性,与各向同性相反,指物体的全部或部分物理、化学等性质随方向的不同而有所变化的特性,例如石墨单晶的电导率在不同方向的差异可达数千倍,又如天文学上,宇宙微波背景辐射亦拥有些微的非均向性。许多的物理量都具有非均向性,如弹性模量、电导率、在酸中的溶解速度等。
ps:我还是喜欢非均向性这个名词。
那么这和我们unity shader有什么关系呢?
其实也和我们需要渲染效果的光照计算有关,比如某些特殊材质,光照计算就和传统的不一样,拿unity官方wiki的来说:wiki unity anisotropy
这种金属的光照就比较适合用非均向性计算去做。
还有这种波浪发型的高光。
女性的大波浪头发,如果发质养护的很好,基本就和上面一样细腻有光泽。
我们细分来看,一根头发像一个sin函数曲线的扭曲圆柱体一样,普通的光照模型函数计算就能达到这种三段式高光的效果,如图:
传统的specular计算中半角向量和顶点(片段)法向量夹脚越小点积越大越亮。但是没有头发和金属那种“拉丝感”。
同时我们不可能让美术把女性一头的秀发给做出来,就算做出来了性能问题也无法实时渲染,只能制作网格面的方式把头发面片做出来。
我在amd ati官网找到了几个各向异性头发渲染的技术文档:amd hair rendering
anisotropic strand lighting model,各向异性kajiya-kay光照模型,认为某些材质的表面是由“一根一根丝细”覆盖而形成的表面,specular计算公式如下:
specular = sin(T,H)^gloss = (√(1-dot(T,H)^2))^gloss
公式中需要的参数计算,比如sin(a,b)的计算方法:
(1).dot(a,b)=|a|*|b|*cos(a,b)=cos(a,b)
(2).sin(a,b)^2+cos(a,b)^2=1
(3).sin(a,b)=√(1-cos(a,b)^2)=√(1-dot(a,b)^2)
shader中实现一下:
Shader "Anisotropy/AnisotropyKajiyaKayShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_AmbientFactor("Ambient Factor",Color) = (1,1,1,1)
_LightFactor("Light Factor",Color) = (1,1,1,1)
_DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
_SpecularFactor("_Specular Factor",Color) = (1,1,1,1)
_SpecularGloss("_Specular Gloss",Range(0,500)) = 1
[Enum(SEMANTIC,0,TEXTURE,1)]_AnisotropyType("Anisotropy Tangent Type",int) = 0
_AnisotropyTex("Anisotropy Texture",2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 tangentLightDir : TEXCOORD1;
float3 tangentViewDir : TEXCOORD2;
float3 tangentNormal : TEXCOORD3;
float3 tangentTangent : TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _AmbientFactor;
float4 _LightFactor;
float4 _DiffuseFactor;
float4 _SpecularFactor;
float _SpecularGloss;
int _AnisotropyType;
sampler2D _AnisotropyTex;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//在切线空间计算
float3 bitangent = cross(v.normal, v.tangent.xyz) * v.tangent.w;
float3x3 model2tangent = float3x3(v.tangent.xyz, bitangent, v.normal.xyz);
o.tangentLightDir = normalize(mul(model2tangent,ObjSpaceLightDir(v.vertex)));
o.tangentViewDir = normalize(mul(model2tangent,ObjSpaceViewDir(v.vertex)));
o.tangentNormal = normalize(mul(model2tangent,v.normal.xyz));
o.tangentTangent = normalize(mul(model2tangent,v.tangent.xyz));
return o;
}
float gotdot(float3 a,float3 b)
{
float d = dot(a,b);
return max(d,0);
}
float gotsin(float3 a,float3 b)
{
float sin = sqrt(1-pow(gotdot(a,b),2));
return sin;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex,i.uv);
float4 anisoColor = tex2D(_AnisotropyTex,i.uv);
float3 anisoTangent = normalize(UnpackNormal(anisoColor)); //切线空间切线向量
float3 tangentHalfDir = normalize(i.tangentLightDir+i.tangentViewDir); //切线空间半角向量
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _AmbientFactor;
fixed3 light = _LightColor0.rgb * _LightFactor;
float3 t;
if(_AnisotropyType == 0)
{
t = i.tangentTangent;
}
else if(_AnisotropyType == 1)
{
t = anisoTangent;
}
float3 h = tangentHalfDir;
float3 l = i.tangentLightDir;
float3 v = i.tangentViewDir;
float3 n = i.tangentNormal;
fixed3 diffuse = _LightColor0.rgb * gotdot(n,l)*_DiffuseFactor;
fixed3 specular = _LightColor0.rgb * pow(sqrt(1-pow(gotdot(t,h),2)),_SpecularGloss)*_SpecularFactor;
col *= fixed4(ambient+light+diffuse+specular,1);
return col;
}
ENDCG
}
}
}
代码需要注意的是和以前法线映射一样,我们需要在切线空间进行各种光照计算。
我们继续回顾以前切线空间中RGB和Vector转换的方法,切线空间中的normal(0,0,1),因为向量分量处于[-1,1],而颜色值分量处于[0,1],所以:
vector->pixel:pixel(rgb) = (vector(xyz) + 1) / 2 (也就是PackNormal)
pixel->vector:vector(xyz) = pixel(rgb) * 2 - 1; (也就是UnpackNormal)
所以常见的切线空间法线(normal(0,0,1))贴图是淡蓝色(0.5,0.5,1),而切线贴图(tangent(1,0,0))则是淡红色(1,0.5,0.5)。
效果如下:
我分别使用semantic语义的tangent和tangentTexture的color进行光照计算,效果一样,不过texture可以用ps制作,可以笔刷一些扰动效果。btw这个光照模型我是没看出展示了什么效果。可能改变一下各向异性贴图的“毛刺”感能看到效果吧,不过这种不是我需要的。
下面看下marschner模型:
两层高光,第一层tangent偏移向发梢,第二层tangent偏移向发根,说实话我想想不出来效果,哈哈,还是看下具体公式做法:
为了沿着头发的长度移动高光,我们沿着法线的方向移动切线,切线本身是和法线垂直的,切线与法线的叉积也就是副切线,切线绕着副切线旋转。具体的切线变换计算参数就由shifttexture保存。
用噪声纹理调节二级高光,加上图中计算specular的公式,我们就来shader实现一下:
Shader "Anisotropy/AnisotropyMarschnerShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_AmbientFactor("Ambient Factor",Color) = (1,1,1,1)
_LightFactor("Light Factor",Color) = (1,1,1,1)
_DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
_SpecularFactor("_Specular Factor",Color) = (1,1,1,1)
_SpecularGlossFirst("_Specular Gloss First",Range(0,500)) = 1
_SpecularGlossSecond("Specular Gloss Second",Range(0,500)) = 1
_SpecularColorFirst("Specular Color First",Color) = (1,1,1,1)
_SpecularColorSecond("Specular Color Second",Color) = (1,1,1,1)
_ShiftTex("First Shift Texture",2D) = "white" {}
_ShiftOffset("Shift Offset",Range(-1,1)) = 0
_NoiseTex("Second Noise Texture",2D) = "white" {}
_FirstShift("First Shift",Range(0,1)) = 0.5
_SecondShift("Second Shift",Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 tangentLightDir : TEXCOORD1;
float3 tangentViewDir : TEXCOORD2;
float3 tangentNormal : TEXCOORD3;
float3 tangentTangent : TEXCOORD4;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _AmbientFactor;
float4 _LightFactor;
float4 _DiffuseFactor;
float4 _SpecularFactor;
float _SpecularGlossFirst;
float _SpecularGlossSecond;
float _ShiftOffset;
sampler2D _ShiftTex;
sampler2D _NoiseTex;
float4 _SpecularColorFirst;
float4 _SpecularColorSecond;
float _FirstShift;
float _SecondShift;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//在切线空间计算
float3 bitangent = cross(v.normal, v.tangent.xyz) * v.tangent.w;
float3x3 model2tangent = float3x3(v.tangent.xyz, bitangent, v.normal.xyz);
o.tangentLightDir = normalize(mul(model2tangent,ObjSpaceLightDir(v.vertex)));
o.tangentViewDir = normalize(mul(model2tangent,ObjSpaceViewDir(v.vertex)));
o.tangentNormal = normalize(mul(model2tangent,v.normal.xyz));
o.tangentTangent = normalize(mul(model2tangent,v.tangent.xyz));
return o;
}
float3 shiftTangent(float3 t,float3 n,float shift)
{
float3 shiftedtangent = t + shift*n;
return normalize(shiftedtangent);
}
float strandSpecular(float3 t,float3 v,float3 l,float gloss)
{
float3 h = normalize(l+v);
float tdoth = dot(t,h);
float tsinh = sqrt(1-tdoth*tdoth);
float diratten = smoothstep(-1,0,dot(t,h));
return diratten*pow(tsinh,gloss);
}
fixed4 frag(v2f i) : SV_Target
{
float3 t = i.tangentTangent;
float3 n = i.tangentNormal;
float3 v = i.tangentViewDir;
float3 l = i.tangentLightDir;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _AmbientFactor;
fixed3 light = _LightColor0.rgb * _LightFactor;
float shift = tex2D(_ShiftTex,i.uv).r + _ShiftOffset;
float3 t1 = shiftTangent(t,n,_FirstShift+shift);
float3 t2 = shiftTangent(t,n,_SecondShift+shift);
fixed3 diffuse = _LightColor0.rgb * saturate(lerp(0.25,1,dot(n,l))) * _DiffuseFactor;
fixed3 specular = _SpecularColorFirst.rgb * strandSpecular(t1,v,l,_SpecularGlossFirst);
float noise = tex2D(_NoiseTex,i.uv).r;
specular += _SpecularColorSecond.rgb * noise * strandSpecular(t2,v,l,_SpecularGlossSecond);
fixed4 col = tex2D(_MainTex,i.uv);
col *= fixed4(ambient+light+diffuse+specular,1);
return col;
}
ENDCG
}
}
}
以上公式中参数:t=切线单位向量,l=点到光源单位向量,v=点到视口单位向量,h=半角单位向量
代码需要注意的几点:
1.使用贴图的r通道作为shift参数,所以贴图就是黑白的插值色,也就是r[0,1]
2.使用两层高光叠加计算,可以做到外边缘淡化的效果
3.我为了可能的tangent参数使用“粉红帖图”代替,可以ps笔刷“扰动”,依旧是在tangent空间计算,如果不需要可以改成world空间计算
4.参数我设置了很多,所以可调节选项很多
效果如下:
比较符合我要做的效果。
还有就是,我在百度找到一些博主介绍的其他算法,比如如下:
diffuse = sin(t,h)*factor
specular = (dot(t,l)*dot(t,v) + sin(t,l)*sin(t,v))*factor
可以看得出来diffuse,如果切线和半角向量夹角越大,diffuse强度越高
specular中,dot(t,l)(t,v)如果夹角越趋近90度,值越小,而sin(t,l)(t,v)如果夹角越大,则值越大。
继续写个shader看下效果:
Shader "Custom/AnisotropyTextureShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_AmbientFactor("Ambient Factor",Color) = (1,1,1,1)
_LightFactor("Light Factor",Color) = (1,1,1,1)
_DiffuseFactor("Diffuse Factor",Color) = (1,1,1,1)
_SpecularFactor("_Specular Factor",Color) = (1,1,1,1)
_SpecularGloss("_Specular Gloss",Range(0,500)) = 1
_AnisotropyTex("Anisotropy Texture",2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 tangentLightDir : TEXCOORD1;
float3 tangentViewDir : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _AmbientFactor;
float4 _LightFactor;
float4 _DiffuseFactor;
float4 _SpecularFactor;
float _SpecularGloss;
sampler2D _AnisotropyTex;
float _AnisotropyOffset;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//在切线空间计算
float3 bitangent = cross(v.normal, v.tangent.xyz) * v.tangent.w;
float3x3 model2tangent = float3x3(v.tangent.xyz, bitangent, v.normal.xyz);
o.tangentLightDir = normalize(mul(model2tangent,ObjSpaceLightDir(v.vertex)).xyz);
o.tangentViewDir = normalize(mul(model2tangent,ObjSpaceViewDir(v.vertex)).xyz);
return o;
}
float gotsin(float3 a,float3 b)
{
float sin = sqrt(1-pow(dot(a,b),2));
return sin;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex,i.uv);
float4 ansoCol = tex2D(_AnisotropyTex,i.uv);
float3 ansoVec = normalize(UnpackNormal(ansoCol)); //切线空间切线向量
float3 tangentHalfDir = normalize(i.tangentLightDir+i.tangentViewDir); //切线空间半角向量
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _AmbientFactor;
fixed3 light = _LightColor0.rgb * _LightFactor;
float3 t = ansoVec;
float3 h = tangentHalfDir;
float3 l = i.tangentLightDir;
float3 v = i.tangentViewDir;
fixed3 diffuse = _LightColor0.rgb * gotsin(t,h) * _DiffuseFactor;
fixed3 specular = _LightColor0.rgb * pow((dot(t,l)*dot(t,v) + gotsin(t,l)*gotsin(t,v)),_SpecularGloss)*_SpecularFactor;
col *= fixed4(ambient+diffuse+specular,1);
return col;
}
ENDCG
}
}
}
效果如下:
虽然有一丝金属感,但是高光方向很奇怪,我尝试了用普通的淡蓝色法线贴图,效果如下:
这就很正常,难道百度上的公式t变成了n?
当然如果我们不需要在贴图上进行扰动,也就是用ps笔刷改变切线rgb值,直接用tangent语义值就行了。
vert:
o.tangentTangent = normalize(mul(model2tangent,v.tangent.xyz));
frag:
float3 t = i.tangentTangent;
最后我百度google看到很多不同的各向异性金属毛发等效果计算公式,我们直接拿来用就行了。