纹理映射
纹理最初的目的就是使用一张图片来控制模型的外观。使用纹理映射(texture mapping)技术,我们可以把一张图“黏”在模型表面,逐纹素(texel)(纹素的名字是为了和像素进行区分)地控制模型的颜色。
在美术人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates) 存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D 坐标。通常,这些坐标使用一个二维变量(u, v)来表示,其中u是横向坐标,而v是纵向坐标。因此,纹理映射坐标也被称为UV坐标。
尽管纹理的大小可以是多种多样的,例如可以是256×256或者1028×1028,但顶点UV坐标的范围通常都被归一化到[0,1]范围内。需要注意的是,纹理采样时使用的纹理坐标不一定是在[0,1]范围内。实际上,这种不在[0,1]范围内的纹理坐标有时会非常有用。与之关系紧密的是纹理的平铺模式,它将决定渲染引擎在遇到不在[0,1]范围内的纹理坐标时如何进行纹理采样。
纹理采样就是漫反射系数值设定为纹理
普通纹理映射
由像素的位置算出uv的值,得到对应的纹理值,纹理作为漫反射系数
纹理的像素为纹素
物体表面的每个点的k_d属性表示纹理颜色等等,是漫反射系数
三维物体的表面是2d的,所以物体的纹理可以由图片来表示
空间的三角形映射到纹理的三角形关系由Artist实现,或者自动化,我们不需要管
纹理用uv表示坐标系
Unity 单张纹理采样的实现
Shader "TurBoQShader/Chapter7/ST(单张纹理加BP模型)"
{
Properties
{
_Color("Color Tint",Color)=(1,1,1,1)//控制纹理的色调
_MainTex("Main Tex",2D)="white" {}//2D是纹理属性的声明方式
_Specular("Specular",Color)=(1,1,1,1)//高光反射的颜色
_Gloss("Gloss",Range(8.0,256))=20//高光区域的大小
}
SubShader
{
Pass{
//指定该Pass的光照模式,LightMode属于Pass标签
Tags { "LightMode"="ForwardBase" }
CGPROGRAM//包裹CG代码片
//顶点着色器名称
#pragma vertex vert
//片元着色器名称
#pragma fragment frag
//为了使用Unity内置的变量,如_LightColor0,引入内置文件
#include "Lighting.cginc"
//CG代码中需要和Unity面板对应的属性
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;//纹理的缩放和平移
fixed4 _Specular;
float _Gloss;
//顶点着色器的输入结构体a2v,顶点着色器的输出结构体和片元着色器的输入结构体v2f
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=UnityObjectToClipPos(v.vertex);
o.worldNormal=UnityObjectToWorldNormal(v.normal);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
// o.uv=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
//在顶点着色器中,我们使用纹理的属性值_MainTex_ST来对顶点纹理坐标进行变换,得到最终的纹理坐标。
//计算过程是,首先使用缩放属性_MainTex_ST.xy对顶点纹理坐标进行缩放,然后再使用偏移属性_MainTex_ST.zw
//对结果进行偏移。Unity 提供了一个内置宏TRANSFORM_TEX来帮我们计算上述过程。TRANSFORM_TEX是在 UnityCGcginc中定义的:
//它接受两个参数,第一个参数是顶点纹理坐标,第二个参数是纹理名,在它的实现中,将利用纹理名_ST的方式来计算变换后的uv坐标。
o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);//使用缩放和偏移计算纹理,v.texcoord是材质的顶点纹理坐标,_MainTex是2D图片的名称
return o;
}
fixed4 frag(v2f i):SV_Target{//片元着色器代码,负责计算高光反射和漫反射,漫反射中加入纹理的uv坐标
fixed3 worldNormal=normalize(i.worldNormal);//normalize归一化世界法线
//fixed3 worldLigthDir=normalize(_WorldSpaceLightPos0.xyz);//normalize截取前三行三列光源方向
fixed3 worldLigthDir=normalize(UnityWorldSpaceLightDir(i.worldPos));//世界光照方向
//纹理进行采样。它的第一个参数是需要被采样的纹理,第二个参数是一个float2类型的纹理坐标,它将返回计算得到的纹素值。
fixed3 albedo=tex2D(_MainTex,i.uv).rgb*_Color.rgb;//纹素计算
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*albedo;//世界光照乘纹素
fixed3 diffuse=_LightColor0.rgb*albedo*max(0,dot(worldNormal,worldLigthDir));//漫反射=入射光颜色强度*材质的漫反射系数*dot(光源方向,表面法线),乘世界光照和纹素
// fixed3 reflectDir=normalize(reflect(-worldLigthDir,worldNormal));
//fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
fixed3 viewDir=normalize(UnityWorldSpaceViewDir(i.worldPos));//视角方向
fixed3 halfDir=normalize(worldLigthDir+viewDir);//normalize归一化
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);//高光反射=入射光颜色强度*材质的高光反射系数*dot(视角方向,
//反射方向(由表面法线和光源方向求得))
return fixed4(ambient+diffuse+specular,1.0);
}
ENDCG
}
}
FallBack "Specular"
}
Unity中纹理的属性
纹理太小的解决方案
Unity的处理方式(最近邻插值,双线性插值,双三次插值)
在内部实现上,Point模式使用了最近邻(nearest neighbor)滤波,在放大或缩小时,它的采样像素数目通常只有一个,因此图像会看起来有种像素风格的效果。而 Bilinear滤波则使用了线性滤波,对于每个目标像素,它会找到4个邻近像素,然后对它们进行线性插值混合后得到最终像素,因此图像看起来像被模糊了。而 Trilinear滤波几乎是和 Bilinear一样的,只是 Trilinear还会在多级渐远纹理之间进行混合。如果一张纹理没有使用多级渐远纹理技术,那么Trilinear得到的结果是和 Bilinear就一样的。通常,我们会选择Bilinear滤波模式。需要注意的是,有时我们不希望纹理看起来是模糊的,例如对于一些类似棋盘的纹理,我们希望它就是像素风的,这时我们可能会选择Point模式。
图形学的处理方式
双线性插值
纹理图太小,想要像素平滑过渡的解决方法
邻近四个点做插值
水平插值结束后竖直插值
Bicubic双三次插值
函数f在点 (x,y) 的值可以通过矩形网格中最近的十六个采样点的[加权平均]得到,在这里需要使用两个多项式插值三次函数,每个方向使用一个。
纹理太大的解决方案
Unity的处理方式
纹理缩小的过程比放大更加复杂一些,此时原纹理中的多个像素将会对应一个目标像素。纹理缩放更加复杂的原因在于我们往往需要处理抗锯齿问题,一个最常使用的方法就是使用多级渐远纹理(mipmapping)技术。其中“mip”是拉丁文“multum in parvo”的缩写,它的意思是“在一个小空间中有许多东西”。如同它的名字,多级渐远纹理技术将原纹理提前用滤波处理来得到很多更小的图像,形成了一个图像金字塔,每一层都是对上一层图像降采样的结果。这样在实时运行时,就可以快速得到结果像素,例如当物体远离摄像机时,可以直接使用较小的纹理。但缺点是需要使用一定的空间用于存储这些多级渐远纹理,通常会多占用33%的内存空间。这是一种典型的用空间换取时间的方法。在Unity中,我们可以在纹理导入面板中,首先将纹理类型(TextureType)选择成Advanced,再勾选Generate Mip Maps 即可开启多级渐远纹理技术。同时,我们还可以选择生成多级渐远纹理时是否使用线性空间(用于伽玛校正,详见18.4.2.节)以及采用的滤波器等,如图7.8所示。
图形学的处理方式
离摄像机远的像素,像素里面覆盖(包含)的纹理太多,一个纹素代表不了纹理的结果
超采样(增加像素数量)
求范围的平均值,Mipmap
需要原纹理图的三分之一空间存储
点的右边和上边投影到纹理空间,求对应的长度最大值,作为纹理空间的边长,边长决定去第几层mipmap找像素,立即找到区域的平均值。
不平滑用插值来解决,比如1.8层,在1和2层做双线性插值,再做插值,叫做三线性插值
各项异性过滤
解决三线性插值以及mipmap对于不是正方形的计算不准确的问题
mipmap是对角线
解决长条和矩形区域问题
重心坐标(三角形顶点属性知道,其他通过插值得到)
三角形内的点三个系数都是非负的
A点重心坐标
重心坐标求法
重心的重心坐标(0.3,0.3,0.3)
属性通过重心坐标插值
纹理模式
纹理应用
凹凸映射
纹理的另一种常见的应用就是凹凸映射(bump mapping)。凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。
有两种主要的方法可以用来进行凹凸映射:一种方法是使用一张==高度纹理(height map)来模拟表面位移(displacement),然后得到一个修改后的法线值,这种方法也被称为高度映射(heightmapping);另一种方法则是使用一张法线纹理(normal map)==来直接存储表面法线,这种方法又被称为法线映射(normal mapping)。尽管我们常常将凹凸映射和法线映射当成是相同的技术,但读者需要知道它们之间的不同。
高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息。也就是说,我们通常会使用法线映射来修改光照。
纹理定义点相对于自己模型的相对高度,应用导致法线变换,最后导致光照结果出现变换
橙色为凹凸贴图存的内容
先算切线再逆时针旋转90度
现代位移贴图,会按照橙色线变换三角形顶点的位置
纹理图记录其他计算结果
在真实的游戏制作过程中,遮罩纹理已经不止限于保护某些区域使它们免于某些修改,而是可以存储任何我们希望逐像素控制的表面属性。通常,我们会充分利用一张纹理的RGBA四个通道,用于存储不同的属性。例如,我们可以把高光反射的强度存储在R通道,把边缘光照的强度存储在G通道,把高光反射的指数部分存储在B通道,最后把自发光强度存储在A通道。
在游戏《DOTA2》的开发中,开发人员为每个模型使用了4张纹理:一张用于定义模型颜色,一张用于定义表面法线,另外两张则都是遮罩纹理。这样,两张遮罩纹理提供了共8种额外的表面属性,这使得游戏中的人物材质自由度很强,可以支持很多高级的模型属性。读者可以在他们的官网上找到关于《DOTA2》的更加详细的制作资料,包括游戏中的人物模型、纹理以及制作手册等。这是非常好的学习资料。
光照纹理
环境光照可以用纹理保存
遮罩纹理
遮罩纹理(mask texture)是本章要介绍的最后一种纹理,它非常有用,在很多商业游戏中都可以见到它的身影。那么什么是遮罩呢?简单来讲,遮罩允许我们可以保护某些区域,使它们免于某些修改。例如,在之前的实现中,我们都是把高光反射应用到模型表面的所有地方,即所有的像素都使用同样大小的高光强度和高光指数。但有时,我们希望模型表面某些区域的反光强烈一些,而某些区域弱一些。为了得到更加细腻的效果,我们就可以使用一张遮罩纹理来控制光照。另一种常见的应用是在制作地形材质时需要混合多张图片,例如表现草地的纹理、表现石子的纹理、表现裸露土地的纹理等,使用遮罩纹理可以控制如何混合这些纹理。
使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值(例如 texel.r)来与某种表面属性进行相乘,这样,当该通道的值为1时,可以保护表面不受该属性的影响。总而言之,使用遮罩纹理可以让美术人员更加精准(像素级别)地控制模型表面的各种性质。