游戏动态相机系统的迭代——gdc session翻译

转载:https://zhuanlan.zhihu.com/p/432965447
https://www.gdcvault.com/play/1014605/Iterating-on-a-Dynamic-Camera
Selection(模式选择,选择我们感兴趣的相机)
环境
战斗
脚本
过滤器
我们通常会有很多可选的潜在摄像机,一个经典的场景可能有五十个左右的相机,很多都被设计成同时生效。
他们由三个系统来确定,环境中的区域设置,战斗中动作的触发,和对象脚本。

环境中的可寻路区域,每个区域关联了一个或多个相机,当我们的角色进入区域,相关的摄像机也会被选中
相机也会被战斗系统所确定,比如奎托斯抓住了一只怪,会进入斩杀动画,会有对应的镜头动画来做表现
我们还有脚本化sequence,当玩家触发了某些事情,脚本系统会告诉相机系统开始和结束一段固定的相机动画来highlight游戏中的事件。
当我们有一系列被选中的相机时,我们需要过滤掉不关注的相机。
每个相机都有一个列表来标记到其他相机是否可以转换。
我们还根据玩家状态来选择,比如有一个相机被标记为只有玩家坠落时才可以使用
最后我们通过优先级筛选掉优先级低的相机,然后我们就得到了最后剩下的可用相机列表
Blending(混合到一个相机中)
混合树
权重
模式
参数(集)

这个系统有一个混合树,这个树结构的确定取决于相机优先级,相机被设定为如何混合,还有相机在这一帧是否真的被使用到。
失效的相机会被移出,有用的相机会被插入到树中合适的位置上。
*每一个相机都有一个权重和混合模式,来决定如何使用它的参数输入来计算出对应的输出。

权重混合由定时器来生成,计时器并非线性的,而是由一个埃米特曲线驱动来做缓动效果。
Hermite 曲线
已知曲线的两个端点坐标P0、P1,和端点处的切线R0、R1,确定的一条曲线。
就是两个端点可以调切线的曲线
通常情况下定时器是正向推进的,比如在新旧相机交替的时候,但是有的时候比如战斗相机序列结束的时候,我们会反向推进定时器。
权重可以由角色在区域中的位置来驱动。由碰撞系统来计算。最有效的权重是区域的部分有一个方向向量,让我们可以在穿越某个区域的时候在相机间淡入淡出。
当然我们也可以通过相机自身来计算权重,我们有一个实验性的相机,它基于朝向和轨迹方向来控制自身的权重。

这个模式决定了子节点如何混合。
淡入淡出模式把第二个节点混入了第一个节点,混入权重在0-1之间,这种混合方式被使用于计时器和自我计算权重的节点。
平均模式把所有输入节点的权重平均后计算结果,这种模式用于计算基于区域位置的混合。
操作部分
当我们有了混合树,所有的权重也被确定下来,我们把混合树分解为一系列二进制操作,用累加器存储中间值。
相机被分解位多个参数,他们被独立的插值计算,包括相机朝向,目标世界空间坐标,目标相机空间坐标,以此来生产一段美观的相机运动。

相机位置xyz,半球坐标系便宜(方位角,仰角,距离),欧拉角朝向(Yaw,Pitch,Roll)
相机是和目标关联的,所以相机也会围绕目标补间而不是穿过目标。
相机空间目标位置存储为 相机到目标方向,和相机朝向的差值。通过两个角度(方位角,仰角/水平旋转,垂直旋转)和相机到目标距离(球面坐标系)
最后相机朝向存储为欧拉角,欧拉角对相机插值很友好,而不像四元数插值那么丑(会有奇怪的滚动)

当然欧拉角对云台锁镜头不友好,而且会在直上直下查看的时候发生奇怪的事情。
解决方案是保持住相机然后再垂直方向上轻微倾斜。

然后策划又扔来了这么一个东西,垂直向上看的角度。尽管我们可能应该这样做,但我们不仅是将布景放在一边。
为了解决这个问题,我们添加了一个额外的transform,让我们可以修改云台锁欧拉角方向,我们不常修改这个值,多数情况下这是个明确的旋转值,但是也有例外。



下一个问题是,方位角,Yaw,Roll都是绕圈转的。每一个参数都有两种方式来混合,走靠近的方向,或者反向转一圈。方位角几乎遇不到这个问题,因为目标基本上都在相机前面,Roll一般都是0。
对于Yaw来说,这是个问题,整个系统不是静态的,所有东西都在动,意味着哪一边最短,会一直更改,因为相机从角色一边转到另一边的时候会有很多的计算推送。
这种傻逼事情很容易发现处理,但是有时候他也会在很长一条混合步骤的中间步骤出现。

比如设计师设计了一绺相机,用来控制相机从玩家下面运动过去,但是有两个相机在线上面,他们之间的最短距离是在玩家上面的。就会导致相机翻面









这个问题的解决方法是,把每个相机的yaw表达为一个2维单位向量,然后我们不再混合角度,而是对多个相机向量做混合,得到一个二维的点,然后再把这个二维点转换为角度。
比如两个相机朝向的单位向量是(xa,ya)(xb,yb),那么混合结果就是((xa+xb)/2,(ya+yb)/2)。
这解决了大多数问题,而且当计算结果太接近中心值(正反转相差角度很小)的时候,还会报警,让策划调整相机
最终的混合参数
目标世界坐标的向量
相机球面坐标系的目标偏移
相机的pitch,roll
相机的yaw的2d向量
云台相机的四元数表示的朝向
视角
Dynamic(描述我们如何计算一个相机的位置和朝向)
动画相机
动态相机
战斗相机
三种基础相机,匹配三种不同的相机匹配方式。
动画相机用于过场动画或者由脚本驱动。
动态相机处理环境和区域相关情况。
战斗相机处理战斗动作的近摄镜头。
动画相机由动画师设计,直接应用到相机上,通常用与过场动画,用maya的aim point当作他的目标点。

动画相机轨迹我们利用nurbs曲线(由多个点控制的平滑曲线,区别于普通的必须穿过点的曲线)我们把动画长度和轨迹长度做了映射,计算曲线上到角色最近的点,然后根据映射设置动画时间。
你可以对动态相机的大多数参数做动画变化,所以你也可以基于玩家位置,通过动画相机当作动态相机的变化。

动态相机的模型和我们混合相机的模型一样

我们还有其他一些信息要关注,比如 Dolly,也是一个空间中的修正点,或者nurbs曲线(轨迹)上的点。
还有boom,和相机朝向向量一致,从Dolly开始,到目标平面结束。我们用boom来计算和约束相机的朝向和距离(相机到目标)
我们还将偏移距离转变为从相机到目标平面的距离,因而防止偏移约束导致的目标在屏幕上大小的变化。

这是一系列的约束。按顺序应用。
首先我们设置framing,用于再相机空间约束方位角和俯仰角,目标的水平垂直位置。
然后我们更新dolly的位置来约束boom的长度和朝向
然后我们根据目标平面约束相机的距离
最后我们约束相机的朝向。

framing约束是一个指定的屏幕空间坐标范围,定义了屏幕上的一个正方形,我们叫safezone
如果奎托斯移出了屏幕,我们也要相应的移动相机

第一件事我们通过fov和分辨率把安全区转换到一个球面空间的角度范围,我们每一帧都会进行这种计算,因为这些约束值也会由动画控制。显然球面空间的映射从上到下是扭曲的,既然水平的fov没那么夸张,我们操作的范围内出的问题很小,可以忽略。




我们清理掉框并放大,首先为更新过的目标位置更新便宜角度。
然后我们clamp到左右角度内,这里我们已经在水平约束内了,但是需要向下移动来匹配垂直的约束。
我们在这个地方不会修改朝向和距离,它的效果是垂直方向上跟踪玩家的方向。





在我们约束距离和朝向之前,我们需要计算boom,也就相当于决定dolly的位置,如果没有rail,我们就用一个空间中的修正点。
如果我们有rail,我们需要让dolly沿着rail移动来满足约束。
可以是距离约束
可以是角度约束,可以在穿过dolly的修正向量的两侧对称,也可以和在dolly所在rail点的切线相关
他们可以用来产生权重,通过一个极小化的函数来吧dolly移动到最适合约束的点。

极小化函数主要需要一个计算权重函数,和初始化的参数值。
权重计算函数把参数把参数转换为dolly的位置,了解目标会超出约束多少,并把这个作为返回值。
然后极小化函数在处置参数附近的空间搜索来寻找一个局部极小值。
为了让他能平滑,我们还加入了一些简单的阻尼




