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

1. परिचय

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

WebGPU क्या है?

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

Modern API

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

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

रेंडरिंग

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

Compute

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

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

आपको क्या बनाने को मिलेगा

इस कोडलैब में, WebGPU का इस्तेमाल करके कॉनवे का गेम ऑफ़ लाइफ़ बनाया गया है. आपका ऐप्लिकेशन:

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

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

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

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

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

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

आपको इन चीज़ों की ज़रूरत होगी

  • ChromeOS, macOS या Windows पर Chrome का नया वर्शन (113 या इसके बाद का वर्शन). WebGPU, अलग-अलग ब्राउज़र और प्लैटफ़ॉर्म पर काम करने वाला एपीआई है. हालांकि, यह अभी हर जगह उपलब्ध नहीं है.
  • एचटीएमएल, JavaScript, और Chrome DevTools के बारे में जानकारी.

WebGL, Metal, Vulkan या Direct3D जैसे अन्य Graphics API के बारे में जानकारी होना ज़रूरी नहीं है. हालांकि, अगर आपको इनके बारे में जानकारी है, तो आपको WebGPU में कई समानताएं दिखेंगी. इससे आपको WebGPU के बारे में जानने में मदद मिल सकती है!

2. सेट अप करें

कोड पाएं

इस कोडलैब में कोई भी डिपेंडेंसी नहीं है. इसमें WebGPU ऐप्लिकेशन बनाने के लिए ज़रूरी हर चरण के बारे में बताया गया है. इसलिए, शुरू करने के लिए आपको किसी कोड की ज़रूरत नहीं है. हालांकि, कुछ काम करने वाले उदाहरण https://github.com/GoogleChromeLabs/your-first-webgpu-app-codelab पर उपलब्ध हैं. इनका इस्तेमाल चेकपॉइंट के तौर पर किया जा सकता है. अगर आपको कोई समस्या आती है, तो इन्हें देखा जा सकता है और इनका इस्तेमाल किया जा सकता है.

Developer Console का इस्तेमाल करें!

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

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

किसी भी वेब ऐप्लिकेशन पर काम करते समय, कंसोल को खुला रखना हमेशा फ़ायदेमंद होता है. हालांकि, यह खास तौर पर यहां लागू होता है!

3. WebGPU शुरू करना

<canvas> से शुरू करें

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

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

  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 को कॉन्फ़िगर करना

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

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

index.html

const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
  device: device,
  format: canvasFormat,
});

यहां कुछ विकल्प पास किए जा सकते हैं. हालांकि, सबसे अहम विकल्प device है. इसका इस्तेमाल कॉन्टेक्स्ट के साथ किया जाता है. इसके अलावा, format भी एक अहम विकल्प है. यह टेक्सचर फ़ॉर्मैट है, जिसका इस्तेमाल कॉन्टेक्स्ट को करना चाहिए.

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

खुशी की बात यह है कि आपको इसके बारे में ज़्यादा चिंता करने की ज़रूरत नहीं है, क्योंकि WebGPU आपको बताता है कि आपको अपने कैनवस के लिए किस फ़ॉर्मैट का इस्तेमाल करना चाहिए! ज़्यादातर मामलों में, आपको ऊपर दिखाए गए तरीके से navigator.gpu.getPreferredCanvasFormat() को कॉल करके मिली वैल्यू को पास करना होता है.

कैनवस से ड्रॉइंग मिटाना

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

ऐसा करने के लिए या WebGPU में कोई भी अन्य काम करने के लिए, आपको जीपीयू को कुछ निर्देश देने होंगे. इन निर्देशों से जीपीयू को यह पता चलेगा कि उसे क्या करना है.

  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 प्रॉपर्टी के तौर पर दी जाती है. रेंडर पास के लिए, आपको GPUTextureView के बजाय GPUTexture देना होगा. इससे यह पता चलता है कि टेक्सचर के किन हिस्सों को रेंडर करना है. यह सिर्फ़ ज़्यादा बेहतर इस्तेमाल के उदाहरणों के लिए ज़रूरी है. इसलिए, यहां टेक्सचर पर कोई तर्क दिए बिना createView() को कॉल किया जाता है. इससे पता चलता है कि आपको रेंडर पास के लिए पूरे टेक्सचर का इस्तेमाल करना है.

आपको यह भी बताना होगा कि रेंडर पास, टेक्सचर के साथ क्या करे, जब वह शुरू हो और जब वह खत्म हो:

  • loadOp की वैल्यू "clear" का मतलब है कि रेंडर पास शुरू होने पर, आपको टेक्सचर को मिटाना है.
  • "store" की storeOp वैल्यू से पता चलता है कि रेंडर पास पूरा होने के बाद, आपको रेंडर पास के दौरान की गई किसी भी ड्रॉइंग के नतीजे को टेक्सचर में सेव करना है.

रेंडर पास शुरू होने के बाद, आपको कुछ नहीं करना है! कम से कम अभी के लिए. 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 जैसे एपीआई का इस्तेमाल किया जा रहा है, तो ऐसा होना सामान्य है. इसका मतलब है कि आपको कुछ ज़्यादा मुश्किल काम करने हैं.

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

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

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

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

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

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

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

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

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

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

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

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

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

उन कोऑर्डिनेट को जीपीयू में भेजने के लिए, आपको वैल्यू को TypedArray में रखना होगा. अगर आपको इसके बारे में पहले से जानकारी नहीं है, तो हम आपको बता दें कि TypedArrays, JavaScript ऑब्जेक्ट का एक ग्रुप है. इसकी मदद से, मेमोरी के लगातार ब्लॉक असाइन किए जा सकते हैं. साथ ही, सीरीज़ में मौजूद हर एलिमेंट को किसी खास डेटा टाइप के तौर पर इंटरप्रेट किया जा सकता है. उदाहरण के लिए, Uint8Array में, कलेक्शन का हर एलिमेंट एक सिंगल, अनसाइंड बाइट होता है. टाइप्ड ऐरे, एपीआई के साथ डेटा भेजने और पाने के लिए बहुत अच्छे होते हैं. ये एपीआई, मेमोरी लेआउट के लिए संवेदनशील होते हैं. जैसे, 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,
]);

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

वर्टेक्स बफ़र बनाना

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

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

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

index.html

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

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

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

  • GPUVertexBufferLayout डिक्शनरी का इस्तेमाल करके, वर्टेक्स डेटा स्ट्रक्चर तय करें:

index.html

const vertexBufferLayout = {
  arrayStride: 8,
  attributes: [{
    format: "float32x2",
    offset: 0,
    shaderLocation: 0, // Position, see vertex shader
  }],
};

पहली नज़र में यह थोड़ा मुश्किल लग सकता है, लेकिन इसे समझना काफ़ी आसान है.

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

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

अपने एक एट्रिब्यूट में, सबसे पहले डेटा का format तय करें. यह GPUVertexFormat टाइप की सूची से मिलता है. इसमें हर तरह के वर्टेक्स डेटा के बारे में बताया गया है, जिसे जीपीयू समझ सकता है. आपके वर्टेक्स में हर एक के लिए दो 32-बिट फ़्लोट हैं. इसलिए, float32x2 फ़ॉर्मैट का इस्तेमाल करें. अगर आपका वर्टेक्स डेटा, चार 16-बिट के बिना हस्ताक्षर वाले पूर्णांकों से बना है, तो आपको uint16x4 का इस्तेमाल करना होगा. क्या आपको कोई पैटर्न दिख रहा है?

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

आखिर में, आपके पास shaderLocation है. यह 0 से 15 के बीच की कोई भी संख्या हो सकती है. साथ ही, यह आपके तय किए गए हर एट्रिब्यूट के लिए यूनीक होनी चाहिए. यह एट्रिब्यूट, वर्टेक्स शेडर में किसी इनपुट से लिंक होता है. इसके बारे में अगले सेक्शन में बताया गया है.

ध्यान दें कि आपने इन वैल्यू को अभी तय किया है. हालांकि, आपने इन्हें WebGPU API में अभी तक कहीं भी पास नहीं किया है. यह आने वाला है, लेकिन वर्टेक्स तय करते समय इन वैल्यू के बारे में सोचना सबसे आसान होता है. इसलिए, इन्हें अभी सेट अप किया जा रहा है, ताकि बाद में इनका इस्तेमाल किया जा सके.

शेडर का इस्तेमाल शुरू करना

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

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

WebGPU में शेडर, WGSL (WebGPU Shading Language) नाम की शेडिंग लैंग्वेज में लिखे जाते हैं. सिंटैक्टिक तौर पर 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 attribute होना चाहिए, ताकि यह पता चल सके कि यह किस शेडर स्टेज को दिखाता है. 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) की स्टैटिक वैल्यू दिखाएं. आपके पास तकनीकी तौर पर एक मान्य वर्टेक्स शेडर है. हालांकि, यह कभी कुछ भी नहीं दिखाता, क्योंकि जीपीयू यह पहचान लेता है कि इससे बनने वाले ट्रायंगल सिर्फ़ एक पॉइंट हैं. इसके बाद, वह इसे खारिज कर देता है.

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

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

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

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

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

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

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 फ़ंक्शन हो सकते हैं!) buffers, 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. ग्रिड बनाना

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

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

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

ग्रिड तय करना

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

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

index.html

const GRID_SIZE = 4;

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

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

यूनिफ़ॉर्म बफ़र बनाना

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

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

  • नीचे दिया गया कोड जोड़कर, एक जैसा बफ़र बनाएं:

index.html

