Tutorial Quickstart
Inhaltsverzeichnis
Quickstart: Delphi & OpenGL
Einleitung
Willkommen beim Delphi & OpenGL Quickstart. Diese kurze Einleitung soll euch auf die Tutorials bei DelphiGL.com und allgemein auf die Grafikprogrammierung mit OpenGL und Delphi vorbereiten. Dieser Quickstart ist kein Tutorial für sich, sondern soll ein Grundgefühl vermitteln wie OpenGL und Delphi miteinander arbeiten.
Wozu OpenGL? Mit OpenGL kann man eine Vielzahl von Aufgaben bewältigen. Ob man Forschungsergebnisse aller Art visualisieren, 2D oder 3D Spiele schreiben oder einfach seiner Anwendung eine Oberfläche geben möchte, die nicht dem windowsgrauen Standardlook entspricht. All das ist möglich mit OpenGL.
Wie fange ich an?
Genau zwei Dinge braucht der OpenGL-Programmierer um effektiv arbeiten zu können:
- Einen OpenGL Header
- Eine Codebasis von der aus man neue Projekte starten kann (ein sog. Template)
Der OpenGL-Header
Das is ja easy! Denn Delphi bringt ja schon einen OpenGL-Header mit...
STOP!
Denn wir reden von einem guten Header. Leider ist der original von Delphi mitgelieferte Header alles andere als zu empfehlen. Er ist fehlerhaft, hält sich nicht an OpenGL-Normen und außerdem ist er absolut veraltet.
Was nun? Ganz einfach: Bei DelphiGL.com (kurz DGL) gibt es DEN OpenGL-Header für alle Pascalsprachen: Die DGLOpenGL.pas.
Diesen solltet Ihr euch jetzt besorgen, wenn Ihr ihn nicht schon habt. Der Header wird bei neuen OpenGL-Versionen vom DGL-Team aktualisiert.
Codebasis/Templates
So... das war schon alles was Ihr aus dem Netz benötigt. Den Rest machen wir jetzt per Hand.
Im nächsten Kapitel zeige ich euch wie man sich ein einfaches Template schreibt. Natürlich hat DelphiGL.com auch bereits fertige Lösungen, die durchaus zu empfehlen sind und auch extra Features wie Vollbildrendering besitzen. ABER aus Erfahrung kann ich sagen: Man findet sich im eigenen Code viel einfacher zurecht. (Und die Extras kann man nachher immer noch einbauen.)
Das Template - Delphi fit für OpenGL machen
Bevor man wirklich loslegen kann, muss noch die runtergeladene DGLOpenGL.pas an den richtigen Ort gebracht werden. Gut wäre z. B., sie in das Verzeichnis "\lib" in Eurem Delphiverzeichnis zu legen. (Wenn ihr den DGLSDK verwendet wurden die Suchpfade schon eingerichtet.)
Dann startet mal Delphi. Vor euch sollte jetzt ein leeres Projekt erscheinen. Das leere Formular kann gleich minimiert werden, denn jetzt wird erstmal hübsch gecodet.
(Am Ende von Kapitel 1.3 findet ihr den Kopf der Template-Klasse. Dort seht ihr auch welche Variablen, von welchem Typ deklariert werden müssen. )
Initialisieren von OpenGL
Dieser Teil ließ früher dem OpenGL Anfänger die Haare nicht nur zu Berge stehen, sondern gleich ausfallen. Dank der DGLOpenGL.pas wurde das aber um Längen einfacher.
Zuerst einmal solltet Ihr die DGLOpenGL.pas in die uses-Klausel des interface-Teils der Unit1 schreiben.
Die eigentliche Initialisierung soll direkt beim Erstellen des Formulars gemacht werden. Deshalb kommt der folgende Quelltext ins OnCreate-Ereignis des Formulars.
procedure TForm1.FormCreate(Sender: TObject); begin DC:= GetDC(Handle); if not InitOpenGL then Application.Terminate; RC:= CreateRenderingContext( DC, [opDoubleBuffered], 32, 24, 0,0,0, 0); ActivateRenderingContext(DC, RC); end;
Zeile 3: Hier wird der Gerätekontext (Device Context) von Formular Form1 abgefragt.
Zeile 4: Mit InitOpenGL wird OpenGL initialisiert. Wenn das nicht funktioniert wird die gesamte Anwendung sofort beendet.
Zeile 5: Hier wird der Renderkontext erzeugt. Den braucht OpenGL zum Zeichnen auf das Formular. Was die Parameter genau bewirken lernt ihr im Tutorial_lektion1.
Zeile 11: Abschließend wird der Renderkontext aktiviert. OpenGL ist jetzt prinzipiell startbereit.
DC und RC sind Eigenschaften des Formulars. Siehe Definition des Templateformulars. |
Nach dieser durchaus simplen Initialisierung (man kann auch alles per Hand machen was InitOpenGL macht!) steht OpenGL ziemlich nackt da. Soll heißen, alle OpenGL Eigenschaften/Zustände stehen auf den definierten Anfangswerten. Es kommt aber durchaus oft - eigentlich ständig - vor, dass bestimmte Einstellungen von OpenGL benutzt werden sollen. Deshalb schreiben wir uns noch eine kleine Zusatzprozedur: SetupOpenGL
procedure TForm1.SetupGL; begin glClearColor(0.3, 0.4, 0.7, 0.0); //Hintergrundfarbe glEnable(GL_DEPTH_TEST); //Tiefentest aktivieren glEnable(GL_CULL_FACE); //Backface Culling aktivieren end;
Was hier passiert wird durch die Kommentare bereits erklärt (Für mehr Infos siehe Tiefentest bzw. Backface Culling). Die Hintergrundfarbe könnt ihr nach Belieben einstellen. (Wenn ihr später einmal geschlossene Szenen rendern wollt, dann ist es günstig eine sehr schräge Farbe als Hintergrundfarbe einzustellen, so findet man leichter Fehler in der Szene.)
Außerdem hat man ja hin und wieder auch noch globale Variablen, die man initialisieren möchte. Da wir mit solchen Sachen unser schön aufgeräumtes FormCreate nicht zumüllen wollen bietet sich ein Unterprogramm namens InitGlobals oder kurz Init an. Beide Unterprogramme (SetupGL und Init) sollten am Ende von FormCreate gerufen werden:
[...] ActivateRenderingContext(DC, RC); SetupGL; Init; end;
Die Ereignisbehandlung
Für OpenGL sind vor allem die Ereignisse von Bedeutung, die an der Zeichenfläche von OpenGL herumwerkeln. Da OpenGL direkt auf das Formular (oder auch auf ein Panel) zeichnet, müssen Ereignisse, die diese Zeichenfläche ändern, behandelt werden. Dies wären das OnResize- und das OnDestroy-Ereignis.
Zuerst FormResize:
procedure TForm1.FormResize(Sender: TObject); var tmpBool : Boolean; begin glViewport(0, 0, ClientWidth, ClientHeight); glMatrixMode(GL_PROJECTION); glLoadIdentity; gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping); glMatrixMode(GL_MODELVIEW); glLoadIdentity; IdleHandler(Sender, tmpBool); end;
Außerdem müssen im const Teil die beiden Konstanten Near- bzw. FarClipping definiert werden. Diese geben die Entfernung für die Clippingebenen (Szenenbegrenzung) an.
NearClipping = 1; FarClipping = 1000;
Zu FormResize:
Zeile 2: Diese Boolean-Variable wird in Zeile 11 verwendet und ist nur ein Dummy.
Zeile 4: Mittels glViewport sagt Ihr OpenGL, wie groß die OpenGL-Ausgabe werden soll. Genau diese Größe hatte sich ja durch das Resize verändert.
Zeile 5/9: Hier seht Ihr 2 der 3 möglichen Matrixmodi. GL_PROJECTION wird benutzt um nachfolgend die OpenGL-Ausgabe zu manipulieren, GL_MODELVIEW benutzt man um OpenGL mit Daten zu füttern.
Zeile 6: glLoadIdentity füllt die aktuelle Matrix mit der Identitätsmatrix.
Zeile 7: Hier wird eingestellt wie der Betrachter die Welt sehen soll.
Zeile 11: Was der IdleHandler macht kommt später im Abschnitt 1.3.3 (Zeichenroutine).
Nun noch schnell das FormDestroy:
procedure TForm1.FormDestroy(Sender: TObject); begin DeactivateRenderingContext; DestroyRenderingContext(RC); ReleaseDC(Handle, DC); end;
Was es macht? Steht doch da: Den RenderingContext deaktivieren und freigeben.
Die Zeichenroutine
Das Herzstück unseres Templates fehlte bisher. Irgendwann muss der Grafikkarte ja auch gesagt werden, was sie denn überhaupt ausgeben soll. Das kommt jetzt: TForm1.Render
procedure TForm1.Render; begin glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_PROJECTION); glLoadIdentity; gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping); glMatrixMode(GL_MODELVIEW); glLoadIdentity; glTranslatef(0, 0, -5); glBegin(GL_QUADS); glColor3f(1, 0, 0); glVertex3f(0, 0, 0); glColor3f(0, 1, 0); glVertex3f(1, 0, 0); glColor3f(0, 0, 1); glVertex3f(1, 1, 0); glColor3f(1, 1, 0); glVertex3f(0, 1, 0); glEnd; SwapBuffers(DC); end;
Zeile 3: Dieser Aufruf sorgt dafür, dass der Farbpuffer und Tiefenpuffer gelöscht werden. Wenn man das nicht macht, sieht man alles mögliche, nur nicht das was Ihr rendern wollt. Probiert es ruhig mal ohne aus! Man wird dadurch nicht dümmer.
Zeile 7: Hier wird wieder die Perspektive gesetzt. Dieser Aufruf und der bei FormResize müssen von den Parametern identisch sein. Sonst sieht die Ausgabe nach einem Resize kurz anders aus. Wenn sich die Perspektive zwischen den Renderdurchgängen nicht ändert kann das auch weg gelassen werden.
Zeile 12: Dieser Aufruf verschiebt die "Kamera" (so etwas gibt es eigentlich nicht, aber das nur am Rande. Mehr zu diesem Thema gibts im Tutorial Matrix2) etwas nach hinten. Schließlich wollen wir das, was wir zeichnen auch sehen. Alles was zu nah ist wird durch die Near-Clipping Plane abgeschnitten.
Zeile 14: glBegin/glEnd kapseln die eigentlichen Zeichenbefehle. Diese sorgen hier für ein hübsches buntes Viereck. Das soll für den ersten Test ausreichen. Wichtig ist in dem Zusammenhang noch folgendes: OpenGL ist es egal woher ein Befehl kommt. Alles wird ausgewertet und landet unter Umständen im Framebuffer. Ihr könnt also ein Unterprogramm, welches ein Unterprogramm, welches ... ..., welches die OpenGL Befehle enthält schreiben. Das interessiert OpenGL bzw. die Grafikkarte überhaupt nicht.
Zeile 21: SwapBuffers sorgt dafür, dass der Inhalt des Framebuffers auf dem Bildschirm erscheint. Ohne diesen Befehl seht Ihr gar nichts von OpenGL. (Interessanter Artikel dazu: Doppelpufferung)
So... ganz toll. Jetzt habt Ihr Eure Zeichenfunktion... und nun? Irgendwie müsst Ihr diese auch aufrufen. Das wäre aber zu einfach. Die Ausgabe ändert sich ja normalerweise (z. B. in Spielen). Deshalb muss die Zeichenfunktion immer wieder ausgegeben werden. Dazu gibt es zwei Möglichkeiten, die beide Ihre Vor- und Nachteile haben.
Argument | Timer | OnIdle |
---|---|---|
Maximale Framezahl erreichbar "Benchmark" |
nein | ja |
Framezahl steuerbar | ja | bedingt (/umständlich) |
Für flüssige Animationen nutzbar (Egoshooter) |
bedingt/schlecht | ja |
Für Menüs nutzbar | ja | ja |
Für einfache Animationen nutzbar (Strategiespiele) |
ja | ja |
Laptopfreundlich (Anti-Akku-Killer) | ja | NEIN! |
Wer einfache Anwendungen schreiben möchte, die auch mit 25 FpS (Bilder Pro Sekunde) auskommen und den Akku von Laptopusern schonen will, sollte die Timervariante nutzen. Wer die Potenziale der Grafikkarten voll ausnutzen möchte sollte OnIdle verwenden.
Methode 1: Timer
Bei dieser Methode muss ein Timer (zu finden bei den Systemkomponenten) auf das Formular gezogen werden. Der Timer besitzt eine Eigenschaft names "Interval". Mit dieser Eigenschaft kann man einstellen nach wie vielen Millisekunden das Ereignis OnTimer ausgelöst wird. Man kann "Interval" nicht beliebig verkleinern. Werte unter 25 können vom Standardtimer den Windows verwendet nicht mehr korrekt erzeugt werden.
Der Inhalt von OnTimer könnte dieser sein:
procedure TForm1.Timer1Timer(Sender: TObject); begin inc(FrameCount); Render; If FrameCount = 20 then begin ErrorHandler; FrameCount := 0; end; end;
Tiefgreifende Erklärungen sind hier nicht notwendig. Was der ErrorHandler ist wird nach der Methode 2 erklärt.
Methode 2: OnIdle
OnIdle ist ein besonderes Ereignis, welches das gesamte Programm betrifft. Wenn die Anwendung nichts zu tun hat, also faul ist (engl. idle), tritt das Ereignis ein.
Die Methode mit OnIdle kann gleich mit zum Auswerten der Framezahlen (Anzahl Bildwiederholungen pro Sekunde) benutzt werden (Framecounter). Der nachfolgende Code enthält selbigen bereits.
procedure TForm1.IdleHandler(Sender: TObject; var Done: Boolean); begin StartTime:= GetTickCount; Render; DrawTime:= GetTickCount - StartTime; Inc(TimeCount, DrawTime); Inc(FrameCount); if TimeCount >= 1000 then begin Frames:= FrameCount; TimeCount:= TimeCount - 1000; FrameCount:= 0; Caption:= InttoStr(Frames) + 'FPS'; ErrorHandler; end; Done:= false; end;
Zeile 3: Mittels GetTickCount wird die Systemzeit gemessen. Dies ist nicht nötig um erfolgreich zu zeichnen, sondern dient ausschließlich der Berechnung der Framerate. Diese wiederum ist ein guter Performancemesser.
Zeile 4: Hier erfolgt der Aufruf unserer Zeichenroutine.
Zeile 9: Der hier angeordnete Block wird nur pro Sekunde einmal ausgeführt und sorgt dafür, dass die Framerate angezeigt wird. Außerdem wird der Errorhandler aufgerufen.
Zeile 14: Der Errorhandler wird im Anschluß beschrieben.
Zeile 17: Wenn Done nach der Ausführung false ist und das Programm wieder nichts zu tun hat, wird OnIdle erneut ausgeführt. Wenn Done = true ist wird OnIdle nur einmal ausgeführt.
Um auf das "Idle-Event" reagieren zu können, müsst ihr jetzt nur noch diese Funktion an das Event koppeln. Das macht ihr, indem ihr den nachfolgenden Code in die letzte Zeile eurer FormCreate-Methode schreibt.
Application.OnIdle := IdleHandler;
Wie ihr bald selbst feststellen werdet, sorgt diese Methode für eine hundertprozentige Prozessorauslastung. Umgehen könnt ihr dies mit einem kleinen Trick:
Fügt vor dem "Done := false" (Zeile 17 des IdleHandlers) noch ein sleep(1) oder sleep(5) ein. Dadurch sinkt die Prozessorlast auf ca. 80%, was schon ein Fortschritt ist.
Der ErrorHandler - Fehler erkannt, Fehler gebannt
Der Errorhandler ist wieder eine total einfache Funktion, denn OpenGL bietet von Haus aus eine Möglichkeit OpenGL-Fehler zu erkennen. Deshalb ist der ErrorHandler auch so klein:
procedure TForm1.ErrorHandler; begin Form1.Caption := gluErrorString(glGetError); end;
Enttäuscht? Ihr könnt den ErrorHandler nach belieben auch komplexer machen. Zum Beispiel könnt Ihr anstatt die Fehler im Fenstertitel anzuzeigen lieber den Fehler in ein Logfile schreiben. Ganz nebenbei: Falls kein Fehler auftritt liefert glGetError GL_NO_ERROR.
Das fertige Templateformular
Eure Klasse TForm1 sollte also jetzt so, oder so ähnlich aussehen:
TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure IdleHandler(Sender: TObject; var Done: Boolean); procedure FormResize(Sender: TObject); procedure FormDestroy(Sender: TObject); private { Private-Deklarationen } StartTime, TimeCount, FrameCount : Cardinal; //FrameCounter Frames, DrawTime : Cardinal; //& Timebased Movement procedure SetupGL; procedure Init; procedure Render; procedure ErrorHandler; public { Public-Deklarationen } DC : HDC; //Handle auf Zeichenfläche RC : HGLRC;//Rendering Context end;
Zeile 3: Wie man sieht benutzt mein Template die zweite Methode (OnIdle).
Zeile 8: Was Timebased Movement ist könnt ihr ja mal nachlesen.
Zeile 14/15: HDC und HGLRC sind Typen die von Windows zur Verfügung gestellt werden. Ihr findet sie in der Unit "Windows". (Diese Unit sollte bei einem neuen Projekt bereits durch Delphi eingebunden worden sein.)
So... fertig. Eigentlich seid Ihr jetzt soweit von den Tutorialschreibern so richtig mit OpenGL-Wissen voll gepumpt zu werden. Das nachfolgende Kapitel könnt Ihr euch trotzdem ruhigen Gewissens durchlesen. Wer es liest tappt vielleicht nicht gleich bei der ersten Frage im DGL-Forum ins berüchtigte Fettnäpfchen.
Tipps für den OpenGL Anfänger
Wenn man mit OpenGL anfängt ist man meist total perplex und ein "Das war ja einfach!" huscht einem nicht nur einmal über die Lippen. Vor allem zu Beginn Eurer OpenGL-Karriere werdet Ihr viel lernen und dabei nur auf verhältnismäßig geringen Widerstand stoßen. Aber glaubt mir es gibt ihn...
Häufig tauchen hoch motivierte OpenGL Anfänger im Forum auf und verkünden stolz, sie würden gerade an einer Engine arbeiten die "nur auf Doom 1 Niveau arbeiten soll".
Dazu gibt es häufig eine durchaus beachtliche Anzahl von Forenmitgliedern die dann ungefähr folgendes sagen: "Fang gar nicht erst damit an. Vergiss es, und komm in einem Jahr noch mal darauf zurück!" (Die Forensuche sollte euch einige dieser Threads zeigen)
Sind das böse Pessimisten, die Probleme nutzen um aufzugeben? Nein! Sie haben zumeist die Erfahrungen gemacht, die Ihr noch machen werdet. Deshalb will ich euch an dieser Stelle einweihen:
1. Je älter der Code ist desto besser wird er.
Leider nein. Code und Wein unterscheiden sich hier leider grundlegend. Viele... die meisten... eigentlich alle von uns bekommen Ausschlag, wenn sie sich ihren Code von vor 1 Jahr angucken. Erst wenn man so schätzungsweise 3 bis 5 Jahre mit OpenGL gearbeitet hat, hat man ein echtes Gefühl für den Code. Am Anfang hat man nämlich, ob man will oder nicht, die Tendenz schon nach den ersten 3 Wochen Programmierarbeit sich den Code so zu zerschießen, dass die weitere Arbeit keinen Spaß mehr macht.
Ich selbst wollte jetzt ein Projekt weiterbearbeiten, welches ungefähr ein 3/4 Jahr alt ist... Ich hab den Code weggeworfen und das fast fertige Spiel neu angefangen. (Glaubt mir: Aus solchen Fehlern lernt man!)
2. Große Projekte = Großer Ruhm
Stimmt! ABER Ruhm gibt es in der Szene nur für beendete Projekte. Und dreimal dürft Ihr raten was durch die im Punkt 1 angesprochenen Probleme meist nicht mit euren Projekten passiert...
Wenn Ihr OpenGL nur zur Visualisierung von z. B. wissenschaftlichen Ergebnissen benutzt, ist die Arbeit im Bezug auf OpenGL sehr übersichtlich. Wenn Ihr allerdings Spiele programmieren wollt, werdet Ihr schnell merken, dass Probleme auf euch zukommen werden, die euch beim Projektstart völlig unbekannt waren. Dies ist ein sicheres Zeichen dafür, dass doch noch etwas mehr Erfahrung nötig sein wird.
Und woher soll ich bitte Erfahrung nehmen?
Darin liegt der Trick:
Alle OpenGLer haben einmal "klein" angefangen. Berühmt-berüchtigt sind die 3DPong-Clone, die zahlreich im Internet anzufinden sind. Auch Tetris-, Memory- oder "Vier Gewinnt"-Clone sind solche stillen Zeugen eines großen Lernvorgangs. Das Besondere an solchen Spielen ist, dass die Spiellogik relativ einfach ist und euch damit nur wenig von der Visualisierung ablenkt.
Ihr denkt jetzt bestimmt "Was kann man denn schon in so nem Clon unterbringen?" VERDAMMT VIEL! Hier mal ein kleiner Auszug:
- 3D-Spieldarstellung
- 2D Menüführung (glOrtho)
- Blending
- Texturen
- Licht und Materialien (Ihr werdet Augen machen!)
- Selektion (Hin und wieder zum Haare raufen.)
- Kamerasteuerung /Bewegung durch die Szene (Da gibt es ganze Tutorials dazu)
Als Ansporn solltet Ihr euch am Anfang ein kleines Ziel setzen und sagen "Ich möchte den besten und schönsten XYZ-Clon im ganzen Internet schaffen!". So etwas trifft in der Szene auf wesentlich mehr Anerkennung als wenn mal wieder ein Anfänger etwas von Engine und Doom X faselt.
Wenn Ihr ein oder zwei solcher Projekte abgeschlossen habt, und regelmäßig im Forum bzw. Wiki gelesen habt werdet Ihr schon merken, wenn Euer Traumprojekt endlich in Angriff genommen werden kann. (Ganz vergessen müsst ihr es nämlich doch nicht ;) )
Als kurze Zusammenfassung solltet Ihr euch merken:
- Ein fertiges Projekt bringt euch Ruhm (Seelenbalsam).
- Ein hübsches, fertiges Projekt bringt euch mehr Ruhm.
- Ein großes, fertiges, hübsches Projekt bringt euch noch mehr Ruhm.
- Ein abgebrochenes oder eingefrorenes Projekt bringt euch Frust.
- Ein großes, abgebrochenes Projekt bringt euch noch mehr Frust, denn der verschwendeten Zeit werdet Ihr nachtrauern.
3.Das es lang dauert ist egal. Ich interessiere mich halt dafür.
Diese Aussage gilt nur dann, wenn Ihr schon über ein Jahr an ein und demselben Projekt gearbeitet habt. Anfänglich ist das kein Problem. Aber wenn man lange an etwas arbeitet und die gemachten Änderungen sind nicht sichtbar (weil sie z. B. den eigentlichen Motor der Anwendung betreffen und nicht die Ausgabe) dann verliert man schon mal die Lust. Folge sind die berüchtigten "Erfolgsmeldungen" wie: "Das Projekt wurde von mir bis auf weiteres aufs Eis gelegt. Ich werde sicherlich später daran weiterarbeiten." In Verbindung mit Punkt 1 und den letzen Satz aus Punkt 2 sollte euch das Endergebnis klar sein.
Nachwort
Soviel zur Euphoriebremse. OpenGL ist toll. OpenGL ist die Lösung für Eure Traumanwendung. Aber die wird auch mit OpenGL nicht von heute auf morgen programmiert. Deshalb heißt es Tutorials lesen kleine Testanwendungen schreiben um Effekte zu testen und Projekte bearbeiten. Dann wird es auch etwas mit den Traumprojekten. Zudem kann man anfangs auch versuchen in sehr kleinen Anwendungen einen Effekt auszuprobieren und diesen in Form eines kleinen Beispielprogramms oder eines Tutorials zu veröffenlichen. Somit lernt man nicht nur selbst etwas dazu...
Nach diesen unglaublich Weise klingenden Worten die ich mit einem hoch ernsten Gesicht geschrieben habe, könnt ihr euch jetzt hochmotiviert an die restlichen Tutorials machen.
Bis bald im Forum
Euer
Flash (Kevin Fleischer)
PS: Feedback wird nicht nur gewünscht, sondern ausdrücklich gefordert. Deshalb: Ab mit deinen Meinungen und Ratschläge ins Feedback-Forum!
|
||
Vorhergehendes Tutorial: - |
Nächstes Tutorial: Tutorial_lektion1 |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |