आपका पहला WebGPU ऐप्लिकेशन

आपका पहला WebGPU ऐप्लिकेशन

इस कोडलैब (कोड बनाना सीखने के लिए ट्यूटोरियल) के बारे में जानकारी

subjectपिछली बार अप्रैल 15, 2025 को अपडेट किया गया
account_circleBrandon Jones, François Beaufort ने लिखा

1. परिचय

WebGPU के लोगो में कई नीले रंग के त्रिकोण हैं, जो एक स्टाइलिश 'W' बनाते हैं

WebGPU क्या है?

WebGPU एक नया और आधुनिक एपीआई है. इसका इस्तेमाल करके, वेब ऐप्लिकेशन में अपने जीपीयू की सुविधाओं को ऐक्सेस किया जा सकता है.

आधुनिक एपीआई

WebGPU से पहले, WebGL था. इसमें WebGPU की सुविधाओं का एक सबसेट उपलब्ध था. इससे रिच वेब कॉन्टेंट की एक नई कैटगरी बनी और डेवलपर ने इसकी मदद से शानदार चीज़ें बनाईं. हालांकि, यह 2007 में रिलीज़ किए गए OpenGL ES 2.0 एपीआई पर आधारित था, जो कि इससे भी पुराने OpenGL API पर आधारित था. इस दौरान, जीपीयू का काफ़ी विकास हुआ है. साथ ही, उनसे इंटरफ़ेस करने के लिए इस्तेमाल किए जाने वाले नेटिव एपीआई भी Direct3D 12, Metal, और Vulkan के साथ-साथ बेहतर हुए हैं.

WebGPU, वेब प्लैटफ़ॉर्म पर इन आधुनिक एपीआई की बेहतर सुविधाएं उपलब्ध कराता है. यह क्रॉस-प्लैटफ़ॉर्म तरीके से GPU की सुविधाओं को चालू करने पर फ़ोकस करता है. साथ ही, यह ऐसा एपीआई उपलब्ध कराता है जो वेब पर आसानी से इस्तेमाल किया जा सकता है. यह एपीआई, उन कुछ नेटिव एपीआई की तुलना में कम शब्दों में काम करता है जिन पर इसे बनाया गया है.

रेंडर करना

जीपीयू का इस्तेमाल, आम तौर पर तेज़ी से और ज़्यादा जानकारी वाले ग्राफ़िक को रेंडर करने के लिए किया जाता है. WebGPU भी इसी तरह का है. इसमें ऐसी सुविधाएं हैं जिनकी मदद से, डेस्कटॉप और मोबाइल, दोनों जीपीयू पर आज की सबसे लोकप्रिय रेंडरिंग तकनीकों का इस्तेमाल किया जा सकता है. साथ ही, आने वाले समय में हार्डवेयर की क्षमताओं के बेहतर होने के साथ-साथ, नई सुविधाओं को जोड़ने का रास्ता भी मिलता है.

Compute

रेंडरिंग के अलावा, WebGPU आपके जीपीयू की क्षमता को सामान्य कामों के लिए, बहुत ज़्यादा पैरलल वर्कलोड करने के लिए इस्तेमाल करता है. इन कंप्यूट शेडर का इस्तेमाल, किसी रेंडरिंग कॉम्पोनेंट के बिना या अपनी रेंडरिंग पाइपलाइन के साथ इंटिग्रेट करके किया जा सकता है.

आज के कोडलैब में, आपको WebGPU की रेंडरिंग और कंप्यूट की सुविधाओं का फ़ायदा पाने का तरीका पता चलेगा. इससे, आपको शुरुआती प्रोजेक्ट बनाने में मदद मिलेगी!

आपको क्या बनाना है

इस कोडलैब में, WebGPU का इस्तेमाल करके Conway's Game of Life बनाया जाता है. आपका ऐप्लिकेशन:

  • आसान 2D ग्राफ़िक बनाने के लिए, WebGPU की रेंडरिंग क्षमताओं का इस्तेमाल करें.
  • सिम्युलेशन करने के लिए, WebGPU की कंप्यूट क्षमताओं का इस्तेमाल करें.

इस कोडलैब के फ़ाइनल प्रॉडक्ट का स्क्रीनशॉट

गेम ऑफ़ लाइफ़ को सेल्युलर ऑटोमेट भी कहा जाता है. इसमें सेल के ग्रिड की स्थिति, समय के साथ कुछ नियमों के आधार पर बदलती रहती है. गेम ऑफ़ लाइफ़ में, सेल चालू या बंद होने पर यह इस बात पर निर्भर करता है कि उनके आस-पास की कितनी सेल चालू हैं. इससे, आपको देखने के दौरान दिलचस्प पैटर्न दिखते हैं.

आपको क्या सीखने को मिलेगा

  • 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 का इस्तेमाल कर सकता है या नहीं.

  1. WebGPU के एंट्री पॉइंट के तौर पर काम करने वाले navigator.gpu ऑब्जेक्ट के मौजूद होने की जांच करने के लिए, यह कोड जोड़ें:

index.html

if (!navigator.gpu) {
 
throw new Error("WebGPU not supported on this browser.");
}

आम तौर पर, अगर WebGPU उपलब्ध नहीं है, तो आपको उपयोगकर्ता को इसकी जानकारी देनी चाहिए. इसके लिए, पेज को ऐसे मोड पर स्विच करें जो WebGPU का इस्तेमाल न करता हो. (क्या इसके बजाय WebGL का इस्तेमाल किया जा सकता है?) हालांकि, इस कोडलैब के लिए, कोड को आगे चलने से रोकने के लिए, सिर्फ़ गड़बड़ी का मैसेज दिखाया जाता है.

यह जानने के बाद कि ब्राउज़र पर WebGPU काम करता है, अपने ऐप्लिकेशन के लिए WebGPU को शुरू करने का पहला चरण, GPUAdapter का अनुरोध करना है. अडैप्टर को, आपके डिवाइस में मौजूद GPU हार्डवेयर के किसी खास हिस्से के तौर पर देखा जा सकता है.

  1. अडैप्टर पाने के लिए, 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 का अनुरोध करना ज़रूरी है. डिवाइस वह मुख्य इंटरफ़ेस है जिससे जीपीयू के साथ ज़्यादातर इंटरैक्शन होता है.

  1. adapter.requestDevice() को कॉल करके डिवाइस पाएं. इससे एक प्रॉमिस भी मिलता है.

index.html

const device = await adapter.requestDevice();

requestAdapter() की तरह ही, यहां भी ऐसे विकल्प हैं जिन्हें पास किया जा सकता है. इनका इस्तेमाल, बेहतर सुविधाओं के लिए किया जा सकता है. जैसे, किसी खास हार्डवेयर की सुविधाओं को चालू करना या ज़्यादा सीमाओं का अनुरोध करना. हालांकि, आपके काम के लिए डिफ़ॉल्ट विकल्प ही सही रहेंगे.

Canvas को कॉन्फ़िगर करना

अब आपके पास एक डिवाइस है. अगर आपको पेज पर कुछ दिखाना है, तो आपको एक और काम करना होगा: अपने बनाए गए डिवाइस के साथ इस्तेमाल करने के लिए कैनवस को कॉन्फ़िगर करें.

  • ऐसा करने के लिए, पहले canvas.getContext("webgpu") को कॉल करके कैनवस से GPUCanvasContext का अनुरोध करें. (यह वही कॉल है जिसका इस्तेमाल, 2d और webgl कॉन्टेक्स्ट टाइप का इस्तेमाल करके, कैनवस 2D या WebGL कॉन्टेक्स्ट को शुरू करने के लिए किया जाता है.) इसके बाद, यह configure() तरीके का इस्तेमाल करके, डिवाइस से जुड़ा context दिखाता है. जैसे:

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 में कोई भी अन्य काम करने के लिए, आपको जीपीयू को कुछ निर्देश देने होंगे.

  1. ऐसा करने के लिए, डिवाइस को GPUCommandEncoder बनाएं. इससे, जीपीयू के निर्देशों को रिकॉर्ड करने के लिए इंटरफ़ेस मिलता है.

index.html

const encoder = device.createCommandEncoder();

आपको GPU को जो निर्देश भेजने हैं वे रेंडरिंग से जुड़े हैं. इस मामले में, कैनवस को खाली करना. इसलिए, अगले चरण में रेंडर पास शुरू करने के लिए, encoder का इस्तेमाल करें.

WebGPU में ड्रॉइंग से जुड़े सभी काम, रेंडर पास के दौरान होते हैं. हर एक, beginRenderPass() कॉल से शुरू होता है. इससे उन टेक्सचर के बारे में पता चलता है जिन्हें ड्रॉइंग के लिए दिए गए किसी भी निर्देश का आउटपुट मिलता है. ज़्यादा बेहतर इस्तेमाल के लिए, कई टेक्सचर उपलब्ध कराए जा सकते हैं. इन्हें अटैचमेंट कहा जाता है. इनका इस्तेमाल अलग-अलग कामों के लिए किया जा सकता है. जैसे, रेंडर की गई ज्यामिति की गहराई को सेव करना या ऐंटी-ऐलिऐसिंग (ऐलिऐसिंग को कम करना) की सुविधा देना. हालांकि, इस ऐप्लिकेशन के लिए, आपको सिर्फ़ एक खाता चाहिए.

  1. 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" से रेंडर पास शुरू करना ही काफ़ी है.

  1. beginRenderPass() के तुरंत बाद यह कॉल जोड़कर, रेंडर पास को खत्म करें:

index.html

pass.end();

यह जानना ज़रूरी है कि सिर्फ़ इन कॉल को करने से, GPU कुछ नहीं करता. वे सिर्फ़ जीपीयू के लिए कमांड रिकॉर्ड कर रहे हैं, ताकि जीपीयू बाद में उन्हें पूरा कर सके.

  1. GPUCommandBuffer बनाने के लिए, कमांड एन्कोडर पर finish() को कॉल करें. कमांड बफ़र, रिकॉर्ड किए गए कमांड का एक ऐसा हैंडल है जिसे समझना मुश्किल है.

index.html

const commandBuffer = encoder.finish();
  1. GPUDevice के queue का इस्तेमाल करके, GPU को कमांड बफ़र सबमिट करें. कतार, जीपीयू के सभी निर्देशों को पूरा करती है. इससे यह पक्का होता है कि निर्देशों को सही क्रम में और सही तरीके से सिंक किया गया है. कतार के 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() को फिर से कॉल करना होगा.

  1. पेज को फिर से लोड करें. ध्यान दें कि कैनवस का रंग काला है. बधाई हो! इसका मतलब है कि आपने अपना पहला WebGPU ऐप्लिकेशन बना लिया है.

काला कैनवस, जो यह दिखाता है कि कैनवस के कॉन्टेंट को मिटाने के लिए, WebGPU का इस्तेमाल किया गया है.

कोई रंग चुनें!

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

  1. 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 } डिफ़ॉल्ट रूप से, पारदर्शी काला होता है.

इस कोडलैब में दिए गए उदाहरण के कोड और स्क्रीनशॉट में गहरे नीले रंग का इस्तेमाल किया गया है. हालांकि, अपनी पसंद का कोई भी रंग चुना जा सकता है!

  1. अपना रंग चुनने के बाद, पेज को फिर से लोड करें. आपको कैनवस में अपना चुना गया रंग दिखेगा.

डिफ़ॉल्ट तौर पर मौजूद, खाली कैनवस को गहरे नीले रंग में बदला गया है. इससे, खाली कैनवस का रंग बदलने का तरीका पता चलता है.

4. जियोमेट्री ड्रॉ करना

इस सेक्शन के आखिर तक, आपका ऐप्लिकेशन कैनवस पर कुछ आसान ज्यामितीय आकृतियां बना देगा: रंगीन स्क्वेयर. ध्यान रखें कि इस तरह के आसान आउटपुट के लिए, आपको बहुत ज़्यादा काम करना पड़ेगा. ऐसा इसलिए है, क्योंकि WebGPU को बहुत ज्यामिति को बेहतर तरीके से रेंडर करने के लिए डिज़ाइन किया गया है. इस एपीआई की परफ़ॉर्मेंस का एक साइड इफ़ेक्ट यह है कि आसान काम भी मुश्किल लग सकते हैं. हालांकि, WebGPU जैसे एपीआई का इस्तेमाल करने का मकसद ही कुछ ऐसा करना होता है जो थोड़ा मुश्किल हो.

जानें कि जीपीयू कैसे ड्रॉ करते हैं

कोड में कोई और बदलाव करने से पहले, यह जानना ज़रूरी है कि जीपीयू, स्क्रीन पर दिखने वाले आकार कैसे बनाते हैं. (अगर आपको GPU रेंडरिंग के काम करने के तरीके के बारे में पहले से पता है, तो सीधे 'वेक्टर्स तय करना' सेक्शन पर जाएं.)

Canvas 2D जैसे एपीआई के मुकाबले, आपका जीपीयू सिर्फ़ कुछ अलग-अलग तरह के शेप (या प्राइमटिव, जैसा कि WebGPU में उन्हें कहा जाता है) के साथ काम करता है: पॉइंट, लाइनें, और त्रिकोण. Canvas 2D में, इस्तेमाल के लिए कई शेप और विकल्प पहले से मौजूद होते हैं. इस कोडलैब में, सिर्फ़ त्रिकोणों का इस्तेमाल किया जाएगा.

जीपीयू, खास तौर पर त्रिभुजों के साथ काम करते हैं. ऐसा इसलिए, क्योंकि त्रिभुजों में गणित से जुड़ी कई अच्छी प्रॉपर्टी होती हैं. इनकी मदद से, त्रिभुजों को आसानी से और बेहतर तरीके से प्रोसेस किया जा सकता है. जीपीयू से खींची गई लगभग हर चीज़ को, जीपीयू के खींचने से पहले त्रिभुजों में बांटना ज़रूरी है. साथ ही, उन त्रिभुजों को उनके कोने वाले पॉइंट से तय किया जाना चाहिए.

ये पॉइंट या वर्टिक्स, X, Y, और (3D कॉन्टेंट के लिए) Z वैल्यू के हिसाब से दिए जाते हैं. ये वैल्यू, WebGPU या मिलते-जुलते एपीआई से तय किए गए कार्तीय निर्देशांक सिस्टम पर किसी पॉइंट की जानकारी देती हैं. कोऑर्डिनेट सिस्टम के स्ट्रक्चर को इस हिसाब से समझना आसान है कि यह आपके पेज के कैनवस से कैसे जुड़ा है. आपका कैनवस कितना भी चौड़ा या लंबा हो, बायां किनारा हमेशा X ऐक्सिस पर -1 पर और दायां किनारा हमेशा X ऐक्सिस पर +1 पर होता है. इसी तरह, Y ऐक्सिस पर सबसे नीचे का किनारा हमेशा -1 होता है और सबसे ऊपर का किनारा +1 होता है. इसका मतलब है कि (0, 0) हमेशा कैनवस का बीच होता है, (-1, -1) हमेशा सबसे नीचे बाईं ओर होता है, और (1, 1) हमेशा सबसे ऊपर दाईं ओर होता है. इसे क्लिप स्पेस कहा जाता है.

नॉर्मलाइज़ किए गए डिवाइस कोऑर्डिनेट स्पेस को विज़ुअलाइज़ करने वाला आसान ग्राफ़.

आम तौर पर, शुरुआत में इस निर्देशांक सिस्टम में वर्टिक्स तय नहीं किए जाते. इसलिए, जीपीयू वर्टिक्स शेडर नाम के छोटे प्रोग्राम पर निर्भर होते हैं. इन प्रोग्राम की मदद से, वर्टिक्स को क्लिप स्पेस में बदलने के लिए ज़रूरी गणित के साथ-साथ, वर्टिक्स को ड्रॉ करने के लिए ज़रूरी अन्य गणनाएं भी की जाती हैं. उदाहरण के लिए, शेडर कुछ ऐनिमेशन लागू कर सकता है या किसी लाइट सोर्स से वर्टिक्स तक की दिशा का हिसाब लगा सकता है. ये शेडर, WebGPU डेवलपर यानी आपने लिखे हैं. इनसे, जीपीयू के काम करने के तरीके को बेहतर तरीके से कंट्रोल किया जा सकता है.

इसके बाद, GPU इन बदले हुए वर्टिसेस से बने सभी ट्राएंगल लेता है और यह तय करता है कि उन्हें ड्रॉ करने के लिए, स्क्रीन पर किन पिक्सल की ज़रूरत है. इसके बाद, यह आपके लिखे गए एक छोटे प्रोग्राम को चलाता है. इसे फ़्रेगमेंट शेडर कहा जाता है. यह यह हिसाब लगाता है कि हर पिक्सल का रंग क्या होना चाहिए. यह गणना हरा रंग दिखाएं जैसी आसान हो सकती है या फिर आस-पास की अन्य सतहों से टकराकर आने वाली सूरज की रोशनी के हिसाब से, सतह के कोण का हिसाब लगाने जैसी मुश्किल भी हो सकती है. इसमें, धुंध के ज़रिए फ़िल्टर की गई रोशनी और सतह के मेटल होने या न होने के हिसाब से भी बदलाव किया जा सकता है. यह पूरी तरह से आपके कंट्रोल में है.

इसके बाद, उन पिक्सल के रंगों के नतीजों को एक टेक्स्चर में इकट्ठा किया जाता है, जिसे स्क्रीन पर दिखाया जा सकता है.

वर्टिसेस तय करना

जैसा कि पहले बताया गया है, लाइफ़ ऑफ़ गेम सिम्युलेशन को सेल के ग्रिड के तौर पर दिखाया जाता है. आपके ऐप्लिकेशन में ग्रिड को विज़ुअलाइज़ करने का तरीका होना चाहिए, ताकि ऐक्टिव सेल को इनऐक्टिव सेल से अलग किया जा सके. इस कोडलैब में, ऐक्टिव सेल में रंगीन स्क्वेयर बनाए जाएंगे और इनऐक्टिव सेल को खाली छोड़ दिया जाएगा.

इसका मतलब है कि आपको जीपीयू को चार अलग-अलग पॉइंट देने होंगे. ये पॉइंट, स्क्वेयर के चारों कोनों के लिए होंगे. उदाहरण के लिए, कैनवस के बीच में खींचा गया एक स्क्वेयर, किनारों से थोड़ा अंदर की ओर खींचा गया है. इसके कोने के निर्देशांक इस तरह के हैं:

नॉर्मलाइज़ किए गए डिवाइस कोऑर्डिनेट का ऐसा ग्राफ़ जिसमें स्क्वेयर के कोनों के कोऑर्डिनेट दिखाए गए हैं

उन निर्देशांकों को GPU में फ़ीड करने के लिए, आपको वैल्यू को TypedArray में डालना होगा. अगर आपको इसके बारे में पहले से जानकारी नहीं है, तो TypedArrays, JavaScript ऑब्जेक्ट का एक ग्रुप है. इसकी मदद से, मेमोरी के एक-साथ कई ब्लॉक को एलोकेट किया जा सकता है. साथ ही, सीरीज़ के हर एलिमेंट को किसी खास डेटा टाइप के तौर पर समझा जा सकता है. उदाहरण के लिए, Uint8Array में, कलेक्शन का हर एलिमेंट एक बिना साइन वाला बाइट होता है. TypedArrays, उन एपीआई के साथ डेटा को एक से दूसरी जगह भेजने के लिए बहुत अच्छे हैं जो मेमोरी लेआउट के हिसाब से काम करते हैं. जैसे, WebAssembly, WebAudio, और (बेशक) WebGPU.

स्क्वेयर के उदाहरण के लिए, वैल्यू भिन्नात्मक होने की वजह से, Float32Array सही है.

  1. अपने कोड में नीचे दिया गया कलेक्शन एलान करके, डायग्राम में सभी वर्टिक्स पोज़िशन वाला कलेक्शन बनाएं. इसे सबसे ऊपर, 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) वर्टिसेस को दो बार डालना होगा. एक बार नीले ट्रायंगल के लिए और एक बार लाल ट्रायंगल के लिए. (इसके बजाय, स्क्वेयर को दो अन्य कोनों से भी बांटा जा सकता है. इससे कोई फ़र्क़ नहीं पड़ता.)

  1. अपने पिछले 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 की तरह माना जा सकता है.

  1. अपने वर्टिसेस को सेव करने के लिए बफ़र बनाने के लिए, 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 बनाने के बाद, उसका साइज़ नहीं बदला जा सकता. साथ ही, इसके इस्तेमाल से जुड़े फ़्लैग में भी बदलाव नहीं किया जा सकता. हालांकि, इसकी मेमोरी में मौजूद कॉन्टेंट में बदलाव किया जा सकता है.

बफ़र बनाने पर, उसमें मौजूद मेमोरी को शून्य पर सेट कर दिया जाएगा. इसके कॉन्टेंट को बदलने के कई तरीके हैं. हालांकि, सबसे आसान तरीका यह है कि TypedArray के साथ device.queue.writeBuffer() को कॉल करें.

  1. वर्टिक्स डेटा को बफ़र की मेमोरी में कॉपी करने के लिए, यह कोड जोड़ें:

index.html

device.queue.writeBuffer(vertexBuffer, /*bufferOffset=*/0, vertices);

वर्टिक्स लेआउट तय करना

अब आपके पास वर्टेक्स डेटा वाला बफ़र है, लेकिन जीपीयू के लिए यह सिर्फ़ बाइट का एक ब्लॉब है. अगर आपको इसकी मदद से कुछ बनाना है, तो आपको थोड़ी और जानकारी देनी होगी. आपको वेबजीपीयू को, वर्टिक्स डेटा के स्ट्रक्चर के बारे में ज़्यादा जानकारी देनी होगी.

  • 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 को यह बताना होगा कि उसे कैसे प्रोसेस करना है. इसका ज़्यादातर हिस्सा शेडर की मदद से होता है.

शेडर छोटे प्रोग्राम होते हैं. इन्हें लिखकर, जीपीयू पर चलाया जाता है. हर शेडर, डेटा के अलग-अलग स्टेज पर काम करता है: वर्टिक्स प्रोसेसिंग, फ़्रैगमेंट प्रोसेसिंग या सामान्य कंप्यूट. ये जीपीयू पर काम करते हैं. इसलिए, इन्हें सामान्य 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 कीवर्ड का इस्तेमाल करके फ़ंक्शन दिखाए जाते हैं. साथ ही, किसी भी आर्ग्युमेंट का एलान करने के लिए ब्रैकेट का इस्तेमाल किया जाता है और स्कोप तय करने के लिए कर्ली ब्रैकेट का इस्तेमाल किया जाता है.

  1. खाली @vertex फ़ंक्शन बनाएं, जैसे:

index.html (createShaderModule कोड)

@vertex
fn vertexMain() {

}

हालांकि, यह मान्य नहीं है, क्योंकि किसी वर्टिक्स शेडर को क्लिप स्पेस में प्रोसेस किए जा रहे वर्टिक्स की फ़ाइनल पोज़िशन कम से कम दिखानी चाहिए. इसे हमेशा चार डाइमेंशन वाले वेक्टर के तौर पर दिया जाता है. शेडर में वेक्टर का इस्तेमाल बहुत आम है. इसलिए, उन्हें भाषा में फ़र्स्ट-क्लास प्राइमिटिव के तौर पर माना जाता है. साथ ही, उनके अपने टाइप होते हैं, जैसे कि चार डाइमेंशन वाले वेक्टर के लिए vec4f. 2D वेक्टर (vec2f) और 3D वेक्टर (vec3f) के लिए भी इसी तरह के टाइप मौजूद हैं!

  1. यह बताने के लिए कि दिखाई गई वैल्यू, ज़रूरी पोज़िशन है, उसे @builtin(position) एट्रिब्यूट से मार्क करें. -> सिंबल का इस्तेमाल यह बताने के लिए किया जाता है कि फ़ंक्शन से यह वैल्यू मिलती है.

index.html (createShaderModule कोड)

@vertex
fn vertexMain() -> @builtin(position) vec4f {

}

