Shader笔记十九——三种凹凸纹理实现

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中进行了循环操作,比较费性能。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值