卡通着色器的实现一般是描边加上一张渐变贴图对顶点亮度值进行离散化,按照此思路我们在unity shader里来进行实现。
描边
在shader里对模型进行描边有好几种思路。
第一种是使用屏幕后处理,用一个卷积来查找物体边缘部分并进行描边着色,不过效果并不好,在只能得到图像数据的情况下又需要进行描边才这样处理。
如果可以直接得到模型数据,当然是直接用模型数据进行处理。用模型数据进行描边也有好多种思路,例如获取摄像机法线纹理对相邻顶点的法线进行点积,角度超过阈值就判断为边缘部分。
我们这里还是用最简单的一种方式,直接用单独的一个pass,把物体顶点朝法线方向进行挤出。
需要单独定义一个pass,关键代码也很简单
float3 pos = v.vertex + v.normal * _OutLine;
另外还需要注意需要打开正面剔除,因为如果直接在所有方向把物体顶点朝法线方向进行挤出的话,描边颜色会完全覆盖物体,所以我们这里只对物体背面进行挤出,所以需要打开正面剔除
Cull Front
该pass单独效果如下,目前就是纯黑色(我们定义的描边颜色)看不出什么效果,要再附加正常的光照上去才能看出描边效果
在unity shader入门精要中也有另外一种稍微复杂一点挤出顶点的方式。是通过先变换模型到观察空间,变换法线之后再进行挤出
float4 pos=mul(UNITY_MATRIX_MV,v.vertex);
float3 normal=mul((float3x3)UNITY_MATRIX_IT_MV,v.normal);
normal.z=0.5;
pos=pos+float4(normalize(normal),0)*_OutLine;
o.vertex=mul(UNITY_MATRIX_P,pos);
渐变映射
接下来第二个pass我们要绘制物体受到光照后的效果了。主要思路是将物体的亮度离散化,从而达到卡通中亮度缺少过渡的那种效果。
离散化也有两种方法,第一种是使用代码手动离散化例如
if(x>1) x=1 不过亮度级别过多的话就比较复杂
第二种就类似查表了,使用一张渐变纹理来进行映射,我们这里使用第二种方法,使用的渐变纹理如下
接下来就是正常的lambert光照模型的计算,lambert光照模型会得到一个亮度值,将亮度值进行一个映射,从单位向量点积结果的[-1,1]映射到纹理uv坐标的[0,1],然后使用这个亮度值对这个纹理贴图进行采样。
fixed value = dot(worldNormal, worldLightDir) / 2 + 0.5;
float3 diffuse = tex2D(_RampTex,fixed2(value, value)) * col * _RampColor;
效果如下
高光
最后还需要加上镜面反射也就是高光。
高光也需要进行一个离散化处理,也有不同方法。首先是可以重新为高光也定义一张纹理贴图,但是一般来说高光也只有一个颜色,所以直接在代码里进行处理就好了