Deferred Shading

Aus DGL Wiki
Wechseln zu: Navigation, Suche

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.

Weblinks

Siehe auch