游戏程序设计中有趣的绕轴旋转

请添加图片描述

前言

在游戏设计中,绕轴旋转综合了位移与旋转两种空间变化,通过位移变换得到旋转的效果,是一个非常有趣的事情,整个过程有着很多基本的高中几何数学知识来帮助我们做出变换推导

不过如果单纯的推导一个旋转的数学公式,对于游戏开发来说就太无聊了。就我个人来说,对于理论知识上的学习乐趣在于理解后灵活的去做工程上的使用,就像生活大爆炸中霍华德与谢尔顿,理论物理学谢尔顿的弦理论研究固然很酷,但不总是有趣,反而工程师霍华德工作产出带来更多的快乐

所以在对于旋转的理论知识做一些简单的推导后,会对推导知识进行一个小应用,实现游戏中绕轴旋转的应用,即通常动作游戏或RPG游戏中存在的视角锁定状态下角色左右移动功能,如下图所示

分析旋转中的坐标计算公式

1、二维坐标系绕点旋转

从数学的角度出发,在二维坐标系中,某一点围绕另一点发生一定角度的旋转,点位坐标是固定且可以计算的。通过一个简单的图解表述推导出计算公式:
在这里插入图片描述

在上图的二维坐标中,假设某一点A ( x 1 , y 1 ) (x_1,y_1) x1,y1)围绕某一固定点B ( x 2 , y 2 ) (x_2,y_2) x2y2旋转一定角度θ,得到新的点位坐标B ( x , y ) (x,y) xy,同时,为了方便理解做出通过C点的水平辅助线,并分别将A点与B点映射到该辅助线上,并申明交点分别为D点与E点

为了便于推导,声明ACBC的长度为r,BCE之间的夹角为α,然后就可以根据以自的条件建立等式(由于实在找不到其他纸张,只能用抽纸来凑合)如下:

在这里插入图片描述

上面的等式中,角度α是未知的,但是在对等式分解后,恰巧可以消除掉α得到关于 ( x , y ) (x,y) xy的求解公式,如下:
x = ( x 1 − x 2 ) c o s θ − ( y 1 − y 2 ) s i n θ + x 2 x = (x_1 - x_2)cosθ - (y_1 - y_2)sinθ + x_2 x=(x1x2)cosθ(y1y2)sinθ+x2 x = ( x 1 − x 2 ) c o s θ − ( y 1 − y 2 ) s i n θ + x 2 x = (x_1 - x_2)cosθ - (y1 - y2)sinθ + x_2 x=(x1x2)cosθ(y1y2)sinθ+x2

通过程序语言来表达:

 Vector3 GetRotatePoint(Vector3 startPos,Vector3 centerPos,float angle)
    {
    
    
        float randian = angle * Mathf.PI / 180;
        float tragetX = (startPos.x - centerPos.x) * Mathf.Cos(randian) - (startPos.y - centerPos.y) * Mathf.Sin(randian) +centerPos.x ;
        float targetY = (startPos.y - centerPos.y) * Mathf.Cos(randian) +(startPos.x - centerPos.x) * Mathf.Sin(randian) + centerPos.y;       
        return new Vector3(tragetX, targetY, centerPos.z);
    }

2、三维坐标系的绕轴旋转

基于二维的旋转计算公式去理解三维状态下的旋转,需要多使用一个旋转轴向量参数来确立物体在三维空间内的旋转平面,即以旋转轴为平面法向量来参与三维坐标系的旋转坐标求解计算

