আপনার প্রথম WebGPU অ্যাপ

1. ভূমিকা

The WebGPU Logo consists of several blue triangles that form a stylized 'W'

WebGPU কি?

WebGPU হল ওয়েব অ্যাপে আপনার GPU-এর ক্ষমতা অ্যাক্সেস করার জন্য একটি নতুন, আধুনিক API।

আধুনিক API

WebGPU-এর আগে, WebGL ছিল, যা WebGPU-এর বৈশিষ্ট্যগুলির একটি উপসেট অফার করত। এটি সমৃদ্ধ ওয়েব সামগ্রীর একটি নতুন শ্রেণি সক্ষম করেছে এবং বিকাশকারীরা এটির সাথে আশ্চর্যজনক জিনিস তৈরি করেছে৷ যাইহোক, এটি 2007 সালে প্রকাশিত OpenGL ES 2.0 API-এর উপর ভিত্তি করে তৈরি করা হয়েছিল, যা আরও পুরানো OpenGL API-এর উপর ভিত্তি করে ছিল। সেই সময়ে GPU গুলি উল্লেখযোগ্যভাবে বিকশিত হয়েছে, এবং তাদের সাথে ইন্টারফেস করতে ব্যবহৃত নেটিভ APIগুলি Direct3D 12 , Metal , এবং Vulkan এর সাথেও বিবর্তিত হয়েছে।

WebGPU এই আধুনিক APIগুলির অগ্রগতিগুলিকে ওয়েব প্ল্যাটফর্মে নিয়ে আসে৷ এটি একটি ক্রস-প্ল্যাটফর্ম উপায়ে GPU বৈশিষ্ট্যগুলিকে সক্রিয় করার উপর ফোকাস করে, এমন একটি API উপস্থাপন করার সময় যা ওয়েবে প্রাকৃতিক মনে হয় এবং এটির উপরে নির্মিত কিছু নেটিভ APIগুলির তুলনায় কম শব্দভাষী।

রেন্ডারিং

GPU গুলি প্রায়শই দ্রুত, বিস্তারিত গ্রাফিক্স রেন্ডারিংয়ের সাথে যুক্ত থাকে এবং WebGPU এর ব্যতিক্রম নয়। এটিতে ডেস্কটপ এবং মোবাইল GPU উভয় জুড়ে আজকের সবচেয়ে জনপ্রিয় রেন্ডারিং কৌশলগুলিকে সমর্থন করার জন্য প্রয়োজনীয় বৈশিষ্ট্যগুলি রয়েছে এবং হার্ডওয়্যার ক্ষমতাগুলি ক্রমাগত বিকশিত হওয়ার সাথে সাথে ভবিষ্যতে নতুন বৈশিষ্ট্যগুলি যোগ করার জন্য একটি পথ সরবরাহ করে৷

গণনা

রেন্ডারিং ছাড়াও, WebGPU সাধারণ উদ্দেশ্য, অত্যন্ত সমান্তরাল ওয়ার্কলোডগুলি সম্পাদন করার জন্য আপনার GPU-এর সম্ভাব্যতা আনলক করে। এই কম্পিউট শেডারগুলি কোনও রেন্ডারিং উপাদান ছাড়াই, বা আপনার রেন্ডারিং পাইপলাইনের একটি শক্তভাবে সমন্বিত অংশ হিসাবে স্বতন্ত্রভাবে ব্যবহার করা যেতে পারে।

আজকের কোডল্যাবে আপনি শিখবেন কিভাবে WebGPU-এর রেন্ডারিং এবং কম্পিউট উভয় ক্ষমতারই সুবিধা নিতে হয় যাতে একটি সাধারণ পরিচায়ক প্রকল্প তৈরি করা যায়!

আপনি কি নির্মাণ করবেন

এই কোডল্যাবে, আপনি WebGPU ব্যবহার করে কনওয়ের গেম অফ লাইফ তৈরি করেন। আপনার অ্যাপ হবে:

  • সহজ 2D গ্রাফিক্স আঁকতে WebGPU এর রেন্ডারিং ক্ষমতা ব্যবহার করুন।
  • সিমুলেশন সঞ্চালনের জন্য WebGPU এর গণনা ক্ষমতা ব্যবহার করুন।

A screenshot of the final product of this codelab

গেম অফ লাইফ হল সেলুলার অটোমেটন হিসাবে পরিচিত, যেখানে কোষগুলির একটি গ্রিড কিছু নিয়মের উপর ভিত্তি করে সময়ের সাথে সাথে অবস্থার পরিবর্তন করে। গেম অফ লাইফ-এ কোষগুলি সক্রিয় বা নিষ্ক্রিয় হয়ে যায় তাদের প্রতিবেশী কোষগুলির কতগুলি সক্রিয় তার উপর নির্ভর করে, যা আকর্ষণীয় প্যাটার্নের দিকে নিয়ে যায় যা আপনি দেখার সাথে সাথে ওঠানামা করে৷

আপনি কি শিখবেন

  • কিভাবে WebGPU সেট আপ করবেন এবং একটি ক্যানভাস কনফিগার করবেন।
  • কিভাবে সহজ 2D জ্যামিতি আঁকতে হয়।
  • কী আঁকা হচ্ছে তা সংশোধন করার জন্য ভার্টেক্স এবং ফ্র্যাগমেন্ট শেডারগুলি কীভাবে ব্যবহার করবেন।
  • একটি সাধারণ সিমুলেশন সঞ্চালনের জন্য কিভাবে কম্পিউট শেডার ব্যবহার করবেন।

এই কোডল্যাব ওয়েবজিপিইউ-এর পিছনে মৌলিক ধারণাগুলি প্রবর্তনের উপর দৃষ্টি নিবদ্ধ করে। এটি API এর একটি ব্যাপক পর্যালোচনা করার উদ্দেশ্যে নয়, বা এটি 3D ম্যাট্রিক্স গণিতের মতো ঘন ঘন সম্পর্কিত বিষয়গুলিকে কভার করে না (বা প্রয়োজন)।

আপনি কি প্রয়োজন হবে

  • ChromeOS, macOS বা Windows-এ Chrome-এর সাম্প্রতিক সংস্করণ (113 বা তার পরে)। WebGPU একটি ক্রস-ব্রাউজার, ক্রস-প্ল্যাটফর্ম API কিন্তু এখনও সব জায়গায় পাঠানো হয়নি।
  • HTML, JavaScript এবং Chrome DevTools এর জ্ঞান।

অন্যান্য গ্রাফিক্স API, যেমন WebGL, Metal, Vulkan, বা Direct3D-এর সাথে পরিচিতি প্রয়োজন নেই , তবে আপনার যদি সেগুলির সাথে কোনো অভিজ্ঞতা থাকে তবে আপনি সম্ভবত WebGPU-এর সাথে প্রচুর মিল লক্ষ্য করবেন যা আপনার শেখার শুরু করতে সাহায্য করতে পারে!

2. সেট আপ করুন

কোড পান

এই কোডল্যাবের কোনো নির্ভরতা নেই, এবং এটি আপনাকে WebGPU অ্যাপ তৈরি করার জন্য প্রয়োজনীয় প্রতিটি ধাপে নিয়ে যায়, তাই শুরু করার জন্য আপনার কোনো কোডের প্রয়োজন নেই। যাইহোক, কিছু কাজের উদাহরণ যা চেকপয়েন্ট হিসাবে কাজ করতে পারে https://glitch.com/edit/#!/your-first-webgpu-app এ উপলব্ধ। আপনি সেগুলি পরীক্ষা করে দেখতে পারেন এবং আপনি আটকে গেলে সেগুলি উল্লেখ করতে পারেন৷

বিকাশকারী কনসোল ব্যবহার করুন!

WebGPU হল একটি মোটামুটি জটিল API যার অনেকগুলি নিয়ম রয়েছে যা যথাযথ ব্যবহার প্রয়োগ করে৷ আরও খারাপ, এপিআই কীভাবে কাজ করে, এটি অনেক ত্রুটির জন্য সাধারণ জাভাস্ক্রিপ্ট ব্যতিক্রম বাড়াতে পারে না, সমস্যাটি ঠিক কোথা থেকে আসছে তা চিহ্নিত করা কঠিন করে তোলে।

WebGPU এর সাথে বিকাশ করার সময় আপনি সমস্যার সম্মুখীন হবেন , বিশেষ করে একজন শিক্ষানবিস হিসাবে, এবং এটি ঠিক আছে! API-এর পিছনে থাকা বিকাশকারীরা GPU ডেভেলপমেন্টের সাথে কাজ করার চ্যালেঞ্জগুলি সম্পর্কে সচেতন, এবং আপনার ওয়েবজিপিইউ কোডের কারণে যেকোন সময় একটি ত্রুটি দেখা দেয় তা নিশ্চিত করার জন্য কঠোর পরিশ্রম করেছেন আপনি বিকাশকারী কনসোলে খুব বিশদ এবং সহায়ক বার্তাগুলি ফিরে পাবেন যা আপনাকে সনাক্ত করতে এবং ঠিক করতে সহায়তা করে। সমস্যা

যেকোন ওয়েব অ্যাপ্লিকেশনে কাজ করার সময় কনসোলটি খোলা রাখা সর্বদা দরকারী, তবে এটি এখানে বিশেষভাবে প্রযোজ্য!

3. WebGPU আরম্ভ করুন

একটি <canvas> দিয়ে শুরু করুন

ওয়েবজিপিইউ স্ক্রিনে কিছু না দেখিয়ে ব্যবহার করা যেতে পারে যদি আপনি এটিকে গণনা করার জন্য ব্যবহার করতে চান। কিন্তু আপনি যদি কিছু রেন্ডার করতে চান, যেমন আমরা কোডল্যাবে করতে যাচ্ছি, আপনার একটি ক্যানভাস দরকার। তাই এটি শুরু করার জন্য একটি ভাল জায়গা!

একটি নতুন এইচটিএমএল ডকুমেন্ট তৈরি করুন যেখানে একটি <canvas> উপাদান রয়েছে, সেইসাথে একটি <script> ট্যাগ যেখানে আমরা ক্যানভাস উপাদানটি জিজ্ঞাসা করি। (বা গ্লিচ থেকে 00-starter-page.html ব্যবহার করুন।)

  • নিম্নলিখিত কোড সহ একটি index.html ফাইল তৈরি করুন:

index.html

<!doctype html>

<html>
  <head>
    <meta charset="utf-8">
    <title>WebGPU Life</title>
  </head>
  <body>
    <canvas width="512" height="512"></canvas>
    <script type="module">
      const canvas = document.querySelector("canvas");

      // Your WebGPU code will begin here!
    </script>
  </body>
</html>

একটি অ্যাডাপ্টার এবং ডিভাইস অনুরোধ করুন

এখন আপনি WebGPU বিটগুলিতে যেতে পারেন! প্রথমত, আপনার বিবেচনা করা উচিত যে WebGPU-এর মতো APIগুলি সমগ্র ওয়েব ইকোসিস্টেম জুড়ে প্রচার করতে কিছুটা সময় নিতে পারে। ফলস্বরূপ, একটি ভাল প্রথম সতর্কতামূলক পদক্ষেপ হল ব্যবহারকারীর ব্রাউজার WebGPU ব্যবহার করতে পারে কিনা তা পরীক্ষা করা।

  1. navigator.gpu অবজেক্ট, যা WebGPU-এর প্রবেশ বিন্দু হিসাবে কাজ করে, বিদ্যমান কিনা তা পরীক্ষা করতে, নিম্নলিখিত কোড যোগ করুন:

index.html

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

আদর্শভাবে, আপনি ব্যবহারকারীকে জানাতে চান যদি WebGPU অনুপলব্ধ থাকে তাহলে পৃষ্ঠাটি এমন একটি মোডে পড়ে যা WebGPU ব্যবহার করে না। (হয়তো এটি এর পরিবর্তে WebGL ব্যবহার করতে পারে?) এই কোডল্যাবের উদ্দেশ্যে, যদিও, আপনি কোডটিকে আরও কার্যকর করা থেকে থামাতে একটি ত্রুটি নিক্ষেপ করেন।

একবার আপনি জানবেন যে WebGPU ব্রাউজার দ্বারা সমর্থিত, আপনার অ্যাপের জন্য WebGPU আরম্ভ করার প্রথম ধাপ হল একটি GPUAdapter অনুরোধ করা। আপনি আপনার ডিভাইসে GPU হার্ডওয়্যারের একটি নির্দিষ্ট অংশের WebGPU এর উপস্থাপনা হিসাবে একটি অ্যাডাপ্টারকে ভাবতে পারেন।

  1. একটি অ্যাডাপ্টার পেতে, navigator.gpu.requestAdapter() পদ্ধতি ব্যবহার করুন। এটি একটি প্রতিশ্রুতি ফেরত দেয়, তাই এটি await সাথে কল করা সবচেয়ে সুবিধাজনক।

index.html

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
  throw new Error("No appropriate GPUAdapter found.");
}

যদি কোন উপযুক্ত অ্যাডাপ্টার পাওয়া না যায়, প্রত্যাবর্তিত adapter মান null হতে পারে, তাই আপনি সেই সম্ভাবনাটি পরিচালনা করতে চান। এটি ঘটতে পারে যদি ব্যবহারকারীর ব্রাউজার WebGPU সমর্থন করে কিন্তু তাদের GPU হার্ডওয়্যারে WebGPU ব্যবহার করার জন্য প্রয়োজনীয় সমস্ত বৈশিষ্ট্য না থাকে।

বেশিরভাগ সময় ব্রাউজারকে একটি ডিফল্ট অ্যাডাপ্টার বাছাই করতে দেওয়া ঠিক আছে, যেমন আপনি এখানে করেন, তবে আরও উন্নত প্রয়োজনের জন্য এমন আর্গুমেন্ট রয়েছে যা requestAdapter()পাস করা যেতে পারে যা নির্দিষ্ট করে যে আপনি কম-পাওয়ার ব্যবহার করতে চান নাকি উচ্চ- একাধিক GPU সহ ডিভাইসে কর্মক্ষমতা হার্ডওয়্যার (যেমন কিছু ল্যাপটপ)।

আপনার একবার অ্যাডাপ্টার হয়ে গেলে, আপনি GPU এর সাথে কাজ শুরু করার আগে শেষ ধাপ হল একটি GPUDevice-এর অনুরোধ করা। ডিভাইসটি হল প্রধান ইন্টারফেস যার মাধ্যমে জিপিইউ-এর সাথে বেশিরভাগ মিথস্ক্রিয়া ঘটে।

  1. adapter.requestDevice() কল করে ডিভাইসটি পান, যা একটি প্রতিশ্রুতিও প্রদান করে।

index.html

const device = await adapter.requestDevice();

requestAdapter() এর মতই, নির্দিষ্ট হার্ডওয়্যার বৈশিষ্ট্যগুলি সক্রিয় করা বা উচ্চ সীমার অনুরোধ করার মতো আরও উন্নত ব্যবহারের জন্য এখানে পাস করা যেতে পারে এমন বিকল্প রয়েছে, তবে আপনার উদ্দেশ্যে ডিফল্টগুলি ঠিক কাজ করে।

ক্যানভাস কনফিগার করুন

এখন আপনার কাছে একটি ডিভাইস আছে, আপনি যদি পৃষ্ঠায় কিছু দেখানোর জন্য এটি ব্যবহার করতে চান তবে আরও একটি জিনিস করতে হবে: আপনি যে ডিভাইসটি তৈরি করেছেন তার সাথে ব্যবহার করার জন্য ক্যানভাস কনফিগার করুন৷

  • এটি করার জন্য, প্রথমে canvas.getContext("webgpu") কল করে ক্যানভাস থেকে একটি GPUCanvasContext অনুরোধ করুন। (এটি একই কল যা আপনি যথাক্রমে 2d এবং webgl প্রসঙ্গ প্রকার ব্যবহার করে ক্যানভাস 2D বা WebGL প্রসঙ্গ শুরু করতে ব্যবহার করবেন।) এটি যে context ফেরত দেয় তা অবশ্যই configure() পদ্ধতি ব্যবহার করে ডিভাইসের সাথে যুক্ত হতে হবে, যেমন তাই:

index.html

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

এখানে কিছু বিকল্প আছে যা পাস করা যেতে পারে, কিন্তু সবচেয়ে গুরুত্বপূর্ণ হল সেই device যা আপনি প্রসঙ্গটি ব্যবহার করতে যাচ্ছেন এবং format , যেটি টেক্সচার ফর্ম্যাট যা প্রসঙ্গটি ব্যবহার করা উচিত।

টেক্সচার হল সেই বস্তু যা WebGPU ইমেজ ডেটা সঞ্চয় করতে ব্যবহার করে, এবং প্রতিটি টেক্সচারের একটি ফর্ম্যাট থাকে যা GPU-কে জানতে দেয় কীভাবে সেই ডেটা মেমরিতে রাখা হয়। টেক্সচার মেমরি কীভাবে কাজ করে তার বিশদ বিবরণ এই কোডল্যাবের সুযোগের বাইরে। জানা গুরুত্বপূর্ণ বিষয় হল যে ক্যানভাস প্রসঙ্গটি আপনার কোডে আঁকার জন্য টেক্সচার প্রদান করে এবং আপনি যে বিন্যাসটি ব্যবহার করেন সেটি ক্যানভাস সেই চিত্রগুলিকে কতটা দক্ষতার সাথে দেখায় তার উপর প্রভাব ফেলতে পারে। বিভিন্ন টেক্সচার ফরম্যাট ব্যবহার করার সময় বিভিন্ন ধরনের ডিভাইস সর্বোত্তম কার্য সম্পাদন করে এবং আপনি যদি ডিভাইসের পছন্দের ফরম্যাট ব্যবহার না করেন তবে এটি পৃষ্ঠার অংশ হিসাবে চিত্রটি প্রদর্শিত হওয়ার আগে পর্দার পিছনে অতিরিক্ত মেমরির অনুলিপি ঘটতে পারে।

সৌভাগ্যবশত, আপনাকে এর কোনোটি নিয়ে খুব বেশি চিন্তা করতে হবে না কারণ WebGPU আপনাকে বলে যে আপনার ক্যানভাসের জন্য কোন ফর্ম্যাট ব্যবহার করতে হবে! প্রায় সব ক্ষেত্রে, আপনি navigator.gpu.getPreferredCanvasFormat() কল করে ফেরত দেওয়া মানটি পাস করতে চান, যেমন উপরে দেখানো হয়েছে।

ক্যানভাস সাফ করুন

এখন আপনার কাছে একটি ডিভাইস আছে এবং ক্যানভাস এটির সাথে কনফিগার করা হয়েছে, আপনি ক্যানভাসের বিষয়বস্তু পরিবর্তন করতে ডিভাইসটি ব্যবহার করা শুরু করতে পারেন। শুরু করতে, একটি কঠিন রঙ দিয়ে এটি পরিষ্কার করুন।

সেটি করার জন্য—বা WebGPU-তে আরও কিছু—আপনাকে GPU-কে কিছু নির্দেশ দিতে হবে যা করতে হবে।

  1. এটি করার জন্য, ডিভাইসটিকে একটি GPUCommandEncoder তৈরি করতে বলুন, যা GPU কমান্ড রেকর্ড করার জন্য একটি ইন্টারফেস প্রদান করে।

index.html

const encoder = device.createCommandEncoder();

আপনি জিপিইউতে যে কমান্ডগুলি পাঠাতে চান তা রেন্ডারিংয়ের সাথে সম্পর্কিত (এই ক্ষেত্রে, ক্যানভাস পরিষ্কার করা), তাই পরবর্তী পদক্ষেপটি হল একটি রেন্ডার পাস শুরু করতে encoder ব্যবহার করা।

রেন্ডার পাস হল যখন WebGPU-তে সমস্ত অঙ্কন অপারেশন হয়। প্রতিটি একটি beginRenderPass() কল দিয়ে শুরু হয়, যা টেক্সচারগুলিকে সংজ্ঞায়িত করে যা সঞ্চালিত যেকোনো অঙ্কন কমান্ডের আউটপুট গ্রহণ করে। আরো উন্নত ব্যবহার বিভিন্ন টেক্সচার প্রদান করতে পারে, যাকে সংযুক্তি বলা হয়, বিভিন্ন উদ্দেশ্য যেমন রেন্ডার করা জ্যামিতির গভীরতা সংরক্ষণ করা বা অ্যান্টিলিয়াসিং প্রদান করা। এই অ্যাপের জন্য, তবে, আপনার শুধুমাত্র একটি প্রয়োজন।

  1. context.getCurrentTexture() কল করে আপনার আগে তৈরি করা ক্যানভাস প্রসঙ্গ থেকে টেক্সচারটি পান, যা ক্যানভাসের width এবং height বৈশিষ্ট্যের সাথে মিলে একটি পিক্সেল প্রস্থ এবং উচ্চতা সহ একটি টেক্সচার প্রদান করে এবং আপনি যখন context.configure() কল করেন তখন নির্দিষ্ট করা format

index.html

const pass = encoder.beginRenderPass({
  colorAttachments: [{
     view: context.getCurrentTexture().createView(),
     loadOp: "clear",
     storeOp: "store",
  }]
});

টেক্সচারটি একটি colorAttachment view প্রোপার্টি হিসাবে দেওয়া হয় রেন্ডার পাসগুলির জন্য আপনাকে একটি GPUTexture এর পরিবর্তে একটি GPUTextureView প্রদান করতে হবে, যা এটিকে বলে যে টেক্সচারের কোন অংশগুলিকে রেন্ডার করতে হবে৷ এটি শুধুমাত্র আরও উন্নত ব্যবহারের ক্ষেত্রে গুরুত্বপূর্ণ, তাই এখানে আপনি টেক্সচারের উপর কোন আর্গুমেন্ট ছাড়াই createView() কল করুন, এটি নির্দেশ করে যে আপনি সম্পূর্ণ টেক্সচার ব্যবহার করতে রেন্ডার পাস চান।

টেক্সচারের সাথে রেন্ডার পাসটি কখন শুরু হয় এবং কখন শেষ হয় তাও আপনাকে নির্দিষ্ট করতে হবে:

  • "clear" এর একটি loadOp মান নির্দেশ করে যে রেন্ডার পাস শুরু হলে আপনি টেক্সচারটি সাফ করতে চান।
  • "store" এর একটি storeOp মান নির্দেশ করে যে রেন্ডার পাস শেষ হয়ে গেলে আপনি রেন্ডার পাসের সময় টেক্সচারে সংরক্ষিত যেকোনো অঙ্কনের ফলাফল চান।

একবার রেন্ডার পাস শুরু হলে আপনি কিছু করবেন না! অন্তত আপাতত। loadOp: "clear" যথেষ্ট।

  1. beginRenderPass() পরপরই নিম্নলিখিত কল যোগ করে রেন্ডার পাসটি শেষ করুন:

index.html

pass.end();

এটা জানা গুরুত্বপূর্ণ যে কেবল এই কলগুলি করার ফলে GPU আসলে কিছু করতে পারে না। তারা শুধু GPU-এর জন্য পরবর্তীতে করার জন্য কমান্ড রেকর্ড করছে।

  1. একটি GPUCommandBuffer তৈরি করতে, কমান্ড এনকোডারে finish() কল করুন। কমান্ড বাফার রেকর্ড করা কমান্ডের একটি অস্বচ্ছ হ্যান্ডেল।

index.html

const commandBuffer = encoder.finish();
  1. GPUDevice এর queue ব্যবহার করে GPU-তে কমান্ড বাফার জমা দিন। সারিটি সমস্ত GPU কমান্ড সঞ্চালন করে, নিশ্চিত করে যে তাদের কার্যকর করা ভালভাবে অর্ডার করা হয়েছে এবং সঠিকভাবে সিঙ্ক্রোনাইজ করা হয়েছে। সারির submit() পদ্ধতিটি কমান্ড বাফারগুলির একটি অ্যারে নেয়, যদিও এই ক্ষেত্রে আপনার কাছে শুধুমাত্র একটি আছে।

index.html

device.queue.submit([commandBuffer]);

একবার আপনি একটি কমান্ড বাফার জমা দিলে, এটি আবার ব্যবহার করা যাবে না, তাই এটি ধরে রাখার দরকার নেই। আপনি যদি আরও কমান্ড জমা দিতে চান তবে আপনাকে অন্য কমান্ড বাফার তৈরি করতে হবে। এই কারণেই এই কোডল্যাবের নমুনা পৃষ্ঠাগুলিতে এই দুটি ধাপকে একটিতে ভেঙে যাওয়া দেখতে মোটামুটি সাধারণ:

index.html

// Finish the command buffer and immediately submit it.
device.queue.submit([encoder.finish()]);

আপনি জিপিইউতে কমান্ড জমা দেওয়ার পরে, জাভাস্ক্রিপ্টকে ব্রাউজারে নিয়ন্ত্রণ ফিরিয়ে দিতে দিন। সেই সময়ে, ব্রাউজারটি দেখে যে আপনি প্রসঙ্গটির বর্তমান টেক্সচার পরিবর্তন করেছেন এবং সেই টেক্সচারটিকে একটি চিত্র হিসাবে প্রদর্শন করতে ক্যানভাস আপডেট করেছেন। আপনি যদি এর পরে আবার ক্যানভাস বিষয়বস্তু আপডেট করতে চান, তাহলে আপনাকে রেন্ডার পাসের জন্য একটি নতুন টেক্সচার পেতে context.getCurrentTexture() কে আবার কল করে একটি নতুন কমান্ড বাফার রেকর্ড করে জমা দিতে হবে।

  1. পৃষ্ঠাটি পুনরায় লোড করুন। লক্ষ্য করুন যে ক্যানভাস কালো দিয়ে ভরা। অভিনন্দন! এর মানে হল যে আপনি সফলভাবে আপনার প্রথম WebGPU অ্যাপ তৈরি করেছেন।

A black canvas that indicates that WebGPU has successfully been used to clear the canvas contents.

একটি রঙ চয়ন করুন!

সত্যি বলতে, যদিও, কালো স্কোয়ারগুলি বেশ বিরক্তিকর। তাই এটিকে কিছুটা ব্যক্তিগত করার জন্য পরবর্তী বিভাগে যাওয়ার আগে কিছুক্ষণ সময় নিন।

  1. encoder.beginRenderPass() কলে, colorAttachment এ একটি clearValue সহ একটি নতুন লাইন যোগ করুন, এইভাবে:

index.html

const pass = encoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    loadOp: "clear",
    clearValue: { r: 0, g: 0, b: 0.4, a: 1 }, // New line
    storeOp: "store",
  }],
});

clearValue রেন্ডার পাসকে নির্দেশ দেয় যে পাসের শুরুতে clear অপারেশন করার সময় এটি কোন রঙ ব্যবহার করা উচিত। এটিতে পাস করা অভিধানটিতে চারটি মান রয়েছে: লালের জন্য r , সবুজের জন্য g , নীলের জন্য b এবং a (স্বচ্ছতার জন্য)। প্রতিটি মান 0 থেকে 1 পর্যন্ত হতে পারে, এবং তারা একসাথে সেই রঙের চ্যানেলের মান বর্ণনা করে। যেমন:

  • { r: 1, g: 0, b: 0, a: 1 } উজ্জ্বল লাল।
  • { r: 1, g: 0, b: 1, a: 1 } উজ্জ্বল বেগুনি।
  • { r: 0, g: 0.3, b: 0, a: 1 } গাঢ় সবুজ।
  • { r: 0.5, g: 0.5, b: 0.5, a: 1 } হল মাঝারি ধূসর।
  • { r: 0, g: 0, b: 0, a: 0 } হল ডিফল্ট, স্বচ্ছ কালো।

এই কোডল্যাবের উদাহরণ কোড এবং স্ক্রিনশটগুলি একটি গাঢ় নীল ব্যবহার করে, তবে আপনি যে রঙ চান তা বেছে নিতে দ্বিধা বোধ করুন!

  1. একবার আপনি আপনার রঙ বাছাই করার পরে, পৃষ্ঠাটি পুনরায় লোড করুন। আপনি ক্যানভাসে আপনার নির্বাচিত রঙ দেখতে হবে.

A canvas cleared to a dark blue color to demonstrate how to change the default clear color.

4. জ্যামিতি আঁকুন

এই বিভাগের শেষে, আপনার অ্যাপ ক্যানভাসে কিছু সাধারণ জ্যামিতি আঁকবে: একটি রঙিন বর্গক্ষেত্র। এখন সতর্ক থাকুন যে এই ধরনের সাধারণ আউটপুটের জন্য এটি অনেক কাজের মতো মনে হবে, কিন্তু এর কারণ হল WebGPU অনেকগুলি জ্যামিতি খুব দক্ষতার সাথে রেন্ডার করার জন্য ডিজাইন করা হয়েছে। এই দক্ষতার একটি পার্শ্বপ্রতিক্রিয়া হল যে তুলনামূলকভাবে সহজ জিনিসগুলি করা অস্বাভাবিকভাবে কঠিন মনে হতে পারে, কিন্তু আপনি যদি WebGPU-এর মতো একটি API-তে ফিরে যান - আপনি একটু জটিল কিছু করতে চান।

GPU কিভাবে আঁকে তা বুঝুন

আর কোন কোড পরিবর্তন করার আগে, আপনি স্ক্রিনে যে আকারগুলি দেখতে পাচ্ছেন GPU কীভাবে তৈরি করে তার একটি খুব দ্রুত, সরলীকৃত, উচ্চ-স্তরের ওভারভিউ করা মূল্যবান। (জিপিইউ রেন্ডারিং কীভাবে কাজ করে তার মূল বিষয়গুলির সাথে আপনি যদি ইতিমধ্যেই পরিচিত হন তবে নির্দ্বিধায় সংজ্ঞায়িত শীর্ষ বিভাগে যান।)

ক্যানভাস 2D-এর মতো একটি API এর বিপরীতে যেখানে আপনার ব্যবহারের জন্য প্রচুর আকার এবং বিকল্প প্রস্তুত রয়েছে, আপনার GPU সত্যিই শুধুমাত্র কয়েকটি ভিন্ন ধরণের আকারের সাথে কাজ করে (বা আদিম যেমন সেগুলিকে WebGPU দ্বারা উল্লেখ করা হয়): পয়েন্ট, লাইন এবং ত্রিভুজ . এই কোডল্যাবের উদ্দেশ্যে আপনি শুধুমাত্র ত্রিভুজ ব্যবহার করবেন।

GPU গুলি প্রায় একচেটিয়াভাবে ত্রিভুজগুলির সাথে কাজ করে কারণ ত্রিভুজগুলির অনেকগুলি চমৎকার গাণিতিক বৈশিষ্ট্য রয়েছে যা তাদের একটি অনুমানযোগ্য এবং দক্ষ উপায়ে প্রক্রিয়া করা সহজ করে তোলে। আপনি GPU দিয়ে আঁকেন এমন প্রায় সবকিছুই GPU আঁকতে পারার আগে ত্রিভুজগুলিতে বিভক্ত হওয়া দরকার এবং সেই ত্রিভুজগুলিকে অবশ্যই তাদের কোণার বিন্দু দ্বারা সংজ্ঞায়িত করতে হবে।

