ich habe ein Rudel Shader geschrieben, die für ein 2D-Spiel aus der Vogelperspektive Lichtquellenschatten umsetzen sollen. Der Grundgedanke ist, dass ich pro Bildschirmpixel eine fiktive Oberflächenhöhe ausrendere, anhand derer ich errechnen kann, wie weit die Schattenkante davon trägt. Also in etwa so:
Um jetzt nicht jeden Pixel einzeln fragen zu müssen, benutze ich den Trick exponentiell skalierter Sample-Strecken, den sie damals bei Crysis für den Radial Blur benutzt haben: Ich sample zuerst die ersten 8 Samples vom Texel zur Lichtquelle und berechne jeweils Höhe und Sinktempo der Schattenkante. Danach nehme ich in einem weiteren Pass wieder 8 Samples, dieses Mal aber auf die vierfache Länge gestreckt, so dass ich garantiert einen oder zwei der Samples vom letzten Pass erwische. Danach folgt noch ein Pass mit sechszehnfacher Länge und schlimmstenfalls noch einer mit 64facher Länge, so dass ich mit 4 Passes und 32 Samples eine Strecke von bis zu 512 Texeln sicher abdecken kann.
Nur zur Klarheit: das funktioniert so auch prächtig. Ich habe allerdings jetzt zwei Probleme.
a) Die dämliche Grafikkarte clampt mir den Shader-Output auf 0 bis 1, obwohl ich (zu Testzwecken) in ein fp16-Target schreibe. Wie kann ich ihm das abgewöhnen? Google gibt mir dazu gar keine Info, anscheinend bin ich der Einzige mit diesem Problem. Ich weiß allerdings auch von früheren Splitterwelten-Zeiten, dass ich auch da die ShadowMap-Tiefen auf 0..1 skalieren musste, obwohl ich in ein fp32-Target gerendert habe. Die DX-Doku meint aber, dass sowas nur bei wertebeschränkten Targets passiert. Was ist hier faul?
b) Die Performance ist mies. Schlechter als erwartet jedenfalls. Leider macht NVPerfHUD bei mir nur Grütze, für den ist meine GPU immer vollständig im Idle. Zahlenmäßig sieht es so aus, dass ich etwa 800 FPS ohne Schatten habe und bei fünf Lichtquellen mit Schatten noch bei 120 fps bin. Das ist enttäuschend, da ich den ganzen Schattentrick eh bei 1/4 Auflösung betreibe und damit jede ShadowMap gerade mal noch 150 bis 200 Pixel im Quadrat ist. Bei fünf Lichtern und 2 Verlängerungs-Passes plus ein Init-Pass bin ich damit bei 25 Samples pro Schatten-Texel und demzufolge bei 150x150x5x25 == 2,8 Millionen Samples und nochmal vielleicht 3 MatheOps pro Sample - ich nutze damit bei 120 fps grade mal 350 Millionen Samples von den geschätzt 20 Milliarden Texturzugriffen, die meine GeforceGTX460 pro Sekunde zu bieten hat.
Zur Anschauung hier der Shadercode zu den Verlängerungs-Passes - der initiale Shadow-Pass sieht sehr ähnlich aus und der finale Schattenkante-Zu-Helligkeit-Shader ist trivial.
Code: Alles auswählen
struct Vertex
{
float4 mPosition : POSITION0;
float3 mVertexPos : TEXCOORD0; // Vertexposition in Weltkoords
float3 mLichtPos : TEXCOORD1; // Lichtposition in Weltkoords
float3 mLichtParams : TEXCOORD2; // Lichtparameter: XY Strecken-Ausdehnung, Z radiale Ausdehnung
float4 mWeltZuTex : TEXCOORD3; // Weltkoords zu Texkoords: XY Skalierung, ZW Basisaddition
};
// Temp-Textur mit den Werten des letzten Passes: X Intensität 0..1, Y Höhe der Schattenkante, Z Sinktempo der Schattenkantenhöhe, W durchgeschleifte Höhe des Pixels
sampler2D TexSchatten;
// Laenge des Schatten-Teststrahls in Tiles.
float gTestStrahlLaenge;
// ------------------------------------------------------------------------------------------------
// VertexShader
Vertex VertexMain( const Vertex rein)
{
return rein;
}
// ------------------------------------------------------------------------------------------------
// PixelShader
float4 PixelMain( const Vertex rein) : COLOR0
{
float2 vertexZuLicht = rein.mLichtPos.xy - rein.mVertexPos.xy;
float abstandZuLicht = length( vertexZuLicht);
float strecke = min( gTestStrahlLaenge, abstandZuLicht);
float2 richtung = normalize( vertexZuLicht);
// den ersten Sample schreiben wir manuell aus, um den Texturwert an der Stelle dazuhaben
float2 startTexkoords = rein.mVertexPos.xy * rein.mWeltZuTex.xy + rein.mWeltZuTex.zw;
float4 startTexel = tex2D( TexSchatten, startTexkoords);
// weitere 7 Samples entlang des Wegs zum Licht nehmen
[unroll]
for( int a = 1; a < 8; ++a )
{
float entf = strecke * float( a) / 8.0f;
float2 texkoords = (rein.mVertexPos.xy + entf * richtung) * rein.mWeltZuTex.xy + rein.mWeltZuTex.zw;
float4 t = tex2D( TexSchatten, texkoords);
// Höhe dieser Schattenkante bei uns berechnen
t.y = t.y - t.z * entf;
// Werte übernehmen, falls die Schattenkante bei uns höher ist als die bisherige
if( t.y > startTexel.y )
startTexel.xyz = t.xyz;
}
// und das können wir so zurückgeben
return startTexel;
}
Hat jemand eine Idee, wie ich die Performance verbessern könnte?
bye, Thomas