Interfejs ortograficzny 3D WebGL

Gregg Tavares
Gregg Tavares

WebGL Orthographic 3D

Ten post jest kontynuacją serii postów na temat WebGL. Pierwsza zaczyna się od podstaw, a poprzednia dotyczyła macierzy 2D oraz macierzy 2D. Jeśli ich nie znasz, zapoznaj się z nimi najpierw. W poprzednim poście omawialiśmy działanie macierzy 2D. Omawialiśmy przesuwanie, obracanie, skalowanie, a nawet przekształcanie pikseli w przestrzeń klipu. Wszystko to można wykonać za pomocą 1 matrycy i kilku magicznych obliczeń. Od tego do tworzenia modeli 3D dzieli tylko jeden krok. W poprzednich przykładach 2D mieliśmy punkty 2D (x, y), które pomnożyliśmy przez macierz 3 x 3. Aby uzyskać obraz 3D, potrzebujemy punktów 3D (x, y, z) i macierzy 4 x 4. Weźmy nasz ostatni przykład i zmodyfikujmy go, aby był trójwymiarowy. Ponownie użyjemy litery F, ale tym razem w wymiarach 3D. Najpierw musimy zmienić shader wierzchołkowy, aby obsługiwał 3D. Oto stary 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>

A oto nowy

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

Stało się to jeszcze prostsze. Następnie musimy podać 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 macierzy z 2D na 3D. Oto wersje 2D (wcześniejsze) funkcji 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 tutaj są 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 obrotu. Potrzebowaliśmy tylko 1 w formacie 2D, ponieważ obracanie odbywało się tylko wokół osi Z. Aby jednak uzyskać obraz 3D, chcemy też mieć możliwość obracania obiektu wokół osi X i Y. Jak widać, są one bardzo podobne. Jeśli je rozwiążemy, zobaczysz, że uproszczą się tak jak wcześniej.

Obrót Z

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

Obrót w osi Y


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

Obrót w 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óre zostały przekonwertowane z pikseli na miejsce w klipie. Nasz pierwszy test rozszerzenia obrazu 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,
];
}

Podobnie jak w przypadku x i y, w przypadku z musimy przekształcić piksele na przestrzeń ujętą w ramki. W tym przypadku ustawiam też piksele przestrzeni Z. Przekażę wartość podobną do width dla głębi, więc nasza przestrzeń będzie miała szerokość od 0 pikseli do szerokości, a wysokość od 0 pikseli do wysokości, ale głębia będzie się zmieniać od -głębi / 2 do +głębi / 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);

Pierwszym problemem jest to, że geometria jest płaska, co utrudnia oglądanie w 3D. Aby to naprawić, rozszerz geometrię do 3D. Nasz obecny F składa się z 3 prostokątów i 2 trójkątów. Aby uzyskać efekt 3D, potrzebujesz łącznie 16 prostokątów. To całkiem sporo. 16 prostokątów x 2 trójkąty na prostokąt x 3 wierzchołki na trójkąt = 96 wierzchołków. Jeśli chcesz zobaczyć wszystkie, wyświetl źródło w przypadku próbki. Musimy narysować więcej wierzchołków, aby

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

Podczas przesuwania suwaków trudno zauważyć, że jest to obraz 3D. Spróbujmy pokolorować każdy prostokąt innym kolorem. Aby to zrobić, dodamy do naszego shadera wierzchołka kolejny atrybut i zmienną, która przekaże go z shadera wierzchołka do shadera fragmentu. Oto nowy shader wierzchołkowy

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

Musimy użyć tego koloru w fragment shader

<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 sprawdzić lokalizację, aby uzyskać kolory, a potem skonfigurować inny 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);
}

Co to za bałagan? Okazuje się, że wszystkie różne części tego 3D „F”, przód, tył, boki itp. są rysowane w kolejności, w jakiej występują w naszej geometrii. Nie przynosi to jednak oczekiwanych rezultatów, ponieważ czasami te z tyłu są rysowane po tych z przodu. Trójkąty w WebGL mają pojęcie przód i tył. Trójkąt zwrócony przodem ma wierzchołki skierowane zgodnie z ruchem wskazówek zegara. Trójkąt zwrócony w tyłek ma wierzchołki skierowane w przeciwnym kierunku do ruchu wskazówek zegara.

Trójkątny.

WebGL może rysować tylko skierowane do przodu lub do tyłu trójkąty. Możemy włączyć tę funkcję za pomocą

gl.enable(gl.CULL_FACE);

