wow镜头模拟

        3D游戏编程中,镜头的控制相当重要,不同的镜头表现,能给玩家完全不同的体验;比如《跑跑卡丁车》中的跟随镜头,每当甩尾的时候,镜头也会有相应的运动轨迹,如果只是单单的垂直俯视,那肯定全无甩尾的感觉。废话少说,这里分析下魔兽世界中主角的跟随镜头行为,因为这个跟随模型很简单,网上也很少有镜头跟随模型相关的文章,希望能起到个抛砖引玉的效果。阅读本文需要有一定3D图形学基础。


 在wow中,主角的镜头主要可以分解为3种运动,这3中运动相互独立,为编程带来了方便:

1.镜头forward方向的进、退,根据鼠标滚轮控制

2.绕主角垂直中心轴的转动,鼠标按住左键后左右移动控制

3.绕某水平轴的转动,鼠标按住左或右键后上下移动控制

玩过wow的朋友可以细想一下,主要方式是不是就这三种。其它方面,比如wow中有个选项,主角移动的时候镜头会自动旋转到人物后方;还有就是镜头因为场景碰撞离主角很近以后,当碰撞没有的时候,会自动退回到原始的跟随距离等。这里将分析并实现以上描述的5点。


接下来做一些约定:

世界坐标的垂直向上方向为Y轴正方向

同时主角的Y轴始终与世界Y轴一致,主角可在XOZ平面内旋转

主角的朝向为其Z轴正方向,主角的X轴和Z轴始终在XOZ平面内,即其y分量为0

镜头的朝向为其Z轴正方向,镜头的X轴始终水平,在XOZ平面内

镜头与场景采用球形碰撞


不太明白的可以看示意图:


                                              相机跟随模型侧视图 

侧视图中,椭球体代表主角,点T是主角的Root Bone位置,一般为脚底;点P是相机位置;点A是主角垂直中心轴上一个点,相机始终盯着这个点



                                            相机跟随模型俯视图,对应3种运动的第2种

俯视图中可以看到,主角的Z轴代表其正前方,相机的Z轴代表其观察方向


在这里,相机的运动其实就是向量P->A的运动:

P->A缩短、变长就是运动方式1

P->A绕主角垂直中心轴360°无限制旋转就是运动方式2,见俯视图

P->A绕经过A点的某水平轴有限制旋转就是运动方式3

下面看下运动方式3的有限制旋转示意图:


                                             相机俯仰角示意图,对应3种运动的第3种

theta角为向量P->A上下旋转的最大限制角,可以设定为上下对称,也可以设定为不对称(不过需要另外写代码判断,这里不做分析)

        为简化编程,我们将向量P->A的运动在跟随目标(主角)的局部坐标系内先实现,再转换到世界坐标系。跟随目标的X轴和Z轴见‘相机跟随模型俯视图’,Y轴见‘相机跟随模型侧视图’,在此局部坐标系中,点T成为坐标系原点


        鼠标的运动可以分为三种:左右移动,上下移动和滚轮滚动,这里分别叫做delta X, delta Y,delta Z,简称 dx, dy, dz,dx、dy的符号跟屏幕坐标系定义有关,这些参数可以从系统处获取,这里不讲。

鼠标左右移动的时候,dx不为0,向左dx < 0,向右dx > 0

鼠标上下移动的时候,dy不为0,向上dy < 0,向下dy > 0

鼠标滚动轮动的时候,dz不为0


        下面根据实现代码逐步分析,代码是用python写的,熟悉c/c++的朋友很容易看懂,代码复制下来并不能直接运行,因为依赖其它模块,要用到自己游戏中需要做些修改

先定义一个类CMouseInputEvent,上层逻辑将镜头用到的相关输入通过该类传递给镜头控制类:

KEY_PAD = 0x01

KEY_TRG = 0x02

KEY_RLS = 0x04

#KEY_PAD表示按键当前是按下状态,KEY_TRG表示按键当前有按下动作,KEY_RLS表示按键当前有松开动作

#下面的lbutton、rbutton、mbutton都是KEY_PAD/KEY_TRG/KEY_RLS按位或的组合


