Quantcast
Channel: "Mit ohne Haare"» Shader 101
Viewing all articles
Browse latest Browse all 2

Shader 101: Diffuse / Directional Light

$
0
0

Weiter geht es mit der nächsten Art von Beleuchtung in der Reihe Shader 101. Insgesamt gibt es vier unterschiedliche Arten von Lichtern, zumindest sind dies die Arten, die wir für die 3D-Programmierung benötigen. Dies sind:

Ich werde die Erzeugung aller dieser Arten von Licht in Shadern beschreiben, wenn nicht schon geschehen, so dass Licht in Kürze keine Hürde mehr für euch darstellen wird.

In diesem Artikel geht es nun um die zweite Art von Licht, dem diffusen Licht.

Das diffuse Licht, oder auch “Directional Light” um das es hier geht ist eine weitere, recht leicht zu implementierende Lichtart. Dieses Licht hat wieder eine Farbe und eine Intensität, aber diesmal – im Gegensatz zum Umgebungslicht – auch eine Richtung. Warum nur eine Richtung? Relativ einfach: Es handelt sich bei dieser Lichtquelle um eine sehr weit entfernte Lichtquelle. Zur (mathematischen) Vereinfachung sogar um eine unendlich weit entfernte Lichtquelle, die zur Vereinfachung auch noch eine unendliche Reichweite hat. Dadurch entsteht ein Vorteil: Die einzelnen Lichtstrahlen sind praktisch parallel zueinander und da wir von einer unendlich weiten Lichtquelle ausgehen sind sie parallel. Daher benötigen wir lediglich eine Richtung und keine Position der Lichtquelle.

Ein gutes Beispiel für eine derartige Lichtquelle ist die Sonne. Sie ist so weit von der Erde entfernt, das der Winkel der einzelnen Strahlen sich praktisch nicht voneinander unterscheiden. Dieses Licht hat damit eine einheitliche Richtung und daher hängt die Stärke der Beleuchtung nicht nur von der Stärke der Lichtquelle ab, sondern auch von der Ausrichtung der Fläche zur Lichtquelle. Dies klingt vielleicht ein wenig kompliziert, ist aber sehr leicht zu erklären. Eine Fläche, die der Lichtquelle zugewendet ist, wird stark beleuchtet. Eine Fläche die der Lichtquelle abgewendet ist, wird nicht beleuchtet. Dazwischen gibt es unendlich viele Zwischenschritte. Die Intensität der Beleuchtung hängt also vom Winkel zwischen der Lichtquelle und der Fläche ab.

Wie erhalten wir nun diesen Winkel? Das ist relativ einfach, aber es gibt dafür eine Grundvoraussetzung: Wir benötigen eine Normale. Die Normale ist ein Einheitsvektor (also ein Vektor der Länge 1, welche man durch normalisieren erreicht) der senkrecht auf der jeweiligen Fläche steht. Dazu eine kleine Grafik (zur Vereinfachung in 2D), die ich auch für die weiteren Erklärungen verwenden werde.

Die liebevoll gestaltete Sonne (feinste Programmers-Art übrigens) in der rechten, oberen Ecke ist unsere Lichtquelle. Links unten befindet sich unser grüner Würfel, der schwarze Flächen hat. Auf diesen Flächen stehen die roten Normalen. Da diese senkrecht sind, haben diese natürlich einen 90° Winkel zur Fläche. Dann sehen wir noch zwei orange Lichtstrahlen. Einmal der Lichtstrahl der auf die Fläche trifft und einmal der Lichtstrahl, der reflektiert wird.

Der Shader ist nach wie vor nicht sehr kompliziert. Wir benötigen ein paar neue Parameter: Lichtfarbe, -stärke und nun auch die Richtung, die ich in den Zeilen 8 bis 10 definiert habe.

float4x4 World;
float4x4 View;
float4x4 Projection;

float4 AmbientColor			= float4(1, 1, 1, 1);
float  AmbientIntensity		= 0.75;

float4 DirectionalColor		= float4(1, 1, 1, 1);
float  DirectionalIntensity = 0.50;
float3 DirectionalLightDir  = float3(-1, -1, 1);

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal   : NORMAL0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 Normal   : TEXCOORD0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.Normal = normalize( mul( input.Normal, World) );

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float3 normalizedLightDirection = normalize(DirectionalLightDir);
	float4 diffuse = dot( normalizedLightDirection, input.Normal ) * DirectionalIntensity * DirectionalColor;
	float4 ambient = AmbientColor * AmbientIntensity;

    return saturate( diffuse + ambient );
}

technique DirectionalLight
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Als Eingabeparameter benötigen wir nun zusätzlich eine Normale, welche im Vertex-Stream des Render-Aufrufs enthalten sein muss. Näheres dazu im Beispielprogramm. In Zeile 32 wird diese Normale im Vertex-Shader vom Objekt- in das Welt-Koordinaten-System überführt und normalisiert, damit die Einheitslänge erhalten bleibt.

Weiter geht es im Pixel-Shader. Hier wird in Zeile 39 erstmal die Richtung zur Sicherheit nochmal normalisiert. Diesen Schritt kann man sich natürlich sparen, wenn man dies beim setzen des Effekt-Parameters sicherstellt. In Zeile 40 wird es dann interessant. Mit dem Skalarprodukt errechnen wir zunächst den Winkel zwischen Licht und Normale. Dieser wird nun mit der Intensität des Lichtes multipliziert, damit starke Lichter auch hell sind und schwache dunkel und zum Schluss multiplizieren wir mit der Lichtfarbe, damit alles auch die korrekte Farbgebung hat.

In Zeile 41 berechnen wir das Umgebungslicht, so wie es hier schon beschrieben wurde.

In Zeile 43 kombinieren wir nun das Umgebungslicht mit dem diffusen Licht und sehen, dass dort ein neuer Befehl auftaucht: saturate. Saturate ist ein recht einfacher Befehl. Er stellt einfach sicher, dass sich der Eingabeparameter im Bereich von 0 bis 1 befindet. Dies ist wichtig, da eine Farbe in HLSL für jede Komponente einen Wertebereich von 0 bis 1 hat. Würden wir nun zwei besonders starke Lichter miteinander kombinieren, so könnt es passieren, dass das Ergebnis größer als 1 ist und daher begrenzen wir es hier. Bisher ist dies noch nicht so wichtig, aber im weiteren Verlauf der “Licht-Serie” wird dies noch sehr wichtig werden, daher machen wir es direkt korrekt.

Das fertige Ergebnis sieht dann z.B. so aus, wie im folgenden Bild, in dem ich eine direktionales Licht mit dem Umgebungslicht des vorherigen Artikels kombiniert habe. Schön zu sehen ist dort, wie jede Seite des Würfels eine Helligkeit in Abhängigkeit vom Winkel zur Lichtquelle erhält. So werden die einzelnen Seiten sehr schön definiert und sichtbar.

In diesem Artikel habe ich euch erklärt, wie man ein Diffuses bzw. Directional Light in einem Shader erzeugt. Ich habe dabei bewusst auf die mathematischen und physikalischen Hintergründe verzichtet, da diese nicht so wichtig sind. Wichtig ist eigentlich nur, dass die Stärke des Lichtes auf der Fläche abhängig ist vom Skalarprodukt zwischen Lichtrichtung und Flächennormale. Für den interessierten Leser habe ich jedoch die jeweiligen Detail-Erklärungen verlinkt.

Sicherlich können wir immer noch kein High-End-Spiel entwickeln und auch werden wir niemanden mit Umgebungs- und diffusem Licht vom Hocker reißen, trotzdem können wir damit schon einiges erreichen. Wir können nun z.B. die Sonne simulieren und jetzt nicht nur das Umgebungslicht für einen Sonnenuntergang verwenden, sondern auch noch das direkte Licht der Sonne. Durch die Richtungsänderung der Sonne beim Sonnenuntergang, wird dies auf jeder Fläche in der Szene sichtbar, wodurch der Betrachter die Lichtposition abschätzen kann. Zusammen mit weiteren Lichtquellen, die wir noch besprechen werden, ist dies eine weitere wichtige Grundlage für schöne und realistische Szenen.

Eine Übersicht über alle Artikel der Reihe Shader 101 findet ihr im Einstiegsartikel: Shader 101: Grundlagen.


Einsortiert unter:Shader, XNA, XNA 3.1, XNA 4.0 Tagged: Beleuchtung, dynamisches Licht, HLSL, Licht, Shader, Shader 101, XNA, XNA 4.0

Viewing all articles
Browse latest Browse all 2

Latest Images





Latest Images