1. 數學分析
為什麼我們要畫2D直線,要做2D的直線-矩形裁剪?原因很簡單,無論遊戲世界是2D的還是3D的,最終都要投影到玩家的螢幕上,3D的東西最終要是要投影到視平面上。所以3D遊戲仍然有很多東西要在2D視平面上做。對於3D遊戲的裁剪就有兩種方法:一是在3D空間做裁剪,利用視域體的各個面與三角形中直線的關係來做,這叫做立方體空間裁剪;另一種是,把3D物體先投影在2D視平面上,然後在視平面上對2D直線做矩形裁剪,叫作映像空間裁剪。三角形都是由直線組成的,螢幕是矩形的,所以用矩形裁剪一條直線是非常重要的,而且也不是想當然的簡單的,也有很多因素要考慮。
1) 直線被矩形裁剪的四種情況
直線被矩形裁剪只有以下4種情況,
2) 裁剪的核心問題?
我們來看看,裁剪的核心問題是什麼:
如,兩個端點分別為p0,p1的直線線段,被某一矩形裁剪。裁剪的過程是給定輸入值x0,y0,x1,y1和矩形RECT,輸出被裁剪後的直線兩端點p0',p1'的座標x0',y0',x1',y1'。
通過觀察,我們可以知道,我們其實就在求兩條直線的交點,一條是紅線,一條是矩形的某個邊。這讓我們想起了幾何學時的幾種直線表示形式,然後求交點就是解一個方程組的過程。這沒有錯,但是我們可以走捷徑,因為矩形的某個邊不是水平線,就是垂直線,所以這第二條直線的表示非常簡單。垂直線是x = c,水平線是y = c。
我們來表示一下這條紅線。比如我們用點斜式:
斜率m = (y2 - y1) / (x2 - x1)
點斜式: y - y1 = m * (x - x1)
整理後,用y表示x:
x = (y - y1) / m + x1
也可以用x表示y:
y = m * (x - x1) + y1
所以與某個矩形的邊的交點,可以把那個邊線的表示形式直接代入,即可求得。
3) Cohen-Sutherland
上面我們解決了核心問題,那就是如何求交點。而Cohen-Sutherland演算法為我們解決了另一個文章開頭所說的問題,就是如何處理直線與矩形之間的4種關係。
該演算法雖然簡單,但極其精妙,後面會講如何精妙!它把空間分成了各個部分,並賦予了相應的位代碼,所以只使用少量的if語句,並且還是位元運算,根據直線的兩端點,即可判斷出是上面哪種關係,
如果你想感動一下,那一定要非常仔細的研究這個圖了。
首先,我們看紅色代碼的部分,請不要看那個黑色的十進位code,中間裁剪矩形的代碼是0000。正左方、正右方、正下方、正上方分別是:
0001
0010
0100
1000
即,他們每個是其中的一個位標記,所以可以通過兩兩做或操作(|),來得到斜著的4個角的代碼(綠色)。
首先,根據這些特性,我們可以做一些工作了,我們定義p1code、p2code分別表示直線的兩個端點在這張圖的位置編碼,我們以p1code為例,p2code相同,就不貼代碼了,下面一組簡單的判斷,就能求得恰當的p1code:
int p1code = 0;</p><p>if (y1 < rect.top)<br />p1code |= 8;<br />else if (y1 > rect.bottom)<br />p1code |= 4;</p><p>if (x1 < rect.left)<br />p1code |= 1;<br />else if (x1 > rect.right)<br />p1code |= 2;
也許你覺得這沒有什麼,但神奇的是,通過現在的p1code和p2code的位編碼的組合,我們已經可以使用僅僅一個位元運算操作來判斷線段是否完全被刪除,即求p1code & p2code的值。你可以嘗試一下,當點p1和點p2在同一側時,比如都在上方,無論是在左上、正上還是右上,這個運算式的結果都是1;而只要有一個點不在這一側,則這個運算式必然是0。所以可以只用一個位元運算來過濾掉大量的裁剪操作。
還有一個運算式則很容易理解了:p1code == 0 && p2code == 0,當這個運算式為true時,則直線完全被保留。
這時可能讀者會問了,還有一種完全刪除的情況並沒有被排除,那就是雖然兩個點在不同側,但是直線仍都在矩形的外面,應該也被全刪除。對於這種情況,沒有辦法,我們無法在現有的條件下來判斷了,在上面這兩個運算式過濾完大部分情況後,這種特殊情況只能在我們馬上要做的具體求點的過程裡面來判斷了。下面我們來做這件事。
對於直線的開始端點點p0來說,在正方向和斜方向處理的複雜度不同,請看:
請注意,我只畫了從p0發射的必然與相鄰的矩形邊緣有交點的情況,而所有沒畫其他情況,其實那些其他情況,大部分已經被上面所說的那兩個運算式排除了。還有一種情況是,比如p0在N這個位置,但是他與矩形上邊緣直線的交點在矩形的外面,對於這種情況,我們先把交點算出來,在計算完兩個端點分別的新值後再來判斷,這個後面會講。
對於在正方向(W、E、S、N),已經可以直接求出p0與矩形邊緣線的交點座標了,以N為例:
switch(p1code)<br />{<br />case 0: // C<br />break;<br />case 8: // N<br />{<br />ny1 = rect.top;<br />nx1 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;<br />} break;<br />}
上面的代碼求得了p1點的裁剪後的新點np1的座標(nx1, ny1)。這個就是利用上文的點斜式推得的公式計算的。可能你會發現有點地方不一樣,在求nx1的時候我們加了個0.5,這是因為出現了除法,我們要把最後一步除法變成浮點數除法,然後通過+0.5,再捨棄小數位來達到四捨五入的目的。
然後就是斜方向的問題了。這個問題我們採取好理解的方式來處理,p0nw。我們首先假定那根紅色的線是和矩形上方相交,我們使用正方向求解交點的方法,可以先把這個交點給求出來,求出來之後,判斷這個nx1,是不是在矩形上邊的範圍之內,如果在,那麼這個點就是對的,如果不在,那麼說明直線與左邊線相交,我們就需要再求直線與矩形左邊線的交點,而這個交點就是真正的np1了。讀者可以邊看著中最左邊的那條紅線,邊看這段文字,非常好理解,以nw為例,代碼如下:
switch(p1code)<br />{<br />case 0: // C<br />break;<br />case 8: // N<br />{<br />ny1 = rect.top;<br />nx1 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;<br />} break;<br />case 9: // NW<br />{<br />// 先和求N的一樣<br />ny1 = rect.top;<br />nx1 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;<br />// 然後判斷結果<br />if (nx1 < rect.left || nx1 > rect.right) // 上面的假設錯誤,需要算與左邊線的交點<br />{<br />nx1 = rect.left;<br />ny1 = y1 + (rect.left - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />}
聰明的讀者不難發現,其實對p2code的處理和對p1code的是一模一樣的。哈哈,因為本來就和是哪個點沒關係,只和他們所在的地區可能與相鄰的哪些矩形邊有關係。
於是我們只剩下最後一個問題了,就是上文提到過的,可能直線的兩個點在不同側,但是與矩形沒交點的情況,我們只要把上面的結果做一下邊界檢查就可以找出這種情況了,代碼如下:
if (nx1 < rect.left || nx1 > rect.right ||<br />ny1 < rect.top || ny1 > rect.bottom ||<br />nx2 < rect.left || nx2 > rect.right ||<br />ny2 < rect.top || ny2 > rect.bottom)<br />{<br />return 0;<br />}
至此,大功告成,下面是完整的函數實現。
2. 代碼實現
// 返回1則有交點,返回0則無交點,無需畫了<br />int _CPPYIN_3DLib::ClipLineByRect(int x1, int y1, int x2, int y2, RECT rect, int &nx1, int &ny1, int &nx2, int &ny2)<br />{<br />// 建立並設定起點p1和終點p2的位置代碼p1code和p2code<br />int p1code = 0;<br />int p2code = 0;</p><p>if (y1 < rect.top)<br />p1code |= 8;<br />else if (y1 > rect.bottom)<br />p1code |= 4;</p><p>if (x1 < rect.left)<br />p1code |= 1;<br />else if (x1 > rect.right)<br />p1code |= 2;</p><p>if (y2 < rect.top)<br />p2code |= 8;<br />else if (y2 > rect.bottom)<br />p2code |= 4;</p><p>if (x2 < rect.left)<br />p2code |= 1;<br />else if (x2 > rect.right)<br />p2code |= 2;</p><p>// 過濾同側情況<br />if ((p1code & p2code))<br />return 0;</p><p>// 完全保留<br />if (p1code == 0 && p2code == 0)<br />{<br />nx1 = x1;<br />ny1 = y1;<br />nx2 = x2;<br />ny2 = y2;<br />return 1;<br />}</p><p>// 計算np1的座標nx1, ny1<br />switch(p1code)<br />{<br />case 0: // C<br />{<br />nx1 = x1;<br />ny1 = y1;<br />} break;<br />case 8: // N<br />{<br />ny1 = rect.top;<br />nx1 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;<br />} break;<br />case 4: // S<br />{<br />ny1 = rect.bottom;<br />nx1 = x1 + (rect.bottom - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;<br />} break;<br />case 1: // W<br />{<br />nx1 = rect.left;<br />ny1 = y1 + (rect.left - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />} break;<br />case 2: // E<br />{<br />nx1 = rect.right;<br />ny1 = y1 + (rect.right - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />} break;<br />case 9: // NW<br />{<br />// 先和求N的一樣<br />ny1 = rect.top;<br />nx1 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>// 然後判斷結果<br />if (nx1 < rect.left || nx1 > rect.right) // 上面的假設錯誤,需要算與左邊線的交點<br />{<br />nx1 = rect.left;<br />ny1 = y1 + (rect.left - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />case 10: // NE<br />{<br />ny1 = rect.top;<br />nx1 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>if (nx1 < rect.left || nx1 > rect.right)<br />{<br />nx1 = rect.right;<br />ny1 = y1 + (rect.right - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />case 6: // SE<br />{<br />ny1 = rect.bottom;<br />nx1 = x1 + (rect.bottom - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>if (nx1 < rect.left || nx1 > rect.right)<br />{<br />nx1 = rect.right;<br />ny1 = y1 + (rect.right - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />case 5: // SW<br />{<br />ny1 = rect.bottom;<br />nx1 = x1 + (rect.bottom - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>if (nx1 < rect.left || nx1 > rect.right)<br />{<br />nx1 = rect.left;<br />ny1 = y1 + (rect.left - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />}</p><p>// 計算np2的座標nx2, ny2<br />switch(p2code)<br />{<br />case 0: // C<br />{<br />nx2 = x2;<br />ny2 = y2;<br />} break;<br />case 8: // N<br />{<br />ny2 = rect.top;<br />nx2 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;<br />} break;<br />case 4: // S<br />{<br />ny2 = rect.bottom;<br />nx2 = x1 + (rect.bottom - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;<br />} break;<br />case 1: // W<br />{<br />nx2 = rect.left;<br />ny2 = y1 + (rect.left - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />} break;<br />case 2: // E<br />{<br />nx2 = rect.right;<br />ny2 = y1 + (rect.right - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />} break;<br />case 9: // NW<br />{<br />// 先和求N的一樣<br />ny2 = rect.top;<br />nx2 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>// 然後判斷結果<br />if (nx2 < rect.left || nx2 > rect.right) // 上面的假設錯誤,需要算與左邊線的交點<br />{<br />nx2 = rect.left;<br />ny2 = y1 + (rect.left - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />case 10: // NE<br />{<br />ny2 = rect.top;<br />nx2 = x1 + (rect.top - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>if (nx2 < rect.left || nx2 > rect.right)<br />{<br />nx2 = rect.right;<br />ny2 = y1 + (rect.right - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />case 6: // SE<br />{<br />ny2 = rect.bottom;<br />nx2 = x1 + (rect.bottom - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>if (nx2 < rect.left || nx2 > rect.right)<br />{<br />nx2 = rect.right;<br />ny2 = y1 + (rect.right - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />case 5: // SW<br />{<br />ny2 = rect.bottom;<br />nx2 = x1 + (rect.bottom - y1) * (x2 - x1) / (double)(y2 - y1) + 0.5;</p><p>if (nx2 < rect.left || nx2 > rect.right)<br />{<br />nx2 = rect.left;<br />ny2 = y1 + (rect.left - x1) * (y2 - y1) / (double)(x2 - x1) + 0.5;<br />}<br />} break;<br />}</p><p>// 過濾一種直線與矩形不相交的特殊情況<br />if (nx1 < rect.left || nx1 > rect.right ||<br />ny1 < rect.top || ny1 > rect.bottom ||<br />nx2 < rect.left || nx2 > rect.right ||<br />ny2 < rect.top || ny2 > rect.bottom)<br />{<br />return 0;<br />}</p><p>return 1;<br />}
3. 項目下載
這次的樣本,是隨機產生直線,起點和端點的範圍是正負兩個螢幕的最大座標之間,裁剪地區是螢幕裡的一個矩形。每秒將500個這種直線通過矩形裁剪,如果與矩形有交點,則在矩形內畫出相應的線段。如下:
項目完整代碼下載:>>點擊進入下載頁<<
4. 補充內容
如果有時間,我會更新上Cyrus-Beck裁剪演算法,如果您已實現,請留言分享,謝謝。