3D lakukan sendiri. Bagian 1: piksel dan garis



Saya ingin mencurahkan seri artikel ini untuk pembaca yang ingin menjelajahi dunia pemrograman 3D dari awal, kepada orang-orang yang ingin mempelajari dasar-dasar membuat komponen 3D dari permainan dan aplikasi. Kami akan menerapkan setiap operasi dari awal untuk memahami setiap aspek, bahkan jika sudah ada fungsi siap pakai yang membuatnya lebih cepat. Setelah belajar, kami akan beralih ke alat bawaan untuk bekerja dengan 3D. Setelah membaca serangkaian artikel, Anda akan memahami cara membuat adegan tiga dimensi yang kompleks dengan cahaya, bayangan, tekstur dan efek, cara melakukan semua ini tanpa pengetahuan mendalam dalam matematika, dan banyak lagi. Anda dapat melakukan semua ini baik secara mandiri maupun dengan bantuan alat yang sudah jadi.

Pada bagian pertama kami akan mempertimbangkan:

  • Konsep rendering (perangkat lunak, perangkat keras)
  • Apa itu pixel / permukaan?
  • Analisis terperinci dari keluaran saluran

Agar tidak membuang waktu berharga Anda membaca artikel, yang mungkin tidak bisa dipahami oleh orang yang tidak siap, saya akan segera beralih ke persyaratan. Anda dapat dengan aman mulai membaca artikel pada 3D, jika Anda tahu dasar-dasar pemrograman dalam bahasa apa pun, karena Saya hanya akan fokus pada studi pemrograman 3D, dan bukan pada studi fitur bahasa dan dasar-dasar pemrograman. Adapun persiapan matematika, Anda tidak perlu khawatir di sini, meskipun banyak yang tidak memiliki keinginan untuk belajar 3D, karena mereka takut dengan perhitungan yang rumit dan formula-formula murka karena itu mimpi buruk kemudian diimpikan, tetapi sebenarnya tidak ada yang perlu dikhawatirkan. Saya akan mencoba menjelaskan sejelas mungkin semua yang diperlukan untuk 3D, Anda hanya harus dapat melipatgandakan, membagi, menambah dan mengurangi. Jadi, jika Anda telah melewati kriteria seleksi, Anda dapat mulai membaca.

Sebelum mulai menjelajahi dunia 3D yang menarik, mari kita pilih bahasa pemrograman untuk contoh, serta lingkungan pengembangan. Bahasa apa yang harus saya pilih untuk pemrograman grafik 3D? Siapa pun, Anda dapat bekerja di mana Anda merasa paling nyaman, matematika akan sama di mana-mana. Dalam artikel ini, semua contoh akan ditampilkan dalam konteks JS (di sini tomat terbang ke saya). Mengapa js? Ini sederhana - akhir-akhir ini saya telah bekerja terutama dengannya, dan karena itu saya dapat lebih efektif menyampaikan esensi kepada Anda. Saya akan memotong semua fitur JS dalam contoh, karena kami hanya membutuhkan fitur paling dasar yang dimiliki bahasa apa pun, jadi kami akan memperhatikan 3D secara khusus. Tetapi Anda memilih apa yang Anda sukai, karena dalam artikel, semua rumus tidak akan terikat ke fitur bahasa pemrograman apa pun. Lingkungan mana yang harus dipilih? Tidak masalah,dalam kasus JS, editor teks apa pun cocok, Anda dapat menggunakan yang lebih dekat dengan Anda.

Semua contoh akan menggunakan kanvas untuk melukis dengan itu, Anda dapat mulai menggambar dengan sangat cepat, tanpa analisis rinci. Kanvas adalah alat yang ampuh, dengan banyak metode yang sudah jadi untuk menggambar, tetapi dari semua fitur-fiturnya, untuk pertama kalinya, kami hanya akan menggunakan keluaran piksel! 

Semua tampilan tiga dimensi di layar menggunakan piksel, kemudian di artikel Anda akan melihat bagaimana ini terjadi. Apakah akan melambat? Tanpa akselerasi perangkat keras (misalnya, akselerasi dengan kartu video) - akan terjadi. Pada artikel pertama, kami tidak akan menggunakan akselerasi, kami akan menulis semuanya dari awal untuk memahami aspek dasar 3D. Mari kita lihat beberapa istilah yang akan disebutkan di artikel mendatang:

  • (Rendering) β€” 3D- . , 3D- , , .
  • (Software Rendering) β€” . , , , - . , . 3D- , β€” .
  • Hardware Rendering - Proses rendering yang dibantu perangkat keras. Saya menggunakannya game dan aplikasi. Semuanya bekerja sangat cepat, karena banyak komputasi rutin mengambil alih kartu video, yang dirancang untuk ini.

