Normal Map
法线纹理是通过一张与漫反射纹理相对应的法线图,存储法线信息,使用的时候对应纹理坐标进行采样,通过法线值影响光影计算的结果,从而产生凹凸效果。Normal Map可能是目前使用最为广泛的一种凹凸贴图技术了。之前的内容也有介绍过, https://zhuanlan.zhihu.com/p/31450857 这里不详述。贴一下Shader代码:
Shader "Bump/001_normal"
{
Properties{
_MainColor("MainColor",Color)=(1,1,1,1)
_SpecularColor("SpecularColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_BumpTex("BumpTex",2D)="bump"{}
_BumpScale("BumpScale",Float)=1.0
_Gloss("Gloss",Range(8.0,256))=20
}
SubShader{
Pass{
Tags{"RenderType"="Opaque" "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#define PI 3.14159265359
fixed4 _MainColor;
fixed4 _SpecularColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _BumpScale;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float4 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
//主纹理与法线纹理通常使用同一组纹理坐标
o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
o.uv.zw=v.texcoord.xy*_BumpTex_ST.xy+_BumpTex_ST.zw;
//内置宏,取得切线空间旋转矩阵
TANGENT_SPACE_ROTATION;
o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex).xyz);
o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex).xyz);
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed3 tangentLightDir=normalize(i.lightDir);
fixed3 tangentViewDir=normalize(i.viewDir);
fixed3 tangentNormal=UnpackNormal(tex2D(_BumpTex,i.uv.zw));
tangentNormal.xy*=_BumpScale;
tangentNormal.z=sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));
fixed3 albedo=_MainColor.rgb*tex2D(_MainTex,i.uv.xy);
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;
//改进版 BRDF 函数
fixed3 diffuse=_LightColor0.rgb*albedo*max(0,saturate(dot(tangentNormal,tangentLightDir)))/PI;
fixed3 halfDir=normalize(tangentLightDir+tangentViewDir);
fixed3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss)*max(0,saturate(dot(tangentNormal,tangentLightDir)))*(_Gloss+8)/(8*PI);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
Parallax Map
法线纹理在运用中有一个问题是,当视角发生变化时,并不会影响到凹凸的结果(漫反射计算与视角方向无关)。而实际上,当视角发生变化时,观察到的凹凸不平表面的结果是不同的,为了尽量反映出凹凸效果与视角的相关性,有了Parallax Map和后面的Relief Map。Parallax Map 叫做视差贴图,通过下图(来自“Parallax Mapping with Offset Limiting: A PerPixel Approximation of Uneven")简单了解下:
假如我们从eye所示方向观察表面,由于表面的凹凸关系,我们实际看到的点应该是B点(即此时应该从B点处的纹理坐标进行采样),但由于纹理本身是一张平面图,所以此时计算用的是A点的纹理坐标采样结果。为了纠正这一偏差结果,需要将采样点的纹理坐标进行适当偏移,使它靠近正确的B点,所以 Parallax Map 又叫做 Offset Map。但是想要正确的找到A点相对于B点的偏移量是比较麻烦的,大多是采用近似的偏移量来靠近B点(并不能精确到B点),这里说一种:
通过视角方向在切线空间下的分量来确定方向,通过对应的高度图中的采样结果来确定偏移距离,即高度值大的偏移距离大:
//根据切线空间下的视角方向计算UV的采样偏移
inline float2 CaculParallaxUVOffset(v2f i){
//高度图高度采样
float height=tex2D(_HeightTex,i.uv).r;
float3 viewDir=normalize(i.viewDir);
float2 offset=viewDir.xy/viewDir.z*height*_HeightScale;
return offset;
}
这里用一个_HightScale系数外部控制偏移程度。Parallax Map的关键在于对切线空间的法线进行采样并计算之前,通过视角方向上的偏移纠正采样时的纹理坐标,使采样结果尽量靠近正确的采样点。
完整Shader:
Shader "Bump/002_parallax"
{
Properties{
_MainColor("MainColor",Color)=(1,1,1,1)
_SpecularColor("SpecularColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_BumpTex("BumpTex",2D)="bump"{}
_HeightTex("HeightTex",2D)="black"{}
_HeightScale("HeightScale",Range(0,0.2))=0.05
_Gloss("Gloss",Range(8,255))=20
}
SubShader{
Pass{
Tags{"RenderType"="Opaque" "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#define PI 3.14159265359
fixed4 _MainColor;
fixed4 _SpecularColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
sampler2D _HeightTex;
float _HeightScale;
float _Gloss;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
//根据切线空间下的视角方向计算UV的采样偏移
inline float2 CaculParallaxUVOffset(v2f i){
//高度图高度采样
float height=tex2D(_HeightTex,i.uv).r;
float3 viewDir=normalize(i.viewDir);
float2 offset=viewDir.xy/viewDir.z*height*_HeightScale;
return offset;
}
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
TANGENT_SPACE_ROTATION;
o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex).xyz);
o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex).xyz);
return o;
}
fixed4 frag(v2f i): SV_Target{
fixed3 albedo=_MainColor.rgb*tex2D(_MainTex,i.uv);
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb*albedo;
//在对法线进行采样前,新进行UV偏移
i.uv+=CaculParallaxUVOffset(i);
fixed3 tangentNormalDir=UnpackNormal(tex2D(_BumpTex,i.uv));
fixed3 tangentLightDir=normalize(i.lightDir);
fixed3 tangentViewDir=normalize(i.viewDir);
fixed3 halfDir=normalize(tangentViewDir+tangentLightDir);
//改进版 BRDF
fixed3 diffuse=_LightColor0.rgb*ambient*max(0,saturate(dot(tangentNormalDir,tangentLightDir)))/PI;
fixed3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(max(0,dot(tangentNormalDir,halfDir)),_Gloss)*max(0,saturate(dot(tangentNormalDir,tangentLightDir)))*(_Gloss+8)/(8*PI);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
实现效果:
这里将之前的Normal Map与Parallax Map进行对比(左:Normal 右:Parallax),在视角变化的情况下,Parallax的凹凸效果会发生变化。
Relief Map
Relief Map又叫 浮雕纹理 ,是对 Parallax的进一步精确。如果说Parallax只是根据视角方向在切线空间下的投影和高度图作近似的偏移,那么Relief Map则是以找到正确点B点进行采样为目标,具体分为两步:
- 通过步进法找到交点的大致范围
- 通过二分法进一步找到交点
步进法找到交点大致范围,其中一种思路:
根据切线空间下的视角方向的垂直分量,确定层级高度和UV每次偏移采样的步进距离,视角方向的垂直分量越大,说明需要偏移的距离越小,因此划分的密度越大。每次步进时,层高逐层增加,并沿着视角方向的UV偏移进行高度图采样,直到满足 currentLayerDepth>currentDepthMapValue的条件,即图中红线和黄线所示,说明此时已经找到对应交点的大致范围,进入二分法精确确定交点,这一步的具体算法:
//根据 切线空间视角方向在垂直于纹理表面的分量,确定步进的层数,越接近垂直,层数越多,步进距离越小
float layerNum=lerp(_MinLayerNum,_MaxLayerNum,abs(dot(float3(0,0,1),tangent_viewDir)));
float layerDepth=1.0/layerNum;
float currentLayerDepth=0.0;
float2 deltaUV=tangent_viewDir.xy/tangent_viewDir.z*_HeightScale/layerNum;
float2 currentTexCoords=uv;
float currentDepthMapValue=tex2D(_DepthTex,currentTexCoords).r;
while(currentLayerDepth<currentDepthMapValue){
currentTexCoords-=deltaUV;
//在循环内需要加上unroll来限制循环次数或者改用tex2Dlod,直接使用tex2D采样会出现报错
currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
currentLayerDepth+=layerDepth;
}
二分法精确求交点:
在确定大致范围后,步进距离每次减半,直到逼近目标值,一般是进行五次二分逼近能得到比较接近的结果:
Relief Map与Parallax Map的区别在于一个是精确求点,一个是向正确方向大致偏移,后续的处理结果类似。 完整Shader:
Shader "Bump/003_relief"
{
Properties
{
_MainColor("MaincColor",Color)=(1,1,1,1)
_SpecularColor("SpecualrColor",Color)=(1,1,1,1)
_MainTex("MainTex",2D)="white"{}
_BumpTex("BumpTex",2D)="bump"{}
_DepthTex("DepthTex",2D)="black"{}
_Gloss("Gloss",Range(8,256))=20
_HeightScale("HightScale",Range(-1.0,1.0))=0.1
_MinLayerNum("MinlayerNum",Range(0,100))=30
_MaxLayerNum("MaxLayerNum",Range(0,200))=50
}
SubShader{
Tags{"RenderType"="Opaque"}
Pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#define PI 3.14159265359
fixed4 _MainColor;
fixed4 _SpecularColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
sampler2D _DepthTex;
float _Gloss;
float _HeightScale;
float _MinLayerNum;
float _MaxLayerNum;
struct a2v{
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float3 normal:NORMAL;
float4 tangent:TANGENT;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
//通过步进方式找到 视角方向 与纹理交点 的实际高度值
float2 ReliefMappingUV(float2 uv,float3 tangent_viewDir){
//根据 切线空间视角方向在垂直于纹理表面的分量,确定步进的层数,越接近垂直,层数越多,步进距离越小
float layerNum=lerp(_MinLayerNum,_MaxLayerNum,abs(dot(float3(0,0,1),tangent_viewDir)));
float layerDepth=1.0/layerNum;
float currentLayerDepth=0.0;
float2 deltaUV=tangent_viewDir.xy/tangent_viewDir.z*_HeightScale/layerNum;
float2 currentTexCoords=uv;
float currentDepthMapValue=tex2D(_DepthTex,currentTexCoords).r;
while(currentLayerDepth<currentDepthMapValue){
currentTexCoords-=deltaUV;
//在循环内需要加上unroll来限制循环次数或者改用tex2Dlod,直接使用tex2D采样会出现报错
currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
currentLayerDepth+=layerDepth;
}
//进行二分法查找
float2 halfDeltaUV=deltaUV/2.0;
float halfLayerDepth=layerDepth/2.0;
currentTexCoords+=halfDeltaUV;
currentLayerDepth+=halfLayerDepth;
int searchesNum=5;
for(int i=0;i<searchesNum;i++){
halfDeltaUV=halfDeltaUV/2.0;
halfLayerDepth=halfLayerDepth/2.0;
currentDepthMapValue=tex2Dlod(_DepthTex,float4(currentTexCoords,0,0)).r;
if(currentLayerDepth<currentDepthMapValue){
currentTexCoords-=halfDeltaUV;
currentLayerDepth+=halfLayerDepth;
}
else{
currentTexCoords+=halfDeltaUV;
currentLayerDepth-=halfLayerDepth;
}
}
return currentTexCoords;
}
v2f vert(a2v v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
TANGENT_SPACE_ROTATION;
o.lightDir=normalize(mul(rotation,ObjSpaceLightDir(v.vertex).xyz));
o.viewDir=normalize(mul(rotation,ObjSpaceViewDir(v.vertex).xyz));
return o;
}
fixed4 frag(v2f i):SV_Target{
float3 tangent_lightDir=normalize(i.lightDir);
float3 tangent_viewDir=normalize(i.viewDir);
float2 uv=ReliefMappingUV(i.uv,tangent_viewDir);
//去掉边缘越界造成的纹理采样异常
if(uv.x>1.0||uv.y>1.0||uv.x<0.0||uv.y<0.0)
discard;
float3 albedo=_MainColor.rgb*tex2D(_MainTex,uv).rgb;
float3 ambient=UNITY_LIGHTMODEL_AMBIENT.rgb*albedo;
float3 tangent_normal=normalize(UnpackNormal(tex2D(_BumpTex,uv)));
//改进版 BRDF
float3 diffuse=_LightColor0.rgb*albedo*max(0,saturate(dot(tangent_normal,tangent_lightDir)))/PI;
float3 halfDir=normalize(tangent_viewDir+tangent_lightDir);
float3 specular=_LightColor0.rgb*_SpecularColor.rgb*pow(saturate(dot(halfDir,tangent_normal)),_Gloss)*(8+_Gloss)/(8*PI);
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
}
实际效果:
(左:Normal 中:Parallax 右:Relief)
Relief由于在Shader中进行了循环操作,比较费性能。