3C细节打磨-关于相机的遮挡拉近问题
在 搬柴运水:3D游戏的镜头设计原理 一文中,我提到了第三人称视角中,当角色和相机之间存在遮挡物时,有遮挡拉近和遮挡剔除2种解决方法,所谓的遮挡拉近,就是用缩短相机和角色之间的距离的方法,将相机拽到障碍物的前方去。如下图所示:
针对这个看似简单的功能,我们提出如下问题:
1.什么样的阻挡会触发相机的遮挡拉近功能?什么样的阻挡不会触发?如何识别这两种阻挡?
2.当相机与场景物件或角色重合时,应该怎么处理?
3.在相机拉近(恢复)的过程中,实际上是哪个相机参数在变化?如何让过程尽可能顺滑?
4.如果玩家可以手动拉近、拉远相机,如何处理手动操作与自动行为之间的关系?
在解答之前,需要简单介绍一下英伟达开发的名为PhysX的物理系统,这是当今主流的游戏引擎普遍使用的物理解决方案,其中包含了大家熟知的ue4和unity3d。PhysX提供了3种基础的查询种类,分别是:
(1)RaycastQuery:从一点发射一根定长的线段,检测与这根线段相交的碰撞体
(2)SweepQuery:从一点投射一个/一组形状,形状向指定方向移动一段距离,检测与这个形状相交的碰撞体
(3)OverlapQuery:给定一个形状,检测与这个形状相交的碰撞体
本文会结合这三种物理查询方式,来实现镜头与场景、角色的交互。
解答:
1.什么样的阻挡会触发相机的遮挡拉近功能?什么样的阻挡不会触发?如何识别这两种阻挡?
我们想知道相机和角色中间有没有障碍物,可以使用RaycastQuery发射一条射线,检测有没有相交的碰撞体。接着我们讨论一个具体的问题,当你操作的角色在一个宫殿中移动时,角色的周围遍布了一人粗的柱子,这些柱子有可能出现在相机和角色的连线上(如下图),我们操作角色按上图的箭头从左向右移动,相机始终与行进方向保持90度夹角,这种情况是否应当触发遮挡拉近呢?
如果触发,显而易见,相机会在柱子、墙边多次折返,相机的位置会频繁改变。相机的位置频繁变动,会影响玩家对空间的判断能力,尤其当玩家需要在这样的环境中战斗时,绝对不会喜欢相机位置频繁变动。那我们在射线击中碰撞体时,如何区分“柱子”和“墙壁”呢?
(1)最符合直觉的归纳方法是从单个资源直接区分:A.会触发遮挡拉近的物件 B.不会触发遮挡拉近的物件。
显然,柱子是属于不会触发遮挡拉近的物件,而墙壁是会触发遮挡拉近的物件。但是考虑一种情况,如果这些柱子彼此紧密相连,两两之间没有任何空隙,延续了10米,柱子是否就成为墙壁了?所以从单个资源分类作为切入点,需要我们对场景中每一个实例都仔细检查并分类,是一种需要持续打补丁的做法。
(2)另一种方法是在设计之初,进行一个更“逻辑层面”的区分:A.划分空间的物件 B.非划分空间的物件。
这样的分隔物,可以是墙,也可以是一排柱子,还可以是水帘洞前的瀑布。假定场景需要有白模搭建环节,我们可以在白模阶段就确定所有的分隔物。我们可以在分隔物的位置上摆上不可见的box,用这些box实现遮挡拉近的效果。日后即便将一面墙换做别的美术资源,镜头也可以保持预期。这也是很多策划推崇的“表现和逻辑分离”。
实际操作中,往往会将上述2种方式进行结合。第一个方式适合对确定性的资源(例如所有墙面)进行标记,第二个方式适合对Gameplay重点区域(例如Boss房)进行标记。第一个方式的优点是效率,处理开放大世界茫茫大的场景更加高效;第二个方式的优点是精细和灵活,能够不改动美术场景的情况下进行操作。
2.当相机与场景物件或角色重合时,应该怎么处理?
由于第一步需要处理会遮挡拉近的物件,所以相机永远不会与它们重合。剩下的其他场景静态物或动态物,都有可能与相机重合,例如大街上,一辆汽车突然驶过,正好穿过了摄像机,如果什么也不作处理,相机就会和汽车穿插,看到汽车的内部,也许还会和汽车驾驶员的人体穿模。
大家很容易将这个问题和第一个问题混淆成同一个问题,认为将汽车和角色打上“遮挡拉近”的标签,就可以避免穿插。但显然不是一样的问题——我们并不希望车辆或行人在主角和相机中间经过时,也拉近相机。所以我们必须定义一个新的功能。
这里会用到PhysX的另一个基础查询功能OverlapQuery,需要给相机附加一个球形或胶囊体物理,表示相机自身的“体积”,这个球大小不宜过小,也不能太大,需要根据游戏类型考量。每一次设置相机位置时,会先进行遮挡判断,若有遮挡则拉近;再判断相机有没有与其他碰撞体重叠,若有重叠则再次拉近。
上面说的是“车来撞我”的情况,当然还有“我去撞车”的情况,即相机移动时,如何避免与其他碰撞穿插?假设相机自身有一个球形物理,如下图所示,红色部分是相机的SweepShape,灰色部分是遮挡物,蓝色部分是普通的碰撞体。只要相机移动,SweepQuery就会被执行,找到从角色到遮挡面(灰色墙壁)之间,最远的不会发生重叠的位置。顺带一提,角色的移动一般也会使用胶囊体shape进行SweepQuery,用来防止角色和碰撞体穿插。
由于是否和相机重叠的判断是绝大多数碰撞体都需要做的事情,我们实际操作时可以将此视为一个默认的标签。再将一些特殊的、可以和相机穿插的物件,设置为单独的碰撞层,让他们可以与相机重叠。
还有一个问题是相机移动路径上有一个碰撞,怎么“过”碰撞。即“我去撞车”时,如果有个碰撞体正好在角色和相机之间,相机是直接一帧从碰撞后跳到碰撞前,还是想办法绕过这个碰撞?
这里推荐一帧跳过碰撞。如果有个别类型的资源,例如角色,想要相机经过角色时从侧面绕过,保持相机朝向不变,那么屏幕空间的主角位置就会发生变化,需要确保这个别类型的资源的碰撞体尽可能“瘦”一些。
3.在相机拉近(复位)的过程中,实际上是哪个相机参数在变化?如何让过程尽可能顺滑?
先看一张图,能够帮助我们更好地理解相机拉近的过程。下图是一张俯视图,代表了玩家操作相机绕自身旋转,从一开始左前方的位置逐渐旋转到正后方的位置。假定角色的朝向是画面正上方,α代表了相机与角色连线、玩家面向之间的夹角,d代表了相机与角色的距离。不难发现,所谓的相机拉近,就是保持相机的注视点不变的前提下,缩小d的数值。
我们发现,如果角色处于一个直角建筑的前方,d和α的关系将会是下图的样子:
之前的文章中,我们提到过流畅丝滑的观感来源于连续性。在相机遇到一个直角碰撞时,显而易见d会发生一个跳变,从原长度瞬间减少到一个较短的长度。我们如何处理这样的跳变呢?
最简单的方法是对曲线上不连续的点进行平滑处理,我们可以用一个较短的时间作为过渡,如下图所示:
但是这样做缺陷很明显——过渡的那段时间里,相机的d逐渐缩小,α同时还在增大,相机并不会按直线轨迹绕过直角,而是会走一个曲线,穿到直角的模型里,这时候会触发重叠拉近,将相机弹到直角模型外面。
最棘手的问题终于来了: 相机如何才能预测接下来会碰到的障碍物?
这里的障碍物专指会触发遮挡拉近的物件,即第一个问题讨论的物件。游戏行业有一个经典的预测算法是whisker raycast,即通过多条射线辅助判断“运动的趋势”,在游戏相机和AI模块都有应用。对我们假设相机到角色的连线不是一条直线,而是构成扇形的多条射线,如下图所示:
上图中紫色的5条虚线是从摄像机发射向角色方向的whisker raycast,当最右边的射线率先碰墙时,相机不做任何拉近处理,但是当右边第二条射线碰墙时,相机就会开始触发遮挡拉近。绿色的虚线就是相机的运动轨迹。这样处理的好处,就是给拉近的过渡预留了一些“空间”。辅助射线的数量越多、密度越高,预测的效果就越好。顺带一提,当角色驾驶载具,或处于高速运动状态时,射线彼此的夹角,可以适当扩大。
提前拉近相机 https://www.zhihu.com/video/1507335355812073472上面的视频可以看到,在摄像机、直角、角色三点一线之前,相机就已经提前开始拉近了。原神也注意到了这个碰撞预测的问题,但使用的未必是一样的方案。whisker raycast的缺点是当相机离碰撞位置越近,预测能力越弱,但是相应地,需要的过渡空间也越小。
除了直角以外,还会有一些棘手的复杂碰撞体或碰撞体集合,会让d的变化不太理想。例如下图所示,在一面墙前,堆满了各种杂物,杂物有大有小,角色和相机保持图中的相对位置关系,角色向右侧移动。
这种情况下,我们不难想象到,d会有一些抖动现象。同样的问题,也存在于同样的场景,角色静止不动,玩家旋转相机绕角色旋转的时候。这样的抖动现象还有另一种表现形式,就是在角色上下楼梯的时候,如果楼梯的碰撞是若干个直角而不是一个斜面,那么每一次SweepQuery找到的位置随时间绘制成的函数必然是阶梯状的。防抖动的做法一般是加个插值过程,例如以加速的函数渐进,能保证过程是连续、不跳变的,但相机会有一些滞后感,如下面的例子所示:
相机响应碰撞变化的滞后感 https://www.zhihu.com/video/1507727884525084672通过观察美末的相机拉近、拉远,我们还可以发现一个小细节: 在玩家操作相机较快地围绕角色旋转时,相机是不会执行拉远操作的 。即相机在旋转时,SweepQuery一直在执行,但仅当出现离角色更近的碰撞时,d才会变化(缩小),相机逼近玩家。当相机的旋转结束后0.3秒,才会根据物理查询的结果尝试将d扩大。所以在玩家旋转相机的过程里,d只会越来越小,而不会一会变小、一会变大,保证了流畅性。
4.如果玩家可以手动拉近、拉远相机,如何处理手动操作与自动行为之间的关系?
这个问题对于大部分游戏来说不用考虑,因为设计上不会让玩家主动拉近、拉远相机。一般有这种需求的游戏是mmo,需要能够从远、近、不同的角度自由观察角色。这个需求和上面讨论的相机拉近功能是有一些矛盾的,就像两个人在互不知情的情况下操作同一个文件,那一定会冲突的。
我们通过上文的分析,知道了只要相机移动,都会进行自身球体范围向行进路线上投射的SweepQuery,这个查询能确保相机不会在移动时和其他物件穿插,所以如果玩家在手动拉近、拉远相机时,碰到一个碰撞体,会瞬间跳过这个碰撞体。所以仅看相机重叠问题,玩家手动拉近拉远相机,其实和玩家操作角色在场景中移动是一样的效果。所以第二大类重叠的问题,是不会和手动拉近、拉远相机,造成冲突的。
剩下会造成冲突的就只有第一大类的遮挡拉近以及复位的情况。如下图所示,当角色经过一个墙角,相机触发遮挡拉近时,玩家反向操作,手动将相机拉远,就造成了冲突。
这俩操作显然不能共存。正常情况下,我们要尽可能尊重玩家的主动操作,但遮挡拉近是个例外,因为遮挡拉近是从当前数值向一个确定的数值进行过渡的过程,这个过程最好不要被干扰,否则过渡的效果必然无法得到保障。而在复位的过程里,无论是手动拉近、或是拉远,都可以直接中断复位的过程,认为玩家不想复位了。
这样的逻辑从策划设计角度比较清晰合理,但程序实现角度,一是难以区分d的变化到底是什么环节导致的,二是不同的引擎架构中对于相机的位置计算流程差距非常大,万一手动操作对数据的影响在计算流程的前面环节,而遮挡拉近对数据的影响在后处理环节,那就一切皆休了。