一、基础纹理
1、基础纹理实现:
Shader "Custom/TextureSingle"
{
Properties
{
_Color("Color", color) = (1, 1, 1, 1)
_MainTex("Main Tex", 2d) = ""{}
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
//为纹理类型声明一个 float4 类型的变量 _MainTex_ST.
//命名规则为纹理名_ST,
//ST是缩放(scale)和平移(transform)的缩写。
//使用_MainTex_ST.xy 存储了缩放值。
//使用_MainTex_ST.zw 存储了偏移值。
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
//使用 TEXCOORD0 语义声明了一个新的变量
//这样unity就会将模型的第一组纹理坐标存储到该变量总。
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
//然后再这里使用 uv 变量,以便在 片元着色器中进行纹理采样。
float2 uv : TEXCOORD2 ;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
//使用纹理的属性值 _MainTex_ST 对顶点纹理坐标进行变换,得到最终的纹理坐标。
//首先使用 _MainTex_ST.xy 对顶点纹理坐标进行缩放,
//然后再使用偏移属性 _MainTex_ST.zw 对结果进行偏移。
//【这里理解为,需要对纹理进行缩放和平移的操作】
o.uv = _MainTex_ST.zw + v.texcoord.xy * _MainTex_ST.xy;
//unity 提供了一个内置的宏 TRANSFORM_TEX 来帮助
//计算上述过程。这是再 UnityCG.cginc 中定义的
//它接受两个参数
//第一个参数是顶点纹理坐标【v.texcoord】
//第二个参数是纹理名字【_MainTex】
//o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
//tex2D 函数对纹理进行采样
//第一个参数是被需要采样的纹理
//第二个参数是一个 float2 类型的纹理坐标, 它将返回计算得到的纹素坐标。
//它将返回计算得到的纹素值。
//然后使用采样结果和颜色属性 _Color 的乘积作为材质的反射率 albedo
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
//并把它和环境光照相乘得到环境光部分。
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//随后,使用 albedo 来计算漫反射光照的结果,并和环境光、高光反射照相后返回。
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
//这里不使用高光的效果了
//fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
//fixed3 halfDir = normalize(worldLightDir + viewDir);
//fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse, 1.0);
}
ENDCG
}
}
}
//为纹理类型声明一个 float4 类型的变量 _MainTex_ST.
//命名规则为纹理名_ST,
//ST是缩放(scale)和平移(transform)的缩写。
//使用_MainTex_ST.xy 存储了缩放值。
//使用_MainTex_ST.zw 存储了偏移值。
2、
//使用 TEXCOORD0 语义声明了一个新的变量
//这样unity就会将模型的第一组纹理坐标存储到该变量总。
3、
//然后再这里使用 uv 变量,以便在 片元着色器中进行纹理采样。
4、
//使用纹理的属性值 _MainTex_ST 对顶点纹理坐标进行变换,得到最终的纹理坐标。
//首先使用 _MainTex_ST.xy 对顶点纹理坐标进行缩放,
//然后再使用偏移属性 _MainTex_ST.zw 对结果进行偏移。
//【这里理解为,需要对纹理进行缩放和平移的操作】
5、
//unity 提供了一个内置的宏 TRANSFORM_TEX 来帮助
//计算上述过程。这是再 UnityCG.cginc 中定义的
//它接受两个参数
//第一个参数是顶点纹理坐标【v.texcoord】
//第二个参数是纹理名字【_MainTex】
//o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
6、
//tex2D 函数对纹理进行采样
//第一个参数是被需要采样的纹理
//第二个参数是一个 float2 类型的纹理坐标, 它将返回计算得到的纹素坐标。
//它将返回计算得到的纹素值。
//然后使用采样结果和颜色属性 _Color 的乘积作为材质的反射率 albedo
7、 //并把它和环境光照相乘得到环境光部分。
//随后,使用 albedo 来计算漫反射光照的结果,并和环境光、高光反射照相后返回。
2、纹理的属性
很多资料把 unity 的纹理映射描述得很简单 —— 声明一个纹理变量,再使用 tex2D 函数采样。
实际上,再渲染流水线中, 纹理映射的实现远比想象中的复杂。本书不会涉及具体的实现细节,但是要解释一些我们认为读者必须要知道的事情。
Texture Type 纹理类型
Alpha from Grayscale
如果勾选了,那么透明通道的值将会由每个像素的灰度值生成。
Wrap Mode
Repeat 模式:如果纹理的坐标超过了 1, 那么它的整数部分将会被舍弃,而直接使用小数部分进行采样,这样的结果是纹理将会不断重复。
Clamp 模式:如果纹理坐标大于1,那么将会截取到1, 如果小于 0 ,那么将会截取到 0.
需要达到这样的效果,必须使用纹理的属性,在 unity shader 中对顶点纹理坐标进行相应的变换。代码中需要包含类似下面的代码:
二、凹凸纹理
凹凸纹理【凹凸映射】:
凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。
主要有两种方法:
1、
使用一个高度纹理来模拟表面位移,然后得到一个修改后的法线值,【高度映射】
2、
使用一张法线纹理来直接存储表面法线,【法线映射】
1、高度纹理
高度图中存储的是强度值,它用于表示模型表面局部的海拔高度。因此,颜色越浅表面该
位置的表面越向外凸起,而颜色越深表面该位置越向里凹。
2、法线纹理
法线纹理中存储的就是表面的法线方向。由于法线的方向的分量范围再[-1, 1]之中,而
像素的分量范围为[0, 1],因此这需要做一个映射,通常为:
这就要求,我们再shader中对法线纹理进行纹理采样后,还需要对结果进行一次反映射
的过程,以得到原先的法线方向。
反映射的过程实际上就是使用上面的映射函数的逆函数:
然而,由于方向是相对于坐标空间来说的,那么法线纹理中存储的法线再哪个坐标空中了?
对于模型顶点自带的法线,它们是定义再模型空间中的,因此一种直接的想法就是将修改后
的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是
模型空间的法线纹理【object - space normal map】
然而在实际制作中,往往会采样另外一种坐标空间,即模型顶点的
切线空间【tangent space】
如图所示:
存储的纹理被称为:切线空间的法线纹理【tangent - space normal map】
所以总体来说,使用模型空间来存储法线的优点如下:
1、实现简单,更加直观。
2、在纹理坐标的缝合处和尖锐的边角不会,可见的突变(缝隙)较少,即可以提供平滑的边界。
但使用切线空间有更多的优点:
1、自由的很高。模型空间下的纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,不能用于其他。而切线空间下的法线纹理记录的是相对法线信息,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。
2、可以进行UV动画。
3、可以重用法线纹理。
4、可压缩。
切线空间下的实践:
在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向进行计算,得到最终的光照结果。为此,首先需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中,即需要知道从模型空间到切线空间的变换矩阵。
1、在切线空间下的实践:
Shader "Custom/NormMapTangentSpace"
{
Properties
{
_Color("Color", color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
//对于纹理法线,使用“bump”作为它的默认值。
//当没有任何提供法线纹理时,"bump"就对应了模型自带的法线信息。
_BumpMap("Normal Map", 2D) = "bump"{}
_BumpScale("Bump Scale", float) = 1.0
_Specular("Specular", color) = (1, 1, 1, 1)
_Gloss("Gloss", range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
// 切线空间是由顶点法线和切线构建的一个坐标空间,
//因此需要得到顶点的切线信息。
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
//使用 TANGENT 语义来描述 float4 类型的tangent 变量,
//告诉 unity 把顶点切线方向填充到tangent变量中。
//【需要注意的是,tangent的类型是float4, normal的类型是float3】
//这是因为需要使用 tangent.w 分量来决定切线空间中的第三个坐标轴 --- 副切线方向性。
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
//需要在顶点着色器中计算切线空间下的光照和视角方向,
//因此,添加:lightDir 和 viewDir 变量。
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//使用变量 uv 来存储_MainTex的xy分量和_BumpMap的zw分量的值
//【实际上,_MainTex 和 _BumpMap 通常会使用同一组纹理坐标,
// 出于减少插值寄存器的数目的目的,往往只计算和存储一个纹理坐标即可】
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
//然后,把模型空间下切线方向、副切线方向和法线方向按行排列
//得到切线空间的变换矩阵 rotation,【cross是把两个矩阵进行叉乘】
//需要注意的是,在计算副切线时,使用 v.tangent.w 和叉积结果进行相乘,
//这是因为和切线与法线方向都垂直的方向有两个, 而 w 决定了我们选择其中的哪一个。
//【决定选择的方向】
float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
//以上的步骤可以使用 unity 内置的 宏来进行完成
//TANGENT_SPACE_ROTATION; //【在 UnityCG.cginc 中】
//使用内置函数ObjectSpaceLightDir和ObjSpaceViewDir
//得到模型空间下的光照和视角方向,再利用变换矩阵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);
//对_BumpMap进行纹理采样。
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentNormal;
//正如本节一开头所讲的,法线纹理中存储的是把法线经过映射后
//得到的像素值,因此我们需要把它们反映射回来。
//首先把 packedNormal 的xy分量按之前提到的公司映射回法线方向。
//然后乘以 _BumpScale(控制凹凸程度)得到tangentNormal的xy分量。
//tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpSacle;
//由于法线都是单位矢量,因此z分量可以由 xy 计算得到。
//由于使用的是切线空间下的法线纹理,因此可以保证法线方向的 z 分量为正。
//tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//★【这里有解释】
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
}
在★里要注意的解释。
//在unity中,为了方便 unity 对法线纹理的存储进行优化,通常会把法线纹理的
//纹理类型标识成 Normal map, unity会更加平台来选择不同的压缩方法。
//这时,如果再使用上面的方法来计算就会得到错误的结果,因为此时_BumpMap
//的rgb分量并不再是切线空间下法线方向的xyz值了。
//在这种情况下,可以使用Untiy内置的函数 UnpackNormal 来得到正确的法线方向。
2、在世界空间下实现。
我们需要在片元着色器中吧法线方向从切线方向变换到世界空间下。
基本思路:
在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。
变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间下的表示来得到。最后,只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间下即可。
【尽管这种方法需要更多的计算,但在需要使用 cubemap 进行环境映射等情况下,就需要使用这种方法】
Shader "Custom/NormalMapWorldSpace"
{
Properties
{
_Color("Color", color) = (1, 1, 1, 1)
_MainTex("MainTex", 2d) = ""{}
_BumpMap("BumpMap", 2d) = "Bump"{}
_BumpScale("Bump Scale", float) = 1.0
_Specular("Specular", color) = (1, 1, 1, 1)
_Gloss("Gloss", range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
float4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
//定义从切线空间到世界空间的变换矩阵。
//★【注解】
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};
//计算从切线空间到世界空间的变换矩阵。
v2f vert(a2v v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, 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 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;
//按列摆放得到从切线空间到世界空间的变换矩阵。
//并且把世界空间下的顶点位置的 xyz 分量分别存储在了
//TtoW0、TtoW1、TtoW2、的 w 分量中,以便充分利用寄存器的空间
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));
//使用内置的 UnpackNormal 函数对法线纹理进行采样和解码,
//【把法线纹理的格式标识成 Normal Map】
//并利用 _BumpScale 对其进行缩放。
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
//使用 TtoW0、TtoW1、TtoW2存储的变换矩阵把法线变换到世界空间下,
//这是通过使用点乘操作来实现矩阵的每一行和法线相乘得到。
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;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
}
//★【注解】
一个插值寄存器最多只能存储float4大小的变量,对于矩阵这样的变量,可以把它们按行拆成多个变量进行存储。TtoW0、TtoW1、TtoW2就依次存储了从切线空间到世界空间的变换矩阵的每一行。
实际上,对方向矢量的变换只需要使用3x3大小的矩阵,但为了充分利用插值寄存器的存储空间,所以就把世界空间下的顶点位置存储在这些变量的 w 分量中。
在视觉表现上,在切线空间下和在世界空间下计算光照几乎没有任何差别。
在unity 4.x 的版本中,在不需要使用 Cubeamp 进行环境映射的情况下,内置的 unity shader 使用的是切线空间来进行法线映射和光照计算。
而在 5.x 中,所有内置的 unity shader 都使用了世界空间来进行光照计算。这样会比较容易报错,因为它们使用了更多的插值寄存器来存储变换矩阵(还有一些额外的插值寄存器用来辅助计算雾效。)