3D mit Processing.py

Kugeln und Kisten

Processing und damit auch Processing.py besitzt die Möglichkeit, sehr einfach 3D-Objekte zu erzeugen, allerdings sind als Primitive nur eine Kugel und eine Kiste (sphere() und box()) vorgesehen. Als Erstes möchte ich zeigen, wie man schnell eine sich drehende Kugel damit zaubert:

Screenshot

Um mit Processing in drei Dimensionen zu arbeiten, muß man das bei der Initialisierung des Fensters dem Programm mitteilen:

def setup():
    size(200, 200, P3D)

Eigentlich teilt man Processing auch mit, wenn man in zwei Dimensionen hantieren will, nur ist P2D einfach der Default und kann entfallen.

Dann besitzt Processing eine einfache Methode, die 3D-Landschaft auszuleuchten, nämlich lights(). Und ähnlich wie den Kreisen und Ellipsen ist auch bei einer Kugel per Default, der Ursprung der Koordinaten die Mitte. Daher habe ich mit

    translate(width/2, height/2, 0)

die x- und y-Achsen des Koordinatensystems in die Mitte des Fensters gelegt. Mit sphereDetail(n) wird die Anzahl der Dreiecke bestimmt, aus denen die Kugel zusammengesetzt werden soll. Je mehr Dreiecke, desto »runder« die Kugel, aber auch um so größer die Rechenzeit. Bei diesem einfachen Programm spielt das noch keine Rolle, die Zahl 30 ist eher dem Umstand geschuldet, daß die Kugel vor lauter Dreieicken sonst nicht mehr zu erkennen ist.

Und dann kommt wieder das geniale with-Statement zu Einsatz:

    with pushMatrix():
        rotateX(radians(-10))
        rotateY(a)
        a += 0.01
        sphere(80)

Mit rotateX() wird die Kugel ein wenig geneigt und mit rotateY() dreht sie sich um die eigene Achse. Einfacher kann man eine sich bewegende Kugel in 3D eigentlich gar nicht programmieren.

Der Quellcode

Hier noch einmal der komplette Quellcode des Sketches zum Nachbauen:

a = 0

def setup():
    size(200, 200, P3D)

def draw():
    global a
    background(160)
    lights()
    translate(width/2, height/2, 0)
    sphereDetail(30)
    with pushMatrix():
        rotateX(radians(-10))
        rotateY(a)
        a += 0.01
        sphere(80)

Und es geht doch: Kugeln und Texturen

Ich hatte irrtümlich angenommen, daß man die einfachen 3D-Primitive sphere() und box() nicht mit Texturen versehen kann und man darum dann eigene 3D-Objekte bauen müsse. Nun gibt es jedoch einen einfachen Weg, diese Beschränkung zu umgehen. Denn der Befehl createShape() erzeugt nicht nur ein Objekt, sondern er kann auch Parameter übernehmen. Und so kann man mit

earth = loadImage("bluemarble.jpg")
noStroke()
globe = createShape(SPHERE, 80)
globe.setTexture(earth)

auf einfachste Weise einen Shape erzeugen, den man mit Texturen versehen kann.

Blue Marble als Textur

Hier der vollständige Sketch, der uns diese Erdkugel erzeugt:

a = 0

def setup():
    global globe
    earth = loadImage("bluemarble.jpg")
    size(200, 200, P3D)
    noStroke()
    globe = createShape(SPHERE, 80)
    globe.setTexture(earth)

def draw():
    global a, globe
    background(160)
    lights()
    translate(width/2, height/2, 0)
    sphereDetail(30)
    with pushMatrix():
        rotateX(radians(-25))
        rotateY(a)
        a += 0.01
        shape(globe)

Und noch eine Textur

Die Erde

Und hier noch einmal die Erdkugel mit einer anderen Textur, die ich hier gefunden habe. Schaut man genau hin, entdeckt man, daß die Erde an der Datumsgrenze einen Riß aufweist – ein Phänomen, daß ich hin und wieder schon beobachtet, für daß ich allerdings bis jetzt noch keine Erklärung habe.

Der Quellcode wurde nur geringfügig geändert, aber der Vollständigkeit halber hier noch einmal:

a = 0

def setup():
    global globe
    earth = loadImage("earth.jpg")
    size(400, 400, P3D)
    noStroke()
    globe = createShape(SPHERE, 160)
    globe.setTexture(earth)

def draw():
    global a, globe
    background(51)
    lights()
    translate(width*.5, height*.5, 0)
    # sphereDetail(120)
    with pushMatrix():
        rotateX(radians(-25))
        rotateY(a)
        a += 0.01
        shape(globe)

Die Erde ist eine Kiste

Natürlich kann man das, was im letzten Abschnitt mit einer Kugel angestellt habe, auch mit einer Kiste (in Processing BOX genannt) anstellen. Der einzige Unterschied ist, daß die Textur jeweils komplett auf alle sechs Seiten der Box abgebildet wird.

Screenshot

Aber dann hat man den Beweis: Die Erde ist eine Kiste!

Quellcode

a = 0

def setup():
    global chest
    earth = loadImage("bluemarble.jpg")
    size(400, 400, P3D)
    noStroke()
    chest = createShape(BOX, 180)
    chest.setTexture(earth)

def draw():
    global a, chest
    background(51)
    lights()
    translate(width*.5, height*.5, 0)
    sphereDetail(30)
    with pushMatrix():
        rotateZ(radians(frameCount))
        rotateX(radians(frameCount*.5))
        rotateY(radians(a))
        a += 0.01
        shape(chest)

Licht und Schatten

Screenshot

Bei dreidimensionalen Applikationen gilt für jede Software genau wie im wirklichen Leben: »Ohne Licht sehen Sie nichts!« Das ist bei den spezialisierten Programmen wie Blender oder PoVRay genau so, wie auch in Processing.py. Daher möchte ich in folgendem Skript zeigen, welche Möglichkeiten der Beleuchtung es in Processing gibt und welche Auswirkung sie auf die Szene haben.

Dazu habe ich eine Kugel verschachtelt in einer Box erzeugt und sie in ein 3D-Fenster gesetzt. Sie ist im Grunde farblos, nur für eine Belichtung (lights()) habe ich der Kugel eine hell- und der Box eine dunkelblaue Farbe verpaßt.

Bevor ich die Kugel und die Box zeichnen lasse, überprüfe ich, welche Beleuchtungsfunktion aktuell angewählt ist. Processing kennt sechs Beleuchtungfunktionen. Diese sind

  1. noLights(): Diese schaltet alle Beleuchtung aus und die dreidimensionalen Objekte wirken zweidimensional. Diese Funktion kann benutzt werden, um dreidimensionale Objekte mit zweidimensionalen Zeichnungen zu kombinieren.
  2. lights(): Das ist die einfachste Beleuchtungsfunktion, die die Umgebung in ein neutrales, ambientes Licht taucht. Sie kann immer erst einmal für den Test der dreidiemnsionalen Objekte eingesetzt verwendet werden, bevor man sich an spektakulärere Beleuchtungsmodelle wagt.
  3. directionalLight(v1, v2, v3, nx, ny, nz): Diese Beleuchtungsfunktion besitzt sechs Parameter. Die ersten drei geben die Farbwerte an (es können je nach gewähltem Farbmode entweder RGB- oder HSB-Werte sein). Die letzten drei Werte geben jeweils die Richtung des Lichtes aus der x-, y, und/oder z-Richtung an. Direkte Richtungen sind 0, -1, 0 nach oben, 0, 1, 0 nach unten, 1, 0, 0 nach rechts und -1, 0, 0 nach links. Analog sind die Werte für »Licht von vorne« und »Licht von hinten« einzustellen und durch Kombinationen der drei Parameter bekommt man auch Licht aus beliebigen Richtungen.
  4. ambientLight(v1, v2, v3) taucht die Umgebung in ein ambientes Licht in der mit v1, v2, v3 spezifizierten Farbe (RGB oder HSB). Ambientes Licht wird meist mit anderen Lichtquellen kombiniert, um zum Beispiel die Schlagschatten der anderen, gerichteten Lichtquellen aufzuhellen.
  5. pointLight(v1, v2, v3, x, y, z) setzt ein punktförmiges Licht in den Farben v1, v2, v3 aus der Position x, y, z.
  6. spotLight(v1, v2, v3, x, y, z, nx, ny, nz, angle, concentration) ergibt ein kegelförmiges Licht in den Varben v1, v2, v3 von der Quelle x, y, z in die Richtung nx, ny, nz mit dem Winkel angle und der Intensität concentration.

Alle Beleuchtungsfunktionen müssen innerhalb der draw()-Funktion aufgerufen werden. Werden sie stattdessen in der setup-Funktion aufgerufen, sind sie nur beim ersten Durchlauf wirksam. Daher habe ich das auch im Sketch so gehalten, wobei je nach gewähltem lightMode die Beleuchtung gesetzt wird.

Licht aus – Spot an!

Die Beleuchtung kann man während der Sketch läuft mit der Tastatur auswählen. Die Tasten sind sprechend gewählt:

def keyPressed():
    global lightMode, lightDirection
    if key == "n":
        lightMode = 0            # no lights
    elif key == "l":
        lightMode = 1            # lights
    elif key == "d":
        lightMode = 2            # directional light
    elif key == "a":
        lightMode = 3            # ambient light
    elif key == "p":
        lightMode = 4            # point light
    elif key == "s":
        lightMode = 5            # spot light

Warnung

Bevor man die Tasten drückt, sollte man darauf achten, daß das Graphikfenster von Processing.py im Vordergrund ist, also den Fokus besitzt. Denn sonst tippt man versehentlich gnadenlos Buchstaben in sein Skript und wundert sich, warum es anschließend nicht mehr läuft. Ich verstehe nicht ganz, warum im Python-Mode das Ausgabefenster beim Start des Programmes nicht automatisch den Fokus bekommt, wie das im Java-Mode von Processing der Fall ist?

Ist das direktionale Licht ausgewählt kann man zusätzlich noch mit den Pfeiltasten die Richtung des Lichtes bestimmen.

Quellcode

Wenn man bedenkt, daß in diesem Programm doch einiges passiert, ist der Quellcode immer noch sehr kurz. Das Nachvollziehen sollte auch keine besondere Mühe machen, schließlich wird Python ja oft und zu Recht als lauffähiger Pseudocode bezeichnet.

lightMode = 0
lightDirection = 0

def setup():
    size(640, 480, P3D)
    frame.setTitle("Licht und Schatten")

def draw():
    global lightMode, lightDirection
    background(0)

    # Lichter setzen
    if lightMode == 0:
        noLights()
    elif lightMode == 1:
        lights()
    elif lightMode == 2:
        if lightDirection == 0:
            directionalLight(255, 128, 0, 0, -1, 0) # up
        elif lightDirection == 1:
            directionalLight(0, 255, 0, 1, 0, 0)    # right
        elif lightDirection == 2:
            directionalLight(255, 0, 255, 0, 1, 0)  # down
        elif lightDirection == 3:
            directionalLight(0, 255, 255, -1, 0, 0) # left
    elif lightMode == 3:
        ambientLight(0, 255, 255)
    elif lightMode == 4:
        pointLight(255, 255, 0, 100, height*0.3, 100)
    elif lightMode == 5:
        spotLight(128, 255, 128, 800, 20, 300, -1, .25, 0, PI, 2)
    else:
        noLights()

    # Kugel und Box zeichnen
    with pushMatrix():
        translate(width/2, height/2)
        with pushMatrix():
            rotateY(radians(frameCount))
            fill(255)
            if lightMode == 1:
                fill(151, 255, 255)
            noStroke()
            sphere(160)
        with pushMatrix():
            rotateZ(radians(frameCount))
            rotateX(radians(frameCount/2.0))
            fill(255)
            if lightMode == 1:
                fill(0, 0, 139)
            noStroke()
            box(240)

def keyPressed():
    global lightMode, lightDirection
    if key == "n":
        lightMode = 0            # no lights
    elif key == "l":
        lightMode = 1            # lights
    elif key == "d":
        lightMode = 2            # directional light
    elif key == "a":
        lightMode = 3            # ambient light
    elif key == "p":
        lightMode = 4            # point light
    elif key == "s":
        lightMode = 5            # spot light

    if key == CODED:
        if keyCode == UP:
            lightDirection = 0
        elif keyCode == RIGHT:
            lightDirection = 1
        elif keyCode == DOWN:
            lightDirection = 2
        elif keyCode == LEFT:
           lightDirection = 3

Credits

Dieses Beispielprogramm folgt einer Idee aus dem Bucn »Processing 2: Creative Programming Cookbook« von Jan Vantomme. Ich habe sie geringfügig überarbeitet und vom Processing 2 Java-Mode in den Python-Mode von Processing 3 umgeschrieben.

Literatur

  • Jan Vantomme: Processing 2: Creative Programming Cookbook, Birmingham (Packt Publishing), 2012

