Interfejs ortograficzny 3D WebGL

Gregg Tavares
Gregg Tavares

Ortograficzny 3D WebGL

Ten post jest kontynuacją serii postów na temat WebGL. Najpierw zaczęło się od podstaw, a poprzednie – około dwuwymiarowych matryc – takich jak macierze 2D. Jeśli ich nie znasz, wyświetl je najpierw. W poprzednim poście omówiliśmy, jak działają macierze 2D. Mówiliśmy o przesunięciu, obrocie, skalowaniu, a nawet o przenoszeniu obrazu z pikseli w miejsce klipu, używając jednej macierz i magicznej matematyki. Aby przejść do 3D, wystarczy zrobić tylko kilka kroków. W naszych poprzednich przykładach 2D punkty 2D (x, y) pomnożyliśmy przez macierz 3 x 3. Do wykonania 3D potrzebne są punkty 3D (x, y i z) oraz macierz 4 x 4. Przejdźmy do ostatniego przykładu i przekształcimy go w widok 3D. Ponownie użyjemy litery F, ale tym razem litery „F”. Najpierw trzeba zmienić cieniowanie wierzchołków, aby obsługiwał 3D. To jest stary program do cieniowania.

<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>

A to już nowa

<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>

To jeszcze prostsze. Następnie musimy dostarczyć dane 3D.

...

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);
}

Następnie musimy zmienić wszystkie funkcje matrycy z 2D na 3D. Oto wersje 2D (przed) funkcjami MakeTranslation, MakeRotation i 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
];
}

A oto zaktualizowane wersje 3D.

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,
];
}

Zwróć uwagę, że mamy teraz 3 funkcje rotacji. Potrzebowaliśmy tylko jednego obrazu w 2D, ponieważ obracaliśmy się tylko wokół osi Z. Przy 3D chcemy też obracać widok wokół osi X i Y. Patrząc na nie, widać, że są one bardzo podobne. Gdybyśmy je sprawdzili, zauważylibyście, że są prostsze

Obrót Z

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

Obrót na osi Y


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

Obrót na osi X

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

Musimy też zaktualizować funkcję projekcji. Oto stary

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
];
}

który przekonwertował piksele w miejsce klipu. Najpierw spróbujemy rozwinąć go do 3D.

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,
];
}

Tak jak trzeba było przekonwertować piksele z pikseli na znaczniki x i y i przekształcić je w miejsce na klips, tak samo jak trzeba zrobić to samo. W tym przypadku też używam jednostek w pikselach Z. Przekażę wartość podobną do width jako głębokość, więc przestrzeń będzie mieć szerokość od 0 do szerokości w pikselach, od 0 do wysokości w pikselach, ale głębokość będzie miała postać -głębokości / 2 + głębokość / 2. Na koniec musimy zaktualizować kod, który oblicza macierz.

// 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);

Pierwszy problem polega na tym, że nasza geometria ma postać płaskiego f, przez co trudno jest zobaczyć trójwymiar. Aby to naprawić, rozszerz geometrię do 3D. Obecne F składa się z 3 prostokątów, z których każdy to 2 trójkąty. Aby obraz był trójwymiarowy, trzeba mieć łącznie 16 prostokątów. To całkiem sporo do wymienić. 16 prostokątów x 2 trójkąty na prostokąt x 3 wierzchołki na trójkąta to 96 wierzchołków. Jeśli chcesz zobaczyć wszystkie, wyświetl źródło dla przykładu. Musimy narysować więcej wierzchołków,

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

Po przesunięciu suwaków ciężko stwierdzić, czy to obraz 3D. Spróbujmy pokolorować każdy prostokąt na inny kolor. W tym celu dodamy kolejny atrybut do programu do cieniowania wierzchołków i zmieniający się sposób przekazywania go z poziomu wierzchołków do cieniowania fragmentów. Oto nowy program do cieniowania wierzchołków

<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>

Tego koloru trzeba użyć w cieniowaniu fragmentów.

<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>

Musimy wyszukać lokalizację, aby dostarczyć kolory, a następnie skonfigurować kolejny bufor i atrybut, aby nadać mu kolory.

...
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);
}

Oj, co tu bałaganu? Okazało się, że wszystkie części trójwymiarowego „F”, przodu, tyłu, boków itd. są rysowane w tej kolejności, w jakiej są widoczne w naszej geometrii. Nie przynosi to oczekiwanych wyników, ponieważ czasem elementy z tyłu są pobierane po tych z przodu. Trójkąty w WebGL mają pojęcie „z przodu i z tyłu”. Trójkąty z przodu mają wierzchołki kierunkowe w kierunku zgodnym z ruchem wskazówek zegara. Wierzchory w trójkącie tylnym biegną przeciwnie do kierunku ruchu wskazówek zegara.

Kręcimy trójkąt.

WebGL umożliwia rysowanie tylko trójkątów skierowanych do przodu lub do tyłu. Możemy włączyć tę funkcję za pomocą

