Must-have-Features moderner Computerspiele

OpenGL: Weltraumeffekte mit Motion Blur, Lens Flare & Co. [How-to]
Kommentare

Cinematografische Effekte gehören zu den Must-have-Features moderner Computerspiele. In diesem Artikel gehen wir der Frage nach, wie sich in OpenGL Motion Blur, Hitzeflimmern, Depth of Field, God Rays und Lens Flares in eine bestehende Deferred Rendering Pipeline integrieren lassen.

Die Einführung des Shader-Models 3.0 im Rahmen der DirectX-Version 9c und die Veröffentlichung der OpenGL-Spezifikation 2.0 im Jahr 2004 bereiteten das Fundament für eine regelrechte Revolution im Bereich der Echtzeitgrafikprogrammierung, die zu einer grundlegenden Änderung der 3-D-Szenendarstellung führen sollte. So genannte Deferred Rendering Pipelines ersetzten zusehends das in Game Engines bisher übliche Forward Rendering (die zeitgleiche Positionierung und Beleuchtung von 3-D-Objekten), da sie einerseits eine deutlich realistischere Beleuchtung und andererseits die Implementierung einer Vielzahl von coolen Post-Processing-Effekten, darunter Unschärfe- und Verzerrungsfilter, wie Motion Blur, Depth of Field und Hitzeflimmern oder Lichteffekte wie God Rays und Lens Flares, ermöglichten.

In bereits erschienenen Artikeln zum Thema OpenGL geht Alexander Rudolph unter anderem der Frage nach, wie sich in Weltraumspielen Planeten prozedural generieren lassen und wie man vollständige Sonnensysteme erschafft.

Große Macht bringt große Verantwortung

Spieleentwickler neigen im Eifer des Gefechts gerne mal zu Übertreibungen. In den Anfangszeiten des HDR-Renderings (HDR: High Dynamic Range) beispielsweise klagten viele Spieler über die viel zu intensiven Blend- und Glanzeffekte. Und auch heute noch ist die nachfolgende Aussage sicher nicht ganz von der Hand zu weisen: „Würde uns die reale Welt erscheinen, als entstamme sie einem Computerspiel, dann nichts wie hin zum Augenarzt!“

Verstehen Sie mich bitte nicht falsch: Mit Bedacht eingesetzte Spezialeffekte sind eine Bereicherung für jedes Computerspiel. Übertreibt man es jedoch, dann verkehrt sich ihre Wirkung schnell ins Gegenteil. Nicht nur, dass man diese grafischen Spielereien dann als störend empfindet, es gibt auch Spieler, die nach einiger Zeit sogar über Kopfschmerzen oder Übelkeit (Motion Sickness) klagen. Sowohl beim Motion-Blur-(Bewegungsunschärfe) wie auch beim Depth-of-Field-(Schärfentiefe)Effekt werden Teile des Bilds unscharf dargestellt. Die besagten gesundheitlichen Probleme sind nun das Resultat der permanent zum Scheitern verurteilten Versuche des menschlichen Auges, diese unscharfen Bildausschnitte irgendwie scharfzustellen (zu fokussieren). Und wer nun denkt, alle Spieleentwickler gingen verantwortlich mit dem Einsatz von Unschärfe- und Überblendungseffekten um, kann sich durch eine Google-Suche mit den Stichwörtern „disable motion blur“ oder „Unschärfeeffekt abschalten“ eines Besseren belehren lassen. Auch bei aktuellen Spieletiteln ist es nicht immer ganz leicht und manchmal sogar unmöglich, die Unschärfeeffekte den eigenen Vorlieben entsprechend anzupassen – was nicht zuletzt daran liegt, dass diese gerne mal missbraucht werden, um über die grafischen Unzulänglichkeiten eines Spiels bzw. der Hardware hinwegzutäuschen. Selbstverständlich wünschen wir uns alle möglichst realistische Spielewelten und enorme Weitsicht bei gleichzeitig hohen Frameraten. In der Realität müssen wir jedoch einige Abstriche in Kauf nehmen.

Natürlich kommt man als Spieleentwickler nicht umhin, den Detailgrad der Umgebung mit zunehmendem Kameraabstand nach und nach zu reduzieren, und natürlich lassen sich gelegentliche Ruckler beim Bildaufbau nicht völlig vermeiden. Wir können jedoch dafür Sorge tragen, dass der Spieler möglichst wenig davon mitbekommt. Mithilfe eines dezent eingesetzten Depth-of-Field-Effekts lässt sich beispielsweise die zunehmende Detailarmut im Hintergrund kaschieren. Jedoch darf man dem Spieler zu keinem Zeitpunkt das Gefühl vermitteln, er sei plötzlich kurzsichtig geworden. Wird das Bild bereits nach wenigen (dutzend) Metern sichtlich unscharf, dann ist die Spielfreude schnell dahin. Genau das Gleiche geschieht bei dem Versuch, die Auswirkungen einer konstant niedrigen Framerate bzw. die von kurzzeitigen Einbrüchen (Ruckler) durch Einsatz von Motion Blur abzuschwächen. Schießt man als Programmierer über das Ziel hinaus, läuft das betreffende Spiel zwar deutlich flüssiger, aber dafür wirkt die Umgebung selbst bei kleinsten Bewegungen verwaschen und unscharf.

Der permanente Einsatz des Depth-of-Field-Effekts ist generell nicht ganz unproblematisch, denn um die Schärfentiefe glaubhaft simulieren zu können, müssten wir zuvor erst einmal die korrekte Blickrichtung des Spielers ermitteln. Da dies jedoch zurzeit nicht möglich ist, bleibt nur der Griff in die Trickkiste. Nimmt man beispielsweise an, ein Spieler würde die meiste Zeit in Richtung Bildschirmmitte blicken – was natürlich nicht stimmt –, ließe sich die Kamera auf die dortigen Objekte fokussieren. Es ist jedenfalls keine zufriedenstellende Lösung (und für viele Spieler schlicht inakzeptabel), einfach den kompletten Hintergrund unscharf darzustellen.

Auch beim Einsatz von Motion Blur gibt es einiges zu beachten: Für die glaubhafte Simulation eines Geschwindigkeitsgefühls ist es notwendig, sowohl die Kamerabewegung als auch die Relativgeschwindigkeiten und Kameraabstände aller Szenenobjekte bzw. Szenenpixel zu berücksichtigen. Tunneleffekte in Rennspielen, bei denen die Objekte in der Bildschirmmitte scharf gezeichnet werden und die Ränder verwischen, wären ansonsten nicht möglich. Würde man in die Unschärfeberechnung lediglich die Kamerabewegung einbeziehen, käme es zwangsläufig zu Renderfehlern. Ein typisches Beispiel hierfür wären unscharf dargestellte Waffen, wenn der Spieler in einem Ego-Shooter seine Blickrichtung ändert. Für Spezialeffekte wie Bewegungsspuren ist es zudem erforderlich, dass die Bewegungsunschärfe für einzelne Szenenobjekte/Szenenpixel selektiv generiert werden kann.

Extreme Bildunschärfen sollten mit Bedacht und in Form von Spezialeffekten immer nur für einen kurzen Zeitraum eingesetzt werden, beispielsweise

  • wenn sich ein Raumschiff mit Warp-Geschwindigkeit vom Spieler entfernt und dabei eine Science-Fiction-Film-typische Bewegungsspur (selektiver Motion Blur) hinterlässt.
  • wenn der Spieler-Avatar verletzt wird (eine Kombination von Motion Blur und Kurzsicht-Depth-of-Field-Effekt getreu dem Motto: „Nach einem Schlag auf den Kopf nimmt man die Umgebung eine Zeitlang verschwommen wahr.“).
  • wenn man durch ein Zielfernrohr blickt (Fernsicht-Depth-of-Field-Effekt: Die nähere Umgebung erscheint unscharf, nur die Details in größerer Entfernung lassen sich genau erkennen).
  • bei Interaktionen mit einer anderen Spielfigur (Kurzsicht-Depth-of-Field-Effekt: Die Kamera wird auf den Gesprächspartner fokussiert, alles Weitere wird verschwommen dargestellt).
  • als Stilmittel, um Zwischensequenzen (Cutscenes) interessanter zu gestalten, und um auf bedeutsame Details wie geheime Dokumente und wichtige Gegenstände hinzuweisen (Kurzsicht-Depth-of-Field-Effekt).

Aufmacherbild: spring sunset von iStockphoto / Urheberrecht: IakovKalinin
[ header = Seite 2: Einfache Deferred Rendering Pipeline ]

Einfache Deferred Rendering Pipeline

Das Herzstück der in unseren OpenGL-Programmbeispielen implementierten Deferred Rendering Pipeline bildet ein in der Literatur als G-Buffer (Geometry Buffer) bezeichnetes Frame-Buffer-Objekt, an das wir insgesamt fünf Texturen (Render Targets) zum Speichern aller Geometrie- und Farbinformationen einer 3-D-Szene binden:

  • Textur 1: Die texturierten, jedoch nicht beleuchteten sichtbaren Szenenpixel (PrimaryScreenTexture)
  • Textur 2: Kameraraumpositionen und Tiefenwerte der sichtbaren Szenenpixel (SceneCameraSpacePosAndDepthTexture)
  • Textur 3: Kameraraumnormalen der sichtbaren Szenenpixel (CameraViewNormalTexture)
  • Textur 4: Reflektionsvermögen (Farbe und Intensität) der sichtbaren Szenenpixel (CameraViewSpecularTexture)
  • Textur 5: Selbstleuchtende Szenenpixel (z. B. mittels Light Map beleuchtet) (CameraViewEmissiveTexture)

Nach Abschluss der Geometrieverarbeitung können wir dann im Verlauf der Post-Processing-Phase auf diese Daten zugreifen und in mehreren Schritten das endgültige Szenenbild berechnen.

Bevor wir gleich tiefer in die einzelnen Post-Processing-Berechnungen einsteigen, werden wir uns zunächst einmal damit befassen, wie die Darstellung einer 3-D-Szene im Rahmen des Deferred Renderings vonstattengeht und in diesem Zusammenhang die hierfür erforderlichen Schritte anhand eines einfachen Beispiels gemeinsam durcharbeiten.

Ein Beispiel in einzelnen Schritten

Schritt 1: Lichteigenschaften für die Szenenbeleuchtung festlegen und den neuen Render-Durchgang vorbereiten:

PostProcessingEffects->Reset_ScreenSpaceLights();
PostProcessingEffects->Add_Light(...);
PostProcessingEffects->Prepare_DeferredLighting(&g_BackgroundScreenColor);

Schritt 2: Hintergrunddarstellung:

PostProcessingEffects->Continue_SceneRendering(RM_SceneColorOnly);
SkyBox->Render_BackgroundBox(...);
PostProcessingEffects->Stop_SceneRendering();

Schritt 3: Terrain und andere undurchsichtige (opake) Szenenobjekte rendern:

PostProcessingEffects->Continue_SceneRendering(RM_ScreenSpaceLighting);
[Terrain und andere undurchsichtige Szenenobjekte rendern]
PostProcessingEffects->Stop_SceneRendering();

Schritt 4: Ambient-Occlusion-, Beleuchtungs- sowie Schattenraumberechnungen (VolumetricLightScattering, God-Ray-Effekt) durchführen:

PostProcessingEffects->Calculate_ScreenSpaceAmbientOcclusion(...);
PostProcessingEffects->Calculate_ScreenSpaceLighting(&g_CameraViewDirection);

PostProcessingEffects->Calculate_ComplexVolumetricLightScattering(
&g_LightScatteringParameter, &g_LightScatteringPrimaryColorFilter,
&g_LightScatteringSecondaryColorFilter, &LightWorldPos, &g_CameraViewDirection,
&g_ViewProjectionMatrix, &g_CameraPosition);

Schritt 5: Transparente Objekte (beispielsweise Partikel) rendern, sofern diese bei den Motion-Blur- bzw. Depth-of-Field-Berechnungen berücksichtigt werden sollen:

if(g_CameraMovement == true)
{
  PostProcessingEffects->Continue_SceneRendering(RM_SceneColorOnly);
  [Partikeleffekte rendern]
  PostProcessingEffects->Stop_SceneRendering();
}

Schritt 6: Motion-Blur- sowie Depth-of-Field-Berechnungen durchführen:

if(g_CameraMovement == true)
PostProcessingEffects->Calculate_MotionBlur(0.65f, false);

// Weitsichtigkeit:
if(g_ShortSightedDepthOfFieldEffect == FALSE)
PostProcessingEffects->Calculate_DepthOfFieldEffect(false, 1.0f, 200.0f,
                                                    1000.0f, 10.0f, true);

// Kurzsichtigkeit:
else if(g_ShortSightedDepthOfFieldEffect == TRUE)
PostProcessingEffects->Calculate_DepthOfFieldEffect(true, 1.0f, 10.0f,
                                                    500.0f, 10.0f, true);

Schritt 7: Verzerrungseffekte simulieren (z. B. Hitzeflimmern):

if(g_CameraMovement == false)
{
  PostProcessingEffects->Begin_SecondarySceneRendering(&g_BackgroundScreenColor);
  [Partikel in die zweite Screen-Textur (Auflösung 64x64 Pixel) rendern]
  PostProcessingEffects->Stop_SecondarySceneRendering();

  // Verzerrungseffekte (Flimmern und Unschärfe) berechnen:
  PostProcessingEffects->Combine_Primary_And_Secondary_ScreenTexture(
  &D3DXVECTOR4(frnd(g_MinDistortionX, g_MaxDistortionX),
               frnd(g_MinDistortionY, g_MaxDistortionY),
               g_DistortionInvWavelength,
               g_DistortionFrequency*GetTickCount()));
}

Schritt 8: Komplettieren des ersten Render-Durchgangs. Das fertig berechnete Szenenbild wird als Textur auf ein bildschirmfüllendes Vertexquad gemappt und für die Motion-Blur-Berechnungen im nächsten Frame zwischengespeichert:

PostProcessingEffects->Render_PrimaryScreenQuad(...);

if(g_CameraMovement == true)
PostProcessingEffects->Prepare_MotionBlur_ForNextFrame(false);

Schritt 9: Darstellung aller transparenten Szenenobjekte (Explosionen, Feuer, Partikel, Lens Flares usw.), die einen Einfluss auf die vom Auge wahrgenommene Helligkeit haben (zweiter Render-Durchgang):

PostProcessingEffects->Prepare_SceneRendering(&g_BackgroundScreenColor);

if(g_CameraMovement == false)
{
  PostProcessingEffects->Continue_SceneRendering(RM_SceneColorOnly);
  [Partikeleffekte rendern]
  PostProcessingEffects->Stop_SceneRendering();
}

PostProcessingEffects->Calculate_ScreenSpaceLensFlares(&LightWorldPos,
&g_CameraViewDirection, &g_ViewProjectionMatrix, &g_CameraPosition, 0.0f);

Schritt 10: Zum Abschluss des zweiten Render-Durchgangs wird die Helligkeitsadaptionsfähigkeit des menschlichen Auges simuliert und das fertig berechnete Szenenbild als Textur auf ein transparentes, bildschirmfüllendes Vertexquad gemappt. Im Zuge der Helligkeitsberechnungen werden dunkle Szenen etwas aufgehellt (in dunklen Räumen weitet sich die Pupille des Auges, wodurch sich der Lichteinfall vergrößert) und helle Szenen ein wenig abgedunkelt (in hellen Räumen verengt sich die Pupille des Auges, wodurch sich der Lichteinfall verringert):

PostProcessingEffects->Calculate_SceneGlow(...);
PostProcessingEffects->Calculate_Luminance();
PostProcessingEffects->Render_TransparentScreenQuad(...);

[ header = Seite 3: Depth-of-Field-Effekt (Schärfentiefe) ]

Depth-of-Field-Effekt (Schärfentiefe)

Im Unterschied zum menschlichen Auge oder einer realen Kamera sind die in einem Computerspiel generierten Bilder über die gesamte Kameradistanz betrachtet absolut scharf. Mit der Absicht, die Änderung der Schärfenwahrnehmung bei unterschiedlichen Entfernungen möglichst einfach nachzubilden, legen wir zunächst einen Kameraabstand fest, bei dem die Schärfentiefe in der 3-D-Szene ihren maximalen Wert haben soll (zugehöriger Tiefenwert in den Depth-of-Field-Shader-Programmen (Listings 1 und 2): FocalPlaneDepth). Ferner definieren wir mithilfe einer minimalen und maximalen Kameradistanz einen so genannten Fokusbereich, innerhalb dessen die Schärfentiefe ausgehend vom Fokus mehr und mehr abnimmt (zugehörige Tiefenwerte der minimalen/maximalen Kameradistanz in den Depth-of-Field-Shader-Programmen: NearBlurDepth bzw. FarBlurDepth). Alle Szenenpixel, die außerhalb des Fokusbereichs entweder im Nah- oder im Fernbereich liegen, werden mit minimaler Schärfentiefe dargestellt. Anhand der Abbildungen 1 und 2 können Sie die Ergebnisse der Schärfentiefenberechnungen in unserem Depth-of-Field-Programmbeispiel für zwei unterschiedliche Fokusdistanzen nachvollziehen.

Abb. 1: Depth-of-Field-Programmbeispiel (geringe Sichtweite)

Abb. 2: Depth-of-Field-Programmbeispiel (Fernsicht)

Während sich die Schärfentiefe der einzelnen Terrain-Pixel mithilfe der im G-Buffer gespeicherten Tiefenwerte problemlos berechnen lässt, müssen wir beim Szenenhintergrund aufgrund der fehlenden Tiefeninformation ein wenig tricksen. Aus diesem Grund kommen in Abhängigkeit davon, ob der Hintergrund scharf (Abb. 2; Listing 2) oder unscharf (Abb. 1; Listing 1) dargestellt werden soll, zwei unterschiedliche Fragment Shader (DepthOfFieldFarSighted.frag bzw. DepthOfFieldShortSighted.frag) zum Einsatz.

in  vec4 gs_TexCoord[8];
out vec4 gs_FragColor;

uniform sampler2D ScreenTexture;
uniform sampler2D ScreenTextureLowRes;
uniform sampler2D CameraDepthTexture;
uniform float Texelsize;
uniform vec4 DepthOfFieldParameter;

// DepthOfFieldParameter.x := NearBlurDepth
// DepthOfFieldParameter.y := FocalPlaneDepth
// DepthOfFieldParameter.z := FarBlurDepth

void main()
{
  float CameraDepth = texture(CameraDepthTexture, gs_TexCoord[0].st).w;

vec4 BlurredColor = 0.25*(
    texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(Texelsize, Texelsize))+
     texture(ScreenTextureLowRes, gs_TexCoord[0].st-vec2(Texelsize, Texelsize))+
    texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(Texelsize, -Texelsize))+
    texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(-Texelsize, Texelsize)));

  if(CameraDepth == -1.0) // Hintergrund
  gs_FragColor = BlurredColor;
  else
  {
    if(CameraDepth < DepthOfFieldParameter.x ||
    CameraDepth > DepthOfFieldParameter.z)
    gs_FragColor = BlurredColor;
    else
    {
      if(CameraDepth > DepthOfFieldParameter.y)
      {
        float factor = (DepthOfFieldParameter.z-CameraDepth)/
          (DepthOfFieldParameter.z-DepthOfFieldParameter.y);
        gs_FragColor = factor*texture(ScreenTexture, gs_TexCoord[0].st) +
          (1.0-factor)*BlurredColor;
      }
      else
      {
        float factor = (CameraDepth-DepthOfFieldParameter.x)/
          (DepthOfFieldParameter.y-DepthOfFieldParameter.x);
        gs_FragColor = factor*texture(ScreenTexture, gs_TexCoord[0].st) +
          (1.0-factor)*BlurredColor;
}}}}

 

in  vec4 gs_TexCoord[8];
out vec4 gs_FragColor;

uniform sampler2D ScreenTexture;
uniform sampler2D ScreenTextureLowRes;
uniform sampler2D CameraDepthTexture;
uniform float Texelsize;
uniform vec4 DepthOfFieldParameter;

// DepthOfFieldParameter.x := NearBlurDepth
// DepthOfFieldParameter.y := FocalPlaneDepth
// DepthOfFieldParameter.z := FarBlurDepth

void main()
{
  float CameraDepth = texture(CameraDepthTexture, gs_TexCoord[0].st).w;

  if(CameraDepth == -1.0) // Hintergrund
  gs_FragColor = texture(ScreenTexture,gs_TexCoord[0].st);
  else
  {
    vec4 BlurredColor = 0.25*(
      texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(Texelsize, Texelsize))+
      texture(ScreenTextureLowRes, gs_TexCoord[0].st-vec2(Texelsize, Texelsize))+
      texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(Texelsize, -Texelsize))+
      texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(-Texelsize, Texelsize)));

    if(CameraDepth < DepthOfFieldParameter.x ||
    CameraDepth > DepthOfFieldParameter.z)
    gs_FragColor = BlurredColor;
    else
    {
      if(CameraDepth > DepthOfFieldParameter.y)
      {
        float factor = (DepthOfFieldParameter.z-CameraDepth)/
          (DepthOfFieldParameter.z-DepthOfFieldParameter.y);
        gs_FragColor = factor*texture(ScreenTexture, gs_TexCoord[0].st) +
          (1.0-factor)*BlurredColor;
      }
      else
      {
        float factor = (CameraDepth-DepthOfFieldParameter.x)/
          (DepthOfFieldParameter.y-DepthOfFieldParameter.x);
        gs_FragColor = factor*texture(ScreenTexture, gs_TexCoord[0].st) +
          (1.0-factor)*BlurredColor;
}}}}

Das Funktionsprinzip der beiden Programme ist nahezu identisch und dementsprechend schnell erklärt: Je nach Lage eines Szenenpixels muss bei der Unschärfeberechnung zwischen den folgenden vier Fällen unterschieden werden:

  • Fall 1: Hintergrunddarstellung (scharf bzw. mit maximaler Unschärfe)
  • Fall 2: Szenendarstellung mit maximaler Unschärfe im Nahbereich bzw. Fernbereich
  • Fall 3: Schärfentiefenberechnung im Fokusbereich (Tiefenwert größer als die Fokusdistanz)
  • Fall 4: Schärfentiefenberechnung im Fokusbereich (Tiefenwert gleich oder kleiner als die Fokusdistanz)

Die Darstellung eines Szenenpixels mit maximaler Unschärfe erfolgt in unserem Programmbeispiel in zwei Schritten. Bevor unsere Depth-of-Field-Shader überhaupt zum Einsatz kommen, wird das fertig ausgeleuchtete Szenenbild zunächst in eine Textur mit reduzierter Auflösung (ScreenTextureLowRes) kopiert und dabei mit einem Unschärfefilter bearbeitet (verantwortlich hierfür ist der DownScale4X4-Fragment-Shader unseres OpenGL-Frameworks). Auf Grafikkarten mit geringer Rechenleistung würde es sich nun anbieten, in den Depth-of-Field-Shader-Programmen das Low-Resolution-Szenenbild direkt – also ohne zusätzliche Filterberechnungen – für die Darstellung aller Szenenpixel mit maximaler Unschärfe zu verwenden:

vec4 BlurredColor = texture(ScreenTextureLowRes, gs_TexCoord[0].st);
gs_FragColor = BlurredColor;

Das optische Resultat ist jedoch ungleich realistischer (minimales Jittering/Bildrauschen), wenn wir stattdessen, wie in den Listings 1 und 2 gezeigt, in einer zweiten Filteroperation den Mittelwert aus vier benachbarten Low-Resolution-Szenenpixeln bestimmen:

vec4 BlurredColor = 0.25*(
  texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(Texelsize, Texelsize))+
  texture(ScreenTextureLowRes, gs_TexCoord[0].st-vec2(Texelsize, Texelsize))+
  texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(Texelsize, -Texelsize))+
  texture(ScreenTextureLowRes, gs_TexCoord[0].st+vec2(-Texelsize, Texelsize)));

Die Änderung der Schärfenwahrnehmung innerhalb des Fokusbereichs lässt sich nun simulieren, indem man unter Zuhilfenahme zweier Gewichtungsfaktoren die Farbwerte bei maximaler und minimaler Schärfentiefe wie folgt miteinander kombiniert:

gs_FragColor = factor*texture(ScreenTexture, gs_TexCoord[0].st) +
               (1.0-factor)*BlurredColor;

Je nachdem, ob sich ein Szenenpixel vor (ScenePixelCameraDepth <= FocalPlaneDepth) bzw. hinter (ScenePixelCameraDepth > FocalPlaneDepth) der Fokusebene befindet, kommen bei der Berechnung der Gewichtungsfaktoren zwei unterschiedliche Formeln zum Einsatz:

1. ScenePixelCameraDepth > FocalPlaneDepth
Formel für die Berechnung der Gewichtungsfaktoren:

float factor = (FarBlurDepth - ScenePixelCameraDepth)/
               (FarBlurDepth - FocalPlaneDepth);

Beispiel 1: ScenePixelCameraDepth = FocalPlaneDepth
Gewichtungsfaktoren: factor = 1; (1-factor) = 0
=> Szenenpixel wird mit maximaler Tiefenschärfe dargestellt:

gs_FragColor = texture(ScreenTexture, gs_TexCoord[0].st);

Beispiel 2: ScenePixelCameraDepth = FarBlurDepth
Gewichtungsfaktoren: factor = 0; (1-factor) = 1
=> Szenenpixel wird mit minimaler Tiefenschärfe dargestellt:

gs_FragColor = BlurredColor;

2. ScenePixelCameraDepth <= FocalPlaneDepth Formel für die Berechnung der Gewichtungsfaktoren:

float factor = (ScenePixelCameraDepth - NearBlurDepth)/
               (FocalPlaneDepth - NearBlurDepth);

Beispiel 1: ScenePixelCameraDepth = FocalPlaneDepth
Gewichtungsfaktoren: factor = 1; (1-factor) = 0
=> Szenenpixel wird mit maximaler Tiefenschärfe dargestellt:

gs_FragColor = texture(ScreenTexture, gs_TexCoord[0].st);

Beispiel 2: ScenePixelCameraDepth = NearBlurDepth
Gewichtungsfaktoren: factor = 0; (1-factor) = 1
=> Szenenpixel wird mit minimaler Tiefenschärfe dargestellt:

gs_FragColor = BlurredColor;

[ header = Seite 4: Motion Blur (Bewegungsunschärfe) und Verzerrungseffekte (Hitzeflimmern) ]

Motion Blur (Bewegungsunschärfe)

Bei der Simulation von Bewegungsunschärfen lassen sich prinzipiell zwei unterschiedliche Ansätze verfolgen. Bei der speicherfreundlichen dafür jedoch sehr rechenintensiven Variante nutzt man einen Post-Processing-Filter, um für alle Szenenpixel in Abhängigkeit von der Kamerabewegung und den im G-Buffer gespeicherten Tiefenwerten einen individuellen Unschärfeeffekt zu berechnen. Die Methode hat jedoch zwei Haken. Zum einen liefert sie nur bei statischen Szenen korrekte Ergebnisse, da die unterschiedlichen Geschwindigkeiten der einzelnen 3-D-Objekte (Beispiel: die Fahrzeuge in einem Rennspiel) relativ zur Kamera unberücksichtigt bleiben und zum anderen muss über ein vorgeschaltetes Maskierungsverfahren sichergestellt werden, dass für alle Objekte, die sich mit der Kamera mitbewegen (Beispiel: die vom Spieler verwendete Waffe in einem Ego-Shooter), kein Motion Blur berechnet wird.

Bei der zweiten Methode, die auch in unseren Programmbeispielen zum Einsatz kommt, lassen sich auf Kosten eines erhöhten Speicherverbrauchs sowohl für statische wie auch für dynamische Szenen glaubhafte Motion-Blur-Effekte generieren. Das dem Verfahren zugrunde liegende Funktionsprinzip ist denkbar einfach – man addiert zum aktuellen Szenenbild (ScreenTexture) das zwischengespeicherte Szenenbild des vorangegangenen Frames (ScreenTextureOldFrame), wobei die Intensität der Bewegungsunschärfe mithilfe zweier Gewichtungsfaktoren (BlurFactor sowie InvBlurFactor = 1 – BlurFactor) festgelegt wird:

gs_FragColor = InvBlurFactor*texture(ScreenTexture, gs_TexCoord[0].st) +
               BlurFactor*texture(ScreenTextureOldFrame, gs_TexCoord[0].st);

Um einerseits den Unschärfeeffekt zu verstärken (Abb. 3), und um andererseits den Speicherverbrauch sowie den Rechenaufwand in Grenzen zu halten, bietet es sich an, das bereits beim Depth-of-Field-Effekt verwendete Low-Resolution-Szenenbild zwischenzuspeichern – zumal dieses im Rahmen des Kopiervorgangs bereits mit einem Unschärfefilter bearbeitet wurde.

Auch ein selektiver Einsatz von Motion Blur als Spezialeffekt – etwa bei Raumschiffen, die sich mit Warp-Geschwindigkeit einem Planeten oder einer Raumstation annähern – ist problemlos möglich. Wie in Listing 3 skizziert, erfolgt die Auswahl der hierbei zu berücksichtigenden 3-D-Objekte bzw. der zugehörigen Szenenpixel im Rahmen unseres Prost-Processing-Frameworks mithilfe einer so genannten Textur-Maske (MaskingTexture):

if(texture(MaskingTexture, gs_TexCoord[0].st).w <= 0.0)
gs_FragColor = InvBlurFactor*texture(ScreenTexture, gs_TexCoord[0].st) +
               BlurFactor*texture(ScreenTextureOldFrame, gs_TexCoord[0].st);
else
gs_FragColor = texture(ScreenTexture, gs_TexCoord[0].st);

Abb. 3: Motion-Blur-Programmbeispiel

uniform sampler2D ScreenTexture;
uniform sampler2D ScreenTextureOldFrame;
uniform sampler2D MaskingTexture;
uniform float BlurFactor;
uniform float InvBlurFactor;

// Hinweis: InvBlurFactor = 1.0-BlurFactor

void main()
{
  if(texture(MaskingTexture, gs_TexCoord[0].st).w <= 0.0)
  gs_FragColor = InvBlurFactor*texture(ScreenTexture, gs_TexCoord[0].st) +
    BlurFactor*texture(ScreenTextureOldFrame, gs_TexCoord[0].st);
  else
  gs_FragColor = texture(ScreenTexture, gs_TexCoord[0].st);
}

Verzerrungseffekte (Hitzeflimmern)

Flimmernde Luftmassen über glühend heißem Asphalt, ein geborstenes Dampfrohr, der Abgasstrahl von Raumschiffen (Abb. 4), Raketen und Flugzeugen, Feuer, Explosionen, Energiewaffen und Schutzschilde – Computerspiele bieten ein großes Spektrum an Einsatzmöglichkeiten für Verzerrungseffekte, bei deren Berechnung wir in unseren Programmbeispielen auf das in Listing 4 gezeigte Shader-Programm zurückgreifen.

Abb. 4: Verzerrungseffekte (Hitzeflimmern)

in  vec4 gs_TexCoord[8];
out vec4 gs_FragColor;

uniform sampler2D PrimaryScreenTexture;
uniform sampler2D SecondaryScreenTexture;
uniform vec4 CombinationValues;
uniform vec2 BlurIntensity;

void main()
{
  vec4 SecondaryScreenCol = texture(SecondaryScreenTexture, gs_TexCoord[0].st);

  float intensity = SecondaryScreenCol.r + SecondaryScreenCol.g +
                    SecondaryScreenCol.b;

  // Vor dem Szenenpixel befindet sich kein transparentes Objekt:
  if(intensity < 0.05)
  gs_FragColor = texture(PrimaryScreenTexture, gs_TexCoord[0].st);
  // Spezialeffekte berechnen (Flimmern und Unschärfe):
  else
  {
/* Verzerrungs- und Unschärfeeffekte berechnen:
     * Parameter für die Berechnung des Flimmereffekts:
     * CombinationValues.w := aktuelle Zeit
     * CombinationValues.z := Wellenzahl
     * CombinationValues.x, CombinationValues.y := Amplituden
     * Je stärker die Texturkoordinaten modifiziert werden, umso stärker
     * der Flimmereffekt!
     * Der Unschärfeeffekt kommt durch das Zweifach-Sampling zustande (hierfür
     * werden zwei modifizierte Texturkoordinatensätze benötigt):*/

    vec2 ModifiedTexCoord1 = gs_TexCoord[0].st + intensity*
    vec2(sin(CombinationValues.w+CombinationValues.z*gs_TexCoord[0].x)*
      CombinationValues.x,
      sin(CombinationValues.w+CombinationValues.z*gs_TexCoord[0].y)*
      CombinationValues.y);

    vec2 ModifiedTexCoord2 = ModifiedTexCoord1 + BlurIntensity;

    gs_FragColor = 0.5*(texture(PrimaryScreenTexture, ModifiedTexCoord1)+
                        texture(PrimaryScreenTexture, ModifiedTexCoord2));
}}

Während sich einfache Verzerrungen bereits mittels einer simplen periodischen (zeitabhängigen) Modifizierung der Texturkoordinaten realisieren lassen, benötigen wir für einen kombinierten Verzerrungsunschärfeeffekt zwei Sätze modifizierter Texturkoordinaten:

// Verzerrungseffekt:
gs_FragColor = texture(PrimaryScreenTexture, ModifiedTexCoord);

// kombinierter Verzerrungsunschärfeeffekt:
gs_FragColor = 0.5*(texture(PrimaryScreenTexture, ModifiedTexCoord1)+
                    texture(PrimaryScreenTexture, ModifiedTexCoord2));

Bei der Berechnung der modifizierten Texturkoordinatensätze ist darauf zu achten, dass sich sowohl die Frequenz (bzw. die Wellenzahl) als auch die Intensität (Amplitude) der Verzerrungen beliebig variieren lässt:

vec2 ModifiedTexCoord1 = gs_TexCoord[0].st + intensity*
     vec2(sin(time+Wellenzahl*gs_TexCoord[0].x)*Amplitude_xDir,
          sin(time+Wellenzahl*gs_TexCoord[0].y)*Amplitude_yDir);

vec2 ModifiedTexCoord2 = ModifiedTexCoord1+BlurIntensity;

