Tutorial Renderpass

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Renderpass - Die Welt daneben

Vorwort

Greetings!

Nach einer längeren Pause gibt es dann auch mal wieder eine Kleinigkeit von mir. Ich setze an dieser Stelle nicht die bisherige Tutorial-Serie fort, sondern mache diesmal etwas zu einem Spezial-Thema. Mir war gerade privat danach, mit einer anderen Technik rumzuspielen und da dachte ich, ich könnte gleich ein Tutorial drauß machen. Vor allem, weil hier eine Grundlage vermittelt wird, die unter anderem auch dafür eingesetzt werden kann, Objekte zu spiegeln oder für sie einen Schatten zu erzeugen. So kompliziert werden wir das Ganze aber heute gar nicht machen, sondern ich werde nur ein Beispiel geben, wie man mit Hilfe von mehren Render-Durchgängen eine Szene in Echtzeit rendert und auf einen Bildschirm projizieren kann.

Vielleicht kennen einige ja noch die etwas älteren Spiele. Man ist an eine Kamera gegangen und konnte plötzlich das Geschehen auf einem anderen Teil der Karte verfolgen. Der Trick dabei bestand darin, dass man damals die Spielersicht nicht mehr berechnen mußte, sondern nur die Kamera an einen anderen Ort setzte. Dank der modernen Technik können wir jedoch direkt im Spiel eine Szene sehen, die in Echtzeit berechnet wird und irgendwo anders geschieht. Man stelle sich vor, dass man bei einem Auto-Rennen durch eine Stadt fährt und am Himmel ist ein Zeppelin mit einer Holo-Wand. Darauf wird gerade die Ziellinie gezeigt, die die Konkurrenz gerade überfährt! Oder einen Weltraum-Shooter, in dem man gerade einen Angriff auf den Feind fliegt und in dem plötzlich die Warnlampen angehen, ein kleines HUD-Fenster aufspringt und man in Echtzeit verfolgen kann, wie die Wärme-Rakete einem den Antrieb zerfetzt ... ahem *hust* Irgendwie alles nur negative Beispiele, das liegt an mir, nicht an der Technik ;D Viel Spaß beim "Spielen" ;))

Multiple Render-Passes

Die Welt, die neben unserer liegt

Zunächst sollten wir erstmal klären, was eigentlich ein Render-Pass ist. Moderne Spiele wie Doom3 versprechen bis zu 50 Render-Passes. Damit sind keineswegs Frames gemeint, sondern wie oft eine Szene gerendert wird, bevor sie letztendlich auf den Bildschirm gebracht wird. Was? Wieso sollte man denn eine Szene mehrfach rendern? Nun, dafür kann es unterschiedliche Gründe geben. Nehmen wir doch einfach mal an, wir wollen Multi-Texturing auf einem Rechner mit nur einer Textur-Unit realisieren. Folglich müssen wir zweimal ein Quad mit unterschiedlichen Texturen und Blending zeichnen, um das Ergebnis zu erzielen, welches wir wollen. Genauso kann es sein, dass wir auch andere Dinge mehrfach rendern müssen, um den Effekt zu erzielen, den wir auch wollen. Bei jedem dieser Vorgänge redet man von einem Pass (Durchgang). Man sollte klar dazu sagen, dass jeder zusätzliche Durchgang tödlich für die Geschwindigkeit ist und auch vermieden werden sollte! Wenn es geht...

Aber nun nehmen wir doch mal an, wir wollen viele Bäume rendern, die in der Ferne stehen. Es bietet sich dann an, ein 3D-Modell aus der gebrauchten Perspektive auf eine Textur zu rendern und dann nur noch Quads mit der Textur zu rendern. Ein Quad hat in jedem Fall weniger Polygone als mehrere Bäume und auf die Distanz fällt das nicht mehr auf! Man redet hier übrigens von "Impostern", einem modernen Billboarding-Verfahren, auf das wir sicher aber auch nochmal irgendwann zurückkommen. Wir befassen uns erstmal mit etwas Simplerem ;)

Die Welt ist eine (Matt-)Schreibe