robimy to tylko raz, na samym początku programu. Gdy ta funkcja jest włączona, WebGL domyślnie „wycina” trójkąty skierowane w stronę tylną. W tym przypadku słowo „wycinanie” jest wyrafinowanym określeniem „nie rysowania”. Pamiętaj, że w przypadku WebGL to, czy trójkąt jest uważany za rysowany zgodnie z kierunkiem wskazówek zegara czy przeciwnie do niego, zależy od wierzchołków tego trójkąta w przestrzeni wycinania. Inaczej mówiąc, WebGL ustala, czy trójkąt jest z przodu czy z tyłu, PO zastosowaniu obliczeń do wierzchołków w. Oznacza to na przykład, że trójkąt obrócony zgodnie z ruchem wskazówek zegara, który jest pomniejszony w osi X o -1, staje się trójkątem obraconym w przeciwnym kierunku. Trójkąt obrócony zgodnie z ruchem wskazówek zegara o 180 stopni wokół osi X lub Y staje się trójkątem obraconym w przeciwnym kierunku. Ponieważ opcja CULL_FACE była wyłączona, widzimy trójkąty zarówno w kierunku zgodnym z kierunkiem ruchu wskazówek zegara(z przodu), jak i przeciwnym do niego(z tyłu). Teraz, gdy włączyliśmy tę opcję, WebGL nie będzie rysować trójkąta skierowanego przodem, który się przekręca z powodu skalowania, obrotu lub z jakiegokolwiek innego powodu. To dobra rzecz, ponieważ obracając obiekt w 3D, zazwyczaj chcesz, aby wierzchołki, które są skierowane w Twoją stronę, były uważane za wierzchołki przedniej krawędzi.

Hej! Gdzie się podziały wszystkie trójkąty? Okazuje się, że wiele z nich jest skierowanych w nieodpowiednią stronę. Obróć go, a po drugiej stronie zobaczysz te same informacje. Na szczęście można to łatwo naprawić. Wystarczy, że sprawdzimy, które z nich są odwrócone, i zamiennie ustawimy 2 ich wierzchołki. Jeśli na przykład jeden trójkąt wsteczny jest

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

wystarczy odwrócić 2 ostatnie wierzchołki, aby przesunąć obiekt do przodu.

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

To już lepiej, ale nadal jest jeszcze jeden problem. Nawet jeśli wszystkie trójkąty są skierowane we właściwym kierunku, a te, które są skierowane do tyłu, są odrzucane, nadal mamy miejsca, w których trójkąty, które powinny być z tyłu, są rysowane na wierzchu trójkątów, które powinny być z przodu. Wpisz DEPTH BUFFER. Bufor głębi, zwany też czasem buforem Z, to prostokąt złożony z depth pikseli, z których każdy piksel głębi odpowiada jednemu pikselowi koloru użytemu do utworzenia obrazu. Podczas rysowania każdego kolorowego piksela WebGL może też rysować piksele głębi. Robi to na podstawie wartości zwracanych przez shader wierzchołkowy dla Z. Podobnie jak w przypadku X i Y, musieliśmy przekształcić Z do przestrzeni klipu (od -1 do +1). Ta wartość jest następnie przekształcana w wartość przestrzeni głębokości (0 do +1). Zanim WebGL narysuje piksel koloru, sprawdza odpowiedni piksel głębokości. Jeśli wartość głębi piksela, który ma zostać narysowany, jest większa niż wartość odpowiadającego mu piksela głębi, WebGL nie rysuje nowego piksela koloru. W przeciwnym razie rysuje zarówno nowy piksel koloru za pomocą koloru z fragment shadera, jak i piksel głębi za pomocą nowej wartości głębi. Oznacza to, że piksele, które znajdują się za innymi pikselami, nie zostaną narysowane. Możemy włączyć tę funkcję prawie tak samo łatwo, jak włączenie odrzucania z

gl.enable(gl.DEPTH_TEST);

Zanim zaczniemy rysować, musimy też wyczyścić bufor głębi z wartością 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 opiszę, jak nadać zdjęciu perspektywy.

Dlaczego atrybut vec4, ale gl.vertexAttribPointer ma rozmiar 3

Osoby zwracające uwagę na szczegóły mogły zauważyć, że zdefiniowaliśmy 2 atrybuty jako

attribute vec4 a_position;
attribute vec4 a_color;

Oba te typy są typu „vec4”, ale gdy podajemy WebGL informacje o tym, jak pobrać dane z buforów, używamy typu „vec4”.

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

Liczba „3” w każdym z nich oznacza, że mają być wyodrębnione 3 wartości na atrybut. Działa to, ponieważ w shaderze wierzchołkowym WebGL udostępnia domyślne wartości dla tych, których nie podasz. Wartości domyślne to 0, 0, 0, 1, gdzie x = 0, y = 0, z = 0 i w = 1. Dlatego w starym 2D-weksyjnym shaderze wierzchołków musieliśmy jawnie podać 1. Przekazaliśmy wartości x i y, a potrzebowaliśmy 1 dla z, ale ponieważ domyślna wartość z to 0, musieliśmy wyraźnie podać 1. W przypadku obrazu 3D, mimo że nie podajemy wartości „w”, domyślnie jest ona ustawiana na 1, co jest wymagane do działania obliczeń macierzowych.