3D ortografi WebGL

Gregg Tavares
Gregg Tavares

3D ortografis WebGL

Postingan ini adalah kelanjutan dari rangkaian postingan tentang WebGL. Yang pertama dimulai dengan dasar-dasar dan yang sebelumnya membahas matriks 2D tentang matriks 2D. Jika Anda belum membacanya, lihat terlebih dahulu. Dalam postingan terakhir, kita membahas cara kerja matriks 2D. Kita telah membahas bahwa translasi, rotasi, penskalaan, dan bahkan proyeksi dari piksel ke ruang klip semuanya dapat dilakukan dengan 1 matriks dan beberapa matematika matriks ajaib. Untuk membuat 3D, hanya perlu satu langkah kecil dari sana. Dalam 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 mengubahnya menjadi 3D. Kita akan menggunakan F lagi, tetapi kali ini 'F' 3D. Hal pertama yang perlu kita lakukan adalah mengubah shader vertex 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>

Kini makin mudah. Kemudian, kita perlu menyediakan 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 (sebelumnya) 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
];
}

Dan berikut 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 sudut dalam 2D karena kita hanya berputar di sekitar sumbu Z. Namun, untuk melakukan 3D, kita juga ingin dapat memutar di sekitar sumbu x dan sumbu y. Anda dapat melihat bahwa semuanya sangat mirip. Jika kita menyelesaikannya, 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 yang 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 piksel lebar, tinggi 0 hingga piksel tinggi, tetapi untuk kedalaman, nilainya akan menjadi -kedalaman / 2 hingga +kedalaman / 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 hadapi adalah geometri kita adalah F datar yang membuatnya sulit untuk melihat 3D apa pun. Untuk memperbaikinya, mari kita perluas geometri ke 3D. F kita saat ini terdiri dari 3 persegi panjang, masing-masing 2 segitiga. Untuk membuatnya menjadi 3D, diperlukan total 16 persegi panjang. Ada cukup banyak yang bisa 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 pada contoh. Kita harus menggambar lebih banyak verteks, jadi

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

Saat menggerakkan penggeser, sulit untuk mengetahui bahwa ini adalah 3D. Mari kita coba mewarnai setiap persegi panjang dengan warna yang berbeda. Untuk melakukannya, kita akan menambahkan atribut lain ke shader vertex dan variabel untuk meneruskannya dari shader vertex ke shader fragmen. Berikut shader verteks 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 memberikan 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);
}

Waduh, berantakan sekali. Ternyata semua bagian dari 'F' 3D, depan, belakang, samping, dll. digambar sesuai urutan kemunculannya dalam geometri kita. Hal ini tidak memberikan hasil yang diinginkan karena terkadang objek di belakang digambar setelah objek 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 berlawanan arah jarum jam.

Penggulungan segitiga.

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

gl.enable(gl.CULL_FACE);

yang kita lakukan sekali saja, tepat di awal program kita. Dengan mengaktifkan fitur tersebut, WebGL secara default akan "memangkas" segitiga yang menghadap ke belakang. "Culling" dalam hal ini adalah istilah keren untuk "tidak menggambar". Perhatikan bahwa sejauh menyangkut WebGL, apakah segitiga dianggap searah jarum jam atau berlawanan arah jarum jam bergantung pada verteks segitiga tersebut di ruang klip. Dengan kata lain, WebGL menentukan apakah segitiga berada di depan atau belakang SETELAH Anda menerapkan matematika ke verteks di shader verteks. Artinya, misalnya, segitiga searah jarum jam yang diskalakan di X sebesar -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 terbalik, baik karena penskalaan atau rotasi atau karena alasan apa pun, WebGL tidak akan menggambarnya. Hal ini bagus karena saat Anda memutar sesuatu dalam 3D, Anda biasanya ingin segitiga mana pun yang menghadap Anda dianggap menghadap ke depan.

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

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

kita hanya perlu membalik 2 verteks terakhir untuk membuatnya maju.

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

Jawaban itu lebih mendekati, tetapi masih ada satu masalah lagi. Meskipun semua segitiga menghadap ke arah yang benar dan segitiga yang menghadap ke belakang dihilangkan, kita masih memiliki tempat di mana segitiga yang seharusnya berada di belakang digambar di atas segitiga yang seharusnya berada di depan. Masukkan DEPTH BUFFER. Buffer kedalaman, terkadang disebut Z-Buffer, adalah persegi panjang piksel depth, satu piksel kedalaman untuk setiap piksel warna yang digunakan untuk membuat gambar. Saat WebGL menggambar setiap piksel warna, WebGL juga dapat menggambar piksel kedalaman. Hal ini dilakukan berdasarkan nilai yang ditampilkan dari shader verteks untuk Z. Sama seperti kita harus mengonversi ke ruang klip untuk X dan Y, Z juga berada di ruang klip atau (-1 hingga +1). Nilai tersebut kemudian dikonversi menjadi nilai ruang kedalaman (0 hingga +1). Sebelum WebGL menggambar piksel warna, WebGL akan memeriksa piksel kedalaman yang sesuai. Jika nilai kedalaman untuk piksel yang akan digambar lebih besar daripada nilai piksel kedalaman yang sesuai, WebGL tidak akan menggambar piksel warna baru. Jika tidak, GPU 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);
...

Dalam postingan berikutnya, saya akan membahas cara membuat gambar memiliki perspektif.

Mengapa atributnya vec4, tetapi ukuran gl.vertexAttribPointer adalah 3

Bagi Anda yang berorientasi pada detail, Anda mungkin telah memperhatikan bahwa kami mendefinisikan 2 atribut kami sebagai

attribute vec4 a_position;
attribute vec4 a_color;

keduanya adalah 'vec4', tetapi saat memberi tahu WebGL cara mengambil data dari buffer, kita menggunakan

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

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