前情回顾
游戏中的光源数量是决定游戏性能开销的一大重点,很多时候甚至是最大项。但在某些游戏情境中,我们又不得不较长时间地使用多个光源,这就造成了非常尴尬、被动的局面......
笔者花费大概 20 天,思考并解决了这个问题。顺道写下这篇日志,留给那些还不想秃头的人 。
先来回顾一下游戏项目的开发进度,已知可以略过。
目前,已经部署了包括伯耐利 M4 和 AA-12 等自动霰弹枪,其射速非常快。选用爱发电上的 Banner 图展示一下 AA-12:
按照常识讲,照明枪的照明弹应该是 14G 左右口径(霰弹枪铅径),但由于游戏设计,我们选择跟攻击性霰弹枪一起,使用 12G 口径,目的是整体提升霰弹枪种的应用范围和适用性。
一顿操作猛如虎之后,开始制作照明弹效果,马上就遇到了巨大的问题。
0. 问题的出现
照明弹光照存在以下特点:
- 相对于主光照优先级低,但必不可少;
- 同屏存在数量可能巨大;
- 照射范围略大甚至巨大;
- 需要阴影或部分需要阴影;
- 与粒子系统伴随出现。
其中第 2、3 点最为致命。
按照常理,玩家通常只需要射出 1-2 发照明弹,提供场地照明即可。但是本着程序设计人员必须把用户想象成“猴子”的经典理论,可以预见,总免不了有特殊癖好的玩家存在,闲来对着天空射烟花。
然后,你总不能告诉玩家说,5 秒内只能发射一次......吧(板砖警告)?
可是,若要设法满足玩家,则会发生以下交通事故:
本着多想想就会头秃,再想多几天就会“原地去世”的觉悟,赶紧询问了一下小伙伴们:
随即惊喜地发现,这好像是个业界天花板级别的技术问题,我去......
结果很明显:大范围大剂量点光源似乎就没在几个 3A 级游戏里被开放式地允许过,这就是天花板的感觉。但项目已经做了,照明弹通用 12G 霰弹的口径也已经确定,双持 AA-12、美妙的十几发/秒射速连喷摆在那里......只能霸王被弓硬上。
以下是主要技术节点的尝试与结论演示。
1. 光的合并
在我开发的游戏中,爆炸会对小型植物产生烧炙火焰。由于小型火焰也带无阴影光照,所以同样属于额外的非重要点光源,可以跟照明弹问题并列处理,使用同一个优化脚本。
首先,我们看看单纯多个无阴影光照的开销。
如果把多个无阴影光照合并,会发生:
显然,经脚本优化后,帧率(FPS)有明显优势,而优化脚本的开销可以忽略。
也就是说,就算我们不使用阴影,两位数以上的光斑对游戏性能的开销也非常可观(这是 Deferred,如果是 Forward,基本可以自行了断了)。
不过问题还很多,因为当光灭失时,光斑会产生抽搐感或强烈抖动,于是,我们取所有合并光的平均位置。但由于光到耗尽时会有渐变暗和灭失,所以还是存在小抖动和不自然。
这里面就涉及一丁点开销问题。我们按两个光之间的强弱比例使用 Lerp 线性插值求得整体加权平均位置,效果非常爽滑:
2. 阴影控制
由于阴影开销巨大,程序员都会下意识把它们关掉,而这就产生了一个尴尬问题。
比如说,你在房间外面发射了一颗照明弹,房间外面是亮的,房间里面是暗的(正常)。然后另一个玩家(猴子)出现,向你射了十几发过来,结果由于阴影限制,后面的无阴影光照直接就把房间里给照亮了。
所以思索再三,我们最终做出限制:距离镜头最近的前 n 个光照允许阴影。
不过,如果直接把镜头位置跟点光源中心位置比较,似乎会产生不合理现象。毕竟光源是有范围的,它的阴影产生也会有一个范围,但我们的锥形视界明显靠前。所以,具体算法参见下图:
我们为镜头选择一个 Check Point。位置大概是镜头正前方 0.3-0.5 光照 Range 的位置。因为按照 Unity 里光照的实际操作经验和现实世界的光理论,光在超过一半 Range 之后,其光强衰减和距离起码是三次方关系,也就是 1/8,12.5%。所以,我们默认在这个范围外的阴影及其强度不予优先考虑。那么与镜头距离的判断基点就是 Check Point,而非 Camera.position。
此外,基于前面的第 1 节,我们已经将光进行合并,所以只需要在合并后活跃的光照上进行这项判断。
最终效果和 FPS 如下,照明弹数量超过 15 发。
最终效果的配置参数:
- 允许 n=2 个阴影光照
- 光射程 10% 范围内合并
事实上,我把燃烧时间改到 50s,直接对着天空将一盒照明弹(50 梭子)全发射出去,FPS 也是差不多这样,保持在 50 上下,纹丝不动。
秃头了这么多天,感觉还是值得的,我先去植一下发(笑)。
3. 其它可能存在的问题
1. 当然,这种优化逻辑在比较窄或者复杂的地形和建筑内,绝对还是有穿帮的可能,但在那种环境下使用照明弹,基本不是常规思路(猴子),可以拉出去直接击毙。
2. 另外,关于光合并,会涉及一个合并后光强度的取值问题。由于我不想翻阅厚如字典般的 Unity 光照内核资料,加上我到现在还用着 5.6 版本,如果研究半天,新版发生变化岂不是白给了——所以直接肉测了几个场景,取以下代码逻辑:
combineIntensity = intensityMax + (intensitySum-intensityMax>1f? Mathf.Sqrt(intensitySum-intensityMax) : intensitySum-intensityMax);
*注释:intensityMax = 合并群组里的最大光强;intensitySum = 光强总和
太专业了。要是我来,我想想,火焰直接发光贴图,照明弹给cd,携带上限减少。照明弹直接特效都不给,就一个无阴影光源,抽象化表示区域变亮。笑,你就说解没解决吧
@Su Qing:前方差评如潮警告(笑)