概述
纹理除了可以用来进行颜色映射外,另外一种常见的应用就是进行凹凸映射;
凹凸映射的目的是使用一张纹理来修改模型表面的法线,达到不增加顶点让模型看起来有凹凸效果;
凹凸映射的两种主流方式
高度纹理贴图
概述
高度纹理贴图一般简称高度图,存储了模型表面上每个点的高度信息。通常它使用灰度图像,其中不同灰度值表示不同高度,较亮区域表示较高的点,较暗的区域表示较低的点;
它主要用于模拟物体表面的位移。
存储规则
图片中某一像素点的RGB值是相同的,都表示高度值,A值一般情况下为1,高度值范围一般为0~1,0表示最低,1表示最高。
优点
可以通过高度图很明确的知道模型表面的凹凸情况。
缺点
无法在Shader中直接得到模型表面点的法线信息,而是需要通过额外的计算得到,因此会增加性能消耗,所以一般很少使用它。
法线纹理贴图
概述
法线纹理贴图一般简称法线贴图或法线纹理,它存储了模型表面上每个点的法线方向。
存储规则
图片中的RGB值分别存储法线的X、Y、Z分量值,A值可以用于存储其他信息,比如材质光滑等。
优点
从法线贴图中取出的数据便是法线信息,可以直接简单处理后就参与光照计算,性能表现更好。
缺点
无法直观的看出模型表面的凹凸情况。
两种空间下的法线纹理
基于模型空间的法线纹理
模型数据中自带的法线数据,是定义在模型空间中的,因此最直接的存储法线贴图数据的方式就是存储基于模型空间下的法线信息。
基于切线空间的法线纹理
实际开发时,美术给到的法线贴图一般都是基于切线空间的;
每个顶点都有自己的切线空间:
- 原点:顶点本身;
- X轴:顶点切线;
- Z轴:法线方向(顶点的原法线);
- Y轴:X和Z的叉乘结果,也被称为副切线;
为什么要使用切线空间下的法线贴图
- 可以用于不同模型;
- 方便处理模型变形;
- 可以复用:例如一个砖块6个面的贴图都是一样的,可以只用一张法线贴图即可用于6个面计算;
- 可以压缩:可以只存储两个轴的分量;
- 方便制作uv动画;
法线贴图的计算方式
概述
在计算光照模型时,通常有两种选择:
- 在切线空间下进行光照计算,需要把光照方向、视角方向变换到切线空间下参与计算;
- 在世界空间下进行光照计算,需要把法线方向变换到世界空间下参与计算
比较
- 在切线空间中计算,效率更高,因为可以在顶点着色器中完成对光照、视角方向的矩阵变换。对全局效果的表现可能不够准确,例如处理一些镜面反射、环境映射效果时表现效果可能不够准确;
- 在世界空间中计算,效率较低,由于需要对法线贴图进行采样,所以变换过程必须在片元着色器中实现,需要在片元着色器中对法线进行矩阵变换。对全局效果的表现更准确,可以更容易的应用于全局效果的计算;
在切线空间下计算
Shader "Study/DiffuseTextureT"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
// 高光反射颜色
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
// 光泽度
_SpecularLevel("SpecularLevel", Range(0, 20)) = 0.5
// 主纹理
_MainTex("MainTex", 2D) = ""{}
// 凹凸纹理
_BumpMap("BumpMap", 2D) = ""{}
// 凹凸程度
_BumpScale("BumpScale", Range(0, 1)) = 1
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos: SV_POSITION;
float4 uv : TEXCOORD0;
// 光的方向 相对于切线空间下
float3 lightDir : TEXCOORD1;
// 视角方向 相对于切线空间下
float3 viewDir : TEXCOORD2;
};
float4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
float4 _SpecularColor;
fixed _SpecularLevel;
v2f vert(appdata_full 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 * _BumpMap_ST.xy + _BumpMap_ST.zw;
// 在顶点着色器中得到模型空间到切线空间的转换矩阵
// 计算副切线 = 切线与法线叉乘
float3 binormal = cross(normalize(v.tangent), normalize(v.normal)) * v.tangent.w;
// 转换矩阵
float3x3 rotation = float3x3(v.tangent.xyz,
binormal,
v.normal);
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 通过纹理采样函数取出法线纹理贴图中的数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// 将取出来的法线数据进行逆运算并进行解压缩运算 最终得到切线空间下的法线数据
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal *= _BumpScale;
// 漫反射材质颜色与纹理颜色叠加
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _MainColor.rgb;
// 计算兰伯特光照模型颜色
fixed3 lambertColor = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, normalize(i.lightDir)));
// 计算半角向量
float3 halfAngle = normalize(normalize(i.lightDir) + normalize(i.viewDir));
// 计算Phong式高光反射颜色
fixed3 blinnPhongSpecularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
max(0, dot(tangentNormal, halfAngle)), _SpecularLevel);
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + blinnPhongSpecularColor;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
在世界空间下计算
// 世界空间下计算法线纹理贴图
Shader "Study/DiffuseTextureW"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
// 高光反射颜色
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
// 光泽度
_SpecularLevel("SpecularLevel", Range(0, 20)) = 0.5
// 主纹理
_MainTex("MainTex", 2D) = ""{}
// 凹凸纹理
_BumpMap("BumpMap", 2D) = ""{}
// 凹凸程度
_BumpScale("BumpScale", Range(0, 1)) = 1
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos: SV_POSITION;
float4 uv : TEXCOORD0;
// 顶点在世界空间中的坐标
float3 wPos : TEXCOORD1;
// 切线空间到世界空间的变换矩阵
float3x3 rotation : TEXCOORD2;
};
float4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
float4 _SpecularColor;
fixed _SpecularLevel;
v2f vert(appdata_full 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 * _BumpMap_ST.xy + _BumpMap_ST.zw;
o.wPos = mul(unity_ObjectToWorld, v.vertex);
// 把模型空间法线转换到世界空间
float3 wNormal = UnityObjectToWorldNormal(v.normal);
// 把模型空间切线转换到世界空间
float3 wTangent = UnityObjectToWorldDir(v.tangent);
// 计算副切线 = 切线与法线叉乘
float3 wBinormal = cross(normalize(wTangent), normalize(wNormal)) * v.tangent.w;
// 转换矩阵
o.rotation = float3x3(wTangent.x, wBinormal.x, wNormal.x,
wTangent.y, wBinormal.y, wNormal.y,
wTangent.z, wBinormal.z, wNormal.z);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 世界空间下光的方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
// 世界空间下视角方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos));
// 通过纹理采样函数取出法线纹理贴图中的数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// 将取出来的法线数据进行逆运算并进行解压缩运算 最终得到切线空间下的法线数据
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal *= _BumpScale;
float3 wNormal = mul(i.rotation, tangentNormal);
// 漫反射材质颜色与纹理颜色叠加
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _MainColor.rgb;
// 计算兰伯特光照模型颜色
fixed3 lambertColor = _LightColor0.rgb * albedo * max(0, dot(wNormal, normalize(lightDir)));
// 计算半角向量
float3 halfAngle = normalize(normalize(lightDir) + normalize(viewDir));
// 计算Phong式高光反射颜色
fixed3 blinnPhongSpecularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
max(0, dot(wNormal, halfAngle)), _SpecularLevel);
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + blinnPhongSpecularColor;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
改进
修改凹凸系数的计算方式,让法线系数不影响光照
由于当前的算法是直接让 法线 * 凹凸系数 的计算方式,并不是标准算法,因为当凹凸系数趋近于0时,会影响光照模型的计算;
为了让凹凸系数不影响光的效果,有一种专门的算法:
- 只让法线中的xy乘以凹凸系数
tangentNormal.xy *= _BumpScale
; - 保证法线为单位向量(让法线不会为0,而是趋近于顶点法线)
// x^2 + y^2 + z^2 = 1
// z^2 = 1 - (x^2 + y^2)
// z = sqrt(1 - (x^2 + y^2))
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)))
通过这样的计算,当凹凸系数在0~1之间变化时,会保证法线为单位向量,这样就不会影响光照表现了;
// 世界空间下计算法线纹理贴图
Shader "Study/DiffuseTextureW"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
// 高光反射颜色
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
// 光泽度
_SpecularLevel("SpecularLevel", Range(0, 20)) = 0.5
// 主纹理
_MainTex("MainTex", 2D) = ""{}
// 凹凸纹理
_BumpMap("BumpMap", 2D) = ""{}
// 凹凸程度
_BumpScale("BumpScale", Range(0, 1)) = 1
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos: SV_POSITION;
float4 uv : TEXCOORD0;
// 顶点在世界空间中的坐标
float3 wPos : TEXCOORD1;
// 切线空间到世界空间的变换矩阵
float3x3 rotation : TEXCOORD2;
};
float4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
float4 _SpecularColor;
fixed _SpecularLevel;
v2f vert(appdata_full 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 * _BumpMap_ST.xy + _BumpMap_ST.zw;
o.wPos = mul(unity_ObjectToWorld, v.vertex);
// 把模型空间法线转换到世界空间
float3 wNormal = UnityObjectToWorldNormal(v.normal);
// 把模型空间切线转换到世界空间
float3 wTangent = UnityObjectToWorldDir(v.tangent);
// 计算副切线 = 切线与法线叉乘
float3 wBinormal = cross(normalize(wTangent), normalize(wNormal)) * v.tangent.w;
// 转换矩阵
o.rotation = float3x3(wTangent.x, wBinormal.x, wNormal.x,
wTangent.y, wBinormal.y, wNormal.y,
wTangent.z, wBinormal.z, wNormal.z);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 世界空间下光的方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
// 世界空间下视角方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos));
// 通过纹理采样函数取出法线纹理贴图中的数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// 将取出来的法线数据进行逆运算并进行解压缩运算 最终得到切线空间下的法线数据
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
float3 wNormal = mul(i.rotation, tangentNormal);
// 漫反射材质颜色与纹理颜色叠加
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _MainColor.rgb;
// 计算兰伯特光照模型颜色
fixed3 lambertColor = _LightColor0.rgb * albedo * max(0, dot(wNormal, normalize(lightDir)));
// 计算半角向量
float3 halfAngle = normalize(normalize(lightDir) + normalize(viewDir));
// 计算Phong式高光反射颜色
fixed3 blinnPhongSpecularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
max(0, dot(wNormal, halfAngle)), _SpecularLevel);
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + blinnPhongSpecularColor;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
提高性能的写法
目前在v2f结构体中,世界坐标顶点位置和变换矩阵使用了float3和float3x3两点变量来存储,但是在很多世界空间下计算,法线贴图的Shader中往往会使用3个float4类型的变量来存储它们,这样做的目的是因为在很多情况下可以提高性能,因为它更好地与GPU的硬件架构匹配;
float4类型的寄存器是非常高效的,因为现代GPU通常会以4个分量的向量为基本单位进行并行计算,而float3x3矩阵相对来说需要更多的寄存器和指令来表示和计算;
// 世界空间下计算法线纹理贴图
Shader "Study/DiffuseTextureW"
{
Properties
{
_MainColor("MainColor", Color) = (1, 1, 1, 1)
// 高光反射颜色
_SpecularColor("SpecularColor", Color) = (1,1,1,1)
// 光泽度
_SpecularLevel("SpecularLevel", Range(0, 20)) = 0.5
// 主纹理
_MainTex("MainTex", 2D) = ""{}
// 凹凸纹理
_BumpMap("BumpMap", 2D) = ""{}
// 凹凸程度
_BumpScale("BumpScale", Range(0, 1)) = 1
}
SubShader
{
Pass
{
Tags
{
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos: SV_POSITION;
float4 uv : TEXCOORD0;
// 切线空间到世界空间的变换矩阵的三行
// 多出来的w分量存储顶点在世界空间中的坐标
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
float4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
float4 _SpecularColor;
fixed _SpecularLevel;
v2f vert(appdata_full 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 * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 wPos = mul(unity_ObjectToWorld, v.vertex);
// 把模型空间法线转换到世界空间
float3 wNormal = UnityObjectToWorldNormal(v.normal);
// 把模型空间切线转换到世界空间
float3 wTangent = UnityObjectToWorldDir(v.tangent);
// 计算副切线 = 切线与法线叉乘
float3 wBinormal = cross(normalize(wTangent), normalize(wNormal)) * v.tangent.w;
// 转换矩阵
o.TtoW0 = float4(wTangent.x, wBinormal.x, wNormal.x, wPos.x);
o.TtoW1 = float4(wTangent.y, wBinormal.y, wNormal.y, wPos.y);
o.TtoW2 = float4(wTangent.z, wBinormal.z, wNormal.z, wPos.z);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 世界空间下光的方向
fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 wPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
// 世界空间下视角方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(wPos));
// 通过纹理采样函数取出法线纹理贴图中的数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// 将取出来的法线数据进行逆运算并进行解压缩运算 最终得到切线空间下的法线数据
float3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
float3 wNormal = float3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal),dot(i.TtoW2.xyz, tangentNormal));
// 漫反射材质颜色与纹理颜色叠加
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _MainColor.rgb;
// 计算兰伯特光照模型颜色
fixed3 lambertColor = _LightColor0.rgb * albedo * max(0, dot(wNormal, normalize(lightDir)));
// 计算半角向量
float3 halfAngle = normalize(normalize(lightDir) + normalize(viewDir));
// 计算Phong式高光反射颜色
fixed3 blinnPhongSpecularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(
max(0, dot(wNormal, halfAngle)), _SpecularLevel);
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + blinnPhongSpecularColor;
return fixed4(color.rgb, 1);
}
ENDCG
}
}
}
计算光照的方式
模拟定向光源
直接得到_WorldSpaceLightPos0
光照位置作为光照方向,表示光线是平行的,而不是从特定点发射,一般模拟太阳光效果会采用这种方式;
模拟点光源
用光照位置_WorldSpaceLightPos0
减去顶点坐标,表示光线是从特定点发射的,并朝着定点方向,一般定点光源会采用这种方式;
其他
模型空间下的切线数据
模型数据中的切线数据是float4类型的,其中的w表示副切线的方向,用法线和切线叉乘得到的副切线方向可能有两个,用切线数据中的w与之相乘确定副切线方向;
Unity中的法线纹理类型
当把纹理设置为Normal Map(法线贴图)时。可以使用Unity提供的内置函数UnpackNormal来得到正确的法线方向。该函数内部不仅可以进行 法线分量 = 像素分量 * 2 - 1
的逆运算,还会进行解压运算(Unity会根据不同平台对法线纹理进行压缩)。
属性命名
法线纹理属性命名一般为_BumpMap
(凸块贴图);
一般还会声明一个名为_BumpScale
(凸块缩放)的float属性,用于控制凹凸程度;
如果使用了高度纹理
如果使用的凹凸纹理不是法线纹理而是高度纹理,需要进行如下设置
勾选Create from GrayScale,就可以把高度纹理当成切线空间下的法线纹理处理了;
Bumpiness(颠簸值)控制凹凸程度
Filtering(过滤模式)决定计算凹凸程度的算法:
- Sharp:滤波生成法线
- Smooth:平滑的生成法线