Shader入门-------基础纹理
前记:不分行永远滴痛--------------------mx
前置知识:
使用纹理映射技术我们可以吧一张图“黏”在模型表面,逐纹素地控制模型的颜色。
纹理映射坐标:定义了该顶点在纹理中对应的2D坐标,通常这些坐标使用一个二维变量(u,v)表示,期中u是横向坐标,v是纵向坐标。因此纹理坐标通常被称为UV坐标
单张纹理:(实践)
纹理的属性:
Texture Type (纹理类型) :法线纹理一节中会使用Normal map类型,后面还会看到Cubemap等高级纹理类型。当纹理类型设置成Texture后,下面会有一个Alpha from Grayscale复选框,如果勾选了它,那么透明通道的值将会由每个像素的灰度值生成。
Wrap Mode(贴图间拼接模式):它决定当纹理坐标超过[0,1]范围后,将会如何被平铺。Wrap Mode有两种模式:一种Repeat,在这种模式下,如果纹理坐标超过1,那么它的正数部分将会被丢弃,而直接使用小数部分进行采样,这样的结果是纹理将会不断重复;另一种是Clamp,在这种模式上如果纹理坐标大于1,那么会截取到1,如果大于0,那么将会截取到0;代码中需要包含类似下面的代码:
Filter Mode(过滤模式):它决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式。Filter Mode支持3种模式:Point,Bilinear,Trilinear。它们得到的滤波效果依次提升,但是需要耗费的性能也依次增大。纹理滤波会影响放大或缩小纹理时得到的图片质量。
多级渐远纹理(mipmapping)技术:多级渐远纹理技术提前用滤波处理来得到很多更小的图像,形成了一个图像金字塔,每一层都说对上一层图像降采样的结果。这样在实时运行时,就可以快速得到结果像素。但是缺点是需要使用一定空间用于储存这些多级渐远纹理,通常会多占用33%的内存空间。在Unity中,我们可以在纹理导入面板中,首先将纹理类型选择成Advanced,再勾选Generate Mip Map即可开启多级渐远纹理技术
Point模式使用最近邻滤波,在放大缩小时,它的采样像素数目通常只有一个。Bilinear滤波则使用了线性滤波,对于每个目标像素,它会找到四个邻近的像素,然后对他们进行线性插值混合后得到最终像素。而Thrilinear几乎和Bilinear一样,还会在多级渐远纹理之间混合。如果一张纹理没有使用多级渐远纹理技术,那么Thrilinear得到的结果和Bilinear一样。
mx
纹理的最大尺寸和纹理模式:如果导入的纹理大小超过了Max Texture Size中设置的值,那么Unity将会把这个纹理缩放为这个最大分辨率。理想情况下,导入的纹理可以是非正方形的,但是长宽应该是2的幂。如果使用了非2的幂大小,那么这些纹理往往会占用更多的内存空间,而且GPU读取该纹理的速度也会下降,有些平台甚至不支持这种NPOT纹理。
Format(格式):决定了Unity会以哪种格式来存储该纹理。使用的纹理格式精度越高,效果也就越好,但是占用内存也会越大。我们可以从纹理导入面板最下方看到储存该纹理需要占用的内存空间。
凹凸映射:凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。
凹凸映射的方法:
一:高度映射----使用一张高度纹理来模拟表面位移,然后得到一个修改后的法线值
二:法线纹理----使用一张法线纹理来直接储存表面法线
高度纹理:使用一张高度图来实现凹凸映射。高度图中储存的是强度值,用于表示模型表面局部海拔高度,颜色越浅表明该位置的表面越向外凹起,而颜色越深表明该位置越向里凹。我们可以从高度图中明确知道一个模型表面的凹凸情况,但是缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要有像素的灰度值计算得到,因此需要消耗更多的性能。高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息。也就是说我们通常使用法线映射来修改关照。
法线纹理:法线纹理中储存的就是表面的法线方向,由于法线方向的分量范围在[-1,1],而像素的范围为[0,1],因此我们需要做一个映射,通常使用的映射是
。这就要求我们在Shader中对纹理法线进行纹理采样之后,还需要对结果进行一次反映射的过程,以得到原先的法线方向。反映射的过程实际就是使用上面映射函数的逆函数:
模型空间的法线纹理:对于模型顶点自带的法线,它们是定义在模型空间中的,因此一种直接的想法是将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为是模型空间的法线纹理
切线空间的法线纹理:在实际制作中,我们往往使用另一种坐标空间,即模型顶点的切线空间来存储法线。对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是顶点本身,而z轴是顶点的法线方向(n),x轴是切线方向(t),而y轴可以由法线和切线叉积得到,也称为是副切线(bitangent,b)或副法线。这种纹理被称为切线空间的法线纹理。
模型空间存储法线的优点:
实现简单,更加直观。我们甚至都不需要模型原始的法线和切线等信息,也就是说,计算更少。生成它也非常简单,而如果要生成切线空间下的法线纹理,由于模型的切线一般是和UV方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的
在纹理坐标的缝合处和尖锐的边角部分,可见的突变(缝隙)较少,即可以提供平滑的边界。这是因为模型空间下的法线纹理存储的是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。而切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象。
切线空间存储法线的优点:
*度很高。模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误了。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。
可进行UV动画。比如,我们可以移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果。原因同上。这种UV动画在水或者火山熔岩这种类型的物体上会经常用到。可以重用法线纹理。比如,一个砖块,我们仅使用一张法线纹理就可以用到所有的6个面上。原因同上。
可压缩。由于切线空间下的法线纹理中法线的Z方向总是正方向,因此我们可以仅存储XY方向,而推导得到Z方向。而模型空间下的法线纹理由于每个方向都是可能的,因此必须存储3个方向的,不可压缩
在切线空间下的计算
(注意:由于我们使用了两张纹理,因此需要存储两个纹理坐标。为此,我们把v2f中的uv变量的类型定义为float4类型,其中xy分量存储了_MainTex的纹理坐标,而zw分量存储了_BumpMap的纹理坐标(实际上, _MainTex和_BumpMap通常会使用同一组纹理坐标,出于减少插值寄存器的使用数目的目的,我们往往只计算和存储一个纹理坐标即可)。然后,我们把模型空间下切线方向、副切线方向和法线方向按行排列来得到从模型空间到切线空间的变换矩阵rotation。需要注意的是,在计算副切线时我们使用v.tangent.w和叉积结果进行相乘,这是因为和切线与法线方向都垂直的方向有两个,而w决定了我们选择其中哪一个方向。Unity也提供了一个内置宏TANGENT_SPACE _ROTATION (在UnityCGcginc中被定义)来帮助我们直接计算得到rotation变换矩阵,它的实现和上述代码完全一样。然后,我们使用Unity的内置函数ObjSpaceLightDir和ObiSpaceViewDir来得到模型空间下的光照和视角方向,再利用变换矩阵rotation把它们从模型空间变换到切线空间中。)
(注意:在上面的代码中,我们首先利用tex2D对法线纹理_BumpMap进行采样。正如本节一开头所讲的,法线纹理中存储的是把法线经过映射后得到的像素值,因此我们需要把它们反映射回来。如果我们没有在Unity里把该法线纹理的类型设置成Normal map ,就需要在代码中手动进行这个过程。我们首先把packedNormal的xy分量按之前提到的公式映射回法线方向,然后乘以_BumpScale (控制凹凸程度)来得到tangentNormal的xy分量。由于法线都是单位矢量,因此tangentNormal.z分量可以由tangentNormal.xy计算而得。由于我们使用的是切线空间下的法线纹理,因此可以保证法线方向的z分量为正。在Unity中,为了方便Unity对法线纹理的存储进行优化,我们通常会把法线纹理的纹理类型标识成Normal map,Unity会根据平台来选择不同的压缩方法。这时,如果我们再使用上面的方法来计算就会得到错误的结果,因为此时_BumpMap的rgb分量并不再是切线空间下法线方向的xyz值了。在这种情况下,我们可以使用Unity的内置函数UnpackNormal来得到正确的法线方向。)
mx
在世界空间下的计算
Unity中的法线纹理类型
在DXT5nm格式的法线纹理中,纹素的a通道(即w分量)对应了法线的x分量,g通道对应了法线的y分量,而纹理的r和b通道则会被舍弃,法线的z分量可以由xy分量推到而得。
当我们把纹理而理性设置成Normal map后还有一个复选框是Create from Grayscale,这个复选框就是用于从高度图中生成法线纹理的。高度图本身记录的是相对高度,是一张灰度图,白色表示相对更高,黑色表示相对更低。当勾选Create from Grayscale之后还多了两个选项Bumpiness和Filtering,其中Bumpiness用于控制凹凸程度,而Filtering决定我们使用哪种方式来技术按凹凸程度,它有两种选项:一种是Smooth,这使得生成后的法线纹理会比较平滑;另一种是Sharp,它会使用Sobel滤波(一种边缘检测时使用的滤波器)来生成法线,Sobel滤波实现非常简答,我们只要在一个3X3的滤波器中计算x和y方向上的导数,然后从中得到法线即可。具体方法是:对于高度图中的每个像素,我们考虑它与水平方向和竖直方向上的像素差,把它们的差当成该点对应的法线在x和y方向上的位移,然后使用之前提到的映射函数存储成到法线纹理的r和g分量即可。
mx
渐变纹理:
(没找到合适的纹理图,别骂了别骂了)
(注意:我们需要把渐变纹理的Wrap Mode设为Clamp的模式,以防止对纹理采样时由于浮点数精度而造成的问题)
遮罩纹理:
(注意:我们为主纹理_MainTex、法线纹理_BumpMap和遮罩纹理_SpecularMask定义了它们共同使用的纹理属性变量_MainTex_ST,这意味着,在材质面板中修改主纹理的平铺系数和偏移系数会同时影响3个纹理的采样。使用这种方式可以让我们节省需要存储的纹理坐标数目,如果我们为每一个纹理都使用一个单独的属性变量_TextureName_ST,那么随着使用的纹理数目的增加,我们会迅速占满顶点着色器中可以使用的插值寄存器。而很多时候,我们不需要对纹理进行平铺和位移操作,或者很多纹理可以使用同一种平铺和位移操作,此时我们就可以对这些纹理使用同一个变换后的纹理坐标进行采样。)
(注意:在计算高光反射时,我们首先对遮罩纹理_SpecularMask进行采样。由于示例中使用的遮罩纹理中每个纹素的rgb分量其实都是一样的,表明了该点对应的高光反射强度,在这里我们选择使用r分量来计算掩码值。然后,我们用得到的掩码值和_SpecularScale相乘,一起来控制高光反射的强度。需要说明的是,我们使用的这张遮罩纹理其实有很多空间被浪费了----它的rgb分量存储的都是同一个值。在实际的游戏制作中,我们往往会充分利用遮罩纹理中的每一个颜色通道来存储不同的表面属性)
-------------------------------mx