Tutorial Selection

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Objektselektion

Einleitung

Hi Leute! 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...

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!

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.

OpenGL und der Name-Stack

Etwas (aber nur ganz wenig) Theorie vorweg. 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:

const
  dreieck : 1;
  viereck : 2;
  stern   : 3;
  namen   : array[-1..3] of string = ('nichts', '', 'das Dreieck', 'das Viereck', 'den Stern');


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.

Damit euch das ganze etwas klarer wird, stelle ich euch nun erst mal vor, was ich heute mit euch machen will:

Tutorial selection 01.jpg

Dieses nun wirklich einfache Programm hat folgende Funktion: 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.
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:

procedure render;
begin
  glMatrixMode(GL_MODELVIEW);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT); //Farb und Tiefenpuffer löschen
  glLoadIdentity;
  glTranslatef(0,0,-6);

  glInitNames;
  glPushName(0);

  glColor3f(1,0,0);
  glLoadName(dreieck);
  glBegin(gl_triangles);
    glVertex3f(-0.5,1,0);
    glVertex3f(0.5,1,0);
    glVertex3f(0,2,0);
  glEnd;

  glColor3f(0,1,0);
  glLoadName(viereck);
  glBegin(gl_quads);
    glVertex3f(-1,0,0);
    glVertex3f(-2,0,0);
    glVertex3f(-2,-1,0);
    glVertex3f(-1,-1,0);
  glEnd;

  glColor3f(1,1,0);
  glLoadName(stern);
  glBegin(gl_triangles);
    glVertex3f(1,0,0);
    glVertex3f(2,0,0);
    glVertex3f(1.5,-1,0);

    glVertex3f(1,-0.65,0);
    glVertex3f(2,-0.65,0);
    glVertex3f(1.5,0.35,0);
  glEnd;

  SwapBuffers(form1.myDC);                             //scene ausgeben
end;

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 :-)

Die nächsten markierten Zeilen sind "glInitNames;" und "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 ;-)

Der Befehl "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 "glLoadName" aufgerufen wird... ist doch easy, oder?

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.

function Selection : integer;
var
  Puffer       : array[0..256] of GLUInt;
  Viewport     : TGLVectori4;
  Treffer,i    : Integer;
  Z_Wert       : GLUInt;
  Getroffen    : GLUInt;
begin
  glGetIntegerv(GL_VIEWPORT, @viewport);      //Die Sicht speichern
  glSelectBuffer(256, @Puffer);               //Den Puffer zuordnen

  glMatrixMode(GL_PROJECTION);                //In den Projektionsmodus
  glRenderMode(GL_SELECT);                    //In den Selectionsmodus schalten
  glPushMatrix;                               //Um unsere Matrix zu sichern
  glLoadIdentity;                             //Und dieselbige wieder zurückzusetzen

  gluPickMatrix(xs, viewport[3]-ys, 1.0, 1.0, viewport);
  gluPerspective(60.0, Viewport[2]/Viewport[3], 1, 256);
 
  render;                                     //Die Szene zeichnen
  glMatrixMode(GL_PROJECTION);                //Wieder in den Projektionsmodus
  glPopMatrix;                                //und unsere alte Matrix wiederherzustellen

  treffer := glRenderMode(GL_RENDER);         //Anzahl der Treffer auslesen

  Getroffen := High(GLUInt);                  //Höchsten möglichen Wert annehmen
  Z_Wert := High(GLUInt);                     //Höchsten Z - Wert
  for i := 0 to Treffer-1 do
    if Puffer[(i*4)+1] < Z_Wert then
    begin
      getroffen       := Puffer[(i*4)+3];
      Z_Wert := Puffer[(i*4)+1];
    end;

  if getroffen=High(GLUInt) 
    then Result := -1
    else Result := getroffen;
end;


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 "Puffer" ist der eigentliche Selektions-Puffer. In ihm werden die Ergebnisse der Selektionsprüfung gespeichert. Das Array "Viewport" speichert quasi unsere View, da die ja auch irgendwie wichtig ist ;-) Die Variable "Treffer" ist ein reiner Integerwert, der die Anzahl der Treffer aufnimmt und die Variable "Z_Wert" nimmt die Tiefendaten auf.

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.
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: Für jedes getroffene Objekt werden immer 4 Werte gespeichert. Und zwar

  1. Anzahl der Namen auf dem Stack
  2. Kleinster Z-Wert des getroffenen Objektes
  3. Größter Z-Wert des getroffenen Objektes
  4. Name des Objektes

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 "gluPickMatrix(xs, viewport[3]-ys, 1.0, 1.0, @viewport);". In dieser stehen "xs" bzw. "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.
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).
Mit der Zeile "treffer := 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 "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.
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.

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*

Zu guter Letzt müssen wir nun noch diese Prozedur aufrufen, wozu ich das "onMouseDown" - Ereignis des Formulars verwendet habe:

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  xs := x;
  ys := y;

  statusbar1.Simpletext := 'Sie haben auf ' + namen[selection] + ' geklickt!';
end;


Ich denke, es besteht kaum Erklärungsbedarf...

Geht's auch größer?

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

Tutorial selection 02.jpg
Tutorial selection 03.jpg
Licht Aus Licht An (siehe Hintergrund)

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:

Tutorial selection 04.jpg

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. Des weiteren könnte es für euch noch nützlich sein, zu wissen, dass ich für die Lightmaps den Blendmodus "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:

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
xdis : double;
ydis : double;
begin
xdis := my_x+4;
ydis := my_y-3.85;

xs := x;
ys := y;

if selection=1 then
begin
 if sqrt(xdis*xdis + ydis*ydis) <= 3 then
 lichtan := not lichtan;
end;

end;

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

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.

Nachwort

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 ;-) Bis denne

DCW_Mr_T


Nachtrag

Weitere Infos/Beispiele im Artikel "Selektion".

Dateien



Vorhergehendes Tutorial:
Tutorial Renderpass
Nächstes Tutorial:
Tutorial Objektselektion

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