游戏中的相机

阅读量: 鲁文奎 2021-04-22 12:04:39
Categories: Tags:

探究各类相机的通用结构

刚接触相机,认为相机就是一些矩阵变换,处理好就行了。但是,自己在仔细观察曾经玩过的游戏【比如魔兽世界、塞尔达】之后,做好一个在视觉上有极致体验的相机还是比较麻烦的。
下面是自己业余用Unity实现的一款相机的笔记总结,分享给大家。

游戏相机分类

每类游戏,甚至于每个游戏的相机实现都不相同,这是因为贴合游戏内容去设计的相机是最能展现游戏内容、带给玩家极致体验的。
因此,本文只对常用的游戏的相机【特指RPG跟随相机】做一个归纳总结。
首先,从不同的角度来对常见游戏中的相机进行一个大致的分类:
按视角分:

RPG跟随相机的实现思路

总体流程结构

缩放/跟随/旋转

  1. 计算相机的当前位置
    一般情况下, 我们会在跟随的目标上面标记一个点作为相机注视的焦点pivotOffset, 然后赋予相机一个相对跟随目标的一个初始偏移camOffset, 如下图所示:

然后相机的坐标我们可以这样计算:

cam.position = player.position +  Quaternion.identity * pivotOffset + camRotation * Vector3.Normalize(camOffset)  * curCamDistance;

说明:
pivotOffset 和 camOffset 都是相对坐标;
camRotation 是一个四元数, 表示相机当前的旋转,通过输入设备来控制。
2. 计算相机的焦点
因为使用的是Unity,这里就直接使用Camera提供的API处理。如果像BigWorld,就需要自己去处理相机的旋转

cam.LookAt(player.position + pivotOffset);

通过上面2步我们就可以实现相机的简单跟随、旋转了, 跟随的时候我没有做平滑处理, 因为我觉得我们的情况下做与不做的差异不是很明显。但是,某些特殊的场景也是需要做一段平滑处理的。比如说塞尔达刚出山洞的那个镜头, 就需要用插值来做平滑处理。
3. 相机的缩放的相关处理
输入设备的参数变化【比如PC的滚轮、移动设备的双指操作等】决定相机与玩家之间的距离。每一次变化都会重新计算desiredDistance【相机将要达到的位置】,然后我们会在curCamDistance和desiredDistance之间做一个插值计算,获取下一帧相机到达的位置, 然后将相机移动到这个位置。直到相机达到的我们的目标位置, 也就是desiredDistance, 便停止插值计算。
插值计算有很多种方式, 如果希望简单 可以直接使用Unity提供的API:Mathf.SmoothDamp即可。具体实现我用python实现了一遍, 如下:
公式:

def smoothDamp3D(current, target, currentVelocity, smoothTime, maxSpeed, deltaTime):
	smoothTime = max(0.0001, smoothTime)
	omega = 2.0 / smoothTime

	x = omega * deltaTime
	exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x)
	change = current - target
	originalTo = target

	# Clamp maximum speed
	maxChange = Math.Vector3(1,1,1) * maxSpeed * smoothTime
	change = min(max(change, -maxChange), maxChange)
	target = current - change

	temp = (currentVelocity + omega * change) * deltaTime
	currentVelocity = (currentVelocity - omega * temp) * exp
	output = target + (change + temp) * exp

	# Prevent overshooting
	if (originalTo - current > 0.0 == output > originalTo):
		output = originalTo
		currentVelocity = (output - originalTo) / deltaTime

	return output,currentVelocity

如果需要在缩放的细节上有更加极致的函数, 基于上面的插值函数优化即可。

碰撞/半透

  1. 碰撞过程的处理
    碰撞未直接使用Unity提供的API,而是在pivotPoint(相机的焦点)上构建一个和相机近裁面平行的的矩形,由矩形的四个点发出四条射线到相机的近裁面,如果中间遇到碰撞体,则将射线与碰撞体的hitpoint存储到数组closestRaycastHits中, 然后按距离对closetsRaycastHits排序,选出距离玩家最近的hitpoint,然后将相机移动到对应位置。
    如下图所示, 红色的矩形是与相机近裁面平行的一个面, 从矩形的4个顶点沿着相机的方向(也就是图中白色的线)发出4条射线。

    如果遇到碰撞体, 则将相机拉到最近的一个hitpoint即可.大致类似于这样:


    在这个过程中需要保存镜头变动之前的位置,如果相机后面的碰撞体消失了, 需要把相机通过插值的方式再移动到保存的位置上。

  2. 半透过程的处理
    如果相机在遇到碰撞体时需要实现半透功能,则需要将相关逻辑添加进来,可以通过一个开关来控制相机是否对该功能的支持。关于半透的具体实现逻辑就是判断物体的Tag标签是否属于transparent类型, 如果是,则将物体的材质的alpha值设置成0.5即可。当然,半透的值也可以做成一个可以配置的接口,提供给外部。

天下3 中 拖拽类相机 的实现

在很多放置类游戏(比如Two.Point.Hospital)中, 我们都可以找到拖拽类相机。这类相机的主要特点是 通过鼠标移动相机。
因为天下3是基于BigWorld引擎, 没有现成的相机供我们使用, 我们就只有在引擎里面实现了。
思路如下:

  1. 拖拽
    鼠标在屏幕上从A点移动到B点, 对应场景中从C点移动到D点, 然后根据CD计算出C到D的位移, 然后将这个位移附加到相机上,使相机移动即可。
    计算相机变换(deltaMatrix)的代码如下:
	int ox = 0, oy = 0;
	get_cursor_pos(&ox, &oy);

	if (almostEqual(lastFramePosInWorld, Vector2::zero()))
	{
		lastFramePosInWorld = curMouseInScreen;
	}

	// 这里需要保证当前帧 和 上一帧的 变换矩阵是一样的
	curPosInWorld = getClipPosInWorld(lastFramePosInWorld.x, lastFramePosInWorld.y, screenToWorldCoefficient);

	Vector3 framePosInWorld = getClipPosInWorld(ox, oy, screenToWorldCoefficient);// 当前帧 鼠标 在场景中的位置
	lastFramePosInWorld = curMouseInScreen;// 保存当前帧的位置
	
	targetPosInWorld = Vector3(curPosInWorld.x - framePosInWorld.x, curPosInWorld.y - framePosInWorld.y, curPosInWorld.z - framePosInWorld.z);

	curPosInWorld = framePosInWorld;
	deltaMatrix.setTranslate(targetPosInWorld);
  1. 缩放
    因为BigWorld中保存了相机的invView_(从视口变化到世界坐标的矩阵),所以缩放直接取这个矩阵 * 相机缩放的系数即可得到相机下一帧偏移的矩阵,将这个矩阵附加到相机上即可。
    具体实现效果如下:

9yk17-e08il