এই বিন্দুগুলি, বা শীর্ষবিন্দুগুলি , X, Y, এবং (3D সামগ্রীর জন্য) Z মানগুলির পরিপ্রেক্ষিতে দেওয়া হয় যা WebGPU বা অনুরূপ API দ্বারা সংজ্ঞায়িত কার্টেসিয়ান স্থানাঙ্ক সিস্টেমের একটি বিন্দুকে সংজ্ঞায়িত করে৷ আপনার পৃষ্ঠার ক্যানভাসের সাথে এটি কীভাবে সম্পর্কিত তার পরিপ্রেক্ষিতে স্থানাঙ্ক সিস্টেমের গঠনটি চিন্তা করা সবচেয়ে সহজ। আপনার ক্যানভাস যতই চওড়া বা লম্বা হোক না কেন, বাম প্রান্তটি সর্বদা X অক্ষের -1 এ থাকে এবং ডান প্রান্তটি সর্বদা X অক্ষে +1 এ থাকে৷ একইভাবে, নীচের প্রান্তটি সর্বদা Y অক্ষে -1 থাকে এবং উপরের প্রান্তটি Y অক্ষে +1 হয়। এর মানে হল (0, 0) সর্বদা ক্যানভাসের কেন্দ্র, (-1, -1) সর্বদা নীচে-বাম কোণে এবং (1, 1) সর্বদা উপরের-ডান কোণে থাকে। এটি ক্লিপ স্পেস নামে পরিচিত।

A simple graph visualizing the Normalized Device Coordinate space.

শীর্ষবিন্দুগুলিকে এই স্থানাঙ্ক ব্যবস্থায় প্রাথমিকভাবে খুব কমই সংজ্ঞায়িত করা হয়, তাই GPU গুলি শীর্ষবিন্দুগুলিকে ক্লিপ স্পেসে রূপান্তরিত করার জন্য প্রয়োজনীয় গণিতগুলি সম্পাদন করার জন্য ভার্টিক্স শেডার নামক ছোট প্রোগ্রামগুলির উপর নির্ভর করে, সেইসাথে শীর্ষবিন্দুগুলি আঁকার জন্য প্রয়োজনীয় অন্য কোনো গণনা। উদাহরণস্বরূপ, শেডার কিছু অ্যানিমেশন প্রয়োগ করতে পারে বা শীর্ষবিন্দু থেকে আলোর উত্সের দিকনির্দেশ গণনা করতে পারে। এই শেডারগুলি আপনার দ্বারা লেখা হয়েছে, WebGPU বিকাশকারী, এবং তারা GPU কীভাবে কাজ করে তার উপর একটি আশ্চর্যজনক পরিমাণ নিয়ন্ত্রণ প্রদান করে।

সেখান থেকে, GPU এই রূপান্তরিত শীর্ষবিন্দুগুলি দ্বারা তৈরি সমস্ত ত্রিভুজ নেয় এবং সেগুলি আঁকার জন্য স্ক্রিনে কোন পিক্সেল প্রয়োজন তা নির্ধারণ করে। তারপরে এটি আপনার লেখা আরেকটি ছোট প্রোগ্রাম চালায় যাকে একটি ফ্র্যাগমেন্ট শেডার বলা হয় যা প্রতিটি পিক্সেলের রঙ কী হওয়া উচিত তা গণনা করে। এই গণনাটি রিটার্ন গ্রীনের মতো সহজ বা অন্যান্য কাছাকাছি পৃষ্ঠের সূর্যালোক বাউন্সিং অফ, কুয়াশার মধ্য দিয়ে ফিল্টার করা এবং পৃষ্ঠটি কতটা ধাতব তা দ্বারা পরিবর্তিত হওয়ার সাপেক্ষে পৃষ্ঠের কোণ গণনা করার মতো জটিল হতে পারে। এটি সম্পূর্ণরূপে আপনার নিয়ন্ত্রণে, যা ক্ষমতায়ন এবং অপ্রতিরোধ্য উভয়ই হতে পারে।

সেই পিক্সেল রঙগুলির ফলাফলগুলি তারপরে একটি টেক্সচারে জমা হয়, যা তারপরে স্ক্রিনে প্রদর্শিত হতে সক্ষম হয়।

শীর্ষবিন্দু সংজ্ঞায়িত করুন

আগেই উল্লেখ করা হয়েছে, দ্য গেম অফ লাইফ সিমুলেশনকে কোষের গ্রিড হিসাবে দেখানো হয়েছে। আপনার অ্যাপ্লিকেশানের গ্রিডটি কল্পনা করার একটি উপায় প্রয়োজন, সক্রিয় কোষগুলি থেকে নিষ্ক্রিয় কোষগুলিকে আলাদা করে৷ এই কোডল্যাব দ্বারা ব্যবহৃত পদ্ধতিটি সক্রিয় কোষগুলিতে রঙিন বর্গক্ষেত্র আঁকা এবং নিষ্ক্রিয় কোষগুলি খালি রাখা হবে।

এর মানে হল যে আপনাকে চারটি ভিন্ন পয়েন্ট সহ GPU প্রদান করতে হবে, বর্গক্ষেত্রের চারটি কোণের প্রতিটির জন্য একটি। উদাহরণস্বরূপ, ক্যানভাসের কেন্দ্রে আঁকা একটি বর্গক্ষেত্র, প্রান্তগুলি থেকে একটি উপায়ে টানা হয়, এর কোণার স্থানাঙ্কগুলি এইরকম:

A Normalized Device Coordinate graph showing coordinates for the corners of a square

এই স্থানাঙ্কগুলিকে GPU-তে খাওয়ানোর জন্য, আপনাকে একটি TypedArray- এ মানগুলি স্থাপন করতে হবে। যদি আপনি ইতিমধ্যে এটির সাথে পরিচিত না হন, TypedArrays হল জাভাস্ক্রিপ্ট অবজেক্টের একটি গ্রুপ যা আপনাকে মেমরির সংলগ্ন ব্লকগুলি বরাদ্দ করতে এবং সিরিজের প্রতিটি উপাদানকে একটি নির্দিষ্ট ডেটা টাইপ হিসাবে ব্যাখ্যা করতে দেয়। উদাহরণস্বরূপ, একটি Uint8Array এ, অ্যারের প্রতিটি উপাদান একটি একক, স্বাক্ষরবিহীন বাইট। WebAssembly, WebAudio, এবং (অবশ্যই) WebGPU-এর মতো মেমরি লেআউটের প্রতি সংবেদনশীল API-এর সাথে টাইপডঅ্যারেগুলি ডেটা পাঠানোর জন্য দুর্দান্ত।

বর্গক্ষেত্রের উদাহরণের জন্য, যেহেতু মানগুলি ভগ্নাংশের, একটি Float32Array উপযুক্ত।

  1. একটি অ্যারে তৈরি করুন যা আপনার কোডে নিম্নলিখিত অ্যারে ঘোষণাটি রেখে ডায়াগ্রামের সমস্ত শীর্ষস্থানীয় অবস্থান ধারণ করে। এটি রাখার জন্য একটি ভাল জায়গা হল উপরের দিকে, ঠিক context.configure() কলের নীচে।

index.html

const vertices = new Float32Array([
//   X,    Y,
  -0.8, -0.8,
   0.8, -0.8,
   0.8,  0.8,
  -0.8,  0.8,
]);

মনে রাখবেন যে ব্যবধান এবং মন্তব্যের মানগুলির উপর কোন প্রভাব নেই; এটি শুধুমাত্র আপনার সুবিধার জন্য এবং এটি আরও পঠনযোগ্য করে তোলার জন্য। এটি আপনাকে দেখতে সাহায্য করে যে প্রতিটি জোড়া মান একটি শীর্ষবিন্দুর জন্য X এবং Y স্থানাঙ্ক তৈরি করে।

কিন্তু একটা সমস্যা আছে! GPU গুলো ত্রিভুজের পরিপ্রেক্ষিতে কাজ করে, মনে আছে? সুতরাং এর মানে হল যে আপনাকে তিনটি গ্রুপে শীর্ষবিন্দু প্রদান করতে হবে। আপনার চারজনের একটি দল আছে। সমাধান হল বর্গক্ষেত্রের মাঝখানে একটি প্রান্ত ভাগ করে দুটি ত্রিভুজ তৈরি করতে দুটি শীর্ষবিন্দুর পুনরাবৃত্তি করা।

A diagram showing how the four vertices of the square will be used to form two triangles.

ডায়াগ্রাম থেকে বর্গক্ষেত্র তৈরি করতে, আপনাকে (-0.8, -0.8) এবং (0.8, 0.8) শীর্ষবিন্দুগুলিকে দুইবার তালিকাভুক্ত করতে হবে, একবার নীল ত্রিভুজের জন্য এবং একবার লালটির জন্য। (আপনি পরিবর্তে অন্য দুটি কোণে বর্গক্ষেত্রকে বিভক্ত করতেও বেছে নিতে পারেন; এতে কোনো পার্থক্য নেই।)

  1. এইরকম কিছু দেখতে আপনার পূর্ববর্তী vertices অ্যারে আপডেট করুন:

index.html

const vertices = new Float32Array([
//   X,    Y,
  -0.8, -0.8, // Triangle 1 (Blue)
   0.8, -0.8,
   0.8,  0.8,

  -0.8, -0.8, // Triangle 2 (Red)
   0.8,  0.8,
  -0.8,  0.8,
]);

যদিও চিত্রটি স্পষ্টতার জন্য দুটি ত্রিভুজের মধ্যে একটি বিচ্ছেদ দেখায়, শীর্ষবিন্দুর অবস্থানগুলি ঠিক একই, এবং GPU তাদের ফাঁক ছাড়াই রেন্ডার করে। এটি একটি একক, কঠিন বর্গ হিসাবে রেন্ডার হবে।

একটি শীর্ষবিন্দু বাফার তৈরি করুন

GPU একটি JavaScript অ্যারে থেকে ডেটা সহ শীর্ষবিন্দু আঁকতে পারে না। জিপিইউ-এর প্রায়শই নিজস্ব মেমরি থাকে যা রেন্ডারিংয়ের জন্য অত্যন্ত অপ্টিমাইজ করা হয়, এবং তাই আপনি জিপিইউ ব্যবহার করতে চান এমন যেকোন ডেটা সেই মেমরিতে রাখতে হবে।

ভার্টেক্স ডেটা সহ অনেক মানের জন্য, GPU-সাইড মেমরি GPUBuffer অবজেক্টের মাধ্যমে পরিচালিত হয়। একটি বাফার হল মেমরির একটি ব্লক যা GPU-তে সহজেই অ্যাক্সেসযোগ্য এবং নির্দিষ্ট উদ্দেশ্যে পতাকাঙ্কিত। আপনি এটিকে GPU-দৃশ্যমান TypedArray-এর মতো কিছুটা ভাবতে পারেন।

  1. আপনার শীর্ষবিন্দুগুলি ধরে রাখার জন্য একটি বাফার তৈরি করতে, আপনার vertices অ্যারের সংজ্ঞার পরে device.createBuffer() এ নিম্নলিখিত কলটি যুক্ত করুন৷

index.html

const vertexBuffer = device.createBuffer({
  label: "Cell vertices",
  size: vertices.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});

লক্ষ্য করার প্রথম জিনিসটি হল আপনি বাফারটিকে একটি লেবেল দেন। আপনার তৈরি করা প্রতিটি একক WebGPU অবজেক্টকে একটি ঐচ্ছিক লেবেল দেওয়া যেতে পারে এবং আপনি অবশ্যই তা করতে চান! লেবেলটি আপনি চান এমন কোনো স্ট্রিং, যতক্ষণ না এটি আপনাকে বস্তুটি কী তা সনাক্ত করতে সহায়তা করে। আপনি যদি কোনো সমস্যায় পড়েন, তাহলে কী ভুল হয়েছে তা বোঝার জন্য WebGPU তৈরি করা ত্রুটির বার্তাগুলিতে সেই লেবেলগুলি ব্যবহার করা হয়।

এর পরে, বাইটে বাফারের জন্য একটি আকার দিন। আপনার 48 বাইট সহ একটি বাফার প্রয়োজন, যা আপনি একটি 32-বিট ফ্লোটের আকার ( 4 বাইট ) আপনার vertices অ্যারের (12) ফ্লোটের সংখ্যা দ্বারা গুণ করে নির্ধারণ করেন। সুখের বিষয়, TypedArrayগুলি ইতিমধ্যেই আপনার জন্য তাদের বাইট দৈর্ঘ্য গণনা করে, এবং তাই আপনি বাফার তৈরি করার সময় এটি ব্যবহার করতে পারেন।

অবশেষে, আপনাকে বাফারের ব্যবহার নির্দিষ্ট করতে হবে। এটি এক বা একাধিক GPUBufferUsage পতাকা, যার সাথে একাধিক পতাকা যুক্ত করা হচ্ছে | ( বিটওয়াইজ বা ) অপারেটর। এই ক্ষেত্রে, আপনি উল্লেখ করেছেন যে আপনি বাফারটি ভার্টেক্স ডেটার জন্য ব্যবহার করতে চান ( GPUBufferUsage.VERTEX ) এবং আপনি এটিতে ডেটা অনুলিপি করতে সক্ষম হতে চান ( GPUBufferUsage.COPY_DST )৷

আপনার কাছে ফিরে আসা বাফার অবজেক্টটি অস্বচ্ছ - আপনি (সহজে) এটিতে থাকা ডেটা পরিদর্শন করতে পারবেন না। অতিরিক্তভাবে, এর বেশিরভাগ গুণাবলী অপরিবর্তনীয়—আপনি একটি GPUBuffer তৈরি হওয়ার পরে তার আকার পরিবর্তন করতে পারবেন না, বা আপনি ব্যবহারের ফ্ল্যাগগুলি পরিবর্তন করতে পারবেন না। আপনি যা পরিবর্তন করতে পারেন তা হল এর মেমরির বিষয়বস্তু।

যখন বাফারটি প্রাথমিকভাবে তৈরি করা হয়, তখন এতে থাকা মেমরিটি শূন্য হয়ে যাবে। এর বিষয়বস্তু পরিবর্তন করার বিভিন্ন উপায় রয়েছে, তবে সবচেয়ে সহজ হল device.queue.writeBuffer() একটি TypedArray এর সাথে কল করা যা আপনি কপি করতে চান৷

  1. বাফারের মেমরিতে ভার্টেক্স ডেটা কপি করতে, নিম্নলিখিত কোডটি যোগ করুন:

index.html

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

শীর্ষবিন্দু লেআউট সংজ্ঞায়িত করুন

এখন আপনার কাছে এটিতে ভার্টেক্স ডেটা সহ একটি বাফার রয়েছে, তবে যতদূর জিপিইউ উদ্বিগ্ন তা কেবলমাত্র বাইটের একটি ব্লব। আপনি যদি এটির সাথে কিছু আঁকতে যাচ্ছেন তবে আপনাকে আরও কিছু তথ্য সরবরাহ করতে হবে। আপনাকে ওয়েবজিপিইউকে ভার্টেক্স ডেটার গঠন সম্পর্কে আরও জানাতে সক্ষম হতে হবে।

  • একটি GPUVertexBufferLayout অভিধানের সাথে শীর্ষবিন্দু ডেটা কাঠামো সংজ্ঞায়িত করুন:

index.html

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

এটি প্রথম নজরে কিছুটা বিভ্রান্তিকর হতে পারে, তবে এটি ভেঙে ফেলা তুলনামূলকভাবে সহজ।

আপনি প্রথম জিনিসটি arrayStride দেন। GPU পরবর্তী শীর্ষস্থান খুঁজতে গেলে বাফারে এগিয়ে যাওয়ার জন্য এই বাইটের সংখ্যা। আপনার বর্গক্ষেত্রের প্রতিটি শীর্ষবিন্দু দুটি 32-বিট ফ্লোটিং পয়েন্ট সংখ্যা দ্বারা গঠিত। আগেই উল্লেখ করা হয়েছে, একটি 32-বিট ফ্লোট হল 4 বাইট, তাই দুটি ফ্লোট হল 8 বাইট।

পরবর্তী attributes বৈশিষ্ট্য, যা একটি অ্যারে. গুণাবলী হল প্রতিটি শীর্ষে এনকোড করা তথ্যের পৃথক অংশ। আপনার শীর্ষবিন্দুতে শুধুমাত্র একটি বৈশিষ্ট্য (শীর্ষ অবস্থান) থাকে, তবে আরও উন্নত ব্যবহারের ক্ষেত্রে প্রায়শই শীর্ষবিন্দুর একাধিক বৈশিষ্ট্য থাকে যেমন একটি শীর্ষবিন্দুর রঙ বা জ্যামিতি পৃষ্ঠ যে দিকে নির্দেশ করে। যদিও এটি এই কোডল্যাবের সুযোগের বাইরে।

আপনার একক বৈশিষ্ট্যে, আপনি প্রথমে ডেটার format সংজ্ঞায়িত করুন। এটি GPUVertexFormat প্রকারের একটি তালিকা থেকে আসে যা GPU বুঝতে পারে এমন প্রতিটি ধরণের ভার্টেক্স ডেটা বর্ণনা করে। আপনার শীর্ষবিন্দুতে দুটি 32-বিট ফ্লোট রয়েছে, তাই আপনি float32x2 বিন্যাসটি ব্যবহার করুন। যদি আপনার ভার্টেক্স ডেটা পরিবর্তে চারটি 16-বিট স্বাক্ষরবিহীন পূর্ণসংখ্যার দ্বারা গঠিত হয়, উদাহরণস্বরূপ, আপনি পরিবর্তে uint16x4 ব্যবহার করবেন। প্যাটার্ন দেখুন?

এর পরে, offset বর্ণনা করে যে এই বিশেষ বৈশিষ্ট্যটি শুরু হয় শীর্ষবিন্দুতে কত বাইট। আপনার বাফারে একাধিক অ্যাট্রিবিউট থাকলেই আপনাকে এই বিষয়ে চিন্তা করতে হবে, যা এই কোডল্যাবের সময় আসবে না।

অবশেষে, আপনার কাছে shaderLocation আছে। এটি 0 এবং 15 এর মধ্যে একটি অবাধ সংখ্যা এবং আপনার সংজ্ঞায়িত প্রতিটি বৈশিষ্ট্যের জন্য অনন্য হতে হবে। এটি এই বৈশিষ্ট্যটিকে ভার্টেক্স শেডারের একটি নির্দিষ্ট ইনপুটের সাথে লিঙ্ক করে, যা আপনি পরবর্তী বিভাগে শিখবেন।

লক্ষ্য করুন যে আপনি এখন এই মানগুলিকে সংজ্ঞায়িত করলেও, আপনি আসলে এগুলিকে ওয়েবজিপিইউ এপিআইতে এখনও কোথাও পাস করছেন না। এটি আসছে, কিন্তু আপনি যখন আপনার শীর্ষবিন্দুগুলিকে সংজ্ঞায়িত করেন তখন এই মানগুলি সম্পর্কে চিন্তা করা সবচেয়ে সহজ, তাই আপনি পরে ব্যবহারের জন্য এখন সেগুলি সেট আপ করছেন৷

শেডার্স দিয়ে শুরু করুন

আপনি যে ডেটা রেন্ডার করতে চান তা এখন আপনার কাছে রয়েছে, তবে আপনাকে এখনও GPU-কে ঠিক কীভাবে এটি প্রক্রিয়া করতে হবে তা বলতে হবে। যে একটি বড় অংশ shaders সঙ্গে ঘটে.

শেডার্স হল ছোট প্রোগ্রাম যা আপনি লেখেন এবং যেগুলি আপনার GPU-তে চালান। প্রতিটি শেডার ডেটার একটি ভিন্ন পর্যায়ে কাজ করে: ভার্টেক্স প্রসেসিং, ফ্র্যাগমেন্ট প্রসেসিং বা সাধারণ গণনা । যেহেতু তারা GPU-তে রয়েছে, সেগুলি আপনার গড় জাভাস্ক্রিপ্টের চেয়ে আরও কঠোরভাবে গঠন করা হয়েছে। কিন্তু সেই কাঠামোটি তাদের খুব দ্রুত এবং, গুরুত্বপূর্ণভাবে, সমান্তরালভাবে কার্যকর করতে দেয়!

WebGPU-তে Shaders WGSL (ওয়েবজিপিইউ শেডিং ল্যাঙ্গুয়েজ) নামে একটি ছায়াময় ভাষায় লেখা হয়। WGSL, সিনট্যাক্টিকভাবে, কিছুটা মরিচা-এর মতো, বৈশিষ্ট্য সহ সাধারণ ধরনের GPU কাজ (যেমন ভেক্টর এবং ম্যাট্রিক্স ম্যাথ) সহজ এবং দ্রুত করার লক্ষ্যে। শেডিং ভাষার সম্পূর্ণতা শেখানো এই কোডল্যাবের সুযোগের বাইরে, তবে আশা করি আপনি কিছু সাধারণ উদাহরণের মধ্য দিয়ে চলার সময় কিছু মৌলিক বিষয় তুলে ধরবেন।

শেডারগুলি নিজেই স্ট্রিং হিসাবে 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 অবজেক্ট প্রদান করে।

শীর্ষবিন্দু শেডার সংজ্ঞায়িত করুন

ভার্টেক্স শেডার দিয়ে শুরু করুন কারণ সেখানেই GPU শুরু হয়!

একটি vertex shader একটি ফাংশন হিসাবে সংজ্ঞায়িত করা হয়, এবং GPU আপনার vertexBuffer এর প্রতিটি শীর্ষের জন্য একবার সেই ফাংশনটিকে কল করে। যেহেতু আপনার vertexBuffer মধ্যে ছয়টি অবস্থান (শীর্ষ) আছে, তাই আপনি যে ফাংশনটি সংজ্ঞায়িত করেছেন সেটি ছয় বার বলা হয়। প্রতিবার এটিকে কল করা হলে, vertexBuffer থেকে একটি ভিন্ন অবস্থান একটি আর্গুমেন্ট হিসাবে ফাংশনে প্রেরণ করা হয়, এবং এটি ভার্টেক্স শেডার ফাংশনের কাজ ক্লিপ স্পেসে একটি সংশ্লিষ্ট অবস্থান ফিরিয়ে দেওয়া।

এটা বোঝা গুরুত্বপূর্ণ যে তারা অগত্যা অনুক্রমিক ক্রমে ডাকা হবে না। পরিবর্তে, জিপিইউ একই সময়ে শত শত (বা এমনকি হাজার হাজার!) শীর্ষবিন্দুর প্রক্রিয়াকরণের ক্ষেত্রে সমান্তরালভাবে এই ধরনের শেডার চালানোর ক্ষেত্রে পারদর্শী! এটি জিপিইউগুলির অবিশ্বাস্য গতির জন্য দায়ী একটি বিশাল অংশ, তবে এটি সীমাবদ্ধতার সাথে আসে। চরম সমান্তরালকরণ নিশ্চিত করার জন্য, ভার্টেক্স শেডার একে অপরের সাথে যোগাযোগ করতে পারে না। প্রতিটি শেডার আহ্বান একবারে শুধুমাত্র একটি একক শীর্ষের জন্য ডেটা দেখতে পারে এবং শুধুমাত্র একটি একক শীর্ষের জন্য মান আউটপুট করতে সক্ষম।

WGSL-এ, একটি ভার্টেক্স শেডার ফাংশনকে আপনি যা চান তার নাম দেওয়া যেতে পারে, তবে এটির সামনে @vertex অ্যাট্রিবিউট থাকতে হবে যাতে এটি কোন শেডার পর্যায়ে প্রতিনিধিত্ব করে তা নির্দেশ করে। WGSL fn কীওয়ার্ড দিয়ে ফাংশন বোঝায়, যেকোনো আর্গুমেন্ট ঘোষণা করতে বন্ধনী ব্যবহার করে, এবং সুযোগ সংজ্ঞায়িত করতে কোঁকড়া বন্ধনী ব্যবহার করে।

  1. এই মত একটি খালি @vertex ফাংশন তৈরি করুন:

index.html (createShaderModule কোড)

@vertex
fn vertexMain() {

}

এটি বৈধ নয়, যদিও, একটি ভার্টেক্স শেডারকে অবশ্যই ক্লিপ স্পেসে প্রসেস করা ভার্টেক্সের অন্তত চূড়ান্ত অবস্থানে ফিরে আসতে হবে। এটি সর্বদা একটি 4-মাত্রিক ভেক্টর হিসাবে দেওয়া হয়। ভেক্টরগুলি শেডারগুলিতে ব্যবহার করার মতো একটি সাধারণ জিনিস যেগুলিকে ভাষাতে প্রথম-শ্রেণীর আদিম হিসাবে বিবেচনা করা হয়, 4-মাত্রিক ভেক্টরের জন্য vec4f মতো তাদের নিজস্ব প্রকারগুলি সহ। 2D ভেক্টর ( vec2f ) এবং 3D ভেক্টর ( vec3f ) এর জন্যও একই প্রকার রয়েছে!

  1. যে মানটি ফেরত দেওয়া হচ্ছে সেটি প্রয়োজনীয় অবস্থান, এটিকে @builtin(position) বৈশিষ্ট্য দিয়ে চিহ্নিত করুন। A -> চিহ্নটি বোঝাতে ব্যবহৃত হয় যে এটি ফাংশনটি ফেরত দেয়।

index.html (createShaderModule কোড)

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

}

অবশ্যই, যদি ফাংশনের একটি রিটার্ন টাইপ থাকে, তাহলে আপনাকে আসলে ফাংশন বডিতে একটি মান ফেরত দিতে হবে। আপনি সিনট্যাক্স vec4f(x, y, z, w) ব্যবহার করে ফিরে আসার জন্য একটি নতুন vec4f তৈরি করতে পারেন। x , y , এবং z মানগুলি সমস্ত ফ্লোটিং পয়েন্ট সংখ্যা যা, রিটার্ন মানের মধ্যে, ক্লিপ স্পেসে শীর্ষবিন্দুটি কোথায় রয়েছে তা নির্দেশ করে।

  1. (0, 0, 0, 1) এর একটি স্ট্যাটিক মান ফিরিয়ে দিন এবং আপনার প্রযুক্তিগতভাবে একটি বৈধ ভার্টেক্স শেডার রয়েছে, যদিও জিপিইউ স্বীকৃতি দেয় যে এটি উত্পাদিত ত্রিভুজগুলি কেবল একটি একক বিন্দু এবং তারপরে এটি বাতিল করে দেয় বলে কোনও কিছু প্রদর্শন করে না।

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কোড)

@vertex
fn vertexMain() -> @builtin(position) vec4f {
  return vec4f(0, 0, 0, 1); // (X, Y, Z, W)
}

পরিবর্তে আপনি যা চান তা হ'ল আপনি যে বাফারটি তৈরি করেছেন তার থেকে ডেটা ব্যবহার করা এবং আপনি এটি @location() বৈশিষ্ট্য দিয়ে আপনার ফাংশনের জন্য একটি যুক্তি ঘোষণা করে এটি করেন এবং এটি টাইপ করুন যা আপনি vertexBufferLayout বর্ণিত যা মেলে তা টাইপ করুন। আপনি 0 এর একটি shaderLocation নির্দিষ্ট করেছেন, সুতরাং আপনার ডাব্লুজিএসএল কোডে, যুক্তিটি @location(0) দিয়ে চিহ্নিত করুন। আপনি ফর্ম্যাটটিকে একটি float32x2 হিসাবেও সংজ্ঞায়িত করেছেন, যা একটি 2 ডি ভেক্টর, সুতরাং ডাব্লুজিএসএলে আপনার যুক্তিটি একটি vec2f । আপনি যা পছন্দ করেন তা আপনি নাম রাখতে পারেন তবে এগুলি যেহেতু আপনার ভার্টেক্স পজিশনের প্রতিনিধিত্ব করে, তাই পস এর মতো একটি নাম প্রাকৃতিক বলে মনে হয়।

  1. আপনার শেডার ফাংশনটি নিম্নলিখিত কোডে পরিবর্তন করুন:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কোড)

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

এবং এখন আপনাকে সেই অবস্থানটি ফিরিয়ে দিতে হবে। যেহেতু অবস্থানটি একটি 2 ডি ভেক্টর এবং রিটার্ন টাইপ একটি 4 ডি ভেক্টর, তাই আপনাকে এটি কিছুটা পরিবর্তন করতে হবে। আপনি যা করতে চান তা হ'ল পজিশন আর্গুমেন্ট থেকে দুটি উপাদান নেওয়া এবং এগুলি রিটার্ন ভেক্টরের প্রথম দুটি উপাদানগুলিতে যথাক্রমে 0 এবং 1 হিসাবে শেষ দুটি উপাদান রেখে।

  1. কোন অবস্থানের উপাদানগুলি ব্যবহার করতে হবে তা স্পষ্টভাবে উল্লেখ করে সঠিক অবস্থানটি ফিরিয়ে দিন:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কোড)

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

তবে , যেহেতু এই ধরণের ম্যাপিংগুলি শেডারে এত সাধারণ, আপনি সুবিধাজনক শর্টহ্যান্ডে প্রথম যুক্তি হিসাবে পজিশন ভেক্টরটিও পাস করতে পারেন এবং এর অর্থ একই জিনিস।

  1. নিম্নলিখিত কোডটি দিয়ে return স্টেটমেন্টটি পুনরায় লিখুন:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কোড)

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

এবং এটি আপনার প্রাথমিক ভার্টেক্স শেডার! এটি খুব সহজ, কেবল কার্যকরভাবে অপরিবর্তিত অবস্থানটি অতিক্রম করছে, তবে এটি শুরু করার পক্ষে যথেষ্ট ভাল।

খণ্ড শেডার সংজ্ঞায়িত করুন

এরপরে টুকরো টুকরো শেডার। টুকরো টুকরো শেডারগুলি ভার্টেক্স শেডারগুলির সাথে খুব একইভাবে কাজ করে, তবে প্রতিটি ভার্টেক্সের জন্য আহ্বান না করে, তারা প্রতিটি পিক্সেল আঁকার জন্য আহ্বান জানায়।

টুকরো টুকরো শেডারদের সর্বদা ভার্টেক্স শেডারের পরে ডাকা হয়। জিপিইউ ভার্টেক্স শেডারগুলির আউটপুট নেয় এবং ত্রিভুজগুলি তিনটি পয়েন্টের সেটগুলির বাইরে ত্রিভুজ তৈরি করে। এরপরে আউটপুট রঙ সংযুক্তিগুলির কোন পিক্সেল সেই ত্রিভুজটিতে অন্তর্ভুক্ত রয়েছে তা নির্ধারণ করে সেই প্রতিটি ত্রিভুজগুলির প্রতিটিকে রাস্টারাইজ করে এবং তারপরে সেই পিক্সেলের প্রত্যেকটির জন্য একবার খণ্ড শেডারকে কল করে। খণ্ড শেডার একটি রঙ ফেরত দেয়, সাধারণত এটি ভার্টেক্স শেডার এবং টেক্সচারের মতো সম্পদ থেকে প্রেরিত মানগুলি থেকে গণনা করা হয়, যা জিপিইউ রঙ সংযুক্তিতে লিখেছেন।

ভার্টেক্স শেডারগুলির মতো, খণ্ডের শেডারগুলি ব্যাপকভাবে সমান্তরাল ফ্যাশনে কার্যকর করা হয়। এগুলি তাদের ইনপুট এবং আউটপুটগুলির দিক থেকে ভার্টেক্স শেডারগুলির চেয়ে কিছুটা নমনীয়, তবে আপনি প্রতিটি ত্রিভুজের প্রতিটি পিক্সেলের জন্য কেবল একটি রঙ ফেরত দেওয়ার জন্য এগুলি বিবেচনা করতে পারেন।

একটি ডাব্লুজিএসএল ফ্রেগমেন্ট শেডার ফাংশনটি @fragment অ্যাট্রিবিউট দিয়ে চিহ্নিত করা হয় এবং এটি একটি vec4f দেয়। এই ক্ষেত্রে, যদিও, ভেক্টর একটি রঙ উপস্থাপন করে, কোনও অবস্থান নয়। রিটার্নের মানটি একটি @location অ্যাট্রিবিউট দেওয়া দরকার যাতে beginRenderPass কল থেকে প্রাপ্ত colorAttachment লিখিত হয় তা নির্দেশ করতে। যেহেতু আপনার কেবল একটি সংযুক্তি ছিল, অবস্থানটি 0।

  1. এর মতো একটি খালি @fragment ফাংশন তৈরি করুন:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কোড)

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

}

প্রত্যাবর্তিত ভেক্টরের চারটি উপাদান হ'ল লাল, সবুজ, নীল এবং আলফা রঙের মান, যা আপনি আগে শুরুতে beginRenderPass সেট করা clearValue হিসাবে ঠিক একইভাবে ব্যাখ্যা করা হয়। সুতরাং vec4f(1, 0, 0, 1) উজ্জ্বল লাল, যা আপনার বর্গক্ষেত্রের জন্য একটি শালীন রঙের মতো মনে হয়। আপনি যে রঙিন রঙে এটি সেট করতে নির্দ্বিধায়, যদিও!

  1. ফিরে আসা রঙ ভেক্টর সেট করুন, এর মতো:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কোড)

@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 এর অংশ হিসাবে ব্যবহার করতে হবে, এটি কল করে ডিভাইস.সিআরটেন্ডারপিপলাইন () দ্বারা তৈরি করা হয়েছে। রেন্ডার পাইপলাইনটি কীভাবে জ্যামিতি আঁকা হয় তা নিয়ন্ত্রণ করে, কোন শেডার ব্যবহার করা হয়, ভার্টেক্স বাফারে ডেটা কীভাবে ব্যাখ্যা করা যায়, কোন ধরণের জ্যামিতি রেন্ডার করা উচিত (লাইন, পয়েন্ট, ত্রিভুজ ...) এবং আরও অনেক কিছু!

রেন্ডার পাইপলাইনটি পুরো এপিআইয়ের সবচেয়ে জটিল বস্তু, তবে চিন্তা করবেন না! আপনি যে মানগুলিতে এটি পাস করতে পারেন তার বেশিরভাগই al চ্ছিক এবং আপনাকে কেবল শুরু করার জন্য কয়েকটি সরবরাহ করতে হবে।

  • একটি রেন্ডার পাইপলাইন তৈরি করুন, এর মতো:

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 হ'ল জিপুশাডার্মডুল যা আপনার ভার্টেক্স শেডার ধারণ করে এবং entryPoint শেডার কোডে ফাংশনের নাম দেয় যা প্রতিটি ভার্টেক্সের অনুরোধের জন্য ডাকা হয়। (আপনার একক শেডার মডিউলটিতে একাধিক @vertex এবং @fragment ফাংশন থাকতে পারে!) বাফারগুলি হ'ল GPUVertexBufferLayout অবজেক্টগুলির একটি অ্যারে যা বর্ণনা করে যে কীভাবে আপনার ডেটা আপনি এই পাইপলাইনটি ব্যবহার করেন এমন ভার্টেক্স বাফারগুলিতে কীভাবে প্যাক করা হয়। ভাগ্যক্রমে, আপনি ইতিমধ্যে এটি আপনার vertexBufferLayout এর আগে সংজ্ঞায়িত করেছেন! আপনি যেখানে এটি পাস করেছেন তা এখানে।

শেষ অবধি, আপনার fragment মঞ্চ সম্পর্কে বিশদ রয়েছে। এর মধ্যে ভার্টেক্স স্টেজের মতো একটি শেডার মডিউল এবং এন্ট্রিপয়েন্টও অন্তর্ভুক্ত রয়েছে। শেষ বিটটি এই পাইপলাইনটি ব্যবহার করা হয় এমন targets সংজ্ঞায়িত করা। পাইপলাইনটি যে রঙিন সংযুক্তিগুলিতে আউটপুট দেয় তার মতো টেক্সচার format হিসাবে এটি বিশদ দেয় এমন অভিধানের একটি অ্যারে। এই বিবরণগুলি এই পাইপলাইনটি যে কোনও রেন্ডার পাসগুলির সাথে ব্যবহৃত হয় তার colorAttachments প্রদত্ত টেক্সচারগুলির সাথে মেলে। আপনার রেন্ডার পাস ক্যানভাস প্রসঙ্গ থেকে টেক্সচার ব্যবহার করে এবং এর ফর্ম্যাটের জন্য আপনি canvasFormat সংরক্ষণ করা মানটি ব্যবহার করেন, তাই আপনি এখানে একই ফর্ম্যাটটি পাস করেন।

এটি রেন্ডার পাইপলাইন তৈরি করার সময় আপনি নির্দিষ্ট করতে পারেন এমন সমস্ত বিকল্পের কাছাকাছিও নয়, তবে এটি এই কোডেল্যাবের প্রয়োজনের জন্য যথেষ্ট!

স্কোয়ার আঁকুন

এবং এটির সাথে, আপনার বর্গ আঁকতে আপনার এখন যা প্রয়োজন তা এখন আপনার কাছে রয়েছে!

  1. স্কোয়ারটি আঁকতে, encoder.beginRenderPass() ফিরে ঝাঁপুন pass.end()

index.html

// After encoder.beginRenderPass()

pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertices.length / 2); // 6 vertices

// before pass.end()

এটি আপনার স্কোয়ার আঁকতে প্রয়োজনীয় সমস্ত তথ্য সহ ওয়েবজিপিইউ সরবরাহ করে। প্রথমত, আপনি কোন পাইপলাইনটি আঁকতে ব্যবহার করা উচিত তা নির্দেশ করতে আপনি setPipeline() ব্যবহার করেন। এর মধ্যে শেডারগুলি ব্যবহৃত হয়, ভার্টেক্স ডেটার বিন্যাস এবং অন্যান্য প্রাসঙ্গিক রাষ্ট্রীয় ডেটা অন্তর্ভুক্ত রয়েছে।

এরপরে, আপনি আপনার বর্গক্ষেত্রের জন্য উল্লম্বগুলি যুক্ত বাফার দিয়ে setVertexBuffer() কল করুন। আপনি এটিকে 0 এর সাথে কল করুন কারণ এই বাফারটি বর্তমান পাইপলাইনের vertex.buffers সংজ্ঞাতে 0 তম উপাদানটির সাথে মিলে যায়।

এবং সর্বশেষে, আপনি draw() কল করুন, যা আগে এসেছিল এমন সমস্ত সেটআপের পরে অদ্ভুতভাবে সহজ বলে মনে হয়। আপনার কেবলমাত্র যে জিনিসটি প্রবেশ করতে হবে তা হ'ল এটি যে শীর্ষে রেন্ডার করা উচিত তা হ'ল, যা এটি বর্তমানে সেট ভার্টেক্স বাফারগুলি থেকে টানছে এবং বর্তমানে সেট পাইপলাইনের সাথে ব্যাখ্যা করে। আপনি এটি কেবল 6 এ হার্ড-কোড করতে পারেন, তবে এটি ভার্টিস অ্যারে থেকে গণনা করা (12 টি ফ্লোট / 2 স্থানাঙ্ক প্রতি ভার্টেক্স == 6 ভার্চিসেস) এর অর্থ হ'ল আপনি যদি কখনও বর্গক্ষেত্রটি প্রতিস্থাপনের সিদ্ধান্ত নিয়েছেন, উদাহরণস্বরূপ, একটি বৃত্ত, সেখানে কম রয়েছে, হাত দিয়ে আপডেট করতে।

  1. আপনার স্ক্রিনটি রিফ্রেশ করুন এবং (অবশেষে) আপনার সমস্ত কঠোর পরিশ্রমের ফলাফল দেখুন: একটি বড় রঙিন স্কোয়ার।

A single red square rendered with WebGPU

5। একটি গ্রিড আঁকুন

প্রথমে নিজেকে অভিনন্দন জানাতে কিছুক্ষণ সময় নিন! স্ক্রিনে জ্যামিতির প্রথম বিটগুলি পাওয়া প্রায়শই বেশিরভাগ জিপিইউ এপিআইগুলির সাথে সবচেয়ে কঠিন পদক্ষেপগুলির মধ্যে একটি। আপনি এখান থেকে যা কিছু করেন তা ছোট পদক্ষেপে করা যেতে পারে, আপনার অগ্রগতি যাচাই করা সহজ করে তোলে।

এই বিভাগে, আপনি শিখেন:

  • জাভাস্ক্রিপ্ট থেকে শেডারে কীভাবে ভেরিয়েবলগুলি (ইউনিফর্ম বলা হয়) পাস করবেন।
  • রেন্ডারিং আচরণ পরিবর্তন করতে কীভাবে ইউনিফর্ম ব্যবহার করবেন।
  • একই জ্যামিতির বিভিন্ন বিভিন্ন রূপ আঁকতে কীভাবে ইনস্ট্যান্সিং ব্যবহার করবেন।

গ্রিড সংজ্ঞায়িত করুন

একটি গ্রিড রেন্ডার করার জন্য, আপনাকে এটি সম্পর্কে একটি খুব মৌলিক তথ্য জানতে হবে। এতে কতগুলি কোষ রয়েছে, প্রস্থ এবং উচ্চতায় উভয়ই? এটি বিকাশকারী হিসাবে আপনার উপর নির্ভর করে তবে জিনিসগুলি কিছুটা সহজ রাখতে, গ্রিডটিকে বর্গক্ষেত্র (একই প্রস্থ এবং উচ্চতা) হিসাবে বিবেচনা করুন এবং এমন একটি আকার ব্যবহার করুন যা দুটি শক্তি। (এটি পরে কিছু গণিতকে আরও সহজ করে তোলে)) আপনি শেষ পর্যন্ত এটিকে আরও বড় করতে চান, তবে এই বিভাগের বাকি অংশগুলির জন্য, আপনার গ্রিডের আকারটি 4x4 এ সেট করুন কারণ এই বিভাগে ব্যবহৃত কিছু গণিত প্রদর্শন করা সহজ করে তোলে। এটি পরে স্কেল!

  • আপনার জাভাস্ক্রিপ্ট কোডের শীর্ষে একটি ধ্রুবক যুক্ত করে গ্রিডের আকারটি সংজ্ঞায়িত করুন।

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

এটি খুব পরিচিত দেখা উচিত, কারণ এটি প্রায় ঠিক একই কোড যা আপনি আগে ভার্টেক্স বাফারটি তৈরি করতে ব্যবহার করেছিলেন! এর কারণ হ'ল ইউনিফর্মগুলি একই জিপুবুফার অবজেক্টগুলির মাধ্যমে ওয়েবজিপিইউ এপিআই -তে যোগাযোগ করা হয় যা মূল পার্থক্য রয়েছে, মূল পার্থক্যটি হ'ল এই usage GPUBufferUsage.UNIFORM পরিবর্তে GPUBufferUsage.VERTEX এর পরিবর্তে।

একটি শেডারে ইউনিফর্ম অ্যাক্সেস করুন

  • নিম্নলিখিত কোডটি যুক্ত করে একটি ইউনিফর্ম সংজ্ঞায়িত করুন:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

// 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 নামক শেডারে একটি ইউনিফর্ম সংজ্ঞায়িত করে, এটি একটি 2 ডি ফ্লোট ভেক্টর যা আপনি সবেমাত্র ইউনিফর্ম বাফারে অনুলিপি করেছেন এমন অ্যারের সাথে মেলে। এটি এও নির্দিষ্ট করে যে ইউনিফর্মটি @group(0) এবং @binding(0) এ আবদ্ধ। আপনি এক মুহুর্তে এই মানগুলির অর্থ কী তা শিখবেন।

তারপরে, শেডার কোডের অন্য কোথাও, আপনি গ্রিড ভেক্টরটি ব্যবহার করতে পারেন তবে আপনার প্রয়োজন। এই কোডটিতে আপনি গ্রিড ভেক্টর দ্বারা ভার্টেক্স অবস্থানটি ভাগ করেন। যেহেতু pos একটি 2 ডি ভেক্টর এবং grid একটি 2 ডি ভেক্টর, তাই ডাব্লুজিএসএল একটি উপাদান-ভিত্তিক বিভাগ সম্পাদন করে। অন্য কথায়, ফলাফলটি vec2f(pos.x / grid.x, pos.y / grid.y) বলার মতোই।

এই ধরণের ভেক্টর অপারেশনগুলি জিপিইউ শেডারগুলিতে খুব সাধারণ, যেহেতু অনেকগুলি রেন্ডারিং এবং গণনা কৌশল তাদের উপর নির্ভর করে!

আপনার ক্ষেত্রে এর অর্থ কী তা হ'ল (যদি আপনি 4 এর গ্রিডের আকার ব্যবহার করেন) আপনি যে বর্গক্ষেত্রটি রেন্ডার করেন তা তার মূল আকারের এক-চতুর্থাংশ হবে। আপনি যদি তাদের চারটি সারি বা কলামে ফিট করতে চান তবে এটি নিখুঁত!

একটি বাইন্ড গ্রুপ তৈরি করুন

শেডারে ইউনিফর্ম ঘোষণা করা আপনার তৈরি বাফারের সাথে এটি সংযুক্ত করে না। এটি করার জন্য, আপনাকে একটি বাইন্ড গ্রুপ তৈরি এবং সেট করতে হবে।

একটি বাইন্ড গ্রুপ হ'ল সংস্থানগুলির সংকলন যা আপনি একই সাথে আপনার শেডারে অ্যাক্সেসযোগ্য করতে চান। এটিতে বিভিন্ন ধরণের বাফার অন্তর্ভুক্ত থাকতে পারে যেমন আপনার ইউনিফর্ম বাফার এবং অন্যান্য সংস্থান যেমন টেক্সচার এবং স্যাম্পেলারগুলি এখানে আচ্ছাদিত নয় তবে এটি ওয়েবজিপিইউ রেন্ডারিং কৌশলগুলির সাধারণ অংশ।

  • ইউনিফর্ম বাফার তৈরির পরে নিম্নলিখিত কোডটি যুক্ত করে আপনার ইউনিফর্ম বাফারের সাথে একটি বাইন্ড গ্রুপ তৈরি করুন এবং পাইপলাইন রেন্ডার করুন:

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 দেয়, যা একটি অস্বচ্ছ, অপরিবর্তনীয় হ্যান্ডেল। আপনি কোনও বাইন্ড গ্রুপ তৈরি হওয়ার পরে যে সংস্থানগুলি নির্দেশ করে তা আপনি পরিবর্তন করতে পারবেন না, যদিও আপনি সেই সংস্থানগুলির বিষয়বস্তু পরিবর্তন করতে পারেন । উদাহরণস্বরূপ, আপনি যদি নতুন গ্রিডের আকার ধারণ করতে ইউনিফর্ম বাফার পরিবর্তন করেন তবে এটি এই বাইন্ড গ্রুপটি ব্যবহার করে ভবিষ্যতের ড্র কলগুলির দ্বারা প্রতিফলিত হয়।

বাইন্ড গ্রুপটি বাঁধুন

এখন যে বাইন্ড গ্রুপটি তৈরি করা হয়েছে, আপনাকে অঙ্কন করার সময় এটি ব্যবহার করতে ওয়েবজিপিইউকে এখনও বলতে হবে। ভাগ্যক্রমে এটি বেশ সহজ।

  1. রেন্ডার পাসে ফিরে হ্যাপ করুন এবং draw() পদ্ধতির আগে এই নতুন লাইনটি যুক্ত করুন:

index.html

pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);

pass.setBindGroup(0, bindGroup); // New line!

pass.draw(vertices.length / 2);

প্রথম যুক্তি শেডার কোডে @group(0) এর সাথে মিলে যায় 0 আপনি বলছেন যে প্রতিটি @binding এটি @group(0) এই বাইন্ড গ্রুপের সংস্থানগুলি ব্যবহার করে।

এবং এখন ইউনিফর্ম বাফারটি আপনার শেডারের সংস্পর্শে আসে!

  1. আপনার পৃষ্ঠাটি রিফ্রেশ করুন এবং তারপরে আপনার এই জাতীয় কিছু দেখতে হবে:

A small red square in the center of a dark blue background.

হুরে! আপনার স্কোয়ারটি এখন আগের আকারের এক-চতুর্থাংশ! এটি খুব বেশি নয়, তবে এটি দেখায় যে আপনার ইউনিফর্মটি আসলে প্রয়োগ করা হয়েছে এবং শেডারটি এখন আপনার গ্রিডের আকারটি অ্যাক্সেস করতে পারে।

শেডারে জ্যামিতি পরিচালনা করে

সুতরাং এখন আপনি শেডারে গ্রিডের আকারটি উল্লেখ করতে পারেন, আপনি আপনার পছন্দসই গ্রিড প্যাটার্নটি ফিট করার জন্য আপনি যে জ্যামিতি রেন্ডার করছেন তা হেরফের করার জন্য কিছু কাজ শুরু করতে পারেন। এটি করার জন্য, আপনি ঠিক কী অর্জন করতে চান তা বিবেচনা করুন।

আপনার ধারণাগতভাবে আপনার ক্যানভাসকে পৃথক কোষগুলিতে বিভক্ত করতে হবে। কনভেনশনটি রাখার জন্য যে আপনি ডানদিকে সরে যাওয়ার সাথে সাথে এক্স অক্ষটি বৃদ্ধি পায় এবং আপনি উঠে যাওয়ার সাথে সাথে ওয়াই অক্ষটি বৃদ্ধি পায়, বলুন যে প্রথম ঘরটি ক্যানভাসের নীচের বাম কোণে রয়েছে। এটি আপনাকে এমন একটি লেআউট দেয় যা দেখতে এটির মতো দেখতে আপনার বর্তমান বর্গাকার জ্যামিতি সহ:

An illustration of the conceptual grid the Normalized Device Coordinate space will be divided when visualizing each cell with the currently rendered square geometry at it's center.

আপনার চ্যালেঞ্জ হ'ল শেডারে এমন একটি পদ্ধতি সন্ধান করা যা আপনাকে কোষের স্থানাঙ্কগুলি প্রদত্ত সেই কোষগুলির যে কোনও একটিতে বর্গাকার জ্যামিতি অবস্থান করতে দেয়।

প্রথমত, আপনি দেখতে পাচ্ছেন যে আপনার বর্গটি কোনও কোষের সাথে সুন্দরভাবে সংযুক্ত নয় কারণ এটি ক্যানভাসের কেন্দ্রকে ঘিরে সংজ্ঞায়িত করা হয়েছিল। আপনি বর্গক্ষেত্রটি অর্ধেক ঘর দ্বারা স্থানান্তরিত করতে চান যাতে এটি তাদের ভিতরে সুন্দরভাবে লাইন করতে পারে।

আপনি এটি ঠিক করার একটি উপায় হ'ল স্কোয়ারের ভার্টেক্স বাফার আপডেট করা। উল্লম্বগুলি স্থানান্তরিত করে যাতে নীচে-বাম কোণটি থাকে, উদাহরণস্বরূপ, (0.1, 0.1) (-0.8, -0.8) এর পরিবর্তে, আপনি এই বর্গক্ষেত্রটি আরও সুন্দরভাবে কোষের সীমানার সাথে সামঞ্জস্য করতে স্থানান্তরিত করতে চাইবেন। তবে, যেহেতু আপনার শেডারে উল্লম্বগুলি কীভাবে প্রক্রিয়াজাত করা হয় তার উপর আপনার সম্পূর্ণ নিয়ন্ত্রণ রয়েছে, তাই শেডার কোডটি ব্যবহার করে এগুলি কেবল জায়গায় ঠেলাঠেলি করা ঠিক তত সহজ!

  1. নিম্নলিখিত কোড সহ ভার্টেক্স শেডার মডিউলটি পরিবর্তন করুন:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

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

এটি গ্রিডের আকার দ্বারা ভাগ করার আগে প্রতিটি শীর্ষবিন্দু উপরে এবং ডানদিকে এক (যা মনে রাখবেন, ক্লিপ স্পেসের অর্ধেক) সরান। ফলাফলটি উত্সের ঠিক বাইরে একটি দুর্দান্ত গ্রিড-সংযুক্ত বর্গক্ষেত্র।

A visualization of the canvas conceptually divided into a 4x4 grid with a red square in cell (2, 2)

এরপরে, কারণ আপনার ক্যানভাসের স্থানাঙ্ক সিস্টেমের স্থানগুলি (0, 0) কেন্দ্রে এবং (-1, -1) নীচের বামে এবং আপনি (0, 0) নীচের বামে থাকতে চান, আপনাকে আপনার জ্যামিতির অনুবাদ করতে হবে গ্রিডের আকার দ্বারা ভাগ করার পরে (-1, -1) দ্বারা অবস্থান করুন যাতে এটিকে সেই কোণে স্থানান্তরিত করতে।

  1. আপনার জ্যামিতির অবস্থান অনুবাদ করুন, এর মতো:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

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

A visualization of the canvas conceptually divided into a 4x4 grid with a red square in cell (0, 0)

আপনি যদি এটি অন্য কোনও কক্ষে রাখতে চান? আপনার শেডারে একটি cell ভেক্টর ঘোষণা করে এবং এটি let cell = vec2f(1, 1) মতো স্থির মান দিয়ে এটি জনগণের মাধ্যমে চিত্রিত করুন।

যদি আপনি এটি gridPos যুক্ত করেন তবে এটি অ্যালগরিদমে - 1 কে পূর্বাবস্থায় ফেলবে, যাতে আপনি যা চান তা নয়। পরিবর্তে, আপনি প্রতিটি ঘরের জন্য কেবল একটি গ্রিড ইউনিট (ক্যানভাসের এক-চতুর্থাংশ) দ্বারা বর্গক্ষেত্রটি সরিয়ে নিতে চান। আপনার মতো মনে হচ্ছে grid মাধ্যমে আপনার আরও একটি বিভাজন করা দরকার!

  1. আপনার গ্রিডের অবস্থান পরিবর্তন করুন, এর মতো:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

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

আপনি যদি এখনই রিফ্রেশ করেন তবে আপনি নিম্নলিখিতগুলি দেখতে পাবেন:

A visualization of the canvas conceptually divided into a 4x4 grid with a red square centered between cell (0, 0), cell (0, 1), cell (1, 0), and cell (1, 1)

হুম। আপনি যা চেয়েছিলেন তা বেশ নয়।

এর কারণ হ'ল যেহেতু ক্যানভাস স্থানাঙ্কগুলি -1 থেকে +1 এ যায়, এটি আসলে 2 টি ইউনিট জুড়ে । এর অর্থ যদি আপনি ক্যানভাসের এক-চতুর্থাংশ একটি শীর্ষস্থানটি সরাতে চান তবে আপনাকে এটি 0.5 ইউনিট স্থানান্তর করতে হবে। জিপিইউ স্থানাঙ্কের সাথে যুক্তি দেওয়ার সময় এটি করা সহজ ভুল! ভাগ্যক্রমে, ফিক্সটি ঠিক তত সহজ।

  1. আপনার অফসেটটি 2 দ্বারা গুণ করুন, এর মতো:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

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

এবং এটি আপনাকে যা চায় ঠিক তা দেয়।

A visualization of the canvas conceptually divided into a 4x4 grid with a red square in cell (1, 1)

স্ক্রিনশটটি দেখতে এটির মতো:

Screenshot of a red square on a dark blue background. The red square in drawn in the same position as described in the previous diagram, but without the grid overlay.

তদ্ব্যতীত, আপনি এখন গ্রিড সীমানার মধ্যে যে কোনও মানতে cell সেট করতে পারেন এবং তারপরে কাঙ্ক্ষিত স্থানে বর্গাকার রেন্ডারটি দেখতে রিফ্রেশ করুন।

উদাহরণ আঁকুন

এখন আপনি যেখানে বর্গক্ষেত্রটি কিছুটা গণিতের সাথে চান সেখানে রাখতে পারেন, পরবর্তী পদক্ষেপটি গ্রিডের প্রতিটি কক্ষে একটি বর্গক্ষেত্র রেন্ডার করা।

আপনি এটির কাছে যেতে পারার একটি উপায় হ'ল ইউনিফর্ম বাফারে সেল স্থানাঙ্কগুলি লিখুন, তারপরে গ্রিডের প্রতিটি স্কোয়ারের জন্য একবার ড্রকে কল করুন, প্রতিবার ইউনিফর্ম আপডেট করা। তবে এটি খুব ধীর হবে, যেহেতু জিপিইউকে প্রতিবার জাভাস্ক্রিপ্ট দ্বারা নতুন সমন্বয়টি লেখার জন্য অপেক্ষা করতে হবে। জিপিইউ থেকে ভাল পারফরম্যান্স পাওয়ার কীগুলির মধ্যে একটি হ'ল সিস্টেমের অন্যান্য অংশে অপেক্ষা করা সময়টি হ্রাস করা!

পরিবর্তে, আপনি ইনস্ট্যান্সিং নামে একটি কৌশল ব্যবহার করতে পারেন। জিপিইউকে draw একক কল দিয়ে একই জ্যামিতির একাধিক অনুলিপি আঁকতে জিপিইউকে বলার একটি উপায় যা প্রতিটি অনুলিপিটির জন্য একবার draw কল করার চেয়ে অনেক দ্রুত। জ্যামিতির প্রতিটি অনুলিপি উদাহরণ হিসাবে উল্লেখ করা হয়।

  1. জিপিইউকে বলতে যে আপনি গ্রিডটি পূরণ করার জন্য আপনার বর্গক্ষেত্রের পর্যাপ্ত উদাহরণ চান, আপনার বিদ্যমান ড্র কলটিতে একটি যুক্তি যুক্ত করুন:

index.html

pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);

এটি সিস্টেমকে বলে যে আপনি এটি আপনার স্কোয়ার 16 ( GRID_SIZE * GRID_SIZE ) বার ছয়টি (উল্লম্ব। vertices.length / 2 ) উল্লম্ব আঁকতে চান। তবে আপনি যদি পৃষ্ঠাটি রিফ্রেশ করেন তবে আপনি এখনও নিম্নলিখিতগুলি দেখতে পাবেন:

An identical image to the previous diagram, to indicate that nothing changed.

কেন? ঠিক আছে, কারণ আপনি সেই সমস্ত স্কোয়ারের 16 টি একই জায়গায় আঁকেন। আপনার শেডারে কিছু অতিরিক্ত যুক্তি থাকা দরকার যা প্রতি-অনুপ্রেরণার ভিত্তিতে জ্যামিতিটি পুনরায় স্থাপন করে।

শেডারে, আপনার ভার্টেক্স বাফার থেকে আসা pos মতো ভার্টেক্স বৈশিষ্ট্যগুলি ছাড়াও, আপনি ডাব্লুজিএসএল এর অন্তর্নির্মিত মান হিসাবে পরিচিত যা অ্যাক্সেস করতে পারেন। এগুলি এমন মান যা ওয়েবজিপিইউ দ্বারা গণনা করা হয় এবং এরকম একটি মান হ'ল instance_indexinstance_index 0 থেকে 0 থেকে number of instances - 1 যা আপনি আপনার শেডার যুক্তির অংশ হিসাবে ব্যবহার করতে পারেন। এর মানটি একই উদাহরণের অংশ হিসাবে প্রসেস করা প্রতিটি শীর্ষবিন্দুর জন্য একই। এর অর্থ আপনার ভার্টেক্স বাফারে প্রতিটি অবস্থানের জন্য একবার আপনার ভার্টেক্স শেডারটি 0 এর একটি instance_index সাথে ছয়বার কল করা হয়। তারপরে আরও ছয়বার 1 এর instance_index সাথে আরও ছয়টি, তারপরে 2 এর instance_index সহ আরও ছয়টি।

এটি কার্যকরভাবে দেখতে, আপনাকে আপনার শেডার ইনপুটগুলিতে instance_index অন্তর্নির্মিত যুক্ত করতে হবে। এটি পজিশনের মতো একইভাবে করুন, তবে এটি @location অ্যাট্রিবিউট দিয়ে ট্যাগ করার পরিবর্তে, @builtin(instance_index) ব্যবহার করুন এবং তারপরে আপনি যা চান তা যুক্তির নাম দিন। (আপনি উদাহরণ কোডটি মেলে এটি instance বলতে পারেন)) তারপরে এটি শেডার লজিকের অংশ হিসাবে ব্যবহার করুন!

  1. সেল স্থানাঙ্কের জায়গায় instance ব্যবহার করুন:

index.html

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

@vertex
fn vertexMain(@location(0) pos: vec2f,
              @builtin(instance_index) instance: u32) ->
  @builtin(position) vec4f {
  
  let i = f32(instance); // Save the instance_index as a float
  let cell = vec2f(i, i); // Updated
  let cellOffset = cell / grid * 2;
  let gridPos = (pos + 1) / grid - 1 + cellOffset;

  return vec4f(gridPos, 0, 1);
}

আপনি যদি এখন রিফ্রেশ করেন তবে আপনি দেখতে পাচ্ছেন যে আপনার কাছে অবশ্যই একাধিক বর্গক্ষেত্র রয়েছে! তবে আপনি তাদের 16 টি দেখতে পাচ্ছেন না।

Four red squares in a diagonal line from the lower left corner to the top right corner against a dark blue background.

এর কারণ আপনি উত্পন্ন কোষের স্থানাঙ্কগুলি হ'ল (0, 0), (1, 1), (2, 2) ... সমস্ত উপায়ে (15, 15), তবে ক্যানভাসে ফিটগুলির মধ্যে কেবল প্রথম চারটি। আপনি যে গ্রিডটি চান তা তৈরি করতে আপনাকে instance_index এমনভাবে রূপান্তর করতে হবে যাতে প্রতিটি সূচক মানচিত্রগুলি আপনার গ্রিডের মধ্যে একটি অনন্য কক্ষে রূপান্তর করতে পারে, এর মতো:

A visualization of the canvas conceptually divided into a 4x4 grid with each cell also corresponding to a linear instance index.

এর জন্য গণিত যুক্তিসঙ্গতভাবে সোজা। প্রতিটি ঘরের এক্স মানের জন্য, আপনি instance_index এবং গ্রিড প্রস্থের মডুলো চান, যা আপনি % অপারেটরের সাথে ডাব্লুজিএসএলে সঞ্চালন করতে পারেন। এবং প্রতিটি কক্ষের y মানের জন্য আপনি গ্রিড প্রস্থ দ্বারা বিভক্ত instance_index চান, কোনও ভগ্নাংশের অবশিষ্টাংশ বাতিল করে। আপনি ডাব্লুজিএসএল এর floor() ফাংশন দিয়ে এটি করতে পারেন।

  1. গণনা পরিবর্তন করুন, এর মতো:

index.html

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

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

  let i = f32(instance);
  // Compute the cell coordinate from the instance_index
  let cell = vec2f(i % grid.x, floor(i / grid.x));

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

  return vec4f(gridPos, 0, 1);
}

