1. परिचय
WebGPU क्या है?
WebGPU, वेब ऐप्लिकेशन में जीपीयू की सुविधाओं को ऐक्सेस करने के लिए, एक नया और आधुनिक एपीआई है.
मॉडर्न एपीआई
WebGPU से पहले, WebGL उपलब्ध था. इसमें WebGPU की सुविधाओं का एक सबसेट शामिल था. इसने समृद्ध वेब सामग्री की एक नई श्रेणी को सक्षम बनाया और डेवलपर ने इसके साथ शानदार चीज़ें तैयार की हैं. हालांकि, यह 2007 में रिलीज़ किए गए OpenGL ES 2.0 एपीआई पर आधारित था, जो कि इससे भी पुराने OpenGL API पर आधारित था. इस दौरान, जीपीयू का काफ़ी विकास हुआ है. साथ ही, उनसे इंटरफ़ेस करने के लिए इस्तेमाल किए जाने वाले नेटिव एपीआई भी Direct3D 12, Metal, और Vulkan के साथ-साथ बेहतर हुए हैं.
WebGPU, इन मॉडर्न एपीआई को बेहतर बनाने के लिए, वेब प्लैटफ़ॉर्म का इस्तेमाल करता है. यह क्रॉस-प्लैटफ़ॉर्म तरीके से जीपीयू की सुविधाओं को चालू करने पर फ़ोकस करता है. साथ ही, यह ऐसा एपीआई उपलब्ध कराता है जो वेब पर आसानी से इस्तेमाल किया जा सकता है. यह एपीआई, उन कुछ नेटिव एपीआई की तुलना में कम शब्दों में काम करता है जिन पर इसे बनाया गया है.
रेंडरिंग
जीपीयू का इस्तेमाल, आम तौर पर तेज़ी से और ज़्यादा जानकारी वाले ग्राफ़िक को रेंडर करने के लिए किया जाता है. WebGPU भी इसी तरह का है. इसमें ऐसी सुविधाएं हैं जो डेस्कटॉप और मोबाइल जीपीयू, दोनों पर आज की सबसे लोकप्रिय रेंडरिंग तकनीकों के साथ काम करने के लिए ज़रूरी हैं. साथ ही, यह आने वाले समय में हार्डवेयर क्षमताओं के बढ़ने के साथ-साथ नई सुविधाएं जोड़ने का रास्ता भी उपलब्ध कराती है.
Compute
WebGPU, रेंडरिंग के अलावा, आपके जीपीयू को सामान्य कामों के लिए इस्तेमाल करने की क्षमता को अनलॉक करता है. साथ ही, इससे एक साथ कई काम भी किए जा सकते हैं. इन कंप्यूट शेडर का इस्तेमाल, अलग से किसी रेंडरिंग कॉम्पोनेंट के बिना या आपकी रेंडरिंग पाइपलाइन के एक बेहतर तरीके से इंटिग्रेट किए गए हिस्से के तौर पर किया जा सकता है.
आज के कोडलैब में, आपको WebGPU की रेंडरिंग और कंप्यूट की सुविधाओं का फ़ायदा पाने का तरीका पता चलेगा. इससे, आपको शुरुआती प्रोजेक्ट बनाने में मदद मिलेगी!
आपको क्या बनाना है
इस कोडलैब में, WebGPU का इस्तेमाल करके Conway's Game of Life बनाया जाता है. आपका ऐप्लिकेशन:
- आसान 2D ग्राफ़िक्स बनाने के लिए, WebGPU की रेंडरिंग क्षमताओं का इस्तेमाल करें.
- सिम्युलेशन करने के लिए, WebGPU की कंप्यूट क्षमताओं का इस्तेमाल करें.
गेम ऑफ़ लाइफ़ को सेल्युलर ऑटोमेटॉन कहा जाता है. इसमें सेल के ग्रिड की स्थिति, समय के साथ कुछ नियमों के आधार पर बदलती रहती है. Game of Life की सेल चालू या बंद हो जाती हैं. यह इस बात पर निर्भर करता है कि उनके आस-पास की कितनी सेल ऐक्टिव हैं. इस वजह से, गेम में दिलचस्प पैटर्न बन जाते हैं, जो देखने के दौरान बदलते रहते हैं.
आपको इनके बारे में जानकारी मिलेगी
- WebGPU सेट अप करने और कैनवस कॉन्फ़िगर करने का तरीका.
- आसान 2D ज्यामिति कैसे बनाएं.
- दिखाई जा रही चीज़ों में बदलाव करने के लिए वर्टेक्स और फ़्रैगमेंट शेडर का इस्तेमाल कैसे करें.
- आसान सिम्युलेशन करने के लिए, कंप्यूट शेडर का इस्तेमाल करने का तरीका.
इस कोडलैब में, WebGPU के बुनियादी कॉन्सेप्ट के बारे में बताया गया है. इसका मकसद एपीआई की पूरी समीक्षा करना नहीं है. साथ ही, इसमें 3D मैट्रिक्स मैथ जैसे अक्सर इस्तेमाल होने वाले विषयों को शामिल नहीं किया गया है या इनकी ज़रूरत नहीं है.
आपको इन चीज़ों की ज़रूरत होगी
- ChromeOS, macOS या Windows पर, Chrome का नया वर्शन (113 या इसके बाद का वर्शन). WebGPU, क्रॉस-ब्राउज़र और क्रॉस-प्लैटफ़ॉर्म एपीआई है. हालांकि, इसे अभी तक सभी जगह उपलब्ध नहीं कराया गया है.
- एचटीएमएल, JavaScript, और Chrome DevTools के बारे में जानकारी.
WebGL, Metal, Vulkan या Direct3D जैसे अन्य ग्राफ़िक्स एपीआई के बारे में जानना ज़रूरी नहीं है. हालांकि, अगर आपने इनके बारे में कुछ जाना है, तो आपको WebGPU में कई चीज़ें मिलती-जुलती दिखेंगी. इससे आपको WebGPU के बारे में जानने में मदद मिल सकती है!
2. सेट अप करें
कोड पाना
इस कोडलैब के लिए किसी भी कोड की ज़रूरत नहीं होती. यह आपको WebGPU ऐप्लिकेशन बनाने के सभी ज़रूरी चरणों की जानकारी देता है, ताकि शुरू करने के लिए आपको किसी कोड की ज़रूरत न पड़े. हालांकि, https://glitch.com/edit/#!/your-first-webgpu-app पर, कुछ ऐसे काम करने वाले उदाहरण उपलब्ध हैं जो चेकपॉइंट के तौर पर काम कर सकते हैं. अगर आपको कोई समस्या आती है, तो इन उदाहरणों को देखें और उनका रेफ़रंस लें.
डेवलपर कंसोल का इस्तेमाल करें!
WebGPU एक काफ़ी जटिल एपीआई है. इसमें कई नियम हैं, जो सही इस्तेमाल को लागू करते हैं. इससे भी बुरा यह है कि एपीआई के काम करने के तरीके की वजह से, यह कई गड़बड़ियों के लिए सामान्य JavaScript अपवाद नहीं दिखा सकता. इससे यह पता लगाना मुश्किल हो जाता है कि समस्या कहां से आ रही है.
WebGPU का इस्तेमाल करते समय, आपको समस्याओं का सामना करना पड़ेगा. खास तौर पर, नए उपयोगकर्ताओं के लिए यह कोई समस्या नहीं है! एपीआई इस्तेमाल करने वाले डेवलपर जानते हैं कि जीपीयू डेवलपमेंट में क्या चुनौतियां आती हैं. साथ ही, उन्होंने यह पक्का करने के लिए कड़ी मेहनत की है कि आपके WebGPU कोड की वजह से कभी भी गड़बड़ी होने पर, आपको डेवलपर कंसोल में ज़्यादा जानकारी वाले और मददगार मैसेज मिलेंगे. ये मैसेज, समस्या को पहचानने और उसे हल करने में आपकी मदद करेंगे.
किसी भी वेब ऐप्लिकेशन पर काम करते समय कंसोल को खुला रखना हमेशा मददगार होता है, लेकिन यह खास तौर पर यहां लागू होता है!
3. WebGPU शुरू करना
<canvas>
से शुरू करें
अगर आपको सिर्फ़ कैलकुलेशन करने के लिए WebGPU का इस्तेमाल करना है, तो स्क्रीन पर कुछ दिखाए बिना भी इसका इस्तेमाल किया जा सकता है. हालांकि, अगर आपको कुछ रेंडर करना है, जैसे कि हम कोडलैब में करने जा रहे हैं, तो आपको कैनवस की ज़रूरत होगी. इसलिए, शुरुआत करने के लिए यह एक अच्छी जगह है!
सिंगल <canvas>
एलिमेंट वाला नया एचटीएमएल दस्तावेज़ बनाएं. साथ ही, <script>
टैग बनाएं जहां हम कैनवस एलिमेंट के लिए क्वेरी करते हैं. (या glitch से 00-starter-page.html का इस्तेमाल करें.)
- इस कोड की मदद से
index.html
फ़ाइल बनाएं:
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGPU Life</title>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script type="module">
const canvas = document.querySelector("canvas");
// Your WebGPU code will begin here!
</script>
</body>
</html>
अडैप्टर और डिवाइस का अनुरोध करना
अब आप WebGPU बिट के बारे में जान सकते हैं! सबसे पहले, आपको यह ध्यान रखना चाहिए कि WebGPU जैसे एपीआई को पूरे वेब नेटवर्क में लागू होने में कुछ समय लग सकता है. इस वजह से, सावधानी बरतने का एक अच्छा पहला कदम यह है कि यह देख लें कि उपयोगकर्ता का ब्राउज़र WebGPU का इस्तेमाल कर सकता है या नहीं.
- WebGPU के एंट्री पॉइंट के तौर पर काम करने वाले
navigator.gpu
ऑब्जेक्ट के मौजूद होने की जांच करने के लिए, यह कोड जोड़ें:
index.html
if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
}
आम तौर पर, अगर WebGPU के उपलब्ध न होने की वजह से, पेज को ऐसे मोड पर वापस लाया जाता है जिसमें WebGPU का इस्तेमाल नहीं किया जाता, तो आपको उपयोगकर्ता को इसकी जानकारी देनी होगी. (शायद इसके बजाय WebGL का इस्तेमाल किया जा सके?) हालांकि, इस कोडलैब के लिए, कोड को आगे चलने से रोकने के लिए, सिर्फ़ गड़बड़ी का मैसेज दिखाया जाता है.
यह जानने के बाद कि ब्राउज़र पर WebGPU काम करता है, अपने ऐप्लिकेशन के लिए WebGPU को शुरू करने का पहला चरण, GPUAdapter
का अनुरोध करना है. आपके पास अडैप्टर को यह देखने का विकल्प होता है कि WebGPU, आपके डिवाइस में जीपीयू हार्डवेयर का एक खास हिस्सा दिखाता है.
- अडैप्टर पाने के लिए,
navigator.gpu.requestAdapter()
तरीके का इस्तेमाल करें. यह प्रॉमिस रिटर्न करता है, इसलिए इसेawait
के साथ कॉल करना सबसे ज़्यादा आसान है.
index.html
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No appropriate GPUAdapter found.");
}
अगर कोई सही अडैप्टर नहीं मिलता है, तो adapter
की वैल्यू null
हो सकती है. इसलिए, आपको इस संभावना को मैनेज करना है. ऐसा तब हो सकता है, जब उपयोगकर्ता के ब्राउज़र पर WebGPU की सुविधा काम करती हो, लेकिन उसके जीपीयू हार्डवेयर में WebGPU का इस्तेमाल करने के लिए सभी ज़रूरी सुविधाएं मौजूद न हों.
ज़्यादातर मामलों में, ब्राउज़र को डिफ़ॉल्ट अडैप्टर चुनने की अनुमति देना ठीक होता है, जैसा कि आपने यहां किया है. हालांकि, ज़्यादा बेहतर ज़रूरतों के लिए, requestAdapter()
में ऐसे आर्ग्युमेंट पास किए जा सकते हैं जिनसे यह पता चलता है कि आपको एक से ज़्यादा जीपीयू वाले डिवाइसों (जैसे, कुछ लैपटॉप) पर कम पावर या बेहतर परफ़ॉर्मेंस वाले हार्डवेयर का इस्तेमाल करना है या नहीं.
अडैप्टर मिलने के बाद, GPU के साथ काम करने से पहले, GPUDevice का अनुरोध करना ज़रूरी है. डिवाइस मुख्य इंटरफ़ेस है, जिससे जीपीयू के साथ ज़्यादातर इंटरैक्शन होता है.
adapter.requestDevice()
पर कॉल करके डिवाइस पाएं. इससे जवाब में प्रॉमिस भी मिलेगा.
index.html
const device = await adapter.requestDevice();
requestAdapter()
की तरह ही, यहां भी ऐसे विकल्प दिए गए हैं जिन्हें पास किया जा सकता है. इनका इस्तेमाल, बेहतर तरीके से करने के लिए किया जा सकता है. जैसे, किसी खास हार्डवेयर की सुविधाओं को चालू करना या ज़्यादा सीमाओं का अनुरोध करना. हालांकि, आपके काम के लिए डिफ़ॉल्ट विकल्प ही सही रहेंगे.
कैनवस कॉन्फ़िगर करना
अब आपके पास डिवाइस है, तो अगर आपको पेज पर कुछ भी दिखाने के लिए इसका इस्तेमाल करना है, तो आपको एक और काम करना होगा: अभी-अभी बनाए गए डिवाइस के साथ इस्तेमाल करने के लिए कैनवस कॉन्फ़िगर करना.
- ऐसा करने के लिए, पहले
canvas.getContext("webgpu")
को कॉल करके कैनवस सेGPUCanvasContext
का अनुरोध करें. (यह वही कॉल है जिसका इस्तेमाल आपको2d
औरwebgl
कॉन्टेक्स्ट टाइप के हिसाब से, Canvas 2D या WebGL कॉन्टेक्स्ट शुरू करने के लिए करना है.) इसके बाद, जोcontext
दिखाता है उसेconfigure()
तरीके का इस्तेमाल करके, डिवाइस से जोड़ा जाना चाहिए. जैसे:
index.html
const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
});
यहां कुछ विकल्प दिए गए हैं, जिन्हें यहां पास किया जा सकता है. हालांकि, सबसे अहम device
हैं, जिनके साथ आपको कॉन्टेक्स्ट का इस्तेमाल करना है. साथ ही, format
भी है, जो कॉन्टेक्स्ट के लिए इस्तेमाल किया जाने वाला टेक्सचर फ़ॉर्मैट है.
टेक्सचर ऐसे ऑब्जेक्ट होते हैं जिनका इस्तेमाल WebGPU, इमेज डेटा को स्टोर करने के लिए करता है. हर टेक्सचर का एक फ़ॉर्मैट होता है, जिससे GPU को यह पता चलता है कि डेटा को मेमोरी में कैसे रखा गया है. इस कोडलैब में, टेक्सचर मेमोरी के काम करने के तरीके के बारे में नहीं बताया गया है. यह जानना ज़रूरी है कि कैनवस कॉन्टेक्स्ट, आपके कोड को ड्रॉ करने के लिए टेक्सचर उपलब्ध कराता है. साथ ही, इस्तेमाल किए गए फ़ॉर्मैट से यह तय होता है कि कैनवस उन इमेज को कितनी बेहतर तरीके से दिखाता है. अलग-अलग तरह के डिवाइसों पर, अलग-अलग टेक्सचर फ़ॉर्मैट का इस्तेमाल करने पर सबसे अच्छा परफ़ॉर्म किया जा सकता है. अगर डिवाइस के पसंदीदा फ़ॉर्मैट का इस्तेमाल नहीं किया जाता है, तो हो सकता है कि इमेज को पेज के हिस्से के तौर पर दिखाने से पहले, बैकग्राउंड में अतिरिक्त मेमोरी कॉपी बन जाएं.
अच्छी बात यह है कि आपको इनमें से किसी भी चीज़ के बारे में ज़्यादा चिंता करने की ज़रूरत नहीं है, क्योंकि WebGPU आपको बताता है कि कैनवस के लिए किस फ़ॉर्मैट का इस्तेमाल करना है! जैसा कि ऊपर दिखाया गया है, करीब-करीब सभी मामलों में, navigator.gpu.getPreferredCanvasFormat()
को कॉल करके रिटर्न की जाने वाली वैल्यू को पास करना होता है.
कैनवस मिटाना
अब आपके पास डिवाइस है और उसके साथ कैनवस कॉन्फ़िगर कर दिया गया है, तो आप कैनवस का कॉन्टेंट बदलने के लिए डिवाइस का इस्तेमाल करना शुरू कर सकते हैं. शुरू करने के लिए, उसे किसी एक रंग से भरें.
ऐसा करने के लिए—या WebGPU में मौजूद कोई भी दूसरी चीज़—आपको जीपीयू को कुछ निर्देश देने होंगे, जिनमें बताया गया हो कि क्या करना है.
- ऐसा करने के लिए, डिवाइस को
GPUCommandEncoder
बनाएं. इससे, जीपीयू के निर्देशों को रिकॉर्ड करने के लिए इंटरफ़ेस मिलता है.
index.html
const encoder = device.createCommandEncoder();
जो निर्देश आप जीपीयू को भेजना चाहते हैं, वे रेंडरिंग से जुड़े होते हैं (इस मामले में, कैनवस को साफ़ करना), इसलिए अगला चरण है रेंडर पास शुरू करने के लिए encoder
का इस्तेमाल करना.
WebGPU में ड्रॉइंग से जुड़े सभी काम, रेंडर पास के दौरान होते हैं. हर टेक्स्टचर, beginRenderPass()
कॉल से शुरू होता है. इससे उन टेक्स्चर के बारे में पता चलता है जिन्हें ड्रॉइंग के लिए दिए गए किसी भी निर्देश का आउटपुट मिलता है. ज़्यादा बेहतर इस्तेमाल से कई बनावटें मिल सकती हैं, जिन्हें अटैचमेंट कहा जाता है. इसके कई मकसद हैं, जैसे कि रेंडर की गई ज्यामिति की गहराई को स्टोर करना या एंटीएलियासिंग उपलब्ध कराना. हालांकि, इस ऐप्लिकेशन के लिए आपको सिर्फ़ एक की ज़रूरत है.
context.getCurrentTexture()
को कॉल करके, पहले से बनाए गए कैनवस कॉन्टेक्स्ट से टेक्चर पाएं. यह कैनवस केwidth
औरheight
एट्रिब्यूट औरcontext.configure()
को कॉल करते समय बताए गएformat
से मैच करने वाली पिक्सल चौड़ाई और ऊंचाई वाला टेक्चर दिखाता है.
index.html
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
storeOp: "store",
}]
});
टेक्सचर, colorAttachment
की view
प्रॉपर्टी के तौर पर दिया जाता है. रेंडर पास के लिए, आपको GPUTexture
के बजाय GPUTextureView
देना होगा. इससे यह तय होता है कि टेक्सचर के किन हिस्सों को रेंडर करना है. यह वाकई सिर्फ़ बेहतर इस्तेमाल के मामलों में मायने रखता है. इसलिए, यहां टेक्सचर पर बिना किसी आर्ग्युमेंट के createView()
को कॉल किया गया है. इससे यह पता चलता है कि आपको रेंडर पास को पूरे टेक्सचर का इस्तेमाल करने के लिए कहना है.
आपको यह भी बताना होगा कि रेंडर पास शुरू होने और खत्म होने पर, टेक्सचर के साथ क्या करना है:
loadOp
की वैल्यू"clear"
होने का मतलब है कि आपको रेंडर पास शुरू होने पर टेक्चर को हटाना है.storeOp
की वैल्यू"store"
से पता चलता है कि रेंडर पास पूरा होने के बाद, आपको रेंडर पास के दौरान की गई किसी भी ड्रॉइंग के नतीजों को टेक्सचर में सेव करना है.
रेंडर पास शुरू हो जाने के बाद, आपको कुछ नहीं करना चाहिए! कम से कम अभी के लिए. रेंडर पास को loadOp: "clear"
से शुरू करने से, टेक्सचर व्यू और कैनवस को खाली किया जा सकता है.
beginRenderPass()
के तुरंत बाद इस कॉल को जोड़कर, रेंडर पास को बंद करें:
index.html
pass.end();
यह जानना ज़रूरी है कि सिर्फ़ इन कॉल को करने से, GPU कुछ नहीं करता. वे सिर्फ़ जीपीयू के लिए निर्देश रिकॉर्ड कर रहे हैं, ताकि वे बाद में काम कर सकें.
GPUCommandBuffer
बनाने के लिए, कमांड एन्कोडर परfinish()
को कॉल करें. कमांड बफ़र, रिकॉर्ड किए गए निर्देशों के लिए एक ओपेक हैंडल होता है.
index.html
const commandBuffer = encoder.finish();
GPUDevice
केqueue
का इस्तेमाल करके, कमांड बफ़र को जीपीयू पर सबमिट करें. सूची में सभी जीपीयू कमांड काम करते हैं, ताकि यह पक्का किया जा सके कि उनका एक्ज़ीक्यूशन सही क्रम में हो और सही तरीके से सिंक किया गया हो. कतार केsubmit()
तरीके में, कमांड बफ़र का कलेक्शन शामिल होता है. हालांकि, इस मामले में आपके पास सिर्फ़ एक कलेक्शन है.
index.html
device.queue.submit([commandBuffer]);
कमांड बफ़र सबमिट करने के बाद, उसका फिर से इस्तेमाल नहीं किया जा सकता. इसलिए, उसे सेव रखने की ज़रूरत नहीं है. अगर आपको ज़्यादा निर्देश सबमिट करने हैं, तो आपको एक और निर्देश बफ़र बनाना होगा. इसलिए, इन दो चरणों को एक में छोटा देखना आम बात है, जैसा कि इस कोडलैब के सैंपल पेजों में किया गया है:
index.html
// Finish the command buffer and immediately submit it.
device.queue.submit([encoder.finish()]);
जीपीयू को निर्देश सबमिट करने के बाद, JavaScript को ब्राउज़र को कंट्रोल वापस करने दें. इस स्थिति में, ब्राउज़र को पता चलता है कि आपने कॉन्टेक्स्ट का मौजूदा टेक्सचर बदल दिया है. साथ ही, ब्राउज़र उस टेक्सचर को इमेज के तौर पर दिखाने के लिए, कैनवस को अपडेट करता है. इसके बाद, कैनवस के कॉन्टेंट को फिर से अपडेट करने के लिए, आपको एक नई कमांड बफ़र रिकॉर्ड करके सबमिट करनी होगी. साथ ही, रेंडर पास के लिए नया टेक्सचर पाने के लिए, context.getCurrentTexture()
को फिर से कॉल करना होगा.
- पेज को फिर से लोड करें. ध्यान दें कि कैनवस काले रंग से भरा हुआ है. बधाई हो! इसका मतलब है कि आपने अपना पहला WebGPU ऐप्लिकेशन बना लिया है.
कोई रंग चुनें!
हालांकि, सच कहूं, तो काले रंग के स्क्वेयर बहुत ही बोरिंग होते हैं. इसलिए, अगले सेक्शन पर जाने से पहले थोड़ा समय निकालकर, इसे अपने हिसाब से बनाएं.
encoder.beginRenderPass()
कॉल में,colorAttachment
मेंclearValue
वाली एक नई लाइन जोड़ें, इस तरह:
index.html
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0.4, a: 1 }, // New line
storeOp: "store",
}],
});
clearValue
, रेंडर पास को बताता है कि पास की शुरुआत में clear
कार्रवाई करते समय, उसे किस रंग का इस्तेमाल करना चाहिए. इसमें पास किए गए शब्दकोश में चार वैल्यू होती हैं: लाल के लिए r
, हरे के लिए g
, नीले के लिए b
, और ऐल्फ़ा के लिए a
(पारदर्शिता). हर वैल्यू, 0
से 1
तक हो सकती है. ये वैल्यू मिलकर, उस कलर चैनल की वैल्यू के बारे में बताती हैं. उदाहरण के लिए:
{ r: 1, g: 0, b: 0, a: 1 }
का रंग चमकीला लाल है.{ r: 1, g: 0, b: 1, a: 1 }
चमकीला बैंगनी है.{ r: 0, g: 0.3, b: 0, a: 1 }
का रंग गहरा हरा है.{ r: 0.5, g: 0.5, b: 0.5, a: 1 }
का रंग मीडियम स्लेटी है.{ r: 0, g: 0, b: 0, a: 0 }
डिफ़ॉल्ट रूप से, पारदर्शी काला होता है.
इस कोडलैब में, उदाहरण के तौर पर दिए गए कोड और स्क्रीनशॉट में गहरे नीले रंग का इस्तेमाल किया गया है. हालांकि, अपनी पसंद का कोई भी रंग चुना जा सकता है!
- रंग चुनने के बाद, पेज को फिर से लोड करें. कैनवस में, आपको अपना चुना गया रंग दिखेगा.
4. ज्यामिति ड्रॉ करना
इस सेक्शन के आखिर तक, आपका ऐप्लिकेशन कैनवस पर कुछ सामान्य ज्यामिति बना देगा: एक रंगीन स्क्वेयर. ध्यान रखें कि इस तरह के आसान आउटपुट के लिए, आपको काफ़ी काम करना पड़ेगा. ऐसा इसलिए है, क्योंकि WebGPU को ज़्यादा ज्यामिति को बेहतर तरीके से रेंडर करने के लिए डिज़ाइन किया गया है. इस क्षमता का खराब असर यह है कि सामान्य काम करना मुश्किल लग सकता है, लेकिन अगर WebGPU जैसे एपीआई का इस्तेमाल किया जा रहा है, तो आपको और मुश्किल काम करने की ज़रूरत होती है.
जीपीयू के ड्रॉ करने का तरीका समझना
कोड में कुछ और बदलाव करने से पहले, आपको इस बात की बहुत तेज़, आसान, और ज़्यादा जानकारी मिल जाती है कि जीपीयू आपको स्क्रीन पर दिखने वाले आकार कैसे बनाते हैं. (अगर आपको GPU रेंडरिंग के काम करने के तरीके के बारे में पहले से पता है, तो सीधे 'वेक्टर्स तय करना' सेक्शन पर जाएं.)
कैनवस 2D जैसे एपीआई में, आपके इस्तेमाल के लिए कई आकार और विकल्प तैयार होते हैं, जबकि आपका जीपीयू सिर्फ़ कुछ अलग-अलग तरह के आकार (या प्रिमिटिव) के साथ काम करता है, जैसा कि WebGPU में बताया गया है: पॉइंट, लाइनें, और ट्रायएंगल. इस कोडलैब में, सिर्फ़ त्रिकोणों का इस्तेमाल किया जाएगा.
जीपीयू, त्रिभुजों के साथ ही काम करते हैं, क्योंकि त्रिभुजों में गणित से जुड़ी कई अच्छी प्रॉपर्टी होती हैं. इनकी मदद से, त्रिभुजों को आसानी से और बेहतर तरीके से प्रोसेस किया जा सकता है. जीपीयू से खींची गई लगभग हर चीज़ को त्रिभुजों में बांटना ज़रूरी है. इसके बाद ही, जीपीयू उसे खींच सकता है. साथ ही, उन त्रिभुजों को उनके कोने के पॉइंट से तय किया जाना चाहिए.
ये पॉइंट या वर्टेक्स, X, Y, और (3D कॉन्टेंट के लिए) Z की वैल्यू के तौर पर दिए जाते हैं. ये वैल्यू, WebGPU या इससे मिलते-जुलते एपीआई के बताए गए कार्टेशियन कोऑर्डिनेट सिस्टम पर पॉइंट के बारे में बताती हैं. कोऑर्डिनेट सिस्टम के स्ट्रक्चर को इस हिसाब से समझना आसान है कि यह आपके पेज के कैनवस से कैसे जुड़ा है. आपका कैनवस कितना भी चौड़ा या लंबा हो, X ऐक्सिस पर बायां किनारा हमेशा -1 पर होता है और X ऐक्सिस पर दायां किनारे हमेशा +1 पर होता है. इसी तरह, Y ऐक्सिस पर निचला किनारा हमेशा -1 होता है और Y ऐक्सिस पर सबसे ऊपर का किनारा +1 होता है. इसका मतलब है कि (0, 0) हमेशा कैनवस का बीच होता है, (-1, -1) हमेशा सबसे नीचे बाईं ओर होता है, और (1, 1) हमेशा सबसे ऊपर दाईं ओर होता है. इसे क्लिप स्पेस के नाम से जाना जाता है.
आम तौर पर, शुरुआत में इस निर्देशांक सिस्टम में वर्टिक्स तय नहीं किए जाते. इसलिए, जीपीयू वर्टिक्स शेडर नाम के छोटे प्रोग्राम पर निर्भर होते हैं. इन प्रोग्राम की मदद से, वर्टिक्स को क्लिप स्पेस में बदलने के लिए ज़रूरी गणित के साथ-साथ, वर्टिक्स को ड्रॉ करने के लिए ज़रूरी अन्य गणनाएं भी की जाती हैं. उदाहरण के लिए, शेडर कुछ ऐनिमेशन लागू कर सकता है या शीर्ष से प्रकाश स्रोत तक दिशा की गणना कर सकता है. ये शेडर आपने या WebGPU डेवलपर ने लिखे हैं. इनसे जीपीयू के काम करने के तरीके पर शानदार कंट्रोल मिलता है.
इसके बाद, GPU इन बदले हुए वर्टिसेस से बने सभी ट्राएंगल लेता है और यह तय करता है कि उन्हें ड्रॉ करने के लिए, स्क्रीन पर किन पिक्सल की ज़रूरत है. इसके बाद, यह एक छोटा प्रोग्राम चलाता है जिसे फ़्रैगमेंट शेडर कहते हैं. इससे यह हिसाब लगाया जाता है कि हर पिक्सल का रंग क्या होना चाहिए. यह गणना हरा रंग दिखाएं जैसी आसान हो सकती है या फिर आस-पास की अन्य सतहों से टकराकर आने वाली सूरज की रोशनी के हिसाब से, सतह के कोण का हिसाब लगाने जैसी मुश्किल भी हो सकती है. इसमें, धुंध के ज़रिए फ़िल्टर की गई रोशनी और सतह के मेटल होने या न होने के हिसाब से भी बदलाव किया जा सकता है. यह पूरी तरह से आपके कंट्रोल में है.
इसके बाद, पिक्सल के उन रंगों के नतीजे एक बनावट में इकट्ठा हो जाते हैं, जिन्हें बाद में स्क्रीन पर दिखाया जा सकता है.
वर्टिसेस तय करना
जैसा कि पहले बताया गया है, 'लाइफ़ ऑफ़ लाइफ़' सिम्युलेशन को सेल के ग्रिड के तौर पर दिखाया जाता है. आपके ऐप्लिकेशन में ग्रिड को विज़ुअलाइज़ करने का तरीका होना चाहिए, ताकि चालू सेल को बंद सेल से अलग किया जा सके. इस कोडलैब में इस्तेमाल होने वाला तरीका, ऐक्टिव सेल में रंगीन स्क्वेयर बनाना और बंद सेल को खाली छोड़ना होगा.
इसका मतलब है कि आपको जीपीयू को चार अलग-अलग पॉइंट देने होंगे, यानी स्क्वेयर के चारों कोनों में से हर एक पॉइंट के लिए एक पॉइंट. उदाहरण के लिए, कैनवस के बीच में बनाए गए एक स्क्वेयर को, किनारों से इस तरह से खींचा जाता है कि उसके कोने निर्देशांक इस तरह हैं:
उन निर्देशांकों को GPU में फ़ीड करने के लिए, आपको वैल्यू को TypedArray में डालना होगा. अगर आपको इसके बारे में पहले से नहीं पता है, तो TypedArrays JavaScript ऑब्जेक्ट का एक ग्रुप होता है. इसकी मदद से, मेमोरी के लगातार ब्लॉक असाइन किए जा सकते हैं और सीरीज़ के हर एलिमेंट को खास डेटा टाइप के तौर पर समझा जा सकता है. उदाहरण के लिए, Uint8Array
में, कलेक्शन का हर एलिमेंट एक बिना साइन वाला बाइट होता है. TypedArrays, ऐसे एपीआई के साथ डेटा को भेजने और भेजने के लिए बहुत बढ़िया होता है जो मेमोरी लेआउट के प्रति संवेदनशील होते हैं, जैसे कि WebAssembly, WebAudio, और (बेशक) WebGPU.
स्क्वेयर के उदाहरण के लिए, वैल्यू भिन्नात्मक होने की वजह से, Float32Array
सही है.
- अपने कोड में नीचे दिया गया कलेक्शन एलान करके, डायग्राम में सभी वर्टिक्स पोज़िशन वाला कलेक्शन बनाएं. इसे सबसे ऊपर,
context.configure()
कॉल के ठीक नीचे रखना सबसे सही है.
index.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8,
0.8, -0.8,
0.8, 0.8,
-0.8, 0.8,
]);
ध्यान दें कि वैल्यू पर स्पेस और टिप्पणी का कोई असर नहीं पड़ता. यह सिर्फ़ आपकी सुविधा के लिए और इसे ज़्यादा पढ़ने लायक बनाने के लिए है. इसकी मदद से आप देख सकते हैं कि मानों का हर जोड़ा एक शीर्ष के लिए X और Y निर्देशांक बनाता है.
हालांकि, एक समस्या है! जीपीयू, त्रिकोणों के हिसाब से काम करते हैं, याद है? इसका मतलब है कि आपको तीन के ग्रुप में वर्टिसेस देने होंगे. आपके पास चार सदस्यों का एक ग्रुप है. समाधान यह है कि वर्ग के मध्य से किनारे को शेयर करते हुए दो त्रिभुज बनाने के लिए दो शीर्षों को दोहराएं.
डायग्राम से स्क्वेयर बनाने के लिए, आपको (-0.8, -0.8) और (0.8, 0.8) वर्टिसेस को दो बार डालना होगा. एक बार नीले ट्रायंगल के लिए और एक बार लाल ट्रायंगल के लिए. (इसके बजाय, स्क्वेयर को दो अन्य कोनों से भी बांटा जा सकता है. इससे कोई फ़र्क़ नहीं पड़ता.)
- अपने पिछले
vertices
कलेक्शन को इस तरह अपडेट करें:
index.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
]);
हालांकि, डायग्राम में साफ़ तौर पर यह दिखाने के लिए दो त्रिकोणों को अलग-अलग दिखाया गया है, लेकिन वर्टेक्स की पोज़िशन बिलकुल एक जैसी हैं और जीपीयू उन्हें बिना किसी गैप के रेंडर करता है. यह एक एकल, ठोस वर्ग के रूप में रेंडर होगा.
वर्टिक्स बफ़र बनाना
जीपीयू, JavaScript कलेक्शन के डेटा से वर्टिसेस नहीं खींच सकता. जीपीयू में अक्सर अपनी खुद की मेमोरी होती है, जो रेंडरिंग के लिए काफ़ी अच्छी तरह ऑप्टिमाइज़ होती है. इसलिए, जीपीयू को ड्रॉ करते समय जिस डेटा का इस्तेमाल करना है उसे उसी मेमोरी में रखा जाना चाहिए.
वर्टिक्स डेटा के साथ-साथ कई वैल्यू के लिए, GPU-साइड मेमोरी को GPUBuffer
ऑब्जेक्ट की मदद से मैनेज किया जाता है. बफ़र, मेमोरी का एक ब्लॉक होता है. इसे जीपीयू आसानी से ऐक्सेस कर सकता है और कुछ खास कामों के लिए फ़्लैग कर सकता है. यह जीपीयू दिखने वाली TypedArray की तरह है.
- अपने वर्टिसेस को सेव करने के लिए बफ़र बनाने के लिए,
vertices
कलेक्शन की परिभाषा के बाद,device.createBuffer()
में यह कॉल जोड़ें.
index.html
const vertexBuffer = device.createBuffer({
label: "Cell vertices",
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
सबसे पहले ध्यान देने वाली बात यह है कि आप बफ़र को एक लेबल देते हैं. आपके बनाए गए हर एक WebGPU ऑब्जेक्ट को एक वैकल्पिक लेबल दिया जा सकता है और आप वाकई ऐसा करना चाहते हैं! लेबल आपकी पसंद की कोई भी स्ट्रिंग होती है, जब तक कि वह ऑब्जेक्ट की पहचान करने में आपकी सहायता करता है. अगर आपको कोई समस्या आती है, तो उन लेबल का इस्तेमाल WebGPU के गड़बड़ी वाले मैसेज में किया जाता है. इससे आपको गड़बड़ियों को समझने में मदद मिलती है.
इसके बाद, बफ़र को बाइट में साइज़ दें. आपको 48 बाइट वाले बफ़र की ज़रूरत होगी, जिसे तय करने के लिए, 32-बिट फ़्लोट ( 4 बाइट) के साइज़ को अपने vertices
कलेक्शन (12) में मौजूद फ़्लोट की संख्या से गुणा किया जा सकता है. अच्छी बात यह है कि TypedArrays आपके लिए पहले से ही byteLength का हिसाब लगा लेते हैं. इसलिए, बफ़र बनाते समय इसका इस्तेमाल किया जा सकता है.
आखिर में, आपको बफ़र के इस्तेमाल के बारे में बताना होगा. यह एक या उससे ज़्यादा GPUBufferUsage
फ़्लैग हैं. इनमें एक से ज़्यादा फ़्लैग को |
( बिटवाइज़ OR) ऑपरेटर के साथ जोड़ा जाता है. इस मामले में, आपको बताना है कि बफ़र को वर्टेक्स डेटा (GPUBufferUsage.VERTEX
) के लिए इस्तेमाल करना है और इसमें डेटा (GPUBufferUsage.COPY_DST
) भी कॉपी करना है.
आपको जो बफ़र ऑब्जेक्ट दिखता है वह पारदर्शी नहीं होता. इसका मतलब है कि उसमें मौजूद डेटा की जांच आसानी से नहीं की जा सकती. इसके अलावा, इसके ज़्यादातर एट्रिब्यूट में बदलाव नहीं किया जा सकता. GPUBuffer
बनाने के बाद, उसका साइज़ नहीं बदला जा सकता. इसके अलावा, इस्तेमाल के फ़्लैग में भी बदलाव नहीं किया जा सकता. इसमें बदलाव करने के लिए, मेमोरी में मौजूद कॉन्टेंट का इस्तेमाल किया जा सकता है.
बफ़र बनाने पर, उसमें मौजूद मेमोरी को शून्य पर सेट कर दिया जाएगा. इसका कॉन्टेंट बदलने के कई तरीके हैं. हालांकि, device.queue.writeBuffer()
को टाइप किए गए उस कलेक्शन से कॉल करना सबसे आसान है जिसे आपको कॉपी करना है.
- वर्टिक्स डेटा को बफ़र की मेमोरी में कॉपी करने के लिए, यह कोड जोड़ें:
index.html
device.queue.writeBuffer(vertexBuffer, /*bufferOffset=*/0, vertices);
वर्टिक्स लेआउट तय करना
अब आपके पास वर्टेक्स डेटा वाला बफ़र है, लेकिन जहां तक जीपीयू की बात है, तो यह सिर्फ़ बाइट का एक ब्लॉब है. अगर आपको इसकी मदद से कुछ बनाना है, तो आपको थोड़ी और जानकारी देनी होगी. आपको WebGPU को, वर्टिक्स डेटा के स्ट्रक्चर के बारे में ज़्यादा जानकारी देनी होगी.
GPUVertexBufferLayout
डिक्शनरी की मदद से, वर्टिक्स डेटा स्ट्रक्चर तय करें:
index.html
const vertexBufferLayout = {
arrayStride: 8,
attributes: [{
format: "float32x2",
offset: 0,
shaderLocation: 0, // Position, see vertex shader
}],
};
पहली नज़र में यह समझना थोड़ा मुश्किल हो सकता है, लेकिन इसे समझना ज़्यादा आसान होता है.
सबसे पहले आपको arrayStride
डालना होगा. यह बाइट की वह संख्या है जो अगले वर्टिक्स की खोज करते समय, जीपीयू को बफ़र में आगे की ओर स्किप करने की ज़रूरत होती है. आपके स्क्वेयर का हर वर्टिक्स, दो 32-बिट फ़्लोटिंग पॉइंट नंबर से बना होता है. जैसा कि पहले बताया गया है, 32-बिट फ़्लोट 4 बाइट का होता है. इसलिए, दो फ़्लोट 8 बाइट के होते हैं.
आगे है attributes
प्रॉपर्टी, जो एक कलेक्शन है. एट्रिब्यूट, हर वर्टिक्स में एन्कोड की गई जानकारी के अलग-अलग हिस्से होते हैं. आपके वर्टेक्स में सिर्फ़ एक एट्रिब्यूट (वर्टेक्स पोज़िशन) होता है. हालांकि, इस्तेमाल के ज़्यादा बेहतर उदाहरणों में अक्सर एक से ज़्यादा एट्रिब्यूट वाले वर्टेक्स होते हैं, जैसे कि वर्टेक्स का रंग या ज्यामिति की सतह जिस दिशा को दिखा रही है. हालांकि, यह कोडलैब इस कोड के दायरे से बाहर है.
अपने एक एट्रिब्यूट में, पहले डेटा का format
तय करें. यह GPUVertexFormat
टाइप की सूची से आता है. इस सूची में, हर तरह के ऐसे वर्टिक्स डेटा के बारे में बताया जाता है जिसे GPU समझ सकता है. आपके वर्टेक्स में दो 32-बिट फ़्लोट होते हैं, इसलिए आप float32x2
फ़ॉर्मैट का इस्तेमाल करते हैं. अगर आपका वर्टिक्स डेटा, चार 16-बिट के बिना साइन वाले पूर्णांक से बना है, तो इसके लिए uint16x4
का इस्तेमाल किया जाएगा. पैटर्न दिख रहा है?
इसके बाद, offset
से पता चलता है कि यह एट्रिब्यूट, वर्टिक्स में कितने बाइट से शुरू होता है. आपको इस बारे में सिर्फ़ तब चिंता करनी होगी, जब आपके बफ़र में एक से ज़्यादा एट्रिब्यूट हों. हालांकि, इस कोडलैब के दौरान ऐसा नहीं होगा.
आखिर में, आपके पास shaderLocation
है. यह 0 और 15 के बीच की कोई आर्बिट्रेरी संख्या है. साथ ही, यह आपकी तय की गई हर एट्रिब्यूट के लिए यूनीक होना चाहिए. यह इस एट्रिब्यूट को वर्टिक्स शेडर में किसी खास इनपुट से जोड़ता है. इस बारे में अगले सेक्शन में बताया जाएगा.
ध्यान दें कि हालांकि आपने इन वैल्यू को तय किया है, लेकिन आपने इन्हें WebGPU API में अभी कहीं भी पास नहीं किया है. इस बारे में आगे बताया जाएगा. हालांकि, इन वैल्यू के बारे में तब सोचना सबसे आसान होता है, जब आपने अपने वर्टिसेस तय कर लिए हों. इसलिए, इन्हें अभी सेट अप करें, ताकि बाद में इनका इस्तेमाल किया जा सके.
शेडर से शुरुआत करें
अब आपके पास वह डेटा है जिसे रेंडर करना है. हालांकि, आपको अब भी GPU को यह बताना होगा कि उसे कैसे प्रोसेस करना है. इसका ज़्यादातर हिस्सा शेडर की मदद से होता है.
शेडर ऐसे छोटे प्रोग्राम होते हैं जिन्हें लिखा जाता है और जीपीयू पर एक्ज़ीक्यूट किया जाता है. हर शेडर, डेटा के अलग स्टेज पर काम करता है: Vertex प्रोसेसिंग, फ़्रैगमेंट प्रोसेसिंग या सामान्य कंप्यूट. क्योंकि ये जीपीयू पर होते हैं, इसलिए इन्हें आपके औसत JavaScript की तुलना में ज़्यादा व्यवस्थित किया जाता है. हालांकि, इस स्ट्रक्चर की मदद से, उन्हें बहुत तेज़ी से और एक साथ कई काम करने में मदद मिलती है!
WebGPU में शेडर, WGSL (WebGPU शेडर लैंग्वेज) नाम की शेडर लैंग्वेज में लिखे जाते हैं. WGSL, वाक्यात्मक रूप से, Rust की तरह है, जिसमें ऐसी सुविधाएं हैं जो सामान्य रूप से जीपीयू के काम (जैसे कि वेक्टर और मैट्रिक्स मैथ) को आसान और तेज़ बनाती हैं. इस कोडलैब में, शेडिंग लैंग्वेज के बारे में पूरी जानकारी नहीं दी गई है. हालांकि, उम्मीद है कि कुछ आसान उदाहरणों की मदद से, आपको कुछ बुनियादी बातें समझ आएंगी.
शेडर, WebGPU में स्ट्रिंग के तौर पर पास हो जाते हैं.
vertexBufferLayout
के नीचे दिए गए कोड में, यह कोड कॉपी करके अपना शेडर कोड डालें:
index.html
const cellShaderModule = device.createShaderModule({
label: "Cell shader",
code: `
// Your shader code will go here
`
});
शेडर बनाने के लिए, device.createShaderModule()
को कॉल किया जाता है. इसमें स्ट्रिंग के तौर पर, label
और WGSL code
को वैकल्पिक तौर पर दिया जाता है. (ध्यान दें कि मल्टी-लाइन स्ट्रिंग को अनुमति देने के लिए, यहां बैकटिक का इस्तेमाल किया जाता है!) कोई मान्य WGSL कोड जोड़ने के बाद, फ़ंक्शन इकट्ठा किए गए नतीजों के साथ GPUShaderModule
ऑब्जेक्ट दिखाता है.
वर्टिक्स शेडर तय करना
वर्टेक्स शेडर से शुरू करें, क्योंकि यहीं से जीपीयू भी शुरू होता है!
वर्टेक्स शेडर को एक फ़ंक्शन के रूप में परिभाषित किया जाता है और जीपीयू कॉल जो आपके vertexBuffer
में हर वर्टेक्स के लिए एक बार काम करते हैं. आपके vertexBuffer
में छह पोज़िशन (वर्टिसेस) हैं, इसलिए आपके तय किए गए फ़ंक्शन को छह बार कॉल किया जाता है. हर बार इसे कॉल करने पर, vertexBuffer
से एक अलग पोज़िशन को आर्ग्युमेंट के तौर पर फ़ंक्शन में पास किया जाता है. साथ ही, क्लिप स्पेस में उससे जुड़ी पोज़िशन दिखाने की ज़िम्मेदारी, वर्टिक्स शेडर फ़ंक्शन की होती है.
यह समझना ज़रूरी है कि ये ज़रूरी नहीं है कि इन्हें क्रम से कॉल किया जाए. इसके बजाय, जीपीयू इन शेडर को एक साथ चलाने में बेहतर होते हैं. साथ ही, एक ही समय पर सैकड़ों या हज़ारों वर्टिसेस को प्रोसेस कर सकते हैं! यह एक बहुत बड़ी बात है, जिसकी वजह से जीपीयू की रफ़्तार बढ़ती है. हालांकि, इस काम को करने में कुछ सीमाओं का इस्तेमाल किया जाता है. बहुत ज़्यादा समानता बनाए रखने के लिए, वर्टेक्स शेडर एक-दूसरे के साथ कम्यूनिकेट नहीं कर सकते. हर शेडर इंवोकेशन, एक बार में सिर्फ़ एक वर्टिक्स का डेटा देख सकता है. साथ ही, सिर्फ़ एक वर्टिक्स की वैल्यू आउटपुट कर सकता है.
WGSL में, किसी वर्टिक्स शेडर फ़ंक्शन को अपनी पसंद का नाम दिया जा सकता है. हालांकि, इसके सामने @vertex
एट्रिब्यूट होना चाहिए, ताकि यह पता चल सके कि यह किस शेडर स्टेज को दिखाता है. WGSL, fn
कीवर्ड वाले फ़ंक्शन को दिखाता है, किसी भी आर्ग्युमेंट के बारे में बताने के लिए, ब्रैकेट का इस्तेमाल करता है. साथ ही, स्कोप तय करने के लिए, कर्ली ब्रैकेट का इस्तेमाल करता है.
- इस तरह से एक खाली
@vertex
फ़ंक्शन बनाएं:
index.html (createShaderModule कोड)
@vertex
fn vertexMain() {
}
हालांकि, यह मान्य नहीं है, क्योंकि वर्टेक्स शेडर को कम से कम वह वर्टेक्स की आखिरी पोज़िशन देनी होगी जिसे क्लिप स्पेस में प्रोसेस किया जा रहा है. इसे हमेशा चार डाइमेंशन वाले वेक्टर के तौर पर दिया जाता है. शेडर में वेक्टर का इस्तेमाल बहुत आम है. इसलिए, उन्हें भाषा में फ़र्स्ट-क्लास प्राइमिटिव के तौर पर माना जाता है. साथ ही, उनके अपने टाइप होते हैं, जैसे कि चार डाइमेंशन वाले वेक्टर के लिए vec4f
. 2D वेक्टर (vec2f
) और 3D वेक्टर (vec3f
) के लिए भी इसी तरह के टाइप मौजूद हैं!
- यह बताने के लिए कि दिखाई जा रही वैल्यू सही पोज़िशन पर है, इसे
@builtin(position)
एट्रिब्यूट के साथ मार्क करें.->
सिंबल का इस्तेमाल यह बताने के लिए किया जाता है कि फ़ंक्शन से यह वैल्यू मिलती है.
index.html (createShaderModule कोड)
@vertex
fn vertexMain() -> @builtin(position) vec4f {
}
अगर फ़ंक्शन का रिटर्न टाइप है, तो आपको फ़ंक्शन के मुख्य हिस्से में कोई वैल्यू रिटर्न करनी होगी. वापस लौटने के लिए, सिंटैक्स vec4f(x, y, z, w)
का इस्तेमाल करके एक नया vec4f
बनाया जा सकता है. x
, y
, और z
वैल्यू, फ़्लोटिंग पॉइंट वाली संख्याएं हैं. ये वैल्यू, रिटर्न वैल्यू में बताती हैं कि क्लिप स्पेस में वर्टिक्स कहां है.
(0, 0, 0, 1)
की स्टैटिक वैल्यू दिखाएं. हालांकि, तकनीकी तौर पर आपके पास मान्य वर्टिक्स शेडर है, लेकिन यह कभी भी कुछ नहीं दिखाता. ऐसा इसलिए होता है, क्योंकि GPU यह समझ लेता है कि उसके बनाए गए ट्राएंगल सिर्फ़ एक पॉइंट हैं और फिर उन्हें खारिज कर देता है.
index.html (createShaderModule कोड)
@vertex
fn vertexMain() -> @builtin(position) vec4f {
return vec4f(0, 0, 0, 1); // (X, Y, Z, W)
}
इसके बजाय, आपको अपने बनाए गए बफ़र के डेटा का इस्तेमाल करना है. इसके लिए, अपने फ़ंक्शन के लिए @location()
एट्रिब्यूट और टाइप के साथ एक आर्ग्युमेंट का एलान करें, जो vertexBufferLayout
में बताए गए एट्रिब्यूट और टाइप से मेल खाता हो. आपने 0
का shaderLocation
बताया है. इसलिए, अपने WGSL कोड में, @location(0)
का इस्तेमाल करके आर्ग्युमेंट चुनें. आपने फ़ॉर्मैट को float32x2
के तौर पर भी बताया है, जो एक 2D वेक्टर है. इसलिए, WGSL में आपका तर्क vec2f
है. इसे अपनी पसंद का कोई भी नाम दिया जा सकता है. हालांकि, ये आपके वर्टिक्स की पोज़िशन दिखाते हैं, इसलिए pos जैसा नाम देना सही रहेगा.
- अपने शेडर फ़ंक्शन को इस कोड में बदलें:
index.html (createShaderModule कोड)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(0, 0, 0, 1);
}
अब आपको उस पोज़िशन पर वापस आना होगा. क्योंकि स्थिति 2D वेक्टर है और रिटर्न टाइप 4D वेक्टर है, इसलिए आपको इसे थोड़ा सा बदलना होगा. आपको पोज़िशन आर्ग्युमेंट से दो कॉम्पोनेंट लेने हैं और उन्हें रिटर्न वैक्टर के पहले दो कॉम्पोनेंट में डालना है. साथ ही, आखिरी दो कॉम्पोनेंट को क्रमशः 0
और 1
के तौर पर छोड़ना है.
- सही पोज़िशन दिखाने के लिए, साफ़ तौर पर बताएं कि किन पोज़िशन कॉम्पोनेंट का इस्तेमाल करना है:
index.html (createShaderModule कोड)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos.x, pos.y, 0, 1);
}
हालांकि, शेडर में इस तरह की मैपिंग बहुत आम बात है, इसलिए आपके पास पोज़िशन वेक्टर को आसान शॉर्टहैंड में पहले आर्ग्युमेंट के तौर पर पास करने का विकल्प होता है और इसका मतलब भी यही है.
return
स्टेटमेंट को इस कोड से फिर से लिखें:
index.html (createShaderModule कोड)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
यह आपका शुरुआती वर्टेक्स शेडर है! यह बहुत आसान है. इसमें पोज़िशन में कोई बदलाव नहीं किया जाता, लेकिन शुरुआत करने के लिए यह काफ़ी है.
फ़्रैगमेंट शेडर तय करना
आगे है फ़्रैगमेंट शेडर. फ़्रैगमेंट शेडर, वर्टेक्स शेडर की तरह ही काम करते हैं. हालांकि, हर वर्टेक्स के लिए, इनका इस्तेमाल करने के बजाय, हर पिक्सल ड्रॉ किए जाने पर उनका इस्तेमाल किया जाता है.
फ़्रैगमेंट शेडर को हमेशा वर्टेक्स शेडर के बाद कॉल किया जाता है. जीपीयू, वर्टेक्स शेडर का आउटपुट लेता है और उसे ट्राऐंगल करता है. इस तरह, तीन पॉइंट के सेट में से ट्रायएंगल बन जाते हैं. इसके बाद, यह उन सभी त्रिभुजों को रेस्टर करता है. इसके लिए, यह पता लगाता है कि आउटपुट कलर अटैचमेंट के कौनसे पिक्सल उस त्रिभुज में शामिल हैं. इसके बाद, उन सभी पिक्सल के लिए एक बार फ़्रेगमेंट शेडर को कॉल करता है. फ़्रैगमेंट शेडर एक रंग दिखाता है. आम तौर पर, इस रंग का हिसाब, वर्टिक्स शेडर और टेक्सचर जैसी ऐसेट से भेजी गई वैल्यू से लगाया जाता है. GPU, इस रंग को कलर अटैचमेंट में लिखता है.
वर्टिक्स शेडर की तरह ही, फ़्रैगमेंट शेडर भी एक साथ कई प्रोसेस में काम करते हैं. इनपुट और आउटपुट के मामले में, ये वर्टिक्स शेडर से थोड़े ज़्यादा फ़्लेक्सिबल होते हैं. हालांकि, इनकी मदद से हर त्रिभुज के हर पिक्सल के लिए सिर्फ़ एक रंग दिखाया जा सकता है.
WGSL फ़्रैगमेंट शेडर फ़ंक्शन को @fragment
एट्रिब्यूट से दिखाया जाता है. साथ ही, यह एक vec4f
भी दिखाता है. हालांकि, इस मामले में वेक्टर किसी रंग को दिखाता है, न कि किसी स्थिति को. रिटर्न वैल्यू को @location
एट्रिब्यूट देना ज़रूरी है, ताकि यह पता चल सके कि beginRenderPass
कॉल में से किस colorAttachment
में लौटाया गया रंग लिखा गया है. आपके मैसेज में सिर्फ़ एक अटैचमेंट है, इसलिए जगह की जानकारी 0 है.
- इस तरह से एक खाली
@fragment
फ़ंक्शन बनाएं:
index.html (createShaderModule कोड)
@fragment
fn fragmentMain() -> @location(0) vec4f {
}
दिखाए गए वेक्टर के चार कॉम्पोनेंट, लाल, हरा, नीला, और अल्फा कलर वैल्यू होते हैं. इन्हें उसी तरह से समझा जाता है जिस तरह से आपने पहले beginRenderPass
में clearValue
सेट किया था. इसलिए, vec4f(1, 0, 0, 1)
चटक लाल है, जो आपके स्क्वेयर के लिए एक अच्छा रंग है. हालांकि, इसे अपनी पसंद के किसी भी रंग पर सेट किया जा सकता है!
- दिखाए गए कलर वेक्टर को इस तरह सेट करें:
index.html (createShaderModule कोड)
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1); // (Red, Green, Blue, Alpha)
}
यह एक पूरा फ़्रैगमेंट शेडर है! यह बहुत दिलचस्प नहीं है; यह सिर्फ़ हर त्रिभुज के हर पिक्सल को लाल रंग में सेट करता है, लेकिन फ़िलहाल यह काफ़ी है.
आपको याद दिला दें कि ऊपर बताया गया शेडर कोड जोड़ने के बाद, आपका createShaderModule
कॉल अब कुछ ऐसा दिखता है:
index.html
const cellShaderModule = device.createShaderModule({
label: 'Cell shader',
code: `
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`
});
रेंडर पाइपलाइन बनाना
शेडर मॉड्यूल का इस्तेमाल, रेंडरिंग के लिए अपने-आप नहीं किया जा सकता. इसके बजाय, आपको इसे GPURenderPipeline
के हिस्से के तौर पर इस्तेमाल करना होगा. इसे device.createRenderPipeline() को कॉल करके बनाया गया है. रेंडर पाइपलाइन यह कंट्रोल करती है कि किस तरह ज्यामिति कैसे ड्रॉ की जाए, जैसे कि शेडर का इस्तेमाल कैसे किया जाए, वर्टेक्स बफ़र में डेटा को कैसे समझें, किस तरह की ज्यामिति रेंडर की जानी चाहिए (लाइन, पॉइंट, त्रिकोण...), और बहुत कुछ!
रेंडर पाइपलाइन पूरे एपीआई में सबसे मुश्किल ऑब्जेक्ट है, लेकिन चिंता न करें! इसमें पास की जा सकने वाली ज़्यादातर वैल्यू वैकल्पिक होती हैं. शुरू करने के लिए, आपको सिर्फ़ कुछ वैल्यू देने की ज़रूरत होती है.
- इस तरह की रेंडर पाइपलाइन बनाएं:
index.html
const cellPipeline = device.createRenderPipeline({
label: "Cell pipeline",
layout: "auto",
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout]
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat
}]
}
});
हर पाइपलाइन में एक layout
होना चाहिए, जो बताता हो कि पाइपलाइन को किस तरह के इनपुट (वर्टेक्स बफ़र के अलावा) की ज़रूरत है, लेकिन असल में उनमें कोई इनपुट नहीं है. अच्छी बात यह है कि फ़िलहाल, आपके पास "auto"
को पास करने का विकल्प है. इसके बाद, शेडर से पाइपलाइन अपना लेआउट बनाती है.
इसके बाद, आपको vertex
चरण के बारे में जानकारी देनी होगी. module
, GPUShaderModule है, जिसमें आपका वर्टिक्स शेडर मौजूद होता है. साथ ही, entryPoint
, शेडर कोड में उस फ़ंक्शन का नाम देता है जिसे हर वर्टिक्स के लिए कॉल किया जाता है. (आपके पास एक शेडर मॉड्यूल में कई @vertex
और @fragment
फ़ंक्शन हो सकते हैं!) बफ़र, GPUVertexBufferLayout
ऑब्जेक्ट का कलेक्शन होता है. इससे पता चलता है कि इस पाइपलाइन का इस्तेमाल करके, आपके डेटा को वर्टेक्स बफ़र में कैसे पैक किया जाता है. अच्छी बात यह है कि आपने इसे पहले ही अपने vertexBufferLayout
में तय कर दिया था! यहां आपको इसे पास करना है.
आखिर में, आपको fragment
चरण के बारे में जानकारी देनी होगी. इसमें शेडर मॉड्यूल और एंट्रीपॉइंट भी शामिल है, जैसे कि वर्टेक्स स्टेज. आखिरी बिट, targets
को तय करना है, जिसके साथ इस पाइपलाइन का इस्तेमाल किया जाता है. यह डिक्शनरी का एक कलेक्शन है, जिसमें उन कलर अटैचमेंट की जानकारी होती है जिन्हें पाइपलाइन आउटपुट करती है. जैसे, टेक्सचर format
. यह ज़रूरी है कि ये जानकारी, इस पाइपलाइन के साथ इस्तेमाल किए जाने वाले किसी भी रेंडर पास के colorAttachments
में दिए गए टेक्सचर से मेल खाएं. आपका रेंडर पास, कैनवस कॉन्टेक्स्ट से टेक्स्चर का इस्तेमाल करता है और उस वैल्यू का इस्तेमाल करता है जिसे आपने canvasFormat
में उसके फ़ॉर्मैट के लिए सेव किया है, ताकि आप यहां उसी फ़ॉर्मैट को पास कर सकें.
यह उन सभी विकल्पों के करीब भी नहीं है जिन्हें रेंडर करने वाले सिस्टम को बनाते समय तय किया जा सकता है, लेकिन यह कोडलैब इस कोडलैब की ज़रूरतों को पूरा करने के लिए काफ़ी है!
स्क्वेयर बनाना
अब आपके पास स्क्वेयर बनाने के लिए, ज़रूरी सभी चीज़ें हैं!
- स्क्वेयर बनाने के लिए, कॉल के
encoder.beginRenderPass()
औरpass.end()
पेयर पर वापस जाएं. इसके बाद, उनके बीच ये नए निर्देश जोड़ें:
index.html
// After encoder.beginRenderPass()
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertices.length / 2); // 6 vertices
// before pass.end()
इससे WebGPU में वह सारी जानकारी मौजूद होती है जो आपके स्क्वेयर को बनाने के लिए ज़रूरी है. सबसे पहले, setPipeline()
का इस्तेमाल करके यह तय करें कि ड्रॉ करने के लिए किस पाइपलाइन का इस्तेमाल किया जाना चाहिए. इसमें इस्तेमाल किए गए शेडर, वर्टिक्स डेटा का लेआउट, और स्टेटस से जुड़ा अन्य काम का डेटा शामिल होता है.
इसके बाद, अपने स्क्वेयर के वर्टिसेस वाले बफ़र के साथ setVertexBuffer()
को कॉल करें. आप इसे 0
के साथ कॉल करते हैं, क्योंकि यह बफ़र मौजूदा पाइपलाइन की vertex.buffers
परिभाषा में 0वें एलिमेंट से मेल खाता है.
आखिर में, आपको draw()
कॉल करना होता है, जो पहले वाले सेटअप के बाद भी आसान लगता है. आपको सिर्फ़ उन वर्टिसेस की संख्या देनी होगी जिन्हें रेंडर करना है. यह संख्या, फ़िलहाल सेट किए गए वर्टिसेस बफ़र से ली जाती है और फ़िलहाल सेट की गई पाइपलाइन के साथ उसका विश्लेषण किया जाता है. इसे 6
पर हार्ड-कोड किया जा सकता है, लेकिन इसे वर्टिसेस कलेक्शन (हर वर्टिसेस के लिए 12 फ़्लोट / 2 निर्देशांक == 6 वर्टिसेस) से कैलकुलेट करने का मतलब है कि अगर आपको कभी स्क्वेयर को किसी सर्कल से बदलना है, तो आपको हाथ से कम अपडेट करने होंगे.
- अपनी स्क्रीन रीफ़्रेश करें और आखिरकार, अपनी मेहनत का नतीजा देखें: एक बड़ा रंगीन स्क्वेयर.
5. ग्रिड बनाना
सबसे पहले, खुद को बधाई दें! ज़्यादातर जीपीयू एपीआई में, स्क्रीन पर ज्यामिति के पहले बिट हासिल करना अक्सर सबसे मुश्किल चरणों में से एक होता है. यहां से सभी कार्रवाइयां, छोटे-छोटे चरणों में की जा सकती हैं. इससे आपको आगे बढ़ने के दौरान आसानी से अपनी प्रोग्रेस की पुष्टि करने में मदद मिलती है.
इस सेक्शन में, आपको इनके बारे में जानकारी मिलेगी:
- JavaScript से शेडर में वैरिएबल (जिन्हें यूनिफ़ॉर्म कहा जाता है) पास करने का तरीका.
- रेंडरिंग के तरीके को बदलने के लिए, यूनिफ़ॉर्म इस्तेमाल करने का तरीका.
- एक ही ज्यामिति के कई अलग-अलग वैरिएंट बनाने के लिए, इंस्टेंसिंग का इस्तेमाल कैसे करें.
ग्रिड तय करना
ग्रिड दिखाने के लिए, आपको इससे जुड़ी एक बुनियादी जानकारी पता होनी चाहिए. इसमें चौड़ाई और ऊंचाई, दोनों में कितनी सेल हैं? यह डेवलपर के ऊपर है कि वह ग्रिड को किस तरह सेट करता है. हालांकि, चीज़ों को आसान बनाने के लिए, ग्रिड को स्क्वेयर (एक जैसी चौड़ाई और ऊंचाई) के तौर पर सेट करें. साथ ही, ऐसे साइज़ का इस्तेमाल करें जो दो की घात हो. (इससे बाद में, गणित के कुछ सवालों को हल करना आसान हो जाता है.) आपको इसे बाद में बड़ा करना है, लेकिन इस सेक्शन के बाकी हिस्से के लिए, अपने ग्रिड का साइज़ 4x4 पर सेट करें. इससे इस सेक्शन में इस्तेमाल किए गए कुछ गणित को आसानी से दिखाया जा सकता है. बाद में बड़ा करें!
- अपने JavaScript कोड में सबसे ऊपर एक कॉन्स्टेंट जोड़कर, ग्रिड का साइज़ तय करें.
index.html
const GRID_SIZE = 4;
इसके बाद, आपको अपने स्क्वेयर को रेंडर करने का तरीका अपडेट करना होगा ताकि आप कैनवस पर उनमें से GRID_SIZE
गुना GRID_SIZE
फ़िट हो सकें. इसका मतलब है कि वर्ग का आकार बहुत छोटा होना चाहिए और उसमें बहुत सारी संख्या होनी चाहिए.
अब, इस समस्या को हल करने का एक तरीका यह है कि आप अपने वर्टिक्स बफ़र को काफ़ी बड़ा बनाएं और सही साइज़ और पोज़िशन में, GRID_SIZE
के GRID_SIZE
गुना स्क्वेयर तय करें. असल में, इसके लिए कोड बनाना बहुत मुश्किल नहीं होगा! बस कुछ 'for लूप' और थोड़ा सा हिसाब-किताब. हालांकि, इससे जीपीयू का बेहतर तरीके से इस्तेमाल नहीं हो पा रहा है. साथ ही, इफ़ेक्ट पाने के लिए ज़रूरत से ज़्यादा मेमोरी का इस्तेमाल किया जा रहा है. इस सेक्शन में, जीपीयू के हिसाब से बेहतर तरीके के बारे में बताया गया है.
यूनिफ़ॉर्म बफ़र बनाना
सबसे पहले, आपको शेडर को चुना गया ग्रिड साइज़ बताना होगा, क्योंकि वह चीज़ों को दिखाने के तरीके में बदलाव करने के लिए इसका इस्तेमाल करता है. शेडर में साइज़ को हार्ड-कोड किया जा सकता है. हालांकि, इसका मतलब है कि जब भी आपको ग्रिड का साइज़ बदलना हो, तो आपको शेडर और रेंडर पाइपलाइन को फिर से बनाना होगा. यह महंगा होता है. शेडर को ग्रिड साइज़ को एक समान के तौर पर देना एक बेहतर तरीका है.
आपको पहले पता चला था कि वर्टेक्स बफ़र से, एक अलग वैल्यू को वर्टेक्स शेडर के हर बार शुरू करने पर पास किया जाता है. यूनिफ़ॉर्म किसी बफ़र की वैल्यू को डिकोड करता है. यह वैल्यू, शुरू होने वाले हर सवाल के लिए एक जैसी रहती है. वे ज्यामिति के किसी हिस्से (जैसे उसकी स्थिति), ऐनिमेशन के पूरे फ़्रेम (जैसे कि मौजूदा समय), या ऐप्लिकेशन के पूरे जीवनकाल (जैसे उपयोगकर्ता की पसंद) के लिए आम तौर पर इस्तेमाल होने वाली वैल्यू बताने के लिए उपयोगी हैं.
- नीचे दिया गया कोड जोड़कर एक यूनिफ़ॉर्म बफ़र बनाएं:
index.html
// Create a uniform buffer that describes the grid.
const uniformArray = new Float32Array([GRID_SIZE, GRID_SIZE]);
const uniformBuffer = device.createBuffer({
label: "Grid Uniforms",
size: uniformArray.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformArray);
यह आपको काफ़ी जाना-पहचाना लगेगा, क्योंकि यह वही कोड है जिसका इस्तेमाल आपने पहले वर्टिक्स बफ़र बनाने के लिए किया था! ऐसा इसलिए होता है, क्योंकि यूनिफ़ॉर्म को WebGPU API में उन ही GPUBuffer ऑब्जेक्ट के ज़रिए भेजा जाता है जिनका इस्तेमाल वर्टिकस के लिए किया जाता है. हालांकि, इस बार usage
में GPUBufferUsage.VERTEX
के बजाय GPUBufferUsage.UNIFORM
शामिल होता है.
शेडर में यूनिफ़ॉर्म ऐक्सेस करना
- नीचे दिया गया कोड जोड़कर यूनिफ़ॉर्म तय करें:
index.html (createShaderModule कॉल)
// At the top of the `code` string in the createShaderModule() call
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos / grid, 0, 1);
}
// ...fragmentMain is unchanged
इससे आपके शेडर में grid
नाम का एक यूनिफ़ॉर्म तय होता है. यह एक 2D फ़्लोट वेक्टर है, जो उस ऐरे से मेल खाता है जिसे आपने अभी यूनिफ़ॉर्म बफ़र में कॉपी किया है. इससे यह भी पता चलता है कि यूनिफ़ॉर्म की वैल्यू @group(0)
और @binding(0)
है. आपको इन वैल्यू का मतलब कुछ ही समय में पता चल जाएगा.
इसके बाद, शेडर कोड में कहीं भी ग्रिड वेक्टर का इस्तेमाल किया जा सकता है. इस कोड में, वर्टिक्स की पोज़िशन को ग्रिड वेक्टर से भाग दिया जाता है. pos
, 2D वेक्टर है और grid
2D वेक्टर है. इसलिए, WGSL कॉम्पोनेंट के हिसाब से विभाजन करता है. दूसरे शब्दों में, नतीजा वही है जो vec2f(pos.x / grid.x, pos.y / grid.y)
कहने पर मिलता है.
जीपीयू शेडर में इस तरह की वेक्टर कार्रवाइयां बहुत आम हैं, क्योंकि कई रेंडरिंग और कंप्यूट तकनीकें इन पर ही निर्भर करती हैं!
आपके मामले में इसका मतलब यह है कि अगर आपने ग्रिड का साइज़ 4 पर सेट किया है, तो रेंडर किया गया स्क्वेयर अपने मूल साइज़ का एक चौथाई होगा. अगर आपको किसी पंक्ति या कॉलम में चार आइटम फ़िट करने हैं, तो यह सही है!
बाइंड ग्रुप बनाना
हालांकि, शेडर में यूनिफ़ॉर्म का एलान करने से, उसे आपके बनाए गए बफ़र से कनेक्ट नहीं किया जाता. ऐसा करने के लिए, आपको बाइंड ग्रुप बनाना और सेट करना होगा.
बाइंड ग्रुप, उन संसाधनों का कलेक्शन होता है जिन्हें आपको एक ही समय पर अपने शेडर के लिए ऐक्सेस करना है. इसमें कई तरह के बफ़र शामिल हो सकते हैं, जैसे कि यूनिफ़ॉर्म बफ़र. इसके अलावा, टेक्सचर और सैंपलर जैसे अन्य संसाधन भी शामिल हो सकते हैं जो यहां नहीं दिए गए हैं, लेकिन WebGPU रेंडरिंग तकनीकों के सामान्य हिस्से हैं.
- यूनिफ़ॉर्म बफ़र और पाइपलाइन बनाने के बाद, नीचे दिया गया कोड जोड़कर, अपने यूनिफ़ॉर्म बफ़र वाला बाइंड ग्रुप बनाएं:
index.html
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}],
});
आपके मौजूदा स्टैंडर्ड label
के अलावा, आपको layout
की भी ज़रूरत होगी. इससे पता चलेगा कि इस बाइंड ग्रुप में किस तरह के संसाधन शामिल हैं. आने वाले समय में, इस पर और काम किया जाएगा. हालांकि, कुछ समय के लिए, अपनी पाइपलाइन से बाइंड ग्रुप का लेआउट मांगा जा सकता है, क्योंकि आपने layout: "auto"
के साथ पाइपलाइन बनाई है. इससे, पिपलाइन उन बाइंडिंग से अपने-आप बाइंड ग्रुप लेआउट बनाती है जिन्हें आपने शेडर कोड में खुद से एलान किया है. इस मामले में, आपने getBindGroupLayout(0)
से पूछा है, जहां 0
उस @group(0)
से मेल खाता है जिसे आपने शेडर में टाइप किया था.
लेआउट तय करने के बाद, entries
का ऐरे दें. हर एंट्री एक डिक्शनरी है, जिसमें कम से कम ये वैल्यू हैं:
binding
, यह शेडर में डाली गई@binding()
वैल्यू के बराबर होती है. इस मामले में,0
.resource
, यह ऐसा असल संसाधन है जिसे तय किए गए बाइंडिंग इंडेक्स पर, वैरिएबल के लिए दिखाना है. इस मामले में, आपका यूनिफ़ॉर्म बफ़र.
यह फ़ंक्शन GPUBindGroup
दिखाता है, जो एक ओपेक और अपरिवर्तनीय हैंडल है. बंड ग्रुप बनाने के बाद, उन संसाधनों को नहीं बदला जा सकता जिन पर वह ग्रुप पॉइंट करता है. हालांकि, उन संसाधनों के कॉन्टेंट को बदला जा सकता है. उदाहरण के लिए, अगर एक जैसे बफ़र में नया ग्रिड साइज़ शामिल किया जाता है, तो यह बाइंड किए गए इस ग्रुप का इस्तेमाल करके, आने वाले समय में ड्रॉ कॉल में दिखेगा.
बाइंड ग्रुप को बाइंड करें
बाइंड ग्रुप बन जाने के बाद भी, आपको WebGPU को ड्रॉइंग के दौरान इसका इस्तेमाल करने के लिए कहना होगा. अच्छी बात यह है कि यह काफ़ी आसान है.
- रेंडर पास पर वापस जाएं और
draw()
तरीके से पहले यह नई लाइन जोड़ें:
index.html
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setBindGroup(0, bindGroup); // New line!
pass.draw(vertices.length / 2);
पहले आर्ग्युमेंट के तौर पर पास किया गया 0
, शेडर कोड में मौजूद @group(0)
से मेल खाता है. इसका मतलब है कि @group(0)
का हिस्सा होने वाला हर @binding
, इस बाइंड ग्रुप में मौजूद रिसॉर्स का इस्तेमाल करता है.
अब एक जैसा बफ़र, आपके शेडर में दिखने लगा है!
- अपना पेज रीफ़्रेश करें. इसके बाद, आपको कुछ ऐसा दिखेगा:
बहुत बढ़िया! आपका स्क्वेयर अब पहले के मुकाबले एक चौथाई हो गया है! हालांकि, यह ज़्यादा ज़रूरी नहीं है, लेकिन इससे पता चलता है कि असल में आपकी यूनिफ़ॉर्म इस्तेमाल की गई है और शेडर अब आपके ग्रिड के साइज़ को ऐक्सेस कर सकता है.
शेडर में ज्यामिति में बदलाव करना
अब शेडर में ग्रिड के साइज़ का रेफ़रंस दिया जा सकता है. इसलिए, अपने पसंदीदा ग्रिड पैटर्न में फ़िट करने के लिए, रेंडर की जा रही ज्यामिति में बदलाव किया जा सकता है. ऐसा करने के लिए, यह तय करें कि आपको क्या हासिल करना है.
आपको अपने कैनवस को सैद्धांतिक तौर पर अलग-अलग सेल में बांटना होगा. इस नियम को बनाए रखने के लिए कि दाईं ओर जाने पर X ऐक्सिस और ऊपर जाने पर Y ऐक्सिस बढ़ता है, मान लें कि पहला सेल कैनवस के सबसे नीचे बाएं कोने में है. इससे आपको ऐसा लेआउट मिलता है जो कुछ ऐसा दिखता है, जिसमें आपकी मौजूदा स्क्वेयर ज्यामिति बीच में होती है:
आपके लिए शेडर में एक ऐसा तरीका खोजना होगा जिससे आप सेल के निर्देशांकों के आधार पर, इनमें से किसी भी सेल में स्क्वेयर ज्यामिति की जगह तय कर सकें.
सबसे पहले, आपको दिखेगा कि आपका स्क्वेयर किसी भी सेल के साथ ठीक से अलाइन नहीं है, क्योंकि इसे कैनवस के बीच में रखने के लिए तय किया गया था. आपको स्क्वेयर को आधी सेल तक शिफ़्ट करना होगा, ताकि वह उनमें अच्छी तरह से अलाइन हो जाए.
इसे ठीक करने का एक तरीका यह है कि स्क्वेयर के वर्टिक्स बफ़र को अपडेट करें. उदाहरण के लिए, नीचे बाईं ओर का कोना (-0.8, -0.8) के बजाय (0.1, 0.1) पर हो, ताकि इस स्क्वेयर को सेल की सीमाओं के साथ बेहतर तरीके से अलाइन किया जा सके. हालांकि, आपके शेडर में वर्टेक्स को प्रोसेस करने के तरीके पर आपका पूरा कंट्रोल होता है, इसलिए शेडर कोड का इस्तेमाल करके उन्हें सही जगह पर लगाना उतना ही आसान है!
- नीचे दिए गए कोड की मदद से, वर्टिक्स शेडर मॉड्यूल में बदलाव करें:
index.html (createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
// Add 1 to the position before dividing by the grid size.
let gridPos = (pos + 1) / grid;
return vec4f(gridPos, 0, 1);
}
ऐसा करने से, ग्रिड साइज़ से भाग करने से पहले हर वर्टिक्स को ऊपर और दाईं ओर एक-एक (यानी क्लिप स्पेस का आधा) ले जाया जाता है. इसकी वजह से हमें ऑरिजिन से कुछ ही दूरी पर एक ग्रिड से अलाइन किया गया स्क्वेयर मिलता है.
इसके बाद, आपको अपनी ज्यामिति की पोज़िशन को ग्रिड साइज़ से भाग देने के बाद, (-1, -1) से ट्रांसलेट करना होगा, ताकि उसे उस कोने में ले जाया जा सके. ऐसा इसलिए करना होगा, क्योंकि आपके कैनवस के कोऑर्डिनेट सिस्टम में (0, 0) बीच में और (-1, -1) सबसे नीचे बाईं ओर होता है.
- अपनी ज्यामिति की जगह का अनुवाद इस तरह करें:
index.html (createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
// Subtract 1 after dividing by the grid size.
let gridPos = (pos + 1) / grid - 1;
return vec4f(gridPos, 0, 1);
}
अब आपका स्क्वेयर, सेल (0, 0) में सही जगह पर दिख रहा है!
अगर आप इसे किसी दूसरे सेल में रखना चाहते हैं, तो क्या होगा? अपने शेडर में cell
वेक्टर का एलान करके और उसे let cell = vec2f(1, 1)
जैसी स्टैटिक वैल्यू से पॉप्युलेट करके, इसका पता लगाएं.
अगर इसे gridPos
में जोड़ा जाता है, तो यह एल्गोरिदम में - 1
को पहले जैसा कर देता है. इसलिए, ऐसा नहीं करना चाहिए. इसके बजाय, आपको हर सेल के लिए स्क्वेयर को सिर्फ़ एक ग्रिड यूनिट (कैनवस के चौथाई हिस्से) में ले जाना है. ऐसा लगता है कि आपको grid
से फिर से भाग देना होगा!
- अपने ग्रिड की जगह को इस तरह से बदलें:
index.html (createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1); // Cell(1,1) in the image above
let cellOffset = cell / grid; // Compute the offset to cell
let gridPos = (pos + 1) / grid - 1 + cellOffset; // Add it here!
return vec4f(gridPos, 0, 1);
}
रीफ़्रेश करने पर, आपको ये चीज़ें दिखेंगी:
हम्म. ऐसा नहीं है कि आपको जो चाहिए था वह आपको मिल गया.
इसकी वजह यह है कि कैनवस के निर्देशांक -1 से +1 तक होते हैं. इसलिए, यह असल में दो यूनिट चौड़ा होता है. इसका मतलब है कि अगर आपको कैनवस के एक-चौथाई हिस्से पर मौजूद किसी वर्टिक्स को आगे ले जाना है, तो आपको उसे 0.5 यूनिट आगे ले जाना होगा. जीपीयू कोऑर्डिनेट के साथ रीज़निंग से जुड़े सवालों के जवाब देते समय, यह एक आसान गलती है! अच्छी बात यह है कि इसे ठीक करना उतना ही आसान है.
- अपने ऑफ़सेट को 2 से गुणा करें, इस तरह से:
index.html (createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1);
let cellOffset = cell / grid * 2; // Updated
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
इससे आपको अपनी पसंद के मुताबिक नतीजे मिलते हैं.
स्क्रीनशॉट ऐसा दिखता है:
इसके अलावा, अब cell
को ग्रिड के अंदर की किसी भी वैल्यू पर सेट किया जा सकता है. इसके बाद, इमेज को अपनी पसंद की जगह पर देखने के लिए रीफ़्रेश किया जा सकता है.
इंस्टेंस बनाना
अब स्क्वेयर को उस जगह पर रखें जहां आपको उसे कैलकुलेट करना है. इसके बाद, ग्रिड के हर सेल में एक स्क्वेयर रेंडर करें.
इस तक पहुंचने का एक तरीका यह है कि सेल के निर्देशांक को एक समान बफ़र में लिखें. इसके बाद, ग्रिड में हर स्क्वेयर के लिए ड्रॉ करें को कॉल करें और हर बार यूनिफ़ॉर्म को अपडेट करें. हालांकि, यह बहुत धीमा होगा, क्योंकि जीपीयू को हर बार JavaScript से नए निर्देशांक के तैयार होने का इंतज़ार करना पड़ता है. जीपीयू से अच्छी परफ़ॉर्मेंस पाने के लिए, यह ज़रूरी है कि सिस्टम के दूसरे हिस्सों के इंतज़ार में जीपीयू का कम से कम समय बीतता हो!
इसके बजाय, आप इंस्टेंसिंग नाम की तकनीक का इस्तेमाल कर सकते हैं. इंस्टेंसिंग, जीपीयू को यह बताने का एक तरीका है कि draw
को एक कॉल करके एक ही ज्यामिति की कई कॉपी बनाई जा सकती हैं. यह हर कॉपी के लिए, draw
को एक बार कॉल करने के मुकाबले ज़्यादा तेज़ है. ज्यामिति की हर कॉपी को इंस्टेंस कहा जाता है.
- जीपीयू को यह बताने के लिए कि आपको ग्रिड भरने के लिए अपने स्क्वेयर के ज़रूरत के मुताबिक इंस्टेंस चाहिए, अपने मौजूदा ड्रॉ कॉल में एक आर्ग्युमेंट जोड़ें:
index.html
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
इससे सिस्टम को पता चलता है कि आपको अपने स्क्वेयर के छह (vertices.length / 2
) वर्टिसेस को 16 (GRID_SIZE * GRID_SIZE
) बार ड्रॉ करना है. हालांकि, पेज को रीफ़्रेश करने पर भी आपको ये चीज़ें दिखेंगी:
क्यों? यह इसलिए है, क्योंकि आप इन सभी 16 स्क्वेयर को एक ही जगह पर बनाते हैं. आपको शेडर में कुछ अतिरिक्त लॉजिक की ज़रूरत होगी जो हर इंस्टेंस के हिसाब से ज्यामिति की जगह बनाता हो.
शेडर में, pos
जैसे वेरटेक्स एट्रिब्यूट के अलावा, वे वैल्यू भी ऐक्सेस की जा सकती हैं जिन्हें WGSL की बिल्ट-इन वैल्यू कहा जाता है. ये वैल्यू, आपके वेरटेक्स बफ़र से मिलती हैं. ये वे वैल्यू होती हैं जिनका हिसाब WebGPU के ज़रिए लगाया जाता है. इनमें से एक वैल्यू instance_index
होती है. instance_index
, 0
से number of instances - 1
तक साइन नहीं किया गया 32-बिट नंबर होता है. इसका इस्तेमाल शेडर लॉजिक के हिस्से के तौर पर किया जा सकता है. प्रोसेस किए गए हर ऐसे वर्टिक्स के लिए इसकी वैल्यू एक ही होती है जो एक ही इंस्टेंस का हिस्सा होता है. इसका मतलब है कि आपके वर्टेक्स बफ़र की हर पोज़िशन के लिए, 0
के instance_index
के साथ आपके वर्टेक्स शेडर को छह बार कॉल किया जाता है. इसके बाद, instance_index
के 1
के साथ छह और बार, फिर instance_index
के 2
के साथ छह और बार, और इसी तरह आगे भी.
इसे काम करते हुए देखने के लिए, आपको अपने शेडर इनपुट में पहले से मौजूद instance_index
को जोड़ना होगा. इसे उसी तरह सेट करें जिस तरह पोज़िशन को सेट किया जाता है. हालांकि, इसे @location
एट्रिब्यूट के साथ टैग करने के बजाय, @builtin(instance_index)
का इस्तेमाल करें. इसके बाद, आर्ग्युमेंट को अपनी पसंद का नाम दें. (उदाहरण के तौर पर दिए गए कोड से मैच करने के लिए, इसे instance
भी कहा जा सकता है.) इसके बाद, इसे शेडर लॉजिक के हिस्से के तौर पर इस्तेमाल करें!
- सेल निर्देशांक की जगह पर
instance
का इस्तेमाल करें:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance); // Save the instance_index as a float
let cell = vec2f(i, i); // Updated
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
अब रीफ़्रेश करने पर, आपको दिखेगा कि आपके पास एक से ज़्यादा स्क्वेयर हैं! हालांकि, आपको इनमें से सभी 16 विकल्प नहीं दिखेंगे.
ऐसा इसलिए होता है, क्योंकि आपके जनरेट किए गए सेल कोऑर्डिनेट (0, 0), (1, 1), (2, 2)... से लेकर (15, 15) तक होते हैं. हालांकि, इनमें से सिर्फ़ पहले चार को कैनवस पर दिखाया जा सकता है. अपनी पसंद का ग्रिड बनाने के लिए, आपको instance_index
को इस तरह बदलना होगा कि हर इंडेक्स आपके ग्रिड में मौजूद किसी यूनीक सेल से मैप हो, जैसे:
इसका गणित काफ़ी आसान है. हर सेल की X वैल्यू के लिए, instance_index
का modulo और ग्रिड की चौड़ाई चाहिए. इसे WGSL में %
ऑपरेटर के साथ इस्तेमाल किया जा सकता है. साथ ही, आपको हर सेल की Y वैल्यू के लिए, instance_index
को ग्रिड की चौड़ाई से भाग देना है. साथ ही, बचे हुए हिस्से को हटाना है. WGSL के floor()
फ़ंक्शन का इस्तेमाल करके ऐसा किया जा सकता है.
- कैलकुलेशन में बदलाव करें, जैसे:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
// Compute the cell coordinate from the instance_index
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
कोड को अपडेट करने के बाद, आखिर में आपको स्क्वेयर का लंबे समय से इंतज़ार किया हुआ ग्रिड मिल जाता है!
- अब जब यह काम करने लगा है, तो वापस जाएं और ग्रिड का साइज़ बढ़ाएं!
index.html
const GRID_SIZE = 32;
टाडा! अब इस ग्रिड को बहुत बड़ा बनाया जा सकता है और आपका सामान्य जीपीयू इसे आसानी से हैंडल कर लेता है. जीपीयू की परफ़ॉर्मेंस में किसी भी तरह की रुकावट आने से पहले ही, आपको अलग-अलग स्क्वेयर नहीं दिखेंगे.
6. अतिरिक्त क्रेडिट: इसे और भी रंगीन बनाएं!
इस समय, आसानी से अगले सेक्शन पर जाया जा सकता है, क्योंकि आपने कोडलैब के बाकी हिस्सों के लिए बुनियादी काम कर लिया है. हालांकि, एक जैसे रंग वाले स्क्वेयर का ग्रिड इस्तेमाल किया जा सकता है, लेकिन यह मज़ेदार नहीं है, है न? अच्छी बात यह है कि आप थोड़े ज़्यादा गणित और शेडर कोड का इस्तेमाल करके चीज़ों को थोड़ा चमकदार बना सकते हैं!
शेडर में स्ट्रक्चर का इस्तेमाल करना
अब तक, आपने वर्टिक्स शेडर से एक डेटा पास किया है: ट्रांसफ़ॉर्म की गई पोज़िशन. लेकिन असल में, वर्टेक्स शेडर से काफ़ी ज़्यादा डेटा दिखाया जा सकता है और फिर उसका इस्तेमाल फ़्रैगमेंट शेडर में किया जा सकता है!
वर्टिक्स शेडर से डेटा बाहर भेजने का एकमात्र तरीका, उसे वापस भेजना है. पोज़िशन पर वापस जाने के लिए, वर्टेक्स शेडर को हमेशा ज़रूरी होता है. इसलिए, अगर आपको इसके साथ कोई अन्य डेटा भी चाहिए, तो आपको इसे किसी स्ट्रक्चर में रखना होगा. WGSL में स्ट्रक्चर, नाम वाले ऑब्जेक्ट टाइप होते हैं. इनमें एक या उससे ज़्यादा नाम वाली प्रॉपर्टी होती हैं. प्रॉपर्टी को @builtin
और @location
जैसे एट्रिब्यूट के साथ भी मार्क अप किया जा सकता है. इनका एलान किसी फ़ंक्शन के बाहर किया जाता है. इसके बाद, ज़रूरत के हिसाब से इनके इंस्टेंस को फ़ंक्शन में और फ़ंक्शन से बाहर भेजा जा सकता है. उदाहरण के लिए, अपने मौजूदा वर्टिक्स शेडर को देखें:
index.html (createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
- फ़ंक्शन इनपुट और आउटपुट के लिए स्ट्रक्ट का इस्तेमाल करके एक ही चीज़ एक्सप्रेस करें:
index.html (createShaderModule कॉल)
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4f,
};
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
return output;
}
ध्यान दें कि इसके लिए, आपको input
की मदद से इनपुट पोज़िशन और इंस्टेंस इंडेक्स का रेफ़रंस देना होगा. साथ ही, पहले जो स्ट्रक्चर दिखाया जाता है उसे वैरिएबल के तौर पर एलान करना होगा और उसकी अलग-अलग प्रॉपर्टी सेट करनी होंगी. इस मामले में, इससे ज़्यादा फ़र्क़ नहीं पड़ता. असल में, इससे शेडर फ़ंक्शन थोड़ा लंबा हो जाता है. हालांकि, जैसे-जैसे आपके शेडर ज़्यादा जटिल होते जाते हैं, अपने डेटा को व्यवस्थित करने के लिए स्ट्रक्चर का इस्तेमाल करना एक बेहतरीन तरीका हो सकता है.
वर्टेक्स और फ़्रैगमेंट फ़ंक्शन के बीच डेटा पास करना
आपको याद दिला दें कि आपका @fragment
फ़ंक्शन जितना हो सके उतना आसान है:
index.html (createShaderModule कॉल)
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
आपने कोई इनपुट नहीं लिया है और आउटपुट में गहरा रंग (लाल) नहीं दिख रहा है. अगर शेडर को उस ज्यामिति के बारे में ज़्यादा जानकारी होती है जिसे वह रंग रहा है, तो उस अतिरिक्त डेटा का इस्तेमाल करके चीज़ों को थोड़ा ज़्यादा दिलचस्प बनाया जा सकता है. उदाहरण के लिए, क्या होगा अगर आप हर वर्ग के सेल कोऑर्डिनेट के आधार पर उसका रंग बदलना चाहते हैं? @vertex
चरण को पता होता है कि कौनसी सेल रेंडर की जा रही है; आपको बस इसे @fragment
स्टेज तक भेजना होगा.
वर्टेक्स और फ़्रैगमेंट स्टेज के बीच किसी भी डेटा को पास करने के लिए, आपको इसे हमारी पसंद के @location
के साथ आउटपुट स्ट्रक्चर में शामिल करना होगा. आपको सेल कोऑर्डिनेट पास करना है, इसलिए इसे पहले से मौजूद VertexOutput
स्ट्रक्चर में जोड़ें. इसके बाद, रिटर्न करने से पहले इसे @vertex
फ़ंक्शन में सेट करें.
- अपने वर्टिक्स शेडर की रिटर्न वैल्यू को इस तरह बदलें:
index.html (createShaderModule कॉल)
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4f,
@location(0) cell: vec2f, // New line!
};
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell; // New line!
return output;
}
@fragment
फ़ंक्शन में, उसी@location
के साथ आर्ग्युमेंट जोड़कर वैल्यू पाएं. (नाम का मेल खाना ज़रूरी नहीं है, लेकिन चीज़ों को ट्रैक करना आसान होता है!)
index.html (createShaderModule कॉल)
@fragment
fn fragmentMain(@location(0) cell: vec2f) -> @location(0) vec4f {
// Remember, fragment return values are (Red, Green, Blue, Alpha)
// and since cell is a 2D vector, this is equivalent to:
// (Red = cell.x, Green = cell.y, Blue = 0, Alpha = 1)
return vec4f(cell, 0, 1);
}
- इसके अलावा, इसके बजाय किसी स्ट्रक्चर का इस्तेमाल किया जा सकता है:
index.html (createShaderModule कॉल)
struct FragInput {
@location(0) cell: vec2f,
};
@fragment
fn fragmentMain(input: FragInput) -> @location(0) vec4f {
return vec4f(input.cell, 0, 1);
}
- दूसरा विकल्प यह है कि आपके कोड में ये दोनों फ़ंक्शन एक ही शेडर मॉड्यूल में बताए गए हैं. इसलिए,
@vertex
स्टेज के आउटपुट निर्देश का फिर से इस्तेमाल किया जा सकता है! इससे वैल्यू पास करना आसान हो जाता है, क्योंकि नाम और जगहें अपने-आप एक जैसी रहती हैं.
index.html (createShaderModule कॉल)
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
return vec4f(input.cell, 0, 1);
}
आपने कोई भी पैटर्न चुना हो, नतीजा यह होगा कि आपके पास @fragment
फ़ंक्शन में सेल नंबर का ऐक्सेस होगा. साथ ही, कलर में बदलाव करने के लिए, इसका इस्तेमाल किया जा सकेगा. ऊपर दिए गए किसी भी कोड के साथ, आउटपुट इस तरह दिखता है:
अब और ज़्यादा रंग उपलब्ध हो गए हैं, लेकिन दिखने में अच्छा नहीं है. शायद आपको पता चले कि सिर्फ़ बाईं और सबसे नीचे की लाइनें अलग क्यों हैं. इसकी वजह यह है कि @fragment
फ़ंक्शन से मिलने वाली कलर वैल्यू के लिए, हर चैनल की वैल्यू 0 से 1 के बीच होनी चाहिए. इस रेंज से बाहर की वैल्यू को उसमें क्लैंप कर दिया जाता है. दूसरी ओर, आपकी सेल वैल्यू हर अक्ष के साथ 0 से 32 के बीच होती हैं. इसलिए, आपको यहां जो दिख रहा है वह यह है कि पहली पंक्ति और कॉलम, लाल या हरे रंग वाले चैनल पर तुरंत पूरा 1 वैल्यू हिट कर देता है और उसके बाद की हर सेल उसी वैल्यू से जुड़ जाती है.
अगर आपको रंगों के बीच आसानी से ट्रांज़िशन चाहिए, तो आपको हर कलर चैनल के लिए फ़्रैक्शनल वैल्यू दिखानी होगी. यह वैल्यू हर ऐक्सिस पर शून्य से शुरू होकर एक पर खत्म होनी चाहिए. इसका मतलब है कि आपको grid
से फिर से भाग देना होगा!
- फ़्रेगमेंट शेडर को इस तरह बदलें:
index.html (createShaderModule कॉल)
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
return vec4f(input.cell/grid, 0, 1);
}
पेज को रीफ़्रेश करके, देखा जा सकता है कि नया कोड आपको पूरे ग्रिड में कलर का बेहतर ग्रेडिएंट दिखाता है.
हालांकि, यह तो वाकई एक सुधार है, लेकिन अब नीचे बाईं ओर एक अंधेरे कोने वाला अंधेरा है, जहां ग्रिड का रंग काला हो जाता है. Game of Life का सिम्युलेशन शुरू करने पर, ग्रिड का एक ऐसा सेक्शन जो दिखता है उसे छिपाया जाता है. इसे और बेहतर बनाना अच्छा होगा.
अच्छी बात यह है कि आपके पास एक पूरा इस्तेमाल न किया गया कलर चैनल है, जिसका इस्तेमाल किया जा सकता है. आप जो चाहते हैं वह यह है कि अन्य रंगों की तुलना में नीले रंग में सबसे ज़्यादा चमक हो. इसके बाद, जैसे-जैसे दूसरे रंग ज़्यादा गहरे दिखते हैं, वैसे-वैसे गायब हो जाते हैं. ऐसा करने का सबसे आसान तरीका यह है कि चैनल को 1 से शुरू करें और सेल की किसी एक वैल्यू को घटाएं. यह c.x
या c.y
हो सकता है. दोनों को आज़माएं और फिर अपनी पसंद चुनें!
- फ़्रैगमेंट शेडर में ज़्यादा चमकदार रंग जोड़ें, जैसे:
createShaderModule कॉल
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
let c = input.cell / grid;
return vec4f(c, 1-c.x, 1);
}
नतीजा काफ़ी अच्छा लग रहा है!
यह ज़रूरी चरण नहीं है! हालांकि, यह बेहतर दिखता है, इसलिए इसे उससे जुड़ी चेकपॉइंट सोर्स फ़ाइल में शामिल किया गया है. साथ ही, इस कोडलैब के बाकी स्क्रीनशॉट में, इस ज़्यादा रंगीन ग्रिड को दिखाया गया है.
7. सेल की स्थिति मैनेज करना
इसके बाद, आपको यह कंट्रोल करना होगा कि जीपीयू पर सेव की गई किसी स्थिति के आधार पर, ग्रिड की कौनसी सेल रेंडर होंगी. यह आखिरी सिम्युलेशन के लिए ज़रूरी है!
आपको हर सेल के लिए सिर्फ़ एक चालू-बंद सिग्नल चाहिए. इसलिए, ऐसे सभी विकल्प काम करते हैं जिनकी मदद से, किसी भी तरह की वैल्यू का बड़ा कलेक्शन सेव किया जा सकता है. आपको लग सकता है कि यह यूनिफ़ॉर्म बफ़र के इस्तेमाल का एक और उदाहरण है! ऐसा करना ज़्यादा मुश्किल है, लेकिन इससे ज़्यादा मुश्किल हो सकती है. इसकी वजह यह है कि एक जैसे बफ़र का साइज़ ही सीमित होता है और वे डाइनैमिक साइज़ वाले अरे के साथ काम नहीं कर सकते. हालांकि, शेडर में अरे का साइज़ बताना होता है. आखिरी आइटम में सबसे ज़्यादा समस्या होती है, क्योंकि आपको जीपीयू पर कंप्यूट शेडर में गेम ऑफ़ लाइफ़ का सिम्युलेशन करना है.
अच्छी बात यह है कि यहां एक और बफ़र विकल्प है, जो इन सभी सीमाओं से बचाता है.
स्टोरेज बफ़र बनाना
स्टोरेज बफ़र, आम तौर पर इस्तेमाल होने वाले बफ़र होते हैं. इन्हें कंप्यूट शेडर में पढ़ा और लिखा जा सकता है. साथ ही, इन्हें वर्टेक्स शेडर में भी पढ़ा जा सकता है. ये बहुत बड़े हो सकते हैं. साथ ही, उन्हें शेडर में किसी खास साइज़ की ज़रूरत नहीं होती. इस वजह से, ये सामान्य मेमोरी की तरह ही काम करते हैं. सेल की स्थिति को सेव करने के लिए, आप इसी का इस्तेमाल करते हैं.
- अपनी सेल स्टेटस के लिए स्टोरेज बफ़र बनाने के लिए, बफ़र बनाने वाले कोड के उस स्निपेट का इस्तेमाल करें जो अब तक शायद आपको जाना-पहचाना लगने लगा है:
index.html
// Create an array representing the active state of each cell.
const cellStateArray = new Uint32Array(GRID_SIZE * GRID_SIZE);
// Create a storage buffer to hold the cell state.
const cellStateStorage = device.createBuffer({
label: "Cell State",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
अपने वर्टेक्स और यूनिफ़ॉर्म बफ़र की तरह, सही साइज़ के साथ device.createBuffer()
को कॉल करें. इसके बाद, इस बार GPUBufferUsage.STORAGE
के इस्तेमाल के बारे में बताना न भूलें.
बफ़र को पहले की तरह ही पॉप्युलेट किया जा सकता है. इसके लिए, उसी साइज़ के TypedArray में वैल्यू डालें और फिर device.queue.writeBuffer()
को कॉल करें. आपको ग्रिड पर बफ़र का असर देखना है, इसलिए इसे किसी ऐसे डेटा से भरें जिसका अनुमान लगाया जा सकता हो.
- इस कोड की मदद से, हर तीसरी सेल को चालू करें:
index.html
// Mark every third cell of the grid as active.
for (let i = 0; i < cellStateArray.length; i += 3) {
cellStateArray[i] = 1;
}
device.queue.writeBuffer(cellStateStorage, 0, cellStateArray);
शेडर में स्टोरेज बफ़र को पढ़ना
इसके बाद, ग्रिड को रेंडर करने से पहले, स्टोरेज बफ़र का कॉन्टेंट देखने के लिए अपना शेडर अपडेट करें. यह पहले की यूनिफ़ॉर्म से काफ़ी मिलता-जुलता है.
- अपने शेडर को इस कोड से अपडेट करें:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellState: array<u32>; // New!
सबसे पहले, बाइंडिंग पॉइंट जोड़ा जाता है, जो ग्रिड यूनिफ़ॉर्म के ठीक नीचे होता है. आपको grid
यूनिफ़ॉर्म के तौर पर वही @group
रखना है, लेकिन @binding
नंबर अलग होना चाहिए. var
टाइप storage
है, ताकि अलग-अलग तरह के बफ़र को दिखाया जा सके. साथ ही, cellState
के लिए जो टाइप दिया जाता है वह एक वेक्टर के बजाय, u32
वैल्यू का कलेक्शन होता है. ऐसा इसलिए किया जाता है, ताकि JavaScript में Uint32Array
से मैच किया जा सके.
इसके बाद, अपने @vertex
फ़ंक्शन के मुख्य हिस्से में, सेल की स्थिति के बारे में क्वेरी करें. स्टोरेज बफ़र में स्टेट एक फ़्लैट अरे में सेव होती है. इसलिए, मौजूदा सेल की वैल्यू देखने के लिए, instance_index
का इस्तेमाल किया जा सकता है!
अगर सेल को बंद करने का मैसेज मिलता है, तो उसे कैसे बंद किया जा सकता है? दरअसल, कलेक्शन से आपको मिलने वाली ऐक्टिव और इनऐक्टिव स्थितियां 1 या 0 होती हैं. इसलिए, ज्यामिति को ऐक्टिव स्थिति के हिसाब से स्केल किया जा सकता है! इसे 1 से स्केल करने पर, ज्यॉमेट्री में कोई बदलाव नहीं होता. वहीं, इसे 0 से स्केल करने पर, ज्यॉमेट्री एक पॉइंट में सिमट जाती है. इसके बाद, जीपीयू इसे खारिज कर देता है.
- सेल की ऐक्टिव स्थिति के हिसाब से पोज़िशन को स्केल करने के लिए, अपने शेडर कोड को अपडेट करें. WGSL के टाइप की सुरक्षा से जुड़ी ज़रूरी शर्तों को पूरा करने के लिए, राज्य की वैल्यू को
f32
पर कास्ट करना ज़रूरी है:
index.html
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) -> VertexOutput {
let i = f32(instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let state = f32(cellState[instance]); // New line!
let cellOffset = cell / grid * 2;
// New: Scale the position by the cell's active state.
let gridPos = (pos*state+1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell;
return output;
}
बाइंड ग्रुप में स्टोरेज बफ़र जोड़ना
सेल की स्थिति लागू होने से पहले, स्टोरेज बफ़र को बाइंड ग्रुप में जोड़ें. यह यूनिफ़ॉर्म बफ़र के उसी @group
का हिस्सा है. इसलिए, इसे JavaScript कोड में उसी बाइंड ग्रुप में जोड़ें.
- स्टोरेज बफ़र जोड़ें, जैसे:
index.html
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
},
// New entry!
{
binding: 1,
resource: { buffer: cellStateStorage }
}],
});
पक्का करें कि नई एंट्री का binding
, शेडर में मौजूद उससे जुड़ी वैल्यू के @binding()
से मेल खाता हो!
इसके बाद, रीफ़्रेश करें और ग्रिड में पैटर्न देखें.
पिंग-पॉन्ग बफ़र पैटर्न का इस्तेमाल करना
आपने जो सिम्युलेशन बनाया है उससे मिलते-जुलते ज़्यादातर सिम्युलेशन, अपनी स्थिति की कम से कम दो कॉपी का इस्तेमाल करते हैं. सिम्युलेशन के हर चरण में, वे स्टेटस की एक कॉपी से पढ़ते हैं और दूसरी कॉपी में लिखते हैं. इसके बाद, अगले चरण में, उसे पलटें और उस स्थिति से पढ़ें जिस पर उन्होंने पहले लिखा था. इसे आम तौर पर पिंग-पॉंग पैटर्न कहा जाता है, क्योंकि स्टेटस का सबसे अप-टू-डेट वर्शन, हर चरण में स्टेटस की कॉपी के बीच बार-बार बाउंस होता है.
ऐसा क्यों ज़रूरी है? एक आसान उदाहरण देखें: मान लें कि आपने एक बहुत ही आसान सिम्युलेशन लिखा है, जिसमें हर चरण में किसी भी चालू ब्लॉक को दाईं ओर एक सेल आगे बढ़ाया जाता है. चीज़ों को आसानी से समझने के लिए, अपने डेटा और सिम्युलेशन को JavaScript में तय करें:
// Example simulation. Don't copy into the project!
const state = [1, 0, 0, 0, 0, 0, 0, 0];
function simulate() {
for (let i = 0; i < state.length-1; ++i) {
if (state[i] == 1) {
state[i] = 0;
state[i+1] = 1;
}
}
}
simulate(); // Run the simulation for one step.
हालांकि, इस कोड को चलाने पर, ऐक्टिव सेल एक ही चरण में अरे के आखिर तक चली जाती है! क्यों? क्योंकि आप अपनी जगह पर स्थिति अपडेट करते रहते हैं, इसलिए आप चालू सेल को दाईं ओर ले जाते हैं, और फिर अगली सेल को देखते हैं और... हाय! यह चालू है! बेहतर होगा कि आप इसे फिर से दाईं ओर ले जाएं. एक ही समय पर डेटा में बदलाव करने से नतीजे खराब हो जाते हैं.
पिंग-पॉंग पैटर्न का इस्तेमाल करके, यह पक्का किया जा सकता है कि सिम्युलेशन के अगले चरण में, सिर्फ़ पिछले चरण के नतीजों का इस्तेमाल किया जाए.
// Example simulation. Don't copy into the project!
const stateA = [1, 0, 0, 0, 0, 0, 0, 0];
const stateB = [0, 0, 0, 0, 0, 0, 0, 0];
function simulate(inArray, outArray) {
outArray[0] = 0;
for (let i = 1; i < inArray.length; ++i) {
outArray[i] = inArray[i-1];
}
}
// Run the simulation for two step.
simulate(stateA, stateB);
simulate(stateB, stateA);
- दो एक जैसे बफ़र बनाने के लिए, अपने स्टोरेज बफ़र के ऐलोकेशन को अपडेट करके, अपने कोड में इस पैटर्न का इस्तेमाल करें:
index.html
// Create two storage buffers to hold the cell state.
const cellStateStorage = [
device.createBuffer({
label: "Cell State A",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
}),
device.createBuffer({
label: "Cell State B",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
})
];
- दोनों बफ़र के बीच के अंतर को विज़ुअलाइज़ करने के लिए, उन्हें अलग-अलग डेटा से भरें:
index.html
// Mark every third cell of the first grid as active.
for (let i = 0; i < cellStateArray.length; i+=3) {
cellStateArray[i] = 1;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
// Mark every other cell of the second grid as active.
for (let i = 0; i < cellStateArray.length; i++) {
cellStateArray[i] = i % 2;
}
device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray);
- रेंडरिंग में अलग-अलग स्टोरेज बफ़र दिखाने के लिए, अपने बाइंड ग्रुप को अपडेट करें, ताकि दो अलग-अलग वैरिएंट हों:
index.html
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}],
})
];
रेंडर लूप को सेट अप करना
अब तक, आपने हर पेज रीफ़्रेश पर सिर्फ़ एक बार ड्रॉ किया है. हालांकि, अब आपको समय के साथ अपडेट होने वाला डेटा दिखाना है. ऐसा करने के लिए, आपको एक आसान रेंडर लूप की ज़रूरत होगी.
रेंडर लूप, एक ऐसा लूप होता है जो लगातार दोहराया जाता है. यह एक तय इंटरवल पर आपके कॉन्टेंट को कैनवस पर दिखाता है. कई गेम और अन्य कॉन्टेंट जो आसानी से ऐनिमेट करना चाहते हैं, वे requestAnimationFrame()
फ़ंक्शन का इस्तेमाल करके कॉलबैक को उसी दर पर शेड्यूल करते हैं जिस दर पर स्क्रीन रीफ़्रेश होती है (हर सेकंड 60 बार).
यह ऐप्लिकेशन, उस डेटा का भी इस्तेमाल कर सकता है. हालांकि, इस मामले में, हो सकता है कि आपको अपडेट ज़्यादा समय के अंतराल पर चाहिए, ताकि आप आसानी से यह समझ सकें कि सिम्युलेशन क्या कर रहा है. लूप को खुद मैनेज करें, ताकि आप सिम्युलेशन अपडेट होने की दर को कंट्रोल कर सकें.
- सबसे पहले, हमारे सिम्युलेशन के अपडेट होने की दर चुनें (200 मि॰से॰ अच्छी है, लेकिन अगर आप चाहें, तो इसे धीमा या इससे तेज़ किया जा सकता है) और फिर यह ट्रैक करें कि सिम्युलेशन के कितने चरण पूरे हो चुके हैं.
index.html
const UPDATE_INTERVAL = 200; // Update every 200ms (5 times/sec)
let step = 0; // Track how many simulation steps have been run
- इसके बाद, रेंडर करने के लिए मौजूदा समय में इस्तेमाल किए जा रहे सभी कोड को नए फ़ंक्शन में ले जाएं. उस फ़ंक्शन को
setInterval()
के साथ अपने हिसाब से इंटरवल में दोहराने के लिए शेड्यूल करें. पक्का करें कि फ़ंक्शन, चरणों की संख्या को भी अपडेट करता हो. साथ ही, इसका इस्तेमाल करके यह चुनें कि दोनों में से किस बाइंड ग्रुप को बाइंड करना है.
index.html
// Move all of our rendering code into a function
function updateGrid() {
step++; // Increment the step count
// Start a render pass
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0.4, a: 1.0 },
storeOp: "store",
}]
});
// Draw the grid.
pass.setPipeline(cellPipeline);
pass.setBindGroup(0, bindGroups[step % 2]); // Updated!
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
// End the render pass and submit the command buffer
pass.end();
device.queue.submit([encoder.finish()]);
}
// Schedule updateGrid() to run repeatedly
setInterval(updateGrid, UPDATE_INTERVAL);
और अब जब आप ऐप्लिकेशन चलाते हैं, तो आप देखते हैं कि कैनवस आपके द्वारा बनाए गए दो स्थिति बफ़र दिखाने के बीच आगे-पीछे फ़्लिप होता है.
इसके बाद, रेंडरिंग की प्रोसेस पूरी हो जाती है! अब आप गेम ऑफ़ लाइफ़ सिम्युलेशन के आउटपुट को अगले चरण में दिखाने के लिए तैयार हैं. यहां आपको कंप्यूट शेडर का इस्तेमाल शुरू करना होगा.
WebGPU की रेंडरिंग क्षमताओं में, यहां बताए गए छोटे से हिस्से के अलावा और भी बहुत कुछ है. हालांकि, बाकी चीज़ें इस कोडलैब के दायरे से बाहर हैं. हालांकि, उम्मीद है कि इससे आपको WebGPU की रेंडरिंग के काम करने के तरीके की पूरी जानकारी मिल जाएगी. इससे आपको 3D रेंडरिंग जैसी ज़्यादा बेहतर तकनीकों को एक्सप्लोर करने में मदद मिलेगी.
8. सिम्युलेशन चलाएं
अब, पहेली के आखिरी बड़े हिस्से के लिए: कंप्यूट शेडर में गेम ऑफ़ लाइफ़ का सिम्युलेशन आज़माएं!
आखिरकार, कंप्यूट शेडर का इस्तेमाल करें!
इस कोडलैब में, आपने कंप्यूट शेडर के बारे में खास जानकारी नहीं पाई है. हालांकि, ये क्या होते हैं?
कंप्यूट शेडर, वर्टिक्स और फ़्रैगमेंट शेडर से मिलते-जुलते होते हैं. इनका डिज़ाइन, जीपीयू पर एक साथ कई काम करने की सुविधा के साथ चलाने के लिए किया गया है. हालांकि, शेडर के अन्य दो चरणों के विपरीत, इनमें इनपुट और आउटपुट का कोई खास सेट नहीं होता. आपने सिर्फ़ अपने चुने गए सोर्स से डेटा पढ़ने और लिखने का विकल्प चुना है. जैसे, स्टोरेज बफ़र. इसका मतलब है कि हर वर्टिक्स, इंस्टेंस या पिक्सल के लिए एक बार लागू करने के बजाय, आपको यह बताना होगा कि आपको शेडर फ़ंक्शन को कितनी बार लागू करना है. इसके बाद, जब आप शेडर चलाते हैं, तो आपको यह बताया जाता है कि किस अनुरोध को प्रोसेस किया जा रहा है. इसके बाद, आप यह तय कर सकते हैं कि आपको किस डेटा का ऐक्सेस देना है और वहां से क्या कार्रवाई करनी है.
वर्टेक्स और फ़्रैगमेंट शेडर की तरह, शेडर मॉड्यूल में कंप्यूट शेडर बनाए जाने चाहिए. इसलिए, शुरू करने के लिए इसे अपने कोड में जोड़ें. आपने जो अन्य शेडर लागू किए हैं उनके स्ट्रक्चर को देखते हुए, आपके कंप्यूट शेडर के मुख्य फ़ंक्शन को @compute
एट्रिब्यूट के साथ मार्क करना होगा.
- इस कोड का इस्तेमाल करके, कंप्यूट शेडर बनाएं:
index.html
// Create the compute shader that will process the simulation.
const simulationShaderModule = device.createShaderModule({
label: "Game of Life simulation shader",
code: `
@compute
fn computeMain() {
}`
});
जीपीयू का इस्तेमाल अक्सर 3D ग्राफ़िक्स के लिए किया जाता है. इसलिए, कंप्यूट शेडर को इस तरह से बनाया जाता है कि आप X, Y, और Z ऐक्सिस के साथ शेडर को तय संख्या में इस्तेमाल करने का अनुरोध कर सकें. इससे, 2D या 3D ग्रिड के हिसाब से काम को आसानी से डिस्पैच किया जा सकता है. यह आपके काम के लिए बहुत अच्छा है! आपको इस शेडर को अपने सिम्युलेशन के हर सेल के लिए, एक बार GRID_SIZE
बार GRID_SIZE
बार कॉल करना है.
जीपीयू हार्डवेयर आर्किटेक्चर की वजह से, इस ग्रिड को वर्कग्रुप में बांटा जाता है. किसी वर्कग्रुप का X, Y, और Z साइज़ होता है. हालांकि, हर साइज़ एक हो सकता है, लेकिन अपने वर्कग्रुप को थोड़ा बड़ा करने से परफ़ॉर्मेंस में अक्सर फ़ायदे मिलते हैं. अपने शेडर के लिए, 8 x 8 का वर्कग्रुप साइज़ चुनें. यह आपके JavaScript कोड में ट्रैक करने के लिए मददगार है.
- अपने वर्कग्रुप के साइज़ के लिए कॉन्स्टेंट तय करें, जैसे कि:
index.html
const WORKGROUP_SIZE = 8;
आपको शेडर फ़ंक्शन में वर्कग्रुप का साइज़ भी जोड़ना होगा. ऐसा आप JavaScript के टेंप्लेट की लिटरल वैल्यू का इस्तेमाल करके करते हैं, ताकि आपने अभी तय किए गए कॉन्सटेंट का आसानी से इस्तेमाल किया हो.
- शेडर फ़ंक्शन में वर्कग्रुप का साइज़ जोड़ें, इस तरह से:
index.html (Compute createShaderModule कॉल)
@compute
@workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}) // New line
fn computeMain() {
}
इससे शेडर को पता चलता है कि इस फ़ंक्शन से किया गया काम, (8 x 8 x 1) ग्रुप में किया जाता है. (अगर आपने किसी ऐक्सिस को छोड़ दिया है, तो वह डिफ़ॉल्ट रूप से 1 पर सेट हो जाता है. हालांकि, आपको कम से कम X ऐक्सिस ज़रूर तय करना होगा.)
अन्य शेडर स्टेज की तरह ही, @builtin
की कई वैल्यू हैं जिन्हें अपने कंप्यूट शेडर फ़ंक्शन में इनपुट के तौर पर स्वीकार किया जा सकता है. इससे आपको यह पता चलता है कि आप किस इनवोकेशन पर हैं और आपको क्या काम करना है.
@builtin
वैल्यू जोड़ें, जैसे:
index.html (Compute createShaderModule कॉल)
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
आपने global_invocation_id
बिल्ट-इन को पास किया है, जो साइन नहीं किए गए पूर्णांकों का एक थ्री-डाइमेंशन वाला वेक्टर होता है. इससे आपको पता चलता है कि शेडर इन्वेशन के ग्रिड में आप कहां हैं. अपने ग्रिड में हर सेल के लिए, इस शेडर को एक बार चलाया जाता है. आपको (0, 0, 0)
, (1, 0, 0)
, (1, 1, 0)
... से लेकर (31, 31, 0)
तक की संख्याएं मिलती हैं. इसका मतलब है कि इसे उस सेल इंडेक्स के तौर पर इस्तेमाल किया जा सकता है जिस पर आपको काम करना है!
कंप्यूट शेडर, यूनिफ़ॉर्म का भी इस्तेमाल कर सकते हैं जिनका इस्तेमाल वर्टेक्स और फ़्रैगमेंट शेडर की तरह किया जाता है.
- ग्रिड का साइज़ बताने के लिए, अपने कंप्यूट शेडर के साथ यूनिफ़ॉर्म का इस्तेमाल करें. जैसे:
index.html (Compute createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f; // New line
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
वर्टेक्स शेडर की तरह, सेल की स्थिति को भी स्टोरेज बफ़र के तौर पर दिखाया जाता है. लेकिन इस मामले में, आपको उनमें से दो की ज़रूरत है! कंप्यूट शेडर में वर्टेक्स पोज़िशन या फ़्रैगमेंट कलर जैसा ज़रूरी आउटपुट नहीं होता, इसलिए स्टोरेज बफ़र या टेक्सचर में वैल्यू लिखना, कंप्यूट शेडर से नतीजे पाने का सिर्फ़ एक तरीका है. पिंग-पॉन्ग के उसी तरीके का इस्तेमाल करना जो आपने पहले सीखा था; आपके पास एक स्टोरेज बफ़र होता है, जो ग्रिड की मौजूदा स्थिति में फ़ीड होता है और दूसरा बफ़र, जिसके लिए ग्रिड की नई स्थिति को लिखा जाता है.
- सेल के इनपुट और आउटपुट स्थिति को स्टोरेज बफ़र के तौर पर दिखाएं, इस तरह:
index.html (Compute createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
// New lines
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
ध्यान दें कि पहले स्टोरेज बफ़र को var<storage>
के साथ डिक्लेयर्ड किया गया है, जिससे यह रीड-ओनली हो जाता है. हालांकि, दूसरे स्टोरेज बफ़र को var<storage, read_write>
के साथ डिक्लेयर्ड किया गया है. इसकी मदद से, बफ़र को पढ़ा और उसमें लिखा जा सकता है. साथ ही, उस बफ़र का इस्तेमाल अपने कंप्यूट शेडर के आउटपुट के तौर पर किया जा सकता है. (WebGPU में, सिर्फ़ लिखने के लिए स्टोरेज मोड नहीं है).
इसके बाद, आपको अपने सेल इंडेक्स को लीनियर स्टोरेज ऐरे में मैप करने का तरीका पता होना चाहिए. यह वैसा ही है जैसा आपने वर्टिक्स शेडर में किया था. वहां आपने लीनियर instance_index
को लिया और उसे 2D ग्रिड सेल पर मैप किया था. (याद रखें कि इसके लिए आपका एल्गोरिदम vec2f(i % grid.x, floor(i / grid.x))
था.)
- दूसरी दिशा में जाने के लिए, कोई फलन लिखें. यह सेल की Y वैल्यू को ग्रिड की चौड़ाई से गुणा करता है. इसके बाद, सेल की X वैल्यू को जोड़ता है.
index.html (Compute createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
// New function
fn cellIndex(cell: vec2u) -> u32 {
return cell.y * u32(grid.x) + cell.x;
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
आखिर में, यह देखने के लिए कि यह काम कर रहा है या नहीं, एक बहुत ही आसान एल्गोरिदम लागू करें: अगर कोई सेल चालू है, तो उसे बंद करें और अगर वह बंद है, तो उसे चालू करें. हालांकि, यह गेम ऑफ़ लाइफ़ नहीं है, लेकिन इससे पता चल सकता है कि कंप्यूट शेडर काम कर रहा है.
- आसान एल्गोरिदम जोड़ें, इस तरह से:
index.html (Compute createShaderModule कॉल)
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
fn cellIndex(cell: vec2u) -> u32 {
return cell.y * u32(grid.x) + cell.x;
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// New lines. Flip the cell state every step.
if (cellStateIn[cellIndex(cell.xy)] == 1) {
cellStateOut[cellIndex(cell.xy)] = 0;
} else {
cellStateOut[cellIndex(cell.xy)] = 1;
}
}
और आपके कंप्यूट शेडर में बस इतना ही है—फ़िलहाल! हालांकि, नतीजे देखने से पहले आपको कुछ और बदलाव करने होंगे.
बाइंड ग्रुप और पाइपलाइन लेआउट इस्तेमाल करना
ऊपर दिए गए शेडर में, आपको नज़र आ सकता है कि यह काफ़ी हद तक उन्हीं इनपुट (यूनिफ़ॉर्म और स्टोरेज बफ़र) का इस्तेमाल करता है जिनका इस्तेमाल आपकी रेंडर पाइपलाइन के लिए किया जाता है. इसलिए, आपको लग सकता है कि एक ही बाइंड ग्रुप का इस्तेमाल करके, काम पूरा किया जा सकता है, है न? अच्छी खबर यह है कि ऐसा किया जा सकता है! ऐसा करने के लिए, मैन्युअल सेटअप की ज़रूरत होती है.
जब भी बाइंड ग्रुप बनाया जाएगा, तब आपको GPUBindGroupLayout
देना होगा. पहले, रेंडर पाइपलाइन पर getBindGroupLayout()
को कॉल करके, आपको वह लेआउट मिलता था. यह लेआउट अपने-आप बन जाता था, क्योंकि आपने इसे बनाते समय layout: "auto"
दिया था. यह तरीका तब अच्छा काम करता है, जब सिर्फ़ एक पाइपलाइन का इस्तेमाल किया जाता है. हालांकि, अगर आपके पास ऐसी कई पाइपलाइन हैं जो संसाधन शेयर करना चाहती हैं, तो आपको साफ़ तौर पर लेआउट बनाना होगा. इसके बाद, इसे बाइंड ग्रुप और पाइपलाइन, दोनों को देना होगा.
इसकी वजह समझने के लिए, इस पर ध्यान दें: रेंडर पाइपलाइन में, एक ही यूनिफ़ॉर्म बफ़र और एक स्टोरेज बफ़र का इस्तेमाल किया जाता है, लेकिन आपने अभी-अभी जो कंप्यूट शेडर लिखा है उसमें आपको एक और स्टोरेज बफ़र की ज़रूरत पड़ती है. दोनों शेडर एक जैसे और पहले स्टोरेज बफ़र के लिए, एक ही @binding
वैल्यू का इस्तेमाल करते हैं. इसलिए, उन्हें पाइपलाइन के बीच शेयर किया जा सकता है. रेंडर पाइपलाइन दूसरे स्टोरेज बफ़र का इस्तेमाल नहीं करती, जिसका वह इस्तेमाल नहीं करता. आपको ऐसा लेआउट बनाना है जिसमें बाइंड ग्रुप में मौजूद सभी संसाधनों की जानकारी हो, न कि सिर्फ़ उन संसाधनों की जानकारी हो जिनका इस्तेमाल किसी खास पाइपलाइन ने किया है.
- यह लेआउट बनाने के लिए,
device.createBindGroupLayout()
को कॉल करें:
index.html
// Create the bind group layout and pipeline layout.
const bindGroupLayout = device.createBindGroupLayout({
label: "Cell Bind Group Layout",
entries: [{
binding: 0,
// Add GPUShaderStage.FRAGMENT here if you are using the `grid` uniform in the fragment shader.
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: {} // Grid uniform buffer
}, {
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "read-only-storage"} // Cell state input buffer
}, {
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage"} // Cell state output buffer
}]
});
यह बाइंड ग्रुप बनाने जैसा ही है. इसमें entries
की सूची के बारे में बताया गया है. अंतर यह है कि यह बताया जाता है कि संसाधन किस तरह का होना चाहिए और संसाधन देने के बजाय उसका इस्तेमाल कैसे किया जाना चाहिए.
हर एंट्री में, रिसॉर्स के लिए binding
नंबर दिया जाता है, जो कि (जैसा कि बाइंड ग्रुप बनाते समय आपको पता चला था) शेडर में मौजूद @binding
वैल्यू से मेल खाती है. आप visibility
भी देते हैं, जो GPUShaderStage
फ़्लैग होते हैं. इनसे पता चलता है कि शेडर के कौनसे स्टेज इस संसाधन का इस्तेमाल कर सकते हैं. आपको यूनिफ़ॉर्म और पहले स्टोरेज बफ़र, दोनों को वर्टिक्स और कंप्यूट शेडर में ऐक्सेस करना है. हालांकि, दूसरे स्टोरेज बफ़र को सिर्फ़ कंप्यूट शेडर में ऐक्सेस करना है.
आखिर में, आपको यह बताना होगा कि किस तरह के संसाधन का इस्तेमाल किया जा रहा है. यह एक अलग डिक्शनरी कुंजी है. यह इस बात पर निर्भर करता है कि आपको क्या एक्सपोज़ करना है. यहां, तीनों संसाधन बफ़र हैं. इसलिए, हर एक संसाधन के लिए विकल्प तय करने के लिए, buffer
कुंजी का इस्तेमाल किया जा सकता है. अन्य विकल्पों में texture
या sampler
जैसी चीज़ें शामिल हैं, लेकिन आपको यहां इनकी ज़रूरत नहीं है.
बफ़र डिक्शनरी में, आपके पास विकल्प सेट करने का विकल्प होता है. जैसे, बफ़र का कौनसा type
इस्तेमाल किया जाए. डिफ़ॉल्ट "uniform"
होता है. इसलिए, 0 को बांधने के लिए, डिक्शनरी को खाली छोड़ा जा सकता है. हालांकि, आपको कम से कम buffer: {}
सेट करना होगा, ताकि एंट्री को बफ़र के तौर पर पहचाना जा सके. बाइंडिंग 1 को "read-only-storage"
टाइप दिया गया है, क्योंकि इसका इस्तेमाल शेडर में read_write
ऐक्सेस के साथ नहीं किया जाता. वहीं, बाइंडिंग 2 को "storage"
टाइप दिया गया है, क्योंकि इसका इस्तेमाल read_write
ऐक्सेस के साथ किया जाता है!
bindGroupLayout
बन जाने के बाद, बाइंड ग्रुप बनाते समय उसे पास किया जा सकता है. इसके लिए, आपको पाइपलाइन से बाइंड ग्रुप की क्वेरी करने की ज़रूरत नहीं है. ऐसा करने का मतलब है कि आपको हर बाइंड ग्रुप में एक नया स्टोरेज बफ़र एंट्री जोड़नी होगी, ताकि आपने जो लेआउट तय किया है उससे मैच किया जा सके.
- बाइंड ग्रुप बनाने की प्रोसेस को इस तरह अपडेट करें:
index.html
// Create a bind group to pass the grid uniforms into the pipeline
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[1] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[0] }
}],
}),
];
अब जब बाइंड ग्रुप को साफ़ तौर पर बाइंड ग्रुप लेआउट का इस्तेमाल करने के लिए अपडेट कर दिया गया है, तो आपको उसी का इस्तेमाल करने के लिए रेंडर पाइपलाइन को अपडेट करना होगा.
GPUPipelineLayout
बनाएं.
index.html
const pipelineLayout = device.createPipelineLayout({
label: "Cell Pipeline Layout",
bindGroupLayouts: [ bindGroupLayout ],
});
पाइपलाइन लेआउट, बाइंड ग्रुप लेआउट की सूची होती है. इस मामले में, आपके पास एक लेआउट है. इसका इस्तेमाल एक या उससे ज़्यादा पाइपलाइन करती हैं. ऐरे में बाइंड ग्रुप लेआउट का क्रम, शेडर में मौजूद @group
एट्रिब्यूट से मेल खाना चाहिए. (इसका मतलब है कि bindGroupLayout
, @group(0)
से जुड़ा हुआ है.)
- पाइपलाइन का लेआउट मिलने के बाद, रेंडर पाइपलाइन को अपडेट करें, ताकि आप
"auto"
के बजाय उसका इस्तेमाल कर सकें.
index.html
const cellPipeline = device.createRenderPipeline({
label: "Cell pipeline",
layout: pipelineLayout, // Updated!
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout]
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat
}]
}
});
कंप्यूट पाइपलाइन बनाना
जिस तरह आपको अपने वर्टेक्स और फ़्रैगमेंट शेडर का इस्तेमाल करने के लिए, रेंडर पाइपलाइन की ज़रूरत होती है, उसी तरह अपने कंप्यूट शेडर का इस्तेमाल करने के लिए भी आपको एक कंप्यूट पाइपलाइन की ज़रूरत होगी. अच्छी बात यह है कि कंप्यूट पाइपलाइन, रेंडर करने वाले पाइपलाइन के मुकाबले बहुत जटिल होती हैं. इसकी वजह यह है कि उनके पास सेट करने के लिए कोई स्थिति नहीं होती, सिर्फ़ शेडर और लेआउट.
- नीचे दिए गए कोड की मदद से, कंप्यूट पाइपलाइन बनाएं:
index.html
// Create a compute pipeline that updates the game state.
const simulationPipeline = device.createComputePipeline({
label: "Simulation pipeline",
layout: pipelineLayout,
compute: {
module: simulationShaderModule,
entryPoint: "computeMain",
}
});
ध्यान दें कि अपडेट की गई रेंडर पाइपलाइन की तरह ही, "auto"
के बजाय नया pipelineLayout
पास किया जाता है. इससे यह पक्का होता है कि आपकी रेंडर पाइपलाइन और कंप्यूट पाइपलाइन, दोनों एक ही बाइंड ग्रुप का इस्तेमाल कर सकती हैं.
पास कंप्यूट करें
अब आपके पास कंप्यूट पाइपलाइन का इस्तेमाल करने का विकल्प है! रेंडर पास में रेंडरिंग करने के बाद, आपको कंप्यूट पास में कंप्यूट वाला काम करना होगा. एक ही कमांड एन्कोडर में, कैलकुलेट और रेंडर करने का काम किया जा सकता है. इसलिए, आपको अपने updateGrid
फ़ंक्शन में थोड़ा बदलाव करना होगा.
- एन्कोडर बनाने के लिए इस्तेमाल किए गए फ़ंक्शन को फ़ंक्शन में सबसे ऊपर ले जाएं. इसके बाद,
step++
से पहले, इसके साथ कंप्यूट पास शुरू करें.
index.html
// In updateGrid()
// Move the encoder creation to the top of the function.
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
// Compute work will go here...
computePass.end();
// Existing lines
step++; // Increment the step count
// Start a render pass...
कंप्यूट पाइपलाइन की तरह ही, कंप्यूट पास को रेंडरिंग पास के मुकाबले शुरू करना बहुत आसान है. ऐसा इसलिए है, क्योंकि आपको किसी अटैचमेंट की चिंता करने की ज़रूरत नहीं होती.
आपको रेंडर पास से पहले कंप्यूट पास का इस्तेमाल करना होगा. ऐसा इसलिए, क्योंकि इससे रेंडर पास, कंप्यूट पास से नए नतीजों का तुरंत इस्तेमाल कर पाता है. यही वजह है कि पास के बीच step
की संख्या बढ़ाई जाती है, ताकि कैलकुलेट पाइपलाइन का आउटपुट बफ़र, रेंडर पाइपलाइन के लिए इनपुट बफ़र बन जाए.
- इसके बाद, कंप्यूट पास में पाइपलाइन और बाइंड ग्रुप सेट करें. इसके लिए, बाइंड ग्रुप के बीच स्विच करने के लिए उसी पैटर्न का इस्तेमाल करें जिसका इस्तेमाल रेंडरिंग पास के लिए किया जाता है.
index.html
const computePass = encoder.beginComputePass();
// New lines
computePass.setPipeline(simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);
computePass.end();
- आखिर में, रेंडर पास की तरह ड्रॉ करने के बजाय, काम को कंप्यूट शेडर पर भेजा जाता है. साथ ही, यह भी बताया जाता है कि आपको हर ऐक्सिस पर कितने वर्कग्रुप चलाने हैं.
index.html
const computePass = encoder.beginComputePass();
computePass.setPipeline(simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);
// New lines
const workgroupCount = Math.ceil(GRID_SIZE / WORKGROUP_SIZE);
computePass.dispatchWorkgroups(workgroupCount, workgroupCount);
computePass.end();
यहां ध्यान देने वाली सबसे ज़रूरी बात यह है कि आपने dispatchWorkgroups()
को जो नंबर पास किया है वह शुरू करने वालों की संख्या नहीं है! इसके बजाय, यह उन वर्कग्रुप की संख्या है जिन्हें चलाना है. यह संख्या, आपके शेडर में @workgroup_size
से तय होती है.
अगर आपको पूरे ग्रिड को कवर करने के लिए शेडर को 32x32 बार एक्ज़ीक्यूट करना है और आपके वर्कग्रुप का साइज़ 8x8 है, तो आपको 4x4 वर्कग्रुप (4 * 8 = 32) भेजने होंगे. इसलिए, ग्रिड साइज़ को वर्कग्रुप के साइज़ से भाग दिया जाता है और उस वैल्यू को dispatchWorkgroups()
में पास किया जाता है.
अब पेज को फिर से रीफ़्रेश करें. आपको हर अपडेट के साथ ग्रिड अपने-आप इनवर्ट होता हुआ दिखेगा.
Game of Life के लिए एल्गोरिदम लागू करना
फ़ाइनल एल्गोरिदम लागू करने के लिए, कंप्यूट शेडर को अपडेट करने से पहले, आपको उस कोड पर वापस जाना होगा जो स्टोरेज बफ़र कॉन्टेंट को शुरू कर रहा है. साथ ही, उसे अपडेट करना होगा, ताकि हर पेज के लोड होने पर एक रैंडम बफ़र बनाया जा सके. (नियमित पैटर्न से Game of Life का शुरुआती पॉइंट बहुत दिलचस्प नहीं है.) वैल्यू को अपने हिसाब से किसी भी क्रम में लगाया जा सकता है. हालांकि, शुरुआत करने का एक आसान तरीका है, जो सही नतीजे देता है.
- हर सेल को किसी भी क्रम में शुरू करने के लिए, नीचे दिए गए कोड पर
cellStateArray
शुरू करने की प्रक्रिया को अपडेट करें:
index.html
// Set each cell to a random state, then copy the JavaScript array
// into the storage buffer.
for (let i = 0; i < cellStateArray.length; ++i) {
cellStateArray[i] = Math.random() > 0.6 ? 1 : 0;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
अब आपके पास, लाइफ़ ऑफ़ गेम सिम्युलेशन के लिए लॉजिक लागू करने का विकल्प है. यहां तक पहुंचने के लिए, आपको बहुत मेहनत करनी पड़ी होगी. हालांकि, शैडर कोड बहुत आसान हो सकता है!
सबसे पहले, आपको यह जानना होगा कि किसी सेल के आस-पास कितनी सेल चालू हैं. कोई फ़र्क़ नहीं पड़ता कि कौनसे ऐक्टिव हैं, सिर्फ़ गिनती के लिए.
- आस-पास के सेल का डेटा आसानी से पाने के लिए, ऐसा
cellActive
फ़ंक्शन जोड़ें जो दिए गए निर्देशांक काcellStateIn
मान लौटाता है.
index.html (Compute createShaderModule कॉल)
fn cellActive(x: u32, y: u32) -> u32 {
return cellStateIn[cellIndex(vec2(x, y))];
}
अगर सेल चालू है, तो cellActive
फ़ंक्शन नतीजे के तौर पर एक वैल्यू दिखाता है. इसलिए, आस-पास के सभी आठ सेल के लिए cellActive
को कॉल करने की रिटर्न वैल्यू जोड़ने पर, आस-पास के कितने सेल चालू हैं.
- सक्रिय पड़ोसियों की संख्या इस तरह देखें:
index.html (Compute createShaderModule कॉल)
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// New lines:
// Determine how many active neighbors this cell has.
let activeNeighbors = cellActive(cell.x+1, cell.y+1) +
cellActive(cell.x+1, cell.y) +
cellActive(cell.x+1, cell.y-1) +
cellActive(cell.x, cell.y-1) +
cellActive(cell.x-1, cell.y-1) +
cellActive(cell.x-1, cell.y) +
cellActive(cell.x-1, cell.y+1) +
cellActive(cell.x, cell.y+1);
इससे एक छोटी सी समस्या हो जाती है. हालांकि: जब सेल की जांच की जा रही है, वह बोर्ड के किनारे से दूर हो, तो क्या होता है? आपके cellIndex()
लॉजिक के मुताबिक इस समय, यह अगली या पिछली लाइन पर चलता है या बफ़र के किनारे चलता है!
गेम ऑफ़ लाइफ़ में, इस समस्या को हल करने का एक सामान्य और आसान तरीका यह है कि ग्रिड के किनारे की सेल, ग्रिड के दूसरी ओर मौजूद सेल को अपने पड़ोसी के तौर पर इस्तेमाल करें. इससे, एक तरह का रैप-अराउंड इफ़ेक्ट बनता है.
cellIndex()
फ़ंक्शन में थोड़ा बदलाव करके, ग्रिड रैप-अराउंड की सुविधा जोड़ी गई है.
index.html (Compute createShaderModule कॉल)
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
जब सेल X और Y, ग्रिड के साइज़ से ज़्यादा हो जाती हैं, तब उन्हें रैप करने के लिए %
ऑपरेटर का इस्तेमाल करें. इससे यह पक्का किया जा सकता है कि स्टोरेज बफ़र की सीमाओं से बाहर कभी भी ऐक्सेस न किया जाए. इससे, आपको यह भरोसा हो जाता है कि activeNeighbors
की संख्या का अनुमान लगाया जा सकता है.
इसके बाद, इन चार में से कोई एक नियम लागू करें:
- आस-पास के दो से कम लोगों वाली कोई भी सेल बंद हो जाती है.
- आस-पास के दो या तीन पड़ोसी वाला कोई भी सेल ऐक्टिव रहता है.
- ठीक तीन पड़ोसी वाला कोई भी बंद सेल चालू हो जाता है.
- तीन से ज़्यादा नेबर सेल वाली कोई भी सेल, इनऐक्टिव हो जाती है.
इसे if स्टेटमेंट की सीरीज़ की मदद से भी किया जा सकता है. हालांकि, WGSL में स्विच स्टेटमेंट भी काम करते हैं, जो इस लॉजिक के लिए सही हैं.
- Game of Life लॉजिक को इस तरह लागू करें:
index.html (Compute createShaderModule कॉल)
let i = cellIndex(cell.xy);
// Conway's game of life rules:
switch activeNeighbors {
case 2: { // Active cells with 2 neighbors stay active.
cellStateOut[i] = cellStateIn[i];
}
case 3: { // Cells with 3 neighbors become or stay active.
cellStateOut[i] = 1;
}
default: { // Cells with < 2 or > 3 neighbors become inactive.
cellStateOut[i] = 0;
}
}
रेफ़रंस के लिए, फ़ाइनल कंप्यूट शेडर मॉड्यूल कॉल अब ऐसा दिखता है:
index.html
const simulationShaderModule = device.createShaderModule({
label: "Life simulation shader",
code: `
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
fn cellActive(x: u32, y: u32) -> u32 {
return cellStateIn[cellIndex(vec2(x, y))];
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// Determine how many active neighbors this cell has.
let activeNeighbors = cellActive(cell.x+1, cell.y+1) +
cellActive(cell.x+1, cell.y) +
cellActive(cell.x+1, cell.y-1) +
cellActive(cell.x, cell.y-1) +
cellActive(cell.x-1, cell.y-1) +
cellActive(cell.x-1, cell.y) +
cellActive(cell.x-1, cell.y+1) +
cellActive(cell.x, cell.y+1);
let i = cellIndex(cell.xy);
// Conway's game of life rules:
switch activeNeighbors {
case 2: {
cellStateOut[i] = cellStateIn[i];
}
case 3: {
cellStateOut[i] = 1;
}
default: {
cellStateOut[i] = 0;
}
}
}
`
});
और... बस हो गया! आपका काम पूरा हुआ! अपना पेज रीफ़्रेश करें और अपने नए सेल्युलर ऑटोमेटॉन को बढ़ते हुए देखें!
9. बधाई हो!
आपने Conway's Game of Life सिम्युलेशन का एक ऐसा वर्शन बनाया है जो पूरी तरह से आपके जीपीयू पर चलता है. इसके लिए, WebGPU API का इस्तेमाल किया जाता है!
आगे क्या होगा?
- WebGPU के सैंपल देखें
आगे पढ़ें
- WebGPU — सभी कोर हैं, कैनवस का कोई नहीं
- Raw WebGPU
- WebGPU की बुनियादी बातें
- WebGPU इस्तेमाल करने के सबसे सही तरीके