अगर फ़ंक्शन का रिटर्न टाइप है, तो आपको फ़ंक्शन के मुख्य हिस्से में कोई वैल्यू रिटर्न करनी होगी. सिंटैक्स vec4f(x, y, z, w) का इस्तेमाल करके, नया vec4f बनाया जा सकता है. x, y, और z वैल्यू, फ़्लोटिंग पॉइंट वाली संख्याएं हैं. ये वैल्यू, रिटर्न वैल्यू में बताती हैं कि क्लिप स्पेस में वर्टिक्स कहां है.

  1. (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 जैसा नाम देना सही रहेगा.

  1. अपने शेडर फ़ंक्शन को इस कोड में बदलें:

index.html (createShaderModule कोड)

@vertex
fn vertexMain(@location(0) pos: vec2f) ->
  @builtin(position) vec4f {
  return vec4f(0, 0, 0, 1);
}

अब आपको उस पोज़िशन पर वापस जाना होगा. पोज़िशन एक 2D वेक्टर है और रिटर्न टाइप एक 4D वेक्टर है. इसलिए, आपको इसमें थोड़ा बदलाव करना होगा. आपको पोज़िशन आर्ग्युमेंट से दो कॉम्पोनेंट लेने हैं और उन्हें रिटर्न वैक्टर के पहले दो कॉम्पोनेंट में डालना है. साथ ही, आखिरी दो कॉम्पोनेंट को क्रमशः 0 और 1 के तौर पर छोड़ना है.

  1. सही पोज़िशन दिखाने के लिए, साफ़ तौर पर बताएं कि किन पोज़िशन कॉम्पोनेंट का इस्तेमाल करना है:

index.html (createShaderModule कोड)

@vertex
fn vertexMain(@location(0) pos: vec2f) ->
  @builtin(position) vec4f {
  return vec4f(pos.x, pos.y, 0, 1);
}

हालांकि, शेडर में इस तरह की मैपिंग बहुत आम हैं. इसलिए, पोज़िशन वेक्टर को पहले आर्ग्युमेंट के तौर पर आसान शॉर्टहैंड में भी पास किया जा सकता है. इसका मतलब वही है.

  1. return स्टेटमेंट को इस कोड से फिर से लिखें:

index.html (createShaderModule कोड)

@vertex
fn vertexMain(@location(0) pos: vec2f) ->
  @builtin(position) vec4f {
  return vec4f(pos, 0, 1);
}

और यह आपका शुरुआती वर्टिक्स शेडर है! यह बहुत आसान है. इसमें पोज़िशन में कोई बदलाव नहीं किया जाता, लेकिन शुरुआत करने के लिए यह काफ़ी है.

फ़्रैगमेंट शेडर तय करना

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

फ़्रेगमेंट शेडर को हमेशा वर्टिक्स शेडर के बाद कॉल किया जाता है. जीपीयू, वर्टिक्स शेडर का आउटपुट लेता है और उसे त्रिकोण में बदलता है. इसके बाद, तीन बिंदुओं के सेट से त्रिकोण बनाता है. इसके बाद, यह उन सभी त्रिभुजों को रेस्टर करता है. इसके लिए, यह पता लगाता है कि आउटपुट कलर अटैचमेंट के कौनसे पिक्सल उस त्रिभुज में शामिल हैं. इसके बाद, उन सभी पिक्सल के लिए एक बार फ़्रेगमेंट शेडर को कॉल करता है. फ़्रेगमेंट शेडर एक रंग दिखाता है. आम तौर पर, इस रंग का हिसाब, वर्टिक्स शेडर और टेक्सचर जैसी एसेट से भेजी गई वैल्यू से लगाया जाता है. जीपीयू, इस रंग को कलर अटैचमेंट में लिखता है.

वर्टिक्स शेडर की तरह ही, फ़्रैगमेंट शेडर भी एक साथ कई प्रोसेस में काम करते हैं. इनपुट और आउटपुट के मामले में, ये वर्टिक्स शेडर से थोड़े ज़्यादा फ़्लेक्सिबल होते हैं. हालांकि, इनकी मदद से हर त्रिभुज के हर पिक्सल के लिए सिर्फ़ एक रंग दिखाया जा सकता है.

WGSL फ़्रेगमेंट शेडर फ़ंक्शन को @fragment एट्रिब्यूट से दिखाया जाता है. साथ ही, यह एक vec4f भी दिखाता है. हालांकि, इस मामले में वेक्टर किसी पोज़िशन को नहीं, बल्कि रंग को दिखाता है. रिटर्न वैल्यू को @location एट्रिब्यूट देना ज़रूरी है, ताकि यह पता चल सके कि beginRenderPass कॉल में से किस colorAttachment में रिटर्न किया गया रंग लिखा गया है. आपके मैसेज में सिर्फ़ एक अटैचमेंट था, इसलिए रैंक 0 है.

  1. खाली @fragment फ़ंक्शन बनाएं, जैसे:

index.html (createShaderModule कोड)

@fragment
fn fragmentMain() -> @location(0) vec4f {

}

दिखाए गए वेक्टर के चार कॉम्पोनेंट, लाल, हरा, नीला, और अल्फा कलर वैल्यू होते हैं. इन्हें उसी तरह से समझा जाता है जिस तरह से आपने पहले beginRenderPass में clearValue सेट किया था. इसलिए, vec4f(1, 0, 0, 1) चटक लाल है, जो आपके स्क्वेयर के लिए एक अच्छा रंग है. हालांकि, इसे अपनी पसंद के किसी भी रंग में सेट किया जा सकता है!

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

रेंडर पाइपलाइन बनाना

शेडर मॉड्यूल का इस्तेमाल, रेंडरिंग के लिए अपने-आप नहीं किया जा सकता. इसके बजाय, आपको इसे device.createRenderPipeline() को कॉल करके बनाए गए GPURenderPipeline के हिस्से के तौर पर इस्तेमाल करना होगा. रेंडर पाइपलाइन यह कंट्रोल करती है कि ज्यामिति कैसे खींची जाए. इसमें यह तय करना भी शामिल है कि किन शेडर का इस्तेमाल किया जाए, वर्टिक्स बफ़र में डेटा को कैसे समझा जाए, किस तरह की ज्यामिति को रेंडर किया जाए (लाइनें, पॉइंट, ट्राएंगल वगैरह) वगैरह!

रेंडर पाइपलाइन, पूरे एपीआई में सबसे मुश्किल ऑब्जेक्ट है. हालांकि, चिंता न करें! इसमें दी जाने वाली ज़्यादातर वैल्यू ज़रूरी नहीं हैं. शुरुआत करने के लिए, आपको सिर्फ़ कुछ वैल्यू देनी होंगी.

  • इस तरह की रेंडर पाइपलाइन बनाएं:

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 चरण के बारे में जानकारी मिलेगी. इसमें शेडर मॉड्यूल और entryPoint भी शामिल है, जैसे कि वर्टिक्स स्टेज. आखिरी चरण में, उस targets को तय करना है जिसके साथ इस पाइपलाइन का इस्तेमाल किया जाता है. यह डिक्शनरी का एक कलेक्शन है, जिसमें उन कलर अटैचमेंट की जानकारी होती है जिन्हें पाइपलाइन आउटपुट करती है. जैसे, टेक्सचर format. यह जानकारी, इस पाइपलाइन के साथ इस्तेमाल किए जाने वाले किसी भी रेंडर पास के colorAttachments में दिए गए टेक्सचर से मेल खानी चाहिए. आपका रेंडर पास, कैनवस कॉन्टेक्स्ट से टेक्सचर का इस्तेमाल करता है. साथ ही, अपने फ़ॉर्मैट के लिए canvasFormat में सेव की गई वैल्यू का इस्तेमाल करता है. इसलिए, आपको यहां वही फ़ॉर्मैट पास करना होगा.

रेंडर पाइपलाइन बनाते समय, इनके अलावा भी कई विकल्पों का इस्तेमाल किया जा सकता है. हालांकि, इस कोडलैब के लिए ये विकल्प काफ़ी हैं!

स्क्वेयर बनाना

अब आपके पास स्क्वेयर बनाने के लिए, ज़रूरी सभी चीज़ें हैं!

  1. स्क्वेयर बनाने के लिए, कॉल के 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 वर्टिसेस) से कैलकुलेट करने का मतलब है कि अगर आपको कभी स्क्वेयर को किसी सर्कल से बदलना है, तो आपको मैन्युअल तरीके से कम अपडेट करने होंगे.

  1. अपनी स्क्रीन रीफ़्रेश करें और अपनी मेहनत का नतीजा देखें: एक बड़ा रंगीन स्क्वेयर.

WebGPU की मदद से रेंडर किया गया एक लाल स्क्वेयर

5. ग्रिड बनाना

सबसे पहले, खुद को बधाई दें! ज़्यादातर GPU API के लिए, स्क्रीन पर ज्यामिति के शुरुआती बिट दिखाना अक्सर सबसे मुश्किल चरणों में से एक होता है. यहां से जो भी काम किया जाता है उसे छोटे चरणों में किया जा सकता है. इससे, अपनी प्रोग्रेस की पुष्टि करना आसान हो जाता है.

इस सेक्शन में, आपको इनके बारे में जानकारी मिलेगी:

  • JavaScript से शेडर में वैरिएबल (जिन्हें यूनिफ़ॉर्म कहा जाता है) पास करने का तरीका.
  • रेंडरिंग के तरीके को बदलने के लिए, यूनिफ़ॉर्म का इस्तेमाल करने का तरीका.
  • एक ही ज्यामिति के कई अलग-अलग वैरिएंट बनाने के लिए, इंस्टैंसिंग का इस्तेमाल करने का तरीका.

ग्रिड तय करना

ग्रिड को रेंडर करने के लिए, आपको इसके बारे में बुनियादी जानकारी होनी चाहिए. इसमें चौड़ाई और ऊंचाई, दोनों में कितनी सेल हैं? यह डेवलपर के ऊपर है कि वह ग्रिड को किस तरह सेट करेगा. हालांकि, चीज़ों को आसान बनाने के लिए, ग्रिड को स्क्वेयर (एक जैसी चौड़ाई और ऊंचाई) के तौर पर सेट करें. साथ ही, ऐसे साइज़ का इस्तेमाल करें जो दो की घात हो. (इससे बाद में, गणित के कुछ सवालों को हल करना आसान हो जाता है.) आपको इसे बाद में बड़ा करना है, लेकिन इस सेक्शन के बाकी हिस्से के लिए, अपने ग्रिड का साइज़ 4x4 पर सेट करें. इससे इस सेक्शन में इस्तेमाल किए गए कुछ गणित को आसानी से दिखाया जा सकता है. इसके बाद, इसे बड़े पैमाने पर लागू करें!

  • अपने JavaScript कोड में सबसे ऊपर एक कॉन्स्टेंट जोड़कर, ग्रिड का साइज़ तय करें.

index.html

const GRID_SIZE = 4;

इसके बाद, आपको अपने स्क्वेयर को रेंडर करने का तरीका अपडेट करना होगा, ताकि कैनवस पर GRID_SIZE x 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 को ड्रॉइंग के दौरान इसका इस्तेमाल करने के लिए कहना होगा. अच्छी बात यह है कि यह बहुत आसान है.

  1. रेंडर पास पर वापस जाएं और 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, इस बाइंड ग्रुप में मौजूद रिसॉर्स का इस्तेमाल करता है.

अब यूनिफ़ॉर्म बफ़र आपके शेडर के लिए उपलब्ध है!

  1. अपना पेज रीफ़्रेश करें. इसके बाद, आपको कुछ ऐसा दिखेगा:

गहरे नीले रंग के बैकग्राउंड के बीच में एक छोटा लाल वर्ग.

बहुत बढ़िया! आपका स्क्वेयर अब पहले के मुकाबले एक चौथाई हो गया है! यह बहुत नहीं है, लेकिन इससे पता चलता है कि आपका यूनिफ़ॉर्म असल में लागू है और शेडर अब आपके ग्रिड का साइज़ ऐक्सेस कर सकता है.

शेडर में ज्यामिति में बदलाव करना

अब शेडर में ग्रिड के साइज़ का रेफ़रंस दिया जा सकता है. इसलिए, अपने पसंदीदा ग्रिड पैटर्न में फ़िट करने के लिए, रेंडर की जा रही ज्यामिति में बदलाव किया जा सकता है. ऐसा करने के लिए, यह तय करें कि आपको क्या हासिल करना है.

आपको अपने कैनवस को अलग-अलग सेल में बांटना होगा. यह मानते हुए कि दाईं ओर बढ़ने पर X ऐक्सिस बढ़ता है और ऊपर बढ़ने पर Y ऐक्सिस बढ़ता है, मान लें कि पहली सेल कैनवस के सबसे नीचे बाएं कोने में है. इससे आपको ऐसा लेआउट मिलता है जो इस तरह दिखता है. इसमें बीच में आपकी मौजूदा स्क्वेयर ज्यामिति होती है:

कॉन्सेप्ट ग्रिड की इमेज, जिसमें हर सेल को उसके बीच में मौजूद रेंडर की गई स्क्वेयर ज्यामिति के साथ विज़ुअलाइज़ किया गया है. इस ग्रिड में, नॉर्मलाइज़ किए गए डिवाइस कोऑर्डिनेट स्पेस को बांटा जाएगा.

आपका चैलेंज यह है कि शेडर में ऐसा तरीका ढूंढें जिससे सेल के निर्देशांकों के हिसाब से, उनमें से किसी भी सेल में स्क्वेयर ज्यामिति को पोज़िशन किया जा सके.

सबसे पहले, आपको दिखेगा कि आपका स्क्वेयर किसी भी सेल के साथ ठीक से अलाइन नहीं है, क्योंकि इसे कैनवस के बीच में रखने के लिए तय किया गया था. आपको स्क्वेयर को आधी सेल तक शिफ़्ट करना होगा, ताकि वह उनमें अच्छी तरह से अलाइन हो जाए.

इसे ठीक करने का एक तरीका यह है कि स्क्वेयर के वर्टिक्स बफ़र को अपडेट करें. उदाहरण के लिए, नीचे बाईं ओर का कोना (-0.8, -0.8) के बजाय (0.1, 0.1) पर हो, ताकि इस स्क्वेयर को सेल की सीमाओं के साथ बेहतर तरीके से अलाइन किया जा सके. हालांकि, आपके पास शेडर में वर्टिक्स को प्रोसेस करने के तरीके को कंट्रोल करने का पूरा अधिकार होता है. इसलिए, शेडर कोड का इस्तेमाल करके उन्हें आसानी से अपनी जगह पर ले जाया जा सकता है!

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

इससे, ग्रिड साइज़ से भाग करने से पहले हर वर्टिक्स को ऊपर और दाईं ओर एक (यानी क्लिप स्पेस का आधा) ले जाया जाता है. इससे, ऑरिजिन के ठीक बगल में, ग्रिड के साथ अलाइन किया गया एक स्क्वेयर बन जाता है.

कैनवस का विज़ुअलाइज़ेशन, जिसे कॉन्सेप्ट के हिसाब से 4x4 ग्रिड में बांटा गया है. इसमें सेल (2, 2) में लाल रंग का स्क्वेयर है

इसके बाद, आपको अपनी ज्यामिति की पोज़िशन को ग्रिड साइज़ से भाग देने के बाद, (-1, -1) से ट्रांसलेट करना होगा, ताकि उसे उस कोने में ले जाया जा सके. ऐसा इसलिए करना होगा, क्योंकि आपके कैनवस के कोऑर्डिनेट सिस्टम में (0, 0) बीच में और (-1, -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) में सही जगह पर दिख रहा है!

कैनवस का विज़ुअलाइज़ेशन, जिसे कॉन्सेप्ट के हिसाब से 4x4 ग्रिड में बांटा गया है. इसमें सेल (0, 0) में लाल रंग का स्क्वेयर है

अगर आपको इसे किसी दूसरी सेल में डालना है, तो क्या होगा? अपने शेडर में cell वेक्टर का एलान करके और उसे let cell = vec2f(1, 1) जैसी स्टैटिक वैल्यू से पॉप्युलेट करके, इसका पता लगाएं.

अगर इसे gridPos में जोड़ा जाता है, तो यह एल्गोरिदम में - 1 को पहले जैसा कर देता है. इसलिए, ऐसा नहीं करना चाहिए. इसके बजाय, आपको हर सेल के लिए स्क्वेयर को सिर्फ़ एक ग्रिड यूनिट (कैनवस का एक चौथाई) तक ले जाना है. ऐसा लगता है कि आपको grid से फिर से भाग देना होगा!

  1. ग्रिड की पोज़िशनिंग को इस तरह बदलें:

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

अब रीफ़्रेश करने पर, आपको ये चीज़ें दिखेंगी:

कैनवस का विज़ुअलाइज़ेशन, जिसे कॉन्सेप्ट के हिसाब से 4x4 ग्रिड में बांटा गया है. इसमें सेल (0, 0), सेल (0, 1), सेल (1, 0), और सेल (1, 1) के बीच में लाल रंग का स्क्वेयर है

हम्म. ऐसा नहीं है कि आपको जो चाहिए था वह आपको मिल गया.

इसकी वजह यह है कि कैनवस के निर्देशांक -1 से +1 तक होते हैं. इसलिए, असल में यह दो यूनिट चौड़ा होता है. इसका मतलब है कि अगर आपको कैनवस के एक-चौथाई हिस्से पर मौजूद किसी वर्टिक्स को आगे ले जाना है, तो आपको उसे 0.5 यूनिट आगे ले जाना होगा. जीपीयू निर्देशांक के साथ काम करते समय, यह गलती आसानी से हो सकती है! अच्छी बात यह है कि इसे ठीक करना भी उतना ही आसान है.

  1. अपने ऑफ़सेट को दो से गुणा करें, जैसे:

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

