CG中的几何学——与点和向量相关的计算方式【3】

来源:互联网 发布:伦铜库存数据 编辑:程序博客网 时间:2024/06/05 16:30

原文地址:http://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/math-operations-on-points-and-vectors

到目前为止,我们已经详细介绍了坐标系以及点和向量的坐标所表明的信息。现在让我们学习一些可以对点和向量使用的计算。这些计算在任何一个3D应用和渲染器中都会见到。

Vector Class in C++(C++中的向量)

还记得之前我们写的那个用于表示向量的C++模版类吗?忘了也没关系,我们现在把他写到这里来:

template<typename T>class Vec3{public:    //3中最基本的初始化向量的方式    Vec3():x(T(0)),y(T(0)),z(T(0)){}    Vec3(const T &xx):x(xx.x),y(xx.y),z(xx.z){}    Vec3(T xx,T yy,T zz):x(xx),y(yy),z(zz){}    T x,y,z;};

Vector Length(向量的长度)

就像之前我们说过的,一个向量可以看成是从一个点开始指向另一个点结束的箭头。向量不仅说明了从点A到点B的方向,同时也说明了点A和点B之间的距离。向量的长度可以很容易的用下面的公式计算出来:
这里写图片描述
其中V就是我们的向量。在数学领域,两条竖线的记号表示向量的长度。向量的长度有时候也会被称为向量的大小。
现在让我们更加仔细地完成这个模版类吧~

template<typename T>class Vec3{public:    ...    //求长度的函数可以放到这个模版类之内    T length()    {        return sqrt(x*x+y*y+z*z);    }    ...};//当然也可以把求向量长度的函数放到类外面template<typename T>T length(const Vec3<T> &v){    return sqrt(v.x*v.x + v.y*v.y + v.z*v.z);}

向量的单位化(Normalizing a Vector)

这里写图片描述
单位化的向量的长度为1,就像上图中的B向量。这种向量也叫做单位向量。把一个向量单位化是很简单的操作,我们只需要先算出向量的长度,然后将向量的每一个坐标除以这个长度就得到单位化的向量了。
这里写图片描述
通过这个公式我们可以对C++代码进行进一步的优化。我们只会单位化长度超过0的向量,因为用一个数除以0是没有意义的。我们先计算出一个临时变量,这个变量的值是向量长度的倒数。然后用这个倒数与向量的坐标分别做乘法。为什么我们要这样做呢?因为在对计算机来说,做乘法的速度比除法快一些。这种优化是非常重要的,因为单位化这种操作在渲染图像的过程中会非常频繁的被用到。有多频繁呢?可能是成千上万次!所以这个小小的优化也会对最终的渲染图像的速度产生巨大的影响。虽然有些编译器会默默的在后台帮我们优化除法运算,但是请在代码中严格的使用这种优化。

template<typename T>class Vec3{public:    ...    //作为类的成员函数    Vec3<T>& normalize()    {        T len = length();        if(len > 0){            T invLen = 1/len;            x*=invLen,y*=invLen,z*=invLen;        }        return *this;    }    ...};//作为类外的方法template<typename T>void normalize(Vec3<T> &v){    T len2 = v.x*v.x + v.y*v.y + v.z*v.z;    //防止除0    if(len2 > 0){        T invLen = 1/sqrt(len2);        x *= invLen,y *= invLen, z *= invLen;    }}

点积(Dot Product)

点积(或者叫无向积)需要用两个向量来计算,得到的结果是一个向量投影到另一个向量上的长度。也就是说,两个向量的点积得到的结果是一个实数。
我们通过在两个向量中间放置一个点的方式来表示向量之间的点积: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应用里面扮演着异常重要的角色,因为两个向量的点积和这两个向量夹角的余弦有着直接的关系。
下面这张图详细的说明了这种关系:
这里写图片描述
如果B是单位向量,那么A·B得到的结果就是||A||cos(ø),这就是向量A投影的到向量B上的长度。如果ø是钝角,那么A投影到B向量上的长度就是个负数。
若果A和B向量都不是单位向量,我们可以用这个算式:A·B/||B||来计算AdaoB上的无向投影的大小。
若A和B都是单位向量,则cos(ø) = A·B,ø是A与B的夹角。通过使用反余弦函数,可以很轻易的得到夹角的弧度制表示。

点积是3D图形学中很重要的操作,我们可以用这种运算方式来完成很多的图形处理。在正交测试中,当两个向量互相垂直时,这两个向量的点积会得到0。当两个向量指向相反的方向时,点积的结果是-1。当两个向量指向同一个方向,它们点积的结果是1(以上结论建立在向量A,B,C,D都是单位向量的前提下,如下图)。
这里写图片描述
我们也会经常用点积来计算两个向量之间的夹角大小,或者是计算向量和坐标轴的夹角的大小。

叉积(cross product)

叉积也是对两个向量的操作,不过与计算结果是一数的点积不同,两个向量的叉积也是一个向量。叉积计算出的这个向量会同时与这两个向量垂直。在下面这张图片中,A和B向量的叉积得出了一个向量C,这个向量同时与A和B垂直。如果A和B向量也互相垂直,那么A,B以及C共同构成了一个笛卡尔坐标系。
这里写图片描述
通常,我们会用下面的方式来书写两个向量的点积:
这里写图片描述
为了计算出点积,我们需要记住下面的公式:
这里写图片描述
好了,让我们付诸代码实现吧:

template<typename T>class Vec3{    ...    //作为类的成员函数    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);    }    ...};//用类外部函数求叉积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);}

有没有一种口诀来方便我们记忆求叉积的公式呢?最简单的方式就是按照下面这种方式来写出我们求叉积的公式:
这里写图片描述
我们用列的方式来写出向量。通过这种方式我们很容易的看出:如果要求结果的x坐标,就得用原向量的y和z坐标。
求叉积的时候,两个向量的前后顺序影响着结果向量的方向。加入我们的A向量为(1,0,0),B向量为(0,1,0),那么:
这里写图片描述
然而
这里写图片描述
因此我们说:叉积是不可交换的运算。记得在前面一章中我们提到过,如果两个向量定义了一个平面,那么第三个轴可以从我们指向平面里,也可以从平面里指向我们。我们也介绍了一种用左右手区分两种坐标系的方法。当我们计算两个向量的叉积的时候也会经常使用这个方法。我们计算两个向量的叉积只会得出一个结果。比如A= (1,0,0),B= (0,1,0),那么C就只可能是(0,0,1)。所以你可能会问:既然这样,为什么我还必须得关心坐标系是左手坐标系还是右手坐标系呢?因为,虽然我们得到的结果总是不变的,但如何画出这个轴却必须确定使用的是哪种坐标系。我们可以拿出手比划比划,以确定这个z轴到底指向哪个方向。
这里写图片描述
在右手坐标系里面,如果我们用食指指向A向量的方向,中指指向B的方向,那么C向量(也就是大拇指的朝向)就是往上的。同时如果用左手这样做,大拇指却是向下的。
在确定面的朝向时,叉积的顺序格外重要。面的朝向是由两个向量的叉积计算出来的,叉积的顺序直接决定该面的法线方向是从面外指向面里,还是从面里指向面外。法线的方向与面的方向直接相关。

向量和点的加减

另外的对点和向量的操作都是十分直接的。标量和向量的乘法或者与另外一个向量的乘法会得到一个点。我们可以将两个向量进行加减或者作除法等等。有一些3DAPI区分了点,法线以及向量。从技术上来说,这三者确实有些细微的不同,我们也有重组的理由来创建三个不同的类分别处理它们。然而,在现实的处理过程中,如果把它们分别放在不同的类中处理所伴生的复杂性将会让人混乱。通常,我们会把这三者统一处理(事实上这已经变成一种标准),我们会用一个统一的类:Vec3来表示这三种东西。只需要处理在它们代表不同类型时可能出现的错误。

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);        }        ...};