Kembang Api

Starter pack

Selamat datang di Sky Adventure, perusahaan futuristik yang menyediakan kembang api terbaik di galaksi! 🎇

Kita akan membuat situs web 3D untuk memamerkan kembang api kita menggunakan Three.js, React Three Fiber, dan mesin VFX kita.

Inilah yang akan kita bangun bersama!

Proyek Pemula

Proyek pemula kita sudah mencakup yang berikut:

  • Pengaturan dasar React Three Fiber dengan pulau mengambang yang indah dari mana kita akan meluncurkan kembang api kita.
  • Efek pasca-pemrosesan untuk membuat cahaya dari model dan nantinya kembang api.
  • Antarmuka pengguna sederhana yang dibuat dengan Tailwind CSS dengan tiga tombol untuk nantinya meluncurkan kembang api.

Sky adventure preview with floating island

Begini yang kita dapatkan saat kita menjalankan proyek pemula.

Kembang Api

Untuk membuat kembang api, kita akan menggunakan mesin VFX yang kita bangun pada pelajaran sebelumnya. Mesin ini memungkinkan kita untuk membuat dan mengelola beberapa sistem partikel dengan perilaku yang berbeda.

useFireworks

Untuk mengelola kembang api secara efisien, kita akan membuat custom hook yang disebut useFireworks. Hook ini akan menangani pembuatan dan pengelolaan kembang api.

Mari tambahkan pustaka zustand ke proyek kita:

yarn add zustand

Sekarang, di dalam folder yang disebut hooks, buat file baru yang disebut useFireworks.js:

import { create } dari "zustand";

const useFireworks = create((set, get) => {
  return {
    fireworks: [],
  };
});

export { useFireworks };

Sebuah store sederhana dengan array kosong dari kembang api.

Mari kita tambahkan metode untuk membuat kembang api:

// ...
import { randFloat, randInt } dari "three/src/math/MathUtils.js";

const useFireworks = create((set) => {
  return {
    fireworks: [],
    addFirework: () => {
      set((state) => {
        return {
          fireworks: [
            ...state.fireworks,
            {
              id: `${Date.now()}-${randInt(0, 100)}-${state.fireworks.length}`,
              position: [0, 0, 0],
              velocity: [randFloat(-8, 8), randFloat(5, 10), randFloat(-8, 8)],
              delay: randFloat(0.8, 2),
              color: ["skyblue", "pink"],
            },
          ],
        };
      });
    },
  };
});

// ...

addFirework akan menambahkan kembang api baru ke store dengan:

  • id: pengenal unik untuk digunakan sebagai kunci di komponen React.
  • position: di mana kembang api akan dimulai.
  • velocity: arah dan kecepatan kembang api sebelum meledak.
  • delay: waktu sebelum kembang api meledak.
  • color: array warna yang digunakan untuk partikel ledakan kembang api.

Sekarang kita bisa menghubungkan hook ini ke UI kita untuk meluncurkan kembang api.

Buka UI.jsx dan hubungkan metode addFirework ke tombol:

import { useFireworks } dari "../hooks/useFireworks";

export const UI = () => {
  const addFirework = useFireworks((state) => state.addFirework);

  return (
    <section className="fixed inset-0 z-10 flex items-center justify-center">
      {/* ... */}
      <div
      // ...
      >
        {/* ... */}
        <div className="flex gap-4">
          <button
            // ..
            onClick={addFirework}
          >
            🎆 Classic
          </button>
          <button
            // ..
            onClick={addFirework}
          >
            💖 Love
          </button>
          <button
            // ..
            onClick={addFirework}
          >
            🌊 Sea
          </button>
        </div>
        {/* ... */}
      </div>
    </section>
  );
};

Untuk memeriksa apakah berfungsi, mari buat komponen Fireworks.jsx. Kita akan mencatat kembang api di konsol untuk sementara waktu:

import { useFireworks } dari "../hooks/useFireworks";

export const Fireworks = () => {
  const fireworks = useFireworks((state) => state.fireworks);

  console.log(fireworks);
};

Dan mari kita tambahkan ke komponen Experience

// ...
import { Fireworks } dari "./Fireworks";

export const Experience = () => {
  // ...

  return (
    <>
      {/* ... */}

      <Float
        speed={0.6}
        rotationIntensity={2}
        position-x={4}
        floatIntensity={2}
      >
        <Fireworks />
        <Gltf src="/models/SkyIsland.glb" />
      </Float>

      {/* ... */}
    </>
  );
};

Kita mengimpor komponen Fireworks dan menambahkannya di sebelah SkyIsland dalam komponen <Float /> kita.

Fireworks in the console

Saat menekan tombol, kita dapat melihat di konsol bahwa kembang api ditambahkan dengan benar ke store.

Sebelum merepresentasikannya di scene, kita perlu menangani siklus hidup kembang api. Saat ini, kembang api ditambahkan tetapi tidak pernah dihapus.

Di dalam addFirework pada hook useFireworks kita, kita dapat menambahkan setTimeout untuk menghapus kembang api setelah waktu tertentu.

Pertama kita perlu tahu kapan kembang api muncul. Kita bisa menambahkan properti time ke objek kembang api:

{
  id: `${Date.now()}-${randInt(0, 100)}-${state.fireworks.length}`,
  // ...
  time: Date.now(),
},

Kemudian panggil setTimeout untuk menghapus kembang api yang telah meledak dan memudar:

addFirework: () => {
  set((state) => {
    // ...
  });
  setTimeout(() => {
    set((state) => ({
      fireworks: state.fireworks.filter(
        (firework) => Date.now() - firework.time < 4000 // Max delay of 2 seconds +  Max lifetime of particles of 2 seconds
      ),
    }));
  }, 4000);
},

Kita memfilter kembang api yang telah ditambahkan lebih dari 4 detik yang lalu. Dengan cara ini, kita mempertahankan kembang api yang masih aktif.

Sesuaikan waktu sesuai dengan pengaturan akhir yang akan Anda gunakan untuk kembang api.

Fireworks in the console

Saat menekan tombol, kita dapat melihat di konsol bahwa kembang api dihapus dengan benar setelah waktu tertentu.

Sekarang kita bisa masuk ke bagian yang sudah Anda tunggu-tunggu: membuat kembang api di scene!

Mesin VFX (Wawa VFX)

Agar tidak menyalin/menempelkan komponen dari pelajaran sebelumnya, saya menerbitkan mesin VFX sebagai package di npm: Wawa VFX. Anda dapat menginstalnya dengan menjalankan:

yarn add wawa-vfx@^1.0.0

Dengan menggunakan @^1.0.0, kita memastikan bahwa kita selalu menggunakan versi major 1 dari paket tetapi termasuk fitur dan perbaikan terbaru dari versi minor dan patch. Dengan cara ini, kita dapat memanfaatkan fitur terbaru dan perbaikan bug tanpa perubahan yang merusak.

Sekarang kita dapat menggunakan <VFXParticles /> dan <VFXEmitter /> dalam proyek kita!

Dalam experience, mari kita tambahkan komponen VFXParticles ke dalam scene:

// ...
import { VFXParticles } from "wawa-vfx";

export const Experience = () => {
  const controls = useRef();

  return (
    <>
      {/* ... */}

      <VFXParticles
        name="firework-particles"
        settings={{
          nbParticles: 100000,
          gravity: [0, -9.8, 0],
          renderMode: "billboard",
          intensity: 3,
        }}
      />

      <EffectComposer>
        <Bloom intensity={1.2} luminanceThreshold={1} mipmapBlur />
      </EffectComposer>
    </>
  );
};

Kami menambahkan komponen VFXParticles dengan pengaturan berikut:

  • 100000 particles. Jika kita mencapai batas, particles yang paling lama akan dihapus, jumlah ini seharusnya lebih dari cukup untuk banyak kembang api pada saat bersamaan.
  • gravity untuk mensimulasikan gravitasi pada particles. -9.8 adalah gravitasi di Bumi. (Tapi tunggu, kita ada di luar angkasa! 👀)
  • renderMode ke billboard untuk selalu menghadap kamera.
  • intensity ke 3 untuk membuat particles bersinar di langit malam.

Tempatkan komponen <VFXParticles /> tidak terlalu penting. Pastikan saja di tingkat atas scene.

Dan tambahkan komponen VFXEmitter di samping VFXParticles untuk membentuk ledakan dalam mode debug dan mendapatkan akses ke kontrol visual:

// ...
import { VFXEmitter, VFXParticles } from "wawa-vfx";

export const Experience = () => {
  const controls = useRef();

  return (
    <>
      {/* ... */}

      <VFXParticles
        name="firework-particles"
        settings={{
          nbParticles: 100000,
          gravity: [0, -9.8, 0],
          renderMode: "billboard",
          intensity: 3,
        }}
      />
      <VFXEmitter emitter="firework-particles" debug />

      {/* ... */}
    </>
  );
};

Pastikan untuk mengatur prop emitter ke name yang sama dengan komponen VFXParticles dan mengatur debug ke true untuk melihat kontrol.

Rancangkan versi pertama dari ledakan kembang api. 💥

Setelah Anda puas dengan pengaturannya, Anda dapat menghapus prop debug dari komponen VFXEmitter, tekan tombol ekspor, dan tempel pengaturannya ke dalam komponen VFXEmitter.

