
1. 项目概述与核心价值最近在做一个角色项目头发渲染这块儿一直是个老大难问题。Unity自带的Standard Shader或者URP/HDRP里的Hair Shader做做卡通风格还行一旦追求写实那种油腻、塑料感或者像一坨毛线似的质感就全出来了。相信很多做角色、尤其是对角色毛发有高要求的开发者都遇到过这个瓶颈。后来在GitHub上翻找解决方案发现了maajor大佬开源的Marschner-Hair-Unity项目亲测下来效果非常惊艳而且是完全免费的。这个项目不是一个简单的材质球而是一套在Unity中完整实现了基于物理的Marschner毛发着色模型的方案。简单来说Marschner模型是目前业界公认的、用于模拟真实人类头发光散射行为的“金标准”。它最早由Marschner等人在2003年的论文《Light Scattering from Human Hair Fibers》中提出后来被Epic Games吸收并改进成为了Unreal Engine中头发渲染的核心技术。这个开源项目就是把这套顶级的、在3A游戏中广泛应用的技术完整地搬到了Unity里。它不仅仅是一个Shader更包含了从理论基础到实际应用包括发片Haircard和毛壳Furshell两种渲染方式的完整实现。对于想在Unity里做出电影级或次世代游戏级角色毛发的开发者来说这无疑是一个宝藏。2. Marschner毛发着色模型原理解析2.1 为什么传统着色模型做不好头发在深入Marschner之前我们先得明白传统Lambert、Phong甚至PBR模型为什么在头发上“失灵”。头发是一根根半透明的圆柱体光线与它的交互极其复杂。当一束光照射到一根头发上时会发生三种主要的光路R反射R分量光线在头发外表面直接反射。这类似于高光但头发是圆柱体所以这个反射会沿着一个圆锥面分布形成一条细长的亮带。TT透射透射TT分量光线穿透头发在内部经过一次折射从另一侧穿出。这赋予了头发“透光”的特性比如逆光时头发边缘的“金边”效果。TRT透射反射透射TRT分量光线先穿透进入头发在内部的内壁上发生一次反射然后再折射出来。这个分量贡献了头发的主色也是我们通常看到的头发颜色和“天鹅绒”般质感的主要来源。传统的着色模型根本无法模拟这种复杂的光路分裂和散射。它们通常只考虑单一的漫反射和镜面反射导致头发要么看起来像实心塑料缺少TT透光要么颜色暗淡缺少TRT的彩色散射高光也只是一团光斑而非正确的圆锥分布。2.2 Marschner模型的核心三分量Marschner模型的精髓就在于它用数学公式精确地描述了上述R、TT、TRT三条光路。在Shader中它会分别计算这三个分量然后叠加起来形成最终的头发颜色。R分量直接反射这个分量模拟光线在头发角质层最外层的镜面反射。它的强度很高但范围很窄形成头发上最亮的那条“高光丝”。它的颜色偏向光源色通常是白色并且其方向性极强会随着观察角度和光线角度的变化而快速移动。TT分量透射这是头发背光面“透光感”的来源。当光线从头发后方照射时TT分量会让头发边缘发光。它的颜色会受到头发本身色素melanin的影响但通常比较淡。计算这个分量需要考虑光线在头发内部的折射路径。TRT分量次表面散射这是最重要的分量决定了头发的基础颜色和体积感。光线进入头发在内部被色素吸收并散射后再次射出携带了头发的本色如棕色、金色。这个分量的分布比R分量更宽、更柔和形成了头发的主体明暗和色彩过渡。在Shader实现中每一个分量都对应一个复杂的函数输入参数包括光线方向、视线方向、头发切线方向、以及一些控制参数如粗糙度、偏移量等。项目中的Marschner.hlsl文件就是这些核心函数的集合。2.3 与Kajiya-Kay模型的对比在Marschner之前游戏界广泛使用的是Kajiya-Kay模型。它也是一个专为头发设计的模型将高光处理成沿着发丝方向的各向异性高光条带比传统模型好很多。但Kajiya-Kay模型本质还是一个经验模型它没有基于真实的物理光路因此无法正确模拟透光TT和复杂的次表面散射TRT效果。相比之下Marschner是物理正确的它能产生更丰富、更自然的光影层次特别是在复杂光照环境下优势非常明显。注意物理正确也意味着计算更复杂。Marschner模型对Shader的算术逻辑单元ALU消耗远高于Kajiya-Kay。在性能敏感的移动端项目中使用时需要格外谨慎可能需要进行简化或作为高质量选项。3. 项目结构与环境准备3.1 项目仓库与文件解析从GitHub克隆或下载maajor/Marschner-Hair-Unity项目后其目录结构非常清晰Marschner-Hair-Unity/ ├── Assets/ │ ├── MarschnerHair/ # 核心资源文件夹 │ │ ├── Prefabs/ # 预制体包含发片和毛球示例 │ │ ├── Materials/ # 材质球使用了自定义Shader │ │ ├── Models/ # 示例模型如Houdini制作的毛球 │ │ ├── Textures/ # 可能用到的纹理如噪声图 │ │ └── Shaders/ # **核心Shader代码** │ │ ├── MarschnerHairCard.shader # 发片渲染Shader │ │ ├── MarschnerHairShell.shader # 毛壳渲染Shader │ │ ├── MarschnerHairCard.shadergraph # Shader Graph版本如有 │ │ ├── MarschnerHairShell.shadergraph │ │ └── Includes/ # HLSL包含文件 │ │ └── Marschner.hlsl # **Marschner模型的核心数学实现** ├── Packages/ # Unity包管理 └── ProjectSettings/ # 项目设置核心中的核心是Assets/MarschnerHair/Shaders/目录下的Shader文件和Includes/Marschner.hlsl。这个HLSL文件是跨渲染管线可复用的里面定义了计算R、TT、TRT分量的函数。3.2 Unity版本与渲染管线适配原项目可能是在某个特定版本的Unity和渲染管线如Built-in内置管线中创建的。现在主流是URP通用渲染管线和HDRP高清渲染管线。这个项目的价值在于其核心算法Marschner.hlsl是独立于管线的。内置管线Built-in直接使用项目中的.shader文件即可。确保Shader的编译目标级别如#pragma target 3.5与你的项目兼容。URP你需要将Shader代码移植到URP的Shader框架下。主要工作是替换头部的Shader “...”为URP的Lit或Unlit Shader模板结构。包含URP的核心库文件如Packages/com.unity.render-pipelines.universal/ShaderLibrary/...。将光照获取方式从内置的_WorldSpaceLightPos0等改为URP的Lighting.hlsl和GetMainLight()函数。处理阴影接收可能需要额外的Pass或使用ShadowCasterPass。HDRP移植到HDRP更为复杂因为HDRP有自己严格的材质和光照体系。你可能需要将其实现为一个自定义的HDRP Lit Shader并处理好与HDRP的 deferred lighting/deferred material 系统的交互。实操心得对于大多数项目URP是平衡效果和效率的最佳选择。我的做法是以原版Built-in的Shader为蓝本在URP中新建一个Unlit Shader因为Marschner模型自己处理了所有光照然后逐步将原HLSL代码和属性块复制过去并重写顶点/片元着色器函数以适配URP的输入输出结构。这个过程需要对URP的Shader编写有一定了解。3.3 基础场景搭建为了测试效果建议新建一个简单的场景创建一个平面作为地面。将项目Assets/MarschnerHair/Prefabs/下的示例预制体如发片模型拖入场景。创建一个方向光Directional Light调整角度以观察头发高光的变化。可以再创建一个点光源或聚光灯从侧面或后方照射以观察TT透光效果。确保相机的渲染路径和后期处理如果需要设置正确。4. 两种渲染方式发片(Haircard)与毛壳(Furshell)详解这个项目提供了两种主流的实时毛发渲染技术路径对应不同的资源制作流程和视觉效果。4.1 发片Haircard渲染原理将一簇头发烘焙到一张或多个透明的卡片通常是面片纹理上。在渲染时将这些面片像“卡片”一样组合成头发的形状。这是目前游戏角色头发最主流、性能最高的方案。资源制作在DCC工具如Maya, Blender, Ornatrix中制作高精度的头发模型。将头发簇分组并沿着生长方向拍平烘焙到纹理上。通常需要烘焙的贴图包括颜色贴图Albedo头发的基础颜色。Alpha遮罩贴图定义发片的形状和毛发间的透明区域。法线贴图Normal提供每根毛发的微观细节和体积感。流向贴图Flow Map或切线贴图指示每根毛发的方向对于Marschner模型中计算切线Tangent至关重要。在Unity中使用一个或多个交叉或旋转的面片来摆放这些发片。项目中的实现MarschnerHairCard.shader就是为此设计的。它的关键输入除了颜色和Alpha外就是切线Tangent信息。在发片中顶点的切线方向通常就代表了毛发的生长方向。Shader利用这个切线方向结合光线和视线方向来计算Marschner三分量。优点性能极佳用很少的面片几十到几百个就能表现出发量丰富的头发。效果可控美术可以通过纹理精细控制每簇头发的外观。技术成熟管线工具链完善。缺点侧面穿帮从侧面看发片很薄容易穿帮。通常需要用多层发片交叉或使用专门的“发梢片”来弥补。动态僵硬发片整体的动态模拟如Physics Rigidbody或骨骼可以但很难模拟单根毛发的飘动。制作成本需要美术人员具备较高的烘焙和贴图绘制技能。4.2 毛壳Furshell / Hair Shell渲染原理也称为“多层透明壳”技术。它从一个基础网格如头皮开始沿着顶点法线方向逐层向外挤出多个几何壳层Shell。每一层使用相同的纹理但通过一个渐变因子进行偏移和淡化从而模拟出毛发的体积和层次感。这个项目中的“Furshell”版本就是用于此技术。资源制作需要一个基础模型如角色的光头模型。制作毛发的颜色贴图、Alpha贴图和方向贴图。这些贴图是投影到基础模型上的。在Shader中通过顶点偏移Vertex Offset生成多层壳。项目中的实现MarschnerHairShell.shader负责此渲染。它的核心是在顶点着色器中根据当前壳的层数一个从0到1的_ShellFactor沿着法线方向挤出顶点。在片元着色器中会使用一个噪声或渐变纹理来让外层壳逐渐透明并同样基于方向贴图提供的切线信息应用Marschner光照。优点体积感强天生具有很好的体积感和毛发密度尤其适合短发、胡须、绒毛或兽毛。动态灵活可以对基础网格进行骨骼蒙皮所有壳层会随之运动适合做毛发整体的动态。也可以对每层顶点做噪声扰动来模拟毛发扰动。无侧面穿帮因为是从体积极出的侧面看也有厚度。缺点性能开销大需要渲染多遍几何体每个壳层一遍Overdraw严重对填充率要求高。容易产生层间闪烁在摄像机移动时多层透明叠加容易产生Z-fighting或Alpha排序问题导致视觉上的闪烁俗称“shell popping”。发梢处理模拟长发的发梢比较困难容易显得笨重。选择建议写实的长发角色优先选择发片Haircard。这是业界标准在效果和性能上最平衡。短发、胡须、体毛或风格化的毛发可以尝试毛壳Furshell能获得很好的体积感。性能极限场景如移动端发片通常是唯一可行的选择且可能需要简化Marschner模型例如只保留R和TRT分量。5. 材质参数详解与调校指南无论是发片还是毛壳Shader其材质面板都暴露了一系列参数来控制Marschner模型和外观。理解每个参数是调出理想效果的关键。5.1 基础颜色与纹理参数_BaseColor/_BaseMap头发的基础颜色或颜色贴图。这主要影响TRT分量的颜色基调。_AlphaClipAlpha裁剪阈值。用于处理发片Alpha贴图的透明边缘低于此值的像素将被丢弃。_NormalMap/_NormalScale法线贴图。用于增加毛发表面的微观细节使高光反射更加丰富、不规则。对于毛发法线贴图通常比较柔和。5.2 Marschner模型核心参数这是调校的重点通常以“_M”为前缀。_MR控制**R分量直接反射**的强度。调高会让头发表面的那条最亮的高光丝更明显。_MTT控制**TT分量透射**的强度。这是背光透光效果的关键。调高后逆光下的头发边缘会更亮。对于深色头发适当提高此值可以避免头发在逆光下死黑一片。_MTRT控制**TRT分量次表面散射**的强度。这决定了头发主体颜色的饱和度和明亮度。调低会让头发看起来更暗、更干燥调高则更鲜艳、更有“湿润”感。_ColorR,_ColorTT,_ColorTRT分别控制三个分量的颜色偏移。通常_ColorR保持白色1,1,1_ColorTT可以是淡黄色或白色_ColorTRT则用来微调头发的本色。例如想让头发高光带点金色可以微调_ColorR想让透光更暖可以调整_ColorTT。_RoughnessR,_RoughnessTT,_RoughnessTRT分别控制三个分量的粗糙度或理解为光泽度、模糊度。值越大该分量的反射/散射范围越宽、越模糊值越小则越锐利、集中。_RoughnessR影响高光丝的粗细和锐利程度。健康顺滑的头发此值较低毛躁或受损头发此值较高。_RoughnessTRT影响头发主体颜色的过渡柔和度。此值对整体质感影响很大。_ShiftR,_ShiftTT,_ShiftTRT偏移参数。用于微调各分量反射角的角度偏移可以用于匹配不同发质或调整高光位置。5.3 毛壳Furshell特有参数_ShellCount壳层的总数。层数越多毛发体积感越细腻但性能消耗也线性增长。通常8-16层是平衡点。_ShellStep/_ShellLength控制壳层之间的间距和总体毛发长度。_ShellFade控制外层壳的淡出效果通常用一个噪声贴图_FadeNoise来控制使毛发末端看起来参差不齐更自然。5.4 调校流程与经验确立光照环境先在一个标准光照如单一方向光下调试避免复杂环境光的干扰。先调TRT定基调将_MTRT调到一个中间值然后通过_BaseColor和_ColorTRT确定头发的基础颜色。调整_RoughnessTRT获得想要的色彩柔和度。再调R加高光提高_MR观察高光丝是否出现。通过_RoughnessR调整高光的锐利度通过_ShiftR微调高光在头发上的位置。确保高光随着头部旋转而自然移动。最后调TT补透光将角色转到背光位置提高_MTT直到看到明显的边缘光。通过_ColorTT可以改变透光的色温冷/暖。整体微调回到正面光观察三个分量的叠加是否和谐。避免某个分量过于突兀。可以尝试降低_MTRT同时提高_MR和_MTT来获得更“立体”和“通透”的发质。结合纹理最后引入法线贴图和Alpha贴图细节会立刻丰富起来。避坑技巧调试时可以创建一个简单的脚本将_MR_MTT_MTRT三个参数分别绑定到UI Slider上并实时开关方向光旋转这样可以非常直观地看到每个分量的贡献和整体效果的变化效率极高。6. 性能分析与优化策略Marschner模型的计算复杂度较高在项目中大规模使用必须考虑性能。6.1 性能瓶颈分析ALU压力Shader计算Marschner模型涉及大量的复杂数学计算三角函数、指数、查找纹理等对片元着色器的计算能力要求高。这是最主要的瓶颈。带宽压力纹理采样发片方案通常需要多张纹理Color Alpha Normal Flow毛壳方案可能还需要噪声图。高分辨率纹理和多次采样会增加带宽消耗。Overdraw过度绘制发片由于Alpha混合半透明的发片重叠区域会导致像素被多次绘制。毛壳多层壳渲染本身就是严重的Overdraw层数越多越严重。Draw Call大量独立的发片模型或壳层会导致Draw Call上升。6.2 针对发片方案的优化纹理优化合并贴图将Color和Alpha合并到一张贴图的RGB和A通道。将法线贴图和流向贴图如果精度要求不高压缩存储。使用纹理阵列Texture2DArray如果角色有多种发色或发型变体使用纹理阵列可以减少Shader变体和纹理切换。合理设置Mipmap和压缩格式毛发贴图的细节在远处不需要很高开启Mipmap并选用合适的压缩格式如ASTC能节省大量带宽。Shader简化LOD创建简化版Shader为中远距离的头发创建一个简化Shader。例如可以简化Marschner计算只保留最重要的TRT分量和一个简化的R分量甚至回退到Kajiya-Kay模型。使用Shader LOD在Unity Shader中使用LOD指令根据距离自动切换不同复杂度的Shader。模型与合批合并发片网格将相邻的、使用相同材质的发片网格在建模软件或运行时合并以减少Draw Call。利用GPU Instancing确保Shader支持GPU Instancing对于大量重复的发片如人群能极大降低Draw Call。6.3 针对毛壳方案的优化控制壳层数在视觉可接受的范围内尽可能减少_ShellCount。对于短发8层可能就够了对于长发可能需要12-16层。可以通过脚本根据摄像机距离动态调整壳层数。Early-Z / Depth Prepass在渲染毛发前先渲染一次不透明物体的深度图。然后在渲染毛发壳层时使用ZTest LEqual并开启ZWrite Off但利用Clip()函数在片元着色器早期就丢弃被遮挡的像素例如通过_AlphaClip。这能有效减少被遮挡壳层的片元计算。层级淡化与剔除在Shader中让最内层和最外层的壳更透明甚至不渲染只让中间几层完全贡献。这可以通过_ShellFade参数和噪声图精细控制在视觉上不易察觉但能减少有效渲染层数。避免全屏毛壳毛壳技术更适合局部使用如胡须、眉毛、鬓角或兽类角色的局部毛发。避免对整个角色模型使用全毛壳渲染。6.4 平台适配策略PC/主机可以开启全部特性使用较高精度的计算和纹理。高端移动设备iOS/Android旗舰机可以使用完整的Marschner Shader但需要严格控制发片数量、纹理分辨率和壳层数。建议关闭或简化TT分量。中低端移动设备可能需要回退到极度简化的着色模型如只用一个各向异性高光模拟R分量加上一个简单的颜色作为TRT。或者考虑使用预积分的毛发光照贴图Pre-integrated Hair Lighting等更节省的方案。7. 常见问题排查与解决方案实录在实际使用Marschner-Hair-Unity项目时你肯定会遇到各种问题。下面是我踩过的一些坑和解决方法。7.1 渲染问题问题1头发看起来全黑或没有高光。排查首先检查场景中的灯光是否启用强度是否大于0。然后检查材质的_MR直接反射强度和_MTRT次表面散射强度参数是否被误设为0。解决确保有有效的光源照射。将_MR和_MTRT调至0.5左右观察效果。检查Shader是否编译正确在Frame Debugger中查看该材质球是否被正确渲染。问题2发片边缘有锯齿或闪烁。排查这是Alpha TestAlpha裁剪导致的边缘锯齿。在摄像机移动时由于深度缓冲的精度问题边缘像素会在“裁剪”与“不裁剪”之间闪烁。解决优先方案使用Alpha Blend。将渲染类型RenderType从TransparentCutout改为Transparent并使用Blend SrcAlpha OneMinusSrcAlpha进行混合。但这要求发片纹理的Alpha通道有平滑的渐变过渡而不是硬边缘且渲染顺序需要妥善管理以避免透明排序问题。抗锯齿开启MSAA或后处理抗锯齿如SMAA FXAA可以缓解。软化边缘在Photoshop中处理Alpha贴图让发丝边缘有几像素的渐变而不是从1直接到0的硬切。问题3毛壳渲染出现严重的层间闪烁Shell Popping。排查这是毛壳技术的固有问题。当摄像机或物体移动时由于每层壳的深度值差异微小在深度测试和透明混合时会产生Z-fighting导致视觉上的闪烁。解决增加壳层间距稍微增大_ShellStep值让层与层之间的深度差异更明显。深度偏移在Shader中使用Offset指令给每一层壳施加一个微小的深度偏移。可以写一个简单的脚本根据壳的层数动态设置一个递增的偏移值。淡化最内层和最外层如优化策略所述让靠近头皮和最外层的壳更透明减少它们对最终颜色的贡献可以弱化闪烁感。接受瑕疵在快速运动或特定视角下轻微的闪烁有时难以完全避免需要权衡效果与性能/实现成本。问题4透光TT效果不明显或颜色不对。排查检查灯光是否在头发后方。_MTT强度是否足够。_ColorTT是否被设成了黑色或很暗的颜色。解决使用一个强烈的背光进行测试。将_MTT值提高到1.0以上该参数可能没有严格物理范围可以超过1。将_ColorTT设置为亮色如白色(1,1,1)或淡黄色(1,1,0.8)。同时检查_RoughnessTT过高的值会让透光效果过于弥散而不明显。7.2 性能问题问题游戏帧率在角色特写时骤降。排查使用Unity Profiler的GPU模块查看是哪个Pass或Draw Call耗时最长。很可能是毛发渲染的片元着色器复杂度ALU过高或者是毛壳的Overdraw太严重。解决对于发片检查同屏发片数量是否可合并网格。降低发片纹理分辨率。使用上文提到的简化版Shader LOD。对于毛壳果断减少_ShellCount。检查是否可以对非焦点角色禁用毛壳或使用更低配置。通用确保所有毛发材质都正确开启了GPU Instancing如果适用。7.3 工作流与资源问题问题导入的发片模型切线信息错误导致高光方向怪异。排查在Shader中将切线方向Tangent可视化输出为颜色例如将float3 tangent : TANGENT;在片元着色器中直接作为颜色返回。如果颜色混乱或不随模型旋转而变化说明切线数据有问题。解决在3D建模软件中导出时务必勾选“导出切线Tangents”或“导出UV和法线切线”选项。在Unity中检查模型导入设置。在Model分页下确保“Normals Tangents”选项不是“None”或“Calculate”。对于自定义切线如流向贴图定义的切线可能需要选择“Calculate”并从法线计算然后在Shader中用流向贴图去扰动它。对于发片切线方向应该与毛发生长方向通常是从发根到发梢一致。如果方向反了高光位置会错乱。可以在Shader中尝试对切线方向取反tangent.xyz -tangent.xyz;来测试。问题毛壳在角色动画时毛发与身体穿插。排查这是顶点沿法线挤出导致的。当身体网格变形如手臂弯曲时挤出的壳层可能会穿入相邻的身体部位。解决碰撞体约束在基础网格下添加简单的碰撞体如胶囊体在Shader中实现简单的顶点排斥Vertex Repulsion。当挤出的顶点进入碰撞体空间时将其沿法线反向推回。这需要一些自定义Shader代码和CPU-GPU数据传递。美术修正让动画师在极端姿势下调整动作避免明显的穿插。或者要求模型在绑定权重时确保毛发生长区域的皮肤权重平滑避免剧烈变形。分层渲染这是一个取巧但有效的方法。将毛发渲染在一个单独的、稍微放大的透明模型上这个模型只做简单的变形从而与高精度变形的身体模型在视觉上分离减少穿插感。但这会增加Draw Call和渲染状态切换。这个开源项目为我们打开了一扇通往高质量实时毛发渲染的大门。它提供的不仅是一段代码更是一个完整的学习范式和调试起点。从我自己的使用体验来看最大的收获不是直接拿到了一个“完美”的Shader而是通过阅读和修改它的代码真正理解了Marschner模型的每一个参数是如何影响最终像素的。这种理解让你在面对任何毛发渲染需求时都能心中有数知道该从何处着手调整和优化。无论是为了在下一个独立游戏中实现令人眼前一亮的角色发丝还是为了在技术Demo中挑战视觉极限这个项目都值得你花时间深入钻研。记住好的效果永远是艺术指导和技术实现反复磨合的结果多调、多试、多对比你就能掌控这门“发丝上的光影魔术”。