凹凸映射 Bump mapping(unityshader入门精要学习笔记)

  • Post author:
  • Post category:其他



凹凸映射 Bump mapping

使用一张法线纹理(normal map)来直接存储表面法线,又被称为法线映射。

由于法线纹理中存储的法线是切线空间下的方向,因此有2 种选择:

1.在切线空间下进行光照计算,光照方向、视角方向变换到切线空间下。

2.在世界空间下进行光照计算 ,法线方向变换到世界空间下,再和世界空间下的照方向、视角方向进行计算。


引用内容地址:



http://www.cnblogs.com/-867259206/p/5627565.html


关于原理详细参考:



https://en.wikibooks.org/wiki/Cg_Programming/Unity/Lighting_of_Bumpy_Surfaces


1.在切线空间下计算









Shader







LT


/


TangentNormal






{






Properties



{




_MainTex (





Texture





,


2D


) =





white





{}



_Color(





Color





,


Color


) = (


1


,


1


,


1


,


1


)



_BumpMap(





Normal




Map





,


2D


) =





white





{}



_BumpScale(





Bump




Scale





,


Float


) =


1.0



_Specular(





Specular





,


Color


) = (


1


,


1


,


1


,


1


)


//高光反射颜色



_Gloss(





gloss





,


Range


(


5


,


30


)) =


10




//高光区域大小



}





SubShader



{




LOD


100





Tags


{





LightMode





=





ForwardBase





}




Pass



{






CGPROGRAM





#pragma




vertex




vert





#pragma




fragment




frag






#include





UnityCG


.


cginc






#include





Lighting


.


cginc






sampler2D


_MainTex;





float4


_MainTex_ST;





fixed4


_Specular;





fixed4


_Color;





float


_Gloss;





float


_BumpScale;





sampler2D


_BumpMap;





float4


_BumpMap_ST;




struct


appdata



{






float4




vertex


: POSITION;





float4


uv :


TEXCOORD0


;





float3




normal


:


NORMAL


;





float4




tangent


:


TANGENT


;



};




struct


v2f



{






float4


uv :


TEXCOORD0


;





float4




vertex


: SV_POSITION;





float3


lightDir :


TEXCOORD1


;





float3


viewDir :TEXCOORD2;



};






v2f vert (appdata v)



{




v2f o;



o.


vertex


=


mul


(UNITY_MATRIX_MVP, v.


vertex


);



o.uv.xy = TRANSFORM_TEX(v.uv.xy, _MainTex);



//










o


.


uv


.


zw




=




TRANSFORM_TEX


(


v


.


uv


.


zw


,




_BumpMap


)


;






天哪还以为法线也可以这样写。。。其实是没用的



o.uv.zw = v.uv.xy *_BumpMap_ST.xy +_BumpMap_ST.zw;




//


副法线




=




叉积(单位化的法向量,单位化的切线向量)


*切线向量的w分量来确定副切线的方向性





//


float3




binormal




=




cross


(


normalize


(


v


.


normal


,


normalize


(


v


.


tangent


.


xyz


)


)


)




*




v


.


tangent


.


w


;





//构建一个矩阵使向量从对象空间转变到切线空间





//


float3x3




rotation




=




float3x3


(


v


.


tangent


.


xyz


,


binormal


,


v


.


normal




)


;





//或者使用unity提供的宏定义来直接计算得到rotation变换矩阵



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);





//将法线纹理中的颜色重新映射回正确的法线方向值





fixed4


packedNormal =


tex2D


(_BumpMap,i.uv.zw);





fixed3


tangentNormal;




//


如果我们把“Texture




Type”不设置成“Normal




Map”


,未压缩的格式



//










tangentNormal


.


xy




=




(


packedNormal


.


xy




*




2









1


)




*




_BumpScale


;





//


法线是单位向量,




x


^


2


+


y


^


2


+


z


^


2




=




1


.


所以已知2个坐标可以求出第三个。只需2个通道





//










tangentNormal


.


z




=




sqrt


(


1


.


0







saturate


(


dot


(


tangentNormal


.


xy




,


tangentNormal


.


xy


)


)


)


;




//


如果我们把“Texture




Type”设置成“Normal


Map”,那么上面2行代码与下面3行等价。使用内置函数UnpackNormal





//。DXT5nm压缩格式,也就是unity使用的压缩格式



tangentNormal = UnpackNormal(packedNormal);



tangentNormal.xy *= _BumpScale;



tangentNormal.z =


sqrt


(


1.0





saturate


(


dot


(tangentNormal.xy ,tangentNormal.xy)));








fixed3


albedo =


tex2D


(_MainTex, i.uv) * _Color.


rgb


;




fixed3




ambient


= UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;




fixed3




diffuse


= _LightColor0.


rgb


*albedo *


max


(


0


,


dot


(tangentNormal,tangentLightDir));





fixed3


halfDir =


normalize


(tangentLightDir + tangentViewDir);




fixed3




specular


= _LightColor0.


rgb


*_Specular *


pow


(


max


(


0


,


dot


(tangentNormal,tangentLightDir)),_Gloss);




return




fixed4


(


ambient


+


diffuse


+


specular


,


1.0


);



}





ENDCG



}



}





