Deferred Shading: Unterschied zwischen den Versionen
(→Idee) |
(→Vor-/Nachteile) |
||
Zeile 13: | Zeile 13: | ||
'''Vorteile:''' | '''Vorteile:''' | ||
− | * Viele Lichtquellen möglich | + | * Viele Lichtquellen bei komplexer Geometrie möglich |
* Postprocessing Effekte (Bloom, Tonemapping, Depth of Field, etc.) können einfach angeschlossen werden, da bereits alle benötigten Informationen aufbereitet vorliegen. | * Postprocessing Effekte (Bloom, Tonemapping, Depth of Field, etc.) können einfach angeschlossen werden, da bereits alle benötigten Informationen aufbereitet vorliegen. | ||
+ | * Lichtberechnung mit reduzierter Auflösung möglich (sinnvoll z.B. bei FSAA) | ||
'''Nachteile:''' | '''Nachteile:''' | ||
* Kein Hardware Anti-Aliasing möglich | * Kein Hardware Anti-Aliasing möglich | ||
− | * Transparente Objekte müssen getrennt behandelt werden | + | * Transparente Objekte müssen getrennt behandelt werden (Abhilfe schafft [[Inferred Lighting]]) |
* Nur auf neueren Grafikkarten mit MRT (Mutliple Render Target) und Floating Point-Textur Support möglich (ab DirectX 9 kompatiblem Grafikchip; OpenGL Version ???) | * Nur auf neueren Grafikkarten mit MRT (Mutliple Render Target) und Floating Point-Textur Support möglich (ab DirectX 9 kompatiblem Grafikchip; OpenGL Version ???) | ||
Version vom 15. März 2010, 11:55 Uhr
Inhaltsverzeichnis
Deferred Shading
Der Befriff Deferred Shading (zu dt. "verzögertes Schattieren") oder auch Deferred Lighting ("verzögerte Beleuchtung") beschreibt eine Technik, mit deren Hilfe eine komplette Lichtquelle mit nur einem einzigen Draw-Call abgebildet werden kann.
Idee
Um eine Szene mit einem traditionellen Forward-Renderer zu beleuchten, wird normalerweise jedes Objekt der Szene mit den entsprechenden Beleuchtungsparametern der Lichtquelle gezeichnet. Die verschiedenen Ergebnisse werden durch additives Blenden akkumuliert. Für jede Lichtquelle muss also jedes Objekt gezeichnet werden. Angenommen wir haben nL Lichtquellen und nO Objekte, dann liegt die Anzahl der Rendercalls bei O(nO * nL). Natürlich ist es möglich mehrere Lichtquellen in einem Pass (z.B. 8 auf einmal) zu berechnen, aber wir gehen von hunderten Lichtquellen aus, es sind also mehrere Passes notwendig und die Geometrie müsste entsprechend mehrfach verarbeitet werden.
Deferred Shading verfolgt einen anderen Ansatz: Anstatt jedes Objekt der Szene für jeden Licht-Pass neu zu renderen, werden zunächst für jeden Pixel alle beleuchtungsrelevanten Daten (z.B. 3D-Position, Normale, Textur-Farbe, etc.) in ein oder mehrere Rendertargets (oftmals auch G-Buffer genannt) geschrieben. Die eigentliche Lichtberechnung kann nun durchgeführt werden indem ein bildschirmfüllendes Quad gerendert wird. Für jeden Pixel werden die Daten aus dem G-Buffer ausgelesen und die Beleuchtung entsprechend dieser Daten berechnet. Die Zahl der Rendercalls liegt bei diesem Ansatz nur noch bei O(nO + nL). Mit dieser Methode sind hunderte Lichter gleichzeitig in einer hoch-komplexen Szene möglich.
Das Verfahren ist nur dann sinnvoll, wenn man es mit einer sehr komplexen Geometrie und sehr vielen Lichtquellen zu tun hat. Hat man zum Beispiel zwar viele Lichtquellen, aber eine einfache Geometrie, kann bereits ein einfacher Depth-Only-Pass zum initialisieren des Z-Buffers verhindern, dass für unsichtbare Pixel unnötige Lichtberechnungen durchgeführt werden.
Vor-/Nachteile
Vorteile:
- Viele Lichtquellen bei komplexer Geometrie möglich
- Postprocessing Effekte (Bloom, Tonemapping, Depth of Field, etc.) können einfach angeschlossen werden, da bereits alle benötigten Informationen aufbereitet vorliegen.
- Lichtberechnung mit reduzierter Auflösung möglich (sinnvoll z.B. bei FSAA)
Nachteile:
- Kein Hardware Anti-Aliasing möglich
- Transparente Objekte müssen getrennt behandelt werden (Abhilfe schafft Inferred Lighting)
- Nur auf neueren Grafikkarten mit MRT (Mutliple Render Target) und Floating Point-Textur Support möglich (ab DirectX 9 kompatiblem Grafikchip; OpenGL Version ???)
Implementierung
Zunächst müssen die Geometriedaten in den (G-Buffer) geschrieben werden. Hierzu benötigen wir drei 16-Bit Float Rendertarget Texturen. Niedrigere Bittiefen sind für Positionsbeschreibung und Normalenvektor zu wenig. Alle Rendertarget-Texturen müssen die gleiche Bittiefe haben. Sind mehrere Rendertargets aktiviert, so wird Hardware-Anti-Aliasing automatisch abgeschaltet. Die folgenden Beispiele sind mit Andorra 2D entwickelt und sollten recht einfach verständlich und nach OpenGL/DirectX umzusetzen sein.
Schritt 1: Erstellen der Rendertargets
FRT_1_Albedo := TAdRenderTargetTexture.Create(FDraw);
FRT_1_Albedo.BitDepth := adF64Bit;
FRT_1_Albedo.SetSize(surfacew, surfaceh);
FRT_1_Albedo.Filter := atPoint;
FRT_2_Position := TAdRenderTargetTexture.Create(FDraw);
[...]
FRT_3_Normal := TAdRenderTargetTexture.Create(FDraw);
[...]
FRT_4_Composite := TAdRenderTargetTexture.Create(FDraw);
[...]
Schritt 2: Der G-Buffer-Fill Shader (HLSL/Cg)
Der hier gezeigte Shader ist nur ein einfaches Beispiel. Unter anderem kann durch das geschickte Weglassen von Daten (Berechnen von X, Y aus den Z und den Screen-Coordinaten im Lighting-Shader) Bandbreite eingespart werden.
//Fragment Shader
struct fs_res {
float4 position: COLOR1;
float4 normal: COLOR2;
float4 albedo: COLOR0;
};
fs_res fs_std_geometry(
float3 viewpos: TEXCOORD0,
float3 normal: TEXCOORD1
)
{
fs_res OUT;
OUT.position = float4(viewpos, 1.0f);
OUT.normal = float4(normal, 1.0f);
OUT.albedo = float4(1.0f, 1.0f, 1.0f , 1.0f);
return OUT;
}
//Vertex Shader
struct vs_res {
float4 pos: POSITION;
float3 viewpos : TEXCOORD0;
float3 normal: TEXCOORD1;
};
vs_res vs_std_geometry(
float3 position : POSITION,
float3 normal: NORMAL,
uniform float4x4 modelview,
uniform float4x4 modelviewproj
)
{
vs_res OUT;
OUT.pos = mul(float4(position, 1.0f), modelviewproj);
OUT.viewpos = mul(float4(position, 1.0f), modelview);
OUT.normal = normalize(mul(float4(normal, 0.0f), modelview));
return OUT;
}
Schritt 3: Daten in G-Buffer Rendern
//Activate the G-Buffers
FDraw.AdAppl.SetRenderTarget(0, FRT_1_Albedo.Texture);
FDraw.AdAppl.SetRenderTarget(1, FRT_2_Position.Texture);
FDraw.AdAppl.SetRenderTarget(2, FRT_3_Normal.Texture);
FDraw.AdAppl.Viewport := AdRect(0, 0, surfacew, surfaceh);
FDraw.AdAppl.ClearSurface(AdRect(0, 0, surfacew, surfaceh), [alColorBuffer, alZBuffer,
alStencilBuffer], Ad_ARGB(0, 0, 0, 0), 1, 1);
//Activate the shaders
FTransformShader.FragmentShader.BindEffect;
FTransformShader.VertexShader.BindEffect;
FScene.Draw(FCamera, nil, 0);
//Deactivate the shaders
FTransformShader.VertexShader.UnbindEffect;
FTransformShader.FragmentShader.UnbindEffect;
FDraw.AdAppl.SetRenderTarget(2, nil);
FDraw.AdAppl.SetRenderTarget(1, nil);
FDraw.AdAppl.SetRenderTarget(0, nil);
Schritt 4: Einfacher Direktionaler Licht-Shader
Das zur Beleuchtung verwendete Quad liegt in diesem (einfachen) Fall bereits projeziert in normierten Koordinaten vor.
struct ps_light_res {
float4 color: COLOR0;
};
ps_light_res ps_light_geometry(
float3 screenpos: TEXCOORD0,
float3 viewpos: TEXCOORD1,
float3 lightdir: TEXCOORD2,
uniform sampler sPosition,
uniform sampler sNormal,
uniform sampler sAlbedo,
uniform float4 lightcolor
)
{
ps_light_res OUT;
float4 col = tex2D(sAlbedo, float2(screenpos.x, -screenpos.y));
float4 normal = tex2D(sNormal, float2(screenpos.x, -screenpos.y));
OUT.color = float4((lightcolor * col * clamp(dot(normal.xyz, lightdir), 0.0f, 1.0f)).rgb, 1.0f);
return OUT;
}
struct vs_light_res {
float4 pos: POSITION;
float3 screenpos: TEXCOORD0;
float3 viewpos: TEXCOORD1;
float3 lightdir: TEXCOORD2;
};
vs_light_res vs_light_geometry(
float3 position : POSITION,
uniform float4x4 modelview,
uniform float4x4 modelviewproj,
uniform float3 lightdir
)
{
vs_light_res OUT;
OUT.pos = float4(position, 1.0f);
OUT.screenpos = position * float3(0.5f, 0.5f, 0.0f) + float3(0.5f, 0.5f, 0.0f);//mul(pos, modelviewproj);
OUT.viewpos = mul(position, modelview);
OUT.lightdir = normalize(mul(float4(lightdir, 0.0f), modelview));
return OUT;
}
Schritt 5: Lichtquellen in das Composite RT rendern
//Render a directional light using a quad
FDraw.AdAppl.SetRenderTarget(0, FRT_4_Composite.Texture);
FDraw.AdAppl.Viewport := AdRect(0, 0, surfacew, surfaceh);
FDraw.AdAppl.ClearSurface(AdRect(0, 0, surfacew, surfaceh), [alColorBuffer, alZBuffer,
alStencilBuffer], Ad_ARGB(255, 0, 0, 0), 1, 1);
FLightShader.FragmentShader.SetParameter('sAlbedo', FRT_1_Albedo.Texture);
FLightShader.FragmentShader.SetParameter('sPosition', FRT_2_Position.Texture);
FLightShader.FragmentShader.SetParameter('sNormal', FRT_3_Normal.Texture);
FLightShader.FragmentShader.BindEffect;
FLightShader.VertexShader.BindEffect;
//Render a blue light
FLightShader.FragmentShader.SetParameter('lightcolor', Ad_ARGB(255, 50, 50, 100));
FLightShader.VertexShader.SetParameter('lightdir', AcVector_Normalize(AcVector3(2.0, 2.0, -1.0)));
FQuad.BlendMode = bmAdd;
FQuad.Draw(nil);
FLightShader.FragmentShader.UnbindEffect;
FLightShader.VertexShader.UnbindEffect;
Schritt 6: Das Ergebnis Zeichnen
Nun muss nur noch das Ergebnis im "Composite"-RT gezeichnet werden - oder weitere Post-Processing Maßnahmen durchgeführt werden.