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