HTML5 melakukan standarisasi pada tag <canvas>
dan mempopulerkan Canvas API untuk melakukan manipulasi gambar 2D. Selain Canvas API, <canvas>
juga dapat dipakai untuk menampilkan grafis 3D dengan menggunakan WebGL API. Karena WebGL cukup rumit dan berbeda jauh dari Canvas API, pada artikel ini saya akan berusaha menuliskan cara kerja WebGL dengan hanya memakai JavaScript biasa tanpa library eksternal sama sekali.
WebGL adalah salah satu implementasi OpenGL untuk dipakai pada browser tanpa driver atau plugin tambahan. OpenGL adalah API yang digunakan menghasilkan game 3D yang bersaing dengan Microsoft DirectX. Hampir semua game 3D desktop akan memakai salah satu dari OpenGL atau DirectX (yang hanya jalan di Windows). Yup! Game 3D akan memakainya secara langsung maupun tidak langsung (misalnya melalui engine seperti Unity). Seluruh graphic cards modern (sering juga disebut VGA card di pasaran) sudah mendukung OpenGL dan DirectX.
Dulu saat belajar computer graphics, saya menggunakan Visual C++ untuk memanggil OpenGL API secara langsung. Saya masih ingat betapa rumitnya membuat matrix hanya untuk sebuah efek sederhana. Walaupun memeras keringat, tapi ada rasa bangga saat berhasil menyelesaikan quiz-quiz yang ada (karena saya melakukannya tanpa Photoshop 🙂 ). Selain itu setidaknya saya menyadari bahwa mata kuliah aljabar linear bukan mata kuliah yang tak jelas manfaatnya karena ia sering kali diterapkan disini!
Salah satu subset dari OpenGL adalah Open GL ES (Embedded Systems) yang ditujukan untuk perangkat portable. Saya dapat menggunakan OpenGL ES API di Java ME melalui Java Binding for the OpenGL ES API (JSR 239). WebGL adalah API dibuat berdasarkan OpenGL ES API. Bahasa yang dipakai untuk mempogram WebGL adalah JavaScript.
JavaScript??? Sulit membayangkan membuat sebuah game 3D dengan JavaScript, tapi saat ini banyak game engine WebGL yang beredar untuk mempermudah penggunaan WebGL. Masalah lain adalah seluruh kode program JavaScript dapat dilihat dan dimodifikasi oleh pengguna. Ini tidak begitu diharapkan oleh developer game 🙂
Selain itu, walaupun browser mendukung WebGL API, belum tentu hardware di platform pengguna mendukung. Hal ini karena browser perlu mengakses langsung GPU (Graphics Processing Unit) di graphics card. Semua komputer pasti memiliki CPU (contoh merk: Intel atau AMD) untuk mengerjakan instruksi program sehari-hari. Tapi tidak semuanya dilengkapi dengan GPU (contoh merk: NVIDIA) yang mendukung OpenGL terbaru. GPU memiliki tugas mengerjakan instruksi khusus untuk grafis 3D yang membutuhkan presisi tinggi; dengan demikian CPU tidak akan begitu sibuk. Beberapa platform juga yang dilengkapi dengan PPU (Physics Processing Unit) yang akan mengerjakan instruksi untuk perhitungan fisika seperti fraktur, simulasi rambut, dan sebagainya sehingga CPU bisa lebih leluasa mengerjakan hal lainnya. Dan bagi seorang gamer seperti saya, impian utama adalah memiliki GPU terbaru yang dilengkapi PPU plus sebuah monitor HD resolusi tinggi 🙂
Bagaimana contoh penggunaan WebGL? Sebagai latihan, saya akan membuat sebuah halaman HTML sederhana yang melakukan inisialisasi WebGL seperti berikut ini:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<title>Latihan WebGL</title>
</head>
<body>
<canvas id='canvas' width='640' height='480'>
</canvas>
<script type="text/javascript">
var canvas = document.getElementById('canvas');
var context = canvas.getContext('webgl');
if (context) {
context.clearColor(0.0, 0.0, 0.0, 1.0);
context.clear(context.COLOR_BUFFER_BIT);
}
</script>
</body>
</html>
Bila saya menjalankan HTML di atas, saya hanya akan memperoleh kotak kosong. Tujuannya adalah memastikan WebGL dapat bekerja di browser saya.
Berikutnya, saya perlu menggambar sesuatu. Tapi ini tidak mudah! WebGL hanya bisa menggambar titik, baris, dan segitiga! Segitiga adalah yang paling sering dipakai pada grafis 3D. Saya kemudian bisa membuat berbagai bentuk lainnya berdasarkan segitiga,titik atau garis yang ada. Btw, ini adalah letak kesulitan utama dalam mempelajari atau memakai WebGL secara langsung: bingung cara menggambar sebuah bentuk dasar. Hal ini karena untuk menggambar bentuk yang paling dasar sekalipun, saya tidak sekedar memanggil satu atau dua function, tapi ada beberapa proses yang harus dilalui.
Apa yang saya gambar di layar akan diterjemahkan oleh shader menjadi titik dan warna. Dengan demikian, saya bisa memprogram shader untuk melakukan transformasi pada segitiga yang saya berikan. Btw, shader akan dikerjakan oleh GPU (bukan CPU). Pada WebGL, shader didefinisikan dengan menggunakan sebuah bahasa khusus yang disebut OpenGL ES Shading Language (GLSL). Tidak seluruh yang ada di GLSL resmi berlaku di WebGL, misalnya ftransform()
yang ada di spesifikasi GLSL tidak dijumpai di WebGL.
WebGL memiliki 2 jenis definisi shader yaitu fragment shader yang mewakili informasi warna dan vertex shader yang mewakili posisi hasil pemograman. Koordinat yang dapat digambar pada WebGL memiliki nilai minimal -1.0 hingga maksimal 1.0. Jadi, tidak peduli seberapa besar ukuran canvas, -1 adalah nilai minimal, 0 adalah nilai tengah, dan 1 adalah nilai maksimal. Koordinat di WebGL terdiri atas 3 sumbu: X, Y, dan Z.
Agar lebih jelas, saya akan membuat sebuah kode program sederhana seperti berikut ini:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<title>Latihan WebGL</title>
</head>
<body>
<canvas id='canvas' width='640' height='480'>
</canvas>
<script type="text/javascript">
// Inisialisasi WebGL
var canvas = document.getElementById('canvas');
var gl = canvas.getContext('webgl');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Inisialisasi shader
var vertexShaderText = 'attribute vec2 aVertexPosition; void main() { gl_Position = vec4(aVertexPosition, 0.0, 1.0); }';
var fragmentShaderText = 'void main() { gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); }';
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderText);
gl.compileShader(vertexShader);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderText);
gl.compileShader(fragmentShader);
// Inisialisasi program
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// Gambar segitiga
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 0,1, 1,1]), gl.STATIC_DRAW);
var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>
Pada HTML diatas, saya mendeklarasikan vertex shader dengan GLSL seperti berikut ini:
attribute vec2 aVertexPosition;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
}
Kode program di atas menerima input berupa posisi vertex dalam koordinat 2D (vec2
), lalu mengubahnya menjadi koordinat 3D (vec4
) yang ditampung pada gl_Position
. Nilai gl_Position
merupakan nilai hasil transformasi yang akan dipakai. Ia terdiri atas 4 nilai, yaitu koordinat X
, Y
, Z
, dan nilai proyeksi W
. Nilai W
adalah pembagi untuk seluruh X
, Y
, Z
yang ada. Pada GLSL di atas, posisi hasil transformasi adalah posisi X
dan Y
yang sama seperti pada yang diberikan, dengan posisi Z
berupa 0.0 dan nilai W
berupa 1.0 (tidak ada perubahan). Ini akan menghasilkan posisi persis seperti pada koordinat 2D.
Isi dari fragment shader pada HTML di atas adalah:
void main() {
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
Fragment shader di atas akan selalu mengembalikan nilai hijau (Red=0.0, Green=1.0, Blue=0.0, dan Alpha=1.0).
Saya menggambar segitiga berdasarkan deretan array [0,0, 0,1, 1,1]
yang merupakan koordinat 2D. Koordinat ini mewakili titik (0,0), (0,1) dan (1,1). Karena vertex shader tidak melakukan apa-apa dengan koordinat tersebut dan memberikan nilai koordinat Z=0, maka ia akan ditampilkan apa adanya tanpa kedalaman, seperti yang terlihat pada hasil berikut ini:

Tampilan WebGL dengan z=0
Sederhana, bukan? Tapi itu hanya segitiga. Bagaimana bila saya menginginkan bentuk lain? Segiempat, misalnya? Saya dapat menambah sebuah segitiga lagi seperti pada kode program berikut ini:
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 0,1, 1,1, 1,1, 1,0, 0,0]), gl.STATIC_DRAW);
var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
Bila saya menampilkan HTML ini di browser, saya akan memperoleh sebuah segiempat yang terdiri atas 2 segitiga seperti yang terlihat pada gambar berikut ini:

Membuat kotak berdasarkan dua segitiga
Berikutnya, saya akan memodifikasi shader akan memberikan warna bervariasi yang merupakan hasil interpolasi. Tapi sebelumnya, kode program GLSL yang diletakkan dalam bentuk string di kode program JavaScript akan sangat sulit dibaca dan ditulis. Oleh sebab itu, saya dapat meletakkan kode program GLSL pada <script>
dengan memakai atribut type
yang unik sehingga browser tidak akan mengerjakannya sebagai JavaScript. Setelah itu, saya dapat membaca isi teks dari <script>
melalui DOM API seperti biasa (memanggil atribut text
).
Sebagai latihan, saya menambahkan pewarnaan sehingga kode program saya yang baru akan terlihat seperti berikut ini:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<title>Latihan WebGL</title>
<script id='latihan-vertex' type='x-shader/x-vertex'>
attribute vec2 aVertexPosition;
attribute vec4 aVertexColor;
varying lowp vec4 vColor;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1);
vColor = aVertexColor;
}
</script>
<script id='latihan-fragment' type='x-shader/x-fragment'>
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}
</script>
</head>
<body>
<canvas id='canvas' width='640' height='480'>
</canvas>
<script type="text/javascript">
// Inisialisasi WebGL
var canvas = document.getElementById('canvas');
var gl = canvas.getContext('webgl');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Inisialisasi shader
var vertexShaderText = document.getElementById('latihan-vertex').text;
var fragmentShaderText = document.getElementById('latihan-fragment').text;
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderText);
gl.compileShader(vertexShader);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderText);
gl.compileShader(fragmentShader);
// Inisialisasi program
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// Atur warna
var warna = [1.0, 1.0, 1.0, 1.0,
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0,
0.0, 1.0, 0.0, 1.0];
var bufferWarna = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferWarna);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(warna), gl.STATIC_DRAW);
var aVertexColor = gl.getAttribLocation(program, "aVertexColor");
gl.enableVertexAttribArray(aVertexColor);
gl.vertexAttribPointer(aVertexColor, 4, gl.FLOAT, false, 0, 0);
// Gambar Dua Segitiga
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 0,1, 1,1, 1,1, 1,0, 0,0]), gl.STATIC_DRAW); var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
</script>
</body>
</html>
Bila saya menampilkan HTML tersebut, saya akan memperoleh hasil seperti pada gambar berikut ini:

Memberikan warna pada setiap vertex
Pada GLSL shader di atas, saya mendefinisikan variabel vColor
dengan qualifier varying
. Variabel varying
hanya bisa di-isi di vertex shader dan bersifat read-only di fragment shader dimana ia merupakan hasil interpolasi relatif terhadap vertex.
Tipe vColor
adalah vec4
yang menunjukkan bahwa ia adalah sebuah vector yang terdiri atas 4 nilai. Vertex shader akan mengisi nilai vColor
berdasarkan urutan yang di array warna
. Atau dengan kata lain, setiap 4 elemen di array warna
adalah sebuah warna bagi salah satu titik segitiga (yang disimpan di buffer
).
Selanjutnya, saya akan mengubah kode program di atas untuk membuat sebuah benda 3D yang terdiri atas dua sisi, menjadi seperti berikut ini:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<title>Latihan WebGL</title>
<script id='latihan-vertex' type='x-shader/x-vertex'>
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
varying lowp vec4 vColor;
void main() {
gl_Position = vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
}
</script>
<script id='latihan-fragment' type='x-shader/x-fragment'>
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
}
</script>
</head>
<body>
<canvas id='canvas' width='640' height='480'>
</canvas>
<script type="text/javascript">
// Inisialisasi WebGL
var canvas = document.getElementById('canvas');
var gl = canvas.getContext('webgl', {antialias: true});
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Inisialisasi shader
var vertexShaderText = document.getElementById('latihan-vertex').text;
var fragmentShaderText = document.getElementById('latihan-fragment').text;
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderText);
gl.compileShader(vertexShader);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderText);
gl.compileShader(fragmentShader);
// Inisialisasi program
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// Atur warna
var warna = [
0.5, 0.0, 0.0, 1.0,
0.5, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
0.5, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.5, 0.0, 1.0,
0.0, 0.5, 0.0, 1.0,
0.0, 0.5, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
];
var bufferWarna = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferWarna);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(warna), gl.STATIC_DRAW);
var aVertexColor = gl.getAttribLocation(program, "aVertexColor");
gl.enableVertexAttribArray(aVertexColor);
gl.vertexAttribPointer(aVertexColor, 4, gl.FLOAT, false, 0, 0);
// Gambar kubus
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
// sisi depan
-0.5, -0.5, 1.0,
-0.5, 0.5, 1.0,
0.0, 0.5, 1.0,
0.0, 0.5, 1.0,
0.0, -0.5, 1.0,
-0.5, -0.5, 1.0,
// sisi samping
0.0, 0.5, 1.0,
0.0, - 0.5, 1.0,
0.25, -0.25, -1.0,
0.25, -0.25, -1.0,
0.25, 0.75, -1.0,
0.0, 0.5, 1.0
]), gl.STATIC_DRAW);
var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 12);
</script>
</body>
</html>
Bila saya menjalankan program di atas, saya akan memperoleh hasil seperti pada gambar berikut ini:

Membuat bentuk 3D
Kali ini pada vertex shader, saya menerima aVertexPosition
dalam bentuk vec3
dalam koordinat XYZ sehingga saya bisa membuat bentuk 3D secara leluasa. Saya mendefinisikan 12 vertex yang ditampung oleh buffer
. Masing-masing vertex ini memiliki warna sesuatu dengan yang didefinisikan di warna
. Kali ini saya tidak acak memberikan warna sehingga hasilnya lebih rapi.
WebGL API termasuk API low-level tanpa banyak pernak-pernik tambahan seperti kamera dan pencahayaan (lightning). Saya perlu menghitung semua yang butuhkan bila hanya WebGL API. Oleh sebab itu, pada proyek yang lebih serius, saya perlu memakai library atau game engine WebGL yang lebih mudah dipakai dibandingkan dengan mengimplementasikan semuanya sendiri.
Sebagai informasi tambahan, Firefox dilengkapi dengan sebuah shader editor yang memudahkan pengguna untuk mengamati shader yang ada pada HTML yang sedang dibuka. Untuk itu, saya perlu memilih menu Developer, Web Console. Shader editor tidak aktif secara default sehingga saya perlu mengaktifkannya secara manual dengan men-klik Toolbox Options dan memberi tanda centang pada Shader Editor seperti pada gambar berikut ini:

Mengaktifkan shader editor
Setelah itu, saya dapat memantau dan memodifikasi shader secara dinamis untuk halaman yang sedang dibuka seperti yang terlihat pada gambar berikut ini:

Shader editor di Firefox
Saya menemukan bahwa perubahan pada GLSL tidak memiliki efek, walaupun pada dokumentasi resminya, Firefox menyebutkan bahwa fitur shader editor dapat memperbaharui perubahan secara langsung. Mungkin ini tidak berlaku untuk semua kondisi.