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