Tutorial SDL Einstieg

Aus DGL Wiki
Wechseln zu: Navigation, Suche

SDL - Simple Directmedia Layer

Vorwort

Ich weiß, dass ich mich damit unbeliebt machen werde,aber... Was haben GLUT und DirectX gemeinsam? Na, Na! Wer weiß es? Richtig, sie haben beide keine Zukunft mehr. ;) Na gut, war nicht wirklich der Reißer, aber zumindest war es der Gedanke, den ich hatte als ich mich das erste Mal mit dem Simple DirectMedia Layer befaßt habe (kurz SDL). Das Ganze hat nichts mit einem Breitbandanschluß oder einer neuen Designer-Droge zu tun, sondern es handelt sich dabei um eine plattformübergreifende API.

Die Idee die dahinter steckt ist genauso simpel wie genial! DirectX ist eine wirklich hervorragende API, allerdings hat das ganze ein Nachteil! Es kommt von Microsoft und ist nur für Windows verfügbar. Die OpenSource-Gemeinde müsste vor Scham im Boden versinken, wenn man dazu nicht passend ein Projekt ins Leben rufen würde, dass diesem Defizit ein Ende bereitet. Bei SDL handelt es sich um einen abstrakten Layer der auf jeder Plattform gleich ist und dann im Hintergrund die Befehle entsprechend dem darunter befindlichen OS umwandelt. Der Vorteil für den Programmierer ist klar: Wer seine Anwendung mit SDL schreibt, kann diese auch sehr schnell auf andere Systeme portieren. Eine reine SDL Anwendung in Delphi geschrieben, sollte sich also ohne Probleme auch unter Kylix kompilieren lassen und das ganz ohne den ganzen Source-Code umzubauen. Das Ganze ist zwar nicht so komplex wie DirectX von Microsoft, hat aber mindestens genauso viel Potenzial! Wer sich nun fragt, wozu das Ganze für ihn interessant sein soll, hat nicht mitgedacht! SDL für Fensterverwaltung und Benutzerinteraktion und dazu die geilste und portabelste Grafik-API, die es auf der Welt gibt : OpenGL! ;)

Ich hoffe sehr, dass ich mit diesem Artikel einige von euch für die Kombination SDL und OpenGL begeistern kann, denn gerade wir Delpher haben auch im Linux-Sektor eine Menge Potenzial, dass leider nicht genutzt wird! In diesem Sinne viel Erfolg! ;)

Initialisierung von SDL

SDL! Bitte kommen!

Wer sich bereits einmal mit der Programmierung der Windows-API beschäftigt hat, wird hier sicherlich nichts stark Befremdliches vorfinden. Sicherlich, alles heißt irgendwie anders, aber dafür ist das Ganze auch um einiges leichter zu handhaben als die Fenstererzeugung mit der WinAPI. Direkt im Hauptprogramm fangen wir erst einmal damit an SDL zu initialisieren:

 SDL_Init ( SDL_INIT_VIDEO );

Auf diese Weise teilen wir unserem Programm mit welche Teile von SDL initialisiert werden sollen. In unserem Beispiel die Bildschirmausgabe. Wir können als Parameter auch weitere Subsysteme übergeben z. B:

 SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );

Dazu aber später mehr! Wie immer ist es wichtig, dass man nicht nur Code an den Computer schickt, sondern auch darauf vorbereitet ist dass eventuell ein Fehler aufgetreten ist. Dieser soll dann natürlich auch vom Programm abgefangen werden!

// Initalisieren vom Simple DirectMedia Layer
  if ( SDL_Init( SDL_INIT_VIDEO ) < 0 ) then
  begin
    Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');
    Quit_App;
  end;

Sollte ein negativer Wert als Rückgabe erfolgen, so ist ein Fehler aufgetreten. Wir machen uns in diesem Fall die Fehlerbehandlung sehr einfach. Wir nutzen das im SDL integrierte Log-File und geben dort eine Fehlermeldung aus. Um die Orientierung zu erleichtern geben wir noch das Modul an in dem der Fehler auftrat. In diesem Fall eben bei der Initialisierung von SDL. Zu Quit_App kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.

Grafikkarten sind gar nicht so anders

Sicherlich ist es nicht jedem Leser hier bewusst, dass man für eine grafische Ausgabe auch eine Grafikkarte braucht. Deswegen werde ich hier noch einmal explizit darauf eingehen! :) Da SDL uns zur Verfügung steht können wir es auch verwenden um uns Informationen über die eingebaute Grafikkarte einzuholen:

// Information über Grafikkarte einholen
  videoInfo := SDL_GetVideoInfo;
  if ( videoInfo = nil ) then
  begin
    Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );
    Quit_App;
  end;

Bei VideoInfo handelt es sich um eine PSDL_VideoInfo-Struktur. Konnten die Informationen erfolgreich abgefragt werden, so sind alle interessanten Informationen in dieser Struktur enthalten, z.B. wie viel MB Speicher diese hat! Ist die Rückgabe undefiniert, greift natürlich unsere Fehlerbehandlung.

Die Suche nach dem wahren Pixelformat

Unser nächstes Ziel ist nun die Erzeugung der eigentlichen Zeichenfläche. Diese ist zu vergleichen mit dem Canvas eines Windows-Fensters. Natürlich müssen wir auch hier erst einige Einstellungen vornehmen!Immerhin wollen wir ja auch nicht ein paar 2D-Bilder á la DirectDraw rendern, sondern hardwarebeschleunigtes OpenGL!Also beginnen wir die Flags für die eigentliche Initalisierung zu sammeln:

// Flags für den SDL-Grafikmodus setzen
  videoFlags := SDL_OPENGL or                  // OpenGL-Unterstützung aktivieren
                SDL_HWPALETTE;                 // Palette in Hardware speichern

Vermutlich wird sich niemand finden, der die Sinnhaftigkeit dieser Flags wirklich anzweifeln wird!Als nächstes ermitteln wir ob die Möglichkeit besteht den Speicher und die eigentliche Hardwarebeschleunigung auch zu nutzen. Ich denke nicht, dass jemand heutzutage noch darauf verzichtet wenn er es nicht muss ;)

// Kann das Surface in den Speicher?
  if ( videoInfo.hw_available <> 0 ) then
    videoFlags := videoFlags or SDL_HWSURFACE
  else
    videoFlags := videoFlags or SDL_SWSURFACE;

  // Wird hardware blitting unterstützt?
  if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;

Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:

// Setzen der OpenGL-Attribute
  SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );
  SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );
  SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );
  SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );
  SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );

Die Farbwerte sollten so belassen werden. Der Tiefenbuffer wird auf 16 Bit festgelegt und ein BackBuffer soll auch erzeugt werden. Jeder der sich bereits einmal mit der Initialisierung beschäftigt hat, wird hier Gemeinsamkeiten finden und sich auch denken können wie man z.B. den Stencil-Buffer unter SDL setzt:

SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE,  8 );

Nun würden wir einen 8 Bit-Stencil-Buffer initialisieren. Gleiches gilt natürlich auch für den Akkumulations-Buffer!Damit haben wir alle Informationen gesammelt die wir brauchen um ein OpenGL-Fenster mit SDL zu erzeugen. Wenn man sich den Source Code ansieht, wird man merken, dass dieser um einiges schlanker ist als die Initalisierung der WinAPI und wir zudem auch noch plattformunabhängig sind!Einige kleinere Einstellungen nehmen wir allerdings noch vor. Nur kleine kosmetische Änderungen wie der Fenstertitel:

// Fenstertitel festlegen
  SDL_WM_SetCaption( WINDOWS_CAPTION , nil);

