找回密码
 立即注册
查看: 237|回复: 2

[译] Unity shader 实现轮廓线效果_完整篇

[复制链接]
发表于 2022-12-5 13:34 | 显示全部楼层 |阅读模式
这段时间一直都很忙,抽空找了个周末时间写了这篇文章。今天我想给大家分享一下在shader中绘制轮廓线的原理、实现方法和一些小建议。我会基于崩坏3 in-game shader的演讲内容来尝试实现轮廓线效果,这期间也得到了很多灵感,感谢yo米哈游yo!本文的大纲如下。

  • 通过在3D MAX中生成轮廓线来了解原理
  • 使用Unity shader生成轮廓线
  • 制作平滑法线并将其添加至切线
  • 透视校正
  • 使用顶点色控制轮廓线的粗细
  • 在3D Max中用HLSL DirectXShader制作轮廓线shader

1. 通过在3D MAX中生成轮廓来了解原理
我们先来一起了解下在3D图形中生成轮廓线的原理。我先打开了3D Max,做了一个水壶(如下图所示)。


激活水壶的背面剔除功能(Backface Cull),然后再复制了一个相同的水壶,再把它的面翻转过来了。


接着在复制好的第二个对象上,使用Push来扩张顶点的位置,并把材质设置为黑色...


这样,我们就得到了轮廓线。(为了让大家看得更清楚一点,我故意把轮廓线加粗了)这就是实现轮廓线的原理。
1.不是在一个物件上直接生成轮廓线,而是利用两个物件:实际物件和轮廓线物件。(即绘制两次)
2.翻转轮廓线物件的面(剔除正面),再扩张顶点。
3.把实际物件放在轮廓线物件上面。
以上是绘制轮廓线的原理,接下来我们看下怎么在Unity里具体实现吧。

2. 使用Unity shader生成轮廓线
在Unity shader中,如果我要用2pass,得先做些准备。首先写个空的surface shader,


如上图所示,添加一个片元pass。大家可能会问,为什么下面使用surface,上面用fragment呢?因为我想给大家看下,在Pass不同的情况下,surface方式和fragment方式可以混用。(还因为,之后轮廓线shader会利用矩阵处理各种计算,而Surface有部分会自动计算矩阵,所以我没有使用surface。)
Surface和fragment的不同点,片元shader必须要储存在pass{}中,Surface shader则没有这个限制。而且,第一个pass使用front剔除,第二个pass使用back剔除。跟上面讲的在Max中翻面是一回事。这里,我可以使用vertLine和fragLine函数代替 '//内容(绿色注释)'这部分。


添加轮廓线的粗细和颜色变量(请在属性中也添加哦),并使用appdata_full结构体。如果大家查看官方的文档,会发现其结构如下。



[译] -appdata_base:顶点由位置、法线和1个纹理坐标构成。-appdata_tan:由顶点位置、切线和1个纹理坐标构成。-appdata_full:顶点由位置、切线、法线、4个纹理坐标和颜色构成。

后面我会讲到,因为等会要实现的轮廓线需要用到“切线(tangent)"和顶点色,所以我会选择用appdata_full,而不是appdata_tan。我不需要用到4个纹理坐标,所以其实最好是自定义appdata,但之后再详细解释吧,今天我就先用已有的结构。


上图红框标记的o.vertex,如上图结构体所示,使用了SV_POSITION语义,大家可以理解为这部分代表顶点位置的输出。再搬下上面的代码用下,见下图。


在使用UnityObjectToClipPos变换模型视图投影矩阵之前,沿法线方向扩张对象空间中的顶点位置。使用这种方法便可绘制出轮廓线。


为了进行测试,我准备了上面这个模型。用两种明暗色调简化这个模型后,添加轮廓线,


效果如上。我不太满意现在法线断断续续的效果,所以接下来我会在第3部分来解决这个问题。

3.制作平滑法线(smooth normal)放入切线
为什么轮廓线会断断续续呢?因为顶点上的法线不止1个,而是被分成了无数个方向(如左下方图所示)。



左图-硬边(法线朝多个方向),右图-软边(平滑法线)

利用Max中的光滑组可以合并法线,效果如右上图,但这样一来,对象的硬边就被柔化了。所以现在有点尴尬,如果我想实现利索的轮廓线效果,就没办法得到硬边;保留硬边了就实现不了连贯的轮廓线效果。
酱紫,我思考了一下,是不是可以只在“轮廓线对象outline object”上应用平滑法线(smooth normal) 的mesh,而硬边在“实际对象object”上使用实时法线,这样是否可以两全其美呢?
如果大家想得到应用了“平滑法线”的mesh的话,我们可以写脚本来解决这个问题。我先创建了一个Extension类,然后在里面创建一些函数(如下图所示)。


简而言之,就是通过读取mesh信息,对使用了相同顶点ID的所有顶点法线求和后,再做除法运算求得平均值。然后,为了生成mesh,再创建了一个Outline_AvgNormal脚本,并在Awake()里面计算。


照这个方法,就能得到连续整齐的轮廓线了。如下所示。


还有一些其他的资源。


通过平滑法线方式,我们可以得到不错的连贯轮廓线效果,但是其实有些资源已经实现好了这些功能,我们可以直接拿来用。比如说Toony Color Pro2这个资源,


大概20美金,但物有所值。我们可以使用资源里面的Smoothed Normals Utillity,强制把平滑法线(Smooth normal)放进顶点色、切线和UV2中。


