本文仅限于学习参考交流,请勿做商业用途和随意转载。
好久没发文了。
之前一直都在忙着准备NDC的演讲,然后又出去旅游了一下,期间也自学了一些落下的功课,所以隔这么久才发新文。但这次给大家分享的主题非常有趣。
本文我会分析和实现Jack He先生在Unite2018上关于《崩坏3》:在Unity中实现高品质的卡通渲染的演讲内容。在实现过程中,也顺便研究了一下其他卡通渲染方式。
Unite讲座视频 - https://www.youtube.com/watch?v=egHSE0dpWRw
本文的目录如下。
1. 利用Floor实现卡通渲染
2. 利用Ramp实现卡通渲染
3. 利用IF实现卡通渲染 + Shadow Threshold
4. 崩坏3中的卡通渲染
那么,进入正文内容了!
其实,在实现1,2,3步之前,要先用灯光矢量(light vector)和法线矢量的点积(dot)来实现Lambert和HalfLambert。但,
这部分我之前已做过分享,本文不再赘述,详细请见下文链接。
https://blog.naver.com/mnpshino/221441167376blog.naver.com其次,关于如何实现轮廓线,此前也做过分享,不再重复说明,详细请见下文链接。
(https://blog.naver.com/mnpshino/221495979665)
1. 利用Floor实现卡通渲染
先来了解一下Floor函数的定义。Floor函数是指“向下取整”。(四舍五入函数是Round,天花板函数是Ceil)
Floor指“地板”,字面意思一目了然。大家可以理解为"下楼梯"。
0.1, 0.2, 0.3... 0.9等向下取整的话,就是0。大家可以把“向下取整”理解为直接去掉小数部分便可。
那么,从0到0.9999...的下取整就是0吧?我们先举个例子,分为5个阶梯来看。
HalfLambert取0-1之间的值,乘以5,就得到0-5。使用Floor函数,恰好直接就能得到不带小数点的整数0,1,2,3,4,5。刚才乘以了5,现在我们再乘以0.2(相当于除以5),同时把颜色Range重新设置为1,得到0,0.2,0.4,0.6,0.8,1。
上面的图表虽然是3个阶梯...咱先不管这些,现在能得到上图的形状。用Shader实现一下。
在上面,我先任意将阶梯设置为5,但在实现过程中,添加 _StairNum变量的话,就能动态调整阶梯。这里需注意一下,不把光照模式(LightMode)写入为ForwardBase的话,法线可能会翻转。
在Pixel Shader中,先乘以 _Stair阶梯,再用floor下降,再除以 _Stair阶梯,便能得到你想要的结果。
调节 _StairNum便能得到上图的明暗阶梯变化效果。该方式的优点在于计算量小,明暗分界清晰,缺点在于推广应用难。
我们接着来看下一种方式。
2. 利用Ramp实现卡通渲染
该方法是Diffuse Warp(Warped Diffuse)方式,在这个方法中使用了Ramp贴图。
贴图如上。
<军团要塞2>最先使用了该Shading方法,也颇具历史意义。不仅可表现鲜明的明暗,还能表现柔和的明暗表现。我也比较常用这个方法。
相关资料 - https://steamcdn-a.akamaihd.net/apps/valve/2007/NPAR07_IllustrativeRenderingInTeamFortress2.pdf
大家可以结合上面的Ramp贴图和下方的实现效果一起看下,就能知道Ramp贴图是如何影响结果的。我们可以这样理解,采样RampTexture时,把HalfLambert值应用于UV上,HalfLambert值是暗的话,映射纹理的左边,亮的话映射纹理的右边。
除了横轴对应HalfLambert的方法之外,我们可以通过灵活处理纵轴,来得到不同的效果实现。
如在<崩坏3 MMD>中,利用顶点颜色绘制(Vertex color painting),把UV的Y轴移到顶点颜色值,直接调整软硬明暗效果。(MMDshader和游戏内shader的实现方式不同。)
在<军团DOTA2>中,额外使用了一张Diffuse Warp Mask纹理,
被遮罩的部分,通过采样ramp图来实现明暗渐变。把采样坐标值存入顶点色中更助于优化。
参考Siggraph的Pre-Integrated Skin Rendering资料后发现,他在横向UV上使用NdotL,纵向UV上使用1/半径(曲率),
相关资料 - http://advances.realtimerendering.com/s2011/Penner%20-%20Pre-Integrated%20Skin%20Rendering%20(Siggraph%202011%20Advances%20in%20Real-Time%20Rendering%20Course).pptx
用法线偏导数除以位置偏导数的值。这里出现了微分的概念,大家可能会觉得有点难,其实大家简单理解为“变化量”就行。较之位置,法线的变化量可理解为在眼,鼻子,嘴附近增强了SSS的红润气色。(P.S:法线变化量大,说明凹凸明显,不平坦,这些位置通常肉都比较薄,比如耳朵、鼻子、眼皮、嘴唇这些地方,所以会更透光,透出的光带有血色,所以显得红润。)
fwidth的定义如下。
此外,为了更方便使用,我们在纵轴上使用NdotV去采样BRDF图。现在我们来实现一下Ramp方法。
在属性中声明名为 _RampTex的sampler2D变量,便可使用。
float2(halfLambert, 0) <- 这部分是uv,目前纵轴是0,希望大家可以尝试用其他办法来做些变化,应该能得到很多有趣的效果。
我先用了上面的纹理,适用于球和石像上。
下面是全文的重点。
3. 利用IF实现卡通渲染
大家可能会提问,在shader里面用IF的话会不会太慢了,之前在实现蜘蛛侠风格的shader时,已经给大家分享过了Step()解决方法,详情请参考前文。
其实,IF方式的代码很简单。
如果HalfLambert值小于0.5,则乘以阴影颜色,并将其写入shader,即可。
Guilty Gear Xrd中也用了此方式处理。但是,如左图所示,AO乘以顶点颜色绘制的值,使用加权值。
4. 崩坏的卡通渲染
<崩坏>是如何实现明暗呢?先来看下Unite上分享的内容。
如演讲所述,崩坏中使用了Lightmap,这是崩坏in game shader的核心。把Specular贴图和Shadow Threshold贴图逐通道放入名为Lightmap的Mask纹理中打包,为了解每个通道的作用,我们将纹理分为R,G,B来看。
通过观察贴图和上面的模型,我认为R,B通道可以看成高光相关的通道,G通道用来调整明暗的权重。因为使用这种卡通渲染方法,高光的使用极度受限。它用于金属材质、弯曲部分和皮革材质的表现。
R通道
仔细观察R通道,会发现一个材质中的所有数值都是一样的。这个材质中的光泽度全部处理成了一样的,所以我认为这是Specular Glossiness(也叫Power)。
B通道
B通道主要强调弯曲部分。可理解为是Specular Masking通道。那么,G通道的作用就非常重要了。
G通道
G通道的底色(base color)是中间灰。中间灰就是0.5。
0.5...? 忽然灵感噼噼啪啪一闪而过(233...)。Threshold意思是“阈值”。G通道的作用就是阈值(Threshold),用于调节明暗。用Threshold贴图代替0.5。
代码如上,非常简单吧。运行的话,
使用了Threshold的模型,受头发影响,用加权值调整脸部出现的明暗。
(只要超过0.2或0.1,就会变暗)
所以,我们可以通过不断调整,直至得到满意的阴影形态。在一些需要细腻表现明暗的部分,我们可以得到和法线贴图一样的效果。高光在运行R和B通道后得到的结果数值,仅在超出某个变量的部分,表现为高光颜色(SpecularColor)。通过这个方法就能得到崩坏游戏中的shader效果。
最终效果图
大家照这个方法来实现的话,会发现技术其实不难。而且就算不用Shader,它的基础模型的品质非常棒。虽然我们常说Shader有妙手回春之效,但<崩坏>让我重新意识到高品质的模型对于一款优秀的作品来说至关重要。因为如果基础模型的品质不行的话,不管shader如何厉害,也有无力回天的时候。同时我也再次认识到,卡通渲染这块真的需要TA和模型师通力合作才行。
那,我们下回再见,谢谢~~
[文章来源] 붕괴3 방식의 카툰렌더링 구현하기|作者 Madumpa
译 Qinfei