Stellen wir uns einfach mal eine Szene vor, bei der wir dies gebrauchen könnten! In der Mitte der Szene befindet sich ein Objekt, welches sich bewegt. In unserem Fall habe ich die beliebte Pyramiden-Szene genommen ;) Jeweils links und rechts sind leicht schräg zwei Monitore. Genau in der Mitte leicht oberhalb befindet sich ein weiterer. Auf den Bildschirmen soll nun jeweils die Szene angezeigt werden, und zwar so, wie der Betrachter es auch vor einem Bildschirm sieht. Ich denke, bei dieser Aufgabenstellung würden die meisten schon aussteigen, da zahlreiche Probleme auf einen warten. Die Lösung ist jedoch verblüffend einfach!

Damit sich jeder was drunter vorstellen kann, einmal ein kleines Bild der Szene:

Tutorial Renderpass whitescreen.gif

Die weißen Flächen sind jeweils unsere Screens. Sehen erschreckend billig aus, gell? ;)

Die Frage, die sich nun stellt ist, wie wir die Szene auf diese weißen Flächen bekommen. Wie wir bereits festgestellt haben, bieten sich hierfür Texturen an, und mit simplestem UV-Mapping haben wir dann auch eine Textur drauf. Mit jeder geladenen Textur ist dies kein Problem, allerdings wollen wir ja eine Szene darauf zeichnen. Nun, dann erzeugen wir sie uns eben, und zwar direkt im Speicher. Gleich nach dem Initialisieren von OpenGL reservieren wir uns ausreichend Speicher für die neue Textur.

procedure CreateScreenTexture;
var pTexData: Pointer;
begin
  GetMem(pTexData, 256*256*3);


  // Textur generieren und drauf zeigen
  glGenTextures(1, @Screen);
  glBindTexture(GL_TEXTURE_2D, Screen);

  // Daten in den Speicher, Lineares Filtering aktivieren
  glTexImage2D(GL_TEXTURE_2D, 0, 3, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, pTexData);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

  // Freigeben
  FreeMem(pTexData);
end;

Ich denke, dass niemand hier wirklich schockiert sein sollte oder man Unmögliches erwartet. Wir erzeugen im Speicher den Platz, den wir benötigen, erzeugen eine OpenGL Texture, übergeben den reservierten Speicher als Bilddaten, aktivieren gleich noch lineares Filtern (weil’s einfach besser aussieht *g*) und lassen den Pointer dann wieder "frei". Fertig. Schon ist bei uns im Video-Speicher ein Bereich für unsere Textur reserviert und wartet darauf, von uns mißbraucht zu werden :)

The first try

Nun lautet die Frage, wie wir die Szene auf die Texture bekommen. Man kann es bereits erahnen, dass man die Szene rendern müßte und dann nicht auf dem Bildschirm, sondern auf der Textur ausgeben muß. Der Plan steht also fest! Zunächst rendern wir die Szene einmal auf eine Textur, rendern sie dann nochmal und kleben die "gewonnene" Textur auf die Screens. Zwei Renderpasses. Und wie sonst auch fangen wir erstmal mit etwas Simplem an! Setzen der glClearColor und löschen des Color- und Z-Buffers :)

  glClearColor(0.0,0.0,0.0,0);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

Soweit nichts wirklich Neues. Wir behalten im Hintergrund, dass wir für die Textur nur Speicher für 256x256 reserviert haben. Der Grund dafür leuchtet glaube ich auch ein! Viele Grafikkarten unterstützen nicht sehr große Texturen und der Speicher ist rar. Warum also für weiter entfernte Texturen soviel Speicher "verschwenden"? Allerdings werden wir in den meisten Fällen in weitaus höheren Auflösungen auf den Bildschirm rendern. Damit der ganze Bildschirm auch auf die Texture paßt und wir nicht nur einen Teil haben, müssen wir also die Ausgabe auf die Größe der Textur reduzieren. Kinderleicht:

  glViewport(0, 0, 256, 256);

