如果你第一次打开《普罗米亚》这部作品大概率会在某个画面暂停下来心里冒出一句“等一下里面的人真的是穿成这样的吗”火焰消防队的装甲层层叠叠反派的作战服紧得像第二层皮肤还有人干脆把整团火焰披在肩膀上。这个疑问本身就是很好的技术选题为什么一部动画会让观众对“服装”产生如此强烈的记忆点我的判断是它不只是美术风格的选择而是三渲二流程中“角色模型、风格化材质、火焰粒子、后处理”层层配合的结果。这篇文章就从这一帧“穿成这样”的画面出发拆解它背后可以落到 Unity、Godot 等游戏引擎里的技术点。读完这篇文章你能得到三样东西。第一一套把“夸张服装”从概念设计变成可渲染角色的拆解思路第二一个可以复用到项目里的三渲二 Shader 示例第三一套让火焰特效稳定“黏在”角色身上的粒子实战方法。如果你正在做风格化角色、三渲二渲染或者想弄明白“动画中的夸张造型能不能用引擎复现”这一篇就是给你准备的。还有一点想先说明白本文不是动画考据不讨论某个造型致敬了谁、某个镜头用了多强的作画技术。文章的重点是把观众的视觉印象翻译成图形学问题。毕竟对开发者来说“这个角色的衣服为什么好看”不如“这个角色的衣服在引擎里该怎么搭”来得重要。1. 这篇文章真正要解决的问题1.1 为什么从“穿成什么样”切入服装在动画里从来不是小事。对于《普罗米亚》这种以极致色彩、夸张火焰、机械与焰色为记忆点的作品来说服装的功能不是保暖而是信息它告诉你这个人属于哪个阵营、具备什么能力、在战斗中承担什么任务。屏幕上那些“看起来穿得很奇怪”的造型恰好是视觉信息密度最高的地方。如果把角色当成一颗洋葱“穿成什么样”就是剥开角色身份后看到的第一层而这第一层就决定了观众对整部作品风格的初印象。从开发者的角度看服装也是资产层级的第一道分界线。一个穿得很丰满的角色在引擎里往往不是一块完整的 mesh而是身体、外甲、装饰片、特效锚点四个部分组合出来的结果。很多新手项目之所以做不出“角色很有辨识度”的感觉问题恰恰出在这里所有人物的服装模型都混在一起材质也只是把颜色调得不一样自然就没有“穿成那样”的记忆点。1.2 从观众疑问到工程师问题观众问“穿成这样吗”开发者的下一步问题是如果让我做一个同样气质的 3D 角色我应该先建模哪一层服装和身体是同一个 mesh 还是分开火焰是做在材质里的动画贴图还是用粒子系统挂在肩甲上这些问题的答案决定了最后能不能还原出那种“把火焰穿在身上”的视觉。如果只是照着一张概念图去堆模型、堆特效结果往往是一团乱反过来先理解视觉目标再拆分层级后续每一步都有据可查。本文后面所有章节都在回答这一连串工程问题。我会从最基础的资产分层讲起到三渲二 Shader、火焰粒子附着、后处理色彩量化最终让你在自己的项目里也能跑出一个“穿得很夸张但又很统一”的角色。1.3 适合什么样的读者正在做二次元、三渲二风格游戏想给角色加入夸张装备和特效的人。了解 Unity 基础操作但没写过 Shader、只会用默认材质的人。动画专业或游戏专业学生想看理论如何落地到引擎。纯粹想理解“动画视觉为什么好看”的技术爱好者。如果你的情况不在上面本文的架构拆解和风格参数化思路也可以复用把“如何让一套视觉看起来完整”当成一个系统工程问题来思考。2. 基础概念与核心原理把视觉印象翻译成 CG 术语2.1 你看到的“服装”其实是几个资产层的叠加一个看上去完整的“战斗服 火焰斗篷”角色在引擎里通常由四层构成角色身体与贴图、外甲或装备网格、由 Shader 驱动的颜色变化、粒子或特效系统。这四层并没有被“焊死”在一起它们各司其职。比如《普罗米亚》里大量火焰质感是“半几何化”的反映在粒子形状不是柔和的 sprite 贴图而是尖锐的三角形、折线形贴图或网格。这也是第一个关键技术判断要让服装有记忆点不能只做 Shape要做轮廓和色块。换句话说你在屏幕上看到的“衣服”其实是一个多层合成结果。每一层单独拿出来可能很简单但叠在一起之后角色的剪影、色彩、动态全部被强化了。这就是为什么很多风格化角色在静止帧里可能显得“朴素”一动起来、发光起来就变得非常抢眼。2.2 什么是三渲二Toon Rendering三渲二简单说就是用 3D 模型加风格化光照、颜色量化、描边等处理得到接近手绘的视觉。它不追求物理正确追求的是信息读取效率观众扫一眼就能看出体积、方向和阵营。常见做法包括漫反射被压缩成两到四个色阶ramp shading、明暗交界处有清晰边缘、模型边缘有描边、高光被切成硬块。《普罗米亚》大量使用了“有限色板”和“硬边过渡”这也是为什么大幅画面看起来色彩纯度很高、构图很“pop”。在引擎里这些特征通常收敛到一个 Shader 的几个参数上。只要你控制好“光照分几段、描边多宽、色板用哪几个颜色”不同美术制作的模型就能放进同一套视觉系统里。这也回答了一个常见疑问风格化不是随便调高饱和度就行它需要一组明确的约束规则。2.3 为什么不能直接用写实 PBRPBR 追求能量守恒和微表面风格化追求断点和反差。举一个可操作的差异热血角色身上的明暗过渡在 PBR 里是一条连续平滑曲线在三渲二里则是step()或者很窄的smoothstep()视觉差异直接决定情绪差异。从这个角度讲两者没有高低之分只有目标不同。维度写实 PBR三渲二风格化漫反射过渡连续微表面响应两到四段色阶高光GGX 连续高光手动切出的硬块阴影软阴影、天光反弹大色块 AO 加硬阴影轮廓没有明显描边法线外扩或顶点色控制描边纹理高细节法线、置换大色块、手绘笔触参数目标物理真实性角色识别度如果你想让角色“穿得像普罗米亚里那样”首先要明确这不是在写实渲染上叠一个滤镜而是要换一套着色逻辑。2.4 核心结论整套风格约束比单个特效更重要“里面是穿成这样的吗”这个问题的技术翻译是为什么这些服装层的组合方式如此统一、有辨识度。答案不在某一个工艺而在整套风格约束色板约束、光照阶数约束、纹理风格约束、特效形状约束。这四个约束也可以成为我们自己项目的“风格规范”。后面所有代码和配置都围绕这一套规范展开。3. 环境准备与前置条件3.1 软件与工具选择本文的示例以 Unity 为例因为 Unity 的内置渲染管线对新手最友好Shader 写法也比较直观。如果你用 Godot 或 Unreal思路完全一致只是 Shader 语法不同我会把重点放在通用原理上。版本方面本文不绑定某个具体 Unity 版本内置管线的多数 API 相对稳定如果你使用 URP 或 HDRP建议把本文当成思路参考光照变量需要替换成对应管线的写法。你需要准备的工具如下一个可用的 Unity 工程任意近两年版本均可、能编辑贴图的软件Photoshop 或 Krita、一个 3D 建模工具Blender 足够、以及一个可以创建角色模型的测试场景。如果只是验证效果Unity 自带的胶囊体和小方块就够用不需要额外美术资产。3.2 需要准备的最小资产一个带骨骼的角色模型或者最简测试一个胶囊体当“身体”一个 Cube 当“外甲”。一张 Ramp 贴图建议 8x1 或 16x1 像素横向从暗色渐变到亮色用来做色阶映射。一张火焰粒子的 sprite 或几何片。形状越尖锐越接近《普罗米亚》里的几何化火焰柔和圆形贴图会偏传统特效。一个方向光和一个相机。这里真正容易踩坑的地方是很多人会忽略“Ramp 贴图必须关闭 mipmap”。如果贴图设置了 mipmap远处采样时会自动平滑硬边效果就没了这个问题我后面专门列在排查清单里。3.3 渲染管线说明本文的 Shader 示例基于内置渲染管线ForwardBase因为它可以在绝大多数 Unity 版本里直接编译运行。URP 中需要把LightMode改成UniversalForward并且把_LightColor0、_WorldSpaceLightPos0这类内置变量替换成 URP 的 LightData 结构。对于第一次接触的人来说强烈建议先用内置管线跑通理解每一步做了什么再迁移到更复杂的管线。4. 核心流程拆解把“战斗服”穿到角色身上4.1 模型设计分层而不是合并建模时就把“身体”“服装”“外甲”分成不同可开关的对象。很多人偷懒把服装和身体合并最后改配色或换装非常痛苦。在实际项目中“穿成那样”的服装通常有一个主网格、若干装饰片、一个默认贴图通道和一个自发光通道分开的好处是能局部替换坏处是 draw call 变多所以后面要考虑合批和材质排序。做错会遇到的场景是你想把胸甲颜色从暖色改成冷色却要重画整张贴图。正确做法是给每块资产留一个 Mask 通道颜色由材质参数驱动而不是画死在贴图里。4.2 UV 与贴图以“大色块”为目标贴图阶段不要追求物理毛孔和布纹要追求明确的色块边界。AO 不要用复杂的软阴影可以用粗线条处理。Ramp 贴图必须关闭 mipmap并且把纹理的 Wrap Mode 设为 Clamp否则rampU超过边界时会出现奇怪的循环颜色。另外服装贴图通常分成两个区域Diffuse 区域负责图案和阵营标识Color 区域只提供灰色阶让 Shader 里的_Color参数可以统一染色。这样同一套贴图可以给多个阵营复用只要切换不同_Color就能做出红队、蓝队、金队极大降低资产迭代成本。4.3 材质系统Shader 要承担“画风”的 90%满屏的风格感往往集中在 Shader 上。一个风格化角色需要的视觉特征包括漫反射分阶、一张 Ramp 贴图且阈值可调、高光分两道一道硬、一道更硬、可选的边缘发光。这些特征可以由一个 Shader 加一组参数完成不建议一个 Shader 解决所有问题更建议一个 Shader 对应一个团队风格。4.4 绑定与动效服装要跟得上动作服装既可以是蒙皮网格也可以是直接挂在骨骼节点下的场景物体。第一类由角色动画驱动适合衣料和软装备第二类适合外甲、发射器。对火焰粒子来说一定要挂在骨骼节点比如肩甲、武器骨骼下面而不是角色根节点否则手臂摆动时特效会在原地漂着看起来非常假。4.5 后处理最后一层“统一感”色彩量化后处理可以把不同材质、不同模型上的颜色统一到同一套阶调里。很多高质感风格化游戏都会在最后加一层“类色版”后处理。不要小看这一步它能解决“每个模型单独看很好放在一起各自为战”的常见问题。5. 完整示例与代码实现5.1 三渲二 Shader主 Pass 加法线外扩描边下面这段 Shader 是完整的可以在 Unity 内置渲染管线中直接使用。它做了三件事用 Ramp 贴图把漫反射切成有限色阶用边缘光强化角色剪影再用一个法线外扩 Pass 画出黑色描边。Shader Promare/PromareToonOutline { Properties { _MainTex (Main Texture, 2D) white {} _Color (Base Color, Color) (1,1,1,1) _RampTex (Ramp Texture, 2D) white {} _OutlineColor (Outline Color, Color) (0,0,0,1) _OutlineWidth (Outline Width, Range(0.001, 0.1)) 0.01 _RampThreshold (Ramp Threshold, Range(0, 1)) 0.6 _RampSoftness (Ramp Softness, Range(0.001, 0.2)) 0.02 _EmissionStrength (Emission Strength, Float) 1.2 } SubShader { Tags { RenderTypeOpaque LightModeForwardBase } Pass { Name MAIN Cull Back CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc sampler2D _MainTex; float4 _MainTex_ST; sampler2D _RampTex; fixed4 _Color; float _RampThreshold; float _RampSoftness; float _EmissionStrength; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { fixed3 albedo tex2D(_MainTex, i.uv).rgb * _Color.rgb; float3 nDir normalize(i.worldNormal); float3 lDir normalize(_WorldSpaceLightPos0.xyz); float halfLambert dot(nDir, lDir) * 0.5 0.5; float rampU smoothstep(_RampThreshold - _RampSoftness, _RampThreshold _RampSoftness, halfLambert); fixed3 ramp tex2D(_RampTex, float2(rampU, 0.5)).rgb; fixed3 col albedo * ramp; float ambient 0.15; col albedo * ambient; float rim pow(1.0 - saturate(dot(nDir, lDir)), 2.0); col _EmissionStrength * rim * _Color.rgb * 0.2; return fixed4(col, 1.0); } ENDCG } Pass { Name OUTLINE Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc fixed4 _OutlineColor; float _OutlineWidth; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; float3 dir normalize(v.normal); float3 pos v.vertex.xyz dir * _OutlineWidth; o.vertex UnityObjectToClipPos(float4(pos, 1.0)); return o; } fixed4 frag (v2f i) : SV_Target { return _OutlineColor; } ENDCG } } }代码的关键逻辑都写在了注释之外。主 Pass 里halfLambert把光照压到 0 到 1 的区间然后通过smoothstep在_RampThreshold附近形成一个窄过渡带这个过渡带越窄画面就越像“硬边手绘”。描边 Pass 的Cull Front表示只渲染背面模型面沿法线方向外扩一点得到黑色轮廓。需要注意法线外扩画描边是一个简化方案角色动作幅度大时容易出现描边断裂如果你想追求完美可以进一步研究顶点色控制描边权重。5.2 火焰粒子附着脚本很多项目会把火焰粒子挂在角色根节点下面结果角色做冲刺或转身动画时火焰并不会跟随手臂和武器一起摆。正确做法是让粒子系统每帧跟随指定的骨骼锚点并在LateUpdate阶段同步位置因为动画骨骼更新一般发生在Update之后只有LateUpdate才能拿到本帧的最终位置。using UnityEngine; public class FlameAttach : MonoBehaviour { [Tooltip(火焰要附着的骨骼或装备节点)] public Transform anchor; [Tooltip(相对锚点局部坐标的偏移例如肩甲上方)] public Vector3 localOffset new Vector3(0f, 0.4f, 0f); [Tooltip(是否跟随锚点旋转)] public bool matchRotation true; private ParticleSystem ps; void Awake() { ps GetComponentParticleSystem(); if (ps ! null) { var main ps.main; main.simulationSpace ParticleSystemSimulationSpace.Local; } } void LateUpdate() { if (anchor null) { return; } transform.position anchor.TransformPoint(localOffset); if (matchRotation) { transform.rotation anchor.rotation; } } }simulationSpace设为Local是为了让粒子在发射器自己的局部空间里计算这样整团火焰会跟着角色一起转向而不是飘在原地。localOffset可以让你把火焰放到肩甲上方、武器尖端或背上不同锚点共用同一个脚本无需写多个逻辑。5.3 风格参数配置文件同一个 Shader 能产生完全不同的视觉靠的就是参数。下面这份 JSON 展示了如何把“阵营配色”外部化美术改色时不需要去材质面板里逐个改颜色只需要在配置文件里调整一组色板。{ projectName: PromareStyleDemo, teams: [ { name: TeamA, primaryColor: [0.86, 0.26, 0.04], secondaryColor: [0.98, 0.88, 0.25], outlineColor: [0.06, 0.05, 0.08], rampThreshold: 0.65, rampSoftness: 0.02, flameColors: [ [0.14, 0.22, 0.55], [0.76, 0.88, 1.0], [1.0, 0.42, 0.1] ] }, { name: TeamB, primaryColor: [0.02, 0.55, 0.65], secondaryColor: [0.85, 0.95, 1.0], outlineColor: [0.02, 0.03, 0.04], rampThreshold: 0.58, rampSoftness: 0.015, flameColors: [ [0.78, 0.96, 0.32], [0.95, 1.0, 0.98], [0.28, 0.92, 0.82] ] } ], postProcess: { enableColorQuantize: true, quantizeBands: 4 } }这段配置里的rampThreshold和rampSoftness决定光照被切成几段、段与段之间过得多硬flameColors是火焰从内到外的三档颜色。运行时可以用一个脚本读取 JSON、批量给材质赋值这样玩家选择阵营或角色换装时整个视觉体系会一起变化而不是只换一个主角颜色。5.4 后处理色彩量化函数最后再给一个纯数学的量化函数。它不依赖特定引擎版本核心逻辑是把每个颜色通道切分成有限档位再映射回去。这样处理后场景里大面积渐变线会被“剪”成明确的色块接近手绘上色的感觉。public static Color Quantize(Color src, int bands) { Vector3 v new Vector3(src.r, src.g, src.b); v * bands; v new Vector3(Mathf.Floor(v.x), Mathf.Floor(v.y), Mathf.Floor(v.z)); v / bands; v * 1.02f; return new Color(Mathf.Clamp01(v.x), Mathf.Clamp01(v.y), Mathf.Clamp01(v.z), src.a); }这里的1.02f是经验值用来抵消量化后整体亮度略降的问题。如果你希望画面更“硬”把bands设为 3 或 4如果想要柔和一点可以提高到 6 以上。这个函数可以用在自定义后处理脚本里也可以作为材质函数嵌入 Shader。6. 运行结果与效果验证6.1 搭建最小测试场景为了快速验证不需要完整角色模型。新建 Unity 场景放一个胶囊体当作角色主体再放一个 Cube 作为外甲。把上面的 Shader 赋给胶囊体的材质把火焰粒子系统作为 Cube 的子节点挂上FlameAttach脚本把anchor指向 Cube。最后加一盏平行光朝向胶囊体让阴影落在角色侧面。接下来在 Cube 上旋转或者移动观察火焰是否始终贴在锚点附近。如果火焰在旋转时乱漂说明simulationSpace不是Local或者没有在LateUpdate里同步位置。这个测试场景是后面所有调整的基础。6.2 成功判断清单可以用下面几条来判断效果是否符合预期。第一胶囊体上明暗面是两到三段色阶而不是连续灰阶第二包裹角色的黑色描边随视角变化依然稳定不闪烁、不消失第三火焰粒子在 Cube 转动时始终贴服在锚点并且速度方向会随锚点朝向改变第四启用色彩量化后整个场景的颜色被压到有限档位画面不再“像照片”。这四条都满足基本就达到了风格化角色加火焰特效的初始目标。6.3 如果失败第一步看哪里不要急着改代码。先打开 Console 看是否有 Shader 编译报错再用 Frame Debugger 检查材质和 Pass 是否生效。如果材质变量没有传进 Shader最常见的问题就是变量名拼写不一致。其次检查 Ramp 贴图的 Texture Import Setting确认Generate Mip Maps未勾选、Wrap Mode为Clamp。这两个问题占了风格化 Shader“看起来不对”的大半原因。7. 常见问题与排查思路问题现象可能原因排查方式解决方案角色整体变黑描边 Pass 的 Cull 设置错误导致正面被剔除在 Frame Debugger 中查看 Draw Call 数量和剔除方向主 Pass 使用Cull Back描边 Pass 使用Cull Front颜色是平滑渐变而不是硬边Ramp 贴图被 mipmap 平滑采样或 Wrap Mode 为 Repeat检查贴图导入面板和rampU计算关闭 mipmapWrap Mode 设为 Clamp火焰粒子不跟随角色粒子系统没有挂到骨骼锚点或者仿真空间不是 Local检查粒子系统的父节点和simulationSpace使用FlameAttach脚本并在 LateUpdate 同步位置和旋转描边粗细随距离变化模型空间法线外扩固定值远镜头看起来会变细对比不同距离下描边宽度改用屏幕空间描边或根据相机距离动态调整_OutlineWidth多个角色颜色没区分每个角色都手动复制材质检查材质实例数量和 MaterialPropertyBlock 使用情况用同一个基础材质加MaterialPropertyBlock覆盖颜色参数火焰过曝或白成一团Emission 强度过高后处理 Bloom 与 Tonemapping 不匹配先关掉 Bloom单独调_EmissionStrength从 1.0 起步确认色板层次清晰后再开 Bloom这些问题并不是空话而是风格化角色项目里最容易反复出现的六类现象。它们的共同特点是表面看是渲染效果不对实际根因都集中在贴图设置、Shader 剔除方向、粒子层级和参数传递这几个地方。你只要养成“先看 Console、再看贴图设置、最后看材质参数”的排查顺序就能省下大量时间。8. 最佳实践与工程建议8.1 先定“风格规范”再写 Shader项目启动时就要约定色板、光照阶数、描边宽度、贴图分辨率区间。用一两张风格参考图作为美术基准所有角色的“服装”风格就统一了。很多人一上来就让每个美术自由发挥结果角色单独看都挺好看放一起却像来自六个游戏。风格规范不是限制而是保证团队产出的最终一致性。8.2 资产分层与目录结构角色身体、服装、外甲、特效锚点必须分清楚。建议项目里至少保持这样的资产结构Assets/Art/Characters/Common/ Assets/Art/Characters/TeamA/ Assets/Shaders/Stylized/ Assets/VFX/FlameSystem/ Assets/Settings/StyleProfiles/这样的目录结构能让团队协作时不互相踩脏资产美术改 TeamA 配色时不会误碰 TeamB 的贴图特效同学也能在VFX/FlameSystem里统一管理粒子预设。8.3 性能与兼容策略粒子数量要设上限不需要阴影的火焰粒子尽量关闭阴影投射多个火焰发射器优先合并材质实例。描边 Pass 如果不需要远处细节可以在 LOD 切换时裁掉。在后处理量化上优先用低分辨率实现减少逐像素开销。这些优化最好在一开始就写进项目的性能基线里而不是最后上线前再回头改造。8.4 数据安全与可回滚变更涉及全局风格参数时建议先在测试分支修改并用 JSON 或 ScriptableObject 保留一份旧值快照。Ramp 贴图和火焰粒子预设也要纳入版本控制避免实验性调整覆盖掉原始资产。对于没有把握的参数改动先用最小场景验证再同步到所有角色。这不是流程空话而是多人协作项目中真实能帮你避免事故的做法。8.5 不要一个美术风格套所有场景《普罗米亚》的造型有辨识度是因为每个阵营的色板和形状都遵循同一套理念但参数不同。复用 Shader 可以但团队之间要保留差异。比如 TeamA 用暖色和 0.65 的光照阈值TeamB 用冷色和 0.58 的光照阈值两边站在一起时观众一眼就能分清身份。风格统一不等于角色雷同。9. 总结与后续学习方向从“普罗米亚里面是穿成这样的吗”开始我们最后把一个视觉问题拆成了工程问题模型分层负责剪影Shader 负责色阶和描边粒子系统负责“把火焰穿在身上”后处理负责把整套颜色统一起来。这四个环节加起来才是画面里有辨识度的那套“衣服”。下一步你可以做的事很多。如果用的是 Unity可以把这个内置管线的 Toon Shader 改写成 URP 的 Shader Graph 版本顺便熟悉新版光照流程如果对火焰表现有更高追求可以把普通粒子替换成 VFX Graph用更方便的 CPU 事件和 GPU 事件控制发射想继续深入还可以研究风格化流体模拟用 APIC 或 FLIP 实现更大规模的火焰效果。最后给一个真实项目里很实用的提醒不要在一开始就追求把火焰做成“完美流体”。先固定色板和材质火焰用最朴素的几何粒子贴图也能获得很高的风格完成度。等整体管线跑通了再回头替换成更高级的 VFX 方案。很多项目最后能做到高完成度靠的不是某一个特效很秀而是每一步都按同一条风格约束走到头。到那时候你就会发现“穿成那样”并不是随便穿而是一整套工程规范共同作用的结果。