Mit "Clamp" meine ich extra Zeilen im Pixelshader. Das kostet Laufzeitperformance, vermeidet aber jegliche Skalierungen. Das Ding beim bilinearen Filter ist ja, dass er benachbarte Pixel ranzieht. Frei nach D3D9-Rasterizer Rules:
Code: Alles auswählen
0 1 2 3 4
+---+---+---+---+
|...|...|...|...|
+---+---+---+---+
Das sollen vier Texel einer 4x1-Textur sein. Wenn ich korrektes Subpixel Rendering haben will, also meine Sprites auch mit Fließkomma-Pixelkoordinaten auf dem Bildschirm platzieren will, muss ich einen exakten Bereich auf der Textur angeben, der gemappt werden soll. Im obigen Fall ist der Bereich zum Beispiel von (0.0, 0.0) bis (4.0, 1.0). Das bedeutet nach D3D9-Rasterregeln die linke obere Ecke des ersten Texels bis zur rechten unteren Ecke des letzten Texels.
Jetzt wird es kurz hässlich: Direct3D9 hat noch eine seltsame alte Regel, wonach Bildschirmkoordinaten nicht so schön sauber adressiert sind wie Texturkoordinaten. Stattdessen ist (0.0, 0.0) die MITTE des linken oberen Bildschirmpixels. Wenn wir genau Texel auf Pixel mappen wollen, müssen wir unser Sprite also von (-0.5, -0.5) bis (3.5, 0.5) legen. Daher kommt auch die Empfehlung, 2D-Zeichenbefehle immer einen halben Pixel nach links oben zu verschieben. Das tun wir jetzt mal. Dann würde der Pixelshader (ohne AA) genau vier mal laufen, jeweils für (0.0 + x, 0.0) - also für die Mitte der Bildschirmpixel. Die interpolierte Texturkoordinate, die im Pixelshader jeweils ankommen würde, wäre auch genau die Mitte des jeweiligen Texels, in Zahlen (0.5 + x, 0.5). Wir können in dieser perfekten Situation also den bilinearen Filter an oder ausschalten, es würde keinen Unterschied beim Ergebnis machen.
Wir wollen aber bilinear filtern! Es sieht nämlich besser aus, sobald man skaliert, rotiert und halt seine Sprites um Subpixel verschoben platzieren möchte. Und da kommt jetzt das Problem des Filters. Wieder am Beispiel: wir platzierung unser Sprite um 0.2 Pixel nach rechts unten verschoben. Nach den albernen D3D9-Regeln für Bildschirmpixel wäre das also (-0.3, -0.3) bis (3.7, 0.7). Wenn die Grafikkarte diese Dreiecke rastert, läuft unser Pixelshader auf Backbuffern ohne AA wieder viermal, nämlich wieder jeweils für die Mitte des jeweiligen Bildschirmpixels. Die interpolierte Texturkoordinate im PixelShader wäre also nach links oben verschoben! Nämlich (0.3 + x, 0.3). Jetzt würde der bilineare Filter zuschlagen, er würde anteilig ein bisschen Farbe der Texel links und drüber reinmischen. Und links bzw. drüber liegen (nach dem Texturkoord-Wrapping) andere Spritegrafiken auf der Textur! Die GPU filtert uns da also Unsinnsfarben rein.
Dafür gibt es jetzt verschiedene Lösungen. Die billigste: setze die Texturkoord-Methode auf CLAMP anstatt WRAP. Damit macht die Grafikkarte das Clampen für uns, der bilineare Filter filtert in der Mitte des Sprites korrekt und am Rand holt er keinen Grafikmüll mit rein. Das Problem an dieser Lösung ist, dass man für jedes einzelne Sprite eine Textur mit der exakten Größe aufmachen muss. Das kostet beim Rendern einen riesigen Haufen Performance, permanent die Texturen wechseln zu müssen.
Eine alternative Lösung ist es, einfach den Texturbereich zu reduzieren. Wir wissen, dass der Texturfilter bei <0.5 beginnt, den linken Nachbar mit reinzusamplen. Also reduzieren wir unseren Texturbereich um jeweils einen halben Texel nach innen! Aus (0.0, 0.0) / (4.0, 1.0) würde dann (0.5, 0.5) / (3.5, 0.5). Clever, oder? Nein, ist es leider nicht. Diese Methode kann man prima machen, wenn man in 3D arbeitet. Z.B. wenn man Pages einer großen Textur auf ein Terrain mappt. In 2D aber wollen wir ja (unskaliert zumindest) auch genau die Texel der Ausgangstextur auf dem Bildschirm sehen! Und bei dieser Reduktion des Texturbereiches schmiert es uns die übrigen Texel breit, weil wir nur noch einen Texturbereich von (3.0, 0.0) auf einen Bildschirmbereich von (4.0, 1.0) strecken. Das sieht matschig aus.
Da kommt nun die dritte Lösung ins Spiel, die wir bei Splatter einsetzen: wir schreiben ein paar zusätzliche Befehle in den Pixelshader, die den Texturzugriff auf den Innenbereich des Texturteils beschränken, ohne die Texturkoordinaten zu verändern. Als Code sieht das so aus:
Code: Alles auswählen
struct PixelEingabe
{
float4 mFarbe : COLOR0;
float2 mTexKoords : TEXCOORD0;
float4 mMinMaxKoords : TEXCOORD1;
};
sampler2D Textur;
float4 main( const PixelEingabe rein) : COLOR0
{
float2 texk = clamp( rein.mTexKoords.xy, rein.mMinMaxKoords.xy, rein.mMinMaxKoords.zw);
float4 farbe = tex2D( Textur, texk);
farbe *= rein.mFarbe;
farbe.rgb *= farbe.a; // für Premultiplied Alpha
return farbe;
}
Mit D3D9 müssen wir dazu natürlich den Texturbereich mit reinreichen - wir machen das als zusätzliches Vertex-Attribut, dass der VertexShader einfach durchreicht und der Pixelshader benutzt, um die interpolierte Texturkoordinate zu clampen. Unter D3D10 könnte man wahrscheinlich im GeometrieShader was zaubern, was diesen Clamp-Texturbereich aus den Texturkoordinaten des Sprites extrahiert, so dass man den nicht nochmal separat reinreichen muss. Der Vorteil ist, dass man bei exakter Ausrichtung des Sprites genau Texel auf Pixel abbilden kann. Und sobald man mit Rotation, Skalierung oder AntiAliasing arbeitet, stellt der Pixelshader trotzdem sicher, dass man keinen Grafikmüll aus benachbarten Texturbereichen ansaugt. Der Nachteil ist natürlich, dass es zusätzliche Fillrate kostet, und dass es mindestens Shadermodell 2.0 erfordert.
Da wir bei Splatter und besonders bei den Splitterwelten quasi eh nur CPU-limitiert sind, waren uns diese Nachteile recht. Zumal wir bei Splatter den fillrate-intensiven Teil (die Beleuchtung) nur in Viertel-Bildschirmauflösung gemacht haben. Ob das auch für eure Szenarien auch funktioniert, müsst ihr selber abschätzen.