কোডটিতে সেই আপডেটটি তৈরি করার পরে আপনার কাছে শেষ পর্যন্ত স্কোয়ারগুলির দীর্ঘ প্রতীক্ষিত গ্রিড রয়েছে!

Four rows of four columns of red squares on a dark blue background.

  1. এবং এখন এটি কাজ করছে, ফিরে যান এবং গ্রিডের আকারটি ক্র্যাঙ্ক করুন!

index.html

const GRID_SIZE = 32;

32 rows of 32 columns of red squares on a dark blue background.

টাডা ! আপনি আসলে এই গ্রিডটি এখন সত্যিই বড় করতে পারেন এবং আপনার গড় জিপিইউ এটি ঠিকঠাক পরিচালনা করে। আপনি কোনও জিপিইউ পারফরম্যান্স বাধা দেওয়ার আগে আপনি স্বতন্ত্র স্কোয়ারগুলি দেখতে থামবেন।

6 .. অতিরিক্ত ক্রেডিট: এটি আরও রঙিন করুন!

এই মুহুর্তে, আপনি সহজেই পরবর্তী বিভাগে যেতে পারেন যেহেতু আপনি বাকি কোডল্যাবের জন্য ভিত্তি তৈরি করেছেন। তবে স্কোয়ারগুলির গ্রিডগুলি সমস্ত একই রঙ ভাগ করে নেওয়ার ক্ষেত্রে সেবাযোগ্য, এটি ঠিক উত্তেজনাপূর্ণ নয়, তাই না? ভাগ্যক্রমে আপনি আরও কিছুটা গণিত এবং শেডার কোড দিয়ে জিনিসগুলিকে কিছুটা উজ্জ্বল করতে পারেন!

শেডারে স্ট্রাক্ট ব্যবহার করুন

এখন অবধি, আপনি ভার্টেক্স শেডার: রূপান্তরিত অবস্থান থেকে এক টুকরো ডেটা পাস করেছেন। তবে আপনি আসলে ভার্টেক্স শেডার থেকে আরও অনেক ডেটা ফিরিয়ে দিতে পারেন এবং তারপরে এটি খণ্ড শেডারে ব্যবহার করতে পারেন!

ভার্টেক্স শেডার থেকে ডেটা পাস করার একমাত্র উপায় হ'ল এটি ফিরিয়ে দেওয়া। একটি ভার্টেক্স শেডারকে সর্বদা একটি অবস্থান ফেরত দেওয়ার প্রয়োজন হয়, সুতরাং আপনি যদি এটির সাথে অন্য কোনও ডেটা ফিরিয়ে দিতে চান তবে আপনাকে এটি একটি স্ট্রাক্টে রাখতে হবে। ডাব্লুজিএসএলে স্ট্রাক্টগুলিকে অবজেক্ট প্রকারের নাম দেওয়া হয়েছে যাতে এক বা একাধিক নামযুক্ত বৈশিষ্ট্য রয়েছে। বৈশিষ্ট্যগুলি @builtin এবং @location মতো বৈশিষ্ট্যগুলির সাথে চিহ্নিত করা যেতে পারে। আপনি এগুলি কোনও ফাংশনের বাইরে ঘোষণা করুন এবং তারপরে আপনি প্রয়োজন অনুসারে ফাংশনগুলির মধ্যে এবং বাইরে তাদের উদাহরণগুলি পাস করতে পারেন। উদাহরণস্বরূপ, আপনার বর্তমান ভার্টেক্স শেডার বিবেচনা করুন:

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);
  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 (ক্রিয়েটশ্যাডেরডুল কল)

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 (ক্রিয়েটশ্যাডেরডুল কল)

@fragment
fn fragmentMain() -> @location(0) vec4f {
  return vec4f(1, 0, 0, 1);
}

আপনি কোনও ইনপুট নিচ্ছেন না, এবং আপনি আপনার আউটপুট হিসাবে একটি শক্ত রঙ (লাল) বের করছেন। যদি শেডার জ্যামিতি সম্পর্কে আরও জানত যে এটি রঙিন, তবে আপনি সেই অতিরিক্ত ডেটাগুলিকে কিছুটা আকর্ষণীয় করে তুলতে ব্যবহার করতে পারেন। উদাহরণস্বরূপ, আপনি যদি প্রতিটি বর্গক্ষেত্রের কোষের সমন্বয় ভিত্তিতে রঙ পরিবর্তন করতে চান তবে কী হবে? @vertex পর্যায়টি জানে যে কোন ঘরটি রেন্ডার করা হচ্ছে; আপনাকে কেবল এটি @fragment পর্যায়ে পাস করতে হবে।

ভার্টেক্স এবং খণ্ডের পর্যায়ে যে কোনও ডেটা পাস করার জন্য, আপনাকে আমাদের পছন্দের @location সহ এটি একটি আউটপুট স্ট্রাক্টে অন্তর্ভুক্ত করতে হবে। যেহেতু আপনি সেল সমন্বয়টি পাস করতে চান, এটি পূর্ব থেকে VertexOutput স্ট্রাক্টে যুক্ত করুন এবং তারপরে ফিরে আসার আগে এটি @vertex ফাংশনে সেট করুন।

  1. আপনার ভার্টেক্স শেডারের রিটার্ন মান পরিবর্তন করুন, এর মতো:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

struct VertexInput {
  @location(0) pos: vec2f,
  @builtin(instance_index) instance: u32,
};

struct VertexOutput {
  @builtin(position) pos: vec4f,
  @location(0) cell: vec2f, // New line!
};

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

@vertex
fn vertexMain(input: VertexInput) -> VertexOutput  {
  let i = f32(input.instance);
  let cell = vec2f(i % grid.x, floor(i / grid.x));
  let cellOffset = cell / grid * 2;
  let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
  
  var output: VertexOutput;
  output.pos = vec4f(gridPos, 0, 1);
  output.cell = cell; // New line!
  return output;
}
  1. @fragment ফাংশনে, একই @location সহ একটি যুক্তি যুক্ত করে মানটি গ্রহণ করুন। (নামগুলি মেলে না, তবে তারা যদি তা করে তবে ট্র্যাক রাখা সহজ!)

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

@fragment
fn fragmentMain(@location(0) cell: vec2f) -> @location(0) vec4f {
  // Remember, fragment return values are (Red, Green, Blue, Alpha)
  // and since cell is a 2D vector, this is equivalent to:
  // (Red = cell.x, Green = cell.y, Blue = 0, Alpha = 1)
  return vec4f(cell, 0, 1);
}
  1. বিকল্পভাবে, আপনি পরিবর্তে একটি স্ট্রাক্ট ব্যবহার করতে পারেন:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

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

@fragment
fn fragmentMain(input: FragInput) -> @location(0) vec4f {
  return vec4f(input.cell, 0, 1);
}
  1. আরেকটি বিকল্প, যেহেতু আপনার কোডে এই উভয় ফাংশন একই শেডার মডিউলটিতে সংজ্ঞায়িত করা হয়েছে, তাই @vertex স্টেজের আউটপুট স্ট্রাক্টটি পুনরায় ব্যবহার করা! এটি পাসিং মানগুলিকে সহজ করে তোলে কারণ নাম এবং অবস্থানগুলি স্বাভাবিকভাবেই সামঞ্জস্যপূর্ণ।

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

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

আপনি কোন প্যাটার্নটি বেছে নিয়েছেন তা বিবেচনা না করেই ফলাফলটি হ'ল আপনার @fragment ফাংশনে সেল নম্বরটিতে অ্যাক্সেস রয়েছে এবং রঙকে প্রভাবিত করতে এটি ব্যবহার করতে সক্ষম হন। উপরের কোনও কোডের সাথে, আউটপুটটি দেখতে এটির মতো:

A grid of squares where the leftmost column is green, the bottom row is red, and all other squares are yellow.

এখন অবশ্যই আরও রঙ রয়েছে, তবে এটি দেখতে ঠিক সুন্দর নয়। আপনি ভাবতে পারেন কেন কেবল বাম এবং নীচের সারিগুলি আলাদা। এটি কারণ আপনি @fragment ফাংশন থেকে ফিরে আসা রঙের মানগুলি প্রতিটি চ্যানেল 0 থেকে 1 এর পরিসীমাতে প্রত্যাশা করে এবং সেই পরিসরের বাইরের কোনও মান এটিতে আবদ্ধ থাকে। অন্যদিকে আপনার কোষের মানগুলি প্রতিটি অক্ষ বরাবর 0 থেকে 32 অবধি থাকে। সুতরাং আপনি এখানে যা দেখছেন তা হ'ল প্রথম সারি এবং কলামটি তাত্ক্ষণিকভাবে লাল বা সবুজ রঙের চ্যানেল এবং সেই প্রতিটি কোষের পরে একই মানের সাথে সেই পুরো 1 টি মানকে আঘাত করে।

আপনি যদি রঙগুলির মধ্যে একটি মসৃণ রূপান্তর চান তবে আপনাকে প্রতিটি রঙের চ্যানেলের জন্য একটি ভগ্নাংশের মান ফিরিয়ে দিতে হবে, আদর্শভাবে শূন্য থেকে শুরু করে প্রতিটি অক্ষ বরাবর একটিতে শেষ করতে হবে, যার অর্থ grid দ্বারা আরও একটি বিভাজন!

  1. খণ্ডের শেডারটি পরিবর্তন করুন, এর মতো:

INDEX.HTML (ক্রিয়েটশ্যাডেরডুল কল)

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

পৃষ্ঠাটি রিফ্রেশ করুন এবং আপনি দেখতে পাচ্ছেন যে নতুন কোডটি আপনাকে পুরো গ্রিড জুড়ে রঙের অনেক সুন্দর গ্রেডিয়েন্ট দেয়

A grid of squares that transition from black, to red, to green, to yellow in different corners.

যদিও এটি অবশ্যই একটি উন্নতি, এখন নীচের বাম দিকে একটি দুর্ভাগ্যজনক অন্ধকার কোণ রয়েছে, যেখানে গ্রিডটি কালো হয়ে যায়। আপনি যখন গেম অফ লাইফ সিমুলেশন করা শুরু করেন, গ্রিডের একটি হার্ড-টু-ভিজি বিভাগটি কী ঘটছে তা অস্পষ্ট করবে। এটি উজ্জ্বল করতে ভাল লাগবে।

ভাগ্যক্রমে, আপনার কাছে সম্পূর্ণ অব্যবহৃত রঙের চ্যানেল রয়েছে - নীল - যা আপনি ব্যবহার করতে পারেন। আপনি আদর্শভাবে যে প্রভাবটি চান তা হ'ল নীলটি সবচেয়ে উজ্জ্বল হওয়া যেখানে অন্যান্য রঙগুলি সবচেয়ে অন্ধকার এবং তারপরে অন্যান্য রঙগুলি তীব্রতায় বাড়ার সাথে সাথে বিবর্ণ হয়ে যায়। এটি করার সবচেয়ে সহজ উপায় হ'ল চ্যানেলটি 1 থেকে শুরু করা এবং কোষের মানগুলির একটিকে বিয়োগ করা। এটি cx বা cy হতে পারে। উভয় চেষ্টা করুন, এবং তারপরে আপনার পছন্দসইটিকে বেছে নিন!

  1. খণ্ডের শেডারে আরও উজ্জ্বল রঙ যুক্ত করুন, এর মতো:

ক্রিয়েটশ্যাডেরডুল কল

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

ফলাফলটি বেশ সুন্দর দেখাচ্ছে!

A grid of squares that transition from red, to green, to blue to yellow in different corners.

এটি কোনও সমালোচনামূলক পদক্ষেপ নয়! তবে এটি আরও ভাল দেখাচ্ছে বলে এটি এটি সংশ্লিষ্ট চেকপয়েন্ট উত্স ফাইলে অন্তর্ভুক্ত করেছে এবং এই কোডল্যাবের বাকী স্ক্রিনশটগুলি এই আরও রঙিন গ্রিডকে প্রতিফলিত করে।

7 .. সেল রাজ্য পরিচালনা করুন

এরপরে, জিপিইউতে সঞ্চিত কিছু রাজ্যের উপর ভিত্তি করে গ্রিডের রেন্ডারটিতে কোন কোষগুলি রেন্ডার করে তা আপনাকে নিয়ন্ত্রণ করতে হবে। এটি চূড়ান্ত সিমুলেশন জন্য গুরুত্বপূর্ণ!

আপনার যা দরকার তা হ'ল প্রতিটি কক্ষের জন্য একটি অন-অফ সিগন্যাল, সুতরাং যে কোনও বিকল্প যা আপনাকে প্রায় কোনও মান ধরণের কাজের একটি বড় অ্যারে সঞ্চয় করতে দেয়। আপনি ভাবতে পারেন যে এটি ইউনিফর্ম বাফারগুলির জন্য অন্য ব্যবহারের ক্ষেত্রে! While you could make that work, it's more difficult because uniform buffers are limited in size, can't support dynamically sized arrays (you have to specify the array size in the shader), and can't be written to by compute shaders. That last item is the most problematic, since you want to do the Game of Life simulation on the GPU in a compute shader.

Fortunately, there's another buffer option that avoids all of those limitations.

Create a storage buffer

Storage buffers are general-use buffers that can be read and written to in compute shaders, and read in vertex shaders. They can be very large, and they don't need a specific declared size in a shader, which makes them much more like general memory. That's what you use to store the cell state.

  1. To create a storage buffer for your cell state, use what—by now—is probably starting to be a familiar-looking snippet of buffer creation code:

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

Just like with your vertex and uniform buffers, call device.createBuffer() with the appropriate size, and then make sure to specify a usage of GPUBufferUsage.STORAGE this time.

You can populate the buffer the same way as before by filling the TypedArray of the same size with values and then calling device.queue.writeBuffer() . Because you want to see the effect of your buffer on the grid, start by filling it with something predictable.

  1. Activate every third cell with the following code:

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

Read the storage buffer in the shader

Next, update your shader to look at the contents of the storage buffer before you render the grid. This looks very similar to how uniforms were added previously.

  1. Update your shader with the following code:

index.html

@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellState: array<u32>; // New!

First, you add the binding point, which tucks right underneath the grid uniform. You want to keep the same @group as the grid uniform, but the @binding number needs to be different. The var type is storage , in order to reflect the different type of buffer, and rather than a single vector, the type that you give for the cellState is an array of u32 values, in order to match the Uint32Array in JavaScript.

Next, in the body of your @vertex function, query the cell's state. Because the state is stored in a flat array in the storage buffer, you can use the instance_index in order to look up the value for the current cell!

How do you turn off a cell if the state says that it's inactive? Well, since the active and inactive states that you get from the array are 1 or 0, you can scale the geometry by the active state! Scaling it by 1 leaves the geometry alone, and scaling it by 0 makes the geometry collapse into a single point, which the GPU then discards.

  1. Update your shader code to scale the position by the cell's active state. The state value must be cast to a f32 in order to satisfy WGSL's type safety requirements:

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

Add the storage buffer to the bind group

Before you can see the cell state take effect, add the storage buffer to a bind group. Because it's part of the same @group as the uniform buffer, add it to the same bind group in the JavaScript code, as well.

  • Add the storage buffer, like this:

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

Make sure that the binding of the new entry matches the @binding() of the corresponding value in the shader!

With that in place, you should be able to refresh and see the pattern appear in the grid.

Diagonal stripes of colorful squares going from bottom left to top right against a dark blue background.

Use the ping-pong buffer pattern

Most simulations like the one you're building typically use at least two copies of their state. On each step of the simulation, they read from one copy of the state and write to the other. Then, on the next step, flip it and read from the state they wrote to previously. This is commonly referred to as a ping pong pattern because the most up-to-date version of the state bounces back and forth between state copies each step.

