Das Schneider CPC Systembuch

Die Abteilungen des Betriebssystems

Die Grafik-VDU

Der Fill-Algorithmus

Das dickste Plus, das mit dem CPC 664 eingeführt wurde, ist aber sicher der schnelle Rekursion - Selbst-Aufruf einer Prozedur: Füll-AlgorithmusFüll-Algorithmus. CPC-464-Benutzer können da nur neidvoll zusehen.

Bei der Erklärung der Rekursion (Kapitel Grundlagen: ProgrammstrukturenProgrammstrukturen) wurde ja schon ein voll funktionsfähiger Basic-Füll-Einzeiler vorgestellt, der nur leider an praktischen Grenzen scheitert.

Der im CPC 664/6128 realisierte 'Füller' arbeitet auch ein wenig anders. Nicht rekursiv und er speichert nur 'Der Fill-Algorithmus: interessante Punkteinteressante Punkte'.

Um das zu verstehen, muss man sich einmal Gedanken machen, wie so eine Ausmalroutine prinzipiell funktionieren muss.

Die Routine bekommt eine Start-Koordinate übergeben, die in einer beliebig umgrenzten Fläche liegt. Aber es gibt rundherum eine Grenze. Man muss also Erklärung der Anschlussbelegung: Testtesten und unterscheiden können, ob ein Punkt 'gesetzt' oder 'nicht gesetzt' ist. Im ersten Fall begrenzt er die Ausmal-Routine, im zweiten Fall muss er gesetzt werden.

Außerdem müssen auch die nur diagonal verbundenen Grenz-Punkte als 'dicht' angesehen werden. Der im CPC verwendete Linien-Algorithmus produziert Linien, die sehr oft nur diagonal verbundene Punkte enthält. Daraus ergibt sich die Forderung, dass man sich beim Pixel-Testen nur in den vier Grund-Richtungen, nicht aber diagonal bewegen darf.

Füllen mittels Iteration

Nehmen wir einmal an, die Routine würde ein Pixel auf einer bestimmten Position Erklärung der Anschlussbelegung: Testtesten und feststellen, dass es noch nicht gesetzt ist. Was muss sie dann tun?

Zunächst einmal muss sie den Punkt einfärben. Dann muss sie aber auch die Nachbarpunkte Erklärung der Anschlussbelegung: Testtesten, ob diese auch nicht gesetzt sind. Davon gibt es vier Stück. Da man die nicht gleichzeitig untersuchen kann, muss man Die Grafik-VDU: Die Koordinatendie Koordinaten dieser Punkte speichern. Oder man löst das Problem rekursiv, wie im Basic-Einzeiler.

Aber auch jetzt ist der Speicherbedarf enorm: Ein Punkt hat vier Nachbarn, die zusammen haben 16 (dass dadurch einige mehrfach abgedeckt werden, ist einem Algorithmus nur schwer klar zu machen), die haben 64 potentielle Nachbarn, dann 256, 1024, 4096, etc.. Das greift rasend um sich, bis es endlich durch die Umrandung gestoppt wird.

Was passiert, wenn der getestete Punkt gesetzt ist? Dann wird die Rekursion oder das explodierende Abspeichern von 'Weitersuch-Stellen' einfach abgebrochen und 'genau nichts' getan.

Das folgende Bild erstellt in Zeile 100 bis 250 eine Demo-Grafik und ruft das Grundlagen: UnterprogrammeUnterprogramm ab Zeile 260 auf, um die Fläche auszufüllen.