इससे आपको अपनी पसंद के मुताबिक नतीजे मिलते हैं.

कैनवस का विज़ुअलाइज़ेशन, जिसे कॉन्सेप्ट के हिसाब से 4x4 ग्रिड में बांटा गया है. इसमें सेल (1, 1) में लाल रंग का स्क्वेयर है

स्क्रीनशॉट ऐसा दिखता है:

गहरे नीले रंग के बैकग्राउंड पर, लाल रंग के स्क्वेयर का स्क्रीनशॉट. लाल रंग का स्क्वेयर, पिछले डायग्राम में बताई गई जगह पर ही बनाया गया है. हालांकि, इसमें ग्रिड ओवरले नहीं है.

इसके अलावा, अब cell को ग्रिड के दायरे में किसी भी वैल्यू पर सेट किया जा सकता है. इसके बाद, अपनी पसंद की जगह पर स्क्वेयर रेंडर देखने के लिए रीफ़्रेश करें.

इंस्टेंस बनाना

अब आपके पास थोड़े से गणित के हिसाब से, स्क्वेयर को अपनी पसंद की जगह पर रखने का विकल्प है. अगला चरण, ग्रिड की हर सेल में एक स्क्वेयर रेंडर करना है.

इसे एक तरीके से इस तरह से किया जा सकता है: यूनिफ़ॉर्म बफ़र में सेल के निर्देशांक लिखें. इसके बाद, ग्रिड के हर स्क्वेयर के लिए एक बार draw को कॉल करें और हर बार यूनिफ़ॉर्म को अपडेट करें. हालांकि, यह बहुत धीमा होगा, क्योंकि जीपीयू को हर बार JavaScript से नए निर्देश लिखने का इंतज़ार करना पड़ता है. जीपीयू से अच्छी परफ़ॉर्मेंस पाने के लिए, यह ज़रूरी है कि सिस्टम के दूसरे हिस्सों के इंतज़ार में जीपीयू का कम से कम समय बर्बाद हो!

इसके बजाय, इंस्टैंस बनाने की तकनीक का इस्तेमाल किया जा सकता है. इंस्टेंसिंग का इस्तेमाल करके, जीपीयू को एक ही ज्यामिति की कई कॉपी बनाने के लिए कहा जा सकता है. इसके लिए, draw को सिर्फ़ एक बार कॉल करना होता है. यह हर कॉपी के लिए draw को एक बार कॉल करने से काफ़ी तेज़ होता है. ज्यामिति की हर कॉपी को इंस्टेंस कहा जाता है.

  1. जीपीयू को यह बताने के लिए कि आपको ग्रिड भरने के लिए अपने स्क्वेयर के ज़रूरत के मुताबिक इंस्टेंस चाहिए, अपने मौजूदा ड्रॉ कॉल में एक आर्ग्युमेंट जोड़ें:

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 कहा जा सकता है.) इसके बाद, इसे शेडर लॉजिक के हिस्से के तौर पर इस्तेमाल करें!

  1. सेल के निर्देशांक के बजाय 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 को इस तरह बदलना होगा कि हर इंडेक्स आपके ग्रिड में मौजूद किसी यूनीक सेल से मैप हो, जैसे:

कैनवस का विज़ुअलाइज़ेशन, जिसे कॉन्सेप्ट के हिसाब से 4x4 ग्रिड में बांटा गया है. साथ ही, हर सेल एक लीनियर इंस्टेंस इंडेक्स से भी जुड़ी है.

इसका हिसाब लगाना आसान है. आपको हर सेल की X वैल्यू के लिए, instance_index और ग्रिड की चौड़ाई का मॉड्यूलो चाहिए. इसे WGSL में % ऑपरेटर की मदद से किया जा सकता है. साथ ही, आपको हर सेल की Y वैल्यू के लिए, instance_index को ग्रिड की चौड़ाई से भाग देना है. साथ ही, बचे हुए हिस्से को हटाना है. WGSL के floor() फ़ंक्शन का इस्तेमाल करके ऐसा किया जा सकता है.

  1. कैलकुलेशन में बदलाव करें, जैसे कि:

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

कोड में यह अपडेट करने के बाद, आपको आखिरकार स्क्वेयर का ग्रिड मिल जाएगा!

गहरे नीले रंग के बैकग्राउंड पर, लाल रंग के चौकोरों की चार पंक्तियां और चार कॉलम.

  1. अब जब यह काम कर रहा है, तो वापस जाकर ग्रिड का साइज़ बढ़ाएं!

index.html

const GRID_SIZE = 32;

गहरे नीले रंग के बैकग्राउंड पर, लाल रंग के स्क्वेयर की 32 पंक्तियां और 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 फ़ंक्शन में सेट करें.

  1. अपने वर्टिक्स शेडर की रिटर्न वैल्यू को इस तरह बदलें:

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;
}
  1. @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);
}
  1. इसके अलावा, इसके बजाय किसी स्ट्रक्चर का इस्तेमाल किया जा सकता है:

index.html (createShaderModule कॉल)

struct FragInput {
  @location(0) cell: vec2f,
};

@fragment
fn fragmentMain(input: FragInput) -> @location(0) vec4f {
  return vec4f(input.cell, 0, 1);
}
  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 से फिर से भाग देना होगा!

  1. फ़्रेगमेंट शेडर को इस तरह बदलें:

index.html (createShaderModule कॉल)

@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
  return vec4f(input.cell/grid, 0, 1);
}

पेज को रीफ़्रेश करें और देखें कि नए कोड की मदद से, पूरे ग्रिड में रंगों का ग्रेडिएंट बेहतर दिखता है.

स्क्वेयर का ग्रिड, जो अलग-अलग कोनों में काले से लाल, हरे, और पीले रंग में बदलता है.

हालांकि, यह एक बेहतर बदलाव है, लेकिन अब सबसे नीचे बाईं ओर एक गहरा कोना है, जहां ग्रिड का रंग काला हो जाता है. गेम ऑफ़ लाइफ़ सिम्युलेशन शुरू करने पर, ग्रिड का एक ऐसा सेक्शन दिखेगा जिसे देखना मुश्किल होगा. इसे ज़्यादा रोशन किया जा सकता है.

अच्छी बात यह है कि आपके पास एक पूरा इस्तेमाल न किया गया कलर चैनल है, जिसका इस्तेमाल किया जा सकता है. आपको यह इफ़ेक्ट चाहिए कि जहां दूसरे रंग सबसे गहरे हों वहां नीला रंग सबसे ज़्यादा चमकता हो. साथ ही, दूसरे रंगों के गहरे होने पर नीला रंग धीरे-धीरे फीका हो जाए. ऐसा करने का सबसे आसान तरीका यह है कि चैनल को 1 से शुरू करें और सेल की किसी एक वैल्यू को घटाएं. यह c.x या c.y हो सकता है. दोनों को आज़माएं और फिर अपनी पसंद का विकल्प चुनें!

  1. फ़्रैगमेंट शेडर में ज़्यादा चमकदार रंग जोड़ें, जैसे:

createShaderModule कॉल

@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
  let c = input.cell / grid;
  return vec4f(c, 1-c.x, 1);
}

नतीजा काफ़ी अच्छा लग रहा है!

स्क्वेयर का ग्रिड, जो अलग-अलग कोनों में लाल से हरे, नीले से पीले रंग में बदलता है.

यह ज़रूरी चरण नहीं है! हालांकि, यह बेहतर दिखता है, इसलिए इसे उससे जुड़ी चेकपॉइंट सोर्स फ़ाइल में शामिल किया गया है. साथ ही, इस कोडलैब के बाकी स्क्रीनशॉट में इस ज़्यादा रंगीन ग्रिड को दिखाया गया है.

7. सेल की स्थिति मैनेज करना

इसके बाद, आपको यह कंट्रोल करना होगा कि जीपीयू पर सेव की गई किसी स्थिति के आधार पर, ग्रिड की कौनसी सेल रेंडर होंगी. यह आखिरी सिम्युलेशन के लिए ज़रूरी है!

