TSL और WebGPU के साथ GPGPU पार्टिकल्स

Starter pack

इस पाठ में, हम Three Shading Language (TSL) और WebGPU का उपयोग करते हुए सैकड़ों हजारों तैरते हुए पार्टिकल्स बनायेंगे ताकि 3D मॉडल और 3D टेक्स्ट को प्रस्तुत किया जा सके।

फेस का उपयोग करने के बजाय, हम बहुत सारे पार्टिकल्स का उपयोग करते हैं, जो हमें विभिन्न मॉडलों के बीच स्मूथ ट्रांजिशन की अनुमति देता है।

GPGPU पार्टिकल्स के साथ प्रस्तुत की गई एक लोमड़ी का 3D मॉडल, एक किताब, और 3D टेक्स्ट! 🚀

GPGPU पार्टिकल सिस्टम

कोड में गोता लगाने से पहले, आइए समझते हैं कि GPGPU क्या है और इसे Three.js में कैसे उपयोग किया जा सकता है।

GPGPU क्या है?

GPGPU (General-Purpose computing on Graphics Processing Units) एक तकनीक है जो GPUs की समानांतर प्रसंस्करण शक्ति का लाभ उठाकर उन गणनाओं को संचालित करती है जो सामान्यतः CPU द्वारा संचालित होती हैं।

Three.js में, GPGPU का अक्सर रियल-टाइम सिमुलेशन, पार्टिकल सिस्टम, और फिजिक्स के लिए उपयोग किया जाता है, डेटा को टेक्सचर में स्टोर और अपडेट करके, बजाय CPU-बाउंड गणनाओं पर निर्भर रहने के।

यह तकनीक शेडर्स को मेमोरी और कंप्यूट क्षमताएं देती है, जो उन्हें जटिल गणनाएं करने और परिणामों को टेक्सचर में स्टोर करने की अनुमति देती है बिना CPU के हस्तक्षेप के।

यह जीपीयू पर सीधे अत्यधिक कुशल, बड़े पैमाने के कम्प्युटेशन्स के लिए अनुमति देता है।

TSL की बदौलत, GPGPU सिमुलेशन बनाना अब कहीं अधिक आसान और सहज हो गया है। स्टोरेज और बफर नोड्स के साथ संयुक्त कंप्यूट फंक्शन्स के साथ, हम न्यूनतम कोड के साथ जटिल सिमुलेशन बना सकते हैं।

यहाँ कुछ परियोजनाओं के विचार हैं जिनके लिए GPGPU का उपयोग किया जा सकता है:

अब समय है थ्योरी से प्रैक्टिस की ओर जाने का! आइए TSL और WebGPU का उपयोग करके एक GPGPU पार्टिकल सिस्टम बनायें।

कण प्रणाली

स्टार्टर पैक WebGPU/TSL पाठ कार्यान्वयन पर आधारित एक WebGPU तैयार टेम्पलेट है।

GPGPU particles starter pack

चलो गुलाबी mesh को GPGPUParticles नामक एक नए घटक से बदलते हैं। src/components फ़ोल्डर में GPGPUParticles.jsx नाम की एक नई फ़ाइल बनाएं और निम्नलिखित कोड जोड़ें:

import { extend } from "@react-three/fiber";
import { useMemo } from "react";
import { color, uniform } from "three/tsl";
import { AdditiveBlending, SpriteNodeMaterial } from "three/webgpu";

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  const { nodes, uniforms } = useMemo(() => {
    // uniforms
    const uniforms = {
      color: uniform(color("white")),
    };

    return {
      uniforms,
      nodes: {
        colorNode: uniforms.color,
      },
    };
  }, []);

  return (
    <>
      <sprite count={nbParticles}>
        <spriteNodeMaterial
          {...nodes}
          transparent
          depthWrite={false}
          blending={AdditiveBlending}
        />
      </sprite>
    </>
  );
};

extend({ SpriteNodeMaterial });

यहाँ कुछ नया नहीं है, हम एक GPGPUParticles घटक बना रहे हैं जो कणों को रेंडर करने के लिए Sprite के साथ SpriteNodeMaterial का उपयोग करता है।

InstancedMesh पर Sprite के उपयोग का लाभ यह है कि यह हल्का होता है और डिफ़ॉल्ट रूप से बिलबोर्ड प्रभाव के साथ आता है।

चलिए GPGPUParticles घटक को Experience घटक में जोड़ें:

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

export const Experience = () => {
  return (
    <>
      {/* <Environment preset="warehouse" /> */}
      <OrbitControls />
      <GPGPUParticles />
      {/* <mesh>
        <boxGeometry />
        <meshStandardMaterial color="hotpink" />
      </mesh> */}
    </>
  );
};

हम mesh और environment घटकों को हटा सकते हैं।

White sprite particles

हम स्क्रीन के बीच में एक वर्ग देख सकते हैं, यह सफेद sprite कण हैं। सभी एक ही स्थिति में हैं।

अब समय है कि हमारी कण प्रणाली को सेटअप किया जाए!

बफर / स्टोरेज / इंस्टैन्स्ड ऐरे

हमारे GPGPU सिमुलेशन के लिए, हमें आवश्यक है कि हमारे कण उनके स्थिति, वेग, आयु, और रंग को बिना CPU के प्रयोग के याद रखें।

कुछ चीजें हमें डेटा संग्रहीत करने की आवश्यकता नहीं होंगी। हम रंग की गणना आयु के आधार पर और uniforms के संयोजन से कर सकते हैं। और हम एक स्थिर seed मान का उपयोग करके वेग को यादृच्छिक रूप से उत्पन्न कर सकते हैं।

लेकिन स्थिति के लिए, क्योंकि लक्ष्य स्थिति बदल सकती है, हमें इसे एक बफर में संग्रहीत करने की आवश्यकता है। उसी तरह आयु के लिए, हम GPU में कणों के जीवन चक्र को संभालना चाहते हैं।

GPU में डेटा संग्रहीत करने के लिए, हम storage node का उपयोग कर सकते हैं। यह हमें बड़े पैमाने पर संरचित डेटा संग्रहीत करने की अनुमति देता है जो GPU पर अपडेट किया जा सकता है।

इसे न्यूनतम कोड के साथ उपयोग करने के लिए, हम InstancedArray TSL फ़ंक्शन का उपयोग करेंगे जो storage node पर निर्भर करता है।

Three.js nodes का यह हिस्सा अभी तक प्रलेखित नहीं है, यह उदाहरण और स्रोत कोड में गोता लगाकर ही हमें समझने में आता है कि यह कैसे काम करता है।

चलो हमारे बफर को तैयार करते हैं useMemo में जहाँ हम अपने शेडर नोड्स रखते हैं:

// ...
import { instancedArray } from "three/tsl";

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  const { nodes, uniforms } = useMemo(() => {
    // uniforms
    const uniforms = {
      color: uniform(color("white")),
    };

    // buffers
    const spawnPositionsBuffer = instancedArray(nbParticles, "vec3");
    const offsetPositionsBuffer = instancedArray(nbParticles, "vec3");
    const agesBuffer = instancedArray(nbParticles, "float");

    return {
      uniforms,
      nodes: {
        colorNode: uniforms.color,
      },
    };
  }, []);

  // ...
};

// ...

instancedArray एक TSL फ़ंक्शन है जो निर्दिष्ट आकार और प्रकार का एक बफर बनाता है।

वही कोड storage node का उपयोग करके इस प्रकार दिखेगा:

import { storage } from "three/tsl";
import { StorageInstancedBufferAttribute } from "three/webgpu";

const spawnPositionsBuffer = storage(
  new StorageInstancedBufferAttribute(nbParticles, 3),
  "vec3",
  nbParticles
);

इन बफर्स के साथ, हम प्रत्येक कण की स्थिति और आयु को संग्रहीत कर सकते हैं और उन्हें GPU में अपडेट कर सकते हैं।

बफर्स में डेटा को एक्सेस करने के लिए, हम .element(index) का उपयोग कर सकते हैं ताकि निर्दिष्ट इंडेक्स पर मूल्य प्राप्त कर सकें।

हमारे केस में हम प्रत्येक कण के instancedIndex का उपयोग करेंगे बफर्स में डेटा एक्सेस करने के लिए:

// ...
import { instanceIndex } from "three/tsl";

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  const { nodes, uniforms } = useMemo(() => {
    // ...

    // buffers
    const spawnPositionsBuffer = instancedArray(nbParticles, "vec3");
    const offsetPositionsBuffer = instancedArray(nbParticles, "vec3");
    const agesBuffer = instancedArray(nbParticles, "float");

    const spawnPosition = spawnPositionsBuffer.element(instanceIndex);
    const offsetPosition = offsetPositionsBuffer.element(instanceIndex);
    const age = agesBuffer.element(instanceIndex);

    return {
      uniforms,
      nodes: {
        colorNode: uniforms.color,
      },
    };
  }, []);

  // ...
};

// ...

instanceIndex एक बिल्ट-इन TSL फ़ंक्शन है जो वर्तमान प्रोसेस किए जा रहे इंस्टैंस का इंडेक्स वापस करता है।

यह हमें प्रत्येक कण के लिए बफर्स में डेटा का एक्सेस प्रदान करता है।

हमें इस प्रोजेक्ट के लिए इसकी आवश्यकता नहीं होगी, लेकिन किसी अन्य इंस्टैंस के डेटा तक पहुँच पाकर, हम कणों के बीच जटिल इंटरैक्शन बना सकते हैं। उदाहरण के लिए, हम एक पक्षी झुंड बना सकते हैं जो एक दूसरे का अनुसरण करते हैं।

प्रारंभिक कम्प्यूट

कणों की स्थिति और आयु को सेटअप करने के लिए, हमें एक compute फ़ंक्शन बनाने की आवश्यकता है जिसे सिमुलेशन की शुरुआत में GPU पर निष्पादित किया जाएगा।

TSL के साथ एक compute फ़ंक्शन बनाने के लिए, हमें Fn नोड का उपयोग करने की आवश्यकता है, इसे कॉल करने के लिए और जो विधि यह लौटाता है उसका उपयोग compute द्वारा कणों की संख्या के साथ करें:

// ...
import { Fn } from "three/src/nodes/TSL.js";

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  const { nodes, uniforms } = useMemo(() => {
    // ...
    const spawnPosition = spawnPositionsBuffer.element(instanceIndex);
    const offsetPosition = offsetPositionsBuffer.element(instanceIndex);
    const age = agesBuffer.element(instanceIndex);

    // init Fn
    const lifetime = randValue({ min: 0.1, max: 6, seed: 13 });

    const computeInit = Fn(() => {
      spawnPosition.assign(
        vec3(
          randValue({ min: -3, max: 3, seed: 0 }),
          randValue({ min: -3, max: 3, seed: 1 }),
          randValue({ min: -3, max: 3, seed: 2 })
        )
      );
      offsetPosition.assign(0);
      age.assign(randValue({ min: 0, max: lifetime, seed: 11 }));
    })().compute(nbParticles);

    // ...
  }, []);

  // ...
};

// ...

हम एक computeInit फ़ंक्शन बनाते हैं जो हमारे बफ़र्स को रैंडम मानों के साथ असाइन करता है।

randValue फ़ंक्शन अस्तित्व में नहीं है, हमें इसे स्वयं बनाना होगा।

हमारे निपटान में फ़ंक्शन्स हैं:

  • hash(seed): एक बीज के आधार पर 0 और 1 के बीच एक यादृच्छिक मान उत्पन्न करने के लिए।
  • range(min, max): न्यूनतम और अधिकतम के बीच एक यादृच्छिक मान उत्पन्न करने के लिए।

Three.js Shading Language Wiki पर अधिक जानकारी।

लेकिन range फ़ंक्शन एक एट्रीब्युट परिभाषित करता है और इसका मूल्य संग्रहीत करता है। जो हमें नहीं चाहिए।

चलो randValue फ़ंक्शन बनाते हैं जो एक बीज के आधार पर न्यूनतम और अधिकतम के बीच एक यादृच्छिक मान लौटाता है:

import { hash } from "three/tsl";

const randValue = /*#__PURE__*/ Fn(({ min, max, seed = 42 }) => {
  return hash(instanceIndex.add(seed)).mul(max.sub(min)).add(min);
});

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  // ...
};
// ...