Als einfacher String wird der Titelname übergeben, der zweite Paramter kann dazu verwendet werden ein Icon für die Leiste zu definieren. Auch können wir an dieser Stelle entscheiden ob der Benutzer in der Lage sein soll das Fenster in seiner Größe zu verändern. Standardgemäß ist dieses Feature deaktiviert, so dass die Fenstergröße immer gleich bleibt. Wollen wir ein Skalieren jedoch zulassen, übergeben wir einfach ein weiteres Video-Flag:

videoflags := videoFlags or SDL_RESIZABLE;    // Enable window resizing

Nun haben wir alles beisammen und erzeugen unser Surface!

videoflags := // Initalisierung der Surface
  surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );
  if ( surface = nil ) then
  begin
    Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );
    Quit_App;
  end;

Ich denke nicht, dass es einer genaueren Erklärung bedarf was an dieser Stelle geschieht. Die Fenstergröße und Farbtiefe, sowie die Wunschliste unserer Video-Flags wird übergeben und wenn alles angeforderte auch möglich ist, erhalten wir von SDL ein PSDL_Surface zurück mit der wir dann weiterarbeiten können (und auch werden) ;)

OpenGL Initalisierung

Die meisten Leute gehen von einem ziemlich komplexen, aufwendigen und vor allem schweren Vorfang aus, wenn sie hören dass jemand OpenGL initalisiert. Dabei ist OpenGL gar nicht schwer zu initalisieren. Das eigentliche Problem ist vielmehr an ein Fenster vom Betriebsystem zu kommen dass auch OpenGL unterstützt. Dies haben wir allerdings bereits erfolgreich im letzten Kapitel geschafft, so dass wir nun nur noch dafür sorgen müssen, dass wir Zugriff auf die OpenGL-Runtimes erhalten. Dies ist jedoch ziemlich leicht:

 // Laden und Initalisieren von OpenGL
    InitOpenGL;
    ReadExtensions;
    ReadImplementationProperties;

Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spaß ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)

    glClearColor(0.0, 0.0, 0.0, 1.0);         // Bildschirm löschen (schwarz)
    glClearDepth(1.0);                                    // Depth Buffer Setup
    glEnable(GL_DEPTH_TEST);                        // Aktiviert Depth Testing
    glDepthFunc(GL_LEQUAL);                          // Bestimmt den Typ des Depth Testing
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten
                                                       // Interpolation

Das wir OpenGL initalisiert haben ist sicher ein guter Anfang, allerdings wollen wir natürlich auch etwas sehen. Dafür ist es notwendig, dass wir unseren Viewport setzen und die Projektions-Matrix auf die entsprechende Größe transformieren. Aus taktischen Gründen schreiben wir uns dafür eine Funtion, die wir auch später beim Event-Handlung wiederverwenden können:

function glResizeWindow( width : integer; height : integer ) : Boolean;
begin
// Verhindern von "Division by Zero"
  if ( height = 0 ) then height := 1;

  // Viewport und Projektions-Matrix aktualisieren
  glViewport( 0, 0, width, height );

  glMatrixMode( GL_PROJECTION );
    glLoadIdentity;
    gluPerspective( 45.0, width / height, 0.1, 100.0 );
  glMatrixMode( GL_MODELVIEW );

  // Rücksetzen der World-Matrix
  glLoadIdentity;

  // Vorgang erfolgreich
  result := true;
end;

All diese Vorgänge sollten für einen OpenGL-Programmierer nichts erschreckend neues sein. Damit der Viewport auch wirklich richtig gesetzt wird, rufen wir diese Funktion einfach einmal auf:

// Anpassen der Fenstergröße
  glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );

Tag, Post!

Die Idee

Würden wir nun unser Programm in diesem Zustand starten, würden wir für den Bruchteil einer Sekunde ein Fenster angezeigt bekommen (das immerhin OpenGL-kompatibel ist! *g) und danach sofort wieder verschwindet. Überlegt man sich einmal ganz genau, was passiert, wird einem der Grund dafür schnell klar:

begin
  // Initalisierung
  Init_SDL;
  Init_OpenGL;

  // Anpassen der Fenstergröße
  glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );
end.

Unser Hauptprogramm initalisiert SDL, danach OpenGL, passt das Ganze an der Fenstergröße an und beendet danach die Aufgabenliste. Für Windows bedeutet dies, dass das Programm seine Verarbeitung abgeschlossen hat und somit nicht mehr gebraucht wird und schon findet sich unsere SDL-Anwendung auf dem Müllhaufen. (Um Mißverständnisse zu vermeiden: Nicht der Papierkorb und nicht aufm Desktop *g*). Wir brauchen also eine Schleife die sich immer wieder im Programm wiederholt und dafür sorgt, dass diese nur unter einer ganz bestimmten Bedingung verlassen wird und somit das Programm auch beendet wird. Man spricht von dem Main-Loop oder auch Game-Loop:

begin
  // Initalisierung
  Init_SDL;
  Init_OpenGL;

  // Anpassen der Fenstergröße
  glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );

  // Eintritt in Main-Loop
  while ( Done <> -1 ) do
  begin
    glHandleEvents;
    glDrawScene;
  end;
end.

Done ist in unserem Fall ein einfacher Integer-Wert. Sobald dieser im eigentlichen Programm auf -1 gesetzt wird, wird die Schleife nicht ein weiteres Mal durchlaufen. Wir sehen auch, dass in der Schleife zwei Funktionen aufgerufen werden. Dies bietet sich an um die Übersicht zu wahren!Natürlich können wir auch noch weitere Aufgaben in der Schleife verarbeiten! GlDrawSzene ist die Funktion die die OpenGL-Befehle beinhaltet und sich um die grafische Ausgabe kümmert. Dieser Teil ist identisch mit der entsprechenden Funktion unter der WinAPI oder der VCL. Würden wir allerdings die Schleife immer nur mit dieser Funktion durchlaufen, so würde der Benutzer keine Interaktion mit dem Programm durchführen können, da immer nur die Schleife durchlaufen wird. Die Anwendung würde hängen. Es ist daher notwendig, dass diese auf Ereignisse des Betriebsystems oder des Anwenders reagiert.

Event-Handling

Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht, schauen wir uns die Funktion glHandleEvents etwas genauer an:

procedure glHandleEvents;
var event       : TSDL_Event;
begin;
    // Verarbeiten der Events
    while ( SDL_PollEvent( @event ) = 1 ) do
    begin
      case event.type_ of

        // Beenden der Applikation
        SDL_QUITEV :
        begin
          Done := -1;
        end;

        // Taste wurde gedrückt
        SDL_KEYDOWN :
        begin
          glHandleKeyPress( @event.key.keysym );
        end;

        // Fenster-Größe hat sich verändert
        SDL_VIDEORESIZE :
        begin
          glResizeWindow( event.resize.w, event.resize.h );
        end;
      end;//case
    end;//while
end;

Mit SDL_PollEvent fragen wir bei SDL an, ob Nachrichten für unsere Anwendung vorliegen. Ist dies der Fall, so durchlaufen wir alle diese Nachrichten nacheinander. Um die Art der Nachricht zu ermitteln übergeben wir eine Struktur vom Typ TSDL_EVENT und nutzen .type_ um zu ermitteln, um was für eine Nachricht es sich handelt. In unserem Fall reagieren wir auf 3 Ereignisse.

Sein oder nicht sein...

  // Beenden der Applikation
        SDL_QUITEV :
        begin
          Done := -1;
        end;

Liegt ein Ereignis vom Typ SDL_QUITEV vor, so hat die Anwendung die Meldung erhalten dass sie beendet werden soll. Der wahrscheinlichste Grund dafür wird sein, dass der Anwender auf das X im Fenstertitel geklickt hat. Es liegt nun an uns dafür zu sorgen, dass diesem Wunsch auch nachgekommen wird. Wie wir uns erinnern wird das Programm verlassen, sobald {{{1}}} gesetzt ist. Also machen wir dies auch. Nachdem alle Nachrichten abgearbeitet sind und der Main-Loop betreten wird, ist die Bedingung für einen Programmabbruch erfüllt.