100 Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 2:
Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 1:
Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 0:
MODE
1:defint a-z 110 dx=638:dy=398:MOVE 0,0:GOSUB 240 120 MOVE 10,390:dx=10 130 FOR dy=-10 TO -380 STEP -12 140 GOSUB 240:MOVER 20,0 150 NEXT 160 r=100:ORIGIN 10,10 170 DEG:MOVE -50,150 180 FOR w=-20 TO 130 STEP 5 190 DRAW SIN(w)*r,COS(w)*r 200 r=400-r 210 NEXT 220 Die verwendeten Abkürzungen bedeuten: x:x=400:y=20:GOSUB 260 230 GOTO 230 240 DRAWR dx,0:DRAWR 0,dy:DRAWR -dx,0:DRAWR 0,-dy:RETURN 250 ' 260 IF Erklärung der Anschlussbelegung: TestTEST(Die verwendeten Abkürzungen bedeuten: x:x,y) THEN RETURN ' gibt's überhaupt was zu füllen ? 270 d=200:DIM Die verwendeten Abkürzungen bedeuten: x:x(d),y(d):z1=0:z2=1 ' Ringspeicher definieren 280 Die verwendeten Abkürzungen bedeuten: x:x(0)=Die verwendeten Abkürzungen bedeuten: x:x:y(0)=y ' ersten Wert eintragen
290 IF z1=z2 THEN RETURN              ' Erklärung der Anschlussbelegung: TestTest auf Ende
300 Die verwendeten Abkürzungen bedeuten: x:x=Die verwendeten Abkürzungen bedeuten: x:x(z1):y=y(z1):z1=(z1+1)Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 2:
Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 1:
Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 0:
MOD
d ' Wert aus Speicher holen, Zeiger incr. 310 Die verwendeten Abkürzungen bedeuten: x:x=x-2 :GOSUB 400 ' nun alle 320 Die verwendeten Abkürzungen bedeuten: x:x=Die verwendeten Abkürzungen bedeuten: x:x+4 :GOSUB 400 ' Nachbarpunkte 330 Die verwendeten Abkürzungen bedeuten: x:x=x-2:y=y-2:GOSUB 400 ' Erklärung der Anschlussbelegung: Testtesten. 340 y=y+4:GOSUB 400 350 GOTO 290 ' und beim nächsten Punkt weitermachen. 360 ' 400 IF Erklärung der Anschlussbelegung: TestTEST(Die verwendeten Abkürzungen bedeuten: x:x,y) THEN RETURN ' Karteileiche oder Umrandung? 410 PLOT Die verwendeten Abkürzungen bedeuten: x:x,y ' sonst Punkt setzen 420 Die verwendeten Abkürzungen bedeuten: x:x(z2)=Die verwendeten Abkürzungen bedeuten: x:x:y(z2)=y ' und in Speicher eintragen 430 z=(z2+1)Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 2:
Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 1:
Die Kodierung der Tintennummern in den Bildschirm-Bytes: Mode 0:
MOD
d:IF z<>z1 THEN z2=z ' Zeiger nur erhöhen, wenn Speicher nicht 440 RETURN ' voll. Sonst geht Alle noch folgenden Anschlüsse fallen unter die Rubrik STEUER- oder auch CONTROLBUS:: Halthalt was verloren !!!

Zum Speichern der Weitersuch-Stellen wird ein Ringspeicher benutzt, der in Zeile 270 definiert und in Zeile 280 mit dem ersten (Start-) Wert gefüllt wird.

Normalerweise benutzt man einen Stack. Der hat aber den Nachteil, dass 'Karteileichen', also vielfach eingetragene und anderweitig bereits bearbeitete Punkte fast beliebig lange in seinen Tiefen schlummern können, bis der Stack ganz zum Schluss geleert wird. Solchermassen realisierte Paint-Routinen erkennt man daran, dass sie beim Ausmalen regelmäßig Pausen einlegen, weil der Speicher voll ist, um alle Karteileichen zu entfernen.

Bei der Queue kommen aber regelmäßig alle Einträge dran. Dadurch werden ganz automatisch tote Punkte entfernt.

interessante Punkte

Der CPC 664/6128-Füller arbeitet aber noch ein wenig anders: Er speichert nur 'Der Fill-Algorithmus: interessante Punkteinteressante' Punkte. Was unterscheidet einen 'interessanten' von einem 'unwichtigen' Punkt? Dazu die folgenden Bilder. In den 3*3-Feldern ist jeweils der mittlere Punkt gesetzt Datenbreite: Wordsworden:

1)              2)              3)              4)
+---+---+---+   +---+---+---+   +---+---+---+   +---+---+---+
| # | # | # |   | # |   |   |   | # |   | # |   | # | # | # |
+---+---+---+   +---+---+---+   +---+---+---+   +---+---+---+
| # | # | # |   | # | # |   |   |   | # |   |   | # | # |   |
+---+---+---+   +---+---+---+   +---+---+---+   +---+---+---+
|   | # |   |   | # |   |   |   | # |   | # |   | # |   |   |
+---+---+---+   +---+---+---+   +---+---+---+   +---+---+---+

Der Algorithmus soll so funktionieren, dass nicht der gerade gesetzte Punkt gespeichert wird, sondern die vier umliegenden, noch zu testenden Punkte.

Welche Punkte müssen in Bild 1) in den Speicher aufgenommen werden? Keiner, weil ja alle Nachbarn bereits gesetzt sind.

In Bild 2) kommen die Punkte (o)LOW KERNEL JUMPBLOCK: 000B: LOW KL LOW PCHL
LOW KERNEL JUMPBLOCK: 001B: LOW KL FAR PCHL
LOW KERNEL JUMPBLOCK: 003B: LOW EXT INTERRUPT
ben
, (r)echts und (u)nten in Frage. Müssen sie aber auch alle in die Tabelle aufgenommen werden??? Nein. Nur einer muss eingetragen werden, um damit weiterzuarbeiten. Die drei Punkte sind nämlich über die diagonalen Eckpunkte verbunden. Wird mit (o) weitergemacht, so findet man über dessen rechten Nachbar und dann dessen unterem Nachbar auch zu (r). Entsprechendes gilt für (u).

Unter diesem Gesichtspunkt betrachtet, müssen in 3) aber alle vier Nachbar-Punkte gespeichert werden, weil alle Eckpunkte bereits gesetzt sind, und die Nachbarn voneinander trennen.

In Beispiel 4) muss wieder nur ein Punkt weiter betrachtet werden, weil (r) und (u) über den Eckpunkt verbunden sind.

Füllen mit Iteration und interessanten Punkten

Das folgende Programm macht dabei noch zwei weitere Vereinfachungen. Zum Einen werden nur Verzweigungen in die Tabelle aufgenommen. Also nur, wenn zwei Pfade weiter verfolgt werden müssen, wird der eine zwischengespeichert. Mit dem anderen wird direkt weitergemacht.

Zum Anderen wird die aktuelle Zugrichtung festgehalten. Dabei dreht der Algorithmus wenn möglich nach jedem Punkt einmal nach links. Dadurch müssen nicht mehr alle Nachbarpunkte untersucht werden:

+---+---+---+  In diesem Beispiel zog der Algorithmus von (u) in die Mitte (und
| ? | ? | ? |  drehte die Zugrichtung nach links). Wenn man jetzt von der Mitte
+---+---+---+  aus die Nachbarpunkte untersucht, weiß man sicher, dass (u)
| ? | # | ? |  gesetzt ist. Dadurch müssen (ul), (u) und (ur) nicht mehr
+---+---+---+  untersucht werden.
| * | # | * |
+---+---+---+
 
 
260 DIM Die verwendeten Abkürzungen bedeuten: x:x(200),y(200),dx(200),dy(200)
270 dx=2:dy=0:zg=0
280 IF Erklärung der Anschlussbelegung: TestTEST(Die verwendeten Abkürzungen bedeuten: x:x,y) THEN 450
290 PLOT Die verwendeten Abkürzungen bedeuten: x:x,y:z=dy:dy=dx:dx=-z     ' Plot und drehe nach links.
 
