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