VFX इंजन

Starter pack

अब तक, हमने अपने 3D दृश्यों में कणों को बनाने के लिए कस्टम घटकों का निर्माण किया है। अधिकांश समय, हम लगभग वही काम करना चाहते हैं: अंतरिक्ष के एक बिंदु से कणों का उत्सर्जन करना और उन्हें समय के साथ एनिमेट करना। (रंग, आकार, स्थिति, आदि)

एक ही कोड को बार-बार डुप्लिकेट करने के बजाय, हम एक अपेक्षाकृत सामान्य VFX इंजन बना सकते हैं जिसका उपयोग विभिन्न प्रकार के कण प्रभाव बनाने के लिए किया जा सकता है।

इसके साथ कई लाभ आते हैं:

  • पुन: प्रयोज्यता: आप अपने प्रोजेक्ट्स में विभिन्न प्रकार के कण प्रभाव बनाने के लिए उसी इंजन का उपयोग कर सकते हैं।
  • प्रदर्शन: इंजन को अनुकूलित किया जा सकता है ताकि बड़ी संख्या में कणों को प्रभावी ढंग से संभाल सके और कई कण प्रणालियों को एक में मिला सके।
  • लचीलापन: आप इंजन के मापदंडों को बदलकर कणों के व्यवहार को आसानी से वैयक्तिकृत कर सकते हैं।
  • उपयोग में आसानी: आप सिर्फ कुछ कोड की पंक्तियों के साथ जटिल कण प्रभाव बना सकते हैं।
  • कोड का पुनरावृत्ति से बचाव: आपको वही कोड कई बार लिखने की आवश्यकता नहीं है।

हम अगले पाठों में विभिन्न प्रभाव बनाने के लिए इस VFX इंजन का उपयोग करेंगे। जबकि आप इस पाठ को छोड़ सकते हैं और सीधे इंजन का उपयोग कर सकते हैं, यह समझना कि यह कैसे काम करता है, आपको आपके 3D प्रोजेक्ट्स में प्रदर्शन और लचीलापन को अधिक गहराई से समझने में मदद करेगा।

क्या आप अपना VFX इंजन बनाने के लिए तैयार हैं? चलिए शुरू करते हैं!

GPU कण

पिछले पाठों में हमने देखा है कि हम अपने 3D दृश्यों में नियंत्रित कण बनाने के लिए drei से <Instances /> घटक का उपयोग कैसे कर सकते हैं।

लेकिन इस दृष्टिकोण की एक मुख्य सीमा है: हम जिन कणों को संभाल सकते हैं उनकी संख्या CPU द्वारा सीमित होती है। जितने अधिक कण होंगे, CPU को उन्हें संभालना होगा, जिससे प्रदर्शन संबंधी समस्याएँ हो सकती हैं।

यह इस तथ्य के कारण है कि अंदर अंदर, <Instances /> घटक अपने useFrame लूप में प्रत्येक <Instance /> की स्थिति, रंग और आकार प्राप्त करने के लिए गणना करता है। आप कोड यहां देख सकते हैं।

हमारे VFX इंजन के लिए, हम चाहेंगे कि हम <Instances /> घटक के साथ जितने कणों को संभाल सकते हैं, उससे अधिक कण उत्पन्न कर सकें। हम कणों की स्थिति, रंग और आकार को संभालने के लिए GPU का उपयोग करेंगे। जिससे हम बिना किसी प्रदर्शन संबंधी समस्याओं के सैकड़ों हज़ारों कणों (मिलियन? 👀) को संभाल सकते हैं।

Instanced Mesh

जबकि हम particles बनाने के लिए Sprite या Points का उपयोग कर सकते थे, हम InstancedMesh का उपयोग करेंगे।

यह हमें केवल points या sprites जैसी सरल आकृतियाँ ही नहीं बल्कि cubes, spheres, और कस्टम ज्यामितीय आकृतियों जैसे 3D आकृतियों को रेंडर करने की सुविधा देता है।