Bevor sich jedoch die Verzerrungseffekte im Rahmen der Post-Processing-Phase simulieren lassen, müssen wir zunächst alle transparenten Objekte, die einen solchen Effekt verursachen können (Rauch, Dampf, Explosionen, Feuer, Energiewaffen, Schutzschilde usw.) in eine separate Textur rendern (SecondaryScreenTexture mit der Auflösung von 64 x 64 Pixeln). Anhand der in dieser Textur gespeicherten Farbwerte können wir dann die verdeckten Szenenpixel identifizieren und für diese die gewünschte Verzerrung berechnen:

vec4 SecondaryScreenCol = texture(SecondaryScreenTexture, gs_TexCoord[0].st);

float intensity = SecondaryScreenCol.r + SecondaryScreenCol.g +
                  SecondaryScreenCol.b;

if(intensity < 0.05) // keine Verzerrungen
    gs_FragColor = texture(PrimaryScreenTexture, gs_TexCoord[0].st);
else
    [Verzerrungs- und Unschärfeeffekte berechnen]

[ header = Seite 5: God Rays (Volumetric Light Scattering, Volumetric Lighting) ]

God Rays (Volumetric Light Scattering, Volumetric Lighting)

Beeindruckende Effekte benötigen beeindruckende Namen. Wie also sollte man die majestätisch wirkenden Lichtstrahlen nennen, die beim Durchbrechen der Wolkendecke wirken, als hätte Gott höchstpersönlich sie in Richtung Erde gesandt? Während die wissenschaftliche Bezeichnung – Crepuscular Rays (Strahlenbüschel) – alles andere als ehrfurchtgebietend klingt, bringt es die Namensschöpfung der Spieleentwickler auf den Punkt und ist gleichsam einprägsam wie werbewirksam: God Rays.

Physikalisch gesehen handelt es sich um Streuprozesse, bei denen Lichtstrahlen an Wassertropfen und Schmutzpartikeln (Staub, Rauch etc.) in der Atmosphäre in Richtung eines Beobachters abgelenkt werden. Neben den typischen Sichtungen am Himmel lassen sich Strahlenbündel besonders häufig an nebligen Tagen bei niedrigen Sonnenständen im Wald oder an Gebirgshängen wahrnehmen. Man spricht in diesem Zusammenhang daher auch von Dämmerungsstrahlen.

In modernen Spielen generiert man die God Rays standardmäßig im Verlauf der Post-Processing-Phase (Screen-Space-Berechnung). Besonders beeindruckend ist dieser Effekt, wenn die Lichtwege von den einzelnen Streuzentren hin zum Beobachter durch sehr feinteilige lichtundurchlässige (opake) Objekte blockiert werden. Blattwerk beispielsweise erzeugt ähnlich interessante Strahlenverläufe wie das in den Abbildungen 5 und 6 gezeigte völlig zerstörte Raumschiffmodell (obwohl God Rays im Vakuum des Weltraums aufgrund der fehlenden Streuzentren genaugenommen physikalisch unmöglich sind). Die Funktionsweise des in Listing 5 dargestellten God-Ray-Shader-Programms sollte sich auch ohne fundierte physikalische Kenntnisse auf dem Gebiet der atmosphärischen Streuprozesse problemlos nachvollziehen lassen.

Abb. 5: God-Ray-Programmbeispiel (1)

Abb. 6: God-Ray-Programmbeispiel (2)

in  vec4 gs_TexCoord[8];
out vec4 gs_FragColor;

uniform vec3 LightProjectedCameraSpacePos;
uniform sampler2D ScreenTexture;
uniform sampler2D CameraDepthTexture;
uniform float ViewDependentIntensity;
uniform vec4 LightScatteringParameter;
uniform vec4 LightScatteringPrimaryColorFilter;
uniform vec4 LightScatteringSecondaryColorFilter;

#define NUM_SAMPLES 70
const float INV_NUM_SAMPLES = 0.0143;

void main()
{
  // Sample-Richtung:
  vec2 deltaTexCoord = vec2(gs_TexCoord[0].st-LightProjectedCameraSpacePos.xy);
  deltaTexCoord *= INV_NUM_SAMPLES;

  // Sample-Startposition:
  vec2 texCoord = LightProjectedCameraSpacePos.xy;

  // God-Ray-Helligkeit Anfangswert:
  float illuminationDecay = LightScatteringParameter.z;

  vec4 sample;
  vec4 ScatteringColor = LightScatteringPrimaryColorFilter;

  for(int i = 0; i < NUM_SAMPLES; i++)
  {
    // Texturkoordinaten der Sample-Position:
    texCoord += deltaTexCoord;

    // unverdecktes Hintergrund- bzw. Lichtquellenpixel:
    // (CameraDepthTexture-Zugriff bei dunklen opaken Objekten
    // im Gegenlicht normalerweise aber nicht notwendig!)
    if(texture(CameraDepthTexture, texCoord).w == -1.0)
    // => Sample verwerfen, wenn ein opakes Pixel gefunden wurde!
    {
      sample = texture(ScreenTexture, texCoord);
      sample *= illuminationDecay*LightScatteringParameter.w;
      ScatteringColor += sample;
    }

    // Abnahme der Lichtintensität zwischen zwei Sampling-Positionen.
    // Das Licht wird durch Streuprozesse in andere Richtungen abgelenkt,
    // wodurch sich die Lichtintensität entlang der ursprünglichen
    // Richtung (deltaTexCoord) verringert:
    illuminationDecay *= LightScatteringParameter.y;
  }
  
  ScatteringColor *= (max(0.0, ViewDependentIntensity)*
                      LightScatteringParameter.x);

  gs_FragColor = LightScatteringSecondaryColorFilter*
                 texture(ScreenTexture, gs_TexCoord[0].st)+
                 ScatteringColor*ScatteringColor*ScatteringColor;
}

Das Ziel der Berechnungen besteht darin, für jeden Szenenpixel den Lichtanteil zu ermitteln, der aus der Richtung der Lichtquelle kommend in die Blickrichtung des Beobachters gestreut wird. Hierfür wird der Weg, den das Licht von der Lichtquelle zum betreffenden Szenenpixel zurücklegen muss (Lichtweg), zunächst in gleichmäßige Sampling-Bereiche aufgeteilt, wobei jede Sample-Position einem virtuellen Streuzentrum entspricht:

vec2 deltaTexCoord = vec2(gs_TexCoord[0].st-LightProjectedCameraSpacePos.xy);
deltaTexCoord /= float(NUM_SAMPLES);

Im zweiten Schritt legt man den Startpunkt der Sampling-Berechnungen (Position der Lichtquelle im Screen Space bzw. Bildraum) sowie die Anfangshelligkeit der Lichtstrahlen fest:

vec2 texCoord = LightProjectedCameraSpacePos.xy;
float illuminationDecay = MaxIntensity;

