TSL और WebGPU के साथ GPGPU पार्टिकल्स
इस पाठ में, हम 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 तैयार टेम्पलेट है।
चलो गुलाबी 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 घटकों को हटा सकते हैं।
हम स्क्रीन के बीच में एक वर्ग देख सकते हैं, यह सफेद 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
वेक्टर के योग में सेट करते हैं।
क्या यह काम कर रहा है? चलो इसे देखे!
मेडे! सब कुछ सफेद है! ⬜️
थोड़ा ज़ूम-आउट करें?
धन्यवाद, हमें कण दिखाई दे रहे हैं, वे बस बहुत बड़े हैं उन्होंने पूरी स्क्रीन रंग दी! 😮💨
इस समस्या को ठीक करने के लिए 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); // ... }, []); // ... }; // ...
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.