卡通渲染尝试

(保密原因省略了一些截图)

参考图分析

最近在看进击的巨人动画,尝试做一下这个风格的卡通渲染。这篇文章记录了各种波折的过程,所以会有点啰嗦~

首先截了一张有代表性的参考图:

概括一下这个风格:

色块

是硬边,且分为两类

  • 第一类:原色与暗色
    • diffuse亮部是原色(颜色贴图的颜色)
    • diffuse的暗部+阴影是暗色(原色的一个深色调)
  • 第二类:有边缘光与无边缘光

两类颜色做了某种混合之后,最后总共会出现四种颜色:没有边缘光的亮部和暗部、有边缘光的亮部和暗部

描边
  • 大部分是粗细均匀的黑色描边
  • 头发处是描边较细,颜色是头发颜色的加深
各向异性高光
  • 头发高光,也是硬边
  • 还有各向异性的边缘光
排线
  • 某些比较重的阴影处,如眉毛下方

方案构思

描边

先用backface的方法看看,针对表现出来的缺陷想办法改善。

是否用ramp图做映射

我一开始没有考虑用ramp图去映射不同色块颜色,因为感觉使用贴图的话,一方面增加包体大小且采样降低效率,另一方面感觉不方便调整,比如想改颜色或者调整边缘柔和程度都要重新导出一张贴图。但是了解过后,发现美术更习惯于在PS中画,在引擎中调整的体验并不好。这里打算先不用ramp写shader,之后再根据实际情况调整,加上ramp图或者在Unity中做个工具。

暗部颜色的选取

因为暗部颜色是原色基础上的一种加暗,和原贴图上图案的颜色都有关系,所以应该在原色基础上乘一个灰色,如果希望这个颜色偏冷,那么乘一个偏蓝色的灰色,而不是直接给出一个暗部颜色。

边缘光强度的衡量

首先肯定需要用到dot(Normal, View),另外亮部比暗部更容易有边缘光,所以采用(1 - NdV)*NdL。

最后根据这个方案写Shader,关键代码如下:

1
2
3
4
5
6
7
8
9
10
//边缘光
float rim = (1.0 - NdV) * NdL;
//暗部和阴影
fixed shadow_atten = SHADOW_ATTENUATION(i);
float gray = saturate(NdL * shadow_atten);
//_StN和_NtR是区分不同色块的参数,调节这两个值可以扩大或缩小暗部和边缘光的范围
fixed4 shadowCol = step(gray, _StN) * _ShadowColor;
fixed4 albedoCol = step(_StN, gray) * fixed4(1,1,1,1);
fixed4 rimCol = step(_NtR, saturate(rim)) * _LightColor0 * _RimPower; //rimpower调节亮度大小
return (shadowCol + albedoCol + rimCol) * albedo;

结果是这个样子,发现锯齿比较严重

改善锯齿

思考原因,应该是因为使用step,使得颜色发生突变。比如我在ps中画个黑色的圆

可以发现像素点中不止是黑色和白色,还有几种灰色,这些色块使得锯齿感减弱。而使用step的方法,就使得颜色非黑即白,不存在灰色,那么锯齿感就比较重。

于是使用smoothstep,将smooth的区间调到很小,既有硬边的感觉,锯齿感又比较弱。

奇怪的边缘光形状

看实际模型上的效果不太明显,看材质球能发现边缘光似乎怪怪的

边缘光的宽度在暗部的减弱感觉非常不自然,似乎需要调节一下rim = (1.0 - NdV) * NdL这个算法,于是接下来就为数学头秃了一段时间……最后忍不住上网看看别人的边缘光算法。

修改边缘光形状

网上搜索时看到一篇论文,对我有非常多的启发和帮助。
Pascal Barla, Joëlle Thollot, Lee Markosian. X-Toon: An extended toon shader

我们平常所见的ramp图,都是一维的,uv中只有一个轴用到,这篇论文中,把我们前面提到的两类颜色分别放在贴图的两个维度,u轴映射diffuse(NdL),v轴映射边缘光(NdV)

我们前面提到的暗部高光区域较小,也直接可以通过这张ramp图来体现,通过这张图,对边缘光的控制更加精确。

尝试了一下红色ramp图的边缘光曲线,效果见下图,左边是smoothstep,右边是二维ramp图,可以发现亮部的边缘光区域大小相同,但是暗部的边缘光区域明显变得自然了,尤其可以看布料顶端,另外材质球上的表现也变自然了。

这里出现的颜色都是贴图上直接采样得到的,但是由于ramp图的颜色还需要跟一张颜色贴图相乘来获取图案信息,而亮部不可能通过两个颜色相乘出来,所以只好将边缘光的部分画在a通道中,暂时没想到更优雅的办法。

在人物模型上试验一下ramp和albedo图的结合,将边缘光区域大小调节的差不多,差别比较大的是脖子部分,左边图采用用来的做法脖子左右的边缘光显得比较怪异,新的做法就没有这个问题。
(截图待补充)

头发的描边

前面的描边都采用backface的方法,在这个模型和这个描边粗细的情况下看起来问题不大,但是到了头发上,问题就变明显了
可以看到由于发尾比较尖细,描边断开的情况特别明显。先尝试了一下用边缘光的方法做描边:

可以发现描边粗细不均匀,很多应该有描边的地方没有,还在一些奇怪的地方出现深色。于是尝试将所有发尾顶点的顶点色改成黑色,并将面片外扩距离乘上顶点色,使得发尾强行并起来,得到了这样的效果:

效果比起前两个好了许多,就是增加了美术的工作量。

头发高光

取binormal(发根到发尾),用一张noise图根据法线方向做偏移,偏移后的binormal和高光的关联公式是

1
2
3
4
5
6
7
fixed hairspec(float3 T, float3 halfDir)
{
float TdH = dot(T, halfDir); //这里是钝角,不要随手saturate
float sqrtTdH = sqrt(1 - TdH * TdH);
float atten = smoothstep(-1, 0, TdH);
return atten * pow(sqrtTdH, _Glossness * 64.0);
}

另外有参数NoiseIntensityX和NoiseIntensityY,前者调解抖动波纹的密集稀疏,后者调节抖动幅度。Brightness调节高光亮度,Offset可以偏移高光位置。

效果是这样,问题在于高光的边缘比较粗糙,而材质球上的预览效果则没有这种问题,看起来精度比较高,几乎是和参考图一样的效果,猜测是因为模型精度低还没用法线贴图的原因。