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.

Vorgehensweise

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 nl muss also jedes Objekt no gezeichnet werden. Die Anzahl der Rendercalls liegt also bei no * nl. Hierdurch sind nur wenige Lichtquellen gleichzeitig möglich.

Deferred Shading verfolgt einen anderen Ansatz: Anstatt jedes Objekt der Szene für jede Lichtquelle neu zu renderen, werden zunächst alle beleuchtungsrelevanten Daten (Position des Pixels, Normalenvektoren, unbeleuchtete Farbe des Pixels (Albedo), Glanzstärke, etc.) auf Pixelbasis in mehrere Rendertargets (oftmals auch G-Buffer genannt) geschrieben. Nun muss für jede Lichtquelle lediglich die Lichtquellengeometrie gezeichnet werden und in diesem Schritt die Beleuchtung auf die zwischengespeicherten Daten angewendet werden. Die Zahl der Rendercalls liegt also bei no + nl. Mit dieser Methode sind hunderte Lichter gleichzeitig in einer Szene möglich.

Vor-/Nachteile

Vorteile:

  • Viele Lichtquellen möglich
  • Postprocessing Effekte (Bloom, Tonemapping, Depth of Field, etc.) können einfach angeschlossen werden, da bereits alle benötigten Informationen aufbereitet vorliegen.

Nachteile:

  • Kein Hardware Anti-Aliasing möglich
  • Transparente Objekte müssen getrennt behandelt werden
  • 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