65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的向量类型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (135投票s)

2007年1月31日

CPOL

25分钟阅读

viewsIcon

651828

downloadIcon

6789

通过 C# 中的笛卡尔和欧几里得几何来指导向量类型

引言

多年来,我看到人们在向量数学方面苦苦挣扎。本指南将引导您了解如何在 C# 中创建可重用的 Vector3 类型及其背后的数学原理。后缀 3 仅指向量是三维的 (x,y,z)。

代码并非旨在快速或高效,而是尽可能简单易懂。为此,并且作为个人偏好,Vector3 类型封装了相关功能和方法的多个接口(例如,方法的静态和非静态变体)。许多人会认为这是臃肿的代码,但是,我认为它使代码对程序员友好。显然,随着项目的增长,我倾向于在创建函数更适合的对象和类型时,将功能重构出来。我认为这实现了最大的内聚性,并最大限度地减少了耦合和依赖性。

我使用了三维笛卡尔坐标系(即 x、y 和 z 的三个垂直轴)和欧几里得几何。不用担心这些术语;它们只是高中数学中涵盖的一些概念的正式名称。向量空间是体积的(立方体);请注意,您可以使用其他向量空间,例如圆柱空间,其中一个轴(通常是 z)与圆柱的半径相关。

您可能已经猜到,计算机在处理这种类型的数学时速度很慢。矩阵数学效率更高,但更难理解。您需要对三角学和代数有基本的了解才能理解本指南。

除非另有说明,我假设向量是位置的,原点在点 (0,0,0)。位置向量的替代方案是:单位向量,可以解释为没有大小或无限大小;以及向量对,其中向量的原点是另一个向量,大小是距原点向量的距离。

请注意,本指南极其冗长,对于经验丰富的 C# 程序员来说可能显得有些居高临下。请不要介意,我为广泛的读者编写了本指南。

快速词汇表

  • 运算符,这是用于定义操作的符号,例如 (a+b) 中的加号 (+)
  • 操作数,这些是操作中使用的变量,例如 (a+b) 中的 (a) 和 (b)。左侧 (LHS) 操作数是 (a),而右侧 (RHS) 操作数是 (b)。

 

使用代码

首先,让我们定义向量信息的存储方式。我编码时很少创建结构体,但对于我们的 Vector3 来说,这非常完美。如果您正在阅读本文,您可能已经知道向量表示沿多个轴的值。对于本教程,我们将开发一个三维类型,所以...三个变量和三个轴。

public struct Vector3
{
   private readonly double x;

   private readonly double y;

   private readonly double z;

}

轴的方向是什么?我来自可视化背景,我总是假设

您可能已经注意到,当您沿着 Z 轴看时,Z 是负的。这是 OpenGL 等图形库中常见的约定。这在以后考虑俯仰、滚转和偏航方法时将变得很重要。

快速岔开话题:为什么是结构体而不是类?

结构体和类之间的区别

  • 结构体是在堆栈而不是堆上创建的值类型,从而减少了垃圾回收开销。
  • 它们通过值而不是引用传递。
  • 它们创建和处置快速高效。
  • 您不能从它们派生其他类型(即不可继承)。
  • 它们只适用于成员(变量)数量较少的类型。Microsoft 建议结构体应小于 16 字节。
  • 您不需要 new 关键字来实例化结构体。

基本上,它看起来像、行为像并且是一个原始类型。虽然,没有理由不能将向量类型创建为类。开发结构体的一个缺点是 .NET 框架中的集合类将结构体转换为类。这意味着大量的 Vector3 将具有很高的转换开销。泛型类型是在 C# v2.0 中引入的,可用于降低集合中结构体的转换和装箱的性能成本。

S. Senthil Kumar 编写了一篇关于结构体的更深入的文章,请点击此处

访问变量

您是否注意到变量是私有且只读的?

虽然我选择构建一个结构体,但我习惯性地隐藏我的变量并创建公共访问器和修改器属性。这对于结构体来说并非严格意义上的好做法,但我创建它们以防我以后需要转换为类(这对于类结构来说是好做法)。事实上,在这种类型中,我不会创建公共修改器,因为结构体应该是不可变的。

变量是只读的,因为这个类将是不可变的。x、y 和 z 变量只能在构造结构体时设置。这意味着一旦构建了 Vector3,它将始终具有相同的值。对 Vector3 执行的所有操作都将生成另一个结构体。这很重要,因为结构体是按值传递的,这意味着当它们传递给另一个方法时,它们会被复制。如果您在另一个方法中更改(或修改)值,则当您返回到调用方法时,这些更改将丢失。

除了属性之外,还提供了数组样式的接口。这允许用户使用 myVector[x]myVector[y]myVector[z] 调用 Vector3。此外,用户可以使用 Array 属性将所有组件作为数组获取。

public double X
{
   get{return this.x;}
}

public double Y
{
   get{return this.y;}
}

public double Z
{
   get{return this.z;}
}

public double[] Array
{
   get{return new double[] {x,y,z};}
}

public double this[ int index ]
{
   get
   {
      switch (index)
      {
         case 0: {return X; }
         case 1: {return Y; }
         case 2: {return Z; }
         default: throw new ArgumentException(THREE_COMPONENTS, "index");
     }
   }
}

private const string THREE_COMPONENTS = 
   "Array must contain exactly three components, (x,y,z)";

还提供了一个属性来访问向量的大小。向量的大小(或绝对值)是它的长度,与方向无关,可以使用以下公式确定:

SumComponentSquares 方法(如下所示)可以在本文后面看到。

public double Magnitude
{
   get 
   {
      return Math.Sqrt ( SumComponentSqrs() );
   }
}

构造一个 Vector3

为了使用典型的类语法构造类型,提供了以下构造函数方法:

public Vector3(double x, double y, double z)
{
   this.x = x;
   this.y = y;
   this.z = z;
}

public Vector3 (double[] xyz)
{
   if (xyz.Length == 3)
   {
      this.x = xyz[0];
      this.y = xyz[1];
      this.z = xyz[2];
   }
   else
   {
      throw new ArgumentException(THREE_COMPONENTS);
   }
}

public Vector3(Vector3 v1)
{
   this.x = v1.X;
   this.y = v1.Y;
   this.z = v1.Z;
}

private const string THREE_COMPONENTS = 
   "Array must contain exactly three components , (x,y,z)";

运算符重载

我们现在拥有了一个存储、访问和修改 Vector3 及其组件 (x,y,z) 的框架。我们现在可以考虑适用于向量的数学运算。让我们从重载基本数学运算符开始。

重载运算符允许程序员定义类型在代码中的使用方式。例如,加号运算符 (+)。对于数值类型,这表示两个数字的相加。对于字符串,它表示两个字符串的连接。运算符重载对于程序员描述类型如何与系统交互具有巨大的好处。在 C# 中,可以重载以下运算符:

  • 加法、连接和强化 (+)
  • 减法和取反 (-)
  • 逻辑非 (!)
  • 按位补码 (~)
  • 增量 (++)
  • 减量 (--)
  • 布尔真 (true)
  • 布尔假 (false)
  • 乘法 (*)
  • 除法 (/)
  • 除法余数 (%)
  • 逻辑与 (&)
  • 逻辑或 (|)
  • 逻辑异或 (^)
  • 二进制左移 (<<)
  • 二进制右移 (>>)
  • 相等运算符,等于和不等于 (==!=)
  • 差异/比较运算符,小于和大于 (<>)
  • 差异/比较运算符,小于或等于和大于或等于 (<=>=)

快速提示: MSDN 称之为运算符重载,然而,我们所做的更像是运算符覆盖。如果运算符是在基类上定义的,那么我们将使用术语运算符覆盖。

加法 (v3 = v1 + v2)

两个向量的加法是通过简单地将一个向量的 x、y 和 z 分量加到另一个向量中来实现的(即 x+x, y+y, z+z)。

public static Vector3 operator+(Vector3 v1, Vector3 v2)
{
   return new Vector3(
      v1.X + v2.X,
      v1.Y + v2.Y,
      v1.Z + v2.Z);
}

减法 (v3 = v1 - v2)

两个向量的减法只是将一个向量的 x、y 和 z 分量从另一个向量中减去(即 x-x, y-y, z-z)。

public static Vector3 operator-(Vector3 v1, Vector3 v2 )
{
   return new Vector3(
      v1.X - v2.X,
      v1.Y - v2.Y,
      v1.Z - v2.Z);
}

取反 (v2 = -v1)

向量的取反会反转其方向。这可以通过简单地对向量的每个分量进行取反来实现。

public static Vector3 operator-(Vector3 v1)
{
   return new Vector3(
      -v1.X,
      -v1.Y,
      -v1.Z);
}

强化 (v2 = +v1)

向量的强化实际上什么都不做,只是根据加法规则返回原始向量(即 +-x = -x 和 ++x = +x)。

public static Vector3 operator+(Vector3 v1)
{
   return new Vector3(
      +v1.X,
      +v1.Y,
      +v1.Z);
}

其他重载运算符

以下运算符的重载可以在文章的其他地方找到:

比较

在检查相等性时,我们检查向量的分量部分 (x,y,z)。在比较两个向量时,我们比较它们的大小。

比较双精度浮点数(向量的大小或分量部分)看起来很简单,.Net double 类型提供了运算符 <, >, <=, >=, ==, != 并实现了 IComparable 和 IEquatable。然而,这些操作并不可靠。这是因为分数(即 float, double, 和 decimal 变量)在二进制表示中的存储方式以及它们计算的误差范围。当您尝试考虑特殊情况的数字时,这会变得更加复杂:正负 0,正负无穷大和非数字。有关更多信息,请参阅 Bruce Dawson 的文章

关于在计算相等性时使用阈值的问题,本文的评论中有很多讨论。我个人同意红男爵的观点,他建议最终用户应负责与他们的应用程序相关的容差:

“……你不应该在你的代码中实现一个容差值。
这个容差的合适值是多少?
这取决于问题……”

运算符签名不能重载以接受额外的参数,因此 Vector3 的比较运算符将像 double 类型的运算符一样不容忍。在进行向量比较时,建议您使用 <<=>>=,并使用与最终用户应用程序相关的合理容差。这优先于相等运算符 ==!=.Equals。但是,Equals 方法可以重载。

有许多方法可以进行容差比较,Bruce Dawson 的文章中描述的方法是:

  • ε 比较 / 绝对误差容忍度
    最适合接近 0 的数字
  • 相对 ε 比较 / 相对误差容忍度
    最适合大于 0 的数字,但难以定义合适的值
  • Ulp(最后一位的单位)
    最适合大于 0 的数字

就本文而言,仅实现了绝对误差容限,并且仅在适当的情况下作为重载实现。重要的是要记住,运算符(<, >, <=, >=, ==, !=)不接受容限值。

小于 (result = v1 < v2)

小于比较两个向量,仅当左侧向量 (v1) 的大小小于另一个向量 (v2) 的大小才返回 true。为了提高效率,在计算向量大小时我们不需要执行最后一步,即对结果进行平方根运算。平方大小的比较结果将是相同的。

public static bool operator<(Vector3 v1, Vector3 v2)
{
   return v1.SumComponentSqrs() < v2.SumComponentSqrs();
}

小于或等于 (result = v1 <= v2)

小于或等于比较两个向量,仅当左侧向量 (v1) 的大小小于另一个向量 (v2) 的大小或两个大小相等时才返回 true。为了效率,我们再次使用平方大小。

public static bool operator<=(Vector3 v1, Vector3 v2)
{
   return v1.SumComponentSqrs() <= v2.SumComponentSqrs();
}

大于 (result = v1 > v2)

大于比较两个向量,仅当左侧向量 (v1) 的大小大于另一个向量 (v2) 的大小才返回 true。为了效率,我们再次使用平方大小。

public static bool operator>(Vector3 v1, Vector3 v2)
{
   return v1.SumComponentSqrs() > v2.SumComponentSqrs();
}

大于或等于 (result = v1 >= v2)

大于或等于比较两个向量,仅当左侧向量 (v1) 的大小大于另一个向量 (v2) 的大小或两个大小相等时才返回 true。为了效率,我们再次使用平方大小。

public static bool operator>(Vector3 v1, Vector3 v2)
{
   return v1.SumComponentSqrs() >= v2.SumComponentSqrs();
}

相等 (result = v1 == v2)

要检查两个向量是否相等,我们只需检查分量对。我们将结果进行 AND 运算,因此任何不相等的对都将导致 false。

public static bool operator==(Vector3 v1, Vector3 v2)
{
   return
      v1.X == v2.X &&
      v1.Y == v2.Y &&
      v1.Z == v2.Z;
}

不等 (result = v1 != v2)

如果运算符 ==(等于)被覆盖,C# 强制我们覆盖 !=(不等于)。这只是相等的逆运算。

public static bool operator!=(Vector3 v1, Vector3 v2)
{
   return !(v1 == v2);
}

Equals

Equals 方法检查两个向量之间的相等性,并实现 .Net 框架提供的 IEquitable IEquatable<Vector3> 接口。在大多数情况下,结果与 == 运算符相同。但是,双精度浮点数的一个特殊值 NaN(非数字)的处理方式不同。

  • NaN == NaN 为 false
  • NaN.Equals(NaN) 为 true

这与双精度的 == 运算符和 .Equals 的实现是一致的。原因在这篇文章中有描述。

public override bool Equals(object other)
{
    // Check object other is a Vector3 object
    if(other is Vector3)
    {
        // Convert object to Vector3
        Vector3 otherVector = (Vector3)other;

        // Check for equality
        return otherVector.Equals(this);
    }
    else
    {
        return false;
    }
}

