到目前爲止,我們已經詳細介紹了座標系以及點和向量的座標所表明的信息。現在讓我們學習一些可以對點和向量使用的計算。這些計算在任何一個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);
}
...
};