Eine Welt aus Gittern und Vertizes: Pulsierende Wellen

Manchmal stellt man fest, daß die Welt nicht nur aus Kisten und Kugeln besteht, sondern daß man auch andere, komplexere dreidimensionale Formen in Processing.py programmieren möchte. Diese fügt man in der Regel aus Gittern (Meshes) zusammen, bewährt haben sich Gitter aus Drei- oder aus Vierecken. Diese kann man sowohl im zwei-, wie auch im dreidimensionalen in Processing aus Vertizes (vertex()) zusammenbauen. Ich habe das mal am Beispiel einer pulsierenden, dreidimensionalen Welle, die auf einem mathematischen Algorithmus beruht, nachprogrammiert1. Doch zuerst einmal einen Screenshot aus einem Video mit dem fertigen Ergebnis:

Eine pulsierende Welle, leider nur statisch, da Screenshot

Zuerst einmal habe ich einige Konstanten definiert und dann im setup() nur die Fenstergröße festgelegt:

ringWidth = 0.04
ringSteps = 8.0
maxi = 240

def setup():
    size(600, 400, P3D)

Die eigentliche Programmlogik beherbergt die Funktion draw(). Hier wird als erstes die Hintergrundfarbe definiert, das Licht eingeschaltet (ohne Beleuchtung sehen Sie im dreidimensionalen Raum von Processing gar nichts) und dann der Ursprung des Koordinatensystems für die x-Achse in die Mitte des Fensters und für die y-Achse in das untere Viertel des Fensters transformiert.

def draw():
    background(108, 131, 163)

    lights()
    translate(width/2, 3*height/8.0)
    rotateX(radians(55))
    scale(height/2.0, height/2.0)

Das Rotieren der x-Achse um 55 Grad füllt das Fenster vollständig aus, nette Ansichten erhalten Sie aber auch, wenn Sie die x-Achse um 20 Grad (rotateX(radians(20))) oder um 85 Grad (rotateX(radians(84))) rotieren. Im ersten Fall sehen Sie die Funktion von oben, im zweiten Fall von der Seite schräg von unten und so können Sie beobachten, wie sich die Spitze der Welle auch nach unten durchschlägt.

Die Funktion ist sehr »schlank«, ohne die scale()-Funktion seheh Sie nur einen Strich. Daher wurde sie in x- und y-Richtung fensterfüllend ausgehnt.

Das eigentliche Zeichnen erledigt die Processing.py-eigene with-Konstruktion:

    with beginShape(QUAD_STRIP):
        i = -ringSteps
        while i < maxi:
            r0 = ringWidth*(i*1.0/ringSteps)
            r1 = r0 + ringWidth
            if r0 < 0.0:
                r0 = 0.0
            theta = i*(TWO_PI/ringSteps)
            makeVertex(r0, theta)
            makeVertex(r1, theta)
            i += 1

Die Vertizes sollen rechteckige Streifen sein (QUAD_STRIP) und die eigentlich Funktion soll in Polarkoordinaten mit rho und theta berechnet werden. Von rho werden zwei Werte (r0 und r1) berechnet, während theta für beide Werte gleich bleibt.

Die Berechnung und das Zeichnen des Gebildes wird in der Funktion makeVertex() erledigt:

def makeVertex(r, theta):
    x = r*cos(theta)
    y = r*sin(theta)
    z = 100*exp(-3*r*r)*cos((r*2*TWO_PI) - (frameCount/50.0))
    fill(163, 143, 109)
    stroke(0)
    strokeWeight(0.01)
    vertex(x, y, z)

In den ersten beiden Zeilen wird die Ebene von Polarkoordinaten in kartesischen Koordinaten transformiert und in der dritten Zeile findet die Berechnung der Wellenfunktion statt. Hier ist der Ort, wo Sie für eigene Experimente eingreifen können. Ändern Sie einzelne Werte der Wellenfunktion und schauen Sie nach, wie sich das auf das Ergebnis auswirkt.

Da sich – zumindest in meinem Processing 3.3.7 – das scale() auch auf die Liniendicke auswirkte, mußte ich das Gewicht der Gitterlinien mit strokeWeigt(0.01) auf einen sehr kleinen Wert setzen. Ansonsten bekam ich nämlich nur sehr, sehr dicke, schwarze Gitterlinien zu sehen, die die eigentliche Welle fast vollständig bedeckten.

