详解向量与Unity中向量类Vcetor3的使用(二)

详解向量与Unity中向量类Vcetor3的使用(一)中,我们学习了向量的数学意义、几何意义以及向量运算,掌握了向量的概念及使用。在Unity中,向量的表示类为Vector2(2D)与Vector3(3D)。这篇文章,我们结合前面一篇文章,对Vector3的源代码进行解剖,看看内部是如何实现向量的表示及向量计算的,同时例举一个例子帮助理解。掌握了Vector3自然就掌握了Vector2,本篇只讨论Vector3。

1、Vector3向量类

1.1向量类的声明及构造函数

我们先看Unity中Vector3类的声明,采用的是结构体struct而非Class,目的是为了性能优化、内存管理和数据传递效率等。继承了IEquatable接口,主要是为了比较两个向量是否相等,IFormattable接口主要是为了输出向量的字符串形式。

arduino 复制代码
public struct Vector3 : IEquatable<Vector3>, IFormattable

下面的Vector3类中定义了两个float类型的常量以及x、y、z三个float类型的公开字段,分别对应向量在x轴上的分量、y轴上的分量、z轴上的分量,这与我们在上一篇文章说的向量的定义是一致的。为什么选择float类型而不是double类型,其实也是一种提高性能的手段。

arduino 复制代码
public const float kEpsilon = 1E-05f;

public const float kEpsilonNormalSqrt = 1E-15f;

// 摘要:
// X component of the vector.
public float x;

// 摘要:
// Y component of the vector.
public float y;

// 摘要:
// Z component of the vector.
public float z;

我们来看看Vector3的构造函数,如下,有两个构造函数,Vector3(float x, float y, float z)接收三个float类型的值传入,分别为x、y、z赋值。Vector3(float x, float y)接收两个float类型的值传入,分别为x、y赋值,z值为0。 [MethodImpl(MethodImplOptions.AggressiveInlining)]这个特性表示的是:指示编译器对这个方法使用"积极内联"优化,这意味着编译器会尝试将方法的代码直接插入调用处,以提高性能。这个特性在Vector3中的公开的方法中都有。

csharp 复制代码
//
// 摘要:
//     Creates a new vector with given x, y, z components.
// 参数:
//   x:
//   y:
//   z:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3(float x, float y, float z)
{
    this.x = x;
    this.y = y;
    this.z = z;
}

//
// 摘要:
//     Creates a new vector with given x, y components and sets z to zero.
// 参数:
//   x:
//   y:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector3(float x, float y)
{
     this.x = x;
     this.y = y;
     z = 0f;
}

例如我们声明两个向量a、b、c表示Unity三维坐标系空间中的某两个位置。

ini 复制代码
Vector3 a = new Vector3(10, 10, 0);
Vector3 c = new Vector3(10, 10);      //在z轴上的分量为0,向量a与向量b是等同的
Vector3 b = new Vector3(20, 0, 20);

1.2向量类中的向量的模长计算

由上一篇文章我们知道一个向量的模长等于其在x、y、z轴上的分量的平方和的平方根,计算公式如下:

我们看下在Vector3类中代码是如何实现的,如下代码:

arduino 复制代码
// 摘要:
//     Returns the length of this vector (Read Only).
public float magnitude
{
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     get
     {
          return (float)Math.Sqrt(x * x + y * y + z * z);
     }
}

//
// 摘要:
//     Returns the squared length of this vector (Read Only).
public float sqrMagnitude
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
         return x * x + y * y + z * z;
    }
}
    
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Magnitude(Vector3 vector)
{
     return (float)Math.Sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float SqrMagnitude(Vector3 vector)
{
     return vector.x * vector.x + vector.y * vector.y + vector.z * vector.z;
}

Vector3提供了一个模长以及一个模长的平方的float值,主要目的在于实际运用过程中如果你只是比较两个向量的模长而不需要计算确切的模长值时,可以使用Vector3.sqrMagnitude,避免了数学上的开根运算,可以提高性能。需要计算确切的模长时使用Vector3.magnitude。同时也可以使用静态方法Magnitude()和SqrMagnitude()传入Vector3来求模长及模长的平方值。

1.3向量类中的单位向量与零向量

我们知道,单位向量就是模长为1的向量,在Vector3提供了哪些单位向量有呢?如下代码,定义了静态只读的常用的单位向量,零向量是既没有长度也没有方向的向量,下面代码中还有一个oneVector,表示的是该向量在x、y、z轴上的分量均为1,该向量一般用于缩放。

很显然,单位向量都是在x、y、z其中一条轴上的分量为1或-1,其余两轴上的分量为0,由向量的模长计算公式可知其模长均为1。

java 复制代码
private static readonly Vector3 zeroVector = new Vector3(0f, 0f, 0f);

private static readonly Vector3 oneVector = new Vector3(1f, 1f, 1f);

private static readonly Vector3 upVector = new Vector3(0f, 1f, 0f);

private static readonly Vector3 downVector = new Vector3(0f, -1f, 0f);

private static readonly Vector3 leftVector = new Vector3(-1f, 0f, 0f);

private static readonly Vector3 rightVector = new Vector3(1f, 0f, 0f);

private static readonly Vector3 forwardVector = new Vector3(0f, 0f, 1f);

private static readonly Vector3 backVector = new Vector3(0f, 0f, -1f);

单位向量通过相应公开的静态属性对外暴露,如下:

csharp 复制代码
// 摘要:
//     Shorthand for writing Vector3(0, 0, 1).
public static Vector3 forward
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
     {
         return forwardVector;
     }
}

如果向量不是单位向量,我们怎么求它的单位向量呢?就是向量本身除以本身的模长,计算公式如下:

Vector3中实现代码如下:

csharp 复制代码
//
// 摘要:
//     Returns this vector with a magnitude of 1 (Read Only).
public Vector3 normalized
{
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
      get
      {
          return Normalize(this);
      }
}
    
//
// 摘要:
//     Makes this vector have a magnitude of 1.
// 参数:
//   value:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 Normalize(Vector3 value)
{
    float num = Magnitude(value);
    if (num > 1E-05f)
    {
         return value / num;
    }

    return zero;
}

可以通过已经初始化的向量的属性值normalized,也可以通过传参的方式使用Vector3的静态方法获取。这里做了一个判断处理,如果该向量的模长小于1E-05f,就认为这个向量的标准化向量为零向量

单位向量的一个应用场景就是在unity中知道一条有向线段以及一个该线段方向上的点,我们只要知道该点距离该有向线段的起点或终点的距离,就可以求该点的坐标值。

1.4向量类中向量的加减乘除

1.4.1标量与向量的乘除

由上一篇向量的知识我们已知,一个向量可以与一个标量相乘,得到的是一个与原向量平行的向量,是放大还是缩小,是正向还是反向,取决于标量的值,标量与向量不能进行加减操作。

在Vector3类中代码实现如下:

csharp 复制代码
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 operator *(Vector3 a, float d)
{
     return new Vector3(a.x * d, a.y * d, a.z * d);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 operator *(float d, Vector3 a)
{
     return new Vector3(a.x * d, a.y * d, a.z * d);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 operator /(Vector3 a, float d)
{
     return new Vector3(a.x / d, a.y / d, a.z / d);
}

通过重载"*"与"/"运算符,实现了标量与向量的乘除,这与我们在上一篇中向量的知识一致。

1.4.2向量与向量的加减

相同维数的向量与向量可以进行加减操作,这里我们讨论的是Unity中的三维向量。Vector3中实现如下:

css 复制代码
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 operator +(Vector3 a, Vector3 b)
{
     return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 operator -(Vector3 a, Vector3 b)
{
     return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z);
}

同样通过通过重载"+"与"-"运算符实现向量的加减,两个向量的加减为两个向量在x、y、z轴上的向量进行相加或相减。这里提一下负向量,它表示的是与原向量大小相等,方向相反的向量,实现如下:

csharp 复制代码
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3 operator -(Vector3 a)
{
    return new Vector3(0f - a.x, 0f - a.y, 0f - a.z);
}

1.5两个向量的相等性比较

如果两个向量相等,则表示这两个向量大小相等、方向相同。

csharp 复制代码
 // 摘要:
 //     Returns true if the given vector is exactly equal to this vector.
 // 参数:
 //   other:
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public override bool Equals(object other)
 {
      if (!(other is Vector3))
      {
          return false;
      }

      return Equals((Vector3)other);
 }

 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public bool Equals(Vector3 other)
 {
      return x == other.x && y == other.y && z == other.z;
 }
 
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static bool operator ==(Vector3 lhs, Vector3 rhs)
 {
       float num = lhs.x - rhs.x;
       float num2 = lhs.y - rhs.y;
       float num3 = lhs.z - rhs.z;
       float num4 = num * num + num2 * num2 + num3 * num3;
       return num4 < 9.99999944E-11f;
 }

 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static bool operator !=(Vector3 lhs, Vector3 rhs)
 {
      return !(lhs == rhs);
 }

同样通过通过重载"=="与"!="运算符实现向量的相等性比较,运算符"=="判定是否相等时,若两个向量在x、y、z轴上的分量的差值的平方和小于9.99999944E-11f,则两个向量相等。若采用Equals()方法判定一个向量与另外一个向量是否相等时,则需要判定两个向量在x、y、z轴上的分量是否都相等,若相等则两个向量相等。

1.6距离公式

求两点之间的距离公式,公式表示如下:

Vector3中实现代码如下,很显然是一致的。

csharp 复制代码
 // 摘要:
 //     Returns the distance between a and b.
 //
 // 参数:
 //   a:
 //   b:
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static float Distance(Vector3 a, Vector3 b)
 {
      float num = a.x - b.x;
      float num2 = a.y - b.y;
      float num3 = a.z - b.z;
      return (float)Math.Sqrt(num * num + num2 * num2 + num3 * num3);
 }

举个例子:

ini 复制代码
 //求a、b两点之间的距离,结果应为10
 Vector3 a = new Vector3(10, 10, 0);
 Vector3 b = new Vector3(10, 0, 0);
 var distance = Vector3.Distance(a, b);
 Debug.Log($"向量表示的点与向量b表示的点的距离为{distance}");

输出结果如下:

1.7向量的点乘

向量的点击数学意义是向量各分量的积的和,在Vector3类中向量的点乘为静态函数Dot()表示。基于向量的计算公式实现代码如下:

csharp 复制代码
 //
 // 摘要:
 //     Dot Product of two vectors.
 // 参数:
 //   lhs:
 //   rhs:
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static float Dot(Vector3 lhs, Vector3 rhs)
 {
      return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
 }

由上一篇我们可知,通过向量的点乘我们可以求两个向量之间的夹角,公式为:

举个简单例子:

ini 复制代码
 //求向量a与向量b的夹角,输出值应为45°
 Vector3 a = new Vector3(10, 10, 0);
 Vector3 b = new Vector3(10, 0, 0);
 var cos = Vector3.Dot(a, b) / (a.magnitude * b.magnitude);
 var angle = Mathf.Acos(cos);
 Debug.Log($"向量a与向量b的夹角为{angle*180/Mathf.PI}");

输出结果如下:

上面的代码虽然可以求角度,但是过程是先求的两个向量的余弦值,再通过反三角函数及弧度转化为角度得到,代码略显复杂,Vector3提供了一个函数Angle()可以一步到位求两个向量之间的角度值,代码如下:

csharp 复制代码
 // 摘要:
 //     Calculates the angle between vectors from and.
 // 参数:
 //   from:
 //     The vector from which the angular difference is measured.
 //   to:
 //     The vector to which the angular difference is measured.
 // 返回结果:
 //     The angle in degrees between the two vectors.
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static float Angle(Vector3 from, Vector3 to)
 {
      float num = (float)Math.Sqrt(from.sqrMagnitude * to.sqrMagnitude);
      if (num < 1E-15f)
      {
           return 0f;
      }

      float num2 = Mathf.Clamp(Dot(from, to) / num, -1f, 1f);
      return (float)Math.Acos(num2) * 57.29578f;
 }

我们替换下代码看下输出结果是否一致,替换代码如下:

css 复制代码
 //求向量a与向量b的夹角,输出值为45°
 Vector3 a = new Vector3(10, 10, 0);
 Vector3 b = new Vector3(10, 0, 0);
 Debug.Log($"向量a与向量b的夹角为{Vector3.Angle(a, b)}");

输出结果仍然为45°,所以要求两向量之间的夹角的之后直接用Angle()函数,当需要求余弦值时选用Dot()方法。

1.8向量的投影

给定两个向量vn ,能将v 分解成两个分量:v1v2 。它分别平行于和垂直于n ,并满足v=v1+v2 。一般称平行分量v1vn上的投影。

向量的投影用静态函数Project()表示。基于向量的计算公式实现代码如下:

scss 复制代码
 // 摘要:
 //     Projects a vector onto another vector.
 // 参数:
 //   vector:
 //   onNormal:
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static Vector3 Project(Vector3 vector, Vector3 onNormal)
 {
      float num = Dot(onNormal, onNormal);
      if (num < Mathf.Epsilon)
      {
          return zero;
      }

      float num2 = Dot(vector, onNormal);
      return new Vector3(onNormal.x * num2 / num, onNormal.y * num2 / num, onNormal.z * num2 / num);
 }

这里与前面向量的投影计算公式表现不一致,说明一下,向量的计算公式为:

但是上述实现代码貌似与上述表达式不一致,其实表达的是一个意思,我们将上面代码实现的公式写一下,如下:

那么问题来了,为什么n·n=||n||^2呢?上面代码中看到在计算向量vectoronNormal 上的投影向量是做了一个判断,如果onNormal的点积为0,则返回一个零向量,我们知道一个向量在零向量上投影向量肯定为零。若onNormal 向量不为零,那么它本身的点积就等于||onNormal|| ||onNormal|| cosx,它自身向量的夹角为0,正弦值为1,所以n·n =||n||^2。

举个简单例子:

ini 复制代码
 //计算向量a在向量b上的投影向量,结果应为(10,0,0)
 Vector3 a = new Vector3(10, 10, 0);
 Vector3 b = new Vector3(50, 0, 0);
 var projectVector = Vector3.Project(a, b);
 Debug.Log($"向量a在向量b上的投影向量为({projectVector.x},{projectVector.y},{projectVector.z})");

输出结果为:

1.9向量的叉乘

向量的投影用静态函数Cross()表示。基于向量的计算公式实现代码如下:

csharp 复制代码
 // 摘要:
 //     Cross Product of two vectors.
 // 参数:
 //   lhs:
 //   rhs:
 [MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static Vector3 Cross(Vector3 lhs, Vector3 rhs)
 {
      return new Vector3(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x);
 }

向量的叉乘在3D主要运用于创建垂直于两个向量所在的平面的向量 。当然我们也知道通过叉乘得到的向量的模长它可以求出两个向量组成的平行四边的面积。

举个简单例子:

ini 复制代码
 //构建一个底边长为20,高为10的平行四边形的两个向量a,b,面积为200
 Vector3 a = new Vector3(10, 10, 0);
 Vector3 b = new Vector3(20, 0, 0);
 
 //求叉乘的向量
 var crossVector = Vector3.Cross(a, b);
 
 //叉乘向量的模长等于所构建的平行四边形的面积
 var area = crossVector.magnitude;
 Debug.Log($"向量a与向量b所构建的平行四边形的面积为{area}");

输出结果:

2、结语

通过两篇文章,我们已经对向量以及Unity中向量类Vector3有了较为全面的理解,Vector3中向量的计算都是基于向量的基本运算及相关的运算法则的。理解向量的数学意义及几何意义,在Unity中,我们更加关注的是几何意义,更加直观的帮助我们理解向量的运用。

向量的高级运用就涉及到了向量的旋转,向量的旋转在Unity中有欧拉角、四元数等,特别是四元数,它的数学原理更为复杂。对于Unity坐标系中线与线的相交性、线与圆的相交性等后续有机会在探讨。

相关推荐
△曉風殘月〆6 小时前
WPF MVVM入门系列教程(二、依赖属性)
c#·wpf·mvvm
逐·風8 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
m0_6569747411 小时前
C#中的集合类及其使用
开发语言·c#
九鼎科技-Leo11 小时前
了解 .NET 运行时与 .NET 框架:基础概念与相互关系
windows·c#·.net
九鼎科技-Leo14 小时前
什么是 ASP.NET Core?与 ASP.NET MVC 有什么区别?
windows·后端·c#·asp.net·mvc·.net
.net开发14 小时前
WPF怎么通过RestSharp向后端发请求
前端·c#·.net·wpf
小乖兽技术14 小时前
C#与C++交互开发系列(二十):跨进程通信之共享内存(Shared Memory)
c++·c#·交互·ipc
幼儿园园霸柒柒14 小时前
第七章: 7.3求一个3*3的整型矩阵对角线元素之和
c语言·c++·算法·矩阵·c#·1024程序员节
平凡シンプル17 小时前
C# EF 使用
c#
丁德双17 小时前
winform 加载 office excel 插入QRCode图片如何设定位置
c#·excel