Transform--理解性剖析(四元数,轴角,欧拉角,向量的几何意义)

前言

看到草稿箱里第一篇想写的文章还是在19年,这篇Transform,有始有终
Unity开发的都知道transform这个组件,可以说是unity的核心数据了。记录数据位置,旋转,缩放。乍一听好像结束了。其实仔细想想还是有很多嚼头的。

Position

Unity中的位置都是相对位置,都是相对于父节点的位置,对于没有父节点的物体,就是相对于一个(0,0,0)点位置,我们称之为世界坐标的0点。我们经常使用的坐标一个是局部坐标,一个是世界坐标。其实世界坐标是一个特殊的局部坐标,我们为了有一个统一的度量衡,所以将他称之为世界坐标。
相对坐标的计算方式很简单,在一个统一的坐标系下直接做差,就是相对坐标。比如B相对于A的坐标为B1,B的世界坐标为B2,C相对于A的坐标为C1,世界坐标为C2,那么C相对于B的坐标为C1-B1或者C2-B2。记住一定要是统一的坐标系下。

Rotation

旋转是可能是这里最难理解的一个概念。我们先思考一个问题:1.如何描述一个物体的方位?

欧拉角

到底啥是欧拉角?
在位置中,我们使用了笛卡尔坐标系,有xyz三个方向轴。我们同样使用笛卡尔坐标系能不能描述一个物体的方位呢?
好,现在我们把一个物体规定他的前z1,上y1,右x1。然后再世界坐标系中,笛卡尔坐标系的y轴对应上,z对应前,x对应右。
在这里插入图片描述
当物体发生旋转,我们将物体和世界坐标的位置重合。
在这里插入图片描述
通过xyz的夹角,我们可以描述物体的朝向。难道各个夹角就是所谓欧拉角。
以上观点崩盘,大家千万别信,说实话,我刚接触3D的时候我真的是按照上面的方式去理解的,确实是可以唯一的描述方位,但是随后就崩盘了,因为实在没办法旋转到指定角度,旋转任意轴的时候另两个角度都会发生变化。不知道有没有人跟我有一样经历,或者看到上面时有没有感觉原来如此,如果是那恭喜你,你一定要继续下去,不然我就是误导了。
言归正转,欧拉角是在空间中 描述从一个用于表示某个固定的参考系的、已知的方向,经过一系列基本旋转得到、新的代表另一个参考系的方向的方式。这个方向可以被想成从一个初始的方向,旋转到其确切位置的方向。,关于欧拉角的其他的一些概念大家可以自行搜索,从上面的介绍可以看出来,欧拉角的三个角度其实就是围绕三个轴的旋转角度,既然是三个轴,我们就要考虑1.某一个轴旋转之后会不会影响其他两个轴的轴向?如果会的话就一定要有旋转顺序。2.围绕的三个轴都是哪三个轴?

Unity中的旋转

带着上面的两个问题,大家看如下操作,
在这里插入图片描述

1.一个物体绕x旋转90度,然后绕z轴或者y轴旋转,-----》看上去的效果相同?
在这里插入图片描述

2.一直增加x的角度,会不停的抖动?(如果你的电脑拖动不抖,那就用代码去累加x的角度,就会停滞在90附近)

------------------明白的可以retrue了=====================================
问题1:想,使劲想!!比较正统的解释就是万向锁,然后我来白话一下。
首先公布答案,unity中旋转顺序为Y-X-Z,接下来是重点了啊~~先绕Y轴,Y轴是谁的轴?其实是父节点的Y轴(没有父节点则是世界的Y轴)。X和Z都是自身的轴向。 你可能会问为啥Y不是,如果都是了那他不转了个寂寞,旋转也是要有参考系的。

在这里插入图片描述
上面可以看出,Capsule是围绕parent的Y轴进行了旋转。

现在我们来解答一个问题,x轴转了90度,就会导致自身的Z轴和父节点的Y轴在同一条直线上(并且是反向)。所以旋转效果是一样,但是让Y的角度增加和Z的角度增加旋转的方向正好是反向的。
由于这个问题,我们用欧拉角的方式很难做差值或者转动描述了,因为到了万向锁的地方会出现奇怪的现象,所以接下来就引出了四元数。
问题2:这个涉及到一个欧拉角到四元数转换时的计算问题,其实也不是转换的问题,就是unity的转换问题。继续看下去吧~

四元数

轴角

