VFX Engine
Sejauh ini, kita telah membuat komponen khusus untuk membuat partikel dalam adegan 3D kita. Sebagian besar waktu, kita ingin melakukan hal yang hampir sama: memancarkan partikel dari suatu titik di ruang dan menganimasikannya seiring waktu. (Warna, ukuran, posisi, dll.)
Daripada menduplikasi kode yang sama berulang kali, kita bisa membuat mesin VFX yang relatif generik yang dapat digunakan untuk membuat berbagai jenis efek partikel.
Ini memiliki banyak manfaat:
- Penggunaan Kembali: Anda dapat menggunakan mesin yang sama untuk membuat berbagai jenis efek partikel dalam proyek Anda.
- Performa: Mesin dapat dioptimalkan untuk menangani sejumlah besar partikel secara efisien dan menggabungkan beberapa sistem partikel menjadi satu.
- Fleksibilitas: Anda dapat dengan mudah menyesuaikan perilaku partikel dengan mengubah parameter mesin.
- Kemudahan penggunaan: Anda dapat membuat efek partikel yang kompleks hanya dengan beberapa baris kode.
- Menghindari duplikasi kode: Anda tidak perlu menulis kode yang sama beberapa kali.
Kita akan menggunakan mesin VFX ini pada pelajaran selanjutnya untuk membuat berbagai efek. Meskipun Anda dapat melewati pelajaran ini dan langsung menggunakan mesin tersebut, memahami cara kerjanya akan membantu Anda memahami lebih dalam bagaimana menguasai kinerja dan fleksibilitas dalam proyek 3D Anda.
Siap untuk membangun mesin VFX Anda? Mari kita mulai!
GPU Particles
Kita telah melihat pada pelajaran sebelumnya bagaimana kita dapat menggunakan komponen <Instances />
dari drei untuk membuat partikel yang terkendali dalam adegan 3D kita.
Namun, pendekatan ini memiliki satu keterbatasan utama: jumlah partikel yang dapat kita tangani terbatas oleh CPU. Semakin banyak partikel yang kita miliki, semakin banyak CPU harus menangani mereka, yang dapat menyebabkan masalah performa.
Hal ini disebabkan oleh fakta bahwa di balik layar, komponen <Instances />
melakukan perhitungan untuk mendapatkan posisi, warna, dan ukuran dari setiap <Instance />
dalam loop useFrame
-nya. Anda dapat melihat kode tersebut di sini.
Untuk VFX Engine kita, kita ingin dapat mengeluarkan lebih banyak partikel daripada yang bisa kita tangani dengan komponen <Instances />
. Kita akan menggunakan GPU untuk menangani posisi, warna, dan ukuran partikel. Ini memungkinkan kita untuk menangani ratusan ribu partikel (jutaan? 👀) tanpa masalah performa.
Instanced Mesh
Meskipun kita bisa menggunakan Sprite atau Points untuk membuat partikel, kita akan menggunakan InstancedMesh.
Ini memungkinkan kita untuk merender tidak hanya bentuk sederhana seperti titik atau sprite, tetapi juga bentuk 3D seperti kubus, bola, dan geometri khusus.
Mari kita buat komponen dalam folder baru vfxs
yang disebut VFXParticles.jsx
:
import { useMemo, useRef } from "react"; import { PlaneGeometry } from "three"; export const VFXParticles = ({ settings = {} }) => { const { nbParticles = 1000 } = settings; const mesh = useRef(); const defaultGeometry = useMemo(() => new PlaneGeometry(0.5, 0.5), []); return ( <> <instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <meshBasicMaterial color="orange" /> </instancedMesh> </> ); };
Kita membuat geometry yang akan digunakan untuk setiap partikel. Dalam hal ini, kita menggunakan geometri plane sederhana dengan ukuran 0.5
pada kedua sumbu. Nantinya, kita akan menambahkan prop untuk melewatkan geometri apa pun yang kita inginkan.
Komponen instancedMesh
mengambil tiga argumen:
- geometry dari partikel.
- material dari partikel. Kita meneruskan
null
untuk mendefinisikannya secara deklaratif dalam komponen. - Jumlah instance yang dapat ditangani oleh komponen. Untuk kita, ini mewakili jumlah maksimum partikel yang dapat ditampilkan pada saat yang sama.
Mari kita gantikan kubus oranye dengan komponen VFXParticles
kita di file Experience.jsx
:
// ... import { VFXParticles } from "./vfxs/VFXParticles"; export const Experience = () => { return ( <> {/* ... */} <VFXParticles /> </> ); };
Kamu dapat melihat satu partikel oranye di tengah adegan. Ini adalah komponen VFXParticles
kita.
Jumlah partikel kita diatur ke 1000
tapi kita hanya bisa melihat satu. Ini karena semua dirender pada posisi yang sama (0, 0, 0)
. Mari kita ubah posisi tersebut.
Instance Matrix
Instanced mesh menggunakan matrix untuk menentukan posisi, rotasi, dan skala dari setiap instance. Dengan memperbarui properti instanceMatrix dari mesh kita, kita dapat memindahkan, memutar, dan menskalakan setiap partikel secara individu.
Untuk setiap instance, matrix adalah matrix 4x4 yang mewakili transformasi partikel. Kelas Matrix4 dari Three.js memungkinkan kita compose
dan decompose
matrix untuk mengatur/mendapatkan posisi, rotasi, dan skala partikel dengan cara yang lebih mudah dipahami.
Di atas deklarasi VFXParticles
, mari kita deklarasikan beberapa variabel dummy untuk memanipulasi partikel tanpa menciptakan Vectors dan Matrices terlalu sering:
// ... import { Euler, Matrix4, PlaneGeometry, Quaternion, Vector3 } from "three"; const tmpPosition = new Vector3(); const tmpRotationEuler = new Euler(); const tmpRotation = new Quaternion(); const tmpScale = new Vector3(1, 1, 1); const tmpMatrix = new Matrix4();
Sekarang mari kita buat fungsi emit
untuk mengatur partikel kita:
// ... import { useEffect } from "react"; // ... export const VFXParticles = ({ settings = {} }) => { // ... const emit = (count) => { for (let i = 0; i < count; i++) { const position = [ randFloatSpread(5), randFloatSpread(5), randFloatSpread(5), ]; const scale = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; const rotation = [ randFloatSpread(Math.PI), randFloatSpread(Math.PI), randFloatSpread(Math.PI), ]; tmpPosition.set(...position); tmpRotationEuler.set(...rotation); tmpRotation.setFromEuler(tmpRotationEuler); tmpScale.set(...scale); tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale); mesh.current.setMatrixAt(i, tmpMatrix); } }; useEffect(() => { emit(nbParticles); }, []); // ... };
Fungsi emit
ini melakukan iterasi pada jumlah partikel yang ingin kita emit dan mengatur posisi, rotasi, dan skala acak untuk setiap partikel. Kemudian kita menyusun matrix dengan nilai-nilai ini dan mengaturnya pada instance di indeks saat ini.
Anda dapat melihat partikel acak dalam scene. Setiap partikel memiliki posisi, rotasi, dan skala yang acak.
Untuk menganimasikan partikel-partikel kita, kita akan mendefinisikan atribut seperti lifetime, speed, direction sehingga perhitungan dapat dilakukan pada GPU.
Sebelum melakukan itu, kita perlu beralih ke custom shader material untuk menangani atribut-atribut ini karena kita tidak memiliki akses dan kontrol atas atribut dari meshBasicMaterial
.
Partikel Material
Tujuan pertama kita adalah agar tidak ada perubahan antara meshBasicMaterial
dan shaderMaterial
baru kita. Kita akan membuat shader material sederhana yang akan merender partikel dengan cara yang sama seperti meshBasicMaterial
yang ada saat ini.
Dalam komponen VFXParticles
, mari kita buat shader material baru:
// ... import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; import { Color } from "three"; const ParticlesMaterial = shaderMaterial( { color: new Color("white"), }, /* glsl */ ` varying vec2 vUv; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; } `, /* glsl */ ` uniform vec3 color; varying vec2 vUv; void main() { gl_FragColor = vec4(color, 1.0); }` ); extend({ ParticlesMaterial });
Ini adalah shader material yang sangat sederhana yang menggunakan uniform color
dan merender partikel dengan warna tersebut. Hal baru di sini adalah instanceMatrix
yang kita gunakan untuk mendapatkan position
, rotation
, dan scale
dari setiap partikel.
Perlu dicatat bahwa kita tidak perlu mendeklarasikan atribut
instanceMatrix
karena ini adalah salah satu atribut bawaan dariWebGLProgram
ketika menggunakan instancing. Informasi lebih lanjut dapat ditemukan di sini.
Mari kita ganti meshBasicMaterial
dengan ParticlesMaterial
baru kita:
<instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> </instancedMesh>
Sempurna! position
, rotation
, dan scale
kita masih berfungsi seperti yang diharapkan. Partikel dirender dengan warna oranye yang sedikit berbeda. Ini karena kita tidak mempertimbangkan environment dalam shader material kita. Untuk menjaga agar tetap sederhana, kita akan membiarkannya seperti ini.
Kita sekarang siap untuk menambahkan atribut khusus ke partikel kita untuk menganimasikannya.
Atribut Buffer Instanced
Sejauh ini, kita hanya menggunakan atribut instanceMatrix
, sekarang kita akan menambahkan atribut kustom untuk mendapatkan lebih banyak kontrol atas setiap partikel.
Untuk ini, kita akan menggunakan InstancedBufferAttribute dari Three.js.
Kita akan menambahkan atribut berikut ke partikel kita:
instanceColor
: Sebuah vector3 yang mewakili warna partikel.instanceColorEnd
: Sebuah vector3 yang mewakili warna yang akan berubah seiring waktu.instanceDirection
: Sebuah vector3 yang mewakili arah di mana partikel akan bergerak.instanceSpeed
: Sebuah float untuk menentukan seberapa cepat partikel akan bergerak di arah tertentu.instanceRotationSpeed
: Sebuah vector3 untuk menentukan kecepatan rotasi partikel per sumbu.instanceLifetime
: Sebuah vector2 untuk mendefinisikan masa hidup partikel. Nilai pertama (x
) adalah waktu mulai, dan nilai kedua (y
) adalah masa hidup/durasi. Dikombinasi dengan sebuah uniform waktu, kita dapat menghitung usia, progres, dan apakah sebuah partikel hidup atau mati.
Mari kita buat buffer berbeda untuk atribut kita:
// ... import { useState } from "react"; // ... export const VFXParticles = ({ settings = {} }) => { // ... const [attributeArrays] = useState({ instanceColor: new Float32Array(nbParticles * 3), instanceColorEnd: new Float32Array(nbParticles * 3), instanceDirection: new Float32Array(nbParticles * 3), instanceLifetime: new Float32Array(nbParticles * 2), instanceSpeed: new Float32Array(nbParticles * 1), instanceRotationSpeed: new Float32Array(nbParticles * 3), }); // ... }; // ...
Saya menggunakan useState
untuk membuat buffer berbeda untuk atribut kita agar tidak membuatnya ulang setiap kali render. Saya memilih untuk tidak menggunakan hook useMemo
karena mengubah jumlah partikel maksimum selama siklus hidup komponen bukanlah sesuatu yang ingin kita tangani.
Float32Array
digunakan untuk menyimpan nilai-nilai dari atribut. Kita mengalikan jumlah partikel dengan jumlah komponen dari atribut untuk mendapatkan total jumlah nilai dalam array.
Dalam atribut instanceColor
, 3 nilai pertama akan mewakili warna partikel pertama, 3 nilai berikutnya akan mewakili warna partikel kedua, dan seterusnya.
Mari kita mulai dengan mengenal InstancedBufferAttribute
dan cara menggunakannya. Untuk melakukannya, kita akan mengimplementasikan atribut instanceColor
:
// ... import { DynamicDrawUsage } from "three"; export const VFXParticles = ({ settings = {} }) => { // ... return ( <> <instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> <instancedBufferAttribute attach={"geometry-attributes-instanceColor"} args={[attributeArrays.instanceColor]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> </instancedMesh> </> ); };
Dalam <instancedMesh />
kita, kita menambahkan komponen <instancedBufferAttribute />
untuk mendefinisikan atribut instanceColor
. Kita melampirkannya ke atribut geometry-attributes-instanceColor
dari mesh. Kita memberikan array attributeArrays.instanceColor
sebagai sumber data, mengatur itemSize
ke 3
karena kita memiliki vector3, dan count
ke nbParticles
.
Prop usage
diatur ke DynamicDrawUsage
untuk memberi tahu renderer bahwa data akan sering diperbarui. Nilai lain yang mungkin dan detail lebih lanjut bisa ditemukan di sini.
Kita tidak akan memperbaruinya setiap frame, tetapi setiap kali kita memancarkan partikel baru, data akan diperbarui. Cukup untuk mempertimbangkannya sebagai DynamicDrawUsage
.
Bagus, mari kita buat variabel dummy tmpColor
di bagian atas file kita untuk memanipulasi warna partikel:
// ... const tmpColor = new Color();
Sekarang mari kita perbarui fungsi emit
untuk mengatur atribut instanceColor
:
const emit = (count) => { const instanceColor = mesh.current.geometry.getAttribute("instanceColor"); for (let i = 0; i < count; i++) { // ... tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); } };
Kita memulai dengan mendapatkan atribut instanceColor
dari geometri mesh. Kemudian kita lakukan loop atas jumlah partikel yang ingin kita pancarkan dan mengatur warna acak untuk setiap partikel.
Mari kita perbarui particlesMaterial
untuk menggunakan atribut instanceColor
alih-alih uniform warna:
const ParticlesMaterial = shaderMaterial( { // color: new Color("white"), }, /* glsl */ ` varying vec2 vUv; varying vec3 vColor; attribute vec3 instanceColor; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; vColor = instanceColor; } `, /* glsl */ ` varying vec3 vColor; varying vec2 vUv; void main() { gl_FragColor = vec4(vColor, 1.0); }` ); // ...
Kami menambahkan attribute vec3 instanceColor;
ke vertex shader dan mengatur variabel vColor
untuk melewatkan warna ke fragment shader. Kami kemudian mengatur gl_FragColor
ke vColor
untuk merender partikel dengan warnanya.
Kami berhasil mengatur warna acak untuk setiap partikel. Partikel dirender dengan warnanya.
Bagus, mari tambahkan atribut lain ke partikel kita. Pertama, mari kita perbarui fungsi emit
untuk mengatur atribut instanceColorEnd
, instanceDirection
, instanceLifetime
, instanceSpeed
, dan instanceRotationSpeed
dengan nilai acak:
const emit = (count) => { const instanceColor = mesh.current.geometry.getAttribute("instanceColor"); const instanceColorEnd = mesh.current.geometry.getAttribute("instanceColorEnd"); const instanceDirection = mesh.current.geometry.getAttribute("instanceDirection"); const instanceLifetime = mesh.current.geometry.getAttribute("instanceLifetime"); const instanceSpeed = mesh.current.geometry.getAttribute("instanceSpeed"); const instanceRotationSpeed = mesh.current.geometry.getAttribute( "instanceRotationSpeed" ); for (let i = 0; i < count; i++) { // ... tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColorEnd.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); const direction = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceDirection.set(direction, i * 3); const lifetime = [randFloat(0, 5), randFloat(0.1, 5)]; instanceLifetime.set(lifetime, i * 2); const speed = randFloat(5, 20); instanceSpeed.set([speed], i); const rotationSpeed = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceRotationSpeed.set(rotationSpeed, i * 3); } };
Dan buat komponen instancedBufferAttribute
untuk setiap atribut:
<instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> <instancedBufferAttribute attach={"geometry-attributes-instanceColor"} args={[attributeArrays.instanceColor]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceColorEnd"} args={[attributeArrays.instanceColorEnd]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceDirection"} args={[attributeArrays.instanceDirection]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceLifetime"} args={[attributeArrays.instanceLifetime]} itemSize={2} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceSpeed"} args={[attributeArrays.instanceSpeed]} itemSize={1} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceRotationSpeed"} args={[attributeArrays.instanceRotationSpeed]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> </instancedMesh>
Sekarang saatnya menambahkan kehidupan kepada partikel kita dengan mengimplementasikan logika pergerakan, warna, dan masa hidup mereka.
Masa Hidup Partikel
Untuk menghitung perilaku partikel kita, kita perlu melewatkan waktu yang berlalu ke shader kita. Kita akan menggunakan properti uniforms
dari shaderMaterial
untuk melewatkan waktu ke dalamnya.
Mari kita perbarui ParticlesMaterial
kita untuk menambahkan uniform uTime
:
const ParticlesMaterial = shaderMaterial( { uTime: 0, }, /* glsl */ ` uniform float uTime; // ... `, /* glsl */ ` // ... ` );
Dan dalam loop useFrame
, kita akan memperbarui uniform uTime
:
// ... import { useFrame } from "@react-three/fiber"; // ... export const VFXParticles = ({ settings = {} }) => { // ... useFrame(({ clock }) => { if (!mesh.current) { return; } mesh.current.material.uniforms.uTime.value = clock.elapsedTime; }); // ... };
Dalam vertex shader, kita akan menghitung umur dan kemajuan dari setiap partikel berdasarkan uniform uTime
dan atribut instanceLifetime
. Kita akan meneruskan kemajuan ke dalam fragment shader untuk menganimasikan partikel menggunakan varying bernama vProgress
.
uniform float uTime; varying vec2 vUv; varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; attribute float instanceSpeed; attribute vec3 instanceRotationSpeed; attribute vec3 instanceDirection; attribute vec3 instanceColor; attribute vec3 instanceColorEnd; attribute vec2 instanceLifetime; // x: startTime, y: duration void main() { float startTime = instanceLifetime.x; float duration = instanceLifetime.y; float age = uTime - startTime; vProgress = age / duration; gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; vColor = instanceColor; vColorEnd = instanceColorEnd; }
Umur dihitung dengan mengurangi startTime
dari uTime
. Kemajuan kemudian dihitung dengan membagi umur dengan duration
.
Sekarang dalam fragment shader, kita akan menginterpolasi warna partikel antara instanceColor
dan instanceColorEnd
berdasarkan kemajuan:
varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; varying vec2 vUv; void main() { vec3 finalColor = mix(vColor, vColorEnd, vProgress); gl_FragColor = vec4(finalColor, 1.0); }
Kita dapat melihat partikel berubah warna seiring waktu, tetapi kita menghadapi masalah. Semua partikel terlihat di awal sedangkan waktu mulainya acak. Kita perlu menyembunyikan partikel yang belum hidup.
Untuk mencegah partikel yang belum lahir dan mati dari dirender, kita akan menggunakan kata kunci discard
dalam fragment shader:
// ... void main() { if (vProgress < 0.0 || vProgress > 1.0) { discard; } // ... }
Kata kunci discard
memberitahu renderer untuk mengabaikan fragment saat ini dan tidak merendernya.
Sempurna, partikel kita sekarang lahir, hidup, dan mati seiring waktu. Kita sekarang bisa menambahkan logika gerakan dan rotasi.
Gerakan Partikel
Dengan menggunakan direction, speed, dan age dari partikel, kita dapat menghitung position mereka seiring waktu.
Dalam vertex shader, mari sesuaikan gl_Position
untuk mempertimbangkan direction dan speed partikel.
Pertama, kita menormalkan direction untuk menghindari partikel bergerak lebih cepat ketika arah tidak merupakan unit vector:
vec3 normalizedDirection = length(instanceDirection) > 0.0 ? normalize(instanceDirection) : vec3(0.0);
Kemudian kita menghitung offset partikel berdasarkan speed dan age:
vec3 offset = normalizedDirection * age * instanceSpeed;
Mari kita dapatkan instance position:
vec4 startPosition = modelMatrix * instanceMatrix * vec4(position, 1.0); vec3 instancePosition = startPosition.xyz;
Dan terapkan offset padanya:
vec3 finalPosition = instancePosition + offset;
Terakhir, kita mendapatkan posisi model view mvPosition
dengan menerapkan modelViewMatrix ke finalPosition untuk mentransformasikan posisi ke world space:
vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0);
Dan menerapkan projectionMatrix untuk mentransformasikan posisi dunia ke camera space:
gl_Position = projectionMatrix * mvPosition;
Berikut adalah vertex shader lengkap kita sejauh ini:
uniform float uTime; varying vec2 vUv; varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; attribute float instanceSpeed; attribute vec3 instanceRotationSpeed; attribute vec3 instanceDirection; attribute vec3 instanceColor; attribute vec3 instanceColorEnd; attribute vec2 instanceLifetime; // x: startTime, y: duration void main() { float startTime = instanceLifetime.x; float duration = instanceLifetime.y; float age = uTime - startTime; vProgress = age / duration; vec3 normalizedDirection = length(instanceDirection) > 0.0 ? normalize(instanceDirection) : vec3(0.0); vec3 offset = normalizedDirection * age * instanceSpeed; vec4 startPosition = modelMatrix * instanceMatrix * vec4(position, 1.0); vec3 instancePosition = startPosition.xyz; vec3 finalPosition = instancePosition + offset; vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0); gl_Position = projectionMatrix * mvPosition; vUv = uv; vColor = instanceColor; vColorEnd = instanceColorEnd; }
Partikel sekarang bergerak ke berbagai arah dengan kecepatan yang berbeda. Ini memang tampak kacau tetapi disebabkan oleh nilai random kita.
Mari kita perbaiki ini dengan menyesuaikan nilai random dalam fungsi emit
untuk mendapatkan gambaran lebih jelas tentang gerakan partikel:
for (let i = 0; i < count; i++) { const position = [ randFloatSpread(0.1), randFloatSpread(0.1), randFloatSpread(0.1), ]; const scale = [randFloatSpread(1), randFloatSpread(1), randFloatSpread(1)]; const rotation = [ randFloatSpread(Math.PI), randFloatSpread(Math.PI), randFloatSpread(Math.PI), ]; tmpPosition.set(...position); tmpRotationEuler.set(...rotation); tmpRotation.setFromEuler(tmpRotationEuler); tmpScale.set(...scale); tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale); mesh.current.setMatrixAt(i, tmpMatrix); tmpColor.setRGB(1, 1, 1); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); tmpColor.setRGB(0, 0, 0); instanceColorEnd.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); const direction = [randFloatSpread(0.5), 1, randFloatSpread(0.5)]; instanceDirection.set(direction, i * 3); const lifetime = [randFloat(0, 5), randFloat(0.1, 5)]; instanceLifetime.set(lifetime, i * 2); const speed = randFloat(1, 5); instanceSpeed.set([speed], i); const rotationSpeed = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceRotationSpeed.set(rotationSpeed, i * 3); }
Mulai terbentuk!
Kita akan menambahkan kontrol UI sederhana untuk menyesuaikan variabel nanti. Sekarang mari sempurnakan partikel dengan menambahkan logika rotasi.
Sementara kita memisahkan direction dan speed untuk gerakan, untuk rotasi kita akan menggunakan satu atribut instanceRotationSpeed
untuk menentukan kecepatan rotasi per sumbu.
Dalam vertex shader kita dapat menghitung rotation partikel berdasarkan rotation speed dan age:
vec3 rotationSpeed = instanceRotationSpeed * age;
Kemudian, agar dapat menerapkan "offset rotation" ini pada partikel, kita perlu mengonversinya ke matriks rotasi:
mat4 rotX = rotationX(rotationSpeed.x); mat4 rotY = rotationY(rotationSpeed.y); mat4 rotZ = rotationZ(rotationSpeed.z); mat4 rotationMatrix = rotZ * rotY * rotX;
rotationX
, rotationY
, dan rotationZ
adalah fungsi-fungsi yang mengembalikan matriks rotasi di sekitar sumbu X, Y, dan Z secara berturut-turut. Kita akan mendefinisikannya tepat di atas fungsi main
dalam vertex shader:
mat4 rotationX(float angle) { float s = sin(angle); float c = cos(angle); return mat4( 1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1 ); } mat4 rotationY(float angle) { float s = sin(angle); float c = cos(angle); return mat4( c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1 ); } mat4 rotationZ(float angle) { float s = sin(angle); float c = cos(angle); return mat4( c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ); }
Untuk mempelajari lebih lanjut tentang matriks rotasi, Anda dapat memeriksa artikel Wikipedia ini, Game Math Explained Simply yang luar biasa oleh Simon Dev, atau Bagian Matrix dari The Book of Shaders.
Akhirnya, kita dapat menerapkan rotation matrix ke start position partikel:
vec4 startPosition = modelMatrix * instanceMatrix * rotationMatrix * vec4(position, 1.0);
Mari kita coba:
Partikel sekarang bergerak, berubah warna, dan berotasi! ✨
Sempurna, kita memiliki basis yang solid untuk VFX Engine kita. Sebelum menambahkan lebih banyak fitur dan kontrol, mari kita siapkan bagian penting kedua dari engine: emitter.
Emitters
Dalam contoh dan tutorial, bagian ini sering diabaikan dalam sistem partikel. Namun, ini adalah bagian penting untuk mengintegrasikan partikel ke dalam proyek Anda dengan mudah dan efisien:
- Mudah karena komponen
<VFXParticles />
Anda akan berada di bagian atas hierarki dan emitter Anda dapat memunculkannya dari sub-komponen manapun di dalam scene Anda. Membuatnya mudah untuk memunculkan dari titik tertentu, menempelkannya ke objek yang bergerak, atau tulang yang bergerak. - Efisien karena alih-alih membuat ulang instanced meshes, menyusun shader materials, dan mengatur attributes setiap kali Anda ingin memunculkan partikel, Anda dapat menggunakan kembali komponen VFXParticles yang sama dan cukup memanggil fungsi untuk memunculkan partikel dengan pengaturan yang diinginkan.
useVFX
Kita ingin bisa memanggil fungsi emit
dari komponen VFXParticles
kita dari mana saja di proyek kita. Untuk melakukannya, kita akan membuat custom hook yang disebut useVFX
yang akan mengurusi mendaftarkan dan mencabut emitters dari komponen VFXParticles.
Kita akan menggunakan Zustand karena ini adalah cara sederhana dan efisien untuk mengelola global state di React dengan performa yang baik.
Mari tambahkan ke proyek kita:
yarn add zustand
Di dalam folder vfxs
, mari kita buat file VFXStore.js
:
import { create } from "zustand"; export const useVFX = create((set, get) => ({ emitters: {}, registerEmitter: (name, emitter) => { if (get().emitters[name]) { console.warn(`Emitter ${name} already exists`); return; } set((state) => { state.emitters[name] = emitter; return state; }); }, unregisterEmitter: (name) => { set((state) => { delete state.emitters[name]; return state; }); }, emit: (name, ...params) => { const emitter = get().emitters[name]; if (!emitter) { console.warn(`Emitter ${name} not found`); return; } emitter(...params); }, }));
Apa yang dikandungnya:
- emitters: Objek yang akan menyimpan semua emitters dari komponen
VFXParticles
kita. - registerEmitter: Fungsi untuk mendaftarkan emitter dengan nama yang diberikan.
- unregisterEmitter: Fungsi untuk mencabut pendaftaran emitter dengan nama yang diberikan.
- emit: Fungsi untuk memanggil emitter dengan nama dan parameter yang diberikan dari mana saja dalam proyek kita.
Mari kita hubungkan ke komponen VFXParticles
kita:
// ... import { useVFX } from "./VFXStore"; // ... export const VFXParticles = ({ name, settings = {} }) => { // ... const registerEmitter = useVFX((state) => state.registerEmitter); const unregisterEmitter = useVFX((state) => state.unregisterEmitter); useEffect(() => { // emit(nbParticles); registerEmitter(name, emit); return () => { unregisterEmitter(name); }; }, []); // ... }; // ...
Kita menambahkan prop name
ke komponen VFXParticles
kita untuk mengidentifikasi emitter. Kemudian kita menggunakan hook useVFX
untuk mendapatkan fungsi registerEmitter
dan unregisterEmitter
.
Kita memanggil registerEmitter
dengan name
dan fungsi emit
di dalam hook useEffect
untuk mendaftarkan emitter saat komponen mount dan mencabutnya saat komponen unmount.
Di dalam komponen Experience
, mari tambahkan prop name
ke komponen VFXParticles
kita:
// ... import { VFXParticles } from "./vfxs/VFXParticles"; export const Experience = () => { return ( <> {/* ... */} <VFXParticles name="sparks" /> </> ); };
VFXEmitter
Sekarang setelah kita memiliki hook useVFX
, kita dapat membuat komponen VFXEmitter
yang akan bertanggung jawab untuk memunculkan partikel dari komponen VFXParticles
.
Di folder vfxs
, mari kita buat file VFXEmitter.jsx
:
import { forwardRef, useImperativeHandle, useRef } from "react"; import { useVFX } from "./VFXStore"; export const VFXEmitter = forwardRef( ({ emitter, settings = {}, ...props }, forwardedRef) => { const { duration = 1, nbParticles = 1000, spawnMode = "time", // time, burst loop = false, delay = 0, } = settings; const emit = useVFX((state) => state.emit); const ref = useRef(); useImperativeHandle(forwardedRef, () => ref.current); return ( <> <object3D {...props} ref={ref} /> </> ); } );
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.