凹凸映射(包含基础纹理和法线纹理)
凹凸映射
凹凸映射的目的是使用一张纹理来修改模型表面的法线。这种方法不会真正该改变模型顶点位置,使模型看起来具有凹凸的效果,这点从模型的轮廓可以看出来。
凹凸映射的两种方法:
高度纹理
使用一张高度纹理来模拟表面位移,得到一个修改后的法线值,该方法也叫“高度映射"。
高度图中存储的是强度值,用于表示模型表面的海拔高度。颜色越浅表示越向外凸起,颜色越深越向里凹。实时计算中,不能直接得到表面法线,需要由像素的灰度值计算得到,需要消耗更多性能。
法线纹理
使用一张法线纹理直接存储表面法线,该方法也叫“法线映射”。
法线纹理中存储的是表面的法线方向,法线矢量值范围[-1,1],纹理中的颜色值范围[0,1],因此将法线存储在一张纹理中,有一个映射过程:
pixel= ( normal+1)/2
因此在对法线纹理进行采样得到像素颜色后,为了得到对应的发现方向,需要进行反映射过程normal=2(pixel)-1
法线方向所在的坐标空间
模型空间的法线纹理
模型顶点自带的法线,定义在模型空间中,若将修改后的模型空间中的表面法线存储在一张纹理中,则为模型空间中的法线纹理
切线空间的法线
纹理模型的每一个 顶点有自己对应的切线空间,z轴为顶点的法线方向,x轴为顶点的切线方向,y轴为顶点的 副切线方向,由法线和切线的叉积得到,这种纹理被称为切线空间的法线纹理。模型空间下的法线纹理颜色比 较丰富,这是因为模型空间下各顶点的的法线所在的坐标系为同一个坐标系,而各个顶点法线方向各异,因此 映射过后颜色相对比较丰富。
而切线空间下的法线纹理颜色集中在浅蓝色,这是由于各自顶点都有自己的坐标系,法线的方向尽管在同一个 坐标下方向各异,但在自身的切线空间坐标系下,法线方向均指向z轴,因此映射到纹理上,像素颜色单一。
使用哪种坐标空间只是第一步,得到法线信息是为了转化到相应的坐标系(如世界空间)后进行后续的光照计 算。
使用切线空间法线纹理优势
重用性较高。
- 模型空间下法线纹理存储的是绝对法线信息,只能作用于创建时对应的模型,应用到其他模型上无法得到正确的效果。而切线空间的法线纹理的坐标系为各自顶点的坐标系,是一个相对法线信息,应用到其他的模型或者一个砖块使用一张法线纹理应用到6个面上也能得到合理的结果。
可进行UV动画
- 切线空间纹理中的法线方向是根据对应纹理的纹理坐标方向得到。因此可以通过移动一个纹理的UV坐标实现凹凸移动的效果。
可压缩
- 切线空间下的法线纹理中,z轴方向总为正方向,因此可以可以仅存储x,y轴方向,通过计算再得到z方向。
切线空间下的法线纹理在使用上更加灵活,因此基本上选用切线空间作为法线纹理的坐标系。
光照模型计算中坐标空间的选择
由于在计算光照过程中,需要在一个统一的空间下进行,而法线纹理所在的空间为切线空间。因此有两种方式选择。
- 将光照方向和视角方向变换到切线空间进行,这个过程在顶点着色器中可以完成。
- 将法线方向变换到世界空间中进行,这时需要先对发现纹理进行采样,所以该过程在片元着色器中进行,并在片元着色器中进行一次矩阵运算。
Warp Mode属性
每张纹理在导入到Unity后,在纹理的检视面板中有Warp Mode属性,该属性决定了当纹理坐标超过[0,1]范围后如何被平铺,有Repeat(重复平铺),Clamp(截取平铺)截取平铺是当超过1后的纹理坐标的对应的顶点颜色值均为1处的值。
切线空间下的光照模型计算
由于法线纹理中使用的本身就是切线空间,因此需要将光照方向和观察方向先转换到切线空间,这个过程可以在顶点着色器中完成。模型-->切线空间下的转换矩阵计算,首先切线空间-->模型空间的变换矩阵为模型顶点的切线方向(x轴)、副切线方向(y轴)、法线方向(z轴)的按列排列形式,即模型-->切线空间变换矩阵的逆矩阵,而对于一个方向矢量而言,一个变换矩阵若只存在平移和旋转变换,则该矩阵为一个正交阵,即变换矩阵的逆矩阵与转置矩阵相等,因此模型-->切线空间的变换矩阵为逆矩阵的转置,即将模型顶点的切线方向(x轴)、副切线方向(y轴)、法线方向(z轴)的 按行排列。
世界空间下的光照模型计算
世界空间下的光照模型计算需要将切线空间下的法线变换到世界空间中,因此需要先知道切线到世界的变换矩阵,又由于法线是通过在片元着色器中做纹理采样得到,因此需要通过在顶点着色器中得到转换矩阵再传递到片元着色器,在进行纹理采样后再做转换并计算光照效果。
//主要是练习法线平铺和法线反向的功能
Shader "Unlit/Normal map"
{
Properties {
// 材质面板排版
_Color("Color",Color) = (1,1,1,1)
_MainTex("MainTex",2D) = "white"{}
_BumpTex("BumpTex",2D) = "bump"{}
_BumpScale("BumpScale",Range(-5,5)) = 1
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 追加光照相关包含文件
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
//输入参数
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float4 _BumpTex_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
//输入结构
struct VertexInput {
float4 vertex : POSITION;
float2 uv0 : TEXCOORD0;
float4 normal : NORMAL;
float4 tangent : TANGENT;//切线空间
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 屏幕空间顶点位置
float4 uv : TEXCOORD0;
float4 posWS : TEXCOORD1; // 世界空间顶点位置
float3 nDirWS : TEXCOORD2; // 世界空间法线方向
float3 tDirWS : TEXCOORD3; // 世界空间切线方向
float3 bDirWS : TEXCOORD4; // 世界空间副切线方向
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; // 新建输出结构
o.pos = UnityObjectToClipPos(v.vertex); // 从模型空间转换到裁剪空间的顶点位置
o.uv.xy = v.uv0.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 传递UV
o.uv.zw = v.uv0.xy * _BumpTex_ST.xy + _BumpTex_ST.zw;//没什么项目会变法线吧(来自新手的疑惑)
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 从模型空间转换到世界空间的顶点位置
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 从模型空间转换到世界空间的法线方向
o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz); // 从模型空间转换到世界空间的切线方向
o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w); // 从模型空间转换到世界空间的副切线方向
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
float3 nDirTS = UnpackNormal(tex2D(_BumpTex, i.uv.zw)).rgb;//采集法线贴图
nDirTS.xy *= _BumpScale;//切线空间法线贴图xy位置
nDirTS.z = sqrt(1.0 - saturate(dot(nDirTS.xy, nDirTS.xy)));//切线空间法线贴图z位置
float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS); // TBN矩阵
float3 nDirWS = normalize(mul(nDirTS,TBN)); //世界空间法线方向
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); //世界空间下的观察向量
float3 lDirWS = _WorldSpaceLightPos0.xyz; // 世界空间光照方向
fixed3 halfDir = normalize(lDirWS + vDirWS);// 半角向量 = 观察方向 + 光向量 然后把得到的结果进行一次归一化,防止信息出现负数
fixed3 albedo = _Color.rgb * tex2D(_MainTex, i.uv).rgb;//基础颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;//默认环境光
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(lDirWS, nDirWS));//基础颜色和lambert模型相乘
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(halfDir, nDirWS)), _Gloss);//高光使用Blinn-Phong
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
渐变纹理
理的最初使用,是为了给一个模型表面上色。实际上,纹理可以用来存储表面属性,如之前的法线纹理将法线信息存储在一张纹理中。通过纹理也可以控制漫反射光照结果。 渐变纹理控制漫反射实例代码:
Shader "Unlit/NewUnlitShader"
{
Properties{
// 材质面板排版
_Color("Color",Color) = (1,1,1,1)
_RampTex("RampTex",2D) = "white"{}
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 追加光照相关包含文件
#include "Lighting.cginc"
//输入参数
fixed4 _Color;
sampler2D _RampTex;
float4 _RampTex_ST;
fixed4 _Specular;
float _Gloss;
//输入结构
struct VertexInput {
float4 vertex : POSITION;
float2 uv0 : TEXCOORD0;
float3 normal:NORMAL;
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 屏幕空间顶点位置
float2 uv : TEXCOORD0;
float3 posWS : TEXCOORD1; // 世界空间顶点位置
float3 nDirWS :TEXCOORD2; // 世界空间法线方向
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0; // 新建输出结构
o.pos = UnityObjectToClipPos(v.vertex); // 从模型空间转换到裁剪空间的顶点位置
o.nDirWS = UnityObjectToWorldNormal(v.normal);// 从模型空间转换到世界空间的法线方向
o.posWS= mul(unity_ObjectToWorld, v.vertex).xyz;// 从模型空间转换到世界空间的顶点位置
o.uv = v.uv0.xy * _RampTex_ST.xy + _RampTex_ST.zw;//平铺
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR {
fixed3 nDirWS = normalize(i.nDirWS); // 世界空间法线方向
fixed3 lDirWS = normalize(UnityWorldSpaceLightDir(i.posWS));// 世界空间光照方向
fixed3 vDirWS = normalize(UnityWorldSpaceViewDir(i.posWS));//世界空间下的观察向量
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//默认环境光
fixed halfLambert = 0.5 * dot(nDirWS, lDirWS) + 0.5;//半lambert
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)) * _Color.rgb;//采集贴图颜色信息,输出颜色
fixed3 diffuse = diffuseColor * _LightColor0.rgb;//使其受光影响
fixed3 halfDir = normalize(lDirWS + vDirWS);// 半角向量 = 观察方向 + 光向量 然后把得到的结果进行一次归一化,防止信息出现负数
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfDir, nDirWS)),_Gloss);//高光使用Blinn-Phong
return fixed4(ambient + diffuse + specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
结果
- 这里进行纹理采样的uv坐标为半兰伯特值,将法线与光照方向的点积映射到[0,1]也就是说原本光照不到的地方会取到渐变纹理中靠左下部分的颜色。
- 由于采样时uv坐标都是相等的,因此取到的颜色应该是对应纹理坐标[0,1]内的对角线上的颜色。
- 还有一点值得注意的是,当采用突变性的渐变纹理时(如第一张渐变纹理),漫反射的结果是阴影之间更加分明,类似于卡通效果。
//更多渐变效果
遮罩纹理
遮罩纹理应用于很多商业游戏中,用来保护某些区域,免于某些修改。
两个常见的应用:
- 使模型某些区域的高光强烈,某些区域较弱。而不是将高光反射应用到模型的所有地方,使用遮罩纹理可以更加细腻的控制高光的光照效果。
- 制作地形材质时需要混合多张图片,例如表现草地,石子,裸露土地的纹理。使用遮罩纹理可以控制如何混合这些纹理。
使用者遮罩纹理的流程:
通过采样得到遮罩纹理的纹素值,然后使用其中某个通道的值与某种表面属性进行相乘,当该通道值为0时,可以保护表面不受属性影响。
Shader "Custom/Chapter7_MaskTexture"
{
Properties
{
_Color("Color",Color) = (1,1,1,1)
_MainTex("MainTex",2D) = "white"{}
_BumpTex("BumpTex",2D) = "bump"{}
_BumpScale("BumpScale",Float) = 1.0
_SpecularMask("SpecularMask",2D) = "white"{}//遮罩贴图
_SpecularMaskScale("SpecularMaskScale",Float) = 1.0
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20.0
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
//输入参数
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularMaskScale;
fixed4 _Specular;
float _Gloss;
//输入结构
struct VertexInput {
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT;
float4 uv0:TEXCOORD0;
};
//输出结构
struct VertexOutput {
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv= v.uv0 * _MainTex_ST.xy + _MainTex_ST.zw;
float3 binormal = cross(normalize(v.normal), normalize(v.tangent)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR{
fixed3 lDirTS = normalize(i.lightDir);
fixed3 vDirTS = normalize(i.viewDir);
float3 nDirTS = UnpackNormal(tex2D(_BumpTex, i.uv));//采集法线贴图
nDirTS.xy *= _BumpScale;//切线空间法线贴图xy位置
nDirTS.z = sqrt(1.0 - saturate(dot(nDirTS.xy, nDirTS.xy)));//切线空间法线贴图z位置
fixed3 albedo = tex2D(_MainTex,i.uv) * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(lDirTS, nDirTS));
fixed3 halfDir = normalize(lDirTS + vDirTS);
fixed3 specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularMaskScale;//使用其中一个通道r来影响高光的光照效果
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfDir, nDirTS)),_Gloss) * specularMask;
return fixed4(ambient + diffuse + specular,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
Shader "Custom/Chapter7_MaskTexture" {
Properties
{
_Color("Color",Color) = (1,1,1,1)
_MainTex("MainTex",2D) = "white"{}
_BumpTex("BumpTex",2D) = "bump"{}
_BumpScale("BumpScale",Float) = 1.0
_SpecularMask("SpecularMask",2D) = "white"{}//遮罩贴图
_SpecularMaskScale("SpecularMaskScale",Float) = 1.0
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20.0
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
//输入参数
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpTex;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularMaskScale;
fixed4 _Specular;
float _Gloss;
//输入结构
struct VertexInput {
float4 vertex:POSITION;
float3 normal:NORMAL;
float4 tangent:TANGENT;
float4 uv0:TEXCOORD0;
};
//输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 屏幕空间顶点位置
float2 uv : TEXCOORD0;
float4 posWS : TEXCOORD1; // 世界空间顶点位置
float3 nDirWS : TEXCOORD2; // 世界空间法线方向
float3 tDirWS : TEXCOORD3; // 世界空间切线方向
float3 bDirWS : TEXCOORD4; // 世界空间副切线方向
};
//顶点Shader
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv0 * _MainTex_ST.xy + _MainTex_ST.zw;
o.posWS = mul(unity_ObjectToWorld, v.vertex); // 从模型空间转换到世界空间的顶点位置
o.nDirWS = UnityObjectToWorldNormal(v.normal); // 从模型空间转换到世界空间的法线方向
o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz); // 从模型空间转换到世界空间的切线方向
o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w); // 从模型空间转换到世界空间的副切线方向
return o;
}
//像素Shader
float4 frag(VertexOutput i) : COLOR
{
float3 nDirTS = UnpackNormal(tex2D(_BumpTex, i.uv)).rgb;
nDirTS.xy *= _BumpScale;
nDirTS.z = sqrt(1.0 - saturate(dot(nDirTS.xy, nDirTS.xy)));
float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS); // TBN矩阵
float3 nDirWS = normalize(mul(nDirTS,TBN));
float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
float3 lDirWS = _WorldSpaceLightPos0.xyz;
fixed3 albedo = tex2D(_MainTex,i.uv) * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0,dot(lDirWS, nDirWS));
fixed3 halfDir = normalize(lDirWS + vDirWS);
fixed3 specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularMaskScale;//使用其中一个通道r来影响高光的光照效果
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(halfDir, nDirWS)),_Gloss) * specularMask;
return fixed4(ambient + diffuse + specular,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
这里有两点需要注意的地方:
- 主纹理,法线纹理和遮罩纹理的纹理坐标都来自主纹理的uv坐标,也就是说当修改材质面板的主纹理的缩放和偏移值时,法线纹理和遮罩纹理都会相应变化,而修改法线纹理和遮罩纹理的缩放偏移值是不会对计算结果产生任何影响的,事实上测试的结果也是这样。
- 遮罩纹理的计算过程中只用到了纹理的r通道,其他3个通道其实可以用来存储更多的设置值。
通过遮罩处理后,高光的效果不是全部反映到整个区域,而是由r通道进行选择,遮罩纹理中全黑色的地方,即r=0处是不会受到高光影响的
//文章参考 https ://zhuanlan.zhihu.com/p/31450857