我们现在有boom,我们用它来计算距离属性
在这里我们用boom ratio参数,他是boom上的某个位置,从0-1,1是dolly的位置,0是目标平面的位置。
然后我们对到dolly的距离生成一个最大/最小的约束。
然后对目标做一样的事,我们最后做目标距离的约束,来保证相机不会穿过角色导致裁剪。
我们也可以把距离设置固定值
最后我们计算朝向的约束,pitch和yaw分开来处理,roll不会直接产生,基本是设为0的。
首先我们从boom的朝向设置相机的朝向
然后我们用约束boom同样的角度来检查相机的朝向约束
如果朝向在约束外,我们通过旋转相机来修正。因为这些发生在相机上,所以我们需要重新计算距离和偏移在移动中保持住相机。
当然和距离一样,我们可以把朝向设置成固定角度。
当你应用一个约束的时候,不可避免地会影响到另一个约束。当然我们可以通过让约束只影响独立的参数来最小化这个影响,当然,也不是百分百可以保证。比如我们看旋转的约束,有的时候你会影响到整个模型。通常这些冲突都会内部消化掉,但是有的时候他们也会让镜头摇摆导致大问题。
幸运的是,通过放宽某些约束我们可以简单的解决这个问题。我的经验中主要是朝向约束,放开到0-5度可以让相机更平滑。
这些是设计师可以控制的约束。我们对boom和相机本身进行了dolly距离约束和角度约束,保持了约束参数简单。
所有参数都可以由maya相机动画化。
两个例外是最下面两个参数,他们是标记位,rail relative决定了角度约束是否由dolly的切线方向决定。move dolly控制了角度约束是否应用到dolly上。
生成了约束过的偏移,朝向,距离后,我们有了更新之后的相机位置和朝向,最后我们把距离转回混合格式,然后就可以把这些参数送回混合器了。
下面是战斗相机。
这是gow的经典斩杀动画,发生时,我们喜欢直接控制相机,来找到动作的最佳机位。
因为这段动画可能发生在任何位置,所以我们不可能把镜头预设在世界中。
相对的,我们相机运动关联到了角色上,显然屏幕中有三个对象,主角,怪物,和rig(牵引器),这三个对象都是由一个joint驱动移动来匹配位置和朝向,我们称之为synch joint,他们播放的动画也是同步的,在相同的时钟内播完
combat rig由三个节点组成,有一个synch joint作为根节点,然后有一个相机和一个目标点附着在根节点上。
那么为什么我们需要三个节点呢,如果我们在这么一个开放的竞技场中,我们很自信相机动画从任何位置播放都不会穿过环境,没什么大问题。
当然另外来说,大多数环境没有这么好,很大可能相机跑到柱子或者墙后面。我们决定不在相机上做任何碰撞检测,相机的碰撞很难做好,所以我们有了这些节点。
在我们对环境没信心的时候,做法是用环境相机的位置然后再混入动画相机。动画相机被设计为不在意环境所以我们可以认为位置是安全的。
然后我们取用rig target的位置,让相机面向它。
然后我们调整fov让我们在使用rig上的相机的时候,目标在屏幕上的尺寸不变。



我们可以画一个包含了所有感兴趣区域的球体,这个球体由target point,相机和fov决定
然后我们通过把区域相机旋转到看向这个球体,来渲染出这个球体中的内容。
并且把fov带入来适应这个球体。

按照上面的说法。我们取回了一些位置相关的信息,通过沿着向上的轴旋转调整锁定相关的设置,来匹配相机动画
我们会在一段时间中缓动这个旋转,但是有可能在旋转比较大的角度的时候看上去奇怪。
所以为了最小化初始旋转,我们在多个角度寻找相机,然后找一个最接近的通常会给四个相机,东南西北,有的时候也会把他们在动画的控制下合成一个。
一段视频演示,30分15秒开始

以上就是战斗相机,这里是一个不用rig的简化版本,但是我们用了角色的目标和触发动作中的一个宽度。
同样的,旋转和位置覆盖是可选的feature。通常boss都有特殊区域,如果设计师有信心的话,角色周围总会有足够的空间,他们可以把位置覆盖关掉,任意的移动相机。有一个意想不到的作用是,战神三中我们很多黑幕动画其实是使用了战斗相机来跟踪其中一个主角。
Targeting(确定我们关注的是什么)
角色
生物
阻尼
权重
优先级



奎爷的基本信息,逻辑位置在双脚中间,碰撞胶囊体2.25m,目标点在1.5m的位置,角色目标指定的时候没有阻尼,所有的阻尼来自玩家自身逻辑。为了相机能紧跟着角色,相机不会延迟或者超前移动太多。

奎托斯经常到处跳,有的时候我们不希望他把相机上下拽,有一个可选的系统叫做跳跃纠正,它可以在每个相机上独立开关,让我们来忽略跳跃带来的影响。
如果落地时的位置比跳跃时的位置高了,我们把相机移上去跟玩家会和。
如果他落地位置比开始位置高,我们需要重新追踪垂直方向上的位置

前面就是我们选择目标的主要方式,但是对于战斗来说,我们还要继续努力,首先就是我们要支持多个目标,最好每个关注的对象都在目标中,通过某种方式让他们都出现在镜头中。
打开了调试信息。红色圆圈是主角,小的红色圆圈是怪,中间的菱形是bloom目标,用来从动态相机计算boom,白色正方形是安全区域。