आपको हर सेल के लिए सिर्फ़ एक चालू-बंद सिग्नल चाहिए. इसलिए, ऐसे सभी विकल्प काम करते हैं जिनकी मदद से, किसी भी तरह की वैल्यू का बड़ा कलेक्शन सेव किया जा सकता है. आपको ऐसा लग सकता है कि यह यूनिफ़ॉर्म बफ़र के इस्तेमाल का एक और उदाहरण है! ऐसा किया जा सकता है, लेकिन यह ज़्यादा मुश्किल है. इसकी वजह यह है कि यूनिफ़ॉर्म बफ़र का साइज़ सीमित होता है. साथ ही, डाइनैमिक साइज़ वाली ऐरे के साथ काम नहीं कर सकते. आपको शेडर में ऐरे का साइज़ बताना होगा. इसके अलावा, कंप्यूट शेडर में इनमें डेटा नहीं लिखा जा सकता. आखिरी आइटम सबसे समस्याजनक है, क्योंकि आपको कंप्यूट शेडर में जीपीयू पर लाइफ़ ऑफ़ गेम सिम्युलेशन करना है.

हालांकि, बफ़र करने का एक और विकल्प है, जो इन सभी सीमाओं से बचता है.

स्टोरेज बफ़र बनाना

स्टोरेज बफ़र, सामान्य तौर पर इस्तेमाल होने वाले बफ़र होते हैं. इन्हें कंप्यूट शेडर में पढ़ा और लिखा जा सकता है. साथ ही, इन्हें वर्टिक्स शेडर में भी पढ़ा जा सकता है. ये बहुत बड़े हो सकते हैं. साथ ही, उन्हें शेडर में किसी खास साइज़ की ज़रूरत नहीं होती. इस वजह से, ये सामान्य मेमोरी की तरह ही काम करते हैं. सेल की स्थिति सेव करने के लिए, इसका इस्तेमाल किया जाता है.

  1. अपनी सेल स्टेटस के लिए स्टोरेज बफ़र बनाने के लिए, बफ़र बनाने वाले कोड के उस स्निपेट का इस्तेमाल करें जो अब तक शायद आपको जाना-पहचाना लगने लगा है:

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() को कॉल करें. आपको ग्रिड पर बफ़र का असर देखना है, इसलिए इसे किसी ऐसे डेटा से भरें जिसका अनुमान लगाया जा सकता हो.

  1. इस कोड की मदद से, हर तीसरी सेल को चालू करें:

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

शेडर में स्टोरेज बफ़र को पढ़ना

इसके बाद, ग्रिड को रेंडर करने से पहले, स्टोरेज बफ़र का कॉन्टेंट देखने के लिए अपना शेडर अपडेट करें. यह तरीका, यूनिफ़ॉर्म जोड़ने के पिछले तरीके से काफ़ी मिलता-जुलता है.

  1. अपने शेडर को इस कोड से अपडेट करें:

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 से स्केल करने पर, ज्यॉमेट्री एक पॉइंट में सिमट जाती है. इसके बाद, जीपीयू इसे खारिज कर देता है.

  1. सेल की चालू स्थिति के हिसाब से पोज़िशन को स्केल करने के लिए, अपना शेडर कोड अपडेट करें. 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);
  1. दो एक जैसे बफ़र बनाने के लिए, अपने स्टोरेज बफ़र के ऐलोकेशन को अपडेट करके, अपने कोड में इस पैटर्न का इस्तेमाल करें:

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,
  })
];
  1. दो बफ़र के बीच के अंतर को विज़ुअलाइज़ करने के लिए, उन्हें अलग-अलग डेटा से भरें:

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);
  1. रेंडरिंग में अलग-अलग स्टोरेज बफ़र दिखाने के लिए, अपने बाइंड ग्रुप को अपडेट करें, ताकि दो अलग-अलग वैरिएंट हों:

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 बार.

यह ऐप्लिकेशन, उस डेटा का भी इस्तेमाल कर सकता है. हालांकि, इस मामले में, आपको शायद अपडेट ज़्यादा समय के अंतराल पर चाहिए, ताकि आप आसानी से यह समझ सकें कि सिम्युलेशन क्या कर रहा है. इसके बजाय, लूप को खुद मैनेज करें, ताकि आप यह कंट्रोल कर सकें कि आपका सिम्युलेशन किस दर से अपडेट हो.

  1. सबसे पहले, सिम्युलेशन के अपडेट होने की दर चुनें. 200 मिलीसेकंड एक अच्छी दर है, लेकिन आपके पास इसे धीमा या तेज़ करने का विकल्प भी है. इसके बाद, यह ट्रैक करें कि सिम्युलेशन के कितने चरण पूरे हो चुके हैं.

index.html

const UPDATE_INTERVAL = 200; // Update every 200ms (5 times/sec)
let step = 0; // Track how many simulation steps have been run
  1. इसके बाद, रेंडरिंग के लिए फ़िलहाल इस्तेमाल किए जा रहे सभी कोड को नए फ़ंक्शन में ले जाएं. 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 एट्रिब्यूट के साथ मार्क करना होगा.

  1. इस कोड की मदद से, कंप्यूट शेडर बनाएं:

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 कोड में ट्रैक करने के लिए मददगार है.

  1. अपने वर्कग्रुप के साइज़ के लिए एक कॉन्स्टेंट तय करें, जैसे कि:

index.html

const WORKGROUP_SIZE = 8;

आपको शेडर फ़ंक्शन में भी वर्कग्रुप का साइज़ जोड़ना होगा. इसके लिए, JavaScript के टेंप्लेट लिटरल का इस्तेमाल करें, ताकि आपने जो कॉन्स्टेंट तय किया है उसका आसानी से इस्तेमाल किया जा सके.

  1. शेडर फ़ंक्शन में वर्कग्रुप का साइज़ जोड़ें, जैसे कि:

index.html (Compute createShaderModule कॉल)

@compute
@workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}) // New line
fn
computeMain() {

}

इससे शेडर को पता चलता है कि इस फ़ंक्शन से किया गया काम, (8 x 8 x 1) ग्रुप में किया जाता है. (अगर आपने कोई ऐक्सिस नहीं चुना है, तो उसकी वैल्यू डिफ़ॉल्ट रूप से 1 पर सेट हो जाएगी. हालांकि, आपको कम से कम X ऐक्सिस की वैल्यू तय करनी होगी.)

अन्य शेडर स्टेज की तरह ही, @builtin की कई वैल्यू हैं जिन्हें अपने कंप्यूट शेडर फ़ंक्शन में इनपुट के तौर पर स्वीकार किया जा सकता है. इससे आपको यह पता चलता है कि आप किस इनवोकेशन पर हैं और आपको क्या काम करना है.

  1. @builtin वैल्यू जोड़ें, जैसे:

index.html (Compute createShaderModule call)

@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) तक की संख्याएं मिलती हैं. इसका मतलब है कि इसे उस सेल इंडेक्स के तौर पर इस्तेमाल किया जा सकता है जिस पर आपको काम करना है!

कंप्यूट शेडर में यूनिफ़ॉर्म का इस्तेमाल भी किया जा सकता है. इनका इस्तेमाल, वर्टिक्स और फ़्रैगमेंट शेडर में किया जाता है.

  1. ग्रिड का साइज़ बताने के लिए, अपने कंप्यूट शेडर के साथ यूनिफ़ॉर्म का इस्तेमाल करें. जैसे:

index.html (Compute createShaderModule call)

@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) {

}

वेरटेक्स शेडर की तरह ही, सेल स्टेटस को स्टोरेज बफ़र के तौर पर भी दिखाया जाता है. हालांकि, इस मामले में आपको दो की ज़रूरत होगी! कंप्यूट शेडर में कोई ज़रूरी आउटपुट नहीं होता, जैसे कि वर्टिक्स पोज़िशन या फ़्रैगमेंट कलर. इसलिए, कंप्यूट शेडर से नतीजे पाने का एक ही तरीका है, स्टोरेज बफ़र या टेक्सचर में वैल्यू लिखना. पहले सीखे गए पिंग-पॉंग तरीके का इस्तेमाल करें. आपके पास एक स्टोरेज बफ़र है, जो ग्रिड की मौजूदा स्थिति को फ़ीड करता है और एक ऐसा बफ़र है जिसमें ग्रिड की नई स्थिति को लिखा जाता है.

  1. सेल के इनपुट और आउटपुट स्टेटस को स्टोरेज बफ़र के तौर पर दिखाएं, जैसे:

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)) था.)

  1. दूसरी दिशा में जाने के लिए फ़ंक्शन लिखें. यह सेल की Y वैल्यू को ग्रिड की चौड़ाई से गुणा करता है. इसके बाद, सेल की X वैल्यू को जोड़ता है.

