Zum Inhalt springen

Datentypen in WGSL

In WGSL besitzt jeder Wert einen Datentyp. Dieser legt fest, welche Art von Wert wir speichern und welche Berechnungen wir damit durchführen können.

WGSL bietet dafür einfache Datentypen wie Ganzzahlen, Gleitkommazahlen und Wahrheitswerte. Hinzu kommen Vektoren, Matrizen, Arrays und Strukturen, mit denen wir mehrere Werte zusammenfassen können.

Die einfachen Datentypen, auch Skalare genannt, speichern jeweils einen einzelnen Wert.

Datentyp Bedeutung Beispiel
bool Wahrheitswert true, false
i32 32-Bit-Ganzzahl mit Vorzeichen -10, 42
u32 32-Bit-Ganzzahl ohne Vorzeichen 0u, 42u
f32 32-Bit-Gleitkommazahl 0.5, 3.14
f16 16-Bit-Gleitkommazahl 0.5h, 3.0h

Eine Ganzzahl besitzt keine Nachkommastellen. Mit i32 können wir positive und negative Ganzzahlen darstellen, mit u32 nur nichtnegative. Das angehängte u kennzeichnet einen Wert ausdrücklich als u32.

Gleitkommazahlen können auch Bruchteile darstellen. Der Unterschied zwischen f32 und f16 liegt in der Anzahl der verwendeten Bits: f32 bietet einen größeren Zahlenbereich und eine höhere Genauigkeit, während f16 weniger Speicher benötigt.

Damit wir f16 in einem Shader verwenden können, muss das WebGPU-Gerät die optionale Funktion shader-f16 unterstützen. Beim Erstellen des Geräts wird diese Funktion angefordert. Im WGSL-Code aktivieren wir sie zusätzlich am Anfang des Shaders:

WGSL
enable f16;
let brightness: f16 = 0.5h;

Der Datentyp bool speichert dagegen keine Zahl, sondern einen Wahrheitswert. Solche Werte verwenden wir beispielsweise für Bedingungen.

In WGSL können wir den Datentyp bei der Deklaration ausdrücklich angeben:

WGSL
let brightness: f32 = 0.8;
let count: i32 = 10;
let index: u32 = 5u;
let visible: bool = true;

Der Doppelpunkt trennt den Variablennamen vom Datentyp.

Häufig kann WGSL den Typ auch aus dem zugewiesenen Wert bestimmen:

WGSL
let brightness = 0.8;
let visible = true;

In diesem Fall müssen wir den Typ nicht ausschreiben. WGSL überprüft trotzdem, ob die Werte und Operationen zueinander passen.

Vektoren fassen zwei, drei oder vier Werte desselben Datentyps zusammen. In der Grafikprogrammierung verwenden wir sie beispielsweise für Positionen, Richtungen, Normalen und Farben.

WGSL
let uv = vec2f(0.5, 0.25);
let normal = vec3f(0.0, 0.0, 1.0);
let color = vec4f(0.8, 0.4, 0.2, 1.0);

Die Zahl im Typnamen gibt die Anzahl der Komponenten an. Der Buchstabe beschreibt ihren Datentyp.

Kurzform Ausgeschriebener Typ Komponenten
vec2f vec2<f32> 2 Gleitkommazahlen
vec3f vec3<f32> 3 Gleitkommazahlen
vec4f vec4<f32> 4 Gleitkommazahlen
vec2i vec2<i32> 2 Ganzzahlen mit Vorzeichen
vec3u vec3<u32> 3 Ganzzahlen ohne Vorzeichen
vec4h vec4<f16> 4 Gleitkommazahlen mit 16 Bit

Die Tabelle zeigt die möglichen Komponenten-Datentypen und beispielhafte Vektorgrößen. Jede dieser Varianten gibt es mit zwei, drei oder vier Komponenten. Für Wahrheitswerte verwenden wir beispielsweise vec2<bool> oder vec4<bool>; dafür gibt es keine entsprechende Kurzform mit einem einzelnen Buchstaben.

Die Kurzform vec3f und die ausgeschriebene Form vec3<f32> bezeichnen denselben Typ:

WGSL
let normal: vec3<f32> = vec3<f32>(0.0, 0.0, 1.0);

Vektoren können wir direkt mit Zahlen multiplizieren:

WGSL
let color = vec3f(0.8, 0.4, 0.2);
let darkerColor = color * 0.5;