怪物的目标是一个圆圈而不是一个点,包含了权重和优先级。
它也可以附加到一个节点上,意味着我们需要对他设置阻尼。对于主角来说这是没有的毕竟我们基于玩家的逻辑来平滑的移动胶囊体。动画节点保证不了这些,只会让相机移动被动画师随意发挥。只有使用这个target方式才能为添加到相机上的非设计的移动有巨大潜力。
所以我们要过滤掉微小的移动,因此idle下的怪物就不会让相机震荡了。当然我们也要保证我们能追踪到大的移动并且平滑的过度过去




我们通过两个球体间的区域来解决这个问题
内球里面是自由移动区域,不会影响相机。
两个球之间的区域逐步移动让节点回到内圆内。
外球外面不需要阻尼,目标会马上追上去,保证目标不会脱离。

回到奎托斯。现在他周围又一圈敌人。让我们用相机的视角看一看。
我们现在给每个怪都有一个target,最简单的方式是我们平均取值来得到一个boom的target。
奎托斯基本上都是处于战斗的中间的额,通常不会把他移到屏幕外面。

但是并不能保证不会发生,如果他远离了一个跑的慢的敌人,很有可能出现他们两个都被移出了屏幕的情况。所以我们需要为每个目标添加权重,通过加权平均来取到boom target

现在我们的boom target是加权平均后的位置了
每个目标的权重都有三个部分组成,
Base Weight,表示这个目标有多重要
Distance Weight,表示敌人到主角的距离权重
Activation Weight让我们能平滑的加减目标数,通常的来说就是敌人出现或者死亡。


但是当我们减到一个敌人的时候,并不能回到一个合适的位置

问题来自于,这个技术没有区分战斗的规模,可能是个近距离战斗也有可能是散开来的战斗。
这样就让设计者没办法控制战斗的画面了。
症结在于,加权平均的过程中,忽略了其他的战斗目标信息。我们想要一个方案可以把每个目标都考虑进来,同时保证最重要的目标永远在合适的位置。

因此,下一波处理多目标是,我们的目的是保证最高优先级的目标能留在框中,然后尽力将低优先级的目标放进来。
新的算法是由框和距离约束扩展出来的。相较于clamp单个点,这种方式让相机框能追踪其他的目标圆球。
每个生效的优先级都会计算与i便,从最低的开始到最高的结束。以此保证高优先级目标永远在框内。然后层层计算后得到绘制所有目标的最佳方式。

下面是算法展示,她们都是在我们原来做安全区约束的球面空间内完成的

首先我们把框放在主角中间,当然这是可选的,但是这个方法让你可以拥有最大的安全区,而不是明明只有奎托斯一个人的时候奎托斯还出现在边上。

然后我们计算最低优先级的区域

然后把狂移动最小的距离并经量覆盖这个框。

每一个优先级都反复上面的步骤

处理优先级2

处理优先级2,移动框

处理优先级1

处理优先级1移动框

完成之后,我么有了一个新的框,我们实际做的就是统计框的移动,并且引用到相机的方位角仰角参数上,这决定了boom target的位置。

我们替换了boom的距离约束的缩放。
相应的是,我们从boom上计算相机初始位置,使用我们以前用过的权重极小化函数来决定dolly在轨道上的位置。
现在我们用目标的外框区域在安全区外面的部分作为权重把相机拉回dolly,同时我们用boom target的距离当作一个权重把相机向前推。
我们尝试避免距离约束的阻尼是没用的。因为这是一个明显的危害,它干扰了dolly上的阻尼。可能会导致不受欢迎的环形震动
视频展示43:40,可以看到单挑的时候有了明显的提升,同时多目标下也没问题。

还有一些问题, 首先是我们如何处理怪物出生死亡?不像加权平均算法,这个算法没有可以计算的权重。

所以当一个角色出生在屏幕外

我们得到了一个难看的结果

解决方法是给这个系统加上权重,然后用这个权重把目标逐渐的移入移出



我们把目标的位置在目标物理上的位置和主角的位置做补间,我们永远不会移出主角,然后逐渐的让目标移进框里,所以我们可以忽视其他在之后出生和死亡的目标防止出现相机乱飞。
视频演示45:03,注意防止目标比奎托斯打太多(取目标的位置不一定准确)

既然我们有给一个目标算权重的方式,我们可以把这个方式加入到我们之前的加权计算方式了。

第二个问题是个小trick。还记得我们是在球面空间操作的么,有一个不连续的从180到-180的过程







目标穿到了相机后面,所以相机一下反过去了

糟糕的是,下一帧相机又反转了回来


之前这不是一个问题。因为我们有效地保证了一个目标的时候,在约束生效的情况下,不会跑到我们后面。但是我们打破了这个承诺,诱惑着我们故意为之,觉得这个规定没意义。





怎么解决这个问题呢
和我们前面加权的方案很想
我们修正了目标的逻辑位置,让他永远不会跑到相机后面去。我们把位置clamp到了相机平面平行的地方,有目标约束的最小距离来定义。看上去好多了。