为什么一个看似简单的描边,差点让项目延期两周?
去年秋天,我接了一个国漫风格的番剧PV外包,客户要求“三维渲二维”,角色和场景都要有清晰的卡通描边,并且要能在UE里实时预览。当时我心想,描边嘛,Freestyle或者反转法线加个Shader就搞定了,能有多难?结果第一版提交后,导演指着屏幕说:“你这线条粗细跟过山车似的,远景糊成一片,近景粗得像马克笔,而且角色一转身线条就闪烁。”那一刻我才意识到,非真实感渲染(NPR,Non-Photorealistic Rendering)里的描边,远不是加个线框那么简单。这个项目最终我们迭代了四版技术方案,从Blender的Freestyle到基于几何法线的后期描边,再到混合管线,踩遍了几乎所有能踩的坑。今天就把这段经历拆开揉碎,聊聊卡通描边背后的原理、工程取舍,以及我最终交付的那套稳定方案。
先交代项目背景:这是一支三渲二风格的PV,角色有复杂的布料和头发,场景包含大量植被和建筑。客户的核心诉求是“线条要有手绘感,但必须稳定,不能闪烁,且要能实时输出”。预算有限,团队只有三个人,我负责技术管线。下面按照需求分析、技术选型、踩坑、解决、交付的顺序,把整个过程还原出来。

Freestyle、反转法线、后期边缘检测,到底该选哪个?
面对描边需求,Blender里能走的路其实就三条:Freestyle(基于拓扑的线条渲染)、反转法线(Inverted Hull,即背面剔除描边)、以及后期屏幕空间边缘检测(基于深度、法线、ID等缓冲)。我先快速评估了这三者的原理和适用场景。
Freestyle:功能强大但实时性堪忧
Freestyle是Blender内置的NPR线条渲染引擎,它基于网格的边(Edge)进行拓扑分析,可以标记轮廓线、折痕线、材质边界等,还能通过线条集(Line Set)和线条样式(Line Style)做非常细致的控制。听起来很美,对吧?但它的致命伤是:它是CPU渲染,且需要预计算边信息,在动画序列中每帧都要重新计算,速度极慢。我们测试了一个5万面的角色,单帧Freestyle渲染耗时接近8秒,完全无法满足实时预览。而且Freestyle对拓扑变化敏感,角色一变形,线条就跳。所以直接排除,只适合静帧或离线渲染。
反转法线:实时友好但粗细难控
反转法线描边的原理很简单:复制一个模型,沿法线方向外扩(或者用Solidify修改器加厚度),然后翻转法线并剔除正面,只渲染背面,形成一圈轮廓。它的优点是实时性极好,完全在GPU上跑,而且线条粗细由外扩距离控制,比较直观。但问题也很明显:外扩距离是均匀的,导致远近线条粗细一致,不符合透视;而且对于锐利边缘和凹面,外扩后容易出现穿插或断裂。我们试过用顶点着色器根据深度调整外扩距离,但效果依然不够稳定,尤其在头发和布料这种薄片结构上,线条会断裂。
后期边缘检测:灵活但依赖缓冲质量
后期边缘检测是在合成阶段,利用深度缓冲(Z-Depth)、法线缓冲(Normal)和物体ID缓冲,通过Sobel等算子检测边缘。它的优势是线条粗细可以在屏幕空间统一控制,且不受模型拓扑影响,实时性也很好。但难点在于:深度和法线缓冲的精度直接影响边缘质量,尤其是远景和细小结构,容易产生噪点或漏检。另外,它无法区分“想要”和“不想要”的线条,比如角色身上的纹理边界也会被检测出来。
综合评估后,我决定采用反转法线为主,后期边缘检测为辅的混合方案:角色和主要道具用反转法线保证轮廓清晰,场景和远景用后期边缘检测补充细节。但就是这个混合方案,让我在第一个坑里摔得鼻青脸肿。
反转法线描边为什么在角色转身时疯狂闪烁?
第一个坑就是闪烁。我们按照常规做法,给角色加了一个Solidify修改器,厚度设为0.02,材质用背面剔除。渲染序列时发现,当角色旋转时,轮廓线会像呼吸一样忽粗忽细,甚至在某些角度完全消失。我一开始以为是厚度不够,加大到0.05,结果近景线条粗得离谱,远景又因为透视显得太细。这显然不是参数问题,而是原理层面的缺陷。
反转法线的本质是沿顶点法线方向偏移顶点位置。对于平滑着色的模型,顶点法线是相邻面法线的平均值,所以偏移方向是连续的。但问题在于:轮廓线是由视线方向与表面法线垂直的边决定的。当角色旋转时,轮廓边的位置在模型表面滑动,而我们的外扩网格是静态的,它只是把整个模型“膨胀”了一圈。这意味着轮廓线的粗细实际上取决于外扩距离在屏幕上的投影,而投影又随视角变化。更糟糕的是,对于硬边(比如立方体的棱),顶点法线是分裂的,外扩后会产生裂缝,线条就断了。
另一个闪烁源是深度冲突。外扩网格和原网格在轮廓处非常接近,导致深度测试不稳定,出现Z-fighting,线条就闪烁。我们尝试了偏移深度值,但只是把问题转移到了其他地方。
技巧提示:反转法线描边必须配合“平滑法线”使用。在Blender中,可以给模型添加一个“Weighted Normal”修改器,或者手动调整法线,确保外扩方向连续。对于硬边,需要单独处理,比如用边缘拆分(Edge Split)后分别外扩,但会增加Draw Call。
意识到单纯靠修改器无法解决根本问题后,我开始转向自定义Shader。在Shader中,我可以根据视图空间深度动态调整外扩距离,并且用屏幕空间导数(dFdx/dFdy)来检测轮廓,实现更稳定的描边。但这就引出了第二个坑:实时渲染下的线条质量与性能的平衡。

实时管线里,如何让描边既稳定又高效?
为了在UE里实时预览,我们需要把Blender里的方案移植到游戏引擎。我选择了在UE里用后期材质实现边缘检测,同时保留角色上的反转法线网格作为补充。但UE的后期边缘检测对深度和法线缓冲的依赖很强,默认的缓冲精度在4K下会出现锯齿。我们做了以下几件事来稳定线条:
- 深度缓冲优化:将场景深度编码到RGBA通道,提高精度;同时用自定义深度(Custom Depth)标记需要描边的物体,避免全屏检测带来的噪点。
- 法线缓冲重建:从深度重建法线,而不是直接使用GBuffer法线,因为GBuffer法线在抗锯齿后可能失真。重建法线可以保证边缘检测的连续性。
- 线条粗细的透视补偿:在后期材质中,根据像素深度调整Sobel算子的采样步长,使得远处线条变细,近处变粗,模拟手绘的透视感。
- 时间抗锯齿(TAA)的配合:TAA会模糊边缘,导致线条抖动。我们关闭了TAA对描边通道的影响,单独对线条做后处理抗锯齿。
经过这些调整,实时预览的线条终于稳定了。但新的问题又来了:客户想要“手绘感”,也就是线条要有粗细变化、断线、飞白。这在纯后期边缘检测里几乎不可能实现,因为边缘检测是二值的。于是我们回到了Blender,用Freestyle做离线渲染,同时开发了一套基于几何节点的线条生成工具,用于生成手绘风格的线条,再导入UE作为贴图使用。这听起来很绕,但确实解决了问题。
几何节点生成手绘线条:一次大胆的尝试
在Blender 3.x之后,几何节点(Geometry Nodes)变得非常强大。我设计了一个节点组:首先提取模型的轮廓边(通过比较相邻面的法线方向与视线方向),然后把这些边转换为曲线,再对曲线进行随机扰动和粗细变化,最后生成网格线条。这个过程完全程序化,可以批量处理角色和场景。生成的线条可以导出为ABC或USD,在UE里作为静态网格使用。虽然增加了资产量,但效果非常接近手绘,客户很满意。
不过,这个方案对拓扑要求较高,如果模型有非流形边或重叠面,提取的轮廓边就会出错。我们花了大量时间清理模型,确保拓扑干净。这也让我深刻体会到:NPR渲染的上限,往往由模型质量决定。
最终交付的管线长什么样?
经过四轮迭代,我们最终交付的管线是混合式的:
| 模块 | 技术方案 | 用途 | 优缺点 |
|---|---|---|---|
| 角色轮廓 | 反转法线 + 自定义Shader(深度补偿) | 实时预览、游戏内 | 实时性好,但需要精细调整法线和厚度 |
| 场景细节 | 后期边缘检测(深度+法线) | 实时预览、游戏内 | 灵活,但依赖缓冲质量,需抗锯齿处理 |
| 手绘风格线条 | 几何节点生成曲线 → 导出网格 | 离线渲染、特写镜头 | 效果最佳,但资产量大,制作周期长 |
| 离线高质量 | Freestyle + 线条样式 | 最终渲染 | 质量最高,但速度慢 |
这套管线在项目里跑通了,但也暴露了一个问题:多套方案并行导致资产管理和版本控制变得复杂。比如角色模型需要同时维护原网格、外扩网格、线条网格三个版本,一旦模型修改,三处都要同步。我们后来用几何节点把外扩和线条生成做成了修改器,绑定在原网格上,才缓解了这个问题。

最后说点感想。卡通描边看似是渲染的小分支,但它涉及拓扑、法线、缓冲、抗锯齿、甚至项目管理。如果你也在做类似的项目,我的建议是:不要迷信单一方案,根据镜头需求分层处理;模型拓扑一定要干净,这是NPR的基石;实时和离线要分开考虑,不要试图用一套参数通吃。希望这篇复盘能帮你少走点弯路。








评论(0)