आइए एक नए vfxs फोल्डर में 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>
    </>
  );
};

हम geometry बनाते हैं जो प्रत्येक particle के लिए उपयोग की जाएगी। इस मामले में, हम दोनों दिशाओं में 0.5 के आकार के साथ एक सरल plane geometry का उपयोग करते हैं। बाद में हम एक prop जोड़ेंगे ताकि कोई भी ज्यामिति पास कर सकें।

instancedMesh घटक तीन तर्क लेता है:

  • particles की geometry
  • particles का material। हमने इसे घटक के अंदर घोषित रूप से परिभाषित करने के लिए null पास किया।
  • घटक कितने instances को संभाल सकेगा। हमारे लिए यह प्रदर्शित होने वाले particles की अधिकतम संख्या का प्रतिनिधित्व करता है।

चलो Experience.jsx फाइल में हमारे VFXParticles घटक के साथ नारंगी cube को बदलते हैं:

// ...
import { VFXParticles } from "./vfxs/VFXParticles";

export const Experience = () => {
  return (
    <>
      {/* ... */}
      <VFXParticles />
    </>
  );
};

दृश्य के बीच में नारंगी particle

आप दृश्य के बीच में एक नारंगी particle देख सकते हैं। यह हमारा VFXParticles घटक है।

हमारे particles की संख्या 1000 पर सेट है, लेकिन हम केवल एक को देख सकते हैं। ऐसा इसलिए है क्योंकि सभी एक ही स्थान (0, 0, 0) पर रेंडर होते हैं। चलो इसे बदलते हैं।

इंस्टेंस मैट्रिक्स

इंस्टेंस मेष प्रत्येक इंस्टेंस की स्थिति, रोटेशन, और स्केल को परिभाषित करने के लिए एक मैट्रिक्स का उपयोग करता है। हमारे मेष की instanceMatrix प्रॉपर्टी को अपडेट करके, हम प्रत्येक पार्टिकल को व्यक्तिगत रूप से मूव, रोटेट और स्केल कर सकते हैं।

प्रत्येक इंस्टेंस के लिए, मैट्रिक्स एक 4x4 मैट्रिक्स होता है जो पार्टिकल के ट्रांसफॉर्मेशन का प्रतिनिधित्व करता है। Three.js की Matrix4 क्लास हमें मैट्रिक्स को compose और decompose करने की अनुमति देती है ताकि पार्टिकल की स्थिति, रोटेशन, और स्केल को अधिक पढ़ने योग्य तरीके से सेट/गेट किया जा सके।

VFXParticles घोषणा के शीर्ष पर, चलो कुछ डमी चर घोषित करते हैं ताकि वेक्टर्स और मैट्रिक्स को बार-बार पुनः नहीं बनाना पड़े:

// ...
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();

अब चलो एक emit फंक्शन बनाते हैं ताकि हमारे पार्टिकल्स को सेटअप किया जा सके:

// ...
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);
  }, []);
  // ...
};

emit फंक्शन उन पार्टिकल्स की संख्या पर लूप करता है जिन्हें हम उत्पन्न करना चाहते हैं, और प्रत्येक पार्टिकल के लिए एक रैंडम स्थिति, रोटेशन, और स्केल सेट करता है। फिर हम इन मूल्यों के साथ मैट्रिक्स को जोड़ते हैं और इसे मौजूदा इंडेक्स पर इंस्टेंस पर सेट करते हैं।

सीन में रैंडम पार्टिकल्स

आप सीन में रैंडम पार्टिकल्स देख सकते हैं। प्रत्येक पार्टिकल की एक रैंडम स्थिति, रोटेशन, और स्केल होती है।

हमारे पार्टिकल्स को एनिमेट करने के लिए हम लाइफटाइम, स्पीड, डायरेक्शन जैसी विशेषताएँ परिभाषित करेंगे ताकि गणना GPU पर की जा सके।

