Was du hier lernst
- OpenGL-Grundlagen
- Triangle-Strip und Triangle-Fan
- Rotation statt Trigonometrie
- Matrizen sichern mit glPushMatrix
- Farbverläufe mit glShadeModel
- Licht, Normalen und Materialien
Die Erklärung stammt – leicht überarbeitet für Python 3 – aus dem Buch „Coding for Fun mit Python“ von Lars Heppert, Kapitel 1, Abschnitte 1.7 bis 1.12.
Eine schickere Uhr im 3D-Raum
Von unserer analogen Uhr kann man die Zeit zwar ablesen, aber wirklich schön ist sie nicht gerade. Deshalb bauen wir sie jetzt noch einmal, diesmal direkt mit der OpenGL-API: mit schickeren Zeigern, geglätteten Kanten und – weil wir dabei im 3D-Raum zeichnen – einer Uhr, die sich drehen kann.
OpenGL steht für »Open Graphics Library« und ist eine plattform- und programmiersprachenunabhängige Schnittstelle zur Darstellung von 2D- und 3D-Grafik. Ein großer Vorteil und Hauptgrund für ihre weite Verbreitung ist die Unabhängigkeit von einzelnen Herstellern: Über Erweiterungen des Standards entscheidet ein Gremium, heute die Khronos Group. Das Grundprinzip der Uhr ändert sich nicht, nur weil sie jetzt mit OpenGL gezeichnet wird – und auch das Zeichnen selbst ist nicht schwer.
OpenGL heute
Die Befehle in diesem Kapitel – glBegin, glVertex3f, glEnd – gehören zur klassischen »Fixed Function Pipeline« von OpenGL. Moderne Programme schicken ihre Daten stattdessen gebündelt an kleine Programme auf der Grafikkarte, sogenannte Shader, und neue Projekte setzen oft auf Vulkan, Metal oder im Browser auf WebGL und WebGPU. Apple hat OpenGL 2018 für veraltet erklärt, es läuft aber weiterhin. Zum Lernen ist die klassische Variante unschlagbar: Man sieht jeden Punkt, den man zeichnet.
Die erste Frage lautet wieder einmal: Was müssen wir importieren? Neben math und datetime für Winkel und Uhrzeit brauchen wir OpenGL und pygame:
import math
import datetime
from OpenGL.GL import *
import pygame
from pygame.locals import *
Die Hauptroutine unterscheidet sich nicht grundlegend von der 2D-Uhr. Neu ist das Flag OPENGL, und von besonderer Bedeutung sind zwei Zeilen für die Kantenglättung:
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1)
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 4)
Sie sorgen für eine sehr glatte und damit viel schönere Darstellung. Dass es vier Samples pro Pixel sind, ergab sich einfach durch Testen. Mit zwei Samples sieht die Glättung nicht so schön aus, mehr als vier sind dagegen unnötig – glatter als glatt geht einfach nicht. Nebenbei zählt die Hauptschleife die Bilder pro Sekunde und gibt sie beim Beenden aus. Für diese Uhr wäre das nicht nötig, aber bei eigenen, aufwendigeren Projekten siehst du so schnell, ob die Hardware noch mitkommt.
resize() richtet passend zur Fenstergröße die Ansicht ein. Zunächst wird die Projektionsmatrix geladen und durch eine Parallelprojektion ersetzt – glOrtho() –, danach wird die Modellmatrix aktiviert, in der wir zeichnen. init() legt fest, wie Flächen gefüllt werden (glShadeModel(GL_SMOOTH) für Farbverläufe), mit welcher Farbe der Hintergrund gelöscht wird und dass der Tiefentest aktiv ist: Weiter hinten liegende, verdeckte Teile sind dann unabhängig von der Zeichenreihenfolge unsichtbar.
Zeichnen mit OpenGL
In OpenGL ist die kleinste Einheit ein Punkt im Raum. Mehrere Punkte ergeben Flächen, die mit Farbe gefüllt werden – einfarbig oder als Verlauf. Gezeichnet wird in Blöcken zwischen glBegin() und glEnd(), die jeweils auf bestimmte Formen spezialisiert sind.
Wie bei der 2D-Uhr beginnen wir mit einem Ring. Dafür verwenden wir einen Triangle-Strip, einen Streifen aus Dreiecken: Du gibst einfach Punkte in Folge an, und ab dem dritten Punkt bildet jeder neue Punkt mit den beiden vorherigen ein weiteres Dreieck. Setzt man die Punkte abwechselnd auf den inneren und den äußeren Rand, schließt sich der Streifen zu einem Ring:
def drawRing(radius, width, depth):
outerRadius = radius + width / 2
innerRadius = radius - width / 2
numCircleVertices = 1000
inner = True
glColor4f(0.9, 0.4, 0.2, 1.0)
glBegin(GL_TRIANGLE_STRIP)
for i in range(numCircleVertices):
angle = (i / (numCircleVertices - 2)) * 2 * math.pi
if inner:
currentRadius = innerRadius
else:
currentRadius = outerRadius
x = math.cos(angle) * currentRadius
y = math.sin(angle) * currentRadius
inner = not inner
glVertex3f(x, y, depth)
glEnd()
Je mehr Dreiecke, desto glatter der Ring – 1000 Punkte sind mehr als genug. glVertex3f() setzt jeweils einen Punkt aus drei Koordinaten im float-Format, glColor4f() die Farbe aus Rot, Grün, Blau und Deckkraft, jeweils zwischen 0.0 und 1.0. Ganz egal ist die Reihenfolge der Punkte übrigens nicht: OpenGL nimmt Flächen, deren Punkte im Uhrzeigersinn angegeben sind, als von hinten gesehen an. Diese Auswertung heißt Winding.
| Farbe | Rot | Grün | Blau |
|---|---|---|---|
| Schwarz | 0.0 | 0.0 | 0.0 |
| Rot | 1.0 | 0.0 | 0.0 |
| Grün | 0.0 | 1.0 | 0.0 |
| Gelb | 1.0 | 1.0 | 0.0 |
| Blau | 0.0 | 0.0 | 1.0 |
| Orange | 0.98 | 0.625 | 0.12 |
| Lila | 0.6 | 0.4 | 0.7 |
| Weiß | 1.0 | 1.0 | 1.0 |
Mein Rat aus dem Buch gilt noch immer: Rechne bei OpenGL möglichst alles mit Kommazahlen. OpenGL arbeitet intern ebenfalls mit Floats und müsste andere Datentypen ständig umwandeln.
Der Trick mit der Rotation
Nun müssen noch die Zeiger her. Sie werden einfach als spitze Dreiecke dargestellt – und immer auf 12 Uhr gezeichnet:
def drawPointer(width, length, depth, position, scale):
angle = position / scale * 360.0
glPushMatrix()
glRotatef(+angle, 0.0, 0.0, 1.0)
glBegin(GL_TRIANGLES)
glVertex3f(-width / 2, 0.0, depth)
glVertex3f(+width / 2, 0.0, depth)
glVertex3f(0.0, -length, depth)
glVertex3f(-width / 2, 0.0, depth)
glVertex3f(+width / 2, 0.0, depth)
if length > 0:
glVertex3f(0.0, +0.2, depth)
else:
glVertex3f(0.0, -0.2, depth)
glEnd()
glPopMatrix()
Stell dir vor, du willst auf einem Blatt Papier eine Uhr zeichnen. Für den Stundenzeiger auf 3 Uhr drehst du das Blatt um 90 Grad, zeichnest den Zeiger auf 12 Uhr ein und drehst das Blatt wieder zurück – schon zeigt er auf die 3. Auf Papier ist das sicher nicht die geschickteste Methode, aber in OpenGL erspart sie uns eine Menge Arbeit: Wir müssen weder den Endpunkt des Zeigers noch die Eckpunkte des Dreiecks mit Sinus und Kosinus ausrechnen. Die immer gleichen Punkte genügen, den Rest erledigt die Drehung.
Im Buch wurde die Drehung anfangs mit einer zweiten, entgegengesetzten Rotation rückgängig gemacht. Eleganter geht es mit glPushMatrix() und glPopMatrix(): Das erste sichert den aktuellen Zustand der Matrix, das zweite stellt ihn wieder her. Das ist genauer und stört auch eine Drehung der ganzen Uhr nicht, die wir gleich noch einbauen.
In einem GL_TRIANGLES-Block bilden je drei Punkte ein eigenes Dreieck – anders als beim Strip, wo sich benachbarte Dreiecke Punkte teilen. Das zweite, kleine Dreieck zeigt in die Gegenrichtung und lässt die Zeiger ein wenig über die Mitte hinausragen. Die Zeit stellt dann drawCurrentTime() dar, wieder mit der nächstkleineren Zeiteinheit für flüssig laufende Zeiger:
def drawCurrentTime(hour, minute, second, micro):
glColor4f(0.3, 0.7, 0.2, 1.0)
drawPointer(0.20, 1.0, 0.05, hour + minute / 60.0, 12)
glColor4f(0.3, 0.9, 0.5, 1.0)
drawPointer(0.15, 2.0, 0.10, minute + second / 60.0, 60)
glColor4f(0.8, 0.4, 0.8, 1.0)
drawPointer(0.08, 2.0, 0.15, second + micro / 1000000.0, 60)
Ein Zifferblatt für die Uhr
Was noch fehlt, ist eine klare Markierung der Stunden. Das funktioniert genau wie bei den Zeigern, nur dass die Striche nicht in der Mitte beginnen. Weil sie viereckig sein sollen, zeichnen wir sie als GL_QUADS:
def drawClockFace():
for hour in range(12):
angle = hour / 12 * 360.0
glColor4f(0.4, 0.7, 0.9, 1.0)
glPushMatrix()
glRotatef(+angle, 0.0, 0.0, 1.0)
glBegin(GL_QUADS)
glVertex3f(-0.01, 1.5, 0.1)
glVertex3f(+0.01, 1.5, 0.1)
glVertex3f(+0.05, 1.9, 0.1)
glVertex3f(-0.05, 1.9, 0.1)
glEnd()
glPopMatrix()
Für den Hintergrund des Zifferblatts nehmen wir einen Triangle-Fan, einen Fächer aus Dreiecken um einen gemeinsamen Mittelpunkt. Hier siehst du die Wirkung von glShadeModel(GL_SMOOTH): Der Mittelpunkt hat eine andere Farbe als die Randpunkte, und OpenGL malt dazwischen einen Verlauf. Die Randfarbe hängt von der Uhrzeit ab – der Rotanteil von den Sekunden, der Grünanteil von den Minuten, der Blauanteil von den Stunden. So hat deine Uhr zu jeder Zeit ein anderes Zifferblatt.
# Rot, Grün und Blau passend zur aktuellen Uhrzeit
glColor4f(second / 60.0, minute / 60.0, (hour % 12) / 12.0, 1.0)