四元数之前,我们先说如何规避这种万向锁,接下来提出一个轴角的概念,上面呢我们费劲巴啦的绕着三个轴疯狂转,现在我们描述一个物体的旋转就是,此物体绕着某个轴转过一定角度。轴是哪个轴此轴是一个起点为物体的中心点,终点为此物体xyz坐标系表示的一个点。此物体围绕此轴旋转可以转到任意角度,不存在死锁的现象,并且可以很好的处理两次旋转之间的差值。

轴角转换四元数

上面轴角的想法出来后,我们总结下人物关系。

项目 Value
物体的自身坐标系坐标轴 xyz
自身原点 o
旋转轴 aix=ax+by+cz(xyz相互垂直,x^2+ y^2+z^2=1)
旋转角度 anlge

旋转轴在Unity为世界坐标系表示的轴
轴角构造四元数,推导过程啥的说实话我也没算过,到了这个年纪了就知道吃现成的了,理解就行了,函数如下:

public Quaternion(Vector3D axisOfRotation, double angleInDegrees)
{
	angleInDegrees %= 360.0;
	//--
	double num = angleInDegrees * Mathf.Deg2Rad;
	double length = axisOfRotation.Length;
	if (length == 0.0)
	{
		throw new InvalidOperationException(SR.Get("Quaternion_ZeroAxisSpecified"));
	}
	Vector3D vector3D = axisOfRotation / length * Math.Sin(0.5 * num);
	this._x = vector3D.X;
	this._y = vector3D.Y;
	this._z = vector3D.Z;
	this._w = Math.Cos(0.5 * num);
	this._isNotDistinguishedIdentity = true;
}

已知四元数,求轴角函数如下:

//-----求角度
public double Angle
{
		get
		{
			if (this.IsDistinguishedIdentity)
			{
				return 0.0;
			}
			double num = Math.Sqrt(this._x * this._x + this._y * this._y + this._z * this._z);
			double x = this._w;
			if (num > 1.7976931348623157E+308)
			{
				double num2 = Math.Max(Math.Abs(this._x), Math.Max(Math.Abs(this._y), Math.Abs(this._z)));
				double num3 = this._x / num2;
				double num4 = this._y / num2;
				double num5 = this._z / num2;
				num = Math.Sqrt(num3 * num3 + num4 * num4 + num5 * num5);
				x = this._w / num2;
			}
			return Math.Atan2(num, x) * 2 * Mathf.Rad2Deg;
		}
	}
	//---求旋转轴
	public Vector3D Axis
	{
		get
		{
			if (this.IsDistinguishedIdentity || (this._x == 0.0 && this._y == 0.0 && this._z == 0.0))
			{
				return new Vector3D(0.0, 1.0, 0.0);
			}
			Vector3D result = new Vector3D(this._x, this._y, this._z);
			result.Normalize();
			return result;
		}
	}

Unity中可以使用Quaternion中的ToAngleAxis(out float angle, out Vector3 axis)方法获取旋转轴和旋转角。

四元数的乘法的先后关系

四元数乘法在实际开发中会经常用到,这个经常是针对 与3D开发或者插件开发。因为我们不可避免的需求就是旋转某个物体,如果使用Transform的API当然是可以达到效果,但是大多数时候有些旋转结果可能只是中间结果,而API依赖于Gameobject,我们中间变量是没有Transform依赖的,此时我们就需要使用四元数去计算。比较常用的两个插件一个Cinemachine和FinalIK核心计算都是四元数乘法,后期有时间会做一下这两个的代码分析,有兴趣的可以关注下。
先把乘法代码弄出来看看,网上一堆,大家搜搜

q1 * q2 =
(w1*w2 - x1*x2 - y1*y2 - z1*z2) +
(w1*x2 + x1*w2 + y1*z2 - z1*y2) i +
(w1*y2 - x1*z2 + y1*w2 + z1*x2) j +
(w1*z2 + x1*y2 - y1*x2 + z1*w2) k
//代码表示如下
public static Quaternion operator *(Quaternion left, Quaternion right)
{
	if (left.IsDistinguishedIdentity)
	{
		return right;
	}
	if (right.IsDistinguishedIdentity)
	{
		return left;
	}
	double x = left._w * right._x + left._x * right._w + left._y * right._z - left._z * right._y;
	double y = left._w * right._y + left._y * right._w + left._z * right._x - left._x * right._z;
	double z = left._w * right._z + left._z * right._w + left._x * right._y - left._y * right._x;
	double w = left._w * right._w - left._x * right._x - left._y * right._y - left._z * right._z;
	Quaternion result = new Quaternion(x, y, z, w);
	return result;
}

好了,好了,说人话!!!言归正传正传,既然是理解性的,四元数的乘法就是连续旋转,OK~~我们知道四元数和轴角可以相互转换,我们用轴角来解释,四元数乘法就是轴角相乘,也就是说先绕着某个轴转,然后再继续绕着某个轴继续转。
举例论证1:绕着y轴转90度,然后再绕着z轴转90度。

 public Transform A;
void Start()
{
    //--y轴:Vector3(0, 1, 0)   注意这里是自身坐标系
    Quaternion aa = Quaternion.AngleAxis(90f, new Vector3(0, 1, 0));
    Quaternion bb = Quaternion.AngleAxis(90f, new Vector3(0, 0, 1));
    A.rotation = aa*bb;
}

在这里插入图片描述
这里要再次提醒旋转轴的坐标系是自身坐标系,所以第二步的bb绕z轴旋转是绕着y轴旋转90度之后的自身坐标系的z轴。

举例论证2:上面的问题2我们一直增加x的角度,另一种方式我们使用四元数的旋转的方式让他一直x轴转看看效果。代码如下:

public Transform A;
public Transform B;
public float delatAngle = 1;
void Update()
{
    var tempEA = A.rotation.eulerAngles;
    tempEA.x += delatAngle;
    A.rotation=Quaternion.Euler(tempEA);
    Quaternion bb = Quaternion.AngleAxis(delatAngle, new Vector3(1, 0, 0));
    B.rotation *= bb;        
}

在这里插入图片描述

跑偏了啊–插入四元数欧拉角转换

这个我觉的就是比较坑的地方,因为好多文章介绍旋转都会有欧拉角的旋转,大部分介绍都是用y轴旋转做的比较。
unity的Quaternion.eulerAngles获取到的角度,x的取值范围是[-90,90]。可以手动试试,所以,我们每次获取到的角度都会不会超过90。为了验证下,我们做自己实现以下四元数和欧拉角的转换。

using UnityEngine;
public class PeiYuQuaternion
{
    private float x;
    private float y;
    private float z;
    private float w;
    public PeiYuQuaternion(float x, float y, float z,float w)
    {
        this.x = x;this.y = y;this.z = z;this.w = w;
    }
    public PeiYuQuaternion(float xAngle,float yAngle,float zAngle)
    {
        DoAnlge(xAngle, yAngle, zAngle);
    }
    //--欧拉角转换四元数
    private void DoAnlge(float xAngle, float yAngle, float zAngle)
    {
        float halfX = xAngle * Mathf.Deg2Rad / 2;
        float halfY = yAngle * Mathf.Deg2Rad / 2;
        float halfZ = zAngle * Mathf.Deg2Rad / 2;
        float cosX = Mathf.Cos(halfX);
        float sinX = Mathf.Sin(halfX);
        float cosY = Mathf.Cos(halfY);
        float sinY = Mathf.Sin(halfY);
        float cosZ = Mathf.Cos(halfZ);
        float sinZ = Mathf.Sin(halfZ);
        w = cosX * cosY * cosZ + sinX * sinY * sinZ;
        x = sinX * cosY * cosZ - cosX * sinY * sinZ;
        y = cosX * sinY * cosZ + sinX * cosY * sinZ;
        z = cosX * cosY * sinZ - sinX * sinY * cosZ;
    }
    public static PeiYuQuaternion Euler(float xAngle, float yAngle, float zAngle)
    {
        PeiYuQuaternion pyq = new PeiYuQuaternion(xAngle, yAngle, zAngle);
        return pyq;
    }
    public static PeiYuQuaternion UnityQuaternion(Quaternion quaternion)
    {
        PeiYuQuaternion pyq = new PeiYuQuaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
        return pyq;
    }
    public Vector3 eulerAngles
    {
        get
        {
            //--四元数转换欧拉角
            Vector3 result = Vector3.zero;
            result.x = Mathf.Rad2Deg* Mathf.Atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));
            result.y = Mathf.Rad2Deg * Mathf.Asin(2 * (w * y - z * x));
            result.z = Mathf.Rad2Deg * Mathf.Atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z));
            return result;
        }
        set {
            DoAnlge(value.x, eulerAngles.y, eulerAngles.z);
        }
    }
    public override string ToString()
    {
        Vector3 euler = eulerAngles;
        return string.Format("x:{0}-y:{1}-z:{2}-w:{3}   Euler:x:{4}-y:{5}-z:{6}",x,y,z,w, euler.x, euler.y, euler.z);
    }
}

有了自定义的转换后,我们叫旋转代码稍作改动:

public Transform A;
public Transform B;
public float delatAngle = 1;
void Update()
{
    //--获取到unity的角度后,使用我们自定义的方式转换欧拉角
    var tempEA = PeiYuQuaternion.UnityQuaternion(A.rotation).eulerAngles;
    tempEA.x += delatAngle;
    A.rotation=Quaternion.Euler(tempEA);
    Quaternion bb = Quaternion.AngleAxis(delatAngle, new Vector3(1, 0, 0));
    B.rotation *= bb;        
}