Die eigentlichen Streulichtberechnungen basieren nun auf drei einfachen Annahmen:

  • Annahme 1: Wird der Lichtweg an einer Sample-Position durch ein opakes Pixel bzw. Szenenobjekt blockiert, wird das Sample (die Stichprobe) verworfen. In Abhängigkeit von der Anzahl der verworfenen Samples bilden sich zunehmend dunkler werdende Bereiche, die so genannten Schattenräume, aus.
  • Annahme 2: Wird der Lichteinfall von keinem opaken Pixel (Szenenobjekt) blockiert, dann streut das virtuelle Streuzentrum seinerseits ein wenig Licht in Richtung des betrachteten Szenenpixels:
sample = texture(ScreenTexture, texCoordScatteringCenter);
sample *= illuminationDecay*IntensityFactor;
ScatteringColor += sample;
  • Annahme 3: Mit zunehmendem Abstand zwischen der Lichtquelle und der Sample-Position verringert sich die Streulichtintensität:
illuminationDecay *= LightScatteringDecrease;

[ header = Seite 6: Lens-Flare-Effekte (Linsenreflexionen) ]

Lens-Flare-Effekte (Linsenreflexionen)

Kein Artikel über grafische Spezialeffekte wäre vollständig, würde man sich nicht auch mit dem wohl ältesten und bekanntesten dieser Effekte befassen – den Lens Flares.

Im Unterschied zu den God Rays handelt es sich hierbei nicht um ein atmosphärisches Phänomen, sondern um Streulichtreflexionen im Linsensystem einer Kamera, die von überaus hellen Lichtquellen verursacht werden. Über lange Zeit wurden Lens Flares ausschließlich mithilfe so genannter Billboards (einfache Vertexquads, die der Spieler stets vorne sieht) dargestellt. Die Ergebnisse konnten und können sich auch heute noch sehen lassen – jedoch erfordert diese Technik einiges an künstlerischem Geschick. Zum einen müssen die einzelnen Reflexionen entlang einer durch die Bildmitte und Lichtquelle verlaufende Achse (Reflexionsachse) auf möglichst ansprechende Weise arrangiert werden und zum anderen benötigt man diverse Texturen mit schön anzusehenden strahlenförmigen, ringförmigen, tellerförmigen oder (sechs)eckigen Reflexionsmustern (Flare-Texturen, siehe Abb. 8 und 9).

In letzter Zeit jedoch verliert der Billboard-basierte Ansatz zusehends an Bedeutung, da sich die Lens Flares (die in den Abbildungen 78 und 9 gezeigten regenbogenfarbigen Reflexionen) genau wie die God Rays ebenfalls problemlos im Verlauf der Post-Processing-Phase generieren lassen.

Abb. 7: God Rays + Screen Space Lens Flares

Abb. 8: Lens-Flare-Effekt (1)

Abb. 9: Lens-Flare-Effekt (2)

Im ersten Schritt dieser Berechnungen muss man zunächst überprüfen, ob die ursächliche Lichtquelle durch lichtundurchlässige Szenenobjekte verdeckt wird. Sollte dies der Fall sein, dann kann auf die Lens-Flare-Berechnungen verzichtet werden. Der denkbar einfachste Verdeckungstest kommt mit einem einzigen Texturzugriff auf das Tiefenabbild der 3-D-Szene aus:

if(texture(CameraDepthTexture, LightProjectedCameraSpacePos.xy).w == -1.0)
{
  [Darstellung der Linsenreflexionen]
}
else // Szenenbild unverändert belassen
  gs_FragColor = texture(ScreenTexture, gs_TexCoord[0].st);

Bei großen 3-D-Objekten in geringer Kameradistanz liefert ein solch einfacher Test zwar ganz zufriedenstellende Ergebnisse; problematisch wird es jedoch, wenn eine Vielzahl weit entfernter kleiner Objekte eine Lichtquelle verdecken könnte. In Abbildung 9 beispielsweise bewegen sich unzählige Asteroiden um einen Planeten und sind an der Stelle, wo sie die Sonne verdecken, kaum mehr als einen Pixel groß. Um zu verhindern, dass solch kleine Objekte einen nennenswerten Einfluss auf die Verdeckungsberechnungen ausüben, nutzen wir in unserem Screen-Space-Lens-Flare-Shader-Programm (Listing 6) ein etwas komplexeres Testverfahren, in dessen Verlauf wir insgesamt fünf benachbarte Tiefenwerte der 3-D-Szene auswerten.

in  vec4 gs_TexCoord[8];
out vec4 gs_FragColor;

uniform vec3 LightProjectedCameraSpacePos;
uniform sampler2D LightSourcesTexture;
uniform sampler2D ScreenTexture;
uniform sampler2D CameraDepthTexture;
uniform float BrightnessGhosts;
uniform float BrightnessHalo;
uniform vec3 ChromaticDistortionVector;
uniform float HaloWidth;

const int cNumSamples = 5;

// chromatic distortion:
vec4 textureDistorted(in sampler2D tex, in vec2 texcoord,
                      in vec2 direction, in vec3 distortion)
{
  return vec4(texture(tex, texcoord + direction * distortion.r).r,
texture(tex, texcoord + direction * distortion.g).g,
              texture(tex, texcoord + direction * distortion.b).b, 1.0);
}

void main()
{
  float CameraDepth1 = texture(CameraDepthTexture,
                       LightProjectedCameraSpacePos.xy).w;
  float CameraDepth2 = texture(CameraDepthTexture,
                       LightProjectedCameraSpacePos.xy+vec2(0.01, 0.01)).w;
  float CameraDepth3 = texture(CameraDepthTexture,
                       LightProjectedCameraSpacePos.xy-vec2(0.01, 0.01)).w;
  float CameraDepth4 = texture(CameraDepthTexture,
                       LightProjectedCameraSpacePos.xy+vec2(-0.01, 0.01)).w;
  float CameraDepth5 = texture(CameraDepthTexture,
                       LightProjectedCameraSpacePos.xy-vec2(-0.01, 0.01)).w;

  float MinCameraDepth = CameraDepth1;
  MinCameraDepth = min(MinCameraDepth, CameraDepth2);
  MinCameraDepth = min(MinCameraDepth, CameraDepth3);
  MinCameraDepth = min(MinCameraDepth, CameraDepth4);
  MinCameraDepth = min(MinCameraDepth, CameraDepth5);

  if(MinCameraDepth < 0.0)
    MinCameraDepth = 1000000.0;

  float MaxCameraDepth = CameraDepth1;
  MaxCameraDepth = max(MaxCameraDepth, CameraDepth2);
  MaxCameraDepth = max(MaxCameraDepth, CameraDepth3);
  MaxCameraDepth = max(MaxCameraDepth, CameraDepth4);
  MaxCameraDepth = max(MaxCameraDepth, CameraDepth5);
  if(MaxCameraDepth < 0.0)
    MaxCameraDepth = 1000000.0;

  if(MinCameraDepth < LightProjectedCameraSpacePos.z-0.125 &&
    MaxCameraDepth < LightProjectedCameraSpacePos.z-0.125)
    gs_FragColor = texture(ScreenTexture, gs_TexCoord[0].st);
  else
  {
    vec4 result = vec4(0.0);

    vec2 texCoordFlip = -gs_TexCoord[0].st + vec2(1.0);
    vec2 texCoord;

    vec2 deltaTexCoord = 0.75*(gs_TexCoord[0].st-texCoordFlip)*0.25;
    // Hinweis: 0.25 := 1/(cNumSamples-1)

    vec2 FlareDir = (gs_TexCoord[0].st-texCoordFlip);
    FlareDir = normalize(FlareDir);
    vec2 HaloDir = FlareDir*HaloWidth;

    for(int i = 0; i < cNumSamples; i++)
    {
      texCoord = fract(texCoordFlip+deltaTexCoord*float(i));
      result += BrightnessGhosts*textureDistorted(LightSourcesTexture, texCoord,
        FlareDir, ChromaticDistortionVector);
    }

    result += textureDistorted(LightSourcesTexture, fract(texCoordFlip+HaloDir),
      FlareDir, ChromaticDistortionVector)*BrightnessHalo;

    gs_FragColor = result+texture(ScreenTexture, gs_TexCoord[0].st);
}}

