初音未来这个 IP 在 3D 圈子里一直是个绕不开的题材,尤其是 20 周年那批同人立绘,画师们把葱娘的气质拿捏得死死的,但要把这种 2D 立绘的"味道"搬进 Unity 做成能实时跑的三渲二模型,中间隔着的坑比想象中多得多。我前后折腾了差不多三周,从 Blender 建模、法线处理、Shader 手写到 Unity 里的光照和描边调参,中间返工了四五次,最后总算跑出了一个自己看着还算满意的实时效果。这篇就把整个流程拆开讲清楚,重点放在"为什么这么做"和"哪里容易翻车"上,而不是丢一堆参数让你照抄。如果你手上有立绘想做成三渲二、或者正在用 Unity 做 NPR 实时渲染,这篇应该能帮你少走不少弯路。
1. 先想清楚三渲二到底在渲什么
1.1 三渲二不是"把 3D 渲成 2D 的样子"
很多人第一次接触三渲二,脑子里想的是"让 3D 模型看起来像 2D 画"。这个理解方向对,但太粗。真正要拆的是:2D 立绘的视觉特征到底由哪些元素构成,然后逐个在 3D 管线里找对应手段去模拟。
我拿这次初音立绘举例,画师用的是偏赛璐璐的厚涂风格,特征大概有这么几条:色块边界非常干净,几乎没有渐变过渡;阴影是硬边阴影,不是软阴影;高光是一块一块的,不是连续的光斑;轮廓线粗细有变化,靠近脸的地方细、衣服褶皱处粗;整体色调偏冷,但脸部有暖色偏移。这些特征如果直接拿 PBR 管线去渲,出来的东西会"太真实",反而丢了原画的味。
所以三渲二的核心思路是:主动放弃物理正确的光照模型,改用可控的、风格化的着色逻辑。Unity 里做这件事,主流路线是自定义 Shader(手写 HLSL 或者用 Shader Graph),而不是依赖内置的 Standard 或 URP Lit。这一点必须先定下来,否则后面所有工作都会跑偏。
1.2 为什么选 Unity 而不是别的引擎
这个问题我被问过很多次。Unity 在三渲二这块的优势其实不在渲染本身,而在于迭代速度和生态。NPR 渲染是个极度依赖"调参—预览—再调参"的过程,Unity 的 Scene 视图实时反馈加上 Shader 热重载,改一行代码几秒就能看到结果,这个循环效率是很多离线渲染器比不了的。
另外 Unity 的 Shader Graph 对不写代码的美术同学比较友好,但这次我建议还是手写 HLSL,原因后面会讲——Shader Graph 在处理多 Pass 描边和自定义光照的时候会有些别扭。至于为什么不用 Unreal,主要是项目体量和目标平台的问题,这次目标是能在普通笔记本上实时跑,Unity 的轻量管线更合适。
1.3 整体管线的拆解
把整个流程拆成几个阶段,心里有个地图会清楚很多:
| 阶段 | 主要工作 | 关键产出 |
|---|---|---|
| 建模 | 拓扑、UV、法线处理 | 干净的 mesh |
| 材质准备 | 贴图绘制、通道打包 | BaseMap / 阴影色 / 高光图 |
| Shader 编写 | 光照模型、描边、高光 | 自定义 NPR Shader |
| Unity 集成 | 材质绑定、光照配置、后处理 | 可实时运行的场景 |
| 调优 | 描边粗细、阴影阈值、性能 | 最终效果 |
这个顺序不是死的,实际做的时候经常要来回跳,比如 Shader 写到一半发现法线有问题,就得回 Blender 重新处理。但大方向按这个走不会乱。
2. 建模阶段那些"看起来不重要但会要命"的细节
2.1 拓扑和面数:不是越精细越好
三渲二模型和写实模型在拓扑要求上有个根本区别:写实模型靠法线贴图和高模烘焙来补细节,三渲二模型靠干净的色块和硬边。这意味着你的拓扑要尽量让色块边界落在合理的边线上,而不是靠贴图去糊。
这次初音模型我控制在大概 3 万面左右,头发用了片状建模而不是实体建模。为什么头发要用片?因为三渲二里头发的高光和阴影通常是一整块一整块的,实体头发反而会让光照计算变得复杂,片状头发配合双面材质和自定义法线,效果更接近原画。
注意:片状头发在 Unity 里必须处理双面渲染和法线朝向问题,否则从背面看会一片黑。这个坑后面 Shader 部分会详细讲。
面数分配上,脸部和手部可以适当多给一些,因为这两个部位在镜头里出现频率最高,而且曲面过渡要求高。衣服和配饰可以省,反正三渲二本来就不追求细节密度。
2.2 法线处理:三渲二的法线是"骗"出来的
这是整个建模阶段最关键的一步,也是最容易翻车的地方。三渲二模型的脸部法线通常不是几何法线,而是手动调整过的、朝向前方的平滑法线。为什么要这么做?因为如果按真实几何法线算光照,脸部的阴影会随着角度变化而移动,看起来"很 3D",但原画里的脸部阴影基本是固定的。
具体操作是在 Blender 里给脸部单独做一个法线朝向,让所有脸部顶点的法线大致指向摄像机正前方偏上一点。这样无论角色怎么转头,脸部接收到的光照都相对稳定,阴影不会乱跑。这个技巧在业内叫"法线平滑"或者"法线朝向修正",不同软件里操作方式不一样,Blender 里可以用 Data Transfer 修改法线,也可以用自定义法线插件。
头发同理,片状头发的法线要手动调成朝外发散,这样高光才会形成一条一条的带状,而不是一个点。
2.3 UV 和贴图通道的规划
UV 展开本身没什么好说的,但通道打包这件事要提前规划。三渲二常用的贴图通道包括:
- BaseMap:基础色
- ShadowMap:阴影色(不是阴影遮罩,是阴影区域该显示的颜色)
- HighlightMap:高光区域
- RampMap:光照渐变查找图
我习惯把阴影色和高光信息打包进同一张贴图的 RGBA 通道里,减少采样次数。比如 R 通道放阴影色强度,G 通道放高光遮罩,B 通道放描边粗细控制,A 通道备用。这样 Shader 里一次采样就能拿到多个信息,性能上划算很多。
提示:贴图分辨率不用追求 4K,三渲二本来就是色块风格,2K 甚至 1K 在很多场景下完全够用,还能省显存。
3. NPR Shader 的核心:光照模型怎么"不物理"
3.1 从 Lambert 到 Ramp:光照的离散化
标准 Lambert 光照是NdotL连续变化的,出来的明暗过渡是平滑的。三渲二要的是硬边,所以要把这个连续值"量化"成几个台阶。最常用的做法是采样一张 Ramp 贴图(也叫渐变查找图),横轴是NdotL,纵轴是亮度,贴图里画成阶梯状,采样出来的就是硬边阴影。
float ndotl = dot(normalize(i.normal), normalize(_WorldSpaceLightPos0.xyz)); ndotl = ndotl * 0.5 + 0.5; // 映射到 0-1 float3 rampColor = tex2D(_RampMap, float2(ndotl, 0.5)).rgb;Ramp 贴图的好处是可控。你想要两级阴影就画两级,想要三级就画三级,还能在台阶边缘加一点点过渡让硬边不那么生硬。这次初音我用了三级:亮部、中间调、暗部,暗部再叠一层冷色调,模拟原画里阴影偏蓝紫的感觉。
3.2 阴影色的独立性:别用亮度乘法
新手最容易犯的错是阴影色直接用基础色乘以一个暗系数。这样出来的阴影是"变暗的基础色",但原画里的阴影往往是完全不同的色相。比如初音的头发是青绿色,阴影部分画师用的是偏蓝的深青色,不是简单的变暗。
所以 Shader 里阴影色要单独采样,不能靠乘法。我一般会准备一张 ShadowMap,里面直接画好阴影区域该显示的颜色,光照计算只负责决定"这个像素是亮部还是暗部",具体显示什么颜色由贴图决定。
float3 baseColor = tex2D(_BaseMap, i.uv).rgb; float3 shadowColor = tex2D(_ShadowMap, i.uv).rgb; float3 finalColor = lerp(shadowColor, baseColor, stepValue);这个stepValue就是从 Ramp 采样出来的台阶值。这样色相的控制权完全在贴图手里,Shader 只做逻辑判断。
3.3 高光的"块状"处理
三渲二的高光不是连续的光斑,是一块一块的。实现方式通常有两种:一种是靠贴图直接画高光区域,Shader 只控制显隐;另一种是用NdotH配合阈值和遮罩来控制。
我这次用的是混合方案:头发高光用贴图遮罩加NdotH阈值,脸部高光直接画在贴图里。为什么分开?因为头发高光需要随视角变化(转头的时候高光带会移动),而脸部高光基本固定,画死更稳定。
float ndoth = dot(normalize(i.normal), normalize(_WorldSpaceLightPos0.xyz + viewDir)); float highlightMask = tex2D(_HighlightMap, i.uv).r; float highlight = step(_HighlightThreshold, ndoth) * highlightMask; finalColor += highlight * _HighlightColor;_HighlightThreshold这个阈值调起来很讲究,太低高光面积太大,太高又几乎看不见。我一般从 0.7 开始试,根据实际效果微调。
4. 描边:三渲二的灵魂也是最难调的部分
4.1 法线外扩描边的原理和坑
描边最常用的方法是法线外扩:在顶点着色器里把顶点沿着法线方向往外推一点,然后用背面渲染(Cull Front)画成黑色,就形成了轮廓线。
// 描边 Pass 的顶点着色器 float3 expandedPos = v.vertex.xyz + v.normal * _OutlineWidth;听起来简单,但坑非常多。第一个坑是硬边模型的法线断裂。如果模型有硬边(比如立方体的棱),顶点法线是分裂的,外扩之后描边会在棱角处裂开。解决办法是烘焙一套平滑法线存到顶点色或者 UV 通道里,描边 Pass 用这套平滑法线而不是几何法线。
第二个坑是描边宽度随距离变化。如果直接用固定宽度,远处的模型描边会显得很粗,近处又太细。正确做法是把宽度乘以一个和距离相关的系数,或者直接在裁剪空间里做外扩。
float distanceFactor = 1.0 / (1.0 + length(UnityObjectToViewPos(v.vertex)) * _OutlineDistanceScale); float3 expandedPos = v.vertex.xyz + v.normal * _OutlineWidth * distanceFactor;4.2 描边粗细的变化控制
原画的轮廓线不是等宽的,脸附近细、衣服褶皱处粗。要实现这个效果,可以用一张描边宽度遮罩图,把每个区域的宽度系数画进去,顶点着色器采样后乘以基础宽度。
这次初音我画了一张灰度图,脸部区域接近 0.3,头发和衣服边缘接近 1.0,裙摆褶皱处给到 1.5。这样描边就有了粗细变化,看起来更接近手绘。
注意:描边宽度遮罩图的分辨率不用太高,512 就够,因为它是低频信息,采样精度要求不高。
4.3 内描边和材质边缘线
外描边只能解决轮廓,模型内部的色块边界(比如衣服和皮肤的交界)需要内描边。内描边的做法通常是检测深度或者法线的突变,在突变处画线。Unity 里可以用深度纹理做边缘检测,但性能开销不小。
我这次的内描边是画在贴图里的。因为三渲二模型的色块边界基本是固定的,与其用后处理去检测,不如直接在 BaseMap 上把线画好。这样零性能开销,而且线的粗细和位置完全可控。代价是贴图工作量大一点,但对于静态角色来说完全值得。
5. Unity 里的集成与实时渲染配置
5.1 渲染管线的选择
这次用的是 URP。为什么不用 Built-in?因为 URP 的 Shader 结构更清晰,多 Pass 支持虽然不如 Built-in 灵活,但通过 Renderer Feature 可以补足。而且 URP 的性能在移动端和低配设备上更好,符合"实时跑"的目标。
URP 里写自定义 NPR Shader 有个注意点:光照变量要通过Lighting.hlsl引入,不能像 Built-in 那样直接用_WorldSpaceLightPos0。这个在跨管线迁移的时候经常出问题,写 Shader 之前先把 URP 的 ShaderLibrary 结构过一遍。
5.2 光照配置:方向光加补光
三渲二场景里的光照配置和写实场景完全不同。写实场景追求物理正确,三渲二追求"好看且稳定"。我这次用了三盏灯:
- 主方向光:负责主要明暗关系,角度固定,不随摄像机变化
- 补光:从摄像机方向打过来,填充暗部,避免死黑
- 轮廓光:从背后打,给边缘加一点亮边,增强立体感
主光的强度不用太高,因为三渲二的明暗主要靠 Ramp 控制,光照强度只影响 Ramp 采样的位置。补光要柔和,用点光或者方向光都行,强度控制在主光的 30% 左右。
5.3 后处理:辉光和色调映射
三渲二的后处理要克制。辉光(Bloom)可以加一点,让高光区域有轻微的光晕,但阈值要高,否则整个画面会糊。色调映射建议用 Neutral 或者自定义的,不要用 ACES,ACES 会把颜色压得太灰,丢了原画的鲜艳感。
我这次还加了一个轻微的色差(Chromatic Aberration),强度调到 0.1 左右,几乎看不出来,但能让画面边缘不那么"干"。这个属于个人偏好,不加也完全没问题。
5.4 性能优化:Draw Call 和 Shader 复杂度
实时渲染绕不开性能。三渲二 Shader 虽然比 PBR 轻,但多 Pass 描边会让 Draw Call 翻倍。优化手段有几个:
- 合并材质:能共用一张贴图的部件尽量合并,减少材质球数量
- 描边 Pass 简化:描边不需要采样所有贴图,只算法线外扩就行
- 关闭不必要的阴影:三渲二的阴影是画出来的,实时阴影基本不需要,直接关掉能省不少
这次初音模型在普通笔记本上跑,1080p 分辨率下稳定 60 帧,GPU 占用大概 40% 左右,还有优化空间但够用了。
6. 调参过程中踩过的坑和解决思路
6.1 脸部阴影乱跑的问题
前面提到法线朝向修正,但实际做的时候还是出了问题。原因是 Blender 里修改的法线在导出 FBX 的时候被重新计算了。解决办法是在导出设置里勾选"保留自定义法线",或者在 Unity 里用脚本重新烘焙法线。这个坑卡了我整整一天,最后是在导出面板里找到的选项。
6.2 描边在头发上断裂
片状头发的描边一开始是断的,因为片状模型的法线是垂直于片面的,外扩之后相邻的片之间会有缝隙。解决办法是给头发单独做一套平滑法线,让相邻片的法线方向一致,外扩之后就能连成一条线。
6.3 高光在转动时闪烁
高光闪烁通常是因为NdotH在阈值附近抖动。解决办法是给阈值加一个平滑过渡,用smoothstep代替step,过渡区间不用大,0.02 左右就能明显改善。
float highlight = smoothstep(_HighlightThreshold - 0.02, _HighlightThreshold + 0.02, ndoth) * highlightMask;6.4 暗部死黑的问题
暗部死黑是因为 Ramp 贴图的最暗端直接给了黑色。实际上原画的暗部是有颜色的,只是明度低。把 Ramp 最暗端改成深蓝紫色,再配合补光,暗部就活了。
7. 从立绘到实时模型,哪些东西必须妥协
7.1 立绘的笔触感很难完全还原
原画里画师的手绘笔触、纸张纹理、颜料堆积感,这些在 3D 实时渲染里基本还原不了。能还原的是色块关系、明暗结构、轮廓线这些"骨架"层面的东西。想通这一点之后,心态会好很多,不会死磕那些注定做不到的细节。
7.2 视角变化带来的信息缺失
立绘是单视角的,画师只画了正面。做成 3D 模型之后,侧面和背面需要自己补全。这次初音的后脑勺和侧脸我参考了官方设定集和其他同人作品,尽量保持风格一致。但说实话,侧面看肯定不如正面惊艳,这是所有立绘转 3D 项目的通病。
7.3 实时性能对 Shader 复杂度的限制
离线渲染可以堆几十个 Pass,实时渲染不行。每个 Pass 都要算性能账。这次描边用了两个 Pass(外扩 + 内描边贴图),光照一个 Pass,加起来三个 Pass,已经是比较克制的方案了。如果再加屏幕空间反射、次表面散射这些,帧率直接崩。
8. 一些让效果更接近原画的小技巧
8.1 用顶点色做局部色调偏移
原画里不同区域的色调是有微妙差异的,比如脸部偏暖、头发偏冷。这个可以用顶点色来实现,在 Blender 里给不同区域刷上不同的顶点色,Shader 里采样顶点色后叠加到最终颜色上。这样不用改贴图就能做全局色调微调。
8.2 描边颜色不要用纯黑
纯黑描边看起来很生硬。原画的轮廓线通常是深褐色或者深蓝紫色,比纯黑柔和很多。把描边颜色改成float3(0.1, 0.08, 0.15)这种深色,整体质感会提升一个档次。
8.3 给高光加一点溢出
高光区域如果只是纯色块,会显得很平。可以在高光边缘加一点点亮度溢出,用pow函数做个衰减,让高光中心亮、边缘暗,模拟手绘高光的层次感。
float highlightFalloff = pow(highlight, 0.5); finalColor += highlightFalloff * _HighlightColor * _HighlightIntensity;8.4 阴影边缘加一点暖色过渡
硬边阴影虽然符合赛璐璐风格,但完全硬边会显得很"塑料"。在阴影边缘加 1-2 像素的暖色过渡,能模拟手绘时颜料边缘的自然晕染,效果会自然很多。这个在 Ramp 贴图的台阶边缘画一点渐变就能实现。
9. 关于工具链和后续扩展的一些想法
9.1 Blender 到 Unity 的工作流
这次全程用 Blender 建模和展 UV,导出 FBX 进 Unity。中间遇到的最大问题是坐标系和单位不一致,Blender 是 Z 轴向上,Unity 是 Y 轴向上,导出的时候要勾选正确的轴转换选项。另外 Blender 的材质球在 Unity 里不识别,所有材质都要在 Unity 里重新建,这个要有心理准备。
9.2 Shader Graph 和手写 HLSL 的取舍
如果只是做简单的 Ramp 光照,Shader Graph 完全够用,而且可视化编辑对美术更友好。但一旦涉及多 Pass 描边、自定义光照模型、顶点动画这些,Shader Graph 就会很别扭,有些节点甚至做不到。我的建议是:先用 Shader Graph 快速验证效果,确定方案后用 HLSL 重写。这样既保证了迭代速度,又保证了最终的可控性。
9.3 后续可以扩展的方向
这套管线跑通之后,可以往几个方向扩展。一是加表情和口型动画,用 BlendShape 做,三渲二模型的表情变化通常比较夸张,BlendShape 比骨骼动画更合适。二是加物理模拟,头发和裙摆用 Spring Bone 或者 Magica Cloth,能让角色动起来更自然。三是做多角色场景,这时候要考虑材质合并和 GPU Instancing,性能优化会变成主要矛盾。
9.4 关于实时渲染的一点个人体会
做三渲二实时渲染这几年,我最大的体会是:技术是为风格服务的,不是反过来。很多人一上来就研究各种高级 Shader 技巧,结果做出来的东西技术很炫但风格不对。正确的顺序应该是先分析原画的视觉特征,再去找对应的技术手段,技术不够就简化风格,而不是硬堆技术。这次初音项目里,最出效果的反而是最简单的 Ramp 光照和描边,那些复杂的次表面散射、屏幕空间反射一个都没用上。
另外一点是,调参的时间要留够。建模和写 Shader 可能只占整个项目的一半时间,另一半全花在调参上。描边宽度、阴影阈值、高光强度、Ramp 曲线,这些参数之间是相互影响的,改一个就要重新看整体效果。这个过程很磨人,但也是三渲二项目里最能体现功力的地方。
最后说个实际的:如果你也想做类似的项目,建议先从简单的角色开始,别一上来就挑战初音这种头发复杂、配饰多的。找一个结构简单的角色把整条管线跑通,再逐步增加复杂度,这样踩的坑会少很多。我这次也是因为之前做过几个简单角色,才敢直接上初音,不然光头发就能卡一周。