多边形裁剪(Polygon Clipping) 1
原文地址:?https://sean.cm/a/polygon-clipping-pt1
Greiner-Hormann裁剪算法無法處理重合線。?所以我研究并寫了另一篇適用于所有多邊形的文章。
在此處閱讀后續內容:多邊形裁剪(第 2 部分)
問題
首先, 讓我們定義問題,
假設您有兩個多邊形,每個多邊形都以 2D 形式存在
var poly1 = [ // red[ 181, 270 ],[ 85, 418 ],[ 171, 477 ],[ 491, 365 ],[ 218, 381 ],[ 458, 260 ] ]; var poly2 = [ // blue[ 474, 488 ],[ 659, 363 ],[ 255, 283 ],[ 56, 340 ],[ 284, 488 ],[ 371, 342 ] ];?奇偶規則
多邊形遵循奇偶規則來確定一個點是否被視為區域“內部”。
基本規則是想象您正在用一條水平線上從左到右掃描。每次越過邊緣時,都會在外部和內部之間切換。
?那么:給定這兩個多邊形,我們如何計算不同的布爾運算?
基本理念
?首先,讓我們定義一些基本的規則.
順時針vs逆時針(Forward vs. Backward Movement)
如果我們坐在多邊形的任何一點上,我們總是可以向前一個點或者后一個點移動
順時針只是意味著沿箭頭方向移動,逆時針則相反
?插入點
?在處理過程中,我們需要在多邊形中插入點。只要我們對如何插入它們很聰明,它就不會改變多邊形的形狀:
交叉點
識別和分類交叉點是算法中的魔法.
如果您考慮一下,我們將執行的每個操作(交集、聯合、差異)都會產生一個包含多邊形之間所有交點的多邊形
?我們不關心同一多邊形內的交叉點.
另外:如果你想象我們正沿著一個多邊形行走并遇到一個十字路口,我們有3個選擇.
1.?保持在同一個多邊形上(這是毫無意義的)
2.?切換多邊形,開始順時針移動
3.?切換多邊形,并開始逆時針移動
?
因此,如果我們能夠智能地選擇在每個交叉路口的方向,我們就可以追蹤到正確的結果形狀.
交叉點示例
想象一下, 我們想象一下,我們正在追蹤兩個多邊形合并union的結果.
在每個交叉點,我們都希望朝著最終形狀繼續增長的方向移動.
我們可以這樣做:
我們也可以在相反的方向得到相同的結果:
關于所有四個決定,我們可以說哪些是正確的?
在每個交叉點,我們總是朝著遠離我們離開的多邊形的方向前進.
因此,例如,如果我們沿著 Blue 行駛,然后遇到一個十字路口,我們應該繼續沿著 Red 向遠離Blue的方向行使.
會有什么不同?這是Red-Blue(從Red中減去Blue區域):
?而在另一個方向:
對此我們能說什么?
當從紅色切換到藍色時,我們進入紅色。當從藍色切換到紅色時,我們遠離紅色.
所以我們有兩個基本的決定:
1. 當從紅色切換到藍色時,我們是進入還是遠離紅色?
2.當從藍色切換到紅色時,我們是進入還是遠離藍色?
對于聯合(union)來說, 答案總是離開.?但是對于Red-Blue(Red減Blue),我們想要進入紅色,?遠離藍色。如果你玩玩,你會注意到交叉(intersection?)意味著總是進入你要離開的
這給了我們下邊的表
| 聯合(Union) | False | False |
| Red減Blue(Red - Blue) | True | False |
| Blue減Red(Blue - Red) | False | True |
| 交叉(Intersection) | True | True |
交叉入口/ 交叉出口
我們不知道如何進入或離開——我們只知道沿著多邊形順時針,逆時針移動。我們如何把兩者同意起來.
如果我們在一個交點的兩邊取兩個點,并測試它們是否在另一個多邊形內,我們可以保證一個點在外面,一個點在里面:
如果第一個點在外面,那么我們可以認為這條線是通過交點進入多邊形的。如果第一個點在內,則該線通過交點離開多邊形.
所以,我們真的只需要將每個交叉點標記為交叉入口/ 交叉出口
當我們沿著一條路徑行駛時,每個路口都會切換我們是在里面還是外面。它必須.
因此,我們只需要計算第一個點是否在另一個多邊形內部。如果是,那么第一個交叉點是一個交叉出口——否則第一個交叉點是一個交叉入口.
而且由于路徑上的每個交叉點都在entry和exit之間切換,我們不必繼續測試點是在內部還是外部(這很昂貴).
表現
最后,重要的是要認識到交叉點是相對于多邊形的交叉入口或交叉出口.
這意味著每個交叉點有四種可能性.
?白色代表進入,黑色代表退出。左半球為紅色,右半球為藍色
?實際上,對于每個交點,我們將在每個多邊形中插入一個點。所以每個交點會有兩個點,一個存儲在每個多邊形中。每個點都會跟蹤它是進入還是退出.
現在我們準備好代碼啦.
步驟1. 將多邊形轉換為鏈表
雙鏈表對于這個算法來說是一個有用的多邊形表示,因為我們將同時插入點和遍歷。通過使用雙鏈表,我們不必擔心插入會破壞遍歷.
我們還需要跟蹤一個點是否是一個交點,所以我們可以從false這里初始化它開始:
function UpgradePolygon(p){// converts a list of points into a double linked listvar root = null;for (var i = 0; i < p.length; i++){var node = {point: p[i],intersection: false,next: null,prev: null};if (root === null){// root just points to itself:// +-> (root) <-+// | |// +------------+node.next = node;node.prev = node;root = node;}else{// change this:// ...-- (prev) <--------------> (root) --...// to this:// ...-- (prev) <--> (node) <--> (root) --...var prev = root.prev;prev.next = node;node.prev = prev;node.next = root;root.prev = node;}}return root; }步驟2. 計算并插入交叉點
接下來,我們需要遍歷每個邊組合,看看它們是否相交。如果它們確實彼此相交,那么我們需要在多邊形中插入交點.
線交點
首先,我們需要一個輔助函數來計算兩條線的交點:
function LinesIntersect(a0, a1, b0, b1){var adx = a1[0] - a0[0];var ady = a1[1] - a0[1];var bdx = b1[0] - b0[0];var bdy = b1[1] - b0[1];var axb = adx * bdy - ady * bdx;var ret = {cross: axb,alongA: Infinity,alongB: Infinity,point: [Infinity, Infinity]};if (axb === 0)return ret;var dx = a0[0] - b0[0];var dy = a0[1] - b0[1];ret.alongA = (bdx * dy - bdy * dx) / axb;ret.alongB = (adx * dy - ady * dx) / axb;ret.point = [a0[0] + ret.alongA * adx,a0[1] + ret.alongA * ady];return ret; }它計算兩條線的交點,并返回每條線上的交點“沿”多遠。因此,例如,如果alongA是0.75,那么交集發生在從a0到 的75% 處a1.?
這些值是重要的,因為他們可能是負數或大于1,因此,如果兩條線實際相交,我們需要測試alongA和alongB0和1(不含)之間.
下一個非交點
由于我們將在我們的鏈表中插入交點,所以有一個幫助函數來查找下一個非交點.
function NextNonIntersection(node){do{node = node.next;} while (node.intersection);return node; }每個邊組合(Edge Pair)
現在我們可以編寫迭代每個邊組合的代碼:
var root1 = UpgradePolygon(poly1); var root2 = UpgradePolygon(poly2);var here1 = root1; var here2 = root2; do{do{//// TODO: test intersection between:// here1 -> NextNonIntersection(here1) and// here2 -> NextNonIntersection(here2)//here2 = NextNonIntersection(here2);} while (here2 !== root2);here1 = NextNonIntersection(here1); } while (here1 !== root1);交叉點測試
給定兩個節點,我們可以測試交集:
var next1 = NextNonIntersection(here1); var next2 = NextNonIntersection(here2);var i = LinesIntersect(here1.point, next1.point,here2.point, next2.point );if (i.alongA > 0 && i.alongA < 1 &&i.alongB > 0 && i.alongB < 1){//// TODO: insert intersection points in both polygons at// the correct location, referencing each other// }插入交叉點
最后,如果兩條邊相交,那么我們要在兩個非交點之間插入我們的交叉點.
為了將它插入正確的位置,我們必須跟蹤alongA和alongB值以確保如果兩個交點在同一條邊上,它們以正確的順序插入.
我們將要創建兩個節點,一個用于每個多邊形——但這些節點應該相互指向,以便我們稍后在遇到交叉點時可以在多邊形之間“跳躍”
var node1 = {point: i.point,intersection: true,next: null,prev: null,dist: i.alongA,friend: null }; var node2 = {point: i.point,intersection: true,next: null,prev: null,dist: i.alongB,friend: null };// point the nodes at each other node1.friend = node2; node2.friend = node1;var inext, iprev;// find insertion between here1 and next1, based on dist inext = here1.next; while (inext !== next1 && inext.dist < node1.dist)inext = inext.next; iprev = inext.prev;// insert node1 between iprev and inext inext.prev = node1; node1.next = inext; node1.prev = iprev; iprev.next = node1;// find insertion between here2 and next2, based on dist inext = here2.next; while (inext !== next2 && inext.dist < node2.dist)inext = inext.next; iprev = inext.prev;// insert node2 between iprev and inext inext.prev = node2; node2.next = inext; node2.prev = iprev; iprev.next = node2;步驟3. 計算交叉入口/交叉出口
我們知道交叉口在進入和退出之間交替。但是第一個交叉點是什么?是入口還是出口.
簡單:如果多邊形的第一個點在另一個多邊形內,那么第一個交點必須是出口.
但是,計算一個點是否在多邊形內部實際上有點復雜.
點在多邊形內
function PointInPolygon(point, root){var odd = false;var x = point[0];var y = point[1];var here = root;do {var next = here.next;var hx = here.point[0];var hy = here.point[1];var nx = next.point[0];var ny = next.point[1];if (((hy < y && ny >= y) || (hy >= y && ny < y)) &&(hx <= x || nx <= x) &&(hx + (y - hy) / (ny - hy) * (nx - hx) < x)){odd = !odd;}here = next;} while (here !== root);return odd; }PointInPolygon通過計算水平線相交的邊數來工作。水平線從(-Infinity, y)到(x, y)。它只關心交叉點的數量是奇數還是偶數。它基于光線投射。
交替進入/退出
現在我們可以輕松計算出一個交叉點是入口還是出口:
function CalculateEntryExit(root, isEntry){var here = root;do{if (here.intersection){here.isEntry = isEntry;isEntry = !isEntry;}here = here.next;} while (here !== root); }var is1in2 = PointInPolygon(root1.point, root2); var is2in1 = PointInPolygon(root2.point, root1);CalculateEntryExit(root1, !is1in2); CalculateEntryExit(root2, !is2in1);步驟4. 生成結果
我們已經走了很長一段路!這是我們到目前為止所擁有的.
我們已經計算并插入了交點,并將它們標記為每個多邊形的入口或出口.
現在是有趣的部分!
從哪里開始
?我們從哪里開始追蹤結果?我們不能只選擇一個隨機點,因為有些點實際上可以從結果中完全刪除.
由于所有操作都包括每個交集,我們應該從尋找未處理的交集開始.
我們添加到最終結果中的每個交點,我們都標記為已處理.
然后,我們只是繼續跟蹤,直到我們不再有任何交集需要處理.
var result = []; var isect = root1; var into = [intoBlue, intoRed]; // explained below while (true){do{if (isect.intersection && !isect.processed)break;isect = isect.next;} while (isect !== root1);if (isect === root1)break;//// TODO: process isect// }?轉向哪個方向
最后,我們來到了癥結所在:
當我們遇到十字路口時,我們怎么知道該往哪個方向轉彎?
讓我們來推理一下:
| True | True | True |
| True | False | False |
| False | True | False |
| False | False | True |
因此,如果 ,我們應該繼續前進isEntry === intoPoly
由于我們所在的多邊形來回切換,我們只需通過將intoBlue和存儲intoRed在into列表中來使我們的決策動態化,并將?其curpoly用作索引.
var curpoly = 0; var clipped = [];var here = isect; do{// mark intersection as processedhere.processed = true;here.friend.processed = true;var moveForward = here.isEntry === into[curpoly];do{clipped.push(here.point);if (moveForward)here = here.next;elsehere = here.prev;} while (!here.intersection);// we've hit the next intersection so switch polygonshere = here.friend;curpoly = 1 - curpoly; } while (!here.processed);result.push(clipped);沒有交叉點
如果沒有交叉點?
我們的結果集將是空的……這可能是正確的,也可能是錯誤的——這取決于操作.
一個簡單的檢查就足以修復它:
if (result.length <= 0){if (is1in2 === intoBlue)result.push(poly1);if (is2in1 === intoRed)result.push(poly2); }演示
單擊此處啟動演示!
?您可以拖動每個多邊形的點,并通過單擊按鈕切換操作。
?附錄:限制
抱歉,這個算法有一個嚴重的局限性:
您不能擁有完美重疊的點或邊.
如果你仔細想想,這是有道理的:整個算法都是基于交叉點的思想.
如果點或邊直接重疊,那么您就不會得到那種好的跳躍效果.
最初的論文建議稍微“擾亂”點,這樣線條就不會完全重疊。我最初認為這是一個小調整,不會有有問題.
但是,我錯了.
擾動點會破壞數據——因此可能很重要的源數據的屬性(例如,平滑邊緣)變得無效.
幸運的是我研究了另一種處理一切的算法,并寫了一篇后續文章
此處閱讀后續內容:多邊形裁剪(第 2 部分)
總結
以上是生活随笔為你收集整理的多边形裁剪(Polygon Clipping) 1的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MSP432 FPU与DSP测试
- 下一篇: 移动互联网应用开发概览