ऐसा करने से पहले, हमें इन विशेषताओं को संभालने के लिए एक कस्टम शेडर मटेरियल पर स्विच करना होगा क्योंकि हमारे पास meshBasicMaterial के विशेषताओं पर पहुँच और नियंत्रण नहीं है।

पार्टिकल्स मटेरियल

हमारा पहला लक्ष्य यह होगा कि meshBasicMaterial और हमारे नए shaderMaterial के बीच कोई बदलाव न दिखाई दे। हम एक साधारण शेडर मटेरियल बनाएंगे जो अभी जिस तरह से meshBasicMaterial पार्टिकल्स को रेंडर करता है, वैसे ही रेंडर करेगा।

VFXParticles कौम्पोनेंट में, चलिए एक नया शेडर मटेरियल बनाते हैं:

// ...
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 });

यह एक बहुत ही साधारण शेडर मटेरियल है जो एक color यूनिफॉर्म लेता है और इस रंग के साथ पार्टिकल्स को रेंडर करता है। यहाँ नया सिर्फ़ instanceMatrix है जिसका उपयोग हम प्रत्येक पार्टिकल का position, rotation, और scale प्राप्त करने के लिए करते हैं।

ध्यान दें कि हमें instanceMatrix एट्रीब्यूट घोषित करने की आवश्यकता नहीं थी क्योंकि यह instancing का उपयोग करने पर WebGLProgram का एक इनबिल्ट एट्रीब्यूट है। और जानकारी यहाँ पाई जा सकती है।

चलो meshBasicMaterial को हमारे नए ParticlesMaterial से बदलते हैं:

<instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}>
  <particlesMaterial color="orange" />
</instancedMesh>

सही! हमारे position, rotation, और scale अभी भी अपेक्षित रूप से काम कर रहे हैं। पार्टिकल्स को थोड़ा अलग नारंगी रंग के साथ रेंडर किया गया है। इसका कारण यह है कि हमने अपने शेडर मटेरियल में environment को ध्यान में नहीं रखा है। चीजों को सरल बनाए रखने के लिए, हम इसे इस तरह रखेंगे।

सही! हमारे position, rotation, और scale अभी भी अपेक्षित रूप से काम कर रहे हैं। पार्टिकल्स को थोड़ा अलग नारंगी रंग के साथ रेंडर किया गया है। इसका कारण यह है कि हमने अपने शेडर मटेरियल में environment को ध्यान में नहीं रखा है। चीजों को सरल बनाए रखने के लिए, हम इसे इस तरह रखेंगे।

अब हम अपनी पार्टिकल्स में कस्टम एट्रीब्यूट जोड़ने के लिए तैयार हैं ताकि उन्हें एनिमेट किया जा सके।

इंस्टेंस्ड बफ़र एट्रिब्यूट्स

अब तक, हमने केवल instanceMatrix एट्रिब्यूट का उपयोग किया है, अब हम प्रत्येक कण पर अधिक नियंत्रण प्राप्त करने के लिए कस्टम एट्रिब्यूट्स जोड़ेंगे।

इसके लिए, हम InstancedBufferAttribute का उपयोग करेंगे जो कि Three.js से है।

हम अपने कणों के लिए निम्नलिखित एट्रिब्यूट्स जोड़ेंगे:

  • instanceColor: एक वेक्टर3 जो कण का रंग प्रदर्शित करता है।
  • instanceColorEnd: एक वेक्टर3 जो दर्शाता है कि समय के साथ रंग में क्या परिवर्तन होगा।
  • instanceDirection: एक वेक्टर3 जो दर्शाता है कि कण किस दिशा में चलेगा।
  • instanceSpeed: एक फ्लोट जो बताता है कि कण अपनी दिशा में कितनी तेजी से चलेगा।
  • instanceRotationSpeed: एक वेक्टर3 जो कण के प्रति एक्सिस पर घूर्णन गति को निर्धारित करेगा।
  • instanceLifetime: एक वेक्टर2 जो कण की उम्र को निर्धारित करेगा। पहला मान (x) प्रारंभ समय है, और दूसरा मान (y) उम्र/अवधि है। एक समय यूनिफ़ॉर्म के साथ मिलकर, हम उम्र, प्रगति, और यह जांच सकते हैं कि कण जीवित है या मृत

आइए हमारे एट्रिब्यूट्स के लिए विभिन्न बफ़र बनाएं:

// ...
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),
  });

  // ...
};

// ...

मैं हमारे एट्रिब्यूट्स के लिए विभिन्न बफ़र बनाने के लिए useState का उपयोग कर रहा हूँ ताकि प्रत्येक रेंडर पर इन्हें फिर से न बनाना पड़े। मैंने useMemo हुक का उपयोग नहीं चुना क्योंकि घटक के जीवनचक्र के दौरान कणों की अधिकतम संख्या को बदलना कुछ ऐसा नहीं है जिसे हम संभालना चाहते हैं।

Float32Array का उपयोग एट्रिब्यूट्स के मूल्य को संग्रहीत करने के लिए किया जाता है। हम एट्रिब्यूट की घटकों की संख्या के साथ कणों की संख्या को गुणा करते हैं ताकि एरे में कुल मूल्यों की संख्या प्राप्त हो सके।

Schema explaining the instanceColor attribute

instanceColor एट्रिब्यूट में, पहले 3 मान पहले कण का रंग प्रदर्शित करेंगे, अगले 3 मान दूसरे कण का रंग प्रदर्शित करेंगे, और इसी तरह।

चलिए InstancedBufferAttribute के साथ परिचित हो जाते हैं और इसका उपयोग कैसे करें। इसके लिए, हम 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>
    </>
  );
};

हमारे <instancedMesh /> में, हम instanceColor एट्रिब्यूट को परिभाषित करने के लिए <instancedBufferAttribute /> अवयव जोड़ते हैं। हम इसे जियोमेट्री geometry-attributes-instanceColor एट्रिब्यूट से संलग्न करते हैं। हम attributeArrays.instanceColor एरे को डेटा स्रोत के रूप में पारित करते हैं, itemSize को 3 पर सेट करते हैं क्योंकि हमारे पास एक वेक्टर3 है, और count को nbParticles पर सेट करते हैं।

usage प्रॉप्स को DynamicDrawUsage पर सेट किया जाता है ताकि रेंडरर को बताया जा सके कि डेटा अक्सर अपडेट किया जाएगा। अन्य संभावित मूल्य और अधिक विवरण यहां पाया जा सकता है।

हम उन्हें हर फ्रेम पर अपडेट नहीं करेंगे, लेकिन हर बार जब हम नए कण छोड़ते हैं, डेटा अपडेट होगा। इसे DynamicDrawUsage के रूप में पर्याप्त माना जाता है।

सही, चलिए अपनी फ़ाइल के शीर्ष पर एक डमी tmpColor वेरिएबल बनाते हैं ताकि कणों के रंग को नियंत्रित किया जा सके:

// ...

const tmpColor = new Color();

अब चलिए emit फ़ंक्शन को अपडेट करते हैं ताकि 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);
  }
};

हम मेष की जियोमेट्री से instanceColor एट्रिब्यूट प्राप्त करके शुरुआत करते हैं। इसके बाद, हम उन कणों की संख्या पर एक लूप लगाते हैं जिन्हें हम उत्सर्जित करना चाहते हैं और प्रत्येक कण के लिए एक यादृच्छिक रंग सेट करते हैं।

