Der Chris hat geschrieben:Dann muss ich direkt fragen...was machen Pixelshader technisch gesehen?
Ich habe ihn angedroht, hier ist er: Der sehr lange und langweilige Shader-Post.
Eine kleine Vorwarnung: Ich hab das schon Jahre lang nicht mehr gemacht und auch damals schon nicht so intensiv, wie ich das wollte, kann also gut sein, dass ich ein paar Begriffe verwechsle und die Reihenfolge in der Pipeline irgendwo vertausche. Generell fehlen hier 90% von dem was sonst noch alles gemacht wird, aber ich hoffe, das bietet trotzdem einen Überblick.
Zu Beginn der Renderpipeline hat die CPU einen Haufen Objekte mit ihren Koordinaten und Raumlagen und die Kamera auf Grundlage der Spielmechanik, Physik und KI berechnet und in den Speicher gelegt. Der GPU fällt jetzt die Aufgabe zu, aus diesen Daten ein Bild zu rendern.
Diese Objekte sind zu diesem Zeitpunkt noch in verschiedenen Koordinatensystemen angegeben. Zum Beispiel könnte das Koordinatensystem des Dorfes, durch das man in einem RPG rennt, im Brunnen seinen Ursprung haben. Das Haus vom Dorfchef hat ein eigenes Koordinatensystem das an einer der Ecken den Nullpunkt hat. Und die Spielfigur hat ihr Koordinatensystem vielleicht zu ihren Füssen. Der erste Schritt ist, alle Vertices (Punkte/Knoten) dieser Objekte von ihrem eigenen Koordinatensystem über die Positionierungen der Objekte in ein Weltkoordinatensystem umzurechnen. Das geht tatsächlich einfacher als man denkt über Matrizenmultiplikationen. Ich kann diesen Schritt gerne noch erläutern, falls es interessiert, für die Erklärung von Shadern ist er nicht zwingend nötig, daher blende ich ihn hier aus. Die Weltkoordinaten können irgendwo definiert sein, z.B. auch wieder im Dorfbrunnen.
Am Ende habe ich also sämtliche Vertices der dargestellten Szene ausgerechnet als Punkte im 3D-Raum im Speicher. Während dieses Schrittes werden noch viele Optimierungen vorgenommen, z.B. schmeiß ich hier alles raus, was ich eh hinterher nicht sehe, weil es gar nicht im Sichtbereich liegt.
Als nächstes benötige ich mehr Infos über die Flächen der Polygone, und zwar genauer über ihre Ausrichtung im Raum. Diese Ausrichtungen nennt man Normalenvektoren. Ihre Berechnung ist ebenfalls recht einfach, man benutzt hier zu das
Kreuzprodukt zwischen zwei der immer drei Polygonkanten. Die Polygonkanten kann man als Vektoren auffassen, indem man bei den drei Punkten a, b, und c z.B. a von b abzieht und dann a von c abzieht. Die dadurch erhaltenen Vektoren v1 und v2 bilden jetzt zwei Kanten des Polygons. Das Kreuzprodukt zweier Vektoren ist wieder ein Vektor und hat die originelle Eigenschaft, immer senkrecht auf den multiplizierten Vektoren zu stehen. Das heißt, v1 x v2 = v3 steht senkrecht auf dem ursprünglichen Polygon. Die Länge ist hier für uns uninteressant (sie gibt den doppelten Flächeninhalt des Polygons wieder), sie wird auf die Länge 1 gekürzt (normalisiert). Was wir erhalten ist der Normalenvektor des Polygons. Diesen Schritt wiederholen wir für alle Polygone.
Weil wir das später noch brauchen, berechnen wir an den Knoten zwischen mehreren Polygonen auch die Vertexnormalen. Das sind mehr oder weniger die gemittelten Normalen der angrenzenden Polygone.
Wir haben jetzt also alle Knoten und alle Normalen in einem gemeinsamen Koordinatensystem. Nächster Schritt.
Die Normalen haben wir mit gutem Grund berechnet. Wir benötigen die Information über die "Richtung der Oberflächen", weil wir die Schattierung berechnen können möchten.
Hier kommen jetzt zum ersten Mal Shader ins Spiel, nämlich die Vertex-Shader. Beim Begriff Shader muss man etwas aufpassen. Oft wird er sowohl für das Programm verwendet, das er darstellt, als auch für das Hardwarebauteil, das dieses Programm ausführt. Redet man von einer GPU wie dem X1 mit 256 Shadern bedeutet das, dass 256 Hardwareeinheiten zur Verfügung stehen, die unabhängig voneinander und gleichzeitig Shaderprogramme ausführen. (Diese Programme werden in Shadersprachen geschrieben und direkt als Text an den Grafiktreiber geschickt. Die Shadersprachen werden durch das verwendete API definiert, bei Open GL heißt die Shadersprache GLSL. Meist sehen die aus wie C.)
Vertex-Shader haben eine einfach Aufgabe. Sie berechnen Werte, die den Vertices zugeordnet werden, und meistens (!) haben die was mit Licht und Schatten zu tun. Sie könnten aber auch ein Farbwert sein, das darf der Programmierer entscheiden. Wir gehen mal vom einfachen Fall aus und schattieren ein paar Polygone. Jedenfalls werden in dieser Phase die den Vertices zugeordneten Shaderprogramme auf diese und ihre Normalen losgelassen.
Die Schattierung einer Fläche hängt ab von der Position der Lichtquelle (bei einem Punktlicht) bzw. der Richtung der Lichtstrahlen bei einer global definierten Licht-Richtung (Sonne z.B.) und der Richtung der Fläche. Man könnte sagen, ich berechne „wie schief“ der Lichtstrahl auf die Fläche trifft. Ist das ein sehr flacher Winkel, dann ist am Ende der Helligkeitsanteil, den die Fläche durch die Lichtquelle gewinnt sehr klein. Ist er spitz (also steht die Lichtquelle fast in Richtung der Flächennormalen), dann ist die Helligkeit hoch. Berechnet wird das noch einfacher als die Normalen, mit dem
Skalarprodukt. Das Ergebnis des Skalarprodukts spiegelt genau diesen Anteil wieder.
Ein Vertex-Shaderprogramm könnte jetzt genau diese Helligkeitswerte alle berechnen und jedem Vertex in einer Szene zuordnen. Diese Helligkeits- oder Farbwerte je Knoten werden gespeichert.
Jetzt wird die komplette Szene, bzw. der Sichtbereich den meine Kamera umfasst in einen Würfel von Kantenlänge 1 gequetscht. Also verzerrt und zwar mitsamt der ganzen Normalen. Das heißt, ich transformiere in ein neues Koordinatensystem, das im Wesentlichen meinen Bildschirm darstellt. Das Koordinatensystem könnte zum Beispiel unten links in der Ecke liegen mit Achsen entlang der Bildschirmkanten. Die Z-Achse würde dann aus dem Bildschirm heraus zeigen. Damit kann man jetzt jeden Pixel des Bildschirms einem Satz Koordinaten zuordnen.
Dann kommen die Pixelshader ins Spiel. Sie werden für jeden Pixel einzeln ausgeführt. Zunächst wird bestimmt, welches Polygon unter dem Pixel liegt und welche Shaderprogramme und Daten (Vektoren, Vertices und zugeordnete Ergebnisse aus den Vertexshadern) diesem Polygon zugeordnet sind. Das heißt, die Vertex-Shader liefern zum Teil die Eingangsdaten für die Pixelshader.
Ein Pixelshader kann jetzt etwas ganz einfaches tun und den Helligkeitswert, den meine Vertex-Shader hinterlassen haben, einfach interpolieren. Also salopp gesagt per Dreisatz einen Zwischenwert aus den Helligkeitswerten der drei zum unterliegenden Polygon gehörenden Punkte berechnen. Das Ergebnis sieht aus wie Marios Nase in Mario 64. Das N64 hatte zwar keine frei programmierbaren Shader, aber genau diese Funktion in Hardware gegossen.
Der Shader kann aber auch mit Hilfe einer Textur den richtigen Farbwert aus den Koordinaten innerhalb der Textur berechnen. Oder er kann ganz tolle Sachen mit Dingen wie Normal Maps anstellen, die eigentlich auch nichts anderes als Texturen sind, nur dass da keine Farbwerte drin stehen sondern eben Flächennormalen. Das heißt, der Shader kann zu diesen Normalen mit der gleichen Mathematik wie eben unser Vertex Shader die Lage zum Licht berechnen um auf einen Farbwert zu kommen, obwohl diese Normalen eigentlich gar nicht in der Geometrie existieren, sondern eben nur in der Map. Das ist ein ziemlich billiger, aber höchst effektiver Trick. Doom 3 hat damals genau damit etliche Unterkiefer tiefergelegt.
http://www.opengl-tutorial.org/intermed ... l-mapping/
Etwas anderes was er tun könnte: Er könnte auf die Helligkeitswerte der Vertexshader pfeifen (bzw. die würden ihm dann die Daten gar nicht liefern) und für jeden Pixel einzeln das berechnen, was vorher der Vertexshader für den ganzen Knoten gemacht hat. Das Ergebnis wäre per-Pixel-Lighting und sieht gleich viel besser aus. Frisst aber halt Leistung, vor allem bei großen Polygonen:
http://www.lighthouse3d.com/tutorials/g ... per-pixel/
Oder er könnte irgendwie seinen Farbwert berechnen und dann entscheiden „so genau wollen wir’s gar nicht“ und den Wert klassieren/diskretisieren. Also z.B. so:
Code: Alles auswählen
// The pixel shader that does cel shading. Basically, it calculates
// the color like is should, and then it discretizes the color into
// one of four colors.
float4 CelPixelShader(VertexToPixel input) : COLOR0
{
// Calculate diffuse light amount
float intensity = dot(normalize(DiffuseLightDirection), input.Normal);
if(intensity < 0)
intensity = 0;
// Calculate what would normally be the final color, including texturing and diffuse lighting
float4 color = tex2D(textureSampler, input.TextureCoordinate) * DiffuseColor * DiffuseIntensity;
color.a = 1;
// Discretize the intensity, based on a few cutoff points
if (intensity > 0.95)
color = float4(1.0,1,1,1.0) * color;
else if (intensity > 0.5)
color = float4(0.7,0.7,0.7,1.0) * color;
else if (intensity > 0.05)
color = float4(0.35,0.35,0.35,1.0) * color;
else
color = float4(0.1,0.1,0.1,1.0) * color;
return color;
}
aus
http://rbwhitaker.wikidot.com/toon-shader
Wir hätten damit einen einfachen Cel-Shader geschaffen. Der Shader rechnet einfach eine Intensität zwischen 0 und 1 aus und ordnet dann abhängig vom Wert eine von wenigen festen Farben zu.
Was genau der Shader tut ist völlig Sache des Programmierers. Im einfachsten Fall gibt er nur eine dem Polygon zugeordnete Farbe wieder aus. Das ist dann langweilig, aber das ist seine Aufgabe.
Jedenfalls sind die Shaderprogramme somit praktisch die wichtigsten Bestandteile im ganzen Prozess und sie werden immer für alles sichtbare ausgeführt, auch bei den einfachsten Szenen.