randValue फ़ंक्शन एक min, max, और seed मान लेता है और एक बीज के आधार पर न्यूनतम और अधिकतम के बीच एक यादृच्छिक मान लौटाता है।

/*#__PURE__*/ एक टिप्पणी है जो वृक्ष-हिलाने के लिए उपयोग की जाती है। यह बंडलर से कहता है कि यदि फ़ंक्शन का उपयोग नहीं किया जाता है तो इसे हटा दे। अधिक विवरण यहाँ

अब हमें अपने computeInit फ़ंक्शन को कॉल करना होगा। यह renderer के लिए एक काम है। चलो इसे useThree के साथ इम्पोर्ट करते हैं और इसकी घोषणा के तुरंत बाद इसे कॉल करते हैं:

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

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  const gl = useThree((state) => state.gl);

  const { nodes, uniforms } = useMemo(() => {
    // ...
    const computeInit = Fn(() => {
      // ...
    })().compute(nbParticles);

    gl.computeAsync(computeInit);

    // ...
  }, []);

  // ...
};

// ...

इसे विज़ुअलाइज़ करने के लिए, हमें SpriteNodeMaterial के positionNode को spawnPosition और offsetPosition बफ़र्स का उपयोग करने के लिए बदलना होगा।

// ...

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  // ...

  const { nodes, uniforms } = useMemo(() => {
    // ...

    return {
      uniforms,
      nodes: {
        positionNode: spawnPosition.add(offsetPosition),
        colorNode: uniforms.color,
      },
    };
  }, []);

  // ...
};

// ...

हम positionNode को spawnPosition और offsetPosition वेक्टर के योग में सेट करते हैं।

क्या यह काम कर रहा है? चलो इसे देखे!

Particles with random positions full white

मेडे! सब कुछ सफेद है! ⬜️

थोड़ा ज़ूम-आउट करें?

Particles with random positions zoomed out

धन्यवाद, हमें कण दिखाई दे रहे हैं, वे बस बहुत बड़े हैं उन्होंने पूरी स्क्रीन रंग दी! 😮‍💨

इस समस्या को ठीक करने के लिए scaleNode को एक यादृच्छिक मान के साथ सेट कर देते हैं:

// ...
import { range } from "three/tsl";

// ...

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  // ...

  const { nodes, uniforms } = useMemo(() => {
    // ...

    const scale = vec3(range(0.001, 0.01));

    return {
      uniforms,
      nodes: {
        positionNode: spawnPosition.add(offsetPosition),
        colorNode: uniforms.color,
        scaleNode: scale,
      },
    };
  }, []);

  return (
    <>
      <sprite count={nbParticles}>
        <spriteNodeMaterial
          {...nodes}
          transparent
          depthWrite={false}
          blending={AdditiveBlending}
        />
      </sprite>
    </>
  );
};

// ...

इस परिदृश्य में, हम range फ़ंक्शन का उपयोग 0.001 और 0.01 के बीच एक यादृच्छिक मान उत्पन्न करने के लिए कर सकते हैं।

परफेक्ट, हमारे पास विभिन्न आकार और स्थान वाले कण हैं! 🎉

यह थोड़ी स्थिर लग रही है, इसमें कुछ गति जोड़ने की जरूरत है।

अपडेट कंप्यूट

जैसे हमने init compute function के लिए किया था, वैसे ही हम एक update compute function बनाएंगे जो प्रत्येक frame पर निष्पादित होगा।

इस function में हम particles के position और age को अपडेट करेंगे:

// ...
import { deltaTime, If } from "three/tsl";

export const GPGPUParticles = ({ nbParticles = 1000 }) => {
  // ...

  const { nodes, uniforms } = useMemo(() => {
    // ...

    const instanceSpeed = randValue({ min: 0.01, max: 0.05, seed: 12 });

    // update Fn
    const computeUpdate = Fn(() => {
      age.addAssign(deltaTime);

      If(age.greaterThan(lifetime), () => {
        age.assign(0);
        offsetPosition.assign(0);
      });

      offsetPosition.addAssign(vec3(instanceSpeed));
    })().compute(nbParticles);

    // ...
  }, []);

  // ...
};

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