Tiefenschärfe (DOF , Depht of Field) Effekt mit OGRE3D (1.6) und CG

Inhalt
1. Einleitung
2. Tiefenschärfe in der Theorie
3. Post-Processing in OGRE
4. Implementierung – DOF in der Praxis
5. Schlussbetrachtung

1. Einleitung
Endlich ein weiterer Artikel, diesmal über Tiefenschärfe mit Ogre3D und CG, was sich in der Abstimmung als Favorit herausgestellt hat. Tiefenschärfe ist ein optischer Effekt, der die Qualität eines Gerenderten Bildes stark verbessert, allerdings durch den Spieler schwer zu kontrollieren, und dadurch manchmal Problematisch ist.

Hier sollte ein Bild über Tiefenschärfe sein

Tiefenschärfe mit 3 Verschiedenen Fokusebenen

2. Tiefenschärfe in der Theorie
Alles schön und gut, aber was ist Tiefenschärfe überhaupt ? Nun, probieren sie es einfach aus: Sie nehmen einen Stift in die Hand, und halten ihn ca. 15cm von ihren

Augen entfernt. Dann fokussieren sie ihn, und sie werden merken, dass andere Objekte weiter weg unscharf werden ! An Abbildung 1 kann man das auch gut erkennen, der Fokus wandert mit jedem Teilbild um eine Einheit nach hinten, die anderen beiden werden jeweils unscharf.

Doch wie bekommen wir diesen Effekt in unserem Programm möglichst gut “gefaket” ? Zunächst gehen wir davon aus, dass wir, wie

Hier sollte ein Bild sein

Hier zu sehen sind die Unschärfe zu sehen

in Abblidung 2 zu sehen eine Fokusebene haben, in der Alles Perfekt scharf abgebil

det wird. Ausserdem einen Bereich, in dem die Schärfe linear abnimmt, bis sie komplett unscharf ist.

Somit lässt sich die Unschärfe ganz einfach aus “(Position.z – FokusebeneZ) / Fokusbereich” berechnen.

3. Post-Processing in OGRE
da DOF ein Post-Processing Effekt ist, müssen wir erst einmal das zum laufen zu bekommen:
- Die normale Szene, mit im Alphachannel codiertem Unschärfewert (mehr dazu in Absatz 4) auf ein seperates Rendertarget rendern
- Das Rendertarget mit speziellem Material rendern

Zuerst wird also das Rendertarget erstellt, dazu rufen wir die createManual Methode des Texturmanagers auf, mit der wir das 2. Rendertarget erstellen. Wichtig ist hierbei zu beachten, dass das Format des Rendertargets einen Alphakanal haben muss. :

 // Das Rendertarget Erstellen
 Ogre::TexturePtr RTT  = Ogre::TextureManager::getSingleton().createManual ("SceneBuffer", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
Ogre::TEX_TYPE_2D, mWindow->getWidth (), mWindow->getHeight () /* Höhe und Breite */, 0, Ogre::PF_A8B8G8R8 /* Format, wichtig für DOF: mit
Alphachannel */ , TU_RENDERTARGET /* soll ein Rendertarget werden */);
 Ogre::RenderTexture* SceneBuffer = RTT->getBuffer( )->getRenderTarget ();

Zusätzlich müssen wir noch einige Einstellungen vornehmen. (Genaueres ist dem Render-To-Texture Tutorial in der OGRE-Wiki zu entnehmen).
Das Rendertarget sollte sich natürlich von selbst updaten, und überhaupt aktiv sein ;) . Ausserdem fügen wir einen Viewport hinzu, der bestimmt, was in das Rendertarget gerendert wird, und schalten Overlays aus. Dies ist wichtig, damit ein eventuelles GUI nicht mit dem DOF-Prozess unterzogen wird.

// Einstellungen vornehmen
SceneBuffer->setAutoUpdated (true);
SceneBuffer->setActive (true);
SceneBuffer->addViewport (mCamera, 0);
SceneBuffer->getViewport (0)->setOverlaysEnabled (false);

Weiter geht es damit, dass wir das Rendertarget selbst rendern, und das mit dem Tiefenschärfe Shader. Dazu müssen wir ein neues Objekt vom Typ Rectangle2D erstellen (nicht vergessen den mit new reservierten Speicher wieder freizugeben), und Einstellungen bzgl. Grösse, Bounding Box und natürlich dem Material Vornehmen.

// Ein Rectangle erstellen, mit Material für Tiefenschärfe, und das hinzufügen
rect = new Ogre::Rectangle2D (true);
// Positionierung und Grösse auf dem Bildschirm festlegen
rect->setCorners(-1, 1, 1.0, -1);
// Bounding Box muss gesetzt werden
rect->setBoundingBox (Ogre::AxisAlignedBox (-100000.0*Vector3::UNIT_SCALE, 100000.0*Vector3::UNIT_SCALE));
// Und unser Material Zuweisen
rect->setMaterial ("DephtOfField");
mSceneMgr->getRootSceneNode ()->createChildSceneNode ("Rect")->attachObject (rect);

Und damit wäre schon einmal ein Teil der Arbeit getan. Weiter geht es mit dem PostprocessingMaterial. Um unser Rendertarget als Textur für das DOF Material anzugeben, müssen wir einfach eine Texture-Unit mit “texture”-Attribut – dass den gleichen Namen trägt, den wir unserem Rendertarget vorher gegeben haben – erstellen. Am besten Culling noch Deaktivieren.

material DephtOfField
{
 technique
 {
 pass
 {
 // Culling ausschalten
 cull_hardware none
 cull_software none
 depth_check off

 // Die Bisherige Szene
 texture_unit
 {
 tex_address_mode wrap

 texture SceneBuffer
 }

 fragment_program_ref DephtOfField_FP
 {

 }
 }

 }

}

Nun die Fragment-Program-Deklaration. Da dürfte allerdings nichts besonderes drinnen sein:

fragment_program DephtOfField_FP cg
{
 source SimpleSmooth.cg
 entry_point SSFragment
 profiles ps_2_0
 default_params
 {

 }

}

Damit hätten wir eine Gute Postprocessing Grundlage.

4. Implementierung – DOF in der Praxis
Jetzt stellt sich die Frage, wie wir dem Postpro Shader unseren Unschärfewert mitgeteilt bekommen. Dafür eignet sich der Alphakanal unserers Rendertargets sehr gut. Also sollten wir in jeden unserer Materialshader unserer Meshes folgende Berechnung einfügen.

const float FocalZ  = 100.0f;
const float Radius = 350.0f;
float4 ResultColor = ( usereFarbe,   abs( (eWorldPosition.z - FocalZ) / Radius ) );

Und nun folgt die Implementierung unseres DOF-Shaders:
Dazu brauchen wir erst einmal ein Paar Samples, (umso mehr, umso besser ist die Grafische Qualität der Unschärfe). Diese bekommt man am besten ohne Große Mühe mit diesem Tool. Des weiteren lesen wir den Unschärfewert aus dem Alphakanal des Rendertargets aus.
An unsere Textur kommen wir in Zeile 2. Die erste texture_unit ist also auch im ersten Textur-Register (s0) gespeichert.

float4 SSFragment ( in    float2     eSamplerTexcoord : TEXCOORD0,
 uniform sampler SceneTexture : register (s0)
 ) : COLOR
{
 const float2 Samples[10] = {
 {-0.48164210f, 0.63774880f},
 {-0.75966840f,-0.50366960f},
 { 0.34809640f,-0.14834620f},
 { 0.96111000f,-0.16738010f},
 { 0.21908510f,-0.96834350f},
 { 0.15818150f, 0.70767760f},
 {-0.81477860f,-0.19214370f},
 { 0.51399410f,-0.68631670f},
 { 0.48325500f, 0.23895880f},
 { 0.67731140f,-0.33620170f}
 };

 float Transp = tex2D (SceneTexture, eSamplerTexcoord).a;
 float3 EndColor = {0.0f, 0.0f, 0.0f};

Wir erreichen unserer Unschärfe ganz einfach dadurch, dass wir die Umliegenden Pixel auch Samplen, je nachdem, wie Unscharf unser Pixel werden soll.

 for (int i = 0; i<10; i++)
 {
 float2 Corod = eSamplerTexcoord + (Samples[i] * Transp / 200) ;
 EndColor += tex2D (SceneTexture, Corod).xyz;
 }

 return float4 (EndColor[0] / 10, EndColor[1] / 10, EndColor[2] / 10, 1.0f);
}

Nun gibt es noch ein Problem mit diesem Shader: Der Übergang zwischen Unscharfen und Scharfen Pixeln. Dies lässt sich jedoch extrem simpel lösen, indem man die Samples verwirft, deren Unschärfewert grösser ist, als der unseres aktuellen Pixels auslässt.
Das zu implementieren liegt an euch. ;)

5. Schlussbetrachtung

Dieser Effekt wertet so manches Programm noch einmal auf.