Ich denke, man benötigt zum Erraten der Parameter auch keinen Hellseher. Unser Viewport (Ausgabe-Bereich) fängt bei 0/0 an und hört bei 256/256 auf. Wir beachten, dass bei OpenGL der Ursprung nicht oben links ist, sondern unten links. Ich gehe nun nicht näher darauf ein, wie genau die Szene gezeichnet wird. Wir gehen einfach davon aus, dass wir brav die Bildschirme und das Objekt in der Mitte gerendert haben:

Tutorial Renderpass viewport.gif

Und kein Aufschrei, weil noch die Texturen fehlen! Ich habe der Übersicht halber diese noch nicht eingefügt. Anstatt die Szene nun allerdings auf dem Bildschirm auszugeben, "leiten" wir den Inhalt nun auf die Textur um:

  glBindTexture(GL_TEXTURE_2D, Screen);
  glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, 256, 256, 0);

Mit glBindTexture weisen wir Screen als "aktuelle Textur" zu. Die darauf folgende Zeile sorgt dann dafür, dass wir auf Screen den momentanen Inhalt des Color-Buffers (ergo unserer Szene) schreiben. Die Parameter sollte man auch nicht weiter verändern, man wird meist damit zu recht kommen. Die beiden 256 erklären sich diesmal wohl auch von selbst.

Und ab in die "richtige" Welt

Erschreckend einfach oder? Wir haben nun unseren ersten Render-Pass hinter uns und haben die Szene auf einer Textur! Nun beginnen wir unseren zweiten Render-Pass und haben natürlich im Hinterkopf, dass der Color-Buffer noch vom ersten Durchgang voll ist! Also weg damit:

glClearColor(0,0.0,0.3,0);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);

Man sollte zur Kenntnis nehmen, dass ich als "Reinigungs-Farbe" einen leicht blauen Schwarzton nehme. Macht die Szene interessanter und man erkennt die Screens anschließend besser ;)

  glViewport(0, 0, 800, 600);

Auch diese Zeile sollte nicht vergessen werden, damit wir wieder auf voller Bildschirmbreite rendern. Es bietet sich hier natürlich evtl. an, nicht mit fixen Werten zu arbeiten, sondern jeweils die aktuelle Fensterbreite und Höhe anzugeben.

  glBindTexture(GL_TEXTURE_2D,Screen);
  RenderScreens;
  DrawPyramidScene;

Huch! Das ist alles? Yes... Wir weisen als aktuelle Textur „Screen“ an und zeichnen dann die Bildschirme. Anschließend kommt noch das Objekt in der Mitte dazu. Drawpyramids bindet übrigens seine eigenen Texturen ein, so dass nicht "Screen" verwendet wird. Und fertig ist der zweite Pass und damit auch unsere Szene. OpenGL bringt den Color-Buffer an die Wand, Verzeihung, den Screen ;)

Es kommt nur sehr darauf an, wie wir den ersten Pass durchführen. Ob wir uns mit dem Objekt in der Mitte zufrieden geben…

Tutorial Renderpass simple.jpg

…oder ob wir uns wie beim zweiten Render-Pass die Screen-Textur krallen und noch zusätzlich die Bildschirme einzeichnen. Wir erhalten dann einen verdammt interessanten rekursiven Effekt:

Tutorial Renderpass multiple.jpg

Und die Perfektion...

Damit wäre unser Ziel doch eigentlich erreicht, oder? Dachte ich mir zu diesem Zeitpunkt auch, allerdings habe ich mir noch etwas einfallen lassen. Die Szene ist eigentlich noch einen kleinen Tick zu langweilig! Man kann doch sicherlich noch ein wenig spielen?! Ich dachte mir, dass es doch ganz nett wäre, wenn man die Bildschirme abschalten könnte. Eigentlich nichts Schweres... einfach eine Noise-Textur nehmen und statt dem Screen-Bild drauf zeichnen. Allerdings würde das an sich recht langweilig aussehen, da man nur eine feste "Bildstörung" hätte. Jemand, der mal vor einem Fernseher ohne Empfang gesessen hat, wird jedoch festgestellt haben, dass wir dort durchaus eine Bewegung feststellen können. Was also tun? Einmal meinte einer zu mir ganz cool: "Hey, da nehmen wir einfach viele solcher Noise-Texturen und..." *zack* Schöner Blödsinn ;)

