Look At Function
蓝图节点【Look At Function】解释
- Current Transform
- 需要调整姿态的对象。UE5.3的实现中只使用了它的Location部分
- Target Position
- 由(TargetPos - CurrentPos)计算出一个ToTarget向量
- Look at Vector
- 是一根轴,这根轴带动整个对象调整姿态,直至这根轴与ToTarget对齐
- 注意这个是Local系的一根轴,或者说是待调整姿态的对象本体系的一根轴,以其本体系描述
- 比如:要让Cone锥尖朝向一个Box,则Look at Vector应设定为Cone的竖直Z轴,即常量(0,0,1)
- Use Up Vector
- 开关变量,决定是否启用下面的UpVector属性
- Up Vector
- 平面法线。上面计算出的ToTarget会投影到该平面,如果UpVector属性启用的话。即:旋转动作会被限定在该平面上进行。
- Clamp Cone in Degree
- 用于调整ToTarget向量。如果该属性>一个宏定义的Threshold值(UE5.3里是0.00001f),则会考虑调整ToTarget。
- 当上面的Look at Vector轴与ToTarget的夹角度数超过了Clamp Cone in Degree,则将ToTarget往Look at Vector靠拢,让夹角变小,直至满足要求。
- 当Target的运动轨迹一直使得夹角超标,则可以看到调整姿态的对象被其Look at Vector带着在一个圆锥面上运动。
- 用于调整ToTarget向量。如果该属性>一个宏定义的Threshold值(UE5.3里是0.00001f),则会考虑调整ToTarget。
从上面的描述可以看到,无论如何,该节点要达成的目标都是让Look at Vector轴与ToTarget对齐。UpVector与Clamp Cone in Degree都只是可能导致ToTarget进行调整。
至于为什么要求Look at Vector是固定的一根轴,其直觉理解就是本节点的每次计算都是从对象处于零旋转姿态的起点开始的。而Look at Vector轴也是在这个零旋转姿态下给出的轴向描述,比如:要让对象顶部去追踪一个目标的话,则Look at Vector应设定为(0,0,1),每次计算都是从零姿到Target的直接求解,即便是在一个持续追踪的运动中。如果你用GetActorUpVector()节点输出到Look at Vector,由于GetActorUpVector()的值可能一直在变化,导致Look At Function节点会认为你不同帧中想让对象零姿下的不同部位朝向目标,而Look At想要达成的效果通常是想让对象零姿下的某个固定部位一直朝向目标,因此最终导致的效果是对象不停的大幅度翻转闪烁。
旋转相关的一些思考
针对物体当前的姿态,以及从当前姿态变化到另一个姿态的变化量,我们没有用两种不同的描述形式来区分这两个概念。即,姿态与姿态的变化量虽然是两个不同的概念,但是用同一种形式描述的。也有把姿态叫做方位的。不管怎样,也可以找到一个角度,从这个角度出发可以说姿态与姿态的变化量是同一个事物。姿态的变化量是指从一个姿态到另一个姿态的变化量,而姿态也可以定义为从一个全局唯一初始姿态到当前的姿态的变化量。因此,这种理解下,它们就有了用相同的描述形式来进行描述的逻辑根据。
FTransform(Location(0,0,0), Rotation(0,0,0), Scale3D(1,1,1))就是这个初始姿态。通常叫Identity Transform,这里简称为ID、零姿态。注意:这里蕴含着在一个明确的参照系里定义一个姿态,并将该姿态的数值描述记为Identity Transform
当用一个FTransform描述一个物体当前的姿态时,应该把这个Transform理解为从零姿态到当前姿态的变化量。
当用一个FTransform描述一个物体当前的姿态时,应该把这个Transform理解为从零姿态到当前姿态的变化量。
如果要表达在世界其他点旋转,则要结合位移来表达。FTransform有3个分量,分别是Location、Rotation、Scale3D。对一个具体的FTransform来说,其Location与Rotation是互相独立的,比如FTransform(FVector(100,0,0), FRotator(0,0,90)),其描述的是物体从所处世界的零姿态开始旋转90度后,平移到该世界的(100,0,0)点,那么它看起来就像是在(100,0,0)处的物体原地旋转了90度。因此,这个物体的姿态,就不能只用旋转来表达,必须将位置与旋转结合起来表达。FTransform(FVector::ZeroVector, FRotator(0,0,90))表达的是在原点转了90度的物体。
案例1:如果物体要在当前姿态T0的基础上额外在原地进行一个世界旋转D(FRotator表达),则物体最终的姿态T1=FTransform(T0.Location(), T0.Rotation() * D)。想一想D为0时的特殊情形加深理解。上面也说了,Location与Rotation是互相独立的部分,因此旋转先全部执行到位,就是T0.Rotation()*D了,然后执行位移,由于这里变换只有原地旋转,因此位移保持不变,就是T0.Location()了。这里有一个很重要的问题:D是否就是T0.Invert() * T1? 答案为否。
FTransform的乘法(见TransformVectorized.h的Multiply函数),作用就是在姿态T0的基础上进行增量变换DeltaTransform,最后变成姿态T1。这个变换过程可以直观的理解:在保持物体处于T0姿态的前提下,给它添加一个父节点构成刚性连接,且该父节点处于零姿态(世界原点、无初始旋转),对该父节点应用DeltaTransform,由于刚性连接,该父节点会带动物体也进行完全相同的DeltaTransform变换。因此,很明显,物体最终的姿态就是T1=FTransform(NewLocation, NewRotation),其中:
- NewRotation=T0.Rotation() * DeltaTransform.Rotation()
- NewLocation=DeltaTransform.Rotation().Rotate(T0.Location()) + DeltaTransform.Location()
回到案例1最后的问题。案例1中T1的计算过程,明显不是FTransform的乘法实现,因此那个世界旋转D并不是T0.Invert() * T1。要知道T0*Delta=T1,这个Delta才是T0.Invert()*T1,而T1不是由T0*D实现的,因此Delta != D
案例2:在案例1中,如果该物体还有一个父节点,但整个刚性构造需要以该物体作为中心进行变换,求父节点变换后的姿态?
在案例2这个变换过程中,父节点反而变成该物体逻辑上的子节点被带动。由于是刚性连接,父节点与该物体具有完全相同的变化量:绕相同的轴进行了相同量的旋转,以及进行了相同的平移。如果想求出父节点在变换后的姿态,则先求出物体的变化量即可:DeltaTransform = T0.Invert() * T1。这里要注意:求出DeltaTransform后,不能用Parent->AddWorldTransform(DeltaTransform)来求出父节点变换后的姿态,因为AddWorldTransform执行的并不是FTransform的乘法。需要通过Parent->GetWorldTransform()*DeltaTransform来求得父节点最终的姿态。这是方法一。
那么能否通过Parent->AddWorldTransform(D)的方式求出父节点的姿态呢?也不行。因为仅靠D只能实现父节点原地旋转,并不是绕物体进行变换。需要增加位移信息,用NewRotation=Parent->WorldRotation()*D作为旋转信息,用D.Rotate(Parent->WorldLocation()-Object->WorldLocation())+Object->WorldLocation()求出父节点的位移信息,即可求出正确的姿态,而这个实现其实就是FTransform的乘法过程。
那么能否通过Parent->AddWorldTransform(D)的方式求出父节点的姿态呢?也不行。因为仅靠D只能实现父节点原地旋转,并不是绕物体进行变换。需要增加位移信息,用NewRotation=Parent->WorldRotation()*D作为旋转信息,用D.Rotate(Parent->WorldLocation()-Object->WorldLocation())+Object->WorldLocation()求出父节点的位移信息,即可求出正确的姿态,而这个实现其实就是FTransform的乘法过程。
以上思考均在UE蓝图里验证正确。