3D lakukan sendiri. Bagian 2: itu tiga dimensi



Di bagian sebelumnya, kami menemukan cara menampilkan objek dua dimensi, seperti piksel dan garis (segmen), tetapi Anda benar-benar ingin membuat sesuatu tiga dimensi dengan cepat. Pada artikel ini, untuk pertama kalinya, kami akan mencoba menampilkan objek 3D di layar dan berkenalan dengan objek matematika baru, seperti vektor dan matriks, serta beberapa operasi pada mereka, tetapi hanya yang dapat diterapkan dalam praktiknya.

Pada bagian kedua kami akan mempertimbangkan:

  • Sistem koordinat
  • Dot dan vektor
  • Matriks
  • Verteks dan Indeks
  • Konveyor visualisasi

Sistem koordinat


Perlu dicatat bahwa beberapa contoh dan operasi dalam artikel disajikan secara tidak akurat dan sangat disederhanakan untuk meningkatkan pemahaman materi, memahami esensi, Anda dapat secara mandiri menemukan solusi terbaik atau memperbaiki kesalahan dan ketidakakuratan dalam kode demo. Sebelum kita menggambar sesuatu tiga dimensi, penting untuk diingat bahwa semua tiga dimensi pada layar ditampilkan dalam piksel dua dimensi. Agar objek yang ditarik oleh piksel terlihat tiga dimensi, kita perlu membuat sedikit matematika. Kami tidak akan mempertimbangkan formula dan objek tanpa melihat aplikasi mereka. Itu sebabnya, semua operasi matematika yang akan Anda temui dalam artikel ini akan dipraktikkan, yang akan menyederhanakan pemahaman mereka. 

Hal pertama yang harus dipahami adalah sistem koordinat. Mari kita lihat sistem koordinat mana yang digunakan, dan juga pilih mana yang akan digunakan untuk kita.


Apa itu sistem koordinat? Ini adalah cara untuk menentukan posisi titik atau karakter dalam permainan yang terdiri dari poin menggunakan angka. Sistem koordinat memiliki 2 arah sumbu (kami akan menyatakannya sebagai X, Y) jika kami bekerja dengan grafis 2D. Jika kita mengatur objek 2D dengan Y lebih besar dan itu menjadi lebih tinggi dari sebelumnya, ini berarti bahwa sumbu Y ke atas. Jika kita memberi objek X yang lebih besar dan itu menjadi lebih ke kanan, ini berarti bahwa sumbu X diarahkan ke kanan. Ini adalah arah sumbu, dan bersama-sama mereka disebut sistem koordinat. Jika sudut 90 derajat terbentuk di persimpangan sumbu X dan Y, maka sistem koordinat seperti itu disebut persegi panjang (juga disebut sistem koordinat Cartesian) (lihat Gambar di atas).


Tapi itu adalah sistem koordinat di dunia 2D, dalam tiga dimensi, sumbu lain muncul - Z. Jika sumbu Y (mereka mengatakan ordinat) memungkinkan Anda untuk menggambar lebih tinggi / lebih rendah, sumbu X (mereka juga mengatakan absis) ke kiri / kanan, maka sumbu Z (masih say applicate) memungkinkan Anda untuk memperbesar / memperkecil objek. Dalam grafik tiga dimensi, sering (tetapi tidak selalu) sistem koordinat digunakan di mana sumbu Y diarahkan ke atas, sumbu X diarahkan ke kanan, tetapi Z dapat diarahkan ke satu arah atau di arah lain. Itulah sebabnya kami akan membagi sistem koordinat menjadi 2 jenis - sisi kiri dan sisi kanan (lihat Gambar di atas).

Seperti dapat dilihat dari gambar, sistem koordinat kidal (mereka juga mengatakan sistem koordinat kiri) dipanggil ketika sumbu Z diarahkan menjauh dari kita (semakin besar Z, semakin jauh objek), jika sumbu Z diarahkan ke kita, maka ini adalah sistem koordinat tangan kanan (mereka juga mengatakan sistem koordinat yang tepat). Mengapa mereka disebut demikian? Yang kiri, karena jika tangan kiri diarahkan dengan telapak tangan ke atas, dan dengan jari-jari Anda ke arah sumbu X, maka ibu jari akan menunjukkan arah Z, yaitu, itu akan diarahkan ke monitor, jika X diarahkan ke kanan. Lakukan hal yang sama dengan tangan kanan Anda, dan sumbu Z akan diarahkan menjauh dari monitor, dengan X ke kanan. Bingung dengan jari? Di Internet ada berbagai cara untuk meletakkan tangan dan jari Anda untuk mendapatkan arah sumbu yang diperlukan, tetapi ini bukan bagian yang wajib.

Untuk bekerja dengan grafis 3D, ada banyak perpustakaan untuk bahasa yang berbeda, di mana sistem koordinat yang berbeda digunakan. Sebagai contoh, pustaka Direct3D menggunakan sistem koordinat kidal, dan di OpenGL dan WebGL sistem koordinat tangan kanan, di VulkanAPI, sumbu Y turun (semakin kecil Y, semakin tinggi objek) dan Z dari kita, tetapi ini hanya konvensi, di perpustakaan kita dapat menentukan bahwa sistem koordinat, yang kami anggap lebih nyaman.

Sistem koordinat apa yang harus kita pilih? Ada yang cocok, kita hanya belajar dan arah sumbu sekarang tidak akan mempengaruhi asimilasi materi. Dalam contoh, kita akan menggunakan sistem koordinat tangan kanan dan semakin sedikit kita menentukan Z untuk poin, semakin jauh dari layar, sedangkan X, Y akan diarahkan ke kanan / atas.

Dot dan vektor


Sekarang pada dasarnya Anda tahu sistem koordinat apa dan arah sumbu apa. Selanjutnya, Anda perlu menguraikan apa titik dan vektor, karena kita akan membutuhkannya dalam artikel ini untuk latihan. Titik dalam ruang 3D adalah lokasi yang ditentukan melalui [X, Y, Z]. Sebagai contoh, kita ingin menempatkan karakter kita pada titik asal (mungkin di tengah jendela), maka posisinya akan [0, 0, 0], atau kita dapat mengatakan bahwa dia berada pada titik [0, 0, 0]. Sekarang, kami ingin menempatkan lawan di sebelah kiri pemain 20 unit (misalnya, piksel), yang berarti bahwa ia akan berada di titik [-20, 0, 0]. Kami akan terus bekerja dengan poin, jadi kami akan menganalisisnya lebih detail nanti. 

Apa itu vektor? Ini arahnya. Dalam ruang 3D, dijelaskan, seperti titik, dengan 3 nilai [X, Y, Z]. Misalnya, kita perlu memindahkan karakter ke atas 5 unit setiap detik, artinya kita akan mengubah Y, menambahkan 5 setiap detik, tetapi kita tidak akan menyentuh X dan Z, gerakan ini dapat ditulis sebagai vektor [0, 5, 0]. Jika karakter kita terus bergerak turun 2 unit dan ke kanan 1, maka vektor gerakannya akan terlihat seperti ini: [1, -2, 0]. Kami menulis -2 karena Y down berkurang.

Vektor tidak memiliki posisi, dan [X, Y, Z] menunjukkan arah. Vektor dapat ditambahkan ke suatu titik untuk mendapatkan titik baru yang digeser oleh vektor. Sebagai contoh, saya sudah menyebutkan di atas bahwa jika kita ingin memindahkan objek 3D (misalnya, karakter game) setiap 5 unit ke atas, maka vektor perpindahannya akan seperti ini: [0, 5, 0]. Tapi bagaimana cara menggunakannya untuk bergerak? 

Misalkan karakter berada pada titik [5, 7, 0], dan vektor perpindahannya adalah [0, 5, 0]. Jika kita menambahkan vektor ke titik, kita mendapatkan posisi pemain baru. Anda bisa menambahkan titik dengan vektor, atau vektor dengan vektor sesuai dengan aturan berikut.

Contoh menambahkan titik dan vektor :

[ 5, 7, 0 ] + [ 0, 5, 0 ] = [ 5 + 0, 7 + 5 , 0 + 0 ] = [5, 12, 0] - ini adalah posisi baru dari karakter kita. 

Seperti yang Anda lihat, karakter kami naik 5 unit, dari sini konsep baru muncul - panjang vektor. Setiap vektor memilikinya, kecuali vektor [0, 0, 0], yang disebut vektor nol, vektor tersebut juga tidak memiliki arah. Untuk vektor [0, 5, 0], panjangnya adalah 5, karena vektor seperti itu menggeser titik 5 unit ke atas. Vektor [0, 0, 10] memiliki panjang 10 karena itu dapat menggeser titik sebesar 10 sepanjang sumbu Z. Tetapi vektor [12, 3, -4] tidak memberi tahu Anda berapa panjangnya, jadi kami akan menggunakan rumus untuk menghitung panjang vektor. Muncul pertanyaan, mengapa kita membutuhkan panjang vektor? Satu aplikasi adalah untuk mengetahui seberapa jauh karakter akan bergerak, atau untuk membandingkan kecepatan karakter yang memiliki vektor perpindahan yang lebih lama, itu lebih cepat. Panjang juga digunakan untuk beberapa operasi pada vektor.Panjang vektor dapat dihitung menggunakan rumus berikut dari bagian pertama (hanya Z yang ditambahkan):

Length=X2+Y2+Z2


Mari kita hitung panjang vektor menggunakan rumus di atas [6, 3, -8];

Length=6βˆ—6+3βˆ—3+βˆ’8βˆ—βˆ’8=36+9+64=109β‰ˆ10.44


Panjang vektor [6, 3, -8] adalah sekitar 10,44.

Kita sudah tahu titik apa, vektor, bagaimana menjumlahkan titik dan vektor (atau 2 vektor), dan bagaimana menghitung panjang vektor. Mari kita tambahkan kelas vektor dan mengimplementasikan perhitungan penjumlahan dan panjang di dalamnya. Saya juga ingin memperhatikan fakta bahwa kita tidak akan membuat kelas untuk suatu titik, jika kita membutuhkan suatu titik, maka kita akan menggunakan kelas vektor, karena titik dan vektor menyimpan X, Y, Z, hanya untuk titik posisi ini, dan untuk vektor arah.

Tambahkan kelas vektor ke proyek dari artikel sebelumnya, Anda dapat menambahkannya di bawah kelas Drawer. Saya menelepon Vector my class dan menambahkan 3 properti X, Y, Z ke dalamnya:

class Vector {
  x = 0;
  y = 0;
  z = 0;

  constructor(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
}

Perhatikan bahwa bidang x, y, z tanpa fungsi "accessors", sehingga kita dapat langsung mengakses data dalam objek, ini dilakukan untuk akses yang lebih cepat. Nantinya, kami akan lebih mengoptimalkan kode ini, tetapi untuk saat ini, biarkan saja sehingga dapat meningkatkan keterbacaan.

Sekarang kita menerapkan penjumlahan vektor. Fungsi ini akan mengambil 2 vektor yang dapat diringkas, jadi saya berpikir untuk membuatnya statis. Tubuh fungsi akan bekerja sesuai dengan rumus di atas. Hasil penjumlahan kami adalah vektor baru, yang dengannya kami akan kembali:

static add(v1, v2) {
    return new Vector(
        v1.x + v2.x,
        v1.y + v2.y,
        v1.z + v2.z,
    );
}

Tetap menerapkan fungsi menghitung panjang vektor. Sekali lagi, kami menerapkan semuanya sesuai dengan rumus yang lebih tinggi:

getLength() {
    return Math.sqrt(
        this.x * this.x + this.y * this.y + this.z * this.z
    );
}

Sekarang mari kita lihat operasi lain pada vektor, yang akan dibutuhkan nanti dalam artikel ini dan banyak artikel selanjutnya - β€œnormalisasi vektor”. Misalkan kita memiliki karakter dalam game yang kita gerakkan dengan tombol panah. Jika kita menekan ke atas, maka ia bergerak ke vektor [0, 1, 0], jika turun, maka [0, -1, 0], ke kiri [-1, 0, 0] dan ke kanan [1, 0, 0]. Dapat dilihat dengan jelas di sini bahwa panjang masing-masing vektor adalah 1, yaitu, kecepatan karakter adalah 1. Dan mari kita tambahkan gerakan diagonal, jika pemain menjepit panah ke atas dan ke kanan, apa yang akan menjadi vektor perpindahan? Pilihan yang paling jelas adalah vektor [1, 1, 0]. Tetapi jika kita menghitung panjangnya, kita akan melihat bahwa kira-kira sama dengan 1,414. Ternyata karakter kita akan berjalan lebih cepat secara diagonal? Opsi ini tidak cocok, tetapi agar karakter kita bergerak secara diagonal dengan kecepatan 1, vektornya harus:[0,707, 0,707, 0]. Di mana saya mendapatkan vektor seperti itu? Saya mengambil vektor [1, 1, 0] dan menormalkannya, setelah itu saya mendapat [0,707, 0,707, 0]. Artinya, normalisasi adalah pengurangan vektor ke panjang 1 (satuan panjang) tanpa mengubah arahnya. Perhatikan bahwa vektor [0,707, 0,707, 0] dan [1, 1, 0] menunjuk ke arah yang sama, yaitu, karakter dalam kedua kasus akan bergerak ketat ke kanan, tetapi vektor [0,707, 0,707, 0] dinormalisasi dan kecepatan karakter Sekarang akan sama dengan 1, yang menghilangkan bug dengan gerakan diagonal dipercepat. Itu selalu disarankan untuk menormalkan vektor sebelum perhitungan untuk menghindari berbagai jenis kesalahan. Mari kita lihat bagaimana menormalkan suatu vektor. Kita perlu membagi masing-masing komponennya (X, Y, Z) berdasarkan panjangnya. Fungsi menemukan panjang sudah ada di sana, setengah dari pekerjaan sudah selesai,sekarang kita menulis fungsi normalisasi vektor (di dalam kelas vektor):

normalize() {
    const length = this.getLength();
    
    this.x /= length;
    this.y /= length;
    this.z /= length;
    
    return this;
}

Metode normalisasi menormalkan vektor dan mengembalikannya (ini), ini diperlukan agar di masa depan dimungkinkan untuk menggunakan normalisasi dalam ekspresi.

Sekarang kita tahu apa itu normalisasi vektor, dan kita tahu bahwa itu lebih baik untuk melakukannya sebelum menggunakan vektor, muncul pertanyaan. Jika normalisasi vektor adalah pengurangan ke satuan panjang, yaitu, kecepatan gerakan suatu objek (karakter) akan sama dengan 1, lalu bagaimana cara mempercepat karakter? Misalnya, ketika memindahkan karakter secara diagonal ke atas / kanan pada kecepatan 1, vektornya akan menjadi [0,707, 0,707, 0], dan vektor apa yang akan terjadi jika kita ingin memindahkan karakter 6 kali lebih cepat? Untuk melakukan ini, ada operasi yang disebut "mengalikan vektor dengan skalar." Skalar adalah angka yang biasa digunakan vektor untuk dikalikan. Jika skalar sama dengan 6, maka vektor akan menjadi 6 kali lebih lama, dan karakter kita masing-masing 6 kali lebih cepat. Bagaimana cara melakukan skalar multiplikasi? Untuk ini, perlu untuk mengalikan setiap komponen vektor dengan skalar. Sebagai contoh, kami memecahkan masalah di atas,ketika karakter bergerak ke vektor [0,707, 0,707, 0] (kecepatan 1) perlu dipercepat 6 kali, yaitu, lipat gandakan vektor dengan skalar 6. Rumus untuk mengalikan vektor "V" dengan skalar "s" adalah sebagai berikut:

Vβˆ—s=[Vxβˆ—sVyβˆ—sVzβˆ—s]


Dalam kasus kami, itu akan menjadi:
[0.707βˆ—60.707βˆ—60βˆ—6]=[4.2424.2420]- vektor perpindahan baru yang panjangnya 6.

Penting untuk mengetahui bahwa skalar positif menskala vektor tanpa mengubah arahnya, jika skalar negatif, ia juga menskala vektor (meningkatkan panjangnya) tetapi juga mengubah arah vektor ke sebaliknya.

Mari kita terapkan fungsimultiplyByScalarmengalikan vektor dengan skalar di kelas vektor kita:

multiplyByScalar(s) {
    this.x *= s;
    this.y *= s;
    this.z *= s;
    
    return this;
}

Matriks


Kami menemukan sedikit dengan vektor dan beberapa operasi pada mereka yang akan diperlukan dalam artikel ini. Selanjutnya, Anda perlu berurusan dengan matriks.

Kita dapat mengatakan bahwa matriks adalah array dua dimensi yang paling umum. Hanya saja dalam pemrograman mereka menggunakan istilah "array dua dimensi", dan dalam matematika mereka menggunakan "matriks". Mengapa matriks diperlukan dalam pemrograman 3D? Kami akan menganalisis ini segera setelah kami belajar bekerja dengan mereka sedikit. 

Kami hanya akan menggunakan matriks numerik (array angka). Setiap matriks memiliki ukurannya sendiri (seperti array 2 dimensi). Berikut adalah beberapa contoh matriks:

M=[123456]


2 oleh 3 matriks

M=[2433445βˆ’22]


3 3

M=[2305]


4 1

M=[5072βˆ’17928351]


4 3

Dari semua operasi pada matriks, kami sekarang hanya mempertimbangkan perkalian (sisanya nanti). Ternyata perkalian matriks bukan operasi termudah, dapat dengan mudah membingungkan jika Anda tidak mengikuti urutan perkalian. Tetapi jangan khawatir, Anda akan berhasil, karena di sini kita hanya akan melipatgandakan dan merangkum. Untuk memulainya, kita perlu mengingat beberapa fitur multiplikasi yang kita butuhkan:

  • Jika kita mencoba mengalikan angka A dengan angka B, maka ini sama dengan B * A. Jika kita mengatur ulang operan dan hasilnya tidak berubah di bawah tindakan apa pun, maka mereka mengatakan bahwa operasi itu komutatif. Contoh: a + b = b + a operasinya komutatif, a - b β‰  b - a operasinya non-komutatif, a * b = b * a operasi bilangan gandakan adalah komutatif. Jadi, operasi perkalian matriks adalah non-komutatif, berbeda dengan perkalian angka. Yaitu, mengalikan matriks M dengan matriks N tidak akan sama dengan perkalian matriks N dengan M.
  • Perkalian matriks dimungkinkan jika jumlah kolom dari matriks pertama (yang ada di sebelah kiri) sama dengan jumlah baris dalam matriks kedua (yang ada di sebelah kanan). 

Sekarang kita akan melihat fitur kedua dari perkalian matriks (ketika perkalian dimungkinkan). Berikut adalah beberapa contoh yang menunjukkan kapan multiplikasi dimungkinkan dan kapan tidak:

M1=[12]


M2=[123456]


M1 M2 , .. 2 , 2 .

M1=[325442βˆ’745794]


M2=[10456βˆ’9]


1 2 , .. 3 , 3 .

M1=[5403]


M2=[730363]


1 2 , .. 2 , 3 .

Saya pikir contoh-contoh ini sedikit memperjelas gambaran ketika multiplikasi dimungkinkan. Hasil perkalian matriks akan selalu berupa matriks, jumlah baris yang akan sama dengan jumlah baris matriks 1, dan jumlah kolom sama dengan jumlah kolom ke-2. Sebagai contoh, jika kita mengalikan matriks 2 dengan 6 dan 6 dengan 8, kita mendapatkan matriks ukuran 2 dengan 8. Sekarang kita langsung menuju ke perkalian itu sendiri.

Untuk perkalian, penting untuk diingat bahwa kolom dan baris dalam matriks diberi nomor mulai dari 1, dan dalam array dari 0. Indeks pertama dalam elemen matriks menunjukkan nomor baris, dan yang kedua adalah nomor kolom. Artinya, jika elemen matriks (elemen array) ditulis sebagai: m28, ini berarti bahwa kita beralih ke baris kedua dan kolom kedelapan. Tetapi karena kita akan bekerja dengan array dalam kode, semua pengindeksan baris dan kolom akan mulai dari 0.

Mari kita coba gandakan 2 matriks A dan B dengan ukuran dan elemen tertentu:

A=[123456]


B=[78910]


Dapat dilihat bahwa matriks A memiliki ukuran 3 oleh 2, dan matriks B memiliki ukuran 2 oleh 2, perkalian dimungkinkan:

Aβˆ—B=[1βˆ—7+2βˆ—91βˆ—8+2βˆ—103βˆ—7+4βˆ—93βˆ—8+4βˆ—105βˆ—7+6βˆ—95βˆ—8+6βˆ—10]=[2528576489100]


Seperti yang Anda lihat, kami memiliki matriks 3 x 2, perkalian awalnya membingungkan, tetapi jika ada tujuan untuk belajar bagaimana mengalikan "tanpa stres", beberapa contoh perlu dipecahkan. Berikut adalah contoh lain dari mengalikan matriks A dan B:

A=[32]


B=[2βˆ’3014βˆ’2]


Aβˆ—B=[3βˆ—2+2βˆ—13βˆ—βˆ’3+2βˆ—43βˆ—0+2βˆ—βˆ’2]=[8βˆ’1βˆ’4]


Jika multiplikasi tidak sepenuhnya jelas, maka tidak apa-apa, karena kita tidak harus mengalikan dengan daun. Kami akan menulis sekali fungsi perkalian matriks dan kami akan menggunakannya. Secara umum, semua fungsi ini sudah ditulis, tetapi kami melakukan semuanya sendiri.

Sekarang beberapa istilah lagi yang akan digunakan di masa depan:

  • , , :

[2364]


2 2

[56790βˆ’2451]


3 3

[5673902145131798]


4 4

  • , . ( ): 

[9339]


[933393339]


[9333393333933339]



  • , 1, 0. :

[1001]


[100010001]


[1000010000100001]



Penting juga untuk mengingat properti seperti itu sehingga jika kita mengalikan matriks M dengan matriks satuan yang sesuai ukurannya, misalnya, sebut saja I, kita mendapatkan matriks M asli, misalnya: M * I = M atau I * M = M. Yaitu, mengalikan matriks dengan matriks identitas tidak mempengaruhi hasilnya. Kami akan kembali ke matriks identitas nanti. Dalam pemrograman 3D, kita akan sering menggunakan matriks 4 x 4 persegi.

Sekarang mari kita lihat mengapa kita membutuhkan matriks dan mengapa melipatgandakannya? Dalam pemrograman 3D, ada banyak berbeda 4 oleh 4 matriks yang, jika dikalikan dengan vektor atau titik, akan melakukan tindakan yang kita butuhkan. Sebagai contoh, kita perlu memutar karakter dalam ruang tiga dimensi di sekitar sumbu X, bagaimana melakukan ini? Lipat gandakan vektor dengan matriks khusus, yang bertanggung jawab untuk rotasi di sekitar sumbu X. Jika Anda perlu memindahkan dan memutar titik di sekitar titik asal, maka Anda perlu mengalikan titik ini dengan matriks khusus. Matriks memiliki sifat yang sangat baik - menggabungkan transformasi (kami akan pertimbangkan dalam artikel ini). Misalkan kita membutuhkan karakter yang terdiri dari 100 poin (simpul, tetapi ini juga akan sedikit lebih rendah) dalam aplikasi, naik 5 kali, lalu putar 90 derajat X, lalu naikkan 30 unit.Seperti yang telah disebutkan, untuk tindakan yang berbeda sudah ada matriks khusus yang akan kami pertimbangkan. Untuk menyelesaikan tugas di atas, kita, misalnya, loop melalui semua 100 poin dan setiap pertama kita kalikan dengan matriks 1 untuk meningkatkan karakter, kemudian kita kalikan dengan matriks ke-2 untuk memutar 90 derajat di X, kemudian kita kalikan dengan 3 th untuk memindahkan 30 unit ke atas. Secara total, untuk setiap titik kita memiliki 3 perkalian matriks, dan 100 poin, yang berarti akan ada 300 perkalian. Tetapi jika kita mengambil dan mengalikan matriks di antara kita sendiri untuk meningkat 5 kali, putar 90 derajat di sepanjang X dan bergerak dengan 30 unit. Facebook, kami mendapatkan matriks yang berisi semua tindakan ini. Mengalikan titik dengan matriks seperti itu, titik tersebut akan berada di tempat yang diperlukan. Sekarang mari kita hitung berapa banyak tindakan yang dilakukan: 2 perkalian untuk 3 matriks, dan 100 perkalian untuk 100 poin,total 102 perkalian jelas lebih baik dari 300 perkalian sebelum itu. Momen ketika kita mengalikan 3 matriks untuk menggabungkan tindakan berbeda menjadi satu matriks - disebut kombinasi transformasi dan kita pasti akan melakukannya dengan sebuah contoh.

Cara mengalikan matriks dengan matriks, kami memeriksa, tetapi paragraf yang dibaca di atas berbicara tentang perkalian matriks dengan titik atau vektor. Untuk mengalikan titik atau vektor, cukup dengan merepresentasikannya sebagai matriks.

Sebagai contoh, kami memiliki vektor [10, 2, 5] dan ada sebuah matriks: 

[121221043]


Dapat dilihat bahwa vektor dapat diwakili oleh matriks 1 oleh 3 atau oleh matriks 3 oleh 1. Oleh karena itu, kita dapat mengalikan vektor dengan matriks dengan 2 cara:

[1025]βˆ—[121221043]


Di sini kami menyajikan vektor sebagai matriks 1 oleh 3 (mereka juga mengatakan vektor baris). Penggandaan seperti itu dimungkinkan, karena matriks pertama (vektor baris) memiliki 3 kolom, dan matriks kedua memiliki 3 baris.

[121221043]βˆ—[1025]


Di sini kami menyajikan vektor sebagai matriks 3 oleh 1 (mereka juga mengatakan vektor kolom). Penggandaan seperti itu dimungkinkan, karena dalam matriks pertama ada 3 kolom, dan di baris kedua (vektor kolom) 3.

Seperti yang Anda lihat, kita dapat merepresentasikan vektor sebagai vektor baris dan mengalikannya dengan matriks, atau mewakili vektor sebagai vektor kolom dan melipatgandakan matriks dengan vektor tersebut. Mari kita periksa apakah hasil dari perkalian akan sama dalam kedua kasus:

Kalikan vektor baris dengan matriks:

[1025]βˆ—[121221043]=


=[10βˆ—1+2βˆ—2+5βˆ—010βˆ—2+2βˆ—2+5βˆ—410βˆ—1+2βˆ—1+5βˆ—3]=[144427]


Sekarang, gandakan matriks dengan vektor kolom:

[121221043]βˆ—[1025]=[1βˆ—10+2βˆ—5+1βˆ—52βˆ—10+2βˆ—2+1βˆ—50βˆ—10+4βˆ—2+3βˆ—5]=[192923]


Kita melihat bahwa mengalikan vektor baris dengan matriks dan matriks dengan vektor kolom, kita mendapatkan hasil yang sama sekali berbeda (kita mengingat komutatifitas). Oleh karena itu, dalam pemrograman 3D, ada matriks yang dirancang untuk dikalikan hanya dengan vektor baris, atau hanya dengan vektor kolom. Jika kita mengalikan matriks yang dimaksudkan untuk vektor baris dengan vektor kolom, kita mendapatkan hasil yang tidak akan memberi kita apa pun. Gunakan representasi vektor / titik yang sesuai untuk Anda (baris atau kolom), hanya di masa mendatang, gunakan matriks yang sesuai untuk representasi vektor / titik Anda. Direct3D, misalnya, menggunakan representasi string dari vektor, dan semua matriks dalam Direct3D dirancang untuk mengalikan vektor baris dengan matriks. OpenGL menggunakan representasi vektor (atau titik) sebagai kolom,dan semua matriks dirancang untuk melipatgandakan matriks dengan vektor kolom. Dalam artikel kita akan menggunakan vektor kolom dan kita akan mengalikan matriks dengan vektor kolom.

Untuk meringkas apa yang kita baca tentang matriks.

  • Untuk melakukan tindakan pada vektor (atau titik), ada matriks khusus, beberapa di antaranya akan kita lihat di artikel ini.
  • Untuk menggabungkan transformasi (perpindahan, rotasi, dll.), Kita dapat melipatgandakan matriks dari setiap transformasi dengan satu sama lain dan mendapatkan matriks yang berisi semua transformasi secara bersamaan.
  • Dalam pemrograman 3D, kita akan terus menggunakan 4 dengan 4 matriks persegi.
  • Kita dapat mengalikan matriks dengan vektor (atau titik) dengan menyatakannya sebagai kolom atau baris. Tetapi untuk vektor kolom dan vektor baris, Anda perlu menggunakan matriks yang berbeda.

Setelah sedikit analisis matriks, mari kita tambahkan kelas matriks 4 x 4 dan menerapkan metode untuk mengalikan matriks dengan matriks, dan vektor dengan matriks. Kami akan menggunakan ukuran matriks 4 oleh 4, karena semua matriks standar yang digunakan untuk berbagai tindakan (gerakan, rotasi, skala, ...) berukuran sedemikian, kita tidak perlu matriks dengan ukuran yang berbeda.  

Mari kita tambahkan kelas Matrix ke proyek. Masih kadang-kadang kelas untuk bekerja dengan 4 oleh 4 matriks disebut Matrix4, dan ini 4 dalam judul memberitahu kita tentang ukuran matriks (mereka juga mengatakan matriks urutan ke-4). Semua data matriks akan disimpan dalam array dua dimensi 4 x 4.

Kami beralih ke implementasi operasi multiplikasi. Saya tidak merekomendasikan menggunakan loop untuk ini. Untuk meningkatkan kinerja, kita semua harus melipatgandakan baris demi baris - ini akan terjadi karena semua perkalian akan terjadi dengan matriks ukuran tetap. Saya akan menggunakan siklus untuk operasi perkalian, hanya untuk menyimpan jumlah kode, Anda dapat menulis semua perkalian tanpa siklus sama sekali. Kode multiplikasi saya terlihat seperti ini:

class Matrix {
  static multiply(a, b) {
    const m = [
      [0, 0, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
    ];

    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        m[i][j] = a[i][0] * b[0][j] +
          a[i][1] * b[1][j] +
          a[i][2] * b[2][j] +
          a[i][3] * b[3][j];
      }
    }