आइए particlesMaterial को instanceColor एट्रिब्यूट का उपयोग करने के लिए अपडेट करें, न कि रंग यूनिफ़ॉर्म का:

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);
}`
);
// ...

हमने वर्टेक्स शेडर में attribute vec3 instanceColor; जोड़ा है और फ्रैगमेंट शेडर को रंग देने के लिए vColor वेरियिंग को सेट किया है। इसके बाद हम gl_FragColor को vColor पर सेट करते हैं ताकि कण अपने रंग के साथ रेंडर हों।

Random particles in the scene with random colors

हमने सफलतापूर्वक प्रत्येक कण के लिए एक रैंडम रंग सेट किया। कण उनकी रंग के साथ रेंडर हो रहे हैं।

सही, चलिए हमारे कणों में अन्य एट्रिब्यूट्स जोड़ें। सबसे पहले, हमारे emit फ़ंक्शन को अपडेट करें ताकि instanceColorEnd, instanceDirection, instanceLifetime, instanceSpeed, और instanceRotationSpeed एट्रिब्यूट्स को यादृच्छिक मूल्यों के साथ सेट किया जा सके:

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);
  }
};

और प्रत्येक एट्रिब्यूट के लिए instancedBufferAttribute अवयव बनाएं:

<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>

अब यह समय है कि हम अपने कणों में जीवन भरें और उनके गति, रंग और उम्र संबंधी तर्कों को लागू करें।

कणों की जीवन अवधि

हमारे कणों के व्यवहार की गणना करने के लिए, हमें अपने shader में पारित समय को भेजने की आवश्यकता है। हम shaderMaterial के uniforms प्रॉप का उपयोग समय को पास करने के लिए करेंगे।

आइए अपने ParticlesMaterial को अपडेट करते हैं और uTime uniform जोड़ते हैं:

const ParticlesMaterial = shaderMaterial(
  {
    uTime: 0,
  },
  /* glsl */ `
uniform float uTime;
// ...
`,
  /* glsl */ `
