积雪Shader
如果你是一个shader编程的新手,并且你想学到下面这些酷炫的技术,我觉得你可以看看这篇教程:
- 实现一个积雪效果的shader
- 创建一个具有凹凸纹理的shader
- 为每个像素修改其对应纹理值
- 在表面着色器中修改模型的顶点数据
引论
这是我们系列教程的第二部分,我们将在此部分实现些有用的技术。在学习完第一部分的所有背景知识后,我们将利用所学的知识实现一个简单的积雪效果的shader。效果如下:
准备工作
我们想做的其实很简单,简单介绍一下:
- 随着Snow Level(表示积雪的程度,该值越大,积雪越深)的增大,我们将迎着下雪方向的岩石纹理区域都变成Snow Color(雪的颜色)
- 随着Snow Level的增大,我们想将该岩石模型稍微变大一点,尤其是雪的边缘厚度要增加,这样给人真正的“积”雪效果。
步骤1 – Bumped Diffuse Shader
我们创建一个新的Diffuse shader(默认新建的shader基本都是Diffuse shader),并向其中添加凹凸纹理效果(来自猫大的原话:法线贴图是凸凹贴图(Bump mapping)的一种常见应用,简单说就是在不增加模型多边形数量的前提下,通过渲染暗部和亮部的不同颜色深度,来为原来的贴图和模型增加视觉细节和真实效果。简单原理是在普通的贴图的基础上,再另外提供一张对应原来贴图的,可以表示渲染浓淡的贴图。通过将这张附加的表示表面凸凹的贴图的因素于实际的原贴图进行运算后,可以得到新的细节更加丰富富有立体感的渲染效果。)。
Shader "Custom/SnowShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} //新的凹凸纹理贴图 _Bump ("Bump", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; //必须添加一个与Properties代码区中的同名的_Bump变量,作为Properties中_Bump的引用。
//具体缘由详见教程第一部分。 sampler2D _Bump; struct Input { float2 uv_MainTex; //用来得到_Bump的uv坐标 float2 uv_Bump; }; void surf (Input IN, inout SurfaceOutput o) { half4 c = tex2D (_MainTex, IN.uv_MainTex); //从_Bump纹理中提取法向信息 o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump)); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
Unity已经在新建的shader中帮我们自动添加了很多代码,我们只要添加凹凸纹理相关的代码即可。
在上述代码,我们完成了一下内容:
- 定义了默认值为“bump”的2D类型的纹理_Bump,初始值为空。(默认值为“bump”的2D类型纹理本质就是凹凸纹理类型)
- 在CG代码区域中(CGPROGRAM…ENDCG)中添加了与_Bump同名的sampler2D变量,该sampler2D _Bump做的事情就是再次声明并链接了_Bump,使得接下来的CG代码区域能够使用这个变量。
- 在Input结构体中添加一个变量float2 uv_Bump来获得_Bump纹理的uv坐标。
- 在surf函数中添加UnpackNormal函数来获取对应像素的法向值(因为该surf函数时每个像素调用一次)。注意我们先使用tex2D函数来计算当前像素的具体值(tex2D作用仅仅是通过一个二维uv坐标在纹理上获取该处值,根据纹理的类型不同,获取的值的含义也不一样,比如bump类型纹理上存储的值代表的含义是该点的法向量,而普通纹理一般代表的是该点的颜色值),具体过程是根据对应uv坐标(IN.uv_Bump)在bump类型纹理(_Bump)上得到该点存储的值,注意此时得到的值是一个fixed4的值,但是法向值只需要fixed3类型,所以需要UnpackNormal进行转换(Unity3d的标准法线解压函数 — fixed3 UnpackNormal(fixed4 packednormal))。
经过上述步骤,我们得到了一个很普通的bumped shader。
我们将该shader应用到我们的模型上,并赋予相对应的纹理贴图,具体代码和资源请戳这里。
我在SnowRock文件夹下创建了名为SnowRock1的材质和shader,将我们的代码粘贴到SnowRock1.shader中,并将该shader拖到SnowRock1材质上。
随后我们将Free_Rocks –> _models中的cliff拖到Scene面板中,再将SnowRock1材质赋给该模型。此时我们还看不出任何变化,因为我们还没有给shader中Properties中那两个纹理变量赋值。
此时我们可以看到岩石效果的变化。
为了体现bump纹理的作用,我们将使用bump纹理和不使用bump纹理的岩石做下对比。
步骤2 – 在岩石上添加些雪
接下来我们要做的就是计算对应像素的法向量是否与下雪的方向一致(如果一致,那么就将该像素置为雪的颜色,这个道理和平行光的原理很类似)。
我们使用点乘(dot product)计算方向是否一致。两个单位向量之间的点乘等于这两个向量之间夹角的余弦值。CG中自带了一个dot函数为我们做两向量的点乘计算。这样的话,当我们计算出的点乘结果为1,则说明两向量间余弦值为1,即两者夹角为0,说明两者方向一致,而结果为-1时,说明两者方向相反。所以我们不必算出最后地角度,仅仅通过该点乘结果就可以判断两向量之间方向的关系。
单位向量是指大小为1的向量,也就是说当向量v满足sqrt(v.x*v.x+v.y*v.y+v.z*v.z)=1时,此向量为单位向量。不要认为单位向量就是(1,1,1)啊!!!
如果要使用点乘的结果来表示两向量之间的角度,首先要将两个向量的长度变为单位长度,也就是将两个向量变到单位向量再计算点乘。
知道这些后,我们在shader中定义以下属性。
Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Bump ("Bump", 2D) = "bump" {} _Snow ("Snow Level", Range(0,1) ) = 0 _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0) _SnowDirection ("Snow Direction", Vector) = (0,1,0) _SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1 }
在上面代码中定义了一下变量:
- _Snow 表示覆盖在岩石上雪的数量,范围从0~1.
- _SnowColor 表示雪的颜色,默认为白色。
- _SnowDirection 注意!!!此处表示的其实是雪落下方向的反向,比如我们默认雪是从天空直接落下来的,理论上应该为(0,-1,0),但是此处取得是它的反向,用的是(0,1,0)
- _SnowDepth 我们将在步骤3中使用到该变量,它表示雪的厚度,范围从0~0.3
根据第一部分所学的内容,我们接着在下面的CGPROGRAM…ENDCG代码中添加如下变量:
sampler2D _MainTex; sampler2D _Bump; float _Snow; float4 _SnowColor; float4 _SnowDirection; float _SnowDepth;
现在,除了纹理以外,我们把其他属性都当做float类型进行处理。
接着我们要更新我们shader中的Input结构体。此处要注意,我们不能直接用每个像素的法向值,因为法向贴图(normal map)为我们计算的每个像素法向值是在切空间的法向值,所以我们必须重新计算出每个像素在世界坐标系下真正的法向值,以此来和SnowDirection进行比较来决定此处是否覆盖上雪的颜色。
得到世界坐标系下的法向量还是有点麻烦的,为此我还特意看了下官方文档。因为我们的表面着色器写入了o.Normal,所以按照第一部分的讲解,我们需要使用INTERNAL_DATA来计算世界坐标系下的法向量,此处我们使用了WorldNormalVector函数。
我们将这些数据变量放到Input结构体中:
struct Input { float2 uv_MainTex; float2 uv_Bump; float3 worldNormal; INTERNAL_DATA };
最后我们完善surf函数,结束我们的shader程序。
void surf (Input IN, inout SurfaceOutput o) { //该像素的真实颜色值 half4 c = tex2D (_MainTex, IN.uv_MainTex); //从凹凸贴图中得到该像素的法向量 o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump)); //得到世界坐标系下的真正法向量(而非凹凸贴图产生的法向量,要做一个切空间到世界坐标系的转化)和雪落 //下相反方向的点乘结果,即两者余弦值,并和_Snow(积雪程度)比较 if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow)) //此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于 //lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大, //积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色 o.Albedo = _SnowColor.rgb; else //否则使用物体原先颜色,表示未覆盖积雪 o.Albedo = c.rgb; o.Alpha = 1; }
现在你们肯定迫切想知道if语句中到底做了哪些事情?
- 得到雪落下相反方向的向量和该点世界坐标系下的法向量之间的点乘结果。此处注意使用的法向值是利用WorldNormalVector函数将切空间的法向值转化到世界坐标系下而得来的,另外提一点,切空间的法向值其实就是所使用的凹凸贴图法向值。
- 经过点乘之后,我们得到一个1到-1之间的值,越接近于1,说明该点法向与雪落下相反方向越一致,当为-1时,说明两者方向相反。
- 然后我们将点乘结果和一个插值结果进行比较,该插值使用了函数lerp(1,-1,_Snow)。实际上该函数结果为1+(-1-1)*_Snow=1-2*_Snow,所以当_Snow为1时,函数结果为-1,而当_Snow为0时,函数结果为1。注意为了符合正常的自然现象,我们_Snow一般只取0~0.5,因为大于0.5时,插值的结果将小于0,会造成雪好像穿过了岩石,落到了岩石的后面。这个道理和光照的道理一样,物体背面是见不到阳光的。
- 当该点求得的点乘结果大于插值范围,将显示积雪颜色,反之显示原先的纹理颜色。
下面使我们完整的积雪效果shader:
Shader "Custom/SnowShader2" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Bump ("Bump", 2D) = "bump" {} _Snow ("Snow Level", Range(0,1)) = 0 _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0) _SnowDirection ("Snow Direction", Vector) = (0,1,0) _SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; sampler2D _Bump; float _Snow; float4 _SnowColor; float4 _SnowDirection; float _SnowDepth; struct Input { float2 uv_MainTex; float2 uv_Bump; float3 worldNormal; INTERNAL_DATA }; void surf (Input IN, inout SurfaceOutput o) { //该像素的真实颜色值 half4 c = tex2D (_MainTex, IN.uv_MainTex); //从凹凸贴图中得到该像素的法向量 o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump)); //得到世界坐标系下的真正法向量(而非凹凸贴图产生的法向量)和雪落 //下相反方向的点乘结果,即两者余弦值,并和_Snow(积雪程度)比较 if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow)) //此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于 //lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大, //积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色 o.Albedo = _SnowColor.rgb; else //否则使用物体原先颜色,表示未覆盖积雪 o.Albedo = c.rgb; o.Alpha = 1; } ENDCG } FallBack "Diffuse" }
使岩石随积雪厚度变形
最后一步是将岩石模型沿着下雪的方向变厚,以表示积雪的厚度。
为了达到此效果,我们需要修改模型的顶点数据 - 意味着我们要在表面着色器中写一个修改顶点的函数。
#pragma surface surf Lambert vertex:vert
我们在上述代码的末尾加入了vertex:vert表示我们将使用自己定义的vertex函数,此函数名称为vert。
我们的vertex函数如下:
void vert (inout appdata_full v) { //将_SnowDirection转化到模型的局部坐标系下 float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection); if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3)) { v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow; } }
首先我们传给vert函数一个参数appdata_full v,参数的类型为appdata_full(Unity内置类型),该类型包含了纹理坐标,法向量,顶点位置,以及切线信息。如果你还需要使用其他的数据类型,你可以使用自定义的输入结构体作为pixel函数的第二个参数传递额外的信息 — 目前我们不需要这样做。
_SnowDirection使用的是世界坐标系,但是我们需要的其实是模型局部坐标系下的_SnowDirection。所以我们需要先将_SnowDirection转化到模型的局部坐标系下。而我们只需要将_SnowDirection乘以Unity内置矩阵 – UNITY_MATRIX_IT_MV(IT表示Inverse Transpose逆转置矩阵,MV表示 ModelView矩阵,该矩阵表示是ModelView的逆转置矩阵)。
现在我们得到了该顶点的法向量(vert函数应该是对每个vertex调用一次,相对于surf函数对每个pixel调用一次)。我们仍然像上面做积雪效果时那样,将转换坐标系空间后的雪落下相反方向和模型局部坐标系下的法向量进行点乘,得到的结果仍然和一个插值比较。不过此时插值项不再是_Snow,而是_Snow*2/3,这表示只有那些更接近雪落下方向的区域才会增加雪的厚度,更符合自然现象。
而这些通过测试的区域,沿着(sn.xyz+v.normal)方向进行加厚,也就是将其顶点沿此方向伸展一定距离。注意到增厚的程度取决于_SnowDepth和_Snow,而增厚的方向是由物体法向和雪落的方向综合作用的,这也符合自然现象。
我们来对比下不同的积雪深度效果:
我们的shader终于完成了。
源代码
完整的shader源码呈现如下:
Shader "Custom/SnowShader2" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Bump ("Bump", 2D) = "bump" {} _Snow ("Snow Level", Range(0,1)) = 0 _SnowColor ("Snow Color", Color) = (1.0,1.0,1.0,1.0) _SnowDirection ("Snow Direction", Vector) = (0,1,0) _SnowDepth ("Snow Depth", Range(0,0.3)) = 0.1 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert vertex:vert sampler2D _MainTex; sampler2D _Bump; float _Snow; float4 _SnowColor; float4 _SnowDirection; float _SnowDepth; struct Input { float2 uv_MainTex; float2 uv_Bump; float3 worldNormal; INTERNAL_DATA }; void surf (Input IN, inout SurfaceOutput o) { //该像素的真实颜色值 half4 c = tex2D (_MainTex, IN.uv_MainTex); //从凹凸贴图中得到该像素的法向量 o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump)); //得到世界坐标系下的真正法向量(而非凹凸贴图产生的法向量)和雪落 //下相反方向的点乘结果,即两者余弦值,并和_Snow(积雪程度)比较 if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow)) //此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于 //lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大, //积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色 o.Albedo = _SnowColor.rgb; else //否则使用物体原先颜色,表示未覆盖积雪 o.Albedo = c.rgb; o.Alpha = 1; } void vert (inout appdata_full v) { //将_SnowDirection转化到模型的局部坐标系下 float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection); if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3)) { v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow; } } ENDCG } FallBack "Diffuse" }
所有的资源代码我都放到这里了。
最后来张血雨效果图…