目录标题
纹理的基本概念
纹理:
- 通俗来讲,诸如png、jpg等格式的
图片
就是纹理。 - 在图形学中,纹理更多地是被认作
「一块数据」
,它也不再局限在2D空间,也会有三维纹理、立方体纹理、二维数组纹理等。
纹理是由 「纹素(texel)」 构成的,每个纹素记录着相应的颜色信息,它类似于屏幕的 「像素(pixel)」 但又与像素不保证是一一对应的。
那么纹理为什么会被广泛使用呢?
我们看到现实世界中所呈现出丰富的颜色,是由于不同物体会吸收一部分光的波长并反射出另一部分出来(如红色物体实际上是因为反射了大部分红色波长出来),最终通过眼睛传入大脑计算成像。而通过计算机去实时模拟这一过程无疑是复杂且耗时的,尤其是游戏要求在短短的一帧(少于0.016s)中就完成一次场景渲染。
为了解决这个问题,人们想到了一个办法:找到一张图片(离线渲染生成),然后把它“粘”在物体的表面上,就像贴墙纸一样。而这一“粘”的过程,我们管它叫做 「纹理映射(texture mapping)」,实际上也就是一块纹理数据与3D模型建立关联的过程。
纹理贴图作用一:黏在模型表面用来展现模型的外观
纹理最初的目的就是使用一张2D图片来控制展现3D模型的外观。
如何将纹理贴在模型表面?
- 使用
纹理映射(texture mapping)
技术, 根据纹理的每个纹素与模型的顶点的对应关系,从而将一张纹理贴图 “ 黏 ” 在模型表面,逐纹素地控制模型的颜色。
如何得到模型每个顶点所对应的二维(UV)坐标?
在美术人员建模的时候, 通常会在建模软件中利用 纹理展开(UV展开)
把纹理映射坐标(UV坐标)
存储在模型的每个顶点上。最后可以得到该模型的UV展开图
!
如果纹理贴图想要更好的粘到模型表面,则纹理贴图应该与UV展开图有较好的对应关系!
纹理映射坐标定义了该顶点在纹理中对应的2D坐标。 通常,这些坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,而v是纵向坐标。
纹理贴图大小不一
尽管纹理的大小可以是多种多样的,例如可以是256x256或者1024x1024, 但顶点UV坐标的范围通常都被归一化到(0,1)范围内。
需要注意的是,纹理采样时使用的纹理坐标不一定是在(0,1)范围内。 实际上,这种不在(0,1)范围内的纹理坐标有时会非常有用。
与之关系紧密的是纹理的平铺模式,它将决定渲染引擎在遇到不在(0,1)范围内的纹理坐标时如何进行纹理采样。
纹理贴图作用二:通过凹凸映射,使用法线贴图或者高度贴图去修改模型表面法线方向信息从而参与光照模型的计算,从而改变光照效果(例如高光反射和漫发射结果),最终为模型表面提供凹凸不平的细节
- 这种方法不会真的改变模型的顶点位置,只是让模型看起来好像是“凹凸不平”的,但可以从模型的轮廓处看出“破绽”。
凹凸映射的两种方法:
- 高度映射(height mapping):使用一张高度纹理 (height map)模拟表面位移(displacement), 然后得到一个修改后的法线值。
- 法线映射 (normal mapping):使用一张法线纹理 (normal map) 来直接存储表面法线。
高度纹理(高度图)
高度图中存储的是强度值 (intensity), 它用于表示模型表面局部的海拔高度。因此,颜色越浅(白)表明该位置的表面越向外凸起 ,而颜色越深(黑)表明该位置越向里凹。
这种方法的好处是非常直观,我们可以从高度图中明确地知道一个模型表面的凹凸情况,但缺点是计算更加复杂,在实时计算时不能直接得到表面法线,而是需要由像素的灰度值计算而得,因此需要消耗更多的性能。
法线纹理
法线纹理中存储的是表面的法线方向。
由于法线贴图每个纹素存储的法线方向的分量范围为 [0, 1],而模型空间或者世界空间下表面法线方向各分量范围应该在[-1, 1], 因此我们需要做一个映射/转换,将[0,1]的取值范围转换为[-1,1]的取值范围。即在 Shader 中对法线纹理进行纹理采样tex2D()后,需要对采样结果进行一次转换normal=pixel*2-1的过程,从而得到原先正常的法线方向。
然而,由于方向是相对于坐标空间来说的,那么法线纹理中存储的法线方向在哪个坐标空间中呢?
法线纹理中存储的法线方向在哪个坐标空间中呢?
- 模型空间的法线纹理 (object-space normal map)
对于模型空间下的顶点,将其自带的法线经过修改后直接存储在一张纹理中! - 切线空间的法线纹理 (tangent-space normal map)
对于模型的每个顶点,它都有一个属于自己的切线空间,这个切线空间的原点就是该顶点本身,而 z轴是顶点的法线方向(n) ,x轴是顶点的切线方向(t),而y轴可由法线和切线叉积(满足左手螺旋定则)而得,也被称为是副切线(b) 或副法线。
注意:在Unity中模型空间和世界空间采用左手系,而观察空间采用右手系!
模型空间下的法线纹理看起来是“五颜六色”的。
这是因为模型空间下法线贴图存储的法线信息是模型的绝对法线信息(即模型空间下的法线方向信息),即每个法线方向所在的坐标空间都是相同的空间----模型空间!在模型空间下,模型的每个顶点存储的法线方向是各异的,有的是(0,1, 0), 经过映射后存储到纹理中就对应了 RGB(0.5, 1, 0.5) 浅绿色,有的是(0, -1, 0), 经过映射后存储到纹理中就对应了 0.5, 0, 0.5)紫色。
切线空间下的法线纹理看起来几乎全部是浅蓝色的。
这是因为,切线空间下的法线贴图存储的是模型的相对法线信息(即各点切线空间下的法线方向信息),每个法线方向所在的坐标空间是不一样的,都是相对于各点的切线空间。这种法线纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向。也就是说,如果一个点的法线方向不变,那么在它的切线空间中 ,新的法线方向就是 轴方向,即值为(0,0, 1), 经过映射后存储在纹理中就对应了 RGB(0.5, 0.5, 1) 浅蓝色 。而这个颜色就是法线纹理中大片的蓝色。这些蓝色实际上说明顶点的大部分法线是和模型本身法线一样的,不需要改变。
绝对法线信息与相对法线信息
模型空间下的法线纹理记录的是绝对法线信息,仅可用于创建它时的那个模型,而应用到其他模型上效果就完全错误了。而切线空间下的法线纹理记录的是相对法线信息,这意味着,即便把该纹理应用到一个完全不同的网格上,也可以得到一个合理的结果。
如何根据高度图生成法线贴图?
当我们把一张高度图导入 Unity 后,除了需要把它的Texture Type纹理类型设置成 Nonnal map 外,还需要勾选 Create from Grayscale按钮,从而生成相应的切线空间下的法线贴图!
以切线空间下的法线纹理的应用为例:如何实现?
前提:在计算光照模型时必须统一各个参数所在的坐标空间!而坐标空间的选择可以是任意的!例如模型空间,世界空间,切线空间等
- 如果在切线空间下进行光照计算,光照模型所需的各个参数应该是切线空间下的!例如把光源方向、视角方向由世界空间变换到切线空间下。
- 如果在世界空间下进行光照计算,光照模型所需的各个参数应该是世界空间下的!需要把切线空间下法线贴图采样得到的法线方向变换到世界空间下,再和世界空间下的光源方向和视角方向进行计算。
注意:
第一种方法往往要优于第二种方法----即在应用切线空间下的法线贴图的前提下,光照模型的计算推荐采用切线空间,因为我们可以在顶点着色器中就完成对光源方向和视角方向的变换,而第二种方法由于要先对法线纹理贴图进行采样,所以变换过程必须在片元着色器中实现,这意味若我们需要在片元着色器中进行一次矩阵操作。
但从通用性角度来说,第二种方法要优于第一种方法,因为有时我们需要在世界空间下进行一些计算,例如在使用 Cubemap 进行环境映射时,我们需要使用世界空间下的反射方向对 Cubemap 进行采样 。如果同时需要进行法线映射,我们就需要把法线方向变换到世界空间下。
当然,读者可以选择其他坐标空间进行计算,例如模型空间等,但切线空间和世界空间是最为常用的两种空间。
坐标系间的旋转变换矩阵
对于任意向量,要想从坐标系A转换到坐标系B下表示,则其变换矩阵为: A坐标系的三个基矢量在B坐标系下的表示按列排列成矩阵,则这个矩阵就是从A坐标系到B坐标系的旋转变化矩阵!
代码示例:
1.在切线空间下计算
基本思路:在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。
我们需要知道从模型空间到切线空间的变换矩阵,这个旋转矩阵有点难求得,但这个变换矩阵的逆矩阵,即从切线空间到模型空间的变换矩阵是非常容易求得的,即切线空间的三个基矢量uvw在模型空间的坐标系下的表示按列顺序地排成矩阵就可得出该旋转变换矩阵!将切线 (x 轴)、副
切线 (y 轴)、法线 (z 轴)的顺序地按列排列即可得到旋转变换矩阵,
利用旋转矩阵的正交性可知,旋转矩阵的逆矩阵等价于转置矩阵,故可得出从模型空间到切线空间的变换矩阵!
Shader "Unlit/012"
{
Properties
{
//注意:Properties语义块中的属性名是可以自定义的!
//"white" 是内置纹理的名字,即全白的纹理。
_MainTex("MainTex",2D) = "white"{}
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(0,10)) = 1
//对于法线纹理贴图使用_BumpMap属性名表示,属性类型为2D,使用 "bump" 作为它的默认值。
//"bump"默认值是 Unity 内置的法线纹理,当没有提供任何法线纹理时 "bump"就对应了模型自带 法线信息。
_BumpMap("Normal Map",2D)="bump"{}
//_BumpScale 则是用于控制凹凸程度的,当它为0时,意味着该法线纹理不会对光照产生任何影响。
_BumpScale("Bump Scale",float)=1
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//为了使用 _LightColor0, 需要引入Lighting.cginc库文件
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
//与基础纹理有关的变量
sampler2D _MainTex;
//在 Unity 中,我们需要使用“纹理名_ST”的方式来声明某个纹理的缩放偏移属性。 ST 是缩放scale 和平移translation 的缩写。
//_MainTex_ST可以让我们得到该纹理的缩放和平移 (偏移)值, _MainTex_ST.xy 存储的是缩放值,而_MainTex_ST.zw 存储的是偏移值。
float4 _MainTex_ST;//对于每个纹理都有这样一个附属变量,存储关于该纹理的Tilling值和Offset值
//与法线贴图有关的变量
sampler2D _BumpMap;
float4 _BumpMap_ST;//对于每个纹理都有这样一个附属变量,存储关于该纹理的Tilling值和Offset值
float _BumpScale;
//用作顶点着色输出的输出结构体至少包含绑定了SV_POSITION语义的变量
struct v2f {
float4 vertex:SV_POSITION;
fixed3 objectlightdir:TEXCOORD0;
float3 objectviewdir:TEXCOORD1;
//用于存储基础纹理贴图
float2 uv:TEXCOORD2;
//用于存储法线贴图
float2 normaluv:TEXCOORD3;
};
//需要注意的是,和法线方向 normal 不同, tangent 的类型是 float4, 而非 float3,这是因为我们需要使用 tangent.w 分址来决定切线空间中的第三个坐标轴一副切线的方向性。
v2f vert(appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//使模型的UV展开图与纹理贴图匹配适应,得到适应后的纹理贴图(即适应后的纹理贴图的每个纹素与模型顶点的对应关系已经确定!)
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normaluv= TRANSFORM_TEX(v.texcoord, _BumpMap);
//使用 Unity 的内置库函数ObjSpaceLightDir()和ObjSpaceViewDir()来得模型空间下光源和视角方向向量,
//通过矩阵乘法,乘以旋转变换矩阵 rotation 把它们从模型空间变换到切线空间中。
TANGENT_SPACE_ROTATION;
//这个宏定义实际上完成了两步操作:
//float3 binormal=cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w;
//float3×3 rotation=float3×3(v.tangent.xyz,binormal,v.normal);
o.objectlightdir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
o.objectviewdir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
//片元着色器的另一个重要功能:纹理采样
fixed4 frag(v2f i) : SV_TARGET
{
fixed3 tangentlightdir = normalize(i.objectlightdir);
fixed3 tangentviewdir = normalize(i.objectviewdir);
//纹理采样过程
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
//对切线空间下的法线贴图进行采样
//为了方便Unity对法线纹理的存储进行优化,通常会将法线纹理的纹理类型标识成 Normal map, Unity会根据平台来选择不同的压缩方法。
//在这种情况下 我们可以使用 Unity 的内置函数 UnpackNormal 来得到其原本空间下正确的法线方向。
fixed4 packednormal = tex2D(_BumpMap,i.normaluv);
fixed3 tangentnormal=UnpackNormal(packednormal);
tangentnormal.xy*= _BumpScale;
tangentnormal.z = sqrt(1 - saturate(dot(tangentnormal.xy, tangentnormal.xy)));
//如果没有在 Unity 将法线纹理贴图的类型设置为Norma] map(在Unity中默认所有的纹理贴图都是Defaut类型)
//就需手动进行将法线贴图存储的法线方向信息还原为真正的法线方向信息。
//fixed3 tangentnormal;
//tangentnormal.xy = (packednormal.xy * 2 - 1) * _BumpScale;
//tangentnormal.z = sqrt(1-saturate(dot(tangentnormal.xy, tangentnormal.xy)));
//发现光照模型中的各个参数都统一为切线空间下的
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * albedo * (dot(tangentlightdir, tangentnormal) * 0.5 + 0.5);
fixed3 halfvector = normalize(tangentlightdir + tangentviewdir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentnormal, halfvector)), _Gloss);
return fixed4(ambient + diffuse + specular,1);
}
ENDCG
}
}
}
2.在世界空间下计算
基本思想:
在顶点着色器中计算从切线空间到世界空间的变换矩阵,把它传递给片元着色器。变换矩阵的计算可以由顶点的切线、副切线和法线在世界空间下的表示来得到 。最后,我们只需要在片元着色器中把法线纹理中的法线方向从切线空间变换到世界空间下即可。尽管这种方法需要更多的计算,但在需要使用 Cubemap进行环境映射等情况下,我们就需要使用这种方法。
Shader "Unlit/013"
{
Properties
{
_MainTex("MainTex",2D) = "white"{}
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(0,10)) = 1
_BumpMap("Normal Map",2D) = "bump"{}
_BumpScale("Bump Scale",float) = 1
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
sampler2D _MainTex;
float4 _MainTex_ST;//对于纹理都有这样一个附属变量,存储关于该纹理的Tilling值和Offset值
//与法线贴图有关的变量
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
//用作顶点着色输出的输出结构体至少包含绑定了SV_POSITION语义的变量
struct v2f {
float4 vertex:SV_POSITION;
//用于存储纹理贴图
//float2 uv:TEXCOORD2;
//用于存储法线贴图
//float2 normaluv:TEXCOORD3;
//以上可简化为以下一行代码,
//将uv的xy分量用于存储与模型的UV展开图匹配适应后的基础纹理贴图的信息
//将uv的zw分量用于存储与模型的UV展开图匹配适应后的法线贴图的信息
float4 uv:TEXCOORD0;
float4 Ttiw0:TEXCOORD1;
float4 Ttiw1:TEXCOORD2;
float4 Ttiw2:TEXCOORD3;
};
v2f vert(appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//使模型的UV展开图与纹理贴图匹配适应,得到适应后的纹理贴图(即适应后的纹理贴图的每个纹素与模型顶点的对应关系已经确定!)
o.uv.xy= TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw= TRANSFORM_TEX(v.texcoord, _BumpMap);
//计算世界空间下的顶点坐标,法线,切线,副法/切线
float3 worldpos = mul(unity_ObjectToWorld,v.vertex).xyz;
//一般情况下,将模型空间下的量转成世界空间下的量可以通过矩阵乘法mul(unity_ObjectToWorld,值),但表面法线的转换不行!顶点和切线是可以的!
fixed3 worldnormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldtangent = UnityObjectToWorldDir(v.tangent);
//求世界空间下的副切线
//float3 worldbinormal = cross(worldtangent, worldnormal) * v.tangent.w;
float3 worldbinormal = cross(worldnormal,worldtangent) * v.tangent.w;
//如何存放这些数据的方式
o.Ttiw0 = float4(worldtangent.x, worldbinormal.x, worldnormal.x,worldpos.x);
o.Ttiw1 = float4(worldtangent.y, worldbinormal.y, worldnormal.y,worldpos.y);
o.Ttiw2 = float4(worldtangent.z, worldbinormal.z, worldnormal.z,worldpos.z);
return o;
}
fixed4 frag(v2f i) : SV_TARGET
{
float3 worldvertex = float3(i.Ttiw0.w,i.Ttiw1.w,i.Ttiw2.w);
//计算世界空间中的光源方向向量和视角方向向量
fixed3 worldlightdirection = normalize(_WorldSpaceLightPos0.xyz);
fixed3 viewdirection = normalize(_WorldSpaceCameraPos.xyz - worldvertex);
//纹理采样
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb;
fixed4 packednormal = tex2D(_BumpMap, i.uv.zw);
fixed3 tangentnormal = UnpackNormal(packednormal);
tangentnormal.xy*= _BumpScale;
tangentnormal.z = sqrt(1 - saturate(dot(tangentnormal.xy, tangentnormal.xy)));
//法线贴图未改为normal map时
//fixed3 tangentnormal;
//tangentnormal.xy = (packednormal.xy * 2 - 1) * _BumpScale;
//tangentnormal.z = sqrt(1-saturate(dot(tangentnormal.xy, tangentnormal.xy)));
//将切线空间法线转换到世界空间
fixed3 worldnormal=normalize(float3(dot(i.Ttiw0.xyz, tangentnormal), dot(i.Ttiw1.xyz, tangentnormal), dot(i.Ttiw2.xyz, tangentnormal)));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//以下光照模型的参数都统一为世界空间下的
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * albedo * (dot(worldlightdirection, worldnormal) * 0.5 + 0.5);
fixed3 halfvector = normalize(worldlightdirection + viewdirection);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldnormal, halfvector)), _Gloss);
return fixed4(ambient + diffuse + specular,1);
}
ENDCG
}
}
}
渐变纹理的使用
使用渐变纹理来控制漫反射光照的结果,可以保证物体的轮廓线相比于之前使用的传统漫反射光照更加明显,而且能够提供多色调变化。实现风格化、卡通化渲染!
使用不同的渐变纹理控制漫反射光照,用这种方式可以自由地控制物体的漫反射光照。
代码示例:
渐变纹理在使用时需要特别注意! 需要把渐变纹理的 Wrap Mode 设为 Clamp 模式。
Shader "Unlit/014"
{
Properties
{
_RampTex("MainTex",2D) = "White"{}
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(0,10)) = 1
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
//有关渐变纹理的变量
sampler2D _RampTex;
float4 _RampTex_ST;
//用作顶点着色输出的输出结构体至少包含绑定了SV_POSITION语义的变量
struct v2f {
float4 vertex:SV_POSITION;
fixed3 worldnormal : TEXCOORD0;
float3 worldvertex:TEXCOORD1;
float2 uv:TEXCOORD2;
};
v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldnormal = UnityObjectToWorldNormal(v.normal);
o.worldvertex = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord,_RampTex);
return o;
}
fixed4 frag(v2f i) : SV_TARGET
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldlightdirection = normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert = (dot(i.worldnormal, worldlightdirection) * 0.5 + 0.5);
//对渐变纹理采样
fixed3 albedo = tex2D(_RampTex,fixed2(halfLambert, halfLambert));
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * albedo;
return fixed4(ambient + diffuse,1);
}
ENDCG
}
}
}
遮罩纹理的使用
在之前的实现中,都是把高光反射应用到模型表面的所有地方。但有时希望模型表面某些区域高光反射强烈一些,而某些区域高光反射弱一些。为了得到更加细腻的效果,我们就可以使用一张遮罩纹理来从而控制光照(即使用对遮罩纹理采样的结果的某个通道参与光照模型的计算,使得光只从该通道下经过,而其他通道下无光经过,从而实现控制光照的效果)。
使用遮罩纹理的流程一般是:通过采样得到遮罩纹理的纹素值,然后使用其中某 (或某几)通道的值(例如 texel.r)来与某种表面属性进行相乘(即参与光照模型的计算),这样,当该通道的值为1时,可以保护表面不受该属性的影响。
代码示例:
Shader "Unlit/015"
{
Properties
{
//注意:Properties语义块中的属性名是可以自定义的!
//"white" 是内置纹理的名字,即全白的纹理。
_MainTex("MainTex",2D) = "white"{}
_Diffuse("Diffuse",Color) = (1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(0,10)) = 1
//对于法线纹理贴图使用_BumpMap属性名表示,属性类型为2D,使用 "bump" 作为它的默认值。
//"bump"默认值是 Unity 内置的法线纹理,当没有提供任何法线纹理时 "bump"就对应了模型自带 法线信息。
_BumpMap("Normal Map",2D) = "bump"{}
//_BumpScale 则是用于控制凹凸程度的,当它为0时,意味着该法线纹理不会对光照产生任何影响。
_BumpScale("Bump Scale",float) = 1
_SpecularMask("Specular Mask",2D) ="white"{}
_SpecularMaskScale("Specular Mask Scale",float)=1
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
//与基础纹理有关的变量
sampler2D _MainTex;
float4 _MainTex_ST;
//与法线贴图有关的变量
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
//与遮罩纹理有关的属性
sampler2D _SpecularMask;
float4 _SpecularMask_ST;
float _SpecularMaskScale;
//用作顶点着色输出的输出结构体至少包含绑定了SV_POSITION语义的变量
struct v2f {
float4 vertex:SV_POSITION;
fixed3 objectlightdir : TEXCOORD0;
float3 objectviewdir:TEXCOORD1;
//用于存储匹配适应后基础纹理贴图
float2 uv:TEXCOORD2;
//用于存储匹配适应后法线贴图
float2 normaluv:TEXCOORD3;
//用于存储匹配适应后的遮罩贴图
float2 maskuv:TEXTCOORD4;
};
//需要注意的是,和法线方向 normal 不同, tangent 的类型是 float4, 而非 float3,这是因为我们需要使用 tangent.w 分址来决定切线空间中的第三个坐标轴一副切线的方向性。
v2f vert(appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normaluv = TRANSFORM_TEX(v.texcoord, _BumpMap);
o.maskuv = TRANSFORM_TEX(v.texcoord,_SpecularMask);
TANGENT_SPACE_ROTATION;
o.objectlightdir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
o.objectviewdir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
fixed4 frag(v2f i) : SV_TARGET
{
fixed3 tangentlightdir = normalize(i.objectlightdir);
fixed3 tangentviewdir = normalize(i.objectviewdir);
//纹理采样过程
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
//对切线空间下的法线贴图进行采样
//为了方便Unity对法线纹理的存储进行优化,通常会将法线纹理的纹理类型标识成 Normal map, Unity会根据平台来选择不同的压缩方法。
//在这种情况下 我们可以使用 Unity 的内置函数 UnpackNormal 来得到其原本空间下正确的法线方向。
fixed4 packednormal = tex2D(_BumpMap,i.normaluv);
fixed3 tangentnormal = UnpackNormal(packednormal);
tangentnormal.xy *= _BumpScale;
tangentnormal.z = sqrt(1 - saturate(dot(tangentnormal.xy, tangentnormal.xy)));
//对遮罩贴图进行采样
fixed3 specularmask = tex2D(_SpecularMask,i.maskuv).r*_SpecularMaskScale;
//发现光照模型中的各个参数都统一为切线空间下的
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * albedo * (dot(tangentlightdir, tangentnormal) * 0.5 + 0.5);
fixed3 halfvector = normalize(tangentlightdir + tangentviewdir);
//将遮罩纹理贴图采样的结果用于高光反射模型的计算
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentnormal, halfvector)), _Gloss)*specularmask;
return fixed4(ambient + diffuse + specular,1);
}
ENDCG
}
}
}
纹理贴图的使用(模型表面带有纹理效果的实现)----三部曲
- 设置与纹理贴图有关的属性和变量。
在Properties语义块中:与纹理贴图有关的属性类型是2D
在SubShader语义块中,与纹理贴图有关的变量:同属性名的贴图变量sampler2D数据类型;"属性名_ST"形式的变量记录了该纹理贴图的属性,数据类型为float4. - 使纹理贴图匹配适应,获取匹配适应后的结果(通常与模型的UV展开图进行匹配适应!)
通常在顶点着色器中,通过TRANSFORM_TEX()函数进行匹配适应。 - 将匹配适应的结果用于纹理采样。
纹理采样通常在片元着色器中通过tex2D()进行。 - 将采样的结果用于光照模型的计算。
通常对基础纹理,渐变纹理,遮罩纹理采样的结果可直接用于光照模型的计算,但对切线空间下法线贴图采样的结果必须先进行转换,再用于光照模型的计算。