Dabei wird jede Komponente mit 0.5 multipliziert. Das Ergebnis ist vec3f(0.4, 0.2, 0.1).

Auch zwei Vektoren lassen sich komponentenweise addieren:

WGSL
let a = vec3f(1.0, 2.0, 3.0);
let b = vec3f(4.0, 5.0, 6.0);
let result = a + b;

result enthält vec3f(5.0, 7.0, 9.0).

Weitere Vektoroperationen wie dot(), cross() und normalize() haben wir bereits bei der Beleuchtung verwendet. Wie wir einzelne Komponenten auswählen und neu kombinieren, behandeln wir im nächsten Abschnitt.

Matrizen bestehen aus mehreren Zahlen, die in Spalten und Zeilen angeordnet sind. In der Grafikprogrammierung verwenden wir sie vor allem für Transformationen, beispielsweise zum Drehen und Skalieren von Objekten.

WGSL unterstützt Matrizen mit zwei bis vier Spalten und zwei bis vier Zeilen. Ihre Komponenten sind Gleitkommazahlen vom Typ f32 oder f16.

Der Typname gibt zuerst die Anzahl der Spalten und anschließend die Anzahl der Zeilen an:

Typ Bedeutung
mat2x2f 2 Spalten, 2 Zeilen
mat3x3f 3 Spalten, 3 Zeilen
mat4x4f 4 Spalten, 4 Zeilen
mat2x3f 2 Spalten, 3 Zeilen
mat3x4f 3 Spalten, 4 Zeilen

Die anderen Kombinationen sind ebenfalls möglich. Wie bei Vektoren können wir den Typ ausschreiben, beispielsweise mat4x4<f32> statt mat4x4f.

Eine 2×2-Matrix erstellen wir aus zwei Spaltenvektoren:

WGSL
let matrix = mat2x2f(
vec2f(1.0, 0.0),
vec2f(0.0, 1.0)
);

Das ist eine Einheitsmatrix. Sie verändert einen Vektor bei der Multiplikation nicht:

WGSL
let position = vec2f(2.0, 3.0);
let result = matrix * position;

result enthält weiterhin vec2f(2.0, 3.0).

Für Transformationen im dreidimensionalen Raum werden wir häufig mat4x4f verwenden. Wie diese Matrizen aufgebaut sind und miteinander verrechnet werden, behandeln wir im Kapitel über Transformationen.

Vektoren und Arrays fassen mehrere Werte zusammen, erfüllen aber unterschiedliche Aufgaben.

Einen Vektor verwenden wir, wenn zwei, drei oder vier Komponenten gemeinsam einen Wert beschreiben, beispielsweise eine Position aus drei Koordinaten oder eine Farbe aus vier Farbkomponenten. Mit solchen Vektoren können wir direkt rechnen.

Ein Array verwenden wir dagegen, wenn wir eine Sammlung von Werten benötigen, beispielsweise mehrere Farben, Positionen oder Lichtquellen. Die Elemente eines Arrays haben alle denselben Datentyp.

Ein einfaches Array mit vier Zahlen sieht so aus:

WGSL
let values = array<f32, 4>(0.2, 0.4, 0.6, 0.8);

Bei array<f32, 4> bezeichnet f32 den Datentyp der Elemente und 4 ihre Anzahl.

Wie in JavaScript greifen wir über einen Index auf einzelne Elemente zu. Die Zählung beginnt bei null:

WGSL
let first = values[0];
let second = values[1];

Stellen wir uns vor, wir möchten die Positionen von drei Punkten speichern. Jede Position besteht aus drei Koordinaten und wird deshalb durch einen vec3f beschrieben.

Für die Sammlung dieser Positionen verwenden wir ein Array:

WGSL
let positions = array<vec3f, 3>(
vec3f(0.0, 0.0, 0.0),
vec3f(1.0, 0.0, 0.0),
vec3f(0.0, 1.0, 0.0)
);

Hier enthält das Array drei Vektoren. Jeder Vektor beschreibt eine Position.

Auf die zweite Position greifen wir beispielsweise so zu:

WGSL
let position = positions[1];

position enthält nun vec3f(1.0, 0.0, 0.0).

Der Unterschied lässt sich damit gut zusammenfassen: Ein Vektor beschreibt hier eine einzelne Position, ein Array speichert mehrere Positionen.

Bisher haben wir Arrays mit einer festen Anzahl von Elementen verwendet. WGSL kennt außerdem Arrays, deren Länge sich aus der Größe eines Storage-Buffers ergibt.

Solche Arrays werden wir später kennenlernen, wenn wir größere Datenmengen auf der GPU speichern und verarbeiten. Dann sehen wir uns auch an, wie JavaScript die Daten bereitstellt und wie der Shader darauf zugreift.

Während Arrays mehrere Werte desselben Typs enthalten, können wir mit struct unterschiedliche Datentypen zusammenfassen.

Aus unserem Phong-Beispiel kennen wir bereits diese Struktur:

WGSL
struct Uniforms {
color: vec4f,
lightDirection: vec4f,
viewDirection: vec4f,
ambientFactor: f32,
diffuseStrength: f32,
shininess: f32,
};

Mit struct Uniforms definieren wir einen neuen Datentyp namens Uniforms. Er besitzt sechs Felder mit jeweils eigenem Namen und Datentyp.

Auf die einzelnen Felder greifen wir mit einem Punkt zu:

WGSL
let color = uniforms.color;
let shininess = uniforms.shininess;

Wir können Strukturen auch direkt im Shader erzeugen:

WGSL
struct Material {
color: vec3f,
shininess: f32,
};
let material = Material(vec3f(0.8, 0.4, 0.2), 32.0);

material.color liefert den Farbvektor und material.shininess den Wert 32.0.

Strukturen eignen sich besonders, wenn mehrere Werte gemeinsam ein Objekt oder eine bestimmte Eigenschaft beschreiben.

Neben Zahlen und zusammengesetzten Datenstrukturen kennt WGSL Typen für Ressourcen, auf die ein Shader zugreifen kann.

Dazu gehören insbesondere Texturen und Sampler:

WGSL
@group(0) @binding(0)
var colorTexture: texture_2d<f32>;
@group(0) @binding(1)
var colorSampler: sampler;

texture_2d<f32> bezeichnet eine zweidimensionale Textur, aus der wir Gleitkomma-Farbwerte lesen können. Ein sampler legt fest, wie beim Abfragen einer Textur zwischen ihren Bildpunkten gefiltert und mit Texturkoordinaten umgegangen wird.

WGSL besitzt außerdem weitere Texturtypen, beispielsweise texture_cube<f32> für Würfeltexturen, texture_depth_2d für Tiefentexturen und texture_storage_2d für Texturen, in die Shader Werte schreiben können.

Diese Typen verwenden wir später beim Arbeiten mit Texturen. Hier genügt zunächst die Unterscheidung: Eine Textur ist keine gewöhnliche Variable mit einigen Zahlen, sondern eine GPU-Ressource, auf die wir über entsprechende Shaderfunktionen zugreifen.

Wenn wir Daten aus JavaScript über Uniform- oder Storage-Buffer an einen Shader übergeben, müssen beide Seiten dieselbe Anordnung der Werte im Speicher verwenden.

Dabei kommt es nicht nur auf die Größe der Datentypen an, sondern auch auf ihre Ausrichtung.

Ein vec3f enthält drei f32-Werte und belegt damit 12 Bytes. Er muss in einem Buffer aber an einer Byteposition beginnen, die durch 16 teilbar ist. Deshalb können zwischen aufeinanderfolgenden Feldern einer Struktur ungenutzte Bytes liegen.

Das kennen wir bereits aus unserem Phong-Beispiel: Dort haben wir die Uniform-Daten in JavaScript so angeordnet, dass sie zum Speicherlayout der WGSL-Struktur passen.

WGSL unterscheidet verschiedene Gruppen von Datentypen:

  • Skalare wie bool, i32, u32, f32 und f16 speichern einzelne Werte.
  • Vektoren wie vec3f fassen zwei bis vier gleichartige Werte zusammen.
  • Matrizen wie mat4x4f ordnen Gleitkommazahlen in Spalten und Zeilen an.
  • Arrays speichern beliebig viele Elemente desselben Typs.
  • Strukturen verbinden Felder unterschiedlicher Datentypen.
  • Ressourcentypen wie texture_2d<f32> und sampler ermöglichen den Zugriff auf Texturen und andere GPU-Ressourcen.

Im nächsten Abschnitt sehen wir uns genauer an, wie wir auf die Komponenten von Vektoren zugreifen und sie mit Swizzling neu kombinieren können.