    return m;
  }
}

Seperti yang Anda lihat, metode mengambil matriks a dan b, mengalikannya dan mengembalikan hasilnya dalam array yang sama 4 oleh 4. Pada awal metode saya membuat matriks m diisi dengan nol, tetapi ini tidak perlu, jadi saya ingin menunjukkan dimensi apa hasilnya nanti, Anda Anda dapat membuat array 4 kali 4 tanpa data apa pun.

Sekarang Anda perlu mengimplementasikan perkalian matriks dengan vektor kolom, seperti yang dibahas di atas. Tetapi jika Anda mewakili vektor sebagai kolom, Anda mendapatkan matriks dari bentuk:[xyz]
dimana kita perlu mengalikan dengan 4 oleh 4 matriks untuk melakukan berbagai tindakan. Tetapi dalam contoh ini jelas terlihat bahwa perkalian seperti itu tidak dapat dilakukan, karena vektor kolom memiliki 3 baris, dan matriks memiliki 4 kolom. Lalu apa yang harus dilakukan? Beberapa elemen keempat diperlukan, maka vektor akan memiliki 4 baris, yang akan sama dengan jumlah kolom dalam matriks. Mari kita tambahkan parameter ke-4 untuk vektor dan menyebutnya W, sekarang kita memiliki semua vektor 3D dalam bentuk [X, Y, Z, W] dan vektor-vektor ini sudah dapat dikalikan dengan 4 oleh 4. Bahkan, komponen W tujuan yang lebih dalam, tetapi kita akan mengenalnya di bagian selanjutnya (bukan tanpa alasan kita memiliki matriks 4 x 4, bukan 3 x 3 matriks). Tambahkan ke kelas Vector, yang kami buat di atas komponen w. Sekarang awal kelas Vector terlihat seperti ini:

class Vector {
    x = 0;
    y = 0;
    z = 0;
    w = 1;

    constructor(x, y, z, w = 1) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.w = w;
    }

Saya menginisialisasi W ke satu, tetapi mengapa 1? Jika kita melihat bagaimana komponen dari matriks dan vektor dikalikan (contoh kode di bawah), Anda dapat melihat bahwa jika Anda menetapkan W ke 0 atau nilai lain selain 1, maka ketika mengalikan W ini akan memengaruhi hasil, tetapi kami tidak kita tahu bagaimana menggunakannya, dan jika kita membuatnya 1, maka itu akan berada di vektor, tetapi hasilnya tidak akan berubah dengan cara apa pun. 

Sekarang kembali ke matriks dan implementasikan di kelas Matrix (Anda juga bisa di kelas Vector, tidak ada perbedaan) matriks dikalikan dengan vektor, yang sudah dimungkinkan, terima kasih kepada W:

static multiplyVector(m, v) {
  return new Vector(
    m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z + m[0][3] * v.w,
    m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z + m[1][3] * v.w,
    m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z + m[2][3] * v.w,
    m[3][0] * v.x + m[3][1] * v.y + m[3][2] * v.z + m[3][3] * v.w,
  )
}

Harap dicatat bahwa kami menyajikan matriks sebagai array 4 oleh 4, dan vektor sebagai objek dengan properti x, y, z, w, di masa depan kami akan mengubah vektor dan juga akan diwakili oleh array 1 oleh 4, karena itu akan mempercepat multiplikasi. Tapi sekarang, untuk melihat lebih baik bagaimana multiplikasi terjadi dan meningkatkan pemahaman kode, kita tidak akan mengubah vektor.

Kami menulis kode untuk perkalian matriks di antara kami dan perkalian matriks-vektor, tetapi masih belum jelas bagaimana ini akan membantu kami dalam grafik tiga dimensi.

Saya juga ingin mengingatkan Anda bahwa saya menyebut vektor sebagai titik (posisi dalam ruang) dan arah, karena kedua objek berisi struktur data yang sama x, y, z dan w yang baru diperkenalkan. 

Mari kita lihat beberapa matriks yang melakukan operasi dasar pada vektor. Matriks pertama adalah matriks terjemahan. Mengalikan matriks perpindahan dengan vektor (lokasi), itu akan bergeser dengan jumlah unit yang ditentukan dalam ruang. Dan di sini adalah matriks perpindahan:

[100dx010dy001dz0001]


Di mana dx, dy, dz berarti perpindahan sepanjang sumbu x, y, z, masing-masing, matriks ini dirancang untuk dikalikan dengan vektor kolom. Matriks seperti itu dapat ditemukan di Internet atau literatur apa pun tentang pemrograman 3D, kita tidak perlu membuatnya sendiri, ambil sekarang, sebagai formula yang Anda gunakan dari sekolah, yang hanya perlu Anda ketahui atau pahami mengapa harus menggunakannya. Mari kita periksa apakah, memang, mengalikan matriks seperti itu dengan vektor, akan terjadi offset. Ambil sebagai vektor, kita akan memindahkan vektor [10, 10, 10, 1] (kita selalu meninggalkan parameter 4 W selalu 1), misalkan ini adalah posisi karakter kita dalam permainan dan kita ingin menggesernya 10 unit ke atas, 5 unit ke kanan, dan 1 unit jauhnya dari layar. Maka vektor perpindahan akan menjadi seperti ini [10, 5, -1] (-1 karena kita memiliki sistem koordinat tangan kanan dan selanjutnya Z,semakin kecil). Jika kita menghitung hasilnya tanpa matriks, dengan penjumlahan vektor yang biasa. Itu akan menghasilkan hasil berikut: [10 + 10, 10 + 5, 10 + -1, 1] = [20, 15, 9, 1] - ini adalah koordinat baru dari karakter kita. Mengalikan matriks di atas dengan koordinat awal [10, 10, 10, 1], kita harus mendapatkan hasil yang sama, mari kita periksa ini dalam kode, tulis perkalian setelah kelas Drawer, Vector and Matrix:
const translationMatrix = [
  [1, 0, 0, 10],
  [0, 1, 0, 5],
  [0, 0, 1, -1],
  [0, 0, 0, 1],
]
        
const characterPosition = new Vector(10, 10, 10)
        
const newCharacterPosition = Matrix.multiplyVector(
  translationMatrix, characterPosition
)
console.log(newCharacterPosition)

Dalam contoh ini, kami mengganti offset karakter yang diinginkan (translationMatrix) ke dalam matriks perpindahan, menginisialisasi posisi awalnya (characterPosition) dan kemudian mengalikannya dengan matriks, dan hasilnya adalah keluaran melalui console.log (ini adalah keluaran debug di JS). Jika Anda menggunakan non-JS, maka hasilkan X, Y, Z sendiri menggunakan alat bahasa Anda. Hasil yang kami dapatkan di konsol: [20, 15, 9, 1], semuanya setuju dengan hasil yang kami hitung di atas. Anda mungkin memiliki pertanyaan, mengapa mendapatkan hasil yang sama dengan mengalikan vektor dengan matriks khusus, jika kami mendapatkannya lebih mudah dengan menjumlahkan vektor dengan komponen offset. Jawabannya bukan yang paling sederhana dan kita akan membahasnya secara lebih rinci, tetapi sekarang dapat dicatat bahwa, seperti yang dibahas sebelumnya, kita dapat menggabungkan matriks dengan transformasi yang berbeda di antara mereka sendiri,dengan demikian mengurangi begitu banyak perhitungan. Dalam contoh di atas, kami membuat matriks translationMatrix sebagai array secara manual dan menggantikan offset yang diperlukan di sana, tetapi karena kita akan sering menggunakan ini dan matriks lainnya, mari kita memasukkannya ke dalam metode di kelas Matrix dan meneruskan offset itu dengan argumen:

static getTranslation(dx, dy, dz) {
  return [
    [1, 0, 0, dx],
    [0, 1, 0, dy],
    [0, 0, 1, dz],
    [0, 0, 0, 1],
  ]
}

Lihatlah lebih dekat pada matriks perpindahan, Anda akan melihat bahwa dx, dy, dz berada di kolom terakhir, dan jika kita melihat kode untuk mengalikan matriks dengan vektor, kita akan melihat bahwa kolom ini dikalikan dengan komponen W dari vektor. Dan jika itu, misalnya, 0, maka dx, dy, dz, kita akan mengalikan dengan 0 dan langkah itu tidak akan berhasil. Tetapi kita bisa melakukan W sama dengan 0 jika kita ingin menyimpan arah di kelas Vector, karena tidak mungkin untuk memindahkan arah, jadi kita akan melindungi diri kita sendiri, dan bahkan jika kita mengalikan arah tersebut dengan matriks perpindahan, ini tidak akan merusak vektor arah, karena semua pergerakan akan dikalikan dengan 0.

Total kita bisa menerapkan aturan seperti itu, kita buat lokasi seperti ini:

new Vector(x, y, z, 1) // 1    ,   

Dan kami akan membuat arah seperti ini:

new Vector(x, y, z, 0)