我把Smooth normal放进了切线里面,


shader代码也换成了切线,然后把刚创建的平滑mesh放入mesh filter的话...


zang! 同样可以得到一模一样的效果呢~

4. 透视校正
虽然现在我求得了平滑法线,但有一点还是不太满意,即,靠近镜头的轮廓线会比我的预期要更粗一些(如下图所示),


我们要把这种线条叫做轮廓线好像也不是很合适。所以,我想再来实现下“随着镜头靠近,轮廓线会变细些”的效果。先改下代码吧。


一般,法线乘以 IT_MV 矩阵并在世界空间中计算。如果不这么做的话,就需要乘以MVP矩阵,为了方便在屏幕空间中进行计算,并把它放入ClipNormal变量中。这样处理后,法线就是基于屏幕空间了,所以我只需要在xy值上加上扩张值就行了。那怎么求扩张值?解决这个问题最重要的核心就是 o.vertex.w 了!
大家应该都比较了解顶点xyz,那有人可能会问w是什么呢?




大家请看上图,可知(x,y,z,w)实际是 (x/w, y/w, z/w, 1)这样计算的。即w就是透视值。
因为w是被除数,所以越靠近镜头,xyz就会被无限放大。当w值为0时,越靠近摄像机;而w为1时,就越远离摄像机。(请注意,这跟在ComputeScreenPos中除以w值不一样!)
另外再补充一点,Clipposition的w值等于摄像机空间的z值。


在fragLine函数中,如果我们把w值设为颜色来做debug的话,就能看到如下图轮廓线随距离变化而不同的效果。


因为对象越靠近摄像机,相乘的值越接近于0,所以越靠近摄像机,Width值(宽度)会越细。


放大来看的话,效果也不吓人。


动图的效果如上,大家可以感受一下。通过除以_ScreenParams,既可得到长宽比,也能得到0~1范围的值。

5. 使用顶点色控制轮廓线粗细
虽然现在我已经做好了轮廓线,但假如轮廓线粗细一样的话,效果会有点无聊。为了做出偏漫画风的轮廓线(G-pen效果),就需要能调整下轮廓线的粗细。在透视校正中,给xy值添加Offset值,将其乘以顶点色的话,我们就可以调节轮廓线的粗细了。


我用Polybrush工具来绘制顶点。(这个工具很好用,虽然还没完全做好,会有点小bug)


用这个工具绘制的话,


线条的节奏感就出来了,也能实现各种不同粗细的轮廓线。

6. 在3D Max中使用HLSL DirectX Shader制作轮廓线shader
大家可以在3D Max看到用DirectX编写的实时shader。可能有读者会提起疑问,我们明明可以在Unity里直接生成轮廓线,为什么非得要在Max里面折腾。我的理由如下。
1.如果我们在Max中重新更改模型再导入Unity的话,那之前在Unity中设置好的值就会丢失,所以我们要提前在Max中把值设置好。
2.在Max中操作,可以方便我们边预览边手动实时调整效果,估出一个标准值,来调节轮廓线的粗细。这也是使用Max的优点。


我们可以在材质浏览器中点击“Standard”按钮,把它换成DirectX shader,就能实时确定shader。在3DMax\Maps\fx\文件夹中,有如下的基本预设。


我来拆解一下这个shader,感觉做点改动,就可以马上搞定了。如果大家之前试过这么做的话,应该会了解,Max版本越新,使用DirectX9做的shader就看不到属性了。



对比下default和default10 shader,大家会看到Parameters那部分不太一样(如上图所示)。其实老办法更直观也更方便写代码,但由于我需要用DX10(11)来写代码显示参数,所以我会用default10来做下修改。
读下代码的结构,顶部声明了参数,下方声明了语义,再下面定义了顶点函数和像素shader函数等等,这些跟Unity shader都十分相似。而且在代码最末端,大家可以留意下technique,其功能相当于Unity的Subshader。大家看右上方图片中有technique下拉菜单吧?technique的默认选项是Shaded。


我来改下代码(如上)。
首先添加如下参数设置,


为轮廓线编写顶点shader和像素shader。


顶点shader差不多就这样了。初始化output结构体,扩张顶点位置,通过顶点色控制粗细,变换MVP...虽然我用input颜色来控制轮廓线的粗细,但由于我不会用到output颜色,所以out.col我写成了float4(0,0,0,1)。


像素shader就更简单了,把它和上面的shaded technique连接起来就行。


我把之前的p0改成了p1,再添加了p0,再把剔除模式改成了DataCulling。


大家看上图代码会发现,DataCulling是front culling,而且这里用的是vsLine和psLine函数,不是vs, ps。


这样一来,我们在Max中也能使用实时shader和multi-pass来绘制轮廓线了。


也可以用顶点绘制(vertex paint)来调节粗细了。
基于上面的分享,我相信大家会能做应用透视校正和平滑法线的Max HLSL shader了(这部分留给大家自由探索咯)。那今天我的分享就到此结束了,谢谢大家阅读!

[原文链接]    |作者 Madumpa
本文仅限于学习参考交流,请勿做商业用途和随意转载。

译 Qinfei
20200528

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-12-5 13:36 | 显示全部楼层
本来想翻译这个棒子的文章~原来有人翻译啦,真棒
[欢呼]
发表于 2022-12-5 13:41 | 显示全部楼层
谢谢分享,最近看到的最好的一篇outline文章。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-4 07:36 , Processed in 0.540616 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表