Hi,
für meine Snake on Scope brauche ich an einer Stelle die Umlaufzahl
einer Kurve um einen Punkt.
Die Kurve ist eine Jordan-Kurve, und es ist zu bestimmen, ob der Punkt
im Innern der Kurve liegt oder ausserhalb.
Zur Erinnerung: Eine Jordankurve teilt die Ebene in ein Inneres und ein
Äusseres, sie ist geschlossen, und sie schneidet sich nicht selbst (also
nicht 8-förmig oder so).
Die Elemente der Kurve sind Geradenstücke in den Koordinatenrichtungen.
Die Kurve ist weder konvex noch sternförmig.
Momentan mache ich das wie im Anhang in snake_umlauf zu sehen: Für alle
Mauerelemente wird der Winkel bestimmt, unter dem das Element von dem zu
testenden Punkt aus zu sehen ist.
Diese Winkel werden für alle Segmente aufaddiert.
Für Punkte, die nicht auf der Kurve liegen (was der hier Fall ist), ist
die so bestimmt Umlaufzahl ein ganzzahliges Vielfaches des Vollwinkels.
Ein Punkt liegt im Innern wenn die Umlaufzahl 360° ist.
Irgendwei hab ich den Eindruck, ich mach das viel zu kompliziert, aber
selbst nach einigem Überlegen ist mir kein einfacherer Weg eingefallen,
das zu programmieren. Es funktioniert zwar, aber die
Arcustangens-Tabelle nimmt einiges an Platz weg.
Allein diese Umlaufzahl-Bestimmung brauch an die 500 Bytes Flash!
Codetechnisch muss das ganze natürlich -- wie immer -- schnell sein und
soll wenig Platz brauchen.
Es läuft auf einem ATmega168 @ 24MHz.
Per 37.5 kHz brauch ich einen neuen Pixel für die Röhre (also alle 640
Ticks), was nicht sooo viel Zeit ist.
Also: Ideen sind gesucht, wie und ob man die Umlaufzahl einfacher
berechnen kann.
Hier nochmal das wesentliche Codefragment: Es ist über alle
Kurvensegmente aufzurufen, welche jeweils von P0 bis P1 reichen.
1
#include"atan2tab.h"
2
3
// (A-B)/4 berechnen und so runden, dass das Vorzeichen gleich bleibt.
4
// ./4 damit die Differenz in 8 Bit (signed) passt
5
int8_tsubr(uint8_ta,uint8_tb)
6
{
7
return(a<b)
8
?(a-b)>>2
9
:-((b-a)>>2);
10
}
11
12
void...
13
{
14
// P0: Beginn des Mauerelements
15
uint8_tx0=...
16
uint8_ty0=...
17
18
// P1: Ende des Mauerelements
19
uint8_tx1=...
20
uint8_ty1=...
21
22
// Richtungsvektoren vom Punkt, fuer den der Umlauf
23
// zu bestimmen ist (s->food) zu den Endpunkten des
24
// Mauersegments
25
// P2 = P0 - Food
26
// P3 = P1 - Food
27
int8_tx2=subr(x0,s->food.x);
28
int8_ty2=subr(y0,s->food.y);
29
int8_tx3=subr(x1,s->food.x);
30
int8_ty3=subr(y1,s->food.y);
31
32
// Element so hindrehen, daß P3 auf (K,0) mit K >= 0
33
// abgebildet wird. Matrix ist M=((x3,y3),(y3,-x3))
34
// P4 = M*P2
35
// M*P3 zeigt in positive x-Richtung
36
int16_tx4=x3*x2+y3*y2;
37
int16_ty4=y3*x2-x3*y2;
38
39
// Auf 8 Bits (signed) zusammenschieben
40
// P4 >>= ?
41
while(1)
42
{
43
if(x4<128&&x4>-128)
44
if(y4<128&&y4>-128)
45
break;
46
x4>>=1;
47
y4>>=1;
48
}
49
50
// Auf 4 Bits (signed) zusammenschieben
51
// P5 = P4 >> ?
52
int8_tx5=x4;
53
int8_ty5=y4;
54
55
while(1)
56
{
57
if(x5<8&&x5>-8)
58
if(y5<8&&y5>-8)
59
break;
60
x5>>=1;
61
y5>>=1;
62
}
63
64
// Winkel von P5 gegen die positive X-Achse bestimmen
65
// Das ist der Winkel, unter dem (P0,P1) von Food aus
Und noch ein Bild.
Die Kurve, für die so eine Berechnung zu machen ist, sieht aus wie die
gestrichelte Kurve auf dem Bild, der Rand des Spielfeldes.
Die Kurve kann natürlich auch komplizierter aussehen.
Es gibt mehrere Möglichkeiten für einen Punkt/Polygon Test.
Und ja eine davon ist die Methode des Umlaufwinkels.
Allerdings muss der Winkel nicht sehr exakt berechnet werden. Es reicht
völlig aus, wenn er so ungefähr hinkommt. Die Summe kann ja nur 2 Werte
haben: 0 oder 2*PI (oder Vielfache davon).
Es reicht völlig aus, den Gesamt-Winkel mit 4 Bit Genauigkeit zu
bestimmen und für jeden Einzelwinkel eine 2 Bit Genauigkeit zu haben.
Effektiv läuft es damit darauf hinaus, Quadranten abzuzählen :-)
Damit ist deine Tangens-Tabelle nicht notwendig.
Anbei eine Implementierung dieses Verfahrens von Kevin Weiler aus dem
Buch 'Graphics Gems IV'. Der Code darf beliebig weiter verwendet werden.
(Anpassen an deine Datenstruktur musst du ihn noch selber)
Anbei noch eine Testsuite für verschiedene Point in Polygon Verfahren
aus dem gleichen Buch. Diesmal von Eric Haines. Auch hier wieder: Der
Code kann beliebig verwendet werden.
Hi, danke!
Also kommt man am Berechnen der Umluuf- resp. Windungszahl nicht
vorbei...
Meinen Algorithmus hab ich etwas verbessert und Symmetrien von atan
ausgenutzt, so daß es jetzt nur noch 150 Bytes insgesamt braucht.
Weilers Algorithmus sieht sehr interessant aus!
Den muss ich mir mal näher anschauen. Die unangenehme Division kann man
rauserweitern, dabei geht es ja nur um einen Vorzeichentest. Und 16-Bit
Arithmetik reicht hoffentlich, ansonsten wird das zu komplex.
Johann
> Also kommt man am Berechnen der Umluuf- resp. Windungszahl nicht> vorbei...
Also geht es dir nicht primär darum, den Umlaufwinkel zu bestimmen,
sondern tatsächlich herauszufinden, ob sich ein Punkt innmerhalb eines
gegebenen Polygons befindet? Der Thread-Titel und dein Initialpost haben
mich da etwas verunsichert :)
Eine andere Möglichkeit des Punkt-im-Polygon-Tests besteht darin, dass
man abzählt, wie oft eine in dem zu testenden Punkt beginnende
Halbgerade die Polygonlinie durchquert. Ist die Anzahl ungerade, liegt
der Punkt im Innern.
Das Verfahren könnte in deinem Fall ganz geschickt sein, da alle
Polygonseiten Achsenparallel liegen, so dass die Überprüfung der
Durchquerung relativ leicht ist, wenn man die Halbgerade ebenfalls
achsenparallel wählt. Ob es allerdings schneller ist als das von Karl
Heinz genannte Verfahren, weiß ich nicht.
yalu wrote:
> Das Verfahren könnte in deinem Fall ganz geschickt sein, da alle> Polygonseiten Achsenparallel liegen,
Ja das hab ich mir auch überlegt, hab den Vorschlag aber dann wieder
verworfen, weil ich nicht sicher war, ob er die Achsparallelität nicht
irgendwann aufgeben möchte.
Ansonsten wäre das Schnittpunkte zählen speziell in diesem Fall sicher
die beste Lösung, weil man so gut wie nichts rechnen muss und letztendes
alles auf ein paar Vergleiche rausläuft.
Allerdings gibt es da ein paar unangnehmen Sonderfälle, die Beachtung
brauchen
(vom Punkt ausgehend, nach rechts die Schnittpunkte zählen
+------------------+
| |
| * +------+ |
| | | |
+-------+ +---+
3 'Schnittpunkte' -> Punkt ist drinnen, wie es sein soll
+-----+
| |
* +-----+ |
| |
+-----------+
ebenfalls 3 Schnittpunkte, aber diesmal ist der Punkt draussen
Das Problem besteht in der Behandlung eines 'Schnittpunktes', wenn der
SChnittpunkt mit dem Endpunkt einer Polygonseite identisch ist.
Ich hab das meist so gelöst, dass in so einem Fall der Schnittpunkt nur
dann zählt, wenn er mit dem unteren (also dem mit dem kleineren y Wert)
der beiden Endpunkte identisch ist und im anderen Fall nicht.
Die Funktion isinside liefert eine 1 zurück, wenn der Punkt innerhalb
und eine 0, wenn er außerhalb des Polygons liegt. Linien, die das
Polygon nach links oder unten begrenzen, gehören zum Polygon dazu,
Linien, die es nach oben oder rechts begrenzen, nicht. Wenn ich den
Eingangspost richtig verstanden habe, liegt der Punkt aber nie auf dem
Polygonumfang, so dass dies keine Rolle spielt. Wenn doch, müsste man
die if-Abfrage etwas erweitern, um zu erreichen, dass der Umfang
entweder komplett zum Polygon dazugehört oder komplett nicht.
Das Polygon wird, da die Seiten achsenparallel sind, durch die Hälfte
seiner Eckpunkte vollständig beschrieben (s. Kommentar im Programm).
Die Funktion braucht 74 Bytes Programmspeicher (AVR-GCC 4.2.4) und
sollte auch einigermaßen schnell sein, da pro Polygonpunkt nur einige
wenige Billigoperation ausgeführt werden.
Ich habe sie an folgendem Polygon getestet, bei das Ergebnis für alle
Punkte x, y in [0, 7] gestimmt hat:
1
6_ ___ ___
2
5_ | |_| |
3
4_ |_ _|
4
3_ _| |_
5
2_ | _ |
6
1_ |___| |___|
7
0_
8
| | | | | | |
9
0 1 2 3 4 5 6
Das heißt natürlich nicht, dass keine Bugs vorhanden wären ;-)
Man kann das Verfahren auch auf Polygone mit nicht achsenparallelen
Seiten erweitern. Dann muss die Schleife zum einen über alle Eckpunkte
laufen, zum anderen ist dann die Abfrage poly[i].x>point.x durch einen
komplizierteren Ausdruck zu ersetzen, der prüft, ob der Punkt links oder
rechts von der (möglicherweisen schiefen) Polygonseite liegt. Dazu sind
einige zusätzliche Additionen und Multiplikationen erforderlich, die
teilweise in 16-Bit gerechnet werden müssen.
yalu schrob:
> Also geht es dir nicht primär darum, den Umlaufwinkel zu bestimmen,> sondern tatsächlich herauszufinden, ob sich ein Punkt innmerhalb eines> gegebenen Polygons befindet? Der Thread-Titel und dein Initialpost haben> mich da etwas verunsichert :)
Für ne geschlossene Kurve ist das ja gleichbedeutend.
> Eine andere Möglichkeit des Punkt-im-Polygon-Tests besteht darin, dass> man abzählt, wie oft eine in dem zu testenden Punkt beginnende> Halbgerade die Polygonlinie durchquert. Ist die Anzahl ungerade, liegt> der Punkt im Innern.
ARRRGH Tischkante-beiss, das ist genau das was ich gesucht hab!
Ich wusste doch daß ich wieder viel zu umständlich dachte...
> Ob es allerdings schneller ist als das von Karl> Heinz genannte Verfahren, weiß ich nicht.
Ich würde sagen ja, weil man kein * oder / braucht.
Und es ist schneller als mein Tabellen-Verfahren.
Es braucht 60 Bytes freu.
1
voidsnake_umlauf(snake_t*s,uint8_tdir,uint8_tlen)
2
{
3
if(dir&1)
4
// NORD || SUED
5
return;
6
7
// Q: Testpunkt
8
// P0/P1: Beginn/Ende des Mauerelements
9
uint8_tyq=s->food.y;
10
uint8_ty0=s->draw.y;
11
12
// Teststrahl soll nach oben gehen
13
if(y0<yq)
14
return;
15
16
// Laenge des Mauerelements
17
uint8_tmlen=len<<WALL_SKIP;
18
19
// Trifft der Strahl die Mauer?
20
uint8_txq=s->food.x;
21
uint8_tx0=s->draw.x;
22
uint8_tx1=x0;
23
24
if(dir==SN_E)
25
{
26
// Ost
27
x1+=mlen;
28
if(xq<x0||xq>x1)
29
// Nein
30
return;
31
}
32
else
33
{
34
// West
35
x1-=mlen;
36
if(xq<x1||xq>x0)
37
// Nein
38
return;
39
}
40
41
// Ja, der Strahl trifft
42
// WEST: +1
43
// Ost: -1
44
s->wall_umlauf-=dir-1;
45
}
...und da sie nur 1x verwendet wird braucht's nach demm Inlinen nur noch
32 Bytes.
Und es ist wiklich die Umlaufzahl. Wenn man es über mehrere Kurven macht
gehen auch Löcher und so :-))