Tutorial ColorPicking Shader: Unterschied zwischen den Versionen
Waran (Diskussion | Beiträge) (Die Seite wurde neu angelegt: „=Objektselektion mit Color Picking= ==Einleitung== Jaja, schon das dritte Selektionstutorial auf DelphiGL. Aber glaubt mir wenn ich euch sage, dass es dafür au...“) |
End (Diskussion | Beiträge) (Das Pixel-Array des Mauszeigers ist jetzt WIRKLICH 9 Pixel groß und es werden WIRKLICH nur 9 Pixel eingelesen.) |
||
(17 dazwischenliegende Versionen von 4 Benutzern werden nicht angezeigt) | |||
Zeile 4: | Zeile 4: | ||
Jaja, schon das dritte Selektionstutorial auf DelphiGL. Aber glaubt mir wenn | Jaja, schon das dritte Selektionstutorial auf DelphiGL. Aber glaubt mir wenn | ||
− | ich euch sage, dass es dafür auch gute Gründe gibt. DGL | + | ich euch sage, dass es dafür auch gute Gründe gibt. DGL bot bisher leider |
nur Tutorials, die das Picking auf Basis von OpenGLs Namestack behandeln. Diese | nur Tutorials, die das Picking auf Basis von OpenGLs Namestack behandeln. Diese | ||
Methode ist aber gerade für Einsteiger ungeeignet, nicht etwa, weil sie | Methode ist aber gerade für Einsteiger ungeeignet, nicht etwa, weil sie | ||
Zeile 15: | Zeile 15: | ||
Bedenkt bitte, dass das Tutorial eher darauf ausgelegt ist, euch eine | Bedenkt bitte, dass das Tutorial eher darauf ausgelegt ist, euch eine | ||
performante, für große Projekte taugliche und vor allem zukunftssichere Methode | performante, für große Projekte taugliche und vor allem zukunftssichere Methode | ||
− | zu präsentieren. | + | zu präsentieren. Für Einsteiger mit weniger großen Ambitionen empfiehlt sich aber vorerst eher die [[Tutorial ColorPicking|shaderlose Variante]] ([http://s200005540.online.de/verschiedenes/colorpicking.pdf Vorabversion als PDF]), welche mit einfacheren Mitteln auskommt. |
− | |||
− | |||
− | |||
− | |||
In diesem Tutorial soll es um Color Picking, also Selektion mithilfe von Farben, | In diesem Tutorial soll es um Color Picking, also Selektion mithilfe von Farben, | ||
Zeile 44: | Zeile 40: | ||
* Simplizität | * Simplizität | ||
− | :Das Alles gibt es zum Preis eines kleinen Fragmentshaders. Mehr Aufwand ist kaum nötig, um so gut wie alle Sonderfälle abzudecken. Man kann als Fausregel festhalten: Alles was der Benutzer sieht, kann selektiert werden. Ohne Kompromisse! Pixelgenau. Ohne aufwändige Mathematik dahinter. Und alles _voll_ Hardwarebeschleunigt. Bei der shaderlosen Variante müssen sich allerdings ein paar Gedanken über die zugrundeliegende Geometrie gemacht werden (auch vom | + | :Das Alles gibt es zum Preis eines kleinen Fragmentshaders. Mehr Aufwand ist kaum nötig, um so gut wie alle Sonderfälle abzudecken. Man kann als Fausregel festhalten: Alles was der Benutzer sieht, kann selektiert werden. Ohne Kompromisse! Pixelgenau. Ohne aufwändige Mathematik dahinter. Und alles _voll_ Hardwarebeschleunigt. Bei der shaderlosen Variante müssen sich allerdings ein paar Gedanken über die zugrundeliegende Geometrie gemacht werden (auch vom Performanceaspekt her betrachtet). |
</td> | </td> | ||
Zeile 59: | Zeile 55: | ||
</tr> | </tr> | ||
</table> | </table> | ||
− | |||
==Theorie== | ==Theorie== | ||
Zeile 91: | Zeile 86: | ||
kurz das [[Tutorial glsl|GLSL-Tutorial]] auf DGL zu Gemüte führen. Prinzipiell handelt es sich | kurz das [[Tutorial glsl|GLSL-Tutorial]] auf DGL zu Gemüte führen. Prinzipiell handelt es sich | ||
um kleine Programme, die Teile der festen OpenGL-Pipeline (dazu gehört z.B. | um kleine Programme, die Teile der festen OpenGL-Pipeline (dazu gehört z.B. | ||
− | die Positionierung von Vertices und das Zeichnen oder | + | die Positionierung von Vertices und das Zeichnen oder Verwerfen von Pixeln) |
ersetzen. Shader können entweder in Assembler oder einer Sprache namens GLSL | ersetzen. Shader können entweder in Assembler oder einer Sprache namens GLSL | ||
in einer C-artigen Syntax geschrieben werden - in diesem Beispiel wird letztere | in einer C-artigen Syntax geschrieben werden - in diesem Beispiel wird letztere | ||
Zeile 141: | Zeile 136: | ||
Hier sieht es schon gleich viel interessanter aus. Der Fragmentshader bekommt | Hier sieht es schon gleich viel interessanter aus. Der Fragmentshader bekommt | ||
zwei im Programm gesetzte uniform Variablen: color und use_alpha_tex. Wie man in | zwei im Programm gesetzte uniform Variablen: color und use_alpha_tex. Wie man in | ||
− | der letzten Zeile sieht, ist der Wert dieser ominösen color- | + | der letzten Zeile sieht, ist der Wert dieser ominösen color-Variable genau das, |
was auch im Framebuffer erscheinen wird. | was auch im Framebuffer erscheinen wird. | ||
Zeile 267: | Zeile 262: | ||
Natürlich wird bei so einer einfachen Methode die Reihenfolge der | Natürlich wird bei so einer einfachen Methode die Reihenfolge der | ||
Farbkomponenten verändert. Aus RGB auf Little Endian wird BGR auf Big Endian. Da | Farbkomponenten verändert. Aus RGB auf Little Endian wird BGR auf Big Endian. Da | ||
− | wir die gefärbten Bilder allerdings | + | wir die gefärbten Bilder allerdings sowieso nicht zu Gesicht bekommen, ist es |
egal. | egal. | ||
Ja, wir werden tatsächlich nie sehen können, was während der Selektion passiert. | Ja, wir werden tatsächlich nie sehen können, was während der Selektion passiert. | ||
Zeile 273: | Zeile 268: | ||
so :3 | so :3 | ||
− | Wenn ihr den | + | Wenn ihr den {{ArchivLink|file=tut_colorpicking_src|text=restlichen Code}} analysiert werdet ihr feststellen, dass SwapBuffers |
− | nur in der Eventschleife von SDL aufgerufen wird. Wenn Render in der Select- | + | nur in der Eventschleife von SDL aufgerufen wird. Wenn Render in der Select-Prozedur aufgerufen wird, werden die Buffer nicht getauscht (und damit das |
− | |||
Bild nicht angezeigt). | Bild nicht angezeigt). | ||
Zeile 285: | Zeile 279: | ||
Sehr viel! Wie "The Winner" mir freundlicherweise mitteilte, interpretiert die | Sehr viel! Wie "The Winner" mir freundlicherweise mitteilte, interpretiert die | ||
CPU bei bitweisen Operationen das LongWord "am Stück", also vom höchstwertigen | CPU bei bitweisen Operationen das LongWord "am Stück", also vom höchstwertigen | ||
− | zum untersten Bit von Links nach Rechts | + | zum untersten Bit von Links nach Rechts. Damit bedeutet ein SHL unabhängig von |
der Plattform immer eine Multiplikation mit 2 (= Verschiebung um ein Bit in | der Plattform immer eine Multiplikation mit 2 (= Verschiebung um ein Bit in | ||
Richtung des am meisten signifikanten Bits). | Richtung des am meisten signifikanten Bits). | ||
− | |||
===Der Selektionscode=== | ===Der Selektionscode=== | ||
Zeile 313: | Zeile 306: | ||
function Select(x, y: integer): longword; | function Select(x, y: integer): longword; | ||
var | var | ||
− | PixelData: array [0.. | + | PixelData: array [0..8] of longword; |
count, i: longword; | count, i: longword; | ||
begin | begin | ||
Zeile 319: | Zeile 312: | ||
y := WINDOW_Y-y; | y := WINDOW_Y-y; | ||
if x > 0 then if x < WINDOW_X then dec(x) else dec(x,2); | if x > 0 then if x < WINDOW_X then dec(x) else dec(x,2); | ||
− | if y > 0 then if y < WINDOW_Y then dec( | + | if y > 0 then if y < WINDOW_Y then dec(y) else dec(y,2); |
</source> | </source> | ||
Zeile 374: | Zeile 367: | ||
count := 0; | count := 0; | ||
− | for i := 0 to | + | for i := 0 to 8 do |
begin | begin | ||
if PixelData[4] = PixelData[i] then inc(count); | if PixelData[4] = PixelData[i] then inc(count); | ||
Zeile 415: | Zeile 408: | ||
− | Der von mir festgesetzte Schwellenwert "4" (bedenkt dabei, dass implementations | + | Der von mir festgesetzte Schwellenwert "4" (bedenkt dabei, dass implementations bedingt |
− | + | der Pixel unter dem Mauszeiger mitgezählt wird) ist genaugenommen | |
variabel. Theoretisch würde vermutlich auch 3 reichen, aber ich gehe da gerne | variabel. Theoretisch würde vermutlich auch 3 reichen, aber ich gehe da gerne | ||
auf Nummer sicher. Der Nachteil der Sicherheit ist, das man z.B. keine Linien | auf Nummer sicher. Der Nachteil der Sicherheit ist, das man z.B. keine Linien | ||
− | mit 1px Breite selektieren kann - das | + | mit 1px Breite selektieren kann - das kleinste selektierbare Objekt hat die |
gezeichnete Größe von 2x2 Pixeln. | gezeichnete Größe von 2x2 Pixeln. | ||
Zeile 471: | Zeile 464: | ||
Buffer vom Treiber zu bekommen, wenn wir einen anfordern. Ohne Überprüfungen | Buffer vom Treiber zu bekommen, wenn wir einen anfordern. Ohne Überprüfungen | ||
und Ausweichmöglichkeit auf 24 Bit läuft es also nicht. | und Ausweichmöglichkeit auf 24 Bit läuft es also nicht. | ||
− | Daraus folgt? Genau - wir brauchen | + | Daraus folgt? Genau - wir brauchen sowieso den 24 Bit-Code für solche Härtefälle. |
In Folge dessen ist der "Einfachheitsvorteil" von 32 Bit ad absurdum geführt | In Folge dessen ist der "Einfachheitsvorteil" von 32 Bit ad absurdum geführt | ||
und wir können getrost auf 24 Bit setzen. | und wir können getrost auf 24 Bit setzen. | ||
Zeile 490: | Zeile 483: | ||
Aus diesem Grund habe ich mich dazu entschlossen, das Thema "16 Bit" im Tutorial | Aus diesem Grund habe ich mich dazu entschlossen, das Thema "16 Bit" im Tutorial | ||
nicht weiter zu behandeln. | nicht weiter zu behandeln. | ||
− | |||
==Shaderlose Variante (Anregung)== | ==Shaderlose Variante (Anregung)== | ||
Zeile 568: | Zeile 560: | ||
<i>state complete</i>. Das bedeutet, sie enthalten alle Informationen, die der | <i>state complete</i>. Das bedeutet, sie enthalten alle Informationen, die der | ||
Treiber zum zeichnen braucht und sind nicht auf externe Aufrufe wie glColor | Treiber zum zeichnen braucht und sind nicht auf externe Aufrufe wie glColor | ||
− | angewiesen - der Treiber | + | angewiesen - der Treiber kann sie also in ein internes Format umwandeln, welches |
− | ein | + | das volle Spektrum an Optimierungen und Cachetricks unterstüzt. Der Treiber |
− | + | erhält alle Daten "am Stück" und kann sich 100%ig auf das Rendern | |
− | kann sich 100%ig auf das Rendern "konzentrieren", weil der Benutzer gar nicht | + | "konzentrieren", weil der Benutzer gar nicht die Möglichkeit erhält, irgendetwas |
− | die Möglichkeit erhält, irgendetwas absurdes zu tun. | + | absurdes zu tun. |
Will man diese Art von Displaylisten ohne Shader für die Selektion fit machen, | Will man diese Art von Displaylisten ohne Shader für die Selektion fit machen, | ||
Zeile 580: | Zeile 572: | ||
Bei dieser Methode wird die benötigte Geometrie <i>verdoppelt</i> - mit anderen | Bei dieser Methode wird die benötigte Geometrie <i>verdoppelt</i> - mit anderen | ||
Worten: Sie kommt aus Speichersicht überhaupt nicht in Frage. | Worten: Sie kommt aus Speichersicht überhaupt nicht in Frage. | ||
+ | Es bringt auch nichts, die Displaylisten "unverbindlicher" zu halten und die | ||
+ | Spezifika wie Farbe in der Renderschleife festzulegen - damit würdet ihr nämlich | ||
+ | wieder beim Immediate Mode landen und hättet <i>trotz</i> Listen, die durch die | ||
+ | Mischung mit Immediate Mode nicht mehr optimierbar und damit nutzlos geworden sind, | ||
+ | nichts gewonnen. | ||
Es muss also ein Weg gefunden werden, der folgende Kriterien erfüllt: | Es muss also ein Weg gefunden werden, der folgende Kriterien erfüllt: | ||
− | + | ||
− | + | # Der Treiber muss seine Daten "am Stück" erhalten können. | |
− | + | # Selektion und normales Rendering sollen die gleiche Geometrie nutzen. | |
+ | # Trotzdem müssen die Farben austauschbar sein. | ||
Die Lösung sieht so aus, dass wir selbst ein Vertex Array (Hauptspeicherbasiert) | Die Lösung sieht so aus, dass wir selbst ein Vertex Array (Hauptspeicherbasiert) | ||
Zeile 592: | Zeile 590: | ||
Es werden drei Arrays erstellt: | Es werden drei Arrays erstellt: | ||
− | + | * Vertices | |
− | + | * Farbdaten oder Texturkoordinaten | |
− | + | * Selektionsfarben | |
In OpenGL ist es nun möglich, mittels der gl*Pointer-Funktionen die Datenquellen | In OpenGL ist es nun möglich, mittels der gl*Pointer-Funktionen die Datenquellen | ||
Zeile 606: | Zeile 604: | ||
GL_COLOR_ARRAY oder GL_TEXTURE_2D deaktiviert oder aktiviert. | GL_COLOR_ARRAY oder GL_TEXTURE_2D deaktiviert oder aktiviert. | ||
+ | Bedenkt auch, dass ihr mehrmals verwendete Vertices nicht mehrfach speichern | ||
+ | müsst. Benutzt stattdessen indizierte Arrays um den Speicherverbrauch zu senken | ||
+ | und ganz nebenbei die caching performance des Treibers zu erhöhen. | ||
==Schlusswort== | ==Schlusswort== | ||
Zeile 612: | Zeile 613: | ||
meine Implementation auf's Auge drücken lasst denkt daran, dass es nur eine | meine Implementation auf's Auge drücken lasst denkt daran, dass es nur eine | ||
Lösung von vielen ist. | Lösung von vielen ist. | ||
− | Wann immer in eurer Situation etwas anderes angebracht sein sollte | + | Wann immer in eurer Situation etwas anderes angebracht sein sollte, macht es |
anders! | anders! | ||
Es kann auch nicht schaden, wenn ihr euch den kompletten Beispielcode zum | Es kann auch nicht schaden, wenn ihr euch den kompletten Beispielcode zum | ||
− | Tutorial ( | + | Tutorial (siehe unten) mal anseht. Neben dglOpenGL und |
− | Jedi-SDL verwende ich noch | + | Jedi-SDL verwende ich noch drei Units, die von mir sind. Ich erhebe keinen |
Urheberrechtsanspruch auf diese Units, d.h. ihr könnt sie nach belieben | Urheberrechtsanspruch auf diese Units, d.h. ihr könnt sie nach belieben | ||
ausschlachten, verändern und selbst verwenden (meinetwegen auch kommerziell). | ausschlachten, verändern und selbst verwenden (meinetwegen auch kommerziell). | ||
Zeile 625: | Zeile 626: | ||
Gruß Waran | Gruß Waran | ||
+ | |||
+ | == Dateien == | ||
+ | * {{ArchivLink|file=tut_colorpicking_src|text=Quellcode des Beispieles}} | ||
+ | * {{ArchivLink|file=tut_colorpicking_exe|text=Vorkompilierte Version des Beispieles (Windows)}} | ||
+ | |||
+ | |||
+ | {{TUTORIAL_NAVIGATION|[[Tutorial Objektselektion]]|[[Tutorial TexFilter]]}} | ||
[[Kategorie:Tutorial|ColorPicking]] | [[Kategorie:Tutorial|ColorPicking]] |
Aktuelle Version vom 2. März 2013, 10:34 Uhr
Inhaltsverzeichnis
Objektselektion mit Color Picking
Einleitung
Jaja, schon das dritte Selektionstutorial auf DelphiGL. Aber glaubt mir wenn ich euch sage, dass es dafür auch gute Gründe gibt. DGL bot bisher leider nur Tutorials, die das Picking auf Basis von OpenGLs Namestack behandeln. Diese Methode ist aber gerade für Einsteiger ungeeignet, nicht etwa, weil sie unnötig kompliziert wäre, aber weil sie hoffnungslos veraltet, tendenziell inkompatibel zu nVidias Treibern (Softwaremodus!) und enorm unflexibel ist. Jedenfalls war es kein Anflug aus purer Bosheit, der die Khronos-Group dazu bewogen hat, diese Methode in OpenGL3 zu entfernen.
Ein Wort zur angestrebten Zielgruppe: Bedenkt bitte, dass das Tutorial eher darauf ausgelegt ist, euch eine performante, für große Projekte taugliche und vor allem zukunftssichere Methode zu präsentieren. Für Einsteiger mit weniger großen Ambitionen empfiehlt sich aber vorerst eher die shaderlose Variante (Vorabversion als PDF), welche mit einfacheren Mitteln auskommt.
In diesem Tutorial soll es um Color Picking, also Selektion mithilfe von Farben,
gehen. Damit ihr euch ein Bild davon machen könnt wo ihr steht, wenn ihr die
Technik beherrscht, hier ein kurzer Überblick über die Vor- und Nachteile:
Vorteile | Nachteile |
|
|
Theorie
Die Theorie hinter dem Color Picking ist ganz schnell erklärt. Man zeichnet jedes selektierbare Objekt in einer einmaligen Farbe und merkt sich, welches Objekt in welcher Farbe gezeichnet wurde. Dann wird ein Pixel unter dem Mauszeiger ausgelesen - anhand der ausgelesenen Farbe lässt sich auf das Objekt unter dem Mauszeiger zurückschließen. Das ist vielleicht momentan noch ein bisschen sehr abstrakt, sollte aber schon klarer werden, wenn ihr den Beispielcode verfolgt.
Implementierung
Organisation
Vorweg muss gesagt sein: Wirklich alles was ich in dem Beispielcode mache, ist nur eine Lösung von vielen. Wie euch durch die kurze Theorie schon dämmern sollte gibt es etliche Möglichkeiten Color Picking zu implementieren. In diesem Beispiel sieht das Ganze so aus: Die Daten der selektierbaren Objekte (Displayliste, Position und ein String) sind in einem Record zusammengefasst. Alle diese Records sind in einem Array angeordnet und werden auch per Schleife gezeichnet... exakt dieser Array-Index ist es nun, was jedem individuellen Objekt seine Identifizierbarkeit spendiert.
Der Shader
Jeder der bei Shader an böhmische Dörfer denkt, sollte sich vielleicht vorher kurz das GLSL-Tutorial auf DGL zu Gemüte führen. Prinzipiell handelt es sich um kleine Programme, die Teile der festen OpenGL-Pipeline (dazu gehört z.B. die Positionierung von Vertices und das Zeichnen oder Verwerfen von Pixeln) ersetzen. Shader können entweder in Assembler oder einer Sprache namens GLSL in einer C-artigen Syntax geschrieben werden - in diesem Beispiel wird letztere Methode verwendet, da die Assemblervariante kaum noch weiterentwickelt wird und schlechter zu lesen ist.
Sehen wir uns nun die Shader an, der vor einem Selektionsvorgang gebunden und anschließend wieder ausgeschaltet werden:
// Vertexshader
void main(void)
{
gl_Position = ftransform();
gl_TexCoord[0] = gl_MultiTexCoord0;
}
Der Vertexshader macht offensichtlich nichts (oder besser: nur) weltbewegendes.
Jedes Vertex an seine Stelle setzen und Texturkoordinaten durchschleifen... aber
da fehlt doch was? Der Vertexshader übergibt gar keine Farbdaten - d.h. wir
können schonmal festhalten, dass die aktuelle Farbe in der Statemachine komplett
ignoriert wird.
// Fragmentshader
uniform vec4 color;
uniform float use_alpha_tex;
uniform sampler2D sampler;
void main(void)
{
if (use_alpha_tex == 1.0)
if (texture2D(sampler, vec2(gl_TexCoord[0])).a < 0.2)
discard;
gl_FragColor = color;
}
Hier sieht es schon gleich viel interessanter aus. Der Fragmentshader bekommt zwei im Programm gesetzte uniform Variablen: color und use_alpha_tex. Wie man in der letzten Zeile sieht, ist der Wert dieser ominösen color-Variable genau das, was auch im Framebuffer erscheinen wird.
"use_alpha_tex" wird im Programm auf 1.0 gesetzt, wenn Texturen mit Alphakanal verwendet werden. Sollen solche Texturen verwendet werden, liest der Shader den aktuellen Alphawert und verwirft das Fragment, wenn Alpha unter dem Schwellenwert 0.2 liegt. Es handelt sich also nur um einen handgemachten Alphatest.
Dabei sei angemerkt, dass dies kein sonderlich stilvolles Vorgehen ist: Man versucht möglichst, einen Shader nur auf eine ganz eng umrissene Aufgabe hin zu gestalten - da haben solche Abfragen nichts zu suchen. Es dient in diesem Tutorial aber der Übersichtlichkeit, also werden wir mal beide Augen zudrücken.
Zusammenfassend kann man also sagen, dass wenn der Shader aktiviert ist, jedes einzelne gezeichnete Pixel eine von uns im Hauptprogramm festgelegte Farbe bekommt. Zusätzlich hat der Shader die Fähigkeit, bei Texturen mit Alphakanal "unsichtbare" Pixel zu ignorieren und damit den Hintergrund durchscheinen zu lassen.
Tipp: Objekte mit Textur ohne Alphakanal zählen bei der Selektion wie Objekte ganz ohne Textur, da es sich letztendlich nur um ein Quad handelt, das in einer Einheitsfarbe eingefärbt werden soll.
Die Renderschleife
Sehen wir uns jetzt einmal den Code an, bei dem wirklich etwas gezeichnet wird.
procedure Render;
const
fac: single = 1/255;
var
i: longword;
iv: array [0..3] of byte absolute i; // iv und i überschneiden sich
Im Kopf der Prozedur sieht man schon erste Vorbereitungen auf das, was gleich während der Schleife passieren wird. Durch das absolute-Schlüsselwort überlappen i und iv im Speicher. Rein praktisch bedeutet das, dass man über iv auf die einzelnen Bytes der for-Variable i zugreifen kann, als wäre sie ein Byte-Array.
Durch Multiplikation mit der Konstante fac wird ein Byte-Wert (0..255) in einen Float im Bereich (0..1) umgerechnet. Dies ist notwendig, da die Grafikkarte intern mit Floats arbeitet und man daher nur Floats übergeben kann (auf SM 4.0 Karten funktioniert auch Integer, ist aber sehr unperformant). Das nächste Häppchen:
begin
glClear(GL_COLOR_BUFFER_BIT);
// Das Erste Model soll texturiert werden
glEnable(GL_TEXTURE_2D);
glUniform1f(SelShader.use_alpha_tex, 1.0);
for i := 1 to high(Objekte) do
begin
if i = 2 then begin // Ab dem zweiten nicht mehr
glDisable(GL_TEXTURE_2D);
glUniform1f(SelShader.use_alpha_tex, 0.0);
end;
Offensichtlich sind unsere Objekte im Array so angeordnet, dass zuerst texturierte Objekte (wir setzen use_alpha_tex auf 1.0 und aktivieren TEXTURE_2D) gezeichnet werden sollen. Diese Art von Sortierung macht Sinn, da Statechanges auf jeden Fall zu vermeiden sind, wenn sie vermeidbar sind. Gemeint ist mit Sortierung nicht, dass Alphatexturen zuerst kommen müssen, das ist manchmal auch gar nicht möglich, sondern das nicht mitten in der Schleife zwischen texturiert und nicht texturiert gewechselt werden muss. Bei dem zweiten Durchlauf wird die Texturierung deaktiviert und es geht ohne weiter (ergo: Es gibt nur ein texturiertes Objekt). Der interessante Teil der Renderfunktion folgt:
// Den Index in eine Farbe umrechnen und im Shader setzen
{$ifdef ENDIAN_BIG}
glUniform4f(SelShader.color, iv[1]*fac, iv[2]*fac, iv[3]*fac, 1.0);
{$else}
glUniform4f(SelShader.color, iv[0]*fac, iv[1]*fac, iv[2]*fac, 1.0);
{$endif}
// Zeichnen
glLoadIdentity;
glTranslatef(Objekte[i].posx, Objekte[i].posy, 0);
glCallList(Objekte[i].displayliste);
end;
end;
Wie versprochen werden die 3 untersten (ich sage bewusst nicht "ersten") Bytes einzeln in Floats umgerechnet und landen in color.r, color.g und color.b des Shaders. color.a wird nicht verwendet und auf 1.0 gesetzt (1.0 ist der übliche Standardwert für Alpha). Mit dem so präparierten Shader würde nun gezeichnet werden, wäre er aktiviert.
Die berechtigte Frage ist jetzt: Warum gibt es bei der Index-Umrechnung zwei verschiedene Varianten? Der Grund ist die sogenannte Endianess oder Byte Order. Bei Little-Endian-Systemen (z.B. x86) werden die Bytes mit höherer Adresse signifikanter, bei Big Endian (z.B. PowerPC, Playstation 3) ist es genau umgekehrt.
Da wir nur 24 Bit (= 3 Byte) des Farbpuffers benutzen, interessiert uns das signifikanteste Byte des DWords nicht. Da dieses auf Big-Endian-Systemen gerade iv[0] ist, müssen die Offsets verschoben werden. Die folgende Grafik verdeutlicht dies:
Natürlich wird bei so einer einfachen Methode die Reihenfolge der
Farbkomponenten verändert. Aus RGB auf Little Endian wird BGR auf Big Endian. Da
wir die gefärbten Bilder allerdings sowieso nicht zu Gesicht bekommen, ist es
egal.
Ja, wir werden tatsächlich nie sehen können, was während der Selektion passiert.
Oder hat jemand bis jetzt ein SwapBuffers ausmachen können? Nein? Gut, das muss
so :3
Wenn ihr den restlichen Code analysiert werdet ihr feststellen, dass SwapBuffers nur in der Eventschleife von SDL aufgerufen wird. Wenn Render in der Select-Prozedur aufgerufen wird, werden die Buffer nicht getauscht (und damit das Bild nicht angezeigt).
Erratum: Wie ihr seht wurde die Grafik von mir korrigiert. Ich habe mich entschlossen den Fehler nur durchzustreichen, damit ihr davon lernen könnt und ihn nicht wiederholt: Es handelt sich bei dieser Vorgehensweise NICHT um eine BITweise Verschiebung nach links (SHL 8), sondern um eine BYTEweise. Was macht das schon aus? Sehr viel! Wie "The Winner" mir freundlicherweise mitteilte, interpretiert die CPU bei bitweisen Operationen das LongWord "am Stück", also vom höchstwertigen zum untersten Bit von Links nach Rechts. Damit bedeutet ein SHL unabhängig von der Plattform immer eine Multiplikation mit 2 (= Verschiebung um ein Bit in Richtung des am meisten signifikanten Bits).
Der Selektionscode
Ich hoffe ihr wurdet von dem Bit- und Bytegeschubse noch nicht gänzlich frustiert und überlegt, den Rest des Tutorials erstmal beiseite zu legen. Im Selektionscode (folgt) kommt noch ein bisschen mehr davon ^_^ Bevor ich euch allerdings den Code gebe, solltet ihr erstmal wissen, was bei der Selektion genau gemacht wird. Anders als im kurzen Theorieteil besprochen lesen wir nicht nur einen Pixel, sondern eine 3x3-Box (9 Pixel) um den Mauszeiger herum aus. Das tun wir, um die Umgebung des Pixels zu untersuchen und damit Fehler durch Antialiasing sehr effektiv zu erkennen. Das Problem mit AA ist, dass es Mischfarben erzeugt: Das bedeutet wann man auf den Randbereich eines Objektes klickt, kann es sein, dass man nicht die Farbe des Objektes erwischt, sondern _irgend eine_ Mischfarbe.
Wird diese kritiklos wieder als Index interpretiert landen wir irgendwo im Array und im besten Fall stürzt das Programm ab (im schlimmsten Fall passiert nichts und der Fehler bleibt uns bis zur Veröffentlichung erhalten).
function Select(x, y: integer): longword;
var
PixelData: array [0..8] of longword;
count, i: longword;
begin
// Koordinaten umrechnen (SDL-Fenster in OpenGL / 3x3-Box zentrieren)
y := WINDOW_Y-y;
if x > 0 then if x < WINDOW_X then dec(x) else dec(x,2);
if y > 0 then if y < WINDOW_Y then dec(y) else dec(y,2);
Zuerst wird der "Mauszeiger" (eigentlich nur die Koordinaten, die wir bekommen haben) von Fensterpixeln (Ursprung oben) in OpenGL-Pixel (Ursprung unten) umgerechnet und wenn möglich nach unten links verschoben. Das verschieben sorgt dafür, dass die angeklickte Stelle genau in der Mitte der gleich ausgelesenen 3x3-Box ist.
glUseProgram(SelShader.prog);
glClearColor(0.0, 0.0, 0.0, 0.0);
glEnable(GL_SCISSOR_TEST);
glScissor(x, y, 3, 3); // Die wenigsten Pixel müssen in den Buffer
Render;
// Kein SwapBuffers. Das codierte Bild sieht eh nicht hübsch aus.
glUseProgram(0);
glClearColor(0.1, 0.1, 0.1, 1.0);
glDisable(GL_SCISSOR_TEST);
Das Zeichnen sollte eigentlich selbsterklärend sein. Der Shader wird aktiviert, die Hintergrundfarbe auf (0 0 0 0) gesetzt (entspricht dem Index 0!) und der Scissor Test aktiviert. Der Test macht bei Selektion sehr viel Sinn, da wir uns ja eigentlich nur für 9 Pixel interessieren - warum dann alle im meinem Fall 786.432 Pixel in den Framebuffer schreiben? In Anbetracht dessen, dass Scissor so gut wie kostenlos ist, wäre das eine unverzeiliche Sünde. Was der Test allerdings nicht verhindert ist, dass für jedes Pixel der Fragmentshader abgearbeitet werden muss - seht es also nicht als Freibrief für den Uber-Shader aus if-Abfragen.
Aufgerufen wird die normale Render-Prozedur, die wir so angepasst haben, dass sie dem Shader, wenn er gebunden ist, die relevanten Daten zukommen lässt. Nehmt das als Anreiz euer Rendering bei großen Szenen gut zu optimieren (z.B. mit Octrees/BSP und Frustrum Culling) - eure Selektion profitiert genauso davon. Nach dem Rendering wird Alles zurückgesetzt und erhält damit wieder seine vorherige Ordnung.
Jetzt kommt endlich der wirklich interessante Teil.
glReadPixels(x, y, 3, 3, GL_RGBA, GL_UNSIGNED_BYTE, @PixelData[0]);
count := 0;
for i := 0 to 8 do
begin
if PixelData[4] = PixelData[i] then inc(count);
if count > 3 then
begin
{$ifdef ENDIAN_BIG}
result := (PixelData[4] shr 8)
{$else}
result := PixelData[4] and $FFFFFF;
{$endif}
exit;
end;
end;
// Wenn diese Stelle erreicht wird, hat die Schleife nicht mehr als 3
// gleichfarbige Pixel zusammenbekommen. Selektion ungültig.
result := 0;
end;
Interessant, aber es bleibt weiterhin einfach (ich hoffe es war einfach, bis jetzt ^^). Aus Vogelperspektive: Es werden 9 RGBA-Werte aus dem Framebuffer geholt - Alpha wird immer 255, da wird nur 24 Bit benutzen (Standardwert von OpenGL) - und in 9 DWords geschrieben. Schließlich wird gezählt, wie viele Pixel mit PixelData[4] (das ist die Mitte, der Mauszeigerpixel) identisch sind. Wenn es mindestens 4 Pixel sind, wird die Selektion für gültig erklärt - d.h. der Mauszeigerpixel wird in result geschrieben und die Funktion kehrt zurück. Wenn die Funktion in der Schleife noch nicht verlassen wurde, wird result auf 0 gesetzt.
Hier ein Beispiel in dem gezeigt wird, wie uns diesen Vorgehen hilft, Fehler zu Erkennen:
Der von mir festgesetzte Schwellenwert "4" (bedenkt dabei, dass implementations bedingt
der Pixel unter dem Mauszeiger mitgezählt wird) ist genaugenommen
variabel. Theoretisch würde vermutlich auch 3 reichen, aber ich gehe da gerne
auf Nummer sicher. Der Nachteil der Sicherheit ist, das man z.B. keine Linien
mit 1px Breite selektieren kann - das kleinste selektierbare Objekt hat die
gezeichnete Größe von 2x2 Pixeln.
Wie ihr seht, muss das LongWord vor der Rückgabe noch bearbeitet werden: Bei LE reicht es, den Alpha-Wert per Bitmaske abzuschneiden. Bei Big Endian landet der Alpha-Wert (glaube ich!) im untersten Byte. Daher muss in Richtung "unten" verschoben werden, damit der Alpha-Wert entfernt und unser Pixel-Index im unteren Bereich landet - schaut euch ruhig nochmal die Endian-Grafik an, wenn ihr jetzt nur Bahnhof versteht. Ich kann den Big Endian-Code leider mangels Maschine nicht testen. Sollte er also auf die Art nicht funktionieren, sofort in den IRC kommen oder mir eine PM schreiben (so kommt man auch an sein wohlverdientes Feedback).
Und damit sind wir mit dem Thema durch. Was jetzt noch kommt ist lediglich für den Spezialfall interessant, wenn man mit 32-Bit selektieren will... kann ja sein, dass man die Bitschubserei satt hat oder 16.777.215 codierbare Objekte neben dem Hintergrund nicht genug sind :-)
Ergänzung: 32/16 Bit Farbtiefe
32 Bit sind eigentlich leichter handzuhaben, als 24 Bit. Das kommt daher, dass ein echter 32-Bit-Wert aus dem Framebuffer genau unserem (und eurem, gemeint sind die BEler) nativen LongWord entspricht und damit die ganze Bitschubserei inklusive Unterscheidung zwischen BE und LE überflüssig macht:
glUniform4f(SelShader.color, iv[0]*fac, iv[1]*fac, iv[2]*fac, iv[3]*fac);
if count > 3 then
begin
result := PixelData[4];
exit;
end;
Dazu kommt noch, dass wir auf einmal Raum für 4.294.967.295 Objekte haben, was definitiv mehr ist, als man jemals ausschöpfen könnte - an unsere mitzeitreisdenden Leser: Das Tutorial stammt von 2009, das war damals so ;-)
Der Nachteil an 32 Bit zur Selektion (ja, jetzt wird es nochmal kurz ernst) ist, dass wir uns erstmal nicht darauf verlassen können, auch wirklich einen 32 Bit Buffer vom Treiber zu bekommen, wenn wir einen anfordern. Ohne Überprüfungen und Ausweichmöglichkeit auf 24 Bit läuft es also nicht. Daraus folgt? Genau - wir brauchen sowieso den 24 Bit-Code für solche Härtefälle. In Folge dessen ist der "Einfachheitsvorteil" von 32 Bit ad absurdum geführt und wir können getrost auf 24 Bit setzen.
Ein noch gravierenderer Nachteil ist, dass wir nun auch auf OpenGL-Funktionen aufpassen müssen, die den Alphawert verwenden. Wenn beispielsweise Alphatest während der Selektion aktiviert ist, werden selbst mit einer kulanten Einstellung wie (GL_GREATER, 0.0) alle Selektionen von Objekten mit Alpha=0 verworfen. Das sind auf Little Endian die ersten 16.777.215 und bei Big Endian jedes 256ste.
Erlaubt der Treiber die Verwendung von lediglich 16 Bit, so wären noch 65535 Objekte selektierbar. Das ist zwar in der Regel ebenfalls mehr als genug, aber leider erfordert 16 Bit aufgrund unterschiedlicher Bitlängen der Farbkomponenten (R: 5, G: 6, B: 5) einen enormen Aufwand bei der Zerlegung/Zusammensetzung des Index in/aus Farben, der nicht ohne massive Verwendung von Bitmasken und -shifts auskommt. Aus diesem Grund habe ich mich dazu entschlossen, das Thema "16 Bit" im Tutorial nicht weiter zu behandeln.
Shaderlose Variante (Anregung)
Einleitung
In den letzten Kapiteln wurde die Implementierung mit Shadern besprochen. Diese bieten, wie schon am Anfang kurz in den Vorteilen angerissen, ein Maximum an Flexilibität und Speichereffizienz (da komplett unabhängig von der Geometrie) bei unbeeinträchtigter Performance. Im Prinzip eine Win-Win-Situation, könnte man meinen... leider gibt es aber auch noch Hardware, auf der keine Shader laufen. In diesem Kapitel soll es also darum gehen, euch eine Übersicht darüber zu geben, was in einer solchen Situation machbar ist - inklusive Bewertung im Kontext der angestrebten Zielgruppe (siehe "Einleitung"). Implementieren müsst ihr es dann aber selbst. Wenn euch die Shaderlose Variante nicht weiter interessiert, könnt ihr auch problemlos zum Schluss übergehen - ihr verpasst nichts mehr.
Wesentlich schwieriger sind ohne Shader allerdings Spezialeffekte wie transparente Texturen. Eine Lösung sieht so aus, dass ihr zunächst das texturierte Objekt mit aktiviertem Alphatest in eine Stencil-Maske zeichnet. Danach wird in einem zweiten Pass ein Quad mit der Indexfarbe gezeichnet - durch die Maske werden nur solche Pixel geschrieben, die vorher den Alphatest bestanden und damit die Stencilmaske geformt haben. Nun aber zum grundlegenden Thema:
Prinzipiell stehen euch ohne Shader zwei Möglichkeiten offen: 1) Einstreuen von Immediate Mode in eure Renderschleife oder 2) Anpassung der Geometrie.
Immediate Mode
Möglichkeit 1 sieht in Pseudocode folgendermaßen aus:
procedure Render(mode)
if mode = rendermode
Texturen aktivieren
Lighting aktivieren
usw.
if mode = selection
Texturen deaktiveren
Lighting deaktivieren
usw.
foreach i in models
if mode = rendermode
Normale Farbe[i] setzen oder Textur binden
if mode = selection
glColor3ubv(@i) // nur Little Endian...
Farblose(!) Geometrie[i] zeichnen
Der Vorteil wird schnell ersichtlich: Die Methode bleibt flexibel und einfach handzuhaben - bei gleichzeitig guter Speichereffizienz. Was auf diese Art aber leider niemals erreicht wird ist maximale Performance. Durch die Verwendung von Immediate Mode wird dem Treiber jede Möglichkeit geraubt, den Rendervorgang zu optimieren. Gleichzeitig kann der Treiber auch keine Daten cachen (und ggf. wiederverwenden) sondern muss im Gegenteil noch Leistung dafür verbrauchen, jederzeit auf jeden Fehltritt von euch vorbereitet zu sein. Die Methode stößt also schnell an ihre Grenzen, wenn große Mengen von Daten verarbeitet werden sollen.
Vertex Array / VBO
Betrachtet man Möglichkeit 2 genauer, stellt man fest, dass es auch hier wieder mehrere Optionen gibt. Die Displaylisten, die im Shaderteil verwendet werden, sind state complete. Das bedeutet, sie enthalten alle Informationen, die der Treiber zum zeichnen braucht und sind nicht auf externe Aufrufe wie glColor angewiesen - der Treiber kann sie also in ein internes Format umwandeln, welches das volle Spektrum an Optimierungen und Cachetricks unterstüzt. Der Treiber erhält alle Daten "am Stück" und kann sich 100%ig auf das Rendern "konzentrieren", weil der Benutzer gar nicht die Möglichkeit erhält, irgendetwas absurdes zu tun.
Will man diese Art von Displaylisten ohne Shader für die Selektion fit machen, benötigt man auf der einen Seite Listen mit den "normalen Farben" (welche auch schlichtweg "keine Farben" sein können, wenn nur Texturen verwendet werden) und Listen mit eingebauten Selektionsfarben. Bei dieser Methode wird die benötigte Geometrie verdoppelt - mit anderen Worten: Sie kommt aus Speichersicht überhaupt nicht in Frage. Es bringt auch nichts, die Displaylisten "unverbindlicher" zu halten und die Spezifika wie Farbe in der Renderschleife festzulegen - damit würdet ihr nämlich wieder beim Immediate Mode landen und hättet trotz Listen, die durch die Mischung mit Immediate Mode nicht mehr optimierbar und damit nutzlos geworden sind, nichts gewonnen.
Es muss also ein Weg gefunden werden, der folgende Kriterien erfüllt:
- Der Treiber muss seine Daten "am Stück" erhalten können.
- Selektion und normales Rendering sollen die gleiche Geometrie nutzen.
- Trotzdem müssen die Farben austauschbar sein.
Die Lösung sieht so aus, dass wir selbst ein Vertex Array (Hauptspeicherbasiert) oder ein Vertex Buffer Object (VRAM-Basiert) erstellen. Der Einfachheit halber stelle ich hier nur die Variante mit den VAs vor - es sollte sich aber ziemlich gut auf VBOs übertragen lassen.
Es werden drei Arrays erstellt:
- Vertices
- Farbdaten oder Texturkoordinaten
- Selektionsfarben
In OpenGL ist es nun möglich, mittels der gl*Pointer-Funktionen die Datenquellen für einen Aufruf von glDrawArrays zu bestimmten. glVertexPointer zeigt stets auf unsere Vertices.
glColorPointer wird je nach dem, ob es sich um eine Selektion oder ein normales Rendering handelt, auf die Farbdaten oder die Selektionsfarben gesetzt. Werden nur Texturen verwendet, bleibt glTexCoordPointer immer auf diese gesetzt, der Farbpointer immer auf die Selektionsfarben - und je nach Fall werden dann GL_COLOR_ARRAY oder GL_TEXTURE_2D deaktiviert oder aktiviert.
Bedenkt auch, dass ihr mehrmals verwendete Vertices nicht mehrfach speichern müsst. Benutzt stattdessen indizierte Arrays um den Speicherverbrauch zu senken und ganz nebenbei die caching performance des Treibers zu erhöhen.
Schlusswort
Das war jetzt Color Picking. Ich sage es aber gerne nochmal: Bevor ihr euch meine Implementation auf's Auge drücken lasst denkt daran, dass es nur eine Lösung von vielen ist. Wann immer in eurer Situation etwas anderes angebracht sein sollte, macht es anders! Es kann auch nicht schaden, wenn ihr euch den kompletten Beispielcode zum Tutorial (siehe unten) mal anseht. Neben dglOpenGL und Jedi-SDL verwende ich noch drei Units, die von mir sind. Ich erhebe keinen Urheberrechtsanspruch auf diese Units, d.h. ihr könnt sie nach belieben ausschlachten, verändern und selbst verwenden (meinetwegen auch kommerziell).
Wenn ihr eine Frage oder Feedback zum Tutorial oder zum Verfahren habt, findet ihr mich öfters im IRC-Channel von DGL oder erreicht mich bei Fragen von öffentlichem Interesse auch im Forum.
Gruß Waran
Dateien
|
||
Vorhergehendes Tutorial: Tutorial Objektselektion |
Nächstes Tutorial: Tutorial TexFilter |
|
Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com. Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen. |