Saya tidak bercita-cita untuk judul "definisi tahun" dan saya mencoba untuk menyatakan semua deskripsi istilah sejelas mungkin. Hal utama adalah memahami ide, yang kemudian dapat dikembangkan secara mandiri. Saya juga ingin menarik perhatian pada kenyataan bahwa semua contoh kode yang akan ditampilkan dalam artikel sering tidak dioptimalkan untuk kecepatan, untuk menjaga kemudahan pemahaman. Ketika Anda memahami hal utama - bagaimana grafik 3D bekerja, Anda dapat mengoptimalkan semuanya sendiri.

Pertama, buat proyek, bagi saya itu hanya file text.html teks , dengan konten berikut:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>3D it’s easy. Part 1</title>
</head>

<body>
    <!--         -->
    <canvas id="surface" width="800" height="600"></canvas>

    <script>
        //    
    </script>
</body>

</html>

Saya tidak akan terlalu fokus pada JS dan kanvas sekarang - ini bukan karakter utama dari artikel ini. Tetapi untuk pemahaman umum, saya akan mengklarifikasi bahwa <canvas ...> adalah sebuah persegi panjang (dalam kasus saya, berukuran 800 x 600 piksel) di mana saya akan menampilkan semua gambar. Saya mendaftar kanvas sekali dan tidak akan mengubahnya lagi.

<script> … </script> 

Script - elemen di mana kita akan menulis semua logika untuk rendering grafik 3D dengan tangan kita sendiri (dalam JavaScript). 

Ketika kami baru saja meninjau struktur file index.html dari proyek yang baru dibuat, kami akan mulai berurusan dengan grafik 3D.

Ketika kita menggambar sesuatu di jendela, ini dalam hitungan akhir berubah menjadi piksel, karena monitor itulah yang ditampilkan. Semakin banyak piksel, semakin tajam gambarnya, tetapi komputer juga memuat lebih banyak. Bagaimana cara kita menggambar di jendela yang disimpan? Grafik di jendela apa pun dapat direpresentasikan sebagai larik piksel, dan piksel itu sendiri hanyalah warna. Artinya, resolusi layar 800x600 berarti bahwa jendela kita masing-masing berisi 600 baris 800 piksel, yaitu 800 * 600 = 480000 piksel, banyak, bukan? Pixel disimpan dalam array. Mari kita pikirkan di array mana kita akan menyimpan piksel. Jika kita harus memiliki 800 x 600 piksel, maka opsi yang paling jelas adalah dalam array dua dimensi dari 800 x 600. Dan ini hampir merupakan opsi yang tepat, atau lebih tepatnya, opsi yang sepenuhnya benar. Tetapi piksel jendela, lebih baik untuk menyimpan dalam array satu dimensi dari 480.000 elemen (jika resolusinya adalah 800 oleh 600),hanya karena lebih cepat bekerja dengan array satu dimensi, karena itu disimpan dalam memori dalam urutan byte yang berkelanjutan (semuanya terletak di dekatnya dan karena itu mudah untuk mendapatkannya). Dalam array dua dimensi (misalnya, dalam kasus JS), setiap baris dapat tersebar di berbagai tempat dalam memori, sehingga mengakses elemen-elemen dari array tersebut akan memakan waktu lebih lama. Juga, untuk beralih pada array satu dimensi, hanya diperlukan 1 siklus, dan untuk bilangan bulat dua dimensi 2, mengingat perlunya melakukan puluhan ribu iterasi siklus, kecepatan sangat penting di sini. Apa itu pixel dalam array seperti itu? Seperti disebutkan di atas - ini hanya warna, atau lebih tepatnya 3 komponennya (merah, hijau, biru). Apa pun, bahkan gambar paling berwarna hanyalah serangkaian piksel warna berbeda. Sebuah piksel dalam memori dapat disimpan sesuka Anda, baik array 3 elemen, atau dalam struktur di mana merah, biru,biru; atau sesuatu yang lain. Gambar yang terdiri dari array piksel yang baru saja kita uraikan, saya akan terus memanggil permukaan. Ternyata karena semua yang ditampilkan di layar disimpan dalam array piksel, maka perubahan elemen (piksel) dalam array ini - kami akan mengubah piksel demi piksel gambar di layar. Inilah yang akan kita lakukan di artikel ini.