// ...
`
);

और useFrame लूप में, हम 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;
  });

  // ...
};

वर्टेक्स shader में, हम प्रत्येक कण की age और progress की गणना करेंगे uTime यूनिफॉर्म और instanceLifetime एट्रीब्यूट के आधार पर। हम फ्रैगमेंट shader में प्रगति पास करेंगे ताकि हम कणों को एनिमेट कर सकें जो एक varying के माध्यम से जिसे 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;
}

age की गणना startTime को uTime से घटाकर की जाती है। फिर प्रगति की गणना age को duration से विभाजित करके की जाती है।

अब फ्रैगमेंट shader में, हम कणों के रंग को instanceColor और instanceColorEnd के बीच प्रगति के आधार पर इंटरपोलिट करेंगे:

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);
}

समय के साथ रंग बदलते कण

हम देख सकते हैं कि कण समय के साथ अपना रंग बदल रहे हैं लेकिन हम एक समस्या का सामना कर रहे हैं। सभी कण शुरू में दिखाई देते हैं जबकि उनका प्रारंभिक समय रैंडम है। हमें उन कणों को छिपाने की ज़रूरत है जो अभी तक जीवित नहीं हैं।

अजन्मे और मृत कणों को रेंडरिंग से बचाने के लिए, हम फ्रैगमेंट shader में discard कीवर्ड का उपयोग करेंगे:

// ...
void main() {
  if (vProgress < 0.0 || vProgress > 1.0) {
    discard;
  }
  // ...
}

discard कीवर्ड रेंडरर से बताता है कि वर्तमान फ्रैगमेंट को छोड़ दें और उसे रेंडर न करें।

बढ़िया, हमारे कण अब जन्म लेते हैं, जीते हैं, और समय के साथ मर जाते हैं। अब हम गति और घूर्णन तर्क जोड़ सकते हैं।

कणों का गति

कणों के दिशा, गति और आयु का उपयोग करके, हम समय के साथ उनकी स्थिति की गणना कर सकते हैं।

वर्टेक्स शेडर में, आइए gl_Position को कणों की दिशा और गति को ध्यान में रखते हुए समायोजित करें।

सबसे पहले, हम दिशा को सामान्य करते हैं ताकि कण तेजी से न चलें जब दिशा एक युनिट वेक्टर न हो:

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;

अंत में, हम modelViewMatrix को finalPosition पर लागू करके मॉडल देखने की स्थिति mvPosition प्राप्त करते हैं ताकि स्थिति को वर्ल्ड स्पेस में बदल सकें:

vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0);

और प्रोजेक्शनमैट्रिक्स को लागू करके वर्ल्ड स्थिति को कैमरा स्पेस में बदलें:

gl_Position = projectionMatrix * mvPosition;

अब तक का हमारा पूरा वर्टेक्स शेडर यहां है:

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;
}

कण अब विभिन्न दिशाओं में विभिन्न गतियों से चल रहे हैं। यह अराजक है, लेकिन यह हमारे रैंडम वैल्यू के कारण है।

आइए हमारे रैंडम वैल्यू को emit फंक्शन में समायोजित करके कणों की गति का स्पष्ट दृश्य प्राप्त करें:

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);
}

आकार लेने लगा है!

हम बाद में चर को समायोजित करने के लिए सरल UI नियंत्रण जोड़ेंगे। अब चलो घूर्णन तर्क जोड़कर कणों को अंतिम रूप दें।

जबकि हम आंदोलन के लिए दिशा और गति को अलग करते हैं, घूर्णन के लिए हम धुरी की प्रति धुरी घूर्णन गति को परिभाषित करने के लिए एकल instanceRotationSpeed एट्रिब्यूट का उपयोग करेंगे।

वर्टेक्स शेडर में हम घूर्णन गति और आयु के आधार पर कण की घूर्णन की गणना कर सकते हैं:

vec3 rotationSpeed = instanceRotationSpeed * age;

फिर, इस "ऑफसेट रोटेशन" को कण पर लागू करने में सक्षम होने के लिए, हमें इसे रोटेशन मैट्रिक्स में बदलना होगा:

mat4 rotX = rotationX(rotationSpeed.x);
mat4 rotY = rotationY(rotationSpeed.y);
mat4 rotZ = rotationZ(rotationSpeed.z);
mat4 rotationMatrix = rotZ * rotY * rotX;

rotationX, rotationY, और rotationZ फ़ंक्शन हैं जो क्रमशः X, Y, और Z धुरी के चारों ओर एक रोटेशन मैट्रिक्स लौटाते हैं। हम उन्हें वर्टेक्स शेडर में main फंक्शन के संबंध में परिभाषित करेंगे:

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
  );
}

रोटेशन मैट्रिक्स के बारे में अधिक जानने के लिए, आप यह विकिपीडिया लेख, साइमन देव का शानदार Game Math Explained Simply या The Book of Shaders का Matrix section देख सकते हैं।

अंततः, हम कण की प्रारंभिक स्थिति पर रोटेशन मैट्रिक्स लागू कर सकते हैं:

vec4 startPosition = modelMatrix * instanceMatrix * rotationMatrix * vec4(position, 1.0);

आइए इसे आजमाएं:

कण अब चल रहे हैं, रंग बदल रहे हैं और घूम रहे हैं! ✨

सही, हमारे पास VFX Engine के लिए एक ठोस आधार है। अधिक सुविधाएँ और नियंत्रण जोड़ने से पहले, आइए इंजन के दूसरे महत्वपूर्ण हिस्से को तैयार करें: एमिटर

एमिटर

उदाहरणों और ट्यूटोरियल्स में यह अक्सर पार्टिकल सिस्टम का नजरअंदाज किया गया हिस्सा होता है। लेकिन यह आपके प्रोजेक्ट्स में पार्टिकल्स को आसानी से और प्रभावशाली ढंग से इंटीग्रेट करने के लिए एक महत्वपूर्ण हिस्सा है:

  • आसानी से क्योंकि आपका <VFXParticles /> कॉम्पोनेंट आपके हाइरार्की के शीर्ष पर होगा और आपका एमिटर आपके सीन के किसी भी सब-कॉम्पोनेंट से इसे स्पॉन कर सकता है। जिससे इसे किसी विशिष्ट बिंदु से स्पॉन करना, एक चलती हुई वस्तु से जोड़ना, या चलती हुई हड्डी से जोड़ना सरल हो जाता है।
  • प्रभावशाली ढंग से क्योंकि हर बार जब आप पार्टिकल्स स्पॉन करना चाहते हैं तो इंस्टेंस्ड मेषेस, शेडर मटेरियल्स कम्पाइल करना, और एट्रिब्यूट्स सेट करने की बजाय, आप एक ही VFXParticles कॉम्पोनेंट का पुन: उपयोग कर सकते हैं और बस एक फंक्शन कॉल कर सकते हैं जो मनचाही सेटिंग्स के साथ पार्टिकल्स स्पॉन करेगा।

useVFX

हम चाहते हैं कि हमारी VFXParticles कॉम्पोनेंट से emit फंक्शन को हमारे प्रोजेक्ट में कहीं से भी कॉल किया जा सके। इसके लिए, हम useVFX नामक एक कस्टम हुक बनाएंगे जो emitter को VFXParticles कॉम्पोनेंट से रजिस्टर और अनरजिस्टर करने का ध्यान रखेगा।

हम Zustand का उपयोग करेंगे क्योंकि यह React में ग्लोबल स्टेट को शानदार प्रदर्शन के साथ प्रबंधित करने का एक सरल और प्रभावी तरीका है।

इसे हमारे प्रोजेक्ट में जोड़ें:

yarn add zustand

हमारे vfxs फोल्डर में, एक 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);
  },
}));

इसमें क्या है:

  • emitters: एक ऑब्जेक्ट जो हमारे VFXParticles कॉम्पोनेंट्स से सभी एमिटर को स्टोर करेगा।
  • registerEmitter: एक फंक्शन जो दिए गए नाम के साथ एमिटर को रजिस्टर करने के लिए है।
  • unregisterEmitter: एक फंक्शन जो दिए गए नाम के साथ एमिटर को अनरजिस्टर करने के लिए है।
  • emit: एक फंक्शन जो हमारे प्रोजेक्ट में कहीं से भी दिए गए नाम के साथ और पैरामीटर्स के साथ एमिटर को कॉल करने के लिए है।

इसे हमारे VFXParticles कॉम्पोनेंट में जोड़ें:

// ...
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);
    };
  }, []);
  // ...
};

// ...

हम अपने VFXParticles कॉम्पोनेंट में एक name प्रॉप जोड़ते हैं ताकि एमिटर की पहचान की जा सके। फिर हम useVFX हुक का उपयोग करते हैं ताकि registerEmitter और unregisterEmitter फंक्शन प्राप्त हो सके।

हम useEffect हुक के अंदर name और emit फंक्शन के साथ registerEmitter को कॉल करते हैं ताकि जब कॉम्पोनेंट माउंट हो तो एमिटर रजिस्टर हो जाए और जब अनमाउंट हो तो अनरजिस्टर हो जाए।

Experience कॉम्पोनेंट में, हमारे VFXParticles कॉम्पोनेंट में name प्रॉप जोड़ें:

// ...
import { VFXParticles } from "./vfxs/VFXParticles";

export const Experience = () => {
  return (
    <>
      {/* ... */}
      <VFXParticles name="sparks" />
    </>
  );
};

VFXEmitter

अब जबकि हमारे पास हमारा useVFX hook है, हम एक VFXEmitter घटक बना सकते हैं जो हमारे VFXParticles घटक से कण निकालने के लिए जिम्मेदार होगा।

vfxs फ़ोल्डर में, आइए एक 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} />
      </>
    );
  }
);
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.