既然我们已经解释了(笛卡尔)坐标系的概念(以及点和向量的坐标与坐标系的关系),我们可以看看对点和向量执行的一些最常见的操作。这应该是任何3D应用程序和渲染器中最常见的操作。
C++向量类(Vector)
首先,让我们定义我们的C++Vector类:
template<typename T>
class Vec3
{
public:
// 3 most basic ways of initializing a vector
Vec3() : x(T(0)), y(T(0)), z(T(0)) {}
Vec3(const T &xx) : x(xx), y(xx), z(xx) {}
Vec3(T xx, T yy, T zz) : x(xx), y(yy), z(zz) {}
T x, y, z;
};
向量长度
如前一段所述,向量可以看作是从一个点开始到另一个点结束的箭头。向量本身不仅指示点B从A的方向,还可以用来找到A和B之间的距离。这是由向量的长度给出的,可以很容易地用以下公式计算:
||V|| =sqrt(V.x * V.x + V.y * V.y + V.z * V.z)
在数学中,双杠(||V||)表示法表示向量的长度。向量的长度有时也称为范数或幅度(图1)。
template<typename T>
class Vec3
{
public:
...
// length can be a method from the class...
T length()
{
return sqrt(x * x + y * y + z * z);
}
...
};
// ... or you can also compute the length in a function that is not part of the class
template<typename T>
T length(const Vec3<T> &v)
{ return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }
请注意,三维笛卡尔坐标系的轴是单位向量。
标准化/归一化向量
标准化的向量是一个长度为1的向量(图1中的向量B)。这样的向量也被称为单位向量(具有单位长度的向量)。对向量进行归一化非常简单。我们首先计算向量的长度,并将每个向量的坐标除以这个长度。数学符号是:
图1:向量A和B的大小或长度用双条形符号表示。归一化向量是长度为1的向量(在本例中为向量B)。
请注意,C++实现是可以优化的。首先,我们只在向量长度大于0时对其进行归一化(因为禁止除以0)。然后,我们计算一个临时变量,即向量长度的倒数,并将向量的每个坐标与该值相乘,而不是将其除以向量的长度。正如你可能知道的那样,程序中的乘法运算比除法运算成本更低。这种优化可能很重要,因为规范化向量是渲染器中的一项广泛操作,可以应用于数千、数十万或数百万个向量。当然,任何可能的优化都会影响该级别的最终渲染时间。然而,有些编译器会在后台为您处理这些问题。但是,您可以在代码中明确地进行优化。
template<typename T>
class Vec3
{
public:
...
// as a method of the class Vec3
Vec3<T>& normalize()
{
T len = length();
if (len > 0) {
T invLen = 1 / len;
x *= invLen, y *= invLen, z *= invLen;
}
return *this;
}
...
};
// or as a utility function
template<typename T>
void normalize(Vec3<T> &v)
{
T len2 = v.x * v.x + v.y * v.y + v.z * v.z;
// avoid division by 0
if (len2 > 0) {
T invLen = 1 / sqrt(len2);
x *= invLen, y *= invLen, z *= invLen;
}
}
在数学中,你还可以找到范数(norm)这个术语来定义一个函数,该函数为向量指定长度或大小(或距离)。例如,我们刚刚描述的函数被称为欧几里得范数。
点积
图2:两个向量的点积可以看作A在B上的投影。如果两个向量A和B是单位长度(长度为1),则点积的结果是两个向量所夹角度的余弦。
点积或标量积需要两个向量A和B,可以看作是一个向量投影到另一个向量上。点积的结果是一个实数(在编程中是浮点或双精度)。两个向量之间的点积用点符号表示:A · B。点积包括将A向量的每个元素与向量B的对应元素相乘,并取每个乘积的和。在3D向量的情况下(向量的长度为3,它们有三个系数或元素,即x、y和z),它由以下操作组成:
A dot B = A.x * B.x + A.y * B.y + A.z * B.z
请注意,这与我们计算向量长度(这次是距离)的方式非常相似。如果我们取平方根(sqrt(A·B))两个相等向量(A=B)之间的点积,那么我们得到的是向量的长度。我们可以写:
它可以用于向量的标准化:
template<typename T>
class Vec3
{
public:
...
T dot(const Vec3<T> &v) const
{
return x * v.x + y * v.y + z * v.z;
}
Vec3<T>& normalize()
{
T len2 = dot(*this);
if (len2 > 0) {
T invLen = 1 / sqrt(len2);
x *= invLen, y *= invLen, z *= invLen;
}
return *this;
}
...
};
template<typename T>
T dot(const Vec3<T> &a, const Vec3<T> &b)
{ return a.x * b.x + a.y * b.y + a.z * b.z; }
两个向量之间的点积在任何3D应用中都是必不可少的和常见的运算,因为该运算的结果与两个向量间角度的余弦有关。图2说明了点积的几何解释。在这个例子中,矢量A被投影在矢量B的方向上。
如果B是单位向量,那么乘积A·B等于||A||cos(θ),结果是A在B方向上投影的大小,如果方向相反,则使用减号。这被称为A到B的标量投影。
当A和B都不是单位向量时,我们可以这样写A·B/||B||,因为作为单位向量的B是B/||B||
当两个向量被归一化时,取点积的反余弦得到角度间:
或
点积是3D中必不可少的操作。它可以用于许多事情,例如,作为正交性的测试。当两个向量彼此垂直(A.B)时,这两个向量之间的点积的结果为0。当两个矢量指向相反的方向(A.C)时,点积返回-1。当它们指向同一方向(A.D)时,它将返回1。它还被广泛用于找出两个向量之间的角度或计算向量与坐标系轴之间的角度(当向量的坐标转换为球面坐标时,这很有用。这在三角函数一章中有解释)。
叉积
叉积也是对两个向量的运算,但对于返回数字的点积,叉积返回一个向量。这种运算的特殊性在于,叉积产生的向量垂直于其他两个向量(如图3所示)。叉积运算使用以下公式:
C=AxB
图3:两个向量A和B的叉积给出了一个垂直于A和B定义的平面的向量C。当A和B彼此正交(并且具有单位长度)时,A、B和C形成笛卡尔坐标系。
要计算叉积,我们需要实现以下公式:
叉积的结果是与其他两个向量正交的另一个向量。两个向量之间的叉积用叉号表示:AxB 两个向量A和B定义了一个平面,并且得到的向量C垂直于该平面。向量A和B不必彼此垂直,但当它们垂直时,得到的A、B和C向量形成笛卡尔坐标系(假设向量具有单位长度)。这对于创建坐标系特别有用,我们将在创建局部坐标系一章中对此进行解释。
template<typename T>
class Vec3
{
public:
...
// as a method of the class...
Vec3<T> cross(const Vec3<T> &v) const
{
return Vec3<T>(
y * v.z - z * v.y,
z * v.x - x * v.z,
x * v.y - y * v.x);
}
...
};
// or as a utility function
template<typename T>
Vec3<T> cross(const Vec3<T> &a, const Vec3<T> &b)
{
return Vec3<T>(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x);
}
假设你需要一种记忆这个公式的方法。在这种情况下,我们喜欢使用这样的技术,即问自己“为什么是z?”,y和z是用于计算结果向量C的x坐标的向量A和B的坐标(因为“为什么是z?”——当然,这里的“为什么”代表字母“y”)。更重要的是,逻辑可以很容易地用来重建这个公式。既然你知道叉积的结果是一个垂直于其他两个的向量,你就知道如果A和B是笛卡尔坐标系的x轴和y轴,那么A和B的叉积应该给出z轴,即(0,0,1)。得到这个结果的唯一方法是Cz=1,只有当Cz=A.x*B.y-A.y*B.x时才成立。你可以推导出用于计算Cx和Cy的其他坐标。最后,最简单的方法可能是用以下形式编写叉积运算:
然而
以列向量的形式表示向量表明,要找到结果向量的任何坐标(例如,x),我们需要使用向量A和B中的其他两个坐标(如果x是我们希望计算的坐标,则为y和z)。
需要注意的是,叉积中涉及的向量的顺序会影响生成的向量C。如果我们举前面的例子(以笛卡尔坐标系的x轴和y轴之间的叉积为例),你可以看到A x B并没有给出与B x A相同的结果:
图4:当食指指向A,中指指向B时,用左手或右手确定向量C的方向(例如法线)。
图5:用右手,你可以将食指沿着A或B对齐,将中指对准另一个向量(B或A),以确定C(例如法线)在右手坐标系中是向上还是向下。
我们说叉积是反交换的(交换任意两个参数的位置会得到相反的结果):如果AxB=C,那么BxA=-C。请记住上一章中的内容,当使用两个向量来定义坐标系的前两个基点时,第三个向量可以指向平面的任一侧。我们还介绍了如何用手来区分这两个系统。当你计算向量之间的叉积时,你总是会得到相同的唯一解。例如,如果A=(1,0,0)和B=(0,1,0),则C只能是(0,0,1)。那么,我为什么要关心坐标系的用手习惯呢?因为如果计算结果总是相同的,那么绘制结果向量的方式取决于坐标系的用手习惯。您可以使用相同的助记符技术来确定向量应该指向哪个方向,这取决于您的约定。在右手坐标系的情况下,如果食指沿着A向量(例如,曲面上某点的切线)对齐,中指沿着B向量(如果试图计算法线的方向,则B为副切线)对齐时,拇指将指向C向量(法线)的方向。请注意,如果使用相同的技术,但左手位于相同的向量A和B上,则拇指将指向相反的方向。不过,请记住,这只是一个代表性的问题。
在数学中,叉积的结果被称为伪向量。当从计算法线的点处的切线和副切线计算曲面法线时,叉积运算中向量的顺序至关重要。根据此顺序,生成的法线可以指向曲面内部(向内指向法线),也可以远离曲面(向外指向法线)。您可以在创建方向矩阵一章中找到有关此主题的更多信息。
向量/点的加法和减法
对点的其他数学运算通常很简单。例如,一个向量与一个标量或另一个向量的乘积给出一个点。我们可以将两个向量相加、相减、除法等。请注意,一些3D API区分点、法线和向量。从技术上讲,它们之间存在细微的差异,这可以证明创建三个独立的C++类是合理的。例如,法线不是像点和向量那样变换的(我们将在本课中了解这一点),从技术上讲,两个点相减会得到一个向量,将一个向量加上另一个向量会得到一个向量,等等。然而,从实践中,我们发现,编写这三个不同的C++类来表示每种类型并不值得。与已经成为行业标准的OpenEXR类似,我们选择用一个名为Vec3的模板化类来描述所有类型。因此,我们不区分法线、向量和点(从编码的角度来看)。当表示不同类型(法线、向量、点)但在泛型类型Vec3下声明的变量应该以不同的方式处理时,我们需要管理(罕见的)异常。以下是一些C++代码,用于表示最常见的操作(您将在本课程结束时找到完整的源代码):
template<typename T>
class Vec3
{
public:
...
Vec3<T> operator + (const Vec3<T> &v) const
{ return Vec3<T>(x + v.x, y + v.y, z + v.z); }
Vec3<T> operator - (const Vec3<T> &v) const
{ return Vec3<T>(x - v.x, y - v.y, z - v.z); }
Vec3<T> operator * (const T &r) const
{ return Vec3<T>(x * r, y * r, z * r); }
...
};