第7章 基础纹理
注意:图片的来源基本来自作者冯乐乐的GitHub,感谢作者分享
https://github.com/candycat1992/Unity_Shaders_Book
纹理映射(texture Mapping):使用一张图片控制模型的外观,把图“黏”在模型表面
逐纹理(texel)地控制模型的颜色
注意:这里不是逐像素,逐纹理和逐像素有区别
纹理映射坐标(texture-mapping coordinates):
建模软件,利用纹理展开技术,把纹理映射坐标存储在每个顶点上。
定义了该顶点在纹理中对应的2D坐标
二维变量 (u,v)表示,u:横向坐标 ,v:纵向坐标。
顶点UV坐标 通常 被归一化到 [0,1] 范围内。纹理采样时使用的纹理坐标不一定是在 [0,1]范围内。
Unity 使用的纹理空间符合 OpenGL ,原点位于纹理左下角
使用纹理代替物体的漫反射颜色,使用Blinn-Phong光照模型计算光照:
纹理资源在材质面板上调整属性:
Texture Type:贴图类型。为导入的纹理选择合适的类型,让Unity知道贴图的用处,从而可以让Unity Shader 传递正确的纹理,并且在一些情况下可以让 Unity 对该纹理进行优化。
Alpha From Grayscale:灰度转Alpha。Alpha透明通道的值是否由每个像素的灰度值生成
Wrap Mode:包装方式。当纹理坐标超过 [0,1]范围之后,将会如何被平铺。
Repeat:如果纹理坐标超过1,那么纹理坐标的整数部分会被舍弃,而直接使用小数部分进行采样。(纹理不断重复)
Clamp:如果纹理坐标超过1,那么纹理坐标将会截取到1,如果小于0那么将会截取到0。(截取到边界值,形成一个条形结构)
注意:如果想让纹理得到这样的结果(Repeat 和 Clamp),就必须在Unity Shader中使用纹理的属性(_MainTex_ST变量),对顶点纹理坐标进行相应的变换
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
或者使用 内置函数 TRANSFORM_TEX
o.uv = TRANSFORM_TEX( v.texcoord, _MainTex );
分别使用两种 Wrap Mode,在材质面板中调整纹理的偏移属性(Offset属性决定了纹理坐标的偏移量):
Filter Mode:过滤方式。当纹理由于变换而产生拉伸时,将会采用哪种滤镜。滤波效果提升后消耗的性能依次增大。
纹理滤波影响了放大或者缩小纹理时得到的图片质量。
Point:最近邻(nearest neighbor)滤波。在放大或缩小时采样像素数目通常只有一个,因此图像看起来会有种像素风格的效果。类似棋盘的纹理,像素风就选择Point。
Bilinear:线性滤波。对于每个像素目标会找到4个邻近像素然后对它们进行线性插值混合后得到最终像素,图像看起来像是被模糊。通常会选择Bilinear。
Trilinear:几乎和 Bilinear 一样,但是会在多级渐远纹理之间进行混合。如果一张纹理没有使用多级渐远纹理技术,那么Trilinear得到的结果和Bilinear一样。
1、放大纹理:64×64大小的纹理 贴在 512×512大小的平面上:
随着 Filter Mode 过滤方式的逐渐提升,像素会逐渐模糊不会有太严重的像素风格效果
2、缩小纹理:
原来纹理中的多个像素会对应一个目标像素
纹理缩放更加复杂的原因在于往往需要处理抗锯齿问题:使用 多级渐远纹理(mipmapping)
mip:multum in parvo 在一个小空间中有许多东西
多级渐远纹理技术:提前用滤波处理得到很多更小的图象,形成图象金字塔,每一层都是对上一层图象降采样的结果。
优点:在实时运行时可以快速得到结果像素(当物体远离摄像机时可以直接使用较小的纹理)
缺点:需要使用一定的内存空间(多占用33%)用于存储多级渐远纹理。空间换取时间。
开启多级渐远纹理技术:Generate Mip Maps
会增加纹理的内存占用
纹理最大尺寸 和 纹理模式:
为不同的平台发布游戏,需要考虑目标平台的纹理尺寸和质量纹理,为不同的目标平台选择不同的分辨率
1、如果导入的纹理大小超过了 Max Size 中的设置值,那么Unity 将会把这个纹理缩放为这个最大分辨率。
理想情况:导入的纹理是可以是非正方形的,但是长宽的大小应该是 2的幂(2、4、8、16、32、64 等)。如果使用了非2的幂大小(NPOT)的纹理,那么这些纹理往往会占用更多的内存空间,而且GPU读取这个纹理的速度也会下降。一些平台甚至不支持NPOT纹理,为了解决不支持的问题,Unity在内部会把这个纹理缩放成最近的2的幂大小,这个操作本身是一种性能消耗。出于性能和空间的考虑,应该尽量使用2的幂大小的纹理。
2、Format:在Unity内部使用哪种格式存储纹理。
Advanced:更多的Format选择
使用的纹理格式精度越高(Truecolor 本色),占用的内存空间越大,得到的效果也会越好。
大量的 TrueColor 类型的纹理会让内存迅速增加,因此对于一些不需要使用很高精度的纹理(例如用于漫反射颜色的纹理),应该尽量使用压缩格式。
凹凸映射:纹理的常见应用之一
使用一张纹理来修改模型表面的法线,以便于为模型提供更多的细节。
不会真的改变模型的顶点位置,只是让模型看起来“凹凸不平”
凹凸映射方法:
1、高度映射(height mapping):用一张 高度纹理(height map)模拟表面位移,得到一个修改后的法线值
2、法线映射(normal mapping):使用一张 法线纹理(normal map)直接存储表面法线。
高度纹理(高度图):
高度图 存储了强度值,用于表示模型表面局部的海拔高度(颜色越浅,这个位置的表面越向外凸起,颜色越深,位置越向里凹)
优点:直观。可以从高度图中明确知道一个模型表面的凹凸情况。(程序自动生成大地图,可以直接通过高度纹理的颜色深浅,反推生成大地图的地形)
缺点:计算更加复杂,实时计算不能直接得到表面法线。需要由像素的灰度值计算得到,需要消耗更多的性能。
高度图通常会和法线纹理一起使用,用给出表面凹凸的额外信息。(使用法线映射来修改光照)
法线纹理:
存储了表面的法线方向。
需要做一个映射:法线方向的分量范围在 [-1,1] 之间,像素范围在 [0,1] 之间
所以在 Shader 中对法线纹理进行纹理采样后,需要对结果进行一次反映射的过程(映射函数的逆函数),从而得到原来的法线方向:
注意:方向是相对于坐标空间来说的。
法线纹理中存储的法线方向所在的坐标空间:
1、模型空间的法线纹理(object-space normal map):对于模型顶点自带的法线,法线是定义在模型空间中的。需要将修改后的模型空间中的表面法线存储在一张纹理中。
2、切线空间的法线纹理:模型顶点的切线空间(tangent space)是实际制作中采用的另一种坐标空间,用于 存储法线。模型的每个顶点都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),而y轴可以由法线和切线叉积而得,也被称为 副切线(bitangent,b) 或者 副法线。
模型空间下的法线纹理 和 切线空间下的法线纹理(tangent-space normal map):
使用模型空间存储法线的优点:
1、实现简单,更加直观。
不需要模型原始的法线和切线等信息,计算更少,生成更加简单。
如果是切线空间下的法线纹理,因为模型的切线一般是和UV方向相同,所以想要得到效果比较好的法线映射就要求纹理映射也需要是连续的。
2、在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,可以提供平滑的边界。
因为模型空间下的法线纹理存储的是同一个坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。
如果是切线空间下的法线纹理,法线信息是依靠纹理坐标的方向得到的结果,可能在边缘或者尖锐的部分造成可见的缝合迹象。
使用切线空间存储法线的优点:
切线空间 很多情况下都优于 模型空间,可以节省美术人员的工作
1、自由度高
模型空间下的法线纹理,存储的是绝对法线信息,仅仅可以用于创建它时的模型,而应用到其他模型效果上会完全错误。
切线空间下的法线纹理记录的是相对法线信息,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。
2、可以进行UV动画
切线空间下的法线纹理,可以移动纹理的UV坐标实现凹凸移动的效果,在水或者火山熔岩这种类型的物体上经常用到。
模型空间下的法线纹理,因为存储的是绝对法线信息,仅仅可以用于创建它时的模型,而应用到其他模型效果上会完全错误,所以如果是使用模型空间下的法线纹理,去移动纹理的UV坐标,则会得到完全错误的结果。
3、可以重用法线纹理
切线空间下的法线纹理,一个砖块,可以仅使用一张法线纹理就可以用到所有的6个面上
4、可压缩
切线空间下的法线纹理中法线的Z方向总是正方向,因此可以仅存储XY方向,从而推导得到Z方向。
模型空间下的法线纹理由于每个方向都是有可能的,因此必须存储3个方向的值,不可以压缩。
需要在计算光照模型中统一各个方向矢量所在的坐标空间:
假设使用的是切线空间存储法线,因为法线纹理中存储的法线是切线空间下的方向,因此:
1、在切线空间下进行光照计算:需要把光照方向、视角方向变换到切线空间下
这种方式可以在顶点着色器中就完成对光照方向和视角方向的变换,相比方法2,效率更高
2、在世界空间下进行光照计算:需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算
这种方式因为要先对法线纹理进行采样,所以变换过程必须在片元着色器中实现,因此需要在片元着色器中进行一次矩阵操作。相比方法1,更加通用,因为有时需要在世界空间下的反射方向对 Cubemap 进行采样,如果同时需要进行法线映射时,就需要把法线方向变换到世界空间下。
在切线空间下计算光照模型
1、在片元着色器中通过纹理采样得到切线空间下的法线
2、再与切线空间下的视角方向、光照方向等进行计算
需要在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间中
(即:需要知道从模型空间到切线空间的变换矩阵。
从模型空间到切线空间的变换矩阵的 逆矩阵 = 从切线空间到模型空间的变换矩阵)
从切线空间到模型空间的变换矩阵的计算:
因为一个变换中仅存在平移和旋转变换,那么这个变换的逆矩阵就是它的转置矩阵
即:从切线空间到模型空间的变换矩阵的 转置矩阵 = 从模型空间到切线空间的变换矩阵
转置矩阵即:把切线(x轴)、副切线(y轴)、法线(z轴)的顺序按行排序即可得到
所以:
在顶点着色器中按切线(x轴)、副切线(y轴)、法线(z轴)的顺序按列排序即可得到
3、得到最终光照结果
使用法线纹理:
使用 Bump Scale 属性调整模型的凹凸程度:
在世界空间下计算光照模型
在片元着色器中把法线方向从切线空间变换到世界空间下
1、在顶点着色器中计算从切线空间到世界空间的变换矩阵,并且传递给片元着色器
变换矩阵的计算由顶点的切线、副切线和法线在世界空间下的表示得到
2、在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间(消耗更多的计算,但是更适合在需要使用Cubemap进行环境映射的情况)
Unity中的法线纹理:
当需要使用包含了法线映射的内置的Unity Shader时,就需要 把法线纹理的纹理类型 标识成 Normal map,就可以使用 Unity 的内置函数 UnpackNormal 得到正确的法线方向:
Unity Shader 都使用了 内置的UnpackNormal来进行采样法线方向,因此如果忘记设置 Normal map的情况下,Unity也会在材质面板中提醒。
设置 Normal map 的时候,发生了什么:
1、设置后,Unity会根据不同的平台,对纹理进行不同的压缩。
2、压缩后会通过 UnpackNormal 函数,针对不同的压缩格式对法线纹理进行正确的采样
UnpackNormal 函数的内部实现:
//在某些平台上由于使用了DXT5nm的压缩格式,因此需要针对这种格式对法线进行解码
//在DXT5nm格式的法线纹理中,
纹理的 a通道(即w分量) 对应了法线的 x分量。
纹理的 g通道 对应了法线的 y分量,
纹理的 r和b通道 被舍弃,
法线的 z分量 可以由 xy分量推导得到
为什么之前的普通纹理不能按照 DXT5nm的格式压缩,而法线需要使用 DXT5nm的格式压缩?
注意:法线纹理有两个通道必不可少,第三个通道的值可以通过这两个通道推导得出(法线是单位向量,并且切线空间下的法线方向的z分量始终为正)
如果按照DXT5nm的格式压缩,可以减少法线纹理占用的内存空间
// Unpack normal as DXT5nm (1, y, 1, x) or BC5 (x, y, 0, 1)
// Note neutral texture like "bump" is (0, 0, 1, 1) to work with both plain RGB normal and DXT5nm/BC5
fixed3 UnpackNormalmapRGorAG(fixed4 packednormal)
{
// This do the trick
packednormal.x *= packednormal.w;
fixed3 normal;
normal.xy = packednormal.xy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
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 UnpackNormalmapRGorAG(packednormal);
#endif
}
Create from Grayscale:从高度图中生成法线纹理
Filtering:计算凹凸程度的方式
Smooth:平滑
Sharp:使用 Sobel 滤波(一种边缘检测时使用的滤波器)生成法线
Sobel 滤波,在一个 3×3的滤波器中 计算 x 和 y 方向上的导数,然后从中得到法线
具体方法:对于高度图中的每个像素,考虑它与水平方向和竖直方向上的像素差,把它们的差当成该点对应的法线在 x 和 y 方向上的位移,然后使用之前得到的映射函数存储成法线纹理的 r 和 g 分量即可。
渐变纹理
纹理其实可以存储任何表面属性:使用渐变纹理来控制漫反射光照的结果。
计算漫反射光照:
方式一:使用 表面法线 和 光照方向 的点积结果 与 材质的反射率 相乘 来得到表面的漫反射光照
缺点:无法更加灵活地控制光照结果
方式二:使用 渐变纹理 控制 漫反射光照
自由地控制物体的漫反射光照
不同的渐变纹理有不同的特性,使用不同的渐变纹理控制漫反射光照
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// TRANSFORM_TEX 计算经过平铺和偏移后的纹理坐标
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// Use the texture to sample the diffuse color
//半兰伯特模型:通过对法线方向和光照方向的点积做一次0.5倍的缩放以及一个0.5大小的偏移来计算部分halfLambert
//得到的 halfLambert 范围被映射到 [0,1] 之间
fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
//1、使用 halfLambert 构建纹理坐标,
//2、用这个纹理坐标对 渐变纹理 _RampTex(一维纹理,在纵轴方向上颜色不变,因此纹理坐标的uv方向都使用 halfLambert) 进行采样
//3、从渐变纹理采样得到的颜色 和 材质颜色_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);
//计算 高光反射(specular) 和 环境光(ambient),并且将结果相加
return fixed4(ambient + diffuse + specular, 1.0);
}
注意:Wrap Mode 的 Repeat 模式,在高光区域下的问题
遮罩纹理(mask texture)
保护某些区域,免于修改
1、使用遮罩纹理控制光照:希望高光反射应用到模型表面的所有地方,而模型表面某些区域的反光强烈一些,而某些区域弱一些。可以让美术人员更加精准(像素级别)地控制模型表面的各种性质。
2、使用遮罩纹理控制混合多个纹理:制作地形材质时需要混合多张图片,例如表现草地的纹理、表现石子的纹理、表现裸露土地的纹理等。
遮罩纹理的流程:
1、通过采样得到遮罩纹理的纹理值,
2、使用其中某个(或者某几个)通道的值(例如 texel.r)来与表面属性进行相乘,这样,当该通道为0时,可以保护表面不受该属性的影响
使用一张高光遮罩纹理,逐像素地控制模型表面的高光反射强度:
遮罩纹理不止用于保护某些区域不受修改,而是可以存储任何希望逐像素控制的表面属性:
充分利用一张纹理的RGBA四个通道,不要浪费。在不同的通道中存储不同的属性:
1、高光反射的强度存储在R通道
2、边缘光照的强度存储在G通道
3、高光反射的指数部分存储在B通道
4、自发光强度存储在A通道