【Unity3D Debug】如何在不改变物体自身Transform的情况下,令其绕特定物体进行旋转(含方法可行性证明)
- 1. 问题引入
- 2. 问题的解决方案与可行性证明
- 2.1 命题 I:世界坐标系下,若物体 A A A是物体 G G G的祖先,令 A A A绕特定点 O O O旋转一定角度 θ \theta θ,则 G G G同样会绕点 O O O旋转相同的角度 θ \theta θ。
- 2.2 命题 II:当FPS相机位于点 O O O处时,根物体 A A A中的所有子物体在FPS相机屏幕中的显示不会因为转动而改变。
- 3. 命题在U3D中的实际应用结果演示
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轴为旋转轴进行角度为的旋转。我们只能更改A的Transform来实现它,那该如何旋转A呢?
2. 问题的解决方案与可行性证明
既然只能对A进行操作,那么让物体A以点O为旋转点,O的x轴为旋转轴,旋转角度会如何呢?经过笔者实践,发现这样恰好能达到预期,即让点O处的FPS相机以及点G处的FPS手臂以点O为中心进行绕x轴的旋转。
以下命题的证明虽然是基于二维坐标点(右视图)的,但命题对于三维坐标系、沿任意轴向旋转同样适用。
2.1 命题 I:世界坐标系下,若物体是物体的祖先,令绕特定点旋转一定角度,则同样会绕点旋转相同的角度。
该命题的证明比较简单,主要用到了初中所学的全等三角形的判别与性质。
证明:
1°:不妨首先考虑物体是物体的父亲,如下图所示。以下证明过程中将"绕物体的x轴(也即transform.right
)旋转"简述为"绕旋转"。
绕旋转的过程相当于将线段旋转至线段,有,两线段的夹角。
由于是的父亲,故与的相对位置不变,即,。
由以上条件可知与全等(SAS),故有:
又为和的公共角,故有
到此说明线段由线段绕点旋转而得,而这个旋转是通过点绕点旋转同样的量间接实现的。
这个命题的意义在于,当我们想让某一物体绕特定点和特定轴旋转一定角度时,我们可以借助它的某个祖先,进行相同参数的旋转来实现这个目标。
2°:以上是基于"是的父亲"的假定下证明的。下面简单说明:假定改为"是的祖先"时,命题同样成立。
假设从到的层级路径上还存在中间点,这些中间点均为的祖先,层级路径表示为,其中表示是的父亲。
由之前的证明,当是的父亲时,绕旋转角度,同样会绕旋转角度,由于又是的父亲,此时的转动导致绕转动角度,由此可知也会绕旋转角度,如此传递下去,最终可知绕转动角度,其子孙也会绕旋转角度,说明"是的祖先"时,命题也成立。
至此证毕.
2.2 命题 II:当FPS相机位于点处时,根物体中的所有子物体在FPS相机屏幕中的显示不会因为转动而改变。
命题I解决了怎样转动的问题,即在目标物体外套一个空的父物体来实现绕特定轴旋转。只有这点还不够,比如FPS视角下的抬头/低头,我们还得保证在绕x轴旋转过程中,FPS手臂在屏幕中的显示是不变的,即确保FPS手臂与相机之间相对静止。
结合命题I,这里代入至具体问题中,即FPS角色抬头低头,物体均绕x轴旋转(x轴向由屏幕内指向外,即右视平面的法线方向),可知FPS手臂(位于点)和FPS相机(位于点)保持相对静止,当且仅当两点在旋转前后距离不变,且两点引出的前向向量(物体局部坐标系的+z轴向)平行。距离相等不用多说(),"两个前向向量平行"通过下图图示也可轻松得证,这里就不赘述了。
3. 命题在U3D中的实际应用结果演示
以上两个命题花了一定篇幅去证明,那么如何将它们用于实践中呢?这里以FPS手臂的抬头/低头为例,先展示一下Hierarchy面板中物体的层级,如图所示。
上图中只需要关注用红线(框)标记的部分,对它们的解释如下:
GameObject
- Klee_Rig_DEF:FPS手臂的骨架,待旋转物体之一。不过由于子物体的Transform由Animator控制,运行时不可修改,故不能直接旋转它;FPS相机放在了该物体下的某一骨骼中。
- RotTarget:旋转的Pivot,即命题中的点处的物体,其余物体都以它为旋转中心,以它的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。
经过这番调整,运行结果就符合我们的需求了,最终结果如下动图所示。
笔者不擅长证明,本文中难免有不妥之处,恳请各位大佬们指正谬误,也欢迎大佬们留言分享自己的见解~