保姆级毛发算法调研分析,十万长文带你深入TressFX(一)

毛发算法调研分析

开篇

该系列的文章主要记录我在研究和实现毛发算法的历程。

本文仅在个人博客及个人知乎上采用”CC BY-NC-ND 4.0”(署名-不可商用-禁止演绎)协议发布,转载请注明个人博客的原文链接及作者信息,侵权必究。

博客链接:https://tis.ac.cn/blog/kongdeyou/hair_simulation_and_rendering/

知乎链接:https://zhuanlan.zhihu.com/p/517553643/

目录

本文的目录结构如下:

本系列后续文章中会陆续记录算法细节。

看完这一系列文章后,我们能够:

  • 对毛发系统有一个完整的概念
  • 深入毛发系统的核心实现算法
  • 在自己的引擎中实现毛发系统

技术选型

2022年春节回来后,我就一直在做毛发的调研分析和集成实现的工作,前前后后捣鼓了有三四个月,自己都快没有头发了(大概是极限一换一,我和模型只能有一个有头发了(我没有、我不是,我头发很浓密的!))。言归正传,此次调研的目的是沿着前人的足迹,走出一条落地的通路,然后再把路一点点拓宽:-)

毛发系统

毛发系统由毛发渲染物理模拟组成。

早期的毛发系统仅负责毛发的渲染,毛发的物理模拟通过物理引擎中的绳子模型或刚体的弹簧质点约束进行简单的等效。

后来逐渐发展出发丝模型后,毛发系统开始有了自己独立的毛发物理模拟算法,毛发的物理模拟不再借用通用的物理引擎中。

按我的理解(其实就是技术洁癖),毛发的物理模拟最终最优雅的形态还是放在物理引擎中的,这样物理引擎内场景能够归一,比如物理场景中有风场,那么如果按照当前的架构形态,毛发系统接管了毛发的物理模拟,那么就得在物理引擎中和毛发系统的物理模拟中分别加上这个风场的影响,一个风场得改两处,其他的功能也得两边分别处理,对引擎来说不便于维护。当然,我针对的是物理引擎是纯自研的情景,如果本身已经是“缝合怪”,物理引擎是引用的开源组件,那么还是不要侵入式修改加上毛发的物理模拟功能为好,把它老老实实放在毛发系统中吧。

毛发模型发展史

头发模型的发展过程主要经过了三个阶段:

  1. 整头头发模型
    整头模型图-图源来自网络
    这是最早的头发模型,整头头发部分是一个大的Mesh,直接盖在角色的头部模型之上。这种方式的头发模型制作简单,渲染也简单,实现不了精细的毛发渲染,也无法做物理模拟。
  2. 发片模型
    发片模型图-图源来自网络
    算力的提升带来了新的头发建模方法,发片模型是基于一撮撮头发进行建模的,头发由一系列的Triangle List组成,每撮头发是一个Triangle List,然后在这个Triangle List上贴上头发的Albedo贴图,用alpha blend来将Triangle List上有头发的地方透出来,实现最终的头发渲染。
    发片模型制作难度适中,能够进行头发材质的渲染,但是这种模型的问题在于难以进行精细的物理模拟。
  3. 发丝模型
    发丝模型图-图源来自网络
    发丝模型即只制作每根头发的引导线,在交给引擎进行头发渲染时再由引擎根据发丝的引导线生成每根头发的Triangle List。发丝模型的出现是为了解决头发物理模拟的问题的,从渲染角度上看,发片和发丝都可以让技美“精细化作业”来实现渲染出类似的效果,但发丝模型能很好地做物理计算,实现头发的海飞丝效果。
    发丝模型制作复杂,需要美工针对头部模型上对每根头发进行种植,在没有物理计算时会成“爆炸头”,有特别的头发形状的,还得要求美工种植的发丝模型每根都是已经做好弯曲定型的,加大美工的操作难度。

目前大多数的场景下还是以发片模型为主,一是手游市场上的手机平台性能有限,基于每根发丝的头发物理模拟和运行时每帧生成头发的三角形面片手机吃不消,而发丝的主要优势在物理模拟上,渲染上发片和发丝都能做出比较好的效果,二是目前除了数字人的场景,大多数的场景对人物模型的要求都不高,非3A级大作的核心是玩法足够吸引玩家,投资方能够尽快产生收益,非写实风的人物也不需要在头发上深挖物理模拟的细节,简单使用物理引擎进行粗粒度的等效即可。

