3D ortografi WebGL

Gregg Tavares
Gregg Tavares

3D Ortografik WebGL

Postingan ini adalah kelanjutan dari serangkaian postingan tentang WebGL. Yang pertama dimulai dengan dasar-dasar dan yang sebelumnya adalah tentang matriks 2D tentang matriks 2D. Jika Anda belum membacanya, lihat terlebih dahulu. Di postingan terakhir, kita telah membahas cara kerja matriks 2D. Kita telah membahas tentang terjemahan, rotasi, penskalaan, dan bahkan proyeksi dari piksel ke ruang klip yang semuanya dapat dilakukan oleh 1 matriks dan beberapa matematika matriks ajaib. Untuk melakukan 3D, Anda hanya perlu melakukan langkah kecil dari sana. Pada contoh 2D sebelumnya, kita memiliki titik 2D (x, y) yang dikalikan dengan matriks 3x3. Untuk melakukan 3D, kita memerlukan titik 3D (x, y, z) dan matriks 4x4. Mari kita ambil contoh terakhir dan ubah menjadi 3D. Kita akan menggunakan F lagi, tetapi kali ini 'F' 3D. Hal pertama yang perlu kita lakukan adalah mengubah vertex shader untuk menangani 3D. Berikut adalah shader lama.

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

Dan ini yang baru

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

Caranya kini lebih mudah. Kemudian, kita perlu memberikan data 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);
}

Selanjutnya, kita perlu mengubah semua fungsi matriks dari 2D menjadi 3D Berikut adalah versi 2D (sebelum) dari makeTranslation, makeRotation, dan 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
];
}

Berikut adalah versi 3D yang diperbarui.

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

Perhatikan bahwa sekarang kita memiliki 3 fungsi rotasi. Kita hanya memerlukan satu dalam 2D karena kita hanya berputar secara efektif di sekitar sumbu Z. Namun, untuk melakukan 3D, kita juga ingin dapat memutar di sekitar sumbu x dan sumbu y. Anda dapat melihat dari tampilannya bahwa semuanya sangat mirip. Jika kita mengerjakannya, Anda akan melihatnya disederhanakan seperti sebelumnya

Rotasi Z

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

Rotasi Y


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

Rotasi X

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

Kita juga perlu memperbarui fungsi proyeksi. Berikut versi lama

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

yang dikonversi dari piksel ke ruang klip. Untuk upaya pertama kita dalam memperluasnya ke 3D, mari kita coba

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

Sama seperti kita perlu mengonversi dari piksel ke ruang klip untuk x dan y, untuk z, kita perlu melakukan hal yang sama. Dalam hal ini, saya juga membuat unit piksel ruang Z. Saya akan meneruskan beberapa nilai yang mirip dengan width untuk kedalaman sehingga ruang kita akan memiliki lebar 0 hingga lebar piksel, tinggi 0 hingga tinggi piksel, tetapi untuk kedalaman, nilainya akan menjadi -depth / 2 hingga +depth / 2. Terakhir, kita perlu memperbarui kode yang menghitung matriks.

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

Masalah pertama yang kita miliki adalah geometri kita adalah F datar yang membuat sulit melihat 3D. Untuk memperbaikinya, mari kita luaskan geometri ke 3D. F saat ini terdiri dari 3 persegi panjang, masing-masing 2 segitiga. Untuk membuatnya menjadi 3D, Anda memerlukan total 16 persegi panjang. Ada cukup banyak hal yang harus dicantumkan di sini. 16 persegi panjang x 2 segitiga per persegi panjang x 3 verteks per segitiga adalah 96 verteks. Jika Anda ingin melihat semuanya, lihat sumber tampilan pada contoh. Kita harus menggambar lebih banyak vertex sehingga

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

Saat menggeser penggeser, cukup sulit untuk mengetahui bahwa gambar tersebut adalah 3D. Mari kita coba mewarnai setiap persegi panjang dengan warna yang berbeda. Untuk melakukannya, kita akan menambahkan atribut lain ke shader vertex dan variasi untuk meneruskannya dari shader vertex ke shader fragmen. Berikut adalah shader vertex baru

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

Dan kita perlu menggunakan warna tersebut di shader fragmen

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

Kita perlu mencari lokasi untuk menyediakan warna, lalu menyiapkan buffer dan atribut lain untuk memberinya warna.

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

Uh oh, apa yang berantakan itu? Ternyata, semua bagian 'F' 3D, depan, belakang, samping, dll. digambar sesuai urutan kemunculannya dalam geometri kita. Hal ini tidak memberikan hasil yang diinginkan karena terkadang yang di belakang digambar setelah yang di depan. Segitiga di WebGL memiliki konsep menghadap ke depan dan menghadap ke belakang. Segitiga yang menghadap ke depan memiliki verteks yang mengarah searah jarum jam. Segitiga yang menghadap ke belakang memiliki verteks yang mengarah ke arah berlawanan dengan arah jarum jam.

Penggulungan segitiga.

WebGL memiliki kemampuan untuk menggambar hanya segitiga yang menghadap ke depan atau ke belakang. Kita dapat mengaktifkan fitur tersebut dengan