Tastatur-Handling

   // Taste wurde gedrückt
        SDL_KEYDOWN :
        begin
          glHandleKeyPress( @event.key.keysym );
        end;

Dieses Event wird dann ausgelöst, wenn eine Taste gedrückt wurde. Wir übergeben in diesem Fall das Ereignis weiter an eine Funktion, die sich dann mit der Auswertung beschäftigt:

procedure glHandleKeyPress( keysym : PSDL_keysym );
begin;
  case keysym.sym of
    SDLK_ESCAPE : done := -1;
  end;
end;

Hierzu überprüfen wir, welche Taste gedrückt wurde. In diesem Fall handelt es sich um die Escape-Taste und sie soll beim Betätigen das Programm beenden. Würden wir abfragen wollen, ob die F1-Taste gedrückt wurde, so könnten wir dies mit SDLK_F1 machen. Weitesgehend entsprechen die SDLK-Konsten den VK-Konstanten der WINAPI. Wer über eine neuere Delphi-Version verfügt, kann ja auch mal STRG drücken und dann mit der linken Maustaste auf SDLK_ESCAPE klicken. Delphi wird dann an die Stelle springen, wo die Konstanten definiert sind. Dort werdet ihr sicherlich auch recht schnell die anderen Tasten finden, die ihr sucht.

Eine Frage der wahren Größe

SDL_VIDEORESIZE wird dann ausgelöst, wenn sich die Zeichenfläche in Ihrer Größe verändert hat. Zum Beispiel, weil der Anwender gerade das Fenster größer gezogen hat.

Durch diese Veränderung der Zeichenfläche werden unsere Projektions-Matrix und der Viewport ungültig. Wir müssen diese also neu anpassen. Wer sich gut erinnern kann, wird nun verstehen warum ich anfangs gesagt habe, dass wir uns die glResizeWindow-Funktion so schreiben, dass wir sie in einem Event wieder verwenden können.

glResizeWindow( event.resize.w, event.resize.h );

Wir übergeben einfach die vom Event übergebene neue Größe unseres Fensters und passen die Projektions-Matrix neu an. Schon kann der Anwender nach belieben die Größe des Render-Fensters verändern. So einfach ist das...


OpenGL? Überall gleich!

Wie ich bereits erwähnt habe, ist in der Funktion glDrawScene nichts wirklich Neues anzufinden, was nicht in einer API oder VCL-Lösung anzutreffen wäre. Schließlich ist OpenGL eben dafür geschaffen worden so portabel wie möglich zu sein:

procedure glDrawScene;
begin
  // Screen- und Tiefenbuffer bereinigen
  glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );

  glLoadIdentity;
  glTranslatef( -1.5, 0.0, -6.0 );

  // Zeichne Dreieck
  glBegin( GL_TRIANGLES );
    glVertex3f( 0.0, 1.0, 0.0 );
    glVertex3f( 1.0, -1.0, 0.0 );
    glVertex3f( -1.0, -1.0, 0.0 );
  glEnd;

  glTranslatef( 3.0, 0.0, 0.0 );

  // Zeichne ein Quadrat
  glBegin( GL_QUADS );
    glVertex3f( -1.0, 1.0, 0.0 );
    glVertex3f( 1.0, 1.0, 0.0 );
    glVertex3f( 1.0, -1.0, 0.0 );
    glVertex3f( -1.0, -1.0, 0.0 );
  glEnd;

  // Buffer-Wechseln ==> Anzeigen
  SDL_GL_SwapBuffers;
end;