Tidak ada fungsi menggambar piksel di kanvas, tetapi dimungkinkan untuk mengakses array piksel satu dimensi, yang kita bahas di atas. Cara melakukannya ditunjukkan dalam contoh di bawah ini (ini dan semua contoh di masa depan hanya akan berada di dalam elemen skrip):

//     ()    
const ctx = document
.getElementById('surface')
.getContext('2d')

//     ,    
// +       
const imageData = ctx.createImageData(800, 600)

Dalam contoh, imageData adalah objek di mana ada 3 properti:

  • tinggi dan lebar - bilangan bulat menyimpan tinggi dan lebar jendela untuk menggambar
  • data - array integer tanpa tanda 8-bit (Anda dapat menyimpan angka dalam rentang dari 0 hingga 255 di dalamnya)

Array data memiliki struktur yang sederhana namun jelas. Array satu dimensi ini menyimpan data setiap piksel, yang akan kami tampilkan di layar dalam format berikut:
4 elemen pertama array (indeks 0,1,2,3) adalah data piksel pertama di baris pertama. 4 elemen kedua (indeks 4, 5, 6, 7) adalah data piksel kedua dari baris pertama. Ketika kita sampai pada pixel ke 800 dari baris pertama, asalkan jendelanya selebar 800 pixel - pixel ke 801 sudah menjadi milik baris kedua. Jika kita mengubahnya, di layar kita akan melihat bahwa piksel ke-1 dari baris ke-2 telah berubah (meskipun menurut hitungan dalam array, itu akan menjadi piksel ke-801). Mengapa ada 4 elemen untuk setiap piksel dalam array? Ini karena di kanvas, selain mengalokasikan 1 elemen untuk setiap warna - merah, hijau, biru (ini adalah 3 elemen), 1 elemen lagi untuk transparansi (mereka juga mengatakan saluran alpha atau opacity). Saluran alfa, seperti warna, diatur dalam kisaran dari 0 (transparan) hingga 255 (buram). Dengan struktur ini, kita mendapatkan gambar 32-bit,karena setiap pixel terdiri dari 4 elemen 8 bit. Untuk meringkas: setiap piksel berisi: warna merah, hijau, biru dan saluran alfa (transparansi). Skema warna ini disebut ARGB (Alpha Red Green Blue). Dan fakta bahwa setiap piksel menempati 32 bit mengatakan bahwa kita memiliki gambar 32 bit (mereka juga mengatakan gambar dengan kedalaman warna 32 bit).

Secara default, seluruh array piksel imageData.data (data adalah properti di mana array piksel, dan imageData hanya sebuah objek) diisi dengan nilai 0, dan jika kami mencoba menampilkan array seperti itu, kami tidak akan melihat sesuatu yang menarik di layar, karena 0 , 0, 0 berwarna hitam, tetapi karena transparansi di sini juga akan 0, dan ini adalah warna yang benar-benar transparan, kami bahkan tidak akan melihat warna hitam di layar!

Sangat tidak nyaman untuk bekerja secara langsung dengan array satu dimensi seperti itu, jadi kita akan menulis kelas untuk itu di mana kita akan membuat metode untuk menggambar. Saya akan beri nama kelas - Laci. Kelas ini hanya akan menyimpan data yang diperlukan dan melakukan perhitungan yang diperlukan, mengabstraksi sebanyak mungkin dari alat yang digunakan untuk rendering. Itu sebabnya kami akan menempatkan semua perhitungan dan bekerja dengan array di dalamnya. Dan panggilan untuk metode tampilan di kanvas, kita akan menempatkan di luar kelas, karena mungkin ada sesuatu yang lain, bukan kanvas. Dalam hal ini, kelas kita tidak perlu diubah. Untuk bekerja dengan array piksel (permukaan), lebih mudah bagi kita untuk menyimpannya di kelas Drawer, serta lebar dan tinggi gambar, sehingga kita dapat mengakses piksel yang diinginkan dengan benar. Jadi, kelas Drawer, sambil menjaga data minimum yang diperlukan untuk menggambar, terlihat seperti ini untukku:

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

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

