Pythonstuff GLSL in English Pythonstuff GLSL auf Deutsch Pythonstuff GLSL Pythonstuff
PythonStuff Home
 

 

Beispiel 2 - mein erstes GLSL Programm mit Pyglet: Phong Shading

Basierend auf dem vorigen Beispiel fügen wir unseren ersten GLSL-Shader hinzu. Der implementiert zwei Lichtquellen mit diffuser Beleuchtung und Phong-Shading, das sieht so aus:

Phong-Shaded Torus

Um die Demo ablaufen zu lassen, benötigt man Pyglet und Tristam Macdonald's Modul Shader.py. Alles findest Du auch auf der Installationsseite.

(Falls jemand “shader.py” ansehen will: das File liegt im Verzeichnis App/Lib/site-packages/shader des Installationspaketes glslpythonpack.zip).

Programmbeschreibung

Es beginnt gleich mit dem Shader:

from shader import Shader

wir importieren die “Shader” Klasse von Tristam Macdonald's Library Shader.py und erzeugen ein “shader” Objekt, das aus zwei Zeichenketten besteht:

  • dem Vertex Shader
  • dem Fragment Shader

einfach durch

shader = Shader(['... vertex shader code ...'] , ['... fragment shader code ...'])

(natürlich werden wir die Triple-Quote Notation für mehrzeilige Shader Programme verwenden, wir sehen uns diese weiter unten an).

Zuerst definieren wir eine Taste (ENTER) für eine Variable “shaderon”, um das Binden des Shaders ein- und ausschalten zu können:

...
elif symbol == key.ENTER:
    print 'Shader toggle'
    shaderon = not shaderon
...
if shaderon:
    # bind our shader
    shader.bind()
    if togglefigure:
        batch1.draw()
    else:
        batch2.draw()
    shader.unbind()
else:
    if togglefigure:
        batch1.draw()
    else:
        batch2.draw()
...

(das könnte sicher eleganter werden, aber wir wollen ja auch zwischen den beiden Figuren in batch1 und batch2 umschalten).

Der Vertex Shader Code

Jetzt sehen wir uns den Shader Code an - sobald wird den Shader mit “bind” aktivieren, haben wir volle Kontrolle über die Rendering Pipeline, aber müssen auch die Vertex-Transformation selbst übernehmen:

void main()
{
    gl_Position = ftransform();
}

Das ist einfach eine Art zu sagen: “Mache die Standard Vertex Transformation genau wie in der Fixed Pipeline”.

Im Fragment Shader (oder “Pixel”-Shader) wollen wir die Beleuchtung berechnen - damit erreichen wir eine deutliche Verbesserung gegenüber der Fixed Pipeline, die diese Berechnung nur pro Ecke (Vertex) durchführt und die Helligkeitswerte dazwischen interpoliert. Wir hingegen interpolieren

  • die Oberflächen-Normale
  • die Augenposition (relativ zum Oberflächenelement)
  • die Position der (zwei) Lichtquellen (relativ zum Oberflächenelement)

Damit ergibt sich das folgende Vertex Programm:

varying vec3 normal, lightDir0, lightDir1, eyeVec;
void main()
{
    normal = gl_NormalMatrix * gl_Normal;
    vec3 vVertex = vec3(gl_ModelViewMatrix * gl_Vertex);
    lightDir0 = vec3(gl_LightSource[0].position.xyz - vVertex);
    lightDir1 = vec3(gl_LightSource[1].position.xyz - vVertex);
    eyeVec = -vVertex;
    gl_Position = ftransform();
}

Für die Berechnung der Beleuchtung im Fragment Shader benötigen wird diese Vektoren normalisiert - da aber beim Interpolieren Fehler entstehen (Komponentenweise interpolierte Einheits-Vektoren haben nicht notwendigerweise die Länge Eins), verschieben wir die Normalisierung in den Fragment Shader. Man könnte auch im Vertex Shader normalisieren um ein paar GPU-Zyklen zu sparen, aber das würde auf Kosten der Genauigkeit geschehen.

Der Fragment Shader Code

varying vec3 normal, lightDir0, lightDir1, eyeVec;
void main (void)
{
    vec4 final_color =
    (gl_FrontLightModelProduct.sceneColor * gl_FrontMaterial.ambient) +
    (gl_LightSource[0].ambient * gl_FrontMaterial.ambient) +
    (gl_LightSource[1].ambient * gl_FrontMaterial.ambient);
    gl_FragColor = final_color;
}

Diese Kurzversion berechnet nur den Anteil des Umgebungslichtes (“ambient”) auf die selbe Weise wie die Fixed Pipeline.

Dann kommt richtungsabhängige Beleuchtung dazu:

float lambertTerm0 = dot(N,L0);
if(lambertTerm0 > 0.0)
  {
  final_color += gl_LightSource[0].diffuse *
                 gl_FrontMaterial.diffuse *
                 lambertTerm0;
  }

Das ist der Anteil “diffusen” Lichts - er hängt von der Helligkeit der Lichtquelle ab, dem Material und der Richtung der Oberflächennormale relativ zur Lichtquelle (das wird durch das “innere” Produkt berechnet). Die Berechnung führen wir nur durch, wenn der Anteil positiv ist, also Lichtquelle und Auge über der Oberfläche ist.

Zuletzt kommen Oberflächen-Reflexe dazu. Diese hängen vom Halbwinkel zwischen dem Augenvektor und dem Lichtvektor ab (und natürlich von der Helligkeit und dem Material):

vec3 E = normalize(eyeVec);
vec3 R = reflect(-L0, N);
final_color += gl_LightSource[0].specular *
               gl_FrontMaterial.specular *
               pow( max(dot(R, E), 0.0), gl_FrontMaterial.shininess );

Wir verwenden dabei die Materialparameter der Fixed Pipeline, damit sind die Berechnungen recht allgemein verwendbar.

Im Beispiel-Programm werden diese Berechnungen gleich zweimal gemacht, für jede Lichtquelle.

Abhängig von den Fähigkeiten der Grafikkarte ließe sich das um weiter Lichtquellen erweitern - man könnte sogar eine Schleife programmieren, um über ein Array von Lichtvektoren zu iterieren, um eine beliebige Anzahl von Lichtquellen zu unterstützen (auf Kosten der Framerate).

Das Ergebnis sieht nach einer glatten Oberfläche aus - abhängig von den Materialparametern sieht es aus wie Porzellan, Plastik oder glattgeschliffenes Holz - wenn da noch eine passende Textur drauf wäre.

Das ist es auch, was wir im nächsten Beispiel 3: "Hinzufügen einer Textur" tun.

Wem der Start mit GLSL zu schnell geht, sollte vielleicht einführende Texte ansehen. Ein guter Anfang ist

oder die

(Allerdings alles in englischer Sprache).


English version, Start
Impressum & Disclaimer