存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的 2D 坐标。通常,这些坐标使用一个二维变量(u, v)来表示,其中u 是横向坐标,而v 是纵向坐标。因此,纹理映射坐标也被称为UV 坐标。
尽管纹理的大小可以是多种多样的,例如可以是256 x256 或者1028 x 1028,但顶点UV 坐标的范围通常都被归一化到[0,,1]范围内。需要注意的是,纹理采样时使用的纹理坐标不一定是在[0, 1]范围内。实际上,这种不在[0, 1 ]范围内的纹理坐标有时会非常有用。与之关系紧密的是纹理的平铺模式,它将决定渲染引擎在遇到不在[0, 1 ]范围内的纹理坐标时如何进行纹理来样。
还是直接上代码吧
Shader "xxxx"{
Properties{
_Color("Color Tint", Color) = (1,1,1,1)
_MainTex("Main Tex", 2D) = "white" {} // 声明一个_MainTex纹理,
_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; // 其他变量和 光照的差不多,这个是图片纹理,
//我们需要使用 纹理名_ST 的方式来声明某个纹理的属性。其中, ST 是缩放( scale )
//和平移(translation ),_MainTex_ST.xy 存储的是缩放值,而
// _MainTex ST.zw 存储的是偏移值。这些值可以在材质面板的纹理属性中调节
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex : POSITION;
float3 normal :NORMAL;
float4 texcoord: TEXCOORD0; //存储第一组纹理坐标
};
struct v2f {
float4 pos: SV_POSITION;
float3 worldNormal :TEXCOORD0;
float3 worldPos:TEXCOORD1;
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;
o.uv = v.texcoord.xy * _MainTex_ST.xy +_MainTex_ST.zw;
//先xy对图形缩放, 用zw 偏移量 相加 进行偏移 得出最终的纹理坐标
//有内置的函数可以用
//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));
//用uv 偏移量来对纹理采样, 和颜色相乘 作为材质反射率 albedo
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
// 环境光和漫反射都用了 albedo
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz* 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 +specular , 1.0);
}
FallBack "Specular"
}
}
}
上面代码中的反射率 计算过程 有点意思
用顶点得出的uv值 对纹理采样 用得到的rgb 颜色值和 属性_Color乘积作为材质的 反射率 albedo
就是说 颜色属性影响到了折射率。颜色乘积当做反射率
TextureMode 两种 repeat就是不断重复 这个clamp 需要注意 是边界值不断重复,就边界处颜色不断填充。
有一种是Clamp , 在这种模式下,如果纹理坐标大于1 ,那么将会截取到1,如果小于0 ,那么将会截取到0.
但是这种表现的实现也是要依靠shader的计算
重要的就是 _MainTex_ST 的xy 和zw 值 上代码 这几个值可以在界面调整
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// Or just call the built-in function
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
FilterMode 是滤波模式选项 3种
Point,Bilinear和Triliner。效果依次提升,性能也依次增大。内存会增加。 一般point就够用了。
有些时候也用Bilinear模式。这里注意如果没有开minmap Bilinear 和 Triliner 是一样效果
MinMap 技术 (多级渐远纹理) 除非移动3D场景有透视效果一般不启用,占内存!
其实就是一个分级采样,原图生成小图使用,近处大图使用。 一种空间换时间的方法。 多33%的内存空间。
FilterMode滤波实现细节
- Point 模式使用了最近邻( nearest neighbor ) 滤波,在放大或缩小时,它的采样像素数目通常只有一个,因此图像会看起来有种像素风格的效果。
- 而Bilinear 滤波则使用了线性滤波,对于每个目标像素,它会找到4 个邻近像素,然后对它们进行线性插值混合后得到最终像素,因此图像看起来像被模糊了。
- 而Trilinear 滤波几乎是和Bilinear 一样的,只是Trilinear还会在多级渐远纹理之间进行混合。如果一张纹理没有使用多级渐远纹理技术,那么Trilinear 得到的结果是和Bilinear 就一样的。
通常,我们会选择Bilinear 滤波模式。需要注意的是,有时我们不希望纹理看起来是模糊的,例如对于一些类似棋盘的纹理,我们希望它就是像素风的,这时我们可能会选择Point 模式。
凹凸映射 (Bump mapping)
其实就是一个法线贴图, 用纹理值改变模型表现的法线,让看起来有立体感。实现有两种方法
- 使用一张高度纹理(height map)来模拟表面位移(displacement),然后会得到一个修改后的法线值。这也叫 高度映射(height mapping)
- 用法线纹理(normal map)来直接存储法线,这个法线一般是第三方软件直接导出的。 这种一般称做 法线映射(normal mapping)。这个用的比较多!
高度纹理
高度图中存储的是 强度值 (intensity),它用于表示模型表面的海拔高度, 颜色越浅表明该位置越向外凸起,而颜色越深表明该位置越向里凹。
好处:就是直观,便于观察,
缺点:计算更加复杂,实时计算 不够直接得到法线,通过灰度值计算获得。需要更多性能。
高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息。也就是说,我们通常会使用法线映射来修改光照
法线纹理
法线纹理中存储的就是表面的法线方向。由于法线方向的分量范围在[-1, 1 ],而像素的分量范围为[0, 1],因此我们需要做一个映射,通常使用的映射就是:
所以在shader中要使用的话 需要一次反映射的过程。公式就简单了
由于方向是相对于坐标空间来说的,那么法线纹理中存储的法线方向在哪个坐标空间中呢?对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法就是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理( object-space normal map )。然而,在实际制作中,我们往往会采用另一种坐标空间,即模型顶点的切线空间
( tangent space )来存储法线。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z 轴是顶点的法线方向(n), x 轴是顶点的切线方向(t),而y 轴可由法线和切线叉积而得,也被称为是副切线( bitangent, b )或副法线,如图7.12 所示。
这种纹理被称为是切线空间的法线纹理(tangent-space normal map ) 。图7.13 分别给出了模型空间和切线空间下的法线纹理(图片来源: http://www.surlybird.com/tutorials/TangentSpace/)。
平时使用的都是切线空间的法线贴图。
切线空间下的法线纹理看起来几乎全部是浅蓝色的。这是因为,每个法线方向所在的坐标空间是不一样的,即是表面每点各自的切线空间。这种法线纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向。也就是说,如果一个点的法线方向不变,那么在它的切线空间中, 新的法线方向就是z 轴方向,即值为(0, 0, 1 ) , 经过映射后存储在纹理中就对应了RGB(0.5, 0.5, 1 )浅蓝色。而这个颜色就是法线纹理中大片的蓝色。这些蓝色实际上说明顶点的大部分法线是和模型本身法线一样的,不需要改变。
故意进行这样的区别是因为我们不只是需要法线信息,还需要后续的光照信息。用切线空间就需要把法线转向世界空间中。这样做的模型空间存储法线优点有:
- 实现简单,直观,我们不需要模型原始的法线和切线信息,计算少,生成也非常简单。而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV 方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的。
- 在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息, 因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象
但是 切线空间的存储法线 优点更多
- 自由度很高,模型空间有局限性,只能是创建时的模型,别的模型会错。切线空间是相对法线信息,把纹理用到别的模型上也是可以正常使用的。
- 可以UV 动画,如果用模型空间,法线纹理完全错误。但是这种uv动画却经常用到。
- 可以重复利用法线纹理,一张图可以用到6个方块的面。 原因同上
- 可压缩, 有用切线空间下的法线纹理中 法线的z方向总是正方向。我们仅需要存储 XY方向,推导得到z方向。 模型空间是强行绑定每个方向都不相同,不能压缩。
实践
在对应的光照计算时,有两种坐标系可以选择,一个是切线空间,一个是世界空间。第一种性能优于第一种但是有局限性,很多时候我们还需要环境映射和采样。
切线空间
基本思路:在片元着色器通过纹理采样得到切线空间的法线,与切线空间下的视角方向,光照方向进行计算,得到最终结果。 首先需要在顶点着色器中把视角方向从模型转切线。
上代码吧
Shader "xxx"{
Properties{
_Color("Color Tint", Color) = (1,1,1,1)
_MainTex("Main Tex",2D) = "white" {}
_BumpMap("Normal Map", 2D) = "bump" {} //法线贴图 bump是unity内置的法线纹理
_BumpScale("Bump Scale", Float) =1.0 // 控制凹凸程度。0 时法线对高照没有任何影响
_Specular ("Specular", Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader{
Pass{
Tags { "LightMode" = "ForwardBase"}
··· 这段省略
//声明变量
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,而非float3, 这是因为我们需要使用tangent.w 分量来决定切线空间中的第三个坐标轴一一副切线的方向性。
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
}
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 的偏移采样 分别存储了两组纹理坐标,(实际上,纹理和法线通常使用同一组纹理坐标。 减少插值寄存器的使用数目,只计算和存储一个纹理坐标,反正算法也是一样的)
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
//float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz)*v.tangent.w);
//float3*3 rotation = float3*3(v.tangent.xyz, binormal, v.normal);
//可以直接使用unity内置变量 获得变换矩阵,算法是一样的
TANGENT_SPACE_ROTATION;
//最后求出切线空间的 光线方向和视角方向 用了两个内置函数
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyzx;
}
fixed4 frag(v2f i):SV_Target{
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
fixed4 packedNormal =tex2D(_BumpMap, i.uv,zw);
fixed3 tangentNormal;
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 * albedo;
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);
}
}
}
切线空间法线的变量用TANGENT 语义来描述float4 类型的tangent 变量,以告诉Unity 把顶点的切线方向填充到tangent 变量中,tangent 的类型是float4,而非float3, 这是因为我们需要使用tangent.w 分量来决定切线空间中的第三个坐标轴一一副切线的方向性。
有点没看懂 片元的算法
世界空间下的计算
在顶点着色器中计算从切线空间变换到世界空间的变换矩阵,变换矩阵由顶点的切线,副切线和法线在世界空间下表示得到。这种方法需要更多计算,但是在Cubemap进行环境映射情况下用这种方法
代码就是只写 需要修改的那部分了
struct v2f{
//包含了 从切线空间转到 世界空间的变换矩阵
float4 pos :SV_POSITION;
float4 uv: TEXCOORD0;
float4 TtoW0: TEXCOORD1;
float4 TtoW1: TEXCOORD2;
float4 TtoW2: TEXCOORD3;
}
一个插值寄存器 最多只能存float4 大小的变量,矩阵这样的变量,按行拆成多个变盘再进行存储,TtoW0 到TtoW2就是存储了从切线到世界空间的变换矩阵。方向矢量的变换只需要3*3大小的矩阵,所以矩阵用用了float3的存储。充分利用插值寄存器的存储空间,W分量中,我们存储世界空间下的顶点位置。
//顶点着色器
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;
// 分别存入 世界空间的变换矩阵、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); //w向量存了顶点位置
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//获得在切线空间的法线向量
fixed3 bump = UnpackNormal(tex2D(_BumpMap,iuv.zw));
bump.xy *= _BumpScale; //根据高度纹理 变换
bump.z = sqrt(1.0 - saturate(dot(bump.xy , bump.xy)));
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 *albedo;
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);
}
使用内置的UnpackNormal 函数对法线纹理进行采样和解码(需要把法线纹理的格式标识成Normal map )并使用_BumpScale 对其进行缩放。最后,我们使用TtoW0 、TtoW1 和TtoW2存储的变换矩阵把法线变换到世界空间下。这是通过使用点乘操作来实现矩阵的每一行和法线相乘来得到的。
在Unity 4.x 版本中,在不需要使用Cubemap 进行环境映射的情况下, 内置的Unity Shader 使用的是切线空间来进行法线映射和光照计算。而在Unity 5 .x 中, 所有内置的Unity Shader 都使用了世界空间来进行光照计算。这也是为什么Unity 5.x 中表面着色器更容易报错,因为它们使用了更多的插值寄存器来存储变换矩阵(还有一些额外的插值寄存器是用来辅助计算雾效的,
Unity中的法线纹理
图片类型是NormalMap时,可以用内置的UnpackNormal来得到 正确的法线方向。
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
}
纹理类型设置成normal map 后,有一个复选框Create form Grayscale,。 这个方法就是从高度图中生产法线纹理的。高度图本身是纪录相对高度是一张灰度图。白色表示相对更高,黑色表示更低。这样的纹理导入到unity之后,要选择NormalMap 也要勾选这个选项。这样就可以等同法线纹理对待呢。就是一张图既有了法线图也有了高度图。
还会有两个选项 ——Bumpniess 和Filtering。 Bumpiness 控制凹凸程度, Filtering决定使用哪种计算凹凸的程度。 一种是smooth,生成的法线纹理比较平滑,另一种是Sharp,使用Sobel 滤波(一种边缘检测时使用的滤波器)来生成法线。滤波的实现非常简单,我们只需要在一个3x3的滤波器中计算x 和y 方向上的导数,然后从中得到法线即可。
高度图中的每个像素,考虑它与水平方向和竖直方向上的像素差。把它们的差当成该点对应的法线在x和y方向上的位移。然后使用之前提到映射函数存储成到法线纹理的r和g分量
渐变纹理
这种纹理 可以自由的控制漫反射,不同的渐变有不同的特性。轮廓更明显。可以做类似卡通效果。
在左边的图中,我们使用一张从紫色调到浅黄色调的渐变纹理: 而中间的图使用的渐变纹理则和《军团要塞2》中渲染人物使用的渐变纹理是类似的, 它们都是从黑色逐渐向浅灰色靠拢,而且中间的分界线部分微微发红, 这是因为画家在插画中往往会在阴影处使用这样的色调: 右侧的渐变纹理则通常被用于卡迪风格的渲染,这种渐变纹理中的色调通常是突变的, 即没有平滑过渡,以此来模拟卡通中的阴影色块。
Shader "xxxx"{
Properties{
_Color("Color Tint", Color) = (1,1,1,1)
_RampTex("Ramp Tex", 2D) = "white" {} //这个是存储渐变纹理
_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 _RampTex;
float4 _RampTex_ST; //为纹理 定义了他的纹理属性变量
fixed4 _Specular;
float _Gloss
struct a2v{
float4 vertex :POSITION;
float3 normal: NORMAL;
float4 texcoord :TEXCOORD0; //模型的第一组纹理坐标存储到该变量, 而v2f中的用于存储纹理坐标的uv,便于在偏远着色i中使用该坐标进行纹理采样
} ;
struct v2f{
float4 pos: SV_POSITION;
float3 worldNormal: TEXCOORD0;
float3 worldPos :TEXCOORD1;
float2 uv :TEXCOORD2; //为了执行效率 所以 只用了texcoord 的xy值
}
v2f vert(a2v v){
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.wordlNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
//内置的TRANSFORM_TEX 宏来计算经过平铺和偏移后的纹理坐标
return o;
}
fixed4 frag(v2f i): SV_Target{
fixed3 worldNormal = normalize (i. worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBENT.xyz;
//点积做一个缩放在加0.5的偏移这样范围就映射到【0,1】之间
fixed halfLambert = 0.5 *dot (worldNormal, worldLightDir) +0.5;
//halfLambert 构建一个纹理坐标,用这个坐标对_RampTex采样
//由于 _RampTex 实际就是一个一维纹理(它在纵轴方向上颜色不变〉,因此纹理坐标的u 和v 方向我们都使用了halfLambert。然后,把从渐变纹理采样得到的颜色和材质颜色 _Color 相乘 得到反射颜色
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb
fixed3 diffuse = _LightColor0.rgb * diffuseColor;
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 + specular, 1.0);
}
}
}
}
texcoord 语义在a2v 中就是存储纹理坐标比较多。
texcoord0,texcoord1... 这样是使用多重纹理贴图, 也可以用来存储其他动态计算信息。
texcoord 用float4应该是TEXCOORD0寄存器用的32位的,而后面XBOX360多的texcoord2-texcoord5用的是16位的寄存器,这跟硬件有关系,贴图坐标有1维、2维、3维的 使用float4和half4会更好,
比如我们使用最多的就是2维的普通的纹理贴图坐标,uv 分别存在texcoord.xy,texcoord.zw不使用,当然你也可以用texcoord.xyzw存储两个UV坐标,那么读取第二个UV坐标时,你必须先找到第一个的地址,所以为了执行效率浪费点空间是值得的。
片元着色_RampTex 采样这段不是特别理解。
需要注意的是,我们需要把渐变纹理的Wrap Mode 设为Clamp 模式,以防止对纹理进行采样时由于浮点数精度而造成的问题。图7.19给出了Wrap Mode 分别为Repeat 和Clamp 模式的效果对比。
可以看出,左图(使用Repeat 模式〉中在高光区域有一些黑点。这是由浮点精度造成的,当我们使用
fixed2(haItLambert, haltLambert)对渐变纹理进行采样时,虽然理论上halfLambert 的值在[0, 1 ]之间,但可能会奋1.000 01 这样的值出现。如果我们使用的是Repeat 模式,此时就会舍弃整数部分, 只保留小数部分,得到的值就是0.000 01 ,对应了渐变图中最左边的值,即黑色。因此,就会出现图中这样在高光区域反而有黑点的情况。我们只需要把渐变纹理的Wrap Mode 设为Clamp 模式就可以解决这种问题。
遮罩纹理
遮罩纹理(mask texture) 遮罩可以对一下区域进行保护,可以一些地方强烈,一些地方弱。可以用遮罩纹理才控制光照。 另一种应用是在制作地形混用多张图片,例如草地,石头,地面都可以用遮罩纹理控制混合这些纹理。
使用的流程:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值(texel.r)来与某种表面属性进行相乘,这样,当该通道是 0 时,可以保护表面不受该属性影响。使用遮罩纹理可以让美术人员更加精准(像素级)控制模型表现的各种性质。
Shader "Mask Texture"{
Properties{
_Color("Color Tint", Color) = (1,1,1,1)
_MainTex("Main Tex ", 2D) = "white" {}
_BumpMap("Normap Map", 2D) = "bump" {}
_BumpScale("Bump Scale", Float) =1.0
_SpecularMask ("Specular Mask",2D) = "white" {}
_SpecularScale("Specular 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; // 定义Properties 中各属性的变量
sampler2D _MainTex;
float4 _MainTex_ST; //主纹理,法线纹理(_BumpMap)和遮罩纹理共同使用的纹理变量_MainTex_ST
sampler2D _BumpMap;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularScale;
fixed4 _Specular;
float _Gloss;
//顶点 一般就是 位置, 法线,后面的一般根据需要添加切线空间,纹理坐标
struct a2v{
float4 vertex: POSITION;
float3 normal :NORMAL;
////切线空间的确定是通过存储到模型里面的法线和切线确定的
//tangent.w是用来确定切线空间中坐标轴的方向的
float4 tangent :TANGENT; //用切线空间 就需要这个值
float4 texcoord: TEXCOORD0; //存储的纹理坐标,有些地方可以计算或变化后传递给片元
}
struct v2f{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 lightDir: TEXCOORD1;
float3 viewDir: TEXCOORD2;
}
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;
//们对光照方向和视角方向进行了坐标空间的变换,把它们从模型空间变换到了切线空间中,以便在片元着色器中和法线进行光照运算:
TANGENT_SPACE_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);
fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
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 * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
fixed3 halfDir = normalize(tangentLightDir + tangentViewDir);
// Get the mask value
fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;
// Compute specular term with the specular mask
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss) * specularMask;
return fixed4(ambient + diffuse + specular, 1.0);
}
}
}
}
环境光照和漫反射光照和之前使用过的代码完全一样。在计算高光反射时,我们首先对遮罩纹理 _SpecularMask 进行采样。由于本书使用的遮罩纹理中每个纹素的 rgb 分量其实都是一样的,表明了该点对应的高光反射强度, 在这里我们选择使用 r 分量来计算掩码值。然后,我们用得到的掩码值和 _SpecularScale 相乘, 一起来控制高光反射的强度。
需要说明的是,我们使用的这张遮罩纹理其实有很多空间被浪费了一一它的 rgb 分量存储的都是同一个值。在实际的游戏制作中,我们往往会充分利用遮罩纹理中的每一个颜色通道来存储不同的表面属性,我们会在7.4.2 节中介绍这样一个例子
其他遮罩纹理
在真实的游戏制作过程中,遮罩纹理已经不止限于保护某些区域使它们免于某些修改,而是可以存储任何我们希望逐像素控制的表面属性。通常,我们会充分利用一张纹理的RGBA 四个通道,用于存储不同的属性。例如,我们可以把高光反射的强度存储在R 通道,把边缘光照的强度存储在G 通道,把高光反射的指数部分存储在B 通道,最后把自发光强度存储在A 通道。
在游戏《DOTA 2》的开发中,开发人员为每个模型使用了4 张纹理:一张用于定义模型颜色,一张用于定义表面法线,另外两张则都是遮罩纹理。这样,两张遮罩纹理提供了共8 种额外的表面属性,这使得游戏中的人物材质自由度很强,可以支持很多高级的模型属性。读者可以在他们的官网上找到关于《DOTA 2》的更加详细的制作资料,包括游戏中的人物模型、纹理以及制作手册等。这是非常好的学习资料。