毛发算法发展史

物理算法

毛发的物理算法没有一条清晰的发展脉络线,各家算法本质上都是基于弹簧质点系统的不断细化和改进。由于头发物理模拟的巨大计算量,实现完全物理正确的头发模拟的开销十分巨大且没有必要,因此在毛发的物理计算中常常采用简化的建模模型(所谓“简化”相对于其他的物理模拟来说还是复杂的)。

渲染算法

  1. Kajiya-Kay Model
    相关文章:https://www.cs.drexel.edu/~david/Classes/CS586/Papers/p271-kajiya.pdf
  2. Marschner Model
    相关文章:https://graphics.stanford.edu/papers/hair/hair-sg03final.pdf
  3. Scheuermann Model(结合了Marschner的结论,在Kajiya的基础之上为实时渲染而设计,又称kajiya+)
    相关文章1(相似文章):https://developer.amd.com/wordpress/media/2012/10/Scheuermann_HairRendering.pdf
    相关文章2(相似文章):https://developer.amd.com/wordpress/media/2012/10/Scheuermann_HairSketchSlides.pdf
  4. d’Eon Model(在Marschner上进一步考虑把头发分成两层,内部再次计算一次散射,效果提升不大,又称Marschner+)
    相关文章:http://www.eugenedeon.com/wp-content/uploads/2014/04/egsrhair.pdf

后续的算法基本都是基于Kajiya和Marschner的改进和补充。Kajiya光照模型给我们提供了一套物理不正确、但计算速度快的廉价方案,而Marschner是基于真实的发丝物理结构,进行测算后建模形成的光照模型,物理真实性更高,但计算量相比起来也会大些。

毛发物理算法简析

针对低性能手机端或者要求低功耗、毛发物理计算的精确度需求非常低的场景,有一种极简廉价的等效替代方案。该方案的毛发物理计算方法既不是基于每一根发丝的,也不是基于每一小缕、每一小撮、每一小束的,而是类似于在这一大把毛发上加上一些骨骼,然后把这一大把毛发等效成一系列刚体球,然后在这些球间添加弹簧质点约束,采用通用物理引擎直接进行计算,计算结果驱动毛发的骨骼动画。这种极简的替代方案,可以将一个角色的披头的长发等效成几个刚体球,如下图所示,计算时实际上只有几个刚体球的计算,开销非常小。弊端也非常明显,只能模拟简单的发型,而且物理模拟效果不理想。

简单廉价的等效替代方案,红色圆圈为等效的刚体球

另一种方案是基于发丝的等效,将发丝等效成一个弹簧质点模型,这种等效方案下的毛发模拟与布料模拟是类似的。一根发丝是一条curve曲线,在曲线上选取一系列的点作为质点,在质点间添加上弹簧约束,如下图所示。与布料模拟不同,布料是面状的结构,其中之一的质点与其周围的质点间都有相互作用,而发丝是线状结构,更容易做并行计算(基于每根发丝)。

发丝的弹簧质点模型-图源来自miloyip博客

但是这种过于简化的弹簧质点模型模拟的毛发会使得发丝像锁链一般,因为该模型的约束只有一个,发丝会像绳子一样过分柔软,而生物的毛发往往具有一定的刚度。MiloYip大佬在《爱丽丝惊魂记:疯狂再临》的爱丽丝头发制作中遇到了同样的问题,于是提出了改进的模型,在该模型中再加入一类约束,在发丝中一个质点的前后两个质点间再加上一个弹簧约束,如下图所示。我们可以参考大佬的文章《爱丽丝的发丝——《爱丽丝惊魂记:疯狂再临》制作点滴》

发丝的改进版弹簧质点模型-图源来自miloyip博客

这一改进模型解决了发丝过分柔软的问题,但是仍然有弊端,它只能处理披头的头发,发丝不能有弯曲和扭曲,然而众所周知,有的人的头发是自然卷,有的人的头发烫了个大波浪……

