three.js adalah library JavaScript yang membuat WebGL bisa dipakai tanpa menulis shader dan buffer sendiri: kamu cukup bilang "ada kotak di sini, kamera di sana", dan three.js yang menyiapkan semuanya untuk GPU. Bayangin foto produk di studio Raka. Ada meja tempat produk ditata, ada kamera dengan lensa dan jaraknya, dan ada fotografer yang menekan tombol sehingga jadilah foto. Di three.js, mejanya adalah Scene, kameranya Camera, dan fotografernya Renderer, yang "memotret" scene lewat kamera ke sebuah <canvas>. Kurang satu saja, gak ada gambar.
Di lesson ini kita memasang panggung 3D pertama di toko Raka: kotak kemasan Kaos Sablon Kopi yang berputar.
🧪 Tebakan cepat: wadah 3D di toko Raka menyempit dari 320 jadi 160 px (tingginya tetap 240). Kode resize mengubah
camera.aspectdanrenderer.setSize, tapi lupa memanggilcamera.updateProjectionMatrix(). Kotaknya tampil normal, melar, atau gepeng? Tebak dulu, jawabannya ada di bagian "Resize".
Pasang three.js
Versi terbaru di npm per Oktober 2026 adalah 0.186.1 (r186). Dengan bundler seperti Vite:
npm install three
import * as THREE from 'three';
Tanpa bundler, dokumentasi three.js memakai import map di <head>, supaya browser tahu dari mana 'three' diambil:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<script type="module" src="main.js"></script>
three/addons/berisi tambahan sepertiOrbitControls, diimpor dari'three/addons/controls/OrbitControls.js'.- Kodenya harus
type="module", dan halamannya dibuka lewat server lokal (npx viteataunpx serve .), bukan dengan klik dua kali file HTML.
Scene, kamera, renderer
<div id="toko-3d" style="width: 320px; height: 240px"></div>
import * as THREE from 'three';
const wadah = document.getElementById('toko-3d');
// 1. Scene: panggung tempat semua benda diletakkan
const scene = new THREE.Scene();
scene.background = new THREE.Color('#1c1917');
// 2. Kamera: fov 75°, aspect lebar / tinggi, near 0.1, far 4
const camera = new THREE.PerspectiveCamera(75, wadah.clientWidth / wadah.clientHeight, 0.1, 4);
camera.position.z = 2;
// 3. Renderer: menggambar ke <canvas> miliknya sendiri
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(wadah.clientWidth, wadah.clientHeight);
wadah.appendChild(renderer.domElement);
// Kotak kemasan Kaos Sablon Kopi: 1,6 × 1 × 0,4
const kotak = new THREE.Mesh(
new THREE.BoxGeometry(1.6, 1, 0.4),
new THREE.MeshNormalMaterial()
);
kotak.rotation.x = 0.4;
scene.add(kotak);
scene.add(kotak)memasukkan kotak kescene.children. Dua kaliadd,scene.children.lengthjadi 2.PerspectiveCamera(fov, aspect, near, far): fov adalah sudut pandang vertikal dalam derajat; aspect adalah lebar dibagi tinggi tempat gambarnya (320 / 240 = 1,33); benda yang lebih dekat dari near atau lebih jauh dari far gak digambar. Kamera ini meniru mata: benda jauh terlihat lebih kecil.- Kamera dan benda sama-sama mulai di titik (0, 0, 0), dan kamera melihat ke arah z negatif.
camera.position.z = 2memundurkan kamera supaya kotaknya ada di depannya. setPixelRatio+setSizeadalah trik DPR dari lesson Canvas API Dasar:setSize(320, 240)di DPR 2 membuat canvas 640 × 480 pixel dengan ukuran tampil 320 × 240. Dibatasi 2 karena DPR 3 berarti 2,25 kali pixel DPR 2, dan fragment shader jalan sekali per pixel.renderer.domElementadalah<canvas>buatan renderer. TanpaappendChild, canvas-nya ada tapi gak pernah masuk halaman.- Benda yang tampil adalah Mesh = geometry (bentuk: 24 titik, 12 segitiga untuk kotak) + material (tampilan).
MeshNormalMaterialmewarnai tiap sisi menurut arah hadapnya dan gak butuh cahaya, jadi pas untuk mengecek setup.
Seberapa besar yang terlihat
Tinggi dunia yang terlihat di jarak d adalah 2 × d × tan(fov / 2). Kotak setinggi 1 di jarak 2, di canvas setinggi 240 px:
| fov | Tinggi terlihat | Kotak mengisi | Kira-kira |
|---|---|---|---|
| 30° | 1,07 | 93% | 224 px, seperti lensa tele |
| 75° | 3,07 | 33% | 78 px |
| 110° | 5,71 | 18% | 42 px, seperti lensa lebar |
fov adalah parameter yang paling mengubah tampilan: makin besar, makin banyak yang masuk, makin kecil tiap benda. far di sini sengaja 4, cukup untuk kotak di jarak 2: dorong kamera ke z = 4, dan separuh belakang kotak lewat dari far, jadi terpotong.
Render loop
Satu renderer.render(scene, camera) menghasilkan satu foto. Supaya bergerak, render tiap frame:
renderer.setAnimationLoop((time) => {
kotak.rotation.y = time / 1000; // 1 radian per detik
renderer.render(scene, camera);
});
setAnimationLoopmemakairequestAnimationFramedi dalamnya (lesson Animasi Canvas): jalan sekali per refresh layar dan berhenti saat tab disembunyikan.timeadalah milidetik sejak halaman dibuka.- Sudut dihitung dari waktu, jadi kecepatannya sama di layar 60 Hz dan 120 Hz.
kotak.rotation.y += 0.01per frame juga menumpuk, tapi dua kali lebih cepat di 120 Hz. (THREE.Clocksudah deprecated sejak r183; pakaitimeini atauTHREE.Timer.) renderer.setAnimationLoop(null)menghentikannya. Untukprefers-reduced-motion: reduce, render sekali saja.
Resize
new ResizeObserver(() => {
camera.aspect = wadah.clientWidth / wadah.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(wadah.clientWidth, wadah.clientHeight);
renderer.render(scene, camera); // gambar ulang, juga saat loop berhenti
}).observe(wadah);
Kamera menyimpan hitungan proyeksinya dalam sebuah matriks, dan matriks itu baru dihitung ulang saat updateProjectionMatrix() dipanggil. Tanpa itu, wadah 160 × 240 (aspect 0,67) masih digambar dengan aspect lama 1,33: semua benda gepeng jadi setengah lebarnya. Itu jawaban tebakannya. Hal yang sama berlaku setelah mengubah camera.fov. Kalau canvas-nya selebar jendela, pakai window.addEventListener('resize', ...) dengan window.innerWidth dan window.innerHeight.
⚠️ Jebakan yang sering ketemu
- Canvas hitam tanpa error. Biasanya kamera masih di (0, 0, 0) di dalam benda, benda lupa di-
scene.add, bendanya di luarnear/far, ataurendergak pernah dipanggil. - Lupa
updateProjectionMatrix()setelah mengubahaspectataufov: gambarnya melar atau gepeng. setPixelRatio(window.devicePixelRatio)tanpa batas. Di HP DPR 3, GPU mewarnai 2,25 kali pixel DPR 2 tanpa beda yang terlihat. Batasi denganMath.min(..., 2).import * as THREE from 'three'tanpa bundler atau import map:Failed to resolve module specifier "three".
TL;DR: Pasang three (0.186.1) lewat npm atau import map, lalu import * as THREE from 'three'. Tiga komponen wajib: Scene (wadah, scene.add mengisi scene.children), Camera (PerspectiveCamera(fov, aspect, near, far): fov derajat vertikal, aspect lebar / tinggi, near dan far batas jarak; mundurkan dengan position.z), dan Renderer (WebGLRenderer, setPixelRatio(Math.min(devicePixelRatio, 2)), setSize, appendChild(renderer.domElement)). Mesh = geometry + material. Gerakkan dengan renderer.setAnimationLoop((time) => { ...; renderer.render(scene, camera) }), sudut dari waktu. Saat ukuran berubah: camera.aspect, camera.updateProjectionMatrix(), renderer.setSize.