beginShape() kann mit diversen Parametern aufgerufen werden, die Einfluß darauf haben, wie die Vertizes gezeichnet werden. Die wichtigsten davon sind:

  • POINTS zeichnet nur die einzelnen Eck-Punkte der Vertizes.
  • LINES zeichnet einzelne Linien, die aber nicht zu geschlossenen Polygonen zusammengeführt werden.
  • TRIANGLES zeichnet einzelne Dreiecke, die ebenfalls keine geschlossene Fläche bilden.
  • TRIANGLE_STRIP zeichnet ein Mesh aus Dreiecken.
  • QUADS zeichnet einzelne Vierecke. Auch diese Vierecke bilden keine geschlossene Fläche.
  • QUAD_STRIP zeichnet ein Mesh aus Vierecken, wie in unserem obigen Programm.

Setzen Sie diese Parameter doch im Sketch einfach einmal einmal ein, um ihre Auswirkungen kennenzulernen. Sie werden interessante Dinge sehen2.

Wird beginShape() ohne Parameter aufgerufen und soll die entstehende Fläche mit einer Füllfarbe gefüllt werden, so muß am Ende zwingend ein endShape(CLOSE) aufgerufen werden. In diesem Falle ist die elegante with-Konstruktion leider nicht möglich.

Zum Schluß noch einmal wie gewohnt das vollständige Programm zum Nachprogrammieren und Nachvollziehen:

ringWidth = 0.04
ringSteps = 8.0
maxi = 240

def setup():
    size(600, 400, P3D)

def draw():
    background(108, 131, 163)

    lights()
    translate(width/2, 3*height/8.0)
    rotateX(radians(55))
    scale(height/2.0, height/2.0)

    with beginShape(QUAD_STRIP):
        i = -ringSteps
        while i < maxi:
            r0 = ringWidth*(i*1.0/ringSteps)
            r1 = r0 + ringWidth
            if r0 < 0.0:
                r0 = 0.0
            theta = i*(TWO_PI/ringSteps)
            makeVertex(r0, theta)
            makeVertex(r1, theta)
            i += 1

def makeVertex(r, theta):
    x = r*cos(theta)
    y = r*sin(theta)
    z = 100*exp(-3*r*r)*cos((r*2*TWO_PI) - (frameCount/50.0))
    fill(163, 143, 109)
    stroke(0)
    strokeWeight(0.01)
    vertex(x, y, z)

Pulsing Wave Video

Natürlich möchte man von solch einer Animation gerne ein Video erstellen. Auch hierbei hilft einem Processing. Zuerst einmal muß man natürlich von jedem Frame einen Screenshot erstellen. Dazu fügen Sie einfach nach der with beginShape()-Schleife folgende Zeilen ein:

    saveFrame("output/wave_####.png")
    if frameCount > 1230:
        print("I did it, Babe!")
        noLoop()

Diese sorgen einmal dafür, daß bei jedem Durchlauf im Ordner output eine Datei wave_####.png abgelegt wird, wobei #### für den aktuelle Framecount steht. Sind genügend Bilder zusammengekommen – in diesem Falle nach 1.230 Bildern –, wird der Durchlauf mit noLoop() gestoppt. Dann rufen Sie im Menü Tools den Unterpunkt Movie-Maker auf und erhalten folgenden Dialog:

Movie Maker

Wie Sie sehen, könnten Sie dem Teil sogar noch eine Sounddatei mitgeben, um ihren Film mit Musik zu unterlegen, doch darauf habe ich verzichtet 3. Das Tool erstellt dann einen QuickTime-Film, der die Animation abspielt.

Krieg der Sterne mit Marx und Engels

Nicht nur Körper lassen sich mit Processing durch den dreidimensionalen Raum wirbeln, das geht auch mit flachen Ebenen und daher auch mit Texten. Das bekannteste Beispiel ist die Titelsequenz aus dem Film »Krieg der Sterne« (Star Wars, USA 1977) und dieses Beispiel möcht ich nun in Processing nachprogrammieren. Doch statt der Eröffnungsgeschichte zum Sternenkrieg habe ich als Textvorlage die Einleitung aus dem Manifest der Kommunistischen Partei gewählt, die Karl Marx und Friedrich Engels 1848 veröffentlicht hatten. Sie beginnt mit dem berühmten Satz: »Ein Gespenst geht um in Europa – das Gespenst des Kommunismus«.