TressFX瞄准了这个方向建立了自己的一套新的改进模型。要模拟带有弯曲和扭曲的塑型头发,可以使用刚性弹簧,但是刚性弹簧目前难用于实时的物理模拟,因为强弹簧力的积分很容易使得质量弹簧阻尼系统不稳定,除非使用复杂的如implicit backward integrator的积分方案,另外,在GPU中同一求解器很难同时处理拉伸、弯曲和扭曲弹簧。基于此,TressFX提出了简化的全局形状约束和局部形状约束来等效。

TressFX新的改进模型已经相对比较完善了,即使不是物理正确的。但是这个模型还是仍然有些小的弊端,比如没有考虑马尾的处理,真实的一根头发是一条曲线,如果技美在做马尾的建模时,把这根头发的马尾辫和前面的部分分别建模,做成了两根曲线时,这个算法模型就懵逼了,会当成两根头发来处理,就会出现“后地中海式秃”的现象,算法会把前面部分的曲线的尾部当成了发丝尾,但实际上这个尾部有一个发圈的固定约束。另外TressFX的发丝长度约束是一个迭代缩短的过程,如果角色加速度非常大且保持非常大的加速度在变化,长度约束就比较难收敛,就会出现比较明显的头发拉长现象。

但是我们在看古墓丽影中的劳拉时,会发现她是个马尾,并且拉长现象并不明显,有理由猜测古墓丽影的团队有可能对算法做了更多的优化,另外马尾的建模是一根发丝一体建模的。

毛发渲染算法简析

毛发渲染部分,除了光照模型对毛发的效果产生主要的光照效果影响外,发丝的半透明混合和抗锯齿也非常值得关注。

发丝数量动辄上万根,用传统的针对半透明物体的先排序再按从后往前依次渲染的方法是走不通的,这里的方法基本上是借鉴的OIT的处理思路。

要实现完全的渲染正确,需要使用PPLL(Per-Pixel Linked Lists)的方法,针对RenderTarget的每个像素维护一个GPU并行链表,链表按深度顺序连接,绘制时根据当前发丝在占据的像素的深度在链表中插入,全部发丝绘制结束后最后一个Pass对每个像素的链表进行颜色混合,这种方法对硬件有要求,且显存的消耗不可控。TressFX的PPLL方案不是一个真正的链表结构,它针对每个像素预开了一定大小的空间用来存储深度和颜色信息(TressFX实现上每个像素能存16组深度和颜色信息,即最大链表节点数为16个,这个空间写死了,可详见代码TressFXPPLL.cpp中的TressFXPPLL::Create函数实现),这种方式不受限于硬件,但是当一个像素上的发丝数量过多时,不够存储的部分会丢弃,而当一个像素上的发丝数量过少时,剩余的空间就浪费了。而TressFX的ShortCut方案是借鉴的PPLL思想简化的处理过程,用一个3Layers的图存储不同深度下的颜色,最终只叠加这三层颜色,这种做法比PPLL占用的内存空间和计算量都更小很多,但是是渲染不完全正确的。另外,Intel曾提出Adaptive Transparency的OIT方案,是用来改进PPLL的内存和性能问题的简化方法,它的思想是修改经典的混合公式,引入一个能见度函数(transmittance=visible_function(depth)),近似出一个能见度函数后,就可以不再要求链表按深度顺序连接了,但是同样地,渲染也不完全正确了,混合精度也会有所下降,KlayGE在引擎中实现了该方案可以给我们参考。

另外,由于发丝非常的细,拓展出来的三角形面片非常的小,很容易出现锯齿和空洞,我们需要对发丝渲染结果做抗锯齿处理和补空洞。抗锯齿用一些常用的抗锯齿方法就可以处理。空洞出现的原因大多是因为毛发过细,三角形面片都不足以达到一个像素的大小,在毛发发丝数量稀疏的地方,就会导致空洞的出现,TressFX会在发丝三角形面片不足一个像素时扩展到一个像素,以缓解空洞的问题。

发丝的生物结构主要由纤维构成,可以分为中心的发髓(Medulla)、内部的皮质(Cortex)和表皮的角质层(Cuticle),如下图所示。

