【Unity3D Debug】如何在不改变物体自身Transform的情况下,令其绕特定物体进行旋转(含方法可行性证明)

1. 问题引入

这个问题或许有人会觉得很奇怪:为什么要绕这么大一个弯子,直接改变该物体的Transform,令其绕特定物体旋转不就好了吗?

大多数情况下,确实不用绕弯子。不过有时候物体自身的Transform是无法改变的:比如带骨骼的角色由Animator Controller控制时,骨骼动画所控制的骨骼物体在运行期间无法更改其Transform组件,但却需要以其中心点为旋转点、X轴(水平轴)为旋转轴来旋转该骨骼物体,这时候在该骨骼本身上作文章就行不通了。

具体来说,笔者最近在做FPS手臂(如下图)绕X轴的抬头/低头(非骨骼动画控制),但与此同时FPS手臂有一系列骨骼动画,由Animator Controller来控制动画播放,也就是说运行时直接旋转FPS手臂的根骨骼物体或其中的某一个孩子物体是行不通的;其次,由下图可见,FPS手臂的Transform位置坐标(图中点A)并不是我们想要的旋转中心,真正的旋转中心应该在FPS摄像机的Transform位置(图中点O),值得一提的是,FPS手臂的位置(图中点G)与FPS相机的位置是不一致的,为了提供较好的第一人称视角,FPS相机通常在FPS手臂的后上方位置。
在这里插入图片描述

注:上图中点O和点G都是点A的孩子,点G与点A的关系随意:可以是互为兄弟,也可以是父子关系。

那么现在问题来了,通常点O和点G处的物体属于骨架中的一部分,在Animator Controller接管下是无法修改Transform的,也就是不能直接旋转它们。点A作为FPS手臂的根节点,旋转物体A,其所有子物体也会跟着旋转,我们现在的目标是:让A的所有子物体以O为旋转点、O的x轴为旋转轴进行角度为 θ \theta θ的旋转。我们只能更改A的Transform来实现它,那该如何旋转A呢?

2. 问题的解决方案与可行性证明

既然只能对A进行操作,那么让物体A以点O为旋转点,O的x轴为旋转轴,旋转角度 θ \theta θ会如何呢?经过笔者实践,发现这样恰好能达到预期,即让点O处的FPS相机以及点G处的FPS手臂以点O为中心进行绕x轴的旋转。

以下命题的证明虽然是基于二维坐标点(右视图)的,但命题对于三维坐标系、沿任意轴向旋转同样适用。

2.1 命题 I:世界坐标系下,若物体 A A A是物体 G G G的祖先,令 A A A绕特定点 O O O旋转一定角度 θ \theta θ,则 G G G同样会绕点 O O O旋转相同的角度 θ \theta θ

该命题的证明比较简单,主要用到了初中所学的全等三角形的判别与性质。

证明:
1°:不妨首先考虑物体 A A A是物体 G G G的父亲,如下图所示。以下证明过程中将"绕物体 O O O的x轴(也即transform.right)旋转"简述为"绕 O O O旋转"。

