Minimal WebGL dalam 75 baris kode

OpenGL modern, dan lebih luas lagi WebGL, sangat berbeda dari OpenGL lama yang pernah saya pelajari sebelumnya. Saya memahami cara kerja rasterisasi, jadi saya cukup paham dengan konsepnya. Namun, setiap tutorial yang saya baca menawarkan abstraksi dan fungsi pembantu yang membuat saya lebih sulit untuk memahami bagian mana yang termasuk dalam OpenGL API itu sendiri.



Untuk memperjelas, abstraksi seperti membagi posisi ini dan fungsi rendering ke dalam kelas-kelas terpisah penting dalam aplikasi dunia nyata. Namun, abstraksi ini menyebarkan kode di berbagai area dan menambahkan redundansi karena boilerplate dan transfer data antar unit logis. Saya merasa paling nyaman untuk mempelajari topik pada aliran kode linier, di mana setiap baris terkait langsung dengan topik ini.



Pertama, saya perlu berterima kasih kepada pembuat tutorial yang saya gunakan . Mengambilnya sebagai dasar, saya menyingkirkan semua abstraksi sampai saya mendapatkan "program minimal yang layak". Saya harap ini membantu Anda memulai OpenGL modern. Inilah yang akan kami lakukan:





Segitiga sama sisi, hijau di atas, hitam di kiri bawah, dan merah di kanan bawah, dengan warna interpolasi di antara titik-titiknya. Versi segitiga hitam yang sedikit lebih cerah [ terjemahan dalam bahasa HabrΓ©].



Inisialisasi



Di WebGL, kita perlu canvasmenggambar. Tentu saja, Anda pasti perlu menambahkan semua boilerplate HTML biasa, gaya, dll., Tetapi kanvas adalah hal yang paling penting. Setelah DOM dimuat, kita dapat mengakses kanvas menggunakan Javascript.



<canvas id="container" width="500" height="500"></canvas>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    // All the Javascript code below goes here
  });
</script>


Dengan mengakses kanvas, kita bisa mendapatkan konteks rendering WebGL dan menginisialisasi warna beningnya. Warna di dunia OpenGL disimpan sebagai RGBA dan setiap komponen memiliki nilai dari 0hingga 1. Warna bening adalah warna yang digunakan untuk menggambar kanvas di awal setiap bingkai, menggambar ulang pemandangan.



const canvas = document.getElementById('container');
const gl = canvas.getContext('webgl');

gl.clearColor(1, 1, 1, 1);


Dalam program nyata, inisialisasi dapat dan harus lebih detail. Secara khusus, perlu disebutkan tentang penyangga kedalaman yang memungkinkan Anda mengurutkan geometri berdasarkan koordinat Z. Kami tidak akan melakukan ini untuk program sederhana yang hanya terdiri dari satu segitiga.



Mengompilasi shader



Pada intinya, OpenGL adalah kerangka kerja rasterisasi di mana kita harus memutuskan bagaimana menerapkan segala sesuatu selain rasterisasi. Oleh karena itu, setidaknya dua tahap kode harus dijalankan di GPU:



  1. Sebuah vertex shader yang memproses semua data yang masuk dan mengeluarkan satu posisi 3D (sebenarnya adalah posisi 4D dalam koordinat seragam ) untuk setiap masukan.
  2. Fragmen shader yang memproses setiap piksel di layar, merender warna piksel yang akan dicat.


Di antara dua tahap ini, OpenGL mendapatkan geometri dari vertex shader dan menentukan piksel layar mana yang dicakup oleh geometri itu. Ini adalah tahap rasterisasi.



Kedua shader biasanya ditulis dalam GLSL (OpenGL Shading Language), yang kemudian dikompilasi menjadi kode mesin untuk GPU. Kode mesin kemudian diteruskan ke GPU sehingga dapat dijalankan selama proses rendering. Saya tidak akan membahas GLSL secara mendetail karena saya hanya ingin menunjukkan yang paling dasar, tetapi bahasanya cukup dekat dengan C agar familiar bagi sebagian besar pemrogram.



Pertama, kami mengompilasi dan meneruskan shader vertex ke GPU. Dalam fragmen yang ditunjukkan di bawah ini, kode sumber shader disimpan sebagai string, tetapi dapat dimuat dari tempat lain. Akhirnya, string tersebut diteruskan ke API WebGL.



const sourceV = `
  attribute vec3 position;
  varying vec4 color;

  void main() {
    gl_Position = vec4(position, 1);
    color = gl_Position * 0.5 + 0.5;
  }
`;

const shaderV = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shaderV, sourceV);
gl.compileShader(shaderV);

if (!gl.getShaderParameter(shaderV, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderV));
  throw new Error('Failed to compile vertex shader');
}


Ada baiknya menjelaskan beberapa variabel dalam kode GLSL di sini:



  1. (attribute) position. , , .
  2. Varying color. ( ) . .
  3. gl_Position. , , varying-. , ,


Ada juga tipe variabel seragam , yang merupakan konstanta di semua panggilan shader vertex. Seragam semacam itu digunakan untuk properti seperti matriks transformasi, yang akan konstan untuk semua simpul dari satu elemen geometris.



Selanjutnya, kami melakukan hal yang sama dengan shader fragmen - kami mengompilasinya dan mentransfernya ke GPU. Perhatikan bahwa variabel colordari shader vertex sekarang dibaca oleh shader fragmen.



const sourceF = `
  precision mediump float;
  varying vec4 color;

  void main() {
    gl_FragColor = color;
  }
`;

const shaderF = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shaderF, sourceF);
gl.compileShader(shaderF);

if (!gl.getShaderParameter(shaderF, gl.COMPILE_STATUS)) {
  console.error(gl.getShaderInfoLog(shaderF));
  throw new Error('Failed to compile fragment shader');
}


Selanjutnya, shader vertex dan fragmen ditautkan ke dalam satu program OpenGL.



const program = gl.createProgram();
gl.attachShader(program, shaderV);
gl.attachShader(program, shaderF);
gl.linkProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error(gl.getProgramInfoLog(program));
  throw new Error('Failed to link program');
}

gl.useProgram(program);


Kami memberi tahu GPU bahwa kami ingin menjalankan shader di atas. Sekarang kita hanya perlu membuat data yang masuk dan membiarkan GPU memproses data ini.



Mengirim data yang masuk ke GPU



Data yang masuk akan disimpan di memori GPU dan diproses dari sana. Alih-alih membuat panggilan gambar terpisah untuk setiap bagian data yang masuk, yang mentransfer data yang sesuai satu bagian dalam satu waktu, semua data yang masuk dikirim secara keseluruhan ke dan dibaca dari GPU. (OpenGL lama meneruskan data pada elemen individu, yang memperlambat kinerja.)



OpenGL menyediakan abstraksi yang disebut Vertex Buffer Object (VBO). Saya masih mencari tahu cara kerjanya, tetapi pada akhirnya kami akan melakukan hal berikut untuk menggunakannya:



  1. Simpan urutan data di memori central processing unit (CPU).
  2. Transfer byte ke memori GPU melalui buffer unik yang dibuat dengan gl.createBuffer()dan titik jangkar gl.ARRAY_BUFFER .


Untuk setiap variabel input data (atribut) di vertex shader, kita akan memiliki satu VBO, meskipun dimungkinkan untuk menggunakan satu VBO untuk beberapa elemen data input.



const positionsData = new Float32Array([
  -0.75, -0.65, -1,
   0.75, -0.65, -1,
   0   ,  0.65, -1,
]);

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positionsData, gl.STATIC_DRAW);


Biasanya kita mendefinisikan geometri dengan koordinat apa pun yang dipahami aplikasi kita, dan kemudian menggunakan satu set transformasi dalam shader vertex untuk memetakannya ke dalam ruang klip OpenGL. Saya tidak akan menjelaskan secara rinci tentang ruang pemotongan (ini terkait dengan koordinat homogen), sedangkan Anda hanya perlu mengetahui bahwa X dan Y berubah dalam rentang dari -1 hingga +1. Karena vertex shader hanya meneruskan input apa adanya, kita dapat mengatur koordinat kita secara langsung di ruang kliping.



Kemudian kami juga akan mengikat buffer ke salah satu variabel di shader vertex. Dalam kode, kami melakukan hal berikut:



  1. Kami mendapatkan deskriptor variabel positiondari program yang dibuat di atas.
  2. Kami menginstruksikan OpenGL untuk membaca data dari titik jangkar gl.ARRAY_BUFFERdalam kelompok 3 dengan parameter tertentu, misalnya, dengan offset dan langkah 0.




const attribute = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(attribute);
gl.vertexAttribPointer(attribute, 3, gl.FLOAT, false, 0, 0);


Perlu dicatat bahwa kita dapat membuat VBO dengan cara ini dan mengikatnya ke atribut vertex shader karena kita menjalankan fungsi ini satu demi satu. Jika kita akan memisahkan dua fungsi (misalnya, membuat semua VBO dalam satu lintasan, dan kemudian mengikatnya ke atribut terpisah), maka sebelum memetakan setiap VBO ke atribut yang sesuai, kita perlu memanggil setiap kali gl.bindBuffer(...).



Merender!



Terakhir, ketika semua data dalam memori GPU disiapkan sesuai kebutuhan, kita dapat memberi tahu OpenGL untuk menghapus layar dan menjalankan program untuk memproses array yang telah kita siapkan. Sebagai bagian dari langkah rasterisasi (menentukan piksel mana yang dicakup oleh simpul), kami memberi tahu OpenGL untuk memperlakukan simpul dalam kelompok 3 sebagai segitiga.



gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);


Dengan skema linier seperti itu, program akan dijalankan sekaligus. Dalam aplikasi praktis apa pun, kami akan menyimpan data dengan cara terstruktur, mengirimkannya ke GPU saat berubah, dan merendernya setiap frame.






Untuk meringkas, di bawah ini adalah diagram dengan sekumpulan konsep minimal yang diperlukan untuk menampilkan segitiga pertama kita di layar. Tetapi bahkan skema ini sangat disederhanakan, jadi yang terbaik adalah menulis 75 baris kode yang disajikan dalam artikel ini dan mempelajarinya.





Urutan terakhir yang sangat disederhanakan dari langkah-langkah yang diperlukan untuk menampilkan sebuah segitiga.



Bagi saya, bagian tersulit dari mempelajari OpenGL adalah banyaknya boilerplate yang diperlukan untuk menampilkan gambar paling sederhana di layar. Karena kerangka kerja rasterisasi mengharuskan kami menyediakan fungsionalitas rendering 3D, dan komunikasi dengan GPU sangat besar, banyak konsep yang harus dipelajari secara langsung. Semoga artikel ini menunjukkan kepada Anda dasar-dasarnya dengan cara yang lebih sederhana daripada yang terlihat di tutorial lain.



Lihat juga:








Lihat juga:






All Articles