完整shader代码和示例图片可以看我的github:zaizai77/Shader-Learn: 实现一些书里讲到的shader
使用Unity内置函数
之前的例子中我们都是手动去获取光源方向和视角方向,使用:
使用normalize(_WorldSpace LightPos0.xyz)来得到光源方向(这种方法实际只适用于平行光)
使用normalize(_WorldSpace CameraPos.xyz -i.worldPosition.xyz)来得到视角方向
但如果需要处理更复杂的光照类型,如点光源和聚光灯,我们计算光源方向的方向就是错误的,我们需要先在代码中判断光源类型,再计算光源信息
需要注意的是,这些函数都没有保证得到的方向矢量是单位矢量,因此,我们需要在使用前把它们归一化
基础纹理
使用纹理映射(texture mapping)技术,我们可以把一张图“黏”在模型表面,逐纹素(texel)(纹素的名字是为了和像素进行区分)地控制模型的颜色。
在美术人员建模的时候,通常会在建模软件中利用纹理展开技术把纹理映射坐标(texture-mapping coordinates)存储在每个顶点上。纹理映射坐标定义了该顶点在纹理中对应的2D坐标。通常,这些坐标使用一个二维变量(u, v)来表示,其中u是横向坐标,而v是纵向坐标。因此,纹理映射坐标也被称为UV坐标。
单张纹理
我们通常会使用一张纹理来代替物体的漫反射颜色,在本节中,我们将学习如何在Unity Shader中使用单张纹理来作为模拟的颜色。
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
_MainTex_ST的名字不是任意起的。在Unity中,我们需要使用纹理名_ST的方式来声明某个纹理的属性。其中,ST是缩放(scale)和平移(translation)的缩写。_MainTex_ST可以让我们得到该纹理的缩放和平移(偏移)值,_MainTex_ST.xy存储的是缩放值,而_MainTex_ST.zw存储的是偏移值。
接下来,我们需要定义顶点着色器的输入和输出结构体:
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 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中定义的:
// Transforms 2D UV by scale/bias property
#define TRANSFORM_TEX(tex, name) (tex.xy * name##_ST.xy + name##_ST.zw)
它接受两个参数,第一个参数是顶点纹理坐标,第二个参数是纹理名,在它的实现中,将利用纹理名_ST的方式来计算变换后的纹理坐标。
我们还需要实现片元着色器,并在计算漫反射时使用纹理中的纹素值:
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来计算漫反射光照的结果,并和环境光照、高光反射光照相加后返回。
纹理的属性
我们要为导入的纹理选择合适的类型,因为只有这样才能让Unity知道我们的意图,为Unity Shader传递正确的纹理,并在一些情况下可以让Unity对该纹理进行优化。
Wrap Mode。它决定了当纹理坐标超过[0, 1]范围后将会如何被平铺。
Filter Mode属性,它决定了当纹理由于变换而产生拉伸时将会采用哪种滤波模式。Filter Mode支持3种模式:Point, Bilinear以及Trilinear。它们得到的图片滤波效果依次提升,但需要耗费的性能也依次增大。纹理滤波会影响放大或缩小纹理时得到的图片质量
纹理缩小的过程比放大更加复杂一些,此时原纹理中的多个像素将会对应一个目标像素。纹理缩放更加复杂的原因在于我们往往需要处理抗锯齿问题,一个最常使用的方法就是使用多级渐远纹理(mipmapping)技术
当我们在为不同平台发布游戏时,需要考虑目标平台的纹理尺寸和质量问题。Unity允许我们为不同目标平台选择不同的分辨率
如果导入的纹理大小超过了Max Texture Size中的设置值,那么Unity将会把该纹理缩放为这个最大分辨率。理想情况下,导入的纹理可以是非正方形的,但长宽的大小应该是2的幂,例如2、4、8、16、32、64等。如果使用了非2的幂大小(Non Power of Two, NPOT)的纹理,那么这些纹理往往会占用更多的内存空间,而且GPU读取该纹理的速度也会有所下降。有一些平台甚至不支持这种NPOT纹理,这时Unity在内部会把它缩放成最近的2的幂大小。出于性能和空间的考虑,我们应该尽量使用2的幂大小的纹理。