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