发丝的生物结构图

电子显微镜下我们还可以看到角质层呈现出倾斜的鳞片状,这是头发造成高光和反射的主要介质。由于角质层的鳞片状具有指向性,由发根指向发尾,如下图所示,这个现象可以用发丝的切线方向和毛发材质的各向异性来描述。

发丝的角质层电镜图-图源来自网络

而毛发一般足够细,厚度非常的薄,光穿过头发还会发生透射次反射/散射

毛发光照模型中的Kajiya算法,是一个简单的不符合物理真实的光照模型,但它也足够简单、计算量小。它首先假设了发丝是一个光滑的圆柱体,然后再应用上各项异性(对入射光进行一定角度的偏移)来模拟角质层的鳞片样状带来的影响。Kajiya算法只考虑反射带来的影响,不考虑透射和散射的影响。如下图所示:在计算光的specular分量时,Kajiya算法类似于直接计算镜面反射,但为了加入各项异性的影响,引入了一个小角度的光偏移,该光偏移的角度值是一个正弦函数变化的值,这样我们就能模拟实现头发上的天使环;而在计算光的diffuse分量时,迎着光方向的发丝在0到180度范围内做积分,这样就得到了漫反射值。

Kajiya光照模型

而Marschner算法,是一个基于测量头发光照属性得到相应的测算数据后建立的渲染模型,它将发丝视为半透明圆柱体,然后从物理的角度,将光照分成三条不同的路径:1.R项,代表光在头发表面发生的反射,该项产生毛发主要的高光,呈现出光色,受到毛发的切线和各向异性的影响;2.TRT项,代表光折射进入发丝,反射后再折射出发丝,该项产生次高光,呈现出带有发丝颜色的高光;3.TT项,代表光折射进入发丝,再由发丝的另一侧折射而出,该项产生透射的效果,在光打到毛发上,视角迎着毛发和光方向时,呈现出透光。基于这三条光照路径,将发丝继续分为横截面和纵切面分别进行计算,如下图所示。

Marschner光照模型

这里我们仅简单介绍一下概念,在实现时再详细展开。

已有的商业方案

  • UE4(4.26前):集成了NVIDIA HairWorks;
  • UE4(4.26后)与UE5:毛发渲染采用自研的Groom,基于Marschner渲染模型,物理模拟借用的粒子的物理模拟器Niagara;
  • O3DE:尝试集成的AMD TressFX,并整合进自己的RenderGraph中,但是没有实现完,发丝的碰撞矫正没有实现,会出现毛发穿模进模型中,渲染仅使用了GGX光照模型;
  • 网易《逆水寒》:使用的AMD TressFX;
  • 完美世界《笑傲江湖》:初期集成NVIDIA HairWorks,后转自研;
  • 腾讯手游《王者荣耀》:妲己的尾巴,基于Unity实现的FurShell(算法简单,叠多层的PBR);
  • 《爱丽丝惊魂记:疯狂再临》:MiloYip大大最早在2009年做的头发,基于改良的弹簧质点模型的头发物理模拟和Kajiya光照模型的渲染;
  • 其他一些国外的游戏公司:《古墓丽影》集成了AMD TressFX、《巫师》集成了NVIDIA HairWorks……
  • Frostbite:在SIGGRAPH2019上展示了一段效果很好的头发系统,但是目前仅有视频和PPT,没有项目,没有开源;
  • Unity:未开源,无法一窥究竟;但Unity有文章说他们将TressFX集成到URP和HDRP管线中,其中URP管线还实现了在手机侧高通865的芯片上1w根发丝能跑到60帧。

路该怎么走

回归到原始的调研目的,目前最快最有希望走通的路是,将NVIDIA HairWorks或AMD TressFX先集成到自己的引擎中,然后吸收消化它们的算法和思想,基于它们的基底再进行“改造升级”。

讲毛发的文章真的是少之又少,在这些非常稀少的文章中,不少文章讲讲概念,然后分析了毛发的渲染算法,再然后直接上UE引擎上调效果去了,讲具体在引擎中的实现细节的几乎没有,那只好靠自己啦。