Sternenkrieg mit Marx und Engels

Ich habe die ersten Absätze der Manifests in eine Textdatei geschrieben und diese wie gewohnt in den data-Ordner des Sketches abgelegt, damit Processing sie auch finden kann. Das eigentlich Programm ist mal wieder sehr kurz:


def setup():
    global txt, y
    size(800, 600, P3D)
    y = height/2

    lines = loadStrings("manifest.txt")
    txt = join(lines, "\n")


def draw():
    global txt, y
    background(0)
    translate(width/2, height/2)

    fill(238, 213, 75)
    textSize(width*0.04)
    textAlign(CENTER)
    rotateX(PI/4)
    w = -width*0.6
    text(txt, -w/2, y, w, height*10)

    y -= 1
    if y <= -3300:
        y = height/2

Im setup() habe ich nur die üblichen Vereinbarungen getroffen (Fenstergröße und 3D-Kontext) und dann die Datei zeilenweise geladen und zu einem Gesamttext zusammengefügt.

In draw()bekommt die Animation einen schwarzen Hintergrund verpaßt und den Ursprung des Koordinatensystems habe ich in die Mitte des Fensters gelegt. Der Text bekommt eine goldene Farbe und mit textSize(width*0.04) eine passende Größe (diesen Wert habe ich mal wieder experimentell herausgefunden). Dann wird er zentriert und mit rotateX(PI/4) um 45 Grad nach hinten geneigt. Die nächste Zeile sorgt dafür, daß der Text einen kleinen Abstand rechts und links vom Rand des Fensters hat und dann wird er in den dreidimensionalen Raum gelegt.

Bei jedem Durchlauf wird der y-Wert um eins dekrementiert, so entsteht der Eindruck, daß der Text in den Weiten des Raumes entschwindet. Wenn y <= -3300 wird, ist der Text einmal über den Bildschirm gehuscht und ich lasse das Spiel von vorne beginnen.

Ein Video aus dieser Animation erstellen

Das alles schreit natürlich nach einem Video, das man – wie oben schon gezeigt – mit Processing ganz einfach erstellen kann. Einfach im Sketch diese Zeilen einfügen

saveFrame("output/manifest_####.png")

    y -= 1
    if y <= -3300:
        keepGoing = False
        y = height/2

und dann wieder das Tool Movie Maker aufrufen. Und schon gibt es auch hiervon ein schönes kleines Filmchen.

YouTube zensuriert

Doch versuchen Sie ja nicht, dieses Video bei YouTube hochzuladen. Ich hatte es probiert, doch nur wenige Minuten nach dem Hochladen erhielt ich eine Mail mit folgendem Inhalt:

In unseren Community-Richtlinien wird beschrieben, welche Inhalte auf YouTube erlaubt sind und welche nicht. Dein Video »Manifest« wurde zur Überprüfung gemeldet. Bei der Überprüfung haben wir festgestellt, daß Dein Video gegen unsere Richtlinien verstößt. Deshalb haben wir dieses Video aus YouTube entfernt. Außerdem hat Dein Konto eine Verwarnung wegen der Verletzung unserer Community-Richtlinien bzw. vorübergehende Strafe erhalten.

Das kann doch nicht wahr sein, YouTube hat Angst vor dem Kommunistischen Manifest, einem Text aus dem Jahre 1848, Angst vor Karl Marx und Friedrich Engels und zensiert dieses harmlose Video darüber. Diese Angst der großen Datenkrake vor dem Kommunismus gibt mir ja wieder Hoffnung und macht mich richtig stolz.

Aber es zeigt auch, wohin eine Gesellschaft kommt, wenn man Grundrechte wie das Recht auf freie Meinungsäußerung privatisiert und sie in die Hände von Großkonzernen wie Google und Facebook legt.


  1. Den Algorithmus habe ich in dem wunderbaren Buch »Processing for Visual Artists – How to Create Expressive Images and Interactive Art« von Andrew Glassner (Seiten 101-103) gefunden und (mit einigen Änderungen) von Java nach Python portiert. 

  2. Bei Points müssen Sie allerdings das strokeWeight() heraufsetzten, zum Beispiel auf 0.1, um überhaupt etwas zu sehen. 

  3. Getreu dem Motto von Wilhelm Busch: »Musik wird störend oft empfunden, weil stets sie mit Geräuasch verbunden«.