而关于具体的计算方法,Unity中提供了RotateAround()来完成绕轴的旋转,但是因为引擎为闭源模式,无法通过源代码来理解操作过程。巧的是目前在向UE转换,而UE又恰好开放源代码。即可以通过UE来获取实现方法。不过考虑到目前Unity的开发者数量更多,还是将该方法迁移到Unity中来解释,对其做一些修改后如下:

    Vector3 RotateAngleAxis(Vector3 centerPos, Vector3 aroundPos, float AngleDeg, Vector3 Axis) 
    {
    
    
        Vector3 radius = aroundPos - centerPos;
        float S = Mathf.Sin(AngleDeg * Mathf.PI / 180);
        float C = Mathf.Cos(AngleDeg * Mathf.PI / 180);
        
        float XX = Axis.x * Axis.x;
        float YY = Axis.y * Axis.y;
        float ZZ = Axis.z * Axis.z;

        float XY = Axis.x * Axis.y;
        float YZ = Axis.y * Axis.z;
        float ZX = Axis.z * Axis.x;

        float XS = Axis.x * S;
        float YS = Axis.y * S;
        float ZS = Axis.z * S;

        float OMC = 1f - C;

        return new Vector3(
            (OMC * XX + C) * radius.x + (OMC * XY - ZS) * radius.y + (OMC * ZX + YS) * radius.z + centerPos.x,
            (OMC * XY + ZS) * radius.x + (OMC * YY + C) * radius.y + (OMC * YZ - XS) * radius.z + centerPos.y,
            (OMC * ZX - YS) * radius.x + (OMC * YZ + XS) * radius.y + (OMC * ZZ + C) * radius.z + centerPos.z
            );
    }

为了提升计算效率,代码中轴向量计算做了独特的设计,很难从中反向推导计算过程,不过大概可以看出的是,计算时会对对象的坐标做X、Y
与Z轴的拆解,然后分别与旋转轴做一定的投影计算,不过这些就数学知识就稍微超越高中数学的范畴了,同时也在我都能力范围之外,所以就掠过这部分演示推导,不过无伤大雅,游戏开发者不是数学家,需要数学但并不完全依托数学,我们主要的目的还是在后面对其做一个应用

Unity实现绕轴旋转小案例

结束枯燥的数学推导,终于来到了重点部分。如果稍微一些主机游戏的经验,会发现在一些游戏中,通常是一些RPG类型的游戏中,比如刺客信条系列游戏中,存在视角锁定敌人的机制。通过锁定敌人,可以使玩家忽略视角变化从而专注于战斗方面的控制操作

在该机制下,角色的移动会有一些微妙的变化,通常来说的左右平移会在该情况下转换为绕点做圆周运动,如下图演示中关于刺客信条中的锁定旋转:

请添加图片描述
拆分变换过程,绕轴旋转是通过角色的圆周移动与旋转组合而成。细分到每一帧的逻辑中即角色Forward面向中心点,同时垂直与中心点的连线左右移动,即移动的方向始终为圆周的切线,在Unity中通过程序语言表示为:

        playerTran.LookAt(centerTran);
        float inputAxisX = Input.GetAxis("Horizontal");
        float inputAxisY = Input.GetAxis("Vertical");
        playerTran.Translate(inputX*Time.deltaTime*rotateSpeed, 0, inputY * Time.deltaTime * moveSpeed);

上面的代码可以得到一个不是那么严谨的绕轴旋转效果,之所以说不太严谨,是因为在每一帧控制角色左右位移时会额外的增加对象的旋转半径,经过一段时间累计后,会产生可感知的半径计算误差。很好理解,只需要稍微做一下思考,在角色每次沿着一个圆形的切线移动时,无论移动的距离多少,都会脱离原本的圆周路径一段距离,如下图所示:

在这里插入图片描述

假设当前帧开始前角色位于A点以OA为半径绕轴旋转,每帧移动的距离为AB,那么在该帧逻辑结束后到下一帧运行开始,角色会在B点以OB为半径来绕轴旋转,这样就使得整个绕轴旋转的半径越来越大,下面做一个比较极端的演示:
请添加图片描述

通常来讲,在物体移动速度较慢时,误差积累并不十分明显,但是也没有小到足够被忽略的范围。当然这样的表述不够有说服力,所以这里通过万能的高中的数学知识来量化一下误差,可以假设下面的场景,在一个60帧游戏场景中,当角色围绕半径为1米的中心点开始做绕轴旋转,并为其设定一个常用的移动速度

为了使数据具有代表性,可以以人类现实移动速度做参考。通过百度可以知道人的正常移动速度为5到7千米每小时,取中间值换算过来大约是1.67m/s,我们已经知道游戏帧数为60帧数每秒,即每帧角色的移动距离为0.028m