Möchtest du lieber ein einfarbiges Zifferblatt, stell das Shademodel testhalber auf GL_FLAT. Dann wird statt eines Verlaufs die Farbe des zuletzt gesetzten Punktes für die ganze Fläche verwendet.
Die Ansicht rotieren lassen
Bisher unterscheidet sich die neue Uhr kaum von der 2D-Version – allenfalls ist sie dank Glättung und spitzer Zeiger etwas schöner. Um ein bisschen mehr zu bieten, lassen wir sie um die x- und die y-Achse rotieren. Den Anlass liefert die Sekunde: In den ersten zehn Sekunden jeder Minute dreht sich die Uhr um die x-Achse, ab der halben Minute zehn Sekunden lang um die y-Achse.
def clockRotate(second, micro, start, stop, rotation):
x, y, z = rotation
clockRotation = (second - start + micro / 1000000.0) / (stop - start) * 360.0
# Rückseite sichtbar? Dann die Normale umdrehen
if 90 < clockRotation < 270:
glNormal3f(0.0, 0.0, +1.0)
if clockRotation <= 360:
glRotatef(clockRotation, x, y, z)
Der Drehwinkel entspricht dem prozentualen Fortschritt im Drehzeitraum: Er startet bei 0 Grad, erreicht nach der Hälfte der Zeit 180 Grad und schließt nach der ganzen Zeit die volle Umdrehung ab. Damit das ohne Sprünge geschieht, rechnen wir – wie bei den Zeigern – die Mikrosekunden mit ein.
Alle Zeichenoperationen sammelt draw(). Zu Beginn wird die Ansicht komplett gelöscht, und glLoadIdentity() stellt die ursprüngliche Blickrichtung wieder her. Das macht alle bisherigen Verschiebungen und Drehungen rückgängig. Der folgende Aufruf von glTranslatef() rückt die Uhr in die Mitte der Ansicht. Die Drehung aus clockRotate() bleibt dann für alles gültig, was danach gezeichnet wird – genau deshalb sichern Zeiger und Zifferblatt die Matrix mit glPushMatrix(), statt sie zurückzusetzen.
Ganz zum Schluss kommt der Rand: zwei Ringe, einer etwas vor und einer etwas hinter der Uhr. Wegen der Parallelprojektion sieht man frontal nur einen, erst bei der Drehung zeigen sich beide. Und dabei auch eine unrealistisch wirkende Lücke zwischen ihnen:

Solche »Fehler« beim Perspektivwechsel sind in der 3D-Programmierung nicht unüblich und müssen immer bedacht werden. Wir korrigieren ihn mit einem »Band« am Rand der Uhr – natürlich wieder ein Triangle-Strip. connectRings() wechselt dafür nicht zwischen innerem und äußerem Radius, sondern zwischen vorderem und hinterem Ring. Der Radius 2.05 ist der Außenrand der Ringe: Radius 2.0 plus die halbe Ringbreite von 0.1.

Individuelle Beleuchtung
Wie so oft im Leben kommt das Interessanteste zum Schluss: das Licht. Bisher hatten alle Farben immer dieselbe Intensität, auch wenn die Uhr sich drehte. In der Realität hängt die Helligkeit aber davon ab, in welchem Winkel das Licht von einer Fläche zu uns zurückgeworfen wird.
OpenGL kennt drei Arten von Licht. Ambientes Licht ist die Grundausleuchtung, die alles unabhängig vom Einfallswinkel aufhellt – ganz nach dem Motto: alles ein bisschen heller bitte. Diffuses Licht ändert die Helligkeit je nach Winkel zwischen Licht und Fläche. Spekulares Licht wirkt punktuell wie ein Spotlight. Meist werden die Arten gemischt:
glEnable(GL_LIGHTING)
glLightfv(GL_LIGHT0, GL_AMBIENT, [0.1, 0.1, 0.1, 1.0])
glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.9, 0.9, 0.9, 1.0])
glLightfv(GL_LIGHT0, GL_POSITION, [0.0, 0.0, -6.0, 1.0])
glEnable(GL_LIGHT0)
Schaltest du nur das Licht ein, bleibt die Uhr schwarz: Es fehlen Materialeigenschaften, und es ist unklar, wie das Licht reflektiert werden soll. Die Materialien bekommen wir geschenkt – GL_COLOR_MATERIAL macht aus jedem glColor()-Aufruf eine Materialeigenschaft, das nennt sich Color Tracking. Für die Reflexion braucht OpenGL dagegen Normalen: Vektoren, die senkrecht auf einer Fläche stehen und das Lot für jeden Reflexionswinkel bilden.
Weil die Uhr flach ist, genügt uns eine einzige Normale, senkrecht zur Uhr. draw() setzt sie vor dem Zeichnen mit glNormal3f(0.0, 0.0, -1.0). Dreht sich die Uhr so weit, dass wir ihre Rückseite sehen – zwischen 90 und 270 Grad –, dreht clockRotate() die Normale um. So werden Vorder- und Rückseite richtig ausgeleuchtet. Für nicht ebene Körper legt man je Punkt eine eigene Normale fest.
Das war schon alles?
Zugegeben, auch diese Uhr ist noch nicht der letzte Schrei. Für ein schöneres Zifferblatt könntest du ein Bild auf die Fläche legen. Größere Hürden sind Komplikationen wie eine Mondphasenanzeige oder ein Datum – und wer Automatikuhren mag, baut eine Gangreserve ein, die zum Beispiel anzeigt, wie lange die Uhr schon läuft. Ist die Uhr dann ein Schmuckstück geworden, ließe sich die ganze Anwendung in einen Bildschirmschoner verwandeln. Die anspruchsvollste Ausbaustufe dürfte eine Kuckucksuhr sein. Und wer kleinere Schritte bevorzugt, beziffert das Zifferblatt – dann hätte es seinen Namen auch verdient.
Auf deinem Rechner starten
Im Browser gibt es kein OpenGL-Fenster, deshalb läuft diese Uhr auf deinem eigenen Rechner. Du brauchst Python 3 und zwei Bibliotheken:
pip install pygame-ce PyOpenGL
python main.py
pygame-ce ist die aktiv gepflegte Community-Ausgabe von pygame; importiert wird sie ganz normal mit import pygame. Escape oder das Schließen des Fensters beendet das Programm, danach steht in der Konsole, wie viele Bilder pro Sekunde deine Grafikkarte geschafft hat.
Probier mal
Ideen zum Weiterbauen – auf deinem eigenen Rechner mit Python und den genannten Bibliotheken.
-
01Einfarbiges Zifferblatt
Der Vorschlag aus dem Buch: Stell das Shademodel testhalber auf
GL_FLAT. Dann gibt es keinen Farbverlauf mehr vom Mittelpunkt zum Rand – jede Fläche bekommt einfach die Farbe ihres zuletzt gesetzten Punktes.- glShadeModel(GL_SMOOTH) + glShadeModel(GL_FLAT) -
02Mit Lücke zwischen den Ringen
Ohne das »Band« aus
connectRings()siehst du bei jeder Drehung den Fehler, den Abbildung 1.10 zeigt: Zwischen dem vorderen und dem hinteren Ring klafft eine Lücke. Solche Fehler beim Perspektivwechsel sind in der 3D-Programmierung ganz normal.- connectRings(2.05, -0.1, +0.1) + # connectRings(2.05, -0.1, +0.1) # ohne Band: Lücke sichtbar -
03Die schnelle Pirouette
In der ersten Fassung aus Abschnitt 1.10 dauerte eine Drehung nur zwei Sekunden – eine flotte Pirouette statt einer gemächlichen Drehung. Du musst dafür nur das Zeitfenster verkleinern.
- if 0 <= second <= 10: - clockRotate(second, micro, 0, 10, (1.0, 0.0, 0.0)) + if 0 <= second <= 2: + clockRotate(second, micro, 0, 2, (1.0, 0.0, 0.0)) - elif 30 <= second <= 40: - clockRotate(second, micro, 30, 40, (0.0, 1.0, 0.0)) + elif 30 <= second <= 32: + clockRotate(second, micro, 30, 32, (0.0, 1.0, 0.0)) -
04Weniger Kantenglättung
»Probieren Sie ruhig auch einmal zwei Samples aus«, heißt es im Buch. Mit zwei statt vier Samples pro Pixel werden die Kanten wieder sichtbar treppig. Mehr als vier bringen dagegen kaum noch etwas – glatter als glatt geht nicht.
- pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 4) + pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 2) -
05Eine zweite Lichtquelle
Das Buch regt an, die Szene um weitere Lichtquellen zu ergänzen. Hier kommt ein orangefarbenes Licht von rechts dazu. OpenGL bietet mindestens acht Lichtquellen,
GL_LIGHT0bisGL_LIGHT7, die sich einzeln einschalten lassen.- glEnable(GL_LIGHT0) + glEnable(GL_LIGHT0) + # eine zweite, orangefarbene Lichtquelle rechts von der Uhr + glLightfv(GL_LIGHT1, GL_DIFFUSE, [0.8, 0.4, 0.0, 1.0]) + glLightfv(GL_LIGHT1, GL_POSITION, [6.0, 2.0, -6.0, 1.0]) + glEnable(GL_LIGHT1)
Vom Buch in den Browser
Diese Seite zeigt den Code nur zum Lesen: Die Uhr zeichnet mit OpenGL in ein eigenes Fenster, und das gibt es im Browser nicht. Das Buch entwickelt die Uhr Schritt für Schritt über die Listings 1.12 bis 1.27. Der Code oben ist der fertige Stand – so, wie ihn die Abbildungen zeigen und wie er in openGLClock.py auf der Buch-CD steht.
Python-3-Änderungen:
resize((width, height))entpackte ein Tupel direkt in der Parameterliste. Das gibt es seit Python 3 nicht mehr, deshalb heißt es jetztresize(width, height)und wird mitresize(*screenSize)aufgerufen.print "fps: %d" % ...wird zur Funktionprint(...). Die Division ist mitmax(1, ...)gegen eine Laufzeit von null Millisekunden abgesichert.float(position) / scaleundi / float(numCircleVertices - 2)werden zu einfachen Divisionen: In Python 3 liefert/immer eine Kommazahl. Die Semikolons am Zeilenende, die Python ohnehin ignoriert, entfallen.- Statt
pygame.event.poll(), das pro Bild nur ein Ereignis abholt, arbeitet die Schleife mitpygame.event.get()alle anstehenden Ereignisse ab, undpygame.quit()räumt am Ende auf. HWSURFACEentfällt, pygame 2 ignoriert das Flag. Die Konstanten für die Kantenglättung heißen ausdrücklichpygame.GL_MULTISAMPLEBUFFERSundpygame.GL_MULTISAMPLESAMPLES, damit sie nicht mit gleichnamigen OpenGL-Konstanten verwechselt werden können.- Überall steht
glRotatef– das Buch mischteglRotatefundglRotate.
Unterschiede zu einzelnen Listings, die im Buch Zwischenstände zeigen:
- Hintergrund. Listing 1.15 löscht noch mit Schwarz. Die fertige Uhr in den Abbildungen hat einen weißen Hintergrund, wie in der CD-Datei.
- Drehung. Listing 1.23 ruft
clockRotate()mit vier statt fünf Argumenten auf – ein Überbleibsel einer Zwischenversion mit zwei Sekunden Drehzeit. Die Uhr folgt Listing 1.26 und 1.27: zehn Sekunden um die x-Achse ab der vollen Minute, zehn Sekunden um die y-Achse ab der halben. Die schnelle Variante findest du unter »Probier mal«. - Licht. Der diffuse Anteil ist 0.9 wie in Listing 1.25. Die CD-Datei verwendete 0.7.
- Matrix sichern. Die Listings 1.17 und 1.19 drehen nach dem Zeichnen mit dem negativen Winkel zurück. Wie im Text von Abschnitt 1.10 beschrieben, sichern Zeiger und Stundenstriche die Matrix jetzt mit
glPushMatrix()undglPopMatrix(). - Tiefe. Zeiger und Stundenstriche liegen wie in der CD-Datei ein kleines Stück vor dem Zifferblatt (z = 0.05 bis 0.15 statt 0). Liegen zwei Flächen exakt in derselben Ebene, kann der Tiefentest bei einer Drehung flackern, weil Rundungsfehler mal die eine, mal die andere Fläche vorn sehen. Die Form der Stundenstriche folgt Listing 1.19.
- Blauanteil. Das Zifferblatt rechnet mit
(hour % 12) / 12.0statthour / 12.0. Sonst wäre der Blauanteil am Nachmittag größer als 1.0 – OpenGL schneidet das zwar ab, aber das Zifferblatt bliebe dann bis Mitternacht gleich blau. - Das ungenutzte
from OpenGL.GLU import *entfällt, die Kommentare stehen auf Deutsch.
Getestet ist der Code mit Python 3.14, pygame-ce 2.5 und PyOpenGL 3.1: Er lässt sich übersetzen, und alle verwendeten Namen sind vorhanden.
Original aus dem Buch ansehen openGLClock.py · Python 2
from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, math, datetime
from pygame.locals import *
def resize((width, height)):
if height==0:
height=1
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-0.2, 4.2, 4.2, -0.2, -6.0, 0.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def init():
glShadeModel(GL_SMOOTH)
glClearColor(1.0, 1.0, 1.0, 0.0)
glClearDepth(1.0)
glEnable(GL_DEPTH_TEST)
glDepthFunc(GL_LEQUAL)
# enable Lighting and create lights
glEnable(GL_LIGHTING)
glLightfv(GL_LIGHT0, GL_AMBIENT, [0.1, 0.1, 0.1, 1.0])
glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.7, 0.7, 0.7, 1.0])
glLightfv(GL_LIGHT0, GL_POSITION, [0.0, 0.0, -6.0, 1.0])
glEnable(GL_LIGHT0)
# enable color tracking
glEnable(GL_COLOR_MATERIAL)
# front material ambient and diffuse colors track glColor
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
# enable normalization of normal vectors
glEnable(GL_NORMALIZE)
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
def draw():
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
now = datetime.datetime.now()
micro = now.microsecond
hour = now.hour
minute = now.minute
second = now.second
glLoadIdentity()
glTranslatef(2.0, 2.0, 3.0)
glNormal3f(0.0, 0.0, -1.0)
if 0 <= second <= 10:
clockRotate(second, micro, 0, 10, (1.0, 0.0, 0.0))
elif 30 <= second <= 40:
clockRotate(second, micro, 30, 40, (0.0, 1.0, 0.0))
drawClockBackground(hour, minute, second)
drawClockFace()
drawCurrentTime(hour, minute, second, micro)
drawRing(2.0, 0.1, -0.1)
drawRing(2.0, 0.1, +0.1)
connectRings(2.05, -0.1, +0.1)
def clockRotate(second, micro, start, stop, rotation):
x, y, z = rotation
clockRotation = (second - start + micro/1000000.0)/(stop-start)*360.0
if 90 < clockRotation < 270:
glNormal3f(0.0, 0.0, +1.0)
if clockRotation <= 360:
glRotate(clockRotation, x, y, z)
def connectRings(radius, depth1, depth2):
numCircleVertices = 1000
inner = True
glColor4f(0.9, 0.4, 0.2, 1.0)
glBegin(GL_TRIANGLE_STRIP)
for i in range(numCircleVertices):
angle = (i / float(numCircleVertices-2)) * 2 * math.pi;
if inner:
depth = depth1
else:
depth = depth2
x = math.cos(angle) * radius
y = math.sin(angle) * radius
inner = not inner
glVertex3f(x, y, depth)
glEnd()
def drawRing(radius, width, depth):
outerRadius = radius+width/2
innerRadius = radius-width/2
numCircleVertices = 1000
inner = True
glColor4f(0.9, 0.4, 0.2, 1.0)
glBegin(GL_TRIANGLE_STRIP)
for i in range(numCircleVertices):
angle = (i / float(numCircleVertices-2)) * 2 * math.pi;
if inner:
currentRadius = innerRadius
else:
currentRadius = outerRadius
x = math.cos(angle) * currentRadius
y = math.sin(angle) * currentRadius
inner = not inner
glVertex3f(x, y, depth)
glEnd()
def drawClockFace():
for hour in range(12):
angle = float(hour)/12*360.0
glColor4f(0.4, 0.7, 0.9, 1.0)
glPushMatrix()
glRotatef(+angle, 0.0, 0.0, 1.0)
glBegin(GL_QUADS)
glVertex3f(-0.02, 1.5, 0.1)
glVertex3f(+0.02, 1.5, 0.1)
glVertex3f(+0.04, 1.9, 0.1)
glVertex3f(-0.04, 1.9, 0.1)
glEnd()
glPopMatrix()
def drawClockBackground(hour, minute, second):
glBegin(GL_TRIANGLE_FAN)
glVertex3f(0.0, 0.0, 0.0) # center of the fan
numCircleVertices = 30
clockRadius = 2.0
for i in range(numCircleVertices):
angle = (i / float(numCircleVertices-1)) * 2 * math.pi;
x = math.cos(angle) * clockRadius
y = math.sin(angle) * clockRadius
glColor4f(second/60.0, minute/60.0, hour/12.0, 1.0)
glVertex3f(x, y, 0.0)
glEnd()
def drawPointer(width, length, depth, position, scale):
angle = float(position)/scale*360.0
glPushMatrix()
glRotatef(+angle, 0.0, 0.0, 1.0)
glBegin(GL_TRIANGLES)
glVertex3f(-width/2, 0.0, depth)
glVertex3f(+width/2, 0.0, depth)
glVertex3f(0.0, -length, depth)
glVertex3f(-width/2, 0.0, depth)
glVertex3f(+width/2, 0.0, depth)
if length > 0:
glVertex3f(0.0, +0.2, depth)
else:
glVertex3f(0.0, -0.2, depth)
glEnd()
glPopMatrix()
def drawCurrentTime(hour, minute, second, micro):
glColor4f(0.3, 0.7, 0.2, 1.0)
drawPointer(0.20, 1.0, 0.05, hour+(minute/60.0), 12)
glColor4f(0.3, 0.9, 0.5, 1.0)
drawPointer(0.15, 2.0, 0.10, minute+(second/60.0), 60)
glColor4f(0.8, 0.4, 0.8, 1.0)
drawPointer(0.08, 2.0, 0.15, second+(micro/1000000.0), 60)
def main():
video_flags = OPENGL|HWSURFACE|DOUBLEBUF
screenSize = (600, 600)
pygame.init()
pygame.display.gl_set_attribute(GL_MULTISAMPLEBUFFERS, 1)
pygame.display.gl_set_attribute(GL_MULTISAMPLESAMPLES, 4)
pygame.display.set_mode(screenSize, video_flags)
resize(screenSize)
init()
frames = 0
ticks = pygame.time.get_ticks()
while True:
event = pygame.event.poll()
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
break
draw()
pygame.display.flip()
frames = frames+1
print "fps: %d" % ((frames*1000)/(pygame.time.get_ticks()-ticks))
if __name__ == '__main__': main()


