WebGL orthografische 3D-Ansicht

Gregg Tavares
Gregg Tavares

Orthografische WebGL in 3D

Dieser Beitrag ist die Fortsetzung einer Reihe von Beiträgen zum Thema WebGL. Die erste begann mit den Grundlagen und beim vorherigen ging es um 2D-Matrizen über 2D-Matrizen. Lesen Sie sich diese bitte zuerst durch, falls Sie sie noch nicht gelesen haben. Im letzten Post haben wir die Funktionsweise von 2D-Matrizen behandelt. Wir haben über Übersetzung, Drehung, Skalierung und sogar das Projizieren von Pixeln in den Clipraum gesprochen, alles mit einer Matrix und magischen Matrizenberechnungen erfolgen. Bis dahin ist es nur noch ein kleiner Schritt. In unseren vorherigen 2D-Beispielen hatten wir 2D-Punkte (x, y), die wir mit einer 3x3-Matrix multiplizieren. Für 3D benötigen wir 3D-Punkte (x, y, z) und eine 4x4-Matrix. Ändern wir unser letztes Beispiel in 3D. Wir verwenden wieder ein F, aber diesmal ein 3D-F. Als Erstes müssen wir den Vertex-Shader so ändern, dass er 3D verarbeitet. Hier ist der alte Shader.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

Hier kommt der neue

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;

uniform mat4 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>

Es ist jetzt noch einfacher! Dann müssen wir 3D-Daten bereitstellen.

...

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

...

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        // left column
        0,   0,  0,
        30,   0,  0,
        0, 150,  0,
        0, 150,  0,
        30,   0,  0,
        30, 150,  0,

        // top rung
        30,   0,  0,
        100,   0,  0,
        30,  30,  0,
        30,  30,  0,
        100,   0,  0,
        100,  30,  0,

        // middle rung
        30,  60,  0,
        67,  60,  0,
        30,  90,  0,
        30,  90,  0,
        67,  60,  0,
        67,  90,  0]),
    gl.STATIC_DRAW);
}

Als Nächstes müssen wir alle Matrixfunktionen von 2D in 3D ändern. Hier sind die 2D-Versionen (vorher) von makeTranslation, makeRotation und makeScale.

function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}

function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

Hier sind die aktualisierten 3D-Versionen.

function makeTranslation(tx, ty, tz) {
return [
    1,  0,  0,  0,
    0,  1,  0,  0,
    0,  0,  1,  0,
    tx, ty, tz, 1
];
}

function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};

function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};

function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
    c, s, 0, 0,
-s, c, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
];
}

function makeScale(sx, sy, sz) {
return [
sx, 0,  0,  0,
0, sy,  0,  0,
0,  0, sz,  0,
0,  0,  0,  1,
];
}

Beachten Sie, dass wir jetzt 3 Rotationsfunktionen haben. Wir brauchten nur eine in 2D, da wir nur um die Z-Achse gedreht wurden. Für 3D wollen wir aber auch um die x- und die y-Achse drehen können. Wie Sie sehen, sind sie alle sehr ähnlich. Würden wir sie untersuchen, würden sie wie zuvor vereinfacht

Z-Drehung

newX = x * c + y * s;
newY = x * -s + y * c;

Y-Drehung


newX = x * c + z * s;
newZ = x * -s + z * c;

x-Rotation

newY = y * c + z * s;
newZ = y * -s + z * c;

Wir müssen auch die Projektionsfunktion aktualisieren. Hier ist der alte

function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

der von Pixeln in Abstände konvertiert wird. Beim ersten Versuch einer Erweiterung auf 3D versuchen wir,

function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}

Genauso, wie wir für x und y von Pixeln in Clipspace konvertieren mussten, müssen wir für z dasselbe machen. In diesem Fall erstelle ich auch die Pixel-Einheiten für den Z-Raum. Ich übergebe einen Wert wie width für die Tiefe, sodass der Abstand zwischen 0 und 0 Pixel breit und 0 bis 0 bis 0 Pixel hoch ist. Für die Tiefe lautet der Wert -depth / 2 bis +depth / 2. Schließlich müssen wir den Code aktualisieren, der die Matrix berechnet.

// Compute the matrices
var projectionMatrix =
    make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
    makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);

// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);

Das erste Problem ist, dass unsere Geometrie ein flaches F ist, wodurch 3D schwer zu erkennen sind. Um dieses Problem zu beheben, erweitern wir die Geometrie in 3D. Unser aktuelles F besteht aus drei Rechtecken mit jeweils 2 Dreiecken. Für eine 3D-Ansicht sind insgesamt 16 Rechtecke erforderlich. Das sind eine ganze Menge. 16 Rechtecke x 2 Dreiecke pro Rechteck x 3 Eckpunkte pro Dreieck entsprechen 96 Eckpunkten. Wenn Sie alle sehen möchten, rufen Sie die Quelle des Beispiels auf. Wir müssen mehr Eckpunkte zeichnen,

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

Wenn du die Schieberegler verschiebst, kann ich kaum erkennen, ob es sich um eine 3D-Ansicht handelt. Versuchen wir, jedem Rechteck eine andere Farbe zuzuweisen. Dazu fügen wir unserem Vertex-Shader ein weiteres Attribut und ein anderes Attribut hinzu, um es vom Vertex-Shader an den Fragment-Shader zu übergeben. Hier ist der neue Vertex-Shader

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;

// Pass the color to the fragment shader.
v_color = a_color;
}
</script>

Wir müssen diese Farbe im Fragment-Shader verwenden.

<script id="3d-vertex-shader" type="x-shader/x-fragment">
precision mediump float;

// Passed in from the vertex shader.
varying vec4 v_color;

void main() {
gl_FragColor = v_color;
}
</script>

Wir müssen den Standort suchen, um die Farben zu liefern, und dann einen weiteren Puffer und ein Attribut einrichten, um ihm die Farben zu geben.

...
var colorLocation = gl.getAttribLocation(program, "a_color");

...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);

// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

// Set Colors.
setColors(gl);

...
// Fill the buffer with colors for the 'F'.

