SelfDXD von Martin Pyka
SelfDXD --- DirectXGraphic --- Texturen
Texture Blending
Einleitung
Die Arbeitsweise
Lightmaps
Alphablending
Bumpmaps
Beleuchtete Polygone
Multipass Texture Blending

Einleitung

Texture Blending, das ist einer dieser Effekte, die in Spielen viel her machen können. Texture Blending kommt immer dann zum Einsatz, wenn mehr als eine Textur auf einem Polygon angewendet werden sollen. Die Rede ist ganz bewusst von anwenden und nicht von darstellen, denn nicht immer müssen alle Texturen, die auf einem Polygon angewendet werden auch dargestellt werden. Bei Lightmaps oder Bumpmaps zum Beispiel werden die Informationen einer Textur nur dazu genutzt, um eine andere Textur zu manipulieren.

Da die Möglichkeiten des Texture Blendings extrem gross sind, werden wir uns in diesem Kapitel lediglich mit der grundlegenden Arbeitsweise beschäftigen und anhand von Beispielen einige interessante Effekte vorführen. Mit dem Grundlagenwissen, dass Sie in diesem Kapitel erwerben, werden Sie in der Lage sein, mit Texturen experimentieren zu können und erstaunliche Effekte zu erzielen, wie sie Sie aus vielen kommerziellen Titeln bereits gewohnt sind. Nebenbei werden Sie, so hoffe ich, feststellen, dass alles wesentlich einfacher ist, als ursprünglich vermutet.


Die Arbeitsweise

Beim Texture Blending spielen die bereits erwähnten Texturestages eine Rolle. In jedem Stage können Sie zwei Farbargumente definieren und angeben, wie diese miteinander zu einer Farbe verrechnet werden sollen. Die Farbargumente können sich auch auf vorhergehende Texturestages beziehen, so dass sie mehrere Stages miteinander verknüpfen können. Durch diese Technik lässt sich ein Blendingeffekt erzielen. Die Arbeitsweise verdeutlicht nochmal folgende Grafik.

Argument1 und Argument2 in jedem Stage geben an, woher die Farben genommen werden sollen. Mögliche Quellen sind zum Beispiel die Diffusewerte der Vertices, die Textur des jeweiligen Stages oder, wie in Stage 2, das Resultat des vorhergehenden Stages. Eine Liste aller Argumente bieten Ihnen die Textureargument-Flags. Mit einem Operator bestimmen Sie, wie beide Farbargumente miteinander verrechnet werden sollen. Im oberen Beispiel wird im ersten Stage eine Durchschnittsfarbe gebildet. Diese wird als ein Farbargument im zweiten Stage weiterverwendet. Im zweiten Stage werden beide Farbargumente miteinander addiert. Das Ergebnis der Berechnungen im zweiten Stage kann dann in einem dritten Stage weiter verwendet werden usw..

Die Farbargumente werden miteinander verrechnet, indem auf die Farbwerte jedes Texels beider Argumente die entsprechende Operation ausgeführt wird. Wenn also im zweiten Stage eines der Texel die RGB-Werte (1.0, 0.5, 0.0) hat, was Orange entspricht, und im zweite Argument die RGB-Werte (0.4, 0.4, 0.4), dann erhalten wir durch den Operator Addition als Rückgabewerte (1.0, 0.9, 0.4), diesen hellen Gelbton, wie er im Diagramm zu sehen ist. Theoretische Werte über 255 werden auf diese Obergrenze wieder herabgesetzt.

Für jeden Stage ergeben sich also prinzipiell drei Einstellungen, die Sie verändern können:


// Blending-Einstellungen für das erste Stage
  d3ddev8.SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_DIFFUSE );   // Argument 1
  d3ddev8.SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TEXTURE );   // Argument 2
  d3ddev8.SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG2 ); // Operation

// Blending-Einstellungen für das zweite Stage
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE );

D3DTSS_COLORARG1, D3DTSS_COLORARG2 und D3DTSS_COLOROP sind Konstanten aus den Texturestagestate-Flags. Als Argumente stehen Ihnen die Konstanten der Textureargument-Flags zur Verfügung. Alle Farboperationen finden Sie in den D3DTEXTUREOP-Flags. D3DTA_CURRENT ist dabei die Verknüpfung, mit der Sie das Resultat aus dem vorhergehenden Stage in das aktuelle Stage mit einbinden. Würde im zweiten Stage (also das Stage mit der Nummer 1) als erstes Farbargument D3DTA_DIFFUSE enthalten, würde man in der Anwendung auch nur die zweite Textur in den Farben der Vertices sehen.

Das ist im Prinzip alles, was sich hinter dem Texture Blending befindet. Mit diesem einfachen Operatoren- und Argumentensystem lassen sich alle Blendingeffekte erzielen. Im folgenden seien ein paar dieser Effekte vorgestellt.


Lightmaps

Wenn Sie in einer Anwendung Point- oder Spotlights verwenden, deren Position sich nicht verändert, empfiehlt es sich, die Beleuchtung einer Textur nicht über die Lichtquelle berechnen zu lassen, sondern eine Lightmap zu verwenden. Eine Lightmap ist eine Textur, dessen Bildinformationen dazu genutzt werden, um die Helligkeit der darzustellenden Textur zu bestimmen. Das folgende Schaubild verdeutlicht das Prinzip:

Je heller der Texel einer Lightmap ist, desto stärker erscheint der Texel der anzuzeigenden Textur in seinen Farben. Ist der Texel der Lightmap dunkler, wird auch der Texel der sichtbaren Textur dunkler dargestellt. Wenn tex1 ein Textur-Interface mit der anzuzeigenden Textur ist, und tex2 das Interface mit der Lightmap, dann könnte der Code für diesen Effekt so aussehen:


  d3ddev8.SetTexture( 0, tex1 );
  d3ddev8.SetTexture( 1, tex2 );

  d3ddev8.SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );

  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_MODULATE );

Hier wird also durch das Multiplizieren (D3DTOP_MODULATE) beider Texturen im zweiten Stage eine Textur unter Einbeziehung einer Lightmap dargestellt.


Alphablending

Beim Alphablending sieht man mehrere Texturen gleichzeitig. Sie werden sozusagen ineinander geblendet. Wie stark eine Textur in eine andere eingeblendet wird, können Sie selbst bestimmen.

Im wesentlichen werden beim Alphablending bestimmte Anteile aus beiden Texturen miteinander addiert. In DirectXGraphics haben Sie mehrere Möglichkeiten um diesen Effekt zu erzielen:


  d3ddev8.SetTexture( 0, tex1 );
  d3ddev8.SetTexture( 1, tex2 );

  d3ddev8.SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );

  d3ddev8.SetRenderState( D3DRS_TEXTUREFACTOR, D3DCOLOR_RGBA( 0, 0, 0, 127) );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_BLENDFACTORALPHA );

Dies ist wohl die schnellste und einfachste Methode. Sie legen in den Renderstates einfach einen Alphawerten fest, der das Verhältnis zwischen der ersten und zweiten Textur festlegt. Bei 255 wird die erste Textur (tex1) zu 100% dargestellt. Bei 0 die zweite. Die Rot-, Grün-, und Blauwerte spielen hierbei keine Rolle. Entscheidend ist, dass Sie als Farboperation D3DTOP_BLENDFACTORALPHA verwenden.

Der Nachteil dieses Verfahrens ist allerdings, dass die zweite Textur überall zu gleichen Teilen auf die erste Textur geblendet wird. Mit dem folgenden Code benutzen Sie die Alphawerte der Vertices, um die Texturen zu überblenden.


  d3ddev8.SetTexture( 0, tex1 );
  d3ddev8.SetTexture( 1, tex2 );

  d3ddev8.SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );

  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_BLENDDIFFUSEALPHA  );

Diese Einstellung erlaubt es Ihnen also, bestimmte Bereiche Ihrer Polygone zu unterschiedlichen Verhältnissen mit Ihren Texturen darzustellen. Davor müssen Sie natürlich die entsprechenden Alphawerte Ihren Vertices übergeben haben.