Jadi kita dapat membedakan antara lokasi dan arah, dan ketika kita mengalikan arah dengan matriks perpindahan, kita tidak sengaja mematahkan vektor arah.

Verteks dan Indeks


Sebelum kita melihat apa matriks lain, kita akan melihat sedikit bagaimana menerapkan pengetahuan kita yang ada untuk menampilkan sesuatu tiga dimensi di layar. Semua yang kami simpulkan sebelumnya adalah garis dan piksel. Tapi sekarang mari kita gunakan alat-alat ini untuk menurunkan, misalnya, sebuah kubus. Untuk melakukan ini, kita perlu mencari tahu apa model tiga dimensi terdiri dari. Komponen paling dasar dari setiap model 3D adalah titik (kita akan memanggil simpul di bawah) di mana kita dapat menggambarnya, ini adalah, sebenarnya, banyak vektor lokasi, yang, jika kita menghubungkannya dengan benar dengan garis, kita mendapatkan model 3D (model grid) ) pada layar, itu akan tanpa tekstur dan tanpa banyak properti lainnya, tetapi semuanya memiliki waktu. Lihatlah kubus yang ingin kita hasilkan dan mencoba memahami berapa banyak simpul yang dimilikinya:



Dalam gambar kita melihat bahwa kubus memiliki 8 simpul (untuk kenyamanan, saya beri nomor). Dan semua simpul saling berhubungan oleh garis (tepi kubus). Yaitu, untuk menggambarkan kubus dan menggambarnya dengan garis, kita membutuhkan 8 koordinat dari setiap titik, dan kita juga perlu menentukan dari titik mana ke garis mana untuk menggambar, untuk membuat kubus, karena jika kita menghubungkan simpul secara tidak benar, misalnya, gambarlah garis dari titik tersebut. 0 ke vertex 6, maka itu pasti tidak akan menjadi kubus, tetapi objek lain. Sekarang mari kita jelaskan koordinat masing-masing dari 8 simpul. Dalam grafik modern, model 3D dapat terdiri dari puluhan ribu simpul, dan tentu saja tidak ada yang secara manual menentukannya. Model digambar dalam editor 3D, dan ketika model 3D diekspor, ia sudah memiliki semua simpul dalam kodenya, kita hanya perlu memuat dan menggambarnya, tetapi untuk sekarang kita sedang belajar dan tidak dapat membaca format model 3D, jadi kita akan menggambarkan kubus secara manual.dia sangat sederhana.

Bayangkan bahwa kubus di atas berada di pusat koordinat, tengahnya berada di titik 0, 0, 0 dan harus ditampilkan di sekitar pusat ini:


Mari kita mulai dari titik 0, dan biarkan kubus kami menjadi sangat kecil agar tidak menulis nilai besar sekarang, dimensi kubus saya akan menjadi 2 lebar, 2 tinggi dan 2 dalam, yaitu. 2 oleh 2 oleh 2. Gambar menunjukkan bahwa titik 0 sedikit ke kiri dari pusat 0, 0, 0, jadi saya akan mengatur X = -1, karena sebelah kiri, X yang lebih kecil, juga simpul 0 sedikit lebih tinggi dari pusat 0, 0, 0, dan dalam sistem koordinat kami semakin tinggi lokasi, semakin besar Y, saya akan mengatur simpul Y = 1, juga Z untuk simpul 0, sedikit lebih dekat ke layar sehubungan dengan titik 0, 0, 0, sehingga akan sama dengan Z = 1, karena dalam sistem koordinat tangan kanan, Z meningkat dengan pendekatan objek. Hasilnya, kami mendapatkan koordinat -1, 1, 1 untuk titik nol, mari kita lakukan hal yang sama untuk 7 simpul yang tersisa dan simpan dalam array sehingga Anda dapat bekerja dengannya dalam satu lingkaran,Saya mendapatkan hasil ini (sebuah array dapat dibuat di bawah kelas Drawer, Vector, Marix):

// Cube vertices
const vertices = [
  new Vector(-1, 1, 1), // 0 
  new Vector(-1, 1, -1), // 1 
  new Vector(1, 1, -1), // 2 
  new Vector(1, 1, 1), // 3 
  new Vector(-1, -1, 1), // 4 
  new Vector(-1, -1, -1), // 5 
  new Vector(1, -1, -1), // 6 
  new Vector(1, -1, 1), // 7 
];

Saya menempatkan setiap simpul dalam instance dari kelas Vector, ini bukan pilihan terbaik untuk kinerja (lebih baik dalam array), tetapi sekarang tujuan kami adalah untuk mencari tahu bagaimana semuanya bekerja.

Sekarang mari kita ambil koordinat dari simpul kubus sebagai piksel yang akan kita gambar di layar, dalam hal ini kita melihat bahwa ukuran kubus adalah 2 kali 2 kali 2 piksel. Kami membuat kubus kecil sehingga lihat karya dari scaling matrix, yang dengannya kita akan meningkatkannya. Di masa depan, praktik yang sangat baik untuk membuat model menjadi kecil, bahkan lebih kecil dari model kami, untuk meningkatkannya ke ukuran yang diinginkan dengan skalar yang tidak jauh berbeda.

Hanya saja menggambar titik kubus dengan piksel tidak terlalu jelas, karena semua yang akan kita lihat adalah 8 piksel, satu untuk setiap titik, jauh lebih baik untuk menggambar kubus dengan garis menggunakan fungsi drawLine dari artikel sebelumnya. Tetapi untuk ini kita perlu memahami dari simpul mana garis yang kita lewati. Lihatlah gambar kubus dengan indeks lagi dan kita akan melihat bahwa itu terdiri dari 12 garis (atau tepi). Juga sangat mudah untuk melihat bahwa kita mengetahui koordinat awal dan akhir setiap baris. Misalnya, salah satu garis (dekat atas) harus ditarik dari titik 0 ke titik 3, atau dari koordinat [-1, 1, 1] ke koordinat [1, 1, 1]. Kita harus menulis informasi tentang setiap baris dalam kode secara manual melihat gambar kubus, tetapi bagaimana melakukannya dengan benar? Jika kita memiliki 12 baris dan setiap baris memiliki awal dan akhir, mis. 2 poin, lalu,menggambar kubus, kita perlu 24 poin? Ini adalah jawaban yang benar, tetapi mari kita lihat gambar kubus lagi dan perhatikan fakta bahwa setiap baris kubus memiliki simpul yang sama, misalnya, pada titik 0 3 garis terhubung, dan begitu pula dengan setiap titik. Kita dapat menghemat memori dan tidak menuliskan koordinat awal dan akhir setiap baris, cukup buat array dan tentukan indeks verteks dari array simpul tempat baris ini dimulai dan berakhir. Mari kita buat array seperti itu dan jelaskan hanya dengan indeks titik, 2 indeks per baris (awal baris dan akhir). Dan sedikit lebih jauh, ketika kita menggambar garis-garis ini, kita dapat dengan mudah mendapatkan koordinat mereka dari array simpul. Larik baris saya (saya menyebutnya tepi, karena ini adalah tepi kubus) Saya membuat array simpul di bawah ini dan terlihat seperti ini:tapi mari kita lihat gambar kubus lagi dan perhatikan fakta bahwa setiap baris kubus memiliki simpul yang sama, misalnya, pada titik 0 3 garis terhubung, dan demikian pula dengan setiap titik. Kita dapat menghemat memori dan tidak menuliskan koordinat awal dan akhir setiap baris, cukup buat array dan tentukan indeks verteks dari array simpul tempat baris ini dimulai dan berakhir. Mari kita buat array seperti itu dan jelaskan hanya dengan indeks titik, 2 indeks per baris (awal baris dan akhir). Dan sedikit lebih jauh, ketika kita menggambar garis-garis ini, kita dapat dengan mudah mendapatkan koordinat mereka dari array simpul. Larik baris saya (saya menyebutnya tepi, karena ini adalah tepi kubus) Saya membuat array simpul di bawah ini dan terlihat seperti ini:tetapi mari kita lihat gambar kubus lagi dan perhatikan fakta bahwa setiap baris kubus memiliki simpul yang sama, misalnya, pada titik 0 3 garis terhubung, dan demikian pula dengan setiap titik. Kita dapat menghemat memori dan tidak menuliskan koordinat awal dan akhir setiap baris, cukup buat array dan tentukan indeks verteks dari array simpul tempat baris ini dimulai dan berakhir. Mari kita buat array seperti itu dan jelaskan hanya dengan indeks titik, 2 indeks per baris (awal baris dan akhir). Dan sedikit lebih jauh, ketika kita menggambar garis-garis ini, kita dapat dengan mudah mendapatkan koordinat mereka dari array simpul. Larik baris saya (saya menyebutnya tepi, karena ini adalah tepi kubus) Saya membuat array simpul di bawah ini dan terlihat seperti ini:dan begitu juga dengan masing-masing simpul. Kita dapat menghemat memori dan tidak menuliskan koordinat awal dan akhir setiap baris, cukup buat array dan tentukan indeks verteks dari array simpul tempat baris ini dimulai dan berakhir. Mari kita buat array seperti itu dan jelaskan hanya dengan indeks titik, 2 indeks per baris (awal baris dan akhir). Dan sedikit lebih jauh, ketika kita menggambar garis-garis ini, kita dapat dengan mudah mendapatkan koordinat mereka dari array simpul. Larik baris saya (saya menyebutnya tepi, karena ini adalah tepi kubus) Saya membuat array simpul di bawah ini dan terlihat seperti ini:dan begitu juga dengan masing-masing simpul. Kita dapat menghemat memori dan tidak menuliskan koordinat awal dan akhir setiap baris, cukup buat array dan tentukan indeks verteks dari array simpul tempat baris ini dimulai dan berakhir. Mari kita buat array seperti itu dan jelaskan hanya dengan indeks titik, 2 indeks per baris (awal baris dan akhir). Dan sedikit lebih jauh, ketika kita menggambar garis-garis ini, kita dapat dengan mudah mendapatkan koordinat mereka dari array simpul. Larik baris saya (saya menyebutnya tepi, karena ini adalah tepi kubus) Saya membuat array simpul di bawah ini dan terlihat seperti ini:2 indeks pada setiap baris (awal baris dan akhir). Dan sedikit lebih jauh, ketika kita menggambar garis-garis ini, kita dapat dengan mudah mendapatkan koordinat mereka dari array simpul. Larik baris saya (saya menyebutnya tepi, karena ini adalah tepi kubus) Saya membuat array simpul di bawah ini dan terlihat seperti ini:2 indeks pada setiap baris (awal baris dan akhir). Dan sedikit lebih jauh, ketika kita menggambar garis-garis ini, kita dapat dengan mudah mendapatkan koordinat mereka dari array simpul. Larik baris saya (saya menyebutnya tepi, karena ini adalah tepi kubus) Saya membuat array simpul di bawah ini dan terlihat seperti ini:

// Cube edges
const edges = [
  [0, 1],
  [1, 2],
  [2, 3],
  [3, 0],

  [0, 4],
  [1, 5],
  [2, 6],
  [3, 7],

  [4, 5],
  [5, 6],
  [6, 7],
  [7, 4],
];

Ada 12 pasang indeks dalam array ini, 2 indeks titik per baris.

Mari berkenalan dengan matriks lain yang akan menambah kubus kita, dan akhirnya, coba menggambarnya di layar. Matriks Skala terlihat seperti ini:

[sx0000sy0000sz00001]


Parameter sx, sy, sz pada diagonal utama berarti berapa kali kita ingin menambah objek. Jika kita mengganti 10, 10, 10 ke dalam matriks alih-alih sx, sy, sz, dan kalikan matriks ini dengan simpul-simpul kubus, ini akan membuat kubus kita sepuluh kali lebih besar dan itu tidak akan lagi menjadi 2 dengan 2 dengan 2, tetapi 20 dengan 20 oleh 20.

Untuk matriks penskalaan, serta untuk matriks pemindahan, kami mengimplementasikan metode dalam kelas Matrix, yang akan mengembalikan matriks dengan argumen yang sudah diganti:

static getScale(sx, sy, sz) {
  return [
    [sx, 0, 0, 0],
    [0, sy, 0, 0],
    [0, 0, sz, 0],
    [0, 0, 0, 1],
  ]
}

Konveyor visualisasi


Jika sekarang kita mencoba menggambar kubus dengan garis menggunakan koordinat titik saat ini, kita akan mendapatkan kubus dua piksel yang sangat kecil di sudut kiri atas layar, karena asal kanvas itu ada di sana. Mari siklus semua simpul kubus dan kalikan dengan matriks skala untuk membuat kubus lebih besar, dan kemudian oleh matriks perpindahan untuk melihat kubus tidak di sudut kiri atas, tetapi di tengah layar, saya memiliki kode untuk menghitung simpul dengan perkalian matriks di bawah ini array tepi, dan terlihat seperti ini:

const sceneVertices = []
for(let i = 0 ; i < vertices.length ; i++) {
  let vertex = Matrix.multiplyVector(
    Matrix.getScale(100, 100, 100),
    vertices[i]
  );

  vertex = Matrix.multiplyVector(
    Matrix.getTranslation(400, -300, 0),
    vertex
  );

  sceneVertices.push(vertex);
}

Harap dicatat bahwa kami tidak mengubah simpul asli dari kubus, tetapi menyimpan hasil perkalian ke dalam array sceneVertices, karena kami mungkin ingin menggambar beberapa kubus dengan ukuran yang berbeda dalam koordinat yang berbeda, dan jika kami mengubah koordinat awal, maka kami tidak akan dapat menggambar kubus berikutnya, t .untuk. tidak ada yang memulai, koordinat awal akan rusak oleh kubus pertama. Dalam kode di atas, saya meningkatkan kubus asli sebanyak 100 kali di semua arah, terima kasih untuk mengalikan semua simpul dengan matriks penskalaan dengan argumen 100, 100, 100, dan saya juga memindahkan semua simpul kubus ke kanan dan ke bawah masing-masing sebesar 400 dan -300 piksel, karena kami memiliki ukuran kanvas dari artikel sebelumnya adalah 800 x 600, hanya akan menjadi setengah lebar dan tinggi area gambar, dengan kata lain, tengah.

Kita telah selesai dengan simpul sejauh ini, tetapi kita masih perlu menggambar semua ini menggunakan drawLine dan array edge, mari kita menulis loop lain di bawah loop simpul untuk beralih ke tepi dan menggambar semua garis di dalamnya:

drawer.clearSurface()

for (let i = 0, l = edges.length ; i < l ; i++) {
  const e = edges[i]

  drawer.drawLine(
    sceneVertices[e[0]].x,
    sceneVertices[e[0]].y,
    sceneVertices[e[1]].x,
    sceneVertices[e[1]].y,
    0, 0, 255
  )
}

ctx.putImageData(imageData, 0, 0)

Ingatlah bahwa dalam artikel terakhir, kita mulai semua menggambar dengan membersihkan layar dari keadaan sebelumnya dengan memanggil metode clearSurface, lalu saya mengulangi semua wajah kubus dan menggambar kubus dengan garis-garis biru (0, 0, 255), dan saya mengambil koordinat garis-garis dari larik adeganVertices, t .untuk. sudah ada skala dan pindah simpul dalam siklus sebelumnya, tetapi indeks dari simpul ini bertepatan dengan indeks dari simpul asli dari array simpul, karena Saya memprosesnya dan memasukkannya ke dalam array sceneVertices tanpa mengubah urutannya. 

Jika kita menjalankan kode sekarang, kita tidak akan melihat apa pun di layar. Ini karena dalam sistem koordinat kami, Y melihat ke atas, dan dalam sistem koordinat, kanvas melihat ke bawah. Ternyata ada kubus kami, tetapi ada di luar layar dan untuk memperbaikinya, kita perlu membalik gambar dalam Y (mirror) sebelum menggambar piksel di kelas Drawer. Sejauh ini, opsi ini akan cukup bagi kami, sebagai hasilnya, kode untuk menggambar piksel bagi saya terlihat seperti ini:

drawPixel(x, y, r, g, b) {
  const offset = (this.width * -y + x) * 4;

  if (x >= 0 && x < this.width && -y >= 0 && y < this.height) {
    this.surface[offset] = r;
    this.surface[offset + 1] = g;
    this.surface[offset + 2] = b;
    this.surface[offset + 3] = 255;
  }
}

Dapat dilihat bahwa dalam rumus untuk memperoleh offset, Y sekarang dengan tanda minus dan sumbu sekarang terlihat ke arah yang kita butuhkan, juga dalam metode ini saya menambahkan tanda centang untuk melampaui batas array piksel. Beberapa optimasi lain muncul di kelas Drawer karena komentar pada artikel sebelumnya, jadi saya memposting seluruh kelas Drawer dengan beberapa optimasi dan Anda dapat mengganti Drawer lama dengan yang ini:

Kode Kelas Laci yang Ditingkatkan
class Drawer {
  surface = null;
  width = 0;
  height = 0;

  constructor(surface, width, height) {
    this.surface = surface;
    this.width = width;
    this.height = height;
  }

  drawPixel(x, y, r, g, b) {
    const offset = (this.width * -y + x) * 4;

    if (x >= 0 && x < this.width && -y >= 0 && y < this.height) {
      this.surface[offset] = r;
      this.surface[offset + 1] = g;
      this.surface[offset + 2] = b;
      this.surface[offset + 3] = 255;
    }
  }

  drawLine(x1, y1, x2, y2, r = 0, g = 0, b = 0) {
    const round = Math.trunc;
    x1 = round(x1);
    y1 = round(y1);
    x2 = round(x2);
    y2 = round(y2);

    const c1 = y2 - y1;
    const c2 = x2 - x1;

    const length = Math.max(
      Math.abs(c1),
      Math.abs(c2)
    );

    const xStep = c2 / length;
    const yStep = c1 / length;

    for (let i = 0 ; i <= length ; i++) {
      this.drawPixel(
        Math.trunc(x1 + xStep * i),
        Math.trunc(y1 + yStep * i),
        r, g, b,
      );
    }
  }

  clearSurface() {
    const surfaceSize = this.width * this.height * 4;
    for (let i = 0; i < surfaceSize; i++) {
      this.surface[i] = 0;
    }
  }
}

const drawer = new Drawer(
  imageData.data,
  imageData.width,
  imageData.height
);


Jika Anda menjalankan kode sekarang, maka gambar berikut akan muncul di layar:


Di sini Anda dapat melihat bahwa ada kotak di tengah, meskipun kami berharap mendapatkan kubus, ada apa? Sebenarnya - ini adalah kubus, itu hanya berdiri dengan sempurna selaras dengan salah satu wajah (sisi) ke arah kita, jadi kita tidak melihat sisanya. Juga, kita belum terbiasa dengan proyeksi, dan oleh karena itu permukaan belakang kubus tidak menjadi lebih kecil dengan jarak, seperti dalam kehidupan nyata. Untuk memastikan ini benar-benar sebuah kubus, mari kita putar sedikit sehingga tampak seperti gambar yang kita lihat sebelumnya ketika kita membuat array vertex. Untuk memutar gambar 3D, Anda dapat menggunakan 3 matriks khusus, karena kita dapat memutar di sekitar salah satu sumbu X, Y atau Z, yang berarti bahwa untuk setiap sumbu akan ada matriks rotasi sendiri (ada cara rotasi lain, tetapi ini adalah topik dari artikel selanjutnya). Seperti inilah bentuk matriks ini:

Rx(a)=[10000cos(a)βˆ’sin(a)00sin(a)cos(a)00001]


Matriks rotasi sumbu X

Ry(a)[cos(a)0sin(a)00100βˆ’sin(a)0cos(a)00001]


Matriks rotasi sumbu Y