FallBack







Specular






}










2.在世界空间下进行计算






Shader







LT


/


WorldNormal






{






Properties



{




_MainTex (





Texture





,


2D


) =





white





{}



_Color(





Color





,


Color


) = (


1


,


1


,


1


,


1


)



_BumpMap(





Normal




Map





,


2D


) =





white





{}



_BumpScale(





Bump




Scale





,


Float


) =


1.0



_Specular(





Specular





,


Color


) = (


1


,


1


,


1


,


1


)


//高光反射颜色



_Gloss(





gloss





,


Range


(


5


,


30


)) =


10




//高光区域大小



}





SubShader



{






Tags


{





LightMode





=





ForwardBase





}



LOD


100




Pass



{






CGPROGRAM





#pragma




vertex




vert





#pragma




fragment




frag


#include





UnityCG


.


cginc






#include





Lighting


.


cginc






sampler2D


_MainTex;





float4


_MainTex_ST;





fixed4


_Specular;





fixed4


_Color;





float


_Gloss;





float


_BumpScale;





sampler2D


_BumpMap;





float4


_BumpMap_ST;




struct


appdata



{






float4




vertex


: POSITION;





float4


uv :


TEXCOORD0


;





float3




normal


:


NORMAL


;





float4




tangent


:


TANGENT


;



};




struct


v2f



{






float4


uv :


TEXCOORD0


;





float4




vertex


: SV_POSITION;





float4


TtoW0 :


TEXCOORD1


;





float4


TtoW1 :TEXCOORD2;





float4


TtoW2 :TEXCOORD3;



};






v2f vert (appdata v)



{




v2f o;



o.


vertex


=


mul


(UNITY_MATRIX_MVP, v.


vertex


);



o.uv.xy = TRANSFORM_TEX(v.uv.xy, _MainTex);



o.uv.zw = v.uv.xy *_BumpMap_ST.xy +_BumpMap_ST.zw;




//对象空间坐标系转换到世界空间坐标系





float3


worldPos =


mul


(unity_ObjectToWorld,v.


vertex


).xyz;





fixed3


worldNormal = UnityObjectToWorldNormal(v.


normal


);





fixed3


worldTangent = UnityObjectToWorldDir(v.


tangent


.xyz);





fixed3


worldBinormal =


cross


(worldNormal,worldTangent) * v.


tangent


.w;








//


TtoW切线空间到世界空间的变换矩阵,按列摆放依次存放:切线,副切线,法线,顶点位置


.


(都是世界空间下的)





o.TtoW0 =


float4


(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);



o.TtoW1 =


float4


(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);



o.TtoW2 =


float4


(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);








return


o;



}








fixed4


frag (v2f i) : SV_Target



{






//获得世界空间中的坐标





float3


worldPos =


float3


(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);





//计算光照和视角方向在世界坐标系中





fixed3


lightDir =


normalize


(UnityWorldSpaceLightDir(worldPos));





fixed3


viewDir =


normalize


(UnityWorldSpaceViewDir(worldPos));




//


获得切线空间下的法线,法线纹理“Texture




Type”设置成“Normal


Map”





fixed3


bump = UnpackNormal(


tex2D


(_BumpMap,i.uv.zw));



bump.xy *= _BumpScale;



bump.z =


sqrt


(


1.0





saturate


(


dot


(bump.xy,bump.xy)));





//


将法线从切线空间转换到世界空间,单位化(矩阵每一行的xyz







法线点积)



bump =


normalize


(


half3


(


dot


(i.TtoW0.xyz,bump),


dot


(i.TtoW1.xyz,bump),


dot


(i.TtoW2.xyz,bump)));








fixed3


albedo =


tex2D


(_MainTex, i.uv).


rgb


* _Color.


rgb


;




fixed3




ambient


= UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;




fixed3


halfDir =


normalize


(lightDir + viewDir);





fixed3




diffuse


= _LightColor0.


rgb


* albedo *


max


(


0


,


dot


(bump,lightDir));






fixed3




specular


= _LightColor0.


rgb


*_Specular *


pow


(


max


(


0


,


dot


(bump,halfDir)),_Gloss);




return




fixed4


(


ambient


+


diffuse


+


specular


,


1.0


);



}





ENDCG



}



}





FallBack







Specular






}








