第13章 · Niagara:特效思维与战斗表现

📑 目录

导语:竖切评审那天,BOSS 的蓄力斩砍倒了全部三名测试——真相是没人看见预警——数值本身没问题。特效同学很委屈:这个技能叠了 14 层粒子,火焰拖尾漂亮得能当壁纸。可画面里同时炸着五六个同量级的特效,BOSS 脚边那个 0.8 秒的红色预警圈被盖得严严实实。会后团队定下一条铁律:特效的及格线是——玩家在 200 毫秒内能不能做出正确反应。帅不是第一标准。本章围绕这条铁律展开。

本章地图

mindmap
  root((第13章
Niagara 特效)) 特效思维 可读性优先:辨识与预警 三段式节奏 层级:底/中/闪点 配色 70/25/5 Niagara 数据流 System 与 Emitter 模块栈四个阶段 参数命名空间 Renderer 家族 CPU 还是 GPU 碰撞需求 数量级 模拟精度 三个实战拆解 枪口焰:瞬时 血液雾:短持续 魔法吟唱:长前摇 材质联动 Dynamic Parameter 蓄力驱动自发光 Overdraw 红线 填充率公式 LOD 与预算

写这一章的顺序是刻意的:先立审美与信息的规矩,再讲引擎机制。工具谁都能学会,判断学不会——而特效这行,90% 的翻车翻在判断上。

13.1 特效是信息,然后才是美术

特效在战斗里承担的第一职责是传递信息。谁在打你(敌我辨识)、打过来的东西有多危险(危险预警)、你的攻击有没有生效(命中确认)——这三个问题答不上来,粒子再漂亮也是负资产。

正面例子到处都是。《魔兽世界》的地面技能圈,敌方红、我方绿,二十年没变过;《最终幻想 14》的 AOE 预警圈直接画在地上,朴素得像 UI,但没人嫌它丑,因为它救命。反面教材是"全员火焰大翅膀"那一类网游:十个人同时开大,屏幕上一片橙红,谁也看不清谁在干什么。

三种信息,三种设计语言

敌我辨识走色相通道。红色对绿色、暖色对冷色,是人类视觉系统反应最快的对比——比形状快,比明度快。所以《英雄联盟》的红蓝方、《魔兽世界》的红绿圈,都把阵营信息押在色相上。反过来用就出事:把敌方技能做成冷白色调,等于强迫玩家用"慢通道"做生死判断。

危险预警走形态与运动通道。尖锐聚拢暗示穿刺伤害,匀速扩散的圆环暗示范围,脉动收缩暗示倒计时。好的预警"像 UI":边界清晰、运动规则、不玩花样。特效师总想给预警圈加烟雾扰动,加完玩家读不出边界,等于没预警。

命中确认走闪点与突变通道。命中帧的一次高亮、粒子行为的突然改变(雾的迸溅、光的脉冲),都在回答"中了没有"。确认信息的生命期以帧计,过期不候。

三种语言共用一块画布,优先级是:预警 > 辨识 > 确认 > 装饰。装饰性粒子(环境余烬、氛围光尘)永远排在最后,同屏信息超载时先砍装饰层。可读性也不是表现力的对立面:闪点之所以闪,恰恰因为周围足够暗。克制本身就是一种表现力。

补课:本节补特效设计的三件基础——节奏、层级、配色。美术出身可以跳过理论,直接看下面的时间轴。

节奏:三段式,一段都不能少

一个战斗特效的生命周期固定分三段:预备(Anticipation)、爆发(Climax)、消散(Dissipation)。

预备告诉世界"要出事了"。预警圈、吟唱环、粒子向手心汇聚,都是预备。它的长度有硬底线:12 帧,60fps 下即 200 毫秒——这是玩家从"看见"到"按出闪避键"的反应下限。比这个短的预警等于没有预警。动作游戏有条心照不宣的契约:前摇越长、预告越明显,这一招的伤害才允许越高。违反契约的技能,玩家只会觉得"赖"。

爆发在 1 到 3 帧内把能量全部放掉。爆发的审美是"脆":亮、快、狠。拖到 5 帧以上就"肉"了,打击感直接打折。

消散是余韵,告诉玩家"结束了"。烟、余烬、地面焦痕。消散可以拖一两秒,但对比度必须一路走低——上一场戏的散场不能抢下一场戏的开场。

