使用纹理映射(texture mapping)技术逐纹素(texel)(纹素是为了和像素区分)地控制颜色。
建模软件中可以利用纹理展开技术吧纹理映射坐标(texture-mapping cooridiantes)存储在每个顶点上。纹理映射坐标定义该顶点在纹理中对应的2D坐标。这些坐标使用一个二维变量(u,v)来表示,u是横向坐标,v是纵向坐标。
UV坐标范围通常被归一化到[0,1]范围内,而OpenGl的起点在左下角,DerectX11则在左上角。在Unity中则通常只有一种坐标系,其纹理空间符合OpenGL的传统,原点在左下角。
实践:
[1]构建场景
[2]新建材质与Unity shader,并把该Unity shader赋给该材质
[3]创建一个胶囊体
[4]保存场景,并给该shader编码
代码:
Shader "Unity Shaders Book/Chapter7/Single Texture"{ //shader命名 Properties{ //声明属性 _Color("Color Tint", Color) = (1, 1, 1, 1) //为控制物体的整体色调,从而声明_Color属性 _MainTex("Main Tex", 2D) = "white"{} //声明名为_MainTex的纹理,white是内置纹理的名字,也是全白纹理 _Specular("Specular", Color) = (1, 1, 1, 1) _Gloss("Gloss", Range(8.0, 256)) = 20 } SubShader{ Pass{ Tags {"LightMode" = "ForwardBase"} //标签 CGPROGRAM //开始cg代码块 #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; //定义颜色 sampler2D _MainTex; //定义纹理 float4 _MainTex_ST //值得注意的这里,这里需要为纹理类型的属性声明一个float4类型的变量, //它的名字命名格式为纹理名_ST来声明某个纹理的属性,ST是缩放scale和移动translation缩写 //通过该变量,可以得到该纹理的缩放和平移值,_MainTex_ST.xy存放的是缩放,_MainTex_ST.zw存放的是偏移,以上值可通过材质面板调节 fixed4 _Specular; //定义反射 float _Gloss; //定义亮光 struct a2v{ //顶点着色器结构体 float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; //第一组纹理坐标存放该变量中 }; struct v2f{ //片元着色器结构体 float4 pos : SV_POSITION; float3 worldNormal1 : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; //用于存放纹理坐标的纹理uv,以便在片元着色器中使用该坐标进行纹理采样 }; v2f vert(a2v v){ v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); //顶点坐标从模型空间映射到投影空间 o.worldNormal1 = unityObjectToWorldNormal(v.normal); //法线从模型空间转换到世界空间 o.worldPos = mul(_object2World, v.vertex ).xyz; //顶点从模型空间转换到世界空间 o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; //先用缩放属性_MainTex_ST.xy对顶点纹理进行缩放,后_MainTex_ST.zw进行偏移 return o; } fixed4 frag(v2f i) : SV_Target{ //计算世界空间下的法线方向和光照方向 fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 albedo = tex2D(_MainTex, i.uv).rgb*_Color.rgb; //利用tex2D函数对纹理进行采样,格式:(被采样的纹理,纹理坐标-将返回计算得到的纹素值) //然后与颜色属性_Color乘积后作为材质的反射率albedo fixed ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo //albedo与环境光照相乘得到环境光部分 fixed3 diffuse = _LightColor0.rgb * albedo *max(0, dot(worldNormal, worldLightDir)); //使用albedo计算漫反射光照的结果 fixed3 viewDir = nromalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 halfDir = nromalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb *pow(max(0, dot(worldNormal,halfDir)), _Gloss); return fixed(ambient + diffuse + specular, 1.0); //环境光与漫反射与高光反射相加 } ENDCG } } Fallback"Specular" //设置合适的fallback }
纹理的属性:
当向Unity中导入一张纹理资源后,可以在它的材质面板上调整器属性。
Texture Type为纹理类型,其中有Texture类型、Normal map类型以及Cubemap等高级纹理类型。
Warp Mode属性决定了当纹理坐标超过[0,1]范围后将会被如何平铺。Wrap Mode有两种模式,一种是Repeat,在该模式下若纹理坐标超过1,那么整数部分将会被舍弃掉,直接使用小数部分进行采样,这样的结果纹理将会不断重复;另一种是Clamp,在该模式下,若纹理坐标超过1,则将会截取到0,如果小于0,则将会截取到0。
图 1.44 左Reapeat 右Clamp
Filter Mode(滤波 模式)属性决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式。Filter Mode支持3种模式:Point、Bilinear和Trilinear。它们得到的图片滤波效果依次提升,所需要耗费的性能也在依次增大。纹理滤波会影响放大或缩小纹理时得到的图片质量。而 如果将64x64大小纹理贴在512x512大小的平面上时,就需要放大该纹理。
图 1.45 分别使用3种滤波模式得到的结果
纹理缩小的同时还要解决抗锯齿问题(即会出现一个个很明显的像素边界),常用的方法是使用多级渐远纹理(mipmapping)技术。这是一种用空间换取时间的办法,提前将原纹理用滤波器处理来得到更多的小图像,并使用一定的空间用于存放它们,而当物体远离摄像机时,可直接使用较小的纹理进行代替。但通常会多占用33%的内存空间。在Unity中,可以将纹理导入面板中,将Texture Type选择成Advanced,再勾选Generate Mip Maps即可开启多级渐远纹理技术。同时可以选择生成多级渐远纹理时是否使用线性空间以及采样滤波器等。
图 1.46 不同的Filter Mode得到的效果
Point模式使用了最近邻滤波,在放大或缩小时,它的采样像素数目通常只有一个,从而图片将会有像素风格的效果。
Bilinear模式则采用线性滤波,对于每个目标像素,它将会采用4个临近像素,然后对它们进行线性插值混合后得到最终模式,从而图像看上去变得模糊了。(关于该部分知识可了解数字图像处理一课,其中将会讲解多种不同的滤波器对于图像的处理以及效果,同时还有对追求不同效果的方法介绍,如高斯模糊、高斯噪声等)
Trilinear模式和Bilinear差不多,但Trilinear还会在多级渐远纹理之间进行混合。如果一张纹理没有使用多级渐远纹理,那么Trilinear将会和Bilinear的效果一样。
Unity允许开发者为不同目标平台选择不同的分辨率。如果导入的纹理大小超过了Max Texture Size中的设置值,那么Unity将会把该纹理缩放成这个最大分辨率。导入的纹理可以是非正方形,但是长宽的大小应该是2的幂,如2,4,8,16,32等。若使用非2的幂大小的纹理,那么这些纹理将会占用更多的内存空间。而且GPU读取该纹理的速度也会下降。
纹理格式精度越高,占用的内存空间越大,但得到的效果越好。对于一些不需要使用很高精度的纹理(如用于漫反射颜色的纹理),开发者应该尽量使用压缩格式。
纹理的另一种常见的应用为凹凸映射(bump mapping)。凹凸映射的目的在于使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。该方法不会真的改变模型的顶点位置,只是让模型看起来像是“凹凸不平”,但可以从模型的轮廓看出“破绽”。就比如说,美术人员给一个球体贴上凹凸贴图,让这个图看上去好像凹凸贴图的样子,但是实际上观看该模型侧面的时候该球体表面仍然是光滑的,并不会有真实的凹凸效果。
有两种主要方法可以进行凹凸映射。[1]高度纹理(hight map)来模拟表面位移(displacement)没然后得到一个修改后的法线值,该方法被称为高度映射(height mapping);[2]法线纹理来直接存储表面法线,这种方法被称为法线映射。
高度纹理:
表面位移通过高度图来实现凹凸映射。高度图中存储的是强度值,它用于表示模型表面局部的海拔高度。颜色越浅表明该位置的表面越向外凸起,颜色越深表示越往里凹。我们可以通过高度图中明确的知道一个模型表面的凹凸情况,缺点是计算更加复杂,同时实时计算不能直接得到表面发现,而是需要通过由像素的灰度值计算得出,从而消耗更多的性能。
对于高度图,以黑白图作为高度图会体现的效果更加明显,有时候如果是彩图,之前曾经试过利用blender作为高度图但并没有呈现出凹凸的感觉,换成黑白图就可以呈现。
高度图通常会和法线映射一起使用,用于给出表面凹凸的额外信息,开发者通常会使用法线映射来修改光照。
法线纹理:
法线纹理中存储的就是表面的法线方向。法线方向的分量范围在[-1,1],而像素的分量范围为[0,1],从而通常使用的映射公式为:
在shader中对法线纹理进行纹理采样后,还需要对结果进行一次反映射的过程,以得到原先的法线方向。(因为前文提到过法线纹理存储的是表面的法线方向)即其公式为上面映射公式的逆函数
因为模型顶点自带的法线是定义在模型空间中,从而可以将修改后的模型空间中的表面法线存储在一张纹理中,这种纹理被称为模型空间的法线纹理(object-space map)。而在实际制作中,往往会采用另一种坐标空间,即模型顶点的切线空间(tangent space)来存储法线。对于模型的顶点,它都有一个属于自己的切线空间,这个切线空间的原点是该顶点本身,而z轴是顶点的法线方向(n),x轴是顶点的切线方向(t),y轴可由法线和切线叉积而得,也被称为副切线或副法线。
该纹理被称为是切线空间的法线纹理(tangent-space normal map)。
图 1.47 模型顶点的切线空间
模型空间下的法线纹理看上去五颜六色,因为所有法线所在的坐标空间是同一个坐标空间,即模型空间,每个点存储的法线方向是各异。
————————————————————————————————
书中内容不太直观,引用:https://www.www.zyiz.net/content-4-1067592.html
模型空间下:颜色各异,即证明他们的法线方向是个不一样的
切线空间下:如果顶点没有凹凸效果,即当前顶点在切线空间中的法线就是 z 轴本身(0, 0, 1),映射后的为RGB(0.5, 0.5, 1) 浅蓝色。
由于凹凸效果一般只要对原法线做微小的变化,所以切线空间下的法线纹理一般都有大面积的蓝色。
需要声明的是:模型空间下的法线纹理和模型对应的,绝对法线信息,完全和模型uv匹配,但不能换成别的模型使用,也不支持uv移动。
————————————————————————————————
模型空间下的法线纹理更符合人类的直观认识,而且法线纹理本身也很直观,容易调整,不同的法线方向代表了不同的颜色。美术人员则更加喜欢使用切线空间下的法线纹理。
模型空间存储法线的优点有以下:
[1]实现简单,更加直观,甚至不需要模型原始的法线和切线信息,计算少。生成十分简单。而如果需要生成切线空间下的法线纹理,由于模型的切线一般是和UV方向相同,因此想要得到效果比较好的法线映射就要求纹理映射也是连续的。
[2]在纹理坐标的缝合处和尖锐的边角部分,可见的突变较少,即可以提供平滑的边界。模型空间下的法线纹理存储的是同一坐标系下的法线信息,因此在边界处通过插值得到的法线可以平滑变换。切线空间下的法线纹理中的法线信息是依靠纹理坐标的方向得到的结果,可能会在边缘处或尖锐的部分造成更多可见的缝合迹象。
切线空间存储法线的优点有以下:
[1]自由度很高,模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的模型,而应用到其他模型上效果就完全错误的。切线空间下的法线纹理记录的是相对法线信息。即把该纹理应用到一个完全不同的网络上,也可以得到一个合理的效果。
[2]可进行UV动画。如可移动一个纹理的UV坐标来实现一个凹凸移动的效果,但使用模型空间下的法线纹理会得到完全错误的结果。UV动画在水或火山熔岩这种类型上的物体会经常用到。
[3]可重用法线纹理,如一个砖块,仅使用一张法线纹理就可以用到所有的6个面上了
[4]可压缩。由于切线空间下的法线纹理中法线的Z方向总是正方向,因此开发者可以仅存XY方向,而推导Z方向。而模型空间下的法线纹理由于每个方向都是可能的,从而必须存储3个方向的值,不可压缩。
从以上看,切线空间很多情况下都优于模型空间,从而节省美术人员的工作。所以在很多时候建模贴图也是直接利用法线空间形式进行贴图。
实践:
切线空间下计算:
思路:在片元着色器中通过纹理采样得到切线空间下的法线,然后在于切线空间下的视角方向、光照方向等进行计算得到最终的结果。
解决方法:首先需在顶点着色器中把视角方向和光照方向从模型空间变换到切线空间,即需要知道从模型空间到切线空间的变换矩阵。该变换矩阵的逆矩阵即从切线空间到模型空间的变换矩阵是非常容易求得到的,在顶点着色器中按切线x轴、法线z轴、副切线y轴的顺序按列排列即可得到。从模型空间到切线空间的变换矩阵为切线空间到模型空间的变换矩阵的专职矩阵。
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]创建一个胶囊体,并把该材质赋值给胶囊体
[4]给该shader附上对应代码
Shader "Unity Shaders Book/Chapter7/Normal Map In Tangent Space"{ //shader命名 Properties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white"{} _BumpMap("Normal map", 2D) = "bump"{} //法线纹理,使用bump作为默认值,bump是内置的法线纹理,若没有提供任何法线纹理时,bump就对应模型自带的法线信息 _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") //定义光照模式 CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cgnic" fixed4 _Color; sampler2D _MainTex; sampler2D _BumpMap; float4 _MainTex_ST; //得到纹理属性(平铺、偏移系数) float4 _BumpMap_ST; //得到纹理属性(平铺、偏移系数) float _BumpScale; fixed4 _Specular; float _Gloss; sturct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; //把顶点的切线方向填充到tangent变量中,之所以要float4,这是因为需要用tangent.w来决定切线空间的第三个坐标轴-副切线的方向性 float4 texcoord : TEXCOORD0; } sturct v2f{ //顶点着色器中需计算切线空间下的光照和视角方向,从而需要在v2f中添加梁个变量存储变换后的光照和视角方向 float4 pos : SV_POSITION; float4 uv : TEXCOORD0; //因为使用两张纹理,从而需要存储两个纹理坐标,从而定义为float4类型,xy存储_MainTex的坐标,zw存储_BumpMap float3 lightDir : TEXCOORD1; float3 viewDir : TEXCOORD2; } v2f(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; //计算得到副法线 v.tangent.w; //构建一个可以将坐标从模型空间转移到切线空间的矩阵 TANGENT_SPACE_ROTATION; //该内置宏帮助开发者直接计算得到rotation变化矩阵 //将光照方向从模型空间转换到法线空间 o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;//ObjSpaceLightDir得到模型空间下的光照方向,再通过变换矩阵retation把其将模型空间转换到切线空间中 //将视方向从模型空间转换到法线空间 o.ViewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz; //这一步完成后,将会把光照、视角方向以及uv坐标都转换到法线空间中 return o; } fixed4 frag(v2f i) : SV_Targent{ //顶点着色器中完成了大部分工作,从而在片元着色器中只需要采样得到切线空间下的法线方向,再在切线空间下进行光照计算即可 fixed3 tangent LightDir = normalize(i.lightDir); fixed3 tangent ViewDir = normalize(i.viewDir); //得到法线纹理中的纹素 fixed4 packedNormal1 = tex2d(_BumpMap, i.uv.zw); //利用tex2D对法线纹理_BumpMap进行采样 fixed3 tangentNormal; tangentNormal = UnpackNormal(packedNormal); tangentNormal1.xy *= _BumpScale; //先把packedNormal的xy分量映射回法线方向,再乘以_BmpScale(控制凹凸程度)来得到tangentNormal的xy分量 tangentNormal1.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); //法线都是单位矢量,从而tangentNormal.z分量可以由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)); //lightcolor为内置函数,通过包含进lighting.cginc从而调用该函数 fixed halfDir = normalize(tangentLightDir + tangentViewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss); return fixed4 (ambient + diffuse + specular, 1.0); } ENDCG } } Fallback"Specular" }
世界空间下计算:
思路:片元着色器中将法线方向从切线空间变换到世界空间下。
解决方法:在顶点着色器中计算从切线空间到世界空间的变换矩阵,并把它传递给片元着色器。变换矩阵的计算可以有顶点的切线、副切线和法线再世界空间下的表示得到。最后,只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间下即可。
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]创建一个胶囊体,并把该材质赋值给胶囊体
[4]给该shader附上对应代码
Shader "Unity Shaders Book/Chapter7/Normal Map In Tangent Space"{ //shader命名 Properties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white"{} _BumpMap("Normal map", 2D) = "bump"{} //法线纹理,使用bump作为默认值,bump是内置的法线纹理,若没有提供任何法线纹理时,bump就对应模型自带的法线信息 _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") //定义光照模式 CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cgnic" fixed4 _Color; sampler2D _MainTex; sampler2D _BumpMap; float4 _MainTex_ST; //得到纹理属性(平铺、偏移系数) float4 _BumpMap_ST; //得到纹理属性(平铺、偏移系数) float _BumpScale; fixed4 _Specular; float _Gloss; sturct a2v{ float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; //把顶点的切线方向填充到tangent变量中,之所以要float4,这是因为需要用tangent.w来决定切线空间的第三个坐标轴-副切线的方向性 float4 texcoord : TEXCOORD0; } sturct v2f{ float4 pos : SV_POSITION; float4 uv : TEXCOORD0; //因为使用两张纹理,从而需要存储两个纹理坐标,从而定义为float4类型,xy存储_MainTex的坐标,zw存储_BumpMap float4 TtoW0 : TEXCOORD1; //TtoW0~2依次存储了从切线空间到世界空间的变换矩阵的每一行,每一行只需要float3类型的变量即可,单萎了充分利用插值寄存器的存储空间,从而把世界空间下的顶点位置存储在这些变量的w分量中 float4 TtoW1 : TEXCOORD2; float4 TtoW2 : TEXCOORD3; } v2f(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 = UnityObjectToWorldNormal(v.tangent.xyz); //计算世界空间下的顶点切线的矢量表示 fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; //计算世界空间下的顶点副法线的矢量表示 //并把其按列摆放得到从切线空间到世界空间的变换矩阵 o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); //把该矩阵每一行分别存储在TtoW0~2中,并把世界空间下的顶点位置的xyz分量分别存储在了这些变量的w分量中 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_Targent{ float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w,); //将TtoW0~2的w分量中构建世界空间下的坐标 fixed3 tangent LightDir = normalize(UnityWorldSpaceLightDir(worldPos)); //使用内置函数得到世界空间下的光照和视角方向 fixed3 tangent ViewDir = normalize(UnityWorldSpaceViewDir(worldPos)); fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); //利用内置函数对法线纹理进行采样和解码(需要将法线纹理的格式表示成Normal map) bump.xy *= _BumpScale; //使用_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))); //利用TtoW0~2存储的变换矩阵将法线变换到世界空间下 ...... } ENDCG } } Fallback"Specular" }
Unity中的法线纹理类型
当把法线纹理的纹理类型表示为Normal map时,可以使用Unity的内置函数UnpackNormal来得到正确的法线方向。
图 1.48 选择Normal map类型
这样选择可以让Unity根据不同平台对纹理进行压缩(例如使用DXT5nm格式),再通过UnpackNormal函数来针对不同的压缩格式对法线纹理进行正确的采样。
纹理可以用于存储任何表面属性。一种常用的用法就是使用简便纹理来控制漫反射光照的结果。在之前计算漫反射光照时都是使用渐变纹理来控制漫反射光照的结果。
有时亦需要更加灵活地控制光照结果。如很多卡通风格渲染使用了一种基于冷到暖色调(cold-to-warm tones)的着色技术。使用该技术可以保证物体的轮廓线相比于之前使用的传统漫反射光照更加明显。
图 1.49 不同渐变纹理控制漫反射
通过该方法可以自由地控制物体的漫反射光照,不同的渐变纹理有不同的特征。
步骤:
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]拉入一个Suzanne模型,并把材质赋给该模型
[4]给该shader附上对应代码
Shader"Unity shaders Book/Chapter 7/Ramp Texture"{ 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; //定义渐变纹理属性变量_RampTex_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 = TRANSFORM_TEX(v.texcoord, _RampTex); //通过内置宏来计算经过平铺和偏移后的纹理坐标 return o; } fixed4 frag(v2f i) : SV_Targent{ fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLigthtDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //环境光 fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5; //半兰伯特模型 fixed3 diffuseColor = tex2d(_RampTex, fixed2(halfLambert, halLambert)).rgb * _color.rgb; //利用halfLambert来构建一个纹理坐标,并用该纹理坐标对渐变纹理_RampTex进行采样 fixed3 diffuse = _LightColor0.rgb * diffuseColor; //把渐变纹理采样得到的颜色和材质颜色_Color相乘,得到最终的漫反射颜色 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); //环境光和漫反射光和高光反射相加 } ENDCG } } Fallback "Specular" }
遮罩纹理(mask texture)允许开发者保护某些区域,使它们免于某些修改。
使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或某几个)通道的值(例如texel.r)来与某种表面属性进行相乘,这样,当该通道的值为0时,可以保护表面不受该属性的影响。使用遮罩纹理可以让美术人员更加精确(像素级别)地控制模型表面的各种性质。
步骤:
[1]建场景并去掉天空盒子
[2]构建一个材质与Unity shader,并把该shader赋给材质
[3]拉入一个胶囊体,并把材质赋给该模型
[4]给该shader附上对应代码
Shader"Unity Shader Book/Chapter 7/ Mask Texture"{ Porperties{ _Color("Color Tint", Color) = (1,1,1,1) _MainTex("Main Tex", 2D) = "white"{} _BumpMap("Bump 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{ "LightingMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float _BumpScale; sampler2D _SpecularMask; float _SpecularScale; fixed4 _Specular; float _Gloss; struct a2f{ float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; }; struct v2f{ float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float4 lightDir : TEXCOORD1; float4 viewDir : TEXCOORD2; }; v2f vert(a2f v){ //在顶点着色器中,对光照方向和视角方向进行了坐标空间的变换,把它们从模型空间变换到切线空间,以便在片元着色器中和法线进行光照运算 v2f o; o.pos = mul(UNITY_MAXTRIX_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 - saturare(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 + tangengtViewDir); fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale; //对SpecularMask进行采样,r分量来计算掩码值,在把该两个相乘 fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halDir)), _Gloss) * specularMask; return fixed4(ambient + diffuse + specular, 1.0); } ENDCG } } Fallback"Specular" }
注:在真实游戏制作中,遮罩纹理已经不止限于保护某些区域是它们免于需改,而是可以存储任何开发者希望逐像素控制的表面属性。