法线贴图
法线贴图是一张保存了物体法线信息的纹理,可以用来细化模型的光照效果。
例如一块石头表面坑坑洼洼的,如果全部用建模实现,需要非常多的顶点数和面数才能完成。但是做一个简单的模型,比如表面平整的一块石头,然后使用法线贴图来重设顶点的法线,在远处观看也能得到相当接近的渲染效果。同时只需要简单模型的顶点数和面数,提高了帧率,所以法线贴图其实可以看成是在模型面数和帧率之间妥协的产物。
那么知道了法线贴图就是保存了物体法线信息的纹理,法线贴图从哪里来呢?
首先3D软件中一般都可以在模型上生成法线贴图,此时用到的是真实的模型顶点信息,效果最好。在高精度的模型上生成法线贴图,然后导出低精度的模型使用这张法线贴图。
一些软件也可以根据物体的纹理贴图来生成法线贴图,此时使用的不是模型的顶点信息,而是纹理贴图中的明暗信息,效果稍微差一些。那么明暗信息怎么生成法线呢?大致就是暗的部分法线向里凹,亮的部分法线向外凸。
怎么根据纹理贴图生成法线贴图不是我们研究的重点,我们直接来使用ps软件从一张纹理贴图得到法线贴图。
随便从网上搜索一张墙面的纹理
导入PS中,使用滤镜->3D->生成法线图
得到法线贴图如下
首先要知道一张图片保存的是rgb颜色,每个分量取值[0,1],法线是一个向量,每个分量取值[-1,1],所以把法线保存为图片需要做一个映射。
因为每个向量都属于一个坐标系,所以我们保存法线,也要确定保存在什么坐标系下。而包括PS在内的一些软件生成的法线贴图,都是将法线保存在切线空间下。
切线空间是对于每个顶点来说的,每个顶点的切线空间不同,x轴为顶点切线,y轴为顶点副切线,z轴为原法线。
xyz映射到rgb,正好z轴对应的是蓝色,因此大家看到的切线空间下的法线贴图才会都是蓝乎乎的,因为z轴本来是原法线方向,所以法线贴图里越蓝,代表该法线变化越小。
当然有的软件还可以生成模型空间下的法线贴图,此时如果看上去就是五颜六色的了,不过还是切线空间下的法线贴图应用最多。
(注意这里直接通过纹理贴图生成法线贴图其实并不存在顶点,只是用近似算法来提取亮度和暗部生成对应的法线,只有用原模型来生成法线贴图才是用到了顶点信息)
接下来我们把纹理和法线贴图都导入到unity中,法线贴图需要在导入设置中设置为normal map。
当然不用ps也可以直接在unity中通过纹理生成法线贴图
此时直接导入两次纹理贴图,然后将第二张纹理做如下设置,可以得到和在ps中生成法线贴图类似的结果。
设置纹理类型为normal map
然后勾选从灰度(就是亮度)生成法线图
不管用什么方法得到法线贴图,接下来我们就开始提取法线贴图中的数据
Unity shader实现
接下来编写shader代码
首先声明属性,声明法线贴图和凹凸程度。
我们这里不声明_BumpTexture_ST是因为直接和主贴图共用
sampler2D _BumpTexture;
float _BumpScale;
在顶点着色器中计算要用到的数据传送到片元着色器。
记住纹理采样是只能在片元着色器中处理的,因为顶点着色器采样后再插值是完全不对的。
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _Texture);
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}
然后从法线贴图中提取法线
tex2D中提取出的是颜色信息,我们之前说到法线需要和颜色做一个映射,这个映射通过UnpackNormal函数来完成,unpack函数还会处理一些跨平台压缩的问题,总之不用UnpackNormal提取出来的数据是不对的
float3 normal = UnpackNormal(tex2D(_BumpTexture, i.uv));
此时提取出来的法线是保存在切线空间中的,我们需要把他转换到世界空间中,和其他世界空间中的属性来进行计算。
首先转换到模型空间中,从切线空间到模型空间的转换没有内置函数,我们需要手动构造矩阵。
这个矩阵很简单。我们如果知道子空间在父空间的基向量,那么从子空间到父空间的变换,只要将子空间在父空间的基向量依次排列,最后右乘上在子空间中的向量,就可以完成转换。
在这里我们已经知道切线空间是怎么构造的了,x轴为顶点切线,y轴为顶点副切线,z轴为原法线。
untiy shader构造矩阵是按行来的,我们先用行构造矩阵再转置,就变成我们需要的矩阵了。
这里还有一个注意的点是切线需要乘上它的第四个分量来确定方向
如i.tangent.xyz*i.tangent.w
副切线需要对法线和切线进行叉积
//转换到模型空间的矩阵
float3x3 tangent2Object =
{
i.tangent.xyz*i.tangent.w,
cross(i.tangent*i.tangent.w, i.normal),
i.normal
};
tangent2Object=transpose(tangent2Object);
最后相乘,完成法线从切线空间到模型空间的转换
normal = mul(tangent2Object, normal);
法线从模型空间变换到世界空间,直接使用内置函数
float3 worldNormal = normalize(UnityObjectToWorldNormal(normal));
这样我们就从法线贴图中提取出了我们需要的法线并转换到了世界空间。
接下来可以使用我们之前声明的_BumpScale来控制凹凸程度,这里比较难以理解
将法线的.xy分量乘上_BumpScale。因为z轴是原法线方向,所以可以想象一下,将向量的xy分量进行缩放后,比如乘上2,它就更加朝着原来的偏移方向倾斜了,所以就变的更凹进或者更凸起。
因为normal需要是一个单位向量,所以变换了xy之后需要重新计算z
这就是根据一个简单的公式 z=sqrt(1-x2-y2)
此时还需要使用saturate使最小值大于0,因为_BumpScale过大后可能导致z为负值,负值就直接设置为0。意思就是法线相对于原法线的最大变化只能是90度,当然也只需要90度。
normal.xy*=_BumpScale;
normal.z=sqrt(1-saturate(dot(normal.xy,normal.xy)));
最后加上光照模型的代码,完整代码如下
这里用了phong光照模型
phong光照模型的实现
完整代码
Shader "Test/bump"
{
Properties
{
_Color("Color",Color)=(1,1,1,1)
_EmissiveColor("EmissiveColor",Color)=(0,0,0,0)
_Speclur("Speclur",int)=2
_Texture("Texture",2d)="white"{}
_BumpTexture("Bump Texture",2d)="white"{}
_BumpScale("Bump Scale",float)=-1
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _Color;
float4 _EmissiveColor;
float _Speclur;
sampler2D _Texture;
float4 _Texture_ST;
sampler2D _BumpTexture;
float _BumpScale;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv:TEXCOORD0;
float4 tangent:TANGENT;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 normal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
float2 uv:TEXCOORD2;
float4 tangent:TEXCOORD3;
};
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _Texture);
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float3 texColor = tex2D(_Texture, i.uv);
//从法线贴图中获取法线方向,此时在切线空间下
float3 normal = UnpackNormal(tex2D(_BumpTexture, i.uv));
//使用Bumpscale控制凹凸程度
normal.xy*=_BumpScale;
normal.z=sqrt(1-saturate(dot(normal.xy,normal.xy)));
//转换到模型空间的矩阵
float3x3 tangent2Object =
{
i.tangent.xyz*i.tangent.w,
cross(i.tangent*i.tangent.w, i.normal),
i.normal
};
tangent2Object=transpose(tangent2Object);
normal = mul(tangent2Object, normal);
//最终获得在世界坐标下的法线
float3 worldNormal = normalize(UnityObjectToWorldNormal(normal));
//使用内置宏获取当前片元到光源的方向,里面已经处理了不同光源
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
//世界坐标下的视角方向
float3 worldViewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
//光线在当前片元对应的法线下的反射方向
float3 worldLightReflectDir = normalize(reflect(-worldLightDir, worldNormal));
//环境光分量,直接用宏定义获取 此值是在unity编辑器内的光照窗口设定
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Color;
//漫反射分量,使用表面法线点乘光源方向
float3 diffuse = max(0, dot(worldNormal, worldLightDir)) * _Color;
//高光分量,使用视角方向点乘光的反射方向
float3 speclur = pow(max(0, dot(worldLightReflectDir, worldViewDir)), _Speclur) * _Color;
//自发光分量,直接使用设定值
float3 emissive = _EmissiveColor.xyz;
return fixed4((ambient + diffuse + speclur + emissive) * texColor, 1);
}
ENDCG
}
}
}
效果如下,上面是设置_BumpScale为0的效果(根据上面的计算过程,设置为0就是所有的法线都是原法线方向),下面是设置_BumpScale为1,可以看出效果十分显著。