我站在HairWorks和TressFX的岔路口踌躇,比对了一下两者的效果,TressFX实现出来的物理模拟效果更“海飞丝”一些,另外TressFX可供参考的文章和例子(例如O3DE、the-forge等开源引擎都集成过TressFX)更多一些,于是选择了TressFX(现在回过头来看,可能当初选错路了呢,粗看它们Hairworks的工具链更完整一些,HairWorks甚至做了一个完备的HairWorks Viewer,在Viewer里能够预览和简单编辑,可以方便地提供给技美做验证,而TressFX只有一个示例工程,但是既然已经沿着这条路走下去了,只能硬头皮上吧,说不准HairWorks也有其他的问题呢=_=)。

整体流程

本小节往后与接下来的文章将着重分析TressFX的毛发实现,我将以TressFX中的demo程序进行分析。

在开始之前,强烈建议先读一遍《GPU Pro 5: Advanced Rendering Techniques》中P407-417(TressFX Hair Simulation)/P193-209(TressFX Hair Rendering)和AMD官方的针对TressFX4.1的讲解文章,虽然官方的这篇文章只是简单谈了一下采用的算法及列了部分的源代码(有些已经对不上开源仓中的代码了,而《GPU Pro 5》则是针对的TressFX的3.x版本的讲解),然后说明了一下各个参数的具体意义,方面使用者调参时参考。但是这也已经是全网少有的毛发讲解文章了。看完再上路,中途不尿裤。

TressFX官方给出的集成方案是让开发者替换实现EI_开头的所有类(EI意为Engine-Interface),但是这种方案其实很hardcode,比如渲染引擎本身有场景类Scene,而TressFX中又有EI_Scene类,两者都要共存,会导致两份场景数据,EI_Scene中需要多存毛发需要的场景信息如灯光信息环境光信息等,否则无法带上光照的效果。另外,直接对接EI相关的类,在集成上虽然是方便了一些,但是往往不符合一个引擎正常的pipeline过程,比如引擎本身有RenderGraph,这样做会导致我们绕开了引擎的RenderGraph系统,直接通过RHI接口进行毛发物理模拟和渲染了,这对引擎的架构和维护都会带来很大的冲击。
因此,最佳实践方案应是将TressFX的算法理解吸收,然后遵照引擎的框架重新实现(抄)一遍。这里推荐参考O3DE引擎集成TressFX的方案,截至目前虽然它做的还不算非常的完善,比如基于SDF的碰撞矫正还没有加进来,光照算法也只有一个GGX能完全正常工作,但它的实现思路还是非常值得我们参考和借鉴的。

子功能一览

我们先简单看看TressFX的头发系统都有哪些子功能。

毛发系统子功能图

RenderPass一览

然后再从宏观角度来看看TressFX整体的RenderPass图,这张图费了我一点时间才整理出来,图中的所有Passes将对应头发系统中的所有子功能。

RenderPass总览图

这里我们不要求能够把整张图完全看懂,只需要从感官上有个大体的轮廓,后面将会针对每个Pass逐一展开,进行详细的分析,届时可以再回过头来对照着看。

图例及统计信息

针对整个场景中只有1个头发模型和1个角色模型/头发附着体模型的场景,头发的物理模拟需要9个Passes,渲染根据选择的半透明混合算法的不同,分别需要4个Passes或2个Passes,除此之外,场景中每个灯源产生的阴影要带上头发的影响,还需要独立的Pass给头发做全局阴影。假设一个最简单的头发场景,只有1个灯源且仅往地面这一个面的方向上投射阴影,加上一个简单的ForwardPBR和ToneMapping,一个带头发的最简单的场景至少需要17个Passes(ShortCut)或15个Passes(PPLL)(即:*9(物理模拟) + 4(渲染ShortCut) 或 2(渲染PPLL) + 1(普通模型阴影) + 1(头发模型阴影) + 1(PBR) + 1(ToneMapping)*)。

原始链接:https://blog.kdyx.net/blog/kongdeyou/hair_simulation_and_rendering/

版权声明: "CC BY-NC-ND 4.0" 署名-不可商用-禁止演绎 转载请注明原文链接及作者信息,侵权必究。

×

喜欢或有帮助?赞赏下作者呗!