// Create a uniform buffer that describes the grid.
const uniformArray = new Float32Array([GRID_SIZE, GRID_SIZE]);
const uniformBuffer = device.createBuffer({
  label: "Grid Uniforms",
  size: uniformArray.byteLength,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformArray);

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

शेडर में यूनिफ़ॉर्म ऐक्सेस करना

  • नीचे दिया गया कोड जोड़कर, एक यूनिफ़ॉर्म तय करें:

index.html (createShaderModule कॉल)

// At the top of the `code` string in the createShaderModule() call
@group(0) @binding(0) var<uniform> grid: vec2f;

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

// ...fragmentMain is unchanged 

यह आपके शेडर में grid नाम का एक यूनिफ़ॉर्म तय करता है. यह एक 2D फ़्लोट वेक्टर है, जो उस ऐरे से मेल खाता है जिसे आपने अभी-अभी यूनिफ़ॉर्म बफ़र में कॉपी किया है. इससे यह भी पता चलता है कि यूनिफ़ॉर्म, @group(0) और @binding(0) पर बाउंड है. आपको कुछ ही देर में पता चल जाएगा कि इन वैल्यू का क्या मतलब है.

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

इसके बाद, कैनवस के कोऑर्डिनेट सिस्टम में (0, 0) को बीच में और (-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. अपने ऑफ़सेट को 2 से गुणा करें. जैसे:

index.html (createShaderModule कॉल)

@group(0) @binding(0) var<uniform> grid: vec2f;

@vertex
fn vertexMain(@location(0) pos: vec2f) ->
  @builtin(position) vec4f {

  let cell = vec2f(1, 1);
  let cellOffset = cell / grid * 2; // Updated
  let gridPos = (pos + 1) / grid - 1 + cellOffset;

  return vec4f(gridPos, 0, 1);
}

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

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

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

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

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

ड्रॉ इंस्टेंस

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

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

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

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

अब ऐप्लिकेशन चलाने पर, आपको दिखेगा कि कैनवस, आपके बनाए गए दो स्टेट बफ़र के बीच आगे-पीछे हो रहा है.

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

इसके बाद, रेंडरिंग से जुड़े काम लगभग पूरे हो जाते हैं! अब आपको अगले चरण में, Game of Life सिम्युलेशन का आउटपुट दिखाने के लिए तैयार रहना है. इस चरण में, आपको Compute Shader का इस्तेमाल शुरू करना होगा.

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

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

index.html

const WORKGROUP_SIZE = 8;

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

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

index.html (Compute createShaderModule call)

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

@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 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>;
   
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() में पास किया जाता है.

अब पेज को फिर से रीफ़्रेश करें. आपको दिखेगा कि हर अपडेट के साथ ग्रिड उलट जाती है.

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

Game of Life के लिए एल्गोरिदम लागू करना

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

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

अब आपके पास, Game of Life सिम्युलेशन के लिए लॉजिक लागू करने का विकल्प है. यहां तक पहुंचने के बाद, हो सकता है कि शेडर कोड बहुत आसान हो!

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

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

index.html (Compute createShaderModule call)

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

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. Game of Life का लॉजिक इस तरह लागू करें:

index.html (Compute createShaderModule call)

let i = cellIndex(cell.xy);

// Conway's game of life rules:
switch activeNeighbors {
  case 2: { // Active cells with 2 neighbors stay active.
    cellStateOut[i] = cellStateIn[i];
  }
  case 3: { // Cells with 3 neighbors become or stay active.
    cellStateOut[i] = 1;
  }
  default: { // Cells with < 2 or > 3 neighbors become inactive.
    cellStateOut[i] = 0;
  }
}

रेफ़रंस के लिए, फ़ाइनल कंप्यूट शेडर मॉड्यूल कॉल अब ऐसा दिखता है:

index.html

const simulationShaderModule = device.createShaderModule({
  label: "Life simulation shader",
  code: `
    @group(0) @binding(0) var<uniform> grid: vec2f;

    @group(0) @binding(1) var<storage> cellStateIn: array<u32>;
    @group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;

    fn cellIndex(cell: vec2u) -> u32 {
      return (cell.y % u32(grid.y)) * u32(grid.x) +
              (cell.x % u32(grid.x));
    }

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

    @compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
    fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
      // Determine how many active neighbors this cell has.
      let activeNeighbors = cellActive(cell.x+1, cell.y+1) +
                            cellActive(cell.x+1, cell.y) +
                            cellActive(cell.x+1, cell.y-1) +
                            cellActive(cell.x, cell.y-1) +
                            cellActive(cell.x-1, cell.y-1) +
                            cellActive(cell.x-1, cell.y) +
                            cellActive(cell.x-1, cell.y+1) +
                            cellActive(cell.x, cell.y+1);

      let i = cellIndex(cell.xy);

      // Conway's game of life rules:
      switch activeNeighbors {
        case 2: {
          cellStateOut[i] = cellStateIn[i];
        }
        case 3: {
          cellStateOut[i] = 1;
        }
        default: {
          cellStateOut[i] = 0;
        }
      }
    }
  `
});

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

गेम ऑफ़ लाइफ़ सिमुलेशन की उदाहरण स्थिति का स्क्रीनशॉट. इसमें गहरे नीले रंग के बैकग्राउंड पर, अलग-अलग रंगों की सेल दिख रही हैं.

9. बधाई हो!

आपने क्लासिक कॉन्वेज़ गेम ऑफ़ लाइफ़ सिमुलेशन का एक ऐसा वर्शन बनाया है जो WebGPU API का इस्तेमाल करके, पूरी तरह से आपके जीपीयू पर चलता है!

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

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

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