效果图:
浮雕凹凸贴图效果 ====》 高度图
法线贴图凹凸效果 ====> 法线贴图
简介
法线贴图是目前游戏开发中最常见的贴图之一。我们知道,一般情况下,模型面数越高,可以表现的细节越多,效果也越好。但是,由于面数多了,顶点数多了,计算量也就上去了,效果永远是和性能成反比的。怎么样用尽可能简单模型来做出更好的效果就成了大家研究的方向之一。纹理映射是最早的一种,通过纹理直接贴在模型表面,提供了一些细节,但是普通的纹理贴图只是影响最终像素阶段输出的颜色值,不能让模型有一些凹凸之类的细节表现。而法线贴图就是为了解决上面的问题,给我们提供了通过低面数模型来模拟高面数模型的效果,增加细节层次感,效果与高模相差不多,但是大大降低了模型的面数。
凹凸映射和纹理映射非常相似。然而,纹理映射是把颜色加到多边形上,而凹凸映射是把粗糙信息加到多边形上。这在多边形的视觉上会产生很吸引人的效果。我们只需要添加一点信息到本来需要使用大量多边形的物体上。需要注意的是这个物体是平的,但是它看起来却是粗糙不平的。“凹凸映射和纹理映射有什么不同?”它们的不同之处在于——凹凸映射是一种负责光方向的纹理映射。
法线贴图原理
那么你也许会问:我是怎么知道哪些点要亮,哪些点要暗呢?这不难。绝大多数人生活在这样一种环境下——这个环境的大多数光源来自上方(译者注:比如白天主要的光来自太阳,夜晚主要的光来自天花板上的日光灯)。所以向上倾的地方就会更亮,而向下倾的地方就会更暗。所以这种现象使你的眼睛看到一个物体上亮暗区域时,可以判断出它的凹凸情况。相对亮的块被判断是面向上的,相对暗的块被判断是面向下的。所以我只需要给物体上的线条简单得上色。
如果你想要更多的证据,这里还有一幅几乎相同的图,不同于前的是它旋转了180度。所以它是前一幅图倒转的图像。那些先前看起来是凹进去的区域,现在看起来是凸出来的了。
这个时候你的大脑并没有被完全欺骗,你脑中存留的视觉印象使你仍然有能力判断出这是前一幅图,只是它的光源变了,是从下往上照的,你的大脑可能强迫性地判断出它是第一幅图。事实上,你只要始终盯着它,并且努力地想像着光是从右下方向照射的,你就会理解它是凹的(译者注:因为日常生活的习惯,你会很容易把这些图形判断成凸出的图形,但是因为有了上一幅对照图的印象,你可能才会特别注意到这些图块其实还是凹入的,只是判断方法不符合我们日常生活习惯,因为这时大多数光不是从上方照射,而是从下往上照射)。
既然一个面的光照条件(亮度)的改变,就可以让我们感觉这个面有凹凸感,那么上面说的,通过改变法线来改变面上某点的光照条件,进而忽悠观察者,让他们感觉这个面有凹凸感的方法就行得通了。
假如下面是我们的低面数模型,上面是我们的高面数模型,上面的模型在计算光照时,由于面数多,每个面的法线方向不同,所以各个面的光照计算结果都不同,就有凹凸的感觉了,而下面的低模,只有一个面,整个面的光照条件都是一致的,就没有凹凸的感觉了。我们如果把上面的高模的法线信息保存下来,类似纹理贴图那样,存在一张图里,再给低模使用,低模就可以有跟高模一样的法线,进而在计算光照时达到和高模类似的效果,这也就是常说的烘法线的原理。
只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。
有两种主要的方法可以用来进行凹凸映射: 一种方法是使用一张高度纹理(height map)来模拟表面位移( displacement ), 然后得到一个修改后的法线值,这种方法也被称为高度映射(height mapping);另一种方法则是使用一张法线纹理(normal map )来直接存储表面法线,这种方法又被称为法线映射( normal mapping )。
凹凸贴图(Bump Map)
工作原理:
凹凸映射是补色渲染技术(Phong Shading Technique)的一项扩展,只是在补色渲染里,多边形表面上的法线将被改变,这个向量用来计算该点的亮度。当你加入了凹凸映射,法线向量会略微地改变,怎么改变则基于凹凸图。改变法线向量就会改变多边形的点的颜色值。就这么简单。
现在,有几种方法来达到这个目的(译者注:这个目的指改变法线向量)。我并没有实际编写补色渲染和凹凸映射的程序,但是我在这里将介绍一种我喜欢的方法来实现!
现在我们需要将凹凸图中的高度信息转换成补色渲染用到的法线的调节信息。这个做起来不难,但是解释起来比较费劲。
好的,我们现在将凹凸位图的信息转换成一些小向量——一个向量对应于一个点。请看上面一副放大的凹凸图。相对亮的点比相对暗的点更为凸出。看清楚了吗?现在计算每个点的向量,这些向量表征了每个点的倾斜情况,请看下图的描绘。图中红色小圆点表示向量是向下的:
x_gradient = pixel(x-1, y) – pixel(x+1, y)
y_gradient = pixel(x, y-1) – pixel(x, y+1)
在得出了这两个倾斜度后,你就可以计算多边形点的法线了。
这里有一个多边形,图上绘出了它的一条法线向量——n。除此,还有两条向量,它们将用来调节该点法线向量。这两条向量必须与当前被渲染的多边形的凹凸图对齐,换句话说,它们要与凹凸图使用同一种坐标轴。下边的图分别是凹凸图和多边形,两副图都显示了U、V两条向量(译者注:也就是平面2D坐标的两条轴):
现在你可以看到被调节后的新法线向量了。这个调节公式很简单:
New_Normal = Normal + (U * x_gradient) + (V * y_gradient)
有了新法线向量后,你就可以通过补色渲染技术计算出多边形每个点的亮度了。
注:上面的调节公式你可以理解成:原向量+偏移向量=新向量(向量的加法)
上面的原理简单理解就是:首先,通过灰度图来表现凹凸,那么,我们怎样判断一个点处在凹凸的边缘呢?答案是通过斜率,比如我要对(x,y)进行采样,怎样求这一点的斜率呢,学过数学的都知道,我们可以通过两点确定一条直线,进而求出这条直线的斜率。那么我们就可以对(x-1,y)和(x+1,y)两点进行采样,竖向也是一样,通过(x,y-1)和(x,y+1)进行采样,那么,我们就可以获得这一点上灰度值的变化,如果灰度值不变,说明该点不在边缘,如果灰度值有改变,那么说明该点在边缘,那么我们就可以根据这个斜率值来修改法线,进而修改光照结果。
这种方法的好处是非常直观,我们可以从高度图中明确地知道一个模型表面的凹凸情况,但缺点是计算更加复杂,在实时计算时不能直接得到
表面法线,而是需要由像素的灰度值计算而得,因此需要消耗更多的性能。
Bump Map类型的shader如下:
Shader "Demo/BumpMap"
{
//属性
Properties{
_Diffuse("Diffuse", Color) = (1,1,1,1) // 漫反射颜色
_MainTex("Base 2D", 2D) = "white"{} // 纹理颜色
_BumpMap("Bump Map", 2D) = "black"{} // 高度图
_BumpScale ("Bump Scale", Range(0, 30.0)) = 10.0 // 凹凸程度
}
//子着色器
SubShader
{
Pass
{
//定义Tags
Tags{ "RenderType" = "Opaque" }
CGPROGRAM
//使用vert函数和frag函数
#pragma vertex vert
#pragma fragment frag
//引入头文件
#include "Lighting.cginc"
//定义Properties中的变量
fixed4 _Diffuse;
sampler2D _MainTex;
//使用了TRANSFROM_TEX宏就需要定义XXX_ST
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_TexelSize;
float _BumpScale;
//定义结构体:应用阶段到vertex shader阶段的数据
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
//定义结构体:vertex shader阶段输出的内容
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
//转化纹理坐标
float2 uv : TEXCOORD1;
};
//定义顶点shader
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//把法线转化到世界空间
o.worldNormal = mul(v.normal, (float3x3)_World2Object);
//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
//定义片元shader
fixed4 frag(v2f i) : SV_Target
{
//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
//归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的
fixed3 worldNormal1 = normalize(i.worldNormal);
//采样bump贴图,需要知道该点的斜率,xy方向分别求,所以对于一个点需要采样四次
fixed bumpValueU = tex2D(_BumpMap, i.uv + fixed2(-1.0 * _BumpMap_TexelSize.x, 0)).r - tex2D(_BumpMap, i.uv + fixed2(1.0 * _BumpMap_TexelSize.x, 0)).r;
fixed bumpValueV = tex2D(_BumpMap, i.uv + fixed2(0, -1.0 * _BumpMap_TexelSize.y)).r - tex2D(_BumpMap, i.uv + fixed2(0, 1.0 * _BumpMap_TexelSize.y)).r;
//用上面的斜率来修改法线的偏移值
fixed3 worldNormal = fixed3(worldNormal1.x + bumpValueU * _BumpScale, worldNormal1.y + bumpValueV * _BumpScale, worldNormal1.z);
//把光照方向归一化
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//根据半兰伯特模型计算像素的光照信息
fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
//最终输出颜色为lambert光强*材质diffuse颜色*光颜色
fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
//进行纹理采样
fixed4 color = tex2D(_MainTex, i.uv);
return fixed4(diffuse * color.rgb, 1.0);
}
ENDCG
}
}
//前面的Shader失效的话,使用默认的Diffuse
FallBack "Diffuse"
}
效果图如下:
法线贴图(Normal Map)
DOT3 Bump Map(点乘凹凸贴图),它使用的是Normal Map,这是目前图形硬件中使用的主要方法,它不需要存储高度,只需要将表面的实际法线作为(x,y,z)向量存储在法线图中,然后可以将含有法线的凹凸纹理和经过插值的光源向量在每个象素点结合起来,可以使用点乘。它的一个优点就是可以直接用来计算凹凸块上的镜面高光。
注意在max中,BumpMap用于逐像素光照,在Gamebryo中BumpMap用于模拟顶点高度凹凸,而NormalMap才是逐像素光照。
法线贴图是怎样存储的
这就要求,我们在Shader 中对法线纹理进行纹理来样后,还需要对结果进行一次反映射的过程,以得到原先的法线方向。反映射的过程实际就是使用上面映射函数的逆函数:
normal = pixel x 2 -1
这个步骤,Unity已经为我们完成了,我们在计算法线的时候,只需要调用UnpackNormal这个函数就可以实现区间的重新映射。
从UnityCG.cginc中可以看到UnpackNormal这个函数的实现:
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
return packednormal.xyz * 2 - 1;
#else
return UnpackNormalDXT5nm(packednormal);
#endif
}
为什么法线贴图存储在切线空间
然而,由于方向是相对于坐标空间来说的,那么法线纹理中存储的法线方向在哪个坐标空间中呢?对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理( object-space normal map )。然而,在实际制作中,我们往往会采用另一种坐标空间,即模型顶点的切线空间( tangent space )来存储法线。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z 轴是顶点的法线方向(n), x 轴是顶点的切线方向(t),而y 轴可由法线和切线叉积而得,也被称为是副切线( bitangent, b )或副法线,如下图所示。
N = T × normalize(dx/dv, dy/dv, dz/dv)
B = N × T
// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
实际上,法线本身存储在哪个坐标系中都是可以的, 我们甚至可以选择存储在世界空间下。
但问题是,我们并不是单纯地想要得到法线,后续的光照计算才是我们的目的。而选择哪个坐标系意味着我们需要把不同信息转换到相应的坐标系中。例如,如果选择了切线空间, 我们需要把从法线纹理中得到的法线方向从切线空间转换到世界空间(或其他空间)中。
总体来说,使用模型空间来存储法线的优点如下。
- 实现简单, 更加直观。我们甚至都不需要模型原始的法线和切线等信息, 也就是说,计算更少。生成它也非常简单, 而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV 方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的。
- 在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息, 因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象。
- 自由度很高。 模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误了。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。
- 可进行UV 动画。比如,我们可以移动一个纹理的UV 坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果。原因同上。这种UV 动画在水或者火山熔岩这种类型的物体上会经常用到。
- 可以重用法线纹理。比如, 一个砖块,我们仅使用一张法线纹理就可以用到所有的6 个面上。原因同上。
- 可压缩。由于切线空间下的法线纹理中法线的Z 方向总是正方向,因此我们可以仅存储XY 方向,而推导得到Z 方向。而模型空间下的法线纹理由于每个方向都是可能的, 因此必须存储3 个方向的值,不可压缩。
切线空间下的法线纹理的前两个优点足以让很多人放弃模型空间下的法线纹理而选择它。从上面的优点可以看出,切线空间在很多情况下都优于模型空间,而且可以节省美术人员的工作。
为什么法线贴图都是蓝色的
从图7.13 中可以看出,模型空间下的法线纹理看起来是“五颜六色”的。这是因为所有法线所在的坐标空间是同一个坐标空间,即模型空间,而每个点存储的法线方向是各异的,有的是(0,1, 0),经过映射后存储到纹理中就对应了RGB(0 .5, 1, 0.5)浅绿色,有的是(0,-1 , 0),经过映射后存储到纹理中就对应了(0 .5, 0, 0. 5)紫色。既然法线贴图中存储的是法线的方向,也就是说是一个Vector3类型的变量,刚好和图片的RGB格式不谋而合。
而切线空间下的法线纹理看起来几乎全部是浅蓝色的。这是因为,每个法线方向所在的坐标空间是不一样的,即是表面每点各自的切线空间。这种法线纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向。也就是说,如果一个点的法线方向不变,那么在它的切线空间中, 新的法线方向就是z 轴方向,即值为(0, 0, 1 ) , 经过映射后存储在纹理中就对应了RGB(0.5, 0.5, 1 )浅蓝色。而这个颜色就是法线纹理中大片的蓝色。这些蓝色实际上说明顶点的大部分法线是和模型本身法线一样的,不需要改变。
既然我们知道了法线贴图中存储的是切线空间的法线。而法线贴图所对应的表面,绝大部分的位置肯定是平滑的,只有需要凹凸变化的地方才会有变化,那么大部分地方的法线方向不变,也就是在切线空间的(0,0,1),这个值按照上面介绍的映射关系,从(-1,1)区间变换到(0,1)区间:((-1+1)/2, (-1+1)/2, (1+1)/2)= (0.5,0.5,1),再转化为颜色的(0,255)区间,最终就变成了(127,127,255)。好了,打开photoshop,看一下这个颜色值是什么:
Unity下法线贴图Shader实现
我们就可以看一下Unity Shader中实现法线贴图的方式。光照模型仍然采用之前的半兰伯特光照,vertex fragemnt shader实现:
Shader "Deme/NormalMapInTangentSpace"
{
//属性
Properties{
_Diffuse("Diffuse", Color) = (1,1,1,1) // 漫反射颜色
_MainTex("Base 2D", 2D) = "white"{} // 纹理颜色
_BumpMap("Bump Map", 2D) = "bump"{} // 法线贴图
_BumpScale ("Bump Scale", Range(0.0, 30.0)) = 10.0 // 凹凸程度
}
//子着色器
SubShader
{
Pass
{
//定义Tags
Tags{ "RenderType" = "Opaque" }
CGPROGRAM
//使用vert函数和frag函数
#pragma vertex vert
#pragma fragment frag
//引入头文件
#include "Lighting.cginc"
//定义Properties中的变量
fixed4 _Diffuse;
sampler2D _MainTex;
//使用了TRANSFROM_TEX宏就需要定义XXX_ST
float4 _MainTex_ST;
sampler2D _BumpMap;
float _BumpScale;
//定义结构体:vertex shader阶段输出的内容
struct v2f
{
float4 pos : SV_POSITION;
//转化纹理坐标
float2 uv : TEXCOORD0;
//tangent空间的光线方向
float3 lightDir : TEXCOORD1;
};
//定义顶点shader
v2f vert(appdata_tan v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
// Compute the binormal
// float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
// // Construct a matrix which transform vectors from object space to tangent space
// float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
// Or just use the built-in macro
//这个宏为我们定义好了模型空间到切线空间的转换矩阵rotation,注意后面有个;
TANGENT_SPACE_ROTATION;
//ObjectSpaceLightDir可以把光线方向转化到模型空间,然后通过rotation再转化到切线空间
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
//定义片元shader
fixed4 frag(v2f i) : SV_Target
{
//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
// Get the texel in the normal map
fixed4 packedNormal = tex2D(_BumpMap, i.uv);
fixed3 tangentNormal;
// If the texture is not marked as "Normal map"
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//解出切线空间法线
// Or mark the texture as "Normal map", and use the built-in funciton
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//normalize一下切线空间的光照方向
float3 tangentLight = normalize(i.lightDir);
//根据半兰伯特模型计算像素的光照信息
fixed3 lambert = 0.5 * dot(tangentNormal, tangentLight) + 0.5;
//最终输出颜色为lambert光强*材质diffuse颜色*光颜色
fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
//进行纹理采样
fixed4 color = tex2D(_MainTex, i.uv);
return fixed4(diffuse * color.rgb, 1.0);
}
ENDCG
}
}
//前面的Shader失效的话,使用默认的Diffuse
FallBack "Diffuse"
}
代码详解:
在片元着色器中,我们首先利用tex2D 对法线纹理 _BumpMap 进行采样。正如本节一开头所讲的,法线纹理中存储的是把法线经过映射后得到的像素值, 因此我们需要把它们反映射回来。
如果我们没有在Unity 里把该法线纹理的类型设置成Normal map,就需要在代码中手动进行这个过程。我们首先把packedNormal 的xy 分量按之前提到的公式映射回法线方向,然后乘以 _BumpScale (控制凹凸程度〉来得到tangentNormal 的xy 分量。由于法线都是单位矢量,因此tangentNormal.z 分量可以由tangentNonnal.xy 计算而得。由于我们使用的是切线空间下的法线纹理, 因此可以保证法线方向的z 分量为正。在Unity 中,为了方便Unity 对法线纹理的存储进行优化,我们通常会把法线纹理的纹理类型标识成Normal map, Unity 会根据平台来选择不同的压缩方法。这时,如果我们再使用上面的方法来计算就会得到错误的结果,因为此时 _BumpMap的rgb 分量并不再是切线空间下法线方向的xyz 值了。在这种情况下,我们可以使用Unity 的内置函数UnpackNormal 来得到正确的法线方向。
世界空间下法线切图的计算:
我们需要在片元着色器中把法线方向从切线空间变换到世界空间下。这种方法的基本思想是:在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间下的表示来得到。最后,我们只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间下即可。尽管这种方法需要更多的计算,但在需要使用Cubemap 进行环境映射等情况下,我们就需要使用这种方法。
Shader实现如下:
Shader "Deme/NormalMapInWorldSpace"
{
//属性
Properties{
_Diffuse("Diffuse", Color) = (1,1,1,1) // 漫反射颜色
_MainTex("Base 2D", 2D) = "white"{} // 纹理颜色
_BumpMap("Bump Map", 2D) = "bump"{} // 法线贴图
_BumpScale ("Bump Scale", Range(0.0, 30.0)) = 10.0 // 凹凸程度
}
//子着色器
SubShader
{
Pass
{
//定义Tags
Tags{ "RenderType" = "Opaque" }
CGPROGRAM
//使用vert函数和frag函数
#pragma vertex vert
#pragma fragment frag
//引入头文件
#include "Lighting.cginc"
//定义Properties中的变量
fixed4 _Diffuse;
sampler2D _MainTex;
//使用了TRANSFROM_TEX宏就需要定义XXX_ST
float4 _MainTex_ST;
sampler2D _BumpMap;
float _BumpScale;
//定义结构体:vertex shader阶段输出的内容
struct v2f
{
float4 pos : SV_POSITION;
//转化纹理坐标
float2 uv : TEXCOORD0;
//TtoW0 、TtoW1 和TtoW2 就依次存储了从切线空间到世界空间的变换矩阵的每一行
//实际上,对方向矢量的变换只需要使用3 × 3大小的矩阵
//也就是说,每一行只需要使用float3 类型的变量即可
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
//定义顶点shader
v2f vert(appdata_tan v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
float3 worldPos = mul(_Object2World, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// Compute the matrix that transform directions from tangent space to world space
// Put the world position in w component for optimization
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);
//通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
//定义片元shader
fixed4 frag(v2f i) : SV_Target
{
//unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
// Get the position in world space
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// Compute the light dir in world space
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
// Get the normal in tangent space
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
// Transform the narmal from tangent space to world space
//mul(TtoW, bump) 下面的一句等同于这句,只是矩阵用向量分行表示
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
//根据半兰伯特模型计算像素的光照信息
fixed3 lambert = 0.5 * dot(bump, lightDir) + 0.5;
//最终输出颜色为lambert光强*材质diffuse颜色*光颜色
fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
//进行纹理采样
fixed4 color = tex2D(_MainTex, i.uv);
return fixed4(diffuse * color.rgb, 1.0);
}
ENDCG
}
}
//前面的Shader失效的话,使用默认的Diffuse
FallBack "Diffuse"
}
代码详解:
接着,我们使用内置的UnpackNormal 函数对法线纹理进行采样和解码(需要把法线纹理的格式标识成Normal map ), 并使用_BumpScale 对其进行缩放。最后,我们使用TtoW0 、TtoW1 和TtoW2存储的变换矩阵把法线变换到世界空间下。这是通过使用点乘操作来实现矩阵的每一行和法线相乘来得到的。
从视觉表现上,在切线空间下和在世界空间下计算光照几乎没有任何差别。