Tutorial Nr.3 - Objektselektion
Einleitung
Manchmal ist es nötig, zu wissen welches auf den Schirm gebrachte Objekt vom Benutzer gewählt wurde.Egal ob man es für einen Karteneditor benötigt, um zu wissen welche Wand angeklickt wurde, oder für ein Strategiespiel um zu wissen welches Gebäude gewählt wurde, ist es sehr einfach zu implementieren.Da ich noch keine guten Tutorials über OpenGLs Selection Modus (besonders in Delphi) gesehen habe und diesen für den Karteneditor der ZornGL Engine benötigte, habe ich mich dran gemacht herauszufinden wie diese Technik zu nutzen ist und werde es Ihnen in diesem Tutorial zeigen.
Schritt 1 - Die Objektnamen auf den Name Stack legen
Das Wichtigste am Selection Modus ist der Name Stack.Dies ist der Platz, an dem OpenGL die "Namen" der Objekte (trotz der Bezeichnung "Name" handelt es sich um Integer Werte) ablegt, die später an Sie zur Erkennung des gewählten Objekts zurückgegeben werden.Wie mit allen anderen OpenGL Stacks kann man Namen auf den Stack legen.Obwohl man auch Namen von diesem Stack herunternehmen kann, wird man davon kaum Nutzen machen.Beachten Sie, das alle Kommandos zur Manipulation des Name Stacks nur im Selection Modus funktionieren, dazu aber später mehr.Sehen Sie sich zuerst dieses kleine Beispiel und seine Beschreibung an :

const
 Sun   = 1;
 Mars  = 2;
 Earth = 3;
 Moon  = 4;

procedure TGLForm.DrawScene;
begin
glInitNames;
glPushName(0);
...
glLoadName(Sun);
glCallList(SunList);
...
glLoadName(Mars);
glCallList(MarsList);
...
end;

Die Nutzung des Selection Modus ist wie Sie sehen können sehr einfach.Zuerst müssen Sie den Name Stack leeren and dann muß mindestens ein Name auf den Stack gelegt werden.Wenn Sie dies nicht tun, wird der nächste glLoadName Befehl keinen Namen auf den Stack legen sondern einen GL_INVALID_OPERATION Fehler auslösen.
Nachdem der Name Stack also ordnungsgemäß initialisiert wurde, laden wir für jedes Objekt in unserer Szene (oder besser alle Objekte die der Nutzer wählen kann) einen Namen.Nach dem Laden des Namens zeichnen Sie Ihr Objekt.Dabei macht es keinen Unterschied ob dies eine Displayliste ist oder durch glBegin und glEnd geschieht.
Schritt 2 - Die gewählten Objekte finden
Jetzt kommen wir zum Kern der Sache : Herausfinden welches Objekt unter dem Cursor liegt.Dies ist ein wenig komplizierter, aber sehen Sie sich dazu folgende Prozedur und ihre Beschreibung an :

function TGLForm.GetSelectBufferHit : Integer;
var
 SelectBuffer : array[0..512] of TGLUInt;
 Viewport     : TGLArrayi4;
 Hits,i       : Integer;
 HitZValue    : TGLUInt;
 Hit          : TGLUInt;
begin
glGetIntegerv(GL_VIEWPORT, @viewport);
glSelectBuffer(512, @SelectBuffer);
glRenderMode(GL_SELECT);
glInitNames;
glPushName(0);
glMatrixMode(GL_PROJECTION);
glPushMatrix;
 glLoadIdentity;
 gluPickMatrix(mx, viewport[3]-my, 1.0, 1.0, viewport);
 gluPerspective(45.0, ClientWidth/ClientHeight, 0.1, 1000);
 DrawScene;
 glMatrixMode(GL_PROJECTION);
glPopMatrix;
Hits := glRenderMode(GL_RENDER);
Hit       := High(TGLUInt);
HitZValue := High(TGLUInt);
for i := 0 to Hits-1 do
 if SelectBuffer[(i*4)+1] < HitZValue then
  begin
  Hit       := SelectBuffer[(i*4)+3];
  HitZValue := SelectBuffer[(i*4)+1];
  end;
Result := Hit;
end;

Wenn Sie die Variablendeklaration dieser Funktion betrachten, werden Sie sehen das ein Array namens SelectBuffer benötigt wird.Dies ist der Platz an dem OpenGL alle Treffer mit ihren Z-Werten speichern wird.Ein weiterer Nachteil des Selection Modus ist die Tatsache das die Anzahl der Treffer die man bekommt nicht bekannt ist.Deshalb muß ein Array fester Größe genutzt werden, was jedoch aufgrund der Tatsache das dies lokal deklariert ist keine Probleme macht.
Nach dem Sichern unseres Blickfeldes nutzen wir den Befehl glSelectBuffer(size : Integer;buffer : PGLUint) um OpenGL die Größe und die Adresse unseres SelectBuffers mitzuteilen.
Nun wechseln wir vom Rendermodus in den Selection Modus mit Hilfe des glRenderMode(mode : cardinal) Befehls, initialisieren den Name Stack und legen einen Namen auf ihn.
Danach ist es Zeit eine neue Projekionsmatrix zu erstellen.Da wir wollen, das der Nutzer ein Objekt durch einen Mausklick anwählen kann, erstellen wir mit gluPickMatrix(x, y, width, height: TGLdouble; viewport: TVector4i) eine neue Matrix, die an der Mausposition zentiert wird und 1x1 Pixel groß ist.
Nach einem erneuten setzen des FOV, Aspektratios und des zFar-Wertes zeichnen wir unsere Szene erneut.Diesmal wird sie jedoch im Selection Modus gezeichnet und alle den Name Stack betreffenden Befehle werden jetzt ausgeführt.
Nachdem wir unsere Projektionsmatrix wieder hergestellt haben, wechseln wir wieder in den Rendermodus.Aber diesmal gibt der glRender Befehl die Anzahl der Objekttreffer zurück, die wir für spätere Nutzung speichern.
Da wir jetzt also die Zahl der Treffer kennen ist es sehr einfach das vom Nutzer gewählte Objekt zu finden.Wie Sie wissen sollten, ist dies das Objekt mit dem niedrigsten Z-Wert (also das dem Betrachter nahste).
Um dies zu finden muß man erst wissen was sich im SelectionBuffer befindet.Der Puffer besteht aus Trefferrecords und ein solcher besteht aus folgenden Teilen :

- Anzahl der Namen auf dem Stack zum Zeitpunkt des Treffers
- Kleinste z-Koordinate aller Eckpunkte des gewählten Objekts
- Größte z-Koordinate aller Eckpunkte des gewählten Objekts
- Name des getroffenen Objekts

Die einzigen benötigten Einträge sind der zweite und der letzte.Auf diese kann mit SelectBuffer[(n*4)+1] und SelectBuffer[(n*4)+3] zugegriffen werden, wobei n der gerade bearbeitet Treffer ist.Genau dies tut obige Prozedur in der am Ende gestarteten Schleife.Wenn die z-Koordinate des Treffers niedriger ist als die gespeicherte, wird sie durch selbige ersetzt.Außerdem wird gleichzeitig der Name des Treffers gespeichert.
Wenn die Schleife durchlaufen ist haben wir also den Namen des Objektes, das sich unter dem Mauszeiger befand!
Das Beispielprogramm
Ich habe ein kleines Beispielprogramm geschrieben, das ein winziges Universum simuliert.Beim Klick auf einen der Planeten sehen Sie im unteren Panel dessen Namen.

Download
Das Selektionsdemo (inklusive Quellcode) herunterladen

Letzte Aktualisierung : 18.06.2004