WebGL adalah API JavaScript untuk menggambar di <canvas> memakai GPU, chip grafis di laptop dan HP. Bayangin cara Raka menyablon: sekali tarik rakel, tinta melewati screen dan menempel di ribuan titik kain sekaligus. Screen menentukan di mana tinta lewat, tintanya menentukan warna apa. GPU bekerja begitu: satu perintah gambar, lalu program kecil yang sama dijalankan untuk ribuan pixel bersamaan. Program kecil itu namanya shader, dan di WebGL kamu yang menulisnya.
Raka ingin pembeli bisa memutar mug sablon Kopi dalam 3D di tokonya. Sebelum ke three.js, kita lihat dulu apa yang terjadi di GPU dengan bentuk paling dasar: satu segitiga.
🧪 Tebakan cepat: sebuah segitiga punya tiga titik sudut: coklat
#c47a3ddi atas, krem#f5e6c8di kiri bawah, hijau#7fb069di kanan bawah. Pixel tepat di tengahnya (titik berat segitiga) berwarna apa? Tebak dulu, jawabannya ada di bagian "Attribute, uniform, varying".
WebGL 2 dan GPU
GPU punya ribuan inti kecil yang menjalankan program yang sama di banyak data sekaligus. WebGL 2 adalah OpenGL ES 3.0 untuk browser:
const gl = canvas.getContext('webgl2');
if (!gl) throw new Error('Browser ini belum mendukung WebGL 2');
WebGL 2 ada sejak Chrome 56, Firefox 51 dan Safari 15, sekitar 96% pengguna (caniuse, Oktober 2026). getContext mengembalikan null kalau gak didukung atau GPU-nya diblokir, jadi selalu cek.
| Canvas 2D | WebGL 2 | |
|---|---|---|
| Cara menggambar | panggil fillRect, arc, browser yang mengerjakan |
tulis shader sendiri, GPU menjalankannya |
| Warna tiap pixel ditentukan | browser | fragment shader-mu |
| 3D | gak ada | ya, kamu hitung sendiri posisinya |
| Kode untuk satu bentuk | 3 baris | puluhan baris |
Canvas 2D adalah API 2D tingkat tinggi. Browser bisa saja memakai GPU di belakangnya (Chrome dan Safari sering begitu), tapi kamu gak bisa menulis program untuk GPU. Di WebGL kamu bisa, dan itu yang membuat 3D dan efek berat mungkin.
Pipeline: dari titik ke pixel
Satu gl.drawArrays(gl.TRIANGLES, 0, 3) di canvas 300 × 300 melewati empat tahap:
| Tahap | Berjalan | Tugasnya |
|---|---|---|
| 1. Vertex buffer | sekali diisi | menyimpan data tiap titik (posisi, warna) di memori GPU |
| 2. Vertex shader | 3 kali, sekali per titik | menentukan posisi akhir titik itu |
| 3. Rasteriser | sekali, otomatis | mencari pixel mana yang ada di dalam segitiga: 14.850 pixel |
| 4. Fragment shader | 14.850 kali, sekali per pixel | menentukan warna pixel itu |
Kandidat pixel yang dihasilkan rasteriser disebut fragment. Angka 14.850 bukan tebakan: luas segitiganya 16,5% dari canvas, dan 16,5% × 90.000 = 14.850, sama dengan hitungan readPixels di Lab. Di canvas 600 × 600 (layar DPR 2) fragment shader jalan 59.400 kali, empat kali lipat. Rasteriser adalah satu-satunya tahap yang gak kamu tulis.
Clip space
Vertex shader gak bicara pixel. Ia mengisi gl_Position dalam clip space: x dan y dari -1 sampai 1, (0, 0) di tengah canvas, dan y ke atas, kebalikan dari Canvas 2D. Mengubahnya ke pixel:
pixel x = (x + 1) / 2 × lebar
pixel y = (1 - y) / 2 × tinggi
Titik atas segitiga (0; 0,6) di canvas 300 × 300 jatuh di pixel (150, 60). Titik di luar -1 sampai 1 gak error: bagian segitiga yang keluar dipotong (clipping), dan sisanya tetap digambar.
Segitiga pertama, kode lengkap
<canvas id="gpu" width="300" height="300"></canvas>
const canvas = document.getElementById('gpu');
const gl = canvas.getContext('webgl2', { antialias: false });
if (!gl) throw new Error('Browser ini belum mendukung WebGL 2');
// Vertex shader: jalan sekali per titik
const vsSource = `#version 300 es
in vec2 aPosisi;
in vec3 aWarna;
out vec3 vWarna;
void main() {
vWarna = aWarna;
gl_Position = vec4(aPosisi, 0.0, 1.0);
}`;
// Fragment shader: jalan sekali per pixel
const fsSource = `#version 300 es
precision highp float;
in vec3 vWarna;
out vec4 warnaAkhir;
void main() {
warnaAkhir = vec4(vWarna, 1.0);
}`;
function buatShader(tipe, sumber) {
const shader = gl.createShader(tipe);
gl.shaderSource(shader, sumber);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
return shader;
}
const program = gl.createProgram();
gl.attachShader(program, buatShader(gl.VERTEX_SHADER, vsSource));
gl.attachShader(program, buatShader(gl.FRAGMENT_SHADER, fsSource));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(program));
}
// Tiap titik: x, y (clip space), lalu r, g, b (0 sampai 1)
const data = new Float32Array([
0.0, 0.6, 0.769, 0.478, 0.239, // atas: COKLAT #c47a3d
-0.6, -0.5, 0.961, 0.902, 0.784, // kiri bawah: KREM #f5e6c8
0.6, -0.5, 0.498, 0.690, 0.412, // kanan bawah: HIJAU #7fb069
]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const lokPosisi = gl.getAttribLocation(program, 'aPosisi');
gl.enableVertexAttribArray(lokPosisi);
gl.vertexAttribPointer(lokPosisi, 2, gl.FLOAT, false, 20, 0);
const lokWarna = gl.getAttribLocation(program, 'aWarna');
gl.enableVertexAttribArray(lokWarna);
gl.vertexAttribPointer(lokWarna, 3, gl.FLOAT, false, 20, 8);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.11, 0.098, 0.09, 1); // hitam kaos #1c1917
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.drawArrays(gl.TRIANGLES, 0, 3);
- Shader ditulis dalam GLSL ES 3.00 (OpenGL ES Shading Language), bahasa mirip C, sebagai string.
#version 300 eswajib jadi baris pertama, dan fragment shader wajib menyebutprecision. buatShadermengompilasi satu shader di GPU,linkProgrammenyatukan vertex dan fragment shader jadi satu program.Float32Arrayadalah data mentah: 3 titik × 5 angka.bufferDatamengirimnya ke memori GPU.vertexAttribPointer(lokasi, jumlah, tipe, normalisasi, stride, offset): tiap titik selebar 5 float × 4 byte = 20 byte (stride); posisi mulai di byte 0, warna di byte 8 (setelah 2 float). VAO (vertex array object) mengingat pengaturan ini.drawArrays(gl.TRIANGLES, 0, 3): gambar segitiga dari 3 titik mulai titik ke-0. Di sinilah pipeline berjalan.
Attribute, uniform, varying
Ada tiga cara data masuk ke shader:
| Jenis | Di GLSL ES 3.00 | Nilainya | Contoh |
|---|---|---|---|
| attribute | in di vertex shader |
beda di tiap titik, dari buffer | aPosisi, aWarna |
| uniform | uniform |
sama untuk semua titik dan pixel di satu gambar | warna tunggal, waktu, matriks kamera |
| varying | out di vertex shader, in di fragment shader |
dicampur (interpolasi) GPU untuk tiap pixel | vWarna |
WebGL 1 menulisnya attribute dan varying; GLSL ES 3.00 memakai in dan out. Rasteriser mencampur varying menurut jarak pixel ke tiap titik. Di titik berat, ketiga titik berbobot sama, 1/3: merah (196 + 245 + 127) / 3 = 189, hijau (122 + 230 + 176) / 3 = 176, biru (61 + 200 + 105) / 3 = 122. Pixel tengahnya #bdb07a, krem kecoklatan. Itu jawaban tebakannya; Chrome, Firefox dan Safari sama.
Mau satu warna untuk seluruh segitiga? Itu uniform:
// fragment shader: uniform vec3 uWarna; ... warnaAkhir = vec4(uWarna, 1.0);
gl.useProgram(program);
gl.uniform3f(gl.getUniformLocation(program, 'uWarna'), 0.769, 0.478, 0.239);
Kenapa three.js ada
Kode di atas 55 baris (tanpa baris kosong dan komentar) untuk satu segitiga datar. Versi three.js:
import * as THREE from 'three';
const canvas = document.getElementById('gpu');
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
scene.background = new THREE.Color('#1c1917');
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute([0, 0.6, 0, -0.6, -0.5, 0, 0.6, -0.5, 0], 3));
const warna = ['#c47a3d', '#f5e6c8', '#7fb069'].flatMap((hex) => new THREE.Color(hex).toArray());
geometry.setAttribute('color', new THREE.Float32BufferAttribute(warna, 3));
scene.add(new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ vertexColors: true })));
renderer.render(scene, camera);
12 baris, dan shader, buffer, VAO dan program dibuatkan three.js (versi 0.186). Segitiganya menutup 14.850 pixel yang sama; tengahnya sedikit lebih terang (#c5b78a) karena three.js mencampur warna di ruang warna linear, yang dibahas di lesson Geometri & Material. Selisihnya melebar untuk mug 3D: kamera perspektif, matriks putar, normal permukaan, rumus cahaya dan tekstur. Di WebGL langsung itu ratusan baris; di three.js beberapa baris per hal.
WebGPU hari ini
WebGPU adalah penerus WebGL: API yang lebih dekat ke GPU modern, dengan shader dalam WGSL (WebGPU Shading Language), bukan GLSL. Statusnya per Oktober 2026 (wiki implementasi gpuweb):
| Browser | Aktif tanpa setelan |
|---|---|
| Chrome dan Edge | Windows, macOS, ChromeOS sejak 113; Android 12+ sejak 121. Linux masih di balik flag |
| Firefox | Windows sejak 141; macOS Apple Silicon sejak 145. Linux, Mac Intel dan Android masih dikembangkan |
| Safari | macOS, iOS, iPadOS dan visionOS 26 |
Sekitar 86% pengguna (caniuse), lebih sedikit dari WebGL 2. three.js punya WebGPURenderer yang otomatis turun ke WebGL 2 kalau WebGPU gak ada. Konsepnya sama: buffer, vertex shader, rasteriser, fragment shader.
⚠️ Jebakan yang sering ketemu
- Baris kosong sebelum
#version 300 es. Template string yang dimulai dengan enter membuat versinya di baris 2:#version directive must occur on the first line of the shader. Tulis`#version 300 eslangsung setelah backtick. - Lupa
precision highp float;di fragment shader:No precision specified for (float). Vertex shader punya precision bawaan, fragment shader gak. - Error shader gak melempar exception. Tanpa cek
COMPILE_STATUSdangetShaderInfoLog, yang kamu dapat cuma canvas kosong. - y ke atas, dan stride dalam byte. (0; 0,6) ada di atas tengah, bukan di bawah. Stride 5 float ditulis 20, bukan 5.
TL;DR: WebGL 2 (OpenGL ES 3.0, getContext('webgl2'), cek null) menggambar dengan GPU lewat shader yang kamu tulis dalam GLSL ES 3.00. Pipeline: vertex buffer → vertex shader (sekali per titik, mengisi gl_Position di clip space -1 sampai 1, y ke atas) → rasteriser (mencari pixel di dalam bentuk) → fragment shader (sekali per pixel, menentukan warna). Segitiga di canvas 300 × 300: vertex shader 3 kali, fragment shader 14.850 kali. Attribute beda per titik, uniform sama untuk semua, varying dicampur per pixel (tengah segitiga coklat, krem, hijau = #bdb07a). Satu segitiga butuh 55 baris WebGL atau 12 baris three.js. WebGPU (WGSL) aktif di Chrome, Edge, Safari 26 dan Firefox di Windows dan Mac Apple Silicon, belum di semua Linux.