Tutorial Quickstart

Aus DGL Wiki
Version vom 17. Mai 2005, 13:38 Uhr von Flash (Diskussion | Beiträge) ()

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

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:

  1. Einen OpenGL Header
  2. 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!
Den 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 OpenGLVersionen 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 – Oder Delphi fit für OpenGL machen

Bevor man wirklich loslegen kann, muss noch die runter geladene 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 erstmal minimiert werden, denn jetzt wird erstmal hübsch gecodet.

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 FormCreate.

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 RenderingContext 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 RenderingContext aktiviert. OpenGL ist jetzt prinzipiell startbereit.


Nach dieser durchaus simplen Initialisierung (man kann das 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 Zusatzprozedure: 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 euch 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 FormResize und FormDestroy.

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 nur 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);

  glTranslatef(0, 0, -5);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity;

  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 9: Dieser Aufruf verschiebt die "Kamera" (so etwas gibt es eigentlich nicht, aber da wir uns gerade in der GL_PROJECTION Matrix befinden passt diese Beschreibung am Besten) 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 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 Ihr 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.


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
  gluErrorString(glGetError);
end;

Enttäuscht? Ihr könnt den ErrorHandler nach belieben auch komplexer machen. Oder anstatt die Ausgabe anzuzeigen lieber den Fehler in ein Logfile schreiben.


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 2 (OnIdle).

Zeile 8: Was Timebased Movement ist könnt ihr ja mal nachlesen.