Wenn Sie wirklich texelgenau entscheiden wollen, welcher Texel wie transparent sein soll, dann können Sie auch die Alphawerte einer Textur für das Alphablending verwenden.


  d3ddev8.SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_BLENDTEXTUREALPHA  );

In diesem Beispiel werden die Alphawerte der Textur aus dem aktuellen Stage übernommen. Mit dem folgenden Befehl werden die Alphawerte aus der Textur übernommen, die mit D3DTA_CURRENT übernommen wird.


  d3ddev8.SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_BLENDCURRENTALPHA );

Bumpmaps

Bumpmaps werden dazu verwendet, rauhe, unebene Oberflächen in Abhängigkeit von den Lichtverhältnissen und dem Betrachtungswinkel realistischer erscheinen zu lassen. So brauchen zum Beispiel in einer rissigen Steinwand die Schatten nicht in die Textur eingearbeitet werden. Sie können zur Laufzeit in Abhängigkeit zum Licht berechnet werden, in dem man eine sogenannte Bumpmap, eine Graustufentextur, verwendet, in der Höhen und Tiefen der Steinwand abgebildet sind. Der folgende Screenshot aus der SDK verdeutlicht es nochmal:

Bei der linken Weltkugel wurde die Textur einfach über das Modell gezogen und angezeigt. Dabei wirkt die Kugel spiegelglatt. Im rechten Erdball kam zusätlich noch eine Bumpmap zum Einsatz. Beachten Sie, wie das Spekularlicht reflektiert wird. Man hat den Eindruck eines unebenen (verschrumpelten) Erdballs.

Für den Einsatz von Bumpmaps können bis zu 3 Texturen zum Einsatz kommen:

  1. die anzuzeigende Textur
  2. die Bumpmap
  3. eine Environmentmap (optional)

Damit benötigen Sie mindestens zwei Texturestages. Die Einstellungen des ersten Texturestages stehen Ihnen weitgehend frei. Hier ein Beispiel:


  d3ddev8.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
  d3ddev8.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
  d3ddev8.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);

Im nächsten Stage folgt dann die Bumpmapmap. Dabei haben Sie zwei Farboperatoren zur Auswahl, mit denen Sie entscheiden, wie die Bumpmap angewendet werden soll.


  d3ddev8.SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
  d3ddev8.SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);
  d3ddev8.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_BUMPENVMAP);

Mit D3DTOP_BUMPENVMAP reflektieren Sie eine Environmentmap die im nächsten Stage folgt. Licht fliesst hier also nicht mit in die Berechnung ein.


  d3ddev8.SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
  d3ddev8.SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);
  d3ddev8.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_BUMPENVMAPLUMINANCE);

Mit D3DTOP_BUMPENVMAPLUMINANCE werden sowohl die Environmentmap als auch Licht gemäss der Struktur der Bumpmap korrekt wiedergegeben. Wenn Sie nun im zweiten Stage die Environmentmap weglassen, haben Sie nur die Lichtberechnung, wie im oberen Screenshot.

Auch für die Bumpmap gibt es eine Transformationsmatrix, allerdings ist diese nur 2x2 Felder gross. Sie können diese Matrix direkt über die Texturestagestate-Flags ansprechen, wie im folgenden Beispiel:

function F2DW(f: Single): DWORD;
begin
  Result:= PDWord(@f)^;
end;

begin
  d3ddev8.SetTextureStageState(1, D3DTSS_BUMPENVMAT00, F2DW(1.0));
  d3ddev8.SetTextureStageState(1, D3DTSS_BUMPENVMAT01, F2DW(0.0));
  d3ddev8.SetTextureStageState(1, D3DTSS_BUMPENVMAT10, F2DW(0.0));
  d3ddev8.SetTextureStageState(1, D3DTSS_BUMPENVMAT11, F2DW(1.0));

Standardmässig sind alle Bumpmap-Matrizenwerte auf 0.0 gestellt, weshalb die Matrix am Anfang eingestellt werden muss.

Darüber hinaus können Sie noch einstellen, wie stark das Licht bzw. die Environmentmap vom 3D-Körper wiedergegeben wird. Dazu stehen Ihnen folgende zwei Parameter zur Verfügung:

function F2DW(f: Single): DWORD;
begin
  Result:= PDWord(@f)^;
end;

begin
  d3ddev8.SetTextureStageState(1, D3DTSS_BUMPENVLSCALE,  F2DW(4.0));
  d3ddev8.SetTextureStageState(1, D3DTSS_BUMPENVLOFFSET, F2DW(0.0));

Mit D3DTSS_BUMPENVLSCALE geben Sie an, wie stark das Licht bzw. die Environmentmap reflektiert werden soll und mit D3DTSS_BUMPENVLOFFSET wie stark das Licht bzw. die Environment der normalen Textur grundsätzlich beigemischt werden soll.


Beleuchtete Polygone

Beleuchtung? Hatten wir das nicht schonmal? Ja, im Kapitel Licht und Schatten haben wir gelernt, wie wir unsere Polygone mit realistischen Licht und Schattenverhältnissen ausstatten. Wenn Sie jedoch die Texturestagestates dazu verwenden, mehrere Texturen miteinander zu kombinieren, gibt es ein Problem: das Polygon wird plötzlich nicht mehr beleuchtet (ausgenommen Texturen mit Bumpmaps). Der Grund dafür liegt darin, dass die Beleuchtung der Polygone zu den Bestandteilen der Texturargumente gehört. Es handelt sich hierbei um das Argument D3DTA_DIFFUSE. Wir müssen es also seperat einbauen. Dies könnte dann so aussehen:


  d3ddev8.SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );

  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG1, D3DTA_CURRENT );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLORARG2, D3DTA_TEXTURE );
  d3ddev8.SetTextureStageState( 1, D3DTSS_COLOROP, D3DTOP_BLENDDIFFUSEALPHA  );

  d3ddev8.SetTextureStageState( 2, D3DTSS_COLORARG1, D3DTA_CURRENT );
  d3ddev8.SetTextureStageState( 2, D3DTSS_COLORARG2, D3DTA_DIFFUSE );
  d3ddev8.SetTextureStageState( 2, D3DTSS_COLOROP, D3DTOP_MODULATE );

Beachten Sie hierbei, dass ältere Grafikkarten nicht mehr als zwei Texturestages unterstützen und diese Befehle deshalb wirkungslos sein könnten. Unterstützt Ihre Grafikkarte dieses Feature, so werden in diesem Beispiel zwei Texturen überblendet (abhängig von den Alphawerten der Diffusefarbe der Vertices) und diese dann mit den Diffuse-Werten der Vertices multipliziert, wodurch sich der Lichteinfall auf die Helligkeit der Vertices auswirkt.


Multipass Texture Blending

Zum Schluss möchte ich Ihnen noch eine ganz andere Möglichkeit des Blendings vorstellen. Das so genannte Multipass Texturing. Hierbei werden die Blend-Effekte nicht über die Stages erreicht sondern über Einstellungen, die Sie innerhalb der Renderstates festlegen. Durch mehrfaches Rendern des selben Objektes mit einer anderen Textur können Sie die gleichen Effekte erzielen. Dabei legen Sie bei jedem Rendervorgang lediglich fest, wie die neu hinzukommenden Pixel mit den bereits vorhandenen Pixeln addiert werden sollen. Hierbei spricht man zum einen vom Sourceblendfactor, wobei Source das zu rendernde Objekt meint, und vom Destinationblendfactor, bei dem mit Destination die bereits vorhandene Szene im Buffer gemeint ist. Aus den beiden Pixeln wird nach folgendem Term der neue Pixel gebildet:

NeuerPixel = DestPixel + DestBlendFactor  +  SrcPixel x SrcBlendFactor

Nochmal zum Verständnis: bei der bisher besprochenen Texturblendingmethode verteilen wir alle Texturen auf unterschiedliche Stages und rendern das Objekt einmal. DirectXGraphics bezieht in diesem Fall alle Texturen in diesen einen Rendervorgang mit ein. Beim Multipass Texturing rendern wir das Objekt zunächst mit nur einer Textur. Danach setzen wir in den ersten Stage eine neue Textur und rendern das Objekt nochmal. Mit SrcBlendFaktor und DestBlendFactor stellen wir aber diesmal ein, wie der bereits vorhandene Pixel mit dem neuen Addiert werden soll. Die Grafik verdeutlicht es nochmal:

Der Vorteil dieser Technik ist: Sie können beliebig oft Objekte hintereinander mit Multipass Rendern. Im Gegensatz zum Texturestageblending sind Sie dabei nicht auf die Fähigkeiten der Grafikkarte angewiesen. Der Nachteil dieser Technik: es ist um einiges zeitaufwendiger, da die Vertices in jedem Durchgang neu gerendert werden müssen.

Im Prinzip können Sie alle Blendingoperationen, wie wir Sie oben besprochen haben auch mit der Multipasstechnik machen. Dazu sind folgende Schritte notwendig:

1. Das Objekt mit der ersten Textur rendern

  d3ddev8.SetTexture( 0, tex1 );
  d3ddev8.SetStreamSource( 0, vb, sizeof( TVERTEX ) );
  d3ddev8.SetVertexShader( D3DFVF_TVERTEX );
  d3ddev8.DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );

2. Eine neue Textur in den ersten Stage stellen

  d3ddev8.SetTexture( 0, tex2 );

3. Source- und Destination-Blendfaktoren einstellen

  d3ddev8.SetRenderState(D3DRS_SRCBLEND,  D3DBLEND_INVSRCCOLOR);
  d3ddev8.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);
  d3ddev8.SetRenderState(D3DRS_ALPHABLENDENABLE, 1);

4. Das Objekt nochmal rendern

  d3ddev8.DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
  d3ddev8.SetRenderState(D3DRS_ALPHABLENDENABLE, 0);

Die Schritte 2, 3 und 4 können Sie beliebig oft wiederholen.

Zwei Dinge müssen wir noch erklären. Zum einen müssen Sie, wenn Sie Multipass nutzen wollen, dies explizit einstellen. Dies geschieht mit dem Renderstate D3DRS_ALPHABLENDENABLE. Wenn Sie Multipass nicht mehr nutzen, sollten Sie es immer abstellen, da es sonst in der Render-Pipeline wertvolle Zeit verschlingt. Das zweite sind die eigentlichen Blendingoperatoren. Mit D3DRS_SRCBLEND legen Sie den Faktor fest, der für das zu rendernde Objekt benutzt werden soll und mit D3DRS_DESTBLEND den Faktor für die bereits gerenderte Szene im Buffer. Die Faktoren, die sie als zweiten Parameter angeben, sind Konstanten aus der D3DBLEND-Aufzählung

Für normales Alphablending verwenden Sie folgende Einstellungen zwischen den beiden Rendervorgängen:


  d3ddev8.SetRenderState(D3DRS_SRCBLEND,  D3DBLEND_INVSRCCOLOR);
  d3ddev8.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);

Hiebei werden beide Texturen zu gleichen Teilen vermischt.

Diese Rendertechnik erlaubt Ihnen ganz neue grafische Effekte. So können Sie zum Beispiel im zweiten Rendervorgang ein ganz anderes Objekt darstellen. In der Praxis könnte man zum Beispiel zum einen einen Raum rendern, in den man hineinschaut, und danach eine leicht blau getönte Glasscheibe, durch die man den Raum sieht. Der untere Screenshot zeigt zwar keinen Raum aber dafür die Scheibe.

Multipass ist damit also das typische Werkzeug um zum Beispiel transparente Effekte von Glas, Feuer oder Rauch darzustellen. Doch mit dieser Technik können Sie noch viel mehr anfangen. Um die Vielfalt der Möglichkeiten besser begreifen zu können, liegt dieser Dokumentation im Verzeichnis /directxgraphics/demo/ des SelfDXD-Verzeichnisses ein Kastenprogramm bei, den Multipass Texturing Kasten (Achtung: nur in der Offline-Version).

In diesem Programm können Sie zur Laufzeit alle Blending-Einstellungen in Ruhe ausprobieren. Der Screenshot zeigt nur eine der vielen ungewöhnlichen Möglichkeiten, zwei Texturen miteinander zu kombinieren. Ersetzen Sie auch ruhig mal die Grafikdateien, um mit anderen Farben und Bildern die Effekte zu testen.