ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity三渲二实战:从初音立绘到实时NPR渲染的完整流程

Unity三渲二实战:从初音立绘到实时NPR渲染的完整流程 初音未来这个 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 ShaderUnity 集成材质绑定、光照配置、后处理可实时运行的场景调优描边粗细、阴影阈值、性能最终效果这个顺序不是死的实际做的时候经常要来回跳比如 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 或者自定义的不要用 ACESACES 会把颜色压得太灰丢了原画的鲜艳感。我这次还加了一个轻微的色差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 曲线这些参数之间是相互影响的改一个就要重新看整体效果。这个过程很磨人但也是三渲二项目里最能体现功力的地方。最后说个实际的如果你也想做类似的项目建议先从简单的角色开始别一上来就挑战初音这种头发复杂、配饰多的。找一个结构简单的角色把整条管线跑通再逐步增加复杂度这样踩的坑会少很多。我这次也是因为之前做过几个简单角色才敢直接上初音不然光头发就能卡一周。
返回列表