- 0
- 0
- 0
分享
- 拒绝干枯毛躁,如何给数字人添加亿点点毛发细节
-
原创 2021-10-14
最新一期的「Unity 大咖作客」分享会上,完美世界的资深技术美术&引擎工程师徐行跟大家详细解读了完美和 Unity 合作的毛发系统。
本文节选了部分精彩内容,完整内容已上传至 Unity 社区中的技术专栏。滑至文末,点击“阅读原文”,即可跳转至技术专栏学习:
https://developer.unity.cn/projects/hairworks
大家好,我是来自完美世界移动项目支持部的徐行。今天跟大家分享一下我在完美世界研发的 Unity 毛发系统,在讲 PPT 之前先看一个由我的虚拟闺女小花出演的视频。
其实这个作品还在很初期的阶段,还有很大的提升空间,这次主要是让她帮忙展示一下毛发。这个是本工作的大概时间线,其实前期还不配被称之为自研毛发系统,只能称得上是一个英伟达 HairWorks SDK 的集成。随着后期需求越来越深入,自研的部分也越来越多。现在只是思想上部分参考了 HairWorks,代码已经全部重写了。
说到毛发,现在游戏中比较传统的毛发解决方案有两类,第一类就是网格头发,这类头发其实渲染效果还不错,但是往往物理效果不太好,而且也不太适合做短毛。
另一类是 FurShell,专门用来做短毛的。但是凑近了看,层状瑕疵也非常明显。做毛发的终极解决方案,肯定就是基于发丝的毛发系统。这是一些有代表性的案例,比如《最终幻想》、《怪兽公司》、《长发奇缘》、《古墓丽影》和《巫师 3》。
虽然基于发丝的方案效果很棒,但是问题也是显而易见的。那就是发丝数量太多,导致无论是物理模拟还是渲染还是存储等等都有比较大的困难。
HairWorks 是如何应对这些挑战的呢?这就得从它的资源表示方式讲起了。HairWorks 的毛发资源并不是直接存储每根发丝的信息,而是主要存储了两个东西,图中的黄线被称之为导发,图中的网格被称为生长网格。而物理模拟则在导发上进行,发丝则是导发间插值出来的,这样就能极大的减少计算量和存储间的需求。
发丝具体是怎么插值的呢?其实生长网格是由一些三角面构成的。生长网格上面每一个顶点对应一根导发,随机生成一定数量的重心坐标,在渲染的时候就可以利用重心坐标作为权重,在三根导发间进行插值了。
为了让生成的发丝更加平滑,在生成发丝前还可以对导发做一些平滑,使用平滑过的导发进行插值。
物理模拟又是怎么做的呢?HairWorks 选择了一种非常易于理解的物理模拟方法,也就是质点弹簧法。图中的红线就是进行物理模拟的导发,红线上的小圆圈就是质点,发根处的质点是完全受骨骼蒙皮控制的,其他的质点则会受到诸如风力、重力等等的影响,也会跟碰撞体发生碰撞。
为了使基于质点的物理模拟能够体现毛发的感觉,HairWorks 为质点间增加了一系列的约束,也就是俗称的弹簧。比如图左中,同一个头发上相邻质点间有长度约束,太近了会排斥,太远了会吸引,这有助于保持头发的大致长度。
再比如黄色折线,它表示的是完全受骨骼蒙皮控制的毛发。它跟红色这条线,也就是物理模拟控制的毛发间也有约束,这有助于保持美术所制作的造型。
另外,HairWorks 还有一个比较有特色的约束,就是在相邻的导发之间也有距离约束,有助于保持毛发的体积感,一定程度上避免穿插。当然还有很多其他种类的约束,今天时间关系就不展开了。
为了加速物理模拟,HairWorks 的物理模拟是在 Computer Shader 中并行进行的。一个控制点,也就是一个质点对应一个线程,一个导发对应一个线程组,这就便于使用效率比较高的共享存储。
但是有些约束是有先后顺序依赖关系的,例如同一个质点上的两个长度约束,如果调换执行顺序,那么执行结果就不一样了。正确的做法只能是串行解算这些约束,但是串行显然是不如并行快的。为了加速物理模拟,HairWorks 还是想办法做到了并行。
如图,它把长度约束分为两组,一组内每个约束都互不相临。这样一组的约束就可以并行解算,一组解算完之后,再解算另一组,交替迭代几次就能获得比较稳定的结果。
当然后面我们还做了一些物理模拟的拓展和优化,例如加入了 3D 风场以及允许传入不稳定的物理模拟 TimeStep 等等,由于时间关系也不展开了。
接下来讲一下引擎与跨平台,这张图是我们把 HairWorks 集成到自研引擎之后,在《笑傲江湖》和一些 Demo 中的效果。当时另一个端游项目看到了,觉得效果不错,挺想用。他们是基于 Unity 开发的,所以接下来我就开始了向 Unity 的集成。
其实远在我之前,已经有很多团队进行了这项工作。比如图中是 Unity Japan 团队的成果,但是他们做的集成时间都比较早了,当时条件有限,所以也存在一些问题。比如无法使用 Unity 内部的材质,或者光照不全,没有平行光的投影等等。
他们为什么会遇到问题呢?这就要讲到他们集成的实现原理。
他们选择了原生插件这种集成方式,是由于 HairWorks 要用到诸如 Computer、Tessellation 这类很底层的图形功能。所以说使用能达到底层图形设备接口,例如 D3D Device 的原生插件机制还是比较稳妥的。
但是也正是因为他们是使用底层图形设备接口,例如 D3D Device,进行渲染,而不是使用 Unity 提供的接口例如 DrawRenderer,所以 Unity 内的材质、灯光、环境以及渲染管线内部的很多信息,都很难传过去,也就很难在原生插件里面重现 Unity 的渲染效果,所以原生插件渲染出来的东西往往光照不全。
另外由于 Built-In 管线中提供的插入点有限,所以没有办法渲染阴影深度。上述问题如果按原有思路做下去,是很难解决的。但是这些问题不解决又没有办法实际投产,所以这些集成最后基本上就被搁置了。
我最初也是一筹莫展的,差点因此放弃,但是最后还是想到了一个解决方案,那就是既然 Unity 里面的东西很难拿出来,我是不是可以不把他们拿出来放到原生插件里面?而是想办法把原生插件生成的 HairWorks 的几何信息传到 Unity 里面,这样就可以在 Unity 里做光照了,这样可行吗?
我想的办法其实很简单,我给它起名叫渲染代理。所谓的渲染代理其实就是一个在 Unity 里面创建的头发的包围体,原生插件把 HairWorks 的几何信息渲染到了我自己创建的 GBuffer 中。
然后渲染代理挂上了 Unity 内的材质,直接参与 Unity 的渲染。与普通材质有所不同的仅仅是在渲染的时候会读取我的 GBuffer 中的几何信息,并把它伪装成自己的进行光照。这样的话,就可以直接利用 Unity 自带的渲染机制,所以渲染出来的东西跟 Unity 是可以完美融合的。
这个机制还有另外的几个好处,第一是对项目的渲染管线没有影响,可以直接使用 Unity 材质,也支持 Shader Graph。所以不管对美术用户还是技术用户都比较友好。
另外,Unity 很多时候需要把一个物体渲染多次,而使用渲染代理的话,就可以避免多次提交复杂的毛发几何体去渲染了。因为我每次提交的,都只是一个很简单的包围体。
最后一个优势,大家可以看出来这个机制很类似于延迟渲染,每个像素上只需要进行一次着色,所以是比较高效的。渲染可以使用代理,投影也可以使用代理,如此一来集成就变得很简单了,就不存在之前所说的诸多问题了。当然,后来有了 SRP,就可以在渲影子的时候直接调用原生插件渲染了,不再需要投影代理了。
接下来说一下跨平台,其实刚刚不管是 Unity Japan,还是我们的集成,一直是通过原生插件对接 Nvidia HairWorks SDK 做的。在 SRP 诞生之前,这可能是唯一能够跑通的方法。HairWorks SDK 预留了跨平台的设计,但是 Nvidia 只实现的 DX11 和 DX12 的版本,剩下所有的图形接口都需要去重新实现一份 HairWorks SDK 的底层,这个工程量是比较大的。而且由于很底层,所以难度也比较高。我花几个月的时间实现了一次 PS4 平台,虽然是做完了,但是做完之后觉得不能再用这种方式继续往下做了。
得益于近些年 Unity 的进化,特别是 SRP 的加入,让直接在 Unity 内实现这种复杂功能变成了可能。我利用近期 Unity 提供的一些新功能,例如 Computer Shader、CommandBuffer、RenderFeature、CustomPass 等等,直接把红框内的整个流程包括资源加载、约束初始化、物理模拟、几何体渲染,这些全部都在 Unity 内部重新实现了一遍。
这个流程比之前的 PS4 移植要顺利得多,因为现在 Unity 的渲染开发是很高效的,做任何修改都不需要关编辑器,也不需要花很长的时间来编译,可以即时看到效果。
另外由于有像 CustomPass 和 RenderFeature 这样方便的机制,我对 SRP 的源码修改其实只有几行。
接下来讲一下毛发着色,可以先看一下最终的效果。
说到毛发着色,我们首先会想到的就是 Kajiya-Kay 模型,它用毛发的切线替代了常用的法线,把 cos 换成了 sin,快速实现了类似于头发的效果。
但是如图所示,虽然它产生了类似于头发的高光,但是塑料感很强。在 2003 年 Marschner 提出了一种更接近物理真实的毛发着色模型,中间就是 Marschner 成果,右边是真人照片。在不考虑造型的情况下,可以说效果已经很接近真实的照片了。
它首先是对与毛发产生的交互光线进行分类,打在毛发表面就被反射的光线被称为 R。打入头发,然后又从头发背面射出的光线被称为 TT。打入头发,在头发内部被反射,又从头发正面透出来的光线被称为 TRT,这里面 R 和 T 分别代表反射和透射。
光线在头发内部行进的过程中,他的部分能量会被头发的内核吸收,所以说图中的 TT 和 TRT 都会带有头发的颜色,而R就类似于一般的高光,不受头发颜色的影响。
另外,由于头发表面的鳞片与毛发切线形成了一定的角度,这导致出射的 R 和 TRT 的角度产生了一定的偏差。直观地来说,就是他们两个的中心是分离的。
如中间的部分所示,白色的高光是 R,最上面呈现发色的高光就是 TRT,可以看出他们的中心是有一定偏差的。
Marschner 的另一项重要贡献,就是将散射光线在毛发横截面和纵截面的能量分布分开建模,这极大的简化了建模的难度,减少了单个分布函数的参数。
尽管如此,这个模型的数学计算还是十分复杂的。在 Shader 实现一套比较麻烦,性能也比较低。
为了提高计算性能,英伟达很早就提出了一种简便的方法,就是把横截面和纵截面出射光的能量分布烘培到两张纹理里面,渲染的时候查询即可。这种方法又被称之为查找表法。图中就是英伟达用这种技术制作的美人鱼 Demo。
但是这个方法有一定的问题,也就是只支持一种头发颜色和一种粗糙度。
受到英伟达思路的启发,我写了一个很简单的光追小程序,从各个方向向毛发发射光线,再在各个方向统计出射光线的能量分布,最后把能量分布存入纹理中。
但是在这个思路的基础上,我又做了两个扩展。第一是我希望美术能够调节粗糙度,所以我将 32 个不同粗糙度的能量分布都烘焙了,排成了一个序列。
第二是在能量分布图中,没有包含毛发中心对光线吸收,而是用了一组新的纹理来记录光线在毛发中行进的平均距离。根据这个距离和美术设定的毛发颜色换算得到的吸收率,就可以对毛发光线能量进行衰减,从而体现不同的毛发颜色。
最终的效果如图,大家可以从图左中观察到 R 和 TRT,可以在图右中观察到明显的 TT。
徐老师还分享了多根毛发与光的交互,自研的毛发抗锯齿的方法等精彩内容。欢迎点击“阅读原文”跳转社区专栏学习完整版:
https://developer.unity.cn/projects/hairworks
-
阅读原文
* 文章为作者独立观点,不代表数艺网立场转载须知
- 本文内容由数艺网收录采集自微信公众号Unity官方平台 ,并经数艺网进行了排版优化。转载此文章请在文章开头和结尾标注“作者”、“来源:数艺网” 并附上本页链接: 如您不希望被数艺网所收录,感觉到侵犯到了您的权益,请及时告知数艺网,我们表示诚挚的歉意,并及时处理或删除。