1. 项目概述:Marschner毛发渲染模型在Unity中的落地
如果你在Unity里做过角色渲染,尤其是涉及到写实向的角色,大概率会为头发这个“老大难”问题头疼过。传统的Lambert、Blinn-Phong模型处理头发,要么看起来像一坨塑料,要么就是一片毫无细节的色块,高光要么是硬邦邦的一个点,要么就是糊成一片,离“以假乱真”的毛发质感差了十万八千里。这正是因为真实世界里的头发,其光学特性远比一个简单的漫反射+高光模型复杂。
Marschner-Hair-Unity这个项目,就是冲着解决这个问题来的。它不是一个简单的Shader玩具,而是将学术界和工业界验证过的、目前最主流的物理毛发渲染模型——Marschner模型,完整地移植并工程化到了Unity引擎中。简单来说,它让你能在Unity里,用相对可控的性能开销,渲染出电影级或3A游戏里那种拥有复杂光泽变化、透光感和层次感的写实头发。无论是做独立游戏、影视动画,还是高保真的数字人项目,这个资源都能把你的角色毛发表现力提升一个巨大的台阶。
这个项目的核心价值在于“实现”而非“发明”。它基于Epic Games在Unreal Engine中实现的“Physically Based Hair Shading”方案(该方案本身又源自2003年Stephen R. Marschner等人的经典论文《Light Scattering from Human Hair Fibers》),并参考了后续Yan等人提出的优化模型。作者不仅提供了Shader代码,还配套了Haircard(发片)和Furshell(毛壳)两种主流毛发建模方式的渲染方案,甚至附带了用Houdini和Ornatrix制作的示例模型,形成了一个从理论到实践、从Shader到资产的完整解决方案包。对于想要深入理解并应用次时代角色渲染技术的开发者、TA(技术美术)或美术同学来说,这是一个不可多得的实战学习资料和生产力工具。
2. 核心原理拆解:为什么Marschner模型能模拟真实头发?
在深入代码和操作之前,我们必须先搞明白,Marschner模型到底比传统模型强在哪里。如果你只关心“怎么用”,跳过这节也能操作,但理解了原理,你才能调出精髓,而不是瞎碰参数。
2.1 传统模型的局限:把头发当成了圆柱体
传统的着色模型,比如你在Unity Standard Shader里看到的,通常把表面视为一个微表面(Microfacet)。光线打上去,主要考虑两种行为:漫反射(Diffuse)和镜面反射(Specular)。对于头发这种又细又长的圆柱形纤维,这种简化问题就很大。
你把一根头发横着切开,它的截面接近一个圆形。当光线照射到这根圆柱体上时,会发生非常复杂的光路:
- 直接反射(R):一部分光直接在圆柱表面发生镜面反射,这对应头发最亮、最锐利的那条高光。
- 穿透-反射(TT):一部分光会穿透进头发内部(髓质层),在内部发生散射,然后从另一侧穿出。这带来了头发的“透光”效果,尤其是逆光时头发边缘的“金边”现象。
- 反射-穿透-反射(TRT):更复杂一些,光先部分反射,再穿透进内部,在内部经过一次反射后,再从入射侧附近穿出。这形成了通常比R通道更宽、更柔和、颜色也更丰富(偏向头发本色)的次级高光。
传统的单一高光模型,根本无法区分和模拟这多条光路。这就是为什么用传统Shader做头发,高光要么位置不对,要么缺乏色彩和层次变化。
2.2 Marschner模型的三大光路
Marschner模型的核心贡献,就是精确地建模了上述三条主要光路:R、TT、TRT。在Shader中,这意味着我们要为每一根头发(或者说,每一个着色点)计算三个独立的高光项,再把它们叠加起来。
- R通道(Primary Specular):这是最直接的高光。它的强度最高,范围最窄,颜色最接近光源色(白色)。在实现上,它通常用一个经过修改的、各向异性很强的镜面反射模型来计算,以模拟沿着发丝方向拉伸的高光。
- TT通道(Transmission):这是透射项。它负责头发背光时的透亮感。它的强度取决于头发的吸收系数(Absorption),这直接关联到你设置的发色。深色头发吸收多,TT弱;浅色或染色的头发吸收少,TT强。它的范围通常比R通道宽。
- TRT通道(Secondary Specular):这是次级高光,有时也叫“彩光”(Glint)。它最复杂,也最有趣。因为光在头发内部经历了吸收和散射,其出射方向和颜色会发生变化。TRT高光通常比R高光更宽、更柔和,并且会带有头发本身的底色(比如棕色头发会透出红棕色的高光)。这是让头发产生“天鹅绒”般质感的关键。
注意:在具体实现中,尤其是Epic和本项目参考的优化版本中,可能会对这三条光路的数学模型进行简化或近似,以在视觉效果和性能之间取得平衡。例如,用更高效的经验函数去模拟复杂的散射积分。但核心的“三高光”思想是不变的。
2.3 各向异性是关键
头发的高光不是圆形的,而是沿着发丝方向拉伸的条状。这是由头发的圆柱几何形状决定的。因此,Marschner Shader的另一个核心是各向异性高光的计算。这需要我们知道在着色点处,头发的切线方向(Tangent)。这个切线方向通常来自于模型的顶点切线数据,它必须沿着头发的生长方向。如果切线方向是乱的,那么高光也会是乱的,看起来就像头发打结了。
所以,一个准备良好的头发模型,其切线流必须正确、平滑。这是使用任何高级毛发Shader的前提,不仅仅是Marschner。
3. 项目部署与基础环境搭建
理论说得差不多了,我们动手把项目跑起来。这个过程本身不难,但有些细节不注意,后面调试会非常痛苦。
3.1 获取与导入项目
首先,你需要从GitHub获取项目。访问https://github.com/maajor/Marschner-Hair-Unity,你可以选择直接下载ZIP包,或者使用Git克隆到本地。我个人推荐使用Git,方便后续更新。
git clone https://github.com/maajor/Marschner-Hair-Unity.git打开你的Unity项目(建议使用Unity 2019.4 LTS或更新版本,URP/HDRP项目需注意兼容性,原项目主要针对Built-in Renderer开发),直接将克隆下来的文件夹拖入Unity的Assets目录,或者通过Assets -> Import Package -> Custom Package选择下载的.unitypackage文件(如果有的话)进行导入。
导入后,你应该能在Assets目录下看到MarschnerHair之类的文件夹,里面包含了Shader、材质、示例场景和模型。
3.2 核心资产结构解析
导入后别急着打开场景,先花两分钟理清资产结构,这对理解项目至关重要:
- Shaders/:这里是核心。你会找到像
HairShader.shader、FurShader.shader这样的文件。用文本编辑器打开它们,你会看到大量复杂的HLSL代码,实现了我们前面讲的R、TT、TRT光路计算。这是学习的宝库。 - Materials/:这里存放着配置好的材质球。通常会有针对
Haircard(发片)和Furshell(毛壳,用于动物毛发)的不同材质预设。材质球是连接Shader和模型的桥梁,你后续的调参工作主要在这里进行。 - Models/或ExampleMeshes/:这里包含了作者提供的示例模型。例如用Ornatrix生成的发束模型(用于Haircard Shader),以及用Houdini生成的毛球模型(用于Fur Shader)。这些模型的顶点切线方向已经正确设置,是测试Shader的完美对象。
- Scenes/:示例场景。通常有一个主场景,展示了不同Shader在不同模型上的效果,并可能有简单的灯光和相机控制。
3.3 创建你的第一个Marschner材质
现在,我们从头创建一个属于自己的Marschner材质,应用到自己的模型上。
- 准备模型:确保你的头发模型已经导入Unity,并且导入设置中“切线(Tangents)”选项是“Calculate(计算)”。Unity会根据模型的UV和法线来估算切线,对于简单的发片通常够用。但对于复杂发型,最好在DCC工具(如Maya、Blender)中计算好正确的切线再导出。
- 创建材质:在Project窗口右键 ->
Create -> Material,命名为MyHair_Marschner。 - 指定Shader:在新建材质的Inspector面板,点击
Shader下拉菜单。你应该能在列表中找到以MarschnerHair或项目作者命名的分类,点开,选择Haircard版本(如果你的模型是发片)或Furshell版本(如果是体积毛发)。如果找不到,可能需要手动点击Browse,然后输入Hair等关键词搜索。 - 应用材质:将创建好的材质球拖拽到你的头发模型上。
此时,你的模型很可能看起来一片漆黑或者非常奇怪。别慌,这完全正常。因为我们还没有配置任何参数,而且环境光照可能也不对。
4. 材质参数详解与视觉调试
Marschner Shader暴露的参数比Standard Shader多得多,这是它强大也是复杂的地方。我们把这些参数分分类,一组一组来攻克。
4.1 基础颜色与吸收(Base Color & Absorption)
这是控制头发基础色调和透光性的核心。
- Base Color:头发的基色。注意,它不完全等同于你最终看到的颜色。因为TT和TRT光路会叠加自己的颜色影响。
- Absorption (TT):这是TT通道(透射)的吸收系数。它是一个RGB向量。你可以把它理解为“光线穿过头发时,被吸收掉的颜色”。它的补色就是透射出来的颜色。
- 实操技巧:这里有个反直觉的点。如果你想得到金色的透光效果,
Absorption (TT)应该设置为蓝色系(如(0.1, 0.2, 0.8))。因为吸收蓝色多,透射出来的就是蓝色被削弱后的补色——黄色/金色。多调调这个参数,配合逆光观察,你会立刻明白它的作用。
- 实操技巧:这里有个反直觉的点。如果你想得到金色的透光效果,
- Absorption (TRT):同理,这是TRT通道(次级高光)的吸收系数。它主要影响TRT高光的颜色倾向。通常可以和
Absorption (TT)设置成一样的值,或者让TRT的吸收稍弱一些(数值更小),使其高光更偏向头发基色。
重要心得:不要用纯黑或纯白作为Base Color。纯黑会吞噬所有细节,纯白则失去了色彩信息。从中等明度的颜色开始调试,比如
(0.3, 0.2, 0.1)的深棕色。Absorption参数从(0.2, 0.4, 0.8)这样的值开始尝试,你会看到明显的透光色偏。
4.2 高光参数(Specular Parameters)
这部分控制三条光路的强度、范围和锐利度。
- Specular (R):主高光(R通道)的强度。控制头发上最亮那条“线”的亮度。
- Specular (TT):透射高光(TT通道)的强度。控制头发边缘透光的亮度。
- Specular (TRT):次级高光(TRT通道)的强度。控制头发表面那层柔和光泽的亮度。
- Roughness (R/TT/TRT):分别控制三个通道的粗糙度。数值越大,高光越模糊、范围越大。通常来说:
Roughness (R)最小(如0.1-0.3),保持主高光锐利。Roughness (TT)中等(如0.3-0.5)。Roughness (TRT)最大(如0.5-0.7),使其最柔和。
- Shift (R/TT/TRT):这是一个关键参数。它控制高光沿切线方向的偏移。由于头发不是完美的圆柱,表面有毛鳞片,导致R和TRT高光并不完全对称于法线平面。微调这个值(比如-0.1到0.1之间),可以让高光看起来更自然,打破死板的对称感。
4.3 几何与法线(Geometry & Normal)
- Tangent:如前所述,必须正确。如果你的高光方向是乱的,首先检查模型导入设置和原始模型的切线数据。
- Normal Map:可以使用法线贴图来模拟发丝间的细小凹凸,增加细节。但注意,法线贴图的强度不宜过大,否则会干扰基于切线方向的主高光计算。
- Flow Map:一些高级实现会支持流向图(Flow Map),它是一种特殊的纹理,用于更精确地控制每一点上的发丝方向,尤其适用于复杂的发型或动态效果。本项目可能未直接提供,但你可以了解这是未来的优化方向。
4.4 环境与光照设置
Marschner Shader是物理基础的,它对光照环境非常敏感。在默认的Unity天空盒和单一平行光下,效果可能出不来。
- 使用HDR/高动态范围环境:在
Window -> Rendering -> Lighting设置中,给场景添加一个HDRI环境贴图。可以去Poly Haven等网站下载免费的HDRI。一个丰富的光照环境能立刻让头发的各向异性高光和透光感显现出来。 - 调整光源角度:头发的美感极度依赖光影。尝试将主光源(Directional Light)放在角色侧后方(逆光或侧逆光),这是观察TT透射效果和R高光轮廓的最佳角度。正面光往往会让头发看起来很平。
- 启用阴影:确保头发能投射和接收阴影。自阴影是体现头发体积感和层次感的关键。
5. Haircard与Furshell两种渲染路径实战
项目中通常包含两种主要的渲染方案,对应不同的建模和性能需求。
5.1 Haircard(发片)方案详解
这是游戏中最常见的头发解决方案。用一系列带有透明通道的卡片状网格(Alpha-Clipped或Alpha-Blended)来模拟一簇簇头发。
- 优势:性能高,可控性强,适合中远距离观看。可以配合发丝纹理(Strand Texture)获得丰富的细节。
- Shader适配:使用项目中的
Haircard版本Shader。这个Shader会特别处理透明度测试(Alpha Clip)或混合(Alpha Blend),并优化各向异性计算以适应片状几何体。 - 实操步骤:
- 在3D软件中制作发片模型,并确保UV正确(通常U沿发丝方向,V沿发片宽度方向)。
- 绘制或生成一张高质量的发丝纹理(Strand Texture),包含发丝的颜色、透明度和法线信息。
- 在Unity中,将纹理贴图连接到材质的
Albedo、Alpha和Normal Map通道。 - 关键一步:调整材质的渲染队列(Render Queue)和混合模式(Blend Mode)。对于半透明发片,通常设置为
Transparent,并使用SrcAlpha OneMinusSrcAlpha混合。注意半透明物体的渲染顺序问题,可能需要手动调整或使用渲染队列值。 - 仔细调整
Alpha Clip Threshold(透明度测试阈值),在消除锯齿和保留发丝细节之间找到平衡。
5.2 Furshell(毛壳/体积毛发)方案详解
这种方案用于模拟动物毛发、绒毛或非常浓密的短发。它通常使用多层(Shells)逐渐放大的、相同拓扑的网格叠加,在每层上通过顶点偏移和透明度渐变来模拟体积感。
- 优势:能产生真实的体积感和次表面散射效果,适合近距离特写。动态效果(如风吹动)更自然。
- Shader适配:使用项目中的
Furshell或Fur版本Shader。这个Shader内置或需要配合几何着色器/顶点着色器来实现层(Shell)的偏移和淡出。 - 实操步骤:
- 准备一个基础网格(如动物的皮肤)。
- 材质使用Furshell Shader。Shader内部通常会有一个
Shell Count参数,控制层数(如32层)。层数越多,体积感越平滑,性能开销越大。 - 调整
Shell Step(层间偏移量)和Fade(边缘淡出)参数,控制毛发的长度和形状。 - Furshell方案对法线和切线的要求极高,因为每层都需要正确的方向来计算光照。确保基础网格的法线/切线信息准确无误。
- 性能警告:Furshell是性能消耗大户,尤其是在移动平台。务必严格控制层数,并考虑使用LOD(细节层次),在远距离切换为更简单的Haircard或甚至贴图模拟。
5.3 方案选择与性能考量
| 特性 | Haircard (发片) | Furshell (毛壳) |
|---|---|---|
| 视觉质量 | 中-高,依赖纹理细节 | 极高,有真实体积感 |
| 性能开销 | 低-中 | 高-极高 |
| 适用距离 | 中-远距离 | 近距离、特写 |
| 动态模拟 | 较难,依赖骨骼动画 | 较易,可通过顶点偏移模拟 |
| 建模复杂度 | 中,需要制作发片 | 低,只需基础网格,但Shader复杂 |
| 内存占用 | 中(纹理内存) | 高(顶点数倍增) |
个人建议:对于大多数游戏角色,Haircard方案是性价比最高的选择。用精心制作的发片模型和纹理,配合Marschner Shader,足以在绝大多数情况下达到出色的效果。Furshell方案更适合影视、CG或对特定角色(如毛茸茸的怪物)有极高要求的项目。
6. 常见问题排查与性能优化指南
在实际使用中,你肯定会遇到各种奇怪的问题。这里我整理了一份“踩坑实录”,希望能帮你快速排雷。
6.1 视觉问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 头发全黑或极暗 | 1. 环境光太弱或没有。 2. Absorption参数值设置过大(尤其是TT)。 3. 光源被遮挡或强度为0。 | 1. 添加HDRI环境光,增强平行光强度。 2. 降低 Absorption (TT)和Absorption (TRT)的数值(如从(0.8,0.8,0.8)降到(0.2,0.3,0.5))。3. 检查场景光源。 |
| 高光方向错乱或呈斑点状 | 1. 模型顶点切线(Tangent)数据错误或混乱。 2. 法线贴图太强或方向错误。 | 1.这是最常见原因。在建模软件中重新计算并平滑切线,确保其方向沿发丝生长方向。在Unity导入设置中检查切线计算选项。 2. 降低法线贴图强度,或检查法线贴图是否为正确的切线空间法线。 |
| 没有透光(金边)效果 | 1. 光源角度问题(正面光)。 2. Specular (TT)强度为0或过低。3. Absorption参数导致透光色被吸收。 | 1. 将主光源移至角色侧后方(逆光)。 2. 提高 Specular (TT)值。3. 调整 Absorption (TT),尝试设置一个吸收某种颜色(如蓝色)的值,让补色(黄色)透出。 |
| 头发看起来像金属或塑料 | 1.Specular (R)强度过高,Roughness (R)过低。2. 缺少TRT通道的贡献,显得单调。 | 1. 降低Specular (R),适当提高Roughness (R)(如到0.2)。2. 确保 Specular (TRT)不为0,并给Roughness (TRT)一个较大的值(如0.6),增加柔和光泽。 |
| 半透明发片边缘有硬边或锯齿 | 1. 使用了Alpha Clip(透明度测试),且阈值不当。2. 纹理边缘过渡不自然。 | 1. 尝试使用Alpha Blend(透明度混合)模式,但注意渲染顺序和性能。2. 如果必须用Clip,开启MSAA或FXAA抗锯齿,并微调 Alpha Clip Threshold。在纹理制作时确保alpha通道有平滑过渡。 |
| 在特定角度下高光突然消失 | 可能遇到了各向异性高光计算中的奇点或数值精度问题。 | 这是Shader实现中可能存在的技术问题。可以尝试轻微调整Shift参数,或检查Shader代码中是否有对dot(N, V)等值做clamp或saturate保护。 |
6.2 性能优化要点
Marschner计算比普通Shader复杂,在移动平台或低端PC上需要特别注意。
- 简化计算:检查Shader变体。有些实现可能提供了“简化版”或“移动版”Shader,关闭了TRT等昂贵计算。如果效果要求不高,可以选用。
- 控制精度:在Shader中,将部分中间计算从
float改为half(半精度浮点数),可以在移动端获得显著的性能提升,且通常对画质影响不大。 - 减少纹理采样:合并纹理。例如,将高光强度、粗糙度等参数打包到一张纹理的RGBA通道中,减少采样次数。
- LOD系统:为头发模型设置LOD(Level of Detail)。在远处,使用面数更少的模型,甚至切换回更简单的Standard Shader或一个自定义的、更廉价的头发Shader。
- 批处理与合批:确保使用相同材质的发片模型尽可能静态合批或动态合批,减少Draw Call。注意半透明物体的合批限制较多。
- 针对Furshell:严格控制Shell层数。20层和40层的视觉差异可能并不明显,但性能开销几乎翻倍。根据摄像机距离动态调整层数是一个高级优化策略。
6.3 与URP/HDRP的兼容性说明
原项目主要基于Unity内置渲染管线(Built-in Renderer)开发。如果你需要在URP或HDRP中使用,可能会遇到一些兼容性问题:
- URP:需要将Shader代码移植到URP的Shader Library框架下。这涉及到:
- 替换光照相关的内置函数(如
UnityWorldSpaceLightDir为GetMainLight)。 - 适配URP的CBuffer和常量缓冲区。
- 处理阴影和深度纹理的采样方式。
- 一个可行的捷径:在URP项目中,尝试使用
Universal Render Pipeline/LitShader作为基础,然后将其中的光照计算部分替换为Marschner模型的核心函数。这是一个中级到高级的Shader编程任务。
- 替换光照相关的内置函数(如
- HDRP:HDRP有自己更复杂的材质系统和光照架构。移植工作量更大。幸运的是,Unity官方或社区可能已经有基于HDRP的毛发渲染方案(如HDRP的Hair Master Stack),你可以评估是否直接使用官方方案,或者将Marschner模型的思想整合进去。
我的经验是,如果项目不是必须使用SRP(可编程渲染管线),优先考虑在Built-in管线中使用此项目,最为稳定和直接。如果必须使用URP,做好投入时间进行Shader移植和调试的心理准备。
7. 进阶技巧与效果提升
当你基本参数调通后,下面这些技巧可以让你的头发效果更上一层楼。
7.1 使用发丝纹理(Strand Texture)增强细节
一张好的发丝纹理是Haircard方案的灵魂。它不应该只是简单的颜色条纹。
- 内容:理想的发丝纹理应包含:
- RGB通道:发丝的基础颜色和变化(可以有深浅、染色渐变)。
- Alpha通道:控制发丝的透明度和形状(用于Alpha Clip)。
- 法线信息:可以编码在另一张纹理中,用于模拟单根发丝的圆柱形凹凸感。
- 制作:可以使用Substance Designer、Photoshop或专门的头发纹理生成工具来制作。关键是纹理的方向性和随机性,避免出现明显的重复图案。
7.2 实现动态风吹效果
静态的头发再好,动起来假也白搭。
- 顶点动画:最简单的办法是在顶点着色器中,根据时间、风力方向和强度,沿着法线或副法线(Binormal)方向偏移顶点位置。可以叠加一个简单的噪声纹理来让运动更随机。
// 简化示例(在Shader顶点函数中) float3 windOffset = _WindDirection * _WindStrength * sin(_Time.y * _WindFrequency + vertexWorldPos.x * _WindTiling); windOffset *= noiseTex.Sample(sampler, uv * _NoiseTiling).r; // 添加噪声扰动 v.vertex.xyz += v.normal * windOffset; // 沿法线方向偏移 - 骨骼动画:对于复杂的发型,最好在3D软件中为发片设置骨骼,然后导入Unity进行骨骼动画。这样控制更精确,但工作量更大。
- 物理插件:使用Unity的DOTS Physics、Obi Cloth或第三方插件(如Dynamic Bone的替代品)来实现更真实的物理模拟。这需要将发片或发束作为物理实体来处理。
7.3 与后处理效果结合
Marschner头发可以和全局后处理效果完美结合,提升整体画面质感。
- 屏幕空间环境光遮蔽(SSAO):能增强头发与头皮、头发簇之间的接触阴影,增加体积感。确保你的头发材质能够正确地写入深度和法线纹理(G-Buffer),以供SSAO使用。
- 泛光(Bloom):头发的高光,尤其是R通道的锐利高光,是触发泛光的绝佳来源。适度的泛光可以让高光看起来更柔和、更“发光”,提升质感。
- 色调映射(Tone Mapping):在HDR管线中,正确的色调映射(如ACES)能让头发的高光部分更自然地从亮部过渡,避免惨白。
最后,我想说的是,Marschner模型是一个强大的工具,但它不是魔法。它需要你耐心地调整参数,并结合良好的模型、纹理和光照环境。不要指望套上Shader就立刻得到电影效果。多观察真实头发的照片和视频,分析其高光形状、颜色和透光特性,然后回到Shader参数中去模仿。这个过程本身,就是对计算机图形学中光照模型最深刻的学习。从这个项目出发,你甚至可以尝试去阅读原始的Marschner论文,或者UE的毛发着色源码,那时你会发现,眼前这些参数滑块背后的数学世界,才是真正迷人的地方。