有了上面的数据,我们就可以计算出每一帧的半径误差,通过之前的图可以知道,每一帧产生的误差的计算方式为OB的长度减去OA的长度,而OB的长度可以由上一帧得到的OA长度与通过常识得到的AB求得,通过程序代码表示为:

   double GetRadius(double startRadius, double moveLenght,int frameNum)
    {
    
    
        if (frameNum == 0) return startRadius;
        double powNum = GetRadius(startRadius, moveLenght, frameNum - 1);
        return Math.Sqrt(Math.Pow(powNum,2) + Math.Pow(moveLenght, 2));
    }

该代码只是逻辑演示,由于存在大量的双精度浮点数的计算,不要尝试直接实机演示(大概率卡死,前车之鉴),可以将逻辑拆分到Update中,每帧计算一次,来分销性能压力。通过运算程序,得到如下结果。可以看出,系统运行到五秒后角色的半径误差达到了0.1米,在严格的动作游戏中,足以对玩家在攻击距离的把握上产生较大的影响
在这里插入图片描述

使用绕轴公式预计算圆周点位

为了避免这一问题,我们可以通过刚刚提到的绕轴旋转公式来做对象的移动控制,虽然计算过程比较复杂,但是得到的结果精确度非常高,即使是浮点数计算的误差影响下

在我们得到上面的绕轴旋转公式代码后,整个过程就简单了许多,只需要准备好对应的参数传入得到对应帧的目标位置即可但是对于当前的旋转项目中,有一点需要注意的是,通常角色的移动速度是固定的。如果在该函数中直接传入一固定的角度,会导致对象的移动速度与圆的半径挂钩,很好理解,如果每一帧旋转相同的角度,旋转半径越大,对应的弧的长度也就更长,意为着对象的移动速度更快

由于上面的问题,需要提前做一些转换,即将移动弧长转换为弧度,通过高中知识可以知道弧长与弧度的转换公式为:

弧 长 = 弧 度 ∗ 半 径 弧长=弧度*半径 =

不过有意思的是,我们之前使用的半径并不不一定为对象旋转平面对应的半径,还需要再次使用高中数学知识计算处该半径:
在这里插入图片描述

如上图所示,物体B围绕A以OA为轴做圆周旋转,实际物体B的旋转平面是以OA为法线且通过点B的平面。根据已知条件中的OA方向的单位向量以及点A与点B的坐标信息,利用向量平面投影知识得到OB的长度,代码结构为:

    Vector3 ProjectOnPlane(Vector3 target, Vector3 noraml)
    {
    
    
        return target - noraml * Vector3.Dot(target, noraml) / Vector3.Dot(noraml, noraml);
    }

上面的求解过程简单来说就是通过点乘得到targetnormal上的投影距离,然后与normal的长做除得到倍数,并对normal等比例扩展构造直角三角形,并做向量减法得到投影向量

在得到旋转的实际半径后,重写之前的绕轴旋转方法RotateAngleAxis,使其可以通过对象单位运动弧长来计算出对应的

    Vector3 RotateAngleAxis(Vector3 centerPos, Vector3 aroundPos, float moveSpeed, Vector3 Axis)
    {
    
    
        Vector3 radius = aroundPos - centerPos;
        Vector3 changeRadius = ProjectOnPlane(radius, Axis);
        float S = Mathf.Sin(moveSpeed /changeRadius.magnitude);
        float C = Mathf.Cos(moveSpeed /changeRadius.magnitude);
        ...
     }

可以看到,利用源码可以灵活的修改一些代码来适应自己的需求,即使在性能表现上帮助不大,相比于基于Unity给的方法再封装会更酷一些,运行上面的程序,如图:请添加图片描述

后续

由于最近在上手UE,没有什么特别有意思的事情可以写出文章分享。但是我后面会加油努力的,因为自己在学UE时感觉到相关的文章太少了,所以希望可以贡献出自己的一份力量,来丰富UE的社区环境!

猜你喜欢

转载自blog.csdn.net/xinzhilinger/article/details/123459840
今日推荐