@函数
纹理最初的目的就是使用一张图片来控制模型的外观。使用纹理映射(texture mapping)技术,咱们能够把一张图“黏”在模型表面,逐纹素(texel)(纹素的名字是为和像素进行区分)地控制模型的颜色。
在美术人员建模的时候,一般会在建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates)存储在每一个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。一般,这些坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,而v是纵向坐标。所以,纹理映射坐标也被称为uv坐标。
尽管纹理的大小能够是多种多样的,例如能够是256×256或者1024×1024,但顶点UV坐标的范围一般都被归一化到[0,1]范围内。须要注意的是,纹理采样时使用的纹理坐标不必定是在[0,1]范围内。实际上这种不在[0,1]范围内的纹理坐标有时会很是有用。与之关系紧密的是纹理的平铺模式,它将决定渲染引擎遇到不在[0,1]范围内的纹理坐标时如何进行纹理采样。
Unity使用的纹理空间是符合OpenGl传统的,也就是说,原点位于纹理左下角,以下图所示:
须要提醒读者注意的是,本章着重描述纹理采样的原理,所以实现的Shader每每并不能直接应用到实际的项目中(直接使用的话会缺乏阴影、光照衰减等效果)。咱们会在后面给出包含了纹理采样和完整光照模型的可真正使用的UnityShader。性能
咱们一般会使用一张纹理来代替物体的漫反射颜色。本节中,咱们将学习如何在Unity Shader中使用单张纹理来模拟颜色。在学习完本节后,咱们会获得相似于下图的效果:
学习
(1)为了使用纹理,咱们须要在Properties语义块中添加一个纹理属性:优化
Properties{ _Color("Color Tint",Color)= (1,1,1,1) _MainTex("Main Tex",2D) = "white" {} _Specular("Specular", Color) = (1,1,1,1) _Gloss("Gloss",Range(8.0,256)) = 20 }
上面的代码声明了一个名为_MainTex的纹理,在之前,咱们已经知道2D是纹理的声明方式。咱们使用一个字符串后跟一个花括号做为它的初始值。“white”是内置的纹理的名字,也就是一个全白的纹理。为了控制物体的总体色调,咱们还声明了一个_Color属性。
(2)而后,咱们在SubShader语义块中定义了一个Pass语义块。并且咱们在Pass的第一行指明了该Pass的光照模式:ui
SubShader{ Pass{ Tags{"LightMode"="ForwardBase"} } }
LightMode标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色。
(3)接着咱们使用CGPROGRAM和ENDCG来包围住Cg代码片断,以定义最重要的顶点着色器和片元着色器代码。首先,咱们使用#pragma指令来告诉Unity,咱们定义的顶点着色器和片元着色器叫什么名字,在本例中,它们的名字分别是vert和frag:3d
CGPROGRAM #pragma vertex vert #pragma fragment frag
(4)为了使用Unity内置的一些变量,如_LightColor0,还须要包含进Unity的内置文件Lighting.cginc:rest
#include "Lighting.cginc"
(5)咱们须要在Cg代码片断中声明和上述属性相匹配的变量,以便和材质面板中的属性创建联系:code
fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Specular; float _Gloss;
与其它属性类型不一样的是,咱们还须要为纹理类型的属性声明一个float4类型的变量_MainTex_ST。其中,_MainTex_ST的名字不是任意起的。在Unity中,咱们须要使用纹理名_ST的方式来声明某个纹理的属性。其中ST是缩放(scale)和平移(translation)的缩写。_MainTex_ST可让咱们获得该纹理的缩放和平移(偏移)值,_MainTex_ST.XY存储的是缩放值,而_MainTex_ST.zw存储的是偏移值。这些值能够在材质面板的纹理属性中调节,以下图所示:
(6)接下来,咱们须要定义顶点着色器的输入和输出结构体:orm
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; };
在上面代码中,咱们首先在a2v结构体中使用TEXCOORD0语义声明了一个新的变量texcoord,这样Unity就会将模型的第一组纹理坐标存储到该变量中。而后,咱们在v2f结构体中添加了用于存储纹理坐标的uv,以便在片元着色器中使用该坐标进行纹理采样。
(7)而后咱们定义了顶点着色器:
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; //Or just call the built-in function //o.uv = TRANSFORM_TEX(v.texcoord,_MainTex); return o; }
在顶点着色器中,咱们使用纹理属性值_MainTex_ST来对顶点纹理坐标进行变换,获得最终的纹理坐标。计算过程是,首先使用缩放属性_MainTex_ST.xy对顶点纹理坐标进行缩放,而后再使用偏移属性_MainTex_ST.zw对结果进行偏移。Unity提供了一个内置宏TRANSFORM_TEX来帮咱们计算上述过程。TRANSFORM_TEX是在UnityCG.cginc中定义的:
//Transform 2D UV by scale/bias property #define TRANSFORM_TEX(tex,name) (tex.xy*name##__ST.xy+name##_ST.zw)
它接受两个参数,第一个参数是顶点纹理坐标,第二个参数是纹理名,在它的实现中,将利用纹理名_ST的方式来计算变换后的纹理坐标。
(8)咱们还须要实现片元着色器,并计算漫反射时使用纹理中的纹素值:
fixed4 frag(v2f i):SV_Target{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); //Use the texture to sample the diffuse color 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,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); }
上面的代码首先计算了世界空间下的法线方向和光照方向。而后,使用Cg的tex2D函数对纹理进行采样。它的第一个参数是须要被采样的纹理,第二个参数是一个float2类型的纹理坐标,它将返回计算获得的纹素值。咱们使用采样结果和颜色属性_Color的乘积来做为材质的反射率albedo,并把它和环境光相乘获得环境光部分。随后,咱们使用albedo来计算漫反射光照的结果,并和环境光照、高光反射光照相加后返回。
(9)最后,咱们为该Shader设置了合适的Fallback:
Fallback"Specular"
虽然不少资料把Unity的纹理映射描述的很简单——声明一个纹理变量,再使用tex2D函数采样。实际上,在渲染流水线中,纹理映射的实现远比咱们想象的复杂。本文不会过多的涉及一些具体的实现细节,但要解释一些咱们认为读者必需要知道的事情。在本节中,咱们将关注Unity中的纹理属性。
在咱们向Unity中导入一张纹理资源后,能够在它的材质面板上调整其属性,以下图所示。
纹理面板中的第一个属性是纹理类型。在本节中,咱们使用的是Texture属性,在下面的法线纹理一节中,咱们会使用Normal map类型。而在后面的章节中,咱们还会看到Cubemap等高级纹理类型。咱们之因此要为导入的纹理选择合适的类型,是由于只有这样才能让Unity知道咱们的意图,为Unity Shader传递正确的纹理,并在一些状况下可让Unity对该纹理进行优化。
当把纹理类型设置为Texture后,下面会有一个Alpha from Grayscale 复选框。若是勾选了它,那么透明通道的值将会由每一个像素的灰度值生成。关于透明效果,咱们会在下一章讲到,在这里咱们不须要勾选它。
下面一个属性很是重要——Wrap Mode。它决定了当纹理坐标超过[0,1]范围后将如何平铺。Wrap Mode有两种模式:一种是Repeat,在这种模式下,若是纹理坐标超过了1,那么它的整数部分将会被舍弃,而直接使用小数部分进行采样,这样的结果是纹理将会不断重复;另外一种是Clamp,在这种模式下,若是纹理坐标大于1,那么将会截取到1,若是小于0,那么将会截取到0。下图给出了两种模式下平铺一张纹理的效果。
上图展现了在纹理的平铺(Tiling)属性为(3,3)时分别使用两种Wrap Mode的结果。做图使用了Repeat模式,在这种模式下纹理将会不断重复;右图使用了Clamp模式,在这种模式下超过范围的部分将会截取到边界值,造成一个条形结构。
须要注意的是,想要让纹理获得这样的效果,咱们必须使用纹理的属性(例如上面的_MainTex_ST变量)在Unity Shader中对顶点坐标进行相应的变换。也就是说,代码中须要包含相似下面的代码:
o.uv = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw; //Or just call the bulit-in function o,uv = TRANSFORM_TEX(v.texcoord,_MainTex);
咱们还能够在材质面板中调整纹理的偏移量,下图给出了两种模式下调整纹理偏移量的一个例子:
上图展现了在纹理的偏移属性为(0.2,0.6)时分别使用两种Wrap Mode的结果,左图使用了Repeat模式,右图使用了Clamp模式。
纹理导入面板的下一个属性是Fliter Mode属性,它决定了当纹理因为变换而产生拉伸时将会采用哪一种滤波模式。Fliter Mode支持3种模式:Point,Bilinear以及Trilinear。它们获得的图片滤波效果依次提高,但须要耗费的性能也依次增大。纹理滤波会影响放大或缩小纹理时获得的图片质量。例如,当咱们把一张64×64大小的纹理贴在一个512×512大小的平面上时,就须要放大纹理。下图给出了3种滤波模式下的放大结果。
纹理缩小的过程比放大更加复杂一些,此时原纹理中的多个像素会对应一个目标像素。纹理缩小更加复杂的缘由在于咱们每每须要处理抗锯齿问题,一个最经常使用的方法就是多级渐远纹理(mipmapping)技术,其中“mip”是拉丁文“multum in parvo”的缩写,它的意思是在一个小空间有许多东西。如同它的名字,多级渐远纹理技术将原纹理提早用滤波处理来获得不少更小的图像,造成了一个图像金字塔,每一层都是对上一层图像降采样的结果。这样在实时运行时,就能够快速获得结果像素,例如当物体远离摄像机时,就能够直接使用较小的纹理。但缺点是须要使用必定的空间用于存储这些多级渐远纹理,一般会多占用33%的内存空间,这是一种典型的空间换取时间的方法。在Unity中,咱们能够在纹理导入面板时,首先将纹理类型(Texture Type)选择成Advanced,再勾选Generate Mip Maps便可开启多级渐远纹理技术。同时,咱们也能够选择生成多级渐远纹理时是否使用线性空间(用于伽玛校订)以及采用的滤波器等。以下图所示:
下图给出了从一个倾斜的角度观察一个网格结构的地板时,使用不一样Filter Mode(同时也使用了多级渐远纹理技术)获得的效果。
在内部实现上,Point模式使用了最近邻(nearest neighbor)滤波,再放大或缩小时,它的采样像素数目一般只有一个,一次图像看起来会有种像素风格的效果。而Bilinear滤波则使用了线性滤波,对于每一个目标像素,他都会找到4个临近像素,而后对它们进行线性插值混合后获得最终像素,所以图像看起来被模糊了。而Trilinear滤波几乎是和Bilinear同样的,只是Trilinear还会在多级渐远纹理之间进行混合。若是一张纹理没有使用多级渐远纹理技术,那么Trilinear获得的结果是和Bilinear就是同样的。一般咱们会选择Bilinear滤波模式。须要注意的是,有时咱们不但愿纹理看起来是模糊的,例如对于一些相似棋盘的纹理,咱们但愿它就是像素风的,这时咱们可能会选择Point模式。
最后,咱们来说一下纹理的最大尺寸和纹理模式。当咱们在为不一样平台发布游戏时,须要考虑目标平台的纹理尺寸和质量问题。Unity容许咱们为不一样目标平台选择不一样的分辨率,以下图所示:
若是导入的纹理大小超过了Max Texture Size中的设置值,那么Unity将会把改纹理缩放为这个最大分辨率。理想状况下,导入的纹理能够是非正方形的,但长款的大小应该是2的幂,例如2,4,8,16,32,64等。若是使用了非2的幂大小(Non Power ofTwo,NPOT)的纹理,那么这些纹理每每会占用更多的内存空间,并且GPU读取改纹理的速度也会有所降低。有些平台甚至不支持这种NPOT纹理,这时Unity在内部会把它缩放成最近的2的幂的大小。出于性能和空间考虑,咱们应尽可能使用2的幂大小的纹理。
而Format决定了Unity内部使用哪一种格式来存储该纹理。若是咱们将Texture Type设置为Advanced,那么会有更多的Format供咱们选择。这里再也不依次介绍每种纹理模式,但须要知道的是,使用的纹理格式精度越高(例如使用Turecolor),占用的内存空间越大,获得的效果也越好。咱们能够从纹理导入面板的最下方看到存储该纹理须要占用的内存空间(若是开启了多级渐远纹理技术,也会增长纹理的内存占用)。当游戏中使用了大量Truecolor类型的纹理时,内存可能会迅速增长,所以对于一些不须要使用很高精度的纹理(例如用于漫反射颜色的纹理),咱们应该尽可能使用压缩格式。
.