层级与配色:70/25/5 是硬约束

同一个时间点上,一个特效由三层组成。底层面积最大、对比最低、运动最慢,负责体积与氛围,比如烟雾底和光晕;中层是形态主体,技能长什么样由它决定,比如火球本体、斩击轨迹;闪点是极小面积的近白高亮,1 到 2 帧内能被读完,负责打击感的落点,比如火花和核心闪光。

配色跟着层级走,比例是 70 / 25 / 5:主色占 70% 的面积(铺底层),定技能的阵营与属性;辅色占 25%(中层过渡);闪点色占 5%,通常接近纯白。美学偏好?不,这条比例是视觉系统的工程约束——高亮面积一旦超过 10%,玩家的注意力就开始涣散,"危险"和"背景"变得无法区分。

把三段式和三层次叠在一起,就是一张"三秒生命周期"时间轴:

技能特效三秒生命周期 · 三层次时序预备 0~0.8s(≥12帧)爆发消散 1.1~3.0s底层·70%预警圈/烟底:预备段就登场,慢起慢落中层·25%主体形态:随爆发冲顶,1s 内退场闪点·5%近白高亮:1~2 帧读完,即打击感落点0s1.0s2.0s3.0s12 帧 = 200ms@60fps:预备段不可短于此线

读这张图的要点:底层的预警圈在预备段一开始就登场,闪点只在爆发前后亮一两帧,而消散段只剩底层慢慢衰减。三层在时间上是错开的,永远不同时抢戏。

最后给一个低成本的检验手段:灰度测试。把画面截图去色,如果三层依然清晰可辨,配色过关。很多"华丽"特效死在灰度测试上——去掉颜色,就是一团亮度均匀的浆糊。在「回响峡谷」里,这条测试是特效合入主干前的强制检查项。

13.2 Niagara 的数据流:Emitter 是函数,System 是编排

老引擎 Cascade 里,每个发射器是一个黑盒:引擎写死了参数面板,你只能填格子。Niagara(UE5 的特效系统,全名 Niagara Visual Effects)把黑盒拆开,变成一条可以自己编排的模块流水线。这也是它学习曲线变陡的原因——自由多了,能犯的错也多了。

一个 .niagara 资产(UNiagaraSystem)内部是三层结构。System 是容器,负责激活、每帧调度和对外的参数接口;Emitter 是系统内的独立模拟单元,一个系统可以挂十几个发射器(枪口焰 = 闪光发射器 + 烟发射器 + 光发射器);Module 是发射器内部按阶段排成一列的小算子,每个模块只干一件事:生成几个粒子、加个重力、按曲线改颜色。

模块栈按固定阶段执行,顺序即逻辑:

flowchart TD
    A["System Spawn
系统激活时执行一次"] --> B["System Update
每帧"] B --> C["Emitter Spawn
每个发射器一次"] C --> D["Emitter Update
每帧:决定本帧生成多少粒子"] D --> E["Particle Spawn
每个新粒子一次:初始化"] E --> F["Particle Update
每个粒子每帧:力/阻力/颜色/大小"] F --> G["Solve Forces and Velocity
积分器:v 累加力,x 累加 v"] G --> H["Renderer
Sprite / Mesh / Ribbon / Light"] style G fill:#f0e6d8,stroke:#b5543a

理解这张图要抓住一个类比:Particle Update 阶段的模块栈就是一个 for 循环体,每帧对每个粒子顺序执行一遍。模块顺序写错,效果就错——所有 Force 模块必须排在 Solve Forces and Velocity 之前,因为后者是积分器,先把这一帧的力折算成速度再移动粒子;排在它后面的力,要等下一帧才生效,快粒子直接穿墙。

数据在模块之间靠什么流动?参数命名空间(Namespace)。这是 Niagara 里最容易被忽视、却决定协作效率的一层设计:

命名空间谁能写典型内容
Engine引擎(只读)DeltaTime、Time、场景深度等
User资产给默认值,外部可改蓝图/C++ 传入的 Charge、ImpactStrength
System系统级模块系统年龄、系统级状态
Emitter发射器级模块本帧生成数、发射器年龄
Particle逐粒子属性Position、Velocity、Color、Age

Engine 命名空间有个高频坑:模块里做自定义运动时,务必用 Engine.DeltaTime 乘速度,别把"每帧位移量"写死成常数——写死的特效在 30fps 机器上运动速度翻倍,低帧率玩家的预警时间被悄悄偷走一半。

User 命名空间是对外合约:策划和程序只动 User 参数,特效师改资产内部实现,两边互不阻塞。这和写代码时的 public 接口是同一个思想。

渲染侧,发射器的模拟结果最终交给 Renderer 家族。选型本身也是一种表达:Sprite 是广告牌面片,烟和光斑的主力;Mesh 粒子渲染真实网格体,碎石、弹壳、冲击波环;Ribbon 把粒子轨迹连成条带,做拖尾;Light 让粒子变成动态光源——威力大,代价也大,13.6 节再算账。

classDiagram
    class UNiagaraSystem {
        +.niagara 资产
        +发射器数组
        +User 参数接口
    }
    class UNiagaraEmitter {
        +模块栈 Spawn/Update
        +SimTarget: CPU 或 GPU
        +FixedBounds
    }
    class FNiagaraSystemInstance {
        +运行时实例
        +每帧 Tick
    }
    class FNiagaraEmitterInstance {
        +粒子数据缓冲区
        +顺序执行模块栈
    }
    class UNiagaraRenderer {
        <>
    }
    class UNiagaraSpriteRenderer
    class UNiagaraMeshRenderer
    class UNiagaraRibbonRenderer
    class UNiagaraLightRenderer
    UNiagaraSystem "1" *-- "n" UNiagaraEmitter
    UNiagaraSystem ..> FNiagaraSystemInstance : Activate 时生成
    FNiagaraSystemInstance "1" *-- "n" FNiagaraEmitterInstance
    UNiagaraEmitter "1" *-- "n" UNiagaraRenderer
    UNiagaraRenderer <|-- UNiagaraSpriteRenderer
    UNiagaraRenderer <|-- UNiagaraMeshRenderer
    UNiagaraRenderer <|-- UNiagaraRibbonRenderer
    UNiagaraRenderer <|-- UNiagaraLightRenderer

最后补一条工程纪律:枪口焰的闪光、烟、光三个发射器为什么要共属一个系统,而不是拆成三个系统?同一份资产意味着同一次激活、同一次销毁,共享同一组 User 参数——ImpactStrength 传一次,闪光和烟同时响应;LOD 与裁剪也以系统为单位统一结算。新手常把每种粒子拆成独立系统,战场上十几个系统各自 Spawn,触发差一帧、参数传三份、预算算三笔,维护和统计一起失控。发射器是分工的单位,系统才是部署的单位。

13.3 CPU 还是 GPU:三个维度拍板

每个发射器的 Sim Target(模拟目标)都要做一次选择:粒子在 CPU 上模拟,还是丢给 GPU。选错的代价很具体——要么粒子数上不去,要么碰撞做不了,要么大世界坐标下粒子集体发抖。驱动这件事的唯一因素就是工程取舍,代码洁癖在这里没有发言权。

三个维度,按优先级拍板。

维度一:碰撞需求。 需要粒子与场景精确碰撞,只能用 CPU。CPU 粒子可以做逐粒子射线检测,拿得到命中点和法线,还能触发粒子事件(碰撞时生成新发射器——血滴落地溅出第二圈)。GPU 粒子只有三种近似:场景深度缓冲(屏幕空间,背对镜头的墙等于不存在)、距离场、解析平面。对"血液雾贴地扩散"这种效果,深度缓冲近似足够;对"弹壳在窄走廊里弹跳",近似会穿帮。

维度二:数量级。 CPU 模拟每发射器的舒适区是几百个粒子,到一两千就开始挤 Game 线程的帧预算(第 6 章的账)。GPU 模拟以万为单位,几十万也扛得住——前提是它们是"哑"粒子:生成、受力、渲染,没有个体逻辑。

维度三:模拟精度与确定性。 GPU 粒子用 float32 算位置,离世界原点远了精度下降,几千米外粒子位置开始肉眼可见地抖动。「远港 Online」的大地图要么靠原点重定位(见第 16 章)兜住,要么让发射器在本地空间(Local Space)模拟。CPU 模拟可以配合引擎的双精度世界变换,且有逐帧确定性;GPU 不做逐帧一致保证。

flowchart TD
    Q1{"需要粒子与场景精确碰撞?
血滴落地、弹壳跳墙"} -- "是" --> CPU["CPU 模拟"] Q1 -- "否" --> Q2{"单发射器粒子数
超过 2000?"} Q2 -- "是" --> GPU["GPU 模拟"] Q2 -- "否" --> Q3{"需要逐粒子个体逻辑
或 Gameplay 回读位置?"} Q3 -- "是" --> CPU Q3 -- "否" --> Q4{"需要逐帧确定性?"} Q4 -- "是" --> CPU Q4 -- "否" --> Q5{"碰撞可用近似?
深度缓冲 / 距离场 / 平面"} Q5 -- "是" --> GPU Q5 -- "否" --> CPU style CPU fill:#e2ddd2,stroke:#4a4a45 style GPU fill:#f0e6d8,stroke:#b5543a

两本账的完整对照:

维度CPU 模拟GPU 模拟
数量级每发射器数百至约 2000数万至数十万
碰撞逐粒子射线检测,有命中点与法线深度缓冲/距离场/解析平面,近似
粒子事件完整支持5.x 支持有限,约束多
游戏逻辑回读可以(代价高,慎用)不能
确定性逐帧可复现不保证
坐标精度配合双精度世界变换float32,远离原点会抖
必须配置无特殊要求Fixed Bounds(固定包围盒)

"粒子事件"一栏值得展开半句。"粒子死亡时生成一圈余烬"这类二级联动,CPU 端用事件直接做;GPU 端在 5.8 里支持有限,工程上的替代方案是把余烬做成同一发射器内生命周期末段的行为(尾段切换贴图与颜色),牺牲一点精度换规模。规划技能时先想清楚二级联动是不是刚需——它经常直接把选型锁死在 CPU。

最后一条单列出来强调:GPU 发射器必须设置 Fixed Bounds,包围盒定小了粒子被整体剔除(特效"神秘消失"的第一大嫌疑),定大了引擎无法做有效裁剪,白付渲染成本。调试 GPU 特效消失问题,先查包围盒,再查别的。

13.4 实战拆解:三种典型时长的特效

理论够用就行,拆开三个真实案例看。它们分别代表特效时长的三个量级:瞬时(帧级)、短持续(亚秒级)、长前摇(秒级)。拆解之前先看它们挂在哪条触发链上——战斗特效从不孤立存在,它是动画、结算、材质、音频协同的一环:

sequenceDiagram
    participant Anim as 动画序列(第11章)
    participant GAS as Gameplay 伤害结算
    participant FX as NiagaraComponent
    participant Mat as 材质实例(第8章)
    participant Audio as 音频系统(第14章)
    Anim->>GAS: AnimNotify:命中帧到达
    GAS->>GAS: 结算伤害 / 方向 / 暴击
    GAS->>FX: SpawnAtLocation(受击点)+ SetFloat(ImpactStrength)
    FX->>FX: Burst 生成 → Update → Render
    FX->>Mat: DynamicParameter 写入命中强度
    Mat-->>FX: 亮度与尺寸曲线随之缩放
    GAS->>Audio: PlayAtLocation(命中音效)
    Note over FX,Mat: 以上全部在同一帧内完成
错开一帧,打击感就脱节

这条链上最容易翻车的是时机:特效和音效都必须在命中帧同帧触发。打击感是视听对齐的产物,差 50 毫秒玩家就感到"软"。

枪口焰:瞬时特效,审美是"脆"

时长定位:1 到 3 帧,0.05 秒以内。瞬时特效的全部功力在"短"上——枪口焰做了超过 3 帧就不再是枪口焰,那是喷火器的地盘。

模块组合(两个发射器):闪光发射器用 Spawn Burst Instantaneous 一次生成 3 到 8 个 sprite,沿枪口轴向小锥角喷出,生命周期随机 0.03 到 0.06 秒;贴图用 2×2 flipbook 随机帧,避免每枪一模一样。烟雾发射器单独生成 1 到 2 片灰烟,0.4 秒淡出——烟负责"残留",闪光负责"脆",两者混在一个发射器里会互相拖后腿。

配色逻辑:核心近白(亮度 2 以上,交给 bloom 泛开)占 5%,橙黄火药色占 25%,灰蓝烟占 70%。注意烟的色温要和环境光呼应:「回响峡谷」的夜战里,灰蓝烟比灰白烟可信得多。

最大的坑是灯。每个枪口焰挂一盏动态光(Light Renderer),十把枪齐射就是十盏光,延迟渲染下每盏光都要付一次光照计算。把光的强度曲线压进 2 帧、半径控制在 5 米内,或者干脆只做自发光加 bloom。

还有一条协同纪律:瞬时特效从不单打独斗。枪口焰的"脆"一半来自画面,另一半来自同帧的音效和一次 50 毫秒内的短促镜头震动(分见第 14、15 章)。三者错开哪怕一帧,玩家手里的就是玩具枪。「回响峡谷」把枪口焰、音效、震动封装在同一个"开火反馈"节点里,由武器代码一次触发,禁止各处自行拼装。爆发模块的参考配置:

# 伪代码:枪口焰闪光发射器(GPU 模拟)
emitter muzzle_flash:
    sim_target   = GPU
    fixed_bounds = box(±3m)              # GPU 发射器必须给包围盒

    spawn:
        spawn_burst_instantaneous: count = 5
        initialize_particle:
            lifetime    = random(0.03, 0.06)   # 2~4 帧,"脆"的来源
            sprite_size = random(0.25, 0.45) m
            rotation    = random(0, 360)
            velocity    = cone(muzzle_dir, 12°) * random(8, 20)

    update:
        scale_sprite_size: 1.0 -> 1.6          # 膨胀一瞬
        scale_color: rgb = white(2.5) -> orange(1.2), alpha 1 -> 0
        # 不加重力、不加噪声:两帧内任何运动细节都是浪费

    render:
        sprite_renderer: flipbook 2x2, blend = additive
        # 灯光放独立发射器:半径 5m,强度曲线 2 帧内归零

血液雾:短持续特效,红必须"脏"

时长定位:0.4 到 0.8 秒。职责是命中确认——玩家开枪的瞬间,视觉系统需要一个"中了"的回答。

模块组合:GPU 模拟,Spawn Burst 一次 80 到 300 个;初速度沿命中方向的反向锥形喷出;Update 阶段挂 Gravity Force(血要下坠)、Drag(雾要减速)、Curl Noise Force(让雾"搅"起来,直线运动的血像喷泉);Scale Color 负责生命周期内的变化:深红开局,尾段转暗褐。

为什么用 GPU:同屏五名敌人被扫射,每个 300 粒子就是 1500,CPU 开始吃紧;而血液雾不需要精确碰撞,深度缓冲近似足够让它"贴地"。

配色逻辑是这条经验的核心:红不能纯。纯 #FF0000 的血像番茄酱广告,降饱和、掺棕,才像铁锈色的真相。命中第一帧给一丝近白高光(5% 闪点),那是组织反光的暗示,也是"确认感"的落点。透明度曲线要先快后慢——线性淡出是"消失",前快后慢才是"散开"。

喷射方向不能猜。受击方向由结算系统给出,Spawn 时写入 User.ImpactDirection(Vector 参数),发射器拿它当锥形初速度的轴向。方向写反——比如朝着枪口喷回去——玩家对战场空间的判断会被系统性误导,这比不做特效更糟。

魔法吟唱:长前摇特效,可读性全押在前摇上

时长定位:前摇 1.2 到 2 秒,加爆发帧。这是三种里信息密度最高的一种,因为它要同时服务两方:施法者要看到"我在变强",对手要看到"危险在逼近"。

模块组合(三到四个发射器):地面预警圈(decal 或 Mesh 粒子,敌方红、我方绿,从吟唱第一帧就在);汇聚粒子,Spawn Rate 持续生成,用 Point Attraction 之类的吸引模块向手心收拢,蓄力越深收得越快;吟唱环围绕施法者旋转;爆发帧接一个全屏短闪加冲击波 Mesh。

配色逻辑:属性定主色——冰系蓝白、火系橙红——主色即语言,玩家扫一眼就知道该用什么姿态应对。蓄力变化只改亮度和纯度,不改色相。蓄到一半颜色从蓝变紫,玩家会以为是另一个技能。

预警圈的实现有两条路:Decal 投影贴地、贴合地形起伏,但每多一个圈就多一次屏幕空间投影成本,同屏十几个圈时比粒子还贵;Mesh 粒子(一个薄片网格体)便宜可控,但斜坡上会穿插。取舍规则:数量少、地形复杂用 Decal,数量多、地面平整用 Mesh 粒子。「回响峡谷」的 BOSS 战场地都是整平过的,全部走 Mesh 粒子方案。

长前摇特效最常见的事故是打断反馈。蓄力被打断时特效直接消失,玩家分不清"放出去了"还是"被断了"——这两种结果的后续决策完全相反。正确做法是 0.15 秒的"泄气":粒子四散、亮度塌陷、预警圈碎裂淡出。打断是一种结果,必须有它自己的视觉句号。

13.5 与材质的联动:动态参数是桥梁

粒子系统管"有多少粒子、在哪、什么颜色",材质管"每个像素长什么样"(第 8 章)。两边要对话,走两条通道。

第一条通道在资产内部:Niagara 的 Dynamic Material Parameters 模块把粒子数据(年龄、速度、自定义值)写进渲染材质,材质图里用 Particle Dynamic Parameter 节点接收。血液雾用速度驱动贴图拉伸、吟唱粒子用蓄力值驱动自发光强度,走的都是这条路。

第二条通道在代码侧:C++ 或蓝图每帧把游戏状态写进 NiagaraComponent 的 User 参数。吟唱蓄力是最典型的场景——蓄力进度归 Gameplay 管,特效只是它的显示器:

// 吟唱蓄力:每帧把 0~1 的进度喂给 Niagara 与角色材质
void AEchoMage::TickCasting(float DeltaSeconds)
{
    ChargeAlpha = FMath::Clamp(ChargeAlpha + DeltaSeconds / CastTime, 0.f, 1.f);

    if (UNiagaraComponent* FX = CastingFXComponent)
    {
        // 资产内声明 User.Charge;SetFloatParameter 为兼容接口,
        // 5.8 推荐 SetVariableFloat 系列,签名以官方文档词条为准
        FX->SetFloatParameter(TEXT("User.Charge"), ChargeAlpha);
        FX->SetVec3Parameter(TEXT("User.GatherCenter"),
            GetMesh()->GetSocketLocation(TEXT("hand_r")));
    }

    if (UMaterialInstanceDynamic* RobeMID = GetRobeMID())  // 第8章的MID
    {
        RobeMID->SetScalarParameterValue(TEXT("CastEmissive"), ChargeAlpha * 6.f);
    }
}

注意这里有两块"屏幕"同时更新:Niagara 粒子和角色长袍的动态材质实例(MID)。玩家读到的"蓄力感"是多个载体亮度同步爬升的总和——只亮粒子不亮人,力量没有源头。

蓝图侧的等价操作是 Set Niagara Variable 系列节点,语义相同。两个性能提醒:参数名走 FName,别在每帧的 Tick 里动态拼字符串;User 参数写入本身不贵,但值没变就别重复 Set——把写入挪到值变化的事件里,几十把武器同时吟唱时省下的调用次数可观。

第三条路是全局的:材质参数集(Material Parameter Collection, MPC)。天气系统想一键压暗全场景技能特效的饱和度、雪天想让所有火焰偏冷色,改 MPC 一个参数,所有引用它的特效材质同时响应(呼应第 10 章的氛围预设切换)。别在每个特效资产里单独改——那是维护灾难的开始。

13.6 Overdraw:特效的天敌

半透明粒子不走深度写入,靠从后往前排序逐层混合(第 6 章的 Translucency pass)。这意味着:同一个屏幕像素被叠了几层半透明,就要被完整着色几次。GPU 画不透明物体靠深度测试挡掉遮挡部分,画半透明没有这个护身符。更糟的是混合本身是"读-改-写"操作:每叠一层,GPU 要先读出帧缓冲里的旧颜色,算完再写回去——同样面积的像素,半透明对显存带宽的消耗大约是不透明的两倍起步,而带宽正是第 6 章说过的最贵资源。

把这笔账写成公式。设粒子在屏幕上覆盖的像素面积为 AA(单位:像素),同一像素平均叠加层数为 LL,单个像素的着色加混合成本为 CpxC_{px}(单位:时钟周期),GPU 可用的着色吞吐为 NcorefN_{core} \cdot f(核心数乘频率,再乘一个利用率),则这个特效的耗时为:

Tfx=A×L×CpxNcore×f×ηT_{fx} = \frac{A \times L \times C_{px}}{N_{core} \times f \times \eta}

公式本身平淡,推导的直觉值钱:A×LA \times L 是"实际要画的片元总数",它和粒子在屏幕上占多大面积、叠了多少层成正比——和粒子数量无关。一万个指甲盖大的粒子不可怕,可怕的是二十个占半屏的烟雾片叠在一起。

代入开头那场事故的数字。一张 512×512 的半透明贴图在 1080p(约 207 万像素)下占屏约 12.6%,即 A26A \approx 26 万像素;叠 20 层,A×L520A \times L \approx 520 万片元,相当于把全屏重画 2.5 遍。粒子材质通常带 2 到 4 次纹理采样加噪声计算,CpxC_{px} 按经验折算成等效不透明成本的 3 到 5 倍,结果就是 8 到 12 次全屏重绘的量级。RTX 3080 / 1080p 下一次全屏 PBR 着色的成本在 1ms 量级——这一个特效理论上限就能吃掉大半帧预算。这不是纸面推演:在 5.8 的内容示例里把烟雾发射器的 Spawn Rate 调大一倍,ProfileGPU 里 Translucency 一项从 2ms 涨到 8ms 以上,是分分钟能复现的事。

两个排查视图各管公式里的一个因子:Shader Complexity 按单像素着色成本染色(绿便宜、红贵、白失控),看的是 CpxC_{px};Quad Overdraw 按叠加层数染色,看的是 LL。先看哪个视图颜色异常,再决定走下面决策树的哪一支——单价问题去降材质,层数问题去砍叠加。

排查与优化的决策路径:

flowchart TD
    A["ProfileGPU:Translucency 超标"] --> B{"Shader Complexity 视图
大片红/白?"} B -- "是" --> C["定位屏幕面积最大的粒子"] C --> D{"贴图透明边缘
超过 30%?"} D -- "是" --> E["重新切图:tight crop
或改用 Mesh 粒子"] D -- "否" --> F{"同屏叠加层数 > 8?"} F -- "是" --> G["减 Spawn 数量、合并发射器
用大尺寸低分辨率替代多层小粒子"] F -- "否" --> H["降材质成本:
减采样、关噪声、换更便宜的混合模式"] B -- "否" --> I["问题不在 Overdraw
回第 18 章流程重新定位"] E --> J["复测:灰度检查 + 帧时间"] G --> J H --> J

动手之前先纠正一个流传甚广的误解:把发射器切到 GPU 模拟,Overdraw 一分钱都不省。GPU 模拟省的是 CPU 端的模拟时间;粒子到了渲染端,该叠多少层还是多少层,填充率照付。十万个 Additive 小闪点(每个几像素)可能毫无压力,十万个半透明大 sprite 就是灾难。决策树管的是"在哪模拟",填充率管的是"怎么渲染",两本账别记在一起。

混合模式本身就是一张价格表。Translucent(alpha 混合)最贵,需要排序,能明能暗;Additive(叠加混合)免排序、实现便宜,但只能往亮里加,黑烟和阴影类效果做不了;Modulate(乘法混合)只能变暗,适用面最窄。枪口焰天生该用 Additive,血液雾必须用 Translucent——真相是物理决定的:火发光,血遮光。口味不背这个锅。

三条主线按性价比排序。缩面积:贴图的透明边缘是纯粹的浪费,烟雾贴图四周常有 30% 以上的完全透明区域,裁紧(tight crop)直接砍掉这部分填充。减层数:层数 LL 是公式里的乘数,从 20 层砍到 8 层等于性能翻倍还多——合并发射器、用一张大贴图替代叠罗汉、给 Spawn Rate 设硬上限。降单价:噪声、多次采样、复杂混合都在抬高 CpxC_{px},中远景特效换简化材质,没人看得出来。

远距离和低端平台不该付同样的钱,这就是特效 LOD:

# 伪代码:按距离与画质档位收缩特效成本
function apply_fx_lod(fx_instance, distance_m, quality_tier):
    lod = 0
    if distance_m > 25 or quality_tier == LOW:    lod = 2
    elif distance_m > 10 or quality_tier == MID:  lod = 1

    spawn_scale = [1.0, 0.6, 0.25][lod]
    for emitter in fx_instance.emitters:
        # 用 Emitter 命名空间参数覆盖生成速率,不改资产
        fx_instance.set_param(emitter, "SpawnCountScale", spawn_scale)
        if lod >= 1:
            emitter.cap_max_particles(emitter.budget / (lod + 1))
        if lod == 2 and emitter.renderer == LIGHT:
            emitter.disable()          # 远处直接砍动态光
    # 验收口径:LOD2 的半透明片元总量不超过 LOD0 的 30%

手写 LOD 逻辑适合原型期。团队规模上来以后,用 Niagara 内建的 Effect Type 资产按画质档位集中管理发射器行为(生成数量上限、渲染器开关等),平台分档(第 18 章)统一驱动它——散落在几十个系统里的手写判断,会在第一次全平台适配时集中爆雷。

这套逻辑归预算制度管。第 18 章会给每个系统发"工资":在「回响峡谷」的 4K/60fps 目标下,GPU 帧预算 16.6ms 里半透明的份额是 3ms,特效、UI、体积雾一起分。特效组内部的纪律是:单个技能特效在 Shader Complexity 视图下不允许出现白色区域,同屏 BOSS 战场景 Translucency 峰值不许超过 2.5ms。

「远港 Online」的账留到卷三再细算,这里先埋个数字:40 人团本意味着同屏可能有 30 个技能特效并发,本章的所有纪律——层级克制、LOD、Spawn 上限——在那种密度下不再是优化建议,而是活下来的前提条件。特效的网络复制(Multicast RPC 触发、谁有权 Spawn)则交给第 21 章。

小结

特效的第一职责是信息:敌我辨识、危险预警、命中确认——帅是及格线以上的加分项,可读性是及格线本身。三段式节奏不可缺段:预备不短于 12 帧(200ms@60fps),爆发不超过 3 帧,消散的对比度必须一路走低。配色 70/25/5 是工程约束不是美学偏好;过不了灰度测试的特效不许合入。Niagara 的模块栈是顺序执行的循环体,Force 必须排在积分器之前;User 命名空间是程序与特效师之间的接口合约。CPU/GPU 选型用三个维度拍板:要精确碰撞选 CPU,数量过两千选 GPU,大地图警惕 float32 精度。Overdraw 的成本公式是 A×L×CpxA \times L \times C_{px}:面积、层数、单价,砍哪个都有效,砍层数见效最快。

上手任务

  1. 做一个枪口焰并卡帧验收。 按 13.4 的模块组合实现,逐帧暂停检查:闪光核心不得超过 3 帧;10 把枪齐射时,ProfileGPU 中 Translucency 增量低于 1ms。
  2. 给一个现成特效做 70/25/5 改造。 从引擎内容示例里挑一个"华丽但糊"的特效,按三层次重调配色。验收标准:灰度截图下底层、中层、闪点仍可一眼区分。
  3. 复现并修复一次 Overdraw 事故。 把烟雾发射器 Spawn Rate 调到平时的 4 倍,用 Shader Complexity 截图记录事故;再依次用裁贴图、减层数、降材质成本修复。验收标准:该特效 GPU 成本下降 50% 以上,且灰度下主体形态仍可辨认。
  4. 给吟唱特效接上蓄力参数。 用 User.Charge 驱动粒子汇聚速度与自发光,并实现打断时的"泄气"表现。验收标准:蓄力 0 到 100% 过程中色相不变;打断后 0.15 秒内粒子四散、预警圈碎裂,与"正常释放"肉眼可区分。

下一章

特效负责让玩家"看见",声音负责让玩家"相信"。枪口焰两帧就没了,但枪声的尾巴要在雨夜的空气里挂上一秒——下一章进入 MetaSounds 与声景设计,把「回响峡谷」从默片变成有声电影。

延伸阅读

  • 官方文档:Unreal Engine → Creating Visual Effects → Niagara Overview(系统/发射器/模块的官方定义)
  • 官方文档:Niagara → Sim Target 与 GPU Particles 词条(Fixed Bounds 与 GPU 碰撞的限制清单,以文档为准)
  • 官方文档:Unreal Engine → Rendering → Shader Complexity 视图模式(Overdraw 排查入口)
  • GDC 上关于战斗特效可读性的分享(检索 "VFX readability" 与 "stylized VFX" 方向,《守望先锋》《命运》团队的特效复盘值得对照本章三层次模型阅读)
  • 引擎源码:Engine/Source/Runtime/Niagara/ 下的 FNiagaraEmitterInstance,看模块栈如何被逐阶段调度