在这里插入图片描述
A A A O O O旋转的过程相当于将线段 O A OA OA旋转至线段 O A ′ OA^{'} OA,有 ∣ O A ∣ = ∣ O A ′ ∣ \vert OA\vert=\vert OA^{'}\vert OA=OA,两线段的夹角 ∠ A O A ′ = θ \angle AOA^{'}=\theta AOA=θ

由于 A A A G G G的父亲,故 A A A G G G的相对位置不变,即 ∣ A G ∣ = ∣ A ′ G ′ ∣ \vert AG\vert=\vert A^{'}G^{'}\vert AG=AG ∠ O A G = ∠ O A ′ G ′ \angle OAG=\angle OA^{'}G^{'} OAG=OAG

由以上条件可知 △ O A G \triangle OAG OAG △ O A ′ G ′ \triangle OA^{'}G^{'} OAG全等(SAS),故有:
∣ O G ∣ = ∣ O G ′ ∣ , ∠ A O G = ∠ A ′ O G ′ \vert OG\vert=\vert OG^{'}\vert,\quad \angle AOG=\angle A^{'}OG^{'} OG=OG,AOG=AOG
∠ A ′ O G \angle A^{'}OG AOG θ \theta θ β \beta β的公共角,故有
θ = ∠ A O G + ∠ A ′ O G = ∠ A ′ O G ′ + ∠ A ′ O G = β \theta=\angle AOG+\angle A^{'}OG=\angle A^{'}OG^{'}+\angle A^{'}OG=\beta θ=AOG+AOG=AOG+AOG=β

到此说明线段 O G ′ OG' OG由线段 O G OG OG绕点 O O O旋转 β = θ \beta=\theta β=θ而得,而这个旋转是通过点 A A A绕点 O O O旋转同样的量间接实现的。

这个命题的意义在于,当我们想让某一物体绕特定点和特定轴旋转一定角度时,我们可以借助它的某个祖先,进行相同参数的旋转来实现这个目标

2°:以上是基于" A A A G G G的父亲"的假定下证明的。下面简单说明:假定改为" A A A G G G的祖先"时,命题同样成立。

假设从 A A A G G G的层级路径上还存在中间点 E 1 , E 2 , . . . , E n E_1,E_2,...,E_n E1E2...,En,这些中间点均为 G G G的祖先,层级路径表示为 A → E 1 → . . . → E n → G A\rightarrow E_1 \rightarrow ... \rightarrow E_n \rightarrow G AE1...EnG,其中 A → B A \rightarrow B AB表示 A A A B B B的父亲。

由之前的证明,当 A A A E 1 E_1 E1的父亲时, A A A O O O旋转角度 θ \theta θ E 1 E_1 E1同样会绕 O O O旋转角度 θ \theta θ,由于 E 1 E_1 E1又是 E 2 E_2 E2的父亲,此时 A A A的转动导致 E 1 E_1 E1 O O O转动角度 θ \theta θ,由此可知 E 2 E_2 E2也会绕 O O O旋转角度 θ \theta θ,如此传递下去,最终可知 A A A O O O转动角度 θ \theta θ,其子孙 G G G也会绕 O O O旋转角度 θ \theta θ,说明" A A A G G G的祖先"时,命题也成立。

至此证毕.

2.2 命题 II:当FPS相机位于点 O O O处时,根物体 A A A中的所有子物体在FPS相机屏幕中的显示不会因为转动而改变。

命题I解决了怎样转动的问题,即在目标物体外套一个空的父物体来实现绕特定轴旋转。只有这点还不够,比如FPS视角下的抬头/低头,我们还得保证在绕x轴旋转过程中,FPS手臂在屏幕中的显示是不变的,即确保FPS手臂与相机之间相对静止

结合命题I,这里代入至具体问题中,即FPS角色抬头低头,物体均绕x轴旋转(x轴向由屏幕内指向外,即右视平面的法线方向),可知FPS手臂(位于点 G G G)和FPS相机(位于点 O O O)保持相对静止,当且仅当两点在旋转前后距离不变,且两点引出的前向向量(物体局部坐标系的+z轴向)平行。距离相等不用多说( ∣ O G ∣ = ∣ O G ′ ∣ \vert OG \vert = \vert OG^{'}\vert OG=OG),"两个前向向量平行"通过下图图示也可轻松得证,这里就不赘述了。
在这里插入图片描述

3. 命题在U3D中的实际应用结果演示

以上两个命题花了一定篇幅去证明,那么如何将它们用于实践中呢?这里以FPS手臂的抬头/低头为例,先展示一下Hierarchy面板中物体的层级,如图所示。
在这里插入图片描述
上图中只需要关注用红线(框)标记的部分,对它们的解释如下:
GameObject

  • Klee_Rig_DEF:FPS手臂的骨架,待旋转物体之一。不过由于子物体的Transform由Animator控制,运行时不可修改,故不能直接旋转它;FPS相机放在了该物体下的某一骨骼中。

  • RotTarget:旋转的Pivot,即命题中的点 O O O处的物体,其余物体都以它为旋转中心,以它的x轴为旋转轴,使用时确保RotTarget的位置&旋转与FPS相机的位置&旋转一致

  • UMP-45_WithScope:武器预制体,包含对应的模型与Animator组件,是待旋转物体之一。

Component

  • Local Player Character(Script):包含绕x轴旋转的相关三个字段,即Rotate Target(旋转参考物的Transform)、Trans_cur Weapon(待旋转武器的Transform)和Rotate FPS Arm(待旋转FPS手臂的Transform)。

绕Rotate Target的x轴旋转的脚本逻辑如下:

    public void UpdateRotation()
    {
    
    
		// ...
        // 处理绕X轴旋转,确保在范围内,rotX为正代表抬头,而抬头旋转值应减小,所以加负号
        float rotX = curRotXYInput.x;  // 鼠标沿Mouse Y方向的输入值 
        // 将绕X轴的旋转限制在[rotXDownLimit, rotXUpLimit]范围内
        float tempRot = xRotation;
        xRotation = Mathf.Clamp(xRotation - rotX * rotationXSensitivity * Time.deltaTime, rotXDownLimit, rotXUpLimit);
        // 令FPS手臂以及武器绕rotateTarget的X轴进行旋转(它们共同构成了角色的所有可见物体)
        rotateFPSArm.RotateAround(rotateTarget.position, rotateTarget.right, xRotation - tempRot);
        trans_curWeapon.RotateAround(rotateTarget.position, rotateTarget.right, xRotation - tempRot);
        // ... 
    }

如果直接按照上图的参数来运行,将会发现:武器随着相机正常转动了,但手臂一直没转,因为运行的时候一直在播放Idle骨骼动画,手臂骨架Klee_Rig_DEF由Animator全权控制,我们不能用脚本修改它的Transform,因此直接旋转它是没有用的,如下动图所示。
在这里插入图片描述
既然不能直接修改Klee_Rig_DEF的Transform,那么根据命题I,我们让其父物体Klee_1p(不受AnimatorController控制)绕参考物体RotTarget旋转不就好了?如果只谈论绕x轴的旋转,确实没错,但我们还有绕Y轴的旋转,以及XZ平面的移动,这些移动都是直接作用于Klee_1p的,因此我们需要在Klee_1p外面再套一个空物体(作为根物体),把Klee_1p上面挂载的组件(包括CharacterController、Animator以及各脚本等)转移至新建的空物体(命名为Player)上。现在我们通过一系列操作对上述层级进行调整,如下图所示:
在这里插入图片描述
上图中的要点说明:

  • 若用Transform.RotateAround函数绕Rotate Target旋转的物体有多个(比如这里有两个,分别为武器Trans_cur Weapon和FPS手臂Rotate FPS Arm),请确保它们不具备祖先-子孙关系,否则它们的单位时间旋转量会不一致。图中的待旋转物体之间互为兄弟,如果改为父子关系,比如将Weapons作为Klee_1p的子物体,那么根据命题I,对于Klee_1p的旋转会等价传递给Weapons,同时Weapons自身也有等量旋转,两者相叠加,呈现的情况就是:武器旋转比FPS手臂旋转快1倍 (关于是否为2倍,笔者也是凭感觉,暂未验证命题I是否有叠加性),如下动图所示。
    在这里插入图片描述

  • 这里RotTarget物体放置的位置比较灵活,原则上只需要确保它的X轴轴向以及相对位置不变即可,但保险起见还是作为根物体Player的孩子。

  • 注意Local Player Character脚本组件中的属性Rotate FPS Arm由Klee_Rig_DEF改为了Klee_1p。

经过这番调整,运行结果就符合我们的需求了,最终结果如下动图所示。
在这里插入图片描述

笔者不擅长证明,本文中难免有不妥之处,恳请各位大佬们指正谬误,也欢迎大佬们留言分享自己的见解~

猜你喜欢

转载自blog.csdn.net/weixin_42430021/article/details/123565822