前阵子带一个数字人项目,美术同事第一次把角色丢进UE4里做灯光测试,截图发到群里,几个人看着那张图都沉默了。模型布线、贴图精度都是按写实标准做的,但灯光一打,人就是个蜡像:皮肤是硅胶质感,眼神像贴纸,头发像方便面。我花了两周时间把UE4里跟“人”有关的着色链路从上到下翻了一遍——皮肤、头发、眼睛、口腔,再接入动捕和面捕设备做实时驱动,才终于让这个角色在镜头里算得上“像活人”。这篇文章就是记录那段时间拆出来的底层逻辑和踩过的坑。
UE4里没有叫“人类着色器”的官方材质节点,中文社区这么说的,通常是指一整套专门用来渲染写实人物的着色方案。它不解决“怎么做一个会动的模型”,而是解决“为什么模型明明很精细,看起来却不像人”的问题。下面我按渲染栈的每一层展开。
1. 先从“塑料人”说起:人体着色器在解决什么问题
1.1 人眼对“活人”的容忍度极低
人类视觉系统识别人脸和肤色几乎是“专用电路”。大脑会特别留意皮肤的血色、眼睛的湿润度、头发的高光形状,任何一项光学特征不对,都会被潜意识标记为“不自然”。这就是为什么很多3D角色在技术指标上并不差——面数高、贴图清晰,但观众一眼就觉得恐怖谷。问题不在模型精度,而在着色器反馈给视觉系统的信息错了。
举个例子:把一块浅灰色PBR材质球放在角色脸上,法线和UV都接对了,灯光也打得柔和,结果看起来依然像橡皮泥。因为真实皮肤不是均匀漫反射体,它内部有散射、有毛细血管、有油脂层的微弱镜面反射,这些光学行为需要专门的着色模型才能模拟。
1.2 数字人的渲染是一套着色器组合,不是单个材质
写实人物渲染从来不是“一个材质搞定全身”。至少需要五类着色模型协同工作:
- 皮肤:用次表面散射(SSS)模拟光在皮肤下的扩散和红色透射,这是“血色感”的来源。
- 头发:每一根头发都是半透明圆柱体,需要各向异性双高光,普通PBR高光做出来是绸缎,不是头发。
- 眼睛:角膜是凸面透明体,虹膜在它下面会发生视差偏移,普通球体材质会显得像玻璃珠或死鱼眼。
- 口腔:牙齿半透明、牙龈充血红润、舌头有湿润反射,一个SSS材质包打不了全场。
- 汗毛和肤色过渡带:人脸还有眉毛、睫毛、细汗毛和皮肤与嘴唇的过渡区,都需要独立控制粗糙度和高光强度。
所以“人类着色器”严格说是“人体渲染栈”。在UE4里这个栈由几个不同的ShadingModel配合SubsurfaceProfile、Groom、自写Custom Expression共同组成。
1.3 为什么默认Lit总是差一口气
UE4的默认Lit假设表面是不透明的刚性物质,反射靠粗糙度加金属度,这套逻辑对地面、墙面、车漆是对的,但人体几乎每一种组织都在打破这个假设。皮肤有内散射,头发表面有次表面透射,眼球是折射透镜,口腔有黏膜体液,默认Lit完全覆盖不了。
UE4给你提供了Subsurface Profile、Hair、Eye这些专门的ShadingModel,但需要你自己组装。最花时间的不是某个节点怎么写,而是把整条链路串起来之后逐一微调参数,让每个部位在同一个灯光环境下互相配合。这一章先讲清楚概念,下面逐一拆解链路里最重要的三块:皮肤、头发、眼睛。
2. 皮肤渲染:次表面散射的工程化实现
2.1 物理:为什么皮肤必须是“透”的
用手电筒照手指,另一侧会透出红光。光不是只在皮肤表面反弹,还会进入皮肤内部,被真皮层的胶原蛋白和毛细血管散射,最后从邻近位置射出。散射时蓝绿光被血红蛋白吸收得比红光更厉害,所以透射光偏红。这个现象叫次表面散射。
渲染上如果你把皮肤当成完全不透明的表面,哪怕法线贴图做到8K,看起来还是像喷了漆的塑料。因为缺少了“光在皮下散步”产生的柔和过渡和血色感。这也是为什么我判断一个角色像不像真人,第一步就是把侧逆光打开看脸部边缘透不透红。
2.2 UE4里的SSS实现路径:SubsurfaceProfile加屏幕空间模糊
UE4皮肤渲染的标准做法是:
- 材质ShadingModel选择Subsurface Profile。
- 新建一个SubsurfaceProfile资产,引用给材质。
- 材质输出SubsurfaceColor、Normal、Roughness等常规项。
运行时,UE4先按光照计算出一张带SubsurfaceColor信息的图,然后在屏幕空间做两个方向的高斯卷积(先水平后垂直),用ScatterRadius控制模糊半径。这个模糊过程模拟的就是“光向邻近像素扩散”。所以皮肤边缘的红色透光、脸颊的柔和泛红、耳尖的反光都来自这一步。
我实际调整的时候发现,SSS效果在正面直射光下并不明显,但在侧逆光、边缘逆光、柔和天光下差异极大。给角色打灯做最终效果时,一定要有背光通道,否则SSS调了等于白调。
2.3 SubsurfaceProfile关键参数与我的调参习惯
下面这张表是我在项目里最常用的参数调整清单:
| 参数 | 作用 | 我的经验值 |
|---|---|---|
| ScatterRadius | 光的横向扩散距离,越大越像蜡 | 脸部常用0.5到2.0,超过3会糊 |
| SubsurfaceColor | 内部散射颜色,决定血色 | 脸颊偏红,鼻翼轻微,额头偏白 |
| BoundaryColorBlend | 边界区域颜色混合强度 | 耳朵和鼻尖高一点,大概0.6 |
| Transmission | 背部透射强度 | 手指、耳朵明显,脸部不放大 |
| Opacity | 透射颜色的深度控制 | 别拉满,拉满像半透明蜡像 |
调参顺序我一般是先确定ScatterRadius,稳定大局,再用贴图遮罩去加细节。脸颊做一块偏红润的mask,额头和下巴稍微压暗,胸口的皮肤和脸部用同一个Profile但SubsurfaceColor分开控制。这样做的好处是角色在暖光和冷光下看起来的“血色感”是一致的,不会出现脸白脖子红这种穿帮。
2.4 皮肤调参的三个“一看就假”的坑
第一个坑是惨白。可能是Albedo贴图在线性空间和sRGB之间没理清,或者SubsurfaceColor给成了纯白,也可能ScatterRadius拉太大,把整个脸都糊成了白蜡。我遇到一次排查很久,最后发现是材质实例里引用的SubsurfaceProfile丢失,引擎静默回退到了默认Lit,整个SSS等于没开。
第二个坑是糊。SSS本质上会抹掉毛孔和微结构细节,所以要做细节法线叠加。我的做法是角色脸上放两层法线:一层大尺度面部形状法线,一层高频毛孔细节法线,两层的UV大概率不一样,用Detail Normal节点混合。糊脸问题,一半靠这个解决。
第三个坑是油腻。皮肤不是塑料更不是油面,真实皮肤的高光是宽而弱的,不应该有清晰的镜面光斑。把Roughness控制好,皮肤眼周、鼻头稍微低一点,脸颊和额头相对高,别一刀切。
3. 头发:从“方便面”到真头发,各向异性高光的秘密
3.1 一根头发是一根半透明圆柱
头发的光学行为不是“一根粗糙的细线”,它是一根半透明圆柱体。光在表面直接反射形成第一道高光,一部分光穿透表面进入发丝内部,从背面射出形成透射光,还有一部分在内部反射一次后重新射出,形成第二道高光。
第二道高光的位置比第一道靠下,色调偏暖偏深。这就解释了为什么真人头发在逆光和侧光下经常能看到两条光带,而许多廉价手游头发只有一道白色高光,像用梳子刷了油。
3.2 UE4传统管线与Groom Strand的选择
UE4传统角色的头发材质ShadingModel里有一个Hair选项,内部实现是Kajiya-Kay风格,提供两个高光峰值和一个透射通道。你可以控制PrimarySpecular和SecondarySpecular的比例、强度、光泽的分布。这套方案在远景和普通游戏镜头里够用,但近景特写很容易穿帮,因为发丝之间没有真实几何遮挡。
UE4从4.26开始Hair Strands系统稳定之后,Groom才是写实近景的正解。发丝是真正的曲线几何,材质层使用更接近Marschner风格的双高光散射模型,并且有发丝间的遮挡与自阴影。项目里近景角色,我基本不考虑传统网格头发加Hair材质了。
3.3 头发实操中的四个关键点
切线方向是头发高光的灵魂。传统网格头发的高光延展方向靠切线,而切线又与你拆UV的方向挂钩。如果UV接缝没处理好,或者UV走向和发丝方向不一致,高光会一段段断开。Groom则要保证导入曲线时正确烘焙切线,这一步最容易在DCC里漏掉。
发根发尖要区分。现实中发根靠近头皮处有皮脂和阴影,看起来偏暗;发梢更干更散,粗糙度更高、反光更弱。这些可以用渐变贴图或顶点色控制,偷懒的话至少用一张alpha渐变把发尾的粗糙度提上去,否则整头头发会像塑料帘子。
发丝自阴影不能省。Groom如果关了高级阴影,发丝之间会互相曝光,整头头发像半透明薄纱。开启深层阴影和发丝AO之后,近景头发才有一绺一绺的体积感。
性能预算要在做项目时就定死。几百万根发丝配合实时阴影,在高分辨率下可以吃掉3到6毫秒。我的做法是同一套角色给三套Groom LOD:近景高密度、中景中密度、远景直接切卡片头发加透明贴图,宁可切得明显也不要卡死帧率。
4. 眼球与面部微细节:决定“像不像真人”的最后1%
4.1 眼球是一个多层光学系统
眼球最外是凸面透明的角膜,中间是虹膜,里面是晶状体。我们看到虹膜时,光实际上穿过了角膜,然后被虹膜反射回来,再穿过角膜进入镜头。角膜本身是凸透镜,所以虹膜的位置会随着视角变化发生视差偏移。角膜表面还会反射环境光和光源,形成我们熟悉的“眼睛里的光点”。
如果只是把眼球当普通球体材质处理,做出来的东西叫“死鱼眼”,因为光泽位置不对、虹膜看起来是贴在表面的贴纸,而不是浮在角膜之下的结构。
4.2 UE4里的两种眼球方案
UE4自带Eye ShadingModel,提供了角膜处理相关的输入通道,能控制虹膜遮罩和角膜高光。项目里我用得更多还是自己用Custom Expression加视差偏移的做法,因为可以精细控制每一条UV流:
- 眼球模型拆成眼白和虹膜两层网格,角膜用半透明或低糙度覆盖层模拟凸面。
- 虹膜UV以瞳孔为中心做视差偏移,偏移方向与视线方向挂钩,模拟水下的折射错位。
- 角膜层单独采样法线贴图和反射捕获,制造干净锐利的“玻璃凸面”高光。
- 瞳孔直径做成参数,方便后续被外部设备数据驱动。
这样一套下来,眼睛在摇头时虹膜会微微“漂移”,高光会跟着环境变化,而不是死死钉在眼球上。
4.3 面部微细节:毛孔、皱纹、牙齿、牙龈
面部只有SSS还不够。真人脸的质感很大程度来自高频毛孔细节和肌肉运动产生的皱纹。我的做法是两张法线贴图叠乘,一张是面部整体形态的法线,另一张是高频毛孔细节,UV不同也没关系,用DetailNormal节点处理。近景特写还要加皱纹法线,集中在额纹、眼周纹、法令纹附近,表情驱动时过渡要柔和,不能出现切开的“刀口”。
牙齿和牙龈也不能随手给一个白球。牙齿本身半透明,高光尖锐偏冷;牙龈充血红润,吸光度强,粗糙度比牙齿高得多。口腔内部的AO尤其重要,不然角色一张嘴里头就是一片惨白。
5. 让数字人“活”起来:外接设备映射与动态参数驱动
5.1 静态着色器只是底子,外部数据才是灵魂
写实角色只靠静态着色器,即使材质全对,看起来也像一尊半透明蜡像。活人每一帧都在微动:呼吸的起伏、表情肌肉的抽动、眼球的扫视、脸颊因为情绪涌上的潮红。想要打破恐怖谷,“数据驱动”比“材质精度”更关键。
外接设备映射就是干这个的:把真实人的表情、姿态、视线、甚至心率/呼吸数据实时喂进UE4,让骨骼网格体和材质参数跟着动。
5.2 从设备到UE4的数据链路
我项目里用的标准链路是:
外部设备(如iPhone的ARKit面捕、Xsens/Vicon动捕、眼动仪)→ 中间件(LiveLink Face、第三方动捕插件)→ UE4的LiveLink → 动画蓝图 → Morphtarget权重或骨骼变换 → 材质参数更新 → 着色器实时反馈。
面捕设备输出的一般是52个BlendShape系数,这些系数可以直接驱动角色的Morphtarget权重。动捕设备输出骨骼旋转数据,经过重定向后驱动角色的骨骼。眼动仪输出视线方向,换算成眼球模型的旋转角度和瞳孔缩放参数。
这条链路只是第一步。真正让我觉得角色“活过来”的,是把这些数据进一步映射到着色器参数上。
5.3 把外部数据映射到Shader参数:几个可以直接抄的套路
我用得最多的几个映射效果:
情绪脸红。把面捕的smile、browDown等BlendShape系数加权成一个“激动值”,用这个值去驱动皮肤材质里SubsurfaceColor的红色通道。角色说话说嗨了,脸颊自然会有充血的视觉效果。这个效果比直接调Diffuse红润感要真实得多,因为SSS的红色会像皮肤下面透出来一样。
瞳孔缩放。把眼动仪获取的注视距离或者环境亮度数据,映射到眼睛材质里控制虹膜UV缩放的参数。谈话对象离得近,瞳孔缩小;灯光变暗,瞳孔放大。这是一个很小的细节,但增加眼球的空间深度感特别明显。
呼吸驱动皮肤微动。用动捕胸腔的Y轴位移或心率数据,驱动一个低频Panner噪音,让皮肤细节法线的强度发生周期性微变。镜头怼脸时,观众潜意识能感觉到皮肤下面的微循环在动。
眨眼联动泪膜高光。面捕的blink系数控制眼睑闭合时,同时抬起眼睛材质里“泪膜湿润高光”的强度。因为真实眨眼瞬间泪膜会重新铺展,产生短促的高光反射。这种联动让眼睛的高光变化有了生理逻辑,不再是干巴巴的镜面反射。
5.4 外接设备映射落地的几个大坑
数据要平滑,不能直接用原始值。BlendShape系数自带高频噪声,直接驱动形变会抖得像抽搐。我一般对所有面部曲线做一阶低通滤波,或者限制每帧最大变化速率,让情绪表达更接近真实肌肉状态。
ARKit的BlendShape是相对系数,范围是0到1,但不同表情对同一个系数的敏感度完全不一样。比如JawOpen系数0.5,不等于角色嘴巴真的张开了50%,必须针对每个角色的形变范围做单独校准。
多设备接入时先统一时间基准。动捕设备、面捕设备、眼动仪的数据如果时间戳不对齐,角色会出现身体和脸错位的“魂体分离”。我习惯在系统里做一个统一的数据时钟,所有设备数据进来先打上时间戳再分发。
调试时设备不可能一直在线,所以一定要做一个回放通道。把设备数据录制成曲线数据,调试时用键盘或UI模拟回放,否则每次调材质参数都要把动捕演员叫回来,效率极低。
6. 性能预算与调优:一张RTX卡上的取舍
6.1 开销从哪里来
数字人的性能开销分布和普通场景完全不同。我做过几次GPU Profile,大致分布是:
| 模块 | 典型开销 | 说明 |
|---|---|---|
| 皮肤SSS后处理 | 0.5到1.5毫秒 | 分辨率越高越贵,可半分辨率计算 |
| Groom发丝渲染和阴影 | 1到6毫秒 | 发丝数量和阴影质量影响最大 |
| 眼球复合层 | 0.1到0.3毫秒 | 视差偏移和反射采样不贵,但要做多层 |
| 动态光源和阴影 | 2到5毫秒甚至更高 | 实时阴影是全链路里最重的部分 |
这些数字会随硬件和场景变化,但比例关系基本稳定:头发和实时阴影是大头,皮肤SSS是固定税,眼睛反而廉价。
6.2 我调过的几项优化
SSS后处理可以降半分辨率。UE4提供了控制台变量用于调整SSS的质量和采样规模,远景角色可以调低,近景特写再拉回来。这样既省了固定消耗,又不影响角色出镜时的画质。
Groom的LOD必须提前做好。我的经验是近景Groom给足发丝量,中景切换到一个减少一半发丝的版本,远景直接切到带透明贴图的卡片头发。UE4里同一套Groom可以配多个LOD曲线,切换时机尽量放在旋转镜头看不到的瞬间,避免穿帮。
皮肤AO能用贴图烘焙就不实时跑。全屏SSAO和Groom自阴影同时开,帧时间直接起飞。我的做法是角色自身的AO烘焙进贴图,场景级别的AO用SSAO开低档,Groom阴影只给最靠前的发丝层计算。
眼球反射用静态捕获,不要反射实时平面。反射捕获加Cubemap在绝大多数镜头里够用,还能避免两个角色互相映出对方那种诡异效果。
6.3 用贴图“骗”回来的性能
有些效果完全可以作弊而不损失观感。耳尖和手指的边缘透红光,只在角色特写时开实时Transmission,中远景用贴图里的透射色假抬亮度。角膜的折射效果,不用真的屏幕空间折射,用UV视差贴图就能骗过眼睛,除非有专门的角膜特写镜头。
做数字人项目的优化逻辑是:把最贵的实时计算留给观众最注意的部位。脸部特写时花预算在SSS和Groom上,远景中景靠贴图和LOD撑住。用控制台变量配合LOD在运行时动态调整这些阈值,能兼顾效率和画质。
7. 留档的踩坑记录:排查链路三则
7.1 皮肤一片惨白,像死尸
现象是角色在正常灯光下脸白得发光,看不到血色,连嘴唇都像涂了白蜡。
排查过程我先从最简单的验起:检查Albedo贴图在材质里被当成sRGB还是线性色,如果连色带都错了,后面全白搭。然后确认材质ShadingModel是不是SubsurfaceProfile,这一步经常被美术同事误改成Default Lit。再查材质实例里的SubsurfaceProfile资产是否有效,我之前就遇到过资产引用丢失但材质不报错的情况。最后查ScatterRadius,如果被拉到5以上,整张脸自然糊成白板。
解决方式就是把ShadingModel切回SubsurfaceProfile、重新指定Profile、ScatterRadius降到1.5左右,再配合SubsurfaceColor贴图把脸颊的血色提回来。
7.2 头发近景高光断裂、发丝间一片黑
现象是角色头发正面看问题不大,稍微转到侧面,高光一条一条断开,发丝缝隙里全是黑块和噪点。
排查链路比较长。先排除发丝数量不够导致的欠采样,把发丝密度调高,发现还是有。再检查材质里的Tangent是否完整,尤其是Groom引导线从DCC导出时有没有把切线烘焙进资产。我最后在材质预览里把Normal和Tangent可视化,发现侧面镜头的发丝片段方向疯狂跳动。
问题根源是导出Groom时没有烘焙切线,引擎在运行时计算出的发丝切线不稳定。修复方法是回到DCC里重新烘焙引导线切线再导出,进UE4后高光立刻稳定了。
7.3 眼睛像玻璃球,没有焦点
现象是眼球反光很亮,但看起来像假眼珠,虹膜完全没有了“盯人”的感觉。
我先看角膜层和虹膜层的混合顺序,发现角膜反射Cubemap的贡献值压过了虹膜,导致眼球表面像镜子。再检查虹膜UV的视差偏移方向,发现偏移取反了,虹膜在镜头左右移动时反着漂。还检查了瞳孔缩放参数,被外部设备的初始值顶到了0,瞳孔变成针孔,完全没了自然光下的状态。
修复方式是角膜反射的强度限制在0.3以内,虹膜视差偏移方向调回来,瞳孔给一个基于环境亮度变化的默认映射曲线。改完再开侧逆光测试,眼球终于有了“从角膜下面透出来的虹膜”的分层感。
这三个坑排查下来我有个很深的体会:大多数“假人”问题不是某一个节点参数没调对,而是渲染链路里的某一环被替换成了通用缺省方案。数字人着色没有一个万能按钮,它是一套可组合的着色器栈加外部设备数据流。如果你也在做类似的角色,强烈建议做一个专门的调试关卡,把皮肤、头发、眼睛、口腔拆成独立可开关的层,谁出问题就关掉其它层单独看。这比对着材质节点一个个猜快得多,也是我最近项目效率提升最明显的一步。