300 IF Erklärung der Anschlussbelegung: TestTEST(Die verwendeten Abkürzungen bedeuten: x:x+dx,y+dy)=0 THEN 390 ' Punkt frei? dann nach links weitermachen.
310 z=dx:dx=dy:dy=-z              ' sonst wieder nach rechts drehen.
320 IF Erklärung der Anschlussbelegung: TestTEST(Die verwendeten Abkürzungen bedeuten: x:x+dx,y+dy)=0 THEN 360 ' Punkt frei? dann geradeaus weitermachen.
330 z=dx:dx=dy:dy=-z              ' sonst nochmal nach rechts drehen.
340 Die verwendeten Abkürzungen bedeuten: x:x=Die verwendeten Abkürzungen bedeuten: x:x+dx:y=y+dy                 ' vorwärts gehen (insgesamt nach rechts)
350 GOTO 280                      ' und weiter. Verzweigungen nicht möglich!
 
355 ' Punkt rechts auf Verzweigung Erklärung der Anschlussbelegung: Testtesten, nach vorne schreiten und weiter:
 
360 IF Erklärung der Anschlussbelegung: TestTEST(Die verwendeten Abkürzungen bedeuten: x:x+dy,y-dx)=0          ' Erklärung der Anschlussbelegung: TestTest auf Verzweigung:
       THEN IF TESTR(dx,dy)       ' rechts frei aber rechts vorne gesetzt?
               THEN GOSUB 380     ' Wenn ja, eintragen
 
370 Die verwendeten Abkürzungen bedeuten: x:x=Die verwendeten Abkürzungen bedeuten: x:x+dx:y=y+dy:GOTO 290        ' Schritt nach vorne und weiter
 
380 Einleitung: Sound
MAIN FIRMWARE JUMPBLOCK: SOUND MANAGER
Die Firmware des Schneider CPC: SOUND MANAGER
SOUND
1,200,20: ' Punkt rechts eintragen zg=zg+1:Die verwendeten Abkürzungen bedeuten: x:x(zg)=Die verwendeten Abkürzungen bedeuten: x:x+dy:y(zg)=y-dx:dx(zg)=dy:dy(zg)=-dx: RETURN   385 ' Punkt hinten auf Verzweigung Erklärung der Anschlussbelegung: Testtesten und dann weiter bei 360:   390 IF Erklärung der Anschlussbelegung: TestTEST(x-dx,y-dy) THEN 360 ' Punkt hinten gesetzt? Dann keine Verzweigung 400 IF TESTR(dy,-dx)=0 ' sonst: trennt Punkt rechts oder AND TESTR(dx,dy)=0 THEN 360 ' Punkt rechts hinten? Nein, dann keine Verzw. 410 Einleitung: Sound
MAIN FIRMWARE JUMPBLOCK: SOUND MANAGER
Die Firmware des Schneider CPC: SOUND MANAGER
SOUND
1,200,20: ' sonst erst Punkt hinten eintragen. zg=zg+1:Die verwendeten Abkürzungen bedeuten: x:x(zg)=x-dx:y(zg)=y-dy:dx(zg)=-dx:dy(zg)=-dy: GOTO 360   450 Einleitung: Sound
MAIN FIRMWARE JUMPBLOCK: SOUND MANAGER
Die Firmware des Schneider CPC: SOUND MANAGER
SOUND
1,300,20: ' Punkt aus Speicher holen: IF zg THEN Die verwendeten Abkürzungen bedeuten: x:x=Die verwendeten Abkürzungen bedeuten: x:x(zg):y=y(zg):dx=dx(zg):dy=dy(zg):zg=zg-1:GOTO 280 460 RETURN ' Zeiger zg=0 dann fertig.

Zwar entspricht dieses Programm auch noch nicht dem CPC-Algorithmus, es zeigt aber ein Beispiel, wo nur noch Verzweigungs-Punkte ('interesting points' im Amstrad-Jargon) in die Tabelle eingetragen werden. Die Füll-Routine ist so gestaltet, dass man jedes PUSH und POP auf dem Verzweigungs-Stapel mitbekommt: PUSH = hoher Ton, POP = tiefer Ton.

Die Routine setzt einen Punkt, dreht sich nach links und testet, ob sie in dieser Richtung weiter machen kann. Wenn nicht, dreht sie wieder zurück nach rechts (und zeigt so wieder in die ursprüngliche Richtung), checkt diese Richtung ab und dreht sogar eventuell noch einmal nach rechts.