Rz(a)[cos(a)βˆ’sin(a)00sin(a)cos(a)0000100001]


Matriks rotasi sumbu Z

Jika kita mengalikan simpul kubus dengan salah satu matriks ini, maka kubus akan berputar dengan sudut yang ditentukan (a) di sekitar sumbu, matriks rotasi yang akan kita pilih. Ada beberapa fitur ketika memutar beberapa sumbu sekaligus, dan kita akan melihatnya di bawah. Seperti yang dapat Anda lihat dari contoh matriks, mereka menggunakan 2 fungsi sin dan cos dan JavaScript sudah memiliki fungsional untuk menghitung Math.sin (a) dan Math.cos (a), tetapi mereka bekerja dengan ukuran sudut radian, yang mungkin tampaknya tidak nyaman. jika kita ingin memutar model. Sebagai contoh, jauh lebih nyaman bagi saya untuk mengubah sesuatu 90 derajat (ukuran derajat), yang dalam ukuran radian berartiPi / 2(Ada juga nilai Pi perkiraan di JS, ini adalah Math.PI konstan). Mari kita tambahkan 3 metode ke kelas Matrix untuk mendapatkan matriks rotasi, dengan sudut rotasi yang diterima dalam derajat, yang akan kita konversi ke radian, karena mereka diperlukan agar fungsi dosa / cos berfungsi:

static getRotationX(angle) {
  const rad = Math.PI / 180 * angle;

  return [
    [1, 0, 0, 0],
    [0, Math.cos(rad), -Math.sin(rad), 0],
    [0, Math.sin(rad), Math.cos(rad), 0],
    [0, 0, 0, 1],
  ];
}

static getRotationY(angle) {
  const rad = Math.PI / 180 * angle;

  return [
    [Math.cos(rad), 0, Math.sin(rad), 0],
    [0, 1, 0, 0],
    [-Math.sin(rad), 0, Math.cos(rad), 0],
    [0, 0, 0, 1],
  ];
}

static getRotationZ(angle) {
  const rad = Math.PI / 180 * angle;

  return [
    [Math.cos(rad), -Math.sin(rad), 0, 0],
    [Math.sin(rad), Math.cos(rad), 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1],
  ];
}

Ketiga metode dimulai dengan mengubah derajat ke radian, setelah itu kita mengganti sudut rotasi dalam radian ke dalam matriks rotasi, melewati sudut ke fungsi sin dan cos. Mengapa matriksnya demikian, Anda dapat membaca lebih lanjut pada hub di artikel tematik, dengan penjelasan yang sangat terperinci, jika tidak, Anda dapat menganggap matriks ini sebagai rumus yang telah dihitung untuk kami dan kami dapat memastikan bahwa mereka bekerja.

Di atas dalam kode, kami menerapkan 2 siklus, yang pertama mengubah simpul, yang kedua menggambar garis dengan indeks titik, sebagai hasilnya, kami mendapatkan gambar di layar dari simpul, dan mari kita sebut bagian kode ini pipa visualisasi. Konveyor karena kami mengambil puncak dan pada gilirannya melakukan operasi yang berbeda dengannya, skala, pergeseran, rotasi, rendering, seperti pada conveyor industri normal. Sekarang mari kita tambahkan ke siklus pertama dalam pipa visualisasi, selain penskalaan, rotasi di sekitar sumbu. Pertama, saya akan berbalik X, lalu sekitar Y, kemudian menambah model dan memindahkannya (2 tindakan terakhir sudah ada di sana), sehingga seluruh kode loop akan seperti ini:

for(let i = 0 ; i < vertices.length ; i++) {
  let vertex = Matrix.multiplyVector(
    Matrix.getRotationX(20),
    vertices[i]
  );

  vertex = Matrix.multiplyVector(
    Matrix.getRotationY(20),
    vertex
  );

  vertex = Matrix.multiplyVector(
    Matrix.getScale(100, 100, 100),
    vertex
  );

  vertex = Matrix.multiplyVector(
    Matrix.getTranslation(400, -300, 0),
    vertex
  );

  sceneVertices.push(vertex);
}

Dalam contoh ini, saya memutar semua simpul di sekitar sumbu X sebesar 20 derajat, kemudian sekitar Y sebesar 20 derajat dan saya sudah memiliki 2 transformasi tersisa. Jika Anda melakukan semuanya dengan benar, maka kubus Anda sekarang akan terlihat tiga dimensi:


Membalik sumbu memiliki satu fitur, misalnya, jika Anda memutar kubus terlebih dahulu di sekitar sumbu Y, dan kemudian di sekitar sumbu X, maka hasilnya akan berbeda:



Putar 20 derajat di sekitar X, lalu 20 derajat di sekitar YPutar 20 derajat di sekitar Y, lalu 20 derajat di sekitar X

Ada fitur lain, misalnya, jika Anda memutar kubus 90 derajat pada sumbu X, lalu 90 derajat pada sumbu Y, dan akhirnya 90 derajat di sekitar sumbu Z, maka rotasi terakhir di sekitar Z akan membatalkan rotasi di sekitar X, dan Anda mendapatkan yang sama hasilnya adalah seolah-olah Anda baru saja memutar angka 90 derajat di sekitar sumbu Y. Untuk melihat mengapa hal ini terjadi, ambil benda persegi panjang (atau kubik) di tangan Anda (mis. dirakit kubus Rubik), ingat posisi awal objek dan putar 90 derajat terlebih dahulu sekitar imajiner X, dan kemudian 90 derajat di sekitar Y dan 90 derajat di sekitar Z dan ingat sisi mana yang telah menjadi untuk Anda, kemudian mulai dari posisi awal yang Anda ingat sebelumnya dan lakukan hal yang sama, menghapus putaran X dan Z, putar hanya sekitar Y - Anda akan melihat bahwa hasilnya sama.Sekarang kami tidak akan menyelesaikan masalah ini dan masuk ke detailnya, rotasi ini saat ini benar-benar memuaskan bagi kami, tetapi kami akan menyebutkan masalah ini di bagian ketiga (jika Anda ingin lebih memahami sekarang, coba cari artikel di hub dengan kueri "kunci berengsel") .

Sekarang mari kita sedikit mengoptimalkan kode kita, disebutkan di atas bahwa transformasi matriks dapat digabungkan satu sama lain dengan mengalikan matriks transformasi. Mari kita coba untuk tidak mengalikan masing-masing vektor terlebih dahulu dengan matriks rotasi di sekitar X, kemudian di sekitar Y, kemudian menskalakan dan pada akhir langkah, dan pertama, sebelum loop, kita mengalikan semua matriks, dan dalam loop kita akan mengalikan setiap simpul dengan hanya satu matriks yang dihasilkan, saya memiliki kode keluar seperti ini:

let matrix = Matrix.getRotationX(20);

matrix = Matrix.multiply(
  Matrix.getRotationY(20),
  matrix
);

matrix = Matrix.multiply(
  Matrix.getScale(100, 100, 100),
  matrix,
);

matrix = Matrix.multiply(
  Matrix.getTranslation(400, -300, 0),
  matrix,
);

const sceneVertices = [];
for(let i = 0 ; i < vertices.length ; i++) {
  let vertex = Matrix.multiplyVector(
    matrix,
    vertices[i]
  );

  sceneVertices.push(vertex);
}

Dalam contoh ini, kombinasi transformasi dilakukan 1 kali sebelum siklus, dan oleh karena itu kami hanya memiliki 1 perkalian matriks dengan setiap titik. Jika Anda menjalankan kode ini, pola kubus harus tetap sama.

Mari kita tambahkan animasi paling sederhana, yaitu, kita akan mengubah sudut rotasi di sekitar sumbu Y dalam interval, misalnya, kita akan mengubah sudut rotasi di sekitar sumbu Y sebesar 1 derajat, setiap 100 milidetik. Untuk melakukan ini, masukkan kode pipa visualisasi ke fungsi setInterval, yang pertama kali kita gunakan dalam artikel 1. Kode pipa animasi terlihat seperti ini:

let angle = 0
setInterval(() => {
  let matrix = Matrix.getRotationX(20)

  matrix = Matrix.multiply(
    Matrix.getRotationY(angle += 1),
    matrix
  )

  matrix = Matrix.multiply(
    Matrix.getScale(100, 100, 100),
    matrix,
  )

  matrix = Matrix.multiply(
    Matrix.getTranslation(400, -300, 0),
    matrix,
  )

  const sceneVertices = []
  for(let i = 0 ; i < vertices.length ; i++) {
    let vertex = Matrix.multiplyVector(
      matrix,
      vertices[i]
    )

    sceneVertices.push(vertex)
  }

  drawer.clearSurface()

  for (let i = 0, l = edges.length ; i < l ; i++) {
    const e = edges[i]

    drawer.drawLine(
      sceneVertices[e[0]].x,
      sceneVertices[e[0]].y,
      sceneVertices[e[1]].x,
      sceneVertices[e[1]].y,
      0, 0, 255
    )
  }

  ctx.putImageData(imageData, 0, 0)
}, 100)

Hasilnya harus seperti ini:


Hal terakhir yang akan kita lakukan di bagian ini adalah untuk menampilkan sumbu sistem koordinat pada layar sehingga terlihat di sekitar tempat kubus kami berputar. Kami menggambar sumbu Y dari tengah ke atas, panjang 200 piksel, sumbu X, ke kanan, juga panjang 200 piksel, dan sumbu Z, menarik 150 piksel ke bawah dan ke kiri (secara diagonal), seperti yang ditunjukkan di bagian paling awal artikel pada gambar sistem koordinat tangan kanan. . Mari kita mulai dengan bagian yang paling sederhana, ini adalah sumbu X, Y, karena garis mereka bergeser hanya dalam satu arah. Setelah loop yang menggambar kubus (loop tepi) tambahkan X, render sumbu Y:

const center = new Vector(400, -300, 0)
drawer.drawLine(
  center.x, center.y,
  center.x, center.y + 200,
  150, 150, 150
)