Während man bei der klassischen Darstellung der einzelnen Linsenreflexionen mehrere Billboards entlang der Reflexionsachse positioniert, ersetzt man diese beim Screen-Space-basierten Ansatz durch Mehrfachabbilder (so genannte Ghosts) der für die Reflexionen verantwortlichen Lichtquelle. Damit dieser Trick funktioniert, muss die besagte Lichtquelle zuvor jedoch in eine separate Textur (LightSourcesTexture) gerendert worden sein. Die Texturkoordinaten texCoord für die Darstellung der einzelnen Ghosts berechnen sich nun wie folgt:

vec2 texCoordFlip = -gs_TexCoord[0].st + vec2(1.0);
vec2 deltaTexCoord = 0.75*(gs_TexCoord[0].st-texCoordFlip)/
                     float(NumGhosts-1);
[...]
// Texturkoordinaten i-ter Ghost:
texCoord = fract(texCoordFlip+deltaTexCoord*float(i));

Beispiel: Ein Lichtquellenpixel sei in der LightSourcesTexture an der Texturkoordinatenposition (0.7, 0.3) gespeichert. Gemäß unserer Texturkoordinatenformel ist dieser Lichtquellenpixel nach einem LightSourcesTexture-Zugriff im Szenenbild als Ghost (mit i = 0) an der Texturkoordinatenposition (0.3, 0.7) sichtbar:

texCoord = fract(-(0.3, 0.7)+(1.0, 1.0)+deltaTexCoord*0)
texCoord = (0.7, 0.3)

Mit anderen Worten: Die Szenenbildtexturkoordinaten (0.3, 0.7) ermöglichen einen LightSourcesTexture-Zugriff an der Texturkoordinatenposition (0.7, 0.3):

// Pixelfarbe Ghost:
vec4 LensFlareColor = vec4(0.0);

for(int i = 0; i < NumGhosts; i++)
{
  texCoord = fract(texCoordFlip+deltaTexCoord*float(i));
  LensFlareColor += BrightnessGhosts*texture(LightSourcesTexture, texCoord);
}

// Pixelfarbe Halo (Lichthof, in dessen Zentrum sich die Lichtquelle befindet):
vec2 HaloDir = FlareDir*HaloWidth;
texCoord = fract(texCoordFlip+HaloDir)
LensFlareColor += BrightnessHalo*texture(LightSourcesTexture, texCoord);

Die Intensitäten (BrightnessGhosts bzw. BrightnessHalo) der Reflexionen sind von der Bildschirmposition (Screen-Space-Position) der Lichtquelle abhängig und werden als Parameter an das Shader-Programm übergeben (Listing 7).

float Dir2D[2];
Dir2D[0] = LightProjectedCameraSpacePos.x - 0.5f;
Dir2D[1] = LightProjectedCameraSpacePos.y - 0.5f;

float tempDot = Dir2D[0]*Dir2D[0] + Dir2D[1]*Dir2D[1];

float BrightnessHalo = max(IntensityReductionHalo, tempDot);
BrightnessHalo = 1.0f-BrightnessHalo*BrightnessHalo;
BrightnessHalo = pow(BrightnessValue1*BrightnessHalo, BrightnessValue2);
[...]
ScreenSpaceLensFlaresShader->Set_ShaderFloatValue(BrightnessHalo,
                                                  "BrightnessHalo");
ScreenSpaceLensFlaresShader->Set_ShaderFloatValue(BrightnessGhosts,
                                                  "BrightnessGhosts");

Würde man bei der Darstellung der Lens Flares lediglich die unveränderten Lichtquellenabbilder entlang der Reflexionsachse positionieren, wäre das Resultat wenig spektakulär. Bezieht man jedoch die chromatische Aberration (Chromatic Distortion) einer optischen Linse in die Berechnungen mit ein (ein Abbildungsfehler, der dadurch entsteht, das Licht unterschiedlicher Farbe unterschiedlich stark gebrochen wird), dann ergeben sich die in den Abbildungen 78 und 9 gezeigten regenbogenfarbigen Reflexionen.

  • Die Intensität der Farbverzerrungen legen wir mithilfe des nachfolgenden Shader-Parameters fest:
 D3DXVECTOR3 ChromaticDistortionVector = D3DXVECTOR3(
            -ChromaticDistortion/LightSourcesTextureResolutionWidth, 0.0f,
             ChromaticDistortion/LightSourcesTextureResolutionHeight);

ScreenSpaceLensFlaresShader->Set_ShaderFloatVector3(&ChromaticDistortionVector,
                             "ChromaticDistortionVector");
  • Die Verzerrungsrichtung berechnet sich für ein Ghost- bzw. für ein Halo-Szenenpixel im Shader-Programm wie folgt:
vec2 FlareDir = (gs_TexCoord[0].st-texCoordFlip);
FlareDir = normalize(FlareDir);
  • Unter Berücksichtigung der Verzerrungsrichtung und -intensität kann nun die zugehörige Pixelfarbe mit einer modifizierten Texturzugriffsfunktion ermittelt werden:
 vec4 textureDistorted(in sampler2D tex, in vec2 texcoord,
                      in vec2 direction, in vec3 distortion)
{
  return vec4(texture(tex, texcoord + direction * distortion.r).r,
              texture(tex, texcoord + direction * distortion.g).g,
              texture(tex, texcoord + direction * distortion.b).b, 1.0);
}

// Pixelfarbe Ghost:
LensFlareColor += BrightnessGhosts*textureDistorted(LightSourcesTexture,
                             texCoord, FlareDir,
                             ChromaticDistortionVector);

// Pixelfarbe Halo:
vec2 HaloDir = FlareDir*HaloWidth;
texCoord = fract(texCoordFlip+HaloDir)
LensFlareColor += BrightnessHalo*textureDistorted(LightSourcesTexture,
                             texCoord, FlareDir, ChromaticDistortionVector);
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -