C# 中的向量类型
通过 C# 中的笛卡尔和欧几里得几何来指导向量类型
- 下载源代码 - 285KB
- 最新源代码可在GitHub 上找到,但可能与本文不符。
引言
多年来,我看到人们在向量数学方面苦苦挣扎。本指南将引导您了解如何在 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;
}
投影与拒绝
向量可以使用以下公式投影到另一个向量上:
投影是将一个平面上的向量转换到另一个平面上。您可以将其想象为将光线从原始向量的平面照射到一张卡片(目标平面)上。
图片来源:Weisstein, Eric W. "Projection." From MathWorld--A Wolfram Web Resource. https://mathworld.net.cn/Projection.html
用两个向量表示,它看起来像这样:
在代码中,这是:
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);
}
拒绝是表示向量投影对其原始值的变化的向量。
其公式很简单:
或者
在代码中,这是:
public static Vector3 Rejection(Vector3 v1, Vector3 v2)
{
return v1 - v1.Projection(v2);
}
public Vector3 Rejection(Vector3 direction)
{
return Rejection(this, direction);
}
反射
将向量关于另一个向量反射,以提供原始向量的镜像。
其公式为:
在代码中:
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 轴的欧拉旋转通过 RotateX
、RotateY
和 RotateZ
方法执行。
虽然这些方法是明确的,但我更喜欢更具上下文的俯仰、偏航和滚转。
Eric__ 评论说他会期望不同的配置。
"... 滚转、俯仰和偏航指代飞机的运动概念。
标准表示法是 X 轴向前(机头方向),Y 轴向右翼,Z 轴向下(水平飞行时指向地球)。
因此,滚转围绕 +X 轴为正,俯仰围绕 +Y 轴为正(向上俯仰表示爬升),偏航围绕 +Z 轴为正(正偏航表示飞机机头向右移动)。"
为了说明他的观点,请考虑以下图表:
这似乎非常有道理。确实如此!但这仅在考虑单个飞机对象时。当我们考虑具有多个对象的虚拟场景(例如电脑游戏或虚拟现实环境)时,所有对象都必须与感知它们的用户相关。虚拟场景的标准轴是用户沿着 Z 轴向下看。
因此,以飞机为例,我们很有可能在飞机飞入场景时跟随它。
希望这解释了轴的描述方式以及俯仰、偏航、滚转配置的原因。
俯仰 (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 接口 IComparable
、IComparable<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
类型:
![]() |
构造函数
属性
运算符
静态方法
方法
实现接口的方法
只读和常量值
|
比较
对 .Net 中支持向量的一些常用库的常见功能进行快速比较。本文的代码执行速度似乎处于选择的中间位置。
一些更快的库使用浮点数而不是双精度浮点数,并且应该注意的是,Math.Net 的叉积结果很可能是对库的误解,应该忽略。
兴趣点
在本文和提供的源代码开发过程中,我使用了许多资源,我谨此感谢以下内容:
- CSOpenGL 项目 - Lucas Viñas Livschitz
- Exocortex 项目 - Ben Houston
- 计算机图形学基础数学 - John Vince (ISBN 1-85233-380-4)
- Wolfram Mathworld 网站
历史
(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 下更好地阅读。
- 添加了
Projection
、Rejection
和Reflection
操作 - 将操作分解为区域
- 添加了缩放操作,以前可以通过大小可变属性访问
- 将
Equals(object)
方法更改为使用Equals(Vector3)
- 添加了分量舍入操作
- 添加了绕 x、y 或 z 轴旋转(带或不带轴偏移)
- 添加了带有绝对容差参数的
Equals
重载 - 添加了带有绝对容差参数的
IsUnitVector
重载 - 添加了带有绝对容差参数的
IsPerpendicular
重载 - 添加了带有绝对容差参数的
CompareTo
重载 - 修复了
CompareTo
方法中的无穷大问题 - 单元测试证明
NaN == NaN
与Nan.Equals(NaN)
不同 - 修改了 .Equals 方法以与 .Net 框架保持一致 - 添加了
IsNaN
方法 - 添加了
NormalizeOrDefault
方法 - 添加了可以归一化具有无限分量的向量的特殊情况。
IsPerpendicular
现在使用NormalizeOrDefault
来处理无穷大的特殊情况。