class   CMouseInputEvent:
    def __init__(self):     #构造函数
        self.lbutton = 0    #left   button
        self.rbutton = 0    #right  button
        self.mbutton = 0    #midden button
        self.mousedx = 0
        self.mousedy = 0
        self.mousedz = 0
    
    def setMouse(self, lbt, rbt, mbt, dx, dy, dz):
        self.lbutton = lbt
        self.rbutton = rbt
        self.mbutton = mbt
        self.mousedx = dx
        self.mousedy = dy
        self.mousedz = dz


class   CFollowCameraAI:
    def __init__(self, cmr, scn, target, ofst, lkatofst):
        self.UnitY = math3d.vector(0, 1, 0)  #向上的Y轴
        self.input    = CMouseInputEvent()
        self._camera = cmr                       #场景camera引用
        self._scene = scn                          #场景引用
        #跟随目标
        self.followTarget = target              #跟随目标引用
        self.cameraOffset = ofst               #camera位置相对跟随目标的偏移,向量T->P,局部坐标系参数
        self.lookatOffset = lkatofst            #camera lookAt的点相对跟随目标的偏移,向量T->A,局部坐标系参数
        #控制参数
        self.minOffsetLen = 5.0                #最小跟随距离,向量P->A的最短距离
        self.maxOffsetLen = 168.0           #最大跟随距离,向量P->A的最长距离
        self.limitTgn = math.tan(1.4)        #最大俯视/仰视角度(1.4弧度大约为80°)
        #速度控制
        self.x_scroll_speed = 0.008         #X,左右方向旋转速度,控制向量P->A绕目标Y轴旋转
        self.y_scroll_speed = -0.0022     #Y,上下方向旋转速度,控制向量P->A绕H平面旋转
        self.z_scroll_speed = 0.05           #Z,前后方向滚动速度,控制向量P->A长短
        self.rotback_angular_speed = 0.1     #自动回转速度,控制同x_scroll_speed
        self.z_backaway_speed = 40.0         #轴向位置插值速度,控制同z_scroll_speed
        
        #碰撞检测
        self.radius = 0.8                            #camera采用球形碰撞,这是碰撞球半径
        self.collided_shorten = False      #某内部状态标志位
        u = math3d.vector(1, 1, 1) * self.radius
        self._col_shape = collision.col_object(collision.SPHERE, u)    #构建球形碰撞体
    
    
    def Update(self, dTime, bTargetMoving, bRotBackEnabled):
        #dTime,帧时间,单位秒
        #bTargetMoving, bool, 目标是否在移动
        #bRotBackEnabled, bool, 是否允许camera横向转回到跟随目标背后
        
        rot_back_request = bTargetMoving                      #跟随目标移动的时候才转到背后,停下的时候不会转
        LookAt = self.lookatOffset - self.cameraOffset    #向量P->A
        
        #远近控制
        if self.input.mousedz != 0:
            curLength = abs(LookAt)          #向量当前长度
            LookAt.normalize()
            #根据滚轮位移计算需要缩放向量P->A多少距离,并限定其范围
            curLength -= self.input.mousedz * self.z_scroll_speed
            if curLength > self.maxOffsetLen:
                curLength = self.maxOffsetLen
            elif curLength 
   
   
    
    A绕目标Y旋转一定角度
                #横向旋转矩阵
                upRot = math3d.matrix.make_rotation(self.UnitY, self.input.mousedx * self.x_scroll_speed)
                LookAt *= upRot              #旋转向量P->A,横向
        
        #上下控制
        if self.input.mousedy != 0:
            #左中右三键都可以控制上下方向
            if ( ((self.input.lbutton & KEY_PAD) and (not (self.input.lbutton & KEY_TRG)))
                or ( (self.input.rbutton & KEY_PAD) and (not (self.input.rbutton & KEY_TRG)))
                or ( (self.input.mbutton & KEY_PAD) and (not (self.input.mbutton & KEY_TRG))) ):
                
                #计算纵向旋转轴,该轴与目标Y和Z垂直;相当于目标将其Z方向旋转到向量P->A的水平分量方向
                right = LookAt.cross(self.UnitY)
                right.normalize()
                #纵向旋转矩阵
                rightRot = math3d.matrix.make_rotation(right, self.input.mousedy * self.y_scroll_speed)
                
                temp = LookAt * rightRot     #旋转向量P->A,纵向
                
                #纵向旋转后需要做俯仰角限定
                #先计算当前水平分量对应的最大垂直分量
                maxY =  math.sqrt(temp.x * temp.x + temp.z * temp.z) * self.limitTgn
                if temp.y < -maxY:
                    temp.y = -maxY
                elif temp.y > maxY:
                    temp.y = maxY
                
                temp.normalize()
                LookAt = temp * abs(LookAt)  #最终方向向量 * 向量P->A原始长度
        
        
        #放开左键后,自动转到目标背面
        if bRotBackEnabled and rot_back_request and not (self.input.lbutton & KEY_PAD):
            dst = math3d.vector(0, 0, -1)    #要转到哪根轴(跟随目标的局部坐标系,正前方是(0,0,1),背后就是(0,0,-1))
            src = math3d.vector(LookAt.x, 0, LookAt.z)    #当前轴水平分量
            src.normalize()                  #src归一化,方便计算,dst也是归一化向量
            t =  src.cross(dst)              #src转到dst,因此旋转轴是src叉乘dst,因为是两个水平向量叉乘,结果应该是垂直方向的向量
            cp = t.dot(self.UnitY)         #cp = t.y = sin(theta),src、dst、self.UnitY的混合积
            dp = src.dot(dst)                #计算src到dst的夹角,dp = cos(theta);当src与dst重合时,dp == 1或-1;当src与dst垂直时,dp == 0
            abcp = abs(cp)                  #因为是src叉乘dst,sin(theta)有正负之分;当src与dst重合时,cp == 0;当src与dst垂直时,cp == 1
            #非尾部且非夹角很小;当dp> 0时说明src与dst夹角小于90°,即src在主角背后;当dp<0时src在主角前方
            if  not ((abcp < 0.1) and (dp > 0)):
                r = self.rotback_angular_speed
                if abcp >= 0.1:     #非正向且非夹角很小
                    r *= cp / abcp
                    if abs(r) > abcp:
                        r = -cp          #旋转角度比实际夹角小
                yRotBack = math3d.matrix.make_rotation(self.UnitY, r)
                LookAt *= yRotBack
        
        #计算需求位置
        mat = self.followTarget.rotation_matrix               #跟随目标的方向矩阵
        mat.translation = self.followTarget.position         #跟随目标的位置,点T
        fwd = mat.mulvec3x3(LookAt)                              #fwd是LookAt的世界坐标,将最终变成camera的方向向量
        self.cameraOffset = self.lookatOffset - LookAt   #self.cameraOffset的局部坐标(局部坐标-局部向量)
        ofst = self.cameraOffset * mat                             #ofst是self.cameraOffset的世界坐标,点P
        
        #碰撞检测,需要在世界坐标系里执行
        start = self.lookatOffset * mat                 #start是self.lookatOffset的世界坐标,点A
        delta = ofst - start                                    #ofst是self.cameraOffset的世界坐标,点P
        #由近及远插值
        if self.collided_shorten:                           #如果相机在自己z轴向遇到过碰撞,即向量P->A当前长度可能小于预定长度,则缓慢插值到预定长度
            curOfstLen = abs(self._camera.position - start)
            reqOfstLen = abs(delta)
            if curOfstLen < reqOfstLen:
                curOfstLen += self.z_backaway_speed * dTime * ((bTargetMoving and 2.0) or 1.0)
                if curOfstLen > reqOfstLen:
                    curOfstLen = reqOfstLen
                delta.normalize()
                delta *= curOfstLen
                ofst = start + delta
            else:
                self.collided_shorten = False
        
        hit, hit_point, normal, tt, color, objs = self._scene.col.sweep_test(self._col_shape, start, start + delta)
        if hit:
            self.collided_shorten = True                    #上次曾碰到过,记个标志位
            ofst = hit_point + normal * self.radius     #沿碰撞表面法线外移radius,减少camera穿面情况
        
        #设定最终方位
        self._camera.set_placement(ofst, fwd, mat.up)   #camera pos, camera forward vector, camera up vector
   
   

有不明白的地方可以交流,也欢迎更好的建议。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值