function setColors(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Uint8Array([
        // left column front
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,

        // top rung front
    200,  70, 120,
    200,  70, 120,
    ...
    ...
    gl.STATIC_DRAW);
}

Oje, was ist das Chaos? Nun, es stellt sich heraus, dass alle verschiedenen Teile des 3D-F-Elements, der Vorder- und Rückseite, der Seiten usw. in der Reihenfolge gezeichnet werden, in der sie in unserer Geometrie erscheinen. Das Ergebnis sind nicht ganz. Dreiecke in WebGL haben das Konzept von vorne und hinten gerichtet. Die Eckpunkte eines nach vorne gerichteten Dreiecks verlaufen im Uhrzeigersinn. Die Eckpunkte eines nach hinten gerichteten Dreiecks verlaufen gegen den Uhrzeigersinn.

Gewundene Dreiecke.

Mit WebGL können nur nach vorne oder nach hinten gerichtete Dreiecke gezeichnet werden. Wir können diese Funktion

gl.enable(gl.CULL_FACE);

Das machen wir nur einmal, gleich zu Beginn unseres Programms. Wenn diese Funktion aktiviert ist, werden in WebGL standardmäßig nach hinten gerichtete Dreiecke ausgewählt. „Kulpeln“ ist in diesem Fall ein ausgefallenes Wort für „nicht zeichnen“. Soweit es um WebGL geht, hängt es von den Scheitelpunkten dieses Dreiecks im Clipspace ab, ob es berücksichtigt wird, ob ein Dreieck im oder gegen den Uhrzeigersinn verläuft. Mit anderen Worten, WebGL erkennt, ob ein Dreieck vorn oder hinten liegt, NACHDEM Sie die Mathematik auf die Eckpunkte im Vertex-Shader angewendet haben. So wird beispielsweise aus einem Dreieck im Uhrzeigersinn, das in X um -1 skaliert wird, ein Dreieck gegen den Uhrzeigersinn oder ein Dreieck im Uhrzeigersinn, das um 180 Grad um die X- oder Y-Achse gedreht wurde, zu einem Dreieck gegen den Uhrzeigersinn. Da CULL_FACE deaktiviert war, können wir sowohl Dreiecke im Uhrzeigersinn(vorn) als auch gegen den Uhrzeigersinn(hinten) sehen. Jetzt, da wir sie aktiviert haben, wird ein nach vorne gerichtetes Dreieck entweder wegen Skalierung oder Drehung oder aus irgendeinem Grund nicht gezeichnet. Das ist gut so, denn wenn du etwas in 3D umdrehst, sollen in der Regel die zu dir gerichteten Dreiecke als nach vorne gerichtet betrachtet werden.

Hi, Wo sind die Dreiecke hin? Es hat sich herausgestellt, dass viele von ihnen in die falsche Richtung blicken. Wenn Sie ihn drehen, sehen Sie auf der anderen Seite. Das lässt sich zum Glück leicht beheben. Wir sehen uns nur an, welche rückwärts sind, und tauschen 2 ihrer Eckpunkte aus. Wenn zum Beispiel ein nach hinten gerichtetes Dreieck

1,   2,   3,
40,  50,  60,
700, 800, 900,

drehen wir einfach die letzten beiden Eckpunkte, um sie nach vorne zu bringen.

1,   2,   3,
700, 800, 900,
40,  50,  60,

Das ist näher, aber da gibt es noch ein Problem. Auch wenn alle Dreiecke in die richtige Richtung zeigen und die nach hinten gerichteten Dreiecke entfernt sind, gibt es immer noch Stellen, an denen Dreiecke, die hinten sein sollten, über vorn liegenden Dreiecken gezeichnet werden. Geben Sie den DEPTH BUFFER ein. Ein Tiefenpuffer, manchmal auch Z-Puffer genannt, ist ein Rechteck aus depth Pixeln, einem Tiefenpixel für jedes Farbpixel, das zum Erstellen des Bildes verwendet wird. Da WebGL jedes Farbpixel zeichnet, kann es auch ein Tiefenpixel zeichnen. Dies geschieht auf Basis der Werte, die wir vom Vertex-Shader für Z zurückgeben. Genau wie bei der Umwandlung in einen Clipraum für X und Y, also befindet sich Z im Clipbereich oder (-1 bis +1). Dieser Wert wird dann in einen Tiefenraumwert (0 bis +1) umgewandelt. Bevor WebGL ein Farbpixel zeichnet, wird das entsprechende Tiefenpixel überprüft. Wenn der Tiefenwert des Pixels, das gezeichnet werden soll, größer ist als der Wert des entsprechenden Tiefenpixels, zeichnet WebGL das neue Farbpixel nicht. Andernfalls wird sowohl das neue Farbpixel mit der Farbe aus Ihrem Fragment-Shader als auch das Tiefenpixel mit dem neuen Tiefenwert bezogen. Pixel, die sich hinter anderen Pixeln befinden, werden also nicht gezeichnet. Diese Funktion kann fast genauso einfach aktiviert werden wie

gl.enable(gl.DEPTH_TEST);

Außerdem muss der Tiefenpuffer wieder auf 1,0 gelöscht werden, bevor wir mit dem Zeichnen beginnen.

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

Im nächsten Post zeige ich Ihnen, wie Sie eine Perspektive darstellen.

Warum hat das Attribut „vec4“, aber „gl.vertexAttribPointer“ Größe 3

Denjenigen unter Ihnen, die detailorientiert sind, ist Ihnen vielleicht aufgefallen, dass wir unsere beiden Attribute als

attribute vec4 a_position;
attribute vec4 a_color;

die beide "vec4" sind, aber als wir WebGL mitteilen, wie Daten aus den Puffern entnommen werden sollen,

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

Diese „3“ in jedem dieser Punkte besagt, dass nur 3 Werte pro Attribut ausgegeben werden sollen. Dies funktioniert, da WebGL im Vertex-Shader Standardeinstellungen für nicht angegebene Werte zur Verfügung stellt. Die Standardwerte sind 0, 0, 0, 1, wobei x = 0, y = 0, z = 0 und w = 1 ist. Aus diesem Grund mussten wir in unserem alten 2D-Vertex-Shader die 1 explizit angeben. Wir haben x und y übergeben und brauchten eine 1 für z. Da der Standardwert für z jedoch 0 ist, mussten wir explizit eine 1 angeben. Obwohl wir für 3D kein „w“ angeben, wird standardmäßig 1 verwendet, was wir benötigen, damit die Matrixberechnung funktioniert.