Seperti yang Anda lihat di konstruktor, kelas Drawer mengambil semua data yang diperlukan dan menyimpannya. Sekarang Anda dapat membuat instance kelas ini dan mengirimkan array piksel, lebar, dan tinggi ke dalamnya (kami sudah memiliki semua data ini, karena kami membuatnya di atas dan menyimpannya di imageData):

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

Di kelas Drawer, kita akan menulis beberapa fungsi menggambar, untuk memudahkan pekerjaan di masa depan. Kami akan memiliki fungsi untuk menggambar piksel, fungsi untuk menggambar garis, dan dalam artikel selanjutnya fungsi untuk menggambar segitiga dan bentuk lainnya akan muncul. Tapi mari kita mulai dengan metode menggambar piksel. Saya akan memanggilnya drawPixel. Jika kita menggambar piksel, maka itu harus memiliki koordinat, serta warna:

drawPixel(x, y, r, g, b)  { }

Harap dicatat bahwa fungsi drawPixel tidak menerima parameter alfa (transparansi), dan di atas kami menemukan bahwa susunan piksel terdiri dari 3 parameter warna dan 1 parameter transparansi. Saya tidak secara khusus menunjukkan transparansi, karena kami benar-benar tidak membutuhkannya sebagai contoh. Secara default, kami akan menetapkan 255 (mis. Semuanya akan menjadi buram). Sekarang mari kita pikirkan tentang bagaimana menulis warna yang diinginkan ke dalam array piksel dalam koordinat x, y. Karena kami memiliki semua informasi tentang gambar disimpan dalam array satu dimensi, di mana 1 piksel (8 bit) dialokasikan untuk setiap piksel. Untuk mengakses piksel yang diinginkan dalam array, pertama-tama kita perlu menentukan indeks lokasi merah, karena setiap piksel dimulai dengan itu (misalnya [r, g, b, a]). Sedikit penjelasan tentang struktur array:



Tabel berwarna hijau menunjukkan bagaimana komponen warna disimpan dalam array permukaan satu dimensi. Indeks mereka dalam array yang sama ditunjukkan dengan warna biru, dan koordinat piksel yang menerima fungsi drawPixel, yang perlu kita konversi menjadi indeks dalam array satu dimensi, menunjukkan r, g, b, a untuk piksel berwarna biru. Jadi, dari tabel dapat dilihat bahwa untuk setiap piksel komponen warna merah yang lebih dulu, mari kita mulai dengan itu. Misalkan kita ingin mengubah komponen merah dari warna piksel dalam koordinat X1Y1 dengan ukuran gambar 2 kali 2 piksel. Dalam tabel kita melihat bahwa ini adalah indeks 12, tetapi bagaimana cara menghitungnya? Pertama kita menemukan indeks dari baris yang kita butuhkan, untuk ini kita mengalikan lebar gambar dengan Y dan dengan 4 (jumlah nilai per piksel) - ini akan menjadi:

width * y * 4 
//  :
2 * 1 * 4 = 8

Kita melihat bahwa baris ke-2 dimulai dengan indeks 8. Jika kita bandingkan dengan plat, hasilnya konvergen.

Sekarang Anda perlu menambahkan kolom offset ke indeks baris yang ditemukan untuk mendapatkan indeks merah yang diinginkan. Untuk melakukan ini, tambahkan X kali 4 ke indeks baris. Rumus lengkapnya adalah:

width * y * 4 + x * 4 
//     :
(width * y + x) * 4
//  :
(2 * 1 + 1) * 4 = 12

Sekarang kita membandingkan 12 dengan tabel dan melihat bahwa piksel X1Y1 benar-benar dimulai dengan indeks 12.

Untuk menemukan indeks komponen warna lainnya, Anda perlu menambahkan offset warna ke indeks merah: +1 (hijau), +2 (biru), +3 (alpha) . Sekarang kita bisa menerapkan metode drawPixel di dalam kelas Drawer menggunakan rumus di atas:

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

    this.surface[offset] = r
    this.surface[offset + 1] = g
    this.surface[offset + 2] = b
    this.surface[offset + 3] = 255
}

Dalam metode drawPixel ini, saya merender bagian berulang rumus ke konstanta offset. Juga terlihat bahwa dalam alpha saya hanya menulis 255, karena itu dalam struktur, tetapi sekarang kita tidak perlu untuk menghasilkan piksel.

Sudah waktunya untuk menguji kode dan akhirnya melihat piksel pertama di layar. Berikut adalah contoh menggunakan metode render piksel:

//     Drawer
drawer.drawPixel(10, 10, 255, 0, 0)
drawer.drawPixel(10, 20, 0, 0, 255)

//         canvas
ctx.putImageData(imageData, 0, 0)

Dalam contoh di atas, saya menggambar 2 piksel, satu merah 255, 0, 0, biru lainnya 0, 0, 255. Tetapi perubahan dalam array imageData.data (juga merupakan permukaan di dalam kelas Drawer) tidak akan muncul di layar. Untuk menggambar, Anda perlu memanggil ctx.putImageData (imageData, 0, 0), di mana imageData adalah objek di mana array piksel dan lebar / tinggi area gambar, dan 0, 0 adalah titik relatif di mana array piksel akan ditampilkan (selalu meninggalkan 0, 0 ) Jika Anda melakukan semuanya dengan benar, maka Anda akan memiliki gambar berikut di kiri atas elemen kanvas di jendela browser: Apakah Anda melihat



piksel? Mereka sangat kecil, dan berapa banyak pekerjaan yang telah dilakukan.

Sekarang mari kita coba menambahkan sedikit dinamika pada contoh, misalnya, sehingga setiap 10 milidetik piksel kami bergeser ke kanan (kami akan mengubah piksel X dengan +1 setiap 10 milidetik), kami akan memperbaiki kode gambar piksel satu per satu pada suatu interval:

let x = 10
setInterval(() => {

    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)

}, 10)

Dalam contoh ini, saya hanya menyisakan output dari piksel biru dan membungkus fungsi setInterval dengan parameter 10 dalam JavaScript. Ini berarti bahwa kode akan dipanggil kira-kira setiap 10 milidetik. Jika Anda menjalankan contoh seperti itu, Anda akan melihat bahwa alih-alih piksel bergeser ke kanan, Anda akan memiliki sesuatu seperti ini:



Potongan panjang (atau jejak) tersebut tetap ada karena kami tidak menghapus warna piksel sebelumnya dalam susunan permukaan, jadi dengan setiap pemanggilan interval, kami juga menambahkan satu piksel. Mari kita menulis metode yang akan membersihkan permukaan ke keadaan semula. Dengan kata lain, isi array dengan nol. Tambahkan metode clearSurface ke kelas Drawer:

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

Tidak ada logika dalam array ini, hanya diisi dengan nol. Anda disarankan untuk memanggil metode ini setiap kali sebelum menggambar gambar baru. Dalam hal animasi piksel, sebelum menggambar piksel ini:

let x = 10
setInterval(() => {
    drawer.clearSurface()
    drawer.drawPixel(x++, 20, 0, 0, 255)
    ctx.putImageData(imageData, 0, 0)
}, 10)

Sekarang jika Anda menjalankan contoh ini, piksel akan bergeser ke kanan, satu per satu - tanpa jejak yang tidak perlu dari koordinat sebelumnya.

Hal terakhir yang kami terapkan dalam artikel pertama adalah metode menggambar garis. Tambahkan, tentu saja, ke kelas Drawer. Metode yang saya sebut drawLine. Apa yang akan dia ambil? Tidak seperti titik, garis masih memiliki koordinat di mana ia berakhir. Dengan kata lain, garis memiliki awal, akhir dan warna, yang akan kita sampaikan ke metode:

drawLine(x1, y1, x2, y2, r, g, b) { }

Baris apa pun terdiri dari piksel, hanya tinggal mengisinya dengan benar dari piksel x1, y1 ke x2, y2. Untuk memulainya, karena garis terdiri dari piksel, maka kita akan menampilkannya piksel demi piksel dalam lingkaran, tetapi bagaimana cara menghitung berapa piksel yang akan dihasilkan? Misalnya, untuk menggambar garis dari [0, 0] hingga [3, 0] secara intuitif jelas bahwa Anda memerlukan 4 piksel ([0, 0], [1, 0], [2, 0], [3, 0],) . Tetapi dari [12, 6] hingga [43, 14], belum jelas berapa lama garis itu akan (berapa banyak piksel untuk ditampilkan) dan koordinat apa yang akan mereka miliki. Untuk melakukan ini, ingat geometri kecil. Jadi, kita memiliki garis yang dimulai pada x1, y1 dan berakhir di x2, y2.


