https://wiki.delphigl.com/api.php?action=feedcontributions&user=Akira&feedformat=atomDGL Wiki - Benutzerbeiträge [de]2024-03-29T08:36:37ZBenutzerbeiträgeMediaWiki 1.27.4https://wiki.delphigl.com/index.php?title=Tutorial_Komponentenentwicklung&diff=14617Tutorial Komponentenentwicklung2005-11-29T20:38:40Z<p>Akira: /* Komponenten Tutorial */</p>
<hr />
<div>=Komponenten Tutorial=<br />
<br />
==Einleitung==<br />
Erst mal willkommen bei meinem Komponeten-Tutorial. Ich möchte in diesem Tutorial nicht absolutes Basiswissen über die Komponenten-Programmierung unter Delphi vermitteln, sondern denjenigen, die schon entsprechende Einsteigertutorials gelesen haben (wie etwa das auf Delphi-Treff.de) noch etwas tiefer in die Materie einführen.<br />
<br />
== Eigene Visuelle Komponeten ==<br />
Wenn man seine Komponente komplett selber zeichnen möchte, so empfiehlt es sich diese von '''TGraphicControl''' oder '''TCustomControl''' abzuleiten. TCustomControl ist im Gegensatz zu TGrapicControl ein Nachfahre von TWinControl, und erbt von ihm ein (Fenster-) Handle. Dieses Handle ist eine Nummer, welche Komponenten von Windows erhalten um sie eindeutig zu bezeichnen. Nur wenn die Komponente ein Handle hat, kann Windows mit ihr etwas anfangen.<br />
<br />
Möchte man nur statisch etwas anzeigen, wie etwa ein Label, so kann man auch nur TGraphicControl als Vorfahren wählen. Solche Komponenten ohne Handle werden von Windows nicht unterstützt, und sind nur eine Technik von Delphi, um dem Entwickler einfachere Gestaltungsmöglichkeiten zu bieten. Es ist ja schließlich viel einfacher, eine Komponente auf der Form zu platzieren, als diese mit Zeichenbefehlen selbst zu zeichnen.<br />
<br />
Um selbst festzulegen, wie die Komponente gezeichnet wird, kann man bei Nachfahren von TGraphicControl oder TCustomControl ganz einfach die '''procedure Paint''' überschreiben.<br />
<pascal><br />
TMeineKomponente = class(TGraphicControl)<br />
protected<br />
procedure Paint; override; <br />
</pascal><br />
<br />
Gezeichnet wird dann über die Canvas-Eigenschaft der Komponente. Dabei sollte man nicht vergessen mit inherited die Paint Procedure des Vorfahren aufzurufen.<br />
<br />
<pascal><br />
procedure TMeineKomponente.Paint;<br />
begin<br />
inherited;//Paint Procedure des Vorfarhen aufrufen.<br />
Canvas.//irgendwas<br />
</pascal><br />
<br />
Eine Abfrage der Eigenschaft ComponentState ermöglicht es uns, die Komponente zur Entwicklungszeit anders zu zeichnen.<br />
<br />
<pascal><br />
procedure TMeineKomponente.Paint;<br />
begin<br />
inherited;//Zeichen Procedure des Vorfahren aufrufen.<br />
if csDesigning in ComponentState then //Wenn sich die Komponente in Entwicklung befindet.<br />
begin<br />
{Zeichung eines gestrichelten Rahmens}<br />
Canvas.Brush.style := bsClear;//Durchsichtiges Rechteck<br />
Canvas.Pen.style := psDashDot;//Gestrichelte Linen<br />
Canvas.Rectangle(0,0,width,height);//Rechteck zeichnen<br />
<br />
{Namen der Komponente in die Mitte schreiben}<br />
canvas.TextOut((width - Canvas.TextWidth(Name)) div 2,(height - Canvas.TextHeight(Name)) div 2,name);<br />
<br />
{Keine weitern Zeichnungen mehr ausführen}<br />
exit;//Verlässt die Procedure<br />
end;<br />
//Normale Zeichen-Anweisungen<br />
end;<br />
</pascal><br />
<br />
== Eigenschafts-Editoren ==<br />
<br />
Als erstes sollten wir der Frage nachgehen, was ein Eigenschaftseditor überhaupt ist : Wie schon fast zu erraten, handelt es sich hierbei um eine Möglichkeit, eine als veröffentlicht (published) deklarierte Eigenschaft im Objektinspektor zu bearbeiten. In Delphi sind schon diverse Eigenschaftseditoren vorhanden um die häufigsten Eigenschaftstypen abzudecken. Wenn ihr einer Komponente eine als published deklarierte Eigenschaft vom Typ Integer verpasst, so wird standardgemäß der Editor "TIntegerProperty" genutzt um die Eigenschaft im Objektinspektor darzustellen. Dieser Editor ist ein Eingabefeld für Zahlen.<br />
<br />
Eigenschaftseditoren können aber nicht nur solche Eingabefelder sein, sondern können auch eine ganze Reihe anderer Bearbeitungsmöglichkeiten bieten. Ein richtig multifunktionaler Eigenschaftseditor ist zum Beispiel "TColorProperty". So kann man entweder eine hexadezimale Zahl oder einen Farbnamen eingeben, die entsprechende Farbe über eine Liste auswählen oder gar per Doppelklick einen Farbdialog öffenen.<br />
<br />
Ein solcher Eigenschaftseditor ist programmiertechnisch gesehen nichts anderes als ein Nachfahre der Klasse "TPropertyEditor", welcher in einer Register-Prozedur in Delphi eingebunden wurde. Den Code von einem solchen Eigenschaftseditor sollte man in eine Unit packen, welche nur von Delphi genutzen Code enthält. Für das beiliegende Beispiel TFilePropertyEditor nehmen wir direkt TPropertyEditor als Vorfahren. <br />
<br />
<pascal><br />
TFileNameProperty = class (TPropertyEditor)<br />
{...}<br />
end;<br />
</pascal><br />
<br />
In welcher Form der Eigenschaftseditor angezeigt wird, bestimmt der Rückgabewert der Funktion GetAttributes. Um diese zu ändern überschreiben wir die Funktion einfach.<br />
<br />
<pascal><br />
TFileNameProperty = class (TPropertyEditor)<br />
public<br />
function GetAttributes: TPropertyAttributes; override;<br />
{...}<br />
end;<br />
</pascal><br />
<br />
Dem Eigenschaftseditor werde ich nun zusätzlich zu dem Standardeingabefeld noch per '''paDialog''' die Möglichkeit geben, auf Doppelklicks oder einen (nun evtl. erschienen) Buttonklick zu reagieren. '''paMultiSelect''' bewirkt schlussendlich noch, dass die Eigenschaft auch noch sichtbar ist, wenn mehre Komponenten ausgewählt sind. Weitere mögliche Parameter sind der Delphi Hilfe zu entnehmen.<br />
<br />
<pascal>function TFileNameProperty.GetAttributes: TPropertyAttributes;<br />
begin<br />
Result := [paMultiSelect, paDialog];<br />
end;<br />
</pascal><br />
<br />
Um auf das neu geschaffene Editierereignis zu reagieren, überschreiben wir die '''Procedure Edit;'''<br />
<br />
<pascal>procedure TFileNameProperty.Edit;<br />
var<br />
OpenDialog:TOpenDialog;<br />
begin<br />
OpenDialog := TOpenDialog.create(Application);//Den OpenDialog erstellen<br />
try<br />
//Den akuellen Wert übergeben<br />
OpenDialog.FileName := GetStrValue;<br />
//Anzeigen und bei Erfolg Wert übernehmen<br />
if OpenDialog.Execute then SetStrValue(OpenDialog.FileName);<br />
finally<br />
OpenDialog.Free; //Den OpenDialog freigeben.<br />
end;<br />
end;<br />
</pascal><br />
<br />
Mit '''GetStrValue''' und '''SetStrValue''' kann der Wert einer Stringeigenschaften gelesen und geschrieben werden. Für andere Eigenschaftstypen stehen andere Funktionen zur Verfügung. An dieser Stelle können natürlich auch eigene Formulare eingeblendet werden.<br />
<br />
* Fuer Interessierte: [[Tutorial_Komponentenentwicklung/Eigene Dialoge|Eigene Dialoge]]<br />
<br />
Durch überschreiben von '''GetValue''' kann man regeln, was für ein Text auf der rechten Seite im Objektinspektor angezeigt wird. Ein Überscheiben von '''SetValue''' hingegen legt fest, wie auf eingegebene Werte reagiert werden soll. In unserem Fall soll der Eigenschaftsinhalt direkt angezeigt und übernommen werden.<br />
<br />
<pascal><br />
function TFileNameProperty.GetValue: string;<br />
begin<br />
result := GetStrValue ;<br />
end;<br />
<br />
procedure TFileNameProperty.SetValue(const Value: string);<br />
begin<br />
SetStrValue(Value);<br />
end; <br />
</pascal><br />
<br />
Wie ihr euch vielleicht noch erinnert, haben wir am Anfang in GetAttributes festgelegt, dass die Eigenschaft auch bei mehrfacher Auswahl sichtbar bleibt. Nun liegt es an, uns Delphi zu erklären, wann zwei Eigenschaften wirklich identisch sind. Denn schließlich sagt der im Objektinspektor angezeigte Wert nichts über den tatsächlichen Inhalt der Eigenschaft. Durch ein Überschreiben der Funktion '''AllEqual''' können wir nun unsere eigene Prüfung definieren.<br />
<br />
<pascal><br />
function TFileNameProperty.AllEqual: Boolean;<br />
var<br />
Nr1:String;<br />
I:Integer;<br />
begin<br />
AllEqual := true;<br />
Nr1 := GetstrValue ;<br />
// Ersten Eigenschafts-Wert mit restlichen Werten vergleichen.<br />
// Die Anzahl der zu überprüfenden Eigenschaften liefert PropCount.<br />
for I:= 1 to PropCount-1 do<br />
if Nr1 <> GetstrValueat(I) then<br />
begin<br />
AllEqual := false;<br />
break;<br />
end;<br />
end;<br />
</pascal><br />
<br />
Nach der Erstellung unserer Eigenschaftseditor-Klasse müssen wir nur noch festlegen, für welche Eigenschaften und für welche Komponeten der Editor genutzt werden soll. Dies geschieht durch eine Registrierung des Editors über einen Aufruf von RegisterPropertyEditor inherhalb der Register-Prozedur.<br />
<br />
<pascal><br />
procedure Register;<br />
begin<br />
RegisterPropertyEditor(TypeInfo(TFileName),nil,'FileName',TFileNameProperty);<br />
end;<br />
</pascal><br />
<br />
Der erste übergebene Parameter ist der Typ der Eigenschaft für den der Editor gelten soll. Mit dem zweiten Parameter wird festgelegt, ob der Editor nur für die Eigenschaft einer bestimmten Komponente oder für alle (durch Angabe von nil) gelten soll. Der dritte Parameter legt fest, ob die Eigenschaft einen bestimmten Namen haben muss (Ein leerer String bedeutet keine Festlegung). Der Letzte Parameter beinhaltet nun die Editorklasse selbst.<br />
<br />
== Komponenten-Editoren ==<br />
Bei den Komponenteneditoren verhält sich vieles wie bei den Eigenschaftseditoren. Sie dienen nur dazu, dem Progammierer mehr Möglichkeiten zu geben um Komponenten zu bearbeiten. Oder auch die Arbeit mit Komponenten zu vereinfachen.<br />
<br />
Mit Hilfe eines Komponenteneditors kann festgelegt werden:<br />
* Was bei einem Doppelklick auf die Komponente passiert.<br />
* Welche Zusatzoptionen der Benutzer im PopUp-Menü der Komponente hat.<br />
* Wie die Komponente in die Zwischenablage kopiert wird.<br />
<br />
=== Das Doppelklickverhalten festlegen ===<br />
<br />
Möchte man, dass bei einem Doppelklick auf die Komponente der Code für ein bestimmtes Ereignis generiert wird, so leitet man seinen Komponenteneditor nicht von TComponentEditor ,sondern von seinem Nachfahren '''TDefaultEditor''' ab.<br />
Auf diese Weise kann man die Prozedur '''EditProperty''' überschreiben.<br />
<br />
<pascal><br />
{Die Ereignis-Auswahl bei Doppelklick auf TGLControl}<br />
TGLControlEditor = Class (TDefaultEditor)<br />
public<br />
procedure EditProperty(PropertyEditor: TPropertyEditor; var Continue, FreeEditor: Boolean); override;<br />
end; <br />
</pascal><br />
<br />
Diese Prozedur sieht eigentlich immer gleich aus:<br />
<br />
<pascal><br />
procedure TGLControlEditor.EditProperty(PropertyEditor: TPropertyEditor; var Continue, FreeEditor: Boolean);<br />
begin<br />
// Falls die Eigenschaft ein Ereignis ist<br />
if (PropertyEditor.ClassName = 'TMethodProperty') and<br />
// und sie OnDraw heißt<br />
(PropertyEditor.GetName = 'OnDraw') then<br />
// dann wird die gleiche Procedure beim Vorfahren aufgerufen<br />
// um für dieses Ereignis entsprechenden Code zu generieren.<br />
inherited EditProperty(PropertyEditor, Continue, FreeEditor);<br />
end;<br />
</pascal><br />
<br />
Möchte man keinen Code generieren, sondern etwa einen Dialog öffen, so kann man TComponentEditor als Vorfahren wählen und die Prozedur Edit überschreiben.<br />
<br />
<pascal><br />
TGLControlEditor = Class (TComponentEditor)<br />
public<br />
procedure Edit; override;<br />
end;<br />
</pascal><br />
<br />
Was ihr nun in dieser Prozedur macht ist euch überlassen. Wie ihr einen Dialog anzeigt, habe ich ja schon bei den Eigenschaftseditoren erklärt.<br />
<br />
=== Das Popupmenu erweitern ===<br />
<br />
Als Erstes müssen wir festlegen, wie viele eigene Einträge angezeigt werden sollen. Dazu überscheiben wir die Funktion '''GetVerbCount''' von TComponentEditor.<br />
<br />
<pascal><br />
TFarbAuswahlComponentEditor = class(TComponentEditor)<br />
function GetVerbCount: Integer; override;<br />
{...}<br />
end;<br />
<br />
function TFarbAuswahlComponentEditor.GetVerbCount: Integer;<br />
begin<br />
result := 3;//Anzahl der Menupunkte; In unserm Fall drei (0..2)<br />
end; <br />
</pascal><br />
<br />
Durch überschreiben der '''GetVerb''' Funktion kann nun festgelegt werden, wie die neuen Menüpunkte heißen:<br />
<br />
<pascal><br />
function TFarbAuswahlComponentEditor.GetVerb(Index: Integer): string;<br />
begin<br />
Case Index of<br />
0: Result := 'Farbenauswahl 1' ;<br />
1: Result := 'Farbenauswahl 2' ;<br />
2: Result := 'Farbenauswahl 3';<br />
else<br />
Result := '?';<br />
end;<br />
end;<br />
</pascal><br />
<br />
Damit die Menüpunkte auch Sinn machen, sollte natürlich auch noch festgelegt werden was passiert, wenn man auf sie klickt. Und wieder einmal gilt es, eine Procedure zu überschreiben :<br />
<br />
<pascal><br />
procedure TFarbAuswahlComponentEditor.ExecuteVerb(Index: Integer);<br />
begin<br />
if not (Component is TViereck) then<br />
begin<br />
ShowMessage('Dieser Komponenten Editor ist nicht TViereck zugeordnet');<br />
end<br />
else<br />
Case Index of<br />
0://Klick auf den Ersten Menu Punkt<br />
begin<br />
(Component as TViereck).Farben.Rahmen := clBlack;<br />
(Component as TViereck).Farben.Innen := clWhite;<br />
end;<br />
1://Klick auf den zweiten Menu Punkt<br />
begin<br />
(Component as TViereck).Farben.Rahmen := clYellow;<br />
(Component as TViereck).Farben.Innen := clNavy;<br />
end;<br />
2://Klick auf den dritten Menu Punkt<br />
begin<br />
(Component as TViereck).Farben.Rahmen := clBtnFace ;<br />
(Component as TViereck).Farben.Innen := clBtnFace ;<br />
end;<br />
else<br />
ShowMessage('Diesen Menupunkt gibt es nicht');<br />
end;<br />
end;<br />
</pascal><br />
<br />
Auf diese Weise ist es möglich, einfache Popup-Menüs zu basteln.<br />
<br />
* Für Interessierte: [[Tutorial_Komponentenentwicklung/Erweitertes Popup Menu|Erweitertes Popup Menu]]<br />
<br />
So einfach ist es, ein eigenes Popup-Menü zu erstellen, wobei ich aber hoffe, das ihr sinnvollere Anwendungen dafür finden werdet als ich in diesem Beispiel.<br />
<br />
== Die Komponente in der Zwischenablage ==<br />
<br />
Möchte man noch zusätlich eigene Formate beim Kopieren in die Zwischenablage angeben, so kann man die Prozedur Copy überschreiben. Ein Beispiel zu dieser Procedure findet sich in der Delphi-Hilfe.<br />
<br />
== Den Editor in Delphi einbinden ==<br />
<br />
Genauso wie Eigenschaftseditoren, werden Komponenteneditoren innerhalb der Prozedur '''Register''' registiert. Die Prozedur, die darin aufgerufen wird, ist logischerweise '''RegisterComponentEditor'''.<br />
<br />
<pascal><br />
procedure Register;<br />
begin<br />
RegisterComponentEditor(TViereck, TFarbauswahlComponentEditor);<br />
end;<br />
</pascal><br />
<br />
Der erste Parameter ist die Komponente für die der Komponenten-Editor gelten soll und der zweite der Komponenteneditor selbst.<br />
<br />
== Eigene Eignschafts-Klassen ==<br />
<br />
Ein ganzes, eigenes Objekt im Objektinspektor anzeigen zu lassen ist eigentlich kein Problem : Man muss dazu eigentlich nur wissen, dass man seine Eigenschaftsklasse von '''TPersistent''' ableiten muss.<br />
<br />
<pascal><br />
TGraphColors=class(TPersistent)<br />
{...}<br />
end; <br />
</pascal><br />
<br />
Im privaten Bereich der Komponente speichert man dann die Adresse des Objektes. Im veröffentlichten Bereich deklariert man die Eigenschaft unter der Benutzung einer Prozedur zum Setzen der Eigenschaft.<br />
<br />
<pascal><br />
TGraph = class(TGraphicControl)<br />
private<br />
FColors:TGraphColors;<br />
{...}<br />
procedure SetColors(NewColors:TGraphColors);<br />
public<br />
{...}<br />
property Colors : TGraphColors read FColors write SetColors; <br />
</pascal><br />
<br />
Das übernehmen der Werte läuft allerdings etwas anders ab als man vielleicht auf den ersten Blick vermuten würde. Statt den Zeiger auf das neue Objekt zu richten, nimmt das alte Objekt mittels der '''Assign''' Prozedur (von TPersistent) die Daten vom Neuen an.<br />
<br />
<pascal><br />
procedure TGraph.SetColors(NewColors:TGraphColors);<br />
begin<br />
FColors.Assign(NewColors);<br />
end;<br />
</pascal><br />
<br />
Um auf Eigenschafts-Änderungen des Eigenschaftobjektes reagieren zu können, kann man sich eine Art Ereignis einrichten. Also einen Zeiger auf eine Methode :<br />
<br />
<pascal><br />
TGraphColors=class(TPersistent)<br />
private<br />
FBackGround:TColor;<br />
FPen:TColor;<br />
procedure SetBackGround(NewColor:TColor);<br />
procedure SetPen(NewColor:TColor);<br />
OnChange:procedure of object; //< Variable zur Speicherung eines Methoden-Zeigers<br />
published<br />
property BackGround:TColor read FBackGround write SetBackGround;<br />
property Pen:TColor read FPen write SetPen;<br />
end;<br />
<br />
TGraph = class(TGraphicControl)<br />
private<br />
FGraphFunction:TGraphFunction;<br />
procedure SetColors(NewColors:TGraphColors);<br />
published<br />
property Colors : TGraphColors read FColors write SetColors;<br />
end;<br />
<br />
procedure TGraphColors.SetBackGround(NewColor:TColor);<br />
begin<br />
FBackGround:= NewColor;<br />
if Assigned(@OnChange) then OnChange;//<Falls eine Methode zugewiesen wurde aufrufen<br />
end;<br />
<br />
procedure TGraphColors.SetPen(NewColor:TColor);<br />
begin<br />
FPen := NewColor;<br />
if Assigned(@OnChange) then OnChange;//<Falls eine Methode zugewiesen wurde aufrufen<br />
end;<br />
<br />
Constructor TGraph.Create(AOwner:TComponent);<br />
begin<br />
inherited;<br />
FColors := TGraphColors.Create;<br />
FColors.OnChange := Repaint;//Methode zur Verarbeitung von Veränderungen in FColors festlegen<br />
{...}<br />
end;<br />
</pascal><br />
<br />
== Interaktive Komponenten ==<br />
<br />
Komponenten, die nur etwas anzeigen, mögen zwar auch ihre Daseinsberechtigung haben, allerdings möchte man dem Benutzer auch oft die Möglichkeit bieten, die Komponente in irgendeiner Weise zu beeinflussen. Glücklicherweise bieten die Vorfahren unserer Komponente gleich reihenweise Prozeduren und Funktionen zum überschreiben, um auf verschiedenste Ereignisse zu reagieren.<br />
Wobei diese meist vor den Funktionen aufgerufen werden, auf welche die "Ereignis"-Eigenschaften zeigen, oder diese sogar erst aufrufen.<br />
<br />
=== Allgemeine Ereignisse ===<br />
<br />
Jede visuelle Komponente hat TControl als Vorfahren und somit ein paar praktische Proceduren zum überschreiben.<br />
Wie etwa: Bei einer Größen-Veränderung wäre es für eine OpenGL-Komponente sicherlich nicht schlecht die Matrizen anzupassen.<br />
<br />
<pascal><br />
procedure TGLControl.Resize;<br />
begin<br />
if FOpenGLAktiviert then<br />
begin<br />
glViewport(0, 0, ClientWidth, ClientHeight); // Setzt den neuen Viewport<br />
glMatrixMode(GL_PROJECTION); // Projektions Matrix auswählen<br />
glLoadIdentity; //Idenditätsmatrix laden<br />
gluPerspective(45.0, ClientWidth/ClientHeight, 1, 100.0); // Perspektive den neuen Maßen anpassen.<br />
glMatrixMode(GL_MODELVIEW); // Zurück zur Modelview Matrix<br />
glLoadIdentity(); //Idenditätsmatrix laden<br />
end;<br />
inherited; //Die Vorfahren das Ereignis behandeln lassen.<br />
end;<br />
</pascal><br />
<br />
=== Der Eingabefokus ===<br />
<br />
Wie schon oben erwähnt können nur Komponenten mit Fenster-Handle den Eingabefokus erhalten. Ob eine Funktion den Eingabefokus hat, kann mit der Funktion Focused ermittelt werden. Um besser klar zu machen welche Komponente gerade ausgwählt ist, sollte man die Komponente, wenn sie den Eingabefokus hat, anders zeichnen.<br />
<br />
<pascal><br />
procedure TNachfahrvonTWinControl.Paint;<br />
begin<br />
inherited;<br />
{Komponente zeichen}<br />
if Focused then //Komponente hat den Eingabefokus<br />
begin<br />
{Hervorhebung zeichnen}<br />
end;<br />
end;<br />
</pascal><br />
<br />
Um auf erhalten und entfernen des Eingabefokuses reagieren zu können, bietet TWinControl die Prozeduren DoExit und DoEnter zum überschreiben an.<br />
<br />
<pascal><br />
procedure TDrehElement.DoExit;<br />
begin<br />
inherited;<br />
Refresh;<br />
end;<br />
procedure TDrehElement.DoEnter;<br />
begin<br />
inherited;<br />
Refresh;<br />
end; <br />
</pascal><br />
<br />
Möchte man, dass die Komponente den Eingabefokus erhält, so ruft man einfach SetFocus auf.<br />
<pascal><br />
procedure TNachfahrvonTWinControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);<br />
begin<br />
inherited;<br />
if not Focused then SetFocus;<br />
end;<br />
</pascal><br />
<br />
=== Windows-Botschaften ===<br />
<br />
Als Erstes muss ich wahrscheinlich klären, was Windows-Botschaften überhaupt sind : Wie schon oben erwähnt, erhält jedes Steuerelement unter Windows ein Handle, welches es genau kennzeichnet. Jedem dieser Handle ist eine Art Postfach zugeordnet, in dem andere Anwendungen oder auch Windows Nachrichten/Befehle hinterlassen können.<br />
Eine solche Nachricht kann ein Beenden- oder Zeichenbefehl sein, oder auch eine Benachrichtung über eine Manipulation der Scroll-Leiste.<br />
<br />
Das Besondere an diesen '''W'''indows-'''M'''essages ist, dass sie, wenn sie mit SendMessage gesendet wurden, dem Absender ein Ergebnis mitteilen. Auf diese Weise kann der Absender überprüfen, ob die Nachricht überhaupt behandelt wurde.<br />
Ein praktisches Beispiel dafür wäre vielleicht WM_GETDLGCODE, welche nachfrägt, ob Benachrichtungen über das Drücken besonderer Tasten erwünscht sind.<br />
<br />
<pascal><br />
MeineKomponente=class(TCustomControl)<br />
private<br />
procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;<br />
{...}<br />
<br />
procedure TCodeEditor.WMGetDlgCode(var Message: TWMGetDlgCode);<br />
begin<br />
// Benachrichten wenn Pfeiltasten oder ähnliches gedrückt wurde.<br />
Message.Result := DLGC_WANTARROWS or DLGC_WANTTAB;<br />
end; <br />
</pascal><br />
<br />
Natürlich gibt es auch Nachrichten, bei denen eine Rückantwort eher weniger Sinn macht.<br />
<br />
<pascal><br />
TLaufSchrift = class(TCustomControl)<br />
private<br />
procedure WMTimer(var Message:TWMTimer);message WM_TIMER;<br />
{...}<br />
end;<br />
<br />
procedure TLaufSchrift.WMTimer(var Message:TWMTimer);<br />
begin<br />
if Message.TimerID = My_TimerID then<br />
begin<br />
FVerschoben := FVerschoben+FSpeed;<br />
Refresh;<br />
end<br />
else //Unbekannte TimerID<br />
inherited;//Die Vorfahren werden nach einer Methode mit der gleichen BotschaftsID durchsucht.<br />
end;<br />
</pascal><br />
<br />
Ach ja, diese Möglichkeit auf solche Nachrichten zu reagieren nennt man Botschaftsbehandlungsroutinen.<br />
Sie sind der einfachste Weg solche Botschaften zu behandeln. Alternativ dazu kann man auch die Procedure WndProc überschreiben, oder der Eigenschaft WindowProc eine andere Behandlungsprozedur zuweisen.<br />
<br />
=== Veränderungen anzeigen ===<br />
<br />
Möchte man, dass sich das Aussehen der Komponente ändert, so muss man sie zumindest teilweise neu zeichnen. Praktischerweise bietet TControl gleich schon den entsprechenden Befehl, um den Bereich, den die Komponente bedeckt, neu zu zeichnen. Dieser Befehl nennt sich Repaint und macht im wesentlichen nichts anderes, als Invalidate und Update aufzurufen. Wem das Wort Repaint nicht gefällt, der kann alternativ auch die procedure Refresh aufrufen, welche wiederum Repaint aufruft (Eigentlich völlig sinnlos).<br />
<br />
<pascal><br />
procedure TGraph.SetTransparent(NewValue:Boolean);<br />
begin<br />
FTransparent := NewValue;<br />
Repaint;//Komponente sofort neu zeichnen<br />
end;<br />
</pascal><br />
<br />
Die Zeichengeschwindikeit kann man einmal erhöhen, in dem man in ControlStyle festlegt, dass die Komponente ihren rechteckigen Bereich vollkommen ausfüllt. Auf diese Weise braucht das, was die Komponente verdeckt, nicht umsonst gezeichnet werden.<br />
<br />
<pascal><br />
constructor TDrehElement.Create(AOwner: TComponent);<br />
begin<br />
inherited;<br />
//Das Steuerelement füllt sein Client-Rechteck vollständig aus.<br />
ControlStyle := ControlStyle + [csOpaque];<br />
{...}<br />
end;<br />
</pascal><br />
<br />
Bei dem kompletten Neuzeichnen größerer Flächen kann es passieren, dass man den Zeichenprozess in verschieden Zwischenstufen sieht, was auch gerne als Flackern bezeichnet wird. Eine Technik namens Doublebuffering, welche auch in den meisten OpenGL-Anwendungen genutzt wird, schafft hier Abhilfe.<br />
<br />
Für Komponenten reicht es, die Eigenschaft '''Doublebuffered''' auf true zu setzen.<br />
<br />
<pascal><br />
constructor TDrehElement.Create(AOwner: TComponent);<br />
begin<br />
inherited;<br />
DoubleBuffered := True;//Doublebuffering nutzen.<br />
{...}<br />
end;<br />
</pascal><br />
<br />
== Schlussworte ==<br />
<br />
Mit diesen wenigen Grundlagen, die ich euch hier heute vorgestellt habe, ist es schon möglich gute und neue Komponten zu programmieren. Wie die meisten anderen Tutorialschreiber sicherlich auch, würde ich mich freuen sehen zu können, dass dieses Tutorial euch bei der Programmierung geholfen hat.<br />
Die dazu gehörenden Quelltextbeispiele stehen übrigens unter der Mozilla Public Lizenz, sodass eine entsprechende Nennung im About-Bereich euer Anwendungen reicht um ihn nutzen zu dürfen. Also dann viel Spaß beim Programmieren...<br />
<br />
Euer [[Benutzer:Flo|Flo]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Normale&diff=14610Normale2005-11-29T18:17:14Z<p>Akira: /* Normalisieren */</p>
<hr />
<div>= Normalen =<br />
<br />
<br><br />
== Was ist das? ==<br />
Eine '''Normale''' ist ein Vektor, der Senkrecht auf eine Fläche steht.<br />
<br />
<br><br />
== Wozu brauch ich Normalen? ==<br />
'''Normalen''' haben in OpenGL zwei zentrale Aufgaben.<br />
<br />
# Sie zeigen welche Flächenseite die Vorderseite ist (anhand des Vorzeichens).<br />
# Sie bestimmen wie Licht von der Fläche reflektiert wird.<br />
<br />
<br><br />
== Wie berechne ich Normalen? ==<br />
'''Normalen''' sind Flächennormalen. Das heißt, sie stehen senkrecht auf einer Fläche.<br><br />
<br />
'''Welche Fläche?'''<br />
<br />
Eine beliebige...<br />
<br />
Ersteinmal eine kurze Zusammenfassung.<br><br />
* Flächen bestehen aus Polygonen.<br />
* Polygone bestehen aus n Dreiecken.<br />
* Jedes Dreieck ist definiert durch seine 3 Eckpunkte.<br />
* 3 Punkte liegen immer in einer Ebene<br />
<br />
Wenn man nun die Normale eines Dreiecks berechnen will geht man folgenermaßen vor:<br />
# Einen Eckpunkt als Bezugspunkt (P0) wählen.<br />
# Die beiden Vektoren v1 = P1-P0 und v2 = P2-P0 bestimmen.<br />
# Diese beiden Kreuzmultiplizieren <v1,v2>. (steht in jedem Tafelwerk unter Kreuzprodukt)<br />
# Das Ergebnis hat schonmal die richtige Richtung. (Für OpenGl reicht das.) <br />
<br />
=== Normalisieren ===<br />
Eine Normale heißt aber nur deshalb Normale, weil sie normiert ist. Das heißt sie ist 1 Längeneinheit lang. Wenn man einen Vektor normieren will dividiert man einfach den Vektor durch seine Länge.<br />
Allgemein:<br />
1/sqrt(x^2+y^2+z^2) * (x,y,z)<br />
<br />
Beispiel eines normierten Vektors:<br />
<br />
(1) normieren 1 (1)<br />
(1) ----------> --- * (1)<br />
(0) sqrt(2) (0)<br />
<br />
Das Ergebnis ist die Normale des Dreiecks und damit auch der gesamten Ebene in der das Dreieck liegt.<br />
<br />
Mit [[glNormal]] hat man in OpenGL explizit die Möglichkeit einer Fläche eine '''Normale''' zu zuweisen. Diese muss nicht senkrecht (wie eine echte Normale) auf dieser Fläche stehen. Damit hat man die Möglichkeit, die Beleuchtung von Flächen gezielt zu manipulieren.<br />
<br><br />
<br />
''GL_NORMALIZE''<br><br />
In OpenGL gibt es weiterhin die Möglichkeit die Normalen automatisch normalisieren zu lassen. Dazu muss mittels glEnable(GL_NORMALIZE) das Normalisieren eingeschaltet werden. Alle darauffolgenden Aufrufe von glNormal setzen die aktuelle Normale automatisch normiert. Abgeschaltet werden kann dies natürlich wieder durch glDisable(GL_NORMALIZE).<br />
<br />
== Siehe auch ==<br />
[[glNormal]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Normale&diff=14609Normale2005-11-29T18:16:32Z<p>Akira: /* Normalisieren */</p>
<hr />
<div>= Normalen =<br />
<br />
<br><br />
== Was ist das? ==<br />
Eine '''Normale''' ist ein Vektor, der Senkrecht auf eine Fläche steht.<br />
<br />
<br><br />
== Wozu brauch ich Normalen? ==<br />
'''Normalen''' haben in OpenGL zwei zentrale Aufgaben.<br />
<br />
# Sie zeigen welche Flächenseite die Vorderseite ist (anhand des Vorzeichens).<br />
# Sie bestimmen wie Licht von der Fläche reflektiert wird.<br />
<br />
<br><br />
== Wie berechne ich Normalen? ==<br />
'''Normalen''' sind Flächennormalen. Das heißt, sie stehen senkrecht auf einer Fläche.<br><br />
<br />
'''Welche Fläche?'''<br />
<br />
Eine beliebige...<br />
<br />
Ersteinmal eine kurze Zusammenfassung.<br><br />
* Flächen bestehen aus Polygonen.<br />
* Polygone bestehen aus n Dreiecken.<br />
* Jedes Dreieck ist definiert durch seine 3 Eckpunkte.<br />
* 3 Punkte liegen immer in einer Ebene<br />
<br />
Wenn man nun die Normale eines Dreiecks berechnen will geht man folgenermaßen vor:<br />
# Einen Eckpunkt als Bezugspunkt (P0) wählen.<br />
# Die beiden Vektoren v1 = P1-P0 und v2 = P2-P0 bestimmen.<br />
# Diese beiden Kreuzmultiplizieren <v1,v2>. (steht in jedem Tafelwerk unter Kreuzprodukt)<br />
# Das Ergebnis hat schonmal die richtige Richtung. (Für OpenGl reicht das.) <br />
<br />
=== Normalisieren ===<br />
Eine Normale heißt aber nur deshalb Normale, weil sie normiert ist. Das heißt sie ist 1 Längeneinheit lang. Wenn man einen Vektor normieren will dividiert man einfach den Vektor durch seine Länge.<br />
Allgemein:<br />
1/sqrt(x^2+y^2+z^2) * (x,y,z)<br />
<br />
Beispiel eines normierten Vektors:<br />
<br />
(1) normieren 1 (1)<br />
(1) ----------> --- * (1)<br />
(0) sqrt(2) (0)<br />
<br />
Das Ergebnis ist die Normale des Dreiecks und damit auch der gesamten Ebene in der das Dreieck liegt.<br />
<br />
Mit [[glNormal]] hat man in OpenGL expliziet die Möglichkeit einer Fläche eine '''Normale''' zu zuweisen. Diese muss nicht senkrecht (wie eine echte Normale) auf dieser Fläche stehen. Damit hat man die Möglichkeit, die Beleuchtung von Flächen gezielt zu manipulieren.<br />
<br><br />
<br />
''GL_NORMALIZE''<br><br />
In OpenGL gibt es weiterhin die Möglichkeit die Normalen automatisch normalisieren zu lassen. Dazu muss mittels glEnable(GL_NORMALIZE) das Normalisieren eingeschaltet werden. Alle darauffolgenden Aufrufe von glNormal setzen die aktuelle Normale automatisch normiert. Abgeschaltet werden kann dies natürlich wieder durch glDisable(GL_NORMALIZE).<br />
<br />
== Siehe auch ==<br />
[[glNormal]]</div>Akirahttps://wiki.delphigl.com/index.php?title=GL_ARB_occlusion_query&diff=14608GL ARB occlusion query2005-11-29T18:06:14Z<p>Akira: /* Anwendung */</p>
<hr />
<div>= GL_ARB_occlusion_query =<br />
{{Hinweis|Die Orginalspezifikation finden Sie unter "Ressourcen" am Ende des Artikels.}}<br />
<br />
<br />
== Abfragestring ==<br />
GL_ARB_occlusion_query<br />
<br />
<br><br />
== Beschreibung ==<br />
GL_ARB_occlusion_query bietet eine Möglichkeit festzustellen, wieviele Samples (Pixel) einer [[Primitive]] gezeichnet werden.<br />
<br><br />
<br />
== Neue Prozeduren ==<br />
<br />
procedure <b>glGenQueriesARB</b>(n : integer; ids : PGLuint);<br />
procedure <b>glDeleteQueriesARB</b>(n : integer; const ids : PGLuint);<br />
function <b>glIsQueryARB</b>(id : Cardinal) : Boolean;<br />
procedure <b>glBeginQueryARB</b>(target : Cardinal; id : Cardinal);<br />
procedure <b>glEndQueryARB</b>(target : Cardinal);<br />
procedure <b>glGetQueryivARB</b>(target : Cardinal; pname : Cardinal; params : PGLInt);<br />
procedure <b>glGetQueryObjectivARB</b>(id : Cardinal; pname : Cardinal; params : PGLInt);<br />
procedure <b>glGetQueryObjectuivARB</b>(id : Cardinal, pname : Cardinal; params : PGLuint);<br />
<br />
<br />
== Neue Tokens ==<br />
''Für glBeginQueryARB, glEndQueryARB oder glGetQueryivARB als <target> Parameter'' <br><br />
'''GL_SAMPLES_PASSED_ARB''' <br><br />
<br><br />
''Für glGetQueryivARB als <pname> Parameter'' <br><br />
'''GL_QUERY_COUNTER_BITS_ARB''' <br><br />
'''GL_CURRENT_QUERY_ARB''' <br><br />
<br><br />
''Für glGetQueryObjectivARB oder glGetQueryObjectuivARB als <pname> Parameter'' <br><br />
'''QUERY_RESULT_ARB''' <br><br />
'''QUERY_RESULT_AVAILABLE_ARB''' <br><br />
<br />
== Anwendung ==<br />
Bevor der Occlusion Test zur Verfügung steht, muss erst einmal eine Query mit glGenQueriesARB() erzeugt werden. Danach wird das Zählen der Samples mit glBeginQueryARB() und GL_SAMPLES_PASSED_ARB als <target> Parameter gestartet. Dann wird die Szene gerendert. Um den Test auszuwerten, muss das Zählen erst einmal mit glEndQueryARB() beendet werden. Anschließend kann man sich das Ergebnis des Test, also die gezeichneten Pixel, mittels glGetQueryObjectivARB() (QUERY_RESULT_ARB als <pname> Parameter) zurückliefern lassen.<br />
<br />
== Beispiel ==<br />
<br />
<b>var</b> Query : Gluint;<br />
Pixel : Integer;<br />
<br /><br />
[...]<br />
<br /><br />
<font color="#000080"><i>//== Query erzeugen</i></font><br />
glGenQueriesARB<b>(</b>1,@Query<b>)</b>;<br />
<font color="#000080"><i>//== mit dem Zählen beginnen</i></font><br />
glBeginQueryARB<b>(</b>GL_SAMPLES_PASSED_ARB,Query<b>)</b>;<br />
<br /><br />
<font color="#000080"><i>//== Szene/Primitive zeichnen</i></font><br />
<br /><br />
<font color="#000080"><i>//== aufhören zu Zählen</i></font><br />
glEndQueryARB<b>(</b>GL_SAMPLES_PASSED_ARB<b>)</b>;<br />
<font color="#000080"><i>//== Ergebnis wird nach Pixel zurückgeliefert</i></font><br />
glGetQueryObjectivARB<b>(</b>Query,GL_QUERY_RESULT_ARB,@Pixel<b>)</b>;<br />
<font color="#000080"><i>//== Query wieder freigeben</i></font><br />
glDeleteQueriesARB<b>(</b>1,@Pixel<b>)</b>;<br />
<br />
== Ressourcen ==<br />
[http://delphigl.com/script/do_show.php?name=nv_occlusion_query&action=2 Occlusion Query Tutorial mit GL_OCCLUSION_QUERY_NV]<br><br />
[http://oss.sgi.com/projects/ogl-sample/registry/ARB/occlusion_query.txt Original Extension-Spezifikation]</div>Akirahttps://wiki.delphigl.com/index.php?title=GL_ARB_occlusion_query&diff=14607GL ARB occlusion query2005-11-29T18:05:24Z<p>Akira: /* Beschreibung */</p>
<hr />
<div>= GL_ARB_occlusion_query =<br />
{{Hinweis|Die Orginalspezifikation finden Sie unter "Ressourcen" am Ende des Artikels.}}<br />
<br />
<br />
== Abfragestring ==<br />
GL_ARB_occlusion_query<br />
<br />
<br><br />
== Beschreibung ==<br />
GL_ARB_occlusion_query bietet eine Möglichkeit festzustellen, wieviele Samples (Pixel) einer [[Primitive]] gezeichnet werden.<br />
<br><br />
<br />
== Neue Prozeduren ==<br />
<br />
procedure <b>glGenQueriesARB</b>(n : integer; ids : PGLuint);<br />
procedure <b>glDeleteQueriesARB</b>(n : integer; const ids : PGLuint);<br />
function <b>glIsQueryARB</b>(id : Cardinal) : Boolean;<br />
procedure <b>glBeginQueryARB</b>(target : Cardinal; id : Cardinal);<br />
procedure <b>glEndQueryARB</b>(target : Cardinal);<br />
procedure <b>glGetQueryivARB</b>(target : Cardinal; pname : Cardinal; params : PGLInt);<br />
procedure <b>glGetQueryObjectivARB</b>(id : Cardinal; pname : Cardinal; params : PGLInt);<br />
procedure <b>glGetQueryObjectuivARB</b>(id : Cardinal, pname : Cardinal; params : PGLuint);<br />
<br />
<br />
== Neue Tokens ==<br />
''Für glBeginQueryARB, glEndQueryARB oder glGetQueryivARB als <target> Parameter'' <br><br />
'''GL_SAMPLES_PASSED_ARB''' <br><br />
<br><br />
''Für glGetQueryivARB als <pname> Parameter'' <br><br />
'''GL_QUERY_COUNTER_BITS_ARB''' <br><br />
'''GL_CURRENT_QUERY_ARB''' <br><br />
<br><br />
''Für glGetQueryObjectivARB oder glGetQueryObjectuivARB als <pname> Parameter'' <br><br />
'''QUERY_RESULT_ARB''' <br><br />
'''QUERY_RESULT_AVAILABLE_ARB''' <br><br />
<br />
== Anwendung ==<br />
Bevor der Occlusion Test zur Verfügung steht, muss erst einmal eine Query mit glGenQueriesARB() erzeugt werden. Danach wird das Zählen der Samples mit glBeginQueryARB() und GL_SAMPLES_PASSED_ARB als <target> Parameter gestartet. Dann Szene wird gerendert. Um den Test auszuwerten muss das Zählen erst einmal mit glEndQueryARB() beendet werden. Anschließend kann man sich das Ergebnis des Test, also die gezeichneten Pixel, mittels glGetQueryObjectivARB() (QUERY_RESULT_ARB als <pname> Parameter) zurückliefern lassen.<br />
<br />
== Beispiel ==<br />
<br />
<b>var</b> Query : Gluint;<br />
Pixel : Integer;<br />
<br /><br />
[...]<br />
<br /><br />
<font color="#000080"><i>//== Query erzeugen</i></font><br />
glGenQueriesARB<b>(</b>1,@Query<b>)</b>;<br />
<font color="#000080"><i>//== mit dem Zählen beginnen</i></font><br />
glBeginQueryARB<b>(</b>GL_SAMPLES_PASSED_ARB,Query<b>)</b>;<br />
<br /><br />
<font color="#000080"><i>//== Szene/Primitive zeichnen</i></font><br />
<br /><br />
<font color="#000080"><i>//== aufhören zu Zählen</i></font><br />
glEndQueryARB<b>(</b>GL_SAMPLES_PASSED_ARB<b>)</b>;<br />
<font color="#000080"><i>//== Ergebnis wird nach Pixel zurückgeliefert</i></font><br />
glGetQueryObjectivARB<b>(</b>Query,GL_QUERY_RESULT_ARB,@Pixel<b>)</b>;<br />
<font color="#000080"><i>//== Query wieder freigeben</i></font><br />
glDeleteQueriesARB<b>(</b>1,@Pixel<b>)</b>;<br />
<br />
== Ressourcen ==<br />
[http://delphigl.com/script/do_show.php?name=nv_occlusion_query&action=2 Occlusion Query Tutorial mit GL_OCCLUSION_QUERY_NV]<br><br />
[http://oss.sgi.com/projects/ogl-sample/registry/ARB/occlusion_query.txt Original Extension-Spezifikation]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Pathfinding&diff=14575Tutorial Pathfinding2005-11-28T10:16:47Z<p>Akira: /* Geeignete Welten */</p>
<hr />
<div>Wilkommen zu meinen Tutorial zum Pathfinding.<br />
<br />
== Geeignete Welten ==<br />
<br />
Diese Methode geht davon aus das sich unsere virtuelle Welt aus lauter Quadraten zusammensetzt.<br />
Jedes dieser Felder wird für die Wegberechnung erstmal vereinfacht, in dem davon ausgegangen wird,<br />
das ein Feld entweder begehbar ist, oder nicht.<br />
(Oder sogar Felder haben die zwar begehbar sind aber gemieden werden sollten.)<br />
<br />
Nun wird weiter davon ausgegangen, dass eine Einheit von einem Feld nur in 8 (bzw. 4) Richtungen, nämlich in die 8 (4) benachbarten Felder bewegen kann.<br />
<br />
[[Bild:Tutorial_pathfinding_BewegungsRichtungen.png]]<br />
<br />
Von dem Feld kann sich die Einheit logischerweise weiter in ein nächstes Feld bewegen, allerdings - wie schon erklärt - immer nur in 8 Richtungen.<br />
Wer sich so manches 2D Strategie-Spiel genau anschaut dem fällt sicher auf, dass es dort genauso ist.<br />
<br />
Wir wollen zum Beispiel aus einer solchen Karte,<br />
<br />
[[Bild:Tutorial_pathfinding_Beispiel1-Spiel.png]]<br />
<br />
eine "'''PathMap'''" erzeugen,<br />
<br />
[[Bild:Tutorial_pathfinding_Beispiel1-Pathmap.png]]<br />
<br />
um zu wissen, wie das Objekt auf die andere Seite kommt.<br />
<br />
Wichtige Tastur Befehle des [http://www.delphigl.com/res/tutorials/pathfinding/http://www.delphigl.com/do_download.php?f=5225 Beispiel Programmes(mit Quelltext)]:<br />
<br />
*'''F1''': Pathmap vor dem Füllen anzeigen<br />
*'''F2''': Pathmap anzeigen<br />
*'''F3''': Draufsicht mit Gitter;<br />
<br />
Aufbau des Beispiel-Programmes:<br />
<br />
Alles wesentliche, bis auf das Pathfinding, befindet sich in der SpielFeldUnit.<br />
Hier haben wir unser Feld definiert.<br />
<br />
<pascal> TPlayer=(plnone,pl1,pl2,plNeutral);<br />
TFeld= object<br />
public<br />
Owner:TPlayer;<br />
end;<br />
</pascal><br />
<br />
Und unser gesamtes Spielfeld, welches einen zweifachen Array davon enthält.<br />
<br />
<pascal> TSpielFeld=class(TPersistent)<br />
private<br />
FWidth:Word;<br />
FHeight:Word;<br />
procedure ZeichneSpielFeld;<br />
procedure ZeichneEinheiten;<br />
public<br />
Feld: array of array of TFeld;<br />
Einheit:array of TEinheit;<br />
SelectedUnitIndex:Integer;<br />
function ErstelleEinheit(const EinheitenTyp:TEinheitenTyp;const NewOwner:TPlayer;const XPos,YPos:Word):Boolean;<br />
</pascal><br />
<br />
Die Größe davon spiegeln die Variablen FWidth und FHeight wider.<br />
Außerdem sind hier, wie man sehen kann, ebenfalls unsere Einheiten gespeichert.<br />
Die Variable SelectedUnitIndex gibt an, welche davon gerade ausgewählt wurde.<br />
<br />
<pascal> procedure Draw;<br />
procedure SetSize(NewWidth,NewHeight:Word);<br />
procedure Update;<br />
</pascal><br />
<br />
SetSize benutzt man um die Größe zu ändern, dabei werden allerdings alle Einheiten gelöscht.<br />
Ruft man Draw auf so wird ZeichneSpielFeld und ZeichneEinheiten aufgerufen um alles zu zeichnen.<br />
Der regelmäßge Aufruf von Update sorgt dafür, das überhaupt Bewegung ins Spiel kommt.<br />
<br />
<pascal> procedure Save(FileName:String);<br />
procedure Load(FileName:String);<br />
procedure FloodFill(Player:TPlayer);<br />
procedure OnSelection(Selected: Integer;Button: TMouseButton);<br />
</pascal><br />
<br />
FloodFill stellt den Besitzer aller Felder auf den angegebenen Spieler, <br />
und OnSelection wertet Selections-Ereignisse aus.<br />
<br />
<pascal> TBefehlsTyp=(BT_STAND,BT_MOVE);<br />
TEinheitenTyp=Word;<br />
<br />
TAnimation=record<br />
Pos:Word;<br />
Length:Word;<br />
Typ:TAnimationType;<br />
end; <br />
<br />
TBefehl=record<br />
Case Typ:TBefehlsTyp of<br />
BT_STAND:();<br />
BT_MOVE:(X,Y:Word);<br />
end;<br />
<br />
TEinheit=class<br />
public<br />
Ani:TAnimation;<br />
Befehl:TBefehl;<br />
X,Y:Word;<br />
Owner:TPlayer;<br />
Typ:TEinheitenTyp;<br />
constructor Create(VomTyp:TEinheitenTyp;NewOwner:TPlayer;XPos,YPos:Word);overload;<br />
Constructor Create;overload;<br />
procedure Draw;<br />
procedure Update;<br />
function GetRealX:Double;<br />
function GetRealY:Double;<br />
end;<br />
</pascal><br />
<br />
Eine Einheit ist normalerweise animiert und führt irgend eine Animation aus.<br />
Alle Daten, wie eine Einheit gerade zu Zeichnen(draw) ist, sind hier verwahrt.<br />
Die Funktionen GetRealX und GetRealY sorgen dafür, daß die Einheit abhängig von ihrer Animation und groben Position, an der richtigen Stelle gezeichnet wird.<br />
Die Funktion Update einer Einheit ist die Wichtigste, da sie regelt, welche Aktion und Animation die Einheit als nächstes machen soll.<br />
<br />
<pascal>procedure TEinheit.Update;<br />
begin<br />
Inc(Ani.Pos);<br />
if not((Ani.Pos >= Ani.Length) or (Ani.Typ = ANI_STAND))then exit;<br />
</pascal><br />
<br />
Als erstes wird die Animations-Phase 1 weiter gestellt.<br />
Weitergemacht wird nur, wenn die Einheit entweder eine Animation abgeschlossen hat, oder nichts zu tun hat.<br />
<br />
<pascal> case Befehl.Typ of<br />
BT_STAND:<br />
begin<br />
if (Ani.Pos >= Ani.Length) then Ani.Pos := 0;<br />
Ani.Typ := ANI_STAND;<br />
Ani.Length := 1000;<br />
end;<br />
</pascal><br />
<br />
Wenn die Einheit wirklich stehen soll, wird höchstens die Animations-Position mal zurückgesetzt.<br />
<pascal> BT_MOVE:<br />
begin<br />
if (X = Befehl.x) and (Y = Befehl.Y) then<br />
begin //Ziel erreicht<br />
Befehl.typ := BT_STAND ;<br />
Ani.Pos := 0;<br />
Ani.Typ := ANI_STAND;<br />
Ani.length := 1000;<br />
exit;<br />
end;<br />
</pascal><br />
<br />
Wenn die Einheit den Befehl hat sich zu bewegen, aber ihr Ziel schon erreicht hat, dann erhält sie Befehl zu stehen und wir sind fertig.<br />
<br />
<pascal> Ani.Pos := 0;<br />
Ani.Typ :=FindPath(Befehl.X,Befehl.Y,self); ;//Hier muss der variante Teil hin<br />
Ani.Length := 10;<br />
case Ani.Typ of<br />
ANI_MOVE_LEFT : X := X-1;<br />
ANI_MOVE_Right: X := X+1;<br />
ANI_MOVE_UP : Y := Y+1;<br />
ANI_MOVE_DOWN : Y := Y-1;<br />
<br />
ANI_MOVE_LEFTDOWN: begin X:= X-1; Y:= Y-1 end;<br />
ANI_MOVE_RIGHTDOWN: begin X:= X+1; Y:= Y-1 end;<br />
ANI_MOVE_LEFTUP: begin X:= X-1; Y:= Y+1 end;<br />
ANI_MOVE_RIGHTUP: begin X:= X+1; Y:= Y+1 end;<br />
end;<br />
if SpielFeld.Feld[X,Y].Owner <> Owner then Ani.Length := Ani.Length*LangsamkeitsFaktor;<br />
end;<br />
end;<br />
end;<br />
</pascal><br />
<br />
Nun sind wir soweit gekommen das wir unser Einheit eine Animation zuweisen wollen, aber noch nicht wissen welche das sein soll.<br />
Daher rufen wir die function FindPath aus der Pathfinding Unit auf.<br />
<br />
==Die eigentliche Wegfindungs-Routine==<br />
<br />
===Vorbereiten der "Pathmap"-Variable===<br />
<br />
Um dem Computer unser Level verständlich zu machen, wird in diese Variable:<br />
<br />
<pascal><br />
Pathmap: array of array of Integer;<br />
</pascal><br />
<br />
unser gesamtes Level übertragen.<br />
<br />
Dabei beschreiben folgende Konstanten die einzelnen Felder.<br />
<br />
<pascal><br />
const<br />
Feld_ist_Ziel= 0;<br />
Feld_ist_gesperrt= -1;<br />
Feld_ist_frei= -8;<br />
Feld_ist_feindlich =-9;<br />
Feld_ist_StartFeld= -10;<br />
</pascal><br />
Unsere Pathmap wird allerdings um 1 Feld in jede Richtung größer als unser SpielFeld sein.<br />
Dieser äußere Rand wird eine Art Abgrenzung für unser SpielFeld.<br />
<br />
<pascal><br />
SetLength(Pathmap ,SpielFeld.width +2,SpielFeld.height +2);<br />
for X := 0 to SpielFeld.Width +1 do<br />
begin<br />
Pathmap[X,0] := Feld_ist_Gesperrt;<br />
Pathmap[X,SpielFeld.Height+1] := Feld_ist_Gesperrt;<br />
end;<br />
for Y := 0 to SpielFeld.Height +1 do<br />
begin<br />
Pathmap[0,Y] := Feld_ist_Gesperrt;<br />
Pathmap[SpielFeld.width+1,Y] := Feld_ist_Gesperrt;<br />
end;<br />
</pascal><br />
<br />
Dann müssen wir natürlich noch unsere Felder eintragen.<br />
<br />
<pascal><br />
for X := 0 to SpielFeld.Width-1 do<br />
for Y := 0 to SpielFeld.Height-1 do<br />
begin<br />
if SpielFeld.Feld[X,Y].Owner = plneutral then Pathmap[X+1,Y+1] := Feld_ist_Gesperrt<br />
else if SpielFeld.Feld[X,Y].Owner = TEinheit(Einheit).owner then<br />
Pathmap[X+1,Y+1] := Feld_ist_Frei<br />
else<br />
Pathmap[X+1,Y+1] := Feld_ist_feindlich;<br />
end;<br />
</pascal><br />
<br />
Felder vom Spieler plneutral sollen in meinen Beispiel-Programm gesperrt sein.<br />
Andernfalls wird überprüft ob das Feld vom Feind oder von uns kontrolliert wird.<br />
<br />
Natürlich dürfen alle Felder, auf denen schon Einheiten stehen, nicht betreten werden.<br />
<br />
<pascal><br />
for X := 0 to High(SpielFeld.Einheit) do<br />
Pathmap[SpielFeld.Einheit[X].X+1,SpielFeld.Einheit[X].Y+1] := Feld_ist_Gesperrt ;<br />
</pascal><br />
<br />
Nun müssen wir noch die Einheit eintragen, für die wir den Weg berechnen.<br />
<br />
<pascal><br />
Pathmap[TEinheit(Einheit).X+1,TEinheit(Einheit).Y+1] := Feld_ist_StartFeld;<br />
</pascal><br />
<br />
Jetzt müssen wir überprüfen, ob die Einheit vielleicht schon direkt vor dem Ziel steht, welches aber besetzt ist. Sollte dies so sein so sind wir fertig da wir nichts zu berechen haben. <br />
<br />
<pascal><br />
if (Pathmap[ZielX+1,ZielY+1] = Feld_ist_Gesperrt)<br />
and (Betrag(TEinheit(Einheit).X - ZielX) <= 1 )<br />
and (Betrag(TEinheit(Einheit).Y -ZielY) <=1) then<br />
begin<br />
result := ANI_STAND;<br />
exit;<br />
end;<br />
</pascal><br />
<br />
Ansonsten tragen wir unser Ziel in die Pathmap ein.<br />
<pascal><br />
Pathmap[ZielX+1,ZielY+1] := Feld_ist_Ziel ;<br />
</pascal><br />
<br />
Nun haben wir unsere Pathmap vorbereitet.<br />
Sie könnte nun so aussehen:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel1-NewPathmap.png]]<br />
<br />
=== Füllen der "Pathmap" ===<br />
<br />
<pascal><br />
var<br />
Pathmap: array of array of Integer;<br />
x,y:Integer;<br />
Weg:Integer;<br />
LX,HX:Integer;<br />
LY,HY:Integer;<br />
Fertig:Boolean ;<br />
</pascal><br />
<br />
Wir starten eine Schleife in der die Pathmap gefüllt wird:<br />
<br />
<pascal><br />
Weg:= -1;//Weg+1=0<br />
Fertig := false;<br />
while not Fertig and (Weg < MAX_ENTFERNUNG) do<br />
begin<br />
Weg:=Weg+1;<br />
</pascal><br />
<br />
Es ist sinnvoll, mit einen kleinen Füll-Breich anzufangen. Dieser wird durch LX, HX, LY, HY begrenzt.<br />
<br />
<pascal><br />
LX := ZielX+1 -Weg-1;<br />
HX := ZielX+1 +Weg+1;<br />
LY := ZielY+1 -Weg-1;<br />
HY := ZielY+1 +Weg+1;<br />
<br />
if LX < 1 then LX := 1;<br />
if LY < 1 then LY := 1;<br />
if HX > SpielFeld.Width then HX :=SpielFeld.Width;<br />
if HY > SpielFeld.Height then HY := SpielFeld.Height;<br />
</pascal><br />
<br />
Nun prüfen wir jedes Feld im Füll-Bereich, ob es an ein Feld angrenzt, was den aktuellen Weg-Wert hat.<br />
Wir brauchen übrigens Felder die gesperrt sind oder bereits einen positven Wert besitzen nicht mehr beachten.<br />
<br />
<pascal><br />
for X:= LX to HX do<br />
for Y:= LY to HY do<br />
if Pathmap[X,Y] <= Feld_ist_frei then<br />
{Wenn das Feld noch nicht berechnet und begehbar ist}<br />
if (Pathmap[x-1,y]=Weg)<br />
or (Pathmap[x+1,y]=Weg)<br />
or (Pathmap[x,y-1]=Weg)<br />
or (Pathmap[x,y+1]=Weg)<br />
<br />
or (Pathmap[x-1,y-1]=Weg)<br />
or (Pathmap[x+1,y-1]=Weg)<br />
or (Pathmap[x-1,y+1]=Weg)<br />
or (Pathmap[x+1,y+1]=Weg) then<br />
</pascal><br />
<br />
Normalerweise würden wir jetzt jedes Feld (außer es ist das Ziel) um 1 erhöhen. Da wir aber auch feindliche Felder haben, müssen wir diese auch noch beachten:<br />
<br />
<pascal><br />
case Pathmap[X,Y] of<br />
<br />
Feld_ist_feindlich:Pathmap[X,Y]:= Weg + LangsamkeitsFaktor;<br />
Feld_ist_StartFeld:<br />
begin<br />
Fertig := true;<br />
end;<br />
else<br />
{Feld_ist_frei:}<br />
Pathmap[X,Y]:= Weg +1;<br />
end;<br />
end;<br />
</pascal><br />
<br />
Ein Feld, welches den Wert der Variable Weg + 1 hat, <br />
wird logischerweise im nächsten Durchlauf der Schleife den Wert der Variable Weg haben.<br />
Es wird dann die Bedingung des angrenzenden Feldes erfüllen.<br />
Dadurch ist auch klar, warum das auf feindlichem Gebiet drei mal so lange dauert.... es dauert einfach 3 Runden statt 1 bis das Feld den gleichen Wert wie die Weg-Variable hat<br />
Es entsteht somit eine Ausdehnung der Zahlen um das ZielFeld (Wert 0) herum.<br />
Jedes Feld, das so eine postive Zahl erhalten hat, bescheibt somit die Entfernung zum Ziel.<br />
Machen wir das so lange, bis wir das "StartFeld" mit einer Zahl überschreiben würden, so wäre diese Zahl die Entfernung zum Ziel.<br />
<br />
Versuch doch mal sofort zu sagen, ob sich die Einheit oben oder unten rum bewegen soll:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel2-Spiel.png]]<br />
<br />
Dank unser Pathmap ist viel einfacher wenn man weis wie es geht:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel2-Pathmap.png]]<br />
<br />
=== Richtige Animation auswählen ===<br />
<br />
Nun braucht man sich nur noch das Feld suchen, welches an StartFeld angrenzt, den aktuellen Wert von Weg hat und entspechend die Animation wählen (das Feld, welches die Überschreibung vom Ziel-Feld veranlaßt hätte).<br />
<br />
<pascal><br />
X:= TEinheit(Einheit).X +1;<br />
Y:= TEinheit(Einheit).Y +1;<br />
<br />
if (Pathmap[X+1,Y] = Weg) then Result := ANI_MOVE_RIGHT<br />
else if (Pathmap[X-1,Y] = Weg) then Result := ANI_MOVE_LEFT<br />
else if (Pathmap[X,Y+1] = Weg) then Result := ANI_MOVE_UP<br />
else if (Pathmap[X,Y-1] = Weg) then Result := ANI_MOVE_DOWN<br />
<br />
else if (Pathmap[X-1,Y-1] = Weg) then Result := ANI_MOVE_LEFTDOWN<br />
else if (Pathmap[X+1,Y-1] = Weg) then Result := ANI_MOVE_RIGHTDOWN<br />
else if (Pathmap[X-1,Y+1] = Weg) then Result := ANI_MOVE_LEFTUP<br />
else if (Pathmap[X+1,Y+1] = Weg) then Result := ANI_MOVE_RIGHTUP<br />
<br />
else Result := ANI_STAND;<br />
end;<br />
</pascal><br />
<br />
<br />
Ich hoffe, mein Tutorial hat euch gefallen und ihr könnt damit irgendetwas anfangen.<br />
<br />
Euer [[Benutzer:Flo|Flo]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Komponentenentwicklung&diff=14574Tutorial Komponentenentwicklung2005-11-28T10:08:34Z<p>Akira: /* Komponenten Tutorial */</p>
<hr />
<div>=Komponenten Tutorial=<br />
<br />
==Einleitung==<br />
Erst mal willkommen bei meinem Komponeten-Tutorial. Ich möchte in diesem Tutorial nicht absolutes Basiswissen über die Komponenten-Programmierung unter Delphi vermitteln, sondern denjenigen, die schon entsprechende Einsteigertutorials gelesen haben (wie etwa das auf Delphi-Treff.de) noch etwas tiefer in die Materie einführen.<br />
<br />
== Eigene Visuelle Komponeten ==<br />
Wenn man seine Komponente komplett selber zeichnen möchte, so empfiehlt es sich diese von '''TGraphicControl''' oder '''TCustomControl''' abzuleiten. TCustomControl ist im Gegensatz zu TGrapicControl ein Nachfahre von TWinControl, und erbt von ihm ein (Fenster-) Handle. Dieses Handle ist eine Nummer, welche Komponenten von Windows erhalten um sie eindeutig zu bezeichnen. Nur wenn die Komponente ein Handle hat, kann Windows mit ihr etwas anfangen.<br />
<br />
Möchte man nur statisch etwas anzeigen, wie etwa ein Label, so kann man auch nur TGraphicControl als Vorfahren wählen. Solche Komponenten ohne Handle werden von Windows nicht unterstützt, und sind nur eine Technik von Delphi, um dem Entwickler einfachere Gestaltungsmöglichkeiten zu bieten. Es ist ja schließlich viel einfacher, eine Komponente auf der Form zu platzieren, als diese mit Zeichenbefehlen selbst zu zeichnen.<br />
<br />
Um selbst festzulegen, wie die Komponente gezeichnet wird, kann man bei Nachfahren von TGraphicControl oder TCustomControl ganz einfach die '''procedure Paint''' überschreiben.<br />
<pascal>TMeineKomponente = class(TGraphicControl)<br />
protected<br />
procedure Paint; override; <br />
</pascal><br />
<br />
Gezeichnet wird dann über die Canvas-Eigenschaft der Komponente. Dabei sollte man nicht vergessen mit inherited die Paint Procedure des Vorfahren aufzurufen.<br />
<br />
<pascal>procedure TMeineKomponente.Paint;<br />
begin<br />
inherited;//Paint Procedure des Vorfarhen aufrufen.<br />
Canvas.//irgendwas<br />
</pascal><br />
<br />
Eine Abfrage der Eigenschaft ComponentState ermöglicht es uns, die Komponente zur Entwickungszeit anders zu zeichnen.<br />
<br />
<pascal>procedure TMeineKomponente.Paint;<br />
begin<br />
inherited;//Zeichen Procedure des Vorfahren aufrufen.<br />
if csDesigning in ComponentState then //Wenn sich die Komponente in Entwicklung befindet.<br />
begin<br />
{Zeichung eines gestrichelten Rahmens}<br />
Canvas.Brush.style := bsClear;//Durchsichtiges Rechteck<br />
Canvas.Pen.style := psDashDot;//Gestrichelte Linen<br />
Canvas.Rectangle(0,0,width,height);//Rechteck zeichnen<br />
<br />
{Namen der Komponente in die Mitte schreiben}<br />
canvas.TextOut((width - Canvas.TextWidth(Name)) div 2,(height - Canvas.TextHeight(Name)) div 2,name);<br />
<br />
{Keine weitern Zeichnungen mehr ausführen}<br />
exit;//Verlässt die Procedure<br />
end;<br />
//Normale Zeichen-Anweisungen<br />
end;<br />
</pascal><br />
<br />
== Eigenschafts-Editoren ==<br />
<br />
Als erstes sollten wir der Frage nachgehen, was ein Eigenschaftseditor überhaupt ist : Wie schon fast zu erraten, handelt es sich hierbei um eine Möglichkeit, eine als veröffentlicht (published) deklarierte Eigenschaft im Objektinspektor zu bearbeiten. In Delphi sind schon diverse Eigenschaftseditoren vorhanden um die häufigsten Eigenschaftstypen abzudecken. Wenn ihr einer Komponente eine als published deklarierte Eigenschaft vom Typ Integer verpasst, so wird standardgemäß der Editor "TIntegerProperty" genutzt um die Eigenschaft im Objektinspektor darzustellen. Dieser Editor ist ein Eingabefeld für Zahlen.<br />
<br />
Eigenschaftseditoren können aber nicht nur solche Eingabefelder sein, sondern können auch eine ganze Reihe anderer Bearbeitungsmöglichkeiten bieten. Ein richtig multifunktionaler Eigenschaftseditor ist zum Beispiel "TColorProperty". So kann man entweder eine hexadezimale Zahl oder einen Farbnamen eingeben, die entsprechende Farbe über eine Liste auswählen oder gar per Doppelklick einen Farbdialog öffenen.<br />
<br />
Ein solcher Eigenschaftseditor ist programmiertechnisch gesehen nichts anderes als ein Nachfahre der Klasse "TPropertyEditor", welcher in einer Register-Prozedur in Delphi eingebunden wurde. Den Code von einem solchen Eigenschaftseditor sollte man in eine Unit packen, welche nur von Delphi genutzen Code enthält. Für das beiliegende Beispiel TFilePropertyEditor nehmen wir direkt TPropertyEditor als Vorfahren. <br />
<br />
<pascal>TFileNameProperty = class (TPropertyEditor)<br />
{...}<br />
end;<br />
</pascal><br />
<br />
In welcher Form der Eigenschaftseditor angezeigt wird, bestimmt der Rückgabewert der Funktion GetAttributes. Um diese zu ändern überschreiben wir die Funktion einfach.<br />
<br />
<pascal><br />
TFileNameProperty = class (TPropertyEditor)<br />
public<br />
function GetAttributes: TPropertyAttributes; override;<br />
{...}<br />
end;<br />
</pascal><br />
<br />
Dem Eigenschatseditor werde ich nun zusätzlich zu dem Standardeingabefeld noch per '''paDialog''' die Möglichkeit geben, auf Doppelklicks oder einen (nun evtl. erschienen) Buttonklick zu reagieren. '''paMultiSelect''' bewirkt schlussendlich noch, dass die Eigenschaft auch noch sichtbar ist, wenn mehre Komponenten ausgewählt sind. Weitere mögliche Parameter sind der Delphi Hilfe zu entnehmen.<br />
<br />
<pascal>function TFileNameProperty.GetAttributes: TPropertyAttributes;<br />
begin<br />
Result := [paMultiSelect, paDialog];<br />
end;<br />
</pascal><br />
<br />
Um auf das neu geschaffene Editierereignis zu reagieren, überschreiben wir die '''Procedure Edit;'''<br />
<br />
<pascal>procedure TFileNameProperty.Edit;<br />
var<br />
OpenDialog:TOpenDialog;<br />
begin<br />
OpenDialog := TOpenDialog.create(Application);//Den OpenDialog erstellen<br />
try<br />
//Den akuellen Wert übergeben<br />
OpenDialog.FileName := GetStrValue;<br />
//Anzeigen und bei Erfolg Wert übernehmen<br />
if OpenDialog.Execute then SetStrValue(OpenDialog.FileName);<br />
finally<br />
OpenDialog.Free; //Den OpenDialog freigeben.<br />
end;<br />
end;<br />
</pascal><br />
<br />
Mit '''GetStrValue''' und '''SetStrValue''' kann der Wert einer Stringeigenschaften gelesen und geschrieben werden. Für andere Eigenschaftstypen stehen andere Funktionen zur Verfügung. An dieser Stelle können natürlich auch eigene Formulare eingeblendet werden.</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Lektion_3&diff=14573Tutorial Lektion 32005-11-28T09:57:23Z<p>Akira: /* Hier kommt die Sonne... */</p>
<hr />
<div>= Eine Welt des Grauens =<br />
== Vorwort ==<br />
Liebe Leser,<br />
nachdem Ihr hoffentlich den kleinen Schock des letzten Tutorials alle gut überstanden habt, freue ich mich, Euch hier wieder begrüßen zu können. Wer dachte, dass die letzte Lektion bereits schwer war, der sollte dringend Urlaub nehmen. Seid gewarnt! Bevor ich jetzt richtig loslege, solltet Ihr einigermaßen entspannt sein. Wenn dies einer der Tage ist, an denen Ihr nur noch durch eine Kippe oder die Flasche Coke am Leben gehalten werdet, ordne ich erstmal eine kleine Zwangspause an ;).<br />
<br />
Der Stoff der jetzt kommt ist sicherlich nicht gerade die leichteste Lektüre. Wer Mathematik studiert, hat Vorteile, wer einigermaßen logisch denken kann auch. Ein Fachidiot, wie ich, hat nur eine Chance... probieren, testen und lernen :). Und weil ich weiß, wie schwer es eventuell sein kann, werde ich nun einfach mal versuchen, ganz simpel anzufangen und das Ganze mit vielen Bildern so gut wie nur irgend möglich zu illustrieren. <br />
<br />
Und wenn Ihr nun Angst habt, dass Ihr das nicht packt, weil ich hier so eine Panik verbreite, dann solltet Ihr mich sehen, wie ich hier verzweifelt vor meinen Tasten hänge und mich frage, wie ich das alles bloß in Worte fassen soll :). Aber was soll es? Ran an den Kram!!! Heulen könnt Ihr anschließend im Forum ;).<br />
== Das Grauen hat einen Namen ==<br />
=== "Kinofilm" vs. "Bahnhof" ===<br />
Wer kennt nicht "Matrix" und hätte gedacht, dass es davon nicht nur eine, sondern unendlich viele gibt die so genannten "Matrizen". Vor allem am Anfang werden diese Dinger Euch das Leben erschweren und Ihr werdet leichte Neigungen tief in Euch verspüren, dass am besten Euer ganzes Projekt sich nur im Zentrum Eurer Welt abspielt (und dies ist nicht im wahrsten Sinne des Wortes gemeint).<br />
<br />
Wer mit Matrizen umgehen kann, beherrscht das Wichtigste an der 3D-Programmierung. Wer nicht zu den Genies zählt, sollte nicht sofort aufgeben, sondern sich viele Beispiele ansehen und viel mit diesen herumspielen.<br />
<br />
Wie auch immer... Ihr solltest wissen, dass es bei jedem Rendervorgang mehrere Matrizen gibt, die ganz drastisch das Aussehen der Szene bestimmen. Es gibt in OpenGL drei Bereiche, in denen Matrizen eingesetzt werden. Die so genannte World- oder Modelviewmatrix, die Texturenmatrix und die Perspektivenmatrix. Die zweifellos wichtigste dieser Matrizen ist die Worldmatrix, da sie die Position Eures "Zeichenstiftes" beschreibt und somit Objekte positioniert. Die Texturenmatrix funktioniert fast genauso wie die Worldmatrix nur definiert sie die Ausrichtung der Texturen. Wir werden später auf sie zurückkommen. Die Perspektivenmatrix definiert wesentliche Eigenschaften des Blickfeldes des Betrachters.<br />
<br />
Der Befehl [[glMatrixMode]] erlaubt eine Änderung der aktuellen Matrix. Mit Hilfe der Konstanten GL_MODELVIEW, GL_TEXTURE oder GL_PERSPECTIVE kann man die Matrix bestimmen. Die Namen der Konstanten sind soweit hoffentlich selbst erklärend. Von nun an bewirken alle Matrixoperationen wie beispielsweise [[glLoadIdentity]] oder [[glTranslate|glTranslate*]] eine Veränderung der aktuell gesetzten Matrix.<br />
=== Die Perspektivenmatrix ===<br />
Wie bereits erwähnt beschreibt die Perspektivenmatrix das aktuelle Sichtfeld. Zum Setzen der Perspektivenmatrix verwendet man in der Regel Befehle wie [[glFrustum]], [[gluPerspective]], [[glOrtho]] oder [[gluOrtho2D]].<br />
<br />
gluPerspective lässt den Raum beispielsweise dreidimensional erscheinen, indem sich die Größe von Objekten mit zunehmender Entfernung vom Betrachter verringert. Diese Verringerung ist abhängig von dem im ersten Parameter übergebenen Winkel. <br />
<br />
glOrtho hingegen ist phantastisch für ein 2D-Blickfeld geeignet, denn diese Art der Projektion enthält keine Tiefe mehr.<br />
<br />
Genauere Informationen zu den einzelnen Funktionen könnt ihr unserem OpenGL-Wiki entnehmen.<br />
<br />
Auf eine Sache möchte ich jedoch noch eingehen: Einige dieser Funktionen verlangen die Parameter "znear" und "zfar". Diese Parameter beschreiben die zwei Schnittflächen, die das Blickfeld vor dem Betrachter begrenzen. "znear" definiert die Entfernung der Nearclippingplane vom Betrachter. Alle Objekte, die sich vor dieser Ebene befinden sind nicht sichtbar. "zfar" beschreibt die Entfernung der Farclippingplane vom Betrachter. Alle Objekte, die sich hinter dieser befinden werden weggeschnitten. Wenn also wieder einmal nur die Hälfte Eurer Szene auf dem Bildschirm sichtbar ist, versucht die Schnittflächen entsprechend anzupassen!<br />
<br />
== Die Worldmatrix ==<br />
=== D3D-Verrat ===<br />
Wenn Ihr ebenfalls zu den D3D-Verrätern gehört (wie z.B. ich *g*), dann solltet Ihr Euch möglichst von eurer Denkweise trennen: Jede Positionierung der Kamera erfolgt über ein Drehen und Bewegen der Szene. Das ist vor allem am Anfang ein wenig gewöhnungsbedürftig ("Die Welt dreht sich! Nicht Du!").<br />
<br />
Vielmehr wird jede Manipulation direkt an die Worldmatrix übergeben. Das heißt, wenn Ihr glTranslate* aufruft, so ist diese Manipulation bereits bei Euch in die Worldmatrix eingetragen, alle weiteren Objekte werden mit dieser Matrix transformiert. Wer zweimal glTranslate*(2, 0, 0) aufruft wird feststellen, dass glTranslate* nicht zwei Matrizen zurückliefert, sondern nur eine, die dann auf (4, 0, 0) verweist. Am Anfang solltet Ihr also sehr darauf achten, es wird eine Weile dauern bis Ihr dies fest in Eurem Herzen tragt. Nicht sofort aufgeben :)!<br />
== Einfach, Kompakt und Funktional ==<br />
Neben dem Befehl glTranslate*, den wir im letzten Tutorial lieben gelernt haben, gibt es noch weitere Befehle, die sich auf die einzelnen Matrizen auswirken und auf die ich in dem folgenden Abschnitt eingehen möchte.<br />
=== Dreh- und Angelpunkt des Geschehens ===<br />
Zunächst beschäftigen wir uns mit der Rotation. Wir versuchen ein Objekt zu erzeugen, welches um 90° um seine eigene Achse gedreht wurde. Das ist nicht sonderlich schwer: <pascal>[[glRotate|glRotatef]](90,0,1,0);</pascal><br />
Ein Objekt auf das wir diese Matrix anwenden, wird um 90° um seine Y-Achse gedreht sein. Daraus schließen wir, dass der erste Parameter den Winkel im Gradmaß um den wir rotieren möchten definiert und die 3 folgenden Parameter den Vektor beschreiben um den die Rotation durchgeführt werden soll. Rotationen bergen tatsächlich eine Schwierigkeit, denn es ist nicht immer einfach ein Objekt um die Eigene Achse zu rotieren. Verschiebt Ihr euer Objekt zuerst und beginnt dann die Rotation, so rotiert das Objekt um die eigene Achse.<br />
<br />
Ruft Ihr jedoch zuerst glRotate* und anschließend glTranslate* auf, so unterscheidet sich erzielte Ergebnis von dem vorherigen.<br />
<br />
Um diesen Problemen aus dem Weg zu gehen so müsst Ihr Euch vorstellen, dass Ihr den Rotationspunkt im Moment des Aufrufs von glRotate* setzt. Verschiebt Ihr Euer Objekt anschließend so bleibt der Rotationspunkt derselbe und das Objekt beginnt den Rotationspunkt auf einer Bahn zu umkreisen. So gesehen gibt es keinen wirklichen Unterschied und die Rotation um die eigene Achse ist ein Ausnahmefall, denn der Rotationspunkt liegt dann in dem von uns erwarteten Mittelpunkt des Objektes.<br />
=== Eine Frage der Größe ===<br />
Nun ja... was kann man denn noch alles in einer 3D-Welt machen? Ihr habt gelernt, wie man Objekte bewegt und diese durch Rotationen ausrichten kann. Das ist doch schon eine Menge zum Spielen oder? Nun, bei einer Transformation kann für uns noch eine Sache sehr wichtig werden, nämlich die Möglichkeit, zu bestimmen in welcher Größe ein Objekt dargestellt werden kann.<br />
<br />
Sicher würde es Sinn machen ein benötigtes Objekt gleich in der richtigen Größe in die Anwendung zu laden. Manchmal ist es aber von Vorteil, wenn man die Größe eines Objektes ohne großen Aufwand verändern kann oder aber mehrere Objekte desselben Typs mit unterschiedlicher Größe darzustellen.<br />
<br />
Eine Größenveränderung ist mit Hilfe des Befehls [[glScale|glScale*]] möglich.<br />
<pascal>glScalef(1,1,1);</pascal><br />
In diesem Fall würden wir das Objekt so zeichnen, wie wir es auch angegeben haben. Das heißt in seiner Originalgröße. Sicherlich ahnst Du bereits, was die Parameter aussagen... sie stehen für das Verhältnis der einzelnen Seiten nach dem Muster X, Y und Z. Würden wir als ersten Parameter statt einer 1 eine 2 einsetzen, würde unser Objekt in seiner räumlichen Ausdehnung bezogen auf der X-Achse doppelt so groß sein. Würden wir stattdessen 0.5 angeben, wäre es nur noch halb so groß.<br />
<br />
In Wirklichkeit bewirkt glScale* jedoch keine Veränderung der Größe sondern eine Verzerrung unseres Koordinatensystems. Daher wirkt jeder Aufruf von glScale* auch auf Befehle wie glTranslate*. Aus Sicht der Matrizen lässt sich dieses Phänomen auch recht leicht erklären. In Wirklichkeit erzeugt jeder Aufruf von glRotate*, glTranslate* oder glScale* eine eigene Matrix, die dann mit der aktuellen Matrix (z.B. der Modelviewmatrix) multipliziert wird. Wurde in der Modelviewmatrix bereits ein glScale* verewigt, so wirkt sich dieses bei der Multiplikation der Matrizen auf die Transformationsmatrix aus.<br />
<br />
Ich denke ich muss nicht näher darauf eingehen, dass es keine gute Idee ist das Koordinatensystem mit einem Aufruf von glScalef(0, 0, 0) zu zerstören. Was würde es auch für einen Sinn ergeben, das Koordinatensystem in einem Punkt unterzubringen? <br><br />
'''''Hinweis''': Es ist bereits gefährlich nur eine Achse auf 0 zu skalieren, denn dadurch wird die aktuelle Matrix beschädigt ([http://www.math.unizh.ch/fachverein/forum/detail.jsp?FORUM=120 singulär]) und Befehle wie [[gluProject]] und [[gluUnProject]] funktionieren nicht mehr.''<br />
<br />
Ich hoffe ich habe Euch nun nicht den letzten Funken Hoffnung OpenGL zu verstehen geraubt. Ich kann Euch versichern, dass dieses Verständnis mit der Zeit heranreifen wird.<br />
[[bild:tutimg_lektion3_skalierung.gif|256px|right]]<br />
glScale* ist aber aufgrund seiner phantastischen Eigenschaften nicht nur in der Lage die Größe von Objekten zu verändern sondern es ist auch möglich Objekte zu spiegeln indem man negative Werte an glScale* übergibt. Somit kann sich z.B. ein D3D-Umsteiger das leben vereinfachen indem er mit einem Aufruf von glScalef(1, 1, -1) sicherstellt, dass die Z-Achse in den Bildschirm hinein positiv verläuft.<br />
<br />
Jeder, der die Funktion der Modelviewmatrix begreifen möchte, dem möchte ich das Programm Matrixcontrol von Lithander (siehe Anhang) ans Herz legen. Es erlaubt Euch bestimmte Änderungen an der Matrix direkt nachzuvollziehen und wenn man mit dem Programm ein wenig umherspielt wird selbst dem letzten ziemlich schnell ein Licht aufgehen. Wenn nicht, dann hilft nur fleißiges Programmieren und Anwenden und irgendwann klatscht es dann laut, wenn die eigene Handfläche auf der Stirn zum stehen kommt ;).<br />
<br />
== Virtuelle Gedächtnisse ==<br />
=== Vom Pushen und Poppen ===<br />
Wer diesen Titel ließt und sich nun zwangsläufig daran erinnert, was er bereits alles in seinem Leben konsumiert hat oder neben wenn er am nächsten Morgen aufgewacht ist, sei entwarnt! Wir reden hier von etwas ganz anderem...<br />
<br />
Wie wir bereits gelernt haben, verändert jeder Aufruf von glTranslate*, glRotate* oder glScale* die Worldmatrix und wirkt sich unmittelbar auf andere Objekte aus. Dies kann durchaus sehr erwünscht sein. Wenn wir allerdings in einer großen Welt verschiedene Objekte positionieren, kann dies schnell zum Fluch werden. Natürlich können wir dem entgegenwirken und zum Beispiel jedes Mal glLoadIdentity aufrufen. Dies hat jedoch den Nachteil, dass die World- Matrix dann eben wieder leer ist und wir z.B. die Camera erst wieder setzen müssen, damit die Objekte an die richtige Stelle transformiert werden.<br />
<br />
Eine weitaus elegantere Lösung ist die Verwendung des "Matrixstacks"! Jeder Aufruf von [[glPushMatrix]] bewirkt, dass die momentane Matrix auf den Stack geschrieben wird. Alle Manipulationen, die wir dann durchführen, werden wie gewohnt vollzogen. Ist alles gezeichnet, rufen wir mit [[glPopMatrix]], die letzte Matrix vom Stack wieder herunter und haben im Prinzip den letzten Zustand vor unseren Veränderungen rekonstruiert und können dann wieder anfangen, auf dieser Matrix aufzubauen. Natürlich lassen sich auch mehre Matrizen auf den Stack pushen! Dies kann reichlich Zeit sparen, wenn man bedenkt, dass man ansonsten andauernd die Matrix der Szene neu setzen muss.<br />
<br />
Um das ganze etwas anschaulicher zu machen, verwenden wir ein einfaches Beispiel. Wir werden zwei Dreiecke jeweils rechts und links vom Ursprung zeichnen. Nachdem wir das linke Objekt gezeichnet haben, werden wir nicht glLoadIdentity einsetzen oder das zweite Objekt entsprechend nach rechts transformieren, sondern eben den Matrixstack zu Hilfe nehmen.<br />
<pascal>glLoadIdentity;<br />
glTranslatef(0,0,-10);<br />
glPushMatrix;<br />
glTranslatef(-2,0,0);<br />
glBegin(GL_TRIANGLES);<br />
glColor3f(1,0,0); glVertex3f(-1,-1, 0);<br />
glColor3f(0,1,0); glVertex3f( 0, 1, 0);<br />
glColor3f(0,0,1); glVertex3f( 1,-1, 0);<br />
glEnd;<br />
glPopMatrix;<br />
<br />
glTranslatef(2,0,0);<br />
glBegin(GL_TRIANGLES);<br />
glColor3f(1,0,0); glVertex3f(-1,-1, 0);<br />
glColor3f(0,1,0); glVertex3f( 0, 1, 0);<br />
glColor3f(0,0,1); glVertex3f( 1,-1, 0);<br />
glEnd;</pascal><br />
Wie Ihr seht, setzen wir am Anfang praktisch die Distanz zum Objekt, speichern die Matrix und zeichnen das erste Objekt. Wir setzen dann die Matrix wieder zurück, so dass sie nur noch den Aufruf von glTranslatef(0, 0, -10) beschreibt und transformieren das zweite Objekt... fertig. Man kann auch ohne diese Matrixstacks leben sie können einem aber das Leben auch sehr erleichtern.<br />
== I wanna all! ==<br />
Wer glaubt, dass es das bereits war der irrt. Es gibt noch weitere Möglichkeiten auf die Matrix Einfluss zu nehmen. Der Befehl [[glLoadMatrix|glLoadMatrix*]] erlaubt es eine beliebige 4x4 Matrix zu laden und die aktuelle Matrix durch die geladene komplett zu ersetzen.<br />
<br />
Der Befehl [[glMultMatrix|glMultMatrix*]] multipliziert die übergebene 4x4-Matrix mit der aktuellen Matrix. Mit diesem Befehl könnte man beispielsweise eigene glScale*- und glRotate*-Befehle schreiben, obgleich sich auch hier über den Sinn streiten lässt. Eine sinnvollere Anwendungsmöglichkeit wird in einem späteren Tutorial vorgestellt werden.<br />
<br />
Der Befehl [[glGet|glGet*]] mit den Tokens GL_MODELVIEW_MATRIX, GL_TEXTURE_MATRIX und GL_PROJECTION_MATRIX ermöglicht euch die einzelnen Matrizen anzufordern.<br />
== Weltenformeln ==<br />
=== Hier kommt die Sonne... ===<br />
Genug der Theorie! Es wird langsam Zeit für etwas Praxis...<br />
<br />
Nun, um ehrlich zu sein, habe ich an der folgenden Idee ein wenig Zeit benötigt, um ein einigermaßen gut anschauliches Beispiel zu finden... ich will hier nicht weiter darauf eingehen, was ich gerade gehört habe, als mir die Sonne aufging :-D.<br />
<br />
Wie auch immer, man kann komplexere Matrixtransformationen sehr leicht mit unseren Sonnensystem beschreiben (Hey, wenn jetzt jemand denkt, dass ich spinne...). Um das Ganze etwas übersichtlicher zu gestalten, werden wir unser Prinzip auf Sonne, Mond und Erde beschränken.<br />
<br />
Wie funktioniert unser Sonnensystem? (Warnung, der Autor betritt mal wieder seine ausgeprägte Fantasywelt! [Anm. d. Lektors: Die richtige Sprache dafür hat er ja schon]).<br />
<br />
Die Sonne steht im Mittelpunkt und sollte sich in unserem Beispiel nicht selbst bewegen. Sie ist einfach nur im Mittelpunkt und vegetiert dort vor sich hin! Nun gibt es einen kleinen blauen Planeten, namens Erde (wieso er im Sample ein Dreieck ist, sei Eurer Kreativität überlassen...), der zum einen um seine eigene Achse rotiert, viel wichtiger jedoch, sich auch noch um die Sonne dreht. Nun werden sich vor allem die Neulinge unter Euch sadistisch freuen, das Fenster schließen, ihr Delphi starten und drauf los proggen! Das ist doch alles kein Ding, der gerade mal frisch aufgeschnappte Sinus-Satz lässt sich prima mit einbringen! Und schon berechnet man mit Hilfe von Sinus, die Position auf der Kreisbahn und positioniert den Planeten mit glTranslate* an seine Position. Und oh Wunder es klappt. HALT! Wer es nicht gemerkt hat, da war ne Portion Ironie dabei. Bitte Weiterlesen ^__^!<br />
<br />
Denn spätestens wenn wir folgende Ergänzung zum Besten geben, werden die ersten Augen wässrig werden, weil die meisten Hirne einem Informationskoller unterliegen. Meines stieg ja schon beim einfachen Sinus aus :-D. Ab und an kann man nämlich nachts weißgräuliche Flecken am Himmel beobachten! Wer annimmt, dass es sich hierbei um eine geschickt platzierte, in Echtzeit berechnete, Textur handelt, ist schief gewickelt! Es handelt sich dabei nämlich um ein Decal, namens Mond. Oha... so schlimm war es schon lange nicht mehr. Dieser bewegt sich in unserem Beispiel auch nicht entlang der Erdachse, sondern auf einer schiefen Bahn, damit man auf der unteren Hemissphäre den Mond auch am Tage noch sehen kann ^___^ (*selbstgefälliges, göttliches Grinsen*). Dies lässt sich nur sehr schwer mit Sinus beschreiben. Bevor wir uns nun jedoch mit einem Block bewaffnen oder den nächsten Mathematik-Professor entführen, damit er die Mathematik für uns übernimmt, greifen wir lieber zu den Sternen und nehmen ein einfacheres Verfahren! Les Matrices!<br />
<br />
=== Und es dreht sich doch! ===<br />
Die Vorgehensweise ist eigentlich relativ simpel. Wir Zeichnen unsere Sonne im Mittelpunkt unseres Universums. Mit dem Aufruf von glRotate* setzen wir den Rotationspunkt an die Position unseres "Zeichenstiftes". In unserem Fall ist das noch immer die Position der Sonne. <br />
<br />
Nun verschieben wir die Erde nach links und die erste Hürde ist genommen. Jetzt müssen wir nur noch den Mond setzen und das funktioniert genauso einfach wie zuvor mit der Erde.<br />
<br />
Unser Zeichenstift befindet sich nun an der Position der Erde und daher rufen wir glRotate* auf. Nun müssen wir den Zeichenstift nur noch ein kleines Stück neben der Erde positionieren und den Mond zeichnen. Voila! Es ist vollbracht!<br />
<br />
Wenn man genauer darüber nachdenkt erkennt man sehr schnell wie elegant man dieses verhältnismäßig komplizierte Problem gelöst hat.<br />
<br />
Wer nun auf Wolke Sieben schwebt und glaubt er könnte sich nun an den nächsten DOOMTitel werfen, der sollte das Programm zuvor noch so erweitern, dass die Sonne, die Erde und auch der Mond sich zusätzlich um die eigene Achse drehen.<br />
<br />
== [[Timebased Movement]] ==<br />
Wer bereits versucht hat eines der ersten selbst geschriebenen OpenGL-Programme voller Stolz seinem Kumpel vorzuführen wird festgestellt haben, dass die Bewegungen auf dem anderen Rechner schneller oder langsamer abgelaufen sind als es auf dem eigenen Rechner der Fall war.<br />
<br />
Dieses Phänomen lässt sich sehr einfach erklären. Nehmen wir an wir bewegen ein Dreieck von links nach rechts um den Wert 1 über den Bildschirm. Eine alte Krücke wie mein PC schafft vielleicht 50 FPS. Das bedeutet das Objekt wird in einer Sekunde genau 50mal um eine Einheit nach rechts verschoben. Das bedeutet insgesamt also um 50 Einheiten in einer Sekunde. Wenn das Programm nun aber auf einem High-End-System läuft werden wir die Szene... sagen wir... 500mal aktualisieren können. Folglich bewegt sich auf diesem Rechner das Dreieck in einer Sekunde um 500 Einheiten.<br />
<br />
Das einzige was wir von unserem Dreieck erhaschen können ist ein blitzartiges Zucken auf dem Bildschirm, welches wir schnell als optische Täuschung abtun würden. Wie kann man diesem Problem nun aber entgegen wirken?<br />
<br />
Nehmen wir an wir kennen die Zeit, die wir zum Zeichnen eines Frames benötigen und ermitteln anhand dieses Wertes einen Zeitfaktor, den wir in die Bewegung mit einfließen lassen.<br />
<pascal>NewPosition := OldPosition + Movement * TimeFactor;</pascal><br />
Handelt es sich um einen schnellen Rechner so ist der Zeitfaktor entsprechend niedrig und die Bewegung pro Frame verringert sich. Ist der Rechner hingegen langsam so besitzen wir einen hohen Zeitfaktor. Folglich legt das Dreieck auf dem schnelleren Rechner pro Frame weniger Weg zurück als auf dem langsamen. Insgesamt jedoch legen die beiden Dreiecke im gleichen Zeitabstand (z.B. in einer Sekunde) denselben Weg zurück.<br />
<br />
Um den Zeitfaktor zu ermitteln muss man lediglich die Zeit für einen Schleifendurchlauf ermitteln. Um eine möglichst hohe Genauigkeit zu erhalten sollte man die Zeit für den letzten Schleifendurchlauf ermitteln und im aktuellen Schleifendurchlauf verwenden.<br />
<br />
== Nachwort ==<br />
Wow... und wieder ein wenig Zeit zum Ausspannen und ein paar persönlichen Worten. Ich kann getrost nur eines sagen: Wenn Ihr nun denkt "das war alles ja sehr leicht" habt Ihr irgendetwas falsch gemacht und solltet das Tutorial noch einmal von Vorn durcharbeiten ;). Wenn Ihr jedoch leichte Kopfschmerzen verspürt (ob es nun die Matrizen sind oder der fiese Elementarbereich *sg*...) so habt Ihr alles richtig gemacht ;).<br />
<br />
Matrizen sind vor allem vielen Einsteigern sehr fremd. "Übung macht den Meister". Versucht Euch doch einfach mal die eine oder andere Aufgabe selbst zu stellen oder unser "Sonnensystem" selbst einmal nachzuschreiben. Wenn Ihr glaubt, dass das Ganze ganz nett geworden ist, dann schickt es mir doch einfach mal zu. Ich freue mich immer darüber zu sehen, was für Früchte meine Tutorials tragen. *lach* Und wenn sich plötzlich alles um die Erde dreht, dann habe ich a) das Gefühl, dass die Kenntnisse über das Sonnensystem nicht mehr ganz up to date sind oder b) ich einen ziemlich fatalen Fehler beim Erklären gemacht habe :->.<br />
<br />
Schreckt auch bitte nicht davor zurück, eine Frage diesbezüglich im Forum zu stellen. Es ist keine Schande, mit einer Matrix Probleme zu haben und vor allem die Erfahrenen unter uns, sollten nur zu gut wissen, was alles die Ursache dafür sein kann, wenn man plötzlich gar nichts mehr auf dem Screen sieht :).<br />
<br />
Wir selbst werden uns in den folgenden Kapiteln noch etwas intensiver mit den Matrizen beschäftigen und unter anderem die eine oder andere interessante Spielerei zeigen!<br />
<br />
'''Euer'''<br><br />
'''Phobeus'''<br />
<br />
<br />
== Anhang ==<br />
<br />
[http://www.pixelpracht.net Pixelpracht] - Hier könnt Ihr das Programm "Matrix Control" finden<br />
<br />
<br />
{{TUTORIAL_NAVIGATION | [[Tutorial_lektion2]] | [[Tutorial_lektion4]]}}<br />
[[Kategorie:Tutorial|Lektion3]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Lektion_3&diff=14572Tutorial Lektion 32005-11-28T09:57:00Z<p>Akira: /* Und es dreht sich doch! */</p>
<hr />
<div>= Eine Welt des Grauens =<br />
== Vorwort ==<br />
Liebe Leser,<br />
nachdem Ihr hoffentlich den kleinen Schock des letzten Tutorials alle gut überstanden habt, freue ich mich, Euch hier wieder begrüßen zu können. Wer dachte, dass die letzte Lektion bereits schwer war, der sollte dringend Urlaub nehmen. Seid gewarnt! Bevor ich jetzt richtig loslege, solltet Ihr einigermaßen entspannt sein. Wenn dies einer der Tage ist, an denen Ihr nur noch durch eine Kippe oder die Flasche Coke am Leben gehalten werdet, ordne ich erstmal eine kleine Zwangspause an ;).<br />
<br />
Der Stoff der jetzt kommt ist sicherlich nicht gerade die leichteste Lektüre. Wer Mathematik studiert, hat Vorteile, wer einigermaßen logisch denken kann auch. Ein Fachidiot, wie ich, hat nur eine Chance... probieren, testen und lernen :). Und weil ich weiß, wie schwer es eventuell sein kann, werde ich nun einfach mal versuchen, ganz simpel anzufangen und das Ganze mit vielen Bildern so gut wie nur irgend möglich zu illustrieren. <br />
<br />
Und wenn Ihr nun Angst habt, dass Ihr das nicht packt, weil ich hier so eine Panik verbreite, dann solltet Ihr mich sehen, wie ich hier verzweifelt vor meinen Tasten hänge und mich frage, wie ich das alles bloß in Worte fassen soll :). Aber was soll es? Ran an den Kram!!! Heulen könnt Ihr anschließend im Forum ;).<br />
== Das Grauen hat einen Namen ==<br />
=== "Kinofilm" vs. "Bahnhof" ===<br />
Wer kennt nicht "Matrix" und hätte gedacht, dass es davon nicht nur eine, sondern unendlich viele gibt die so genannten "Matrizen". Vor allem am Anfang werden diese Dinger Euch das Leben erschweren und Ihr werdet leichte Neigungen tief in Euch verspüren, dass am besten Euer ganzes Projekt sich nur im Zentrum Eurer Welt abspielt (und dies ist nicht im wahrsten Sinne des Wortes gemeint).<br />
<br />
Wer mit Matrizen umgehen kann, beherrscht das Wichtigste an der 3D-Programmierung. Wer nicht zu den Genies zählt, sollte nicht sofort aufgeben, sondern sich viele Beispiele ansehen und viel mit diesen herumspielen.<br />
<br />
Wie auch immer... Ihr solltest wissen, dass es bei jedem Rendervorgang mehrere Matrizen gibt, die ganz drastisch das Aussehen der Szene bestimmen. Es gibt in OpenGL drei Bereiche, in denen Matrizen eingesetzt werden. Die so genannte World- oder Modelviewmatrix, die Texturenmatrix und die Perspektivenmatrix. Die zweifellos wichtigste dieser Matrizen ist die Worldmatrix, da sie die Position Eures "Zeichenstiftes" beschreibt und somit Objekte positioniert. Die Texturenmatrix funktioniert fast genauso wie die Worldmatrix nur definiert sie die Ausrichtung der Texturen. Wir werden später auf sie zurückkommen. Die Perspektivenmatrix definiert wesentliche Eigenschaften des Blickfeldes des Betrachters.<br />
<br />
Der Befehl [[glMatrixMode]] erlaubt eine Änderung der aktuellen Matrix. Mit Hilfe der Konstanten GL_MODELVIEW, GL_TEXTURE oder GL_PERSPECTIVE kann man die Matrix bestimmen. Die Namen der Konstanten sind soweit hoffentlich selbst erklärend. Von nun an bewirken alle Matrixoperationen wie beispielsweise [[glLoadIdentity]] oder [[glTranslate|glTranslate*]] eine Veränderung der aktuell gesetzten Matrix.<br />
=== Die Perspektivenmatrix ===<br />
Wie bereits erwähnt beschreibt die Perspektivenmatrix das aktuelle Sichtfeld. Zum Setzen der Perspektivenmatrix verwendet man in der Regel Befehle wie [[glFrustum]], [[gluPerspective]], [[glOrtho]] oder [[gluOrtho2D]].<br />
<br />
gluPerspective lässt den Raum beispielsweise dreidimensional erscheinen, indem sich die Größe von Objekten mit zunehmender Entfernung vom Betrachter verringert. Diese Verringerung ist abhängig von dem im ersten Parameter übergebenen Winkel. <br />
<br />
glOrtho hingegen ist phantastisch für ein 2D-Blickfeld geeignet, denn diese Art der Projektion enthält keine Tiefe mehr.<br />
<br />
Genauere Informationen zu den einzelnen Funktionen könnt ihr unserem OpenGL-Wiki entnehmen.<br />
<br />
Auf eine Sache möchte ich jedoch noch eingehen: Einige dieser Funktionen verlangen die Parameter "znear" und "zfar". Diese Parameter beschreiben die zwei Schnittflächen, die das Blickfeld vor dem Betrachter begrenzen. "znear" definiert die Entfernung der Nearclippingplane vom Betrachter. Alle Objekte, die sich vor dieser Ebene befinden sind nicht sichtbar. "zfar" beschreibt die Entfernung der Farclippingplane vom Betrachter. Alle Objekte, die sich hinter dieser befinden werden weggeschnitten. Wenn also wieder einmal nur die Hälfte Eurer Szene auf dem Bildschirm sichtbar ist, versucht die Schnittflächen entsprechend anzupassen!<br />
<br />
== Die Worldmatrix ==<br />
=== D3D-Verrat ===<br />
Wenn Ihr ebenfalls zu den D3D-Verrätern gehört (wie z.B. ich *g*), dann solltet Ihr Euch möglichst von eurer Denkweise trennen: Jede Positionierung der Kamera erfolgt über ein Drehen und Bewegen der Szene. Das ist vor allem am Anfang ein wenig gewöhnungsbedürftig ("Die Welt dreht sich! Nicht Du!").<br />
<br />
Vielmehr wird jede Manipulation direkt an die Worldmatrix übergeben. Das heißt, wenn Ihr glTranslate* aufruft, so ist diese Manipulation bereits bei Euch in die Worldmatrix eingetragen, alle weiteren Objekte werden mit dieser Matrix transformiert. Wer zweimal glTranslate*(2, 0, 0) aufruft wird feststellen, dass glTranslate* nicht zwei Matrizen zurückliefert, sondern nur eine, die dann auf (4, 0, 0) verweist. Am Anfang solltet Ihr also sehr darauf achten, es wird eine Weile dauern bis Ihr dies fest in Eurem Herzen tragt. Nicht sofort aufgeben :)!<br />
== Einfach, Kompakt und Funktional ==<br />
Neben dem Befehl glTranslate*, den wir im letzten Tutorial lieben gelernt haben, gibt es noch weitere Befehle, die sich auf die einzelnen Matrizen auswirken und auf die ich in dem folgenden Abschnitt eingehen möchte.<br />
=== Dreh- und Angelpunkt des Geschehens ===<br />
Zunächst beschäftigen wir uns mit der Rotation. Wir versuchen ein Objekt zu erzeugen, welches um 90° um seine eigene Achse gedreht wurde. Das ist nicht sonderlich schwer: <pascal>[[glRotate|glRotatef]](90,0,1,0);</pascal><br />
Ein Objekt auf das wir diese Matrix anwenden, wird um 90° um seine Y-Achse gedreht sein. Daraus schließen wir, dass der erste Parameter den Winkel im Gradmaß um den wir rotieren möchten definiert und die 3 folgenden Parameter den Vektor beschreiben um den die Rotation durchgeführt werden soll. Rotationen bergen tatsächlich eine Schwierigkeit, denn es ist nicht immer einfach ein Objekt um die Eigene Achse zu rotieren. Verschiebt Ihr euer Objekt zuerst und beginnt dann die Rotation, so rotiert das Objekt um die eigene Achse.<br />
<br />
Ruft Ihr jedoch zuerst glRotate* und anschließend glTranslate* auf, so unterscheidet sich erzielte Ergebnis von dem vorherigen.<br />
<br />
Um diesen Problemen aus dem Weg zu gehen so müsst Ihr Euch vorstellen, dass Ihr den Rotationspunkt im Moment des Aufrufs von glRotate* setzt. Verschiebt Ihr Euer Objekt anschließend so bleibt der Rotationspunkt derselbe und das Objekt beginnt den Rotationspunkt auf einer Bahn zu umkreisen. So gesehen gibt es keinen wirklichen Unterschied und die Rotation um die eigene Achse ist ein Ausnahmefall, denn der Rotationspunkt liegt dann in dem von uns erwarteten Mittelpunkt des Objektes.<br />
=== Eine Frage der Größe ===<br />
Nun ja... was kann man denn noch alles in einer 3D-Welt machen? Ihr habt gelernt, wie man Objekte bewegt und diese durch Rotationen ausrichten kann. Das ist doch schon eine Menge zum Spielen oder? Nun, bei einer Transformation kann für uns noch eine Sache sehr wichtig werden, nämlich die Möglichkeit, zu bestimmen in welcher Größe ein Objekt dargestellt werden kann.<br />
<br />
Sicher würde es Sinn machen ein benötigtes Objekt gleich in der richtigen Größe in die Anwendung zu laden. Manchmal ist es aber von Vorteil, wenn man die Größe eines Objektes ohne großen Aufwand verändern kann oder aber mehrere Objekte desselben Typs mit unterschiedlicher Größe darzustellen.<br />
<br />
Eine Größenveränderung ist mit Hilfe des Befehls [[glScale|glScale*]] möglich.<br />
<pascal>glScalef(1,1,1);</pascal><br />
In diesem Fall würden wir das Objekt so zeichnen, wie wir es auch angegeben haben. Das heißt in seiner Originalgröße. Sicherlich ahnst Du bereits, was die Parameter aussagen... sie stehen für das Verhältnis der einzelnen Seiten nach dem Muster X, Y und Z. Würden wir als ersten Parameter statt einer 1 eine 2 einsetzen, würde unser Objekt in seiner räumlichen Ausdehnung bezogen auf der X-Achse doppelt so groß sein. Würden wir stattdessen 0.5 angeben, wäre es nur noch halb so groß.<br />
<br />
In Wirklichkeit bewirkt glScale* jedoch keine Veränderung der Größe sondern eine Verzerrung unseres Koordinatensystems. Daher wirkt jeder Aufruf von glScale* auch auf Befehle wie glTranslate*. Aus Sicht der Matrizen lässt sich dieses Phänomen auch recht leicht erklären. In Wirklichkeit erzeugt jeder Aufruf von glRotate*, glTranslate* oder glScale* eine eigene Matrix, die dann mit der aktuellen Matrix (z.B. der Modelviewmatrix) multipliziert wird. Wurde in der Modelviewmatrix bereits ein glScale* verewigt, so wirkt sich dieses bei der Multiplikation der Matrizen auf die Transformationsmatrix aus.<br />
<br />
Ich denke ich muss nicht näher darauf eingehen, dass es keine gute Idee ist das Koordinatensystem mit einem Aufruf von glScalef(0, 0, 0) zu zerstören. Was würde es auch für einen Sinn ergeben, das Koordinatensystem in einem Punkt unterzubringen? <br><br />
'''''Hinweis''': Es ist bereits gefährlich nur eine Achse auf 0 zu skalieren, denn dadurch wird die aktuelle Matrix beschädigt ([http://www.math.unizh.ch/fachverein/forum/detail.jsp?FORUM=120 singulär]) und Befehle wie [[gluProject]] und [[gluUnProject]] funktionieren nicht mehr.''<br />
<br />
Ich hoffe ich habe Euch nun nicht den letzten Funken Hoffnung OpenGL zu verstehen geraubt. Ich kann Euch versichern, dass dieses Verständnis mit der Zeit heranreifen wird.<br />
[[bild:tutimg_lektion3_skalierung.gif|256px|right]]<br />
glScale* ist aber aufgrund seiner phantastischen Eigenschaften nicht nur in der Lage die Größe von Objekten zu verändern sondern es ist auch möglich Objekte zu spiegeln indem man negative Werte an glScale* übergibt. Somit kann sich z.B. ein D3D-Umsteiger das leben vereinfachen indem er mit einem Aufruf von glScalef(1, 1, -1) sicherstellt, dass die Z-Achse in den Bildschirm hinein positiv verläuft.<br />
<br />
Jeder, der die Funktion der Modelviewmatrix begreifen möchte, dem möchte ich das Programm Matrixcontrol von Lithander (siehe Anhang) ans Herz legen. Es erlaubt Euch bestimmte Änderungen an der Matrix direkt nachzuvollziehen und wenn man mit dem Programm ein wenig umherspielt wird selbst dem letzten ziemlich schnell ein Licht aufgehen. Wenn nicht, dann hilft nur fleißiges Programmieren und Anwenden und irgendwann klatscht es dann laut, wenn die eigene Handfläche auf der Stirn zum stehen kommt ;).<br />
<br />
== Virtuelle Gedächtnisse ==<br />
=== Vom Pushen und Poppen ===<br />
Wer diesen Titel ließt und sich nun zwangsläufig daran erinnert, was er bereits alles in seinem Leben konsumiert hat oder neben wenn er am nächsten Morgen aufgewacht ist, sei entwarnt! Wir reden hier von etwas ganz anderem...<br />
<br />
Wie wir bereits gelernt haben, verändert jeder Aufruf von glTranslate*, glRotate* oder glScale* die Worldmatrix und wirkt sich unmittelbar auf andere Objekte aus. Dies kann durchaus sehr erwünscht sein. Wenn wir allerdings in einer großen Welt verschiedene Objekte positionieren, kann dies schnell zum Fluch werden. Natürlich können wir dem entgegenwirken und zum Beispiel jedes Mal glLoadIdentity aufrufen. Dies hat jedoch den Nachteil, dass die World- Matrix dann eben wieder leer ist und wir z.B. die Camera erst wieder setzen müssen, damit die Objekte an die richtige Stelle transformiert werden.<br />
<br />
Eine weitaus elegantere Lösung ist die Verwendung des "Matrixstacks"! Jeder Aufruf von [[glPushMatrix]] bewirkt, dass die momentane Matrix auf den Stack geschrieben wird. Alle Manipulationen, die wir dann durchführen, werden wie gewohnt vollzogen. Ist alles gezeichnet, rufen wir mit [[glPopMatrix]], die letzte Matrix vom Stack wieder herunter und haben im Prinzip den letzten Zustand vor unseren Veränderungen rekonstruiert und können dann wieder anfangen, auf dieser Matrix aufzubauen. Natürlich lassen sich auch mehre Matrizen auf den Stack pushen! Dies kann reichlich Zeit sparen, wenn man bedenkt, dass man ansonsten andauernd die Matrix der Szene neu setzen muss.<br />
<br />
Um das ganze etwas anschaulicher zu machen, verwenden wir ein einfaches Beispiel. Wir werden zwei Dreiecke jeweils rechts und links vom Ursprung zeichnen. Nachdem wir das linke Objekt gezeichnet haben, werden wir nicht glLoadIdentity einsetzen oder das zweite Objekt entsprechend nach rechts transformieren, sondern eben den Matrixstack zu Hilfe nehmen.<br />
<pascal>glLoadIdentity;<br />
glTranslatef(0,0,-10);<br />
glPushMatrix;<br />
glTranslatef(-2,0,0);<br />
glBegin(GL_TRIANGLES);<br />
glColor3f(1,0,0); glVertex3f(-1,-1, 0);<br />
glColor3f(0,1,0); glVertex3f( 0, 1, 0);<br />
glColor3f(0,0,1); glVertex3f( 1,-1, 0);<br />
glEnd;<br />
glPopMatrix;<br />
<br />
glTranslatef(2,0,0);<br />
glBegin(GL_TRIANGLES);<br />
glColor3f(1,0,0); glVertex3f(-1,-1, 0);<br />
glColor3f(0,1,0); glVertex3f( 0, 1, 0);<br />
glColor3f(0,0,1); glVertex3f( 1,-1, 0);<br />
glEnd;</pascal><br />
Wie Ihr seht, setzen wir am Anfang praktisch die Distanz zum Objekt, speichern die Matrix und zeichnen das erste Objekt. Wir setzen dann die Matrix wieder zurück, so dass sie nur noch den Aufruf von glTranslatef(0, 0, -10) beschreibt und transformieren das zweite Objekt... fertig. Man kann auch ohne diese Matrixstacks leben sie können einem aber das Leben auch sehr erleichtern.<br />
== I wanna all! ==<br />
Wer glaubt, dass es das bereits war der irrt. Es gibt noch weitere Möglichkeiten auf die Matrix Einfluss zu nehmen. Der Befehl [[glLoadMatrix|glLoadMatrix*]] erlaubt es eine beliebige 4x4 Matrix zu laden und die aktuelle Matrix durch die geladene komplett zu ersetzen.<br />
<br />
Der Befehl [[glMultMatrix|glMultMatrix*]] multipliziert die übergebene 4x4-Matrix mit der aktuellen Matrix. Mit diesem Befehl könnte man beispielsweise eigene glScale*- und glRotate*-Befehle schreiben, obgleich sich auch hier über den Sinn streiten lässt. Eine sinnvollere Anwendungsmöglichkeit wird in einem späteren Tutorial vorgestellt werden.<br />
<br />
Der Befehl [[glGet|glGet*]] mit den Tokens GL_MODELVIEW_MATRIX, GL_TEXTURE_MATRIX und GL_PROJECTION_MATRIX ermöglicht euch die einzelnen Matrizen anzufordern.<br />
== Weltenformeln ==<br />
=== Hier kommt die Sonne... ===<br />
Genug der Theorie! Es wird langsam Zeit für etwas Praxis...<br />
<br />
Nun, um ehrlich zu sein, habe ich an der folgenden Idee ein wenig Zeit benötigt, um ein einigermaßen gut anschauliches Beispiel zu finden... ich will hier nicht weiter darauf eingehen, was ich gerade gehört habe, als mir die Sonne aufging :-D.<br />
<br />
Wie auch immer, man kann komplexere Matrixtransformationen sehr leicht mit unseren Sonnensystem beschreiben (Hey, wenn jetzt jemand denkt, dass ich spinne ). Um das Ganze etwas übersichtlicher zu gestalten, werden wir unser Prinzip auf Sonne, Mond und Erde beschränken.<br />
<br />
Wie funktioniert unser Sonnensystem? (Warnung, der Autor betritt mal wieder seine ausgeprägte Fantasywelt! [Anm. d. Lektors: Die richtige Sprache dafür hat er ja schon]).<br />
<br />
Die Sonne steht im Mittelpunkt und sollte sich in unserem Beispiel nicht selbst bewegen. Sie ist einfach nur im Mittelpunkt und vegetiert dort vor sich hin! Nun gibt es einen kleinen blauen Planeten, namens Erde (wieso er im Sample ein Dreieck ist, sei Eurer Kreativität überlassen...), der zum einen um seine eigene Achse rotiert, viel wichtiger jedoch, sich auch noch um die Sonne dreht. Nun werden sich vor allem die Neulinge unter Euch sadistisch freuen, das Fenster schließen, ihr Delphi starten und drauf los proggen! Das ist doch alles kein Ding, der gerade mal frisch aufgeschnappte Sinus-Satz lässt sich prima mit einbringen! Und schon berechnet man mit Hilfe von Sinus, die Position auf der Kreisbahn und positioniert den Planeten mit glTranslate* an seine Position. Und oh Wunder es klappt. HALT! Wer es nicht gemerkt hat, da war ne Portion Ironie dabei. Bitte Weiterlesen ^__ ^!<br />
<br />
Denn spätestens wenn wir folgende Ergänzung zum Besten geben, werden die ersten Augen wässrig werden, weil die meisten Hirne einem Informationskoller unterliegen. Meines stieg ja schon beim einfachen Sinus aus :-D. Ab und an kann man nämlich nachts weißgräuliche Flecken am Himmel beobachten! Wer annimmt, dass es sich hierbei um eine geschickt platzierte, in Echtzeit berechnete, Textur handelt, ist schief gewickelt! Es handelt sich dabei nämlich um ein Decal, namens Mond. Oha... so schlimm war es schon lange nicht mehr. Dieser bewegt sich in unserem Beispiel auch nicht entlang der Erdachse, sondern auf einer schiefen Bahn, damit man auf der unteren Hemissphäre den Mond auch am Tage noch sehen kann ^___^ (*selbstgefälliges, göttliches Grinsen*). Dies lässt sich nur sehr schwer mit Sinus beschreiben. Bevor wir uns nun jedoch mit einem Block bewaffnen oder den nächsten Mathematik-Professor entführen, damit er die Mathematik für uns übernimmt, greifen wir lieber zu den Sternen und nehmen ein einfacheres Verfahren! Les Matrices!<br />
=== Und es dreht sich doch! ===<br />
Die Vorgehensweise ist eigentlich relativ simpel. Wir Zeichnen unsere Sonne im Mittelpunkt unseres Universums. Mit dem Aufruf von glRotate* setzen wir den Rotationspunkt an die Position unseres "Zeichenstiftes". In unserem Fall ist das noch immer die Position der Sonne. <br />
<br />
Nun verschieben wir die Erde nach links und die erste Hürde ist genommen. Jetzt müssen wir nur noch den Mond setzen und das funktioniert genauso einfach wie zuvor mit der Erde.<br />
<br />
Unser Zeichenstift befindet sich nun an der Position der Erde und daher rufen wir glRotate* auf. Nun müssen wir den Zeichenstift nur noch ein kleines Stück neben der Erde positionieren und den Mond zeichnen. Voila! Es ist vollbracht!<br />
<br />
Wenn man genauer darüber nachdenkt erkennt man sehr schnell wie elegant man dieses verhältnismäßig komplizierte Problem gelöst hat.<br />
<br />
Wer nun auf Wolke Sieben schwebt und glaubt er könnte sich nun an den nächsten DOOMTitel werfen, der sollte das Programm zuvor noch so erweitern, dass die Sonne, die Erde und auch der Mond sich zusätzlich um die eigene Achse drehen.<br />
<br />
== [[Timebased Movement]] ==<br />
Wer bereits versucht hat eines der ersten selbst geschriebenen OpenGL-Programme voller Stolz seinem Kumpel vorzuführen wird festgestellt haben, dass die Bewegungen auf dem anderen Rechner schneller oder langsamer abgelaufen sind als es auf dem eigenen Rechner der Fall war.<br />
<br />
Dieses Phänomen lässt sich sehr einfach erklären. Nehmen wir an wir bewegen ein Dreieck von links nach rechts um den Wert 1 über den Bildschirm. Eine alte Krücke wie mein PC schafft vielleicht 50 FPS. Das bedeutet das Objekt wird in einer Sekunde genau 50mal um eine Einheit nach rechts verschoben. Das bedeutet insgesamt also um 50 Einheiten in einer Sekunde. Wenn das Programm nun aber auf einem High-End-System läuft werden wir die Szene... sagen wir... 500mal aktualisieren können. Folglich bewegt sich auf diesem Rechner das Dreieck in einer Sekunde um 500 Einheiten.<br />
<br />
Das einzige was wir von unserem Dreieck erhaschen können ist ein blitzartiges Zucken auf dem Bildschirm, welches wir schnell als optische Täuschung abtun würden. Wie kann man diesem Problem nun aber entgegen wirken?<br />
<br />
Nehmen wir an wir kennen die Zeit, die wir zum Zeichnen eines Frames benötigen und ermitteln anhand dieses Wertes einen Zeitfaktor, den wir in die Bewegung mit einfließen lassen.<br />
<pascal>NewPosition := OldPosition + Movement * TimeFactor;</pascal><br />
Handelt es sich um einen schnellen Rechner so ist der Zeitfaktor entsprechend niedrig und die Bewegung pro Frame verringert sich. Ist der Rechner hingegen langsam so besitzen wir einen hohen Zeitfaktor. Folglich legt das Dreieck auf dem schnelleren Rechner pro Frame weniger Weg zurück als auf dem langsamen. Insgesamt jedoch legen die beiden Dreiecke im gleichen Zeitabstand (z.B. in einer Sekunde) denselben Weg zurück.<br />
<br />
Um den Zeitfaktor zu ermitteln muss man lediglich die Zeit für einen Schleifendurchlauf ermitteln. Um eine möglichst hohe Genauigkeit zu erhalten sollte man die Zeit für den letzten Schleifendurchlauf ermitteln und im aktuellen Schleifendurchlauf verwenden.<br />
<br />
== Nachwort ==<br />
Wow... und wieder ein wenig Zeit zum Ausspannen und ein paar persönlichen Worten. Ich kann getrost nur eines sagen: Wenn Ihr nun denkt "das war alles ja sehr leicht" habt Ihr irgendetwas falsch gemacht und solltet das Tutorial noch einmal von Vorn durcharbeiten ;). Wenn Ihr jedoch leichte Kopfschmerzen verspürt (ob es nun die Matrizen sind oder der fiese Elementarbereich *sg*...) so habt Ihr alles richtig gemacht ;).<br />
<br />
Matrizen sind vor allem vielen Einsteigern sehr fremd. "Übung macht den Meister". Versucht Euch doch einfach mal die eine oder andere Aufgabe selbst zu stellen oder unser "Sonnensystem" selbst einmal nachzuschreiben. Wenn Ihr glaubt, dass das Ganze ganz nett geworden ist, dann schickt es mir doch einfach mal zu. Ich freue mich immer darüber zu sehen, was für Früchte meine Tutorials tragen. *lach* Und wenn sich plötzlich alles um die Erde dreht, dann habe ich a) das Gefühl, dass die Kenntnisse über das Sonnensystem nicht mehr ganz up to date sind oder b) ich einen ziemlich fatalen Fehler beim Erklären gemacht habe :->.<br />
<br />
Schreckt auch bitte nicht davor zurück, eine Frage diesbezüglich im Forum zu stellen. Es ist keine Schande, mit einer Matrix Probleme zu haben und vor allem die Erfahrenen unter uns, sollten nur zu gut wissen, was alles die Ursache dafür sein kann, wenn man plötzlich gar nichts mehr auf dem Screen sieht :).<br />
<br />
Wir selbst werden uns in den folgenden Kapiteln noch etwas intensiver mit den Matrizen beschäftigen und unter anderem die eine oder andere interessante Spielerei zeigen!<br />
<br />
'''Euer'''<br><br />
'''Phobeus'''<br />
<br />
<br />
== Anhang ==<br />
<br />
[http://www.pixelpracht.net Pixelpracht] - Hier könnt Ihr das Programm "Matrix Control" finden<br />
<br />
<br />
{{TUTORIAL_NAVIGATION | [[Tutorial_lektion2]] | [[Tutorial_lektion4]]}}<br />
[[Kategorie:Tutorial|Lektion3]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14569Tutorial SDL Einstieg2005-11-26T21:22:20Z<p>Akira: /* Nachwort */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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.<br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht, schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln, um was für eine Nachricht es sich handelt. In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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. <br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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. Wir müssen uns also darum kümmern, dass unser Fenster die neue Größe erhält. Die Funktion, die wir dafür verwenden müssen, kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen, die wir vom Event erhalten haben. Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist. (Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben, dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL? Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
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. <br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14568Tutorial SDL Einstieg2005-11-26T21:18:31Z<p>Akira: /* SDL - Simple Directmedia Layer */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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.<br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht, schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln, um was für eine Nachricht es sich handelt. In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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. <br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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. Wir müssen uns also darum kümmern, dass unser Fenster die neue Größe erhält. Die Funktion, die wir dafür verwenden müssen, kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen, die wir vom Event erhalten haben. Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist. (Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben, dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL? Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
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. <br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14567Tutorial SDL Einstieg2005-11-26T21:12:24Z<p>Akira: /* Die Idee */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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.<br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht,schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln,um was für eine Nachricht es sich handelt.In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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.<br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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.Wir müssen uns also darum kümmern,dass unser Fenster die neue Größe erhält.Die Funktion die wir dafür verwenden müssen,kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen,die wir vom Event erhalten haben.Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist.(Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben,dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL?Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
Ich weis 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.<br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14566Tutorial SDL Einstieg2005-11-26T21:10:41Z<p>Akira: /* OpenGL Initalisierung */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
Unser Hauptprogramm initalisiert SDL,danach OpenGL,paßt 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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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. <br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht,schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln,um was für eine Nachricht es sich handelt.In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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.<br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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.Wir müssen uns also darum kümmern,dass unser Fenster die neue Größe erhält.Die Funktion die wir dafür verwenden müssen,kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen,die wir vom Event erhalten haben.Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist.(Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben,dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL?Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
Ich weis 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.<br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14565Tutorial SDL Einstieg2005-11-26T21:09:00Z<p>Akira: /* Vorwort */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
Unser Hauptprogramm initalisiert SDL,danach OpenGL,paßt 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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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. <br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht,schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln,um was für eine Nachricht es sich handelt.In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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.<br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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.Wir müssen uns also darum kümmern,dass unser Fenster die neue Größe erhält.Die Funktion die wir dafür verwenden müssen,kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen,die wir vom Event erhalten haben.Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist.(Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben,dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL?Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
Ich weis 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.<br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Pathfinding&diff=14564Tutorial Pathfinding2005-11-26T21:07:43Z<p>Akira: /* Die eigentliche Wegfindungs-Routine */</p>
<hr />
<div>Wilkommen zu meinen Tutorial zum Pathfinding.<br />
<br />
== Geeignete Welten ==<br />
<br />
Diese Methode geht davon aus das sich unsere virtuelle Welt aus lauter Quadraten zusammensetzt.<br />
Jedes dieser Felder wird für die Wegberechnung erstmal vereinfacht, in dem davon ausgegangen wird,<br />
das ein Feld entweder begehbar ist, oder nicht.<br />
(Oder sogar Felder haben die zwar begehbar sind aber gemieden werden sollten.)<br />
<br />
Nun wird weiter davon ausgegangen, dass eine Einheit von einem Feld nur in 8 (bzw. 4) Richtungen, nämlich in die 8 (4) benachbarten Felder bewegen kann.<br />
<br />
[[Bild:Tutorial_pathfinding_BewegungsRichtungen.png]]<br />
<br />
Von dem Feld kann sich die Einheit logischerweise weiter in ein nächstes Feld bewegen, allerdings - wie schon erklärt - immer nur in 8 Richtungen.<br />
Wer sich so manches 2D Strategie-Spiel genau anschaut dem fällt sicher auf, dass es dort genauso ist.<br />
<br />
Wir wollen zum Beispiel aus einer solchen Karte,<br />
<br />
[[Bild:Tutorial_pathfinding_Beispiel1-Spiel.png]]<br />
<br />
eine "'''PathMap'''" erzeugen,<br />
<br />
[[Bild:Tutorial_pathfinding_Beispiel1-Pathmap.png]]<br />
<br />
um zu wissen, wie das Objekt auf die andere Seite kommt.<br />
<br />
Wichtige Tastur Befehle des [http://www.delphigl.com/res/tutorials/pathfinding/http://www.delphigl.com/do_download.php?f=5225 Beispiel Programmes(mit Quelltext)]:<br />
<br />
*'''F1''': Pathmap vor dem Füllen anzeigen<br />
*'''F2''': Pathmap anzeigen<br />
*'''F3''': Draufsicht mit Gitter;<br />
<br />
Aufbau des Beispiel-Programmes:<br />
<br />
Alles wesentliche, bis auf das Pathfinding, befindet sich in der SpielFeldUnit.<br />
Hier haben wir unser Feld definiert.<br />
<br />
<pascal><br />
TPlayer=(plnone,pl1,pl2,plNeutral);<br />
<br />
TFeld= object<br />
public<br />
Owner:TPlayer;<br />
end;<br />
</pascal><br />
<br />
Und unser gesamtes Spielfeld, welches einen zweifachen Array davon enthält.<br />
<br />
<pascal><br />
TSpielFeld=class(TPersistent)<br />
private<br />
FWidth:Word;<br />
FHeight:Word;<br />
procedure ZeichneSpielFeld;<br />
procedure ZeichneEinheiten;<br />
public<br />
Feld: array of array of TFeld;<br />
Einheit:array of TEinheit;<br />
SelectedUnitIndex:Integer;<br />
function ErstelleEinheit(const EinheitenTyp:TEinheitenTyp;const NewOwner:TPlayer;const XPos,YPos:Word):Boolean;<br />
</pascal><br />
<br />
Die Größe davon spiegeln die Variablen FWidth und FHeight wider.<br />
Außerdem sind hier, wie man sehen kann, ebenfalls unsere Einheiten gespeichert.<br />
Die Variable SelectedUnitIndex gibt an, welche davon gerade ausgewählt wurde.<br />
<br />
<pascal><br />
procedure Draw;<br />
procedure SetSize(NewWidth,NewHeight:Word);<br />
procedure Update;<br />
</pascal><br />
<br />
SetSize benutzt man um die Größe zu ändern, dabei werden allerdings alle Einheiten gelöscht.<br />
Ruft man Draw auf so wird ZeichneSpielFeld und ZeichneEinheiten aufgerufen um alles zu zeichnen.<br />
Der regelmäßge Aufruf von Update sorgt dafür, das überhaupt Bewegung ins Spiel kommt.<br />
<br />
<pascal><br />
procedure Save(FileName:String);<br />
procedure Load(FileName:String);<br />
procedure FloodFill(Player:TPlayer);<br />
procedure OnSelection(Selected: Integer;Button: TMouseButton);<br />
</pascal><br />
<br />
FloodFill stellt den Besitzer aller Felder auf den angegebenen Spieler, <br />
und OnSelection wertet Selections-Ereignisse aus.<br />
<br />
<pascal><br />
TBefehlsTyp=(BT_STAND,BT_MOVE);<br />
TEinheitenTyp=Word;<br />
<br />
TAnimation=record<br />
Pos:Word;<br />
Length:Word;<br />
Typ:TAnimationType;<br />
end; <br />
<br />
TBefehl=record<br />
Case Typ:TBefehlsTyp of<br />
BT_STAND:();<br />
BT_MOVE:(X,Y:Word);<br />
end;<br />
<br />
TEinheit=class<br />
public<br />
Ani:TAnimation;<br />
Befehl:TBefehl;<br />
X,Y:Word;<br />
Owner:TPlayer;<br />
Typ:TEinheitenTyp;<br />
constructor Create(VomTyp:TEinheitenTyp;NewOwner:TPlayer;XPos,YPos:Word);overload;<br />
Constructor Create;overload;<br />
procedure Draw;<br />
procedure Update;<br />
function GetRealX:Double;<br />
function GetRealY:Double;<br />
end;<br />
</pascal><br />
<br />
Eine Einheit ist normalerweise animiert und führt irgend eine Animation aus.<br />
Alle Daten, wie eine Einheit gerade zu Zeichnen(draw) ist, sind hier verwahrt.<br />
Die Funktionen GetRealX und GetRealY sorgen dafür, daß die Einheit abhängig von ihrer Animation und groben Position, an der richtigen Stelle gezeichnet wird.<br />
Die Funktion Update einer Einheit ist die Wichtigste, da sie regelt, welche Aktion und Animation die Einheit als nächstes machen soll.<br />
<br />
<pascal><br />
procedure TEinheit.Update;<br />
begin<br />
Inc(Ani.Pos);<br />
if not((Ani.Pos >= Ani.Length) or (Ani.Typ = ANI_STAND))then exit;<br />
</pascal><br />
<br />
Als erstes wird die Animations-Phase 1 weiter gestellt.<br />
Weitergemacht wird nur, wenn die Einheit entweder eine Animation abgeschlossen hat, oder nichts zu tun hat.<br />
<br />
<pascal><br />
case Befehl.Typ of<br />
BT_STAND:<br />
begin<br />
if (Ani.Pos >= Ani.Length) then Ani.Pos := 0;<br />
Ani.Typ := ANI_STAND;<br />
Ani.Length := 1000;<br />
end;<br />
</pascal><br />
<br />
Wenn die Einheit wirklich stehen soll, wird höchstens die Animations-Position mal zurückgesetzt.<br />
<pascal><br />
BT_MOVE:<br />
begin<br />
if (X = Befehl.x) and (Y = Befehl.Y) then<br />
begin //Ziel erreicht<br />
Befehl.typ := BT_STAND ;<br />
Ani.Pos := 0;<br />
Ani.Typ := ANI_STAND;<br />
Ani.length := 1000;<br />
exit;<br />
end;<br />
</pascal><br />
<br />
Wenn die Einheit den Befehl hat sich zu bewegen, aber ihr Ziel schon erreicht hat, dann erhält sie Befehl zu stehen und wir sind fertig.<br />
<br />
<pascal><br />
Ani.Pos := 0;<br />
Ani.Typ :=FindPath(Befehl.X,Befehl.Y,self); ;//Hier muss der variante Teil hin<br />
Ani.Length := 10;<br />
case Ani.Typ of<br />
ANI_MOVE_LEFT : X := X-1;<br />
ANI_MOVE_Right: X := X+1;<br />
ANI_MOVE_UP : Y := Y+1;<br />
ANI_MOVE_DOWN : Y := Y-1;<br />
<br />
ANI_MOVE_LEFTDOWN: begin X:= X-1; Y:= Y-1 end;<br />
ANI_MOVE_RIGHTDOWN: begin X:= X+1; Y:= Y-1 end;<br />
ANI_MOVE_LEFTUP: begin X:= X-1; Y:= Y+1 end;<br />
ANI_MOVE_RIGHTUP: begin X:= X+1; Y:= Y+1 end;<br />
end;<br />
if SpielFeld.Feld[X,Y].Owner <> Owner then Ani.Length := Ani.Length*LangsamkeitsFaktor;<br />
end;<br />
end;<br />
end;<br />
</pascal><br />
<br />
Nun sind wir soweit gekommen das wir unser Einheit eine Animation zuweisen wollen, aber noch nicht wissen welche das sein soll.<br />
Daher rufen wir die function FindPath aus der Pathfinding Unit auf.<br />
<br />
==Die eigentliche Wegfindungs-Routine==<br />
<br />
===Vorbereiten der "Pathmap"-Variable===<br />
<br />
Um dem Computer unser Level verständlich zu machen, wird in diese Variable:<br />
<br />
<pascal><br />
Pathmap: array of array of Integer;<br />
</pascal><br />
<br />
unser gesamtes Level übertragen.<br />
<br />
Dabei beschreiben folgende Konstanten die einzelnen Felder.<br />
<br />
<pascal><br />
const<br />
Feld_ist_Ziel= 0;<br />
Feld_ist_gesperrt= -1;<br />
Feld_ist_frei= -8;<br />
Feld_ist_feindlich =-9;<br />
Feld_ist_StartFeld= -10;<br />
</pascal><br />
Unsere Pathmap wird allerdings um 1 Feld in jede Richtung größer als unser SpielFeld sein.<br />
Dieser äußere Rand wird eine Art Abgrenzung für unser SpielFeld.<br />
<br />
<pascal><br />
SetLength(Pathmap ,SpielFeld.width +2,SpielFeld.height +2);<br />
for X := 0 to SpielFeld.Width +1 do<br />
begin<br />
Pathmap[X,0] := Feld_ist_Gesperrt;<br />
Pathmap[X,SpielFeld.Height+1] := Feld_ist_Gesperrt;<br />
end;<br />
for Y := 0 to SpielFeld.Height +1 do<br />
begin<br />
Pathmap[0,Y] := Feld_ist_Gesperrt;<br />
Pathmap[SpielFeld.width+1,Y] := Feld_ist_Gesperrt;<br />
end;<br />
</pascal><br />
<br />
Dann müssen wir natürlich noch unsere Felder eintragen.<br />
<br />
<pascal><br />
for X := 0 to SpielFeld.Width-1 do<br />
for Y := 0 to SpielFeld.Height-1 do<br />
begin<br />
if SpielFeld.Feld[X,Y].Owner = plneutral then Pathmap[X+1,Y+1] := Feld_ist_Gesperrt<br />
else if SpielFeld.Feld[X,Y].Owner = TEinheit(Einheit).owner then<br />
Pathmap[X+1,Y+1] := Feld_ist_Frei<br />
else<br />
Pathmap[X+1,Y+1] := Feld_ist_feindlich;<br />
end;<br />
</pascal><br />
<br />
Felder vom Spieler plneutral sollen in meinen Beispiel-Programm gesperrt sein.<br />
Andernfalls wird überprüft ob das Feld vom Feind oder von uns kontrolliert wird.<br />
<br />
Natürlich dürfen alle Felder, auf denen schon Einheiten stehen, nicht betreten werden.<br />
<br />
<pascal><br />
for X := 0 to High(SpielFeld.Einheit) do<br />
Pathmap[SpielFeld.Einheit[X].X+1,SpielFeld.Einheit[X].Y+1] := Feld_ist_Gesperrt ;<br />
</pascal><br />
<br />
Nun müssen wir noch die Einheit eintragen, für die wir den Weg berechnen.<br />
<br />
<pascal><br />
Pathmap[TEinheit(Einheit).X+1,TEinheit(Einheit).Y+1] := Feld_ist_StartFeld;<br />
</pascal><br />
<br />
Jetzt müssen wir überprüfen, ob die Einheit vielleicht schon direkt vor dem Ziel steht, welches aber besetzt ist. Sollte dies so sein so sind wir fertig da wir nichts zu berechen haben. <br />
<br />
<pascal><br />
if (Pathmap[ZielX+1,ZielY+1] = Feld_ist_Gesperrt)<br />
and (Betrag(TEinheit(Einheit).X - ZielX) <= 1 )<br />
and (Betrag(TEinheit(Einheit).Y -ZielY) <=1) then<br />
begin<br />
result := ANI_STAND;<br />
exit;<br />
end;<br />
</pascal><br />
<br />
Ansonsten tragen wir unser Ziel in die Pathmap ein.<br />
<pascal><br />
Pathmap[ZielX+1,ZielY+1] := Feld_ist_Ziel ;<br />
</pascal><br />
<br />
Nun haben wir unsere Pathmap vorbereitet.<br />
Sie könnte nun so aussehen:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel1-NewPathmap.png]]<br />
<br />
=== Füllen der "Pathmap" ===<br />
<br />
<pascal><br />
var<br />
Pathmap: array of array of Integer;<br />
x,y:Integer;<br />
Weg:Integer;<br />
LX,HX:Integer;<br />
LY,HY:Integer;<br />
Fertig:Boolean ;<br />
</pascal><br />
<br />
Wir starten eine Schleife in der die Pathmap gefüllt wird:<br />
<br />
<pascal><br />
Weg:= -1;//Weg+1=0<br />
Fertig := false;<br />
while not Fertig and (Weg < MAX_ENTFERNUNG) do<br />
begin<br />
Weg:=Weg+1;<br />
</pascal><br />
<br />
Es ist sinnvoll, mit einen kleinen Füll-Breich anzufangen. Dieser wird durch LX, HX, LY, HY begrenzt.<br />
<br />
<pascal><br />
LX := ZielX+1 -Weg-1;<br />
HX := ZielX+1 +Weg+1;<br />
LY := ZielY+1 -Weg-1;<br />
HY := ZielY+1 +Weg+1;<br />
<br />
if LX < 1 then LX := 1;<br />
if LY < 1 then LY := 1;<br />
if HX > SpielFeld.Width then HX :=SpielFeld.Width;<br />
if HY > SpielFeld.Height then HY := SpielFeld.Height;<br />
</pascal><br />
<br />
Nun prüfen wir jedes Feld im Füll-Bereich, ob es an ein Feld angrenzt, was den aktuellen Weg-Wert hat.<br />
Wir brauchen übrigens Felder die gesperrt sind oder bereits einen positven Wert besitzen nicht mehr beachten.<br />
<br />
<pascal><br />
for X:= LX to HX do<br />
for Y:= LY to HY do<br />
if Pathmap[X,Y] <= Feld_ist_frei then<br />
{Wenn das Feld noch nicht berechnet und begehbar ist}<br />
if (Pathmap[x-1,y]=Weg)<br />
or (Pathmap[x+1,y]=Weg)<br />
or (Pathmap[x,y-1]=Weg)<br />
or (Pathmap[x,y+1]=Weg)<br />
<br />
or (Pathmap[x-1,y-1]=Weg)<br />
or (Pathmap[x+1,y-1]=Weg)<br />
or (Pathmap[x-1,y+1]=Weg)<br />
or (Pathmap[x+1,y+1]=Weg) then<br />
</pascal><br />
<br />
Normalerweise würden wir jetzt jedes Feld (außer es ist das Ziel) um 1 erhöhen. Da wir aber auch feindliche Felder haben, müssen wir diese auch noch beachten:<br />
<br />
<pascal><br />
case Pathmap[X,Y] of<br />
<br />
Feld_ist_feindlich:Pathmap[X,Y]:= Weg + LangsamkeitsFaktor;<br />
Feld_ist_StartFeld:<br />
begin<br />
Fertig := true;<br />
end;<br />
else<br />
{Feld_ist_frei:}<br />
Pathmap[X,Y]:= Weg +1;<br />
end;<br />
end;<br />
</pascal><br />
<br />
Ein Feld, welches den Wert der Variable Weg + 1 hat, <br />
wird logischerweise im nächsten Durchlauf der Schleife den Wert der Variable Weg haben.<br />
Es wird dann die Bedingung des angrenzenden Feldes erfüllen.<br />
Dadurch ist auch klar, warum das auf feindlichem Gebiet drei mal so lange dauert.... es dauert einfach 3 Runden statt 1 bis das Feld den gleichen Wert wie die Weg-Variable hat<br />
Es entsteht somit eine Ausdehnung der Zahlen um das ZielFeld (Wert 0) herum.<br />
Jedes Feld, das so eine postive Zahl erhalten hat, bescheibt somit die Entfernung zum Ziel.<br />
Machen wir das so lange, bis wir das "StartFeld" mit einer Zahl überschreiben würden, so wäre diese Zahl die Entfernung zum Ziel.<br />
<br />
Versuch doch mal sofort zu sagen, ob sich die Einheit oben oder unten rum bewegen soll:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel2-Spiel.png]]<br />
<br />
Dank unser Pathmap ist viel einfacher wenn man weis wie es geht:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel2-Pathmap.png]]<br />
<br />
=== Richtige Animation auswählen ===<br />
<br />
Nun braucht man sich nur noch das Feld suchen, welches an StartFeld angrenzt, den aktuellen Wert von Weg hat und entspechend die Animation wählen (das Feld, welches die Überschreibung vom Ziel-Feld veranlaßt hätte).<br />
<br />
<pascal><br />
X:= TEinheit(Einheit).X +1;<br />
Y:= TEinheit(Einheit).Y +1;<br />
<br />
if (Pathmap[X+1,Y] = Weg) then Result := ANI_MOVE_RIGHT<br />
else if (Pathmap[X-1,Y] = Weg) then Result := ANI_MOVE_LEFT<br />
else if (Pathmap[X,Y+1] = Weg) then Result := ANI_MOVE_UP<br />
else if (Pathmap[X,Y-1] = Weg) then Result := ANI_MOVE_DOWN<br />
<br />
else if (Pathmap[X-1,Y-1] = Weg) then Result := ANI_MOVE_LEFTDOWN<br />
else if (Pathmap[X+1,Y-1] = Weg) then Result := ANI_MOVE_RIGHTDOWN<br />
else if (Pathmap[X-1,Y+1] = Weg) then Result := ANI_MOVE_LEFTUP<br />
else if (Pathmap[X+1,Y+1] = Weg) then Result := ANI_MOVE_RIGHTUP<br />
<br />
else Result := ANI_STAND;<br />
end;<br />
</pascal><br />
<br />
<br />
Ich hoffe, mein Tutorial hat euch gefallen und ihr könnt damit irgendetwas anfangen.<br />
<br />
Euer [[Benutzer:Flo|Flo]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Pathfinding&diff=14563Tutorial Pathfinding2005-11-26T21:01:30Z<p>Akira: /* Geeignete Welten */</p>
<hr />
<div>Wilkommen zu meinen Tutorial zum Pathfinding.<br />
<br />
== Geeignete Welten ==<br />
<br />
Diese Methode geht davon aus das sich unsere virtuelle Welt aus lauter Quadraten zusammensetzt.<br />
Jedes dieser Felder wird für die Wegberechnung erstmal vereinfacht, in dem davon ausgegangen wird,<br />
das ein Feld entweder begehbar ist, oder nicht.<br />
(Oder sogar Felder haben die zwar begehbar sind aber gemieden werden sollten.)<br />
<br />
Nun wird weiter davon ausgegangen, dass eine Einheit von einem Feld nur in 8 (bzw. 4) Richtungen, nämlich in die 8 (4) benachbarten Felder bewegen kann.<br />
<br />
[[Bild:Tutorial_pathfinding_BewegungsRichtungen.png]]<br />
<br />
Von dem Feld kann sich die Einheit logischerweise weiter in ein nächstes Feld bewegen, allerdings - wie schon erklärt - immer nur in 8 Richtungen.<br />
Wer sich so manches 2D Strategie-Spiel genau anschaut dem fällt sicher auf, dass es dort genauso ist.<br />
<br />
Wir wollen zum Beispiel aus einer solchen Karte,<br />
<br />
[[Bild:Tutorial_pathfinding_Beispiel1-Spiel.png]]<br />
<br />
eine "'''PathMap'''" erzeugen,<br />
<br />
[[Bild:Tutorial_pathfinding_Beispiel1-Pathmap.png]]<br />
<br />
um zu wissen, wie das Objekt auf die andere Seite kommt.<br />
<br />
Wichtige Tastur Befehle des [http://www.delphigl.com/res/tutorials/pathfinding/http://www.delphigl.com/do_download.php?f=5225 Beispiel Programmes(mit Quelltext)]:<br />
<br />
*'''F1''': Pathmap vor dem Füllen anzeigen<br />
*'''F2''': Pathmap anzeigen<br />
*'''F3''': Draufsicht mit Gitter;<br />
<br />
Aufbau des Beispiel-Programmes:<br />
<br />
Alles wesentliche, bis auf das Pathfinding, befindet sich in der SpielFeldUnit.<br />
Hier haben wir unser Feld definiert.<br />
<br />
<pascal><br />
TPlayer=(plnone,pl1,pl2,plNeutral);<br />
<br />
TFeld= object<br />
public<br />
Owner:TPlayer;<br />
end;<br />
</pascal><br />
<br />
Und unser gesamtes Spielfeld, welches einen zweifachen Array davon enthält.<br />
<br />
<pascal><br />
TSpielFeld=class(TPersistent)<br />
private<br />
FWidth:Word;<br />
FHeight:Word;<br />
procedure ZeichneSpielFeld;<br />
procedure ZeichneEinheiten;<br />
public<br />
Feld: array of array of TFeld;<br />
Einheit:array of TEinheit;<br />
SelectedUnitIndex:Integer;<br />
function ErstelleEinheit(const EinheitenTyp:TEinheitenTyp;const NewOwner:TPlayer;const XPos,YPos:Word):Boolean;<br />
</pascal><br />
<br />
Die Größe davon spiegeln die Variablen FWidth und FHeight wider.<br />
Außerdem sind hier, wie man sehen kann, ebenfalls unsere Einheiten gespeichert.<br />
Die Variable SelectedUnitIndex gibt an, welche davon gerade ausgewählt wurde.<br />
<br />
<pascal><br />
procedure Draw;<br />
procedure SetSize(NewWidth,NewHeight:Word);<br />
procedure Update;<br />
</pascal><br />
<br />
SetSize benutzt man um die Größe zu ändern, dabei werden allerdings alle Einheiten gelöscht.<br />
Ruft man Draw auf so wird ZeichneSpielFeld und ZeichneEinheiten aufgerufen um alles zu zeichnen.<br />
Der regelmäßge Aufruf von Update sorgt dafür, das überhaupt Bewegung ins Spiel kommt.<br />
<br />
<pascal><br />
procedure Save(FileName:String);<br />
procedure Load(FileName:String);<br />
procedure FloodFill(Player:TPlayer);<br />
procedure OnSelection(Selected: Integer;Button: TMouseButton);<br />
</pascal><br />
<br />
FloodFill stellt den Besitzer aller Felder auf den angegebenen Spieler, <br />
und OnSelection wertet Selections-Ereignisse aus.<br />
<br />
<pascal><br />
TBefehlsTyp=(BT_STAND,BT_MOVE);<br />
TEinheitenTyp=Word;<br />
<br />
TAnimation=record<br />
Pos:Word;<br />
Length:Word;<br />
Typ:TAnimationType;<br />
end; <br />
<br />
TBefehl=record<br />
Case Typ:TBefehlsTyp of<br />
BT_STAND:();<br />
BT_MOVE:(X,Y:Word);<br />
end;<br />
<br />
TEinheit=class<br />
public<br />
Ani:TAnimation;<br />
Befehl:TBefehl;<br />
X,Y:Word;<br />
Owner:TPlayer;<br />
Typ:TEinheitenTyp;<br />
constructor Create(VomTyp:TEinheitenTyp;NewOwner:TPlayer;XPos,YPos:Word);overload;<br />
Constructor Create;overload;<br />
procedure Draw;<br />
procedure Update;<br />
function GetRealX:Double;<br />
function GetRealY:Double;<br />
end;<br />
</pascal><br />
<br />
Eine Einheit ist normalerweise animiert und führt irgend eine Animation aus.<br />
Alle Daten, wie eine Einheit gerade zu Zeichnen(draw) ist, sind hier verwahrt.<br />
Die Funktionen GetRealX und GetRealY sorgen dafür, daß die Einheit abhängig von ihrer Animation und groben Position, an der richtigen Stelle gezeichnet wird.<br />
Die Funktion Update einer Einheit ist die Wichtigste, da sie regelt, welche Aktion und Animation die Einheit als nächstes machen soll.<br />
<br />
<pascal><br />
procedure TEinheit.Update;<br />
begin<br />
Inc(Ani.Pos);<br />
if not((Ani.Pos >= Ani.Length) or (Ani.Typ = ANI_STAND))then exit;<br />
</pascal><br />
<br />
Als erstes wird die Animations-Phase 1 weiter gestellt.<br />
Weitergemacht wird nur, wenn die Einheit entweder eine Animation abgeschlossen hat, oder nichts zu tun hat.<br />
<br />
<pascal><br />
case Befehl.Typ of<br />
BT_STAND:<br />
begin<br />
if (Ani.Pos >= Ani.Length) then Ani.Pos := 0;<br />
Ani.Typ := ANI_STAND;<br />
Ani.Length := 1000;<br />
end;<br />
</pascal><br />
<br />
Wenn die Einheit wirklich stehen soll, wird höchstens die Animations-Position mal zurückgesetzt.<br />
<pascal><br />
BT_MOVE:<br />
begin<br />
if (X = Befehl.x) and (Y = Befehl.Y) then<br />
begin //Ziel erreicht<br />
Befehl.typ := BT_STAND ;<br />
Ani.Pos := 0;<br />
Ani.Typ := ANI_STAND;<br />
Ani.length := 1000;<br />
exit;<br />
end;<br />
</pascal><br />
<br />
Wenn die Einheit den Befehl hat sich zu bewegen, aber ihr Ziel schon erreicht hat, dann erhält sie Befehl zu stehen und wir sind fertig.<br />
<br />
<pascal><br />
Ani.Pos := 0;<br />
Ani.Typ :=FindPath(Befehl.X,Befehl.Y,self); ;//Hier muss der variante Teil hin<br />
Ani.Length := 10;<br />
case Ani.Typ of<br />
ANI_MOVE_LEFT : X := X-1;<br />
ANI_MOVE_Right: X := X+1;<br />
ANI_MOVE_UP : Y := Y+1;<br />
ANI_MOVE_DOWN : Y := Y-1;<br />
<br />
ANI_MOVE_LEFTDOWN: begin X:= X-1; Y:= Y-1 end;<br />
ANI_MOVE_RIGHTDOWN: begin X:= X+1; Y:= Y-1 end;<br />
ANI_MOVE_LEFTUP: begin X:= X-1; Y:= Y+1 end;<br />
ANI_MOVE_RIGHTUP: begin X:= X+1; Y:= Y+1 end;<br />
end;<br />
if SpielFeld.Feld[X,Y].Owner <> Owner then Ani.Length := Ani.Length*LangsamkeitsFaktor;<br />
end;<br />
end;<br />
end;<br />
</pascal><br />
<br />
Nun sind wir soweit gekommen das wir unser Einheit eine Animation zuweisen wollen, aber noch nicht wissen welche das sein soll.<br />
Daher rufen wir die function FindPath aus der Pathfinding Unit auf.<br />
<br />
==Die eigentliche Wegfindungs-Routine==<br />
<br />
===Vorbereiten der "Pathmap"-Variable===<br />
<br />
Um dem Computer unser Level verständlich zu machen, wird in diese Variable:<br />
<br />
<pascal><br />
Pathmap: array of array of Integer;<br />
</pascal><br />
<br />
unser gesamtes Level übertragen.<br />
<br />
Dabei beschreiben folgende Konstanten die einzelen Felder.<br />
<br />
<pascal><br />
const<br />
Feld_ist_Ziel= 0;<br />
Feld_ist_gesperrt= -1;<br />
Feld_ist_frei= -8;<br />
Feld_ist_feindlich =-9;<br />
Feld_ist_StartFeld= -10;<br />
</pascal><br />
Unsere Pathmap wird allerdings um 1 Feld in jede Richtung größer als unser SpielFeld sein.<br />
Dieser äußere Rand wird eine Art Abgrenzung für unser SpielFeld.<br />
<br />
<pascal><br />
SetLength(Pathmap ,SpielFeld.width +2,SpielFeld.height +2);<br />
for X := 0 to SpielFeld.Width +1 do<br />
begin<br />
Pathmap[X,0] := Feld_ist_Gesperrt;<br />
Pathmap[X,SpielFeld.Height+1] := Feld_ist_Gesperrt;<br />
end;<br />
for Y := 0 to SpielFeld.Height +1 do<br />
begin<br />
Pathmap[0,Y] := Feld_ist_Gesperrt;<br />
Pathmap[SpielFeld.width+1,Y] := Feld_ist_Gesperrt;<br />
end;<br />
</pascal><br />
<br />
Dann müssen wir natürlich noch unsere Felder eintragen.<br />
<br />
<pascal><br />
for X := 0 to SpielFeld.Width-1 do<br />
for Y := 0 to SpielFeld.Height-1 do<br />
begin<br />
if SpielFeld.Feld[X,Y].Owner = plneutral then Pathmap[X+1,Y+1] := Feld_ist_Gesperrt<br />
else if SpielFeld.Feld[X,Y].Owner = TEinheit(Einheit).owner then<br />
Pathmap[X+1,Y+1] := Feld_ist_Frei<br />
else<br />
Pathmap[X+1,Y+1] := Feld_ist_feindlich;<br />
end;<br />
</pascal><br />
<br />
Felder vom Spieler plneutral sollen in meinen Beispiel-Programm gesperrt sein.<br />
Andernfalls wird überprüft ob dass Feld vom Feind oder von uns Kontrolliert wird.<br />
<br />
Natürlich dürfen alle Felder auf denen schon Einheiten stehen nicht betreten werden.<br />
<br />
<pascal><br />
for X := 0 to High(SpielFeld.Einheit) do<br />
Pathmap[SpielFeld.Einheit[X].X+1,SpielFeld.Einheit[X].Y+1] := Feld_ist_Gesperrt ;<br />
</pascal><br />
<br />
Nun müssen wir noch die Einheit eintragen, für die wir den Weg berechnen.<br />
<br />
<pascal><br />
Pathmap[TEinheit(Einheit).X+1,TEinheit(Einheit).Y+1] := Feld_ist_StartFeld;<br />
</pascal><br />
<br />
Jetzt müssen wir überprüfen, ob die Einheit vielleicht schon direkt vor dem Ziel, steht welches aber besetzt ist. Sollte dies so sein so sind wir fertig da wir nichts zu berechen haben. <br />
<br />
<pascal><br />
if (Pathmap[ZielX+1,ZielY+1] = Feld_ist_Gesperrt)<br />
and (Betrag(TEinheit(Einheit).X - ZielX) <= 1 )<br />
and (Betrag(TEinheit(Einheit).Y -ZielY) <=1) then<br />
begin<br />
result := ANI_STAND;<br />
exit;<br />
end;<br />
</pascal><br />
<br />
Ansonsten tragen wir unser Ziel in die Pathmap ein.<br />
<pascal><br />
Pathmap[ZielX+1,ZielY+1] := Feld_ist_Ziel ;<br />
</pascal><br />
<br />
Nun haben wir unsere Pathmap vorbereitet.<br />
Sie könnte nun so aussehen:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel1-NewPathmap.png]]<br />
<br />
=== Füllen der "Pathmap" ===<br />
<br />
<pascal><br />
var<br />
Pathmap: array of array of Integer;<br />
x,y:Integer;<br />
Weg:Integer;<br />
LX,HX:Integer;<br />
LY,HY:Integer;<br />
Fertig:Boolean ;<br />
</pascal><br />
<br />
Wir starten eine Schleife in der die Pathmap gefüllt wird:<br />
<br />
<pascal><br />
Weg:= -1;//Weg+1=0<br />
Fertig := false;<br />
while not Fertig and (Weg < MAX_ENTFERNUNG) do<br />
begin<br />
Weg:=Weg+1;<br />
</pascal><br />
<br />
Es ist Sinnvoll mit einen kleinen Füll-Breich anzufangen dieser wird durch LX, HX, LY, HY begrenzt.<br />
<br />
<pascal><br />
LX := ZielX+1 -Weg-1;<br />
HX := ZielX+1 +Weg+1;<br />
LY := ZielY+1 -Weg-1;<br />
HY := ZielY+1 +Weg+1;<br />
<br />
if LX < 1 then LX := 1;<br />
if LY < 1 then LY := 1;<br />
if HX > SpielFeld.Width then HX :=SpielFeld.Width;<br />
if HY > SpielFeld.Height then HY := SpielFeld.Height;<br />
</pascal><br />
<br />
Nun prüfen wir jedes Feld im Füll-Bereich ob es an ein Feld angrenzt was den aktuellen Weg-Wert hat.<br />
Wir brauchen übrigens Felder die Gesperrt sind, oder schon einen positven besitzen nicht mehr beachten.<br />
<br />
<pascal><br />
for X:= LX to HX do<br />
for Y:= LY to HY do<br />
if Pathmap[X,Y] <= Feld_ist_frei then<br />
{Wenn das Feld noch nicht berechnet und begehbar ist}<br />
if (Pathmap[x-1,y]=Weg)<br />
or (Pathmap[x+1,y]=Weg)<br />
or (Pathmap[x,y-1]=Weg)<br />
or (Pathmap[x,y+1]=Weg)<br />
<br />
or (Pathmap[x-1,y-1]=Weg)<br />
or (Pathmap[x+1,y-1]=Weg)<br />
or (Pathmap[x-1,y+1]=Weg)<br />
or (Pathmap[x+1,y+1]=Weg) then<br />
</pascal><br />
<br />
Normalerweise würden wir jetzt jedes Feld(außer es ist das Ziel) um 1 erhöhen da wir aber auch feindliche Felder haben, müssen wir diese auch noch beachten:<br />
<br />
<pascal><br />
case Pathmap[X,Y] of<br />
<br />
Feld_ist_feindlich:Pathmap[X,Y]:= Weg + LangsamkeitsFaktor;<br />
Feld_ist_StartFeld:<br />
begin<br />
Fertig := true;<br />
end;<br />
else<br />
{Feld_ist_frei:}<br />
Pathmap[X,Y]:= Weg +1;<br />
end;<br />
end;<br />
</pascal><br />
<br />
Ein Feld welches den Wert der Variable Weg + 1 hat, <br />
wird logischerweise im nächsten Durchlauf der Schleife den Wert der Variable Weg haben.<br />
Es wird dann die Bedinung des angrenzenden Feldes erfüllen.<br />
Dadurch ist auch klar warum das auf feindlichen Gebiet drei mal so lange dauert.... es dauer einfach 3 Runden statt 1 bis das Feld den gleichen Wert wie die Weg-Variable hat<br />
Es entsteht somit eine Außdehnung der Zahlen um das ZielFeld(Wert 0) herum.<br />
Jedes Feld das so eine postive Zahl erhalten hat bescheibt somit die Entfernung zum Ziel.<br />
Machen wir dass so lange bis wir das "StartFeld" mit einer Zahl überschreiben würden, so wäre diese Zahl die Entfernung zum Ziel.<br />
<br />
Versuch doch mal sofort zu sagen ob die Einheit oben oder unten rum sich bewegen soll:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel2-Spiel.png]]<br />
<br />
Dank unser Pathmap ist viel einfacher wenn man weis wie es geht:<br />
<br />
[[Bild:tutorial_pathfinding_Beispiel2-Pathmap.png]]<br />
<br />
=== Richtige Animation auswählen ===<br />
<br />
Nun braucht man sich nur noch das Feld suchen welches an StartFeld angrenzt und den Aktuellen Wert von Weg hat.<br />
Und entspechend die Animation wählen.<br />
(Das Feld welches die Überschreibung vom Ziel-Feld veranlast hätte)<br />
<br />
<pascal><br />
X:= TEinheit(Einheit).X +1;<br />
Y:= TEinheit(Einheit).Y +1;<br />
<br />
if (Pathmap[X+1,Y] = Weg) then Result := ANI_MOVE_RIGHT<br />
else if (Pathmap[X-1,Y] = Weg) then Result := ANI_MOVE_LEFT<br />
else if (Pathmap[X,Y+1] = Weg) then Result := ANI_MOVE_UP<br />
else if (Pathmap[X,Y-1] = Weg) then Result := ANI_MOVE_DOWN<br />
<br />
else if (Pathmap[X-1,Y-1] = Weg) then Result := ANI_MOVE_LEFTDOWN<br />
else if (Pathmap[X+1,Y-1] = Weg) then Result := ANI_MOVE_RIGHTDOWN<br />
else if (Pathmap[X-1,Y+1] = Weg) then Result := ANI_MOVE_LEFTUP<br />
else if (Pathmap[X+1,Y+1] = Weg) then Result := ANI_MOVE_RIGHTUP<br />
<br />
else Result := ANI_STAND;<br />
end;<br />
</pascal><br />
<br />
<br />
Ich hoffe mein Tutorial, hat euch gefallen und ihr könnt damit irgendetwas anfangen.<br />
<br />
Euer [[Benutzer:Flo|Flo]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14555Tutorial SDL Einstieg2005-11-25T21:28:39Z<p>Akira: /* Die Suche nach dem wahren Pixelformat */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
Ich weis dass ich mich damit unbeliebt machen werde,aber...Was haben GLUT und DirectX gemeinsam? Na,Na! Wer weis 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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
Unser Hauptprogramm initalisiert SDL,danach OpenGL,paßt 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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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. <br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht,schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln,um was für eine Nachricht es sich handelt.In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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.<br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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.Wir müssen uns also darum kümmern,dass unser Fenster die neue Größe erhält.Die Funktion die wir dafür verwenden müssen,kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen,die wir vom Event erhalten haben.Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist.(Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben,dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL?Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
Ich weis 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.<br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14554Tutorial SDL Einstieg2005-11-25T21:26:36Z<p>Akira: /* Grafikkarten sind gar nicht so anders */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
Ich weis dass ich mich damit unbeliebt machen werde,aber...Was haben GLUT und DirectX gemeinsam? Na,Na! Wer weis 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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
Unser Hauptprogramm initalisiert SDL,danach OpenGL,paßt 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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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. <br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht,schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln,um was für eine Nachricht es sich handelt.In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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.<br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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.Wir müssen uns also darum kümmern,dass unser Fenster die neue Größe erhält.Die Funktion die wir dafür verwenden müssen,kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen,die wir vom Event erhalten haben.Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist.(Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben,dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL?Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
Ich weis 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.<br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_SDL_Einstieg&diff=14553Tutorial SDL Einstieg2005-11-25T21:25:53Z<p>Akira: /* SDL! Bitte kommen! */</p>
<hr />
<div>=SDL - Simple Directmedia Layer=<br />
==Vorwort==<br />
<br />
Ich weis dass ich mich damit unbeliebt machen werde,aber...Was haben GLUT und DirectX gemeinsam? Na,Na! Wer weis 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.<br />
<br />
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! ;)<br />
<br />
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! ;)<br />
<br />
<br />
==Initialisierung von SDL==<br />
===SDL! Bitte kommen!===<br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO );</pascal><br />
<br />
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:<br />
<br />
<pascal> SDL_Init ( SDL_INIT_VIDEO or SDL_INIT_TIMER );</pascal><br />
<br />
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!<br />
<br />
<pascal>// Initalisieren vom Simple DirectMedia Layer<br />
if ( SDL_Init( SDL_INIT_VIDEO ) &; 0 ) then<br />
begin<br />
Log.LogError('Initalisierung von SDL schlug fehl: '+SDL_GetError,'SDL_Init');<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|Quit_App}} kommen wir später. Es handelt sich dabei um eine selbst geschriebene Funktion zum Freigeben der Ressourcen.<br />
<br />
===Grafikkarten sind gar nicht so anders===<br />
<br />
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:<br />
<br />
<pascal>// Information über Grafikkarte einholen<br />
videoInfo := SDL_GetVideoInfo;<br />
if ( videoInfo = nil ) then<br />
begin<br />
Log.LogError('Grafikkarte ließ sich nicht abfragen: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
Bei VideoInfo handelt es sich um eine {{INLINE_CODE|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.<br />
<br />
===Die Suche nach dem wahren Pixelformat===<br />
<br />
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:<br />
<br />
<pascal>// Flags für den SDL-Grafikmodus setzen<br />
videoFlags := SDL_OPENGL or // OpenGL-Unterstützung aktivieren<br />
SDL_DOUBLEBUF or // Double Buffering aktivieren<br />
SDL_HWPALETTE; // Palette in Hardware speichern</pascal><br />
<br />
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 ;)<br />
<br />
<pascal>// Kann das Surface in den Speicher?<br />
if ( videoInfo.hw_available <> 0 ) then<br />
videoFlags := videoFlags or SDL_HWSURFACE<br />
else<br />
videoFlags := videoFlags or SDL_SWSURFACE;<br />
<br />
// Wird hardware blitting unterstützt?<br />
if ( videoInfo.blit_hw <> 0 ) then videoFlags := videoFlags or SDL_HWACCEL;</pascal><br />
<br />
Nun erfolgt die die Definition des PixelFormats dass für die Initialisierung von OpenGL unentbehrlich ist:<br />
<br />
<pascal>// Setzen der OpenGL-Attribute<br />
SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );<br />
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );<br />
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );</pascal><br />
<br />
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:<br />
<br />
<pascal>SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 );</pascal><br />
<br />
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:<br />
<br />
<pascal>// Fenstertitel festlegen<br />
SDL_WM_SetCaption( WINDOWS_CAPTION , nil);</pascal><br />
<br />
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:<br />
<br />
<pascal>videoflags := videoFlags or SDL_RESIZABLE; // Enable window resizing</pascal><br />
<br />
Nun haben wir alles beisammen und erzeugen unser Surface!<br />
<br />
<pascal>videoflags := // Initalisierung der Surface<br />
surface := SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,videoflags );<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Erzeugen einer OpenGL-Zeichenfläche schlug fehl: '+SDL_GetError,'SDL_Init' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|PSDL_Surface}} zurück mit der wir dann weiterarbeiten können (und auch werden) ;)<br />
<br />
<br />
==OpenGL Initalisierung==<br />
<br />
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:<br />
<br />
<pascal> // Laden und Initalisieren von OpenGL<br />
LoadOpenGL;<br />
InitOpenGL;</pascal><br />
<br />
Fertig! Schon steht nichts mehr zwischen uns und dem OpenGL-Render-Spass ;) Allerdings empfiehlt es sich immer noch einige grundlegende Dinge einzustellen, einfach weil es hübscher gerendert wird ;)<br />
<br />
<pascal> glClearColor(0.0, 0.0, 0.0, 1.0); // Bildschirm löschen (schwarz)<br />
glClearDepth(1.0); // Depth Buffer Setup<br />
glEnable(GL_DEPTH_TEST); // Aktiviert Depth Testing<br />
glDepthFunc(GL_LEQUAL); // Bestimmt den Typ des Depth Testing<br />
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);// Qualitativ bessere Koordinaten<br />
// Interpolation</pascal><br />
<br />
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:<br />
<br />
<pascal>function glResizeWindow( width : integer; height : integer ) : Boolean;<br />
begin<br />
// Verhindern von "Division by Zero"<br />
if ( height = 0 ) then height := 1;<br />
<br />
// Viewport und Projektions-Matrix aktualisieren<br />
glViewport( 0, 0, width, height );<br />
<br />
glMatrixMode( GL_PROJECTION );<br />
glLoadIdentity;<br />
gluPerspective( 45.0, width / height, 0.1, 100.0 );<br />
glMatrixMode( GL_MODELVIEW );<br />
<br />
// Rücksetzen der World-Matrix<br />
glLoadIdentity;<br />
<br />
// Vorgang erfolgreich<br />
result := true;<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );</pascal><br />
<br />
<br />
==Tag, Post!==<br />
===Die Idee===<br />
<br />
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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
end.</pascal><br />
<br />
Unser Hauptprogramm initalisiert SDL,danach OpenGL,paßt 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:<br />
<br />
<pascal>begin<br />
// Initalisierung<br />
Init_SDL;<br />
Init_OpenGL;<br />
<br />
// Anpassen der Fenstergröße<br />
glResizeWindow( SCREEN_WIDTH, SCREEN_HEIGHT );<br />
<br />
// Eintritt in Main-Loop<br />
while ( Done <> -1 ) do<br />
begin<br />
glHandleEvents;<br />
glDrawScene;<br />
end;<br />
end.</pascal><br />
<br />
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!<br />
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. <br />
<br />
===Event-Handling===<br />
<br />
Um zu begreifen wie genau eine solche Ereignis-Reaktion aussieht,schauen wir uns die Funktion {{INLINE_CODE|glHandleEvents}} etwas genauer an:<br />
<br />
<pascal>procedure glHandleEvents;<br />
var event : TSDL_Event;<br />
begin;<br />
// Verarbeiten der Events<br />
while ( SDL_PollEvent( @event ) = 1 ) do<br />
begin<br />
case event.type_ of<br />
<br />
// Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;<br />
<br />
// Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;<br />
<br />
// Fenster-Größe hat sich verändert<br />
SDL_VIDEORESIZE :<br />
begin<br />
surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;<br />
<br />
glResizeWindow( event.resize.w, event.resize.h );<br />
end;<br />
end;//case<br />
end;//while<br />
end;</pascal><br />
<br />
Mit {{INLINE_CODE|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 {{INLINE_CODE|TSDL_EVENT}} und nutzen {{INLINE_CODE|.type_}} um zu ermitteln,um was für eine Nachricht es sich handelt.In unserem Fall reagieren wir auf 3 Ereignisse.<br />
<br />
===Sein oder nicht sein...===<br />
<br />
<pascal> // Beenden der Applikation<br />
SDL_QUITEV :<br />
begin<br />
Done := -1;<br />
end;</pascal><br />
<br />
Liegt ein Ereignis vom Typ {{INLINE_CODE|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 {{INLINE_CODE|done := -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.<br />
<br />
===Tastatur-Handling===<br />
<br />
<pascal> // Taste wurde gedrückt<br />
SDL_KEYDOWN :<br />
begin<br />
glHandleKeyPress( @event.key.keysym );<br />
end;</pascal><br />
<br />
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:<br />
<br />
<pascal>procedure glHandleKeyPress( keysym : PSDL_keysym );<br />
begin;<br />
case keysym.sym of<br />
SDLK_ESCAPE : done := -1;<br />
end;<br />
end;</pascal><br />
<br />
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 {{INLINE_CODE|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 {{INLINE_CODE|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. <br />
<br />
===Eine Frage der wahren Größe===<br />
<br />
{{INLINE_CODE|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.Wir müssen uns also darum kümmern,dass unser Fenster die neue Größe erhält.Die Funktion die wir dafür verwenden müssen,kennt ihr bereits alle:<br />
<br />
<pascal> surface := SDL_SetVideoMode( event.resize.w, event.resize.h, SCREEN_BPP, videoflags );<br />
<br />
if ( surface = nil ) then<br />
begin<br />
Log.LogError('Surface bei Größenänderung verloren: '+SDL_GetError,'EVENT_RESIZE' );<br />
Quit_App;<br />
end;</pascal><br />
<br />
{{INLINE_CODE|SDL_SetVideoMode}} ein und übergeben als neue Größe die Informationen,die wir vom Event erhalten haben.Selbstverständlich kontrollieren wir danach auch noch, ob dieser Vorgang erfolgreich war und uns nicht das Surface abhanden gekommen ist.(Gruß an alle DirectX-Aqcuire-Fetichisten *g)<br />
<br />
Allerdings werden durch diese Veränderung der Zeichenfläche 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 {{INLINE_CODE|glResizeWindow}}-Funktion so schreiben,dass wir sie in einem Event wieder verwenden können.<br />
<br />
<pascal>glResizeWindow( event.resize.w, event.resize.h );</pascal><br />
<br />
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...<br />
<br />
<br />
==OpenGL?Überall gleich!==<br />
<br />
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:<br />
<br />
<pascal>procedure glDrawScene;<br />
begin<br />
// Screen- und Tiefenbuffer bereinigen<br />
glClear( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT );<br />
<br />
glLoadIdentity;<br />
glTranslatef( -1.5, 0.0, -6.0 );<br />
<br />
// Zeichne Dreieck<br />
glBegin( GL_TRIANGLES );<br />
glVertex3f( 0.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
glTranslatef( 3.0, 0.0, 0.0 );<br />
<br />
// Zeichne ein Quadrat<br />
glBegin( GL_QUADS );<br />
glVertex3f( -1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, 1.0, 0.0 );<br />
glVertex3f( 1.0, -1.0, 0.0 );<br />
glVertex3f( -1.0, -1.0, 0.0 );<br />
glEnd;<br />
<br />
// Buffer-Wechseln ==> Anzeigen<br />
SDL_GL_SwapBuffers;<br />
end;</pascal><br />
<br />
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? ;) <br />
<br />
<br />
==Time to say goodbye!==<br />
<br />
Ich weis 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.<br />
<br />
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!<br />
<br />
<pascal>//----------------------------------------------------------------------------<br />
// Terminieren der SDL-Anwendung<br />
//----------------------------------------------------------------------------<br />
procedure Quit_App;<br />
begin;<br />
// Freigeben der Ressourcen<br />
SDL_QUIT;<br />
UnLoadOpenGL;<br />
Halt(0);<br />
end;</pascal><br />
<br />
<br />
==Nachwort==<br />
<br />
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 ;)<br />
<br />
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 ;)<br />
<br />
In diesem Sinne ... viel Spaß ;)<br />
<br />
Euer<br />
:'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_Matrix2]]|-}}<br />
<br />
[[Kategorie:Tutorial|SDL_Einstieg]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_2D&diff=14552Tutorial 2D2005-11-25T21:17:00Z<p>Akira: /* 2D mit OpenGL - Nicht jeder benötigt 3 Dimensionen */</p>
<hr />
<div>=2D mit OpenGL - "Nicht jeder benötigt 3 Dimensionen"=<br />
<br />
==Einleitung==<br />
<br />
Bei der Erwähnung einer API wie OpenGL denken die meisten eigentlich eher an 3D, und sind der festen (aber sehr wohl falschen) Überzeugung eine solche API sei für reine 2D-Anwendung überdimensioniert oder gar ungeeignet. Das dies nicht der Fall ist möchte ich mit diesem (vor allem an Einsteiger gerichtet, denn die Könner wissen wohl was man mit der GL so alles machen kann) Tutorial zeigen und auch gleich mit mehreren praktischen Beispielen aufweisen das 2D mit OpenGL nicht nur möglich ist, sondern auch noch sehr viel einfacher (selbst mit der GDI ist 2D komplizierter) ist und dabei jede Menge Vorteile mit sich bringt.<br />
<br />
==Welche Vorteile bringt mir die GL für eine 2D-Anwendung?==<br />
<br />
Dies ist wohl das wichtigste Kapitel und sollte zugleich auch mit diversen Vorurteilen und Missverständnissen aufräumen. Denn gerade der 3D-Bereich ist es in dem seit Jahren fast monatlich neue Techniken entworfen werden und der dafür sorgt das v.a. Grafikkarten immer leistungsstärker werden, während der 2D-Bereich seit seligen VLB-Zeiten (=Vesa-Local-Bus, alte Haudegen kennen diese Grafikkartengeneration sicherlich noch) keine Innovationen mehr erlebt hat (und warum auch? Im 2D-Bereich reicht ein gutes Bild zusammen mit passabler Darstellungsgeschwindigkeit).<br />
<br />
Deshalb gibt es jetzt gleich mal die wichtigsten Punkte warum man denn gerade OpenGL (D3D würde hier auch zählen, aber das haben wir GL'ler ja nicht so gerne) für die 2D-Darstellung nutzen sollte :<br />
<br />
*'''Hardwarebeschleunigung'''<br />
:Moderne Grafikkarten können inzwischen über 200 Millionen Dreiecke pro Sekunde rendern und besitzen brachiale Füllraten jenseits der 2.000 M(Texel/Pixel)/s. Das bedeutet also das man selbst auf älteren Grafikkarten sehr komplexe 2D-Szenen mit Geschwindigkeiten jenseits der 100 FpS (=Frames per Seconds ~ Bilder pro Sekunde) darstellen kann, während man mit der GDI schon bei einfachen 2D-Grafiken Geschwindigkeitsprobleme bekommen würde.<br />
<br />
*'''"Kostenlose" Objektsortierung'''<br />
:Eine 3D-API braucht einen Tiefenpuffer um zu erkennen ob Fragmente verdeckt sind oder nicht und damit Overdraw zu vermeiden. Eine 2D-Anwendung kann diesen Tiefenpuffer aber auch nutzen, nämlich um Objekte zu sortieren. Man nutzt dann die Z-Koordinate der Objekte (=Tiefenkoordinate) quasi als Layer um zu kennzeichnen welches Objekt auf welcher "Höhe" liegt. Wenn man also z.B. einen 2D-Topdown-Shooter entwickelt bei dem der Spieler mit seinem Flugzeug über den Boden fliegt, dann nutzt man den Z-Puffer um die API (die das dann der Hardware überlässt) seine Objekte sortieren zu lassen. Das Flugzeug bekommt dann einen niedrigen Z-Wert (=oben) und Objekte auf dem Boden einen hohen Tiefenwert (=unten/hinten). Die Sortierung übernimmt dann die Grafikkarte und wir müssen uns darum keine Gedanken machen. Würden wir die Anwendung z.B. über die GDI realisieren, müssten wir diese Objekte selbst entsprechend ihrer Höhe sortieren.<br />
<br />
*'''Jede Menge hardwarebeschleunigte Spezialeffekt'''<br />
:Wie schon oben erwähnt haben im 3D-Bereich innerhalb der letzten Jahre sehr viele Innovationen stattgefunden. Warum sollte man diese also nicht auch für seine 2D-Anwendung nutzen? Klingt logisch und macht auch Sinn! So bietet OpenGL alle Arten von Effekten die auch in einer 2D-Anwendung nützlich sein können. Darunter solche Sachen wie den Alphatest (der dafür sorgt das maskierte Teile eines Objektes transparent sind), Blending und natürlich (auch wenn das jetzt für erfahrene GL'ler sehr trivial klingt) hardwarebeschleunigte Rotation und Skalierung; was zur Folge hat das man seine Objekte nicht für verschiedene Auflösungen in verschiedenen Größen erstellen muss. Für Fortgeschrittene gibt es dann natürlich noch solche Sachen wie Shader, mit denen man Teile der OpenGL-Pipeline durch eigene (kleine) Programme ersetzen kann (entweder in Assemblerform oder aber in der neuen GL-HLSL). Dadurch bietet sich dann ein quasi unendlich großes Spektrum an möglichen Effekten, und das wohlgemerkt alles hardwarebeschleunigt!<br />
<br />
*'''Plattformübergreifend'''<br />
:Auch ein großer Vorteil von OpenGL. Die Tatsache das die GL unter diversen Betriebssystemen verfügbar ist (im Gegensatz zu GDI oder gar DirectX) macht die eigenen Programme recht portabel (einschränkend ist hier halt nur die Verfügbarkeit der genutzten Programmiersprache auf dem passenden OS). Unterstützt werden alle größeren Betriebssysteme wie Windows, Linux, MacOS und Solaris.<br />
<br />
:Ganz nebenbei wurde vor kurzem mit [http://www.khronos.org/opengles/2_X/ OpenGL ES] ein mobiler Standard für OpenGL geschaffen, wodurch es dann auch möglich ist auf mobilen Geräten (Handys, PDAs, Handhelds) OpenGL zu nutzen. Und gerade dort sind 2D-Spiele (aufgrund der oft mangelnden Leistung der Geräte) ja noch stark verbreitet.<br />
<br />
So viel also zu den wichtigsten Vorteilen zur OpenGL unter 2D. Natürlich gibt es noch weiter Dinge die OpenGL für 2D-Anwendungen attraktiv machen, aber allein die oben genannten Gründe sollten jedermann überzeugt haben. Und alle die wirklich mal wissen wollen wie gut OpenGL denn für solche Anwendungen geeignet ist, sollten sich unbedingt eine neuere Version des MacOS ansehen, denn das benutzt OpenGL zur Darstellung seiner GUI.<br />
<br />
<br />
==Und welche Nachteile gibt es?==<br />
<br />
Nichts was der Mensch bisher erfunden hat (mal abgesehen von der Spaltung des Atoms ;) ) hat nur Vorteile. Genauso sieht es auch aus wenn man die OpenGL für seine 2D-Anwendung nutzen will. Welche genau das sind will ich hier grob auflisten.<br />
<br />
*'''Ohne 3D-Beschleuniger mit passenden Treibern geht nichts'''<br />
:Klingt logisch, oder? OpenGL ist eine 3D-API und da 2D nichts weiter als die (fast vollständige) Vernachlässigung der Z-Koordinate ist, kommen wir um einen 3D-Beschleuniger nicht herum, der dazu auch noch einen Treiber mitbringen muss der OpenGL unterstützt. Allerdings schritt der Fortschritt auf diesem Gebiet der IT-Technik in den letzten Jahren so rasant voran wie sonst nirgendwo, und wir werden nur sehr selten auf Rechner stoßen in denen Hardware agiert die keine 3D-Beschleunigung bietet. Ergänzend dazu sollte allerdings trotzdem immer der neuste Treiber installiert sein, denn besonders die in WindowsXP integrierten Grafikkartentreiber wurden ihrer OpenGL-Funktionalität entraubt (Man riecht hier förmlich die Konkurrenz zwischen D3D und der GL). Also ist dies im Endeffekt ein Nachteil der inzwischen kaum noch halt findet und in Zukunft total vernachlässigt werden kann.<br />
<br />
*'''Hardwarelimitationen'''<br />
:Einer der größten Nachteile einer jeden 3D-API die auf Hardwarebasis arbeitet sind die Limitationen die die Hardware mitbringt. Jeder Grafikkartentyp hat andere, was mitunter dazu führen kann das die selbstverfassten OpenGL-Anwendungen nicht auf allen Rechnern laufen. Da wir uns in diesem Tutorial (2D ist ja recht anspruchslos) allerdings in den Niederungen der OpenGL-Funktionalität bewegen, dürfte es hier kaum Probleme geben. Einzig die Tatsache das vor allem ältere 3D-Beschleuniger mit großen Texturen Probleme haben könnte hier und da Schwierigkeiten machen. Wer aber keine Texturen größer 1024x1024 Pixel nutzt und dazu noch sparend mit dem Speicher der Grafikkarte umgeht (nicht jede Grafikkarte hat 128 Mbyte Grafikspeicher oder gar mehr). Einige Leute werden sich übrigens evtl. dadurch verunsichert fühlen das ihnen jemand gesagt hat, man könnte unter OpenGL nur Texturen nutzen die der Dimension 2^n*2^n entsprechen. Das ist grundlegend korrekt, aber wir nutzen hier einen Texturenloader der [[gluBuildMipMaps]] benutzt um [[MipMaps]] (verschiedene Detailstufen) für unsere Texturen zu erstellen. Diese Funktion schluckt jede Größe (sofern diese kleiner oder gleich dem Hardwarelimit ist) und passt die Texturen dann entsprechend an eben genanntes Limit an, also müssen wir uns um diese so oft erwähnte Limitation keine Sorgen machen. Wer zu dem Thema Hardwarelimitation mehr wissen will, der sollte unbedingt mal auf [http://www.delphi3d.net/ Tom Nuydens Seite] vorbei schauen. Dort gibt es eine riesige Datenbank in der fast alle Grafikkarten mit ihren entsprechenden OpenGL-Fähigkeiten vertreten sind.<br />
<br />
*'''Filtering'''<br />
:Zugleich ein großer Vorteil, aber je nach Situation auch ein Nachteil. OpenGL filtert Texturen (sofern man das via GL_LINEAR so will) bilinear, was man auch tunlichst aktiviert lassen sollte (GL_NEAREST filtert nicht, sieht dann aber auch scheußlich blockig aus). Dadurch wirken Texturn meist etwas verschwommen. Ich für meinen Teil umgehe dies aber ganz einfach, denn in fast jedem Bildbearbeitungsprogramm gibt es eine Funktion mit der man ein Bild scharfzeichnen kann. Das sieht auf den ersten Blick dann zwar überzeichnet aus, aber wenn OpenGL das Bild dann als Textur bilinear filtert, heben sich Filtering und Scharfzeichnung gegenseitig fast auf. Das hat sich in meinen Anwendungen bisher bewährt und ist nicht wirklich viel Aufwand.<br />
<br />
Um dieses Kapitel hier abzuschließen sei noch gesagt das man ohne 3D-Beschleuniger nicht unbedingt auf OpenGL verzichten muss. Brian Paul hat mit [http://www.mesa3d.org/ Mesa3D] nämlich ein Projekt am laufen das OpenGL-DLLs zur Verfügung stellen die komplett über die CPU ablaufen. So kann man dann OpenGL-Anwendungen mit einer etwas schnelleren CPU trotz fehlendem 3D-Beschleuniger nutzen, oder auf Funktionen ausprobieren die von der (zu alten) Grafikkarte nicht unterstützt werden.<br />
<br />
==Die Grundlagen==<br />
<br />
Sollte sich der geneigte Leser nun also doch für die GL entschieden haben, so widmen wir uns dann jetzt den Grundlagen der 2D-Darstellung unter OpenGL. Viele Sachen die man bei einer 3D-Anwendung beachten muss, sind hier eigentlich zu vernachlässigen. Wer also schon mal eine kleine 3D-Anwendung unter OpenGL geschrieben hat wird hier sicherlich keine Problem bekommen. Da sich dieses Tutorial aber an blutige (mhh, lecker) Einsteiger richtet, versuche ich so genau und einfach wie möglich zu erklären was man machen muss und v.a. warum. Genau deshalb habe ich auch für einen Großteil der hier erwähnten Techniken im Download zu diesem Tutorial (siehe unsere Files-Sektion und dort unter VCL-Source) jeweils ein eigenes Beispielprogramm + Quellcode (und natürlich ausgiebigen Kommentaren) geschrieben. Wenn zu dem jeweiligen Kapitel ein solches im Download enthalten ist, dann steht das ''kursiv'' unter der Überschrift des Kapitels.<br />
<br />
<br />
<br />
==Die Projektion==<br />
<br />
Wie bekannt (sein sollte), besitzt OpenGL im Groben zwei wichtige Matrizen. Zum einen die Modellansichtsmatrix, in der man im Normalfall seine Szene (egal ob 2D oder 3D) rendert und (für dieses Kapitel wichtiger) die Projektionsmatrix. Diese Matrix lässt sich am besten mit der Linse einer Kamera vergleichen und legt fest wie die Objekte auf den Bildschirm projiziert werden (wer mitdenkt wird jetzt wissen warum diese Matrix so genannt wurde). In einer 3D-Anwendung setzen wir (meist über [[gluPerspective]]) eine Projektionsmatrix die dafür sorgt das unsere Objekte perspektivisch korrekt verzerrt werden (so wie es im echten Leben auch ist). Da Bilder aber mehr als tausend Worte sagen zeige ich das anhand der unteren Bildreihe, die einen Würfel an verschiedenen Positionen auf der X-Achse zeigt :<br />
<br />
[[Bild:Tutorial_2D_illustration_1.jpg|center]]<br />
<br />
Der Würfel wurde auf den beiden Bildausschnitten links und rechts jeweils um 40 Einheiten auf der X-Achse verschoben und man kann sehr gut sehen das die Seiten des Würfels perspektivisch verzerrt werden, also weiter entfernte Kanten kleiner erscheinen (wie im realen Leben, das kann man ja prima mit nem würfelähnlichem Objekt nachprüfen). Diese Art der Darstellung ist für 3D gut geeignet, aber für unseren Zweck nicht. Denn wir wollen ja das unser Objekt, egal an welcher Bildschirmposition es sich befindet, gleich aussieht.<br />
<br />
<br />
Dazu gibt es unter OpenGL den sog. orthogonalen Modus, der dafür sorgt das unser [[Frustum]] (Sichtkegel) nicht wie bei der 3D-Projektion kegelförmig ist (kleine Seite beim Betrachter, große Seite am Ende des Sichtfeldes), sondern wie eine Box aussieht. Für technisch interessierte hier der Vergleich zwischen dem 3D- und dem 2D-Frustum :<br />
<br />
[[Bild:Tutorial_2D_illustration_2.jpg|center]]<br />
<br />
Links sehen wir das Frustum (~Sichtbereich) für die orthogonale Projektion (also 2D) und rechts für die perspektivische Projektion (3D). In diesem Tutorial interessieren wir uns wie gesagt für ersteres Frustum, welches sich über die Funktion [[glOrtho]] erstellen lässt. Diese Funktion will von uns die Dimensionen haben die wir unserem Viewport geben wollen. Ich empfehle hier übrigens immer einen festen Wert der einer der gängigen Auflösungen (z.B. 640x480, 800x600) entspricht. Der feste Wert hat übrigens den Vorteil das unsere Anwendung von der vom Nutzer gewählten Bildschirmauflösung unabhängig ist. Wir müssen dann also nicht mehr umrechnen wo unser Objekt jetzt in der gewählten Auflösung wäre und wie groß es dort sein müsste. Dadurch das wir immer die selben virtuellen Koordinaten haben, überlassen wir der GL (bzw. der Grafikkarte) die Umrechnung. Wenn wir also eine virtuelle Auflösung von 640x480 an glOrtho übergeben, und ein Objekt zentriert bei 320x240 rendern, dann wird dieses egal in welcher Auflösung immer in der Mitte des Schirms gerendert. Die Umrechnung macht wie gesagt OpenGl (oder besser gesagt die Grafikkarte). Zusätzlich übergeben wir der Funktion dann noch die Z-Reichweite. Hier kann man beliebig wählen, und muss nicht wie in 3D darauf achten Z-Near und Z-Far so zu wählen das die Auflösung des Tiefenpuffers nicht unnötig verschwendet wird (z.B. mit einem Z-Near von 0.1 oder gar kleiner). Für Z-Near nehme ich gewöhnlich 0 und für Z-Far einen Wert der dafür sorgt das ich alle Objekte so sortieren kann das ihre Z-Position auf einen Integerwert fällt. Als kleines Codebeispiel könnte unsere Projektionsmatrix nun so aussehen :<br />
<br />
<pascal>glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
glViewport(0,0,ClientWidth,ClientHeight);<br />
glOrtho(0,640,0,480, 0,128);</pascal><br />
<br />
Um den optischen Vergleich zur oben erwähnten 3D-Projektion zu zeigen, gibt es wieder ein Bild des Würfels, diesmal allerdings mit 2D-Projektion :<br />
<br />
[[Bild:Tutorial_2D_illustration_3.jpg|center]]<br />
<br />
Das sieht auf den ersten Blick zugegeben recht langweilig aus, stellt aber genau den selben Szenenverhalt dar wie die Ansicht ein paar Zeilen weiter oben. Diesmal allerdings mit der gerade besprochenen orthogonalen Ansicht, die als Grundlage für unsere 2D-Projektion dient.<br />
<br />
So viel also zur 2D-Projektion und hoffentlich hat das hier jeder verstanden. Die orthogonale Projektion ist ein essentieller Bestandteil einer jeden 2D-Anwendung unter OpenGL und sollte daher allen Interessierten ein Begriff sein. Falls das hier jemandem zu technisch war, im Forum werden weitergehende Fragen gerne beantwortet.<br />
<br />
Noch als kleiner Nachtrag : Wer sich mal die Parameter angesehen hat die glOrtho will, wird bemerkt haben das wir in obigem Quellcode (zumindest augescheinlich) Top mit Bottom verwechselt haben (sprich es sollte 0,640,480,0 statt 0,640,0,480) heißen. Das hat allerdings seine Richtigkeit, denn in OpenGL liegt der Ursprung des Koordinatensystems in der unteren linken Bildschirm(oder Fenster)ecke, wobei er bei Windows in der oberen Ecke liegt. Unter OpenGL liegt also quasi der "Boden" oben, genau umgekehrt wie unter Windows. Genau deshalb übergeben wir als "Oben" an glOrtho den eigentlichen Boden des Viewports. Das klingt verwirrend, aber ist im Endeffekt gar nicht so schwer zu behalten, besonders dann nicht wenn man sich folgende Illustration mal näher ansieht :<br />
<br />
[[Bild:Tutorial_2D_illustration_11.jpg|center]]<br />
<br />
Das sollte man immer in Hinterkopf behalten, und unter 3D ist es genauso. Während ein positiver Y-Wert Objekte in einem Windowsfenster nach unten verschiebt, geschieht unter OpenGL genau das Gegenteil. Wer sich mit dieser Tatsache nicht anfreunden kann, der kann auch gerne glOrtho dazu nutzen die 2D-Matrix von OpenGL an die Windowsgegebenheiten anzupassen :<br />
<br />
<pascal>glOrtho(0,640,480,0 0,128);</pascal><br />
<br />
Und schon verhält es sich unter OpenGL genauso wie unter Windows. Positive Y-Koordinaten zeigen nach unten. Allerdings muss man hier dann auch drauf achten die gerenderten Objekte an diese Gegebenheit anzupassen. Man muss diese also quasi auf den Kopf stellen, damit sie mit der neuen Matrix korrekt angezeigt werden. Das geht aber ganz leicht, indem man beim rendern von Quads oder anderen texturierten Primitiven ganz einfach die T-Texturkoordinaten vertauscht.<br />
<br />
<br />
<br />
==Darstellung der 2D-Objekte==<br />
<br />
Einige 2D-Interessierte haben sich sicherlich schon mal im Funktionsumfang von OpenGL umgesehen und bemerkt, dass es dort eigentlich gar keine Funktionen gibt um Dinge in 2D zu zeichnen. Auf den ersten Blick sieht das auch wirklich so aus, aber man darf halt nie vergessen dass OpenGL primär für den 3D-Bereich entworfen wurde und sich 2D-Sachen dann nur über 3D-Techniken realisieren lassen. So auch die Darstellung unserer 2D-Objekte, für die wir aus genau diesem Grund eine 3D-Technik anwenden müssen, nämlich das sog. Texturemapping (Den Begriff "Textur" gibt's übrigens auch im deutschen Sprachgebrauch, aber gängiger ist die korrekte Übersetzung "Oberfläche"). Unter OpenGL werden ja alle Objekte aus verschiedenen Primitiventypen zusammengesetzt (Dreiecke, Rechtecke, usw.) und diese Objekte kann man mit einer Textur belegen die dann auf dieser Oberfläche "angezeigt" wird. Diese Textur lädt man im Normalfall aus einer vorher erstellten Bilddatei unter Nutzung eines [[Texture_Loader|Texturenloaders]] (alternativ kann man den sich natürlich auch selbst schreiben), der diese Textur für die Grafikkarte vorbereitet (also z.B. ein BMP-Bild vom BGR-Format ins RGB-Format bringt) und dann auf dieser ablegt. Danach kann diese Textur an jeder Stelle im Programm auf eine Primitve "geklebt" werden, und genauso machen wir das auch in unserer 2D-Anwendung.<br />
<br />
Allerdings müssen wir keine komplexen Formen darstellen, da unsere Objekte ja nicht 3D sind, sondern (meistens) schon in einem anderen Programm erstellt (oder vorgerendert wurden) und als Bilddatei abgelegt wurde. Wir laden und stellen dann also nicht die 3D-Daten dieses Modells dar (was bei komplexen 2D-Objekten wohl eh zu viel wäre), sondern kleben diese schon fertige Bilddatei mittels einer Textur auf ein Rechteck (in der GL-Terminologie "Quad" genannt, vom Primitiventyp GL_QUADS). Hoffe mal das kam gut rüber, aber ich verdeutliche dass dann besser nochmal anhand einer kleinen "Bilderserie" :<br />
<br />
[[Bild:Tutorial_2D_illustration_4.jpg|center]]<br />
<br />
Oben sei mal kategorisch der Vorgang geschildert um ein vorgerendertes 3D-Objekt als Textur in seine 2D-Anwendung zu bekommen. Rechts sieht man das 3D-Modell, das dann aus der gewünschten Ansicht (im obigen Falle von der Seite) im 3D-Modellierungsprogramm gerendert wird. Dieses Rendering speichert man dann in einem Format ab das der Texturenloader verarbeiten kann, lädt dies in seine OpenGL-Anwendung und stellt dies dann auf z.B. einem Quad dar (siehe letztes Bild). Natürlich spielt es keine Rolle ob man seine 2D-Objekte vorrendert oder diese von Hand zeichnet, wobei den meisten wohl Ersteres besser von der "Hand" geht.<br />
<br />
<br />
===Welches Bildformat ist das richtige?===<br />
<br />
Bevor wir nun weiter auf das Thema eingehen kümmern wir uns um die '''Frage nach dem richtigen Bildformat''' für unsere Texturen. Bildformate gibt's wie Sand am Meer, aber für unseren Zweck eignen sich nur sehr wenige (eigentlich nur ein einziges). Ich zähle die verbreitetsten Formate kurz auf und sag auch warum (oder warum nicht) und wofür man diese verwenden kann :<br />
<br />
*'''Joint Photographic Experts Group (*.jpg; *.jpeg)'''<br />
:Das in den Weiten des WWWs wohl verbreitetste Format ist für Texturen generell eher weniger zu empfehlen, und für 2D-Objekte erst recht nicht. Zum einen ist die in diesem Format genutzte Kompression verlustbehaftet (also verlieren unsere Texturen an Qualität) und außerdem hat dieses Format keine Möglichkeit Transparenzinformationen zu speichern. Diese benötigt man aber für 2D-Objekte, denn im Normalfall wollen wir den Hintergrund des Objektes ja durchsichtig machen (dazu gleich mehr). Also sollte dieses Format nur verwendet werden wenn wir etwas darstellen wollen das sehr viele Details enthält (dann fällt die verlustbehaftete Kompression nicht so stark auf) und keine transparenten Bereiche enthält.<br />
<br />
*'''Graphical Interchange Format (*.gif)'''<br />
:Direkt aus der Steinzeit des IT-Sektors kommt das (im Netz noch weit verbreitet) GIF-Format. Für unsere Zwecke ist es total unbrauchbar. Es ist eine Palettenformat, das maximal 256 verschiedene Farben unterstützt, allerdings inklusive Transparenzinformationen. Aber die maximal 256 Farben und die kaum vorhandene Kompression machen es für unseren Zweck nutzlos. Die Tatsache dass es Animationen unterstützt ist zwar im Internet für kleine animierte Sachen ganz toll, aber hilft uns auch nicht, denn dazu überwiegen die Nachteile zu stark<br />
<br />
*'''Portable Network Graphic (*.png)'''<br />
:Der Nachfolger des GIF-Formates. Eigentlich auch sehr gut für Texturen geeignet, denn neben einem Alphakanal (maximal 8 Bit) und 8 Bit pro Farbkanal unterstützt es auch verlustfreie Kompression (mit einem recht hohen Kompressionsfaktor). Nachteil ist allerdings der Aufbau des Formates, denn der Chunkaufbau macht das Laden recht schwer und bisher gibt es nur Loader die eine DLL-Datei mit sich schleppen. Alternativ kann man auf neuen Betriebssystemen jedoch via GDI+ auch PNG-Dateien direkt laden.<br />
<br />
*'''TARGA (*.tga)'''<br />
:Dieses Format werden sicherlich nicht viele Einsteiger kennen, allerdings ist dies '''''das perfekte Format für unsere Bedürfnisse bzw. Texturen im Allgemeinen'''''. Es kann nämlich bis zu 8 Bit pro Farbkanal (also das was man heute als 32-Bit Farbtiefe bezeichnet) speichern (=24 Bit für Farben) und dazu noch einen Alphakanal (maximal 8 Bit). Der Alphakanal ist sehr nützlich, denn in ihm kann man die Transparenzinformationen eines Bildes ablegen. Auch viele kommerzielle Titel nutzen dieses Format (u.a. Quake3), und das aus gutem Grund. Kompression wird auch unterstützt, und zwar verlustfrei in Form einer LZW-Kompression. Alles in allem ist das momentan das geeignetste Format für Texturen, zumal so gut wie jedes Bildbearbeitungsprogramm damit umgehen kann. Nebenbei ist dies Format auch recht einfach aufgebaut und damit auch recht leicht einzulesen.<br />
<br />
*'''BITMAP (*.bmp)'''<br />
:Das BMP-Format dürfte sicherlich jedem ein Begriff sein, ist aber genauso wie GIF ein Relikt aus der Steinzeit. Es kann zwar genauso wie das TGA-Format neben den 8 Bits pro Farbkanal auch einen maximal 8 Bits großen Alphakanal anbieten, allerdings kommen damit nur recht wenige Bildbearbeitungprogramme klar. Ausserdem sind die Bilddaten hier im BGR-Format abgelegt, statt dem eher üblichem RGB-Format (Red, Green, Blue). Das ist zwar sehr leicht umzuwandeln, bzw. kann mit passender GL-Konstante auch direkt übergeben werden, aber trotzdem ist dieses Format in keinem Falle dem TARGA-Format vorzuziehen.<br />
<br />
*'''DirectDraw Surface (*.dds)'''<br />
:Ein sehr neues Format, das den Ursprung (wie am Namen zu erkennen) in Microsofts DirectX-Schnittstelle hat. Es ist ein recht modernes Format, das speziell für die Speicherung von Texturen entwickelt wurde. Allerdings ist das eher was für Fortgeschrittene, denn weder das Laden dieses Formates ist einfach, noch seine Speicherung (selbst teure Bildbearbeitungsprogramme brauchen ein passendes Plugin für DDS und bei der Erstellung des Formates muss man auf bestimmte Sachen achten). Aber ich wollte es hier trotzdem mal erwähnt haben, damit man sieht das es auch spezielle Formate für Texturen gibt, und wenn ihr euch eingearbeitet habt, dann könnt ihr im Forum mehr zu dem Format finden (Mars hat dort auch einen grundlegenden Loader gepostet), denn es ist recht interessant. Es unterstützt feste Kompressionsratios, Mip-Maps, 3D-Texturen, uvm.<br />
<br />
Die obige Liste dürfte also einen recht (groben) Überblick über die verbreiteten Bildformate geben, und für dieses Tutorial begnügen wir uns erstmal mit dem TARGA-Format. Das wurde übrigens bereits 1984 erfunden, ist aber trotzdem noch nicht veraltet, sorgt aber dafür das so ziemlich jedes Programm damit umgehen kann.<br />
<br />
Wer sich übrigens nicht auf einen fremden Texturenloader verlassen möchte, sondern sich selbst um das Einlesen der Bildformate kümmern will, der sollte mal eine Blick auf [http://www.wotsit.org wotsig.org] werfen, einer recht großen Bibliothek die es sich zur Aufgabe gemacht hat Spezifikationen für Dateiformate zu sammeln. Dort wird man zu jedem der oben gennannten Bildformate eine solche Spezifikation finden, anhand derer man dann selbst Laderoutinen schreiben kann.<br />
<br />
<br />
===Textur laden===<br />
<br />
Auch wenn es ein sehr simples Unterfangen ist eine Textur zu laden, werde ich hier trotzdem nochmal kurz darauf eingehen. Das Tutorial richtet sich ja an Einsteiger, und von daher kann es nicht schaden auch mal kurz zu zeigen wie man so eine Textur lädt. Nutzen tun wir dazu die ''Textures.pas'' (die im Original von Jan Horn stammt. Ein weitere guter Loader ist [[glbitmap_loader|glBitmap.pas]]), die sich im Download des Beispiels für dieses Tutorial befindet. Der Loader kann JPG, BMP und TGA laden. Außerdem lädt er auch den Alphakanal aus einer TGA-Textur.<br />
<br />
Bevor wir die Textur laden können, benötigen wir eine Variable in der wir den Bezeichner der Textur speichern. OpenGL erstellt für alle Ressourcen eindeutige Bezeichner (Bezeichner hier in Form eines Integerwertes, also einer eindeutigen ID), so auch für Texturen. Dieser Bezeichner ist vom Typ glUInt (U=unsinged Int=Integer, also vorzeichenloser Ganzzahlwert, was in Delphi dem Variablentyp ''Cardinal'' entpricht). Deshalb deklarieren wir unseren Texturenbezeichner auch so :<br />
<br />
<pascal>var<br />
MyTex : glUInt;</pascal><br />
<br />
Wenn wir mehrere Texturen laden und verwalten wollen, bietet sich natürlich ein (dynamisches) ''array of glUInt'' an, aber das sind Delphigrundlagen die in diesem Tutorial nichts zu suchen haben.<br />
<br />
Das Laden der Textur geht nun dank der ''Textures.pas'' ganz einfach :<br />
<br />
<pascal>LoadTexture('MeineTextur.tga', MyTex, False);</pascal><br />
<br />
Die Parameter sollten recht logisch sein, der erste gibt den Dateinamen der Textur an, der zweite das Texturobjekt (in das die ID der Textur geschrieben wird) und der letzte Parameter gibt an ob die Textur aus einer dem Programm angehängten Ressource geladen werden soll. Sollte der Ladevorgang erfolgreich gewesen sein, so müsste sich in ''MyTex'' ein Wert > 0 befinden, nämlich der eindeutige Bezeichner dieses Texturenobjektes.<br />
<br />
===Textur anwenden===<br />
<br />
Wer sich schonmal ein wenig über OpenGL schlau gemacht hat, der wird wissen dass die GL eine Statemachine ist. Das trifft auch auf Texturen zu, denn wenn eine Textur gebunden wurde, wird sie solange auf alle Primitiven angewendet, bis entweder eine andere Textur gebunden wurde oder das Texturemapping über [[glDisable]] abgeschaltet wird. Das hat besonders dann den Vorteil, wenn man viele Objekte mit der gleichen Textur rendern muss, denn Texturenwechsel sind recht kostspielig. Von daher sollte man also bei vielen Objekten eine Sortierung nach Textur vornehmen, dann diese Textur binden und danach dann alle Objekte die diese Textur besitzen rendern.<br />
<br />
Doch bevor Texturen überhaupt angezeigt werden, müssen wir OpenGL erstmal mitteilen das es diese auch anzeigen soll. Dazu gibt es die Funktion '''glEnable''', der man mit der Konstante '''GL_TEXTURE_2D''' mitteilt das wir die 2D-Texturierung aktivieren wollen (1D oder 3D-Texturen benötigen wir ja in diesem Tutorial nicht) :<br />
<br />
<pascal>glEnable(GL_TEXTURE_2D);</pascal><br />
<br />
Unglaublich einfach, oder? So schnell kann das dank einer gut durchdachten API wie OpenGL gehen. Jetzt wo wir der API erstmal gesagt haben dass wir gerne Texturen sehen möchten, müssen wir auch noch sagen welche Textur als nächstes auf unserer Primitiven gezeigt werden soll. Dazu bindet (~aktiviert) man das passende Texturobjekt :<br />
<br />
<pascal>glBindTexture(GL_TEXTURE_2D, MyTex);</pascal><br />
<br />
Von nun an werden alle folgenden Primitiven solange mit der hinter ''MyTex'' abgelegten Textur gerendert, bis das Texturemapping entweder deaktiviert wird oder wir eine andere Textur binden.<br />
<br />
<br />
===Transparenz===<br />
<br />
Eine wichtige Sache die es noch zu klären gibt ist Transparenz. Oben habe ich ja gesagt das wir unsere Objekte auf Quads kleben (über eine Textur), aber unsere vorgefertigten Objekte nur selten auch genau die Form eines Quads haben. Der Panzer auf der oben gezeigten Textur wird z.B. von sehr viel schwarz umgeben, das wir da natürlich nicht sehen wollen. Aber auch um die Transparenz brauchen wir uns unter OpenGL keine Sorgen zu machen, denn dafür gibt es den sog. Alphakanal der Textur, der angibt welche Teile einer Textur später transparent (oder besser gesagt gar nicht, aber dazu gleich mehr) gezeigt werden sollen. Aus diesem Grund haben wir uns mit dem TGA-Format auch ein Format gewählt das diesen Kanal direkt im Bild speichern kann, sodass wir diesen nicht extra erstellen oder aus einer seperaten Bilddatei laden müssen.<br />
<br />
Wie man den Alphakanal nun in die Textur bekommt hängt davon ab wie man seine Textur erstellt. Wer seine Objekte von Hand malt, der muss den Alphakanal im Bildbearbeitungsprogramm selbst erstellen. Wer seine Objekte allerdings vorrendert, der kann diese Arbeit im Normalfall von der 3D-Software erledigen lassen, die Alphainformationen direkt mitexportieren kann. Als kleiner Hinweis sei übrigens gesagt das man beim Rendering des Objektes im 3D-Programm die Kantenglättung deaktivieren muss, da man sonst an den Rändern Artefakte hat (die logischerweise Teile der Hintergrundfarbe enthalten) die dann in der OpenGL-Anwendung zu unschönen Effekten führen. Um das zu verbildlichen hier nochmal unsere Panzertextur, allerdings begleitet vom (im Bildformat gespeichertem) Alphakanal :<br />
<br />
[[Bild:Tutorial_2D_illustration_5.jpg|center]]<br />
<br />
Links also unsere Textur und rechts der Alphakanal. Den sieht man normalerweise nicht, aber fast jedes Bildbearbeitungsprogramm gibt einem die Möglichkeit sich diesen anzeigen zu lassen. Wie zu sehen befinden sich in unserem Falle nur zwei Werte im Alphakanal. Und zwar Schwarz (=0) für komplett transparent und Weiß (=1) für komplett Sichtbar.<br />
<br />
Unter OpenGL nutzen wir jetzt für die Transparenz den [[GlAlphaFunc|Alphatest]]. Transparenz ließe sich auch über Blending realisieren, allerdings hat Blending den Nachteil das man dann die transparenten Objekte nach Tiefe sortieren müsste, da Blending im Framepuffer abläuft und nicht wie der Alphatest auf Fragmentbasis, wo wir uns dann keine Sorge um die Reihenfolge unserer Objekte machen müssen. Wollen wir nun also den Alphakanal der Textur nutzen (natürlich muss dieser vorher im Texturenloader geladen werden, sonst geht's nicht) müssen wir vor dem rendern des mit der Objekttextur belegten Quads den Alphatest aktiveren und festlegen wie der Test auszusehen hat :<br />
<br />
<pascal>glEnable(GL_ALPHA_TEST);<br />
glAlphaFunc(GL_GREATER, 0.1);</pascal><br />
<br />
Zuerst aktiveren wir also den Alphaest (dazu gibt es die Konstante '''GL_ALPHA_TEST'''), bevor wir dann der GL mittels [[GlAlphaFunc|glAphaFunc]] sagen wie der Test aussehen soll. Der erste Parameter ('''GL_GREATER''') gibt an, das nur Fragmente (also Teile der Textur) gerendert werden sollen deren Alphawert größer ist als der im zweiten Wert angegebene (0.1). Wie auch Farbwerte wird der Alphawert unter OpenGL "geclampt", also in eine bestimmte Reichweite gebracht, nämlich 0 (=Schwarz) bis 1 (=Weiß). Die 0,1 (statt der 0) als Vergleichswert nehmen wir quasi aus Toleranz. Wenn wir das obige also getan haben, dürften wir auf unserem Quad (sofern der Alphakanal korrekt erstellt und geladen wurde) also nur noch das eigentliche Objekt sehen, und der Hintergrund müsste an den transparenten (Alpha <= 0,1) zu sehen sein :<br />
<br />
[[Bild:Tutorial_2D_illustration_6.jpg|center]]<br />
<br />
Links sehen wir unsere Bohne ohne aktiven Alphatest, was zur Folge hat das der eigentlich transparente (schwarze) Teil der Objekttextur den Hintergrund überdeckt. Rechts wuede der Alphatest aktiviert und wir sehen nur den Teil unseres Objektes den wir auch sehen wollen.<br />
<br />
<br />
===Das Objekt anzeigen===<br />
<br />
Nachdem wir nun unsere Textur mit passendem Alphakanal geladen haben und den Alphatest auch aktiviert haben, müssen wir schlussendlich noch unser(e) Objekt(e) rendern. Wie schon mehrfach gesagt nutzen wir dazu die GL_QUADS-Primitive. Bei diesem Primitiventyp beschreiben wir mit vier Eckpunkten ein Rechteck (das von der Grafikkarte dann in zwei Dreiecke zerlegt wird), wobei jeder Eckpunkt auch eine Texturkoordinate zugewiesen bekommt. Diese Koordinate gibt an, aus welchem Teil der Textur dieser Eckpunkt seine Bilddaten beziehen soll, und sie wird über das gesamte Quad hinweg interpoliert. Also haben wir in der genauen Mitte des Quads als interpolierte Texturkoordinate das genaue Mittel der übergebenen Texturkoordinaten. Um diese Interpolation müssen wir uns allerdings keine Sorgen machen, das macht die Grafikkarte.<br />
<br />
Und in Sachen Texturkoordinaten sind wir auch schnell fertig, denn für den Anfang haben wir pro Textur immer nur ein Objekt (später zeige ich dann wie man mehrere Objekt in eine Textur packt) und müssen dementsprechend auch nur eine 1 (=Ende der Textur) bzw. 0 (=Anfang der Textur vergeben. Wenn man sich das bildlich vorstellt, sieht das dann so aus :<br />
<br />
[[Bild:Tutorial_2D_illustration_7.jpg|center]]<br />
<br />
Da wir nur mit 2D-Texturen arbeiten, müssen wir pro Eckpunkt auch nur zwei Koordinaten angeben. Und zwar einmal in X-Richtung auf der Textur (unter OpenGL auch S-Richtung genannt, oft auch mit "U" betitelt) und in Y-Richtung (unter OpenGL T-Richtung, oft auch "V" genannt). Wenn S=0 und T=0, bedeutet das also das dieser Eckpunkt seinen [[Texel|Texel]] (wie Pixel, bloß im Bezug auf Texturen) aus der oberen linken Ecke unserer Textur (X=0/Y=0) bezieht, während S=1 und T=1 dafür sorgt das der Eckpunkt sich den Texel in der untersten rechten Ecke der Textur schnappt. Wie bereits oben erwähnt müssen wir uns um den Raum zwischen den vier Eckpunkten nicht kümmern, das wird ja von der Hardware linear interpoliert.<br />
<br />
Der Quellcode zu obigem Beispiel sieht dann so aus :<br />
<br />
<pascal>glBegin(GL_QUADS);<br />
glTexCoord2f(0,0); glVertex3f(-Breite/2, -Höhe/2, -Tiefe);<br />
glTexCoord2f(1,0); glVertex3f(+Breite/2, -Höhe/2, -Tiefe);<br />
glTexCoord2f(1,1); glVertex3f(+Breite/2, +Höhe/2, -Tiefe);<br />
glTexCoord2f(0,1); glVertex3f(-Breite/2, +Höhe/2, -Tiefe);<br />
glEnd;</pascal><br />
<br />
Wer obigen Text aufmerksam gelesen hat, sollte eigentlich problemlos verstehen was der Quellcode denn bewirkt. Wer damit Problem hat, der sollte sich das obige Kapitel nochmal unbedingt sorgfältig durchlesen, denn das ist eine sehr wichtige Sache.<br />
<br />
Soviel also zu den Grundlagen von 2D unter OpenGL. Wer nämlich hier angelangt ist, sollte zumindest 2D-Objekte unter OpenGL anzeigen können. Evtl. ist es hier angebracht das Tutorial einige Minuten ruhen zu lassen und ein wenig mit dem Erlerntem herumzuprobieren. Danach geht's nämlich mit etwas fortgeschritteneren (aus Sicht des Einsteigers) Themen weiter.<br />
<br />
<br />
===Die Rolle des Tiefenpuffers===<br />
<br />
Trotz der Tatsache dass wir in 2D die dritte Dimension vernachlässigen, bedeutet dies nicht das wir den Tiefenpuffer nicht doch nutzen können. Und zwar nutzen wir diesen in 2D zur hardwarebeschleunigten Sortierung unserer Objekte. In 2D-Anwendungen kann es ja genauso vorkommen das ein Objekt unter ("hinter") einem anderen liegt und da wäre es ziemlich dumm diese selbst zu sortieren, wo OpenGL uns doch einen Tiefenpuffer anbietet der dies für uns macht.<br />
<br />
Genau deshalb haben wir mittels glOrtho auch die Reichweite für unseren Tiefenpuffer angegeben. Haben wir dann auch noch den Tiefentest mittels {{INLINE_CODE|glEnable(GL_DEPTH_TEST)}} und dem passenden Tiefentest via {{INLINE_CODE|[[glDepthFunc]](GL_LESS ''oder'' GL_EQUAL)}} aktiviert, so können wir über die Z-Koordinate unserer Objekte angeben wo die nun genau liegen. Ein Objekt dessen Z-Koordinate also näher an Z-Near ist, wird dann über einem an gleicher Stelle befindlichem Objekt mit einer Z-Koordinate näher an Z-Far gerendert, und zwar egal welches dieser Objekte wir als erstes an die GL übergeben haben.<br />
<br />
<br />
===Backface Culling===<br />
<br />
Auch diese von OpenGL angebotene Funktionalität sollte hier nicht verschwiegen werden. Denn in OpenGL bestehen Flächen immer aus einer Vorder- und Rückseite (wie im echten Leben, ein Blatt Papier hat ja auch zwei Seiten, egal wie dünn es ist), was spätestens dann Sinn macht wenn man bedenkt das man sich in einer 3D-Umgebung ja komplett frei bewegen kann. Doch in unserer 2D-Welt sehen wir immer nur eine Seite unserer Objekte, egal was wir anstellen. Und genau deshalb sollten wir OpenGLs [[Backface Culling]] (zu Deutsch heisst das wörtlich "Rückseiten Ausschluß", aber solche Fachbegriffe deutsch man auch besser nicht ein) aktivieren, denn ist dies nicht der Fall, so werden alle Berechnungen immer für beide Seiten eines Polygons ausgeführt. Und dabei spielt es keine Rolle welche der Seiten sichtbar ist oder nicht. Wir aktivieren also das Backfaceculling und sagen OpenGL dass die Rückseiten der Primitiven nicht dargestellt werden soll (letzteres könnte man sich sparen, da das die Voreinstellung ist, allerdings sollte man lieber auf Nummer sicher gehen) :<br />
<br />
<pascal>glEnable(GL_CULL_FACE);<br />
glCullFace(GL_BACK);</pascal><br />
<br />
Solltet ihr jetzt übrigens eure Objekte nicht mehr sehen, dann habt ihr die Eckpunkte eurer Primitiven in der falschen Reihenfolge (Standard ist für OpenGL '''GL_CCW''' ('''C'''ounter'''C'''lock'''W'''ise, gegen den Uhrzeigersinn) angegeben. Ich empfehle dann entweder einen Blick ins Reedbook oder in eine der Beispielanwendungen, wo es eine Funktion namens ''DrawQuad'' gibt, die ein korrektes Quad rendert. Bei modernen Grafikkarten sollte das Backface Culling zwar nur recht wenig Performance bringen, aber es wäre trotzdem Performanceverschwendung dieses nette Feature einfach ungenutzt zu lassen.<br />
<br />
<br />
<br />
==Animation==<br />
Bisher können wir zwar unsere 2D-Objekte mittels texturierter Quads auf dem Bildschirm darstellen, aber das ist natürlich noch recht statisch; und was wäre ein Computerspiel ohne Animationen? Und genau darum kümmern wir uns jetzt. Aber zuerst kurz zum Unterschied "Animation in 3D" und "Animation in 2D". In 3D ist es normalerweise so, dass man ein 3D-Modell lädt und dann die einzelnen Teile des Modells animiert, sei dies über die GL-Befehle oder Animationsdaten im 3D-Format (Bones, Keyframes). Das bedeutet also das man in 3D quasi unendlich viele Freiheiten hat, denn man kann ja z.B. den Turm eines Panzers über [[glRotate]]f in jeden beliebigen Winkel bringen. In 2D ist das anders, denn da sind ja alle Grafiken vorgefertigt und werden nur abwechselnd auf unsere Quads geklebt. Würden wir dort also den Turm eines vorgefertigten Panzers rotieren lassen wollen, so müssten wir jeden Animationsframe vorfertigen und als Textur laden. Man muss sich also vorher Gedanken drüber machen ob 2D denn im Endeffekt wirklich weniger Aufwand bedeutet. Aber diese Entscheidung muss jeder für sich selbst fällen, weshalb wir uns nun zwei verschiedenen Animationsmöglichkeiten widmen werden.<br />
<br />
<br />
===Animation über Einzeltexturen===<br />
''(Projektdatei : openGL2D_demo1)''<br />
[[Bild:Tutorial_2D_illustration_8.jpg|center]]<br />
<br />
Die wohl offensichtlichste (und auch einfachste) Art ein 2D-Objekt über Texturen zu animieren, ist über in einzelnen Texturen abgelegte Frames. Besonders aus einem 3D-Animationsprogramm (3D Studio, Maya) geht das besonders einfach. Denn diese Programme können eine Animation als Einzelbilder auf die Platte rendern. Dadurch hat man dann für eine Animation mit 60 Bildern 60 Texturen die dann einfach in ein Array geladen werden :<br />
<br />
<pascal>var<br />
Frame : array of glUInt;<br />
<br />
SetLength(Frame, NumFrames);<br />
for i := 0 to NumFrames-1 do<br />
LoadTexture('animframe'+IntToStr(i)+'.tga', Frame[i], False);</pascal><br />
<br />
Das zu animierende Objekt verpasst man dabei mit einem Fließkommawert der den aktuell anzuzeigenden Frame darstellt, und erhöht diesen dann über die Zeit. Beim Rendern des Objektes nutzt man diesen Wert gerundeten (Indizes müssen ja Ganzzahlwerte sein) als Index in das Texturenarray :<br />
<br />
<pascal>glBindTexture(GL_TEXTURE_2D,Frame[Round(CurrentFrame)]);<br />
...<br />
CurrentFrame := CurrentFrame + 0.025 * TimeFactor;<br />
if CurrentFrame > Length(Frame) then<br />
CurrentFrame := 0;</pascal><br />
<br />
Der Vorteil dieser Methode ist das man diese Animationen direkt aus seinem 3D-Programm heraus speichern kann und das die Animationen quasi unendlich lange (wobei sowohl Speicherplatz als auch Speicherausbau der Grafikkarte hier limitierende Faktoren sind) sein können. Nachteil ist der hohe Speicherverbrauch, sowohl auf der Platte als auch im Grafikspeicher, sowie die Tatsache das man dann recht oft die Textur wechseln muss.<br />
<br />
<br />
===Animation in einer einzigen Textur===<br />
''(Projektdatei : openGL2D_demo2)''<br />
<br />
Diese Art der Animation ist wie oben schon angedeutet recht speichersparend und vermindert auch die Zahl der Texturenwechsel. Allerdings ist sie auch aufwendiger zu implementieren und unterliegt einigen Einschränkungen die man bei der Erstellung der Animationstextur berücksichtigen sollte. Statt also jeden Frame der Animation in einer eigenen Datei (und damit später auch Textur, obwohl sich das natürlich auch kombinieren lässt) abzulegen, wird versucht bei dieser Animationsart alle Frames in einem Gitter auf einer Textur anzuordnen.<br />
<br />
Wenn man nicht noch mit der Erstellung komplexer Texturkoordinaten rumfuchteln will, dann sollte man drauf achten dass das gewählte Gitter so hoch wie breit ist und die Texturen an diesem Gitter ausgerichtet sind. So habe ich bei dieser Beispieltextur (bevor sie für das Tutorial etwas verkleinert wurde) die Frames in einem Raster von 256x256 Pixeln untergebracht.<br />
<br />
[[Bild:Explosion_det.jpg|center]]<br />
<br />
Zu sehen sind hier alle Animationsframes einer Explosionstextur aus der Beispielanwendung. Diese wurd mit einem entsprechenden Tool generiert, aber kaum ein 3D-Porgramm kann Animationen direkt in eine einzelne Textur exportieren. Hier muss man sich dann mit passenden Plug-Ins oder Drittprogrammen aushelfen.<br />
<br />
Das "Abspielen" dieser Animation ist nun recht einfach, denn wir erinnern uns ja daran das die Texturkoordinaten in OpenGL immer im Bereich 0 bis 1 liegen, unabhängig (es gibt Ausnahmen, aber interessiert hier nicht) von der Größe unserer Textur. Also müssen wir im Endeffekt nur wissen wie viele Spalten und Zeilen unsere Animationstextur enthält um für jeden Frame die passenden Texturkoordinaten errechnen zu können. Um es konkret zu machen nehmen wir obiges Beispiel. Es besteht aus drei Reihen und drei Spalten, wobei die Animationen von links nach rechts durchlaufen. Also bedeutet dies das unser erster Animationsframe bei S=0 / T=0 beginnt und bei S=1/4 / T=1/4 Ende, Frame 2 beginnt dann bei S=1/4 / T=0 und endet bei S=2/3 / T=1/4, usw. Dies zu errechnen sollte als keine Probleme bereiten, genauso wie das Rendern.<br />
<br />
Doch dürfen wir die Nachteile dieser Animationsart auch nicht verschweigen. Das es oft schwierig ist (wenn man die Objekte und Animationen sowieso von Hand zeichnet dann natürlich nicht) aus den Animationsframes eine einzelne Textur zu machen habe ich bereits gesagt, aber man muss auch wie immer auf die Hardwarelimitationen achten. Denn wenn man viele Animationen in einer Textur unterbringen will, wird diese oft recht groß und selbst auf modernen Karten sollte die Textur nicht größer als 2048x2048 Pixel sein. Wenn wir also ein Objekt der Größe 256x256 haben, bekommen wir im besten Falle (2048 wird auf älteren Karten entweder nicht machbar sein, deren Grafikkartenspeicher fast ganz aufbrauchen, oder zu langsam sein) 64 Animationsframes auf eine Textur. Alles darüber müsste man dann in eine weitere Textur auslagern, was den Verwaltungsaufwand stark erhöhen würde. Ausserdem muss man drauf achten, dass sich die Animationsframes nicht direkt berühren, also das zwischen einem sichtbarer Objektteil in Frame N mindestens ein Pixel Abstand zum sichtbaren Objektteil in Frame N+1 besteht. Denn dadurch das die Textur von OpenGL gefiltert wird, verlaufen Pixel die direkt aneinander liegen ineinander; wodurch dann unschöne Effekte entstehen. Ein weitere (allerdings nicht so gravierender) Nachteil ist die Tatsache dass man solche Animationen nicht über Texturenwiederholung mehrfach auf ein Objekt legen kann. Wenn man bei der erstgenannten Animationsart z.B. statt S=1/T=1 S=2/T=2 nutzt, dann wird dann Animation insgesamt viermal auf dem Quad wiederholt. Da man hier aber über die Texturkoordinaten quasi einen Frame aus der Textur herauspickt ist das nicht möglich, was im Normalfall aber kaum Gewicht haben sollte.<br />
<br />
<br />
===Rotation und Skalierung===<br />
''(Projektdatei : openGL2D_demo3)''<br />
<br />
Wenn man alles selbst zeichnet (also z.B. mit der GDI arbeitet), dann hat man für die Rotation (oder Skalierung) eines Objektes im Normalfall nur zwei Möglichkeiten : Entweder man schreibt sich eigene Routinen die den Pixelsalat rotieren (was meist langsam ist, solange man es nicht in Assembler schreibt) oder man erstellt Bilddateien auf denen die Objekte schon vorrotiert sind. Erstere Methode ist wie gesagt langsam und sieht hässlich aus (es sei denn man filtert auch noch selbst) und die zweite Methode schränkt einen auf die vorberechneten Drehwinkel ein.<br />
<br />
Alle gerade genannten Probleme kann man unter OpenGL dank der hardwarebeschleunigung ad acta legen. Nicht nur dass man dank Hardware unendlich schnell rotieren kann, diese Rotation auch noch frei ist, nein, man bekommt sogar das Filtering dank Hardware umsonst :<br />
<br />
[[Bild:Tutorial_2D_illustration_10.jpg|center]]<br />
<br />
Links ist das um 15° gegenüber dem Ausgangsbild gedrehte Raumschiff ohne Filtering zu sehen. Gut ist hier die dadurch entstehende "Körnung" erkennbar, die in Bewegung noch sehr viel unschöner aussieht. Rechts sieht man das gleiche, diesmal allerdings mit Filtering. Der Unterschied dürfte direkt auffallen, und besonders auf hellen Hintergründen ist er noch frapierender. Natürlich sollte man die Grundlagen von OpenGL beherrschen um die Rotation korrekt anwenden zu können, denn folgender Code :<br />
<br />
<pascal>glRotatef(45, 0,0,1);<br />
glTranslatef(320, 240, 0);<br />
DrawQuad(0,0,0, 256,256);</pascal><br />
<br />
Bewirkt etwas total anderes als folgender Code :<br />
<br />
<pascal>glTranslatef(320, 240, 0);<br />
glRotatef(45, 0,0,1);<br />
DrawQuad(0,0,0, 256,256);</pascal><br />
<br />
Wer sich die OpenGL-Grundlagen bereits angeeignet hat, der wird sicher schnell erkannt haben wo der Unterschied liegt. Denn in OpenGL bewegt man Objekte nicht direkt, sondern immer nur den Ursprung der Matrix. Ersterer Code rotiert also unsere Matrix um 45° und verschiebt diese dann um 320 Einheiten auf der X- und 240 auf der Y-Achse, was dazu führt das unser Objekt in einem recht großen Kreis um den Ursprung rotiert wird. Der zweite Codeschnippsel hingegen verschiebt unser Objekt an den Punkt 320/240 (in unserem Falle genau die Bildschirmmitte) und rotiert dann dort unser Objekt um die eigene Achse. Das ist auch der Rotationscode den wir im Normalfall nutzen um ein Objekt um sich selbst rotieren zu lassen. In unserer 2D-Anwendung müssen wir auch prinzipiell nur um die Z-Achse rotieren, denn die ragt in OpenGL "in" den Bildschirm hinein. Um das nachzuvollziehen einfach mal den rechten Arm grade ausstrecken und "um" den ausgestreckten Zeigefinger rotieren lassen. Euer Arm stellt dann in diesem Falle die Z-Achse dar.<br />
<br />
Das Skalieren eines Objektes gestaltet sich noch leichter, denn hier muss man nichts weiter machen als vor dem Rendern des Quads ein glScalef aufzurufen und diesem dann den Skalierungsfaktor (1=keine Skalierung) pro Achse zu übergeben. Da wir hier allerdings nur in 2D arbeiten sollten wir die Z-Skalierung immer bei 1 belassen :<br />
<br />
<pascal>glScalef(2,2,1);<br />
DrawQuad(0,0,0, 256,256);</pascal><br />
<br />
Obiger Quellcode zeichnet unser Objekt in der doppelten Größe (also statt 256x256 Einheiten 512x512 Einheiten groß). Über die Skalierung kann man schön in 2D einen Tiefeneffekt realisieren, zum Beispiel ein Raumschiff das mit einer Skalierung von 0,5 /0,5 / 1 auf einem Träger startet und dann zu Beginn des Levels langsam in Richtung 1 / 1 / 1 skaliert wird. Dadurch wird das Schiff größer und es entsteht beim Betrachter der Eindruck, das Schiff würde hinaufsteigen.<br />
<br />
<br />
==Scrollen und zoomen==<br />
''(Projektdatei : openGL2D_demo4)''<br />
<br />
Selbst viele kommerzielle 3D-Spiele benötigen oft einen 2D-Teil um solche Sachen wie Übersichtskarten (man will ja gerne wissen wo man hin will) oder evtl. Credits und ähnliche größere 2D-Anzeigen zu tätigen, die oft den ganzen Bildschirm einnehmen oder gar größer sind. Besonders bei solchen Sachen wie z.B. einer großen Übersichtskarte (sei es nun die Strategiekarte für einen Schlachtplatz im Zweiten Weltkrieg, oder die Übersichtskarte eines Fantasyreiches) sind die Vorteile von OpenGL (wie an dem im Download enthaltenem Beispiel zu erkennen) gut ersichtlich. Das Scrollen wird durch einen einfachen Aufruf an [[glTranslate]]f erledigt (um die außerhalb des sichtbaren Bereichs liegenden Teile der Karte muss man sich dabei keine Sorgen machen), während man das vergrößern der Karte (~zoomen) mit einem einfachem [[glScale]]f vollbringen kann.<br />
<br />
Und dank der Hardwarebeschleunigung wird diese Karte schneller angezeigt als man dies von Hand (oder über andere 2D-APIs je tun) könnte. Auf einer halbwegs modernen Grafikkarte sollten locker an die 1000 FpS drin sein.<br />
<br />
Nachdem man die Übersichtskarte geladen hat, gestaltet sich das Rendern selbiger als sehr einfach :<br />
<br />
<pascal>glBindTexture(GL_TEXTURE_2D, MapTex);<br />
glTranslatef(MapPos.x, MapPos.y, 0);<br />
glScalef(MapScale, MapScale, MapScale);<br />
DrawQuad(0,0,0, MapSize.x,MapSize.y);</pascal><br />
<br />
Sieht schockierend einfach aus. Mehr braucht es nicht um eine Übersichtskarte zu scrollen und zu zoomen. Allerdings sollte man wie bereits einige Male erwähnt wurde auf die Hardwarelimiationen achten. Wenn die Übersichtskarte also allzu groß sein sollte, dann kann man diese ja ohne Probleme unterteilen und als mehrere Quads rendern. Außer der etwas abgeänderten Positionierung der einzelnen Kartenteile ändert sich aber am Grundprinzip nichts.<br />
<br />
==Tiling (Kachelung)==<br />
''(Projektdatei : openGL2D_demo5)''<br />
<br />
Die oben genannte Technik mag zwar gut genug für statische Übersichtskarten sein, aber wenn man eine dynamische Karte aus der Vogelperspektive realisieren will, so kommt man mit einer riesigen vorgefertigten Textur nicht weit. Dafür gibt es dann aber eine Technik namens "Tiling", was zu Deutsch so viel wie "Kachelung" heißt. Hier hat man ähnlich einem Schachbrett ein zweidimensionales Spielfeld das aus unterschiedlichen Kacheln besteht die als Texturen vorliegen. In einem zweidimensionalem Array wird dann für jedes Feld gespeichert welche Textur zu ihm gehört. Diese Technik ist immer noch recht weit verbreitet (zumindest im Hobbybereich) und für 2D-Spiele aus der Vogelperspektive recht gut geeignet, wobei hier der Großteil der Arbeit eher beim Grafiker als beim Programmierer liegt, denn da die Teils nur Vierecke darstellen müssen Übergänge von z.B. einem Terraintyp zum anderen in die Textur gezeichnet werden. <br />
<br />
Das Rendern eines solchen Spielfeldes gestaltet sich dabei sehr einfach :<br />
<br />
<pascal>for x := 0 to MapWidth-1 do<br />
for y := 0 to MapHeight-1 do<br />
begin<br />
glBindTexture(GL_TEXTURE_2D, MapTexture[Map[x,y]]);<br />
DrawQuad(MapPos.x+x*32, MapPos.y+y*32, 0, 32,32);<br />
end;</pascal><br />
<br />
Allerdings ist obige Methode alles andere als optimiert. Das erste (und größte) Problem ist die Tatsache das unabhängig von aktuellen Betrachtungsposition immer die komplette Karte gerendert wird. Bei großen Karten (z.B. 128x128 Kacheln) gehen dann selbst moderne Karten in die Knie. Die Kacheln außerhalb des Sichtbereiches werden zwar von der Grafikkarte nicht gerendert (da sie außerhalb des Viewports liegen), aber müssen dennoch in jedem Frame über den Bus gesendet werden, genauso wie die Texturenwechsel in jedem Frame stattfinden müssen. Das erste Problem lässt sich recht schnell lösen, in dem wir einfach prüfen ob die aktuell zu rendernde Kachel auch im momentan sichtbarem Bereich liegt :<br />
<br />
<pascal>glScalef(MapScale, MapScale, MapScale);<br />
for x := 0 to High(Map) do<br />
for y := 0 to High(Map[x]) do<br />
if (MapPos.x + x*32 >= -16*MapScale) and (MapPos.x + x*32 <= SizeX/MapScale+16) and<br />
(MapPos.y + y*32 >= -16*MapScale) and (MapPos.y + y*32 <= SizeY/MapScale+16) then<br />
begin<br />
glBindTexture(GL_TEXTURE_2D, MapTex[Map[x,y]]);<br />
DrawQuad(MapPos.x+x*32, MapPos.y+y*32, 0, 32,32);<br />
end;</pascal><br />
<br />
In der If-Abfrage, die wir vor dem Rendern einer jeden Kachel machen, prüfen wir jetzt ganz einfach ob die aktuelle Kachel im Sichtfeld liegt. Da wir eine dynamische Karte haben und die Vorteile von OpenGL nutzen wollen, müssen wir bei dieser Abfrage natürlich auch den Zoomfaktor (''MapScale'') und das Scrolling (''MapPos'') mit einbeziehen.<br />
<br />
Dank dieser offensichtlichen Optimierung dürften wir jetzt einen starken Performancegewinn von mehreren hundert Prozent sehen, denn sehr viele Texturenwechsel fallen weg und es wird sehr viel weniger Geometrie über den Bus gesendet. Aber natürlich sind wir noch nicht am Ende der Fahnenstange angelangt, denn wie gesagt sind Texturenwechsel recht performancelastig. Also liegt es nahe diese zu optimieren :<br />
<br />
<pascal>for t := 0 to High(MapTex) do<br />
begin<br />
glBindTexture(GL_TEXTURE_2D, MapTex[t]);<br />
for x := 0 to High(Map) do<br />
for y := 0 to High(Map[x]) do<br />
if Map[x,y] = t then<br />
if (MapPos.x + x*32 >= -16*MapScale) and (MapPos.x + x*32 <= SizeX/MapScale+16) and<br />
(MapPos.y + y*32 >= -16*MapScale) and (MapPos.y + y*32 <= SizeY/MapScale+16) then<br />
DrawQuad(MapPos.x+x*32, MapPos.y+y*32, 0, 32,32);<br />
end;</pascal><br />
<br />
Wie zu sehen haben wir unsere Schleife etwas umgebaut. Wir gehen jetzt in der Hauptschleife durch alle unsere Kacheln (abgelegt in ''MapTex'') und dann für jede Kachel die komplette Karte in beide Dimensionen durch. So binden wir jede Texur nur einmal und rendern dann alle Kacheln die diese Textur nutzen. Das klingt erstmal so (zumindest vom Schleifenaufbau her) als würden wir hier Performance vernichten, denn schliesslich gehen wir jetzt nicht einmal die komplette Karte durch, sondern so viele Male wie wir Kacheltexturen haben. Allerdings ist es meistens so das der CPU-Overhead durch die zusätzlichen Schleifendurchläuft weniger kostet als uns die gesparten Texturenwechsel eingebracht haben. Im Normalfall (es gibt natürlich Situationen wo der CPU-Overhead zu groß wird, z.B. bei wirklich riesigen Karten) sollten wir auch durch diese Optimierung einen Performancezuwachs im zweistelligen Prozentbereich erkennen.<br />
<br />
So viel also zum Thema Kachelung. Besonders für kleinere Projekte ist diese Technik ein guter Start und vor allem ist diese Technik leicht umzusetzen. Ist natürlich nicht mehr zeitgemäß, aber bei den meisten Hobbyprojekten geht es ja eher um den Inhalt und nicht nur um das Äußere. <br />
<br />
<br />
==Spezialeffekte==<br />
<br />
Nach diesem langen Marsch sind wir jetzt so ziemlich mit allen im 2D-Bereich verwendeten Techniken durch und können uns nun einem interessanterem Kapitel zuwenden; den Spezialeffekten. Hier ist der eigenen Kreativität natürlich keine (oder kaum, je nach Hardware) Grenze gesetzt, weshalb ich nur einige häufig unter 2D genutzte Effekte erwähnen möchte.<br />
<br />
===Beleuchtung ("Per-Pixel")===<br />
''(Projektdatei : openGL2D_demo6)''<br />
<br />
Beleuchtung ist ja (auch wenn man das nicht so wahrnimmt, da es ja im echten Leben selbstverständlich ist) eine Sache die man in keinem Spiel vernachlässigen sollte. Ohne korrekte Beleuchtung geht viel Atmosphäre flöten, aber besonders in einer 3D-Umgebung ist korrektes Per-Pixel-Licht (die OpenGL-Beleuchtung arbeitet auf Vertexbasis) oft schwer zu implementieren (was dank Shader aber leichter geworden ist). In einer 2D-Umgebung ist das aber zum Glück sehr viel einfacher, denn dort haben wir z.B. im Falle einer 2D-Karte ja keine dritte Dimension um die wir uns kümmern müssten, müssen unsere Lichtquellen also nicht auf die Szene draufprojizieren.<br />
<br />
In 2D geht das also ganz einfach und wir machen die Beleuchtung bequem über eine Lichttextur in der die Intensität der Lichtquelle abgelegt ist. Für die Demo habe ich dazu einen einfachen radialen Verlauf gewählt, aber der Fantasie sind da keine Grenzen gesetzt :<br />
<br />
[[Bild:Tutorial_2D_illustration_12.jpg|center]]<br />
<br />
Farbinformationen speichern wir nicht in der Textur, denn wir wollen ja dynamisch bleiben und realisieren die Färbung des Lichtes später in unserem Programm über einen [[glColor]]3f Aufruf. Bevor wir allerdings loslegen, sollten noch ein paar Kleinigkeiten im Bezug auf diese (einfache Form der) Beleuchtung geklärt werden. Damit wir unsere Szene mit einer solchen Textur "beleuchten" können, nutzen wir additives Blending. Das bedeutet also das wir zur Farbe im Framepuffer (der unsere Karte zeigt) den Farbwert in unserer Lichttextur addieren. Dort wo unsere Textur also komplett weiß ist, haben wir auf der Karte die volle Lichtfarbe. Blending hat aber einen Nachteil, der in 3D oft sehr viel Kopfzerbrechen macht, aber in 2D leicht gelöst werden kann : Blending arbeitet "auf" dem Framepuffer, was also bedeutet das OpenGL in diesem Falle nicht für uns sortiert. Wenn wir dann also eine Lichttextur rendern die nahe am Betrachter ist und dahinter (im Raum) eine zweite Lichttextur rendern die weiter entfernt ist, werden wir diese nicht sehen, da im Framepuffer bereits die erste Lichttextur "liegt" (sprich dort der Z-Wert der ersten Textur abgelegt wurde und die Fragmente der zweiten Textur deshalb aufgrund des Z-Tests verworfen werden). Das ist natürlich nicht korrekt, aber in 2D lässt sich dieses Problem mit dem passenden Tiefentest lösen :<br />
<br />
<pascal>glDeptFunc(GL_ALWAYS);</pascal><br />
<br />
Normalerweise sollte man diese Tiefenfunktion erst setzen bevor man die Lichtquellen zeichnet, sofern man vorher den Tiefenpuffer nutzen will um die Objekte auf seiner Karte in der Höhe zu sortieren. Denn wie der Name vermuten lässt bewirkt diese Form des Tiefentests das Fragmente (="Pixel auf Probe") immer rasterisiert werden, egal ob sie jetzt durch ein bereits im Frampuffer liegendes Fragment verdeckt werden würden oder nicht. Wenn wir mit diesem Tiefentest nun also zwei Lichtquellen übereinander rendern würden, werden diese korrekt angezeigt. Aber wie immer soll euch dieser etwas technisch klingende Text nicht aus dem Konzept bringen, weshalb ich dann hier folgende zwei Bilder reden lasse :<br />
<br />
[[Bild:Tutorial_2D_illustration_13.jpg|center]]<br />
<br />
Links sehen wir zwei sich überlappende Lichtquellen mit einem normalerweise verwendetem Tiefentest ('''GL_LEQUAL''' oder '''GL_LESS'''). Dass die grüne Lichtquelle die zweite verdeckt ist reiner Zufall und liegt wohl daran das diese als erstes gerendert wurde. Wie oben gesagt (vereinfacht) steht dann im Z-Puffer drin das an dieser Stelle (des grünen Lichtes) bereits ein Objekt im Z-Puffer liegt. Danach wird dann irgendwann die zweite (blaue) Lichtquelle gerendert, aber die Stellen an denen bereits steht dass dort im Z-Puffer ein Fragment liegt werden nicht mehr gerendert, aufgrund des Tiefentests. Das zweite Bild ist hingegen korrekt, einzig der Tiefentest wurde auf '''GL_ALWAYS''' umgestellt. Dann werden die mit der Lichttextur belegten Quads immer gezeichnet, egal ob sich an der Z-Position bereits ein Fragment befindet oder nicht, und bei Lichtquellen ist das reichlich egal welche zuerst gerendert wird. Denn ob ich jetzt Blau mit Grün addiere, oder Grün mit Blau ist egal, denn beides ergibt im Ende Türkis. Das dürfte dann hoffentlich verstanden worden sein, wenn nicht dann einfach mal das Beispielprogramm öffnen und den Tiefetest wie hier angesprochen ändern; Probieren geht je bekannter weise meist über Studieren.<br />
<br />
Abschließend auch noch kurz zum verwendeten Blendmodus :<br />
<br />
<pascal>glBlendFunc(GL_ONE, GL_ONE);</pascal><br />
<br />
Wer sich schon mal ein wenig mit dem Thema Blending beschäftigt hat, wird schnell erkennen was hier abläuft. Sowohl der Quellfaktor als auch der Zielfaktor stehen auf '''GL_ONE''', was bedeutet dass bei unserer Blendoperation die bereits im Framepuffer befindliche Farbe mit der Farbe unseres Lichtes addiert wird (additives Blending genannt). Bei einer grünen Lichtquelle haben wir in der Mitte also RGB = 0/1/0 und wenn wir das dann auf einen Pixel legen würden der halb grau ist (RGB = 0,5/0,5/0,5) hätten wir als Ergebnis RGB = 0,5/1/0,5 (1+0,5 ist eigentlich 1,5 , aber OpenGL zwängt Farbwerte in den Bereich 0..1).<br />
<br />
===Texturenverläufe===<br />
''(Projektdatei : openGL2D_demo7)''<br />
<br />
Im Kapitel "Kachelung" habe ich erwähnt, dass es eigentlich Aufgabe des Grafikers ist die Überläufe zwischen zwei unterschiedlichen Kacheln zu erstellen. Allerdings lässt sich das auch mit OpenGL bewerkstelligen, und zwar dank Blending und des Alphakanals. Dazu rendern wir zuerst ein Quad mit der ersten Kachel, und zwar dort wo diese Kachel komplett erscheinen soll mit vollem Alpha (vierter Parameter von [[glColor]]4f = 1) und dort wo später die andere Kachel komplett erscheinen soll mit Alpha = 0. Danach rendern wir an exakt der gleichen Stelle die zweite Kachel, müssen aber natürlich darauf achten das wir den richtigen Tiefentest aktiviert haben damit diese auch sichtbar ist ('''GL_ALWAYS''' oder '''GL_LEQUAL'''), allerdings mit genau umgekehrten Alphawerten und aktiviertem Blending. Fürs Blending nutzen wir als Quellfaktor '''GL_SRC_ALPHA''' und als Zielfaktor dann in logischer Konsequenz '''GL_DST_ALPHA'''. Dadurch wird dann im finalen Ergebnis dort wo die erste Kachel eine Alpha von 1 hat ihr kompletter Farbwert übernommen und dort wo die zweite Kachel einen Alpha von 1 hat deren voller Farbwert. Dazwischen wird in gewohnter Weise interpoliert :<br />
<br />
<pascal>glDisable(GL_BLEND);<br />
glBindTexture(GL_TEXTURE_2D, TileA);<br />
glBegin(gl_Quads);<br />
glColor4f(1,1,1,1); glTexCoord2f(0,0); glVertex3f(-128, -128, 0);<br />
glColor4f(1,1,1,0); glTexCoord2f(1,0); glVertex3f( 128, -128, 0);<br />
glColor4f(1,1,1,0); glTexCoord2f(1,1); glvertex3f( 128, 128, 0);<br />
glColor4f(1,1,1,1); glTexCoord2f(0,1); glvertex3f(-128, 128, 0);<br />
glEnd;<br />
<br />
glEnable(GL_BLEND);<br />
glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);<br />
glBindTexture(GL_TEXTURE_2D, TileB);<br />
glBegin(gl_Quads);<br />
glColor4f(1,1,1,0); glTexCoord2f(0,0); glVertex3f(-128, -128, 0);<br />
glColor4f(1,1,1,1); glTexCoord2f(1,0); glVertex3f( 128, -128, 0);<br />
glColor4f(1,1,1,1); glTexCoord2f(1,1); glVertex3f( 128, 128, 0);<br />
glColor4f(1,1,1,0); glTexCoord2f(0,1); glVertex3f(-128, 128, 0);<br />
glEnd;</pascal><br />
<br />
Allerdings wird das bei komplexeren Kachelübergängen recht aufwendig und endet in jeder Menge Overdraw, da man dann viele Übergänge rendern muss. Aber wer sich mal näher damit beschäftigen will, sollte einen Blick auf das "Verblendet"-Tutorial (Einsteiger) von Phobeus werfen, in dem auch diese Technik hier näher besprochen wird.<br />
<br />
===Schatten===<br />
''(Projektdatei : openGL2D_demo8)''<br />
<br />
Im 3D-Bereich sind Schatten eigentlich immer noch die "Königsdisziplin" für jeden Programmierer, besonders wenn diese volumetrisch und vor allem auch korrekt sein sollen. Meist muss man dazu die Szene analysieren, Umrisse von 3D-Objekten generieren und auch noch diverse Spezialfälle beachten (Stichwort "Betrachter im Schattenvolumen"). In unserer heilen 2D-Welt geht das aber schon wie bei der Beleuchtung sehr viel einfacher und hat mit dreidimensionalen Schatten rein gar nichts gemeinsam.<br />
<br />
Hier gehen wir nämlich ganz einfach hin und täuschen Schatten vor indem wir unser Objekt (das dazu natürlich einen Alphakanal besitzen muss) mittels {{INLINE_CODE|glColor3f(0, 0, 0)}} schwarz machen und dann unter dem eigentlichen Objekt leicht versetzt (die Richtung des Versatzes kann man je nach Lichteinfall verändern) rendern. Über [[glScalef]] passen wir dann noch die Größe des Schattens an um einfach vermitteln zu können wie hoch sich unser Objekt befindet. Wenn wir also den Schatten eines Spielers rendern, dann kann der genauso groß sein wie der Spieler selbst, aber bei einem Flugzeug das hoch über dem Boden fliegt sollte der Schatten schon etwas verkleinert werden um einen Eindruck von der Flughöhe zu vermitteln.<br />
<br />
Rein programmiertechnisch sind Schatten in 2D (auch wenn diese keineswegs physikalisch korrekt sind) also genau das Gegenteil von 3D-Schatten : Einfach und ohne jegliche Hindernisse :<br />
<br />
<pascal>glPushMatrix;<br />
glBindTexture(GL_TEXTURE_2D, ShipTex);<br />
glEnable(GL_ALPHA_TEST);<br />
glAlphaFunc(GL_GREATER, 0.1);<br />
glScalef(0.75, 0.75, 0.75);<br />
glColor3f(0, 0, 0);<br />
DrawQuad(60,-60,1, 256,192);<br />
glPopMatrix;<br />
<br />
glPushMatrix;<br />
glColor3f(1, 1, 1);<br />
glBindTexture(GL_TEXTURE_2D, ShipTex);<br />
glEnable(GL_ALPHA_TEST);<br />
glAlphaFunc(GL_GREATER, 0.1);<br />
DrawQuad(0,0,0, 256,192);<br />
glPopMatrix;</pascal><br />
<br />
Im ersten Block rendern wir unser Objekt etwas verkleinert und nach unten/rechts versetzt, natürlich komplett schwarz. Danach müssen wir nur noch unser Objekt drüberrendern (in gewohnter Weise) und fertig ist unser Schatten :<br />
<br />
[[Bild:Tutorial_2D_illustration_14.jpg|center]]<br />
<br />
<br />
==Schlusswort==<br />
<br />
Das war es also zum Thema 2D in OpenGL. Ich hoffe stark das dieser doch recht ausführliche Bericht so ziemlich alle Bereiche abgedeckt hat und damit auch häufig gestellte Fragen beantwortet. Natürlich war das nicht alles was in diesem Bereich machbar ist, allerdings kann ich hier schlecht auf jeden Effekt eingehen, denn sonst könnte ich das Tutorial gleich als ein einige hundert Seiten starkes Buch verkaufen.<br />
<br />
Doch eines sei noch am Schluss gesagt : 2D ist zwar programmiertechnisch fast immer (besonders bei in 3D komplexen Sachen wie Beleuchtung und Schatten) sehr viel einfacher und man kommt dann auch meist schneller ans Ziel, allerdings ist hier die Erstellung des Inhalts meist aufwendiger. Während z.B. ein 3D-Terrain zwar von der Programmierung her aufwendiger ist, muss man dort am Ende nur eine Terraintextur draufkleben und fertig ist die Sache. In 2D muss man dies hingegen über Kacheln lösen und dann für jeden Übergang verschiedene Kacheln in einem Bildbearbeitungsprogramm erstellen. Oder zum Beispiel das in unserem letzten Beispiel verwendete Raumschiff; wenn man das in 3D über ein 3D-Modell animiert (Keyframes), dann ist es kaum Aufwand was am Modell zu verändern (andere Textur, Form ändern), aber in 2D liegen diese Animationen vorberechnet auf der Platte, und sobald man dann was am Raumschiff ändert, müssen auch alle Animationen geändert werden.<br />
<br />
Wie so oft im Leben muss man also abwägen was jetzt für die eigene Anwendung richtig ist. Essentiell reicht es sich die Frage ''"Ist es für mich aufwendiger in die 3D-Programmierung einzusteigen, oder es ist es aufwendiger den Content für meine 2D-Anwendung zu erstellen"'' zu beantworten, aber das kann ich euch nicht abnehmen, da müsst ihr selbst drauf antworten können. Außerdem sei noch gesagt das die moderne Generation der Computerspieler inzwischen 3D gewohnt ist, was dazu führt das 2D-Anwendungen dann spielerisch sehr überzeugen müssen.<br />
<br />
<br />
In dem Sinne also viel Spaß in der zweiten Dimension (die auch Spaß machen) kann. Und vergesst nicht : Ich WILL Feedback zu diesem Tutorial und außerdem will das DGL-Team sehen was ihr so mit unseren Tutorials auf die Beine stellt. Wenn dabei also was halbwegs brauchbares raus gekommen ist, lasst es uns wissen!<br />
<br />
<br />
Euer<br />
:'''Sascha Willems''' (webmaster AT delphigl.de)<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[Tutorial_Matrix2]]}}<br />
<br />
[[Kategorie:Tutorial|2D]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Lektion_4&diff=14551Tutorial Lektion 42005-11-25T21:12:14Z<p>Akira: /* Texturen, Tapeten und Ihre Tücken */</p>
<hr />
<div>=Texturen, Tapeten und Ihre Tücken=<br />
<br />
==Vorwort==<br />
Hi Leute,<br />
kaum zu glauben, aber wahr. Dieses Tutorial wird ausnahmsweise mal etwas mehr Erholung sein. Zumindest am Anfang. Was wir bisher erreicht haben ist ja alles schön, nett, praktisch und auch wichtig als Grundlage, aber wenn wir aus dem Fenster sehen, hören wir die Vöglein zwitschern. Hö? Wir hören was sehend? Es ist tiefste Nacht? Es muss Frühling sein ^__^.<br><br />
Und was macht man da Normalweise? Wir kramen herum und machen einen großen Frühjahrsputz... die Schränke abziehen und alles schön sauber und ordentlich machen. Hach... es dürstet mich richtig danach... :D (means... *würg*).<br><br />
Da unsere Tutorials bisher Gott sei Dank keine dreckige Sache waren, werden wir das mit dem Saubermachen einfach mal wegfallen lassen und uns nur mit einem Tapetenwechsel begnügen. In der Tat werden wir ab jetzt unseren Tutorials mehr grafisches Gewicht zumuten. Endlich haben die blauen Dreiecke ein Ende! Ich wünsche Euch viel Spaß ;).<br />
<br />
<br />
==Crash-Kurs im Handwerk des Tapezierens==<br />
<br />
===Ich verstehe nur "Renovierung"?===<br />
Ich finde es immer wieder erschreckend Leute im Internet zu treffen, die nicht wissen was eine Textur ist... ich meine, kennen keinen Kafka, keine Quantenphysik und wissen nicht einmal wo man die Systemsteuerung findet. Wie soll man solch einem Menschen erklären, was eine Textur ist?<br />
Nun, am Besten fangen wir mit einem möglichst praktischen Beispiel an. Texturen sind wie... Tapeten. Wir blicken nun zu unserer linken Seite. Der Autor erwartet nun ein DirectMind-Uplink zum Empfangen der visuellen Bildsignale. Dort haben wir eine schöne Wand... quadratisch in ihrer Form. Wollen wir diese mit einer Tapete verzieren, gehen wir in den nächsten Baumarkt, suchen uns eine hübsche aus und bringen diese an der Wand an. Natürlich haben wir eine große geholt, und fangen nicht mit kleinen Streifen an.<br><br />
Wenn dann alles geklappt hat stehen wir vor dieser Wand und begutachten die Tapete in voller Pracht direkt vor uns. Wir haben die Wand texturiert. Streng genommen machen wir auch bei OpenGL nichts anderes, als ein Bild zu laden und dieses auf eine Fläche zu kleben. Wir werden uns jedoch auf Dauer nicht damit begnügen immer nur quadratische Flächen zu texturieren, sondern durchaus auch mal Dreiecke oder andere Vielecke. Und auch hat man uns Werkzeuge in die Hand gegeben, um diese Textur noch nachträglich an der Wand zu verschieben, ohne dass wir sie abnehmen müssen. Ist doch super. Es lebe die virtuelle Welt!<br />
<br />
===Tapezieren leicht gemacht!===<br />
Okay, wir haben lange genug um den heißen Brei herumgeredet und sollten uns nun endlich auf die Arbeit stürzen. Jeder von uns sollte in der Lage sein, ein Quadrat in OpenGL genau vor unserer Nase zu erzeugen.<br />
<br />
[[Bild:Tutimg_lektion4_nonetex.gif|thumb|256px|left|Leeres Quadrat]]<br />
Spätestens nun sollte die gleiche Frage aufkommen, wie bei jedem, der zum ersten Mal versucht, eine Tapete an die Wand zu bringen: "Wie rum muss ich das Ding da befestigen?". Immerhin müssen wir uns nicht um den Leim kümmern, das erledigt unsere Grafikkarte bzw. OpenGL für uns ;).<br />
<br />
Wer noch nie 3D programmiert hat, wird im ersten Moment vielleicht fälschlicherweise denken, dass man die Position der Textur per Weltkoordinaten definiert. Das hätte allerdings fatale Folgen, sobald sich das Objekt im Raum bewegt. Wir müssten die Position jedes mal neu berechnen. Um eben dieses Problem zu umgehen, geht man in der 3D-Programmierung einen anderen Weg. Man vergibt einfach für jede Ecke eines Objektes eine Koordinate der Textur. OpenGL errechnet dann aus diesen Texturkoordinaten das Stück der Textur, das dann über das gesamt Objekt projiziert wird.<br />
<br />
Die Rede ist hierbei vom so genannten UV-Mapping, welches vor allem bei Anfängern ein leichtes Gefühl des Unbehagens auslösen sollte. Es ist jedoch nur halb so wild, wenn man es halbwegs verstanden hat.<br />
<br />
Betrachten wir gleich mal folgendes Bild und versuchen, uns in die Problematik hineinzuversetzen. Da Texturen unterschiedliche Größen haben können wurden so genannte Texturkoordinaten eingeführt. Es ist total egal, wie groß eine Textur ist, da sie nur einen Wertebereich von 0 bis 1 haben kann. Das heißt, wenn eine Textur 256x256 groß ist und eine andere 512x512, so haben beide eine maximale Größe von 1. Wir brauchen also das UV-Mapping nicht verändern, selbst wenn ein Objekt eine neue Textur mit anderer Größe erhält!<br />
<br />
[[Bild:Tutimg_lektion4_texuv.gif|thumb|256px|right|Textur und leere Quadratfläche]]<br />
Auf diesem Bild sehen wir zum einen die Textur, die wir verwenden wollen, zum anderen das Objekt, auf des "geklebt" werden soll. Natürlich sieht das in der Realität nicht so aus, ich denke aber diese Darstellung erleichtert das Verstehen ;).<br />
<br />
Der untere linke Bereich der Textur trägt die Texturkoordinaten von (0 / 0) (d.h. u = 0 und v = 0), der obere linke Bereich (0 / 1), der obere rechte Teil (1 / 1) und schließlich der untere rechte Bereich (1 / 0). Das heißt, alles was wir machen müssen, um auf unser Objekt eine Textur zu kleben, ist dem jeweiligen Eckpunkt unseres Quadrates die entsprechende Texturkoordinate zuzuweisen. Wobei in diesem Sinne korrekt in Anführungszeichnen stehen sollte. Es gibt kein falsches UV-Mapping! Man kann tolle Sachen mit diesen Textur-Koordinaten machen und so z.B. auch eine Textur auf einem Objekt spiegeln. Dafür müssten wir in unserem Beispiel nur die linken und rechten UV-Mapping vertauschen und z.B. für den zweiten Punkt die Koordinaten von (1 / 1) setzen und dafür beim dritten (0/ 1). Genauso würden wir auch die unteren vertauschen. Die UV-Koordinaten, wie sie oben angegeben sind, bedeuten nur, dass die Textur, so wie sie in der Datei vorkommt, auch auf das Objekt geklebt wird. Selbstverständlich ist es auch möglich eine Textur gekachelt aufkleben indem Ihr Texturkoordinaten > 1 vergebt, ebenso wie es möglich ist nur Teile einer Textur zu verwenden. Spielt ruhig ein wenig damit herum, und schaut Euch an was passiert! Auf einige tolle Spielerei kommen wir zum Schluss noch mal zurück :).<br />
<br />
[[Bild:Tutimg_lektion4_texuv_02.gif|thumb|256px|right|Textur und leere Quadratfläche]]<br />
Sicherlich brennt es einigen von Euch nun bereits unter den Finger und Ihr fragt "wie kann ich denn die UV-Koordinaten den Eckpunkten der Fläche zuweisen?". Nun, zunächst funktioniert das ähnlich wie bei allen Dingen unter OpenGL. Wir setzen eine UV-Koordinate und solange wir nichts verändern, werden alle Punkte mit diesen Koordinaten versehen, bis wir andere Instruktionen geben. In unserem Fall müssen wir dies natürlich nach jedem Punkt machen. Die Funktion, die dafür verwendet wird heißt glTexCoord. Um also eine Textur auf unserem Quad zu projizieren, benötigen wir folgenden Code:<br />
<br><br />
<br><br />
<br><br />
<br><!-- Zeilen umbrüche sind drinnen damit das Bild nicht die Code-Boc verunstaltet --><br />
<br><br />
<pascal>glBegin(GL_QUADS);<br />
glTexCoord2f(0,0); glVertex3f(-1,-1,0);<br />
glTexCoord2f(0,1); glVertex3f(-1,1,0);<br />
glTexCoord2f(1,1); glVertex3f(1,1,0);<br />
glTexCoord2f(1,0); glVertex3f(1,-1,0);<br />
glEnd;</pascal><br />
<br />
Das klappt doch wunderbar oder? Damit wir die Textur aber auch wirklich genießen können müssen wir Texturing mit Hilfe von glEnable und dem Token GL_TEXTURE_2D aktivieren. Mit Hilfe von glDisable und demselben Token ist es dann auch möglich Objekte zu Zeichnen, die über keine Texturen verfügen. Andernfalls würde nämlich die zuletzt gesetzte Textur und die Texturkoordinaten des letzten glTexCoord-Aufrufs verwendet werden.<br />
<br />
Doch eine Kleinigkeit habe ich Euch nun doch noch verschwiegen! Wir müssen natürlich noch die richtige Textur setzen, damit OpenGL überhaupt weiß, was auf diesem Quad gezeichnet werden soll. Dies ist an sich noch relativ einfach, allerdings müssen wir diese auch noch in den Speicher bekommen, damit sie überhaupt zur Verfügung steht. Nun wird's theoretisch ;).<br />
<br />
===Tapeten besorgen===<br />
Ich werde jetzt nicht näher darauf eingehen, wie man Texturen erstellt, vielleicht gibt's ja einige Photoshop-Experten unter Euch, die Lust haben, einige Ihrer Tricks den anderen in Form eines Tutorials zu zeigen?<br />
<br />
Zunächst einmal müssen wir die uns die eigentlichen Bilddaten besorgen. Wir werden das jetzt in diesem Tutorial mit [[SDL]] machen es gibt jedoch auch die möglichkeit die Daten manuell zu laden das könnt ihr hier nachlesen:<br />
* [[TGA Bilder laden]]<br />
Es gibt allerdings auch Textur loader die euch die nächsten Kapitel abnehmen und das alles für euch machen. glBitmap ist so ein Loader, mehr dazu erfahrt ihr in dem [[Glbitmap_loader|glBitmap]]-Artikel.<br />
<br />
Bei SDL rufen wir nur [[IMG_Load]] auf und prüfen dann ob das Laden erfolgreich war. Hierbei sei erwähnt, dass es unter Linux zu Problemen führen kann, wenn ein Programm nicht aus einer Konsole heraus gestartet wurde. In diesem Fall sind die Pfade zu den Texturen nämlich falsch gesetzt und das Laden würde fehlschlagen. Abhilfe schafft man durch das Verwenden von absoluten Pfaden.<br />
<br />
<pascal>uses SDL, SDL_Image;<br />
<br />
var <br />
tex : PSDL_Surface;<br />
begin<br />
tex := IMG_Load(ExtractFileDir(paramStr(0))+'/wiki.jpg');<br />
if assigned(tex) then<br />
begin</pascal><br />
<br />
Das war es dann auch schon... wir haben die Textur im Speicher Doch was nun?<br />
<br />
===Texturen richtig zubereitet===<br />
Nachdem sich unsere Textur nun im Speicher des Computers befindet, geht es darum, daraus auch eine richtige Textur zu machen, damit wir diese in OpenGL anzeigen können. Bisher befinden sich ja nur die Rohdaten im Speicher! Hierfür teilen wir OpenGL mit, dass wir eine neue Textur erzeugen wollen:<br />
<br />
<pascal>glGenTextures(1, @TexID);</pascal><br />
<br />
TexID ist in diesem Fall ein gluInt, kann aber genauso gut ein Array davon sein, um mehrere Texturen zu erzeugen. Genau dafür wird dann auch der erste Parameter verwendet, der OpenGL mitteilt, wie viele Texturen in dieses Array geschrieben werden sollen. In unserem Fall ist dies eben nur ein Element. Aber was ist ist das für ein Wert in TexID? OpenGL verwaltet die Texturen anhand eindeutiger Namen. glGenTextures ermittelt einen oder mehrere bisher ungenutzte Namen und schreibt diese in TexID. Durch TexId können wir unsere Textur ab sofort also eindeutig identifizieren.<br />
<br />
<pascal>glBindTexture(GL_TEXTURE_2D, TexID);</pascal><br />
<br />
Wir teilen OpenGL mit, dass sich von nun an alle Änderungen und Anweisungen, die sich auf Texturen beziehen auf die Textur TexID beziehen.<br><br />
Die folgenden beiden Zeilen sind zwar nicht wirklich nötig, um eine Textur zu erzeugen aber glaubt mir, sie werden ansonsten potthässlich aussehen. Wir werden in einem anderen Tutorial näher auf dessen Bedeutung eingehen, nämlich den so genannten Textur-Filtern. Die momentane Einstellung ist leicht rechenlastig, jedoch auch von recht guter Qualität. Ihr werdet anfangs keine Probleme mit der Geschwindigkeit bekommen ;).<br />
<br />
<pascal>glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);<br />
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);</pascal><br />
<br />
Zu guter letzt werden müssen wir die Bildinformationen in unserem TBitmap-Objekt irgendwie OpenGL mitteilen. Dies übernimmt die Funktion glTexImage2D:<br />
<br />
<pascal>glTexImage2D(GL_TEXTURE_2D, 0, 3, tex^.w, tex^.h,0, GL_RGB, GL_UNSIGNED_BYTE, tex^.pixels);</pascal><br />
<br />
Der erste Parameter steht für den Typ der Textur. Die Dimension des Typs muss hier mit der des Befehls übereinstimmen (glTexImage2D erlaubt also nur GL_TEXTURE_2D). Der zweite Parameter gibt die Nummer des Level of Detail (LoD) an. Für den Anfang reicht hier der Level 0. Der dritte Parameter gibt an, wie viele Farbkomponenten in dem Bild enthalten sind (1-4). Die zwei folgenden Parameter übermitteln OpenGL die Breite und die Höhe des Bildes. Der sechste Parameter gibt die Breite des Rahmens an. Im siebenten Parameter wird das Format verlangt, in welcher Reihenfolge die einzelnen Farbkomponenten gespeichert sind. Bei Bitmaps ist das immer die Reihenfolge Blau, Grün, Rot. Der Typ, der einzelnen Farbwerte muss im 8. Parameter angegeben werden. Letztendlich müssen im 9. Parameter nur noch die Bildpunkte selbst übergeben werden.<br><br />
Mit Hilfe dieser Funktion sollten nur Texturen der Größe 2^n x 2^n erzeugt werden. Andernfalls werdet Ihr die Textur nicht in Ihrer vollen Schönheit, d.h. überhaupt nicht betrachten können. Es gibt jedoch Möglichkeiten Texturen zu laden, die nicht die Größe 2^n entsprechen. Die Funktion [[gluBuild2DMipmaps]] bildet hier beispielsweise eine Alternative.<br><br />
Das war es auch schon. Wer mehr über die einzelnen Parameter und Befehle wissen will ist herzlich eingeladen in unserem Wiki umherzustöbern und sein Wissen zu erweitern um später selbst vielleicht einmal ein paar Artikel im Wiki zu veröffentlichen.<br />
<br />
Die Daten im Arbeitsspeicher brauchen wir nun nicht mehr. Bei SDL geben wir sie so frei:<br />
<br />
<pascal>SDL_FreeSurface(tex);</pascal><br />
<br />
Fassen wir das ganze bei SDL nochmal zusammen:<br />
<pascal>var<br />
tex : PSDL_Surface;<br />
begin<br />
tex := IMG_Load('./wiki.jpg');<br />
if assigned(tex) then<br />
begin <br />
glGenTextures(1, @texture);<br />
glBindTexture(GL_TEXTURE_2D, texture);<br />
<br />
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);<br />
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);<br />
<br />
// Achtung! Einige Bildformate erwarten statt GL_RGB, GL_BGR. Diese Konstante fehlt in den Standard-Headern<br />
glTexImage2D(GL_TEXTURE_2D, 0, 3, tex^.w, tex^.h,0, GL_RGB, GL_UNSIGNED_BYTE, tex^.pixels);<br />
<br />
SDL_FreeSurface(tex);<br />
end;</pascal><br />
<br />
Nun kommt aber bitte nicht auf die Idee die Textur in euerer Hauptschleife wieder und wieder neu zu laden. Es reicht die Textur einmal zu laden und von da an steht sie einem solange zur Verfügung bis man gedenkt sie wieder aus dem Grafikkartenspeicher zu entfernen.<br><br />
Das übernimmt die Funktion [[glDeleteTextures]]. glDeleteTextures funktioniert ähnlich wie [[glGenTextures]], nur dass die Texturen entfernt werden. Der erste Parameter gibt die Anzahl der zu löschenden Texturen an, während der zweite Parameter den Namen der Textur bzw. ein Array der Namen mehrerer Texturen verlangt.<br><br />
Das ist doch für den Anfang nicht schlecht. Ihr solltet nun in der Lage sein, zumindest einfache Objekte zu texturieren. Das ist eigentlich das gesamte Grundprinzip. Natürlich gestaltet es sich schwieriger, ein komplexeres Objekt mit UV-Koordinaten zu versehen, als ein Quad, aber an der Technik selbst ändert sich nur wenig. Wir werden nun einige Spielereien zeigen, die man mit Texturen machen kann, damit Ihr ein Gefühl dafür bekommt, wie man ein Problem elegant umschiffen kann!<br />
<br />
==Die Rückkehr der Matrizen==<br />
===Texturen===<br />
Tja... und da ich ein Sadist bin [Anm. des Lektors: Ooooh ja!] werden wir uns nun nochmals den Matrizen zuwenden! Dachtet Ihr etwa, Ihr seid die Dinger schon wieder los? Ich habe Euch im letzten Tutorial angedroht, dass es nicht nur eine Matrix für die "Welt" gibt, sondern auch noch weitere. Unter anderem eben auch für Texturen.<br />
<br />
Die Texturmatrix funktioniert streng genommen genauso wie auch die Worldmatrix. Solange wir sie aktiv haben, wird sie von jedem Matrixbefehl berücksichtigt! Einziger wirklicher Unterschied ist, dass sie nicht die Position oder die Form eines Objektes beeinflusst, sondern nur das Rendern der Textur selbst. Hö? Was meint der Kerl bloß damit?! Nun... stellt Euch vor, Ihr habt ein Quad und wollt darauf eine Textur bewegen, so dass es aussieht, als würde sie sich von rechts nach links bewegen! Was wir jedoch nicht wollen ist, dass sich das Objekt bewegt, sondern nur, das was darauf zu sehen ist. Stellt Euch vor, Ihr schaut aus einem Fenster und seht einen wolkigen Himmel, der Wind bläst die Wolken von einer Himmelsrichtung zur anderen. Ihr könntet so z.B. das Fenster als Quad nehmen und dann die Textur darauf zeichnen und glaubt mir, die Illusion würde auffliegen, sobald sich das Fenster mit der Textur bewegen soll. Nein, stattdessen bewegen wir nur die Textur auf dem Quad und zwar ohne das UV-Mapping anzutasten. Wir beeinflussen einfach die Art, wie die Textur auf das Quad projiziert werden soll. Dafür dient die Texturmatrix.<br />
<br />
Stellen wir uns mal vor, dass wir unser Quad zeichnen und eine Variable X haben, die wir bei jedem Rendervorgang leicht erhöhen. Dies soll die Bewegung darstellen. Alles was wir nun tun müssen ist, die Texturmatrix entsprechend anzupassen.<br />
<br />
<pascal>glMatrixMode(GL_TEXTURE);<br />
glLoadIdentity;<br />
glTranslatef(x,0,0);<br />
glMatrixMode(GL_MODELVIEW);<br />
</pascal><br />
<br />
Das ist bereits der ganze Spuk! Wir teilen OpenGL durch glMatrixMode mit, dass sich ab sofort alle Veränderungen der Matrix nur noch auf die Texturmatrix beziehen sollen. Danach setzen wir diese sofort auf ihren Standardwert zurück. Der Grund hierfür sollte Euch von der Worldmatrix noch in Erinnerung sein. Anschließend ändern wir die Position der Textur auf dem Quad. Würden wir X jedes Mal um 1 Einheit erhöhen, so würde diese Operation ohne Effekt bleiben, da wir die Textur immer um ihre ganze Größe nach links projizieren würden. Würden wir X z.B. bei jedem Vorgang um 0.01 erhöhen, so würde die Textur sich langsam von rechts nach links bewegen. Ich denke, Ihr könnt bereits erahnen, welche Parameter dafür verwendet werden, um eine Textur von unten nach oben zu bewegen ;).<br />
<br />
Wichtig ist auf jeden Fall, dass Ihr anschließend mit glMatrixMode wieder die Worldmatrix aktiviert, da sonst alle weiteren Matrixmanipulationen auf die Texturmatrix angewandt werden würden. Denkt nicht, dass die Texturmatrix damit deaktiviert wird! Ab sofort wird auf das Objekt sowohl die World- als auch die Texturmatrix angewendet. Seht Ihr? Das Ganze ist doch gar nicht ganz so schlimm. Es versteht sich auch von selbst, dass Ihr glRotate und glScale ebenfalls darauf anwenden könnt, natürlich auch nach den gleichen Regeln auf die Worldmatrix. Experimentiert am Besten auch etwas mit diesen Einstellungen herum!<br />
<br />
<br />
==Wir tapezieren unsere Welt mal anders==<br />
===Am Fließband===<br />
Eigentlich sollte an dieser Stelle bereits Schluss sein. Ich wurde allerdings während des Schreibens des Tutorials nach einer Sache gefragt und möchte die Chance nutzen, diese Frage zu beantworten und gleichzeitig das angewandte Wissen der vorhergehenden Lektion etwas zu vertiefen. <br />
<br />
Wir haben folgende Problematik: Wir brauchen eine animierte Textur auf einem Objekt. Dies könnte z.B. eine bewegte Lavamasse sein oder irgendwas Glibbriges, was am Boden wabbelt. Oder eben in unserem Fall eine Folge von Zahlen, die wie ein Countdown aufgelistet werden. Sicherlich könnte nun jemand von Euch auf die Idee kommen, viele einzelne Texturen zu laden und diese in einem Array zu speichern. Dies mag auch durchaus sinnvoll sein nicht jedoch, wenn es sich um kleine Bilder handelt (bei uns z.B. 32x32 Pixel).<br />
<br />
Auch bei Bitmap-Fonts würde man nie auf die Idee kommen, für jeden Buchstaben eine einzelne Textur zu verwenden, sondern vielmehr eine Textur mit allen Buchstaben darauf erstellen, da dies u.a. den Ladevorgang erheblich beschleunigt! Klingt einleuchtend oder? Aber wie sollen wir dem Programm mitteilen, welcher Teil der Textur auf welche "Ecke" geklebt werden soll? Nun... auch hierbei heißt des Lösungs Rätsel [Anm. des Lektors: Er macht's schon wieder. Herrlich...] UV-Mapping!<br />
<br />
[[Bild:Tutimg_lektion4_numbers.gif]]<br />
<br />
Dies ist unsere Textur! Sie hat eine Gesamtlänge von 256x32 Bildpunkten und wie man leicht sehen kann, soll sie aus 8 Teilstücken bestehen. Die V-Koordinate können wir in diesem Fall getrost vernachlässigen, weil sie in diesem Fall immer konstant sein wird, weil wir die ganze Höhe der Textur verwenden wollen. Sie wäre nur dann interessant, wenn wir z.B. noch eine zweite Reihe darunter setzen würden. Ich denke jedoch, dass jemand der das gleich folgende verstanden hat, sofort eine Lösung für diese "Problematik" finden wird ;).<br />
<br />
[[Bild:Tutimg_lektion4_numbers2.gif]]<br />
<br />
Wichtig ist, dass wir uns bewusst werden, dass eine Textur beim UV-Mapping immer von 0 bis 1 reicht. Um nun die einzelnen Bilder aus einer Textur auf ein Objekt zu setzen, müssen wir nichts anderes machen, als zu errechnen, an welcher Stelle ein Bild anfängt und wo es aufhört. Nur zur Kontrolle, damit es auch jeder begreift: Würden wir nur die erste Hälfte der Textur auf ein Objekt kleben, so müsste unsere U-Koordinate von 0 bis 0.5 reichen. Die zweite Hälfte hingegen von 0.5 - 1.0. Soweit klingt es doch noch alles logisch oder?<br />
<br />
Genauso müssen wir auch vorgehen, wenn wir einzelne Bilder auf einem Quad abbilden wollen. In unserem Fall müsste die U-Koordinate von 0 bis 1/8 reichen. Das zweite Bildchen hingegen von 1/8 bis 2/8 etc. D.h. wir wissen, dass jedes unserer Bilder 1/8 "Einheiten" lang ist! Und somit haben wir ja bereits eine Lösung für unser Problem. Um das ganze dynamisch auszudrücken: Wir brauchen nur die Größe der Textur durch die Anzahl der Bilder zu teilen. Bevor jemand einen Denkfehler macht: Es ist hierbei ganz egal, wie groß die Textur wirklich ist (hier 256x32 Pixel). Dank OpenGL errechnen wir das UV-Mapping ja in absoluten Größen.<br />
Nun der Code:<br />
<br />
<pascal>PicLength:= 1 / PicCount;<br />
PicPos:=Round(Pic)*PicLength;<br />
glBegin(GL_QUADS);<br />
glTexCoord2f(PicPos, 1); glVertex3f(-1,1,0);<br />
glTexCoord2f(PicPos + PicLength, 1); glVertex3f(1,1,0);<br />
glTexCoord2f(PicPos + PicLength, 0); glVertex3f(1,-1,0);<br />
glTexCoord2f(PicPos, 0); glVertex3f(-1,-1,0);<br />
glEnd;<br />
<br />
Pic:=Pic + MovementValue;<br />
</pascal><br />
<br />
Pic ist in diesem Fall eine Variable vom Typ Single, die langsam erhöht wird. Wir runden den Wert hier, so dass beim Erreichen eines jeden ganzzahligen Wertes das nächste Bild angezeigt wird. Wir multiplizieren die Nummer des anzuzeigenden Bildes mit der Breite eines Bildes, um die Anfangsposition zu erhalten und addieren dann noch eine volle Bildbreite dazu, um die Endposition zu erhalten. Hört sich gewaltig gefährlich an, liegt aber mehr an meinem mangelnden Ausdruck als an der Schwierigkeit dieses Problems ;).<br />
<br />
Im Sample werdet Ihr noch sehen, was passiert, wenn man den Wert nicht rundet. Man erhält in diesem Fall einen "flüssigen" Bildübergang. Letztendlich gibt es viele Möglichkeiten, solche Ideen zu implementieren. Nehmt dies einfach als kleineren Gedankenschub und vor allem: Werdet Euch bewusst, was genau dort passiert! Eine Menge toller Dinge lassen sich mit einem guten UV-Mapping erzielen.<br />
Wer noch etwas umherexperimentieren will kann gern versuchen selbes Ziel mit Hilfe der Texturenmatrix zu erreichen. Die Lösung ist verblüffend einfach.<br />
<br />
===Terraforming mal anders===<br />
Relativ lange habe ich nach einem guten Beispiel für folgende Problematik gesucht: Ich wollte Euch die UV-Koordinaten etwas näher bringen und Euch zeigen, wofür man sie einsetzen kann. Irgendwie wollte mir nichts Interessantes aus meinem Kopf entspringen bis ich irgendwann in einigen alten Programmen von mir rumgewühlt habe und einen alten Terrainrenderer von mir fand. Schon war die Idee da! Wir schreiben ein kleines Programm, das eine ganz simple, unoptimierte Landschaft rendern wird. Das hört sich sicherlich Anfangs relativ gewaltig an, mit etwas Verständnis für die oberen Probleme sollte dies jedoch kein Problem für Euch sein.<br />
<br />
Zuvor allerdings ein paar Gedankenspiele. Zunächst widmen wir uns kurz der Texturiering. Wie würde die simpelste Landschaft aussehen, die wir uns vorstellen können? Richtig! Sie wäre ein einfaches, flach liegendes Quadrat mit einer Textur überzogen, der Wand aus unserem ersten Versuch sehr ähnlich! Dies hat jedoch einen klitzekleinen Nachteil: Wir könnten keine Höhenstufen einbauen und ohne die wäre die Landschaft nur halb so realistisch. Denn wir wollen versuchen, folgende Szene zu zaubern:<br />
<br />
[[Bild:Tutimg_lektion4_landscape.gif|thumb|256px|none|eine Lanschaft]]<br />
Um allerdings Höhen einzubinden, müssen wir die Landschaft in viele kleinen Quads unterteilen, die natürlich an Ihren Eckpunkten unterschiedliche Höhen haben, sich jedoch jeweils einige Punkte teilen. Es versteht sich von selbst, dass diese die gleiche Höhe haben müssen, damit die Landschaft auch durchgängig ist und nicht irgendwelche mysteriösen Löcher darauf erscheinen ;).<br />
Auf folgendem Screenshot kann man dies deutlich erkennen:<br />
<br />
[[Bild:Tutimg_lektion4_landwire.gif|thumb|256px|left|Gitter Ansicht der Landschaft]]<br />
Technisch gesehen ist das Ganze einfach zu realisieren, da wir nur begreifen müssen, dass alle Quads nebeneinander liegen und sich - bis auf die äußeren alle einen Eckpunkt teilen. Nun müssen wir beim Rendern jeden Eckpunkt nur noch vom angrenzenden Quad lesen und fertig ist die Landschaft. Ich will da nicht näher drauf eingehen, weil es sich hier nicht um ein Tutorial zur Landschaftsgestaltung handelt. Der Code sollte sich eigentlich von selbst erklären.<br />
<br />
Vielmehr sollten wir uns einer anderen Problematik widmen! Nämlich wie zur Hölle texturieren wir die Landschaft so, dass sie nicht zur Marke "augenfeindlich" gehört?<br />
<br />
[[Bild:Tutimg_lektion4_landscapeerror.gif|thumb|256px|right|wiederkehrende Landschaft]]<br />
Wenn Euer erster Gedanke dabei "Boah, cool!" ist, dann gibt's eins auf die Finger! Das wollen wir doch nicht... wie sieht den die Landschaft aus. Eine immer wiederkehrende Landschaft -> man erkennt sofort, dass wir hierbei auf jedes Quad die gleiche Textur geklebt haben. Doch wie schaffen wir es nun, dass wir eine Textur über alle Quads ziehen, so dass die ganze Landschaft mit einer Textur überzogen ist anstatt nur über ein einzelnes Feld?<br />
Nun, des Lösungs Geheimnis [Anm. des Lektors: Ich liebe ihn dafür! Andere lösen Rätsel. Er geheimnist Lösungen] sind eben unsere UV-Koordinaten und einige pfiffige Köpfe unter Euch sollten bereits einen ersten Verdacht haben. Denn die erste Idee sollte es sein, den linken unteren Punkt eine UV-Koordinate von (0/0) zu geben und der rechten oberen (1/1).<br />
Alles was wir nun also machen müssen, ist, uns die entsprechenden UV-Koordinaten für die Quads dazwischen auszurechnen und sie dann den Punkten zuzuweisen. Dafür benötigen wir zunächst die Breite eines Quads. Mit normaler Logik lässt sich folgende Aussage aufstellen.<br />
<pascal>qw:=1 / XCount<br />
qh:=1 / YCount;<br />
</pascal><br />
Ein Quad benötigt die Länge von 1 durch die Anzahl der Quads in einer Reihe oder Spalte. Genauso verhält es sich auch mit der Höhe. Nun brauchen wir beim Rendern nur noch dem jeweiligen Quad in einer For-Schleife die entsprechende Koordinate zuzuweisen:<br />
<pascal>U:=1 / XCount;<br />
V:=1 / YCount;<br />
<br />
for y:=0 to YCount-1 do<br />
begin<br />
glPushMatrix;<br />
for x:=0 to XCount-1 do<br />
begin<br />
glBegin(GL_QUADS);<br />
glTexCoord2f(U*x, V*(y+1)); <br />
glVertex3f(0, Map[x,y+1], 0);<br />
glTexCoord2f(U*x, V*y); <br />
glVertex3f(0, Map[x,y], 1);<br />
glTexCoord2f(U*(x+1), V*y); <br />
glVertex3f(1, Map[x+1,y], 1);<br />
glTexCoord2f(U*(x+1), V*(y+1)); <br />
glVertex3f(1, Map[x+1,y+1], 0);<br />
glEnd;<br />
glTranslatef(1,0,0);<br />
end;<br />
glPopMatrix;<br />
glTranslatef(0,0,-1);<br />
end;<br />
</pascal><br />
Das sieht nach einem herben Stück Arbeit aus oder? Aber wenn Ihr Euch das ganze mal aufzeichnet und im Kopf durchspielt, wird der Groschen fallen. Denkt mal etwas darüber nach! Wenn es dann doch noch Probleme mit dem Verständnis geben sollte, wird das nächste Kapitel hoffentlich jedes Missverständnis aus dem Wege räumen ;).<br />
<br />
==Nachwort==<br />
Okay... ich hoffe Ihr fühlt Euch nach der Abarbeitung dieses Tutorials genauso wie ich, nachdem ich es für Euch geschrieben habe... nämlich elend. Irgendwie wurde das immer mehr und ich sagte mir immer wieder "nein, dass ist nicht genug. Das muss genauer und anschaulicher werden". Dies ist auch der Grund dafür, warum dieses eines der größten Tutorials mit den meisten Bildern usw geworden ist. Erst sollte es noch etwas mehr werden und dann in mehrere Tutorials aufgespaltet werden, allerdings glaube ich mit diesem Tutorial einen guten Mittelweg zwischen Information und Totlabern gefunden zu haben. Lasst es mich wissen, wie Ihr dazu steht!<br><br />
Und bevor die Kritiker gleich wieder alle aus Ihren Löchern kommen und mir sagen, dass einige Bereich einfach als gegeben angenommen werden; denen sei nur gesagt, dass wir u.a. auch versuchen Rücksicht auf Leute zu nehmen, die noch nie 3D programmiert haben und vielleicht Eurem Wissen nicht standhalten können. Daher halte ich es hier für wichtiger, Grundlagen wie UV-Koordinaten und den Einsatz von Texturen zu erklären, als ihnen tausende von Zeilen um die Ohren zu hauen, wie sie die Bytes von der Festplatte in den Arbeitsspeicher laden können. Um jedoch keine Wissenslöcher offen zu lassen: Ihr könnt Euch sicher sein, dass spezielle Tutorials folgen werden, die sich speziell mit dem Laden verschiedener Formate beschäftigen und die genaue Interna (was im Hintergrund abläuft) zu erklären versuchen. Auch das Alpha Blending wird hier mehr zu "Show-Zwecken" verwendet und wird zusammen mit dem Z-Buffer genauer erklärt werden! <br><br />
Also bloß keine falsche Hektik! Ich weiß ... einige von Euch sind recht ungeduldig, aber ständiges Nachfragen und Drängeln führt zu nichts. Versucht Fragen lieber ins Forum zu setzen, damit dort ein wenig Leben reinkommt. Denn wenn es dort belebter wird, kommen auch schneller neue Leute und vielleicht sind ja auch welche dabei, die dann das DGL-Team entlasten können. Also nutzt bitte das Forum, anstatt andauernd per ICQ oder Mail irgendwelche Fragen zu stellen! Die Antwort wird auch nicht viel länger auf sich warten lassen. Im Forum jedoch ist alles dokumentiert und auch anderen zugänglich, so dass ich nicht die gleiche Frage bis zu 5 mal am Tag beantworten muss. Sorry aber das geht einem auf die Nerven und vor allem auf die Zeit ;). Schließlich sind wir keine Maschinen sondern Menschen, die auch ein privates Leben haben ^__-.<br />
<br />
Glaubt uns, wir investieren einen sehr großen Teil unserer Zeit in dieses Projekt und solch ein Tutorial lässt sich nicht binnen weniger Tage anfertigen. Jeder der so etwas bereits einmal gemacht hat, wird wissen was ich meine. Es muss ein Konzept her, es müssen Samples geschrieben, Screenshots gemacht werden und ein halbwegs verständlicher Text her. Und nichtsdestotrotz soll es am Ende auch noch passen, einem möglichst großen Publikum Wissen vermitteln, am Besten von Schreib- und Sprachfehlern befreit und in einem halbwegs ansehnlichen HTML-Dokument präsentiert werden. Ein langer Weg ;).<br />
Okay, in diesem Sinne, bis bald ;)!<br />
btw: Vielen Dank an Magellan für die Bereitstellung und Integration seiner "besonderen Lernleistung".<br />
<br />
<br />
Euer<br><br />
'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_lektion3]]|[[Tutorial_lektion5]]}}<br />
<br />
[[Kategorie:Tutorial|Lektion4]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Lektion_4&diff=14550Tutorial Lektion 42005-11-25T21:09:17Z<p>Akira: /* Texturen, Tapeten und Ihre Tücken */</p>
<hr />
<div>=Texturen, Tapeten und Ihre Tücken=<br />
<br />
==Vorwort==<br />
Hi Leute,<br />
kaum zu glauben, aber wahr. Dieses Tutorial wird ausnahmsweise mal etwas mehr Erholung sein. Zumindest am Anfang. Was wir bisher erreicht haben ist ja alles schön, nett, praktisch und auch wichtig als Grundlage, aber wenn wir aus dem Fenster sehen, hören wir die Vöglein zwitschern. Hö? Wir hören was sehend? Es ist tiefste Nacht? Es muss Frühling sein ^__^.<br><br />
Und was macht man da Normalweise? Wir kramen herum und machen einen großen Frühjahrsputz... die Schränke abziehen und alles schön sauber und ordentlich machen. Hach... es dürstet mich richtig danach... :D (means... *würg*).<br><br />
Da unsere Tutorials bisher Gott sei Dank keine dreckige Sache waren, werden wir das mit dem Saubermachen einfach mal wegfallen lassen und uns nur mit einem Tapetenwechsel begnügen. In der Tat werden wir ab jetzt unseren Tutorials mehr grafisches Gewicht zumuten. Endlich haben die blauen Dreiecke ein Ende! Ich wünsche Euch viel Spaß ;).<br />
<br />
<br />
==Crash-Kurs im Handwerk des Tapezierens==<br />
<br />
===Ich verstehe nur "Renovierung"?===<br />
Ich finde es immer wieder erschreckend Leute im Internet zu treffen, die nicht wissen was eine Textur ist... ich meine, kennen keinen Kafka, keine Quantenphysik und wissen nicht einmal wo man die Systemsteuerung findet. Wie soll man solch einem Menschen erklären, was eine Textur ist?<br />
Nun, am Besten fangen wir mit einem möglichst praktischen Beispiel an. Texturen sind wie... Tapeten. Wir blicken nun zu unserer linken Seite. Der Autor erwartet nun ein DirectMind-Uplink zum Empfangen der visuellen Bildsignale. Dort haben wir eine schöne Wand... quadratisch in ihrer Form. Wollen wir diese mit einer Tapete verzieren, gehen wir in den nächsten Baumarkt, suchen uns eine hübsche aus und bringen diese an der Wand an. Natürlich haben wir eine große geholt, und fangen nicht mit kleinen Streifen an.<br><br />
Wenn dann alles geklappt hat stehen wir vor dieser Wand und begutachten die Tapete in voller Pracht direkt vor uns. Wir haben die Wand texturiert. Streng genommen machen wir auch bei OpenGL nichts anderes, als ein Bild zu laden und dieses auf eine Fläche zu kleben. Wir werden uns jedoch auf Dauer nicht damit begnügen immer nur quadratische Flächen zu texturieren, sondern durchaus auch mal Dreiecke oder andere Vielecke. Und auch hat man uns Werkzeuge in die Hand gegeben, um diese Textur noch nachträglich an der Wand zu verschieben, ohne dass wir sie abnehmen müssen. Ist doch super. Es lebe die virtuelle Welt!<br />
<br />
===Tapezieren leicht gemacht!===<br />
Okay, wir haben lange genug um den heißen Brei herumgeredet und sollten uns nun endlich auf die Arbeit stürzen. Jeder von uns sollte in der Lage sein, ein Quadrat in OpenGL genau vor unserer Nase zu erzeugen.<br />
<br />
[[Bild:Tutimg_lektion4_nonetex.gif|thumb|256px|left|Leeres Quadrat]]<br />
Spätestens nun sollte die gleiche Frage aufkommen, wie bei jedem, der zum ersten Mal versucht, eine Tapete an die Wand zu bringen: "Wie rum muss ich das Ding da befestigen?". Immerhin müssen wir uns nicht um den Leim kümmern, das erledigt unsere Grafikkarte bzw. OpenGL für uns ;).<br />
<br />
Wer noch nie 3D programmiert hat, wird im ersten Moment vielleicht fälschlicherweise denken, dass man die Position der Textur per Weltkoordinaten definiert. Das hätte allerdings fatale Folgen, sobald sich das Objekt im Raum bewegt. Wir müssten die Position jedes mal neu berechnen. Um eben dieses Problem zu umgehen, geht man in der 3D-Programmierung einen anderen Weg. Man vergibt einfach für jede Ecke eines Objektes eine Koordinate der Textur. OpenGL errechnet dann aus diesen Texturkoordinaten das Stück der Textur, das dann über das gesamt Objekt projiziert wird.<br />
<br />
Die Rede ist hierbei vom so genannten UV-Mapping, welches vor allem bei Anfängern ein leichtes Gefühl des Unbehagens auslösen sollte. Es ist jedoch nur halb so wild, wenn man es halbwegs verstanden hat.<br />
<br />
Betrachten wir gleich mal folgendes Bild und versuchen, uns in die Problematik hineinzuversetzen. Da Texturen unterschiedliche Größen haben können wurden so genannte Texturkoordinaten eingeführt. Es ist total egal, wie groß eine Textur ist, da sie nur einen Wertebereich von 0 bis 1 haben kann. Das heißt, wenn eine Textur 256x256 groß ist und eine andere 512x512, so haben beide eine maximale Größe von 1. Wir brauchen also das UV-Mapping nicht verändern, selbst wenn ein Objekt eine neue Textur mit anderer Größe erhält!<br />
<br />
[[Bild:Tutimg_lektion4_texuv.gif|thumb|256px|right|Textur und leere Quadratfläche]]<br />
Auf diesem Bild sehen wir zum einen die Textur, die wir verwenden wollen, zum anderen das Objekt, auf des "geklebt" werden soll. Natürlich sieht das in der Realität nicht so aus, ich denke aber diese Darstellung erleichtert das Verstehen ;).<br />
<br />
Der untere linke Bereich der Textur trägt die Texturkoordinaten von (0 / 0) (d.h. u = 0 und v = 0), der obere linke Bereich (0 / 1), der obere rechte Teil (1 / 1) und schließlich der untere rechte Bereich (1 / 0). Das heißt, alles was wir machen müssen, um auf unser Objekt eine Textur zu kleben, ist dem jeweiligen Eckpunkt unseres Quadrates die entsprechende Texturkoordinate zuzuweisen. Wobei in diesem Sinne korrekt in Anführungszeichnen stehen sollte. Es gibt kein falsches UV-Mapping! Man kann tolle Sachen mit diesen Textur-Koordinaten machen und so z.B. auch eine Textur auf einem Objekt spiegeln. Dafür müssten wir in unserem Beispiel nur die linken und rechten UV-Mapping vertauschen und z.B. für den zweiten Punkt die Koordinaten von (1 / 1) setzen und dafür beim dritten (0/ 1). Genauso würden wir auch die unteren vertauschen. Die UV-Koordinaten, wie sie oben angegeben sind, bedeuten nur, dass die Textur, so wie sie in der Datei vorkommt, auch auf das Objekt geklebt wird. Selbstverständlich ist es auch möglich eine Textur gekachelt aufkleben indem Ihr Texturkoordinaten > 1 vergebt, ebenso wie es möglich ist nur Teile einer Textur zu verwenden. Spielt ruhig ein wenig damit herum, und schaut Euch an was passiert! Auf einige tolle Spielerei kommen wir zum Schluss noch mal zurück :).<br />
<br />
[[Bild:Tutimg_lektion4_texuv_02.gif|thumb|256px|right|Textur und leere Quadratfläche]]<br />
Sicherlich brennt es einigen von Euch nun bereits unter den Finger und Ihr fragt "wie kann ich denn die UV-Koordinaten den Eckpunkten der Fläche zuweisen?". Nun, zunächst funktioniert das ähnlich wie bei allen Dingen unter OpenGL. Wir setzen eine UV-Koordinate und solange wir nichts verändern, werden alle Punkte mit diesen Koordinaten versehen, bis wir andere Instruktionen geben. In unserem Fall müssen wir dies natürlich nach jedem Punkt machen. Die Funktion, die dafür verwendet wird heißt glTexCoord. Um also eine Textur auf unserem Quad zu projizieren, benötigen wir folgenden Code:<br />
<br><br />
<br><br />
<br><br />
<br><!-- Zeilen umbrüche sind drinnen damit das Bild nicht die Code-Boc verunstaltet --><br />
<br><br />
<pascal>glBegin(GL_QUADS);<br />
glTexCoord2f(0,0); glVertex3f(-1,-1,0);<br />
glTexCoord2f(0,1); glVertex3f(-1,1,0);<br />
glTexCoord2f(1,1); glVertex3f(1,1,0);<br />
glTexCoord2f(1,0); glVertex3f(1,-1,0);<br />
glEnd;</pascal><br />
<br />
Das klappt doch wunderbar oder? Damit wir die Textur aber auch wirklich genießen können müssen wir Texturing mit Hilfe von glEnable und dem Token GL_TEXTURE_2D aktivieren. Mit Hilfe von glDisable und demselben Token ist es dann auch möglich Objekte zu Zeichnen, die über keine Texturen verfügen. Andernfalls würde nämlich die zuletzt gesetzte Textur und die Texturkoordinaten des letzten glTexCoord-Aufrufs verwendet werden.<br />
<br />
Doch eine Kleinigkeit habe ich Euch nun doch noch verschwiegen! Wir müssen natürlich noch die richtige Textur setzen, damit OpenGL überhaupt weiß, was auf diesem Quad gezeichnet werden soll. Dies ist an sich noch relativ einfach, allerdings müssen wir diese auch noch in den Speicher bekommen, damit sie überhaupt zur Verfügung steht. Nun wird's theoretisch ;).<br />
<br />
===Tapeten besorgen===<br />
Ich werde jetzt nicht näher darauf eingehen, wie man Texturen erstellt, vielleicht gibt's ja einige Photoshop-Experten unter Euch, die Lust haben, einige Ihrer Tricks den anderen in Form eines Tutorials zu zeigen?<br />
<br />
Zunächst einmal müssen wir die uns die eigentlichen Bilddaten besorgen. Wir werden das jetzt in diesem Tutorial mit [[SDL]] machen es gibt jedoch auch die möglichkeit die Daten manuell zu laden das könnt ihr hier nachlesen:<br />
* [[TGA Bilder laden]]<br />
Es gibt allerdings auch Textur loader die euch die nächsten Kapitel abnehmen und das alles für euch machen. glBitmap ist so ein Loader, mehr dazu erfahrt ihr in dem [[Glbitmap_loader|glBitmap]]-Artikel.<br />
<br />
Bei SDL rufen wir nur [[IMG_Load]] auf und prüfen dann ob das Laden erfolgreich war. Hierbei sei erwähnt, dass es unter Linux zu Problemen führen kann, wenn ein Programm nicht aus einer Konsole heraus gestartet wurde. In diesem Fall sind die Pfade zu den Texturen nämlich falsch gesetzt und das Laden würde fehlschlagen. Abhilfe schafft man durch das Verwenden von absoluten Pfaden.<br />
<br />
<pascal>uses SDL, SDL_Image;<br />
<br />
var <br />
tex : PSDL_Surface;<br />
begin<br />
tex := IMG_Load(ExtractFileDir(paramStr(0))+'/wiki.jpg');<br />
if assigned(tex) then<br />
begin</pascal><br />
<br />
Das war es dann auch schon... wir haben die Textur im Speicher Doch was nun?<br />
<br />
===Texturen richtig zubereitet===<br />
Nachdem sich unsere Textur nun im Speicher des Computers befindet, geht es darum, daraus auch eine richtige Textur zu machen, damit wir diese in OpenGL anzeigen können. Bisher befinden sich ja nur die Rohdaten im Speicher! Hierfür teilen wir OpenGL mit, dass wir eine neue Textur erzeugen wollen:<br />
<br />
<pascal>glGenTextures(1, @TexID);</pascal><br />
<br />
TexID ist in diesem Fall ein gluInt, kann aber genauso gut ein Array davon sein, um mehrere Texturen zu erzeugen. Genau dafür wird dann auch der erste Parameter verwendet, der OpenGL mitteilt, wie viele Texturen in dieses Array geschrieben werden sollen. In unserem Fall ist dies eben nur ein Element. Aber was ist ist das für ein Wert in TexID? OpenGL verwaltet die Texturen anhand eindeutiger Namen. glGenTextures ermittelt einen oder mehrere bisher ungenutzte Namen und schreibt diese in TexID. Durch TexId können wir unsere Textur ab sofort also eindeutig identifizieren.<br />
<br />
<pascal>glBindTexture(GL_TEXTURE_2D, TexID);</pascal><br />
<br />
Wir teilen OpenGL mit, dass sich von nun an alle Änderungen und Anweisungen, die sich auf Texturen beziehen auf die Textur TexID beziehen.<br><br />
Die folgenden beiden Zeilen sind zwar nicht wirklich nötig, um eine Textur zu erzeugen aber glaubt mir, sie werden ansonsten potthässlich aussehen. Wir werden in einem anderen Tutorial näher auf dessen Bedeutung eingehen, nämlich den so genannten Textur-Filtern. Die momentane Einstellung ist leicht rechenlastig, jedoch auch von recht guter Qualität. Ihr werdet anfangs keine Probleme mit der Geschwindigkeit bekommen ;).<br />
<br />
<pascal>glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);<br />
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);</pascal><br />
<br />
Zu guter letzt werden müssen wir die Bildinformationen in unserem TBitmap-Objekt irgendwie OpenGL mitteilen. Dies übernimmt die Funktion glTexImage2D:<br />
<br />
<pascal>glTexImage2D(GL_TEXTURE_2D, 0, 3, tex^.w, tex^.h,0, GL_RGB, GL_UNSIGNED_BYTE, tex^.pixels);</pascal><br />
<br />
Der erste Parameter steht für den Typ der Textur. Die Dimension des Typs muss hier mit der des Befehls übereinstimmen (glTexImage2D erlaubt also nur GL_TEXTURE_2D). Der zweite Parameter gibt die Nummer des Level of Detail (LoD) an. Für den Anfang reicht hier der Level 0. Der dritte Parameter gibt an, wie viele Farbkomponenten in dem Bild enthalten sind (1-4). Die zwei folgenden Parameter übermitteln OpenGL die Breite und die Höhe des Bildes. Der sechste Parameter gibt die Breite des Rahmens an. Im siebenten Parameter wird das Format verlangt, in welcher Reihenfolge die einzelnen Farbkomponenten gespeichert sind. Bei Bitmaps ist das immer die Reihenfolge Blau, Grün, Rot. Der Typ, der einzelnen Farbwerte muss im 8. Parameter angegeben werden. Letztendlich müssen im 9. Parameter nur noch die Bildpunkte selbst übergeben werden.<br><br />
Mit Hilfe dieser Funktion sollten nur Texturen der Größe 2^n x 2^n erzeugt werden. Andernfalls werdet Ihr die Textur nicht in Ihrer vollen Schönheit, d.h. überhaupt nicht betrachten können. Es gibt jedoch Möglichkeiten Texturen zu laden, die nicht die Größe 2^n entsprechen. Die Funktion [[gluBuild2DMipmaps]] bildet hier beispielsweise eine Alternative.<br><br />
Das war es auch schon. Wer mehr über die einzelnen Parameter und Befehle wissen will ist herzlich eingeladen in unserem Wiki umherzustöbern und sein Wissen zu erweitern um später selbst vielleicht einmal ein paar Artikel im Wiki zu veröffentlichen.<br />
<br />
Die Daten im Arbeitsspeicher brauchen wir nun nicht mehr. Bei SDL geben wir sie so frei:<br />
<br />
<pascal>SDL_FreeSurface(tex);</pascal><br />
<br />
Fassen wir das ganze bei SDL nochmal zusammen:<br />
<pascal>var<br />
tex : PSDL_Surface;<br />
begin<br />
tex := IMG_Load('./wiki.jpg');<br />
if assigned(tex) then<br />
begin <br />
glGenTextures(1, @texture);<br />
glBindTexture(GL_TEXTURE_2D, texture);<br />
<br />
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);<br />
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);<br />
<br />
// Achtung! Einige Bildformate erwarten statt GL_RGB, GL_BGR. Diese Konstante fehlt in den Standard-Headern<br />
glTexImage2D(GL_TEXTURE_2D, 0, 3, tex^.w, tex^.h,0, GL_RGB, GL_UNSIGNED_BYTE, tex^.pixels);<br />
<br />
SDL_FreeSurface(tex);<br />
end;</pascal><br />
<br />
Nun kommt aber bitte nicht auf die Idee die Textur in euerer Hauptschleife wieder und wieder neu zu laden. Es reicht die Textur einmal zu laden und von da an steht sie einem solange zur Verfügung bis man gedenkt sie wieder aus dem Grafikkartenspeicher zu entfernen.<br><br />
Das übernimmt die Funktion [[glDeleteTextures]]. glDeleteTextures funktioniert ähnlich wie [[glGenTextures]], nur dass die Texturen entfernt werden. Der erste Parameter gibt die Anzahl der zu löschenden Texturen an, während der zweite Parameter den Namen der Textur bzw. ein Array der Namen mehrerer Texturen verlangt.<br><br />
Das ist doch für den Anfang nicht schlecht. Ihr solltet nun in der Lage sein, zumindest einfache Objekte zu texturieren. Das ist eigentlich das gesamte Grundprinzip. Natürlich gestaltet es sich schwieriger, ein komplexeres Objekt mit UV-Koordinaten zu versehen, als ein Quad, aber an der Technik selbst ändert sich nur wenig. Wir werden nun einige Spielereien zeigen, die man mit Texturen machen kann, damit Ihr ein Gefühl dafür bekommt, wie man ein Problem elegant umschiffen kann!<br />
<br />
==Die Rückkehr der Matrizen==<br />
===Texturen===<br />
Tja... und da ich ein Sadist bin [Anm. des Lektors: Ooooh ja!] werden wir uns nun nochmals den Matrizen zuwenden! Dachtet Ihr etwa, Ihr seid die Dinger schon wieder los? Ich habe Euch im letzten Tutorial angedroht, dass es nicht nur eine Matrix für die "Welt" gibt, sondern auch noch weitere. Unter anderem eben auch für Texturen.<br />
<br />
Die Texturmatrix funktioniert streng genommen genauso wie auch die Worldmatrix. Solange wir sie aktiv haben, wird sie von jedem Matrixbefehl berücksichtigt! Einziger wirklicher Unterschied ist, dass sie nicht die Position oder die Form eines Objektes beeinflusst, sondern nur das Rendern der Textur selbst. Hö? Was meint der Kerl bloß damit?! Nun... stellt Euch vor, Ihr habt ein Quad und wollt darauf eine Textur bewegen, so dass es aussieht, als würde sie sich von rechts nach links bewegen! Was wir jedoch nicht wollen ist, dass sich das Objekt bewegt, sondern nur, das was darauf zu sehen ist. Stellt Euch vor, Ihr schaut aus einem Fenster und seht einen wolkigen Himmel, der Wind bläst die Wolken von einer Himmelsrichtung zur anderen. Ihr könntet so z.B. das Fenster als Quad nehmen und dann die Textur darauf zeichnen und glaubt mir, die Illusion würde auffliegen, sobald sich das Fenster mit der Textur bewegen soll. Nein, stattdessen bewegen wir nur die Textur auf dem Quad und zwar ohne das UV-Mapping anzutasten. Wir beeinflussen einfach die Art, wie die Textur auf das Quad projiziert werden soll. Dafür dient die Texturmatrix.<br />
<br />
Stellen wir uns mal vor, dass wir unser Quad zeichnen und eine Variable X haben, die wir bei jedem Rendervorgang leicht erhöhen. Dies soll die Bewegung darstellen. Alles was wir nun tun müssen ist, die Texturmatrix entsprechend anzupassen.<br />
<br />
<pascal><br />
glMatrixMode(GL_TEXTURE);<br />
glLoadIdentity;<br />
glTranslatef(x,0,0);<br />
glMatrixMode(GL_MODELVIEW);<br />
</pascal><br />
<br />
Das ist bereits der ganze Spuk! Wir teilen OpenGL durch glMatrixMode mit, dass sich ab sofort alle Veränderungen der Matrix nur noch auf die Texturmatrix beziehen sollen. Danach setzen wir diese sofort auf ihren Standardwert zurück. Der Grund hierfür sollte Euch von der Worldmatrix noch in Erinnerung sein. Anschließend ändern wir die Position der Textur auf dem Quad. Würden wir X jedes Mal um 1 Einheit erhöhen, so würde diese Operation ohne Effekt bleiben, da wir die Textur immer um ihre ganze Größe nach links projizieren würden. Würden wir X z.B. bei jedem Vorgang um 0.01 erhöhen, so würde die Textur sich langsam von rechts nach links bewegen. Ich denke, Ihr könnt bereits erahnen, welche Parameter dafür verwendet werden, um eine Textur von unten nach oben zu bewegen ;).<br />
<br />
Wichtig ist auf jeden Fall, dass Ihr anschließend mit glMatrixMode wieder die Worldmatrix aktiviert, da sonst alle weiteren Matrixmanipulationen auf die Texturmatrix angewandt werden würden. Denkt nicht, dass die Texturmatrix damit deaktiviert wird! Ab sofort wird auf das Objekt sowohl die World- als auch die Texturmatrix angewendet. Seht Ihr? Das Ganze ist doch gar nicht ganz so schlimm. Es versteht sich auch von selbst, dass Ihr glRotate und glScale ebenfalls darauf anwenden könnt, natürlich auch nach den gleichen Regeln auf die Worldmatrix. Experimentiert am Besten auch etwas mit diesen Einstellungen herum!<br />
<br />
<br />
==Wir tapezieren unsere Welt mal anders==<br />
===Am Fließband===<br />
Eigentlich sollte an dieser Stelle bereits Schluss sein. Ich wurde allerdings während des Schreibens des Tutorials nach einer Sache gefragt und möchte die Chance nutzen, diese Frage zu beantworten und gleichzeitig das angewandte Wissen der vorhergehenden Lektion etwas zu vertiefen. <br />
<br />
Wir haben folgende Problematik: Wir brauchen eine animierte Textur auf einem Objekt. Dies könnte z.B. eine bewegte Lavamasse sein oder irgendwas Glibbriges, was am Boden wabbelt. Oder eben in unserem Fall eine Folge von Zahlen, die wie ein Countdown aufgelistet werden. Sicherlich könnte nun jemand von Euch auf die Idee kommen, viele einzelne Texturen zu laden und diese in einem Array zu speichern. Dies mag auch durchaus sinnvoll sein nicht jedoch, wenn es sich um kleine Bilder handelt (bei uns z.B. 32x32 Pixel).<br />
<br />
Auch bei Bitmap-Fonts würde man nie auf die Idee kommen, für jeden Buchstaben eine einzelne Textur zu verwenden, sondern vielmehr eine Textur mit allen Buchstaben darauf erstellen, da dies u.a. den Ladevorgang erheblich beschleunigt! Klingt einleuchtend oder? Aber wie sollen wir dem Programm mitteilen, welcher Teil der Textur auf welche "Ecke" geklebt werden soll? Nun... auch hierbei heißt des Lösungs Rätsel [Anm. des Lektors: Er macht's schon wieder. Herrlich...] UV-Mapping!<br />
<br />
[[Bild:Tutimg_lektion4_numbers.gif]]<br />
<br />
Dies ist unsere Textur! Sie hat eine Gesamtlänge von 256x32 Bildpunkten und wie man leicht sehen kann, soll sie aus 8 Teilstücken bestehen. Die V-Koordinate können wir in diesem Fall getrost vernachlässigen, weil sie in diesem Fall immer konstant sein wird, weil wir die ganze Höhe der Textur verwenden wollen. Sie wäre nur dann interessant, wenn wir z.B. noch eine zweite Reihe darunter setzen würden. Ich denke jedoch, dass jemand der das gleich folgende verstanden hat, sofort eine Lösung für diese "Problematik" finden wird ;).<br />
<br />
[[Bild:Tutimg_lektion4_numbers2.gif]]<br />
<br />
Wichtig ist, dass wir uns bewusst werden, dass eine Textur beim UV-Mapping immer von 0 bis 1 reicht. Um nun die einzelnen Bilder aus einer Textur auf ein Objekt zu setzen, müssen wir nichts anderes machen, als zu errechnen, an welcher Stelle ein Bild anfängt und wo es aufhört. Nur zur Kontrolle, damit es auch jeder begreift: Würden wir nur die erste Hälfte der Textur auf ein Objekt kleben, so müsste unsere U-Koordinate von 0 bis 0.5 reichen. Die zweite Hälfte hingegen von 0.5 - 1.0. Soweit klingt es doch noch alles logisch oder?<br />
<br />
Genauso müssen wir auch vorgehen, wenn wir einzelne Bilder auf einem Quad abbilden wollen. In unserem Fall müsste die U-Koordinate von 0 bis 1/8 reichen. Das zweite Bildchen hingegen von 1/8 bis 2/8 etc. D.h. wir wissen, dass jedes unserer Bilder 1/8 "Einheiten" lang ist! Und somit haben wir ja bereits eine Lösung für unser Problem. Um das ganze dynamisch auszudrücken: Wir brauchen nur die Größe der Textur durch die Anzahl der Bilder zu teilen. Bevor jemand einen Denkfehler macht: Es ist hierbei ganz egal, wie groß die Textur wirklich ist (hier 256x32 Pixel). Dank OpenGL errechnen wir das UV-Mapping ja in absoluten Größen.<br />
Nun der Code:<br />
<br />
<pascal><br />
PicLength:= 1 / PicCount;<br />
PicPos:=Round(Pic)*PicLength;<br />
glBegin(GL_QUADS);<br />
glTexCoord2f(PicPos, 1); glVertex3f(-1,1,0);<br />
glTexCoord2f(PicPos + PicLength, 1); glVertex3f(1,1,0);<br />
glTexCoord2f(PicPos + PicLength, 0); glVertex3f(1,-1,0);<br />
glTexCoord2f(PicPos, 0); glVertex3f(-1,-1,0);<br />
glEnd;<br />
<br />
Pic:=Pic + MovementValue;<br />
</pascal><br />
<br />
Pic ist in diesem Fall eine Variable vom Typ Single, die langsam erhöht wird. Wir runden den Wert hier, so dass beim Erreichen eines jeden ganzzahligen Wertes das nächste Bild angezeigt wird. Wir multiplizieren die Nummer des anzuzeigenden Bildes mit der Breite eines Bildes, um die Anfangsposition zu erhalten und addieren dann noch eine volle Bildbreite dazu, um die Endposition zu erhalten. Hört sich gewaltig gefährlich an, liegt aber mehr an meinem mangelnden Ausdruck als an der Schwierigkeit dieses Problems ;).<br />
<br />
Im Sample werdet Ihr noch sehen, was passiert, wenn man den Wert nicht rundet. Man erhält in diesem Fall einen "flüssigen" Bildübergang. Letztendlich gibt es viele Möglichkeiten, solche Ideen zu implementieren. Nehmt dies einfach als kleineren Gedankenschub und vor allem: Werdet Euch bewusst, was genau dort passiert! Eine Menge toller Dinge lassen sich mit einem guten UV-Mapping erzielen.<br />
Wer noch etwas umherexperimentieren will kann gern versuchen selbes Ziel mit Hilfe der Texturenmatrix zu erreichen. Die Lösung ist verblüffend einfach.<br />
<br />
===Terraforming mal anders===<br />
Relativ lange habe ich nach einem guten Beispiel für folgende Problematik gesucht: Ich wollte Euch die UV-Koordinaten etwas näher bringen und Euch zeigen, wofür man sie einsetzen kann. Irgendwie wollte mir nichts Interessantes aus meinem Kopf entspringen bis ich irgendwann in einigen alten Programmen von mir rumgewühlt habe und einen alten Terrainrenderer von mir fand. Schon war die Idee da! Wir schreiben ein kleines Programm, das eine ganz simple, unoptimierte Landschaft rendern wird. Das hört sich sicherlich Anfangs relativ gewaltig an, mit etwas Verständnis für die oberen Probleme sollte dies jedoch kein Problem für Euch sein.<br />
<br />
Zuvor allerdings ein paar Gedankenspiele. Zunächst widmen wir uns kurz der Texturiering. Wie würde die simpelste Landschaft aussehen, die wir uns vorstellen können? Richtig! Sie wäre ein einfaches, flach liegendes Quadrat mit einer Textur überzogen, der Wand aus unserem ersten Versuch sehr ähnlich! Dies hat jedoch einen klitzekleinen Nachteil: Wir könnten keine Höhenstufen einbauen und ohne die wäre die Landschaft nur halb so realistisch. Denn wir wollen versuchen, folgende Szene zu zaubern:<br />
<br />
[[Bild:Tutimg_lektion4_landscape.gif|thumb|256px|none|eine Lanschaft]]<br />
Um allerdings Höhen einzubinden, müssen wir die Landschaft in viele kleinen Quads unterteilen, die natürlich an Ihren Eckpunkten unterschiedliche Höhen haben, sich jedoch jeweils einige Punkte teilen. Es versteht sich von selbst, dass diese die gleiche Höhe haben müssen, damit die Landschaft auch durchgängig ist und nicht irgendwelche mysteriösen Löcher darauf erscheinen ;).<br />
Auf folgendem Screenshot kann man dies deutlich erkennen:<br />
<br />
[[Bild:Tutimg_lektion4_landwire.gif|thumb|256px|left|Gitter Ansicht der Landschaft]]<br />
Technisch gesehen ist das Ganze einfach zu realisieren, da wir nur begreifen müssen, dass alle Quads nebeneinander liegen und sich - bis auf die äußeren alle einen Eckpunkt teilen. Nun müssen wir beim Rendern jeden Eckpunkt nur noch vom angrenzenden Quad lesen und fertig ist die Landschaft. Ich will da nicht näher drauf eingehen, weil es sich hier nicht um ein Tutorial zur Landschaftsgestaltung handelt. Der Code sollte sich eigentlich von selbst erklären.<br />
<br />
Vielmehr sollten wir uns einer anderen Problematik widmen! Nämlich wie zur Hölle texturieren wir die Landschaft so, dass sie nicht zur Marke "augenfeindlich" gehört?<br />
<br />
[[Bild:Tutimg_lektion4_landscapeerror.gif|thumb|256px|right|wiederkehrende Landschaft]]<br />
Wenn Euer erster Gedanke dabei "Boah, cool!" ist, dann gibt's eins auf die Finger! Das wollen wir doch nicht... wie sieht den die Landschaft aus. Eine immer wiederkehrende Landschaft -> man erkennt sofort, dass wir hierbei auf jedes Quad die gleiche Textur geklebt haben. Doch wie schaffen wir es nun, dass wir eine Textur über alle Quads ziehen, so dass die ganze Landschaft mit einer Textur überzogen ist anstatt nur über ein einzelnes Feld?<br />
Nun, des Lösungs Geheimnis [Anm. des Lektors: Ich liebe ihn dafür! Andere lösen Rätsel. Er geheimnist Lösungen] sind eben unsere UV-Koordinaten und einige pfiffige Köpfe unter Euch sollten bereits einen ersten Verdacht haben. Denn die erste Idee sollte es sein, den linken unteren Punkt eine UV-Koordinate von (0/0) zu geben und der rechten oberen (1/1).<br />
Alles was wir nun also machen müssen, ist, uns die entsprechenden UV-Koordinaten für die Quads dazwischen auszurechnen und sie dann den Punkten zuzuweisen. Dafür benötigen wir zunächst die Breite eines Quads. Mit normaler Logik lässt sich folgende Aussage aufstellen.<br />
<pascal><br />
qw:=1 / XCount<br />
qh:=1 / YCount;<br />
</pascal><br />
Ein Quad benötigt die Länge von 1 durch die Anzahl der Quads in einer Reihe oder Spalte. Genauso verhält es sich auch mit der Höhe. Nun brauchen wir beim Rendern nur noch dem jeweiligen Quad in einer For-Schleife die entsprechende Koordinate zuzuweisen:<br />
<pascal><br />
U:=1 / XCount;<br />
V:=1 / YCount;<br />
<br />
for y:=0 to YCount-1 do<br />
begin<br />
glPushMatrix;<br />
for x:=0 to XCount-1 do<br />
begin<br />
glBegin(GL_QUADS);<br />
glTexCoord2f(U*x, V*(y+1)); <br />
glVertex3f(0, Map[x,y+1], 0);<br />
glTexCoord2f(U*x, V*y); <br />
glVertex3f(0, Map[x,y], 1);<br />
glTexCoord2f(U*(x+1), V*y); <br />
glVertex3f(1, Map[x+1,y], 1);<br />
glTexCoord2f(U*(x+1), V*(y+1)); <br />
glVertex3f(1, Map[x+1,y+1], 0);<br />
glEnd;<br />
glTranslatef(1,0,0);<br />
end;<br />
glPopMatrix;<br />
glTranslatef(0,0,-1);<br />
end;<br />
</pascal><br />
Das sieht nach einem herben Stück Arbeit aus oder? Aber wenn Ihr Euch das ganze mal aufzeichnet und im Kopf durchspielt, wird der Groschen fallen. Denkt mal etwas darüber nach! Wenn es dann doch noch Probleme mit dem Verständnis geben sollte, wird das nächste Kapitel hoffentlich jedes Missverständnis aus dem Wege räumen ;).<br />
<br />
==Nachwort==<br />
Okay... ich hoffe Ihr fühlt Euch nach der Abarbeitung dieses Tutorials genauso wie ich, nachdem ich es für Euch geschrieben habe... nämlich elend. Irgendwie wurde das immer mehr und ich sagte mir immer wieder "nein, dass ist nicht genug. Das muss genauer und anschaulicher werden". Dies ist auch der Grund dafür, warum dieses eines der größten Tutorials mit den meisten Bildern usw geworden ist. Erst sollte es noch etwas mehr werden und dann in mehrere Tutorials aufgespaltet werden, allerdings glaube ich mit diesem Tutorial einen guten Mittelweg zwischen Information und Totlabern gefunden zu haben. Lasst es mich wissen, wie Ihr dazu steht!<br><br />
Und bevor die Kritiker gleich wieder alle aus Ihren Löchern kommen und mir sagen, dass einige Bereich einfach als gegeben angenommen werden; denen sei nur gesagt, dass wir u.a. auch versuchen Rücksicht auf Leute zu nehmen, die noch nie 3D programmiert haben und vielleicht Eurem Wissen nicht standhalten können. Daher halte ich es hier für wichtiger, Grundlagen wie UV-Koordinaten und den Einsatz von Texturen zu erklären, als ihnen tausende von Zeilen um die Ohren zu hauen, wie sie die Bytes von der Festplatte in den Arbeitsspeicher laden können. Um jedoch keine Wissenslöcher offen zu lassen: Ihr könnt Euch sicher sein, dass spezielle Tutorials folgen werden, die sich speziell mit dem Laden verschiedener Formate beschäftigen und die genaue Interna (was im Hintergrund abläuft) zu erklären versuchen. Auch das Alpha Blending wird hier mehr zu "Show-Zwecken" verwendet und wird zusammen mit dem Z-Buffer genauer erklärt werden! <br><br />
Also bloß keine falsche Hektik! Ich weiß ... einige von Euch sind recht ungeduldig, aber ständiges Nachfragen und Drängeln führt zu nichts. Versucht Fragen lieber ins Forum zu setzen, damit dort ein wenig Leben reinkommt. Denn wenn es dort belebter wird, kommen auch schneller neue Leute und vielleicht sind ja auch welche dabei, die dann das DGL-Team entlasten können. Also nutzt bitte das Forum, anstatt andauernd per ICQ oder Mail irgendwelche Fragen zu stellen! Die Antwort wird auch nicht viel länger auf sich warten lassen. Im Forum jedoch ist alles dokumentiert und auch anderen zugänglich, so dass ich nicht die gleiche Frage bis zu 5 mal am Tag beantworten muss. Sorry aber das geht einem auf die Nerven und vor allem auf die Zeit ;). Schließlich sind wir keine Maschinen sondern Menschen, die auch ein privates Leben haben ^__-.<br />
<br />
Glaubt uns, wir investieren einen sehr großen Teil unserer Zeit in dieses Projekt und solch ein Tutorial lässt sich nicht binnen weniger Tage anfertigen. Jeder der so etwas bereits einmal gemacht hat, wird wissen was ich meine. Es muss ein Konzept her, es müssen Samples geschrieben, Screenshots gemacht werden und ein halbwegs verständlicher Text her. Und nichtsdestotrotz soll es am Ende auch noch passen, einem möglichst großen Publikum Wissen vermitteln, am Besten von Schreib- und Sprachfehlern befreit und in einem halbwegs ansehnlichen HTML-Dokument präsentiert werden. Ein langer Weg ;).<br />
Okay, in diesem Sinne, bis bald ;)!<br />
btw: Vielen Dank an Magellan für die Bereitstellung und Integration seiner "besonderen Lernleistung".<br />
<br />
<br />
Euer<br><br />
'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_lektion3]]|[[Tutorial_lektion5]]}}<br />
<br />
[[Kategorie:Tutorial|Lektion4]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Lektion_3&diff=14549Tutorial Lektion 32005-11-25T21:02:17Z<p>Akira: /* Eine Welt des Grauens */</p>
<hr />
<div>= Eine Welt des Grauens =<br />
== Vorwort ==<br />
Liebe Leser,<br />
nachdem Ihr hoffentlich den kleinen Schock des letzten Tutorials alle gut überstanden habt, freue ich mich, Euch hier wieder begrüßen zu können. Wer dachte, dass die letzte Lektion bereits schwer war, der sollte dringend Urlaub nehmen. Seid gewarnt! Bevor ich jetzt richtig loslege, solltet Ihr einigermaßen entspannt sein. Wenn dies einer der Tage ist, an denen Ihr nur noch durch eine Kippe oder die Flasche Coke am Leben gehalten werdet, ordne ich erstmal eine kleine Zwangspause an ;).<br />
<br />
Der Stoff der jetzt kommt ist sicherlich nicht gerade die leichteste Lektüre. Wer Mathematik studiert, hat Vorteile, wer einigermaßen logisch denken kann auch. Ein Fachidiot, wie ich, hat nur eine Chance... probieren, testen und lernen :). Und weil ich weiß, wie schwer es eventuell sein kann, werde ich nun einfach mal versuchen, ganz simpel anzufangen und das Ganze mit vielen Bildern so gut wie nur irgend möglich zu illustrieren. <br />
<br />
Und wenn Ihr nun Angst habt, dass Ihr das nicht packt, weil ich hier so eine Panik verbreite, dann solltet Ihr mich sehen, wie ich hier verzweifelt vor meinen Tasten hänge und mich frage, wie ich das alles bloß in Worte fassen soll :). Aber was soll es? Ran an den Kram!!! Heulen könnt Ihr anschließend im Forum ;).<br />
== Das Grauen hat einen Namen ==<br />
=== "Kinofilm" vs. "Bahnhof" ===<br />
Wer kennt nicht "Matrix" und hätte gedacht, dass es davon nicht nur eine, sondern unendlich viele gibt die so genannten "Matrizen". Vor allem am Anfang werden diese Dinger Euch das Leben erschweren und Ihr werdet leichte Neigungen tief in Euch verspüren, dass am besten Euer ganzes Projekt sich nur im Zentrum Eurer Welt abspielt (und dies ist nicht im wahrsten Sinne des Wortes gemeint).<br />
<br />
Wer mit Matrizen umgehen kann, beherrscht das Wichtigste an der 3D-Programmierung. Wer nicht zu den Genies zählt, sollte nicht sofort aufgeben, sondern sich viele Beispiele ansehen und viel mit diesen herumspielen.<br />
<br />
Wie auch immer... Ihr solltest wissen, dass es bei jedem Rendervorgang mehrere Matrizen gibt, die ganz drastisch das Aussehen der Szene bestimmen. Es gibt in OpenGL drei Bereiche, in denen Matrizen eingesetzt werden. Die so genannte World- oder Modelviewmatrix, die Texturenmatrix und die Perspektivenmatrix. Die zweifellos wichtigste dieser Matrizen ist die Worldmatrix, da sie die Position Eures "Zeichenstiftes" beschreibt und somit Objekte positioniert. Die Texturenmatrix funktioniert fast genauso wie die Worldmatrix nur definiert sie die Ausrichtung der Texturen. Wir werden später auf sie zurückkommen. Die Perspektivenmatrix definiert wesentliche Eigenschaften des Blickfeldes des Betrachters.<br />
<br />
Der Befehl [[glMatrixMode]] erlaubt eine Änderung der aktuellen Matrix. Mit Hilfe der Konstanten GL_MODELVIEW, GL_TEXTURE oder GL_PERSPECTIVE kann man die Matrix bestimmen. Die Namen der Konstanten sind soweit hoffentlich selbst erklärend. Von nun an bewirken alle Matrixoperationen wie beispielsweise [[glLoadIdentity]] oder [[glTranslate|glTranslate*]] eine Veränderung der aktuell gesetzten Matrix.<br />
=== Die Perspektivenmatrix ===<br />
Wie bereits erwähnt beschreibt die Perspektivenmatrix das aktuelle Sichtfeld. Zum Setzen der Perspektivenmatrix verwendet man in der Regel Befehle wie [[glFrustum]], [[gluPerspective]], [[glOrtho]] oder [[gluOrtho2D]].<br />
<br />
gluPerspective lässt den Raum beispielsweise dreidimensional erscheinen, indem sich die Größe von Objekten mit zunehmender Entfernung vom Betrachter verringert. Diese Verringerung ist abhängig von dem im ersten Parameter übergebenen Winkel. <br />
<br />
glOrtho hingegen ist phantastisch für ein 2D-Blickfeld geeignet, denn diese Art der Projektion enthält keine Tiefe mehr.<br />
<br />
Genauere Informationen zu den einzelnen Funktionen könnt ihr unserem OpenGL-Wiki entnehmen.<br />
<br />
Auf eine Sache möchte ich jedoch noch eingehen: Einige dieser Funktionen verlangen die Parameter "znear" und "zfar". Diese Parameter beschreiben die zwei Schnittflächen, die das Blickfeld vor dem Betrachter begrenzen. "znear" definiert die Entfernung der Nearclippingplane vom Betrachter. Alle Objekte, die sich vor dieser Ebene befinden sind nicht sichtbar. "zfar" beschreibt die Entfernung der Farclippingplane vom Betrachter. Alle Objekte, die sich hinter dieser befinden werden weggeschnitten. Wenn also wieder einmal nur die Hälfte Eurer Szene auf dem Bildschirm sichtbar ist, versucht die Schnittflächen entsprechend anzupassen!<br />
<br />
== Die Worldmatrix ==<br />
=== D3D-Verrat ===<br />
Wenn Ihr ebenfalls zu den D3D-Verrätern gehört (wie z.B. ich *g*), dann solltet Ihr Euch möglichst von eurer Denkweise trennen: Jede Positionierung der Kamera erfolgt über ein Drehen und Bewegen der Szene. Das ist vor allem am Anfang ein wenig gewöhnungsbedürftig ("Die Welt dreht sich! Nicht Du!").<br />
<br />
Vielmehr wird jede Manipulation direkt an die Worldmatrix übergeben. Das heißt, wenn Ihr glTranslate* aufruft, so ist diese Manipulation bereits bei Euch in die Worldmatrix eingetragen, alle weiteren Objekte werden mit dieser Matrix transformiert. Wer zweimal glTranslate*(2, 0, 0) aufruft wird feststellen, dass glTranslate* nicht zwei Matrizen zurückliefert, sondern nur eine, die dann auf (4, 0, 0) verweist. Am Anfang solltet Ihr also sehr darauf achten, es wird eine Weile dauern bis Ihr dies fest in Eurem Herzen tragt. Nicht sofort aufgeben :)!<br />
== Einfach, Kompakt und Funktional ==<br />
Neben dem Befehl glTranslate*, den wir im letzten Tutorial lieben gelernt haben, gibt es noch weitere Befehle, die sich auf die einzelnen Matrizen auswirken und auf die ich in dem folgenden Abschnitt eingehen möchte.<br />
=== Dreh- und Angelpunkt des Geschehens ===<br />
Zunächst beschäftigen wir uns mit der Rotation. Wir versuchen ein Objekt zu erzeugen, welches um 90° um seine eigene Achse gedreht wurde. Das ist nicht sonderlich schwer: <pascal>[[glRotate|glRotatef]](90,0,1,0);</pascal><br />
Ein Objekt auf das wir diese Matrix anwenden, wird um 90° um seine Y-Achse gedreht sein. Daraus schließen wir, dass der erste Parameter den Winkel im Gradmaß um den wir rotieren möchten definiert und die 3 folgenden Parameter den Vektor beschreiben um den die Rotation durchgeführt werden soll. Rotationen bergen tatsächlich eine Schwierigkeit, denn es ist nicht immer einfach ein Objekt um die Eigene Achse zu rotieren. Verschiebt Ihr euer Objekt zuerst und beginnt dann die Rotation, so rotiert das Objekt um die eigene Achse.<br />
<br />
Ruft Ihr jedoch zuerst glRotate* und anschließend glTranslate* auf, so unterscheidet sich erzielte Ergebnis von dem vorherigen.<br />
<br />
Um diesen Problemen aus dem Weg zu gehen so müsst Ihr Euch vorstellen, dass Ihr den Rotationspunkt im Moment des Aufrufs von glRotate* setzt. Verschiebt Ihr Euer Objekt anschließend so bleibt der Rotationspunkt derselbe und das Objekt beginnt den Rotationspunkt auf einer Bahn zu umkreisen. So gesehen gibt es keinen wirklichen Unterschied und die Rotation um die eigene Achse ist ein Ausnahmefall, denn der Rotationspunkt liegt dann in dem von uns erwarteten Mittelpunkt des Objektes.<br />
=== Eine Frage der Größe ===<br />
Nun ja... was kann man denn noch alles in einer 3D-Welt machen? Ihr habt gelernt, wie man Objekte bewegt und diese durch Rotationen ausrichten kann. Das ist doch schon eine Menge zum Spielen oder? Nun, bei einer Transformation kann für uns noch eine Sache sehr wichtig werden, nämlich die Möglichkeit, zu bestimmen in welcher Größe ein Objekt dargestellt werden kann.<br />
<br />
Sicher würde es Sinn machen ein benötigtes Objekt gleich in der richtigen Größe in die Anwendung zu laden. Manchmal ist es aber von Vorteil, wenn man die Größe eines Objektes ohne großen Aufwand verändern kann oder aber mehrere Objekte desselben Typs mit unterschiedlicher Größe darzustellen.<br />
<br />
Eine Größenveränderung ist mit Hilfe des Befehls [[glScale|glScale*]] möglich.<br />
<pascal>glScalef(1,1,1);</pascal><br />
In diesem Fall würden wir das Objekt so zeichnen, wie wir es auch angegeben haben. Das heißt in seiner Originalgröße. Sicherlich ahnst Du bereits, was die Parameter aussagen... sie stehen für das Verhältnis der einzelnen Seiten nach dem Muster X, Y und Z. Würden wir als ersten Parameter statt einer 1 eine 2 einsetzen, würde unser Objekt in seiner räumlichen Ausdehnung bezogen auf der X-Achse doppelt so groß sein. Würden wir stattdessen 0.5 angeben, wäre es nur noch halb so groß.<br />
<br />
In Wirklichkeit bewirkt glScale* jedoch keine Veränderung der Größe sondern eine Verzerrung unseres Koordinatensystems. Daher wirkt jeder Aufruf von glScale* auch auf Befehle wie glTranslate*. Aus Sicht der Matrizen lässt sich dieses Phänomen auch recht leicht erklären. In Wirklichkeit erzeugt jeder Aufruf von glRotate*, glTranslate* oder glScale* eine eigene Matrix, die dann mit der aktuellen Matrix (z.B. der Modelviewmatrix) multipliziert wird. Wurde in der Modelviewmatrix bereits ein glScale* verewigt, so wirkt sich dieses bei der Multiplikation der Matrizen auf die Transformationsmatrix aus.<br />
<br />
Ich denke ich muss nicht näher darauf eingehen, dass es keine gute Idee ist das Koordinatensystem mit einem Aufruf von glScalef(0, 0, 0) zu zerstören. Was würde es auch für einen Sinn ergeben, das Koordinatensystem in einem Punkt unterzubringen? <br><br />
'''''Hinweis''': Es ist bereits gefährlich nur eine Achse auf 0 zu skalieren, denn dadurch wird die aktuelle Matrix beschädigt ([http://www.math.unizh.ch/fachverein/forum/detail.jsp?FORUM=120 singulär]) und Befehle wie [[gluProject]] und [[gluUnProject]] funktionieren nicht mehr.''<br />
<br />
Ich hoffe ich habe Euch nun nicht den letzten Funken Hoffnung OpenGL zu verstehen geraubt. Ich kann Euch versichern, dass dieses Verständnis mit der Zeit heranreifen wird.<br />
[[bild:tutimg_lektion3_skalierung.gif|256px|right]]<br />
glScale* ist aber aufgrund seiner phantastischen Eigenschaften nicht nur in der Lage die Größe von Objekten zu verändern sondern es ist auch möglich Objekte zu spiegeln indem man negative Werte an glScale* übergibt. Somit kann sich z.B. ein D3D-Umsteiger das leben vereinfachen indem er mit einem Aufruf von glScalef(1, 1, -1) sicherstellt, dass die Z-Achse in den Bildschirm hinein positiv verläuft.<br />
<br />
Jeder, der die Funktion der Modelviewmatrix begreifen möchte, dem möchte ich das Programm Matrixcontrol von Lithander (siehe Anhang) ans Herz legen. Es erlaubt Euch bestimmte Änderungen an der Matrix direkt nachzuvollziehen und wenn man mit dem Programm ein wenig umherspielt wird selbst dem letzten ziemlich schnell ein Licht aufgehen. Wenn nicht, dann hilft nur fleißiges Programmieren und Anwenden und irgendwann klatscht es dann laut, wenn die eigene Handfläche auf der Stirn zum stehen kommt ;).<br />
<br />
== Virtuelle Gedächtnisse ==<br />
=== Vom Pushen und Poppen ===<br />
Wer diesen Titel ließt und sich nun zwangsläufig daran erinnert, was er bereits alles in seinem Leben konsumiert hat oder neben wenn er am nächsten Morgen aufgewacht ist, sei entwarnt! Wir reden hier von etwas ganz anderem...<br />
<br />
Wie wir bereits gelernt haben, verändert jeder Aufruf von glTranslate*, glRotate* oder glScale* die Worldmatrix und wirkt sich unmittelbar auf andere Objekte aus. Dies kann durchaus sehr erwünscht sein. Wenn wir allerdings in einer großen Welt verschiedene Objekte positionieren, kann dies schnell zum Fluch werden. Natürlich können wir dem entgegenwirken und zum Beispiel jedes Mal glLoadIdentity aufrufen. Dies hat jedoch den Nachteil, dass die World- Matrix dann eben wieder leer ist und wir z.B. die Camera erst wieder setzen müssen, damit die Objekte an die richtige Stelle transformiert werden.<br />
<br />
Eine weitaus elegantere Lösung ist die Verwendung des "Matrixstacks"! Jeder Aufruf von [[glPushMatrix]] bewirkt, dass die momentane Matrix auf den Stack geschrieben wird. Alle Manipulationen, die wir dann durchführen, werden wie gewohnt vollzogen. Ist alles gezeichnet, rufen wir mit [[glPopMatrix]], die letzte Matrix vom Stack wieder herunter und haben im Prinzip den letzten Zustand vor unseren Veränderungen rekonstruiert und können dann wieder anfangen, auf dieser Matrix aufzubauen. Natürlich lassen sich auch mehre Matrizen auf den Stack pushen! Dies kann reichlich Zeit sparen, wenn man bedenkt, dass man ansonsten andauernd die Matrix der Szene neu setzen muss.<br />
<br />
Um das ganze etwas anschaulicher zu machen, verwenden wir ein einfaches Beispiel. Wir werden zwei Dreiecke jeweils rechts und links vom Ursprung zeichnen. Nachdem wir das linke Objekt gezeichnet haben, werden wir nicht glLoadIdentity einsetzen oder das zweite Objekt entsprechend nach rechts transformieren, sondern eben den Matrixstack zu Hilfe nehmen.<br />
<pascal>glLoadIdentity;<br />
glTranslatef(0,0,-10);<br />
glPushMatrix;<br />
glTranslatef(-2,0,0);<br />
glBegin(GL_TRIANGLES);<br />
glColor3f(1,0,0); glVertex3f(-1,-1, 0);<br />
glColor3f(0,1,0); glVertex3f( 0, 1, 0);<br />
glColor3f(0,0,1); glVertex3f( 1,-1, 0);<br />
glEnd;<br />
glPopMatrix;<br />
<br />
glTranslatef(2,0,0);<br />
glBegin(GL_TRIANGLES);<br />
glColor3f(1,0,0); glVertex3f(-1,-1, 0);<br />
glColor3f(0,1,0); glVertex3f( 0, 1, 0);<br />
glColor3f(0,0,1); glVertex3f( 1,-1, 0);<br />
glEnd;</pascal><br />
Wie Ihr seht, setzen wir am Anfang praktisch die Distanz zum Objekt, speichern die Matrix und zeichnen das erste Objekt. Wir setzen dann die Matrix wieder zurück, so dass sie nur noch den Aufruf von glTranslatef(0, 0, -10) beschreibt und transformieren das zweite Objekt... fertig. Man kann auch ohne diese Matrixstacks leben sie können einem aber das Leben auch sehr erleichtern.<br />
== I wanna all! ==<br />
Wer glaubt, dass es das bereits war der irrt. Es gibt noch weitere Möglichkeiten auf die Matrix Einfluss zu nehmen. Der Befehl [[glLoadMatrix|glLoadMatrix*]] erlaubt es eine beliebige 4x4 Matrix zu laden und die aktuelle Matrix durch die geladene komplett zu ersetzen.<br />
<br />
Der Befehl [[glMultMatrix|glMultMatrix*]] multipliziert die übergebene 4x4-Matrix mit der aktuellen Matrix. Mit diesem Befehl könnte man beispielsweise eigene glScale*- und glRotate*-Befehle schreiben, obgleich sich auch hier über den Sinn streiten lässt. Eine sinnvollere Anwendungsmöglichkeit wird in einem späteren Tutorial vorgestellt werden.<br />
<br />
Der Befehl [[glGet|glGet*]] mit den Tokens GL_MODELVIEW_MATRIX, GL_TEXTURE_MATRIX und GL_PROJECTION_MATRIX ermöglicht euch die einzelnen Matrizen anzufordern.<br />
== Weltenformeln ==<br />
=== Hier kommt die Sonne... ===<br />
Genug der Theorie! Es wird langsam Zeit für etwas Praxis...<br />
<br />
Nun, um ehrlich zu sein, habe ich an der folgenden Idee ein wenig Zeit benötigt, um ein einigermaßen gut anschauliches Beispiel zu finden... ich will hier nicht weiter darauf eingehen, was ich gerade gehört habe, als mir die Sonne aufging :-D.<br />
<br />
Wie auch immer, man kann komplexere Matrixtransformationen sehr leicht mit unseren Sonnensystem beschreiben (Hey, wenn jetzt jemand denkt, dass ich spinne ). Um das Ganze etwas übersichtlicher zu gestalten, werden wir unser Prinzip auf Sonne, Mond und Erde beschränken.<br />
<br />
Wie funktioniert unser Sonnensystem? (Warnung, der Autor betritt mal wieder seine ausgeprägte Fantasywelt! [Anm. d. Lektors: Die richtige Sprache dafür hat er ja schon]).<br />
<br />
Die Sonne steht im Mittelpunkt und sollte sich in unserem Beispiel nicht selbst bewegen. Sie ist einfach nur im Mittelpunkt und vegetiert dort vor sich hin! Nun gibt es einen kleinen blauen Planeten, namens Erde (wieso er im Sample ein Dreieck ist, sei Eurer Kreativität überlassen...), der zum einen um seine eigene Achse rotiert, viel wichtiger jedoch, sich auch noch um die Sonne dreht. Nun werden sich vor allem die Neulinge unter Euch sadistisch freuen, das Fenster schließen, ihr Delphi starten und drauf los proggen! Das ist doch alles kein Ding, der gerade mal frisch aufgeschnappte Sinus-Satz lässt sich prima mit einbringen! Und schon berechnet man mit Hilfe von Sinus, die Position auf der Kreisbahn und positioniert den Planeten mit glTranslate* an seine Position. Und oh Wunder es klappt. HALT! Wer es nicht gemerkt hat, da war ne Portion Ironie dabei. Bitte Weiterlesen ^__ ^!<br />
<br />
Denn spätestens wenn wir folgende Ergänzung zum Besten geben, werden die ersten Augen wässrig werden, weil die meisten Hirne einem Informationskoller unterliegen. Meines stieg ja schon beim einfachen Sinus aus :-D. Ab und an kann man nämlich nachts weißgräuliche Flecken am Himmel beobachten! Wer annimmt, dass es sich hierbei um eine geschickt platzierte, in Echtzeit berechnete, Textur handelt, ist schief gewickelt! Es handelt sich dabei nämlich um ein Decal, namens Mond. Oha... so schlimm war es schon lange nicht mehr. Dieser bewegt sich in unserem Beispiel auch nicht entlang der Erdachse, sondern auf einer schiefen Bahn, damit man auf der unteren Hemissphäre den Mond auch am Tage noch sehen kann ^___^ (*selbstgefälliges, göttliches Grinsen*). Dies lässt sich nur sehr schwer mit Sinus beschreiben. Bevor wir uns nun jedoch mit einem Block bewaffnen oder den nächsten Mathematik-Professor entführen, damit er die Mathematik für uns übernimmt, greifen wir lieber zu den Sternen und nehmen ein einfacheres Verfahren! Les Matrices!<br />
=== Und es dreht sich doch! ===<br />
Die Vorgehensweise ist eigentlich relativ simpel. Wir Zeichnen unsere Sonne im Mittelpunkt unseres Universums. Mit dem Aufruf von glRotate* setzen wir den Rotationspunkt an die Position unseres Zeichenstiftes. In unserem Fall ist das noch immer die Position der Sonne. <br />
<br />
Nun verschieben wir die Erde nach links und die erste Hürde ist genommen. Jetzt müssen wir nur noch den Mond setzen und das funktioniert genauso einfach wie zuvor mit der Erde.<br />
<br />
Unser Zeichenstift befindet sich nun an der Position der Erde und daher rufen wir glRotate* auf. Nun müssen wir den Zeichenstift nur noch ein kleines Stück neben der Erde positionieren und den Mond zeichnen. Voila! Es ist vollbracht!<br />
<br />
Wenn man genauer darüber nachdenkt erkennt man sehr schnell wie elegant man dieses verhältnismäßig komplizierte Problem gelöst hat.<br />
<br />
Wer nun auf Wolke Sieben schwebt und glaubt er könnte sich nun an den nächsten DOOMTitel werfen, der sollte das Programm zuvor noch so erweitern, dass die Sonne, die Erde und auch der Mond sich zusätzlich um die eigene Achse drehen.<br />
== [[Timebased Movement]] ==<br />
Wer bereits versucht hat eines der ersten selbst geschriebenen OpenGL-Programme voller Stolz seinem Kumpel vorzuführen wird festgestellt haben, dass die Bewegungen auf dem anderen Rechner schneller oder langsamer abgelaufen sind als es auf dem eigenen Rechner der Fall war.<br />
<br />
Dieses Phänomen lässt sich sehr einfach erklären. Nehmen wir an wir bewegen ein Dreieck von links nach rechts um den Wert 1 über den Bildschirm. Eine alte Krücke wie mein PC schafft vielleicht 50 FPS. Das bedeutet das Objekt wird in einer Sekunde genau 50mal um eine Einheit nach rechts verschoben. Das bedeutet insgesamt also um 50 Einheiten in einer Sekunde. Wenn das Programm nun aber auf einem High-End-System läuft werden wir die Szene... sagen wir... 500mal aktualisieren können. Folglich bewegt sich auf diesem Rechner das Dreieck in einer Sekunde um 500 Einheiten.<br />
<br />
Das einzige was wir von unserem Dreieck erhaschen können ist ein blitzartiges Zucken auf dem Bildschirm, welches wir schnell als optische Täuschung abtun würden. Wie kann man diesem Problem nun aber entgegen wirken?<br />
<br />
Nehmen wir an wir kennen die Zeit, die wir zum Zeichnen eines Frames benötigen und ermitteln anhand dieses Wertes einen Zeitfaktor, den wir in die Bewegung mit einfließen lassen.<br />
<pascal>NewPosition := OldPosition + Movement * TimeFactor;</pascal><br />
Handelt es sich um einen schnellen Rechner so ist der Zeitfaktor entsprechend niedrig und die Bewegung pro Frame verringert sich. Ist der Rechner hingegen langsam so besitzen wir einen hohen Zeitfaktor. Folglich legt das Dreieck auf dem schnelleren Rechner pro Frame weniger Weg zurück als auf dem langsamen. Insgesamt jedoch legen die beiden Dreiecke im gleichen Zeitabstand (z.B. in einer Sekunde) denselben Weg zurück.<br />
<br />
Um den Zeitfaktor zu ermitteln muss man lediglich die Zeit für einen Schleifendurchlauf ermitteln. Um eine möglichst hohe Genauigkeit zu erhalten sollte man die Zeit für den letzten Schleifendurchlauf ermitteln und im aktuellen Schleifendurchlauf verwenden.<br />
<br />
== Nachwort ==<br />
Wow... und wieder ein wenig Zeit zum Ausspannen und ein paar persönlichen Worten. Ich kann getrost nur eines sagen: Wenn Ihr nun denkt "das war alles ja sehr leicht" habt Ihr irgendetwas falsch gemacht und solltet das Tutorial noch einmal von Vorn durcharbeiten ;). Wenn Ihr jedoch leichte Kopfschmerzen verspürt (ob es nun die Matrizen sind oder der fiese Elementarbereich *sg*...) so habt Ihr alles richtig gemacht ;).<br />
<br />
Matrizen sind vor allem vielen Einsteigern sehr fremd. "Übung macht den Meister". Versucht Euch doch einfach mal die eine oder andere Aufgabe selbst zu stellen oder unser "Sonnensystem" selbst einmal nachzuschreiben. Wenn Ihr glaubt, dass das Ganze ganz nett geworden ist, dann schickt es mir doch einfach mal zu. Ich freue mich immer darüber zu sehen, was für Früchte meine Tutorials tragen. *lach* Und wenn sich plötzlich alles um die Erde dreht, dann habe ich a) das Gefühl, dass die Kenntnisse über das Sonnensystem nicht mehr ganz up to date sind oder b) ich einen ziemlich fatalen Fehler beim Erklären gemacht habe :->.<br />
<br />
Schreckt auch bitte nicht davor zurück, eine Frage diesbezüglich im Forum zu stellen. Es ist keine Schande, mit einer Matrix Probleme zu haben und vor allem die Erfahrenen unter uns, sollten nur zu gut wissen, was alles die Ursache dafür sein kann, wenn man plötzlich gar nichts mehr auf dem Screen sieht :).<br />
<br />
Wir selbst werden uns in den folgenden Kapiteln noch etwas intensiver mit den Matrizen beschäftigen und unter anderem die eine oder andere interessante Spielerei zeigen!<br />
<br />
'''Euer'''<br><br />
'''Phobeus'''<br />
<br />
<br />
== Anhang ==<br />
<br />
[http://www.pixelpracht.net Pixelpracht] - Hier könnt Ihr das Programm "Matrix Control" finden<br />
<br />
<br />
{{TUTORIAL_NAVIGATION | [[Tutorial_lektion2]] | [[Tutorial_lektion4]]}}<br />
[[Kategorie:Tutorial|Lektion3]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Lektion_2&diff=14548Tutorial Lektion 22005-11-25T20:51:28Z<p>Akira: /* Entdeckung einer neuen Welt */</p>
<hr />
<div>= Entdeckung einer neuen Welt =<br />
== Vorwort ==<br />
Ich möchte Euch an dieser Stelle bei DGL herzlich willkommen heißen. Vermutlich wird dies eines der ersten Tutorials sein, das Ihr als Einsteiger lesen werdet. Wahrscheinlich werdet Ihr dann auch noch nicht lange bei uns sein und Euch nicht vorstellen können, dass all die Schreiber auf unserer Seite keine Gurus, sondern ganz normale Menschen sind.<br />
<br />
Man kann uns also jeder Zeit im Forum "anfassen", uns Fragen stellen, Vorschläge unterbreiten oder einfach nur einmal kurz mit einem Lob ermutigen weitere Texte zu verfassen ;-).<br />
<br />
Ich wünsche Euch an dieser Stelle viel Erfolg bei dem Einstieg in die Thematik OpenGL und ermahne Euch noch einmal, dass Ihr Eure Ziele nicht zu weit steckt. Wirklich Spaß beginnt OpenGL nämlich erst dann zu machen, wenn man stets kleinere Erfolge feiern kann.<br />
== Höhere Mächte - Matrizen und ihre Folgen ==<br />
=== Saubere Arbeit ===<br />
Bevor wir direkt beginnen etwas zu Zeichnen müssen wir erst einmal das Bild löschen, denn wie in der vorherigen Lektion beschrieben zeichnen wir die Szene jeden Schleifendurchlauf neu. Das Löschen der Puffer, in dem die Bildinformationen enthalten sind übernimmt die Funktion [[glClear]]. Die Farbinformationen unser [[Fragment|Fragmente]] (Fragmente sind vergleichbar mit den [[Pixel|Pixeln]] auf dem Bildschirm besitzen aber weitere Informationen wie z.B. Tiefenwerte. Bei der Ausgabe des Bildes werden diese Fragmente in Pixel umgewandelt) sind in dem so genannten [[Farbpuffer]] (Colorbuffer) gespeichert. Aus diesem Grund übergeben wir an die Funktion glClear die Konstante GL_COLOR_BUFFER_BIT.<br />
<pascal>//Farbbuffer entleeren<br />
glClear(GL_COLOR_BUFFER_BIT);</pascal><br />
[[bild:tutimg_lektion2_puffereffekt.gif|right]]<br />
Mit Hilfe des Befehls [[glClearColor]] können wir festlegen, welche Farbinformationen mit dem Aufruf von glClear in den Colorbuffer geschrieben werden sollen. Wenn wir es also genau nehmen so löscht glClear den Colorbuffer nicht, sondern es schreibt ihn mit den definierten Werten voll. Standardmäßig erhalten die Fragmente eine schwarze Farbe.<br />
<br />
Es wird also nicht bei jedem Zeichenvorgang (Rendervorgang) der Bildspeicher gelöscht, sondern der Programmierer entscheidet, wann dies geschehen soll. Theoretisch könnt Ihr auch die Szene wiedergeben und dann löschen, bevor sie ausgegeben wird... wer's mag :).<br />
<br />
Wer sich entscheidet den Colorbuffer nicht zu löschen, der kann so auch einige nette Effekte erzeugen. Den Cheatern unter euch sollte der erreichte Effekt bekannt vorkommen, wenn man beispielsweise bei Counter-Strike durch die Wand gelaufen ist. Der Grund für das Verwischen ist in beiden Fällen derselbe: Das neue Bild wird gezeichnet, ohne dass das alte Bild aus dem Puffer entfernt wurde.<br />
=== Und die Welt ist doch keine Scheibe... ===<br />
[[bild:tutimg_lektion2_koordinatensystem.jpg|256px|right]]<br />
Nachdem wir nun wieder Ordnung im Speicher geschafft haben, können wir zum interessanten Teil kommen: "Wie darf man sich einen 3D-Raum vorstellen?" Nun ... warum schaut Ihr Euch nicht einmal in Eurem Zimmer um?<br />
<br />
Versuchen wir doch mal, ein Beispiel direkt aus dem Leben gegriffen zu nehmen und stellen uns unsere Umgebung als Koordinaten-System vor (Igitt!). Der Monitor, der hoffentlich direkt vor uns steht, ist in diesem Fall unser Ursprung, d.h. er liegt an dem Punkt (0, 0, 0).<br />
<br />
Blicken wir seitwärts neben den Monitor, so sehen wir eine Linie (wenn Ihr es nicht tun solltest, macht Euch keine Sorgen ... solche Anfälle hat der Autor häufiger *g*), die von links nach rechts verläuft. Es handelt sich hierbei um die X-Achse, die links vom Monitor im negativen Bereich verläuft, rechts davon in den positiven.<br />
<br />
Nun blicken wir einmal nach oben und einmal nach unten und schon erspähen wir die Y-Achse, die oberhalb des Monitors positiv verläuft, unterhalb negativ. Wunderbar! Wenn Ihr bereits in 2D gearbeitet habt, solltet Ihr an dieser Stelle ein dümmliches Grinsen auf Eurem Gesichte haben und das wohltuende Gefühl, dass Euch das doch alles bereits irgendwie bekannt vorkommt. Doch zu unserem Entsetzen können wir uns ja auch noch von unserem Bildschirm wegbewegen oder auch dichter heran (alle Kurzsichtigen unter uns sollten das nicht so persönlich nehmen, auch sie leben in einem 3D-Raum ^__-).<br />
<br />
Dieses Phänomen ist im 3D-Raum typisch, jedoch nicht unerklärlich, da wir uns auf der Z-Achse bewegen. Rollen wir mit dem Stuhl vom Monitor weg, so gelangen wir in den positiven Bereich der Z-Achse, bewegen wir uns drauf zu, gelangen wir in den negativen Bereich. Jeder, der bereits in D3D programmiert hat, sollte sich schleunigst einprägen, dass das ein großer Unterschied zwischen D3D und OpenGL ist. Das kann sonst zu einigen wirklich fiesen Fehlern führen, wenn man es nicht weiß... *fg*.<br />
<br />
Soweit so gut! Klingt bisher hoffentlich immer noch nicht so kompliziert. Nur keine Sorge, das Niveau versuchen wir zu halten ;).<br />
<br />
Speziell wenn Ihr bereits 2D-Spiele programmiert habt, solltet Ihr Euch sehr schnell von folgenden Gedanken trennen: Die Koordinaten, die Ihr seht und angebt, entsprechen in der Regel nicht den Pixeln des Bildschirmes, sondern so genannten Weltkoordinaten. Eine Definition für diese Weltkoordinaten gibt es nicht, denn wie groß diese sind ist dem Programmierer überlassen. Ob Ihr euer Objekt eine Einheit vor Euch und 0,1 Einheit rechts von Euch oder aber 100 Einheiten vor euch und 10 Einheiten neben Euch positioniert ist egal. Die euch zur Verfügung stehende Welt ist grenzenlos ;). Ihr könnt Eure Szene theoretisch unendlich klein oder aber unendlich groß darstellen. Das einzige was euch wirklich daran hindert ist die Größe und Auflösung der Euch zur Verfügung stehenden Typen wie Integer oder Single ;). Der eigentliche Größeneindruck eines Objektes entsteht durch die Geschwindigkeit mit der sich der Betrachter und andere Objekte in der Welt bewegen.<br />
<br />
=== Der erste Kontakt ===<br />
Um nun etwas mit OpenGL rendern zu können, müssen wir uns bewusst werden, was die Worldmatrix ist. In dieser Matrix wird festgehalten, wie und wo ein Objekt gezeichnet wird. Das hört sich vielleicht zunächst recht merkwürdig an, lässt sich aber leicht veranschaulichen.<br />
<br />
Stellen wir uns vor, die Worldmatrix zeigt auf einen Punkt, an dem ein Objekt gezeichnet werden soll (ganz übel... wir werden später detaillierter darauf eingehen. Sollte es jemand also nach der folgenden Erläuterung nicht verstanden haben, kann er mich selbstverständlich persönlich per Mail zur "Rechenschaft" ziehen oder aber er wirft einen Blick in unser OpenGLWiki :)).<br />
<br />
Diesen Punkt setzen wir nun am Anfang in den Mittelpunkt unseres Koordinatensystems. Um oberes Beispiel aufzugreifen: Den Bildschirm. Dies geschieht mit dem Befehl [[glLoadIdentity]] und entspricht einem Reset der Worldmatrix. Theoretisch können wir nun an dieser Stelle etwas zeichnen. Dies würde allerdings dazu führen, dass das Objekt sehr groß oder gar nicht zu sehen ist, weil wir uns eben auch an diesen Stellen befinden. Gehen wir doch einmal 1,5 Einheiten nach links und 6 Einheiten nach hinten!<br />
<br />
Hierfür sollte man sich bewusst werden, dass wir in OpenGL praktisch an einen Stuhl gefesselt sind, d.h. wir können uns gar nicht bewegen. Das soll uns aber nicht davon abhalten. Wir brauchen ja nur die ganze Welt so zu bewegen, dass es für uns aussieht, als ob wir uns bewegen. Denn wenn sich alles außer uns nach links bewegt, haben wir den Eindruck, wir wären nach rechts gewandert, right?<br />
<br />
Nun aber zu der Bewegung unseres ersten Objektes:<br />
<pascal>glTranslatef(-1.5,0,0);<br />
glTranslatef(0, 0,-6);</pascal><br />
Dies hat bewirkt, dass sich unser Zeichenstift 1,5 Einheiten nach links (negativer Bereich der X-Achse) und anschließend 6Einheiten nach hinten bewegt hat. Ich habe diesen Fall absichtlich in zwei Schritten gefasst, um es zu verdeutlichen. Sicherlich wäre es einfacher, alles mit nur einem Aufruf von [[glTranslate|glTranslate*]] zu machen:<br />
<pascal>glTranslatef(-1.5, 0,-6);</pascal><br />
Man beachte, dass in diesem Fall die 6 Schritte nach hinten notwendig sind um ein bisschen Distanz zum Objekt zu bekommen und nicht direkt in ihm zu stehen!<br />
<br />
Fertig! Schon haben wir dort unseren "Zeichenstift" positioniert, der nun auf weitere Zeichenkommandos von uns wartet. Das mag sicherlich alles ein wenig verwirrend klingen... testet es am Besten aus und spielt mit den Parametern und erkennt, was gemeint ist :).<br />
== Von Sichtungen... ==<br />
=== Die Büchse der Pandora ===<br />
Nun sind wir aber auch alle scharf darauf, endlich etwas zu rendern und auf dem Bildschirm auszugeben. Dies geschieht z.B. mit folgenden Zeilen:<br />
<pascal>glBegin(GL_TRIANGLES);<br />
glVertex3f(-1,-1, 0); <br />
glVertex3f( 1,-1, 0);<br />
glVertex3f( 0, 1, 0);<br />
glEnd;</pascal><br />
Wir erkennen hierbei eindeutig eine Art Block. Gerade wir Pascaler sollten diese ja lieben :->. Wir teilen OpenGL mit Hilfe von [[glBegin]] mit, dass wir ein Objekt zeichnen wollen. In diesem Fall übergeben wir den Parameter GL_TRIANGLE, der OpenGL angibt, dass folgende Punkte als ein Dreieck interpretiert werden sollen.<br />
<br />
Anschließend folgt ein dreifacher Aufruf von [[glVertex|glVertex3f]]. Jeder Aufruf erzeugt nun einen Punkt (Vertex) in unserem 3D-Raum. Da wir OpenGL bei glBegin mitgeteilt haben, dass diese Punkte zu einem Dreieck zusammengefügt werden sollen wird das auch so getan ;).<br />
<br />
Wie wir auf dem Bild erkennen können, wurde unser Dreieck wie erwartet nach links verschoben abgebildet und auch mit einem leichten Abstand zur Kamera, nämlich 6 Einheiten entlang der Z-Achse.<br />
=== Guter Stoff! ===<br />
Nun ... ein wenig trostlos sieht unser Dreieck nun doch aus, oder? Ich habe übrigens damals bei D3D rund eine Woche benötigt, bis ich ein schwarzes Dreieck hatte. Wir haben immerhin schon ein weißes ... aber wir gehen nun einen Schritt weiter und werden das Dreieck schön bunt einfärben.<br />
<br />
Bevor nun irgendjemand anfängt und Pixel für Pixel den Bildschirm nach zu pinseln oder gar sein PaintShop bereits offen hat, um die ersten Texturen zu erstellen, sei gestoppt! Es gibt für einfache Einfärbungen in OpenGL eine bessere Methode. Wir definieren einfach für jeden Eckpunkt eine Farbe und OpenGL wird dann sogar eigenständig die Farbverläufe dafür erstellen. Jeder der D3D kennt, wird diese Vorgehensweise bekannt vorkommen und er wird beginnen verzweifelt nach dem FVF zu suchen, um es korrekt zu definieren... Wenn Ihr einen Vertex zeichnet, so rendert OpenGL diesen automatisch "weiß" (Ah huch! Deswegen ist unser Dreieck auch weiß???). Alles was wir nun machen müssen, ist OpenGL mitzuteilen, dass es eine andere Farbe einsetzen soll. Und zwar wird OpenGL diese Farbe für alle folgenden Eckpunkte nutzen, so lange bis von uns ein anderes Kommando kommt.<br />
<br />
Der mysteriöse Befehl, um den ich nun schon die ganze Zeit herumschwafle ist [[glColor|glColor*]]:<br />
<pascal>// Alle folgenden Eckpunkte werden rot gefärbt<br />
glColor3f(1,0,0);</pascal><br />
Wobei jeder Parameter einen Farbwert repräsentiert und zwar nach dem Muster RGB. Es wird ein Wert zwischen 1 und 0 erwartet, wobei eine Eins "volle Farbsättigung" bedeutet. Das heißt in unserem Fall haben wir als Farbe "rot" gesetzt.<br />
<br />
Und weil wir schließlich die Welt ein wenig bunter machen und nicht nur das weiße Dreieck rot färben wollten, werden wir nach jedem Eckpunkt eine neue Farbe setzen:<br />
<pascal>glBegin(GL_TRIANGLES);<br />
glColor3f(1, 0, 0); glVertex3f(-1,-1, 0); <br />
glColor3f(0, 0, 1); glVertex3f( 1,-1, 0);<br />
glColor3f(0, 1, 0); glVertex3f( 0, 1, 0);<br />
glEnd;</pascal><br />
Und dies ist dann unser farbenfrohes Ergebnis. Beeindruckend, wenn man bedenkt, wie wenig Aufwand letztendlich dahinter steckt, oder?<br />
<br />
<br />
[[bild:tutimg_lektion2_dreieck.gif]]<br />
<br />
== Nachwort ==<br />
Wer meine Tutorials kennt weiß, dass zum Abschluß noch immer ein wenig Geblubber von mir kommt (man beachte diese entzückende Wortwahl von meiner einer... hoffe, der Lektor findet es auch amüsant...). (Anm. des Lektors: Und wie!) Schauen wir doch mal stolz auf das zurück, was wir heute erreicht haben! Wir haben OpenGL initialisiert, ein erstes Dreieck auf den Bildschirm gezaubert und dieses in den Farbtopf gesteckt! Das solltet Ihr als ein historisches Ereignis ansehen. Als ich damals mit D3D angefangen habe, brauchte ich, um es mir selbst zu erarbeiten, ca. eine Woche (Das ist jetzt der Moment, in dem Ihr Eure Hände heben solltest und ein lautes "Call me God" aus Euch herauskommen sollte, so dass zumindest Eure unmittelbaren Mitmenschen denken, dass Ihr einen Dachschaden habt!!! Das gehört einfach mit dazu ^__- )<br />
<br />
Wie immer solltet Ihr Euch nun hinsetzen und Euch ein wenig mit den Parametern vertraut machen. Speziell bei glTranslate* solltet Ihr ein wenig mit den Parametern experimentieren, damit Ihr seht, was es mit den Matrizen auf sich hat und wie diese funktionieren. Wenn Ihr dann auch denkt, dass Ihr damit vertraut seid, versucht ein wenig mit den einzelnen Vertexpositionen zu experimentieren und verändert diese. Wenn auch das klar ist, setzt Euch in die Ecke und warte sehnsüchtig auf mehr von uns :D! (Anm: die "Ecke" ist nicht die Kneipe nebenan ... :)<br />
<br />
Im nächsten Kapitel werden wir dann Matrizen-Hardcore machen ... legt also schon mal Euer Aspirin parat, es wird witzig werden *grunz* :).<br />
<br />
Und wie immer freuen wir uns sehr über Feedback. Schreibt uns doch einfach ein paar Worte in unser [http://www.delphigl.com/forum/viewforum.php?f=8 Feedback-Forum]. Sagt was Ihr gut und was hingegen Ihr als schlecht empfunden habt! Auch freuen wir uns immer über Ideen für weitere Tutorials, teilt uns also bitte Eure Ideen mit ;)!<br />
<br />
Okay... ich wünsche Euch einen angenehmen Tag / Nacht!<br />
<br />
'''Euer'''<br><br />
'''Phobeus'''<br />
<br />
{{TUTORIAL_NAVIGATION | [[Tutorial_lektion1]] | [[Tutorial_lektion3]]}}<br />
<br />
[[Kategorie:Tutorial|Lektion2]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Lektion_1&diff=14547Tutorial Lektion 12005-11-25T20:41:15Z<p>Akira: /* Nicht zu weit aus dem Fenster lehnen */</p>
<hr />
<div>= Nicht zu weit aus dem Fenster lehnen =<br />
== Vorwort ==<br />
Hallo,<br />
in dem folgenden Tutorial möchte ich Euch etwas näher mit OpenGL vertraut machen. Der Großteil dieses Tutorials besteht aus bloßer Theorie, die jedoch absolut notwendig ist. Es ist nicht leicht über ein so trockenes Thema zu schreiben, denn in aller Regel sind Eure Erwartungen groß und wenn es schon keine aufwendige Grafikdemo wird, so wollt Ihr doch wenigstens ein Dreieck am Ende auf den Bildschirm gezaubert sehen. Seit nicht frustriert, wenn ich Euch nun verrate, dass das eigentliche Zeichnen (Rendern) erst im zweiten Tutorial behandelt wird. Ein leeres Fenster ist doch auch nicht zu unterschätzen oder ;)?<br />
== Was ist (die) OpenGL? ==<br />
Womit wir schon bei der ersten Frage wären. Ist es der, die oder das OpenGL. Um diese Frage zu beantworten müssen wir wissen, dass OpenGL ein Akronym für "Open Graphics Library" ist. Der englischen Begriff "Library" bedeutet Bibliothek, womit wir auf "die OpenGL" schließen. Häufig findet OpenGL aber auch ganz ohne Pronomen Verwendung. Nachdem wir diese Sache klargestellt haben kommen wir zur eigentlichen Frage zurück. Was ist OpenGL?<br />
<br />
OpenGL ist eine API, die es uns erlaubt relativ einfach Grafiken auf dem Bildschirm auszugeben ohne jedoch zu wissen, was im Hintergrund genau geschieht. Das ganze ist in gewisser Weise mit der Windows-API vergleichbar. Wir wissen zwar, wie man ein Fenster erzeugt, was im Hintergrund jedoch genau geschieht wissen wir nicht. <br />
<br />
Weiterhin ist OpenGL plattformunabhängig. Theoretisch könntet Ihr OpenGL also unter jedem Betriebssystem nutzen und Eure Programme würden auch auf jeder Hardware laufen. Dies bringt jedoch den Nachteil mit sich, dass bestimmte Dinge wie die Fensterverwaltung, Tastatur- oder Maussteuerung aus der Bibliothek ausgeschlossen sind, da diese sich von Betriebssystem zu Betriebssystem unterscheiden können. Um diese Dinge müssen wir uns also selbst kümmern. Zum Glück bietet uns die VCL (Visual Component Library) von Delphi ein mächtiges Werkzeug um solchen Herausforderungen entgegenzutreten.<br />
<br />
OpenGL selbst besitzt nur einen relativ kleinen Sprachumfang. Die wenigen Funktionen, die jedoch vielseitig einsetzbar sind lassen sich sehr leicht erlernen. Die folge hiervon ist aber auch, dass es keine Funktion zum Zeichnen komplexer Objekte wie z.B. die eines Vogels gibt. Solche Objekte muss man sich daher selbst aus den so genannten Primitiven, wie z.B. Punkten, Linien oder Dreiecken zusammensetzen.<br />
<br />
OpenGL ist ein riesiger Zustandsautomat. Je nachdem wie Ihr die einzelnen Zustände schaltet können die Bilder, welche ihr auf dem Bildschirm ausgebt völlig unterschiedlich aussehen. Beispielsweise sieht ein Würfel bei aktiviertem Licht gezeichnet wurde anders aus als derselbe Würfel ohne Licht. Verändert Ihr einmal einen Zustand wie z.B. die Farbe so werden sämtliche Primitive, die Ihr anschließend zeichnet mit dieser Farbe versehen, bis ihr die Farbe erneut wechselt.<br />
<br />
== Von Tausend und einer Funktion ==<br />
<pascal> glColor3b, glColor3d, glColor3f, glColor3i, glColor3s, <br />
glColor3ub, glColor3ui, glColor3us, glColor4b, glColor4d, <br />
glColor4f, glColor4i, glColor4s, glColor4ub, glColor4ui, <br />
glColor4us, glColor3bv, glColor3dv, glColor3fv, glColor3iv, <br />
glColor3sv, glColor3ubv, glColor3uiv, glColor3usv, <br />
glColor4bv, glColor4dv, glColor4fv, glColor4iv, glColor4sv, <br />
glColor4ubv, glColor4uiv, glColor4usv</pascal><br />
Es sind nicht ganz tausend und eine geworden, allerdings kommt dies schon sehr nahe dran ;). Sprach ich nicht gerade noch von einem kleinen Sprachumfang? Schnell werdet Ihr aber erkannt haben, dass das Grundgerüst dieser Funktion glColor heißt. All diese Funktionen verhalten sich gleich und unterscheiden sich lediglich in der Übergabe der Parameter. So werden bei glColor3i 3 Parameter vom Typ Integer erwartet. glColor4b verlangt hingegen 4 Parameter vom Typ Byte.<br />
<br />
Alle OpenGL-Funktionen besitzen den Präfix "gl" gefolgt von dem Befehlsstamm. Einige Funktionen besitzen noch einen Suffix, der den Typ oder auch die Anzahl der übergebenen Parameter wieder spiegelt. (siehe auch [[Funktions_Anhang|diese Erklärung]])<br />
<br />
OpenGL-Konstanten sind immer an ihren Großbuchstaben erkennbar und beginnen mit der Vorsilbe "GL_". Obgleich Delphi keine Unterschiede zwischen Groß- und Kleinschreibung macht empfehle ich doch diese Schreibweise beizubehalten, denn sie steigert die Übersicht ungemein.<br />
<br />
== Wie sind OpenGL-Programme aufgebaut? ==<br />
Um ein Dreieck zu rendern reicht es vielleicht das Bild einmal zu Zeichnen und auszugeben. Der Großteil von Euch wird sich jedoch nicht mit einem Dreieck zufrieden geben und möchte doch wenigstens Bewegungen und Animationen in seinen Programmen verwenden. Die Vorgehensweise ist so einfach wie genial: Um Bewegungen zu ermöglichen ist es notwenig, das Bild mehrmals zu zeichnen und auszugeben. Stellt Euch vor Ihr möchtet ein Dreieck von links nach rechts über den Bildschirm bewegen! Ihr zeichnet Euer Dreieck ganz links und gebt es aus. Noch bevor das menschliche Auge genug Zeit hat dieses Bild als einzelnes Bild zu deuten habt ihr dank der hohen Geschwindigkeit der heutigen PCs das Dreieck bereits um ein paar Pixel nach rechts verschoben und erneut gezeichnet. Ist Euer Rechner schnell genug, so erscheint den Betrachter die Bewegung als völlig flüssig und die eigentlichen Sprünge von wenigen Pixeln sind als solche nicht erkennbar. Das menschliche Auge kann etwa 25-30 Bilder pro Sekunde unterscheiden, also solltet Ihr immer versuchen mehr als 30 Bilder pro Sekunde (FPS = Frames per Second) zu zeichnen. <br />
<br />
Spätestens jetzt sollte Euch klar sein, warum es bei modernen Spielen manchmal ruckeln kann, wenn Euer Rechner zu langsam ist.<br />
<br />
Soweit zur Theorie. Wie aber realisiert man diesen Vorgang. Ganz klar: Wir zeichnen in einer Schleife. Nun stellt sich die Frage wie man dieses Problem elegant löst. Man könnte sich z.B. eine eigene Hauptprogrammschleife schreiben, in der man rendert. In dieser müsste man dann aber auch die Botschaften des Betriebssystems verarbeiten.<br />
<br />
Eine elegantere Lösung ist meiner Meinung nach das OnIdle-Ereignis der Anwendung hierfür zu nutzen. Wenn Ihr darin den Parameter "Done" auf "False" setzt wird diese Nachricht so oft ausgeführt, wie es nur irgendwie möglich ist. Das OnIdle-Ereignis wird im Hauptthread der Anwendung aufgerufen, was den positiven Nebeneffekt hat, dass die Nachrichten des Betriebssystems von der VCL verarbeitet werden und man die VCL in vollem Umfang nutzen kann.<br />
<br />
Nachteil der vorgestellten Methoden ist, dass die Anwendung nie zur Ruhe kommt, da die Schleife permanent durchlaufen wird. Damit steigt die Prozessorauslastung in der Regel auf 100%. Wenn man aus bestimmten Gründen in keiner echten Schleife rendern möchte könnte man sich auch mit einem Timer Abhilfe schaffen.<br />
<br />
== Die Initialisierung ==<br />
Wer sofort loslegen möchte, den kann ich beruhigen, denn die Initialisierung von OpenGL ist mit unserem Header sehr einfach. Ihr müsst einfach bevor Ihr OpenGL nutzen möchtet, z.B. im OnCreate-Ereignis Eures Formulars die Funktion "InitOpenGL" aufrufen, nachdem Ihr die Unit "dglOpenGL" in Eurer Usesklausel eingebunden habt.<br />
<br />
Damit OpenGL auch weiß, wohin gezeichnet werden soll müssen wir ihr das irgendwie mitteilen. Hierfür ermitteln wir den Gerätekontext unseres Formulars mit Hilfe der Funktion "GetDC" und dem Handle unseres Formulars.<br />
<br />
Anschleißend erstellen wir einen Zeichenkontext mit Hilfe der Funktion "CreateRenderingContext".<br />
<br />
Nachdem wir diesen erstellt haben müssen wir Ihn noch aktivieren. Dies geschieht mit der Funktion "ActiveRenderingContext". Das ganze sieht dann in etwa so aus:<br />
<br />
<pascal>var<br />
DC, RC:HDC;<br />
procedure TfrmMain.FormCreate(Sender: TObject);<br />
begin<br />
DC:=GetDC(Handle);<br />
RC:=CreateRenderingContext(DC, [opDoubleBuffered], 32, 24, 0, 0, 0, 0);<br />
ActivateRenderingContext(DC, RC);<br />
end;</pascal><br />
<br />
Im Prinzip könntet Ihr nun loslegen. Wenn Ihr Euer Bild nun im OnIdle-Ereignis zeichnet werdet Ihr schnell feststellen, dass Ihr nichts zu sehen bekommt. Das ist auch ganz normal, denn Ihr müsst das Bild auch noch ausgeben. Das geschieht mit Hilfe der Funktion "SwapBuffers".<br />
<br />
Da wir sauber programmieren wollen müssen wir den Renderingkontext und den Gerätekontext wieder freigeben, wenn wir unser Programm beenden oder aber OpenGL nicht mehr benötigen. Die Funktionen "ReleaseDC" und "DestroyRenderingContext" erfüllen diese Aufgaben.<br />
<br />
Jetzt müssen wir nur noch die Größe des Bildes, welches wir rendern möchten definieren. Mit dem Befehl [[glViewport]] definieren wir die Zeichenfläche. Im Gegensatz zu Windows liegt der Ursprung bei OpenGL unten links. Da die Größe unserer Zeichenfläche der des Fensters entsprechend soll rufen wir diesen Befehl im OnResize-Ereignis unseres Formulars auf:<br />
<pascal>glViewport(0, 0, ClientWidth, ClientHeight);</pascal><br />
<br />
Die vier kommenden Zeilen solltet Ihr ganz schnell wieder aus Eurem Gedächtnis streichen und erst einmal als gegeben hinnehmen. In Tutorial 2 und 3 werden wir näher darauf eingehen. Soviel sei jedoch gesagt: Es wird nichts weiter unternommen als unsere dreidimensionale Welt aufzuspannen. Damit ist sichergestellt, dass Objekte in großer Entfernung kleiner erscheinen, so wie wir es gewohnt sind.<br />
<br />
Mit diesem Wissen könnt Ihr nun direkt in Tutorial 2 einsteigen.<br />
<br />
== Nachwort ==<br />
Genug dazu. Ich freue mich sehr, dass Ihr Euch die Zeit genommen habt, dieses Tutorial zu lesen und OpenGL lernen wollt. Ich hoffe, dass wir die nächsten Tutorials genauso gut durchbekommen und dass am Ende jeder etwas gelernt hat und die Zeit nicht total vergebens war. Ich schreibe sicherlich nicht immer alles auf dem direktesten Weg, sondern rede gerne mal um den Brei herum. Wer das nicht mag, soll sich die OpenGL-Dokumentation zu Herzen nehmen, dort ist alles kurz und schmerzlos beschrieben ;). Für alle, die es nicht so trocken mögen, ist mein Schreibstil hoffentlich eine gute Alternative ;).<br />
<br />
'''Euer'''<br><br />
'''Magellan'''<br />
<br />
{{TUTORIAL_NAVIGATION|[[Tutorial_quickstart]]|[[Tutorial_lektion2]]}}<br />
[[Kategorie:Tutorial|Lektion1]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Selection&diff=14546Tutorial Selection2005-11-25T20:36:42Z<p>Akira: /* OpenGL und der Name-Stack */</p>
<hr />
<div>=Objektselektion=<br />
==Einleitung==<br />
<br />
Hi Leute!<br />
Viele werden nun erst mal denken, was denn ein Tut von mir hier zu suchen hat. Na ja: Über dieses Thema gab es bei der DGL bislang reichlich wenig und deshalb habe ich mich entschossen es denn auch hier zu veröffentlichen. (Juhuu! Endlich darf ich hier auch mein Debüt feiern!) Es soll nun also über die Selektion von OpenGL-Objekten (Na ja ihr wisst schon: Dreiecke, Quadrate, Würfel, usw...) gehen. Viele werden sich sicherlich denken, wozu das überhaupt gut sein soll...<br />
<br />
Na ja, wenn ich hier gerade so in die Tasten haue, da fällt mir etwas ein, was wir bislang mit OpenGL noch nicht können (wir können Level basteln, wenn wir gut sind evtl. unser Eigenheim nachbauen, usw..): ich interagiere mit meiner Welt bzw. ich bediene die Tastatur. Was wäre das den für eine Welt, wenn wir nichts anheben, Türen öffnen oder einen Lichtschalter betätigen könnten? Na ja: eine ziemlich üble würde ich mal so vermuten... Und genau darum geht es in meinem Tut heute!<br />
<br />
Ich denke mal, ihr alle werdet erkannt haben, dass es wirklich nicht gerade einfach ist, herauszubekommen, auf welches Objekt eurer Szene der User gerade geklickt hat, obwohl diese Infos für viele Programme ziemlich unerlässlich sind. Stellt euch mal vor, ihr habt einen super genialen Map-Editor gebastelt und ihr könnt nicht herausfinden, auf welches Objekt der User gerade zwecks Editieren geklickt hat! Oder ihr habt ein super geniales Hacker-Agenten-Spiel geschrieben und ihr wisst nicht, welche Kombination der gute Herr Spieler gerade in den Computer einer Sicherheitstür getippt hat! Wäre doch ne echt schwache Leistung, oder? Und um dieses Defizit auszugleichen habe ich hier niedergeschrieben, wie es geht.<br />
<br />
==OpenGL und der Name-Stack==<br />
<br />
Etwas (aber nur ganz wenig) Theorie vorweg.<br />
Und zwar will ich euch zunächst sagen, wie OpenGL Namen überhaupt ansieht: und zwar als reine Integer-Werte! Es gibt keine echten Namen, sondern leider nur Nummern, die man seiner Szene zuweisen kann... Dass soll uns aber nicht wirklich davon abhalten, auch echte Namen zu verwenden. So gibt man normalerweise die Namen selber als Konstanten an, aber wir können ja zum Beispiel ein Array erschaffen, welches die dazugehörigen Namen enthält... Das ganze könnte man sich dann so vorstellen:<br />
<br />
<pascal>const<br />
dreieck : 1;<br />
viereck : 2;<br />
stern : 3;<br />
namen : array[-1..3] of string = ('nichts', '', 'das Dreieck', 'das Viereck', 'den Stern'); </pascal><br />
<br />
<br />
Erklären muss ich das nicht wirklich, oder? Na ja evtl. wäre es für euch noch ganz interessant, weshalb das Namens-Array den Wert "-1" hat: OpenGL kann nämlich auch den Wert "-1" ausgeben, wenn man auf rein gar nichts geklickt hat! Daher brauchen wir auch ein Element, welches "-1" heißt.<br />
<br />
Damit euch das ganze etwas klarer wird, stelle ich euch nun erst mal vor, was ich heute mit euch machen will:<br />
[[Bild:Tutorial_selection_01.jpg|center]]<br />
<br />
Dieses nun wirklich einfache Programm hat folgende Funktion:<br />
Es werden wie man sieht ein Dreieck, ein Viereck und ein Stern gezeichnet und je nachdem, auf was man geklickt hat, soll die Message unten in der Statusbar angezeigt werden.<br><br />
Wie man eine derartige (für unsere Verhältnisse einfache) Szene rendert, dürfte recht einfach sein, nicht aber, wie man den Objekten nun die Namen zuweißt... aber das zeige ich euch hier:<br />
<br />
<pascal>procedure render;<br />
begin<br />
glMatrixMode(GL_MODELVIEW);<br />
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT); //Farb und Tiefenpuffer löschen<br />
glLoadIdentity;<br />
gltranslate(0,0,-6);<br />
<br />
glinitnames;<br />
glpushname(0);<br />
<br />
glcolor3f(1,0,0);<br />
glloadname(dreieck);<br />
glbegin(gl_triangles);<br />
glvertex3f(-0.5,1,0);<br />
glvertex3f(0.5,1,0);<br />
glvertex3f(0,2,0);<br />
glend;<br />
<br />
glcolor3f(0,1,0);<br />
glloadname(viereck);<br />
glbegin(gl_quads);<br />
glvertex3f(-1,0,0);<br />
glvertex3f(-2,0,0);<br />
glvertex3f(-2,-1,0);<br />
glvertex3f(-1,-1,0);<br />
glend;<br />
<br />
glcolor3f(1,1,0);<br />
glloadname(stern);<br />
glbegin(gl_triangles);<br />
glvertex3f(1,0,0);<br />
glvertex3f(2,0,0);<br />
glvertex3f(1.5,-1,0);<br />
<br />
glvertex3f(1,-0.65,0);<br />
glvertex3f(2,-0.65,0);<br />
glvertex3f(1.5,0.35,0);<br />
glend;<br />
<br />
SwapBuffers(form1.myDC); //scene ausgeben<br />
end;</pascal><br />
<br />
Also, das Erste ist, dass wir auf jeden Fall in die ModelViewMatrix schalten müssen. In dieser Matrix sind wir zwar auch normalerweise, aber in unserer späteren Selection-Funktion müssen wir zeitweilig in eine andere. Da es zum Rendern aber unabdingbar ist, dass wir gerade diese Matrix gesetzt haben, wechsle ich auf jeden fall vor dem Rendern in diese... sicher ist sicher :-)<br />
<br />
Die nächsten markierten Zeilen sind "{{INLINE_CODE|[[glInitNames]];}}" und "{{INLINE_CODE|[[glPushName]](0);}}" Diese beiden Zeilen Quellcode bewirken, dass zunächst mal der Name-Stack initialisiert wird und danach der "Name" "0" auf diesen Stack gelegt wird. Die Null hat eine ganz einfache Funktion: Wird nicht vor dem Laden des ersten Namens mindestens eine Zahl auf den Stack gepusht, dann endet der Versuch, einen Namen zu laden mit einem netten Error... wer's nicht glaubt, kann es ja gerne mal ausprobieren ;-)<br />
<br />
Der Befehl "{{INLINE_CODE|[[glLoadName]]}}", der den restlichen interessanten Teil dieser Render-Prozedur stellt, hat die einfache Wirkung, dass OpenGL nun einen Namen lädt (halt aus unserer Konstanten) und dass alle Objekte, die nun folgen, mit diesem "Namen" belegt werden, bis ein weiteres mal "{{INLINE_CODE|glLoadName}}" aufgerufen wird... ist doch easy, oder?<br />
<br />
Leider wird es nicht so einfach bleiben... Na ja, nützt ja nichts, nun müssen wir nämlich die Funktion anlegen, welche die eigentliche Selektion durchführt. Ein Tipp: Die Funktion muss im Quelltext unter der Render-Prozedur angelegt werden, weil dieselbige nämlich in dieser Funktion aufgerufen wird und Delphi ansonsten die Prozedur nicht findet.<br />
<br />
<pascal>function Selection : integer;<br />
var<br />
Puffer : array[0..256] of GLUInt;<br />
Viewport : TGLVectori4;<br />
Treffer,i : Integer;<br />
Z_Wert : GLUInt;<br />
Getroffen : GLUInt;<br />
begin<br />
glGetIntegerv(GL_VIEWPORT, @viewport); //Die Sicht speichern<br />
glSelectBuffer(256, @Puffer); //Den Puffer zuordnen<br />
<br />
glMatrixMode(GL_PROJECTION); //In den Projektionsmodus<br />
glRenderMode(GL_SELECT); //In den Selectionsmodus schalten<br />
glPushMatrix; //Um unsere Matrix zu sichern<br />
glLoadIdentity; //Und dieselbige wieder zurückzusetzen<br />
<br />
gluPickMatrix(xs, viewport[3]-ys, 1.0, 1.0, viewport);<br />
gluPerspective(60.0, Viewport[2]/Viewport[3], 1, 256);<br />
<br />
render; //Die Szene zeichnen<br />
glMatrixMode(GL_PROJECTION); //Wieder in den Projektionsmodus<br />
glPopMatrix; //und unsere alte Matrix wiederherzustellen<br />
<br />
treffer := glRenderMode(GL_RENDER); //Anzahl der Treffer auslesen<br />
<br />
Getroffen := High(GLUInt); //Höchsten möglichen Wert annehmen<br />
Z_Wert := High(GLUInt); //Höchsten Z - Wert<br />
for i := 0 to Treffer-1 do<br />
if Puffer[(i*4)+1] < Z_Wert then<br />
begin<br />
getroffen := Puffer[(i*4)+3];<br />
Z_Wert := Puffer[(i*4)+1];<br />
end;<br />
<br />
if getroffen=High(GLUInt) <br />
then Result := -1<br />
else Result := getroffen;<br />
end;</pascal><br />
<br />
<br />
Alle Stellen, zu denen ich bereits Kommentare geschrieben habe, brauche ich (glaube ich) nicht mehr wirklich erklären... aber zu den Variablen will ich durchaus was sagen: Der "{{INLINE_CODE|Puffer}}" ist der eigentliche Selektions-Puffer. In ihm werden die Ergebnisse der Selektionsprüfung gespeichert. Das Array "{{INLINE_CODE|Viewport}}" speichert quasi unsere View, da die ja auch irgendwie wichtig ist ;-) Die Variable "{{INLINE_CODE|Treffer}}" ist ein reiner Integerwert, der die Anzahl der Treffer aufnimmt und die Variable "{{INLINE_CODE|Z_Wert}}" nimmt die Tiefendaten auf.<br />
<br />
Was nun in dieser Funktion passiert, ist einfach zu beschreiben. Zunächst wird unsere View zwischengespeichert und dann OpenGL klar gemacht, was für einen Puffer mit welcher Größe es verwenden soll. Eine Größe von 64 ist quasi der Normalwert, da jede OpenGL-Implementation (auf den verschiedenen Grafikkarten) diese Größe mindestens liefern muss... größere Werte sind aber durchaus möglich.<br><br />
Der Puffer hat ja nun an sich aber die 4-Fache Größe dieses Wertes. (bzw. wir übergeben als Parametergröße ja auch den Wert 256) Das hat folgenden Grund:<br />
Für jedes getroffene Objekt werden immer 4 Werte gespeichert. Und zwar<br />
# Anzahl der Namen auf dem Stack<br />
# Kleinster Z-Wert des getroffenen Objektes<br />
# Größter Z-Wert des getroffenen Objektes<br />
# Name des Objektes<br />
<br />
Danach wird in den Selektions-Modus geschaltet, die Matrix konfiguriert und OpenGL übergeben, welche x bzw. y- Koordinate auf dem Bildschirm angeklickt wurde. Dieses geschieht in der Prozedur "{{INLINE_CODE|[[gluPickMatrix]](xs, viewport[3]-ys, 1.0, 1.0, @viewport);}}". In dieser stehen "{{INLINE_CODE|xs}}" bzw. "{{INLINE_CODE|ys}}" für unsere X und Y-Koordinate, auf die geklickt wurde. Die nächsten zwei Parameter bilden eine Art "''Klick-Rechteck''". Wenn wir da einen Wert >1 angeben, dann wird praktisch eine Art Rechteck um den Punkt, auf den wir geklickt haben, gebastelt, welches komplett ausgewertet wird. Dadurch würde nicht nur das Objekt, welches direkt unter dem Mauscursor lag ausgewertet, sondern auch die Objekte, welche evtl. direkt daneben lagen.<br><br />
''Die nächste Zeile ist auch von Bedeutung: Die gesamte Selektion funktioniert nur dann korrekt, wenn bei [[gluPerspective]] die selbe Perspektive gesetzt ist wie beim normalen Rendern (was logisch erscheint wenn man einmal darüber nachdenkt).''<br><br />
Mit der Zeile "{{INLINE_CODE|treffer <nowiki>:=</nowiki> [[glRenderMode]](GL_RENDER);}}" fragt man nun noch ab, wie viele Treffer es insgesamt gegeben hat. Hierbei gilt es zu beachten: angenommen, man hat eine Große Szene und klickt auf ein Objekt. Dann ist schon fast davon auszugehen, dass dahinter noch irgendetwas liegt (kann auch ganze 100 Einheiten entfernt liegen... das spielt keine echte Rolle), dann werden auch diese Objekte als Treffer zurückgegeben. Um nun nur das Objekt herauszubekommen, welches der Kamera am nächsten lag, ließt man nun in einer Schleife alle Z-Werte "{{INLINE_CODE|Puffer[(i*4)+1]}}" der getroffenen Objekte aus und vergleicht sie mit dem bislang niedrigsten Wert. Ist der ausgelesene Wert niedriger, als der bislang kleinste, dann wird der neue Z-Wert gespeichert und der Name des bislang nächsten Objektes wird gespeichert.<br><br />
Ist die Schleife durchgelaufen, dann steht am Ende also in der Variable "getroffen" der Integer-Name des nächsten, getroffenen Objektes, welches wir dann auch als Rückgabewert der Funktion verwenden.<br />
<br />
Das war nun alles zwar recht viel komplizierter Kram, aber ich kann euch versichern, das nicht mehr kommt. Denn mehr, als das was wir nun eben geschrieben haben, werden wir nie für einen Selektionsvorgang brauchen... egal, ob wir den genialsten Geheimdienstthriller der Welt oder nur einen Moorhuhn - Klon schreiben. *g*<br />
<br />
Zu guter Letzt müssen wir nun noch diese Prozedur aufrufen, wozu ich das "onMouseDown" - Ereignis des Formulars verwendet habe:<br />
<br />
<pascal>procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;<br />
Shift: TShiftState; X, Y: Integer);<br />
begin<br />
xs := x;<br />
ys := y;<br />
<br />
statusbar1.Simpletext := 'Sie haben auf ' + namen[selection] + ' geklickt!';<br />
end;</pascal><br />
<br />
<br />
Ich denke, es besteht kaum Erklärungsbedarf...<br />
<br />
==Geht's auch größer?==<br />
<br />
Jupp, natürlich kann man diese ganze Technik auch zu was anderem verwenden, als nur Formen zu identifizieren. (Das Sample oben ist ja nun wirklich unterhalb unseres Niveaus, auch wenn es einen schönen Einblick in die Materie gebracht hat) Aber - größenwahnsinnig, wie ich nun mal bin - ich strebe zu größerem *muhaha*: Der Weltherrschaft ;-)<br><br />
Nein, Spaß beiseite, ich will euch noch eben ein kleines Briefing geben, wie ihr auch etwas Größeres bauen könnt. (Zum anderen will Phobeus die auch gerne *g*)<br><br />
So habe ich ein kleines Sample gebastelt, in welchem man zwei Räume hat (mit einem kleinen Zwischengang), von denen der zweite Raum unbeleuchtet ist, wobei man das Licht mithilfe eines Schalters im ersten (beleuchtetem) Raum einschalten kann. Das ganze sieht dann etwas so aus: (links: Licht aus, rechts: Licht an)<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
|width="50%" |[[Bild:Tutorial_selection_02.jpg|center]]<br />
|width="50%" |[[Bild:Tutorial_selection_03.jpg|center]]<br />
|-<br />
|align="center" | Licht Aus<br />
|align="center" | Licht An (siehe Hintergrund)<br />
|-<br />
|}<br />
</div><br />
<br />
Das macht doch durchaus was her, oder? OK, ich gebe zu, die Texturen sind nicht alle von mir (die Lightmaps aber schon), aber ansonsten finde ich die Szene recht gelungen. Um euch eine kleine Bauanleitung zu geben, habe ich hier eben den Grundriss der Szene für euch:<br />
<br />
[[Bild:Tutorial_selection_04.jpg|center]]<br />
<br />
An sich sollte so was für euch kein echtes Problem mehr sein... die x und z-Koordinaten stehen alle auf dem Plan, den ihr hier seht. Dazu sei gesagt, dass alles in der Szene genau 2 Einheiten hoch ist.<br />
Des weiteren könnte es für euch noch nützlich sein, zu wissen, dass ich für die Lightmaps den Blendmodus "{{INLINE_CODE|[[glBlendFunc]](gl_dst_color, gl_zero);}}" verwende und dass ansonsten alles etwa so ist, wie auch in den Teilen 7 und 8. Das letzte, was ein kleiner Stolperstein sein könnte, ist die Distanz zum Schalter. Es ist zwar schon wunderherrlich, wenn man ihn überhaupt betätigen kann, aber irgendwie ist es auch schwachsinnig, wenn man am anderen Ende des Raumes stehen kann und ihn dennoch verwenden kann. Deshalb habe ich meine Klick rozedur so verändert, dass die Distanz zum Schalter, wen er funktionieren soll höchstens 3 Einheiten betragen darf:<br />
<br />
<pascal>procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;<br />
Shift: TShiftState; X, Y: Integer);<br />
var<br />
xdis : double;<br />
ydis : double;<br />
begin<br />
xdis := my_x+4;<br />
ydis := my_y-3.85;<br />
<br />
xs := x;<br />
ys := y;<br />
<br />
if selection=1 then<br />
begin<br />
if sqrt(xdis*xdis + ydis*ydis) <= 3 then<br />
lichtan := not lichtan;<br />
end;<br />
<br />
end;</pascal><br />
<br />
Und bitte nicht vergessen: bindet die Unit "Math" mit ein... ansonsten dürftet ihr auf die Wurzelfunktion (sqrt) keinen Zugriff haben! Im Übrigen: Der Schalter hat von mir den Namen "1" bekommen und die ganze Umgebung hat die Nummer "2".<br />
<br />
Damit solltet ihr eigentlich kaum ein Problem haben, dieses Sample zu verstehen, bzw. nachzubasteln. Notfalls stehe ich per Mail, ICQ oder auch in unserem DCW / DGL- Board immer zur Hilfe zur Verfügung. Im Übrigen: falls ihr mal hängt, könnt ihr ja auch ins Sample gucken und sehen, wie ich es gelöst habe.<br />
<br />
==Nachwort==<br />
<br />
Stellt euch mal vor, dass war es auch schon. Ich hoffe, ihr habt alle einigermaßen Spaß gehabt bei dem ganzen und ihr werdet (wenn ihr die DCW-Fassung des Tutorials lest) auch beim nächsten Mal wieder reinschauen. Von allen anderen erwarte ich aber zumindest, dass ihr möglichst viel von diesem Tut behaltet und ich demnächst viele schöne Maps mit vielen schönen Lichtschalten sehe ;-)<br />
Bis denne<br />
<br />
:'''DCW_Mr_T'''<br />
<br />
<br />
==Nachtrag==<br />
Weitere Infos/Beispiele im Artikel "[[Selektion]]".<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[Tutorial TexFilter]]}}<br />
<br />
[[Kategorie:Tutorial|Selection]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Callback&diff=14545Callback2005-11-25T20:23:53Z<p>Akira: /* Konzept */</p>
<hr />
<div>Callbacks sind Zeiger auf Funktionen<br />
<br />
==Konzept==<br />
Normalerweise ist ein Funktionsaufruf bereits während dem Linken bekannt. Das Programm weiß also schon, wo es hinspringen soll. <br />
<br />
Beim Entwerfen von Librarys ist es aber hin und wieder notwendig dem Nutzer freizustellen, wo das Programm weiter abgearbeitet werden soll. Das wird über Callbacks/Funktionspointer gelöst. Anstatt einen althergebrachten Funktionsaufruf zu benutzen wird also ein Zeiger auf eine Funktion gesetzt. Während der Laufzeit kann diesem Pointer ein anderer Wert zugewiesen werden, mit der Folge, dass beim nächsten Aufruf dieser Funktion das Programm an eine andere Stelle springt.<br />
<br />
==Beispiele==<br />
Das gängigste Beispiel sind GUI Klassen. Borland z.B. baut bei der VCL und CLX auf Funktionszeiger bei allen Ereignissen. Möchte man z.B. während der Laufzeit das Verhalten eines Buttons ändern kann man dies per<br />
<pascal>Btn.OnClick := zeigerAufOnClickFunktion;</pascal> machen.<br />
Ein weiteres Beispiel für Callbacks ist die "sort" Funktion bei TList. Siehe dazu die Delphihilfe.<br />
<br />
Callbacks werden z.B. auch bei der [[GLUT]] verwendet. Dort existieren Callbacks für viele verschiedene Funktionen wie z.B. die standard Zeichenfunktion, oder die Mausbehandlungsfunktion. Durch den Callbackansatz kann der Programmierer, der diese Bibiothek nutzt, einfach eine eigene Funktion schreiben (welche den selben Funktionskopf besitzt) und dem Programm mitteilen, dass beim Aufruf von Callback XYZ genau dieser Code ausgeführt werden soll.<br />
<br />
==Siehe Auch==<br />
[http://www.newty.de/fpt/index.html Tutorial zum Thema "function pointer" und "Callbacks" für C/C++ {{Englisch}}]</div>Akirahttps://wiki.delphigl.com/index.php?title=Sprite&diff=14539Sprite2005-11-25T15:34:17Z<p>Akira: /* Sprite */</p>
<hr />
<div>=Sprite=<br />
Ein Sprite ist ein zweidimensionales Rechteck in einem dreidimensionalen Raum, dabei wird das Sprite immer über die Szene gelegt.<br />
Für ein Sprite ist nur die X/Y-Koordinate, sowie die Ausmaße des Rechtecks von nöten. Die Tiefenkoordinate Z fällt dabei völlig weg.<br />
<br />
Früher (in den 80er Jahren) waren Sprites die einzig wirklich schnelle Möglichkeit Grafikobjekte auf dem Bildschirm darzustellen, da nur die X- und Y-Koordinaten angepasst werden mussten und dann direkt in den Grafikspeicher gerendert werden konnte. Heutzutage haben Sprites ihren Vorteil verloren, da moderne Grafikkarten auch komplexe Objekte hardwarebeschleunigt darstellen können.<br />
<br />
Sprites verwendet man heute noch für z.B. pseudo 2D Spiele sowie für GUI Elemente.<br />
Im Unterschied zu früher wird dies durch den sogenannten Orthogonal Modus erreicht. Eine genauere Erklärung hierzu finden Sie im folgendem [[Tutorial_2D|Tutorial]].<br />
<br />
==Vorteile==<br />
Durch die Nutzung von OpenGL kann man durch 2 Möglichkeiten Einfluss auf die Zeichenreihenfolge nehmen:<br />
Zu einem durch das richtige Nacheinanderzeichnen der einzelnen Sprites und zum anderen durch das Hinzuziehen der Z-Koordinate, was unsere Sprite dann über das System der Zeichenreihenfolge versetzt.<br />
<br />
OpenGL nutzt zur Darstellung der Grafiken die GPU, welche um vieles leistungsfähiger ist als alternative 2D Grafikbibliotheken.<br />
<br />
==Nachteil==<br />
Wenn man nicht gerade auf die Z-Koordinate zurückgreift hat man das Problem, dass man auf die Zeichenreihenfolge achten muss um ein korrektes Zeichnen zu Ermöglichen. Algorithmen zur Darstellung können somit größer werden und es kann auch dazu führen, das die Daten vorher per Hand sortiert werden müssen, was ansonsten die GPU für uns übernimmt.<br />
<br />
==Tipp==<br />
Schreiben Sie eine Hilfsfunktion, die die Sprites zeichnet und benutzen Sie dann folgende Reihenfolge für einen einfachen Code:<br />
<pascal> //Matrix auf den Stack legen<br />
glPushMatrix;<br />
//Rotation<br />
glRotatef(Sprite[i].Rotation, 0, 0, 1);<br />
//Skalierung<br />
glScalef(Sprite[i].Scale);<br />
//Transformation<br />
glTranslatef(Sprite[i].X, Sprite[i].X, Sprite[i].Layer);<br />
DrawSprite(Sprite[i].Width,Sprite[i].Height);<br />
//Matrix vom Stack zurück holen<br />
glPopMatrix;<br />
</pascal><br />
<br />
Dieser Variante hat den Vorteil, dass Sie mit "Sprite[i].Layer" die Position der Sprites in der Tiefe festlegen können und somit viel Code für die Sortierung und andere Funktionen sparen.<br />
<br />
== Siehe Auch ==<br />
[http://de.wikipedia.org/wiki/Sprite_%28Computergrafik%29 Wikipediaerklärung zu Sprites]</div>Akirahttps://wiki.delphigl.com/index.php?title=A-Stern&diff=14537A-Stern2005-11-25T14:34:25Z<p>Akira: /* A-Stern mit Kosten */</p>
<hr />
<div>Der A-Stern Algorithmus besitzt unter den Pathfinding-begeisterten nahezu den Status des heiligen Grales, und dies nicht zu unrecht. Der A-Stern Algorithmus liefert unter gewissen Voraussetzungen den optimalen Weg und dies noch dazu meistens in einem sehr akzeptablen Zeitraum.<br />
<br />
=Vorgehensweise=<br />
Ähnlich zur [[Breitensuche]] wird hier immer der erahnte kürzeste Weg bevorzugt. Zum "erahnen" eines Weges wird eine Heuristik benötigt, also ein Wert welcher Angibt wie weit es ungefähr noch bis zum Ziel sein wird. Die Breitensuche wird nun einfach um diese Heuristik erweitert und dadurch wird der nächste zu überprüfende Knoten bestimmt.<br />
<br />
=Algorithmus=<br />
Suche_Weg( Knoten Ziel, Knoten Start )<br />
Kosten_Heap KHeap<br />
KHeap.Add( Min_Kosten( Start, Ziel ) )<br />
<br />
Solange Ziel nicht gefunden<br />
K = Knoten mit minimalen Kosten aus KHeap<br />
<br />
Wenn K = Ziel<br />
Ziel gefunden<br />
<br />
Für K' = alle (nicht besuchten) Nachbarknoten von K<br />
KHeap.Add( Kosten( Start, K' ) + Min_Kosten( K', Ziel ), K' )<br />
Durch die minimalen Kosten von K zum Ziel setzen wir voraus, das wir eine derartige Funktion besitzen. Und es lässt sich recht einfach durch Worte beweisen, dass dieser Algorithmus wirklich den optimalen Weg findet:<br />
<br />
Dieser Algorithmus erweitert den Knoten mit den minimalen bisherigen Kosten + den minimal zu erwartenden Kosten.<br />
Wenn die minimal zu erwartenden Kosten kleiner (oder gleich) den tatsächlichen Kosten ab dem Punkt wo er<br />
sich befindet sind, so können die tatsächlichen Kosten für den ersten gefundenen Weg niemals höher sein als<br />
die Kosten für einen bereits durchsuchten Weg + dessen minimaler Kosten und somit auch niemals höher als die<br />
Kosten für einen bereits durchsuchten Weg + dessen tatsächliche Kosten zum Ziel.<br />
<br />
Der Algorithmus besitzt jedoch immer noch eine maximale Laufzeit von [[O-Notation|O]](n) jedoch kann man dies wiederum umformen in [[O-Notation|O]](t^v) wobei t die Tiefe des Zieles ist und v der (durchschnittliche) Verzweigungsgrad des Baumes, bzw. des Graphen ohne wiederholte Knoten ist. Jedoch für die durchschnittliche Laufzeit kann man hier v auch als Verzweigungsgrad des Baumes in die "ungefähre" Richtung des Zieles sehen, und dieser ist meistens um einiges geringer als der tatsächliche Verzweigungsgrad. Der maximale Speicherbedarf ist jedoch (leider) wiederum [[O-Notation|O]](t^v).<br />
<br />
Wenn jedoch der Wert der durch Min_Kosten berechnet wird die minimalen Kosten überschreiten kann, so liefert dieser Algorithmus nicht mehr zwingendermaßen den minimalen Weg, denn dann wird obiger (textuelle) Beweis nichtig. Aufzupassen ist somit auf schnellere Fortbewegungsmöglichkeiten wie beispielsweise:<br />
* Die Heuristik von A-Stern wird mit den Daten "zu Fuß gehen" gefüttert, es gibt jedoch auch eine Zugverbindung<br />
* Es gibt einen Teleporter welcher ohne Zeitverlust eine Passage von A nach B öffnet.<br />
Die minimalen Kosten müssen somit mit dem schnellsten Transportmittel über Luftlinie berechnet werden.<br />
<br />
Die Qualität des A-Stern-Algorithmus beruht somit auf der Qualität der Heuristik. Sowohl darauf, dass die Heuristik für die minimalen Kosten niemals höher wird als die tatsächlichen Kosten, als auch darauf, dass die tatsächlichen Kosten niemals allzu viel höher werden als die minimalen Kosten. Wenn man für diese Heuristik immer den Wert 0 verwendet, so entspricht A-Stern einer [[Breitensuche]].<br />
<br />
=A-Stern mit Kosten=<br />
Wie am Ende des vorigen Absatzes bereits klar geworden sein sollte, benötigt A-Stern Kosten (vor allem minimale Kosten), ansonsten degeneriert er zu einer [[Breitensuche]].</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Quickstart&diff=14536Tutorial Quickstart2005-11-25T14:31:00Z<p>Akira: /* Quickstart: Delphi & OpenGL */</p>
<hr />
<div>=Quickstart: Delphi & OpenGL=<br />
<br />
==Einleitung==<br />
<br />
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.<br />
<br />
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.<br />
<br />
==Wie fange ich an?==<br />
Genau zwei Dinge braucht der OpenGL-Programmierer um effektiv arbeiten zu können:<br />
#Einen OpenGL Header<br />
#Eine Codebasis von der aus man neue Projekte starten kann (ein sog. Template)<br />
<br />
<br />
===Der OpenGL-Header===<br />
Das is ja easy! Denn Delphi bringt ja schon einen OpenGL-Header mit...<br />
<br />
'''STOP!'''<br />
<br />
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.<br />
<br />
Was nun? Ganz einfach: Bei [http://www.DelphiGL.com DelphiGL.com] (kurz DGL) gibt es '''DEN''' OpenGL-Header für alle Pascalsprachen: <br />
'''Die [[DGLOpenGL.pas]].'''<br />
<br />
Diesen solltet Ihr euch jetzt besorgen, wenn Ihr ihn nicht schon habt. Der Header wird bei neuen OpenGL-Versionen vom DGL-Team aktualisiert.<br />
<br />
===Codebasis/Templates===<br />
So... das war schon alles was Ihr aus dem Netz benötigt. Den Rest machen wir jetzt per Hand.<br />
<br />
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.)<br />
<br />
<br />
<br />
==Das Template - Delphi fit für OpenGL machen==<br />
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.)<br />
<br />
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.<br />
<br />
''(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. )''<br />
<br />
===Initialisieren von OpenGL===<br />
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. <br />
<br />
Zuerst einmal solltet Ihr die DGLOpenGL.pas in die '''uses'''-Klausel des '''interface'''-Teils der Unit1 schreiben.<br />
<br />
Die eigentliche Initialisierung soll direkt beim Erstellen des Formulars gemacht werden. Deshalb kommt der folgende Quelltext ins OnCreate-Ereignis des Formulars.<br />
<pascal>procedure TForm1.FormCreate(Sender: TObject);<br />
begin<br />
DC:= GetDC(Handle);<br />
if not InitOpenGL then Application.Terminate;<br />
RC:= CreateRenderingContext( DC,<br />
[opDoubleBuffered],<br />
32,<br />
24,<br />
0,0,0,<br />
0);<br />
ActivateRenderingContext(DC, RC);<br />
end;</pascal><br />
'''Zeile 3:''' Hier wird der Gerätekontext (Device Context) von Formular Form1 abgefragt.<br />
<br />
'''Zeile 4:''' Mit InitOpenGL wird OpenGL initialisiert. Wenn das nicht funktioniert wird die gesamte Anwendung sofort beendet. <br />
<br />
'''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]].<br />
<br />
'''Zeile 11:''' Abschließend wird der Renderkontext aktiviert. OpenGL ist jetzt prinzipiell startbereit.<br />
<br />
{{Hinweis|DC und RC sind Eigenschaften des Formulars. Siehe [[Tutorial_quickstart#Das_fertige_Templateformular|Definition des Templateformulars]].}}<br />
<br />
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<br />
<br />
<pascal>procedure TForm1.SetupGL;<br />
begin<br />
glClearColor(0.3, 0.4, 0.7, 0.0); //Hintergrundfarbe<br />
glEnable(GL_DEPTH_TEST); //Tiefentest aktivieren<br />
glEnable(GL_CULL_FACE); //Backface Culling aktivieren<br />
end;</pascal><br />
<br />
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.)<br />
<br />
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.<br />
Beide Unterprogramme (SetupGL und Init) sollten am Ende von '''FormCreate''' gerufen werden:<br />
<br />
<pascal> [...]<br />
ActivateRenderingContext(DC, RC);<br />
SetupGL;<br />
Init;<br />
end;</pascal><br />
<br />
===Die Ereignisbehandlung===<br />
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 '''OnCreate-''' und das '''OnDestroy'''-Ereignis.<br />
<br />
Zuerst '''FormResize''':<br />
<pascal>procedure TForm1.FormResize(Sender: TObject);<br />
var tmpBool : Boolean;<br />
begin<br />
glViewport(0, 0, ClientWidth, ClientHeight);<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping); <br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
IdleHandler(Sender, tmpBool);<br />
end;</pascal><br />
<br />
Außerdem müssen im '''const''' Teil die beiden Konstanten Near- bzw. FarClipping definiert werden. Diese geben die Entfernung für die [[Clipping Plane|Clippingebenen]] (Szenenbegrenzung) an.<br />
<br />
<pascal> NearClipping = 1;<br />
FarClipping = 1000;</pascal><br />
<br />
'''Zu FormResize''':<br />
<br />
'''Zeile 2:''' Diese Boolean-Variable wird in Zeile 11 verwendet und ist nur ein Dummy.<br />
<br />
'''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.<br />
<br />
'''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.<br />
<br />
'''Zeile 6:''' [[glLoadIdentity]] füllt die aktuelle Matrix mit der Identitätsmatrix. <br />
<br />
'''Zeile 7:''' Hier wird eingestellt wie der Betrachter die Welt sehen soll. <br />
<br />
'''Zeile 11:''' Was der IdleHandler macht kommt später im Abschnitt 1.3.3 (Zeichenroutine).<br />
<br />
<br />
Nun noch schnell das '''FormDestroy''':<br />
<br />
<pascal>procedure TForm1.FormDestroy(Sender: TObject);<br />
begin<br />
DeactivateRenderingContext;<br />
DestroyRenderingContext(RC);<br />
ReleaseDC(Handle, DC);<br />
end;</pascal><br />
<br />
Was es macht? Steht doch da: Den RenderingContext deaktivieren und freigeben.<br />
<br />
<br />
===Die Zeichenroutine===<br />
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'''<br />
<br />
<pascal>procedure TForm1.Render;<br />
begin<br />
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);<br />
<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping);<br />
<br />
glTranslatef(0, 0, -5);<br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
<br />
glBegin(GL_QUADS);<br />
glColor3f(1, 0, 0); glVertex3f(0, 0, 0);<br />
glColor3f(0, 1, 0); glVertex3f(1, 0, 0);<br />
glColor3f(0, 0, 1); glVertex3f(1, 1, 0);<br />
glColor3f(1, 1, 0); glVertex3f(0, 1, 0);<br />
glEnd;<br />
<br />
SwapBuffers(DC);<br />
end;</pascal><br />
<br />
'''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.<br />
<br />
'''Zeile 7:''' Hier wird wieder die Perspektive gesetzt. Dieser Aufruf und der bei '''FormResize''' <br />
müssen von den Parametern identisch sein. Sonst sieht die Ausgabe nach einem Resize kurz anders aus. <br />
Wenn sich die Perspektive zwischen den Renderdurchgängen nicht ändert kann das auch weg gelassen werden.<br />
<br />
'''Zeile 9:''' Dieser Aufruf verschiebt die "Kamera" (so etwas gibt es eigentlich nicht, aber da wir <br />
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.<br />
<br />
[[Bild:glShadeModel_SMOOTH.jpg|right]]<br />
<br />
'''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.<br />
<br />
'''Zeile 21:''' [[SwapBuffers]] sorgt Ihr dafür, dass der Inhalt des [[Framebuffer]]s auf dem <br />
Bildschirm erscheint. Ohne diesen Befehl seht Ihr gar nichts von OpenGL. (Interessanter Artikel dazu: [[Doppelpufferung]])<br />
<br />
<br />
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.<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Argument<br />
!Timer<br />
!OnIdle<br />
|-<br />
|Maximale Framezahl erreichbar <br>"Benchmark"<br />
|nein<br />
|ja<br />
|-<br />
|Framezahl steuerbar<br />
|ja<br />
|bedingt (/umständlich)<br />
|-<br />
|Für flüssige Animationen nutzbar<br>(Egoshooter)<br />
|bedingt/schlecht<br />
|ja<br />
|-<br />
|Für Menüs nutzbar<br />
|ja<br />
|ja<br />
|-<br />
|Für einfache Animationen nutzbar<br>(Strategiespiele)<br />
|ja<br />
|ja<br />
|-<br />
|Laptopfreundlich (Anti-Akku-Killer)<br />
|ja<br />
|NEIN!<br />
|}<br />
</div><br />
<br />
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.<br />
<br />
<br />
====Methode 1: Timer====<br />
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.<br />
<br />
Der Inhalt von OnTimer könnte dieser sein:<br />
<br />
<pascal>procedure TForm1.Timer1Timer(Sender: TObject);<br />
begin<br />
inc(FrameCount);<br />
Render;<br />
If FrameCount = 20 then<br />
begin<br />
ErrorHandler;<br />
FrameCount := 0;<br />
end;<br />
end;</pascal><br />
<br />
Tiefgreifende Erklärungen sind hier nicht notwendig. Was der ErrorHandler ist wird nach der Methode 2 erklärt.<br />
<br />
<br />
====Methode 2 : OnIdle====<br />
<br />
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.<br />
<br />
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.<br />
<br />
<pascal>procedure TForm1.IdleHandler(Sender: TObject; var Done: Boolean);<br />
begin<br />
StartTime:= GetTickCount;<br />
Render;<br />
DrawTime:= GetTickCount - StartTime;<br />
Inc(TimeCount, DrawTime);<br />
Inc(FrameCount);<br />
<br />
if TimeCount >= 1000 then begin<br />
Frames:= FrameCount;<br />
TimeCount:= TimeCount - 1000;<br />
FrameCount:= 0;<br />
Caption:= InttoStr(Frames) + 'FPS';<br />
ErrorHandler;<br />
end;<br />
<br />
Done:= false;<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Mittels GetTickCount wird die Systemzeit gemessen. Dies ist nicht nötig um <br />
erfolgreich zu zeichnen, sondern dient ausschließlich der Berechnung der Framerate. Diese wiederum ist ein guter Performancemesser.<br />
<br />
'''Zeile 4:''' Hier erfolgt der Aufruf unserer Zeichenroutine.<br />
<br />
'''Zeile 9:''' Der hier angeordnete Block wird nur pro Sekunde einmal ausgeführt und sorgt <br />
dafür, dass die Framerate angezeigt wird. Außerdem wird der Errorhandler aufgerufen.<br />
<br />
'''Zeile 14:''' Der Errorhandler wird im Anschluß beschrieben.<br />
<br />
'''Zeile 17:''' Wenn ''Done'' nach der Ausführung ''false'' ist und das Programm wieder nichts zu tun <br />
hat, wird OnIdle erneut ausgeführt. Wenn ''Done = true'' ist wird OnIdle nur einmal ausgeführt.<br />
<br />
<br />
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.<br />
<br />
<pascal>Application.OnIdle := IdleHandler;</pascal><br />
<br />
Wie ihr bald selbst feststellen werdet, sorgt diese Methode für eine hundertprozentige Prozessorauslastung. Umgehen könnt ihr dies mit einem kleinen Trick:<br />
<br />
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.<br />
<br />
<br />
===Der ErrorHandler - Fehler erkannt, Fehler gebannt===<br />
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:<br />
<br />
<pascal>procedure TForm1.ErrorHandler;<br />
begin<br />
Form1.Caption := gluErrorString(glGetError);<br />
end;</pascal><br />
<br />
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'''.<br />
<br />
===Das fertige Templateformular===<br />
Eure Klasse TForm1 sollte also jetzt so, oder so ähnlich aussehen:<br />
<pascal>TForm1 = class(TForm)<br />
procedure FormCreate(Sender: TObject);<br />
procedure IdleHandler(Sender: TObject; var Done: Boolean);<br />
procedure FormResize(Sender: TObject);<br />
procedure FormDestroy(Sender: TObject);<br />
private { Private-Deklarationen }<br />
StartTime, TimeCount, FrameCount : Cardinal; //FrameCounter<br />
Frames, DrawTime : Cardinal; //& Timebased Movement<br />
procedure SetupGL;<br />
procedure Init;<br />
procedure Render;<br />
procedure ErrorHandler;<br />
public { Public-Deklarationen }<br />
DC : HDC; //Handle auf Zeichenfläche<br />
RC : HGLRC;//Rendering Context<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Wie man sieht benutzt mein Template die zweite Methode (OnIdle).<br />
<br />
'''Zeile 8:''' Was [[Timebased Movement]] ist könnt ihr ja mal nachlesen.<br />
<br />
'''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.)<br />
<br />
<br />
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.<br />
<br />
<br />
<br />
<br />
<br />
==Tipps für den OpenGL Anfänger==<br />
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...<br />
<br />
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". <br />
<br />
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)<br />
<br />
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:<br />
<br />
<br />
<u>1. Je älter der Code ist desto besser wird er.</u><br />
<br />
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.<br />
<br />
''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!)''<br />
<br />
<br />
<u>2. Große Projekte = Großer Ruhm</u><br />
<br />
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 ... .<br />
<br />
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.<br />
<br />
'''Und woher soll ich bitte Erfahrung nehmen?'''<br />
<br />
Darin liegt der Trick:<br />
<br />
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. <br />
<br />
<br />
Ihr denkt jetzt bestimmt "Was kann man denn schon in so nem Clon unterbringen?" '''VERDAMMT VIEL!''' Hier mal ein kleiner Auszug:<br />
<br />
*3D-Spieldarstellung<br />
*2D Menüführung ([[glOrtho]])<br />
*[[Blending]] <br />
*[[Textur]]en <br />
*Licht und Materialien (Ihr werdet Augen machen!)<br />
*[[Selektion]] (Hin und wieder zum Haare raufen.)<br />
*Kamerasteuerung /Bewegung durch die Szene (Da gibt es ganze Tutorials dazu)<br />
<br />
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.<br />
<br />
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 ;) )<br />
<br />
'''Als kurze Zusammenfassung solltet Ihr euch merken:'''<br />
<br />
*Ein fertiges Projekt bringt euch Ruhm (Seelenbalsam).<br />
*Ein hübsches, fertiges Projekt bringt euch mehr Ruhm.<br />
*Ein großes, fertiges, hübsches Projekt bringt euch noch mehr Ruhm.<br />
<br />
*Ein abgebrochenes oder eingefrorenes Projekt bringt euch Frust.<br />
*Ein großes, abgebrochenes Projekt bringt euch noch mehr Frust, denn der verschwendeten Zeit werdet Ihr nachtrauern.<br />
<br />
<br />
<u>3.Das es lang dauert ist egal. Ich interessiere mich halt dafür.</u><br />
<br />
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.<br />
<br />
==Nachwort==<br />
<br />
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...<br />
<br />
<br />
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.<br />
<br />
<br />
Bis bald im [http://delphiGL.com/forum Forum]<br />
<br />
'''Euer'''<br />
<br />
'''Flash (Kevin Fleischer)'''<br />
<br />
<br />
PS: Feedback wird nicht nur gewünscht, sondern ausdrücklich gefordert. Deshalb: Ab mit deinen Meinungen und Ratschläge ins [http://www.delphigl.com/forum/viewforum.php?f=8 Feedback-Forum]!<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[Tutorial_lektion1]]}}<br />
<br />
[[Kategorie:Tutorial|Quickstart]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Quickstart&diff=14535Tutorial Quickstart2005-11-25T14:20:58Z<p>Akira: /* Das Template Oder Delphi fit für OpenGL machen */</p>
<hr />
<div>=Quickstart: Delphi & OpenGL=<br />
<br />
==Einleitung==<br />
<br />
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.<br />
<br />
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.<br />
<br />
==Wie fange ich an?==<br />
Genau zwei Dinge braucht der OpenGL-Programmierer um effektiv arbeiten zu können:<br />
#Einen OpenGL Header<br />
#Eine Codebasis von der aus man neue Projekte starten kann (ein sog. Template)<br />
<br />
<br />
===Der OpenGL-Header===<br />
Das is ja easy! Denn Delphi bringt ja schon einen OpenGL-Header mit... <br><br />
'''STOP!'''<br />
<br />
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.<br />
<br />
Was nun? Ganz einfach: Bei [http://www.DelphiGL.com DelphiGL.com] (kurz DGL) gibt es '''DEN''' OpenGL-Header für alle Pascalsprachen: <br />
'''Die [[DGLOpenGL.pas]].'''<br />
<br />
Diesen solltet Ihr Euch jetzt besorgen, wenn Ihr ihn nicht schon habt. Der Header wird bei neuen OpenGL-Versionen vom DGL-Team aktualisiert.<br />
<br />
===Codebasis/Templates===<br />
So... das war schon alles was Ihr aus dem Netz benötigt. Den Rest machen wir jetzt per Hand.<br />
<br />
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.)<br />
<br />
<br />
<br />
==Das Template - Delphi fit für OpenGL machen==<br />
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.)<br />
<br />
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.<br />
<br />
''(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. )''<br />
<br />
===Initialisieren von OpenGL===<br />
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. <br />
<br />
Zuerst einmal solltet Ihr die DGLOpenGL.pas in die '''uses'''-Klausel des '''interface'''-Teils der Unit1 schreiben.<br />
<br />
Die eigentliche Initialisierung soll direkt beim Erstellen des Formulars gemacht werden. Deshalb kommt der folgende Quelltext ins OnCreate-Ereignis des Formulars.<br />
<pascal>procedure TForm1.FormCreate(Sender: TObject);<br />
begin<br />
DC:= GetDC(Handle);<br />
if not InitOpenGL then Application.Terminate;<br />
RC:= CreateRenderingContext( DC,<br />
[opDoubleBuffered],<br />
32,<br />
24,<br />
0,0,0,<br />
0);<br />
ActivateRenderingContext(DC, RC);<br />
end;</pascal><br />
'''Zeile 3:''' Hier wird der Gerätekontext (Device Context) von Formular Form1 abgefragt.<br />
<br />
'''Zeile 4:''' Mit InitOpenGL wird OpenGL initialisiert. Wenn das nicht funktioniert wird die gesamte Anwendung sofort beendet. <br />
<br />
'''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]].<br />
<br />
'''Zeile 11:''' Abschließend wird der Renderkontext aktiviert. OpenGL ist jetzt prinzipiell startbereit.<br />
<br />
{{Hinweis|DC und RC sind Eigenschaften des Formulars. Siehe [[Tutorial_quickstart#Das_fertige_Templateformular|Definition des Templateformulars]].}}<br />
<br />
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<br />
<br />
<pascal>procedure TForm1.SetupGL;<br />
begin<br />
glClearColor(0.3, 0.4, 0.7, 0.0); //Hintergrundfarbe<br />
glEnable(GL_DEPTH_TEST); //Tiefentest aktivieren<br />
glEnable(GL_CULL_FACE); //Backface Culling aktivieren<br />
end;</pascal><br />
<br />
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.)<br />
<br />
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.<br />
Beide Unterprogramme (SetupGL und Init) sollten am Ende von '''FormCreate''' gerufen werden:<br />
<br />
<pascal> [...]<br />
ActivateRenderingContext(DC, RC);<br />
SetupGL;<br />
Init;<br />
end;</pascal><br />
<br />
===Die Ereignisbehandlung===<br />
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 '''OnCreate-''' und das '''OnDestroy'''-Ereignis.<br />
<br />
Zuerst '''FormResize''':<br />
<pascal>procedure TForm1.FormResize(Sender: TObject);<br />
var tmpBool : Boolean;<br />
begin<br />
glViewport(0, 0, ClientWidth, ClientHeight);<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping); <br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
IdleHandler(Sender, tmpBool);<br />
end;</pascal><br />
<br />
Außerdem müssen im '''const''' Teil die beiden Konstanten Near- bzw. FarClipping definiert werden. Diese geben die Entfernung für die [[Clipping Plane|Clippingebenen]] (Szenenbegrenzung) an.<br />
<br />
<pascal> NearClipping = 1;<br />
FarClipping = 1000;</pascal><br />
<br />
'''Zu FormResize''':<br><br />
'''Zeile 2:''' Diese Boolean-Variable wird in Zeile 11 verwendet und ist nur ein Dummy.<br />
<br />
'''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.<br />
<br />
'''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.<br />
<br />
'''Zeile 6:''' [[glLoadIdentity]] füllt die aktuelle Matrix mit der Identitätsmatrix. <br />
<br />
'''Zeile 7:''' Hier wird eingestellt wie der Betrachter die Welt sehen soll. <br />
<br />
'''Zeile 11:''' Was der IdleHandler macht kommt später im Abschnitt 1.3.3 (Zeichenroutine).<br />
<br />
<br />
Nun noch schnell das '''FormDestroy''':<br />
<br />
<pascal>procedure TForm1.FormDestroy(Sender: TObject);<br />
begin<br />
DeactivateRenderingContext;<br />
DestroyRenderingContext(RC);<br />
ReleaseDC(Handle, DC);<br />
end;</pascal><br />
<br />
Was es macht? Steht doch da: Den RenderingContext deaktivieren und freigeben.<br />
<br />
<br />
===Die Zeichenroutine===<br />
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'''<br />
<br />
<pascal>procedure TForm1.Render;<br />
begin<br />
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);<br />
<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping);<br />
<br />
glTranslatef(0, 0, -5);<br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
<br />
glBegin(GL_QUADS);<br />
glColor3f(1, 0, 0); glVertex3f(0, 0, 0);<br />
glColor3f(0, 1, 0); glVertex3f(1, 0, 0);<br />
glColor3f(0, 0, 1); glVertex3f(1, 1, 0);<br />
glColor3f(1, 1, 0); glVertex3f(0, 1, 0);<br />
glEnd;<br />
<br />
SwapBuffers(DC);<br />
end;</pascal><br />
<br />
'''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.<br />
<br />
'''Zeile 7:''' Hier wird wieder die Perspektive gesetzt. Dieser Aufruf und der bei '''FormResize''' <br />
müssen von den Parametern identisch sein. Sonst sieht die Ausgabe nach einem Resize kurz anders aus. <br />
Wenn sich die Perspektive zwischen den Renderdurchgängen nicht ändert kann das auch weg gelassen werden.<br />
<br />
'''Zeile 9:''' Dieser Aufruf verschiebt die "Kamera" (so etwas gibt es eigentlich nicht, aber da wir <br />
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.<br />
<br />
[[Bild:glShadeModel_SMOOTH.jpg|right]]<br />
<br />
'''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.<br />
<br />
'''Zeile 21:''' [[SwapBuffers]] sorgt Ihr dafür, dass der Inhalt des [[Framebuffer]]s auf dem <br />
Bildschirm erscheint. Ohne diesen Befehl seht Ihr gar nichts von OpenGL. (Interessanter Artikel dazu: [[Doppelpufferung]])<br />
<br />
<br />
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.<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Argument<br />
!Timer<br />
!OnIdle<br />
|-<br />
|Maximale Framezahl erreichbar <br>"Benchmark"<br />
|nein<br />
|ja<br />
|-<br />
|Framezahl steuerbar<br />
|ja<br />
|bedingt (/umständlich)<br />
|-<br />
|Für flüssige Animationen nutzbar<br>(Egoshooter)<br />
|bedingt/schlecht<br />
|ja<br />
|-<br />
|Für Menüs nutzbar<br />
|ja<br />
|ja<br />
|-<br />
|Für einfache Animationen nutzbar<br>(Strategiespiele)<br />
|ja<br />
|ja<br />
|-<br />
|Laptopfreundlich (Anti-Akku-Killer)<br />
|ja<br />
|NEIN!<br />
|}<br />
</div><br />
<br />
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.<br />
<br />
<br />
====Methode 1: Timer====<br />
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.<br />
<br />
Der Inhalt von OnTimer könnte dieser sein:<br />
<br />
<pascal>procedure TForm1.Timer1Timer(Sender: TObject);<br />
begin<br />
inc(FrameCount);<br />
Render;<br />
If FrameCount = 20 then<br />
begin<br />
ErrorHandler;<br />
FrameCount := 0;<br />
end;<br />
end;</pascal><br />
<br />
Tiefgreifende Erklärungen sind hier nicht notwendig. Was der ErrorHandler ist wird nach der Methode 2 erklärt.<br />
<br />
<br />
====Methode 2 : OnIdle====<br />
<br />
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.<br><br />
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.<br />
<br />
<pascal>procedure TForm1.IdleHandler(Sender: TObject; var Done: Boolean);<br />
begin<br />
StartTime:= GetTickCount;<br />
Render;<br />
DrawTime:= GetTickCount - StartTime;<br />
Inc(TimeCount, DrawTime);<br />
Inc(FrameCount);<br />
<br />
if TimeCount >= 1000 then begin<br />
Frames:= FrameCount;<br />
TimeCount:= TimeCount - 1000;<br />
FrameCount:= 0;<br />
Caption:= InttoStr(Frames) + 'FPS';<br />
ErrorHandler;<br />
end;<br />
<br />
Done:= false;<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Mittels GetTickCount wird die Systemzeit gemessen. Dies ist nicht nötig um <br />
erfolgreich zu zeichnen, sondern dient ausschließlich der Berechnung der Framerate. Diese wiederum ist ein guter Performancemesser.<br />
<br />
'''Zeile 4:''' Hier erfolgt der Aufruf unserer Zeichenroutine.<br />
<br />
'''Zeile 9:''' Der hier angeordnete Block wird nur pro Sekunde einmal ausgeführt und sorgt <br />
dafür, dass die Framerate angezeigt wird. Außerdem wird der Errorhandler aufgerufen.<br />
<br />
'''Zeile 14:''' Der Errorhandler wird im Anschluß beschrieben.<br />
<br />
'''Zeile 17:''' Wenn ''Done'' nach der Ausführung ''false'' ist und das Programm wieder nichts zu tun <br />
hat, wird OnIdle erneut ausgeführt. Wenn ''Done = true'' ist wird OnIdle nur einmal ausgeführt.<br />
<br />
<br />
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.<br />
<br />
<pascal>Application.OnIdle := IdleHandler;</pascal><br />
<br />
Wie ihr bald selbst feststellen werdet, sorgt diese Methode für eine hundertprozentige Prozessorauslastung. Umgehen könnt ihr dies mit einem kleinen Trick:<br> <br />
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.<br><br />
<br />
===Der ErrorHandler - Fehler erkannt, Fehler gebannt===<br />
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:<br />
<br />
<pascal>procedure TForm1.ErrorHandler;<br />
begin<br />
Form1.Caption := gluErrorString(glGetError);<br />
end;</pascal><br />
<br />
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'''.<br />
<br />
===Das fertige Templateformular===<br />
Eure Klasse TForm1 sollte also jetzt so, oder so ähnlich aussehen:<br />
<pascal>TForm1 = class(TForm)<br />
procedure FormCreate(Sender: TObject);<br />
procedure IdleHandler(Sender: TObject; var Done: Boolean);<br />
procedure FormResize(Sender: TObject);<br />
procedure FormDestroy(Sender: TObject);<br />
private { Private-Deklarationen }<br />
StartTime, TimeCount, FrameCount : Cardinal; //FrameCounter<br />
Frames, DrawTime : Cardinal; //& Timebased Movement<br />
procedure SetupGL;<br />
procedure Init;<br />
procedure Render;<br />
procedure ErrorHandler;<br />
public { Public-Deklarationen }<br />
DC : HDC; //Handle auf Zeichenfläche<br />
RC : HGLRC;//Rendering Context<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Wie man sieht benutzt mein Template die zweite Methode (OnIdle).<br />
<br />
'''Zeile 8:''' Was [[Timebased Movement]] ist könnt ihr ja mal nachlesen.<br />
<br />
'''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.)<br />
<br />
<br />
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.<br />
<br />
<br><br><br />
<br />
==Tipps für den OpenGL Anfänger==<br />
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...<br />
<br />
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". <br />
<br />
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)<br />
<br />
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:<br />
<br />
<br />
<u>1. Je älter der Code ist desto besser wird er.</u><br />
<br />
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.<br />
<br />
''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!)''<br />
<br />
<br />
<u>2. Große Projekte = Großer Ruhm</u><br />
<br />
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 ... .<br />
<br />
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.<br />
<br />
'''Und woher soll ich bitte Erfahrung nehmen?'''<br><br />
Darin liegt der Trick: <br><br />
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. <br />
<br />
<br />
Ihr denkt jetzt bestimmt "Was kann man denn schon in so nem Clon unterbringen?" '''VERDAMMT VIEL!''' Hier mal ein kleiner Auszug:<br />
<br />
*3D-Spieldarstellung<br />
*2D Menüführung ([[glOrtho]])<br />
*[[Blending]] <br />
*[[Textur]]en <br />
*Licht und Materialien (Ihr werdet Augen machen!)<br />
*[[Selektion]] (Hin und wieder zum Haare raufen.)<br />
*Kamerasteuerung /Bewegung durch die Szene (Da gibt es ganze Tutorials dazu)<br />
<br />
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.<br><br />
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 ;) )<br />
<br />
'''Als kurze Zusammenfassung solltet Ihr Euch merken:'''<br />
<br />
*Ein fertiges Projekt bringt Euch Ruhm (Seelenbalsam).<br />
*Ein hübsches, fertiges Projekt bringt Euch mehr Ruhm.<br />
*Ein großes, fertiges, hübsches Projekt bringt Euch noch mehr Ruhm.<br><br />
<br />
*Ein abgebrochenes oder eingefrorenes Projekt bringt Euch Frust.<br />
*Ein großes, abgebrochenes Projekt bringt Euch noch mehr Frust, denn der verschwendeten Zeit werdet Ihr nachtrauern.<br />
<br />
<br />
<u>3.Das es lang dauert ist egal. Ich interessiere mich halt dafür.</u><br />
<br />
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.<br />
<br />
==Nachwort==<br />
<br />
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...<br />
<br />
<br />
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.<br />
<br />
<br />
Bis bald im [http://delphiGL.com/forum Forum]<br><br />
'''Euer''' <br><br />
'''Flash (Kevin Fleischer)'''<br />
<br />
<br />
PS: Feedback wird nicht nur gewünscht, sondern ausdrücklich gefordert. Deshalb: Ab mit deinen Meinungen und Ratschläge ins [http://www.delphigl.com/forum/viewforum.php?f=8 Feedback-Forum]!<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[Tutorial_lektion1]]}}<br />
<br />
[[Kategorie:Tutorial|Quickstart]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Quickstart&diff=14534Tutorial Quickstart2005-11-25T14:15:42Z<p>Akira: /* Der OpenGL-Header */</p>
<hr />
<div>=Quickstart: Delphi & OpenGL=<br />
<br />
==Einleitung==<br />
<br />
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.<br />
<br />
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.<br />
<br />
==Wie fange ich an?==<br />
Genau zwei Dinge braucht der OpenGL-Programmierer um effektiv arbeiten zu können:<br />
#Einen OpenGL Header<br />
#Eine Codebasis von der aus man neue Projekte starten kann (ein sog. Template)<br />
<br />
<br />
===Der OpenGL-Header===<br />
Das is ja easy! Denn Delphi bringt ja schon einen OpenGL-Header mit... <br><br />
'''STOP!'''<br />
<br />
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.<br />
<br />
Was nun? Ganz einfach: Bei [http://www.DelphiGL.com DelphiGL.com] (kurz DGL) gibt es '''DEN''' OpenGL-Header für alle Pascalsprachen: <br />
'''Die [[DGLOpenGL.pas]].'''<br />
<br />
Diesen solltet Ihr Euch jetzt besorgen, wenn Ihr ihn nicht schon habt. Der Header wird bei neuen OpenGL-Versionen vom DGL-Team aktualisiert.<br />
<br />
===Codebasis/Templates===<br />
So... das war schon alles was Ihr aus dem Netz benötigt. Den Rest machen wir jetzt per Hand.<br />
<br />
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.)<br />
<br />
<br />
<br />
==Das Template Oder Delphi fit für OpenGL machen==<br />
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.)<br />
<br />
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.<br><br />
''(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. )''<br />
<br />
===Initialisieren von OpenGL===<br />
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. <br />
<br />
Zuerst einmal solltet Ihr die DGLOpenGL.pas in die '''uses'''-Klausel des '''interface'''-Teils der Unit1 schreiben.<br />
<br />
Die eigentliche Initialisierung soll direkt beim Erstellen des Formulars gemacht werden. Deshalb kommt der folgende Quelltext ins OnCreate-Ereignis des Formulars.<br />
<pascal>procedure TForm1.FormCreate(Sender: TObject);<br />
begin<br />
DC:= GetDC(Handle);<br />
if not InitOpenGL then Application.Terminate;<br />
RC:= CreateRenderingContext( DC,<br />
[opDoubleBuffered],<br />
32,<br />
24,<br />
0,0,0,<br />
0);<br />
ActivateRenderingContext(DC, RC);<br />
end;</pascal><br />
'''Zeile 3:''' Hier wird der Gerätekontext (Device Context) von Formular Form1 abgefragt.<br />
<br />
'''Zeile 4:''' Mit InitOpenGL wird OpenGL initialisiert. Wenn das nicht funktioniert wird die gesamte Anwendung sofort beendet. <br />
<br />
'''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]].<br />
<br />
'''Zeile 11:''' Abschließend wird der Renderkontext aktiviert. OpenGL ist jetzt prinzipiell startbereit.<br />
<br />
{{Hinweis|DC und RC sind Eigenschaften des Formulars. Siehe [[Tutorial_quickstart#Das_fertige_Templateformular|Definition des Templateformulars]].}}<br />
<br />
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<br />
<br />
<pascal>procedure TForm1.SetupGL;<br />
begin<br />
glClearColor(0.3, 0.4, 0.7, 0.0); //Hintergrundfarbe<br />
glEnable(GL_DEPTH_TEST); //Tiefentest aktivieren<br />
glEnable(GL_CULL_FACE); //Backface Culling aktivieren<br />
end;</pascal><br />
<br />
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.)<br />
<br />
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.<br />
Beide Unterprogramme (SetupGL und Init) sollten am Ende von '''FormCreate''' gerufen werden:<br />
<br />
<pascal> [...]<br />
ActivateRenderingContext(DC, RC);<br />
SetupGL;<br />
Init;<br />
end;</pascal><br />
<br />
===Die Ereignisbehandlung===<br />
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 '''OnCreate-''' und das '''OnDestroy'''-Ereignis.<br />
<br />
Zuerst '''FormResize''':<br />
<pascal>procedure TForm1.FormResize(Sender: TObject);<br />
var tmpBool : Boolean;<br />
begin<br />
glViewport(0, 0, ClientWidth, ClientHeight);<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping); <br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
IdleHandler(Sender, tmpBool);<br />
end;</pascal><br />
<br />
Außerdem müssen im '''const''' Teil die beiden Konstanten Near- bzw. FarClipping definiert werden. Diese geben die Entfernung für die [[Clipping Plane|Clippingebenen]] (Szenenbegrenzung) an.<br />
<br />
<pascal> NearClipping = 1;<br />
FarClipping = 1000;</pascal><br />
<br />
'''Zu FormResize''':<br><br />
'''Zeile 2:''' Diese Boolean-Variable wird in Zeile 11 verwendet und ist nur ein Dummy.<br />
<br />
'''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.<br />
<br />
'''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.<br />
<br />
'''Zeile 6:''' [[glLoadIdentity]] füllt die aktuelle Matrix mit der Identitätsmatrix. <br />
<br />
'''Zeile 7:''' Hier wird eingestellt wie der Betrachter die Welt sehen soll. <br />
<br />
'''Zeile 11:''' Was der IdleHandler macht kommt später im Abschnitt 1.3.3 (Zeichenroutine).<br />
<br />
<br />
Nun noch schnell das '''FormDestroy''':<br />
<br />
<pascal>procedure TForm1.FormDestroy(Sender: TObject);<br />
begin<br />
DeactivateRenderingContext;<br />
DestroyRenderingContext(RC);<br />
ReleaseDC(Handle, DC);<br />
end;</pascal><br />
<br />
Was es macht? Steht doch da: Den RenderingContext deaktivieren und freigeben.<br />
<br />
<br />
===Die Zeichenroutine===<br />
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'''<br />
<br />
<pascal>procedure TForm1.Render;<br />
begin<br />
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);<br />
<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping);<br />
<br />
glTranslatef(0, 0, -5);<br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
<br />
glBegin(GL_QUADS);<br />
glColor3f(1, 0, 0); glVertex3f(0, 0, 0);<br />
glColor3f(0, 1, 0); glVertex3f(1, 0, 0);<br />
glColor3f(0, 0, 1); glVertex3f(1, 1, 0);<br />
glColor3f(1, 1, 0); glVertex3f(0, 1, 0);<br />
glEnd;<br />
<br />
SwapBuffers(DC);<br />
end;</pascal><br />
<br />
'''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.<br />
<br />
'''Zeile 7:''' Hier wird wieder die Perspektive gesetzt. Dieser Aufruf und der bei '''FormResize''' <br />
müssen von den Parametern identisch sein. Sonst sieht die Ausgabe nach einem Resize kurz anders aus. <br />
Wenn sich die Perspektive zwischen den Renderdurchgängen nicht ändert kann das auch weg gelassen werden.<br />
<br />
'''Zeile 9:''' Dieser Aufruf verschiebt die "Kamera" (so etwas gibt es eigentlich nicht, aber da wir <br />
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.<br />
<br />
[[Bild:glShadeModel_SMOOTH.jpg|right]]<br />
<br />
'''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.<br />
<br />
'''Zeile 21:''' [[SwapBuffers]] sorgt Ihr dafür, dass der Inhalt des [[Framebuffer]]s auf dem <br />
Bildschirm erscheint. Ohne diesen Befehl seht Ihr gar nichts von OpenGL. (Interessanter Artikel dazu: [[Doppelpufferung]])<br />
<br />
<br />
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.<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Argument<br />
!Timer<br />
!OnIdle<br />
|-<br />
|Maximale Framezahl erreichbar <br>"Benchmark"<br />
|nein<br />
|ja<br />
|-<br />
|Framezahl steuerbar<br />
|ja<br />
|bedingt (/umständlich)<br />
|-<br />
|Für flüssige Animationen nutzbar<br>(Egoshooter)<br />
|bedingt/schlecht<br />
|ja<br />
|-<br />
|Für Menüs nutzbar<br />
|ja<br />
|ja<br />
|-<br />
|Für einfache Animationen nutzbar<br>(Strategiespiele)<br />
|ja<br />
|ja<br />
|-<br />
|Laptopfreundlich (Anti-Akku-Killer)<br />
|ja<br />
|NEIN!<br />
|}<br />
</div><br />
<br />
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.<br />
<br />
<br />
====Methode 1: Timer====<br />
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.<br />
<br />
Der Inhalt von OnTimer könnte dieser sein:<br />
<br />
<pascal>procedure TForm1.Timer1Timer(Sender: TObject);<br />
begin<br />
inc(FrameCount);<br />
Render;<br />
If FrameCount = 20 then<br />
begin<br />
ErrorHandler;<br />
FrameCount := 0;<br />
end;<br />
end;</pascal><br />
<br />
Tiefgreifende Erklärungen sind hier nicht notwendig. Was der ErrorHandler ist wird nach der Methode 2 erklärt.<br />
<br />
<br />
====Methode 2 : OnIdle====<br />
<br />
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.<br><br />
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.<br />
<br />
<pascal>procedure TForm1.IdleHandler(Sender: TObject; var Done: Boolean);<br />
begin<br />
StartTime:= GetTickCount;<br />
Render;<br />
DrawTime:= GetTickCount - StartTime;<br />
Inc(TimeCount, DrawTime);<br />
Inc(FrameCount);<br />
<br />
if TimeCount >= 1000 then begin<br />
Frames:= FrameCount;<br />
TimeCount:= TimeCount - 1000;<br />
FrameCount:= 0;<br />
Caption:= InttoStr(Frames) + 'FPS';<br />
ErrorHandler;<br />
end;<br />
<br />
Done:= false;<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Mittels GetTickCount wird die Systemzeit gemessen. Dies ist nicht nötig um <br />
erfolgreich zu zeichnen, sondern dient ausschließlich der Berechnung der Framerate. Diese wiederum ist ein guter Performancemesser.<br />
<br />
'''Zeile 4:''' Hier erfolgt der Aufruf unserer Zeichenroutine.<br />
<br />
'''Zeile 9:''' Der hier angeordnete Block wird nur pro Sekunde einmal ausgeführt und sorgt <br />
dafür, dass die Framerate angezeigt wird. Außerdem wird der Errorhandler aufgerufen.<br />
<br />
'''Zeile 14:''' Der Errorhandler wird im Anschluß beschrieben.<br />
<br />
'''Zeile 17:''' Wenn ''Done'' nach der Ausführung ''false'' ist und das Programm wieder nichts zu tun <br />
hat, wird OnIdle erneut ausgeführt. Wenn ''Done = true'' ist wird OnIdle nur einmal ausgeführt.<br />
<br />
<br />
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.<br />
<br />
<pascal>Application.OnIdle := IdleHandler;</pascal><br />
<br />
Wie ihr bald selbst feststellen werdet, sorgt diese Methode für eine hundertprozentige Prozessorauslastung. Umgehen könnt ihr dies mit einem kleinen Trick:<br> <br />
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.<br><br />
<br />
===Der ErrorHandler Fehler erkannt, Fehler gebannt===<br />
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:<br />
<br />
<pascal>procedure TForm1.ErrorHandler;<br />
begin<br />
Form1.Caption := gluErrorString(glGetError);<br />
end;</pascal><br />
<br />
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'''.<br />
<br />
===Das fertige Templateformular===<br />
Eure Klasse TForm1 sollte also jetzt so, oder so ähnlich aussehen:<br />
<pascal>TForm1 = class(TForm)<br />
procedure FormCreate(Sender: TObject);<br />
procedure IdleHandler(Sender: TObject; var Done: Boolean);<br />
procedure FormResize(Sender: TObject);<br />
procedure FormDestroy(Sender: TObject);<br />
private { Private-Deklarationen }<br />
StartTime, TimeCount, FrameCount : Cardinal; //FrameCounter<br />
Frames, DrawTime : Cardinal; //& Timebased Movement<br />
procedure SetupGL;<br />
procedure Init;<br />
procedure Render;<br />
procedure ErrorHandler;<br />
public { Public-Deklarationen }<br />
DC : HDC; //Handle auf Zeichenfläche<br />
RC : HGLRC;//Rendering Context<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Wie man sieht benutzt mein Template die zweite Methode (OnIdle).<br />
<br />
'''Zeile 8:''' Was [[Timebased Movement]] ist könnt ihr ja mal nachlesen.<br />
<br />
'''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.)<br />
<br />
<br />
So....fertig. Eigentlich seid Ihr jetzt soweit von den Tutorialschreibern so richtig mit OpenGLWissen 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.<br />
<br />
<br><br><br />
<br />
==Tipps für den OpenGL Anfänger==<br />
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...<br />
<br />
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". <br />
<br />
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)<br />
<br />
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:<br />
<br />
<br />
<u>1. Je älter der Code ist desto besser wird er.</u><br />
<br />
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.<br />
<br />
''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!)''<br />
<br />
<br />
<u>2. Große Projekte = Großer Ruhm</u><br />
<br />
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 ... .<br />
<br />
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.<br />
<br />
'''Und woher soll ich bitte Erfahrung nehmen?'''<br><br />
Darin liegt der Trick: <br><br />
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. <br />
<br />
<br />
Ihr denkt jetzt bestimmt "Was kann man denn schon in so nem Clon unterbringen?" '''VERDAMMT VIEL!''' Hier mal ein kleiner Auszug:<br />
<br />
*3D-Spieldarstellung<br />
*2D Menüführung ([[glOrtho]])<br />
*[[Blending]] <br />
*[[Textur]]en <br />
*Licht und Materialien (Ihr werdet Augen machen!)<br />
*[[Selektion]] (Hin und wieder zum Haare raufen.)<br />
*Kamerasteuerung /Bewegung durch die Szene (Da gibt es ganze Tutorials dazu)<br />
<br />
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.<br><br />
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 ;) )<br />
<br />
'''Als kurze Zusammenfassung solltet Ihr Euch merken:'''<br />
<br />
*Ein fertiges Projekt bringt Euch Ruhm (Seelenbalsam).<br />
*Ein hübsches, fertiges Projekt bringt Euch mehr Ruhm.<br />
*Ein großes, fertiges, hübsches Projekt bringt Euch noch mehr Ruhm.<br><br />
<br />
*Ein abgebrochenes oder eingefrorenes Projekt bringt Euch Frust.<br />
*Ein großes, abgebrochenes Projekt bringt Euch noch mehr Frust, denn der verschwendeten Zeit werdet Ihr nachtrauern.<br />
<br />
<br />
<u>3.Das es lang dauert ist egal. Ich interessiere mich halt dafür.</u><br />
<br />
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.<br />
<br />
==Nachwort==<br />
<br />
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...<br />
<br />
<br />
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.<br />
<br />
<br />
Bis bald im [http://delphiGL.com/forum Forum]<br><br />
'''Euer''' <br><br />
'''Flash (Kevin Fleischer)'''<br />
<br />
<br />
PS: Feedback wird nicht nur gewünscht, sondern ausdrücklich gefordert. Deshalb: Ab mit deinen Meinungen und Ratschläge ins [http://www.delphigl.com/forum/viewforum.php?f=8 Feedback-Forum]!<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[Tutorial_lektion1]]}}<br />
<br />
[[Kategorie:Tutorial|Quickstart]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Quickstart&diff=14533Tutorial Quickstart2005-11-25T14:14:03Z<p>Akira: /* Der OpenGL-Header */</p>
<hr />
<div>=Quickstart: Delphi & OpenGL=<br />
<br />
==Einleitung==<br />
<br />
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.<br />
<br />
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.<br />
<br />
==Wie fange ich an?==<br />
Genau zwei Dinge braucht der OpenGL-Programmierer um effektiv arbeiten zu können:<br />
#Einen OpenGL Header<br />
#Eine Codebasis von der aus man neue Projekte starten kann (ein sog. Template)<br />
<br />
<br />
===Der OpenGL-Header===<br />
Das is ja easy! Denn Delphi bringt ja schon einen OpenGL-Header mit... <br><br />
'''STOP!''' <br><br />
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. <br><br />
Was nun? Ganz einfach: Bei [http://www.DelphiGL.com DelphiGL.com] (kurz DGL) gibt es '''DEN''' OpenGL-Header für alle Pascalsprachen: <br />
'''Die [[DGLOpenGL.pas]].'''<br />
<br />
Diesen solltet Ihr Euch jetzt besorgen, wenn Ihr ihn nicht schon habt. Der Header wird bei neuen OpenGL-Versionen vom DGL-Team aktualisiert.<br />
<br />
===Codebasis/Templates===<br />
So... das war schon alles was Ihr aus dem Netz benötigt. Den Rest machen wir jetzt per Hand.<br />
<br />
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.)<br />
<br />
<br />
<br />
==Das Template Oder Delphi fit für OpenGL machen==<br />
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.)<br />
<br />
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.<br><br />
''(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. )''<br />
<br />
===Initialisieren von OpenGL===<br />
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. <br />
<br />
Zuerst einmal solltet Ihr die DGLOpenGL.pas in die '''uses'''-Klausel des '''interface'''-Teils der Unit1 schreiben.<br />
<br />
Die eigentliche Initialisierung soll direkt beim Erstellen des Formulars gemacht werden. Deshalb kommt der folgende Quelltext ins OnCreate-Ereignis des Formulars.<br />
<pascal>procedure TForm1.FormCreate(Sender: TObject);<br />
begin<br />
DC:= GetDC(Handle);<br />
if not InitOpenGL then Application.Terminate;<br />
RC:= CreateRenderingContext( DC,<br />
[opDoubleBuffered],<br />
32,<br />
24,<br />
0,0,0,<br />
0);<br />
ActivateRenderingContext(DC, RC);<br />
end;</pascal><br />
'''Zeile 3:''' Hier wird der Gerätekontext (Device Context) von Formular Form1 abgefragt.<br />
<br />
'''Zeile 4:''' Mit InitOpenGL wird OpenGL initialisiert. Wenn das nicht funktioniert wird die gesamte Anwendung sofort beendet. <br />
<br />
'''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]].<br />
<br />
'''Zeile 11:''' Abschließend wird der Renderkontext aktiviert. OpenGL ist jetzt prinzipiell startbereit.<br />
<br />
{{Hinweis|DC und RC sind Eigenschaften des Formulars. Siehe [[Tutorial_quickstart#Das_fertige_Templateformular|Definition des Templateformulars]].}}<br />
<br />
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<br />
<br />
<pascal>procedure TForm1.SetupGL;<br />
begin<br />
glClearColor(0.3, 0.4, 0.7, 0.0); //Hintergrundfarbe<br />
glEnable(GL_DEPTH_TEST); //Tiefentest aktivieren<br />
glEnable(GL_CULL_FACE); //Backface Culling aktivieren<br />
end;</pascal><br />
<br />
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.)<br />
<br />
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.<br />
Beide Unterprogramme (SetupGL und Init) sollten am Ende von '''FormCreate''' gerufen werden:<br />
<br />
<pascal> [...]<br />
ActivateRenderingContext(DC, RC);<br />
SetupGL;<br />
Init;<br />
end;</pascal><br />
<br />
===Die Ereignisbehandlung===<br />
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 '''OnCreate-''' und das '''OnDestroy'''-Ereignis.<br />
<br />
Zuerst '''FormResize''':<br />
<pascal>procedure TForm1.FormResize(Sender: TObject);<br />
var tmpBool : Boolean;<br />
begin<br />
glViewport(0, 0, ClientWidth, ClientHeight);<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping); <br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
IdleHandler(Sender, tmpBool);<br />
end;</pascal><br />
<br />
Außerdem müssen im '''const''' Teil die beiden Konstanten Near- bzw. FarClipping definiert werden. Diese geben die Entfernung für die [[Clipping Plane|Clippingebenen]] (Szenenbegrenzung) an.<br />
<br />
<pascal> NearClipping = 1;<br />
FarClipping = 1000;</pascal><br />
<br />
'''Zu FormResize''':<br><br />
'''Zeile 2:''' Diese Boolean-Variable wird in Zeile 11 verwendet und ist nur ein Dummy.<br />
<br />
'''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.<br />
<br />
'''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.<br />
<br />
'''Zeile 6:''' [[glLoadIdentity]] füllt die aktuelle Matrix mit der Identitätsmatrix. <br />
<br />
'''Zeile 7:''' Hier wird eingestellt wie der Betrachter die Welt sehen soll. <br />
<br />
'''Zeile 11:''' Was der IdleHandler macht kommt später im Abschnitt 1.3.3 (Zeichenroutine).<br />
<br />
<br />
Nun noch schnell das '''FormDestroy''':<br />
<br />
<pascal>procedure TForm1.FormDestroy(Sender: TObject);<br />
begin<br />
DeactivateRenderingContext;<br />
DestroyRenderingContext(RC);<br />
ReleaseDC(Handle, DC);<br />
end;</pascal><br />
<br />
Was es macht? Steht doch da: Den RenderingContext deaktivieren und freigeben.<br />
<br />
<br />
===Die Zeichenroutine===<br />
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'''<br />
<br />
<pascal>procedure TForm1.Render;<br />
begin<br />
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);<br />
<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping);<br />
<br />
glTranslatef(0, 0, -5);<br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
<br />
glBegin(GL_QUADS);<br />
glColor3f(1, 0, 0); glVertex3f(0, 0, 0);<br />
glColor3f(0, 1, 0); glVertex3f(1, 0, 0);<br />
glColor3f(0, 0, 1); glVertex3f(1, 1, 0);<br />
glColor3f(1, 1, 0); glVertex3f(0, 1, 0);<br />
glEnd;<br />
<br />
SwapBuffers(DC);<br />
end;</pascal><br />
<br />
'''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.<br />
<br />
'''Zeile 7:''' Hier wird wieder die Perspektive gesetzt. Dieser Aufruf und der bei '''FormResize''' <br />
müssen von den Parametern identisch sein. Sonst sieht die Ausgabe nach einem Resize kurz anders aus. <br />
Wenn sich die Perspektive zwischen den Renderdurchgängen nicht ändert kann das auch weg gelassen werden.<br />
<br />
'''Zeile 9:''' Dieser Aufruf verschiebt die "Kamera" (so etwas gibt es eigentlich nicht, aber da wir <br />
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.<br />
<br />
[[Bild:glShadeModel_SMOOTH.jpg|right]]<br />
<br />
'''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.<br />
<br />
'''Zeile 21:''' [[SwapBuffers]] sorgt Ihr dafür, dass der Inhalt des [[Framebuffer]]s auf dem <br />
Bildschirm erscheint. Ohne diesen Befehl seht Ihr gar nichts von OpenGL. (Interessanter Artikel dazu: [[Doppelpufferung]])<br />
<br />
<br />
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.<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Argument<br />
!Timer<br />
!OnIdle<br />
|-<br />
|Maximale Framezahl erreichbar <br>"Benchmark"<br />
|nein<br />
|ja<br />
|-<br />
|Framezahl steuerbar<br />
|ja<br />
|bedingt (/umständlich)<br />
|-<br />
|Für flüssige Animationen nutzbar<br>(Egoshooter)<br />
|bedingt/schlecht<br />
|ja<br />
|-<br />
|Für Menüs nutzbar<br />
|ja<br />
|ja<br />
|-<br />
|Für einfache Animationen nutzbar<br>(Strategiespiele)<br />
|ja<br />
|ja<br />
|-<br />
|Laptopfreundlich (Anti-Akku-Killer)<br />
|ja<br />
|NEIN!<br />
|}<br />
</div><br />
<br />
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.<br />
<br />
<br />
====Methode 1: Timer====<br />
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.<br />
<br />
Der Inhalt von OnTimer könnte dieser sein:<br />
<br />
<pascal>procedure TForm1.Timer1Timer(Sender: TObject);<br />
begin<br />
inc(FrameCount);<br />
Render;<br />
If FrameCount = 20 then<br />
begin<br />
ErrorHandler;<br />
FrameCount := 0;<br />
end;<br />
end;</pascal><br />
<br />
Tiefgreifende Erklärungen sind hier nicht notwendig. Was der ErrorHandler ist wird nach der Methode 2 erklärt.<br />
<br />
<br />
====Methode 2 : OnIdle====<br />
<br />
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.<br><br />
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.<br />
<br />
<pascal>procedure TForm1.IdleHandler(Sender: TObject; var Done: Boolean);<br />
begin<br />
StartTime:= GetTickCount;<br />
Render;<br />
DrawTime:= GetTickCount - StartTime;<br />
Inc(TimeCount, DrawTime);<br />
Inc(FrameCount);<br />
<br />
if TimeCount >= 1000 then begin<br />
Frames:= FrameCount;<br />
TimeCount:= TimeCount - 1000;<br />
FrameCount:= 0;<br />
Caption:= InttoStr(Frames) + 'FPS';<br />
ErrorHandler;<br />
end;<br />
<br />
Done:= false;<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Mittels GetTickCount wird die Systemzeit gemessen. Dies ist nicht nötig um <br />
erfolgreich zu zeichnen, sondern dient ausschließlich der Berechnung der Framerate. Diese wiederum ist ein guter Performancemesser.<br />
<br />
'''Zeile 4:''' Hier erfolgt der Aufruf unserer Zeichenroutine.<br />
<br />
'''Zeile 9:''' Der hier angeordnete Block wird nur pro Sekunde einmal ausgeführt und sorgt <br />
dafür, dass die Framerate angezeigt wird. Außerdem wird der Errorhandler aufgerufen.<br />
<br />
'''Zeile 14:''' Der Errorhandler wird im Anschluß beschrieben.<br />
<br />
'''Zeile 17:''' Wenn ''Done'' nach der Ausführung ''false'' ist und das Programm wieder nichts zu tun <br />
hat, wird OnIdle erneut ausgeführt. Wenn ''Done = true'' ist wird OnIdle nur einmal ausgeführt.<br />
<br />
<br />
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.<br />
<br />
<pascal>Application.OnIdle := IdleHandler;</pascal><br />
<br />
Wie ihr bald selbst feststellen werdet, sorgt diese Methode für eine hundertprozentige Prozessorauslastung. Umgehen könnt ihr dies mit einem kleinen Trick:<br> <br />
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.<br><br />
<br />
===Der ErrorHandler Fehler erkannt, Fehler gebannt===<br />
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:<br />
<br />
<pascal>procedure TForm1.ErrorHandler;<br />
begin<br />
Form1.Caption := gluErrorString(glGetError);<br />
end;</pascal><br />
<br />
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'''.<br />
<br />
===Das fertige Templateformular===<br />
Eure Klasse TForm1 sollte also jetzt so, oder so ähnlich aussehen:<br />
<pascal>TForm1 = class(TForm)<br />
procedure FormCreate(Sender: TObject);<br />
procedure IdleHandler(Sender: TObject; var Done: Boolean);<br />
procedure FormResize(Sender: TObject);<br />
procedure FormDestroy(Sender: TObject);<br />
private { Private-Deklarationen }<br />
StartTime, TimeCount, FrameCount : Cardinal; //FrameCounter<br />
Frames, DrawTime : Cardinal; //& Timebased Movement<br />
procedure SetupGL;<br />
procedure Init;<br />
procedure Render;<br />
procedure ErrorHandler;<br />
public { Public-Deklarationen }<br />
DC : HDC; //Handle auf Zeichenfläche<br />
RC : HGLRC;//Rendering Context<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Wie man sieht benutzt mein Template die zweite Methode (OnIdle).<br />
<br />
'''Zeile 8:''' Was [[Timebased Movement]] ist könnt ihr ja mal nachlesen.<br />
<br />
'''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.)<br />
<br />
<br />
So....fertig. Eigentlich seid Ihr jetzt soweit von den Tutorialschreibern so richtig mit OpenGLWissen 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.<br />
<br />
<br><br><br />
<br />
==Tipps für den OpenGL Anfänger==<br />
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...<br />
<br />
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". <br />
<br />
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)<br />
<br />
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:<br />
<br />
<br />
<u>1. Je älter der Code ist desto besser wird er.</u><br />
<br />
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.<br />
<br />
''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!)''<br />
<br />
<br />
<u>2. Große Projekte = Großer Ruhm</u><br />
<br />
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 ... .<br />
<br />
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.<br />
<br />
'''Und woher soll ich bitte Erfahrung nehmen?'''<br><br />
Darin liegt der Trick: <br><br />
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. <br />
<br />
<br />
Ihr denkt jetzt bestimmt "Was kann man denn schon in so nem Clon unterbringen?" '''VERDAMMT VIEL!''' Hier mal ein kleiner Auszug:<br />
<br />
*3D-Spieldarstellung<br />
*2D Menüführung ([[glOrtho]])<br />
*[[Blending]] <br />
*[[Textur]]en <br />
*Licht und Materialien (Ihr werdet Augen machen!)<br />
*[[Selektion]] (Hin und wieder zum Haare raufen.)<br />
*Kamerasteuerung /Bewegung durch die Szene (Da gibt es ganze Tutorials dazu)<br />
<br />
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.<br><br />
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 ;) )<br />
<br />
'''Als kurze Zusammenfassung solltet Ihr Euch merken:'''<br />
<br />
*Ein fertiges Projekt bringt Euch Ruhm (Seelenbalsam).<br />
*Ein hübsches, fertiges Projekt bringt Euch mehr Ruhm.<br />
*Ein großes, fertiges, hübsches Projekt bringt Euch noch mehr Ruhm.<br><br />
<br />
*Ein abgebrochenes oder eingefrorenes Projekt bringt Euch Frust.<br />
*Ein großes, abgebrochenes Projekt bringt Euch noch mehr Frust, denn der verschwendeten Zeit werdet Ihr nachtrauern.<br />
<br />
<br />
<u>3.Das es lang dauert ist egal. Ich interessiere mich halt dafür.</u><br />
<br />
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.<br />
<br />
==Nachwort==<br />
<br />
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...<br />
<br />
<br />
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.<br />
<br />
<br />
Bis bald im [http://delphiGL.com/forum Forum]<br><br />
'''Euer''' <br><br />
'''Flash (Kevin Fleischer)'''<br />
<br />
<br />
PS: Feedback wird nicht nur gewünscht, sondern ausdrücklich gefordert. Deshalb: Ab mit deinen Meinungen und Ratschläge ins [http://www.delphigl.com/forum/viewforum.php?f=8 Feedback-Forum]!<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[Tutorial_lektion1]]}}<br />
<br />
[[Kategorie:Tutorial|Quickstart]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_Quickstart&diff=14532Tutorial Quickstart2005-11-25T14:13:11Z<p>Akira: /* Einleitung */</p>
<hr />
<div>=Quickstart: Delphi & OpenGL=<br />
<br />
==Einleitung==<br />
<br />
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.<br />
<br />
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.<br />
<br />
==Wie fange ich an?==<br />
Genau zwei Dinge braucht der OpenGL-Programmierer um effektiv arbeiten zu können:<br />
#Einen OpenGL Header<br />
#Eine Codebasis von der aus man neue Projekte starten kann (ein sog. Template)<br />
<br />
<br />
===Der OpenGL-Header===<br />
Das is ja easy! Denn Delphi bringt ja schon einen OpenGL-Header mit... <br><br />
'''STOP!''' <br><br />
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. <br><br />
Was nun? Ganz einfach: Bei [http://www.DelphiGL.com DelphiGL.com] (kurz DGL) gibt es '''DEN''' OpenGL-Header für alle Pascalsprachen: <br />
'''Die [[DGLOpenGL.pas]].'''<br />
<br />
Diesen solltet Ihr Euch jetzt besorgen, wenn Ihr ihn nicht schon habt. Der Header wird bei neuen OpenGL-Versionen vom DGL-Team aktualisiert. <br />
<br />
===Codebasis/Templates===<br />
So... das war schon alles was Ihr aus dem Netz benötigt. Den Rest machen wir jetzt per Hand.<br />
<br />
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.)<br />
<br />
<br />
<br />
==Das Template Oder Delphi fit für OpenGL machen==<br />
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.)<br />
<br />
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.<br><br />
''(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. )''<br />
<br />
===Initialisieren von OpenGL===<br />
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. <br />
<br />
Zuerst einmal solltet Ihr die DGLOpenGL.pas in die '''uses'''-Klausel des '''interface'''-Teils der Unit1 schreiben.<br />
<br />
Die eigentliche Initialisierung soll direkt beim Erstellen des Formulars gemacht werden. Deshalb kommt der folgende Quelltext ins OnCreate-Ereignis des Formulars.<br />
<pascal>procedure TForm1.FormCreate(Sender: TObject);<br />
begin<br />
DC:= GetDC(Handle);<br />
if not InitOpenGL then Application.Terminate;<br />
RC:= CreateRenderingContext( DC,<br />
[opDoubleBuffered],<br />
32,<br />
24,<br />
0,0,0,<br />
0);<br />
ActivateRenderingContext(DC, RC);<br />
end;</pascal><br />
'''Zeile 3:''' Hier wird der Gerätekontext (Device Context) von Formular Form1 abgefragt.<br />
<br />
'''Zeile 4:''' Mit InitOpenGL wird OpenGL initialisiert. Wenn das nicht funktioniert wird die gesamte Anwendung sofort beendet. <br />
<br />
'''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]].<br />
<br />
'''Zeile 11:''' Abschließend wird der Renderkontext aktiviert. OpenGL ist jetzt prinzipiell startbereit.<br />
<br />
{{Hinweis|DC und RC sind Eigenschaften des Formulars. Siehe [[Tutorial_quickstart#Das_fertige_Templateformular|Definition des Templateformulars]].}}<br />
<br />
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<br />
<br />
<pascal>procedure TForm1.SetupGL;<br />
begin<br />
glClearColor(0.3, 0.4, 0.7, 0.0); //Hintergrundfarbe<br />
glEnable(GL_DEPTH_TEST); //Tiefentest aktivieren<br />
glEnable(GL_CULL_FACE); //Backface Culling aktivieren<br />
end;</pascal><br />
<br />
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.)<br />
<br />
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.<br />
Beide Unterprogramme (SetupGL und Init) sollten am Ende von '''FormCreate''' gerufen werden:<br />
<br />
<pascal> [...]<br />
ActivateRenderingContext(DC, RC);<br />
SetupGL;<br />
Init;<br />
end;</pascal><br />
<br />
===Die Ereignisbehandlung===<br />
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 '''OnCreate-''' und das '''OnDestroy'''-Ereignis.<br />
<br />
Zuerst '''FormResize''':<br />
<pascal>procedure TForm1.FormResize(Sender: TObject);<br />
var tmpBool : Boolean;<br />
begin<br />
glViewport(0, 0, ClientWidth, ClientHeight);<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping); <br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
IdleHandler(Sender, tmpBool);<br />
end;</pascal><br />
<br />
Außerdem müssen im '''const''' Teil die beiden Konstanten Near- bzw. FarClipping definiert werden. Diese geben die Entfernung für die [[Clipping Plane|Clippingebenen]] (Szenenbegrenzung) an.<br />
<br />
<pascal> NearClipping = 1;<br />
FarClipping = 1000;</pascal><br />
<br />
'''Zu FormResize''':<br><br />
'''Zeile 2:''' Diese Boolean-Variable wird in Zeile 11 verwendet und ist nur ein Dummy.<br />
<br />
'''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.<br />
<br />
'''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.<br />
<br />
'''Zeile 6:''' [[glLoadIdentity]] füllt die aktuelle Matrix mit der Identitätsmatrix. <br />
<br />
'''Zeile 7:''' Hier wird eingestellt wie der Betrachter die Welt sehen soll. <br />
<br />
'''Zeile 11:''' Was der IdleHandler macht kommt später im Abschnitt 1.3.3 (Zeichenroutine).<br />
<br />
<br />
Nun noch schnell das '''FormDestroy''':<br />
<br />
<pascal>procedure TForm1.FormDestroy(Sender: TObject);<br />
begin<br />
DeactivateRenderingContext;<br />
DestroyRenderingContext(RC);<br />
ReleaseDC(Handle, DC);<br />
end;</pascal><br />
<br />
Was es macht? Steht doch da: Den RenderingContext deaktivieren und freigeben.<br />
<br />
<br />
===Die Zeichenroutine===<br />
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'''<br />
<br />
<pascal>procedure TForm1.Render;<br />
begin<br />
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);<br />
<br />
glMatrixMode(GL_PROJECTION);<br />
glLoadIdentity;<br />
gluPerspective(45.0, ClientWidth/ClientHeight, NearClipping, FarClipping);<br />
<br />
glTranslatef(0, 0, -5);<br />
<br />
glMatrixMode(GL_MODELVIEW);<br />
glLoadIdentity;<br />
<br />
glBegin(GL_QUADS);<br />
glColor3f(1, 0, 0); glVertex3f(0, 0, 0);<br />
glColor3f(0, 1, 0); glVertex3f(1, 0, 0);<br />
glColor3f(0, 0, 1); glVertex3f(1, 1, 0);<br />
glColor3f(1, 1, 0); glVertex3f(0, 1, 0);<br />
glEnd;<br />
<br />
SwapBuffers(DC);<br />
end;</pascal><br />
<br />
'''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.<br />
<br />
'''Zeile 7:''' Hier wird wieder die Perspektive gesetzt. Dieser Aufruf und der bei '''FormResize''' <br />
müssen von den Parametern identisch sein. Sonst sieht die Ausgabe nach einem Resize kurz anders aus. <br />
Wenn sich die Perspektive zwischen den Renderdurchgängen nicht ändert kann das auch weg gelassen werden.<br />
<br />
'''Zeile 9:''' Dieser Aufruf verschiebt die "Kamera" (so etwas gibt es eigentlich nicht, aber da wir <br />
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.<br />
<br />
[[Bild:glShadeModel_SMOOTH.jpg|right]]<br />
<br />
'''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.<br />
<br />
'''Zeile 21:''' [[SwapBuffers]] sorgt Ihr dafür, dass der Inhalt des [[Framebuffer]]s auf dem <br />
Bildschirm erscheint. Ohne diesen Befehl seht Ihr gar nichts von OpenGL. (Interessanter Artikel dazu: [[Doppelpufferung]])<br />
<br />
<br />
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.<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Argument<br />
!Timer<br />
!OnIdle<br />
|-<br />
|Maximale Framezahl erreichbar <br>"Benchmark"<br />
|nein<br />
|ja<br />
|-<br />
|Framezahl steuerbar<br />
|ja<br />
|bedingt (/umständlich)<br />
|-<br />
|Für flüssige Animationen nutzbar<br>(Egoshooter)<br />
|bedingt/schlecht<br />
|ja<br />
|-<br />
|Für Menüs nutzbar<br />
|ja<br />
|ja<br />
|-<br />
|Für einfache Animationen nutzbar<br>(Strategiespiele)<br />
|ja<br />
|ja<br />
|-<br />
|Laptopfreundlich (Anti-Akku-Killer)<br />
|ja<br />
|NEIN!<br />
|}<br />
</div><br />
<br />
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.<br />
<br />
<br />
====Methode 1: Timer====<br />
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.<br />
<br />
Der Inhalt von OnTimer könnte dieser sein:<br />
<br />
<pascal>procedure TForm1.Timer1Timer(Sender: TObject);<br />
begin<br />
inc(FrameCount);<br />
Render;<br />
If FrameCount = 20 then<br />
begin<br />
ErrorHandler;<br />
FrameCount := 0;<br />
end;<br />
end;</pascal><br />
<br />
Tiefgreifende Erklärungen sind hier nicht notwendig. Was der ErrorHandler ist wird nach der Methode 2 erklärt.<br />
<br />
<br />
====Methode 2 : OnIdle====<br />
<br />
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.<br><br />
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.<br />
<br />
<pascal>procedure TForm1.IdleHandler(Sender: TObject; var Done: Boolean);<br />
begin<br />
StartTime:= GetTickCount;<br />
Render;<br />
DrawTime:= GetTickCount - StartTime;<br />
Inc(TimeCount, DrawTime);<br />
Inc(FrameCount);<br />
<br />
if TimeCount >= 1000 then begin<br />
Frames:= FrameCount;<br />
TimeCount:= TimeCount - 1000;<br />
FrameCount:= 0;<br />
Caption:= InttoStr(Frames) + 'FPS';<br />
ErrorHandler;<br />
end;<br />
<br />
Done:= false;<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Mittels GetTickCount wird die Systemzeit gemessen. Dies ist nicht nötig um <br />
erfolgreich zu zeichnen, sondern dient ausschließlich der Berechnung der Framerate. Diese wiederum ist ein guter Performancemesser.<br />
<br />
'''Zeile 4:''' Hier erfolgt der Aufruf unserer Zeichenroutine.<br />
<br />
'''Zeile 9:''' Der hier angeordnete Block wird nur pro Sekunde einmal ausgeführt und sorgt <br />
dafür, dass die Framerate angezeigt wird. Außerdem wird der Errorhandler aufgerufen.<br />
<br />
'''Zeile 14:''' Der Errorhandler wird im Anschluß beschrieben.<br />
<br />
'''Zeile 17:''' Wenn ''Done'' nach der Ausführung ''false'' ist und das Programm wieder nichts zu tun <br />
hat, wird OnIdle erneut ausgeführt. Wenn ''Done = true'' ist wird OnIdle nur einmal ausgeführt.<br />
<br />
<br />
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.<br />
<br />
<pascal>Application.OnIdle := IdleHandler;</pascal><br />
<br />
Wie ihr bald selbst feststellen werdet, sorgt diese Methode für eine hundertprozentige Prozessorauslastung. Umgehen könnt ihr dies mit einem kleinen Trick:<br> <br />
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.<br><br />
<br />
===Der ErrorHandler Fehler erkannt, Fehler gebannt===<br />
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:<br />
<br />
<pascal>procedure TForm1.ErrorHandler;<br />
begin<br />
Form1.Caption := gluErrorString(glGetError);<br />
end;</pascal><br />
<br />
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'''.<br />
<br />
===Das fertige Templateformular===<br />
Eure Klasse TForm1 sollte also jetzt so, oder so ähnlich aussehen:<br />
<pascal>TForm1 = class(TForm)<br />
procedure FormCreate(Sender: TObject);<br />
procedure IdleHandler(Sender: TObject; var Done: Boolean);<br />
procedure FormResize(Sender: TObject);<br />
procedure FormDestroy(Sender: TObject);<br />
private { Private-Deklarationen }<br />
StartTime, TimeCount, FrameCount : Cardinal; //FrameCounter<br />
Frames, DrawTime : Cardinal; //& Timebased Movement<br />
procedure SetupGL;<br />
procedure Init;<br />
procedure Render;<br />
procedure ErrorHandler;<br />
public { Public-Deklarationen }<br />
DC : HDC; //Handle auf Zeichenfläche<br />
RC : HGLRC;//Rendering Context<br />
end;</pascal><br />
<br />
'''Zeile 3:''' Wie man sieht benutzt mein Template die zweite Methode (OnIdle).<br />
<br />
'''Zeile 8:''' Was [[Timebased Movement]] ist könnt ihr ja mal nachlesen.<br />
<br />
'''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.)<br />
<br />
<br />
So....fertig. Eigentlich seid Ihr jetzt soweit von den Tutorialschreibern so richtig mit OpenGLWissen 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.<br />
<br />
<br><br><br />
<br />
==Tipps für den OpenGL Anfänger==<br />
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...<br />
<br />
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". <br />
<br />
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)<br />
<br />
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:<br />
<br />
<br />
<u>1. Je älter der Code ist desto besser wird er.</u><br />
<br />
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.<br />
<br />
''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!)''<br />
<br />
<br />
<u>2. Große Projekte = Großer Ruhm</u><br />
<br />
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 ... .<br />
<br />
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.<br />
<br />
'''Und woher soll ich bitte Erfahrung nehmen?'''<br><br />
Darin liegt der Trick: <br><br />
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. <br />
<br />
<br />
Ihr denkt jetzt bestimmt "Was kann man denn schon in so nem Clon unterbringen?" '''VERDAMMT VIEL!''' Hier mal ein kleiner Auszug:<br />
<br />
*3D-Spieldarstellung<br />
*2D Menüführung ([[glOrtho]])<br />
*[[Blending]] <br />
*[[Textur]]en <br />
*Licht und Materialien (Ihr werdet Augen machen!)<br />
*[[Selektion]] (Hin und wieder zum Haare raufen.)<br />
*Kamerasteuerung /Bewegung durch die Szene (Da gibt es ganze Tutorials dazu)<br />
<br />
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.<br><br />
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 ;) )<br />
<br />
'''Als kurze Zusammenfassung solltet Ihr Euch merken:'''<br />
<br />
*Ein fertiges Projekt bringt Euch Ruhm (Seelenbalsam).<br />
*Ein hübsches, fertiges Projekt bringt Euch mehr Ruhm.<br />
*Ein großes, fertiges, hübsches Projekt bringt Euch noch mehr Ruhm.<br><br />
<br />
*Ein abgebrochenes oder eingefrorenes Projekt bringt Euch Frust.<br />
*Ein großes, abgebrochenes Projekt bringt Euch noch mehr Frust, denn der verschwendeten Zeit werdet Ihr nachtrauern.<br />
<br />
<br />
<u>3.Das es lang dauert ist egal. Ich interessiere mich halt dafür.</u><br />
<br />
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.<br />
<br />
==Nachwort==<br />
<br />
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...<br />
<br />
<br />
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.<br />
<br />
<br />
Bis bald im [http://delphiGL.com/forum Forum]<br><br />
'''Euer''' <br><br />
'''Flash (Kevin Fleischer)'''<br />
<br />
<br />
PS: Feedback wird nicht nur gewünscht, sondern ausdrücklich gefordert. Deshalb: Ab mit deinen Meinungen und Ratschläge ins [http://www.delphigl.com/forum/viewforum.php?f=8 Feedback-Forum]!<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[Tutorial_lektion1]]}}<br />
<br />
[[Kategorie:Tutorial|Quickstart]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14504Tutorial glsl2005-11-24T16:02:41Z<p>Akira: /* Der Fragment Shader */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Terminologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
[http://www.ati.com ATI] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
[http://www.nvidia.com NVidia] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
[http://www.3dlabs.com 3DLabs], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparameter (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden muß.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt der Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit, Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen von diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört.<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3 und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüber hinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfach) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt, sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist.<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projizierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was in einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kapiteln zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Farbtextur zu texturieren. Für alle, die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14503Tutorial glsl2005-11-24T16:00:36Z<p>Akira: /* Texturenzugriffe */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Terminologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
[http://www.ati.com ATI] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
[http://www.nvidia.com NVidia] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
[http://www.3dlabs.com 3DLabs], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparameter (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden muß.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt der Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit, Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen von diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört.<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3 und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüber hinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfach) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt, sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist.<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projizierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was in einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kaptilen zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Fargtextur zu Texturieren. Für alle die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14502Tutorial glsl2005-11-24T15:58:17Z<p>Akira: /* Variablen im Vertex Shader */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Terminologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
[http://www.ati.com ATI] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
[http://www.nvidia.com NVidia] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
[http://www.3dlabs.com 3DLabs], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparameter (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden muß.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt der Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit, Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen von diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört.<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3 und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüber hinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfach) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt, sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist.<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projezierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was ihn einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kaptilen zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Fargtextur zu Texturieren. Für alle die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14501Tutorial glsl2005-11-24T15:57:20Z<p>Akira: /* Die Shadersprache */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Terminologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
[http://www.ati.com ATI] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
[http://www.nvidia.com NVidia] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
[http://www.3dlabs.com 3DLabs], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparameter (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden muß.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt der Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit, Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen von diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört.<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3 und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüber hinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfaceh) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist. <br />
<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projezierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was ihn einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kaptilen zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Fargtextur zu Texturieren. Für alle die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14499Tutorial glsl2005-11-24T15:50:33Z<p>Akira: /* Parameterübergabe */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Terminologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
[http://www.ati.com ATI] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
[http://www.nvidia.com NVidia] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
[http://www.3dlabs.com 3DLabs], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparameter (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden muß.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen vom diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört. Di<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3, und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüberhinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfaceh) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist. <br />
<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projezierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was ihn einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kaptilen zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Fargtextur zu Texturieren. Für alle die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14498Tutorial glsl2005-11-24T15:46:50Z<p>Akira: /* Was ist glSlang? */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Terminologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
[http://www.ati.com ATI] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
[http://www.nvidia.com NVidia] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
[http://www.3dlabs.com 3DLabs], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparmater (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden müssen.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen vom diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört. Di<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3, und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüberhinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfaceh) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist. <br />
<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projezierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was ihn einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kaptilen zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Fargtextur zu Texturieren. Für alle die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14497Tutorial glsl2005-11-24T15:45:30Z<p>Akira: /* Voraussetzungen */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Termonologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
[http://www.ati.com ATI] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
[http://www.nvidia.com NVidia] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
[http://www.3dlabs.com 3DLabs], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparmater (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden müssen.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen vom diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört. Di<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3, und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüberhinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfaceh) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist. <br />
<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projezierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was ihn einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kaptilen zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Fargtextur zu Texturieren. Für alle die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akirahttps://wiki.delphigl.com/index.php?title=Tutorial_glsl&diff=14496Tutorial glsl2005-11-24T15:43:04Z<p>Akira: /* Voraussetzungen */</p>
<hr />
<div>=Präambel=<br />
Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen, sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren, in der Lage sein, zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk, dienen.<br />
<br />
<br />
==Vorkenntnisse==<br />
Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich, sondern auch manchmal recht schwer verdaulich.<br />
<br />
<br />
<br />
----<br />
<br />
<br />
<br />
=Was ist glSlang?=<br />
Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache, in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in Assembler geschriebenen Vertex- und Fragmentprogrammen ([[GL_ARB_Vertex_Program]]/[[GL_ARB_Fragment_Program]]) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.<br />
<br />
Die in glSlang geschriebenen Programme nennen sich, angepasst an die Termonologie von RenderMan und DirectX, [[Shader]] (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.<br />
<br />
<br />
==Voraussetzungen==<br />
<br />
glSlang ist ein recht neues Feature, dass mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. <br />
''Aktueller Stand (November 2005) ist wie folgt :''<br />
<br />
ATI[http://www.ati.com] haben bereits seit fast 2 Jahren (Catalyst 3.10) glSlang-fähige Treiber, allerdings kommt es besonders mit neueren Treibern hier und da immernoch zu Fehlern (oder es werden gar neue Fehler eingführt) und ATI zeigt momentan kein sehr starkes Interesse am fixen dieser Fehler.<br />
<br />
NVidia[http://www.nvidia.com] haben sich etwas mehr Zeit gelassen, allerdings ist deren glSlang-Implementation inzwischen recht ausgereift. Bugs gibts allerdings trotzdem hier und da, aber NVidias Entwicklersupport ist da recht offen für Fehlerberichte. Die aktuellen Treiber der 80er Reihe sind daher für glSlang-Nutzer bestens geeignet.<br />
<br />
3DLabs[http://www.3dlabs.com], die glSlang quasi erfunden haben, haben natürlich hervorragenden glSlang Support in ihren Treiber, allerdings sind deren Wildcat-Karten kaum verbreitet.<br />
<br />
Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header [[DGLOpenGL.pas]] der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.<br />
<br />
==Neue Extensions==<br />
Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht, wenn ihr euch zumindest die Einleitungen dazu durchlest :<br />
* [[GL_ARB_Shader_Objects]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shader_objects.txt Orginal Spezifikation])<br />
: Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind. <br />
* [[GL_ARB_Vertex_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/vertex_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu. <br />
* [[GL_ARB_Fragment_Shader]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt Orginal Spezifikation])<br />
: Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu. <br />
* [[GL_ARB_Shading_Language_100]] ([http://oss.sgi.com/projects/ogl-sample/registry/ARB/shading_language_100.txt Orginal Spezifikation])<br />
: Gibt die unterstützte Version von glSlang an, momentan 1.00.<br />
<br />
<br />
==Objekte==<br />
Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist, dabei die Programmierung der GL uniform zu machen, so dass z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte dazu). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :<br />
<br />
* '''Programmobjekt'''<br />
:Ein Objekt, an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.<br />
<br />
* '''Shaderobjekt'''<br />
:Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ '''GL_VERTEX_SHADER_ARB''' oder '''GL_FRAGMENT_SHADER_ARB'''.<br />
<br />
<br />
==Resourcen==<br />
Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten, dass auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache, dass sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die [http://www.3dlabs.com/support/developer/ogl2/index.htm GL2-Seite von 3D-Labs], wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.<br />
<br />
=glSlang im Programm=<br />
Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal, wie ihr Shader in euer Programm einbindet und nutzt. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das, was ihr im glSlang-Syntaxteil lernt, direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.<br />
<br />
Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein ''Programmobjekt'', an das unsere Shader gebunden werden, und zwei ''Shaderobjekte'', die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" {{INLINE_CODE|glHandleARB}} eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :<br />
<br />
ProgramObject : GLhandleARB;<br />
VertexShaderObject : GLhandleARB;<br />
FragmentShaderObject : GLhandleARB;<br />
<br />
<br />
Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :<br />
<br />
ProgramObject := glCreateProgramObjectARB;<br />
<br />
Die Funktion [[glCreateProgramObjectARB]] erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.<br />
<br />
Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :<br />
<br />
VertexShaderObject := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);<br />
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);<br />
<br />
[[glCreateShaderObjectARB]] dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.<br />
<br />
Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :<br />
<br />
glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);<br />
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);<br />
<br />
Via [[glShaderSourceARB]] setzen wir den Quellcode eines Shaderobjektes ''komplett'' neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList geradezu an. Es sollte beachtet werden, dass der Quellcode zu diesem Zeitpunkt ''nicht geparst'' wird, also keine Fehleruntersuchung stattfindet.<br />
<br />
Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
glCompileShaderARB(FragmentShaderObject);<br />
<br />
Der glSlang-Compiler des Treibers wird bei einem Aufruf von [[glCompileShaderARB]] versuchen, unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen, sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.<br />
<br />
Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit, diese an unser anfangs erstelltes Programmobjekt anzuhängen :<br />
<br />
glAttachObjectARB(ProgramObject, VertexShaderObject);<br />
glAttachObjectARB(ProgramObject, FragmentShaderObject);<br />
<br />
<br />
Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :<br />
<br />
glDeleteObjectARB(VertexShaderObject);<br />
glDeleteObjectARB(FragmentShaderObject);<br />
<br />
<br />
Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :<br />
<br />
glLinkProgramARB(ProgramObject);<br />
<br />
Während [[glCompileShaderARB]] unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch [[glLinkProgramARB]] die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem '''Linkerfehler''':<br />
<br />
* Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten<br />
* Der Speicherplatz für Uniformvariablen wurde überschritten<br />
* Die Zahl der von der Implementation angebotenen Sampler wurde überschritten<br />
* Die main-Funktion fehlt<br />
* Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein<br />
* Funktions- oder Variablenname nicht gefunden<br />
* Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden<br />
* Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit<br />
* Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert<br />
<br />
Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.<br />
<br />
<br />
==Fehlererkennung==<br />
Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex- oder Fragmentshader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich [[glGetInfoLogARB]] und [[glGetObjectParameterivARB]] mit dem Argument {{INLINE_CODE|GL_OBJECT_INFO_LOG_LENGTH_ARB}}. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.<br />
<br />
Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :<br />
<br />
<pascal>function glSlang_GetInfoLog(glObject : GLHandleARB) : String;<br />
var<br />
blen,slen : GLInt;<br />
InfoLog : PGLCharARB;<br />
begin<br />
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);<br />
if blen > 1 then<br />
begin<br />
GetMem(InfoLog, blen*SizeOf(GLCharARB));<br />
glGetInfoLogARB(glObject, blen, slen, InfoLog);<br />
Result := PChar(InfoLog);<br />
Dispose(InfoLog);<br />
end;<br />
end;</pascal><br />
<br />
<br />
Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über {{INLINE_CODE|glGetObjectParameterivARB}} mitteilen wie lang der aktuelle Infolog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via {{INLINE_CODE|glGetInfoLogARB}} in {{INLINE_CODE|InfoLog}} ausgeben und liefern diesen als Ergebnis zurück.<br />
<br />
Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direkt danach einen solchen Aufruf zu machen :<br />
<br />
glCompileShaderARB(VertexShaderObject);<br />
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));<br />
<br />
Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :<br />
<br />
[[Bild:GLSL_error_vshader.jpg|center]]<br />
<br />
Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :<br />
<br />
[[Bild:GLSL info programobject.jpg|center]]<br />
<br />
Wie zu sehen, wird uns nach dem erfolgreichen Linken auch gesagt, ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.<br />
<br />
<br />
==Parameterübergabe==<br />
Uniformparmater (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von [[glUniformARB]] sind. Während mit {{INLINE_CODE|glUniform4fARB}} z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels {{INLINE_CODE|glUniformMatrix4fvARB}} ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion [[glGetUniformLocationARB]] gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :<br />
<br />
glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);<br />
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);<br />
<br />
<br />
Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor {{INLINE_CODE|glUniform4fARB}} und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden müssen.<br />
<br />
=Die Shadersprache=<br />
<br />
Nachdem wir uns mit der Einbindung der glSlang-Shader in unser Programm beschäftigt haben, wollen wir uns in den folgenden Kapiteln um die Sprachelemente von glSlang kümmern. Wie schon gesagt basiert glSlang auf ANSI-C, wurde allerdings um speziell auf den Zielbereich angepasste Vektor- und Matrixtypen und einige C++-Features wie das freie deklarieren von Variablen an jeder Stelle und das Funktionsüberladen auf Basis des Argumenttyps erweitert. Wer sich ein wenig mit C/C++ auskennt sollte also in der nun folgenden Materie keine Probleme bekommen.<br />
<br />
'''Obligatorische Hinweise für verwöhnte Delphi-Nutzer : '''<br />
*Wie von C/C++ her gewohnt, spielt auch in glSlang die Groß- und Kleinschreibung eine wichtige Rolle, also bitte achtet darauf. gl_Position ist eine komplett andere Variable als z.B. gl_position.<br />
*Es findet keine automatische Typenkonvertierung statt. Das bedeutet also das float MyFloat = 1 ungültig ist und es in dem Falle float MyFloat = 1.0 heissen muss. Typecasts müssen also immer manuell stattfinden, z.B. MyFloat = float(MyInt).<br />
<br />
'''Kleine Programmstrukturkunde für C-Unkundige :'''<br><br />
Da sicherlich einige Delpher nie richtig was mit C gemacht haben, zeige ich mal anhand eines kleinen Beispieles (das auf keinen Fall nen brauchbaren Shader darstellt) den grundlegenden Aufbau eines glSlang-Shaders, der natürlich dem Aufbau eines C-Programmes stark ähnelt :<br />
<br />
uniform vec4 VariableA;<br />
float VariableB;<br />
vec3 VariableC;<br />
const float KonstanteA = 256.0;<br />
<br />
float MyFunction(vec4 ArgumentA)<br />
{<br />
float FunktionsVariableA = float(5.0);<br />
<br />
return float(ArgumentA * (FunktionsVariableA + KonstanteA));<br />
}<br />
<br />
// Ich bin ein Kommentar<br />
/* Und ich auch */<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Sieht doch recht bekannt aus, unser Programmaufbau. Delphi und C haben ja so einige Grundlagen gleich, darunter auch der ungefähre Programmaufbau. Ausserhalb jeglicher Funktionen legen wir am Programmanfang unsere Variablen, Konstanten und Attribute fest, die dann ''global'' nutzbar sind, also in jeder Funktion.<br />
<br />
Darunter deklarieren wir dann eine kleine Funktion. Wie auch bei den Variablendeklarationen wird hier der Rückgabetyp nicht wie bei Pascal nach dem Funktionsnamen untergebracht, sondern davor. Innerhalb der Funktion können dann wieder Variablen deklariert werden, die dann allerdings ''lokal'', also nur in dieser Funktion nutzbar sind. Vorteil dieser Deklaration ist die Tatsache, dass je nach Grafikkarte nur bestimmt viele globale Variablen deklariert werden können. Wenn möglich sollte man also mit lokalen Vorlieb nehmen. Unsere Funktion gibt dann natürlich noch via return einen Wert zurück, ''was gemacht werden muss'', sofern man diese nicht als void deklariert hat (entspräche dann einer Prozedur in Pascal). Wird dies nicht getan, so spuckt Compiler einen Fehler aus.<br />
<br />
Auch wichtig sind natürlich Kommentare. Erste Variante (Doppelslash) ist auch in der Pascalwelt verfügbar und kommentiert eine einzelne Zeile aus. Die Variante darunter kann man für Kommentarblöcke nutzen (/* .. */) und entspricht den Kommentaren in geschweiften Klammern in Delphi.<br />
<br />
Danach kommt dann die '''wichtigste Funktion''' des Shaders, nämlich '''main''', die in keinem Shader fehlen darf. Sie stellt quasi den Programmkörper dar und ist oft auch die einzige Funktion in einem Shader. Sie erhält weder ein Argument, noch gibt sie einen Wert zurück.<br />
<br />
Soviel also zum grundlegenden Aufbau eines Shader. Hoffe das jetzt alle die in C nicht so bewandert sind damit klar kommen, und dann bald ihre ersten glSlang-Shader schreiben können.<br />
<br />
<br />
==Datentypen==<br />
<br />
Obwohl einige Datentypen aus C übernommen wurden, sieht man der Typenliste an, das diese speziell auf den 3D-Bereich zugeschnitten wurde. Variablen müssen vor ihrer Nutzung eindeutig deklariert sein, Typecasting erfolgt über Konstruktoren (dazu später mehr). Folgende Datentypen stehen sowohl im Vertex- als auch Fragmentshader zur Verfügung :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Datentyp <br />
!Erklärung<br />
|-<br />
|void <br />
|Für Funktionen die keinen Wert zurückgeben<br />
|-<br />
|bool <br />
|Konditionaler Typ, entweder true (wahr) oder false (falsch)<br />
|-<br />
|int <br />
|Vorzeichenbehafteter Integerwert<br />
|-<br />
|float <br />
|Fließkommaskalar mit Singlegenauigkeit (32 Bit)<br />
|-<br />
|vec2 <br />
|2-Komponenten Fließkommavektor<br />
|-<br />
|vec3 <br />
|3-Komponenten Fließkommavektor<br />
|-<br />
|vec4 <br />
|4-Komponenten Fließkommavektor<br />
|-<br />
|bvec2 <br />
|2-Komponenten Booleanvektor<br />
|-<br />
|bvec3 <br />
|3-Komponenten Booleanvektor<br />
|-<br />
|bvec4 <br />
|4-Komponenten Booleanvektor<br />
|-<br />
|ivec2 <br />
|2-Komponenten Integervektor<br />
|-<br />
|ivec3 <br />
|3-Komponenten Integervektor<br />
|-<br />
|ivec4 <br />
|4-Komponenten Integervektor<br />
|-<br />
|mat2 <br />
|2x2 Fließkommamatrix<br />
|-<br />
|mat3 <br />
|3x3 Fließkommamatrix<br />
|-<br />
|mat4 <br />
|4x4 Fließkommamatrix<br />
|-<br />
|sampler1D <br />
|Zugriff auf 1D-Textur<br />
|-<br />
|sampler2D <br />
|Zugriff auf 2D-Textur<br />
|-<br />
|sampler3D <br />
|Zugriff auf 3D-Textur<br />
|-<br />
|samplerCube <br />
|Zugriff auf Cubemap<br />
|-<br />
|sampler1DShadow <br />
|Zugriff auf 1D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|sampler2DShadow <br />
|Zugriff auf 2D-Tiefentextur mit Vergleichsoperation<br />
|-<br />
|}<br />
</div><br />
Die sampler-Typen stellen eine besondere Klasse dar und werden im Kapitel 6.7 genauer erklärt, inklusive einiger Anwendungsbeispiele.<br />
<br />
<br />
===Arrays===<br />
<br />
Natürlich unterstützt glSlang auch Arrays, die wie in C deklariert werden und deren Index bei 0 beginnt. Folgendes Array im Shader :<br />
<br />
float temp[3];<br />
<br />
beginnt also bei Index 0 und endet bei Index 2. Im Gegensatz zu C lassen sich Arrays in glSlang allerdings ''nicht bei der Initialisierung vorbelegen''. Wenn ein Array als Parameter einer Funktion deklariert wird, so darf dieses keine Dimensionierung erhalten.<br />
<br />
<br />
===Strukturen===<br />
<br />
Neu ggü. ARB_FP/VP ist nun auch die Möglichkeit Strukturen in einem Shader zu deklarieren. Vor allem die Übersicht komplexerer Shader kann dadurch stark verbessert werden. Strukturen werden wie gewohnt mit dem Schlüsselwort {{INLINE_CODE|struct}} eingeleitet und können dann zur Typisierung von Variablen genutzt werden. Folgendes Beispiel dürfte die Nutzung verdeutlichen :<br />
<br />
struct light<br />
{<br />
bool active;<br />
float intensity;<br />
vec3 position;<br />
vec3 color;<br />
};<br />
<br />
Im Shader können dann neue Variablen vom diesem Typ ganz einfach deklariert werden :<br />
<br />
light LightSource[3];<br />
<br />
Der Zugriff auf die Elemente der Struktur erfolgt dann wie gewohnt über den Punkt :<br />
<br />
LightSource[3].position = vec3(1.0, 1.0, 5.0);<br />
<br />
<br />
<br />
==Typenqualifzierer==<br />
<br />
Zusätzlich zur Typendeklaration kann eine Variable noch einen Typenqualifizerer vorangestellt bekommen, der an den Anfang der Deklaration gehört. Di<br />
<br />
* '''const'''<br />
: Festgelegte (nur lesen) Konstante bzw. nur lesbarer Funktionsparameter.<br />
<br />
* '''uniform'''<br />
: Ein den ganzen Shader über gleichbleibender Wert, der eine Schnittstelle zwischen dem Shader und der OpenGL-Anwendung darstellt. Ein Uniformwert wird in der Hauptanwendung an den entsprechenden Shader übergeben und kann dort dann genutzt werden.<br />
<br />
* '''attribute'''<br />
: Nur lesbare Werte die eine Verbindung zwischen dem Shader und der OpenGL-VertexAPI darstellen (z.B. VertexParameter eines VertexArrays). Natürlich nur in einem Vertex Shader nutzbar.<br />
<br />
* '''varying'''<br />
: Stellt die Verbindung zwischen einem Vertex- und einem FragmentShader dar. Werden im VertexShader geschrieben und dann perspektivisch korrekt über die Primitive interpoliert, um dann im Fragment Shader gelesen werden zu können. Nutzbar sind hier nur die Typen float, vec2, vec3, vec4, mat2, mat3, und mat4, Strukturen und andere Datentypen können nicht varying sein. Die Namen einer varying-Variable müssen sowohl im VertexShader als auch im FragmentShader gleich sein.<br />
<br />
* '''in'''<br />
: Für Variablen die an eine Funktion übergeben und dort ausgelesen werden.<br />
<br />
* '''out'''<br />
: Für Variablen die von einer Funktion nach aussen zurückgegeben werden.<br />
<br />
* '''inout'''<br />
: Für Variablen die sowohl an eine Funktion übergeben als auch von dieser zurückgegeben werden.<br />
<br />
<br />
<br />
Um obige Auflistung nicht leer im Raum stehen zu lassen zeige ich ein paar Beispiele die hoffentlich zum Verständnis beitragen :<br />
<br />
===Beispiel A=== <br />
Vertexnormale soll an einen FragmenShader (interpoliert) übergeben werden :<br />
<br />
:Im VertexShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
VertexNormal = normalize(MV_IT * gl_Normal);<br />
<br />
:Im FragmentShader :<br />
<br />
varying vec3 VertexNormal;<br />
...<br />
TempVector = VertexNormal*...<br />
<br />
<br />
===Beispiel B=== <br />
Uniformparameter zur nachträglichen Farbänderung der Szene wird im Programm übergeben :<br />
<br />
:Im VertexShader :<br />
<br />
uniform vec4 GlobalColor;<br />
...<br />
gl_FrontColor = GlobalColor * gl_Color;<br />
<br />
:Im Programm :<br />
<br />
glUniform4fARB(glSlang_GetUniLoc(ProgramObject, 'GlobalColor'), Col[0], Col[1], Col[2], Col[3]);<br />
<br />
<br />
===Beispiel C=== <br />
Konstante zur festen Farbänderung :<br />
<br />
:Im VertexShader :<br />
<br />
const vec4 ColorBias = vec4(0.2, 0.3, 0.0, 0.0);<br />
...<br />
gl_FrontColor = ColorBias * gl_Color;<br />
<br />
==Konstruktoren==<br />
<br />
Um in einem Shader ''Vektoren'' oder ''Matrizen'' mit Werten zu belegen, gibt es sogenannte Konstruktoren (nicht zu verwechseln mit z.B. Klassenkonstruktoren unter Delphi), die im Endeffekt nichts anderes als Funktionen zur Vorbelegung von Vektoren oder Matrizen darstellen. Dabei trägt der Konstruktor den selben Namen wie die Typendeklaration, also lässt sich eine Variable vom Typ {{INLINE_CODE|vec4}} mit dem Konstruktor {{INLINE_CODE|vec4(float, float, float, float)}} initialisieren.<br />
<br />
Allerdings hat man sich recht viel Mühe bei dieser Konstruktorgeschichte gemacht, so dass man einen vec4 nicht unbedingt mit einem {{INLINE_CODE|vec4}}-Konstruktor vorbelegen muss, sondern es vielseitige Möglichkeiten gibt. Um dies zu verdeutlichen gibts ein paar Beispiele :<br />
<br />
vec4 Color = vec4(1.0, 0.0, 0.0, 0.0);<br />
vec4 Color = vec4(MyVec3, 1.0);<br />
vec4 Color = vec4(MyVec2_A, MyVec2_B);<br />
<br />
vec3 LVec = vec3(MyVec4);<br />
vec2 Tmp = vec2(MyVec3);<br />
<br />
<br />
Trotz der recht wenigen Beispiele sollte schnell erkennbar sein, das man hier wirklich sehr viele Kombinationsmöglichkeiten hat, die dann gültig sind ''wenn man mindestens auf die benötigte Anzahl der Argumente kommt''. Im vorletzten Beispiel wird z.B. ein 3-Komponentenvektor aus einem 4-Komponentenvektor initialisiert. Das erzeugt keinen Fehler, sondern führt dazu das {{INLINE_CODE|vec3.x, vec3.y, vec3.z}} aus MyVec4 übernommen werden und MyVec4.w einfach ignoriert wird.<br />
<br />
Das Umkehrbeispiel, also<br />
vec4 Color = vec4(MyVec3)<br />
funktioniert allerdings nicht, da hier die Zahl der benötigten Argumente nicht erreicht wird. In diesem Falle müsste es dann<br />
vec4 Color = vec4(MyVec3, 0.0)<br />
heissen.<br />
<br />
Obiges gilt natürlich auch für ''Matrixkonstruktoren'', hier sind z.B. folgende Konstuktoren denkbar, obwohl eigentlich alle Möglichkeiten nutzbar sind, ''solange die benötigte Zahl an Argumenten erreicht wird'' :<br />
<br />
mat4 MyMatrix = mat4(MyVec4, MyVec4, MyVec4, MyVec4);<br />
mat2 MyMatrix = mat4(1.0, 0.0, 0.0, 0.0,<br />
0.0, 1.0, 0.0, 0.0,<br />
0.0, 0.0, 1.0, 0.0,<br />
0.0, 0.0, 0.0, 1.0);<br />
<br />
<br />
==Vektor- und Matrixkomponenten==<br />
<br />
Was natürlich in keiner Shadersprache fehlen darf, ist der leichte Zugriff auf die einzelnen Komponenten eines Vektors. glSlang bietet, je nach Anwendungsgebiet gleich drei Namensets für den Zugriff auf die Komponenten eines solchen Vektors, welches Set man nutzen will bleibt natürlich frei und ist unabhängig von der Deklaration eines Vektors. Man sollte nur darauf achten, beim gleichzeitigen Zugriff auf mehrere Komponenten im gleichen Namenset zu verbleiben :<br />
<br />
* {x, y, z, w}<br />
:Für den Zugriff auf Vektoren die Punkte, Normale oder sonstige Vertexdaten repräsentieren.<br />
<br />
* {r, g, b, a}<br />
:Für den Zugriff auf Vektoren die Farbwerte repräsentieren.<br />
<br />
* {s, t, p, q}<br />
:Für den Zugriff auf Vektoren die Texturkoordinaten repräsentieren.<br />
<br />
Ein paar Beispiele zur Unterstreichung des oben gesagten :<br />
<br />
v4.rgba = vec4(1.0, 0.0, 0.0, 0.0); // gültig<br />
v4.rgzw = vec4(1.0, 1.0, 1.0, 2.0); // Ungültig, da verschiedenen Namensets<br />
v2.rgb = vec3(1.0, 2.0, 1.0); // Ungültig, da vec2 nur r+g besitzt<br />
v2.xx = vec2(5.0, 3.0); // Ungültig, da 2 mal gleiche Komponente<br />
<br />
<br />
Auch der Zugriff auf die Komponenten einer Matrix geht leicht von der Hand. Namensets wie bei den Vektoren gibt es hier natürlich keine, aber folgende Beispiele sollen den Zugriff aufzeigen :<br />
<br />
MyMat4[2] = vec4(1.0); // Setzt die 3.Zeile der Matrix komplett auf 1.0<br />
MyMat4[3][3] = 3.5; // Setzt das Element unren rechts auf 3.5<br />
<br />
<br />
Ein Zugriff auf Matrixelemente ausserhalb ihrer Dimension (also z.B. MyMat4[4][4]) liefert unvorhersehabre Ergebnise, also sollte man auf diese Fälle prüfen. <br />
<br />
<br />
==Vektor- und Matrixoperationen==<br />
<br />
Wie von C gewohnt sind in glSlang so ziemlich alle Operatoren die man auf Matrizen oder Vektoren anwenden kann überladen, so das man nicht umständlich über selbstgeschriebene Funktionen kombinieren muss. Darüberhinaus ist es in den meisten Fällen auch möglich ohne Konvertierung Fließkommawerte mit kompletten Matrizen oder Vektoren zu kombinieren. Folgende Beispiele zeigen einige der vielfältigen Kombinationsmöglichkeiten auf :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
float factor;<br />
<br />
vec3 dest = source + factor; <br />
<br />
// Ist gleich<br />
dest.x = source.x + factor;<br />
dest.y = source.y + factor;<br />
dest.z = source.z + factor;<br />
<br />
<br />
Matrix * Vektor ist auch ohne manuelle Konvertierung möglich :<br />
<br />
vec3 dest;<br />
vec3 source;<br />
mat3 MyMat;<br />
<br />
dest = source * MyMat; <br />
<br />
// Ist gleich<br />
dest.x = dot(source, MyMat[0]);<br />
dest.y = dot(source, MyMat[1]);<br />
dest.z = dot(source, MyMat[2]);<br />
<br />
<br />
Auch hier sind die Möglichkeiten fast unbeschränkt und zeigen wieder wie flexibel glSlang ausgelegt ist. <br />
<br />
==Operatoren==<br />
<br />
glSlang bietet (momentan) folgende Operatoren, die Liste ist nach ihrer Gewichtung sortiert (Anfang = höchste). Alle ''reservierten'' Operatoren werden erst in kommender Hardware/glSlang-Versionen nutzbar sein :<br />
<br />
<div align="center"><br />
{|{{Prettytable}}<br />
!Operatorklasse <br />
!Operatoren <br />
!Assoziation<br />
|-<br />
|Gruppering <br />
|() <br />
| -<br />
|-<br />
|Arrayindizierung<br>Funktionsaufrufe und Konstruktoren<br>Strukturfeldwahl und Swizzle<br>Postinkrement und -dekrement<br> <br />
|[]<br>()<br>.<br>++ -- <br />
|Links n. Rechts<br />
|-<br />
|Prefixinkrement- und dekrement<br>Einheitlich (~ reserviert) <br />
| ++ --<br> + - ~ ! <br />
|Rechts n. Links<br />
|-<br />
|Mulitplikation (% reserviert) <br />
|* / % <br />
|Links n. Rechts<br />
|-<br />
|Additiv <br />
| + - <br />
|Links n. Rechts<br />
|-<br />
|Bitweises Verschieben (reserviert) <br />
|<< >> <br />
|Links n. Rechts<br />
|-<br />
|Relation <br />
|< > <= >= <br />
|Links n. Rechts<br />
|-<br />
|Vergleich <br />
|== != <br />
|Links n. Rechts<br />
|-<br />
|Bitweises AND (reserviert) <br />
|& <br />
|Links n. Rechts<br />
|-<br />
|Bitweises XOR (reserviert) <br />
|^ <br />
|Links n. Rechts<br />
|-<br />
|Bitweises OR (reserviert) <br />
| <nowiki>|</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Logisches AND <br />
|&& <br />
|Links n. Rechts<br />
|-<br />
|Logisches XOR <br />
|^^ <br />
|Links n. Rechts<br />
|-<br />
|Logisches OR <br />
| <nowiki>||</nowiki> <br />
|Links n. Rechts<br />
|-<br />
|Auswahl <br />
|?: <br />
|Rechts n. Links<br />
|-<br />
|Zuweisung<br>Arithmetrische Zuweisung<br>(Modulis, Shift und bitweise Op. reserviert) <br />
|<nowiki>=</nowiki><br> <nowiki>+= -= *= /= %=</nowiki> <br> <nowiki><<= >>= &= ^= |=</nowiki> <br />
|Rechts n. Links<br />
|-<br />
|Aufzählung <br />
|, <br />
|Links n. Rechts<br />
|-<br />
|}<br />
</div><br />
<br />
<br />
==Funktionen==<br />
<br />
Ein großer Vorteil von Hochsprachen ist u.A. die Möglichkeit oft genutzte Codeteile in Funktionen (bzw. auch Prozeduren unter Pascal) zu verpacken um so Flexibilität als auch Übersichtlichkeit zu steigern. Wer schonmal was in C geschrieben hat, der wird sich jetzt sicherlich kein Kopfzerbrechen machen müssen. Funktionen werden in glSlang genauso nach folgendem Prinzip deklariert :<br />
<br />
RückgabeTyp FunktionsName(Typ0 Argument0, Typ1, Argument1, ... , TypN, ArgumentN)<br />
{<br />
return RückgabeWert;<br />
}<br />
<br />
<br />
Funktionen die ''nichts zurückgeben'' müssen mit dem RückgabeTyp {{INLINE_CODE|void}} deklariert werden, ausserdem entfällt dann logischerweise das {{INLINE_CODE|return}}. Falls die Funktion eines ihrere Argumente nach aussen übergeben soll, muss dieses Argument mit dem Typenqualifizierer out (Siehe Kapitel 4.2) versehen werden. ''Arrays'' können nur als Eingabeargumente übergeben werden und dürfen nich dimensioniert als Argument verwendet werden, sondern müssen mit leeren Klammern argumentiert werden.<br />
Ein paar Beispiele :<br />
<br />
void MeineFunktion(float EingabeWert; out float AusgabeWert)<br />
{<br />
AusgabeWert = EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Diese Funktion gibt ''nichts'' zurück, aber gibt EingabeWert*MyConstValue im Ausgabeargument AusgabeWert nach aussen.<br />
<br />
float MeineFunktion(float EingabeWert)<br />
{<br />
return EingabeWert*MyConstValue;<br />
}<br />
<br />
<br />
Bietet genau die selbe Funktionalität wie das Beispiel darüber. Allerdings wird hier der berechnete Wert als Ergebnis der Funktion zurückgeliefert.<br />
<br />
float VektorSumme(float v[])<br />
{<br />
return v[0]+v[1]+v[2]+v[3];<br />
}<br />
<br />
<br />
Wie bereits gesagt darf ein Array als Argument keine Dimensionierung enthalten. Wenn man der Funktion also ein Array übergibt, sollte man vorher drauf achten das es entsprechend der in der Funktion genutzten Indizes dimensioniert wurde.<br />
<br />
<br />
==if-Anweisung==<br />
<br />
Selektion über eine if-Anweisung darf auch in keiner Hochsprache fehlen. Genauso wie in C oder Delphi erwartet auch hier die If-Anweisung einen boolschen Ausdruck (Wahr oder Falsch) und wird dann ausgeführt (wahr) bzw. verzweigt auf ein (wenn vorhanden) else (falsch). Verschachtelung ist wie erwartet auch möglich.<br />
<br />
'''Hinweis : ''' <br />
Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten im Fragmentshader kein Early-Out, was zur Folge hat das bei einer If-Anweisung immer alle Zweige ausgeführt werden. Am Ende wird dann aber nur ein Ergebnis geschrieben, die anderen verworfen. Auf solchen Karten bringen If-Anweisungen also im Normalfall keine Geschwindigkeitssteigerung, sondern oft eher das Gegenteil.<br />
Neuere SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall, da hier dynamische Verzweigungen und auch Early-Out von der Hardware implementiert werden.<br />
<br />
==Schleifen==<br />
<br />
Auch Schleifen, ein wichtiges Konzept jeder Hochsprache haben ihren Weg in glSlang gefunden. Unterstützt werden folgende Schleifentypen :<br />
<br />
* '''for'''-Schleife<br />
<br />
for (Startausdruck; Durchlaufbedingung; Wiederholungsausdruck;)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''while'''-Schleife<br />
<br />
while (Durchlaufbedingung)<br />
{<br />
statement<br />
}<br />
<br />
<br />
* '''do'''-while-Schleife<br />
<br />
do<br />
{<br />
statement<br />
}<br />
while (Durchlaufbedingung)<br />
<br />
<br />
'''Hinweis :''' Grafikkarten auf dem Stand des Shadermodells 2.0 (Radeon 9x00, Radeon X8x0, GeForceFX 5x00) unterstüzten Schleifen nicht in Hardware. Schleifen werden dann beim Kompilieren vom Treiber entrollt, wodurch natürlich Shader mit weitaus mehr Instruktionen als erwartet generiert werden. Von daher sollte man auf solchen Karten möglichst auf Schleifen verzichten, oder diese nur recht kurz halten. Bei SM3.0-Karten (Radeon X1x00, GeForce6x00 und höher) ist dass nicht mehr der Fall.<br />
<br />
=Eingebaute Variablen, Attribute und Konstanten=<br />
Nachdem wir uns nun lange genug mit den minderinterssanten Elementen der glSlang-Syntax beschäftigt haben, gehts jetzt endlich an die wirklich interessanten Dinge. Wie schon ARB_VP/ARB_FP bringt auch glSlang jede Menge eingabauter Variablen, Attribute und Konstanten mit, deren Aliase sie recht leicht identifizierbar machen (ganz im Gegensatz zum Indexgewusel bei den DX-Shadern).<br />
<br />
<br />
==Variablen im Vertex Shader==<br />
Exklusiv im Vertex Shader stehen die folgenden Variablen zur Verfügung :<br />
<br />
* vec4 gl_Position muss geschrieben werden<br />
:Dieser Variable '''muss''' im Vertexshader ein Wert zugewiesen werden, wird dies nicht getan ist das Ergebnis (sprich die Position des Vertex) undefiniert. Vorgesehen ist diese Variable für die ''homogene Position des Vertex'' und wird u.a. zum Clipping und Culling verwendet. Sie darf natürlich auch (mehrfaceh) geschrieben und ausgelesen werden.<br />
<br />
* float gl_PointSize kann geschrieben werden<br />
:Diese Variable wurde dazu vorgesehen um dort im VertexShader die Punktgröße in Pixeln hineinzuschreiben.<br />
<br />
* vec4 gl_ClipVertex kann geschrieben werden<br />
:Falls genutzt sollten hier die Vertexkoordinaten die im Zusammenhang mit benutzerdefinierten Clippingplanes genutzt werden abgelegt werden. Wichtig ist, das gl_ClipVertex im selben Koordinatenraum wie die Clippingplane definiert ist. <br />
<br />
<br />
==Attribute im Vertex Shader==<br />
<br />
Folgende Attribute stehen nur im Vertex Shader zur Verfügung und '''können nur gelesen werden''' :<br />
<br />
* vec4 gl_Color<br />
: Farbwert des Vertex.<br />
* vec4 gl_SecondaryColor<br />
:Sekundärer Farbwert des Vertex.<br />
* vec4 gl_Normal<br />
:Normale des Vertex.<br />
* vec4 gl_Vertex<br />
:Koordinaten des Vertex;<br />
* vec4 gl_MultiTexCoord0..7<br />
:Texturkoordinaten auf Textureinheit 0..7.<br />
* float gl_FogCoord<br />
:Nebelkoordinate des Vertex. <br />
<br />
<br />
==Variablen im Fragment Shader==<br />
<br />
Im Fragment Shader sind folgende Variablen exklusiv nutzbar :<br />
<br />
* vec4 gl_FragColor<br />
: Speichert den Farbwert des Fragmentes, der von folgenden Funktionen der festen Pipeline genutzt wird. Wird dieser Variable nichts zugewiesen, so ist ihr Inhalt undefiniert und darauf aufbauende Ergebnisse ebenfalls.<br />
<br />
* float gl_FragDepth<br />
: Durch schreiben dieser Variable kann man den von der festen Funktionspipeline ermittelten Tiefenwert überspringen, der mit {{INLINE_CODE|gl_FragCoord.z}} ausgelesen werden kann. Wird dieser Wert nicht geschrieben, nutzen folgende Funktionen der Pipeline den vorher fest berechneten Wert.<br />
<br />
* vec4 gl_FragCoord nur lesen<br />
: In dieser Variable ist die Position des Fragmentes relativ zur Fensterposition im Format x,y,z,1/w abgelegt, wobei z den von der festen Funktionspipeline berechneten Tiefenwert enthält.<br />
<br />
* bool gl_FrontFacing nur lesen<br />
: Gibt an ob das Fragment zu einer nach vorne zeigenden Primitive gehört (=true). <br />
<br />
<br />
Im Bezug auf {{INLINE_CODE|gl_FragColor}} und {{INLINE_CODE|gl_FragDepth}} sei noch anzumerken das diese ''nicht'' in den Wertebereich 0..1 gebracht werden müssen, da dies später durch die feste Funktionspipeline automatisch gemacht wird.<br />
<br />
<br />
==Eingebaute Varyings==<br />
<br />
Wie bereits in Kapitel 4.2 erwähnt, stellen Varyings eine Schnittstelle zwischen dem Vertex und dem Fragment Shader dar. Sie werden im Vertex Shader geschrieben und können dann im Fragment Shader ausgelesen werden, ohne das die folgenden Varyings dafür explizit deklariert werden müssen :<br />
<br />
* vec4 gl_FrontColor<br />
: Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackColor<br />
: Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_FrontSecondaryColor<br />
: Sekundäre Farbe der Vorderseite des Vertex.<br />
<br />
* vec4 gl_BackSecondaryColor<br />
: Sekundäre Farbe der Rückseite des Vertex.<br />
<br />
* vec4 gl_TexCoord[x]<br />
: Texturkoordinaten des Vertex auf Textureinheit x, wobei x die von der Hardware zur Verfügung gestellte Zahl der Textureinheiten-1 nicht überschreiten darf.<br />
<br />
* float gl_FogFragCoord<br />
: Nebelkoordinate des Fragmentes. <br />
<br />
Die Varyings {{INLINE_CODE|gl_FrontColor, gl_FrontSecondaryColor, gl_BackColor}} und {{INLINE_CODE|gl_BackSecondaryColor}} können im FragmentShader nur unter den Aliases gl_Color bzw. gl_SecondaryColor gelesen werden. Welcher Wert des Vertex Shaders im Fragment Shader dort eingesetzt wird ist abhängig davon ob das Fragment zu einer nach vorne oder nach hinten zeigenden Primitive gehört.<br />
<br />
<br />
==Eingebaute Konstanten==<br />
Auch diverse Konstanten wurden definiert um darauf schnell im Shader zugreifen zu können. In den Klammern stehen die von einer GL-Implementation als Mindestanforderung anzubietenden Werte. Alle Konstanten sind sowohl im Vertex als auch im Fragment Shader abrufbar :<br />
<br />
: OpenGL 1.0/1.2 :<br />
* int gl_MaxLights (8)<br />
* int gl_MaxClipPlanes (6)<br />
* int gl_MaxTextureUnits (2)<br />
<br />
<br />
: ARB_Fragment_Program :<br />
* int gl_MaxTextureCoordsARB (2)<br />
<br />
<br />
: Vertex_Shader :<br />
* int gl_MaxVertexAttributesGL2 (16)<br />
* int gl_MaxVertexUniformFloatsGL2 (512)<br />
* int gl_MaxVaryingFloatsGL2 (32)<br />
* int gl_MaxVertexTextureUnitsGL2 (1)<br />
<br />
<br />
: Fragment_Shader :<br />
* int gl_MaxFragmentTextureUnitsGL2 (2)<br />
* int gl_MaxFragmentUniformFloatsGL2 (64)<br />
<br />
<br />
==Eingebaute Uniformvariablen==<br />
<br />
Um den Zugriff auf OpenGL-Staten zu vereinfachen wurden in glSlang diverse Uniformvariablen zur direkten Verwendung im Shader eingebaut. Wie gewohnt wurden auch hier sinnvolle Namen verwendet, so dass eine tiefere Erklärung unnötig sein dürfte :<br />
<br />
* mat4 gl_ModelViewMatrix<br />
* mat4 gl_ProjectionMatrix<br />
* mat4 gl_ModelViewProjectionMatrix<br />
* mat3 gl_NormalMatrix<br />
* mat4 gl_TextureMatrix[gl_MaxTextureCoordsARB]<br />
:{{INLINE_CODE|gl_NormalMatrix}} repräsentiert die inversen oberen 3x3 Werte der Modelansichtsmatrix. {{INLINE_CODE|gl_TextureMatrix[x]}} adressiert maximal Anzahl Textureinheiten-1-Texturmatrizen.<br />
<br />
* float gl_NormalScale<br />
: Gibt den unter OpenGL festgelegten Faktor zur Skalierung der Normalen zurück.<br />
<br />
* struct gl_DepthRangeParameters<br />
<br />
struct gl_DepthRangeParameters<br />
{<br />
float near;<br />
float far;<br />
float diff;<br />
};<br />
gl_DepthRangeParameters gl_DepthRange;<br />
<br />
: Clippingplanes : <br />
* vec4 gl_ClipPlane[gl_MaxClipPlanes]<br />
<br />
*struct gl_PointParameters<br />
struct gl_PointParameters<br />
{<br />
float size;<br />
float sizeMin;<br />
float sizeMax;<br />
float fadeThresholdSize;<br />
float distanceConstantAttenuation;<br />
float distanceLinearAttenuation;<br />
float distanceQuadraticAttenuation;<br />
};<br />
gl_PointParameters gl_Point;<br />
<br />
*struct gl_MaterialParameters<br />
struct gl_MaterialParameters<br />
{<br />
vec4 emission;<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
float shininess;<br />
};<br />
gl_MaterialParameters gl_FrontMaterial;<br />
gl_MaterialParameters gl_BackMaterial;<br />
<br />
*struct gl_LightSourceParameters<br />
struct gl_LightSourceParameters<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
vec4 position;<br />
vec4 halfVector;<br />
vec3 spotDirection;<br />
float spotExponent;<br />
float spotCutoff;<br />
float spotCosCutoff;<br />
float constantAttenuation;<br />
float linearAttenuation;<br />
float quadraticAttenuation;<br />
};<br />
gl_LightSourceParameters gl_LightSource[gl_MaxLights];<br />
<br />
*struct gl_LightModelParameters<br />
struct gl_LightModelParameters<br />
{<br />
vec4 ambient;<br />
};<br />
gl_LightModelParameters gl_LightModel;<br />
<br />
*struct gl_LightModelProducts<br />
struct gl_LightModelProducts<br />
{<br />
vec4 sceneColor;<br />
};<br />
gl_LightModelProducts gl_FrontLightModelProduct;<br />
gl_LightModelProducts gl_BackLightModelProduct;<br />
<br />
*struct gl_LightProducts<br />
struct gl_LightProducts<br />
{<br />
vec4 ambient;<br />
vec4 diffuse;<br />
vec4 specular;<br />
};<br />
gl_LightProducts gl_FrontLightProduct[gl_MaxLights];<br />
gl_LightProducts gl_BackLightProduct[gl_MaxLights];<br />
<br />
* vec4 gl_TextureEnvColor[gl_MaxFragmentTextureUnitsGL2]<br />
* vec4 gl_EyePlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_EyePlaneQ[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneS[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneT[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneR[gl_MaxTextureCoordsARB]<br />
* vec4 gl_ObjectPlaneQ[gl_MaxTextureCoordsARB]<br />
<br />
*struct gl_FogParameters<br />
struct gl_FogParameters<br />
{<br />
vec4 color;<br />
float density;<br />
float start;<br />
float end;<br />
float scale;<br />
};<br />
gl_FogParameters gl_Fog;<br />
<br />
Diese recht umfangreiche GL-Stateliste sollte eigentlich jeden Bedarf decken und momentan gibts kaum einen OpenGL-Status den man so nicht in einem Shader abfragen bzw. nutzen kann.<br />
<br />
<br />
=Eingebaute Funktionen=<br />
glSlang ist mit diversen Skalar- und Vektorfunktionen ausgestattet, die teilweise (idealerweise) sogar direkt in der Hardware ausgeführt werden, weshalb einer fertigen Funktion ggü. gleichwertigen eigenen Berechnungen immer der Vorzug zu geben ist.<br />
{{Hinweis| ''genType'' kann vom Type float, vec2, vec3 oder vec4 sein, ''mat'' vom Typ mat2, mat3 oder mat4.}}<br />
<br />
<br />
==Trigonometire und Winkel==<br />
Alle übergebenen Winkel sollten, soweit nicht anders vermerkt, in Radien angegeben werden.<br />
<br />
* genType radians (genType degrees)<br />
: Wandelt von Grad nach Radien. <br />
* genType degrees (genType radians)<br />
: Wandelt von Radien nach Grad.<br />
* genType sin (genType angle)<br />
: Gibt den Sinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType cos (genType angle)<br />
: Gibt den Cosinus von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType tan (genType angle)<br />
: Gibt den Tangens von Angle zurück, wobei Angle in Radien angegeben wird.<br />
* genType asin (genType x)<br />
: Liefert den Arcsinus von x zurück, also den Winkel dessen Sinus x ergeben würde.<br />
* genType acos (genType x)<br />
: Liefert den Arccosinus von x zurück, also den Winkel dessen Cosinus x ergeben würde.<br />
* genType atan (genType y, genType x)<br />
: Liefert den Winkel zurück, dessen Tangens x/y ergeben würde.<br />
* genType atan (genType y_over_x)<br />
: Liefert den Winkel zurück, dessen Tangens x über y ergeben würde. <br />
<br />
<br />
==Exponentiell==<br />
* genType pow (genType x, genType y)<br />
: Gibt x hoch y zurück.<br />
* genType exp2 (genType x)<br />
: Gibt 2 hoch x zurück.<br />
* genType log2 (genType x)<br />
: Gibt den Logarithmus zur Basis 2 von x zurück.<br />
* genType sqrt (genType x)<br />
: Gibt die Wurzel von x zurück.<br />
* genType inversesqrt (genType x)<br />
: Gibt die umgekehrte Wurzel von x zurück. <br />
<br />
<br />
==Standardfunktionen==<br />
* genType abs (genType x)<br />
: Liefert den absoluten Wert von x zurück.<br />
* genType sign (genType x)<br />
: Gibt -1.0 zurück, wenn x < 0.0, 0.0 wenn x = 0.0 und 1.0 wenn x > 0.0.<br />
* genType floor (genType x)<br />
: Gibt denn nächsten Integerwert zurück, der kleiner oder gleich x ist.<br />
* genType ceil (genType x)<br />
: Gibt den nächsten Integerwert zurück, der größer oder gleich x ist.<br />
* genType fract (genType x)<br />
: Gibt den Nachkommateil von x zurück.<br />
* genType mod (genType x, float y) <br />
* genType mod (genType x, genType y)<br />
: Gibt den Modulus zurück. (=x-y * floor(x/y)) <br />
* genType min (genType x, genType y) <br />
* genType min (genType x, float y)<br />
: Liefert y zurück wenn y < x, ansonsten x. <br />
* genType max (genType x, genType y) <br />
* genType max (genType x, float y)<br />
: Liefert y zurück wenn x < y, ansonsten x. <br />
* genType clamp (genType x, genType minVal, genType maxVal) <br />
* genType clamp (genType x, float minVal, float maxVal)<br />
: Zwängt x in den Bereich minVal..maxVal. <br />
* genType mix (genType x, genType y, genType a)<br />
* genType mix (genType x, genType y, float a)<br />
: Liefert den linearen Blend zwischen x und y zurück. (= x * (1-a) + y * a) <br />
* genType step (genType edge, genType x)<br />
* genType step (float edge, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge, ansonsten 1.0. <br />
* genType smoothstep (genType edge0, genType edge1, genType x)<br />
* genType smoothstep (float edge0, float edge1, genType x)<br />
: Liefert 0.0 zurück, wenn x <= edge und 1.0 wenn x >= edge. Dabei wird eine weiche Hermite Interpolation zwischen 0 und 1 durchgeführt. <br />
<br />
<br />
==Geometrie==<br />
* float length (genType x)<br />
: Gibt die Länge des Vektors x (= sqrt(x[0]² + x[1]² + ... + x[n]²) zurück. <br />
* float distance (genType p0, genType p1)<br />
: Gibt die Distanz zwischen den zwei Vektoren p0 un p1 (= length(p0-p1)) zurück. <br />
* float dot (genType x, genType y)<br />
: Gibt das Punktprodukt von x und y zurück (=x[0]*y[0] + x[1]*y[1] + ... + x[n]*y[n]). <br />
* vec3 cross (vec3 x, vec3 y)<br />
: Gibt das Kreuzprodukt von x und y zurück. <br />
* genType normalize (genType x)<br />
: Normalisiert den Vektor x auf die Länge 1. <br />
* vec4 ftransform()<br />
: Nur im Vertex Shader. Die Funktion stellt sicher, das das eingehende Vertex haargenau so transformiert wird wie in der festen Funktionspipeline. gl_Position = ftransform() wird dann also gebraucht, wenn in mehreren Durchgängen sowohl im Shader als auch in der festen Pipeline gerendert wird, um sicherzustellen das in beiden Fällen die gleiche Vertexposition herauskommt. <br />
* genType faceforward (genType N, genType I, genType Nref)<br />
: Gibt einen nach vorne zeigenden Vektor N zurück. (If dot(NRef, I) < 0 return N else return -N) <br />
* genType reflect (genType I, genType N)<br />
: Gibt den an der Flächenausrichtung N reflektierten Vektor I zurück. (=I-2 * dot(N,I) * N) <br />
<br />
<br />
==Matrixfunktionen==<br />
* mat matrixCompMult (mat x, mat y)<br />
: Multipliziert Matrix X mit Matrix Y komponentenweise. Um eine normale lineare Matrixmultiplikation durchzuführen, sollte der "*"-Operator genutzt werden. <br />
<br />
<br />
==Vektorvergleiche==<br />
Die meisten Vektorvergleichsfunktionen liefern als Ergebnis einen boolvektor zurück, da die Vergleiche per Komponente stattfinden. Wenn man also x = vec4(1.0, 3.0, 0.0, 0.0) mit y = vec4(2.0, 1.5, 1.5, 0.0) via lessThan(x, y) vergleicht, erhält man als Ergebnis bvec(true, false, true, false).<br />
<br />
* bvec lessThan (vec x, vec y)<br />
* bvec lessThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x < y zurück. <br />
* bvec lessThanEqual (vec x, vec y)<br />
* bvec lessThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x <= y zurück. <br />
* bvec greaterThan (vec x, vec y)<br />
* bvec greaterThan (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x > y zurück. <br />
* bvec greaterThanEqual (vec x, vec y)<br />
* bvec greaterThanEqual (ivec x, ivec y)<br />
: Gibt den komponentenweisen Vergleich x >= y zurück. <br />
* bvec equal (vec x, vec y)<br />
* bvec equal (ivec x, ivec y)<br />
* bvec equal (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x == y zurück. <br />
* bvec notEqual (vec x, vec y)<br />
* bvec notEqual (ivec x, ivec y)<br />
* bvec notEqual (bvec x, bvec y)<br />
: Gibt den komponentenweisen Vergleich x != y zurück. <br />
* bool any (bvec x)<br />
: Liefert true zurück, wenn mindestens eine der Komponenten von x true ist.<br />
* bool all (bvec x)<br />
: Liefert true zurück, wenn alle Komponenten von x true sind. <br />
* bvec not (bvec x)<br />
: Liefert die logische Negation von x zurück. <br />
<br />
<br />
==Texturenzugriffe==<br />
<br />
Diese wichtige Funktionskategorie dient dazu, Werte aus einer an eine Textureinheit gebundenen Textur zu ermitteln. Die Texturenzugriffe können sowohl im Vertex (!) als auch im Fragment Shader ausgeführt werden, wobei der optionale Parameter bias im Vertex Shader ignoriert wird. Allerdings gibt es zusätzlich Funktionen die auf "Lod" enden und nur im Vertex Shader genutzt werden dürfen um eben dieses Manko zu umgehen. Funktionen mit dem Suffix "Proj" geben einen projezierten Texturenwert zurück.<br />
<br />
: '''1D-Texturen :'''<br />
* vec4 texture1D (sampler1D sampler, float coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec2 coord [, float bias])<br />
* vec4 texture1DProj (sampler1D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 texture1DLod (sampler1D sampler, float coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec2 coord, float lod)<br />
* vec4 texture1DProjLod (sampler1D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''2D-Texturen :'''<br />
* vec4 texture2D (sampler2D sampler, vec2 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec3 coord [, float bias])<br />
* vec4 texture2DProj (sampler2D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture2DLod (sampler2D sampler, vec2 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec3 coord, float lod)<br />
* vec4 texture2DProjLod (sampler2D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''3D-Texturen :'''<br />
* vec4 texture3D (sampler3D sampler, vec3 coord [, float bias])<br />
* vec4 texture3DProj (sampler3D sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
* vec4 texture3DLod (sampler3D sampler, vec3 coord, float lod)<br />
* vec4 texture3DProjLod (sampler3D sampler, vec4 coord, float lod)<br />
<br />
<br />
: '''Cubemap :'''<br />
* vec4 textureCube (samplerCube sampler, vec3 coord [, float bias])<br />
: Nur im Vertex Shader : <br />
*vec4 textureCubeLod (samplerCube sampler, vec3 coord, float lod)<br />
<br />
<br />
: '''Tiefentextur (Shadowmap) :'''<br />
* vec4 shadow1D (sampler1DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow2D (sampler2DShadow sampler, vec3 coord [, float bias])<br />
* vec4 shadow1DProj (sampler1DShadow sampler, vec4 coord [, float bias])<br />
* vec4 shadow2DProj (sampler2DShadow sampler, vec4 coord [, float bias])<br />
: Nur im Vertex Shader :<br />
* vec4 shadow1DLod (sampler1DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow2DLod (sampler2DShadow sampler, vec3 coord, float lod)<br />
* vec4 shadow1DProjLod (sampler1DShadow sampler, vec4 coord, float lod)<br />
* vec4 shadow2DProjLod (sampler2DShadow sampler, vec4 coord, float lod)<br />
<br />
<br />
Wie bereits eingangs gesagt ist dieses Kapitel ein sehr wichtiges, denn eine 3D-Szene ohne Texturen ist heute kaum denkbar. Darüber hinaus lassen sich durch Texturenzugriffe recht viele interessante Sachen machen, z.B. ein einfacher Blurfilter oder das freie überblenden bestimmter Texturenteile. Deshalb führe ich hier kurz ein paar Beispiele an, welche die Nutzung dieser Funktionen verdeutlichen sollen :<br />
<br />
===Beispiel A=== <br />
Eine Textur gebunden die einfach ausgegeben werden soll<br />
<br />
''Im Vertex Shader'' :<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
Der Vertex Shader ist recht minimal. Neben der homogenen Vertexposition leiten wir hier nur die im OpenGL-Programm angegebenen Texturkoordinaten weiter. ''Dies ist aber unbedingt nötig!'' Ohne die letzte Zeile hätten wir im Fragment Shader keine gültigen Texturkoordinaten auf TMU0, was ihn einer Fehldarstellung enden würde.<br />
<br />
''im Fragment Shader'' :<br />
<br />
uniform sampler2D texSampler;<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSampler, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
Zuerst deklarieren wir hier einen 2D-Texturensampler, wichtig : '''Texturensampler müssen IMMER als uniform deklariert werden!''' In der Hauptfunktion weisen wir dann einfach den über die Funktion texture2D aus unserer gebundenen Textur ausgelesenen Farbwert, anhand der vom Vertex Shader übergebenen Texturkoordinaten, zu.<br />
<br />
===Beispiel B=== <br />
Zwei Texturen, jeweils auf TMU0 und TMU1. Fragmentfarbe soll eine Multiplikation der beiden Texturen darstellen.<br />
<br />
In diesem Beispielfall (der recht häufig vorkommt) müssen wir im Programm festlegen, ''welcher Sampler welche Textureinheit adressiert'', genau deshalb müssen die Texturensampler auch als uniform deklariert werden. Die Standardtextureneinheit eines Samplers ist TMU0, was in unserem Falle natürlich nicht brauchbar ist. Also müssen wir unserem zweiten Textursampler im Programm mitteilen das er seine Daten aus TMU1 beziehen soll :<br />
<br />
glUniform1iARB(glSlang_GetUniLoc(ProgramObject, 'texSamplerTMU1'), 1);<br />
<br />
Dies ist also unbedingt zu machen, sobald ein Texturensampler eine Textureinheit > GL_TEXTURE_0 adressieren will. Die Textureneinheit des Samplers lässt sich also nicht im Shader selbst festlegen. Der Fragment Shader ist nun allerdings schnell hergeleitet (Vertex Shader verändert sich nicht, da TMU1 die Texturkoordinaten auch von TMU0 bezieht) :<br />
<br />
<br />
im Fragment Shader :<br />
<br />
uniform sampler2D texSamplerTMU0;<br />
uniform sampler2D texSamplerTMU1;<br />
<br />
<br />
void main(void)<br />
{<br />
gl_FragColor = texture2D(texSamplerTMU0, vec2(gl_TexCoord[0])) *<br />
texture2D(texSamplerTMU1, vec2(gl_TexCoord[0]));<br />
}<br />
<br />
==Noisefunktionen==<br />
Sowohl im Vertex als auch im Fragment Shader lassen sich Noisefunktionen nutzen, mit deren Hilfe sich einge Gewisse "Zufälligkeit" simulieren lässt (wirklich zufällige Werte sind es natürlich nicht). Ein zurückgegebener Wert liegt dabei immer im Bereich [-1..1] und ist immer bei gleichem Eigabewert auch immer gleich.<br />
<br />
* float noise1 (genType x)<br />
* vec2 noise2 (genType x)<br />
* vec3 noise3 (genType x)<br />
* vec4 noise4 (genType x)<br />
<br />
<br />
==Discard==<br />
Eigentlich keine Funktion, sondern eine Abbruchbedingung '''nur im Fragment Shader'''. Das Schlüsselwort {{INLINE_CODE|discard}} verwirft das aktuell bearbeitete Fragment und beendet gleichzeitig den Shader. Es kann z.B. genutzt werden um Alphamasking manuell durchzuführen.<br />
Man sollte dabei jedoch beachten dass ein Großteil der aktuellen Hardware kein "early-out" (frühes Beenden) im Fragmentshader unterstützt. Wenn dort also ein {{INLINE_CODE|discard}} auftaucht, wird trotzdem auch der Code danach ausgeführt und einfach verworfen. Einen Geschwindigkeitsvorteil durch diesen Befehl wird man also erst auf neueren Karten feststellen, die dieses Faeature auch so unterstützen wie es angedacht war. <br />
<br />
<br />
=Beispielshader=<br />
Wen bis hierhin nicht der Mut verlassen hat, und wer aufmerksam gelesen hat, dürfte jetzt also zumindest in der Lage sein kleinere Shader in glSlang zu schreiben und diese auch im Programm zu nutzen. Ich habe im Themenbereich "glSlang" versucht alle Bereiche der Shadersprache selbst anzusprechen und hoffe das auch brauchbar rübergebracht zu haben. Um oben erlerntes (hoffe ich doch mal) nochmal zu vertiefen werde ich jetzt (wie ich das bereits bei meinem ARB_VP-Tutorial getan habe) einen simplen Beispielshader (Vertex und Fragment Shader) auseinanderpflücken um so u.a. auch die Programmstruktur für alle die in C nicht so bewandert sind zu erörtern.<br />
<br />
<br />
==Der Vertex Shader==<br />
uniform vec4 GlobalColor;<br />
<br />
void main(void)<br />
{<br />
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;<br />
gl_FrontColor = gl_Color * GlobalColor;<br />
gl_TexCoord[0] = gl_MultiTexCoord0;<br />
}<br />
<br />
<br />
Wie gesagt recht simpel. Angefangen wird mit der Deklaration einer globalen Uniformvariable namens {{INLINE_CODE|GlobalColor}}. Wie wir uns erinnern gibt der Typenqualifizierer uniform an, das wir den Wert dieser Variable (ein 4-Komponentenvektor, da Farbwerte aus R,G,B und A bestehen) in unserem Programm an den Shader übermitteln.<br />
<br />
Danach gehts ohne Umwege direkt in unsere Hauptfunktion, da wir im Vertex Shader keine anderen Funktionen benötigen. Dort berechnen wir zuerst die homogene Position unseres Vertex, die sich aus der eingehenden Vertexposition multipliziert mit der Modelansichtsmatrix ergibt. Wie schonmal gesagt '''muss diesem Wert etwas zugewiesen werden''', da sonst alle darauf aufbauenden Funktionen unvorhersehbare Ergebnisse liefern.<br />
Ausserdem wollen wir die Frontfarbe unseres Vertex jedesmal mit der im Programm übergebenen GlobalColor multiplizieren, so dass wir den Farbwert der gesamten Szene aus unserem Programm heraus manipulieren können. Zu guterletzt geben wir dann noch unsere aus der festen Funktionspipeline erhaltenen Texturkoordinaten auf Textureinheit 0 weiter. Wenn im Fragmentshader Texturkoordinaten verwendet werden, '''muss das getan werden'''. <br />
<br />
<br />
==Der Fragment Shader==<br />
uniform sampler2D Texture0;<br />
uniform sampler2D Texture1;<br />
uniform sampler2D Texture2;<br />
uniform sampler2D Texture3;<br />
<br />
void main(void)<br />
{<br />
vec2 TexCoord = vec2( gl_TexCoord[0] );<br />
vec4 RGB = texture2D( Texture0, TexCoord );<br />
<br />
gl_FragColor = texture2D(Texture1, TexCoord) * RGB.r +<br />
texture2D(Texture2, TexCoord) * RGB.g +<br />
texture2D(Texture3, TexCoord) * RGB.b;<br />
}<br />
<br />
<br />
Auch hier passiert nicht wirklich viel Großartiges. Wir deklarieren beim Shaderanfang zuerst vier Texturensampler, da wir insgesamt vier verschiedene Texturen im Shader auslesen wollen, eine Verlaufstextur und drei Oberflächentexturen. Auch hier sei wieder gesagt das man Sampler '''immer als uniform deklarieren muss'''. In der Hauptfunktion deklarieren wir dann einen Farbvektor, der auch direkt einen Farbwert aus Textureinheit 0 zugewiesen bekommt. Auf Textureinheit 0 haben wir ihm Hauptprogramm eine Verlaufstextur gebunden, die angibt wie die drei folgenden Texturen ineinander geblendet werden.<br />
Danach schreiben wir dann den Farbwert des Fragmentes, der '''im Fragment Shader ausgegeben werden muss'''. Der besteht wie einfach zu erkennen aus Farbwert von Textureinheit 1 * Rotwert von Textureinheit 0 + Farbwert von Textureinheit 2 * Grünwert von Textureinheit 0 + Farbwert von Textureinheit 3 * Blauwert von Textureinheit 0. So ist z.B. an Stellen an denen in der Verlaufstextur reines blau liegt nur die dritte Textur sichtbar.<br />
<br />
So viel also zu unserem kleinen Beispielshader. Er ist weder besonders toll noch besonders sinnvoll, sollte aber auch eher dazu dienen euch glSlang ein wenig zu veranschaulichen, was mir hoffentlich gelungen ist.<br />
<br />
Wenn ihr in den vorangegangenen Kaptilen zumindest ein wenig aufgepasst habt, dann könnt ihr euch vor eurem inneren Auge hoffentlich vortstellen was der Shader macht : Er blendet drei Texturen weich anhand der Verlaufstextur ineinander über. Sowas kann man z.B. für ein Terrain nutzen, um dieses anhand einer Fargtextur zu Texturieren. Für alle die damit Probleme haben hier zwei Bilder die den Shader veranschaulichen. Links die Verlaufstextur, die angibt wo welche Textur wie stark gewichtet wird und rechts dann das Ergebnis :<br />
<br />
<div align="center"> [[BILD:GLSL_sample_shader_a.jpg]] [[BILD:GLSL_sample_shader_b.jpg]]</div><br />
<br />
<br />
=Post Mortem=<br />
Das wars also, meine "Einführung" in die OpenGL Shader Sprache. Ich hoffe es hat euch nicht gelangweilt und auch die von mir zur Verfügung gestellten Informationen haben euch hoffentlich ausgereicht. Mit der Veröffentlichung dieser Einführung geht übrigens auch die Eröffnung eines Shaderforums hier auf der DGL einher, in der ihr dann also fleissig Fragen zum Thema stellen oder eure Shader präsentieren könnt. In diesem Post Mortem gehe ich jetzt noch kurz auf die Zukunft von glSlang ein und zeige ein paar Screenshots (damit die Augen entspannen können), bevor ihr euch dann selbst in die Shaderwelt stürzen könnt. <br />
<br />
<br />
=Screenshots=<br />
<br />
Um eure Augen ein wenig zu verwöhnen und zu zeigen was man mit glSlang alles machen, v.a. da man jetzt Shader schön lesbar in einer Hochsprache verfassen kann, mal ein paar Screens. Besonders der zweite Shot sieht animiert noch besser aus :<br />
<br />
{{center|[[BILD:GLSL_sample_Kugel.jpg]] [[BILD:GLSL_sample_Alien.jpg]]}}<br />
<br />
Die Zahl möglicher Effekte ist bei einer so flexiblen Shadersprache natürlich nahezu unbegrenzt, und besonders auf kommender Hardware werden bisher ungesehen Effekte den Einzu in die Echtzeitgrafik finden. Man darf also mehr als gespannt sein.<br />
<br />
=Die Zukunft=<br />
Viele werden sich sicherlich fragen, warum sie z.B. statt ARB_VP/FP oder Nvidias cG denn überhaupt auf glSlang setzen sollen. Doch solche Zweifel dürften bei einem genauen Blick auf die neue Shadersprache schnell verworfen sein. Zum einen steckt hinter glSlang dank des ARBs fast die komplette 3D-Industrie und zum anderen hat man beim Entwurf der Shadersprache, wie z.B. an vielen reservierten Wörtern/Funktionen erkennbar versucht so weit wie möglich in die Zukunft zu planen. So sollen auch Karten der nächsten und übernächsten Generation mit glSlang ausnutzbar sein, und was danach kommt wird durch Spracherweiterungen erreicht. Sich also jetzt (besonders da es krachneu ist) mit glSlang zu befassen, um nicht ganz den Anschluss an kommende Entwicklungen im 3D-Bereich zu verlieren, ist der beste Weg.<br />
<br />
Also viel Spaß beim Experimentieren und Shaderschreiben! Und nicht vergessen : Wir wollen sehen was ihr so treibt,<br />
<br />
Euer<br />
:Sascha Willems ([mailto:webmaster@delphigl.de webmaster@delphigl.de])<br />
<br />
<br />
<br />
{{TUTORIAL_NAVIGATION|-|[[tutorial_glsl2]]}}<br />
[[Kategorie:Tutorial|GLSL]]</div>Akira