文章 | 机核 GCORES ( ) • 2024-04-12 13:45

前言

SIGGRAPH 的分享和GDC的分享,是玩家和普通开发者得以窥见游戏开发技术脉络的重要方式——本身这种分享也是有一定的开源精神的,很多分享者不仅分享了自己的做法,还分享了自己为何选择该方案以及实际遇到的困难,这一方面是相当诚实与宝贵的。
例如这期要谈的头发渲染,在实时渲染领域本身是一个持续发展、不断改进调整的复杂渲染技术。在SIGGRAPH 历程中头发渲染就多次被提到过,Unreal、Frostbite等引擎及顽皮狗都分享过当时他们各自采用的改进方案,整体是一个不断迭代优化的脉络。
鉴于个人水平以及话题规模综合考虑,这次聊头发渲染我觉得最好的方式就是读文档,那么这篇2016年的 《The Process of Creating Volumetric-based Materials in Uncharted 4》就有其承前启后的意义,本身这个游戏也是大家耳熟能详的画面很优秀的作品。
*注:本文的读原文部分的翻译全是我个人完成的,个人认为尽力做到了通顺又精准。如需转载需要询问我。

1 头发渲染概述

这部分我会简单的概括一下头发渲染的发展历程和近况。
真实的头发可以理解成是有鳞片表面的半透明类圆柱结构。(这里的半透明是Translucent)
在实时渲染领域,头发渲染始终还无法做到真实世界头发的大部分物理特性。这种无法做到主要是基于两个方面的限制:一个是归纳其数学模型的难度,一个是数量上的难度。
目前认为最早的比较有效的归纳是Kajiya-Kay模型,它将头发模拟成不透明的类圆柱结构,并且归纳了符合视觉结论的各向异性高光的效果(常表现为沿头部一圈的高光效果)。
Kajiya-Kay模型 Kajiya-Kay模型
后来提出的 Marschner模型开始将头发考虑成透光的类圆柱结构,采用了更符合光传播的归纳方式:对于任意入射光线,其后续光路被拆分成R、TT、TRT三部分,R是指表面直接反射(直接高光)、TT指从内部折射后从离开发丝(表现为背光时的投光)、TRT指从内部折射又发射后从入射表面离开发丝(表现为第二重较弱的高光)。
Marschner模型 Marschner模型
以上两个归纳模型都曾直接应用于离线渲染中,以实际一根一根建模的方式来渲染头发。Marschner 模型在视觉上已经比较接近真实头发了,但是数量级上无法满足实时渲染的需求。因此实时渲染长期都需要用各种trick来近似达到接近的效果。
目前主流的实时头发渲染方案还是Kajiya-Kay模型或基于预积分LUT的Marschner模型。LUT-Look Up Table是一种常见的多维预计算查找方式,引擎中预积分结果存储成一张纹理图,用纹理坐标来查询。
主流的头发建模方案基于半透明面片的(也称为Hair Cards),如果看过之前半透明渲染的文章,会察觉此方案需要处理透明片元排序问题。
这里介绍一种已经被提炼得比较泛用的流程,通过4个Pass——预绘制深度(Z-PrePass)、绘制不透明多边形、绘制半透明背面多边形、绘制半透明正面多边形。(细节的AlphaTest参数和ZWrite参数之类这里不列出了)
对于发梢的柔化,一般有抗锯齿和AlphaBlend两种常见方式,也可以进行结合。

2 方案选择与基础视觉

这一章开始就基本是读原文,再辅以我自己的归纳或者补充了(打星号的部分)。原文是网上能下载到的一篇PPT文档,一共70页4部分,讲解头发渲染的是其中最长的第三部分。
其它几个部分这里粗略介绍下:
——第一部分主要是吐槽立项时采用了过高的技术方案,实际落到PS4上发现太超前了带不动,需要各种减量优化。
——第二部分讲解了布料的方案,包含其细节纹理的方案及一个近似的次表面散射的方案。
——最后一部分讲解了项目中积累的材质库,例如毛发、眼睛、布料和皮肤的打湿效果等。这部分没有拆解而只有一些举例。
文档中好多页都是这个老哥痛苦的表情:
*以下部分开始翻译一些关键的Page(图片备注翻译的是每张图的题头):
建立头发Shader库 建立头发Shader库
头发丝是非常细的,同时它们是半透明的。
每一个头发丝有着不同的法线,并且头发之间会投下自阴影。要实现这种有体积感的头发外观,我们需要一个一个地解决遇到的问题。
头发面片 VS 有独立几何体的头发 头发面片 VS 有独立几何体的头发
PlayStation4尽管在可容纳的多边形数量上有重大提升,但它仍然很难支持实时渲染数以百万计的头发丝。
由于我们并没有太多时间供深入研究曲面细分着色器(由于项目周期),我们最终选择了使用头发面片的方式。
我们以性能开销较大的方式实现了很好的效果 我们以性能开销较大的方式实现了很好的效果
1 高精度的透明纹理
2 使用了AlphaBlend和多重抗锯齿
3 高精度的阴影分辨率
以上几点带来了很棒的渲染效果
But.. 这里省略一张图 直接带入前面老哥的痛苦表情即可)
我们的头发不能那么多使用AlphaBlend 我们的头发不能那么多使用AlphaBlend
这是一张Debug屏幕截图——纹理Overdraw(太多了)
大部分头发渲染我们采用了Dithered Alpha和延时渲染 大部分头发渲染我们采用了Dithered Alpha和延时渲染
*Dithered Alpha就相当于之前文章介绍过的Screen-Door Transparency,是一种用不透明渲染近似达到视觉半透明的方式
对于大部分头发,我们关闭了alpha-blending转而使用dithered alpha,并结合TAA(分帧抗锯齿)。这个方案的劣势是角色移动快或者距离镜头远的时候会带来“鬼影”(ghost effect)效果。
项目中的头发丝不能太细 项目中的头发丝不能太细
为了尽量规避这个问题(鬼影),我们必须调整透明像素裁剪的阈值。
*以下是个人的一点备注:
原文没有提到什么场合使用了AlphaBlend的模式,我的理解是alpha值大于某个阈值视为不透明,小于某个阈值还是需要使用AlphaBlend;或者仅过场动画使用AlphaBlend模式。调高阈值是为了尽量多的片元以不透明的方式来渲染,但是以不透明方式渲染的头发在TAA之后会产生鬼影。
实际玩过这个游戏的能感觉出当时这种以不透明渲染为主的头发方案带来的效果还不错。不过这才刚开始,后面他们还要解决一系列问题。

3 法线方案

但是这里有一个问题——它们看起来还是“面片”而不是头发 但是这里有一个问题——它们看起来还是“面片”而不是头发 寻找原因 寻找原因
1 我们使用的是头发片的顶点法线,而不是单个头发丝的法线
2 日光带来的锋利阴影使头发面片看起来像不透明的物体,但它们预期应该呈现半透明的效果
头发的法线 头发的法线
角色美术手动调整了角色的头发面片,使用了一些trick来让它们尽量相互穿插,以体现体积感。
头发的法线 头发的法线
为了使头发面片之间的过渡显得平滑自然,我们需要统一顶点的法线。
*这里应该是指在空间变换时对头发的法线做处理,使其在世界空间中的方向基准有一致性。
头发的法线 头发的法线
大部分时候,我们不能使用高精度的法线纹理贴图(图中展示了它们采用的头发丝绘制编辑工具和Ramp纹理结构)。

4 阴影方案

为头发带来有体积感的阴影 为头发带来有体积感的阴影
*前2句翻过了,略过。Deep Shadow Maps是一个性能开销较大的多层阴影方案。
解决方案:
减少阴影?这会使头发看起来很平(削弱体积质感)。
我们在自阴影方面的探索 我们在自阴影方面的探索
*原PPT中这里是一段视频,展示了不同灯光亮度下的预渲染结果
这里展示了一个基于预计算的自阴影渲染结果,我们使用了57盏灯光,存储了各种不同情况的阴影信息。
*这是上一个方案的配图 *这是上一个方案的配图 为了实现更好的预计算自阴影,我们的尝试 为了实现更好的预计算自阴影,我们的尝试
我们需要找到一个好的折衷方案,来使预计算的阴影在大部分光照场合表现得真实。
为了实现更好的预计算自阴影,我们的尝试 为了实现更好的预计算自阴影,我们的尝试
我们把灯光划分成了5个组,这会减少我们把不同方向的灯光烘焙成阴影时遇到的问题,同时会会使烘焙出的阴影看起来更软(一次烘焙灯光更多的原因)。
(这里又插入了老哥的痛苦表情,由于工期的原因他们也放弃了这个方案)
最终我们拿出的自阴影方案 最终我们拿出的自阴影方案
我们最终决定组合所有灯光并把阴影烘焙到一张纹理上,放弃了方向性的阴影烘焙信息。
这(仍然)帮助我们使头发看起来呈现了立体感 这(仍然)帮助我们使头发看起来呈现了立体感 在有限的纹理内存中存储头发的细节 在有限的纹理内存中存储头发的细节
左图:1K细节纹理,很多头发面片共用其中一部分纹理
右图:512尺寸纹理的预计算阴影纹理,存储在uvSet2
我们如何在游戏中实现它 我们如何在游戏中实现它
我们曾考虑用烘焙阴影完全替代头发的实时阴影。
虽然结果在编辑工具中看起来很好,但我们也要考虑运行时其它物体投影到头发的情况,那会带来“暗中发亮”的视觉错误。
我们需要修改实时和烘焙阴影的计算 我们需要修改实时和烘焙阴影的计算
减少头发上的实时硬阴影:
如果简单的模糊实时硬阴影会浪费太多GPU性能。
我们之前提到使头发更厚一些以减少overdraw和鬼影效果,但我们不需要对实时阴影也同样处理。重置透明裁剪阈值是最简单和高性能的减少实时阴影的方案。
*阈值等于1指只要不是完全不透明,都裁剪掉
减少头发的实时阴影 减少头发的实时阴影
右侧是减少后的效果
调整头发的烘焙阴影 调整头发的烘焙阴影
为了使最终的阴影pass更有真实感,我们对烘焙阴影进行了一些调整:
1 为烘焙阴影添加LightWrap(LightWrap 是一种相对简化的次表面散射光照方案)
2 使用光照探针的强度值及头发颜色值对烘焙阴影进行调整(光照探针是一种基于球谐光照的布光和渲染元件)
*图中的这些公式为头发阴影这个问题提供了高性价比的效果,这些trick的组合可以认为是他们在头发这件事上的“卡马克时刻”。
*这里再解释一下为什么头发可以使用烘焙阴影,因为头发间的相对自遮挡关系在发型确定时可以相对确定,一定程度上确实不会穿帮。当然,如果是还考虑了各方向光照的烘焙阴影会有更正确的效果,只是他们做了trade off放弃了。