gl.enable(gl.CULL_FACE);

yang kita lakukan hanya sekali, tepat di awal program. Dengan fitur tersebut diaktifkan, WebGL akan "menghilangkan" segitiga yang menghadap ke belakang secara default. Dalam hal ini, "Pemotongan" adalah kata yang bagus untuk "tidak menggambar". Perhatikan bahwa sejauh WebGL, apakah segitiga dianggap bergerak searah jarum jam atau berlawanan arah jarum jam bergantung pada vertex segitiga tersebut di ruang klip. Dengan kata lain, WebGL mengetahui apakah segitiga berada di depan atau di belakang SETELAH Anda menerapkan matematika ke vertikal di vertex shader. Artinya, misalnya segitiga searah jarum jam yang diskalakan di X dengan -1 menjadi segitiga berlawanan arah jarum jam atau segitiga searah jarum jam yang diputar 180 derajat di sekitar sumbu X atau Y menjadi segitiga berlawanan arah jarum jam. Karena CULL_FACE dinonaktifkan, kita dapat melihat segitiga searah jarum jam(depan) dan berlawanan arah jarum jam(belakang). Setelah kita mengaktifkannya, setiap kali segitiga yang menghadap ke depan berbalik karena penskalaan atau rotasi atau karena alasan apa pun, WebGL tidak akan menggambarnya. Hal ini bagus karena saat memutar sesuatu dalam 3D, Anda umumnya ingin segitiga mana pun yang menghadap Anda dianggap menghadap depan.

Hei! Ke mana semua segitiga pergi? Ternyata, banyak dari mereka menghadap ke arah yang salah. Putar dan Anda akan melihatnya muncul saat melihat sisi lain. Untungnya, masalah ini mudah diperbaiki. Kita hanya melihat mana yang mundur dan menukar 2 vertex-nya. Misalnya, jika satu segitiga mundur adalah

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

kita hanya membalik 2 vertex terakhir untuk membuatnya maju.

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

Hasilnya sudah lebih mendekati, tetapi masih ada satu masalah lagi. Meskipun semua segitiga menghadap ke arah yang benar dan segitiga yang menghadap ke belakang dihapus, kita masih memiliki tempat-tempat yang segitiganya seharusnya berada di belakang, tetapi digambar di atas segitiga yang seharusnya berada di depan. Masukkan DEPTH BUFFER. Buffer kedalaman, yang terkadang disebut Buffer Z, adalah persegi panjang dari depth piksel, satu piksel kedalaman untuk setiap piksel warna yang digunakan untuk membuat gambar. Saat menggambar setiap piksel warna, WebGL juga dapat menggambar piksel kedalaman. Hal ini dilakukan berdasarkan nilai yang kita tampilkan dari shader vertex untuk Z. Sama seperti kita harus mengonversi ke ruang klip untuk X dan Y, sehingga Z berada dalam ruang klip atau (-1 ke +1). Nilai tersebut kemudian dikonversi menjadi nilai ruang kedalaman (0 hingga +1). Sebelum menggambar piksel warna, WebGL akan memeriksa piksel kedalaman yang sesuai. Jika nilai kedalaman untuk piksel yang akan digambar lebih besar dari nilai piksel kedalaman yang sesuai, WebGL tidak akan menggambar piksel warna baru. Jika tidak, shader akan menggambar piksel warna baru dengan warna dari shader fragmen DAN menggambar piksel kedalaman dengan nilai kedalaman baru. Artinya, piksel yang berada di belakang piksel lain tidak akan digambar. Kita dapat mengaktifkan fitur ini hampir semudah mengaktifkan pemusnahan dengan

gl.enable(gl.DEPTH_TEST);

Kita juga perlu menghapus buffer kedalaman kembali ke 1.0 sebelum mulai menggambar.

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

Di postingan berikutnya, saya akan membahas cara membuatnya memiliki perspektif.

Mengapa atribut vec4, tetapi ukuran gl.vertexAttribPointer 3

Bagi Anda yang berorientasi pada detail, Anda mungkin telah melihat bahwa kami menentukan 2 atribut sebagai

attribute vec4 a_position;
attribute vec4 a_color;

keduanya adalah 'vec4', tetapi saat memberi tahu WebGL cara mengambil data dari buffering yang kita gunakan

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

'3' di setiap atribut tersebut hanya menunjukkan untuk mengambil 3 nilai per atribut. Hal ini berfungsi karena di vertex shader, WebGL menyediakan default untuk yang tidak Anda berikan. Nilai defaultnya adalah 0, 0, 0, 1 dengan x = 0, y = 0, z = 0, dan w = 1. Itulah sebabnya dalam shader vertex 2D lama, kita harus menyediakan 1 secara eksplisit. Kita meneruskan x dan y dan kita memerlukan 1 untuk z, tetapi karena default untuk z adalah 0, kita harus secara eksplisit memberikan 1. Namun, untuk 3D, walaupun kita tidak menyediakan 'w', nilai defaultnya adalah 1, yang kita perlukan agar matematika matriks berfungsi.