Repository navigation
3 Phong‐Modell im Shader
Diese Seite ist das Nachschlagewerk zu Aufgabe 3: der Template-Shader, die Eingaben, die die Engine jedem eigenen Shader liefert, mit ihren Namen, eigene Konstanten, das Slang, das ein Beleuchtungs-Shader braucht, und der Weg vom Rückgabewert von PSMain zum Bild im Fenster.
Die ganze Datei src/shaders/template.slang vom Branch learning, ohne den Lizenzkopf; auf main fehlen nur die drei Kommentare // ambient, // diffuse und // specular:
// Combined vertex and fragment shader for textured mesh rendering
// Import shared utility modules
import common_types;
// Input from vertex buffer
struct VSInput {
[[vk::location(0)]] float3 Position;
[[vk::location(1)]] float3 Normal;
[[vk::location(2)]] float2 UV;
[[vk::location(3)]] float4 Tangent;
// Per-instance data
[[vk::location(4)]] column_major float4x4 InstanceModelMatrix;
[[vk::location(8)]] float4 InstanceNormal0;
[[vk::location(9)]] float4 InstanceNormal1;
[[vk::location(10)]] float4 InstanceNormal2;
};
// Output from vertex shader / Input to fragment shader
struct VSOutput {
float4 Position : SV_POSITION;
float3 WorldPos;
float3 Normal;
float2 UV : TEXCOORD0;
};
// Bindings
[[vk::binding(0, 0)]] ConstantBuffer<UniformBufferObject> ubo;
[[vk::binding(1, 0)]] Sampler2D baseColorMap;
[[vk::binding(6, 0)]] StructuredBuffer<LightData> lightBuffer;
[[vk::push_constant]] PushConstants material;
// Vertex shader entry point
[[shader("vertex")]]
VSOutput VSMain(VSInput input)
{
VSOutput output;
float4x4 instanceModelMatrix = input.InstanceModelMatrix;
float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0)));
output.Position = mul(ubo.proj, mul(ubo.view, worldPos));
output.WorldPos = worldPos.xyz;
// Transform normals correctly: first by the per-instance normal matrix,
// then by the entity model 3x3 (avoid double-applying instance transform).
float3x3 instNormal = transpose(float3x3(input.InstanceNormal0.xyz, input.InstanceNormal1.xyz, input.InstanceNormal2.xyz));
float3x3 model3x3 = (float3x3)ubo.model;
float3 worldNormal = normalize(mul(model3x3, mul(instNormal, input.Normal)));
output.Normal = worldNormal;
output.UV = input.UV;
return output;
}
// Fragment shader entry point
[[shader("fragment")]]
float4 PSMain(VSOutput input) : SV_TARGET
{
float2 uv = float2(input.UV.x, 1.0 - input.UV.y);
float4 baseColor = baseColorMap.Sample(uv);
// ambient
// diffuse
// specular
return baseColor;
}-
VSMainläuft je Vertex: Es rechnet die Position aus dem Modellraum über die Welt in den Clip-Space und reicht die Weltposition (WorldPos), die Normale in Weltkoordinaten (Normal) und die Texturkoordinate (UV) anPSMainweiter. -
PSMainläuft je Fragment, bekommt diese Werte zwischen den Vertices interpoliert alsinputund gibt die Farbe alsfloat4zurück: Rot, Grün, Blau und Alpha. Im Template ist das die Farbe der Basisfarbtextur, ohne Beleuchtung. - An die Kommentare
// ambient,// diffuseund// specularkommen die drei Terme; diereturn-Zeile bleibt die letzte Zeile vonPSMain.
Jede eigene Pipeline übernimmt das Layout der Pipeline "pbr", und die Engine füllt diese Eingaben in jedem Frame. Die Namen gelten in PSMain des Templates:
| Eingabe | Name im Shader | Typ | Bedeutung |
|---|---|---|---|
| Normale | input.Normal |
float3 |
die Normale der Fläche in Weltkoordinaten, von VSMain normalisiert und zwischen den Vertices interpoliert |
| Weltposition | input.WorldPos |
float3 |
die Position des Oberflächenpunkts, den das Fragment zeigt, in Weltkoordinaten |
| Kameraposition | ubo.camPos.xyz |
float4, davon .xyz
|
die Position der aktiven Kamera in Weltkoordinaten |
| Anzahl der Lichter | ubo.lightCount |
int |
wie viele Einträge lightBuffer hat |
| ein Licht | lightBuffer[i] |
LightData |
Eintrag i, von 0 bis ubo.lightCount - 1. Die Beispielszene hat genau ein Licht, "Sun", also lightBuffer[0]
|
| Lichtrichtung | lightBuffer[0].position.xyz |
float4, davon .xyz
|
bei einem gerichteten Licht (lightType == 1) die Richtung, in die das Licht scheint (w = 0); bei einem Punkt- oder Spotlicht die Position des Lichts (w = 1) |
| Lichtfarbe | lightBuffer[0].color.rgb |
float4, davon .rgb
|
Farbe mal Intensität des Lichts, also SetColor() × SetIntensity()
|
| Lichttyp | lightBuffer[0].lightType |
int |
0 Punktlicht, 1 gerichtetes Licht, 2 Spotlight, 3 Licht eines leuchtenden Materials |
| Basisfarbe | baseColor |
float4 |
die Farbe der Basisfarbtextur an diesem Fragment, im Template aus baseColorMap.Sample(uv). Eine Kugel hat die weiße Standardtextur der Engine |
Die übrigen Eingaben (die Matrizen ubo.model, ubo.view und ubo.proj, die weiteren Materialtexturen, die Materialfaktoren in material, Reichweite und Kegel eines Spotlights) braucht das Phong-Modell nicht.
Eigene Werte, etwa die Konstanten des Materials, stehen als Konstanten in der Shader-Datei, oberhalb von PSMain. Eine C++-Schnittstelle für eigene Parameter gibt es nicht. Die Form, die die Module der Engine benutzen (common_types.slang):
static const float PI = 3.14159265359;Ein Vektor wird ebenso deklariert, mit float3 und drei Werten: static const float3 name = float3(x, y, z);. Ändern heißt: Datei ändern, bauen, starten.
| Schreibweise | Bedeutung |
|---|---|
float3(x, y, z), float4(x, y, z, w), float4(v, 1.0)
|
einen Vektor bilden; float4(v, 1.0) hängt an einen float3 v die vierte Komponente an |
v.xyz, c.rgb, v.x
|
Komponenten eines Vektors auswählen |
a + b, a - b, a * b
|
bei zwei Vektoren komponentenweise; s * v multipliziert jede Komponente von v mit der Zahl s
|
q - p |
der Vektor vom Punkt p zum Punkt q
|
dot(a, b) |
das Skalarprodukt zweier Vektoren |
normalize(v) |
normalisiert v: der Vektor mit derselben Richtung und der Länge 1 |
length(v) |
die Länge eines Vektors |
max(a, b) |
der größere der beiden Werte, bei Vektoren je Komponente |
pow(x, n) |
x hoch n; x darf nicht negativ sein, für negatives x ist das Ergebnis nicht definiert |
saturate(x) |
x auf den Bereich 0 bis 1 begrenzt |
Das Skalarprodukt und die Richtung. Für zwei Vektoren der Länge 1 ist dot(a, b) der Kosinus des Winkels zwischen ihnen: 1, wenn beide in dieselbe Richtung zeigen, 0, wenn sie senkrecht aufeinander stehen, und negativ, wenn sie voneinander wegzeigen. Deshalb werden beide Vektoren vorher normalisiert. Die Vorlesung schreibt das Skalarprodukt aus Normale und Lichtrichtung in der Implementierung als max(dot(n, l), 0.0) mit normalisierten Vektoren n und l: Negative Werte werden zu 0, Rückseiten haben keine Beleuchtung (Vorlesungsskript, Folie VI-14).
Der Build übersetzt jede .slang-Datei in src/shaders/ außer den vier Modulen, die andere Shader importieren, mit slangc aus dem Vulkan SDK nach SPIR-V und kopiert die .spv-Dateien nach src/shaders/, sobald alle Shader fehlerfrei übersetzt sind. Ein Fehler im Shader erscheint deshalb beim Bauen in der Build-Ausgabe als Fehler des Ziels shaders, mit Datei, Zeile und Meldung, und nicht in der Engine-Konsole. Bis zum nächsten fehlerfreien Build lädt die Engine die letzte fehlerfreie .spv. Jede Shader-Datei braucht die Einstiegspunkte VSMain und PSMain.
PSMain gibt eine lineare Farbe zurück. Die Engine schreibt sie in ein Off-Screen-Farbbild, das das Format der Swap Chain hat, mit 8 Bit je Kanal; ein Wert über 1 wird dabei auf 1 abgeschnitten. Danach multipliziert der composite-Pass das Bild mit der Belichtung (Exposure, Standard 1,2, Schieberegler im Panel Renderer der Engine) und bildet es mit einem filmischen Tone Mapping ab, bevor es im Fenster erscheint. Daraus folgt:
- Die Helligkeit im Fenster ist nicht der Wert, den
PSMainzurückgibt. - Ergibt die Rückgabe in einem Kanal mehr als 1, wird dieser Kanal dort abgeschnitten: Die Fläche wird gleichmäßig hell, und ein Glanzlicht darin ist nicht zu sehen. Die Sonne der Beispielszene hat die Intensität 3, ihre Lichtfarbe ist also (3; 3; 3).
Shader-Variablen lassen sich nicht drucken, aber als Farbe ausgeben: Statt der Summe der Terme gibt die return-Zeile einen Zwischenwert zurück, als float4 mit Alpha 1. Die Normale, ihre Komponenten x, y und z als Rot, Grün und Blau, von −1 bis 1 auf 0 bis 1 verschoben:
return float4(normalize(input.Normal) * 0.5 + 0.5, 1.0);Ein Term t vom Typ float3 oder ein einzelner Wert s vom Typ float als Grau:
return float4(t, 1.0);
return float4(s, s, s, 1.0);Wegen des Abschneidens, der Belichtung und des Tone Mappings wird eine solche Farbe danach gelesen, wo das Bild hell oder dunkel ist und welcher Farbkanal überwiegt, nicht nach ihrem Zahlenwert.
Die Kamerasteuerung der Engine bewegt die aktive Kamera: W und S vor und zurück, A und D nach links und rechts, Q und E nach oben und unten; mit gedrückter linker Maustaste dreht die Maus die Kamera. Unbewegt ist die Kamera direkt nach dem Start; ein Neustart stellt die Startposition aus SetupScene() wieder her.
Engine-Dokumentation: docs/ARCHITECTURE.md, Shaders und One frame.