drawer.drawLine(
  center.x, center.y,
  center.x + 200, center.y,
  150, 150, 150
)

Vektor pusat adalah bagian tengah jendela gambar, karena kami memiliki dimensi saat ini 800 oleh 600, dan -300 untuk Y, saya indikasikan, karena fungsi drawPixel membalik Y dan membuat arahnya cocok untuk kanvas (di kanvas, Y melihat ke bawah). Kemudian kita menggambar 2 sumbu menggunakan drawLine, pertama-tama menggeser Y 200 piksel ke atas (ujung garis sumbu Y), kemudian X 200 piksel ke kanan (ujung garis sumbu X). Hasil:


Sekarang mari kita menggambar garis sumbu Z, itu diagonal ke bawah \ kiri dan vektor perpindahannya akan menjadi [-1, -1, 0] dan kita juga perlu menggambar garis dengan panjang 150 piksel, yaitu vektor perpindahan [-1, -1, 0] harus 150 panjang, opsi pertama adalah [-150, -150, 0], tetapi jika kita menghitung panjang vektor seperti itu, itu akan menjadi sekitar 212 piksel. Sebelumnya dalam artikel ini, kami membahas cara mendapatkan vektor dengan panjang yang diinginkan dengan benar. Pertama-tama, kita perlu menormalkannya untuk menghasilkan panjang 1, dan kemudian mengalikannya dengan skalar dengan panjang yang ingin kita dapatkan, dalam kasus kita adalah 150. Dan terakhir, kita meringkas koordinat tengah layar dan vektor perpindahan sumbu Z, jadi kita dapatkan di mana Garis sumbu Z harus diakhiri. Mari kita menulis kode, setelah kode output dari 2 sumbu sebelumnya untuk menggambar garis sumbu Z:

const zVector = new Vector(-1, -1, 0);
const zCoords = Vector.add(
  center,
  zVector.normalize().multiplyByScalar(150)
);
drawer.drawLine(
  center.x, center.y,
  zCoords.x, zCoords.y,
  150, 150, 150
);

Dan sebagai hasilnya, Anda mendapatkan semua 3 sumbu dengan panjang yang diinginkan:


Dalam contoh ini, sumbu Z hanya menunjukkan sistem koordinat yang kita miliki, kita menggambarnya secara diagonal sehingga dapat dilihat, karena sumbu Z sebenarnya tegak lurus terhadap pandangan kita, dan kita bisa menggambarnya dengan titik di layar, yang tidak akan indah.

Secara total, dalam artikel ini kita pada dasarnya memahami sistem koordinat, vektor dan dengan beberapa operasi pada mereka, matriks dan peran mereka dalam transformasi koordinat, mengurutkan simpul dan menulis konveyor sederhana untuk memvisualisasikan kubus dan sumbu sistem koordinat, memperbaiki teori dengan praktik. Semua kode aplikasi tersedia di bawah spoiler:

Kode untuk seluruh aplikasi
const ctx = document.getElementById('surface').getContext('2d');
const imageData = ctx.createImageData(800, 600);

class Vector {
  x = 0;
  y = 0;
  z = 0;
  w = 1;

  constructor(x, y, z, w = 1) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.w = w;
  }

  multiplyByScalar(s) {
    this.x *= s;
    this.y *= s;
    this.z *= s;

    return this;
  }

  static add(v1, v2) {
    return new Vector(
      v1.x + v2.x,
      v1.y + v2.y,
      v1.z + v2.z,
    );
  }

  getLength() {
    return Math.sqrt(
      this.x * this.x + this.y * this.y + this.z * this.z
    );
  }

  normalize() {
    const length = this.getLength();

    this.x /= length;
    this.y /= length;
    this.z /= length;

    return this;
  }
}

class Matrix {
  static multiply(a, b) {
    const m = [
      [0, 0, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
      [0, 0, 0, 0],
    ];

    for (let i = 0 ; i < 4 ; i++) {
      for(let j = 0 ; j < 4 ; j++) {
        m[i][j] = a[i][0] * b[0][j] +
          a[i][1] * b[1][j] +
          a[i][2] * b[2][j] +
          a[i][3] * b[3][j];
      }
    }

    return m;
  }

  static getRotationX(angle) {
    const rad = Math.PI / 180 * angle;

    return [
      [1, 0, 0, 0],
      [0, Math.cos(rad), -Math.sin(rad), 0],
      [0, Math.sin(rad), Math.cos(rad), 0],
      [0, 0, 0, 1],
    ];
  }

  static getRotationY(angle) {
    const rad = Math.PI / 180 * angle;

    return [
      [Math.cos(rad), 0, Math.sin(rad), 0],
      [0, 1, 0, 0],
      [-Math.sin(rad), 0, Math.cos(rad), 0],
      [0, 0, 0, 1],
    ];
  }

  static getRotationZ(angle) {
    const rad = Math.PI / 180 * angle;

    return [
      [Math.cos(rad), -Math.sin(rad), 0, 0],
      [Math.sin(rad), Math.cos(rad), 0, 0],
      [0, 0, 1, 0],
      [0, 0, 0, 1],
    ];
  }

  static getTranslation(dx, dy, dz) {
    return [
      [1, 0, 0, dx],
      [0, 1, 0, dy],
      [0, 0, 1, dz],
      [0, 0, 0, 1],
    ];
  }

  static getScale(sx, sy, sz) {
    return [
      [sx, 0, 0, 0],
      [0, sy, 0, 0],
      [0, 0, sz, 0],
      [0, 0, 0, 1],
    ];
  }

  static multiplyVector(m, v) {
    return new Vector(
      m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z + m[0][3] * v.w,
      m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z + m[1][3] * v.w,
      m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z + m[2][3] * v.w,
      m[3][0] * v.x + m[3][1] * v.y + m[3][2] * v.z + m[3][3] * v.w,
    );
  }
}

class Drawer {
  surface = null;
  width = 0;
  height = 0;

  constructor(surface, width, height) {
    this.surface = surface;
    this.width = width;
    this.height = height;
  }

  drawPixel(x, y, r, g, b) {
    const offset = (this.width * -y + x) * 4;

    if (x >= 0 && x < this.width && -y >= 0 && -y < this.height) {
      this.surface[offset] = r;
      this.surface[offset + 1] = g;
      this.surface[offset + 2] = b;
      this.surface[offset + 3] = 255;
    }
  }
  drawLine(x1, y1, x2, y2, r = 0, g = 0, b = 0) {
    const round = Math.trunc;
    x1 = round(x1);
    y1 = round(y1);
    x2 = round(x2);
    y2 = round(y2);

    const c1 = y2 - y1;
    const c2 = x2 - x1;

    const length = Math.max(
      Math.abs(c1),
      Math.abs(c2)
    );

    const xStep = c2 / length;
    const yStep = c1 / length;

    for (let i = 0 ; i <= length ; i++) {
      this.drawPixel(
        Math.trunc(x1 + xStep * i),
        Math.trunc(y1 + yStep * i),
        r, g, b,
      );
    }
  }

  clearSurface() {
    const surfaceSize = this.width * this.height * 4;
    for (let i = 0; i < surfaceSize; i++) {
      this.surface[i] = 0;
    }
  }
}

const drawer = new Drawer(
  imageData.data,
  imageData.width,
  imageData.height
);

// Cube vertices
const vertices = [
  new Vector(-1, 1, 1), // 0 
  new Vector(-1, 1, -1), // 1 
  new Vector(1, 1, -1), // 2 
  new Vector(1, 1, 1), // 3 
  new Vector(-1, -1, 1), // 4 
  new Vector(-1, -1, -1), // 5 
  new Vector(1, -1, -1), // 6 
  new Vector(1, -1, 1), // 7 
];

// Cube edges
const edges = [
  [0, 1],
  [1, 2],
  [2, 3],
  [3, 0],

  [0, 4],
  [1, 5],
  [2, 6],
  [3, 7],

  [4, 5],
  [5, 6],
  [6, 7],
  [7, 4],
];

let angle = 0;
setInterval(() => {
  let matrix = Matrix.getRotationX(20);

  matrix = Matrix.multiply(
    Matrix.getRotationY(angle += 1),
    matrix
  );

  matrix = Matrix.multiply(
    Matrix.getScale(100, 100, 100),
    matrix,
  );

  matrix = Matrix.multiply(
    Matrix.getTranslation(400, -300, 0),
    matrix,
  );

  const sceneVertices = [];
  for(let i = 0 ; i < vertices.length ; i++) {
    let vertex = Matrix.multiplyVector(
      matrix,
      vertices[i]
    );

    sceneVertices.push(vertex);
  }

  drawer.clearSurface();

  for (let i = 0, l = edges.length ; i < l ; i++) {
    const e = edges[i];

    drawer.drawLine(
      sceneVertices[e[0]].x,
      sceneVertices[e[0]].y,
      sceneVertices[e[1]].x,
      sceneVertices[e[1]].y,
      0, 0, 255
    );
  }

  const center = new Vector(400, -300, 0)
  drawer.drawLine(
    center.x, center.y,
    center.x, center.y + 200,
    150, 150, 150
  );

  drawer.drawLine(
    center.x, center.y,
    center.x + 200, center.y,
    150, 150, 150
  );

  const zVector = new Vector(-1, -1, 0, 0);
  const zCoords = Vector.add(
    center,
    zVector.normalize().multiplyByScalar(150)
  );
  drawer.drawLine(
    center.x, center.y,
    zCoords.x, zCoords.y,
    150, 150, 150
  );

  ctx.putImageData(imageData, 0, 0);
}, 100);


Apa berikutnya?


Pada bagian selanjutnya, kita akan mempertimbangkan bagaimana mengontrol kamera dan bagaimana membuat proyeksi (semakin jauh objek, semakin kecil itu), mengenal segitiga dan mencari tahu bagaimana membangun model 3D dari mereka, menganalisis apa yang normal dan mengapa mereka diperlukan.

All Articles