Memahami Cara Kerja WebGL API


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

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

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

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

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

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

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.

Perihal Solid Snake
I'm nothing...

3 Responses to Memahami Cara Kerja WebGL API

  1. Rhomy Prama Dhieka mengatakan:

    artikel menarik.
    tapi saya masih belum begitu paham dengan web GL.

    untuk mnggunakan web GL, hanya perlu webbrowser yang mndukung ya?
    mksudnya tdak diperlukan install2 apa lah gt..

    • Solid Snake mengatakan:

      Secara software, tidak perlu install yang lain hanya dibutuhkan browser yang mendukung. Secara hardware, dibutuhkan GPU yang mendukung. Pada komputer modern, GPU biasanya sudah built-in dengan CPU, tapi gamer biasanya membeli Graphic Cards eksternal yang memiliki GPU yang lebih cepat.

  2. Taufiq Hidayat mengatakan:

    Makacih gan, akyu jadi dikit ngert, tangkyu!

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: