Di 15 Haunted House kita udah bikin scene utuh. Sekarang kita belajar bikin ribuan titik kecil melayang buat bikin bintang, asap, hujan, debu, atau api.
Particles itu ya persis kaya namanya: partikel. Populer banget dan bisa dipakai buat macam-macam efek: bintang, asap, hujan, debu, api, dan banyak lagi.
Yang bikin particles menarik: kamu bisa naruh ratusan ribu partikel di layar dan frame rate-nya masih masuk akal. Kok bisa? Karena tiap partikel itu cuma satu bidang datar (dua segitiga) yang selalu menghadap kamera. Jadi seringan itu.
Bikinnya juga segampang bikin Mesh, cuma bahannya beda:
BufferGeometry, yang udah kita kenal di 07 Geometry > Bikin Geometry Sendiri (BufferGeometry).PointsMaterial, material khusus partikel yang sempat disinggung di 10 Materials > Material untuk Nanti.Points, bukanMesh.
Partikel Pertama
1. Geometry
Tiap vertex dari geometry bakal jadi satu partikel. Jadi kita bisa pakai geometry bawaan apapun buat mulai. Kita coba bola dulu:
/**
* Particles
*/
// Geometry
const particlesGeometry = new THREE.SphereGeometry(1, 32, 32)2. PointsMaterial
Material biasa gak bisa dipakai, harus PointsMaterial. Dia punya dua properti khas partikel:
size, ukuran semua partikelnya.sizeAttenuation, nentuin apakah partikel yang jauh harus kelihatan lebih kecil daripada yang dekat. Ini yang bikin kesan kedalaman.
// Material
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
sizeAttenuation: true
})Kaya biasa, bisa juga di-set setelah instance-nya jadi:
const particlesMaterial = new THREE.PointsMaterial()
particlesMaterial.size = 0.02
particlesMaterial.sizeAttenuation = true3. Points
Terakhir, kita bikinnya bukan pakai Mesh, tapi pakai Points:
❌ salah, Mesh bikin permukaan padat, bukan titik-titik
const particles = new THREE.Mesh(particlesGeometry, particlesMaterial)✅ benar
// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)Gampang kan? Sekarang bolanya kelihatan sebagai kumpulan titik-titik. Ayo kita bikin sendiri dari nol.
Bikin Geometry Sendiri
Prosesnya persis sama kaya waktu kita nyebar segitiga acak dulu, cuma sekarang hasilnya partikel, bukan segitiga:
// Geometry
const particlesGeometry = new THREE.BufferGeometry()
const count = 500
// Dikali 3 karena satu posisi terdiri dari 3 nilai (x, y, z)
const positions = new Float32Array(count * 3)
for(let i = 0; i < count * 3; i++)
{
// - 0.5 biar nilainya bisa negatif juga
positions[i] = (Math.random() - 0.5) * 10
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))Bedanya sama waktu bikin segitiga: dulu kita kali 3 dua kali (count * 3 * 3), karena satu segitiga butuh 3 vertex. Sekarang cuma sekali, karena satu partikel = satu vertex.
Kalau kamu belum bisa nulis kode ini dari kepala sendiri, jangan frustrasi. Ini emang agak rumit dan formatnya aneh.
Sekarang saatnya seru-seruan: coba tes batas kemampuan komputermu. Ganti count jadi 5000, terus 50000, terus 500000. Kamu bisa punya jutaan partikel dan frame rate-nya masih masuk akal.
Tapi tentu ada batasnya. Di komputer kentang atau HP, kamu gak bakal dapet 60fps dengan jutaan partikel. Dan nanti kita bakal nambahin efek yang bikin frame rate turun drastis. Tapi tetap aja, ini mengesankan.
Untuk sekarang kita balikin ke 5000 dan ukurannya kita gedein:
const count = 5000
// ...
particlesMaterial.size = 0.1Warna, Map, dan Alpha Map
1. Color
Warna semua partikel bisa diganti pakai properti color. Dan lagi-lagi wajib dibungkus Color kalau di-set setelah material dibikin, aturan yang sama kaya di 10 Materials > 2. Color dan 13 Lights > AmbientLight:
particlesMaterial.color = new THREE.Color('#ff88cc')2. Map
Kita bisa nempelin gambar ke tiap partikel pakai map. Gambar-gambarnya ada di /static/textures/particles/:
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const particleTexture = textureLoader.load('/textures/particles/2.png')
// ...
particlesMaterial.map = particleTextureTexture-texture ini versi kecil dari paket bikinan Kenney, tapi kamu juga bisa bikin sendiri.
Kelihatan kan, color itu mewarnai gambarnya, sama persis kaya di material lain.
Tapi kalau diperhatiin, partikel yang di depan nutupin partikel yang di belakang dengan kotak hitam. Kenapa? Karena gambarnya itu kotak persegi dengan latar hitam, dan latar hitamnya ikut kegambar.
3. AlphaMap
Solusinya: nyalain transparansi dan pindahin texture-nya dari map ke alphaMap, sesuai cara kerja alphaMap:
// particlesMaterial.map = particleTexture
particlesMaterial.transparent = true
particlesMaterial.alphaMap = particleTextureJauh lebih bagus. Tapi masih ada masalah: kalau diliatin, kadang masih kelihatan garis tepi kotak partikelnya secara acak.
Penyebabnya: partikel digambar sesuai urutan dibuatnya, dan WebGL gak tau mana yang sebenarnya di depan. Jadi partikel belakang bisa keburu nutup partikel depan.
Ada tiga cara ngatasinnya, dan gak ada yang sempurna.
4. Cara 1: alphaTest
alphaTest itu nilai antara 0 sampai 1 yang ngasih tau WebGL kapan sebuah piksel gak usah digambar berdasarkan transparansinya.
Bawaannya 0, artinya semua piksel tetap digambar walaupun transparan total. Kalau kita kasih nilai kecil kaya 0.001, piksel dengan alpha 0 gak bakal digambar sama sekali:
particlesMaterial.alphaTest = 0.001Solusinya gak sempurna, kalau diliatin banget masih ada cacat, tapi udah jauh lebih enak dipandang.
5. Cara 2: depthTest
Waktu menggambar, WebGL ngetes apakah yang lagi digambar itu lebih dekat daripada yang udah digambar. Itu namanya depth testing, dan bisa kita matiin:
// particlesMaterial.alphaTest = 0.001
particlesMaterial.depthTest = falseKelihatannya masalahnya beres total. Tapi hati-hati banget sama solusi ini, karena kalau di scene kamu ada objek lain atau partikel dengan warna berbeda, partikelnya bisa kegambar seolah-olah dia di depan segalanya, padahal harusnya kehalang.
Coba tambahin kubus buat buktiin:
const cube = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshBasicMaterial()
)
scene.add(cube)Nah kan, partikelnya nembus kubus.
6. Cara 3: depthWrite
Tadi kita bilang WebGL ngetes apakah yang digambar lebih dekat dari yang udah ada. Nah, kedalaman dari yang udah digambar itu disimpan di tempat bernama depth buffer.
Daripada gak ngetes sama sekali (yang bikin masalah tadi), kita bisa nyuruh WebGL jangan nulis partikelnya ke depth buffer itu:
// particlesMaterial.alphaTest = 0.001
// particlesMaterial.depthTest = false
particlesMaterial.depthWrite = falseBedanya halus tapi penting:
depthTest = falseartinya “jangan ngecek apakah ada sesuatu di depanku”. Partikelnya jadi nembus semua.depthWrite = falseartinya “tetap ngecek, tapi jangan catat keberadaanku buat objek berikutnya”. Partikelnya tetap kehalang objek lain, tapi gak saling nutup sesama partikel.
Di kasus kita, ini solusi terbaik dengan hampir tanpa kerugian. Tapi kadang objek lain masih bisa kegambar di depan atau di belakang partikel tergantung banyak faktor, kaya transparansinya atau urutan objek dimasukin ke scene.
Gak ada solusi yang sempurna. Kamu harus nyoba dan nyari kombinasi terbaik buat project-mu.
Blending
Sekarang WebGL menggambar piksel saling menimpa satu sama lain.
Dengan ngubah properti blending, kita bisa nyuruh WebGL bukan cuma menggambar pikselnya, tapi menambahkan warnanya ke warna piksel yang udah ada di situ. Efeknya jenuh dan berpendar, kaya cahaya yang numpuk, dan hasilnya bisa keren banget:
particlesMaterial.depthWrite = false
particlesMaterial.blending = THREE.AdditiveBlendingBayangin dua senter yang sorotannya nimpa di tembok, bagian yang kena dua-duanya jadi lebih terang. Nah gitu.
Tambahin partikelnya jadi 20000 biar efeknya makin kerasa. Tapi hati-hati, efek ini berat, jadi kamu gak bakal bisa punya sebanyak tadi sambil tetap 60fps. Kubus tadi juga udah boleh dihapus.
Warna Berbeda Tiap Partikel
Kita bisa kasih warna beda buat tiap partikel. Caranya: tambahin attribute baru namanya color, persis kaya kita nambahin position.
Warna itu terdiri dari merah, hijau, dan biru, alias 3 nilai juga, jadi kodenya mirip banget sama posisi. Malah satu perulangan bisa dipakai buat dua-duanya:
const positions = new Float32Array(count * 3)
const colors = new Float32Array(count * 3)
for(let i = 0; i < count * 3; i++)
{
positions[i] = (Math.random() - 0.5) * 10
colors[i] = Math.random()
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))Hati-hati sama tunggal dan jamaknya: variabelnya colors (jamak) tapi nama attribute-nya color (tunggal). Ini jebakan klasik yang bikin bengong.
❌ salah, ThreeJS nyari nama 'color', bukan 'colors'
particlesGeometry.setAttribute('colors', new THREE.BufferAttribute(colors, 3))✅ benar
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))Sama kaya nama 'position' yang harga mati di 07 Geometry > 4. Pasang ke Geometry Pakai setAttribute, 'color' juga nama yang dicari ThreeJS.
Terus nyalain biar warnanya kepakai:
particlesMaterial.vertexColors = trueWarna utama material tadi masih ikut mewarnai warna per-vertex ini. Kalau mau warnanya murni acak, komen aja:
// particlesMaterial.color = new THREE.Color('#ff88cc')Animasi
Ada beberapa cara nganimasiin partikel, dari yang paling gampang sampai yang paling bener.
1. Menggerakkan Seluruhnya Sebagai Satu Objek
Karena class Points itu turunan dari Object3D, kita bisa geser, puter, dan perbesar dia kaya objek biasa:
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update particles
particles.rotation.y = elapsedTime * 0.2
// ...
}Ini udah keren, tapi kita gak punya kendali atas tiap partikel, semuanya gerak barengan kaya satu benda padat.
2. Dengan Ngubah Attribute-nya Langsung
Cara kedua: ubah posisi tiap vertex satu-satu, jadi tiap partikel bisa punya jalur sendiri-sendiri. Kita bikin partikelnya bergelombang kaya ombak.
Ingat, array position itu satu dimensi, di mana 3 nilai pertama itu x, y, z vertex pertama, 3 nilai berikutnya x, y, z vertex kedua, dan seterusnya.
Kita cuma mau gerakin naik-turun, artinya cuma sumbu y yang diubah. Jadi kita telusuri array-nya tiga-tiga dan cuma sentuh nilai kedua:
for(let i = 0; i < count; i++)
{
const i3 = i * 3
}Perhatiin bedanya sama perulangan waktu bikin posisi tadi. Di sini i cuma sampai count (5000), bukan count * 3, terus kita bikin variabel i3 yang loncat tiga-tiga. Dengan begitu:
i3itu koordinat x partikel ke-i.i3 + 1itu koordinat y-nya.i3 + 2itu koordinat z-nya.
Sekarang kita gerakin y-nya pakai sinus:
for(let i = 0; i < count; i++)
{
const i3 = i * 3
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime)
}Kok gak ada yang gerak? Karena ThreeJS harus dikasih tau kalau geometry-nya berubah. Ini persis kaya jebakan texture.needsUpdate waktu kita load gambar manual dulu:
❌ salah, ThreeJS gak tau ada yang berubah, jadi diem aja
for(let i = 0; i < count; i++)
{
const i3 = i * 3
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime)
}✅ benar, dikasih tau setelah semua vertex selesai diubah
for(let i = 0; i < count; i++)
{
const i3 = i * 3
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime)
}
particlesGeometry.attributes.position.needsUpdate = trueSekarang semua partikel naik-turun barengan kaya satu bidang datar, bukan ombak. Karena semuanya dikasih sudut sinus yang sama persis.
Biar jadi ombak, kita kasih pergeseran sudut antar partikel, dan yang paling masuk akal dipakai itu koordinat x-nya sendiri. Partikel yang posisinya beda di sumbu x bakal naik-turun di waktu yang beda:
for(let i = 0; i < count; i++)
{
const i3 = i * 3
const x = particlesGeometry.attributes.position.array[i3]
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime + x)
}
particlesGeometry.attributes.position.needsUpdate = trueHasilnya ombak partikel yang cantik banget.
Tapi jangan pakai teknik ini. Serius.
Kenapa? Karena kalau kita punya 20000 partikel, tiap frame kita menelusuri semuanya satu-satu, ngitung posisi baru, terus ngirim ulang seluruh attribute ke GPU. Itu 60 kali per detik. Buat partikel dikit sih jalan, tapi kita kan pengennya jutaan partikel.
Ini pekerjaan CPU yang harusnya dikerjain GPU.
3. Pakai Shader Sendiri
Buat nge-update jutaan partikel tiap frame dengan frame rate yang bagus, kita harus bikin material sendiri dengan shader sendiri. Tapi shader itu babnya sendiri nanti.
Code Lengkap
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
/**
* Base
*/
// Canvas
const canvas = document.querySelector('canvas.webgl')
// Scene
const scene = new THREE.Scene()
// Sizes
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const particleTexture = textureLoader.load('/textures/particles/2.png')
/**
* Particles
*/
// Geometry
const particlesGeometry = new THREE.BufferGeometry()
const count = 20000
const positions = new Float32Array(count * 3)
const colors = new Float32Array(count * 3)
for(let i = 0; i < count * 3; i++)
{
positions[i] = (Math.random() - 0.5) * 10
colors[i] = Math.random()
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
// Material
const particlesMaterial = new THREE.PointsMaterial()
particlesMaterial.size = 0.1
particlesMaterial.sizeAttenuation = true
particlesMaterial.transparent = true
particlesMaterial.alphaMap = particleTexture
// particlesMaterial.alphaTest = 0.001
// particlesMaterial.depthTest = false
particlesMaterial.depthWrite = false
particlesMaterial.blending = THREE.AdditiveBlending
particlesMaterial.vertexColors = true
// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)
/**
* Camera
*/
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 3
scene.add(camera)
// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update particles
particles.rotation.y = elapsedTime * 0.2
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()Ringkasan
- Tiap partikel itu satu bidang datar (dua segitiga) yang selalu menghadap kamera, makanya enteng banget dan bisa dipakai ratusan ribu sekaligus.
- Bahannya tiga:
BufferGeometry,PointsMaterial, danPoints(bukanMesh). - Tiap vertex dari geometry jadi satu partikel, jadi geometry bawaan apapun bisa langsung dipakai buat coba-coba.
sizengatur ukuran semua partikel,sizeAttenuationbikin partikel jauh kelihatan lebih kecil dan itu yang ngasih kesan kedalaman.- Bikin sendiri cuma butuh
count * 3, bukancount * 3 * 3kaya waktu bikin segitiga, karena satu partikel cuma butuh satu vertex. - Texture partikel dipasang di
alphaMap, bukanmap, biar latar hitam kotaknya gak ikut kegambar. Dan jangan lupatransparent = true. - Partikel digambar sesuai urutan dibuatnya, bukan sesuai jarak, makanya muncul tepi kotak yang saling nutup.
- Ada 3 obat dan gak ada yang sempurna:
alphaTest(buang piksel transparan),depthTest = false(jangan ngecek kedalaman, bahaya, partikelnya nembus semua objek), dandepthWrite = false(tetap ngecek tapi gak nyatet diri sendiri, biasanya paling aman). THREE.AdditiveBlendingmenambahkan warna piksel ke warna yang udah ada, bikin efek berpendar yang keren, tapi berat.- Warna per-partikel dibikin dengan attribute bernama
'color'(tunggal!) dan baru aktif kalauvertexColors = true. - Warna material masih ikut mewarnai warna per-vertex, jadi komen kalau mau warnanya murni.
Pointsitu turunanObject3D, jadi seluruh kumpulan partikel bisa digeser dan diputer kaya objek biasa.- Kalau mau tiap partikel gerak sendiri-sendiri, telusuri array-nya tiga-tiga pakai
i3 = i * 3, di manai3itu x,i3 + 1itu y, dani3 + 2itu z. - Wajib
attributes.position.needsUpdate = truesetelah selesai ngubah, kalau nggak ThreeJS gak tau ada yang berubah dan gak ada yang gerak. - Pergeseran sudut sinus pakai koordinat x itu yang mengubah gerakan naik-turun serempak jadi gerakan ombak.
- Tapi teknik ngubah attribute tiap frame itu sebaiknya dihindari, karena CPU-nya kerja rodi. Solusi yang benar itu bikin shader sendiri, dan itu bab nanti.
Bab selanjutnya kita pakai ilmu partikel ini buat bikin galaksi spiral yang bisa diatur jumlah lengan, ketebalan, dan warnanya lewat Debug UI.