《游戏编程算法与技巧》摄像机篇

摄像机的基础属性

视场

观看世界的角度和广度,称为视场(FOV)。人双目并视大约能看到120°的视场,而剩余的视场在边缘能够发现运动,但不清晰

如果视场变的太大就会有鱼眼效果,屏幕边缘会弯曲,类似于广角镜头

高宽比

视口的宽度和高度的比率(如4:3、16:9等)

摄像机的跟随实现

基础跟随

跟随在某个对象后方,保持固定距离,固定的俯视角摄像机

 1// tPos: 目标位置
 2// tForward: 目标方向
 3// hDist: 水平跟随距离
 4// vDist: 垂直跟随距离 
 5void BasucFollowCamera(GameObject target, float hDist, float vDist) {
 6    // 计算相机位置
 7    Vector3 pos = target.position - target.forward * hDist + target.up * vDist;
 8    // 计算相机朝向
 9    Vector3 cameraForward = tPos - pos;
10    // 设置相机位置和朝向
11    //...
12}

弹性跟随相机

实现相机到相机目标位置的逐渐变化,可以理解为真实位置的相机和在基础跟随所要到达的理想位置中间有一个弹簧

 1class SpringCamera {
 2    float hDist, vDist;     // 水平和垂直跟随距离
 3    float springConstant;   // 弹性常量:越高弹性越小
 4    float dampConstant;     // 阻尼常量:由弹性常量决定
 5    Vector3 velocity, actualPosition; // 速度和摄像机真实位置
 6    GameObject target;      // 跟随目标
 7
 8    void init(GameObject _target, float _springConstant, float _hDist, float vDist) {
 9        target = _target;
10        springConstant = _springConstant;
11        hDist = _hDist;
12        vDist = _vDist;
13        // 计算阻尼常量
14        dampConstant = 2.0f * sqrt(springConstant);
15        // 最开始真实位置 = 理想位置
16        actualPosition = target.position - target.forward * hDist + target.up * vDist;
17        // 设置相机位置和朝向
18        //...
19    }
20
21    void update(float deltaTime) {
22        // 计算理想位置
23        Vector3 idealPosition = target.position - target.forward * hDist + target.up * vDist;
24        // 计算理想位置到真实位置的向量
25        Vector3 displacement = actualPosition - idealPosition;
26        // 根据弹簧计算加速度,然后积分
27        Vector3 springAccel = (-springConstant * displacement) - (dampConstant * velocity);
28        velocity += springAccel * deltaTime;
29        actualPosition += velocity * deltaTime;
30        // 设置相机位置和朝向
31        //...
32    }
33}

样条摄像机

样条可以看作曲线,用线上的点定义的。使用她进行插值能够在整条曲线上得到平滑的效果。

Catmull-Rom样条

邻近点进行插值。需要四个点,两个在前和后的控制点$P_0$和$P_3$,两个在中间的激活点$P_1$和$P_2$。计算t值介于0到1的所有样条公式(需要在控制点均匀的情况下使用):

$$ P(t) = 0.5 \cdot (2 \cdot P_1) + (-P_0 + P_2) \cdot t + \newline (2 \cdot P_0 - 5 \cdot P_1 + 4 \cdot P_2 - P_3) \cdot t^2 + \newline (-P_0 + 3 \cdot P_1 - 3 \cdot P_2 + P_3) \cdot t^3 $$

 1class CRSpline {
 2    Vector3[] controlPoints; // 样条点的动态数组
 3    
 4    // 计算曲线
 5    // start: t=0对应的控制点
 6    Vector3 Compute(int start, float t) {
 7        // 检查start-1,start,start+1和start+2都要存在
 8        Vector3 P0 = controlPoints[start - 1];
 9        Vector3 P1 = controlPoints[start];
10        Vector3 P2 = controlPoints[start + 1];
11        Vector3 P3 = controlPoints[start + 2];
12
13        Vector3 position = 0.5 *((2*P1)+(-P0+P2)*t+
14                            (2*P0-5*P1+4*P2-P3)*t*t+
15                            (-P0+3*P-3*P2+P3)*t*t*t);
16        return position;
17    }
18}

这个公式也可用于计算t介于0到1之间的切线。首先,计算任意的t位置。然后,计算t加很小的增量$\Delta t$的位置。就可以构造一个$p(t)$到$P(t+\Delta t)$的向量并且正规化,从而近似得到切线。为了表示摄像机的移动方向

相机支持算法

相机碰撞检测

为了避免穿墙等问题,最简单的方式就是让从目标位置到相机进行光线检测,碰撞到物体则让相机位置改变到碰撞的物体之前。缺点是变换很突然。也可以用物理的方法做碰撞检测。

让离相机过近的物体进行消失或者淡出,避免穿模。

拣选(鼠标点击从相机发射射线)

鼠标的位置是屏幕空间中的一个2D点。将这个2D点从屏幕空间变换回世界空间去,称之为反投影。反投影为摄像机矩阵乘以投影矩阵的逆矩阵: $$ unprojection = (camera \times projection)^{-1} $$ 但2D点不能乘以4x4矩阵,所以要线将2D点转换到齐次坐标系。z分量通常设置为0或1,取决于近平面或远平面。而作为一个点,w分量总为1.

1Vector3 Unproject(Vector4 screenPoint, Matrix camera, Matrix projection) {
2    // 计算逆矩阵
3    Matrix unprojection = camera * projection;
4    unprojection.Invert();
5    return screenPoint * unprojection;
6}