我们看到,有两种转换法线的函数,这两种函数是针对不同法线贴图格式的。

如果是未压缩的格式,就将法线贴图里存储的xyz值乘2减1,变回原来的坐标。

如果是DXT5nm压缩格式(可以推测这应该是Unity使用的压缩格式),则要用另外一种转换方法。(这也是为什么要将纹理类型设置为Normal Map的原因,因为不同类型的图片要用不同的方法转换)

DXT5nm压缩格式,只有G和A通道。因为法线是单位向量,所以只要知道任意两个坐标值,就能求出另一个坐标值(z

2

=1-x

2

-y

2

),只要两个通道存储两个坐标值即可。因为这样的计算并不复杂,所以可以节省空间以使用更多的贴图。DXT5nm将R通道的值移到了Alpha通道,保留G通道的值,R和B通道则以某种颜色填充。

所以转换DXT5nm的法线贴图,要先将A通道和G通道的值(wy)转换为xy,再计算z值。

后面的代码就好解释了。转换了法线坐标以后将法线赋值给输出结构体的Normal变量。因为法线(0,0,1)就表明这点的法线和高模的法线相同,所以Z坐标没有必要乘_NormalIntensity。

法线贴图原理

法线贴图的基本原理前面已经讲过了,就是将高模的法线信息xyz存储在rgb中,应用法线贴图的时候将rgb值转换为xyz,在计算光照的时候,使用高模的法线进行计算,从而得到高模的光影效果,造成低模呈现高模较为平滑的表面的假象,这是一种视觉欺骗。

这个小节里要再详细谈谈法线的坐标。

要表示一个向量在三维空间的位置需要一个三维坐标系作参考。要表示法线的方位可以用世界坐标(World Space),也可以用模型坐标(Object Space)。这两个坐标系大家应该听说过。

如果用世界坐标,那么模型就不能旋转和移动了。因为模型的位置改变了,它在世界坐标系里的坐标也改变了,但法线的坐标是基于世界坐标系的,这样法线的位置就不对了。不然读取了法线信息还要再进行世界坐标的转换。

如果用模型坐标,就没有世界坐标不能旋转和移动的局限了。不管模型怎么旋转移动,法线的位置是相对于模型的,所以法线的坐标不会变。相比于世界坐标,使用模型坐标还要进行模型坐标到世界坐标的转换,效率低点,但灵活性更好了。

但是使用模型坐标还有局限,就是模型不能变形。因为模型变形了,法线相对模型的坐标也变了,一些会有形变的物体(比如有骨骼蒙皮动画的模型)就不适用基于模型坐标的法线贴图。

所以还需要另外一种坐标系,使法线贴图不仅仅适用于某个模型,还可以用在其他模型上。

这种坐标系就是切线坐标(Tangent Space)。切线坐标就以表面上某一点的切线作为XY轴,法线作为Z轴(即垂至表面的轴)。这样的话,符合要求的坐标系有无数种。我们可以直接将纹理的UV坐标当作XY轴,这样就有一个现成的坐标系可以使用。

可以这样理解切线坐标,它表示的是高模上的法线相对对应点上的低模的法线的扰动程度。当法线坐标是(0,0,1)的时候,说明高模法线和低模法线一致,相当于切线坐标表示的是高模法线相对低模法线在XY方向上的偏移程度。这也解释了上文代码中为什么Z轴不用乘_NormalIntensity。

法线贴图是用于表现表面微小的凹凸的,比如皮肤皱纹、鱼鳞等,所以法线的扰动值不会太大,也就是Z坐标的值是比较大,所以B通道的值比较大,所以法线贴图通常都是蓝盈盈的。

使用切线坐标也就是说每个面都有一个自己的坐标系。在Vertex Shader里进行光照计算的时候要将光线向量的坐标从World Space转换到Tangent Space。所以我们可以在Surface Shader里使用lightDir变量和normal变量进行计算,因为Unity帮我们做了很多工作。

相比模型坐标,切线坐标更加灵活,可以应用在与原模型形状不同的模型上,比如可以把花岗岩的法线贴图应用在一个圆柱体表面,使圆柱体表面也具有花岗岩的凹凸效果。

法线贴图通常有两种,一种是Object Space Normal Map,一种是Tangent Space Normal Map。如果模型没有动画,那么可以使用Object Space Normal Map,如果模型有动画,或者会变形(比如流动的水、火焰)那就要使用Tangent Space Normal Map。因为Tangent Space Normal Map要灵活的多,可以应用于不同的物体,所以Tangent Space Normal Map使用得更多一些。

世界坐标系下计算需要更多的计算,但是需要使用Cubemap进行环境映射等情况下,我们就需要用这个方法。



版权声明:本文为u013354943原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。