Einzig und alleine die letzte Zeile ist anders. Hat unter der WINAPI an dieser Stelle noch eine WGL-Funktion ihren Dienst verrichtet, so macht dies hier eine SDL-Funktion. WGL steht unter Linux nicht zur Verfügung und man würde sich ansonsten auf Windows-Systeme festlegen. Technisch geschieht hier aber nichts anders als auch bei der WGL-Funktion nämlich das der hintere Framebuffer nach "vorne" geholt wird, sprich auf dem Bildschirm angezeigt wird. Ohne diesen Aufruf würde OpenGL zwar brav im Hintergrund rendern, aber niemals etwas anzeigen. Das kann auch nicht in unserem Interesse sein, oder? ;)


Time to say goodbye!

Ich weiß ja wirklich nicht wie es Euch geht, aber ich bin immer wenn ich etwas wegschmeiße ziemlich sensibel drauf. Und wenn ich mich hier im Zimmer umsehe, habe ich auch das Gefühl dass ich mich nie wirklich von einem meiner Computer getrennt habe O_o (habe sie halt immer noch alle ziemlich lieb *schnief). Aber es gibt eben Momente bei denen man sich von etwas was man gerne hat auch wieder trennt und wenn es sich nicht mehr vermeiden läßt, sollte man den Moment wenigstens in Ehre halten.

Nein, wer nun erwartet dass ich für euch große unsinkbare Schiffe versenke, wird enttäuscht sein ;) Wir schreiben einfach eine kleine Prozedur die unsere Anwendung umweltfreundlich entsorgt. Diese Prozedur können wir auch dann einsetzen wenn ein Fehler augetreten ist. Sicher könnte man den ganzen Kram auch einfach seinem Schicksal (Windows) überlassen, aber zu einem sauberen Code gehört es sich eben, dass freizugeben was man auch angefordert hat. Ich denke nicht dass der Code wirklich einer näheren Erklärung bedarf!

//----------------------------------------------------------------------------
// Terminieren der SDL-Anwendung
//----------------------------------------------------------------------------
procedure Quit_App;
begin;
  // Freigeben der Ressourcen
  SDL_QUIT;
  Halt(0);
end;

Nachwort

Das war also bereits unser kleiner Crash-Kurs in die Welt des SDL. Ich hoffe sehrmdass dieses Tutorial verständlich genug war um SDL auch künftig einzusetzen. Es ist meiner Meinung nach wichtig ein Gegengewicht zu Microsoft in der Welt zu haben und der Programmierer soll ja ökonomisch denken. Was spricht also dagegen seine Anwendung so zu gestalten, dass man sie ohne Probleme auch nach Linux übersetzen könnte?Gerade in Verbindung mit OpenGL entfaltet sich ein richtges Dream-Team. Der eine für die Fensterverwaltung, der andere für die grafische Ausgabe. Jeder der bereits (oder immer noch?!) mit GLUT arbeitet, sollte schleunigst davon weg kommen und auf SDL umsteigen. Weil es einfach besser ist ;)

Sicherlich werden nun nicht unbedingt SDL-Anwendungen aus dem Boden schießen, aber der eine oder andere hat ja vielleicht schon ein wenig Blut geleckt und möchte etwas weiter damit herum spielen? Habe ich bereits erwähnt, dass SDL auch etwas für Joysticks, Mäuse und Sound zur Verfügung stellt? Auch ein abstraktes System für mehre Threads ist mit von der Partie, sowie einige Funktionen zum Benutzen von Audio-CDs. Wer Lust auf mehr SDL hat, sollte unbedingt einmal einen Blick in die SDL-Hilfe werfen. Das Projekt ist jung, aber motiviert und hat eine Menge Potenzial!Die Delphi-Portierung wird von den JEDIs selbst unter der Projekt-Führung von Dominique Louis durchgeführt. Wer bereits seit DelphiX-Zeiten in der Szene unterwegs ist wird wissen, was es bedeutet! Gute Arbeit und Sicherheit für die Zukunft ;) In diesem Sinne ... viel Spaß ;)

Euer

Phobeus

Vorhergehendes Tutorial:
-
Nächstes Tutorial:
Tutorial_SDL_RWops

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.