public bool Equals(Vector3 other)
{
     return
        this.X.Equals(other.X) &&
        this.Y.Equals(other.Y) &&
        this.Z.Equals(other.Z);
}

如前所述,Equals 方法可以重载以允许容差值。

public bool Equals(object other, double tolerance)
{
   if (other is Vector3)
   {
      return this.Equals((Vector3)other, tolerance);
   }
   return false;
}

public bool Equals(Vector3 other, double tolerance)
{
   return
      AlmostEqualsWithAbsTolerance(this.X, other.X, tolerance) &&
      AlmostEqualsWithAbsTolerance(this.Y, other.Y, tolerance) &&
      AlmostEqualsWithAbsTolerance(this.Z, other.Z, tolerance);
}

public static bool AlmostEqualsWithAbsTolerance(double a, double b, double maxAbsoluteError)
{
   double diff = Math.Abs(a - b);

   if (a.Equals(b))
   {
      // shortcut, handles infinities
      return true;
   }

   return diff <= maxAbsoluteError;
}

GetHashCode

GetHashCode 方法在 C# 中所有对象的基类上定义,用于为基于哈希的集合(如 Dictionary)提供快速相等性比较。哈希码比较应产生与 Equals 方法相同的结果,因此当我们覆盖 Equals 方法时,必须覆盖 GetHashCode。有关更多信息,请参阅 MSDN

向量的每个分量部分的哈希码通过 XOR 进行位组合。素数 397 足够大,可以导致分量哈希码溢出,从而提供更好的哈希码分布。unchecked 阻止编译器检查数字溢出,在这种特殊情况下,这是我们想要的。有许多有效的方法来计算哈希码。

public override int GetHashCode()
{
   unchecked
   {
      var hashCode = this.x.GetHashCode();
      hashCode = (hashCode * 397) ^ this.y.GetHashCode();
      hashCode = (hashCode * 397) ^ this.z.GetHashCode();
      return hashCode;
   }
}

CompareTo

CompareTo 方法实现两个向量的比较,返回:

  • 如果大小小于另一个大小,则为 -1
  • 如果大小等于另一个大小,则为 0
  • 如果大小大于另一个大小,则为 1

这允许向量类型实现 .Net 框架提供的 IComparable IComparable<Vector3> 接口。

public	int	CompareTo(object other)
{
    if (other is Vector3)
    {
        return this.CompareTo((Vector3)other);
    }

    // Error condition: other is not a Vector3 object
    throw new ArgumentException(
        NON_VECTOR_COMPARISON + "\n" + 
        ARGUMENT_TYPE + other.GetType().ToString(),
        "other");
}

public int CompareTo(Vector3 other)
{
   if (this < other)
   {
      return -1;
   }
   else if (this > other)
   {
      return 1;
   }

   return 0;
}

private const string NON_VECTOR_COMPARISON = 
"Cannot compare a Vector3 to a non-Vector3";

private const string ARGUMENT_TYPE = 
"The argument provided is a type of ";

同样,CompareTo 可以重载以允许容差值。这里,绝对容差计算由 Equals 方法重载处理。

public int CompareTo(object other, double tolerance)
{
    if (other is Vector3)
    {
        return this.CompareTo((Vector3)other, tolerance);
    }

    // Error condition: other is not a Vector3 object
    throw new ArgumentException(
        NON_VECTOR_COMPARISON + "\n" + 
        ARGUMENT_TYPE + other.GetType().ToString(),
        "other" );
}
        
public int CompareTo(Vector3 other, double tolerance)
{
    var bothInfinite = 
        double.IsInfinity(this.SumComponentSqrs()) && 
        double.IsInfinity(other.SumComponentSqrs());
    
    if (this.Equals(other, tolerance) || bothInfinite)
    {
        return 0;
    }
    
    if (this < other)
    {
        return -1;
    }
    
    return 1;
}

乘法(点积、叉积和标量乘法)

向量乘法很复杂。有三种不同类型的向量乘法:

  • 与标量相乘 (v3 = v1 * s2)
  • 点积 (s3 = v1 . v2)
  • 叉积 (v3 = v1 * v2)

只有标量乘法和标量除法被实现为运算符重载。我见过诸如 ~ 之类的运算符被重载用于点积,以区别于叉积;但我认为这可能导致混淆,因此选择不为点积和叉积提供运算符,而是让用户调用相应的方法。

标量乘法是通过将每个分量乘以标量值来实现的。

public static Vector3 operator*(Vector3 v1, double s2)
{
   return
      new Vector3
      (
         v1.X * s2,
         v1.Y * s2,
         v1.Z * s2
      );
}

乘法中操作数的顺序可以颠倒;这被称为可交换的。

public static Vector3 operator*(double s1, Vector3 v2)
{
   return v2 * s1;
}

两个向量的叉积产生一个与给定两个向量所创建平面垂直的法向量。

其公式(其中 v1 = A 且 v2 = B)为:

这个方程总是产生一个向量作为结果。

正弦 theta 用于考虑向量的方向。Theta 总是取 A 和 B 之间的最小角度(即 )。

公式的右侧是通过使用规则展开和简化左侧得到的:

Sin 0° = 0

Sin 90° = 1

以矩阵样式表示,这看起来像:

您应该知道,这个方程是不可交换的。这意味着 v1 叉积 v2 与 v2 叉积 v1 是不一样的。

所有这些的 C# 代码是:

public static Vector3 CrossProduct(Vector3 v1, Vector3 v2)
{
   return
      new Vector3
      (
         v1.Y * v2.Z - v1.Z * v2.Y,
         v1.Z * v2.X - v1.X * v2.Z,
         v1.X * v2.Y - v1.Y * v2.X
      );
}

在可能的情况下,我创建了静态方法来扩展程序员在使用该类型时的选项。直接影响或受实例影响的方法只是调用静态方法。因此,静态方法的实例对应项是:

public Vector3 CrossProduct(Vector3 other)
{
   return CrossProduct(this, other);
}

请注意,此实例方法不影响调用它的实例,而是返回一个新的 Vector3 对象。我选择以这种方式实现叉积有两个原因;第一,使其与不能生成向量的点积保持一致;第二,因为叉积通常用于生成在其他地方使用的法线,原始 Vector3 需要保持不变。

[旁注] 手动计算两个向量叉积的快速模板是:

两个向量的点积是一个标量值,由公式定义;

该方程应始终产生一个标量作为结果。

余弦 theta 用于解释向量的方向。Theta 总是取 A 和 B 之间的最小角度(即 )。

公式的右侧是通过使用规则展开和简化左侧得到的:

Cos 0° =1

Cos 90° = 0

C# 代码如下:

public static double DotProduct(Vector3 v1, Vector3 v2)
{
   return
   (
      v1.X * v2.X +
      v1.Y * v2.Y +
      v1.Z * v2.Z
   );
}

及其对应物

public double DotProduct(Vector3 other)
{
   return DotProduct(this, other);
}

除法

向量除以标量(例如 2)是通过将每个分量除以除数 (s2) 来实现的。

public static Vector3 operator/(Vector3 v1, double s2)
{
   return
   (
      new Vector3
      (
         v1.X / s2,
         v1.Y / s2,
         v1.Z / s2
      )
   );
}

扩展功能

我们现在拥有了 Vector3 类型所需的所有基本功能。为了使此类型真正有用,我提供了额外功能。

归一化和单位向量

单位向量的长度为 1。要测试向量是否为单位向量,我们只需将其与已定义的长度方法进行比较,看是否为 1。

public static bool IsUnitVector(Vector3 v1)
{
   return v1.Magnitude == 1;
}

public bool IsUnitVector()
{
   return IsUnitVector(this);
}

由于这是一种比较,我们还提供了一个容差重载。

public bool IsUnitVector(double tolerance)
{
    return IsUnitVector(this, tolerance);
}

public static bool IsUnitVector(Vector3 v1, double tolerance)
{
    return AlmostEqualsWithAbsTolerance(v1.Magnitude, 1, tolerance);
}

归一化是将某个向量转换为单位向量的过程。其公式为:

在实现此公式时,需要处理一些特殊情况:

  • 要归一化的向量没有大小。
  • 要归一化的向量已经是单位向量。
  • 要归一化的向量有一个或多个 NaN 分量 (x,y,z)。
  • 向量具有无限大小,且所有分量都是 (+/-) 无穷大或 (+/-) 0。
  • 向量具有无限大小和实数分量。

从数学上讲,您不能归一化大小为 0 的向量,但是,为了方便程序员,许多向量实现会返回一个零向量 (0,0,0)。类似地,对于列出的所有特殊情况,我在其他库中发现的实现结果各不相同。

我提供的第一个归一化实现会针对特殊情况抛出异常,除非可以提供数学上正确的值,例如 (infinity, 0, 0) 归一化后是 (1,0,0)。

public static Vector3 Normalize(Vector3 v1)
{
    var magnitude = v1.Magnitude;

    // Check that we are not attempting to normalize a vector of magnitude 0
    if (magnitude == 0)
    {
        throw new NormalizeVectorException(NORMALIZE_0);
    }

    // Check that we are not attempting to normalize a vector of magnitude NaN
    if (double.IsNaN(magnitude))
    {
        throw new NormalizeVectorException(NORMALIZE_NaN);
    }

    // Special Cases
    if (double.IsInfinity(v1.Magnitude))
    {
        var x = 
            v1.X == 0 ? 0 : 
                v1.X == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.X) ? 1 : 
                        double.IsNegativeInfinity(v1.X) ? -1 : 
                            double.NaN;
        var y = 
            v1.Y == 0 ? 0 : 
                v1.Y == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.Y) ? 1 : 
                        double.IsNegativeInfinity(v1.Y) ? -1 : 
                            double.NaN;
        var z = 
            v1.Z == 0 ? 0 : 
                v1.Z == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.Z) ? 1 : 
                        double.IsNegativeInfinity(v1.Z) ? -1 : 
                            double.NaN;

        var result = new Vector3(x, y, z);

        // If this wasnt' a special case throw an exception
        if (result.IsNaN())
        {
            throw new NormalizeVectorException(NORMALIZE_Inf);
        }

        // If this was a special case return the special case result
        return result;
    }

    // Run the normalization as usual
    return NormalizeOrNaN(v1);
}

public Vector3 Normalize()
{
    return Normalize(this);
}


private static Vector3 NormalizeOrNaN(Vector3 v1)
{
    // find the inverse of the vectors magnitude
    double inverse = 1 / v1.Magnitude;

    return new Vector3(
        // multiply each component by the inverse of the magnitude
        v1.X * inverse,
        v1.Y * inverse,
        v1.Z * inverse);
}

private const string NORMALIZE_0 = 
    "Cannot normalize a vector when it's magnitude is zero";

private const string NORMALIZE_NaN = 
    "Cannot normalize a vector when it's magnitude is NaN";

我还实现了一个替代方法,它将在异常条件下表现不同。如果大小为 0,NormalizeOrDefault 将返回 (0,0,0) 向量;如果任何分量为 NaN,则返回 (NaN,NaN,NaN) 向量。

public static Vector3 NormalizeOrDefault(Vector3 v1)
{
    /* Check that we are not attempting to normalize a vector of magnitude 1;
       if we are then return v(0,0,0) */
    if (v1.Magnitude == 0)
    {
        return Origin;
    }

    /* Check that we are not attempting to normalize a vector with NaN components;
       if we are then return v(NaN,NaN,NaN) */
    if (v1.IsNaN())
    {
        return NaN;
    }

    // Special Cases
    if (double.IsInfinity(v1.Magnitude))
    {
        var x = 
            v1.X == 0 ? 0 : 
                v1.X == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.X) ? 1 : 
                        double.IsNegativeInfinity(v1.X) ? -1 : 
                            double.NaN;
        var y = 
            v1.Y == 0 ? 0 : 
                v1.Y == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.Y) ? 1 : 
                        double.IsNegativeInfinity(v1.Y) ? -1 : 
                            double.NaN;
                            
        var z = 
            v1.Z == 0 ? 0 : 
                v1.Z == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.Z) ? 1 : 
                        double.IsNegativeInfinity(v1.Z) ? -1 : 
                            double.NaN;

        var result = new Vector3(x, y, z);

        // If this was a special case return the special case result otherwise return NaN
        return result.IsNaN() ? NaN : result;
    }

    // Run the normalization as usual
    return NormalizeOrNaN(v1);
}

public Vector3 NormalizeOrDefault()
{
    return NormalizeOrDefault(this);
}

使用此 Vector3 代码的开发人员应该为其应用程序选择正确的重载。区别在于 Normalize 将需要异常处理程序,但会告诉开发人员异常情况是什么,而 NormalizeOrDefault 方法将允许执行继续,但调用代码必须能够处理异常结果。

绝对值

向量的绝对值是其大小。提供 Abs 方法是为了帮助不了解这两个函数相同的程序员,并提供对大小运算符的静态接口。

public static Double Abs(Vector3 v1)
{
  return v1.Magnitude;
}

public double Abs()
{
  return this.Magnitude;
}

角度

此方法使用归一化和点积来计算两个向量之间的角度。

^ 指的是归一化(单位)向量。
|| 指的是向量的大小。

由于二进制系统中十进制数计算固有的不精确性,应该产生 0 或 1 弧度角的计算结果通常会略有偏差。为了缓解这种情况,添加了相等性测试和 Min 来“将”结果“捕捉”回正确的范围。感谢评论中的 Dennis E. Cox 提出了这个解决方案。

public static double Angle(Vector3 v1, Vector3 v2)
{
    if (v1 == v2)
    {
        return 0;
    }
    
    return 
        Math.Acos(
            Math.Min(1.0f, NormalizeOrDefault(v1).DotProduct(NormalizeOrDefault(v2))));
}

public double Angle(Vector3 other)
{
   return Angle(this, other);
}

背面

此方法将向量解释为面法线,并根据视线向量确定法线是否代表背面平面。背面平面在渲染场景中将不可见,因此可以排除在许多场景计算之外。

如果 则如果

如果 则如果

public static bool IsBackFace(Vector3 normal, Vector3 lineOfSight)
{
    return normal.DotProduct(lineOfSight) < 0;
}

public bool IsBackFace(Vector3 lineOfSight)
{
    return IsBackFace(this, lineOfSight);
}

距离

此方法使用勾股定理计算两个位置向量之间的距离。

public static double Distance(Vector3 v1, Vector3 v2)
{
   return
      Math.Sqrt
      (
          (v1.X - v2.X) * (v1.X - v2.X) +
          (v1.Y - v2.Y) * (v1.Y - v2.Y) +
          (v1.Z - v2.Z) * (v1.Z - v2.Z)
      );
}

public double Distance(Vector3 other)
{
   return Distance(this, other);
}

插值与外推

此方法从两个向量之间获取插值。此方法接受三个参数:起点 (向量 v1)、终点 (向量 v2) 和一个介于 1 和 0 之间的控制分数。控制参数决定了 v1 和 v2 之间的哪个点被取用。控制参数为 0 将返回 v1,控制参数为 1 将返回 v2。

n = n1(1-t) + n2t

n = n1 + t(n2-n1)

n = n1 + tn2 -tn1

其中

n = 当前值
n1 = 初始值 (v1)
n2 = 最终值 (v2)
t = 控制参数,其中 ,其中,


外推,其中控制值大于 1 或小于 0,是允许的,但前提是设置了标志。这允许返回位于穿过 v1 和 v2 的虚线上的向量,但不必位于两者之间。
 

 public static Vector3 Interpolate(
    Vector3 v1, 
    Vector3 v2, 
    double control, 
    bool allowExtrapolation)
{
    if (!allowExtrapolation && (control > 1 || control < 0))
    {
        // Error message includes information about the actual value of the argument
        throw new ArgumentOutOfRangeException(
            "control",
            control,
            INTERPOLATION_RANGE + "\n" + ARGUMENT_VALUE + control);
    }

    return new Vector3(
        v1.X * (1 - control) + v2.X * control,
        v1.Y * (1 - control) + v2.Y * control,
        v1.Z * (1 - control) + v2.Z * control);
}

public static Vector3 Interpolate(Vector3 v1, Vector3 v2, double control)
{
    return Interpolate(v1, v2, control, false);
}

public Vector3 Interpolate(Vector3 other, double control)
{
    return Interpolate(this, other, control);
}

public Vector3 Interpolate(Vector3 other, double control, bool allowExtrapolation)
{
    return Interpolate(this, other, control);
}

private const string INTERPOLATION_RANGE = 
   "Control parameter must be a value between 0 & 1";

最大值和最小值

这些方法比较两个向量的大小,并分别返回具有最大或最小大小的向量。

public static Vector3 Max(Vector3 v1, Vector3 v2)
{
   return v1 >= v2 ? v1 : v2;
}

public Vector3 Max(Vector3 other)
{
   return Max(this, other);
}

public static Vector3 Min(Vector3 v1, Vector3 v2)
{
   return v1 <= v2 ? v1 : v2;
}

public Vector3 Min(Vector3 other)
{
   return Min(this, other);
}

混合积

此方法的代码由 Michał Bryłka 提供。该方法计算三个向量的标量三重积。这是平行六面体几何体的体积。更多信息可在 Wikipedia 上找到。此方法是不可交换的。

public static double MixedProduct(Vector3 v1, Vector3 v2, Vector3 v3)
{
   return DotProduct(CrossProduct(v1, v2), v3);
}

public double MixedProduct(Vector3 other_v1, Vector3 other_v2)
{
   return DotProduct(CrossProduct(this, other_v1), other_v2);
}

垂直

此方法检查两个向量是否垂直(即,如果一个向量是另一个向量的法线)。
 

public static bool IsPerpendicular(Vector3 v1, Vector3 v2)
{
    // Use normalization of special cases to handle special cases of IsPerpendicular
    v1 = NormalizeSpecialCasesOrOrigional(v1);
    v2 = NormalizeSpecialCasesOrOrigional(v2);

    // If either vector is vector(0,0,0) the vectors are not perpendicular
    if (v1 == Zero || v2 == Zero)
    {
        return false;
    }

    // Is perpendicular
    return v1.DotProduct(v2).Equals(0);
}

public bool IsPerpendicular(Vector3 other)
{
    return IsPerpendicular(this, other);
}

// Helpers
private static Vector3 NormalizeSpecialCasesOrOrigional(Vector3 v1)
{
    if (double.IsInfinity(v1.Magnitude))
    {
        var x = 
            v1.X == 0 ? 0 : 
                v1.X == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.X) ? 1 : 
                        double.IsNegativeInfinity(v1.X) ? -1 : 
                            double.NaN;
        var y = 
            v1.Y == 0 ? 0 : 
                v1.Y == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.Y) ? 1 : 
                        double.IsNegativeInfinity(v1.Y) ? -1 : 
                            double.NaN;
        var z = 
            v1.Z == 0 ? 0 : 
                v1.Z == -0 ? -0 : 
                    double.IsPositiveInfinity(v1.Z) ? 1 : 
                        double.IsNegativeInfinity(v1.Z) ? -1 : 
                            double.NaN;

        return new Vector3(x, y, z);
    }

    return v1;
}

Normalize 方法中发现的一些特殊情况逻辑已被提取到一个私有类中,用于这种略有不同的用法。这只是为了代码重用和关注点分离。

public static bool IsPerpendicular(Vector3 v1, Vector3 v2, double tolerance)
{
    // Use normalization of special cases to handle special cases of IsPerpendicular
    v1 = NormalizeSpecialCasesOrOrigional(v1);
    v2 = NormalizeSpecialCasesOrOrigional(v2);

    // If either vector is vector(0,0,0) the vectors are not perpendicular
    if (v1 == Zero || v2 == Zero)
    {
        return false;
    }

    // Is perpendicular
    return v1.DotProduct(v2).AlmostEqualsWithAbsTolerance(0, tolerance);
}

public bool IsPerpendicular(Vector3 other, double tolerance)
{
    return IsPerpendicular(this, other, tolerance);
}

// Helpers
private static bool AlmostEqualsWithAbsTolerance(
    this double a, 
    double b, 
    double maxAbsoluteError)
{
    double diff = Math.Abs(a - b);

    if (a.Equals(b))
    {
        // shortcut, handles infinities
        return true;
    }

    return diff <= maxAbsoluteError;
}

投影与拒绝

向量可以使用以下公式投影到另一个向量上:

Projection Formula

投影是将一个平面上的向量转换到另一个平面上。您可以将其想象为将光线从原始向量的平面照射到一张卡片(目标平面)上。

Projection Depiction

图片来源:Weisstein, Eric W. "Projection." From MathWorld--A Wolfram Web Resource. https://mathworld.net.cn/Projection.html

用两个向量表示,它看起来像这样:

Projection Depiction

在代码中,这是:

public static Vector3 Projection(Vector3 v1, Vector3 v2)
{
    return new Vector3(v2 * (v1.DotProduct(v2) / Math.Pow(v2.Magnitude, 2)));
}

public Vector3 Projection(Vector3 direction)
{
    return Projection(this, direction);
}

拒绝是表示向量投影对其原始值的变化的向量。

Rejection Depiction

其公式很简单:

Rejection Formula

或者

Rejection Formula

在代码中,这是:

public static Vector3 Rejection(Vector3 v1, Vector3 v2)
{
    return v1 - v1.Projection(v2);
}

public Vector3 Rejection(Vector3 direction)
{
    return Rejection(this, direction);
}

反射

将向量关于另一个向量反射,以提供原始向量的镜像。

Reflection Depiction

其公式为:

Reflection Formula

在代码中:

public Vector3 Reflection(Vector3 reflector)
{
    this = Vector3.Reflection(this, reflector);
    return this;
}

public static Vector3 Reflection(Vector3 v1, Vector3 v2)
{
    // if v2 has a right angle to vector, return -vector and stop
    if (Math.Abs(Math.Abs(v1.Angle(v2)) - Math.PI / 2) < Double.Epsilon)
    {
        return -v1;
    }

    Vector3 retval = new Vector3(2 * v1.Projection(v2) - v1);
    return retval.Scale(v1.Magnitude);
}

旋转

绕 x、y、z 轴的欧拉旋转通过 RotateXRotateY RotateZ 方法执行。

虽然这些方法是明确的,但我更喜欢更具上下文的俯仰、偏航和滚转。

Screenshot - imageRotate.gif

Eric__ 评论说他会期望不同的配置。

"... 滚转、俯仰和偏航指代飞机的运动概念。
标准表示法是 X 轴向前(机头方向),Y 轴向右翼,Z 轴向下(水平飞行时指向地球)。
因此,滚转围绕 +X 轴为正,俯仰围绕 +Y 轴为正(向上俯仰表示爬升),偏航围绕 +Z 轴为正(正偏航表示飞机机头向右移动)。"

为了说明他的观点,请考虑以下图表:

Screenshot - real.gif

 

这似乎非常有道理。确实如此!但这仅在考虑单个飞机对象时。当我们考虑具有多个对象的虚拟场景(例如电脑游戏或虚拟现实环境)时,所有对象都必须与感知它们的用户相关。虚拟场景的标准轴是用户沿着 Z 轴向下看。

Screenshot - axisOrient.gif

因此,以飞机为例,我们很有可能在飞机飞入场景时跟随它。
Screenshot - view.gif

希望这解释了轴的描述方式以及俯仰、偏航、滚转配置的原因。

 

俯仰 (RotateX)

RotateX Pitch 通过给定的弧度数(绕 X 轴的欧拉旋转)旋转向量。

斜边 (R) 在等式中抵消。

public static Vector3 RotateX(Vector3 v1, double rad)
{
    double x = v1.X;
    double y = (v1.Y * Math.Cos(rad)) - (v1.Z * Math.Sin(rad));
    double z = (v1.Y * Math.Sin(rad)) + (v1.Z * Math.Cos(rad));
    return new Vector3(x, y, z);
}

public Vector3 RotateX(double rad)
{
    return RotateX(this, rad);
}

public static Vector3 Pitch(Vector3 v1, double rad)
{
    return RotateX(v1, rad);
}
        
public Vector3 Pitch(double rad)
{
    return Pitch(this, rad);
}

偏航 (RotateY)

RotateY Yaw 以给定的度数(绕 Y 轴的欧拉旋转)旋转向量。

斜边 (R) 在等式中抵消。

public static Vector3 RotateY(Vector3 v1, double rad)
{
    double x = (v1.Z * Math.Sin(rad)) + (v1.X * Math.Cos(rad));
    double y = v1.Y;
    double z = (v1.Z * Math.Cos(rad)) - (v1.X * Math.Sin(rad));
    return new Vector3(x, y, z);
}

public Vector3 RotateY(double rad)
{
    return RotateY(this, rad);
}
        
public static Vector3 Yaw(Vector3 v1, double rad)
{
    return RotateY(v1, rad);
}

public Vector3 Yaw(double rad)
{
    return Yaw(this, rad);
}

滚转 (RotateZ)

RotateZ Roll 通过给定的弧度数(绕 Z 轴的欧拉旋转)旋转向量。

斜边 (R) 在等式中抵消。

public static Vector3 RotateZ(Vector3 v1, double rad)
{
    double x = (v1.X * Math.Cos(rad)) - (v1.Y * Math.Sin(rad));
    double y = (v1.X * Math.Sin(rad)) + (v1.Y * Math.Cos(rad));
    double z = v1.Z;
    return new Vector3(x, y, z);
}

public Vector3 RotateZ(double rad)
{
    return RotateZ(this, rad);
}

public static Vector3 Roll(Vector3 v1, double rad)
{
    return RotateZ(v1, rad);
}

public Vector3 Roll(double rad)
{
    return Roll(this, rad);
}

任意旋转

已经实现了绕指定点旋转向量的方法。这也可以认为是旋转前对轴进行偏移。

public static Vector3 RotateX(Vector3 v1, double yOff, double zOff, double rad)
{
    double x = v1.X;
    double y = 
        (v1.Y * Math.Cos(rad)) - (v1.Z * Math.Sin(rad)) + 
        (yOff * (1 - Math.Cos(rad)) + zOff * Math.Sin(rad));
    double z = 
        (v1.Y * Math.Sin(rad)) + (v1.Z * Math.Cos(rad)) + 
        (zOff * (1 - Math.Cos(rad)) - yOff * Math.Sin(rad));
    return new Vector3(x, y, z);
}

public Vector3 RotateX(double yOff, double zOff, double rad)
{
    return RotateX(this, yOff, zOff, rad);
}

public static Vector3 RotateY(Vector3 v1, double xOff, double zOff, double rad)
{
    double x = 
        (v1.Z * Math.Sin(rad)) + (v1.X * Math.Cos(rad)) + 
        (xOff * (1 - Math.Cos(rad)) - zOff * Math.Sin(rad));
    double y = v1.Y;
    double z = 
        (v1.Z * Math.Cos(rad)) - (v1.X * Math.Sin(rad)) + 
        (zOff * (1 - Math.Cos(rad)) + xOff * Math.Sin(rad));
    return new Vector3(x, y, z);
}

public Vector3 RotateY(double xOff, double zOff, double rad)
{
    return RotateY(this, xOff, zOff, rad);
}

public static Vector3 RotateZ(Vector3 v1, double xOff, double yOff, double rad)
{
    double x = 
        (v1.X * Math.Cos(rad)) - (v1.Y * Math.Sin(rad)) + 
        (xOff * (1 - Math.Cos(rad)) + yOff * Math.Sin(rad));
    double y = 
        (v1.X * Math.Sin(rad)) + (v1.Y * Math.Cos(rad)) + 
        (yOff * (1 - Math.Cos(rad)) - xOff * Math.Sin(rad));
    double z = v1.Z;
    return new Vector3(x, y, z);
}

public Vector3 RotateZ(double xOff, double yOff, double rad)
{
    return RotateZ(this, xOff, yOff, rad);
}

四舍五入

这些方法将向量的分量四舍五入到

  • 最接近的整数值
public static Vector3 Round(Vector3 v1)
{
    return new Vector3(Math.Round(v1.X), Math.Round(v1.Y), Math.Round(v1.Z));
}

public static Vector3 Round(Vector3 v1, MidpointRounding mode)
{
    return new Vector3(
        Math.Round(v1.X, mode), 
        Math.Round(v1.Y, mode), 
        Math.Round(v1.Z, mode));
}

public Vector3 Round()
{
    return new Vector3(Math.Round(this.X), Math.Round(this.Y), Math.Round(this.Z));
}

public Vector3 Round(MidpointRounding mode)
{
    return new Vector3(
        Math.Round(this.X, mode), 
        Math.Round(this.Y, mode), 
        Math.Round(this.Z, mode));
}
  • 指定的小数位数
public static Vector3 Round(Vector3 v1, int digits)
{
    return new Vector3(
        Math.Round(v1.X, digits), 
        Math.Round(v1.Y, digits), 
        Math.Round(v1.Z, digits));
}

public static Vector3 Round(Vector3 v1, int digits, MidpointRounding mode)
{
    return new Vector3(
        Math.Round(v1.X, digits, mode), 
        Math.Round(v1.Y, digits, mode), 
        Math.Round(v1.Z, digits, mode));
}

public Vector3 Round(int digits)
{
    return new Vector3(
        Math.Round(this.X, digits), 
        Math.Round(this.Y, digits), 
        Math.Round(this.Z, digits));
}

public Vector3 Round(int digits, MidpointRounding mode)
{
    return new Vector3(
        Math.Round(this.X, digits, mode), 
        Math.Round(this.Y, digits, mode), 
        Math.Round(this.Z, digits, mode));
}

缩放

此方法在不改变方向的情况下将向量的大小更改为指定值。

public static Vector3 Scale(Vector3 vector, double magnitude)
{
    if (magnitude < 0)
    {
        throw new ArgumentOutOfRangeException("magnitude", magnitude, NEGATIVE_MAGNITUDE);
    }

    if (vector == new Vector3(0, 0, 0))
    {
        throw new ArgumentException(ORIGIN_VECTOR_MAGNITUDE, "vector");
    }

    return vector * (magnitude / vector.Magnitude);
}

public Vector3 Scale(double magnitude)
{
    return Vector3.Scale(this, magnitude);
}

private const string NEGATIVE_MAGNITUDE = 
    "The magnitude of a Vector3 must be a positive value, (i.e. greater than 0)";
    
private const string ORIGIN_VECTOR_MAGNITUDE = 
    "Cannot change the magnitude of Vector3(0,0,0)";

分量函数

我提供了许多针对向量分量的函数。这些函数对于整个向量来说在数学上是无效的。例如,没有将向量提升到幂的概念(据我所知),但是 PowComponents 方法可用于将 x、y、z 的每个分量提升到给定幂。

分量求和

此方法只是将向量分量 (x, y, z) 相加。

public static double SumComponents(Vector3 v1)
{
   return (v1.X + v1.Y + v1.Z);
}

public double SumComponents()
{
   return SumComponents(this);
}

次方

此方法将向量分量乘以给定幂。

public static Vector3 PowComponents(Vector3 v1, double power)
{
   return
      new Vector
      (
         Math.Pow(v1.X, power),
         Math.Pow(v1.Y, power),
         Math.Pow(v1.Z, power)
      );
}

public void PowComponents(double power)
{
   this = PowComponents(this, power);
}

平方根

此方法将平方根函数应用于向量的每个分量。

public static Vector3 SqrtComponents(Vector3 v1)
{
   return
   (
      new Vector3
      (
         Math.Sqrt(v1.X),
         Math.Sqrt(v1.Y),
         Math.Sqrt(v1.Z)
      )
   );
}

public void SqrtComponents()
{
   this = SqrtComponents(this);
}

平方

此方法将向量的每个分量进行平方。

public static Vector3 SqrComponents(Vector3 v1)
{
    return
    (
       new Vector3
       (
           v1.X * v1.X,
           v1.Y * v1.Y,
           v1.Z * v1.Z
       )
     );
}

public void SqrComponents()
{
   this = SqrtComponents(this);
}

平方和

此方法计算向量每个分量的平方和。

public static double SumComponentSqrs(Vector3 v1)
{
   Vector3 v2 = SqrComponents(v1);
   return v2.SumComponents();
}

public double SumComponentSqrs()
{
   return SumComponentSqrs(this);
}

非数字 (Not A Number)

如果任何分量 (x,y,z) 是 NaN,则整个向量应被视为“非数字”。如果任何分量 (x,y,z) 是 NaN,此方法返回 true。

public static bool IsNaN(Vector3 v1)
{
   return double.IsNaN(v1.X) || double.IsNaN(v1.Y) || double.IsNaN(v1.Z);
}

public bool IsNaN()
{
   return IsNaN(this);
}

可用性函数

我们已经看到了 .Net 接口 IComparableIComparable<Vector3>IEquatable<Vector3> 的实现。为了完整起见,还实现了 IFormattable。由接口定义的方法 ToString 返回类型的文本描述。VerbString 也已提供,以提供详细的文本描述。ToString 可以接受数字格式字符串,其前面可选地加上字符 x、y 或 z,指示要描述的相关向量分量。

public string ToVerbString()
{
   string output = null;
   
   if (IsUnitVector()) 
   { 
      output += UNIT_VECTOR; 
   } 
   else 
   { 
      output += POSITIONAL_VECTOR; 
   } 
   
   output += string.Format("( x={0}, y={1}, z={2})", X, Y, Z); 
   output += MAGNITUDE + Magnitude; 
   return output; 
} 

private const string UNIT_VECTOR = 
   "Unit vector composing of "; 

private const string POSITIONAL_VECTOR = 
   "Positional vector composing of "; 

private const string MAGNITUDE = 
   " of magnitude "; 

public string ToString(string format, IFormatProvider formatProvider) 
{ 
   // If no format is passed 
   if (format == null || format == "") 
      return String.Format("({0}, {1}, {2})", X, Y, Z); 

   char firstChar = format[0]; 
   string remainder = null; 

   if (format.Length > 1) 
      remainder = format.Substring(1);

   switch (firstChar) 
   { 
      case 'x': 
         return X.ToString(remainder, formatProvider); 
      case 'y': 
         return Y.ToString(remainder, formatProvider); 
      case 'z': 
         return Z.ToString(remainder, formatProvider); 
      default: 
         return 
            String.Format
            (
               "({0}, {1}, {2})", 
               X.ToString(format, formatProvider), 
               Y.ToString(format, formatProvider), 
               Z.ToString(format, formatProvider) 
            ); 
   } 
}

public override string ToString() 
{ 
   return ToString(null, null); 
}

标准笛卡尔向量和常量

最后定义了四个标准向量常量:

public static readonly Vector3 Origin = new Vector3(0,0,0);
public static readonly Vector3 XAxis = new Vector3(1,0,0);
public static readonly Vector3 YAxis = new Vector3(0,1,0);
public static readonly Vector3 ZAxis = new Vector3(0,0,1);

以及各种只读值:

public static readonly Vector3 MinValue = 
   new Vector3(Double.MinValue, Double.MinValue, Double.MinValue);

public static readonly Vector3 MaxValue = 
   new Vector3(Double.MaxValue, Double.MaxValue, Double.MaxValue);

public static readonly Vector3 Epsilon = 
   new Vector3(Double.Epsilon, Double.Epsilon, Double.Epsilon);

public static readonly Vector3 Zero = 
   Origin;

public static readonly Vector3 NaN = 
   new Vector3(double.NaN, double.NaN, double.NaN);

序列化

Vector3 实现了 [Serializable] 属性,因此可以写入文件。我建议以下内容:

static void Main(string[] args)
{
   Vector3 vect = new Vector3(1, 2, 3);
   XmlSerializer x = new XmlSerializer(vect.GetType());
   x.Serialize
   (
      new System.IO.FileStream("test.xml", System.IO.FileMode.Create), 
      vect
   );
}

它生成一个包含以下内容的 XML 文件:

<?xml version="1.0"?>
<Vector
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <X>1</X>
   <Y>2</Y>
   <Z>3</Z>
   <Magnitude>3.7416573867739413</Magnitude>
</Vector>

总结

我们现在拥有一个具有以下公共功能的 Vector3 类型:

 

ClassDiagram

构造函数

  • Vector3(double x, double y, double z)
  • Vector3(double[] xyz)
  • Vector3(Vector v1)

属性

  • 数组
  • X
  • Y
  • Z

运算符

  • this[] 索引器
  • +
  • -
  • ==
  • !=
  • *
  • /
  • <
  • >
  • <=
  • >=

静态方法

  • CrossProduct
  • DotProduct
  • MixedProduct
  • Normalize
  • IsUnitVector
  • Interpolate
  • Distance
  • Abs
  • Angle
  • 最大值
  • 最小值
  • Yaw
  • Pitch
  • Roll(滚动)
  • IsBackFace
  • IsPerpendicular
  • SumComponents
  • SumComponentSqrs
  • SqrComponents
  • SqrtComponents
  • PowComponents

方法

  • Abs
  • Angle
  • CrossProduct
  • Distance
  • DotProduct
  • Interpolate
  • IsBackFace
  • IsNaN
  • IsPerpendicular
  • IsUnitVector
  • 最大值
  • 最小值
  • MixedProduct
  • Normalize
  • Pitch
  • PowComponents
  • Projection
  • Reflection(反射)
  • Rejection
  • Roll(滚动)
  • RotateX
  • RotateY
  • RotateZ
  • Round
  • Scale
  • SqrComponents
  • SqrtComponents
  • SumComponentSqrs
  • ToVerbString
  • Yaw

实现接口的方法

  • CompareTo
  • Equals
  • ToString
  • GetHashCode

只读和常量值

  • Epsilon
  • 最大值
  • 最小值
  • NaN
  • Origin
  • xAxis
  • yAxis
  • zAxis
  • Zero

比较

对 .Net 中支持向量的一些常用库的常见功能进行快速比较。本文的代码执行速度似乎处于选择的中间位置。

一些更快的库使用浮点数而不是双精度浮点数,并且应该注意的是,Math.Net 的叉积结果很可能是对库的误解,应该忽略。

Vector Type Comparison Chart

兴趣点

在本文和提供的源代码开发过程中,我使用了许多资源,我谨此感谢以下内容:

历史

(v1.00-v1.20)

  • Magnitude 方法现在封装在属性中
  • 已删除不正确的序列化属性
  • Equality IsUnitVector 方法允许容差
  • Abs 方法现在返回大小
  • 已实现泛型 IEquatable IComparable 接口
  • 已实现 IFormattable 接口
  • 实现了混合积函数
  • 添加并重命名了基于组件的附加函数(例如 SumComponentSquares
  • Vector 重命名为 Vector3

(v1.20-v1.30)

  • Square components 方法指向了平方根静态方法
  • 添加了关于两个向量比较操作含义的注释
  • 将对度的引用更改为弧度
  • 更改了 Angle 的实现以帮助避免 NaN 结果
  • Vector3 类现在是不可变的,已删除属性设置器,可变方法返回一个新的 Vector3
  • 更新了注释,以便在 Intellisense 下更好地阅读。
  • 添加了 ProjectionRejection Reflection 操作
  • 将操作分解为区域
  • 添加了缩放操作,以前可以通过大小可变属性访问
  • Equals(object) 方法更改为使用 Equals(Vector3)
  • 添加了分量舍入操作
  • 添加了绕 x、y 或 z 轴旋转(带或不带轴偏移)
  • 添加了带有绝对容差参数的 Equals 重载
  • 添加了带有绝对容差参数的 IsUnitVector 重载
  • 添加了带有绝对容差参数的 IsPerpendicular 重载
  • 添加了带有绝对容差参数的 CompareTo 重载
  • 修复了 CompareTo 方法中的无穷大问题
  • 单元测试证明 NaN == NaNNan.Equals(NaN) 不同 - 修改了 .Equals 方法以与 .Net 框架保持一致
  • 添加了 IsNaN 方法
  • 添加了 NormalizeOrDefault 方法
  • 添加了可以归一化具有无限分量的向量的特殊情况。
  • IsPerpendicular 现在使用 NormalizeOrDefault 来处理无穷大的特殊情况。
© . All rights reserved.