效果如图:
在这里插入图片描述
具体Unity的转换做了什么限定还不知道。。等过几天会去看看源码把结果贴上来。
另外附上四元数和欧拉角的转换公式图:
在这里插入图片描述

在这里插入图片描述

回来了,四元数乘法的应用

相对自身和世界轴旋转

Transfom.Rotate的API,就是绕xyz轴旋转多少角度,

public void Rotate(Vector3 eulers, [DefaultValue("Space.Self")] Space relativeTo)
{
	Quaternion rhs = Quaternion.Euler(eulers.x, eulers.y, eulers.z);
	if (relativeTo == Space.Self)
	{
	//--相对自身旋转
		this.localRotation *= rhs;
	}
	else
	{
	//--相对世界旋转,this.rotation *Quaternion.Inverse(this.rotation)可以理解为将所有角度归0。
	//--还想了解更多可以搜搜四元数的逆。
		this.rotation *= Quaternion.Inverse(this.rotation) * rhs * this.rotation;
	}
}

上面的代码中,我们还可以修正成:

public void Rotate(Vector3 eulers, [DefaultValue("Space.Self")] Space relativeTo)
{
	Quaternion rhs = Quaternion.Euler(eulers.x, eulers.y, eulers.z);
	if (relativeTo == Space.Self)
	{
		this.rotation =this.rotation* rhs;
	}
	else
	{
		this.rotation = rhs * this.rotation;
	}
}

发现了什么?前后互换,相对置换。怎么去理解这个呢?我们把旋转理解为过程,相乘就是两个连续的过程,而且每一次旋转针对的旋转轴都是自身坐标系,而非世界坐标系。因此,相对于自身旋转直接在当前自身的旋转基础上进行乘法即可。至于为什么this.localRotation换成this.rotation结果一致,this.rotation=this.parentRotationthis.localRotation rhs,所以我认为相对自身旋转使用this.rotation更容易理解,因为无论是localRotation还是rotation都是表示物体当前的旋转位置,所以使用哪个都可以。
相对世界为什么位置互换?初始位置时,自身坐标和世界坐标完全一致,所以先进行需要围绕世界坐标进行的旋转,然后在进行之前自身的旋转即可。
这段理解起来可能有点不太好懂~~举个例子:自己是一个参考系,现在自己做向左前倾斜的动作,然后下令,让我们绕着地面转90度(世界坐标y)。此时我们先立正(此时和世界坐标一直),我们先转90度,然后在此基础上,我们朝自己钱左前方向倾斜。嘿嘿,这个例子感觉还可以哦。。

相对于某个点的某个轴旋转(四元数和向量的乘法)

当这个点不再是自己的中心点,轴不再是以中心点为起点的轴时。

public void RotateAround(Vector3 point, Vector3 axis, float angle)
{
   //----第一步
	Vector3 vector = this.position;
	Quaternion rotation = Quaternion.AngleAxis(angle, axis);
	Vector3 vector2 = vector - point;
	vector2 = rotation * vector2;
	vector = point + vector2;
	this.position = vector;
	//--第二步
	this..rotation *=Quaternion.Inverse(this..rotation) *rotation * this..rotation;
}

首先这里一共有两步:
第一步,计算旋转之后的物体的位置,这里就是注意下四元数和向量的乘法,其几何意义就是将这个向量按照世界坐标系旋转这个四元数代表的角度之后,向量的位置。所以通过第一步我们知道了旋转之后的物体的位置。
第二步:围绕某个点旋转,有点像我们手拉手烤篝火,大家围着篝火转,我们的位置再变化,同时我们的角度也一直在变,我们要始终朝向篝火。我们自转的角度变化和围绕的轴和角度一致,所以直接使用按照世界坐标系的方式计算即可。

Scale

缩放这个其实还是比较简单的。物体本身的缩放会影响到顶点信息从局部坐标到世界坐标的转换,所以缩放系数越大,单个物体就会越大。这个不想多说了,你可能会注意到这里根本没有提交矩阵,缩放会在转换矩阵的时候讨论下本质,打算在shader中进行矩阵的实用分析,我们平时用到的矩阵都是进行空间变换,其实变换矩阵中包含了上面所说的大部分信息~先到此为止吧

结语

这篇文章还是主要是个人理解,里面可能会有一些错误的地方,希望大家多多探讨~

猜你喜欢

转载自blog.csdn.net/u010778229/article/details/116046872