Wir verwenden einfach die Textur-Matrix, um die Texture so zu bewegen, dass der Eindruck entsteht, dass sie sich bewegen würde. Zunächst erhöhen wir eine Variable vor dem ersten Render-Pass:

x:=x+0.01;

Diese Variable verwenden wir dazu, dass der Bildschirm sich immer leicht von oben nach unten bewegt. Es entsteht somit die Illusion des Bildtasters, der das Signal auf die Röhre projiziert. (*hust* Seit wann haben Flachbildschirme Kathoden-Strahler *ggg*... bin echt geil im Märchen erzählen ^__-) Nun haben wir mehr eine einfache Bewegung der Textur von oben nach unten... langweilig! Das Bild soll flackern! Also werden wir chaotisch und erzeugen einfach wilde Zufallswerte, so dass die Textur sehr schnell von links nach rechts springt. Schon ergibt sich folgende Abfrage, bevor ein Screen gerendert wird:

if s1 then
  begin
    glBindTexture(GL_TEXTURE_2D,screen);
    glMatrixMode(GL_TEXTURE);
    glLoadIdentity;
    glMatrixMode(GL_MODELVIEW);
  end
  else
  begin
    glBindTexture(GL_TEXTURE_2D,noise);
    glMatrixMode(GL_TEXTURE);
    glLoadIdentity;
    gltranslate(random(100)/10,x,0);
    glMatrixMode(GL_MODELVIEW);
  end

In S1 ist gespeichert, welchen Zustand der Screen haben soll. If TRUE (an), dann nehmen wir die Screen-Textur und setzen die Textur-Matrix auf den Standard zurück; falls der Bildschirm aus sein soll, nehmen wir die Noise-Texture und verändern die Textur-Matrix so, wie wir sie brauchen. Das Ganze ist an sich nichts Neues, da dies bereits beim Matrix-Tutorial gezeigt wurde. Allerdings sieht man hier sehr schön, wie man zahlreiche KB für einen Effekt sparen kann, den man so ganz leicht "simulieren" kann ;) (sicherlich wäre es noch besser, die Textur ebenfalls im RAM zu erzeugen!)

Besonders geil hierbei: auch in der Rekursion sind die Bildstörungen enthalten ohne weitere Source! Ist ein Bildschirm aus, sieht man eh nur noch das Rauschen und keine Rekursion! Der Code ist perfekt ;D

Tutorial Renderpass noise.jpg

Nachwort

So, das war es mal wieder. Das Ganze war sicherlich nicht so ein Monster-Tutorial, wie ihr es sonst von mir gewohnt seid, aber ich denke, dass es trotzdem spannend und unterhaltsam war ;) Mir hat es zumindest sehr viel Spaß gemacht, weil man sich endlich mal kreativ auslassen konnte und ich denke, dass wir inzwischen auch ein Niveau erreicht haben, bei dem man sich den Techniken an sich widmen kann und nicht jeden einzelnen Schritt erklären muss. Laßt mich wissen, wie es Euch gefallen hat und ob ihr alle gut mitgekommen seid. Für Ideen und Kritiken stehe ich wie immer Verfügung!

Btw: Ich frage mich langsam ernsthaft, wie ich auf den Gedanken gekommen bin, dass ein futuristischer Monitor sowas wie eine Störung beim Bildaufbau haben kann. Ich meine... nicht nur technisch! Man wird doch sowas sicherlich in der Zukunft in den Griff bekommen. Interessant ist dabei nur, dass in fast jedem Sciene-Fiction-Film sowas vorkommt. Lauter digitale Screens mit Bildstörungen, die auf analoge Übertragung schließen lassen :-/ . Wer ne Antwort weiß... i-Mehl an mich ;D

Btw2: Dank an Anita für die germanistische Beratung ;)

Viel Spaß beim Experimentieren!

Euer

Phobeus

Dateien


Vorhergehendes Tutorial:
Tutorial_Abseits_eckiger_Welten
Nächstes Tutorial:
Tutorial_Selection

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