<VFXEmitter
  emitter="firework-particles"
  settings={{
    nbParticles: 5000,
    delay: 0,
    spawnMode: "burst",
    colorStart: ["skyblue", "pink"],
    particlesLifetime: [0.1, 2],
    size: [0.01, 0.4],
    startPositionMin: [-0.1, -0.1, -0.1],
    startPositionMax: [0.1, 0.1, 0.1],
    directionMin: [-1, -1, -1],
    directionMax: [1, 1, 1],
    startRotationMin: [degToRad(-90), 0, 0],
    startRotationMax: [degToRad(90), 0, 0],
    rotationSpeedMin: [0, 0, 0],
    rotationSpeedMax: [3, 3, 3],
    speed: [1, 12],
  }}
/>

Kita sudah siap menghubungkan fireworks dengan mesin VFX!

Ledakan Kembang Api

Di dalam file Fireworks.jsx, mari kita buat komponen Firework yang akan mewakili satu kembang api:

// ...
import { useRef } from "react";
import { VFXEmitter } from "wawa-vfx";

export const Fireworks = () => {
  // ...
};

const Firework = ({ velocity, delay, position, color }) => {
  const ref = useRef();
  return (
    <>
      <group ref={ref} position={position}>
        <VFXEmitter
          emitter="firework-particles"
          settings={{
            nbParticles: 5000,
            delay: 0,
            spawnMode: "burst",
            colorStart: ["skyblue", "pink"],
            particlesLifetime: [0.1, 2],
            size: [0.01, 0.4],
            startPositionMin: [-0.1, -0.1, -0.1],
            startPositionMax: [0.1, 0.1, 0.1],
            directionMin: [-1, -1, -1],
            directionMax: [1, 1, 1],
            startRotationMin: [degToRad(-90), 0, 0],
            startRotationMax: [degToRad(90), 0, 0],
            rotationSpeedMin: [0, 0, 0],
            rotationSpeedMax: [3, 3, 3],
            speed: [1, 12],
          }}
        />
      </group>
    </>
  );
};

Cukup potong/tempel VFXEmitter dari komponen Experience ke komponen Firework.

Kami membungkusnya dalam <group /> agar dapat memindahkan kembang api di dalam scene berdasarkan position dan velocity.

Sekarang kita bisa menampilkan komponen Firework untuk setiap kembang api dalam penyimpanan useFireworks:

// ...

export const Fireworks = () => {
  const fireworks = useFireworks((state) => state.fireworks);

  return fireworks.map((firework) => (
    <Firework key={firework.id} {...firework} />
  ));
};

// ...

Kami melakukan pemetaan pada array fireworks dan menampilkan komponen Firework untuk setiap kembang api. Kami menggunakan id sebagai key untuk mengidentifikasi setiap kembang api secara unik.

Luar biasa, ketika menekan tombol, kita dapat melihat ledakan dalam scene!

Mari hubungkan parameter untuk menunda ledakan dan menetapkan warna yang tepat untuk kembang api:

// ...
const Firework = ({ velocity, delay, position, color }) => {
  const ref = useRef();
  return (
    <>
      <group ref={ref} position={position}>
        <VFXEmitter
          emitter="firework-particles"
          settings={{
            // ...
            delay,
            // ...
            colorStart: color,
            // ...
          }}
        />
      </group>
    </>
  );
};

Kami mengatur delay dan colorStart ke properti delay dan color yang diteruskan ke komponen Firework.

Karena hanya ada kemungkinan warna skyblue dan pink, kita dapat menambahkan lebih banyak kemungkinan dalam hook useFireworks:

// ...
import { randInt } from "three/src/math/MathUtils.js";

const colors = [
  ["skyblue", "pink"],
  ["orange", "yellow"],
  ["green", "teal"],
  ["mediumpurple", "indigo"],
  ["#ff9fed", "#e885ff", "#ff85b2", "#d69eff"],
];

const useFireworks = create((set) => {
  return {
    fireworks: [],
    addFirework: () => {
      set((state) => {
        return {
          fireworks: [
            ...state.fireworks,
            {
              // ...
              color: colors[randInt(0, colors.length - 1)],
              // ...
            },
          ],
        };
      });
      // ...
    },
  };
});

export { useFireworks };

Kami membuat array dari array warna. Setiap array mewakili kombinasi warna kembang api yang mungkin. Kami memilih secara acak salah satu dari mereka saat membuat kembang api baru.

Ketika menekan tombol, kita dapat melihat berbagai warna dan penundaan kembang api.

Namun, kembang api hanya muncul di tengah scene. Kita perlu membuatnya terbang dari pulau sebelum meledak! 💥

Peluncuran Kembang Api

Untuk membuat kembang api diluncurkan dari pulau, kita perlu memperbarui position berdasarkan velocity dan waktu yang telah berlalu.

Kita dapat menggunakan hook useFrame untuk memperbarui posisi kembang api:

import { useFrame } from "@react-three/fiber";

// ...

const Firework = ({ velocity, delay, position, color }) => {
  const ref = useRef();

  useFrame((_, delta) => {
    if (ref.current) {
      ref.current.position.x += velocity[0] * delta;
      ref.current.position.y += velocity[1] * delta;
      ref.current.position.z += velocity[2] * delta;
    }
  });

  // ...
};

Kita memperbarui posisi x, y, dan z kembang api berdasarkan velocity dan waktu yang telah berlalu sejak frame terakhir (delta).

Saat menekan tombol, kita dapat melihat kembang api meledak di posisi baru!

Bagus, tetapi kembang api tanpa jejak tidaklah terlalu mengesankan. Mari kita tambahkan jejak ke kembang api!

Jejak Kembang Api

Untuk membuat jejak, kita dapat memunculkan partikel di belakang kembang api yang akan memudar seiring waktu.

Karena kita sudah memiliki grup bergerak yang merepresentasikan kembang api, kita dapat menambahkan VFXEmitter dengan pengaturan berbeda untuk membuat jejak:

// ...

const Firework = ({ velocity, delay, position, color }) => {
  // ...

  return (
    <>
      <group ref={ref} position={position}>
        <VFXEmitter
          emitter="firework-particles"
          // ...
        />
        <VFXEmitter
          emitter="firework-particles"
          settings={{
            duration: delay,
            nbParticles: 100 * delay,
            delay: 0,
            loop: false,
            colorStart: ["white", "skyblue"],
            particlesLifetime: [0.1, 0.6],
            size: [0.01, 0.05],
            startPositionMin: [-0.02, 0, -0.02],
            startPositionMax: [0.02, 0, 0.02],
            startRotationMin: [0, 0, 0],
            startRotationMax: [0, 0, 0],
            rotationSpeedMin: [-12, -12, -12],
            rotationSpeedMax: [12, 12, 12],
            directionMin: [-1, -1, -1],
            directionMax: [1, 1, 1],
            speed: [0, 0.5],
          }}
        />
      </group>
    </>
  );
};

Kita menambahkan VFXEmitter baru dengan duration yang sama dengan delay kembang api. Dengan cara ini, jejak akan berlangsung sepanjang kembang api sebelum meledak.

Kami menghitung jumlah partikel berdasarkan delay untuk memiliki jumlah partikel yang tepat di jejak.

Dan kami menetapkan spawnMode ke time untuk memunculkan partikel seiring waktu dan tidak semuanya sekaligus.

Karena kita masih menggunakan bidang sederhana untuk partikel, kita menggunakan komponen <VFXParticles /> yang sama untuk jejak untuk performa maksimum.

Kita sekarang dapat melihat jejak kembang api sebelum meledak!

Ini lebih baik, tetapi pergerakan kembang api tidak terasa benar. Kita perlu menerapkan kurva pada gerakan untuk membuatnya lebih alami.

Kurva Kembang Api

Alih-alih menggerakkan kembang api dalam garis lurus, kita dapat menerapkan kurva pada gerakannya agar terlihat lebih alami.

Untuk melakukan perhitungan, kita akan membuat ref bernama age untuk melacak waktu yang telah berlalu sejak kembang api dibuat dan menyesuaikan posisi y berdasarkan hal tersebut.

// ...

const Firework = ({ velocity, delay, position, color }) => {
  const ref = useRef();
  const age = useRef(0);

  useFrame((_, delta) => {
    if (ref.current) {
      // ...
      ref.current.position.y +=
        velocity[1] * delta + age.current * age.current * -9.0 * delta;
      // ...
      age.current += delta;
    }
  });

  // ...
};

PS: Ini adalah rumus yang sama digunakan di komponen VFXParticles untuk mensimulasikan gravitasi.

Kembang api sekarang memiliki kurva yang indah sebelum meledak!

Peluncur

Saat ini kembang api kita muncul di tengah-tengah scene. Namun, kita ingin mereka muncul di peluncur yang telah ditentukan di pulau tersebut.

Mari kita buka model dengan versi online Gltfjsx:

Sky island with launchers

Kita dapat melihat mesh model kita dengan nama dan posisinya.

Mari kita salin semua posisi peluncur ke dalam array spawns di bagian atas file hook useFireworks:

const spawns = [
  [1.004, -0.001, 3.284],
  [-2.122, -0.001, 2.678],
  [-0.988, -0.001, 3.287],
  [2.888, -0.001, 1.875],
  [2.115, -0.001, 2.684],
];

Untuk membuat kembang api muncul di posisi yang tepat, kita perlu memperbarui position dari kembang api saat membuatnya:

// ...

const useFireworks = create((set) => {
  return {
    fireworks: [],
    addFirework: () => {
      set((state) => {
        return {
          fireworks: [
            ...state.fireworks,
            {
              id: `${Date.now()}-${randInt(0, 100)}-${state.fireworks.length}`,
              position: spawns[randInt(0, spawns.length - 1)],
              // ...
            },
          ],
        };
      });
      // ...
    },
  };
});

// ...

Kembang api muncul di posisi peluncur tetapi di bagian bawah setiap peluncur.

Mari kita geser posisi y dari spawns agar kembang api muncul di bagian atas peluncur:

const SPAWN_OFFSET = 0.2;

const spawns = [
  [1.004, -0.001 + SPAWN_OFFSET, 3.284],
  [-2.122, -0.001 + SPAWN_OFFSET, 2.678],
  [-0.988, -0.001 + SPAWN_OFFSET, 3.287],
  [2.888, -0.001 + SPAWN_OFFSET, 1.875],
  [2.115, -0.001 + SPAWN_OFFSET, 2.684],
];

Kita menambahkan konstanta SPAWN_OFFSET untuk menggeser posisi y dari spawns. Lebih mudah untuk menyesuaikan jika diperlukan.

Jauh lebih baik, kembang api sekarang muncul di bagian atas peluncur! Terasa lebih seperti pertunjukan kembang api yang nyata!

SFX

Untuk pengalaman yang lebih mendalam, kita dapat menambahkan efek suara pada kembang api. 🎶

Soundbank

Cara terbaik untuk mendapatkan efek suara adalah dengan merekamnya sendiri dan memprosesnya agar sesuai dengan kebutuhan Anda. Namun, karena kita tidak semua adalah desainer suara, kita bisa menggunakan efek suara yang kita temukan secara online!

Pastikan untuk menghormati lisensi efek suara yang Anda gunakan. Ketika mencari efek suara gratis, seperti model 3D, cari efek suara yang royalty-free.

Pixabay dan Freesound adalah tempat yang baik untuk memulai mencari sumber daya gratis.

Epidemic Sound dan Artlist adalah opsi berbayar yang bagus jika Anda menginginkan pilihan yang lebih luas dan efek suara berkualitas tinggi.

Untuk kembang api kita, saya menemukan:

Unduh dan tambahkan ke proyek Anda dalam folder bernama sfxs.

Positional audio

Daripada hanya membuat komponen Audio standar yang memutarnya saat kembang api diluncurkan, kita akan menggunakan audio yang diposisikan secara spasial agar suaranya mengikuti kembang api.

Untuk melakukannya, kita akan menggunakan PositionalAudio dari Three.js.

Wrapper drei-nya dapat digunakan dengan komponen <PositionalAudio />.

Apa yang dilakukannya adalah berdasarkan posisi komponen PositionalAudio dan pendengarnya (kamera), ini akan menyesuaikan volume dan panning stereo dari suara agar terasa seperti berasal dari posisi komponen.

Mari mulai dengan menambahkan suara peluit ke dalam komponen Firework:

// ...
import { PositionalAudio } from "@react-three/drei";

const Firework = ({ velocity, delay, position, color }) => {
  // ...

  return (
    <>
      <group ref={ref} position={position}>
        {/* Ledakan */}
        <VFXEmitter
        // ...
        />

        {/* Jejak */}
        <PositionalAudio
          url="sfxs/firework-whistle-190306.mp3"
          distance={20}
          loop={false}
          autoplay
        />
        <VFXEmitter
        // ...
        />
      </group>
    </>
  );
};

Kita menambahkan komponen <PositionalAudio /> dengan url dari suara peluit.

Kita set distance ke 20. Ini merepresentasikan jarak maksimum di mana suara bisa terdengar. Di luar jarak ini, suara akan memudar.

Sesuaikan dengan scene dan efek suara Anda.

Suara dimainkan ketika kembang api diluncurkan dan mengikuti kembang api dalam scene!

Sekarang mari kita tambahkan suara untuk ledakan:

import { useEffect } from "react";

const Firework = ({ velocity, delay, position, color }) => {
  const ref = useRef();
  const age = useRef(0);
  const audioRef = useRef();

  useEffect(() => {
    setTimeout(() => {
      audioRef.current?.play();
    }, delay * 1000);
  }, []);

  // ...

  return (
    <>
      <group ref={ref} position={position}>
        {/* Ledakan */}
        <PositionalAudio
          url="sfxs/firecracker-corsair-4-95046.mp3"
          ref={audioRef}
          distance={20}
          loop={false}
          autoplay={false}
        />
        <VFXEmitter
          emitter="firework-particles"
          //
        />

        {/* ... */}
      </group>
    </>
  );
};

Untuk yang satu ini, kita menggunakan useEffect untuk memutar suara setelah delay sebelum kembang api meledak. (Kita mengalikan delay dengan 1000 untuk mengonversinya menjadi milidetik).

Jangan lupa untuk set autoplay ke false untuk mencegah suara diputar ketika komponen dimount.

Suara dimainkan ketika kembang api meledak dan kita bisa merasakan jika meledak di kiri atau kanan!

Memuat Awal Suara

Apakah Anda memperhatikan bahwa adegan menghilang ketika suara dimainkan? Itu karena seluruh adegan kita dibungkus dalam komponen <Suspense />. Ketika suara dimuat, itu merender fallback dari komponen <Suspense /> (dalam kasus kita tidak ada).

Kita memiliki 2 solusi. Bungkus komponen <PositionalAudio /> dalam komponen <Suspense /> dengan fallback atau muat suara di awal aplikasi.

Untuk proyek kita, kita akan memuat suara di awal aplikasi.

Karena tidak ada hook berbasis useLoader untuk suara dalam React Three Fiber, kita akan memasang komponen <positionAudio /> dummy di bagian atas App untuk memuat suara:

// ...
import { Loader, PositionalAudio } from "@react-three/drei";

function App() {
  return (
    <>
      {/* ... */}
      <Canvas shadows camera={{ position: [12, 8, 26], fov: 30 }}>
        <color attach="background" args={["#110511"]} />
        <Suspense>
          <Preloader />
          {/* ... */}
        </Suspense>
      </Canvas>
    </>
  );
}

const Preloader = () => {
  return (
    <>
      <PositionalAudio
        url="sfxs/firework-whistle-190306.mp3"
        autoplay={false}
      />
      <PositionalAudio
        url="sfxs/firecracker-corsair-4-95046.mp3"
        autoplay={false}
      />
    </>
  );
};

export default App;

Dengan memasang komponen <PositionalAudio /> di bagian atas App, adegan akan hanya dirender ketika suara sudah dimuat.

Kami menggunakan autoplay diatur ke false untuk mencegah suara bermain ketika dimuat.

Kita hanya memiliki 2 file audio untuk dimuat, tetapi jika Anda memiliki lebih banyak, Anda dapat membuat array sounds dan memetakan untuk memuat semua suara.

Anda dapat melihat dua mp3 dimuat di awal aplikasi di tab Network dari DevTools. Tidak ada pemuatan lainnya yang dilakukan ketika meluncurkan kembang api.

Tahap pro

Mari kita sisihkan kembang api untuk sesaat dan fokus pada pengaturan panggung dari tampilan.

Untuk benar-benar membuat kembang api bersinar, kita perlu mengatur suasana yang tepat.

Langit Berwarna

Ada banyak cara untuk menciptakan latar belakang dan langit di Three.js. Salah satu solusi favorit saya adalah membuat bola besar yang mengelilingi tampilan dan mengaplikasikan tekstur atau custom shader padanya.

Mari kita buat file GradientSky.jsx:

import { shaderMaterial } from "@react-three/drei";
import { extend } from "@react-three/fiber";
import { useControls } from "leva";
import { BackSide, Color } from "three";
import { degToRad } from "three/src/math/MathUtils.js";

export const GradientSky = () => {
  const { colorBottom, colorTop, colorMiddle, blendMiddle, blendIntensity } =
    useControls("Sky 🌄", {
      colorTop: "#0e1c3e",
      colorMiddle: "#ffa200",
      colorBottom: "#160c2a",
      blendMiddle: {
        value: 0.2,
        min: 0,
        max: 1,
        step: 0.01,
      },
      blendIntensity: {
        value: 0.06,
        min: 0,
        max: 1,
        step: 0.01,
      },
    });

  return (
    <mesh rotation-x={degToRad(-5)} depthWrite={false} depthTest={false}>
      <sphereGeometry args={[40]} />
      <gradientMaterial
        side={BackSide}
        colorBottom={colorBottom}
        colorTop={colorTop}
        colorMiddle={colorMiddle}
        blendMiddle={blendMiddle}
        blendIntensity={blendIntensity}
        toneMapped={false}
        depthWrite={false}
      />
    </mesh>
  );
};

const GradientMaterial = shaderMaterial(
  {
    colorTop: new Color("white"),
    colorBottom: new Color("skyblue"),
    colorMiddle: new Color("pink"),
    blendMiddle: 0.2,
    blendIntensity: 1,
  },
  /* glsl */ `
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
  /* glsl */ `
uniform vec3 colorTop;
uniform vec3 colorBottom;
uniform vec3 colorMiddle;
uniform float blendMiddle;
uniform float blendIntensity;
varying vec2 vUv;
void main() {
  vec3 mixedTop = mix(colorMiddle, colorTop, smoothstep(0.498, 0.502, vUv.y));
  vec3 mixedBottom = mix(colorMiddle, colorBottom, smoothstep(0.502, 0.498, vUv.y));

  vec3 mixedColor = mix(colorBottom, colorTop, smoothstep(0.45, 0.55, vUv.y));
  float blendMiddle = smoothstep(0.5-blendMiddle, 0.5, vUv.y)  * smoothstep(0.5 + blendMiddle, 0.5, vUv.y) * blendIntensity;
  vec3 finalColor = mix(mixedColor, colorMiddle, blendMiddle);
  gl_FragColor = vec4(finalColor, 1.0);
  
}
`
);

extend({ GradientMaterial });

Karena tidak ada yang baru di file ini, saya akan langsung menjelaskannya dengan singkat:

  • Kita membuat komponen GradientSky yang akan menciptakan bola raksasa di sekitar tampilan.
  • Kita menggunakan shaderMaterial untuk membuat material kustom untuk bola tersebut.
  • Kita menggunakan useControls untuk membuat antarmuka guna menyesuaikan warna langit.
  • Kita membuat gradasi dari colorBottom ke colorTop dengan colorMiddle di tengahnya.
  • Kita menggunakan smoothstep untuk menggabungkan warna-warna tersebut dan menciptakan gradasi yang indah.
  • Kita mengatur side ke BackSide karena kita berada di dalam bola dan ingin gradasi terlihat dari dalam.
  • Kita menggunakan extend untuk menambahkan GradientMaterial ke dalam material yang tersedia di React Three Fiber.

Kita sekarang bisa menambahkan komponen GradientSky ke dalam komponen Experience:

// ...
import { GradientSky } from "./GradientSky";

export const Experience = () => {
  const controls = useRef();

  return (
    <>
      {/* ... */}
      <directionalLight
        position={[1, 0.5, -10]}
        intensity={2}
        color="#ffe7ba"
      />

      <GradientSky />
      {/* ... */}
    </>
  );
};

Kini Anda bisa menyesuaikan warna langit sesuai selera Anda dan menciptakan suasana yang indah!

Untuk mendapatkan nuansa lebih luar-angkasa/langit-malaml, Anda bisa menambahkan bintang di langit. Silakan gunakan yang Anda buat di pelajaran sebelumnya atau yang dari drei.

Saya akan menambahkan yang dari drei:

// ...
import { Stars } from "@react-three/drei";

export const Experience = () => {
  const controls = useRef();

  return (
    <>
      {/* ... */}

      <Stars fade speed={3} count={2000} />
      <GradientSky />

      {/* ... */}
    </>
  );
};

Langit dengan bintang

Langit kini dipenuhi dengan beberapa bintang untuk membuatnya semakin magis! ✨

Awan

Untuk menambahkan kedalaman dan suasana, kita dapat menambahkan awan ke dalam adegan.

Kita akan menggunakan komponen Cloud dari drei dalam komponen Experience:

// ...
import { MeshBasicMaterial } from "three";
import { useControls } from "leva";
import { Cloud, Clouds } from "@react-three/drei";

export const Experience = () => {
  const controls = useRef();
  const { cloud1Color, cloud2Color, cloud3Color } = useControls("Clouds ☁️", {
    cloud1Color: "#54496c",
    cloud2Color: "orange",
    cloud3Color: "#9d7796",
  });

  return (
    <>
      {/* ... */}

      <Clouds material={MeshBasicMaterial}>
        <Cloud
          position-z={0}
          position-y={-5}
          seed={2}
          scale={2}
          volume={8}
          color={cloud1Color}
          fade={1000}
        />
        <Cloud
          position-x={12}
          position-z={-10}
          seed={1}
          scale={2}
          volume={6}
          color={cloud2Color}
          fade={800}
        />
        <Cloud
          position-x={-8}
          position-z={10}
          seed={5}
          scale={1}
          volume={12}
          color={cloud3Color}
          fade={100}
        />
      </Clouds>

      {/* ... */}
    </>
  );
};

Bermainlah dengan position, seed, scale, volume, color, dan fade untuk menciptakan awan yang Anda suka.

Saya menambahkan kontrol untuk warna awan agar dapat menyesuaikannya dengan mudah saat menyesuaikan warna langit.

Sky with clouds

Lihat bagaimana sedikit demi sedikit adegan ini mulai terasa hidup! ☁️

Animasi Kamera

Untuk membuat adegan lebih menarik, dinamis, dan untuk melihat kembang api dengan lebih baik, kita dapat menganimasikan kamera.

Pertama, mari kita animasikan kamera saat adegan dimuat agar melihat ke arah pulau:

// ...

import { useEffect } from "react";

export const Experience = () => {
  const controls = useRef();

  useEffect(() => {
    controls.current.setLookAt(0, 15, 10, 0, 25, 0);
    controls.current.setLookAt(12, 8, 26, 4, 0, 0, true);
  }, []);

  // ...
};

Kita mengatur kamera untuk melihat jauh secara instan ketika komponen dimuat, setelah itu kita memanggil metode setLookAt untuk menganimasikan kamera agar melihat ke arah pulau. (Dengan parameter terakhir diset ke true untuk menganimasikan kamera secara halus).

Sekarang kamera kita menganimasikan dengan halus untuk melihat ke arah pulau saat adegan dimuat. Pulau sekarang menjadi pusat perhatian!

Sekarang mari kita animasikan kamera untuk zoom-out saat kembang api diluncurkan:

// ...
import { Fireworks } from "./Fireworks";

export const Experience = () => {
  // ...

  const fireworks = useFireworks((state) => state.fireworks);
  useEffect(() => {
    if (fireworks.length) {
      controls.current.setLookAt(0, 12, 42, 0, 0, 0, true);
    } else {
      controls.current.setLookAt(12, 8, 26, 4, 0, 0, true);
    }
  }, [fireworks]);

  // ...
};

Kita menggunakan useEffect dengan array fireworks sebagai dependensi untuk memicu animasi kamera ketika jumlah kembang api berubah.

Ketika kembang api diluncurkan, kita menganimasikan kamera untuk zoom-out dan melihat kembang api. Ketika tidak ada kembang api, kita menganimasikan kamera untuk melihat lebih dekat ke pulau.

Sekarang kamera melakukan zoom-out saat kembang api diluncurkan untuk melihat ledakan dengan lebih baik!

Tema Warna

Waktunya menambahkan sentuhan akhir ke adegan kita: tema warna untuk kembang api!

Kita memiliki 3 tombol berbeda untuk meluncurkan kembang api:

  • Classic: Warna acak saat ini
  • Love: Warna merah muda dan merah
  • Sea: Warna biru, putih, dan biru kehijauan

Dalam useFireworks.js mari kita ubah array colors sederhana kita menjadi objek themeColors:

const themeColors = {
  classic: [
    ["skyblue", "pink"],
    ["orange", "yellow"],
    ["green", "teal"],
    ["mediumpurple", "indigo"],
    ["#ff9fed", "#e885ff", "#ff85b2", "#d69eff"],
  ],
  love: [
    ["red"],
    ["red", "fuchsia"],
    ["red", "pink"],
    ["pink"],
    ["fuchsia", "yellow"],
  ],
  sea: [
    ["skyblue", "white"],
    ["deepskyblue", "skyblue"],
    ["aquamarine", "mediumaquamarine"],
    ["#368bff"],
  ],
};

Dan dalam fungsi addFirework, kita akan menambahkan parameter firework untuk mendapatkan tema dari kembang api:

addFirework: (firework) => {
  set((state) => {
    const colors = themeColors[firework.theme];
    // ...
  });
  // ...
},

Kita menggunakan firework.theme untuk mendapatkan warna yang tepat dari objek themeColors.

Sekarang dalam UI kita perlu menentukan tema kembang api saat meluncurkannya:

 <button
  onClick={() => addFirework({ theme: "classic" })}
  // ...
>
  🎆 Classic
</button>
<button
  onClick={() => addFirework({ theme: "love" })}
  // ...
>
  💖 Love
</button>
<button
  onClick={() => addFirework({ theme: "sea" })}
  // ...
>
  🌊 Sea
</button>

Dengan mengirimkan objek, kita mempermudah penambahan lebih banyak parameter ke kembang api di masa depan.

Setiap tombol sekarang meluncurkan kembang api dengan tema warna yang berbeda!

Kesimpulan

Kita berhasil! 🎉 Tekan tombol untuk merayakannya! 🎇

Kita telah menciptakan pertunjukan kembang api yang indah di angkasa dengan efek suara, langit berwarna, awan, dan kamera dinamis.

Apa yang kita pelajari dalam pelajaran ini dapat diterapkan untuk menciptakan banyak adegan dan pengalaman berbeda. Kemungkinannya tidak terbatas!

Jenis efek yang paling mirip dengan kembang api adalah konfeti. Ini adalah latihan yang baik untuk mencoba membuat ledakan konfeti dengan mesin VFX. 🎊

Saya tidak sabar untuk melihat efek luar biasa apa yang akan Anda buat dengan mesin VFX! 🚀

Three.js logoReact logo

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
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.