index.html (Compute createShaderModule call)

@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) {
 
}

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

  1. आसान एल्गोरिदम जोड़ें, जैसे:

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 वैल्यू का इस्तेमाल करते हैं. इसलिए, उन्हें पाइपलाइन के बीच शेयर किया जा सकता है. साथ ही, रेंडर पाइपलाइन दूसरे स्टोरेज बफ़र को अनदेखा करती है, जिसका इस्तेमाल नहीं किया जाता. आपको एक ऐसा लेआउट बनाना है जिसमें बाइंड ग्रुप में मौजूद सभी संसाधनों के बारे में जानकारी हो, न कि सिर्फ़ उन संसाधनों के बारे में जिनका इस्तेमाल किसी खास पाइपलाइन ने किया है.

  1. यह लेआउट बनाने के लिए, 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 बन जाने के बाद, बाइंड ग्रुप बनाते समय उसे पास किया जा सकता है. इसके लिए, आपको पाइपलाइन से बाइंड ग्रुप की क्वेरी करने की ज़रूरत नहीं है. ऐसा करने का मतलब है कि आपको हर बाइंड ग्रुप में एक नया स्टोरेज बफ़र एंट्री जोड़नी होगी, ताकि आपने जो लेआउट तय किया है उससे मैच किया जा सके.

  1. बाइंड ग्रुप बनाने की प्रोसेस को इस तरह अपडेट करें:

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

अब जब बाइंड ग्रुप को साफ़ तौर पर बाइंड ग्रुप लेआउट का इस्तेमाल करने के लिए अपडेट कर दिया गया है, तो आपको उसी का इस्तेमाल करने के लिए रेंडर पाइपलाइन को अपडेट करना होगा.

  1. GPUPipelineLayout बनाएं.

index.html

const pipelineLayout = device.createPipelineLayout({
  label: "Cell Pipeline Layout",
  bindGroupLayouts: [ bindGroupLayout ],
});

पाइपलाइन लेआउट, बाइंड ग्रुप लेआउट की सूची होती है. इस मामले में, आपके पास एक लेआउट है. इसका इस्तेमाल एक या उससे ज़्यादा पाइपलाइन करती हैं. ऐरे में बाइंड ग्रुप लेआउट का क्रम, शेडर में मौजूद @group एट्रिब्यूट से मेल खाना चाहिए. इसका मतलब है कि bindGroupLayout, @group(0) से जुड़ा है.

  1. पाइपलाइन लेआउट मिलने के बाद, "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 फ़ंक्शन में थोड़ा बदलाव करना होगा.

  1. एन्कोडर बनाने की प्रोसेस को फ़ंक्शन में सबसे ऊपर ले जाएं. इसके बाद, 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 की संख्या बढ़ाई जाती है, ताकि कैलकुलेट पाइपलाइन का आउटपुट बफ़र, रेंडर पाइपलाइन के लिए इनपुट बफ़र बन जाए.

  1. इसके बाद, कंप्यूट पास में पाइपलाइन और बाइंड ग्रुप सेट करें. बाइंड ग्रुप के बीच स्विच करने के लिए, रेंडरिंग पास में इस्तेमाल किए गए पैटर्न का इस्तेमाल करें.

index.html

const computePass = encoder.beginComputePass();

// New lines
computePass.setPipeline(simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);

computePass.end();
  1. आखिर में, रेंडर पास की तरह ड्रॉ करने के बजाय, काम को कंप्यूट शेडर पर भेजा जाता है. साथ ही, यह भी बताया जाता है कि आपको हर ऐक्सिस पर कितने वर्कग्रुप चलाने हैं.

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() में डालें.

अब पेज को फिर से रीफ़्रेश करें. आपको हर अपडेट के साथ ग्रिड अपने-आप इनवर्ट होता हुआ दिखेगा.

गहरे नीले रंग के बैकग्राउंड में, रंग-बिरंगे स्क्वेयर की डायगनल स्ट्राइप, सबसे नीचे बाईं ओर से सबसे ऊपर दाईं ओर जा रही हैं. गहरे नीले रंग के बैकग्राउंड में, नीचे बाईं ओर से ऊपर दाईं ओर तक, दो वर्ग चौड़ी तिरछी धारियां, जिनमें रंग-बिरंगे वर्ग हैं. पिछली इमेज को उलटा किया गया.

लाइफ़ ऑफ़ गेम के लिए एल्गोरिदम लागू करना

आखिरी एल्गोरिदम लागू करने के लिए, कंप्यूट शेडर को अपडेट करने से पहले, आपको उस कोड पर वापस जाना होगा जो स्टोरेज बफ़र कॉन्टेंट को शुरू कर रहा है. साथ ही, उसे अपडेट करके हर पेज लोड होने पर एक रैंडम बफ़र जनरेट करना होगा. (सामान्य पैटर्न, लाइफ़ ऑफ़ गेम के शुरुआती पॉइंट के तौर पर बहुत दिलचस्प नहीं होते.) वैल्यू को अपनी पसंद के मुताबिक रैंडम किया जा सकता है. हालांकि, इसे शुरू करने का एक आसान तरीका है, जिससे आपको बेहतर नतीजे मिलते हैं.

  1. हर सेल को किसी भी स्थिति में शुरू करने के लिए, 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);

अब आपके पास, लाइफ़ ऑफ़ गेम सिम्युलेशन के लिए लॉजिक लागू करने का विकल्प है. यहां तक पहुंचने के लिए, आपको जो भी करना पड़ा हो, शायद आपको शेडर कोड बहुत आसान लगे!

सबसे पहले, आपको यह जानना होगा कि किसी सेल के आस-पास कितनी सेल चालू हैं. आपको यह नहीं देखना है कि कौनसे खाते चालू हैं, सिर्फ़ उनकी संख्या देखनी है.

  1. आस-पास की सेल का डेटा आसानी से पाने के लिए, cellActive फ़ंक्शन जोड़ें. यह फ़ंक्शन, दिए गए निर्देशांक की cellStateIn वैल्यू दिखाता है.

index.html (Compute createShaderModule कॉल)

fn cellActive(x: u32, y: u32) -> u32 {
  return cellStateIn[cellIndex(vec2(x, y))];
}

अगर सेल चालू है, तो cellActive फ़ंक्शन एक वैल्यू दिखाता है. इसलिए, आस-पास की सभी आठ सेल के लिए cellActive फ़ंक्शन की रिटर्न वैल्यू जोड़ने से, आपको यह पता चलता है कि आस-पास की कितनी सेल चालू हैं.

  1. सक्रिय पड़ोसियों की संख्या देखें. इसके लिए, यह तरीका अपनाएं:

index.html (Compute createShaderModule call)

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() लॉजिक के मुताबिक, यह या तो अगली या पिछली पंक्ति में ओवरफ़्लो हो जाता है या बफ़र के किनारे तक चला जाता है!

गेम ऑफ़ लाइफ़ में, इस समस्या को हल करने का एक सामान्य और आसान तरीका यह है कि ग्रिड के किनारे की सेल, ग्रिड के दूसरी ओर मौजूद सेल को अपने पड़ोसी के तौर पर इस्तेमाल करें. इससे, एक तरह का रैप-अराउंड इफ़ेक्ट बनता है.

  1. 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 में switch स्टेटमेंट भी काम करते हैं, जो इस लॉजिक के लिए सही हैं.

  1. इस तरह से, लाइफ़ ऑफ़ गेम लॉजिक लागू करें:

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

और... बस इतना ही! आपका काम पूरा हुआ! अपना पेज रीफ़्रेश करें और अपने नए सेल्युलर ऑटोमेटॉन को बढ़ते हुए देखें!

&#39;लाइफ़ ऑफ़ गेम&#39; सिम्युलेशन के किसी स्टेटस का स्क्रीनशॉट. इसमें गहरे नीले रंग के बैकग्राउंड में रंग-बिरंगी सेल दिख रही हैं.

9. बधाई हो!

आपने Conway's Game of Life सिम्युलेशन का एक ऐसा वर्शन बनाया है जो पूरी तरह से आपके जीपीयू पर चलता है. इसके लिए, WebGPU API का इस्तेमाल किया जाता है!

आगे क्या करना है?

इसके बारे में और पढ़ें

रेफ़रंस दस्तावेज़