【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 AA是物体G GG的祖先,令A AA绕特定点O OO旋转一定角度θ \thetaθ,则G GG同样会绕点O OO旋转相同的角度θ \thetaθ。
该命题的证明比较简单,主要用到了初中所学的全等三角形的判别与性质。
证明:
1°:不妨首先考虑物体A AA是物体G GG的父亲,如下图所示。以下证明过程中将"绕物体O OO的x轴(也即transform.right)旋转"简述为"绕O OO旋转"。

A AA绕O OO旋转的过程相当于将线段O A OAOA旋转至线段O A ′ OA^{'}OA′,有∣ O A ∣ = ∣ O A ′ ∣ \vert OA\vert=\vert OA^{'}\vert∣OA∣=∣OA′∣,两线段的夹角∠ A O A ′ = θ \angle AOA^{'}=\theta∠AOA′=θ。
由于A AA是G GG的父亲,故A AA与G GG的相对位置不变,即∣ A G ∣ = ∣ A ′ G ′ ∣ \vert AG\vert=\vert A^{'}G^{'}\vert∣AG∣=∣A′G′∣,∠ O A G = ∠ O A ′ G ′ \angle OAG=\angle OA^{'}G^{'}∠OAG=∠OA′G′。
由以上条件可知△ O A G \triangle OAG△OAG与△ O A ′ G ′ \triangle OA^{'}G^{'}△OA′G′全等(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=∠A′OG′
又∠ A ′ O G \angle A^{'}OG∠A′OG为θ \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+∠A′OG=∠A′OG′+∠A′OG=β
到此说明线段O G ′ OG'OG′由线段O G OGOG绕点O OO旋转β = θ \beta=\thetaβ=θ而得,而这个旋转是通过点A AA绕点O OO旋转同样的量间接实现的。
这个命题的意义在于,当我们想让某一物体绕特定点和特定轴旋转一定角度时,我们可以借助它的某个祖先,进行相同参数的旋转来实现这个目标。
2°:以上是基于"A AA是G GG的父亲"的假定下证明的。下面简单说明:假定改为"A AA是G GG的祖先"时,命题同样成立。
假设从A AA到G GG的层级路径上还存在中间点E 1 , E 2 , . . . , E n E_1,E_2,...,E_nE1,E2,...,En,这些中间点均为G GG的祖先,层级路径表示为A → E 1 → . . . → E n → G A\rightarrow E_1 \rightarrow ... \rightarrow E_n \rightarrow GA→E1→...→En→G,其中A → B A \rightarrow BA→B表示A AA是B BB的父亲。
由之前的证明,当A AA是E 1 E_1E1的父亲时,A AA绕O OO旋转角度θ \thetaθ,E 1 E_1E1同样会绕O OO旋转角度θ \thetaθ,由于E 1 E_1E1又是E 2 E_2E2的父亲,此时A AA的转动导致E 1 E_1E1绕O OO转动角度θ \thetaθ,由此可知E 2 E_2E2也会绕O OO旋转角度θ \thetaθ,如此传递下去,最终可知A AA绕O OO转动角度θ \thetaθ,其子孙G GG也会绕O OO旋转角度θ \thetaθ,说明"A AA是G GG的祖先"时,命题也成立。
至此证毕.
2.2 命题 II:当FPS相机位于点O OO处时,根物体A AA中的所有子物体在FPS相机屏幕中的显示不会因为转动而改变。
命题I解决了怎样转动的问题,即在目标物体外套一个空的父物体来实现绕特定轴旋转。只有这点还不够,比如FPS视角下的抬头/低头,我们还得保证在绕x轴旋转过程中,FPS手臂在屏幕中的显示是不变的,即确保FPS手臂与相机之间相对静止。
结合命题I,这里代入至具体问题中,即FPS角色抬头低头,物体均绕x轴旋转(x轴向由屏幕内指向外,即右视平面的法线方向),可知FPS手臂(位于点G GG)和FPS相机(位于点O OO)保持相对静止,当且仅当两点在旋转前后距离不变,且两点引出的前向向量(物体局部坐标系的+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 OO处的物体,其余物体都以它为旋转中心,以它的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。
经过这番调整,运行结果就符合我们的需求了,最终结果如下动图所示。
笔者不擅长证明,本文中难免有不妥之处,恳请各位大佬们指正谬误,也欢迎大佬们留言分享自己的见解~