Why is that necessary? Look at a simplified example: imagine that you're writing a very simple simulation in which you move any active blocks right by one cell each step. To keep things easy to understand, you define your data and simulation in 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.

But if you run that code, the active cell moves all the way to the end of the array in one step! কেন? Because you keep updating the state in-place, so you move the active cell right, and then you look at the next cell and... hey! It's active! Better move it to the right again. The fact that you change the data at the same time that you observe it corrupts the results.

By using the ping pong pattern, you ensure that you always perform the next step of the simulation using only the results of the last step.

// Example simulation. Don't copy into the project!
const stateA = [1, 0, 0, 0, 0, 0, 0, 0];
const stateB = [0, 0, 0, 0, 0, 0, 0, 0];

function simulate(inArray, outArray) {
  outArray[0] = 0;
  for (let i = 1; i < inArray.length; ++i) {
     outArray[i] = inArray[i-1];
  }
}

// Run the simulation for two step.
simulate(stateA, stateB);
simulate(stateB, stateA); 
  1. Use this pattern in your own code by updating your storage buffer allocation in order to create two identical buffers:

index.html

// Create two storage buffers to hold the cell state.
const cellStateStorage = [
  device.createBuffer({
    label: "Cell State A",
    size: cellStateArray.byteLength,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
  }),
  device.createBuffer({
    label: "Cell State B",
     size: cellStateArray.byteLength,
     usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
  })
];
  1. To help visualize the difference between the two buffers, fill them with different data:

index.html

// Mark every third cell of the first grid as active.
for (let i = 0; i < cellStateArray.length; i+=3) {
  cellStateArray[i] = 1;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);

// Mark every other cell of the second grid as active.
for (let i = 0; i < cellStateArray.length; i++) {
  cellStateArray[i] = i % 2;
}
device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray);
  1. To show the different storage buffers in your rendering, update your bind groups to have two different variants, as well:

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

Set up a render loop

So far, you've only done one draw per page refresh, but now you want to show data updating over time. To do that you need a simple render loop.

A render loop is an endlessly repeating loop that draws your content to the canvas at a certain interval. Many games and other content that want to animate smoothly use the requestAnimationFrame() function to schedule callbacks at the same rate that the screen refreshes (60 times every second).

This app can use that, as well, but in this case, you probably want updates to happen in longer steps so that you can more easily follow what the simulation is doing. Manage the loop yourself instead so that you can control the rate at which your simulation updates.

  1. First, pick a rate for our simulation to update at (200ms is good, but you can go slower or faster if you like), and then keep track of how many steps of simulation have been completed.

index.html

const UPDATE_INTERVAL = 200; // Update every 200ms (5 times/sec)
let step = 0; // Track how many simulation steps have been run
  1. Then move all of the code you currently use for rendering into a new function. Schedule that function to repeat at your desired interval with setInterval() . Make sure that the function also updates the step count, and use that to pick which of the two bind groups to bind.

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

And now when you run the app you see that the canvas flips back and forth between showing the two state buffers you created.

Diagonal stripes of colorful squares going from bottom left to top right against a dark blue background. Vertical stripes of colorful squares against a dark blue background.

With that, you're pretty much done with the rendering side of things! You're all set to display the output of the Game of Life simulation you build in the next step, where you finally start using compute shaders.

Obviously there is so much more to WebGPU's rendering capabilities than the tiny slice that you explored here, but the rest is beyond the scope of this codelab. Hopefully, it gives you enough of a taste of how WebGPU's rendering works, though, that it helps make exploring more advanced techniques like 3D rendering easier to grasp.

8. Run the simulation

Now, for the last major piece of the puzzle: performing the Game of Life simulation in a compute shader!

Use compute shaders, at last!

You've learned abstractly about compute shaders throughout this codelab, but what exactly are they?

A compute shader is similar to vertex and fragment shaders in that they are designed to run with extreme parallelism on the GPU, but unlike the other two shader stages, they don't have a specific set of inputs and outputs. You are reading and writing data exclusively from sources you choose, like storage buffers. This means that instead of executing once for each vertex, instance, or pixel, you have to tell it how many invocations of the shader function you want. Then, when you run the shader, you are told which invocation is being processed, and you can decide what data you are going to access and which operations you are going to perform from there.

Compute shaders must be created in a shader module, just like vertex and fragment shaders, so add that to your code to get started. As you might guess, given the structure of the other shaders that you've implemented, the main function for your compute shader needs to be marked with the @compute attribute.

  1. Create a compute shader with the following code:

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

    }`
});

Because GPUs are used frequently for 3D graphics, compute shaders are structured such that you can request that the shader be invoked a specific number of times along an X, Y, and Z axis. This lets you very easily dispatch work that conforms to a 2D or 3D grid, which is great for your use case! You want to call this shader GRID_SIZE times GRID_SIZE times, once for each cell of your simulation.

Due to the nature of GPU hardware architecture, this grid is divided into workgroups . A workgroup has an X, Y, and Z size, and although the sizes can be 1 each, there are often performance benefits to making your workgroups a bit bigger. For your shader, choose a somewhat arbitrary workgroup size of 8 times 8. This is useful to keep track of in your JavaScript code.

  1. Define a constant for your workgroup size, like this:

index.html

const WORKGROUP_SIZE = 8;

You also need to add the workgroup size to the shader function itself, which you do using JavaScript's template literals so that you can easily use the constant you just defined.

  1. Add the workgroup size to the shader function, like this:

index.html (Compute createShaderModule call)

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

}

This tells the shader that work done with this function is done in (8 x 8 x 1) groups. (Any axis you leave off defaults to 1, although you have to at least specify the X axis.)

As with the other shader stages, there's a variety of @builtin values that you can accept as input into your compute shader function in order to tell you which invocation you're on and decide what work you need to do.

  1. Add a @builtin value, like this:

index.html (Compute createShaderModule call)

@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {

}

You pass in the global_invocation_id builtin, which is a three-dimensional vector of unsigned integers that tells you where in the grid of shader invocations you are. You run this shader once for each cell in your grid. You get numbers like (0, 0, 0) , (1, 0, 0) , (1, 1, 0) ... all the way to (31, 31, 0) , which means that you can treat it as the cell index you're going to operate on!

Compute shaders can also use uniforms, which you use just like in the vertex and fragment shaders.

  1. Use a uniform with your compute shader to tell you the grid size, like this:

index.html (Compute createShaderModule call)

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

@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {

}

Just like in the vertex shader, you also expose the cell state as a storage buffer. But in this case, you need two of them! Because compute shaders don't have a required output, like a vertex position or fragment color, writing values to a storage buffer or texture is the only way to get results out of a compute shader. Use the ping-pong method that you learned earlier; you have one storage buffer that feeds in the current state of the grid and one that you write out the new state of the grid to.

  1. Expose the cell input and output state as storage buffers, like this:

index.html (Compute createShaderModule call)

@group(0) @binding(0) var<uniform> grid: vec2f;
    
// New lines
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;

@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {

}

Note that the first storage buffer is declared with var<storage> , which makes it read-only, but the second storage buffer is declared with var<storage, read_write> . This allows you to both read and write to the buffer, using that buffer as the output for your compute shader. (There is no write-only storage mode in WebGPU).

Next, you need to have a way to map your cell index into the linear storage array. This is basically the opposite of what you did in the vertex shader, where you took the linear instance_index and mapped it to a 2D grid cell. (As a reminder, your algorithm for that was vec2f(i % grid.x, floor(i / grid.x)) .)

  1. Write a function to go in the other direction. It takes the cell's Y value, multiplies it by the grid width, and then adds the cell's X value.

index.html (Compute createShaderModule call)

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

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

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

@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
  
}

And, finally, to see that it's working, implement a really simple algorithm: if a cell is currently on, it turns off, and vice versa. It's not the Game of Life yet, but it's enough to show that the compute shader is working.

  1. Add the simple algorithm, like this:

index.html (Compute createShaderModule call)

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

@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
   
fn cellIndex(cell: vec2u) -> u32 {
  return cell.y * u32(grid.x) + cell.x;
}

@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
  // New lines. Flip the cell state every step.
  if (cellStateIn[cellIndex(cell.xy)] == 1) {
    cellStateOut[cellIndex(cell.xy)] = 0;
  } else {
    cellStateOut[cellIndex(cell.xy)] = 1;
  }
}

And that's it for your compute shader—for now! But before you can see the results, there are a few more changes that you need to make.

Use Bind Group and Pipeline Layouts

One thing that you might notice from the above shader is that it largely uses the same inputs (uniforms and storage buffers) as your render pipeline. So you might think that you can simply use the same bind groups and be done with it, right? ভাল খবর হল যে আপনি পারেন! It just takes a bit more manual setup to be able to do that.

Any time that you create a bind group, you need to provide a GPUBindGroupLayout . Previously, you got that layout by calling getBindGroupLayout() on the render pipeline, which in turn created it automatically because you supplied layout: "auto" when you created it. That approach works well when you only use a single pipeline, but if you have multiple pipelines that want to share resources, you need to create the layout explicitly, and then provide it to both the bind group and pipelines.

To help understand why, consider this: in your render pipelines you use a single uniform buffer and a single storage buffer, but in the compute shader you just wrote, you need a second storage buffer. Because the two shaders use the same @binding values for the uniform and first storage buffer, you can share those between pipelines, and the render pipeline ignores the second storage buffer, which it doesn't use. You want to create a layout that describes all of the resources that are present in the bind group, not just the ones used by a specific pipeline.

  1. To create that layout, call 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
  }]
});

This is similar in structure to creating the bind group itself, in that you describe a list of entries . The difference is that you describe what type of resource the entry must be and how it's used rather than providing the resource itself.

In each entry, you give the binding number for the resource, which (as you learned when you created the bind group) matches the @binding value in the shaders. You also provide the visibility , which are GPUShaderStage flags that indicate which shader stages can use the resource. You want both the uniform and first storage buffer to be accessible in the vertex and compute shaders, but the second storage buffer only needs to be accessible in compute shaders.

Finally, you indicate what type of resource is being used. This is a different dictionary key, depending on what you need to expose. Here, all three resources are buffers, so you use the buffer key to define the options for each. Other options include things like texture or sampler , but you don't need those here.

In the buffer dictionary, you set options like what type of buffer is used. The default is "uniform" , so you can leave the dictionary empty for binding 0. (You do have to at least set buffer: {} , though, so that the entry is identified as a buffer.) Binding 1 is given a type of "read-only-storage" because you don't use it with read_write access in the shader, and binding 2 has a type of "storage" because you do use it with read_write access!

Once the bindGroupLayout is created, you can pass it in when creating your bind groups rather than querying the bind group from the pipeline. Doing so means that you need to add a new storage buffer entry to each bind group in order to match the layout you just defined.

  1. Update the bind group creation, like this:

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

And now that the bind group has been updated to use this explicit bind group layout, you need to update the render pipeline to use the same thing.

  1. Create a GPUPipelineLayout .

index.html

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

A pipeline layout is a list of bind group layouts (in this case, you have one) that one or more pipelines use. The order of the bind group layouts in the array needs to correspond with the @group attributes in the shaders. (This means that bindGroupLayout is associated with @group(0) .)

  1. Once you have the pipeline layout, update the render pipeline to use it instead of "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
    }]
  }
});

Create the compute pipeline

Just like you need a render pipeline to use your vertex and fragment shaders, you need a compute pipeline to use your compute shader. Fortunately, compute pipelines are far less complicated than render pipelines, as they don't have any state to set, only the shader and layout.

  • Create a compute pipeline with the following code:

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

Notice that you pass in the new pipelineLayout instead of "auto" , just like in the updated render pipeline, which ensures that both your render pipeline and your compute pipeline can use the same bind groups.

Compute passes

This brings you to the point of actually making use of the compute pipeline! Given that you do your rendering in a render pass, you can probably guess that you need to do compute work in a compute pass. Compute and render work can both happen in the same command encoder, so you want to shuffle your updateGrid function a bit.

  1. Move the encoder creation to the top of the function, and then begin a compute pass with it (before the 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...

Just like compute pipelines, compute passes are much simpler to kick off than their rendering counterparts because you don't need to worry about any attachments.

You want to do the compute pass before the render pass because it allows the render pass to immediately use the latest results from the compute pass. That's also the reason that you increment the step count between the passes, so that the output buffer of the compute pipeline becomes the input buffer for the render pipeline.

  1. Next, set the pipeline and bind group inside the compute pass, using the same pattern for switching between bind groups as you do for the rendering pass.

index.html

const computePass = encoder.beginComputePass();

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

computePass.end();
  1. Finally, instead of drawing like in a render pass, you dispatch the work to the compute shader, telling it how many workgroups you want to execute on each axis.

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

Something very important to note here is that the number you pass into dispatchWorkgroups() is not the number of invocations! Instead, it's the number of workgroups to execute, as defined by the @workgroup_size in your shader.

If you want the shader to execute 32x32 times in order to cover your entire grid, and your workgroup size is 8x8, you need to dispatch 4x4 workgroups (4 * 8 = 32). That's why you divide the grid size by the workgroup size and pass that value into dispatchWorkgroups() .

Now you can refresh the page again, and you should see that the grid inverts itself with each update.

Diagonal stripes of colorful squares going from bottom left to top right against a dark blue background. Diagonal stripes of colorful squares two squares wide going from bottom left to top right against a dark blue background. The inversion of the previous image.

Implement the algorithm for the Game of Life

Before you update the compute shader to implement the final algorithm, you want to go back to the code that's initializing the storage buffer content and update it to produce a random buffer on each page load. (Regular patterns don't make for very interesting Game of Life starting points.) You can randomize the values however you want, but there's an easy way to start that gives reasonable results.

  1. To start each cell in a random state, update the cellStateArray initialization to the following code:

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

Now you can finally implement the logic for the Game of Life simulation. After everything it took to get here, the shader code may be disappointingly simple!

First, you need to know for any given cell how many of its neighbors are active. You don't care about which ones are active, only the count.

  1. To make getting neighboring cell data easier, add a cellActive function that returns the cellStateIn value of the given coordinate.

index.html (Compute createShaderModule call)

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

The cellActive function returns one if the cell is active, so adding the return value of calling cellActive for all eight surrounding cells gives you how many neighboring cells are active.

  1. Find the number of active neighbors, like this:

index.html (Compute createShaderModule call)

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

This leads to a minor problem, though: what happens when the cell you're checking is off the edge of the board? According to your cellIndex() logic right now, it either overflows to the next or previous row, or runs off the edge of the buffer!

For the Game of Life, a common and easy way to resolve this is to have cells on the edge of the grid treat cells on the opposite edge of the grid as their neighbors, creating a kind of wrap-around effect.

  1. Support grid wrap-around with a minor change to the cellIndex() function.

index.html (Compute createShaderModule call)

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

By using the % operator to wrap the cell X and Y when it extends past the grid size, you ensure that you never access outside the storage buffer bounds. With that, you can rest assured that the activeNeighbors count is predictable.

Then you apply one of four rules:

  • Any cell with fewer than two neighbors becomes inactive.
  • Any active cell with two or three neighbors stays active.
  • Any inactive cell with exactly three neighbors becomes active.
  • Any cell with more than three neighbors becomes inactive.

You can do this with a series of if statements, but WGSL also supports switch statements, which are a good fit for this logic.

  1. Implement the Game of Life logic, like this:

index.html (Compute createShaderModule call)

let i = cellIndex(cell.xy);

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

For reference, the final compute shader module call now looks like this:

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

আর... এটাই! আপনি সম্পন্ন! Refresh your page and watch your newly built cellular automaton grow!

Screenshot of an example state from the Game of Life simulation, with colorful cells rendered against a dark blue background.

9. অভিনন্দন!

You created a version of the classic Conway's Game of Life simulation that runs entirely on your GPU using the WebGPU API!

এরপর কি?

আরও পড়া

রেফারেন্স ডক্স