大家好,我是来自搜狐畅游引擎部的puppet_master,很高兴有机会分享一下最近一段时间在做的一个头发的项目,海飞丝系统。
所谓海飞丝,是大家对Strand-Based Hair System的一个戏称,是基于引导线生成的程序化头发的一种头发的物理模拟及渲染方案,与发片及动态骨骼等之前的技术相比而言,最大的优势其实就是运动的表现,传统方案的头发运动头发是一个整体的网格,只能按照辫子或者一簇头发整体运动;而在海飞丝系统中,每一根头发都是真实的网格,因而每一根头发都可以进行运动。
在离线渲染领域这个技术已经比较成熟了,在PC及主机领域目前比较熟知的使用了这个方案的游戏就是《古墓丽影9》《逆水寒》,而移动平台目前暂时还没有成品的游戏使用了这个方案,所以我们也是希望能够在这个方向上做了一个小小的尝试,目前的效果还是在开发中,有许多不足,也希望各位指正。
下面是我们的头发的效果的一个小视频
海飞丝头发简版-高清
这一篇,主要介绍一下头发的准备阶段以及物理模拟部分,不过,在正式开始之前,我们先来聊一聊头发特性以及渲染中头发的进化史吧。
令人秃头的头发
就目前来说,PBR等技术对于场景的渲染已经可以达到以假乱真的地步了,而屏幕空间SSS也让角色的皮肤效果比较真实了,但是头发的渲染以及运动的模拟一直是整个角色表现的一个短板。主要原因呢,其实还是由于头发自身的复杂特性。
头发主要有以下的几个特性
1)数量多:如果不秃头的话,成年人的头发数量在10万根左右
2)发丝细:单根头发的宽度在0.02-0.2毫米
3)运动杂:每根头发运动状态都可能不同,头发可能成绺也可能单丝
4)造型怪:千奇百怪的造型,飘逸同时发型不能乱
上面的几个特性造成的问题也很明显:
1)数量多:严重的性能问题,制作成本较高
2)发丝细:不透明渲染会有严重锯齿,透明渲染会有半透穿插,而且光照和阴影表现复杂
3)运动杂:骨骼和布料只能实现成绺的运动表现不理想,头发其实既有离散也有聚合的表现
4)造型怪:各种造型对应的渲染和运动都不同
总结来说,头发既有微观的特性,又有聚集的表现;要想把头发做得更真实,把头发当成一个整体或者完全离散的个体都不合适。
游戏中头发的进化史
基于上面头发的特性以及在渲染中实现会导致的问题,在不同的机能的时代,头发也相应的有不同的表现形式,而一般来说,在同时代的情况下,头发一直也是角色表现的短板之一。总体来说,头发的技术进步分为以下的几个阶段:
秃头阶段
曾经有这样一个段子:“为什么以前的游戏角色都是秃头或者戴着帽子?---因为头发不好做啊”,那么,与其去做一个出戏的头发,不如就不做头发;如果没有头发,就看不出来头发效果差。所以,一个至今仍然流行的秃头派就这样诞生了。早期游戏的很多著名光头反派和主角,至今仍然影响着我们这一代游戏玩家。
锅盖头(上古时代)->体状头发(1997-Now)
提到上古时代的游戏,《反恐精英》应该是最有代表性的一个系列了,在那个年代,角色的头发也仅仅是把头皮做成黑色。而设备机能进一步提升之后,角色的头发开始由短平头,转向了一些体块状的头发,如《最终幻想7》中杀马特的造型。体块化的头发对于特殊风格的发型已经可以较好的体现了,而在今天,风格化的角色渲染中依然在使用类似的方案,并非技术原因,而是本身风格化的设计。
发片渲染(2004-Now)
所谓发片渲染,即Hair Card的方案,把头发一束作为一个面片,插到头上,铺满脑袋就可以实现头发的效果。从GDC 2004 AMD发布的头发渲染的那篇著名的头发渲染分享开始,这种头发制作及渲染的流程一直是头发最主流的方案。发片最大的优势是性能,可以实现比较好的层次感的头发的效果而无需消耗太多性能,因而也是目前绝大部分游戏采用的方案。
上一代海飞丝技术(2013-2019)
由于发片技术对于头发运动表现的限制,进而开始有了新的一个头发渲染的方案,Strand-Based Hair System,即所谓的海飞丝系统。对于类似技术的研究从2008年左右就开始有相关的论文,只是效果比较惨不忍睹。真正第一款爆款的使用海飞丝系统的游戏就是《古墓丽影9》,当年劳拉的头发应该是震惊了无数的宅男,至今为止,这也是最为人熟知的一款海飞丝的游戏。劳拉的头发使用的是AMD的Tress FX系统,而Nvidia也推出了他们的Strand-Based-Hair系统--Nvidia Hair Works,并在《巫师3》等游戏中进行了应用。
下一代海飞丝技术(2020-Now)
在古墓丽影之后,很长的一段时间主流的Strand-Based-Hair一直只有Tress FX和 Hair Works这两个解决方案,直到2019年,EA分别在2019及2020两年进行了两次Siggraph的分享,分享了他们在最新的寒霜引擎中实现的新的Strand-Based-Hair,把整个实时渲染中头发的技术又向前推动了一个时代。而相应的,Unreal Engine也推出了自己的Strand Hair系统-Groom。
我们进行的移动平台海飞丝尝试
由于我们是移动平台,对于手机来说,目前的手机与10年前电脑的机能相比已经不分伯仲(各有优劣)了,其实也可以实现一些我们曾经憧憬的技术了。但是,毕竟移动平台的性能,功耗,以及一些特性的限制,无疑让我们的头发没有办法火力全开,因此我们需要在效果和性能之间做一些权衡。
下图为移动平台上头发的表现
以及手机视频效果:
海飞丝-移动版本视频
准备工作及离线处理
真实的头发大概有10W根左右,但是实际我们有1W左右就可以保证我们的角色不秃头。但是一万根头发,每根头发可能会有15-30个顶点,因而头发的模拟和渲染的开销都会较大,因此我们需要使用一些比较猛一点的工具来计算。当然就是Compute Shader啦。我觉得,这张图很明显的体现了CPU和GPU的对比。
另一方面,即使是使用这些,我们的头发的数量还是过高,这里我们就可以利用头发的特性,即头发本身是有成绺的特性的,有些头发固定是会成绺的,而一部分头发会是单丝的状态,因此,我们要表现这一特性的话,可以让一部分的发丝跟随一根头发进行运动,我们将其称之为引导线。如下图的多马尾的发型所示,左侧为选择的运动引导线,而右侧为实际的全部发丝。
我们通过程序化随机选择的引导线,其他的发丝去寻找距离其最近的引导线发丝,记录位置偏移,就可以实现少量引导线带动全部发丝运动的功能了。这样,我们的准备工作就做好了,下面就是关于物理运动的实现啦。
发丝的物理运动
我们要实现物体的运动,首先就要对头发进行一些抽象。以一根头发为例,我们先不考虑头发的宽度的话,就可以把头发看作一条线也就是strand;然后,我们将头发分为若干段,每一段的端点进一步抽象为一个质点,这样,我们针对上面每一个质点作为一个单独的粒子进行物理的模拟,应用各类力及约束迭代进而实现头发的运动模拟,就可以实现整根头发的运动了。
惯性及受(重)力
第一步,也是最终要的核心部分,我们先要让头发动起来,哪怕是掉头发。
首先,我们需要复习一下中学物理的牛顿三大定律的前两个。第一,任何物体都保持静止或匀速直线运动的状态,直到受到其它物体的作用力迫使它改变这种状态为止。第二,物体在受到合外力的作用会产生加速度,加速度的方向和合外力的方向相同,加速度的大小正比于合外力的大小与物体的惯性质量成反比。
说到一个独立的粒子,在世界中,在没有其他任何外力的作用下会有惯性。假设我们的一个粒子,他当前帧的位置为x(t),我们所要模拟的结果就是经过了△t时间后的下一帧的粒子的位置,即x(t + △t),我们可以用这个近似表示x(t + △t) = x(t) + v * △t ,我们可以换一个方式来表示 v * △t ,假设速度一致的情况下,我们使用上一帧的位置和当前帧的位置差,即x(t)- x(t - △t)。但是上面是理想中的情况,现实情况下,我们需要考虑阻尼的影响,所以我们要将速度乘以一个阻尼的系数d。这样,理想情况下,不受除了阻尼之外外力的粒子的运动可以表示成:
x(t + △t) = x(t) + (x(t)- x(t - △t)) * d
而另一方面,在地球上的任何物体都要受重力,所以我们需要在这个公式上额外添加上重力,那么这个公式就变成了在模拟仿真方面经常使用的(经济实惠)的一个公式-Verlet积分公式:
x(t + △t) = x(t) + (x(t)- x(t - △t)) * d + g * (△t)²
关于公式肯定不像我们分析得这样简单,感兴趣的童鞋可以去搜索下Verlet积分的推导,当然又是本人不喜欢的长篇大论啦,所以简单滴抽象了一波上面的分析,仅是帮助理解。
那么,在Compute Shader中我们要在实际中实现粒子的运动,我们只需要保存头发的每个粒子当前帧的位置,上一帧的位置;额外输入时间,和阻尼,重力系数,带入公式就可以实现头发的运动的模拟啦,当我们兴冲冲的实现这个效果的时候就可以发现一个令人震惊的情况,“小姐姐”的头发全掉了。
1.verlet重力效果
长度约束及根节点处理
我们已经跨过了第一步,头发已经能动了。那么,下面我们就得有点更高的追求:不要脱发,不要拉伸。
首先,我们能想到的是,让头发的根节点不受力,那么头发就不会脱落了。但是会有另外的问题,头发会从第二个节点向下开始拉伸。因此我们需要针对头发实现一个约束,保证头发的长度。我们首先在头发初始化时,记录头发相邻的两个粒子之间的长度作为头发本身的长度。然后针对粒子进行迭代,如果两个粒子间的距离超过了原本记录的距离,则把两个粒子朝向相向的方向拉伸。
关于实现长度的约束,我们可以考虑两种实现,一种是全并行的对每个粒子进行Position Based Dynamic进行迭代,这种方法有一个好处,是可以适当拉伸,效果较好,而且逐粒子全并行计算理论上效果更好,但是这种方案的代价是不收敛,快速运动时仍然会拉伸,因此需要迭代很多次,性能反而会较差。我们如果想实现移动平台的方案,自然要选择一些性价比高(便宜)的方法。另一种方法,可以参考《Fast Simulation of Inextensible Hair and Fur》这篇Paper中,对于粒子的模拟计算。假设头发是完全不可拉伸的,串行计算头发的长度约束。通过对头发的上一帧的位置调制来达到约束头发长度的功能。
有了长度约束及根节点的处理,我们的“小姐姐”终于不脱发啦。
2.长度约束及根节点约束
引导线发丝驱动渲染发丝
当一个人不脱发了,那么他下一步的梦想肯定就是有一头茂密的头发,而不是像上面只有几根毛。所以,我们下一步就是要用少数的引导线驱动大量的头发的运动。
我们在上面准备部分已经介绍过,我们离线处理的阶段会选择一部分头发作为引导线发丝,而其他的所有的发丝会记录下他们距离引导线上对应顶点的偏移值,这样,只要引导线运动了,我们就可以根据引导线的运动结果带动其他所有发丝的运动了。
3.引导线驱动发丝运动
发型约束
好了,现在我们的“小姐姐”已经有了一头浓密的头发,那现在我们的要求就更高了点;我们想给“小姐姐”的头发做个造型,运动要飘逸,但是发型不能乱。
如果我们只保留物理的运动,那么我们辛辛苦苦做的发型就乱套了,整个表现起来就像披头散发的贞子,惊悚程度拉满。所以,我们需要一个机制,让头发能够保持一些原始的造型表现。其实原理也很简单,我们记录一下粒子的原始的位置,在上面的Verlet积分运动之后,让头发根据一个系数插值恢复到原始位置。而这个系数一方面我们可以自己控制,另一方面,我们也会让发根更多的维持原始形态,发梢更多地走物理的运动。有了发型的约束,我们就可以保持“小姐姐”的头发造型啦。
4.发型约束
只是单纯的通过位置进行约束,对于普通的长发效果尚可,但是如果我们的“小姐姐”想烫个卷发,发梢的位置也需要保持形态,这就与我们上面的方式相悖了,单纯的位置约束会使头发运动过于刚体,整体的感觉像弹簧,所以我们需要一个机制来进行更好的角度约束。这里我们可以参考AMD Tress FX的方式,所谓Local Shape Constraint,就是我们需要在每个粒子上创建一个本地的坐标系,并且每一帧根据当前的位置重新计算构建这一坐标系,我们在这里可以使用四元数进行构建,记录每个粒子相对于父节点的旋转值,通过这样的约束,这样,我们计算了头发的弯曲的一个角度约束,既可以保持头发的原始的造型,也不至于让头发过于弹弹弹。
这里补充下对于两点间头发的运动约束的计算,对于可以运动的粒子,我们应用约束时,都是两者分别朝向相向方向运动;而对于不可运动的顶点,则会更多的把其他的顶点拉向自己。
碰撞约束
好了,现在,我们“小姐姐”有了好的发型。但是还有一个问题没有解决,头发穿模啦,这对于强迫症来说肯定是忍不了的,所以我们需要处理一下这个问题。
我们暂时没有采用更高级的体素的碰撞方式,而是比较原始的球形碰撞体。使用球体拼合出角色的大致形状,一般来说只是需要肩膀到头的部分。碰撞约束的计算也相对比较简单,我们计算粒子距离每个碰撞体的距离,如果距离已经小于了碰撞体的半径,那么就认为粒子已经进入了碰撞体,进而我们将其向外移动直到碰撞球的外围。
下面分别是无碰撞和有碰撞情况下的效果对比:
5.碰撞约束
风力模拟
上面的功能我们基本实现了头发的正常的运动,但是如果角色不动,那么头发也就不动了,而风则是一个可以很好的增加头发动态的效果,所以这里,我们要给头发加上风力的支持。其实模拟风力,我们大致需要几个随机的变化,分别是风力的方向的随机变化,强度的随机变化,以及头发响应的随机变化。
风力的方向和强度的随机变化,我们可以参考Unity的风场的计算方式在C#阶段进行计算,相比于逐粒子计算,性能还是好一些的。
而风力的方向变化,我们则是通过随机的在多个方向之前进行旋转作为的风力方向。但在头发运动阶段计算风力时,直接使用这个方向的效果并不好,我们需要根据不同的发丝对头发的受力方向进行调制,通过tangent方向与固定风力方向,得到一个修正后的风力方向,这样,我们每一根发丝的受力以及不同角度下的风力方向都是有变化的,进而就可以实现每根头发不一样的风力变化了。下面是风力的表现
6.风力模拟
发丝间摩擦力
经过了上面的各种力和约束,我们的头发已经比较飘逸了,不过有时候会有些过于飘逸,这也是为什么Strand-Based Hair被戏称为海飞丝的原因,因为太飘逸了,反而会导致是理想中洗发水广告的表现。而现实当中,我们的头发并不会这么飘逸。我们需要进行一些分析一下,上面我们所有的计算实际考虑的都是同一根发丝之间的粒子的模拟,发丝和发丝之间并没有联系,每根发丝各自为政,但是真实的头发,头发之间有着摩擦力和静电等产生的引力,我们姑且统一称之为摩擦力吧。如果要真实的去模拟不同发丝之间的粒子的影响是很困难的 ,超大量的发丝之间的联系会直接让我们的计算爆表,所以我们需要借用一些更加聪明(偷懒)的办法来实现,也是现代图形学常见的一个策略,即体素。
我们在头发周围创建一个32*32*32的格子,然后求得每一个网格的内粒子的平均速度,我们在实际计算每一个粒子的时候,我们使用粒子自身速度和所在格子的平均速度进行插值,就可以让粒子更倾向于周围网格的总体速度,进而也就是实现了头发的整体吸引聚集,摩擦力引力等的表现了。
下面是是无摩擦和有摩擦的情况下的效果对比:
7.摩擦力
浮力
在实现了其他的基本的效果之后,我们也可以做一些好玩的东西,比如角色在水中的效果--浮力。注:此处为了避免角色秃头,我们把头发上半部分加以了固定,只有下半部分可以运动。
浮力的效果相对比较简单,我们只需要把常规的重力的方向进行反向,然后只保留头皮部分的发型约束保持,让头发只依赖Verlet的惯性以及发根的拖动进行运动即可。
8.浮力
总结
本篇文章,我们回顾了一下游戏中头发的进化的历史,从秃头,体块,发片,到Strand-Based-Hair的各种头发的特性。并且对于移动平台海飞丝的头发的准备阶段以及物理模拟阶段有了一个基本的介绍,下一篇将继续介绍海飞丝头发的工作流以及头发的渲染相关的内容。