3D ortografi WebGL

Gregg Tavares
Gregg Tavares

3D Ortografi WebGL

Postingan ini merupakan kelanjutan dari serangkaian postingan tentang WebGL. Yang pertama dimulai dengan dasar-dasar dan sebelumnya adalah tentang matriks 2d sekitar matriks 2D. Jika Anda belum membacanya, harap lihat laporan itu terlebih dahulu. Di postingan sebelumnya kita telah membahas cara kerja matriks 2d. Kita membahas terjemahan, rotasi, penskalaan, dan bahkan memproyeksikan dari piksel ke dalam ruang klip, semuanya dapat dilakukan dengan 1 matriks dan beberapa perhitungan matriks ajaib. Melakukan 3D hanyalah satu langkah kecil dari sana. Pada contoh 2D sebelumnya, kami memiliki titik 2D (x, y) yang kami perkali dengan matriks 3x3. Untuk melakukan 3D kita membutuhkan titik 3D (x, y, z) dan matriks 4x4. Mari kita ambil contoh terakhir kita dan ubah ke 3D. Kita akan menggunakan F lagi tapi kali ini 'F' 3D. Hal pertama yang perlu kita lakukan adalah mengubah shader verteks 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>

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>

Ini bahkan menjadi lebih sederhana. 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 Inilah 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 ini versi 3D terbaru.

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 kita sekarang memiliki 3 fungsi rotasi. Kita hanya perlu satu dalam 2D karena sebenarnya kita hanya berputar di sekitar sumbu Z. Untuk melakukan 3D, kita juga ingin dapat memutar sumbu x dan sumbu y juga. Anda bisa melihat dari melihat mereka semua sangat mirip. Jika kita mengerjakannya, Anda akan melihatnya menyederhanakannya 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. Ini 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 {i>pixel<i} menjadi 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 clipspace 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 menjadi lebar 0 hingga lebar piksel, tinggi piksel 0 hingga tinggi, tetapi untuk kedalaman 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 adalah bahwa geometri kita adalah F datar yang membuat sulit untuk melihat 3D. Untuk memperbaikinya, mari luaskan geometri ke 3D. F saat ini terdiri dari 3 persegi panjang, masing-masing 2 segitiga. Untuk membuatnya 3D membutuhkan total 16 persegi panjang. Itu cukup banyak untuk 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 melihat sumber pada sampel. Kita harus menggambar lebih banyak verteks sehingga

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

Memindahkan penggeser cukup sulit untuk mengatakan bahwa itu 3D. Mari kita coba mewarnai setiap persegi panjang dengan warna yang berbeda. Untuk melakukannya, kita akan menambahkan atribut lain ke shader verteks dan berbagai untuk meneruskannya dari shader verteks ke shader fragmen. Berikut adalah shader verteks yang 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>

Kita perlu menggunakan warna tersebut dalam 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);
}

Aduh, berantakan apa? Ternyata semua bagian 'F' 3D, bagian depan, belakang, sisi, dll. digambar sesuai urutan kemunculannya dalam geometri kita. Hal tersebut tidak memberikan hasil yang diinginkan karena terkadang yang ada di belakang digambar setelah yang di depan. Segitiga di WebGL memiliki konsep menghadap ke depan dan belakang. Segitiga yang menghadap ke depan memiliki verteksnya searah jarum jam. Segitiga yang menghadap ke belakang memiliki verteksnya yang berlawanan arah jarum jam.

Lilitan segitiga.

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

gl.enable(gl.CULL_FACE);

yang hanya sekali saja dilakukan, saat kita memulai program. Dengan mengaktifkan fitur tersebut, WebGL secara default "menghilangkan" segitiga yang menghadap ke belakang. "Culling" dalam hal ini adalah kata yang elegan untuk "tidak menggambar". Perhatikan bahwa sejauh WebGL dipertimbangkan, status segitiga dianggap searah jarum jam atau berlawanan arah jarum jam tergantung pada verteks segitiga tersebut dalam clipspace. Dengan kata lain, WebGL mengetahui apakah sebuah segitiga berada di depan atau di belakang SETELAH Anda menerapkan perhitungan pada verteks dalam shader verteks. Artinya, misalnya segitiga searah jarum jam yang diskalakan di X dengan -1 akan menjadi segitiga berlawanan arah jarum jam, atau segitiga searah jarum jam yang diputar 180 derajat di sekeliling sumbu X atau Y menjadi segitiga berlawanan arah jarum jam. Karena kita menonaktifkan CULL_FACE, 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 karena penskalaan atau rotasi atau karena alasan apa pun, WebGL tidak akan menggambarnya. Ini adalah hal yang baik karena saat memutar sesuatu dalam 3D, umumnya Anda ingin segitiga yang menghadap Anda dianggap menghadap depan.

Hei! Ke mana semua segitiga? Ternyata, banyak dari mereka menghadapi cara yang salah. Putar kartu dan Anda akan melihatnya muncul saat melihat ke sisi lain. Untungnya, masalah ini mudah diperbaiki. Kita hanya melihat yang mundur dan bertukar 2 verteksnya. Misalnya jika satu segitiga mundur adalah

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

kita hanya membalik 2 titik terakhir untuk membuatnya maju.

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

Itu hampir selesai, tapi masih ada satu masalah lagi. Meskipun semua segitiga menghadap ke arah yang benar dan yang bagian belakangnya menghadap ke belakang, kita masih memiliki tempat 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 yang terdiri dari piksel depth, satu piksel kedalaman untuk setiap piksel warna yang digunakan untuk membuat gambar. Karena WebGL menggambar setiap piksel warna, WebGL juga dapat menggambar piksel kedalaman. Hal ini dilakukan berdasarkan nilai yang kita tampilkan dari shader verteks untuk Z. Sama seperti kita harus mengonversi ruang klip untuk X dan Y, jadi Z ada di ruang klip atau (-1 hingga +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, GPU akan menggambar piksel warna baru dengan warna dari shader fragmen Anda DAN menggambar piksel kedalaman dengan nilai kedalaman yang baru. Artinya, piksel yang berada di belakang piksel lain tidak akan digambar. Kita dapat mengaktifkan fitur ini hampir sama seperti kita mengaktifkan penghapusan 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 gl.vertexAttribPointer ukuran 3

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

attribute vec4 a_position;
attribute vec4 a_color;

keduanya adalah 'vec4' tetapi saat kita 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);

'3' di masing-masing atribut hanya dapat menarik 3 nilai per atribut. Cara ini berfungsi karena WebGL menyediakan setelan default untuk shader verteks yang tidak Anda sediakan. Defaultnya adalah 0, 0, 0, 1 dengan x = 0, y = 0, z = 0, dan w = 1. Inilah sebabnya mengapa dalam shader verteks 2D lama, kami harus secara eksplisit menyediakan angka 1. Kita meneruskan x dan y dan membutuhkan 1 untuk z, tetapi karena nilai defaultnya adalah 0, kita harus menyediakan 1 secara eksplisit. Untuk 3D, meskipun kita tidak memberikan 'w', defaultnya adalah 1 yang kita perlukan agar matematika matriks berfungsi.