Mari kita menggambar garis putus-putus dari awal dan akhir sehingga kita mendapatkan segitiga (gambar di atas). Kita akan melihat bahwa di persimpangan garis yang ditarik, sudut 90 derajat telah terbentuk. Jika segitiga memiliki sudut seperti itu, maka segitiga disebut segi empat, dan sisi-sisinya, di antaranya sudut 90 derajat, disebut kaki. Garis solid ketiga (yang kami coba gambar) disebut hypotenuse in a triangle. Dengan menggunakan kedua kaki yang diperkenalkan ini (c1 dan c2 pada gambar), kita dapat menghitung panjang sisi miring menggunakan teorema Pythagoras. Mari kita lihat bagaimana melakukannya. Rumus untuk panjang sisi miring (atau panjang garis) adalah sebagai berikut: 

=12+22


Cara mendapatkan kedua kaki juga dilihat dari segi tiga. Sekarang, menggunakan rumus di atas, kami menemukan sisi miring, yang akan menjadi garis panjang (jumlah piksel):

 drawLine(x1, y1, x2, y2, r, g, b) {
         const c1 = y2 - y1
         const c2 = x2 - x1

         const length = Math.sqrt(c1 * c1 + c2 * c2)

Kita sudah tahu berapa banyak piksel untuk menggambar untuk menggambar garis. Namun kami belum tahu bagaimana cara menggeser piksel. Artinya, kita perlu menggambar garis dari x1, y1 ke x2, y2, kita tahu bahwa panjang garis akan, misalnya, 20 piksel. Kita dapat menggambar piksel pertama dalam x1, y1 dan yang terakhir dalam x2, y2, tetapi bagaimana cara menemukan koordinat piksel menengah? Untuk melakukan ini, kita perlu mendapatkan cara menggeser setiap piksel berikutnya sehubungan dengan x1, y1 untuk mendapatkan garis yang diinginkan. Saya akan memberikan satu contoh lagi untuk lebih memahami perpindahan seperti apa yang sedang kita bicarakan. Kami memiliki poin [0, 0] dan [0, 3], kita perlu menggambar garis pada mereka. Dari contoh tersebut terlihat dengan jelas bahwa titik berikutnya setelah [0, 0] adalah [0, 1], dan kemudian [0, 2] dan akhirnya [0, 3]. Artinya, X dari setiap titik tidak bergeser, baik, atau kita dapat mengatakan bahwa itu digeser oleh 0 piksel, dan Y digeser oleh 1 piksel, ini adalah offset,dapat ditulis sebagai [0, 1]. Contoh lain: kita memiliki titik [0, 0] dan titik [3, 6], mari kita coba hitung dalam pikiran kita bagaimana mereka berubah, yang pertama adalah [0, 0], lalu [0,5, 1], lalu [1, 2] kemudian [1,5, 3] dan seterusnya ke [3, 6], dalam contoh ini offsetnya adalah [0,5, 1]. Bagaimana cara menghitungnya? 

Anda dapat menggunakan rumus berikut:

   = 2 /  
  Y = 1 /   

Dalam kode program, kita akan memiliki ini:

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

Semua data sudah ada di sana: panjang garis, offset piksel sepanjang X dan Y. Kita mulai dalam siklus untuk menggambar:

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

Sebagai koordinat X dari fungsi Pixel, kami melewatkan awal garis X + mengimbangi X * i, dengan demikian, mendapatkan koordinat piksel ke-i, kami juga menghitung koordinat Y. Math.trunc adalah metode dalam JS yang memungkinkan Anda membuang bagian fraksional dari suatu bilangan. Seluruh kode metode terlihat seperti ini:

drawLine(x1, y1, x2, y2, r, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * 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,
        )
    }
}

Bagian pertama telah berakhir, sebuah jalan panjang namun menarik untuk memahami dunia 3D. Belum ada yang tiga dimensi, tetapi kami melakukan operasi persiapan untuk menggambar: kami menerapkan fungsi menggambar pixel, garis, membersihkan jendela dan belajar beberapa istilah. Semua kode kelas Drawer dapat dilihat di bawah spoiler:

Kode kelas laci
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

    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, g, b) {
    const c1 = y2 - y1
    const c2 = x2 - x1

    const length = Math.sqrt(c1 * c1 + c2 * 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
    }
  }
}

Apa berikutnya?


Pada artikel selanjutnya, kita akan melihat bagaimana operasi sederhana seperti output dari pixel dan garis dapat berubah menjadi objek 3D yang menarik. Kami akan berkenalan dengan matriks dan operasi pada mereka, menampilkan objek tiga dimensi di jendela dan bahkan menambahkan animasi.

All Articles