gl.enable(gl.CULL_FACE);

co robimy tylko raz, na początku programu. Po włączeniu tej funkcji WebGL domyślnie „zaznacza” trójkąty skierowane do tyłu. „Culling” to w tym przypadku fantazyjne słowo „nie rysować”. Pamiętaj, że przy skonwertowaniu WebGL to, czy uznamy, że trójkąt skierowany w prawo czy w lewo zależy od wierzchołków tego trójkąta w przestrzeni klipu, zależy od jego wierzchołków. Inaczej mówiąc, WebGL określa, czy trójkąt jest z przodu, czy z tyłu, PO wykonaniu obliczeń matematycznych do wierzchołków w cieniowaniu wierzchołków. Oznacza to, że na przykład trójkąt w prawo, który skalowany jest w X o -1, staje się trójkątem skierowanym przeciwnie do ruchu wskazówek zegara, a trójkąt w prawo obrócony o 180 stopni wokół osi X lub Y staje się trójkątem skierowanym w lewo. Mieliśmy wyłączoną funkcję CULL_FACE, więc widać zarówno trójkąty zgodne z ruchem wskazówek zegara, jak i przeciwnie do kierunku ruchu wskazówek zegara. Po włączeniu tej funkcji za każdym razem, gdy trójkąt skierowany w przód obraca się wokół niej ze względu na skalowanie lub obrót bądź z innego powodu, WebGL nie zdoła go narysować. To dobrze, bo gdy obrócisz coś w 3D, zwykle chcesz, aby wszystkie trójkąty skierowane w Twoją stronę były postrzegane jako przód.

Cześć, Gdzie są wszystkie trójkąty? Okazuje się, że wiele osób patrzy w niewłaściwą stronę. Obróć go, a zobaczysz je, gdy spojrzysz na drugą stronę. Na szczęście można to łatwo naprawić. Sprawdzamy, które z nich są wsteczne, i wymieniamy 2 ich wierzchołki. Na przykład, jeśli jeden trójkąt skierowany do tyłu

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

odwracamy 2 ostatnie wierzchołki, aby przejść dalej.

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

To bliżej, ale jest jeszcze jeden problem. Nawet gdy wszystkie trójkąty są skierowane w odpowiednim kierunku, a trójkąty z tyłu są zabierane, nadal można rysować trójkąty, które powinny znajdować się z tyłu, nad trójkątami, które powinny znajdować się z przodu. Wpisz DEPTH BUFFER. Bufor głębinowy, czasem nazywany buforem Z, to prostokąt składający się z depth piks., czyli po jednym pikselu głębi na każdy piksel koloru użyty do utworzenia obrazu. Rysując każdy piksel kolorów w WebGL, można też narysować piksel głębi. Dzieje się tak na podstawie wartości zwracanych z cieniowania wierzchołków dla Z. Tak jak musieliśmy przekonwertować na przestrzeń klipu dla X i Y, więc Z jest w klipsie lub (-1 do +1). Wartość ta jest następnie konwertowana na wartość przestrzeni głębi (od 0 do +1). Przed tym, jak WebGL napisze piksel kolorów, sprawdzi odpowiedni piksel głębi. Jeśli wartość głębi w przypadku piksela, który ma zostać narysowana, jest większa niż wartość odpowiadającego mu piksela, WebGL nie rysuje nowego piksela. W przeciwnym razie piksel nowego koloru będzie rysowany z użyciem koloru z cienia fragmentów ORAZ będzie rysował piksel głębi z nową wartością głębi. Oznacza to, że piksele znajdujące się za innymi pikselami nie zostaną narysowane. Tę funkcję możemy włączyć niemal tak samo prosto, jak dotychczas

gl.enable(gl.DEPTH_TEST);

Zanim zaczniemy rysować, musimy też wyczyścić bufor głębi do 1,0.

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

W następnym poście omówię, jak stworzyć z nich różną perspektywę.

Dlaczego atrybut vec4 ma rozmiar gl.vertexAttribPointer 3?

Osoby koncentrujące się na szczegółach mogą zauważyć, że 2 atrybuty są zdefiniowane jako

attribute vec4 a_position;
attribute vec4 a_color;

oba są 'vec4', ale kiedy podamy WebGL, jak pobrać dane z buforów, użyliśmy

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

„3” w każdym z nich wskazuje tylko na pobranie 3 wartości na atrybut. To działa, ponieważ w cieniowaniu wierzchołków WebGL udostępnia ustawienia domyślne dla tych, których nie podasz. Wartości domyślne to 0, 0, 0, 1, gdzie x = 0, y = 0, z = 0, a w = 1. Właśnie dlatego w naszym starym cieniowaniu 2D musieliśmy wyraźnie podawać wartość 1. Przekazujemy wartości x i y i potrzebujemy wartości 1 dla z, ale ponieważ wartość domyślna dla z to 0, musieliśmy podać wartość 1. W przypadku widoku 3D wartość domyślna to 1, której nie wymagamy do działania matematyki.