Im letzten Fall kann keine Verzweigung vorliegen. Von hinten kommt sie ja, links und vorne sind dicht (weshalb sie ja überhaupt nur bis rechts drehen musste), allenfalls nach rechts kann es noch weitergehen.

Geht die Routine geradeaus, muss nur nach rechts auf eine Verzweigung getestet werden, nach links ist der Weg versperrt (sonst würde sie ja da weiter machen). Eine Verzweigung liegt vor, wenn es nach rechts weitergeht, die Verbindung nach vorne aber durch ein gesetztes Pixel rechts vorne unterbrochen ist.

Ist aber schon der Weg nach links frei, so müssen erst noch die Verzweigungen nach vorne und nach rechts überprüft werden.

Vektor-Drehung

Zum Verständnis der Routine ist noch die Kodierung der 'Richtung' zu erklären. Diese setzt sich aus dx und dy zusammen. Einer der beiden Werte ist Real: NullNull, der andere kann den Wert -2 oder +2 haben. Damit sind die vier Grund-Richtungen darstellbar:

            dx=0,dy=+2

               /\
               ||
 dx=-2,dy=0 <--++--> dx=+2:dy=0
               ||
               \/

            dx=0,dy=-2

Zum Drehen dieses Richtungsvektors in 90-Grad-Schritten werden folgende Formeln benutzt:

vorwärts:    dy=+dy und dx=+dx
nach links:  dy=+dx und dx=-dy      (in Einleitung: BASIC
Anhang: Basic
Basic
muss dazu eine Hilfsvariable nach rechts: dy=-dx und dx=+dy benutzt werden!! Im Programm immer z.) rückwärts: dy=-dy und dx=-dx

Entsprechend kann man, ohne die 'Richtung' zu ändern, mit den folgenden Kombinationen in alle vier Richtungen Erklärung der Anschlussbelegung: Testtesten:

vorwärts:    Erklärung der Anschlussbelegung: TestTEST (Die verwendeten Abkürzungen bedeuten: x:x+dx,y+dy)
nach links:  Erklärung der Anschlussbelegung: TestTEST (x-dy,y+dx)
nach rechts: Erklärung der Anschlussbelegung: TestTEST (Die verwendeten Abkürzungen bedeuten: x:x+dy,y-dx)
rückwärts:   Erklärung der Anschlussbelegung: TestTEST (x-dx,y-dy)

Diese Angaben sind ein Spezialfall für eine Formel, mit der man Vektoren drehen kann. Normalerweise würde man einen Vektor der Länge 'l' wie folgt mit Sinus und Cosinus in eine bestimmte Richtung 'w' drehen, wobei die Null-Richtung nach oben zeigen soll (wie in Einleitung: LOGO
Übergabe von Argumenten und Ergebnissen: LOGO:
LOGO
):

  dx = l*sin(w)
  dy = l*cos(w)

um ihn um den Winkel dw zu drehen, würde man dann wohl so vorgehen:

  dx = l*sin(w+dw)
  dy = l*cos(w+dw)

Speziell bei Anwendungen, bei denen der Vektor aber immer um einen konstanten Betrag gedreht wird (Bei Rekursion - Selbst-Aufruf einer Prozedur: Füll-AlgorithmusFüll-Algorithmus immer um 90 Grad, oder beim Zeichnen von Kreise zum Beispiel!), ist es sinnvoll, auf folgende trigonometrische Gleichungen zurückzugreifen:

  d.Die verwendeten Abkürzungen bedeuten: x:x = dx*cos(dw) - dy*sin(dw)
  d.y = dy*cos(dw) + dx*sin(dw)

Wobei d.x und d.y die Komponenten des neuen Vektors sind, dx und dy die des alten. Für diese Formel braucht man dann SIN(dw) und COS(dw) nur noch einmal zu berechnen bzw. bei 90 Grad sind SIN(90)=1 und COS(90)=0

Valid HTML   Valid CSS