Zadanie obcinania polega na
wyznaczeniu fragmentu odcinka lub prostej, który leży wewnątrz okna na ekranie (ogólniej: dowolnego wielokąta), lub
wyznaczeniu fragmentu odcinka lub prostej, który leży wewnątrz ustalonej bryły wielościennej.
Rozwiązanie jednego z tych zadań może być potrzebne w celu narysowania pewnej części odcinka, a także w konstrukcyjnej geometrii brył, w algorytmach widoczności i w śledzeniu promieni, o czym będzie mowa dalej.
Jeśli problem do rozwiązania polega na wyznaczeniu części wspólnej odcinka (lub prostej) i wielokąta (lub wielościanu) wypukłego, to możemy ten wielokąt (wielościan) przedstawić jako przecięcie pewnej liczby półpłaszczyzn (półprzestrzeni) i sprowadzić zadanie do obcinania półpłaszczyznami (półprzestrzeniami).
Dane są punkty końcowe odcinka,
Przedstawienie parametryczne odcinka,
(3.1) |
wstawiamy do równania prostej, otrzymując równanie
którego rozwiązaniem jest
Jeśli
Prawie identyczne jest wyprowadzenie odpowiednich wzorów w przestrzeni
trójwymiarowej, dla danych punktów końcowych
Omówimy najbardziej popularną wersję obcinania do okna prostokątnego. Dla dowolnego wielokąta/wielościanu wypukłego zasada działania algorytmu jest identyczna.
Dane są punkty końcowe odcinka i prostokątne okno. Proste, na których
leżą krawędzie okna, dzielą płaszczyznę na
Kody obszarów, do których należą końce odcinka, możemy wyznaczyć
na podstawie ich współrzędnych. Zauważmy, że jeśli oba kody na
dowolnej pozycji mają jedynkę, to cały odcinek leży poza oknem
(rys. 3.1b).
Jeśli oba punkty końcowe mają kod
Dla dowolnego wielokąta lub wielościanu określa się kody o liczbie
bitów równej liczbie krawędzi albo ścian.
Algorytm wyznacza punkt przecięcia odcinka z krawędzią,
której odpowiada
#define NIC 0 #define CAŁY 1 #define CZĘŚĆ 2 char SC_clip ( punkt &$p_1$, punkt &$p_2$ ) { char wynik, $c_1$, $c_2$; wynik = CAŁY; $c_1$ = KodPunktu ( $p_1$ ); $c_2$ = KodPunktu ( $p_2$ ); while ( $c_1$ || $c_2$ ) { if ( $c_1$ & $c_2$ ) return NIC; if ( !$c_1$ ) { zamień ( $p_1$, $p_2$ ); zamień ( $c_1$, $c_2$ ); } switch ( $c_1$ ) { case 0001: case 0101: case 1001: *$p_1$ = punkt przecięcia z prostą $x=x_{\min}$; break; case 0010: case 0110: case 1010: *$p_1$ = punkt przecięcia z prostą $x=x_{\max}$; break; case 0100: *$p_1$ = punkt przecięcia z prostą $y=y_{\min}$; break; case 1000: *$p_1$ = punkt przecięcia z prostą $y=y_{\max}$; } $c_1$ = KodPunktu ( $p_1$ ); *kod = CZĘŚĆ; } return wynik; } /*SC_clip*/
Wadą algorytmu Sutherlanda-Cohena jest to, że obliczane są punkty, które
mogą być następnie odrzucone (ponieważ leżą poza oknem). Zabiera to
czas, a ponadto wprowadza większe błędy zaokrągleń. Aby otrzymać
algorytm pozbawiony tych wad, skorzystamy z parametrycznego przedstawienia
odcinka (3.1). Oznaczymy
float t1, t2; char Test ( float p, float q ) { float r; if ( p < 0 ) { r = q/p; if ( r > t2 ) return 0; else if ( r > t1 ) t1 = r; } else if ( p > 0 ) { r = q/p; if ( r < t1 ) return 0; else if ( r < t2 ) then t2 = r; } else if ( q < 0 ) return 0; return 1; } /*Test*/ char LB_clip ( punkt *p1, punkt *p2 ) { float dx, dy; t1 = 0; t2 = 1; dx = p2->x - p1->x; if ( Test ( -dx, p1->x - $x_{\min}$ ) ) if ( Test ( dx, $x_{\max}$ - p1->x ) ) { dy = p2->y - p1->y; if ( Test ( -dy, p1->y - $y_{\min}$ ) ) if ( ( Test ( dy, $y_{\max}$ - p1->y ) ) { if ( t2 != 1 ) { p2->x += t2*dx; p2->y += t2*dy; } if ( t1 != 0 ) { p1->x += t1*dx; p1->y += t1*dy; } return 1; } } return 0; } /*LB_clip*/
Wartością funkcji jest 1
jeśli przynajmniej część odcinka
jest widoczna i 0
w przeciwnym razie.
Zasada działania algorytmu wiąże się z interpretacją prostych, na
których leżą krawędzie okna, jako zbiorów algebraicznych.
Funkcja Test
otrzymuje wartość tej funkcji
w punkcie 0
. W przeciwnym razie, po obcięciu wszystkimi krawędziami okna,
procedura oblicza punkty końcowe części odcinka widocznej w oknie (uwaga
na kolejność obliczania tych punktów; w pewnej książce wykryłem
błąd, polegający na zmianie kolejności).
Algorytm Lianga-Barsky'ego, podobnie jak algorytm Sutherlanda-Cohena, może być łatwo zmodyfikowaniy w celu obcinania odcinków do dowolnych wielokątów lub wielościanów wypukłych.
Jeśli mamy wyznaczyć część wspólną danej prostej i prostokątnego okna,
to możemy użyć algorytmu Lianga-Barsky'ego. Opierając się na
przedstawieniu prostej w postaci niejawnej, za pomocą równania liniowego
Zajmiemy się wyznaczaniem części wspólnej wielokąta i półpłaszczyzny. Wyznaczanie przecięcia dwóch wielokątów, z których przynajmniej jeden jest wypukły (np. jest prostokątnym oknem) można wykonać za pomocą kilkakrotnego obcinania do półpłaszczyzny.
Założymy, że brzeg wielokąta jest jedną łamaną zamkniętą. Jeśli tak nie jest (wielokąt ma dziury i łamanych jest więcej), to każdą łamaną trzeba obciąć osobno. Brzeg wielokąta obciętego przez opisany niżej algorytm Sutherlanda-Hodgmana też jest łamaną zamkniętą, zorientowaną zgodnie z brzegiem wielokąta danego.
W algorytmie Sutherlanda-Hodgmana kolejno
przetwarzamy boki wielokąta (odcinki łamanej zamkniętej). Zmienna Inside
ma
wartość true
jeśli punkt podany jako argument leży we ,,właściwej”
półpłaszczyźnie. Funkcja Intersect
oblicza punkt
przecięcia odcinka (o końcach podanych jako parametry). Procedura
Output
ma za zadanie wyprowadzić kolejny wierzchołek obciętego
wielokąta, co może polegać na wstawieniu go do tablicy.
void SH_clip ( int n, punkt w[] ); { punkt p, q, s; char is, ip; s = w[n-1]; is = Inside ( s ); for ( i = 0; i < n; i++ ) { p = w[i]; ip := Inside ( p ); if ( is ) { if ( ip ) Output ( p ); else { q = Intersect ( s, p ); Output ( q ); } } else if ( ip ) { q = Intersect ( s, p ); Output ( q ); Output ( p ); } s = p; is = ip; } } /*SH_clip*/
Zauważmy, że jeśli wielokąt nie jest wypukły, to jego część wspólna z półpłaszczyzną (albo oknem) może nie być spójna. Dostajemy wtedy na wyjściu łamaną, której krawędzie mają wspólne części (i części te są przeciwnie zorientowane). Jeśli po obcięciu wypełniamy wielokąt algorytmem przeglądania liniami poziomymi, to obecność tych ,,fałszywych krawędzi” nie ma znaczenia, choć jeśli brzeg półpłaszczyzny jest ukośny (tj. nie jest prostą pionową ani poziomą), to po zaokrągleniu współrzędnych wierzchołków do liczb całkowitych błędy mogą się ukazać (w postaci błędnie zamalowanych pikseli).
W różnych zastosowaniach zdarza się potrzeba wyznaczenia części wspólnej, sumy lub różnicy dwóch wielokątów dowolnych, tj. niekoniecznie wypukłych. Umożliwia to algorytm Weilera-Athertona, który dopuszcza jako dane wielokąty, których brzegi mogą składać się z wielu zamkniętych łamanych; wielokąty takie mogą być niejednospójne, tj. z dziurami, albo nawet niespójne.
Podstawowe wymaganie, konieczne aby wynik był dobrze określony, to właściwe zorientowanie brzegu. Dla ustalenia uwagi przyjmiemy umowę, że każda krawędź jest zorientowana w ten sposób, że poruszając się wzdłuż niej zgodnie z orientacją mamy wnętrze wielokąta po prawej stronie (rys. 3.4). Aby wyznaczyć przecięcie wielokątów, kolejno
Konstruujemy reprezentacje dwóch grafów zorientowanych; wierzchołkami i krawędziami każdego z nich są wierzchołki i zorientowane krawędzie jednego z wielokątów.
Znajdujemy wszystkie punkty przecięcia krawędzi jednego wielokąta z krawędziami drugiego. Dzielimy krawędzie, na których leżą te punkty, na kawałki; dołączamy w ten sposób nowe wierzchołki do grafu, zachowując uporządkowanie punktów podziału krawędzi w grafie. Wprowadzamy dodatkowe powiązanie między wierzchołkami grafów, które odpowiadają temu samemu punktowi (przecięcia). Tworzy się w ten sposób jeden graf, którego krawędzie reprezentują kawałki brzegu jednego lub drugiego wielokąta.
Znajdujemy wierzchołek grafu, który jest wierzchołkiem przecięcia wielokątów. Może to być wierzchołek odpowiadający punktowi przecięcia brzegów wielokątów, a jeśli takiego nie ma, to możemy sprawdzić, czy jakiś wierzchołek leży wewnątrz drugiego wielokąta, korzystając z reguły parzystości. Wybieramy wychodzącą ze znalezionego wierzchołka krawędź, która jest krawędzią przecięcia (wyboru dokonujemy na podstawie orientacji brzegów).
Zaczynając od znalezionego wierzchołka, obchodzimy graf, aż do trafienia ponownie na wierzchołek, z którego wyszliśmy. W każdym wierzchołku, który odpowiada przecięciu brzegów, wybieramy krawędź wielokąta innego niż ten, po którego krawędzi poruszaliśmy się. Odwiedzone wierzchołki wyprowadzamy; ich ciąg reprezentuje odpowiednio zorientowany fragment (zamkniętą łamaną) przecięcia wielokątów danych.
Algorytm kończy działanie po stwierdzeniu braku nieodwiedzonego wierzchołka należącego do brzegu wielokątów.
Mając algorytm, który wyznacza przecięcie wielokątów, łatwo możemy otrzymać algorytm wyznaczania sumy lub różnicy; dany brzeg określa wielokąt, ale także jego dopełnienie — wystarczy tylko odwrócić orientację wszystkich krawędzi. W ten sposób sumę wielokątów otrzymamy jako dopełnienie przecięcia ich dopełnień.
Ważne dla poprawnej implementacji algorytmu jest uwzględnienie przypadków, gdy wielokąty mają wspólne wierzchołki lub gdy ich krawędzie mają wspólne odcinki. Pewne punkty mogą też być końcami więcej niż dwóch krawędzi wielokąta. Istotna jest możliwość wykonywania działań na wielokątach otrzymanych za pomocą tego algorytmu, i wtedy takie sytuacje zdarzają się często.
Treść automatycznie generowana z plików źródłowych LaTeXa za pomocą oprogramowania wykorzystującego LaTeXML.
strona główna | webmaster | o portalu | pomoc
© Wydział Matematyki, Informatyki i Mechaniki UW, 2009-2010. Niniejsze materiały są udostępnione bezpłatnie na licencji Creative Commons Uznanie autorstwa-Użycie niekomercyjne-Bez utworów zależnych 3.0 Polska.
Projekt współfinansowany przez Unię Europejską w ramach Europejskiego Funduszu Społecznego.
Projekt współfinansowany przez Ministerstwo Nauki i Szkolnictwa Wyższego i przez Uniwersytet Warszawski.