5 散射方案

头发的散射效果 头发的散射效果 头发的散射 头发的散射
我们决定采用一种GPU性能开销很低的方案来时玩家有一种头发有光散射的视觉印象(trick)。
这一方案有2个关键要素:
1 头发丝间的散射
2 背光的散射
发丝间的散射 发丝间的散射
近似于布料的“Cheap SSS”,我们发现我们游戏中的大部分头发颜色是棕色、褐色、黑色甚至白色,添加微红的颜色可以使头发看起来更柔顺和有真实感。
*本文中没介绍他们布料的散射方案。SSS就是次表面散射,那是另一个trick。
背光散射 背光散射
在这个课题上,我们把头发视为球体。我们总结出背光散射主要基于光方向、摄像机角度和表面法线。
散射的量会基于头发颜色、形状、长度光强度不同而变化。
背光散射 背光散射
*代码部分掠过,翻译一下变量说明
通过调整scatterPower可以使scatterFresnel(菲涅尔 折射相关)的形状和范围看起来更可信。
在我们的游戏中,scatterPower 是11时代表短发,9代表长发或蓬松的头发。
lightScale代表光散射的范围。对于大部分棕色头发,我们选择了一个更高的数值。
为背光散射添加变化 为背光散射添加变化
正如之前提到的,我们不得不让头发丝尽量粗一些,以减少排序问题和overdraw。与此同时,为散射光添加一些变化是一个低开销的提高发丝质量的方案。
头发的光反射模型 头发的光反射模型
1 Kajiya-Kay ——在这个模型的基础上我们没有进行改进
2 高光的变化——我们在所有头发材质上共享了同一个变化遮罩层。原因:
1)高光变化仅仅用来给玩家提供一种更像头发丝的感觉
2)视觉上不像其它变化遮罩那么容易穿帮
3)我们的头发面片的UV布局是统一化的
*这个效果我个人理解主要是在Kajiya-Kay高光的基础上增加了一些扰动感
为NPC和次要角色减少纹理内存 为NPC和次要角色减少纹理内存
左侧是主要角色Nadine的质量,右侧是多玩家模式下的质量。
*可以认为是采用了LOD形式或不同预设的画质分级。

结语

原文档的很多部分还是略微有一些缺少上下文的感觉,如果能有讲解的原视频会更容易理解,可惜我没有找到相关的视频。
如果有点游戏开发经验,可能会感觉他们当时考虑的一部分问题到现在变成特别初级的事情了;一方面是技术也在工业化领域不断发展,另一方面是硬件性能在之后也有了质变(尽管还不到10年)。但考虑到年代感,能读到一点他们当时真实的想法,并稍微揭开顽皮狗这样的公司的神秘感,我觉得也是很好的视角。
在最新的实时渲染领域,已经可以模拟发丝(Strand-based 基于线的)进行头发渲染了,但是数量上仍然有一定限制,因为除了渲染开销外,物理模拟上的性能消耗也是不可忽视的。(玩过《死亡搁浅》的想必对strand这个词不会陌生)
更科学的归纳模型还会考虑头发的髓质及其影响,这在人头发的占比不大,但是在动物毛发中的占比很大,因此动物毛发不能仅仅采用Marschner模型。这一点Games101的闫老师提出了他的模型并被正式采用到了工业领域,有兴趣的可以去看Games101的相关内容(这一技术被用到了动画影片《新狮子王》的渲染中,效果很好)。
无论是容纳更多数量,还是归纳出更精进的模型,在顽皮狗完成《神秘海域4》制作并进行技术分享的2016年之后,这个领域又不断有了长足的进步;当初用一堆trick堆出来的特别“真实”的渲染质感,又在不断被后人挑战。
我觉得游戏行业从业人员本身是有其反功利乐于分享的一面的,而研究图形技术及其应用的分享又是各类分享中的重头戏。不同的公司在其不断提升画面的路上,都或多或少的各自迎来了属于自己的“卡马克时刻”,他们将其分享出来,带来行业共同的发展,还能反哺到电影、动画、教育等很多其它领域。
只要这份分享精神还在,这个行业就始终充满希望。

最后是一些资料链接: